forked from iarv/meshing-around
Compare commits
160 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b43c21fc98 | |||
| e115f33d47 | |||
| b8016aafc9 | |||
| 743b0ab10b | |||
| e06b2a3581 | |||
| 582e00402a | |||
| 82551e0b4a | |||
| a9c2660ec1 | |||
| fa802ba313 | |||
| 874d56045e | |||
| 8204cbe60f | |||
| a50c06206c | |||
| 895e5a2b07 | |||
| 2012986aff | |||
| 63d1f84887 | |||
| d8233bc9e2 | |||
| bdea3d6036 | |||
| 2fe2009b97 | |||
| dcad12935f | |||
| 0e2f6343a2 | |||
| 56bd6f9ea7 | |||
| 5718a43d20 | |||
| f759e2e7e5 | |||
| 1e97554cbf | |||
| 04d4a2f5a7 | |||
| fb47756deb | |||
| a33fed711d | |||
| bcb741102d | |||
| 8b2d933fd1 | |||
| f8d6419551 | |||
| cf518aeff5 | |||
| 95eebcde2b | |||
| 5cd7dca9b0 | |||
| eb87cf1bc8 | |||
| 8a510a7b11 | |||
| e2631407e8 | |||
| eb86fa911c | |||
| 448ad65c67 | |||
| bb8d2167ce | |||
| a2bf33d71d | |||
| e287bdeaef | |||
| 16e5acbd27 | |||
| 1ea6961393 | |||
| bd2bce0029 | |||
| 33c8d4c0ad | |||
| d453c3cac1 | |||
| 187fc7c2e4 | |||
| 33154626e5 | |||
| cfdbf1836f | |||
| 054692adf0 | |||
| ce33421b16 | |||
| d2cde424fc | |||
| 517ae5d4b4 | |||
| e69ee5c1a8 | |||
| b2eae85cc2 | |||
| 0749df04e5 | |||
| a66ea58d24 | |||
| 13738d1042 | |||
| 695d510b9f | |||
| f5e80c31b1 | |||
| 572a15fbab | |||
| 8dc9a5de3f | |||
| c8643b7ce9 | |||
| 786dcab420 | |||
| ab2f9a9846 | |||
| daf43f306b | |||
| 53adb4be70 | |||
| 2458a4d141 | |||
| 1c78a8f593 | |||
| 6077eef26e | |||
| 8f3aaaba25 | |||
| b1cd0ca44f | |||
| 879555915f | |||
| f61ba7c1af | |||
| 7cb2ea33c7 | |||
| 855a9ac0d0 | |||
| 3e2e1de8ce | |||
| 372f49d6ef | |||
| a31b3e1c79 | |||
| d3ecef9216 | |||
| 1175e23525 | |||
| 08e3e21306 | |||
| 7e3de5e490 | |||
| abc3eccf4e | |||
| 80751f9cfc | |||
| 7209992887 | |||
| 6c18d97f27 | |||
| cdd7d6e766 | |||
| 8d5334126f | |||
| bcd23ebb83 | |||
| 5d581c2319 | |||
| 8e3b449c42 | |||
| 0975b3235a | |||
| 9a2a4f1b77 | |||
| 5df17b5905 | |||
| 894c5f155f | |||
| f848e12571 | |||
| 7adf6e7a1d | |||
| c6958c7c69 | |||
| 05c6e56a4f | |||
| c45cf5d207 | |||
| a3995f7cce | |||
| fb3652a954 | |||
| b385001db2 | |||
| ac96ca9e2f | |||
| 02ffe0eb3a | |||
| 389945e023 | |||
| 446fa0c049 | |||
| 8b4409c115 | |||
| 5684a75c65 | |||
| 1c6a98fea5 | |||
| 7c1b886c3d | |||
| 75bbd1a0cd | |||
| a53f5a033b | |||
| ea37405149 | |||
| e16ecbe1b7 | |||
| db6f20dd3b | |||
| 9fa60d0c84 | |||
| 2fdad79dbb | |||
| 20342fb58c | |||
| b7e815cf85 | |||
| 8e3d1c432e | |||
| 1a8ed573a8 | |||
| 63516b36e4 | |||
| d17b05a40a | |||
| e4cefa2264 | |||
| 90bf3459c9 | |||
| 0983259117 | |||
| 377e5a9825 | |||
| 7edcb4457a | |||
| 3fec7867d9 | |||
| 7e447616d9 | |||
| e59c3de0aa | |||
| db808568cb | |||
| 0615733445 | |||
| 402c58c111 | |||
| dde6c2ed32 | |||
| 766ff0a195 | |||
| d614cbcff5 | |||
| 81798c1fc2 | |||
| 210a75671f | |||
| f3e113dcc1 | |||
| 145664a42f | |||
| acc770732e | |||
| ded4c79911 | |||
| ad0c9c710f | |||
| 259c4991f9 | |||
| 5fe185ab7f | |||
| 974caaff42 | |||
| 41d8758969 | |||
| 92e1e3168e | |||
| a608e29911 | |||
| 015b72c8c6 | |||
| 74cf5841ff | |||
| 9ba7b1c972 | |||
| 5bf0417203 | |||
| 2b7a20f8d9 | |||
| 2afb49cbc7 | |||
| 17008b7711 | |||
| 36ff328380 |
@@ -1,14 +1,15 @@
|
||||
# Mesh Bot for Network Testing and BBS Activities
|
||||
|
||||
Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance your [Meshtastic](https://meshtastic.org/docs/introduction/) network experience with a variety of powerful tools and fun features, connectivity and utility through text-based message delivery. Whether you're looking to perform network tests, send messages, or even play games, this bot has you covered.
|
||||
Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance your [Meshtastic](https://meshtastic.org/docs/introduction/) network experience with a variety of powerful tools and fun features, connectivity and utility through text-based message delivery. Whether you're looking to perform network tests, send messages, or even play games, [mesh_bot.py](mesh_bot.py) has you covered.
|
||||
|
||||

|
||||
|
||||
## Key Features
|
||||
|
||||
### Intelligent Keyword Responder
|
||||
- **Automated Responses**: The bot traps keywords like "ping" and responds with "pong" in direct messages (DMs) or group channels.
|
||||
- **Automated Responses**: The bot detects keywords like "ping" and responds with "pong" in direct messages (DMs) or group channels.
|
||||
- **Customizable Triggers**: Monitor group channels for specific keywords and set custom responses.
|
||||
- **Emergency Response**: Monitor channels for keywords indicating emergencies and alert a wider audience.
|
||||
|
||||
### Network Tools
|
||||
- **Build, Test Local Mesh**: Ping allow for message delivery testing with more realistic packets vs. telemetry
|
||||
@@ -23,12 +24,14 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
- **Scheduler**: Schedule messages like weather updates or reminders for weekly VHF nets.
|
||||
- **Store and Forward**: Replay messages with the `messages` command, and log messages locally to disk.
|
||||
- **Send Mail**: Send mail to nodes using `bbspost @nodeNumber #message` or `bbspost @nodeShortName #message`.
|
||||
- **BBS Linking**: Combine multiple bots to expand BBS reach
|
||||
- **BBS Linking**: Combine multiple bots to expand BBS reach.
|
||||
- **E-Mail/SMS**: Send mesh-messages to E-Mail or SMS expanding visability.
|
||||
|
||||
### Interactive AI and Data Lookup
|
||||
- **NOAA location Data**: Get localized weather(alerts) and Tide information. Open-Meteo is used for wx only outside NOAA coverage.
|
||||
- **Wiki Integration**: Look up data using Wikipedia results.
|
||||
- **Ollama LLM AI**: Interact with the [Ollama](https://github.com/ollama/ollama/tree/main/docs) LLM AI for advanced queries and responses.
|
||||
- **Satalite Pass Info**: Get passes for satalite at your location.
|
||||
|
||||
### Proximity Alerts
|
||||
- **Location-Based Alerts**: Get notified when members arrive back at a configured lat/long, perfect for remote locations like campsites.
|
||||
@@ -41,13 +44,14 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
- **SNR RF Activity Alerts**: Monitor a radio frequency and get alerts when high SNR RF activity is detected.
|
||||
- **Hamlib Integration**: Use Hamlib (rigctld) to watch the S meter on a connected radio.
|
||||
|
||||
### NOAA EAS Alerts
|
||||
- **EAS Alerts via NOAA API**: Use an internet connected node to message Emergency Alerts from NOAA
|
||||
- **EAS Alerts over the air**: Utalizing external tools to report EAS alerts offline over mesh
|
||||
### EAS Alerts
|
||||
- **FEMA iPAWS/EAS Alerts via API**: Use an internet-connected node to message Emergency Alerts from FEMA
|
||||
- **NOAA EAS Alerts via API**: Use an internet-connected node to message Emergency Alerts from NOAA.
|
||||
- **EAS Alerts over the air**: Utilizing external tools to report EAS alerts offline over mesh.
|
||||
|
||||
### File Monitor Alerts
|
||||
- **File Mon**: Monitor a flat/text file for changes, brodcast the contents of the message to mesh channel.
|
||||
- **News File**: on request of news the contents of the file is returned.
|
||||
- **File Monitor**: Monitor a flat/text file for changes, broadcast the contents of the message to the mesh channel.
|
||||
- **News File**: On request of news, the contents of the file are returned.
|
||||
|
||||
### Data Reporting
|
||||
- **HTML Generator**: Visualize bot traffic and data flows with a built-in HTML generator for [data reporting](logs/README.md).
|
||||
@@ -56,11 +60,12 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
- **Message Chunking**: Automatically chunk messages over 160 characters to ensure higher delivery success across hops.
|
||||
|
||||
## Getting Started
|
||||
This project is developed on Linux (specifically a Raspberry Pi) but should work on any platform where the [Meshtastic protobuf API](https://meshtastic.org/docs/software/python/cli/) modules are supported, and with any compatible [Meshtastic](https://meshtastic.org/docs/getting-started/) hardware.
|
||||
This project is developed on Linux (specifically a Raspberry Pi) but should work on any platform where the [Meshtastic protobuf API](https://meshtastic.org/docs/software/python/cli/) modules are supported, and with any compatible [Meshtastic](https://meshtastic.org/docs/getting-started/) hardware. For pico or low-powered devices, or [femtofox](https://github.com/noon92/femtofox) project for embedding, possibly see the [buildroot](https://github.com/buildroot-meshtastic/buildroot-meshtastic). 🥔 Please use responsibly and follow local rulings for such equipment. This project captures packets, logs them, and handles over the air communications which can include PII such as GPS locations.
|
||||
|
||||
### Installation
|
||||
|
||||
#### 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
|
||||
```
|
||||
@@ -73,7 +78,7 @@ The code is under active development, so make sure to pull the latest changes re
|
||||
#### Docker Installation
|
||||
If you prefer to use Docker, follow these steps:
|
||||
|
||||
1. Ensure your serial port is properly shared and the GPU is configured if using LLM with [NVIDIA](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/docker-specialized.html).
|
||||
1. Ensure your serial port is properly shared and the GPU is configured if using LLM in docker with [NVIDIA](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/docker-specialized.html).
|
||||
2. Build the Docker image:
|
||||
```sh
|
||||
cd meshing-around
|
||||
@@ -122,16 +127,17 @@ enabled = False
|
||||
```
|
||||
|
||||
### General Settings
|
||||
The following settings determine how the bot responds. By default, the bot will not spam the default channel. Setting `respond_by_dm_only` to `True` will force all messages to be sent via DM, which may not be desired. Setting it to [`False`] will allow responses in the channel for all to see. If you have no default channel you can set this value to `-1` or any unused channel index.
|
||||
The following settings determine how the bot responds. By default, the bot will not spam the default channel. Setting `respond_by_dm_only` to `True` will force all messages to be sent via DM, which may not be desired. Setting it to [`False`] will allow responses in the channel for all to see. If you have no default channel you can set this value to `-1` or any unused channel index. You can also have the bot ignore the defaultChannel for any commands, but still observe the channel.
|
||||
|
||||
```ini
|
||||
[general]
|
||||
respond_by_dm_only = True
|
||||
defaultChannel = 0
|
||||
ignoreDefaultChannel = False # ignoreDefaultChannel, the bot will ignore the default channel set above
|
||||
```
|
||||
|
||||
### Location Settings
|
||||
The weather forecasting defaults to NOAA, for locations outside the USA, you can set `UseMeteoWxAPI` to `True`, to use a global weather API. The `lat` and `lon` are default values when a node has no location data. It is also the default used for Sentry.
|
||||
The weather forecasting defaults to NOAA, for locations outside the USA, you can set `UseMeteoWxAPI` to `True`, to use a global weather API. The `lat` and `lon` are default values when a node has no location data. It is also the default (or value when none found for user) for Sentry, all NOAA, repeater lookup, etc.
|
||||
|
||||
```ini
|
||||
[location]
|
||||
@@ -167,14 +173,70 @@ Sentry Bot detects anyone coming close to the bot-node. uses the Location Lat/Lo
|
||||
|
||||
```ini
|
||||
SentryEnabled = True # detect anyone close to the bot
|
||||
emailSentryAlerts = True # if SMTP enabled send alert to sysop email list
|
||||
SentryRadius = 100 # radius in meters to detect someone close to the bot
|
||||
SentryChannel = 9 # holdoff time multiplied by seconds(20) of the watchdog
|
||||
SentryHoldoff = 2 # channel to send a message to when the watchdog is triggered
|
||||
sentryIgnoreList = # list of ignored nodes numbers ex: 2813308004,4258675309
|
||||
```
|
||||
|
||||
### E-Mail / SMS Settings
|
||||
To enable connectivity with SMTP/IMAP.
|
||||
|
||||
```ini
|
||||
[smtp]
|
||||
# enable or disable the SMTP module, minimum required for outbound notifications
|
||||
enableSMTP = True # enable or disable the IMAP module for inbound email, not implimented yet
|
||||
enableImap = False # list of Sysop Emails seperate with commas, used only in emergemcy responder currently
|
||||
sysopEmails =
|
||||
# See config.template for all the SMTP settings
|
||||
SMTP_SERVER = smtp.gmail.com
|
||||
SMTP_AUTH = True
|
||||
EMAIL_SUBJECT = Meshtastic✉️
|
||||
```
|
||||
|
||||
### Emergency Response Handler
|
||||
Traps the following ("emergency", "911", "112", "999", "police", "fire", "ambulance", "rescue") keywords. Responds to the user, and calls attention to the text message in logs and via another network or channel.
|
||||
|
||||
```ini
|
||||
[emergencyHandler]
|
||||
# enable or disable the emergency response handler
|
||||
enabled = True
|
||||
# channel to send a message to when the emergency handler is triggered
|
||||
alert_channel = 2
|
||||
alert_interface = 1
|
||||
```
|
||||
|
||||
### EAS Alerting
|
||||
To Alert on Mesh with the EAS API you can set the channels and enable, checks every 20min.
|
||||
|
||||
#### FEMA iPAWS/EAS
|
||||
This uses the SAME, FIPS, ZIP code to locate the alerts in the feed. By default ignoring Test messages. femaAlertBroadcastCh is currently not written, still under development.
|
||||
|
||||
```ini
|
||||
# FEMA IPAWS/CAP Alert Broadcast
|
||||
femaAlertBroadcastEnabled = True
|
||||
# FEMA IPAWS/CAP Alert Broadcast Channels
|
||||
femaAlertBroadcastCh = 2,4
|
||||
# Ignore any headline that includes the word Test
|
||||
ignoreFEMAtest = True
|
||||
# comma separated list of codes trigger local alert. (e.g., SAME, FIPS, ZIP)
|
||||
# find your SAME https://www.weather.gov/nwr/counties
|
||||
mySAME = 053029,053073
|
||||
```
|
||||
|
||||
#### NOAA EAS
|
||||
This uses the defined lat-long of the bot for collecting of data from the API. see [File-Monitoring](#File-Monitoring) for ideas to collect EAS alerts from a RTL-SDR.
|
||||
|
||||
```ini
|
||||
# EAS Alert Broadcast
|
||||
wxAlertBroadcastEnabled = True
|
||||
# EAS Alert Broadcast Channels
|
||||
wxAlertBroadcastCh = 2,4
|
||||
```
|
||||
|
||||
### 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.
|
||||
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.
|
||||
|
||||
```ini
|
||||
[repeater] # repeater module
|
||||
@@ -183,7 +245,7 @@ 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)
|
||||
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.
|
||||
|
||||
```ini
|
||||
# Enable ollama LLM see more at https://ollama.com
|
||||
@@ -206,7 +268,7 @@ A module allowing a Hamlib compatible radio to connect to the bot. When function
|
||||
|
||||
```ini
|
||||
[radioMon]
|
||||
enabled = False
|
||||
enabled = True
|
||||
rigControlServerAddress = localhost:4532
|
||||
sigWatchBroadcastCh = 2 # channel to broadcast to can be 2,3
|
||||
signalDetectionThreshold = -10 # minimum SNR as reported by radio via hamlib
|
||||
@@ -226,15 +288,8 @@ broadcastCh = 2,4
|
||||
enable_read_news = False
|
||||
news_file_path = news.txt
|
||||
```
|
||||
#### NOAA EAS
|
||||
To Alert on Mesh with the NOAA EAS API you can set the channels and enable, checks every 30min
|
||||
|
||||
```ini
|
||||
# EAS Alert Broadcast
|
||||
wxAlertBroadcastEnabled = False
|
||||
# EAS Alert Broadcast Channels
|
||||
wxAlertBroadcastCh = 2,4
|
||||
```
|
||||
#### Offline EAS
|
||||
|
||||
To Monitor EAS with no internet connection see the following notes
|
||||
- [samedec](https://crates.io/crates/samedec) rust decoder much like multimon-ng
|
||||
@@ -266,7 +321,7 @@ In the config.ini enable the module
|
||||
# enable or disable the scheduler module
|
||||
enabled = True
|
||||
```
|
||||
The actions are via code only at this time. See mesh_bot.py around line [1050](https://github.com/SpudGunMan/meshing-around/blob/e94581936530c76ea43500eebb43f32ba7ed5e19/mesh_bot.py#L1050) to edit the schedule. See [schedule documentation](https://schedule.readthedocs.io/en/stable/) for more. Recomend to backup changes so they dont get lost.
|
||||
The actions are via code only at this time. See mesh_bot.py around line [1097](https://github.com/SpudGunMan/meshing-around/blob/e94581936530c76ea43500eebb43f32ba7ed5e19/mesh_bot.py#L1097) to edit the schedule. See [schedule documentation](https://schedule.readthedocs.io/en/stable/) for more. Recomend to backup changes so they dont get lost.
|
||||
|
||||
```python
|
||||
#Send WX every Morning at 08:00 using handle_wxc function to channel 2 on device 1
|
||||
@@ -284,55 +339,11 @@ schedule.every(2).days.at("10:00").do(lambda: send_message("bbslink MeshBot look
|
||||
```
|
||||
```ini
|
||||
bbslink_enabled = True
|
||||
# list of whitelisted nodes numbers ex: 2813308004,4258675309 empty list allows all
|
||||
bbslink_whitelist =
|
||||
bbslink_whitelist = # list of whitelisted nodes numbers ex: 2813308004,4258675309 empty list allows all
|
||||
```
|
||||
|
||||
### MQTT Notes
|
||||
There is no direct support for MQTT in the code, however, reports from Discord are that using [meshtasticd](https://meshtastic.org/docs/hardware/devices/linux-native-hardware/) with no radio and attaching the bot to the software node, which is MQTT-linked, allows routing. 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 methods have been mentioned as allowing MQTT routing for the project.
|
||||
|
||||
### Requirements
|
||||
Python 3.8? or later is needed (dev on latest). The following can be installed with `pip install -r requirements.txt` or using the [install.sh](install.sh) script for venv and automation:
|
||||
|
||||
```sh
|
||||
pip install meshtastic
|
||||
pip install pubsub
|
||||
```
|
||||
|
||||
Mesh-bot enhancements:
|
||||
|
||||
```sh
|
||||
pip install pyephem
|
||||
pip install requests
|
||||
pip install geopy
|
||||
pip install maidenhead
|
||||
pip install beautifulsoup4
|
||||
pip install dadjokes
|
||||
pip install geopy
|
||||
pip install schedule
|
||||
pip install wikipedia
|
||||
```
|
||||
|
||||
For open-meteo use:
|
||||
|
||||
```sh
|
||||
pip install openmeteo_requests
|
||||
pip install retry_requests
|
||||
pip install numpy
|
||||
```
|
||||
|
||||
For the Ollama LLM:
|
||||
|
||||
```sh
|
||||
pip install ollama
|
||||
pip install googlesearch-python
|
||||
```
|
||||
|
||||
To enable emoji in the Debian console, install the fonts:
|
||||
|
||||
```sh
|
||||
sudo apt-get install fonts-noto-color-emoji
|
||||
```
|
||||
There is no direct support for MQTT in the code, however, reports from Discord are that using [meshtasticd](https://meshtastic.org/docs/hardware/devices/linux-native-hardware/) with no radio and attaching the bot to the software node, which is MQTT-linked, allows routing.~~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~~ methods have been mentioned as allowing MQTT routing for the project. Tested working fully Firmware:2.5.15.79da236 with [mosquitto](https://meshtastic.org/docs/software/integrations/mqtt/mosquitto/).
|
||||
|
||||
## Full list of commands for the bot
|
||||
|
||||
@@ -343,6 +354,7 @@ sudo apt-get install fonts-noto-color-emoji
|
||||
| `test` | Returns like ping but also can be used to test the limits of data buffers `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 | ✅ |
|
||||
| `motd` | Displays the message of the day or sets it. Example: `motd $New Message Of the day` | ✅ |
|
||||
| `lheard` | Returns the last 5 heard nodes with SNR. Can also use `sitrep` | ✅ |
|
||||
| `history` | Returns the last commands run by user(s) | ✅ |
|
||||
@@ -356,8 +368,9 @@ sudo apt-get install fonts-noto-color-emoji
|
||||
| `hfcond` | Returns a table of HF solar conditions | |
|
||||
| `tide` | Returns the local tides (NOAA data source) |
|
||||
| `rlist` | Returns a table of nearby repeaters from RepeaterBook | |
|
||||
| `wx` and `wxc` | Return local weather forecast (wxc is metric value), NOAA or Open Meteo for weather forecasting |
|
||||
| `wxa` and `wxalert` | Return NOAA alerts. Short title or expanded details |
|
||||
| `wx` and `wxc` | Return local weather forecast (wxc is metric value), NOAA or Open Meteo for weather forecasting | |
|
||||
| `wxa` and `wxalert` | Return NOAA alerts. Short title or expanded details | |
|
||||
| `ea` and `ealert` | Return FEMA iPAWS/EAS alerts. Headline or expanded details | |
|
||||
|
||||
### Bulletin Board & Mail
|
||||
| Command | Description | |
|
||||
@@ -368,7 +381,12 @@ sudo apt-get install fonts-noto-color-emoji
|
||||
| `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) | ✅ |
|
||||
| `bbllink` | Links Bulletin Messages between BBS Systems | ✅ |
|
||||
| `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 | |
|
||||
@@ -377,8 +395,7 @@ sudo apt-get install fonts-noto-color-emoji
|
||||
| `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`| |
|
||||
|
||||
### Games (via DM)
|
||||
| Command | Description | |
|
||||
@@ -412,10 +429,52 @@ I used ideas and snippets from other responder bots and want to call them out!
|
||||
- **xdep**: For the reporting tools.
|
||||
- **Nestpebble**: For new ideas and enhancements.
|
||||
- **mrpatrick1991**: For Docker configurations.
|
||||
- **[https://github.com/A-c0rN](A-c0rN)**: Assistance with iPAWS and EAS
|
||||
- **Mike O'Connell/skrrt**: For [eas_alert_parser](etc/eas_alert_parser.py) enhanced by **sheer.cold**
|
||||
- **PiDiBi**: For looking at test functions and other suggestions like wxc, CPU use, and alerting ideas.
|
||||
- **Cisien, bitflip, **Woof**, and Hailo1999**: For testing and feature ideas on Discord and GitHub.
|
||||
- **Cisien, bitflip, **Woof**, **propstg**, **Josh** and Hailo1999**: For testing and feature ideas on Discord and GitHub.
|
||||
- **Meshtastic Discord Community**: For tossing out ideas and testing code.
|
||||
|
||||
### Tools
|
||||
- **Node Backup Management**: [Node Slurper](https://github.com/SpudGunMan/node-slurper)
|
||||
|
||||
### Requirements
|
||||
Python 3.8? or later is needed (dev on latest). The following can be installed with `pip install -r requirements.txt` or using the [install.sh](install.sh) script for venv and automation:
|
||||
|
||||
```sh
|
||||
pip install meshtastic
|
||||
pip install pubsub
|
||||
```
|
||||
|
||||
Mesh-bot enhancements:
|
||||
|
||||
```sh
|
||||
pip install pyephem
|
||||
pip install requests
|
||||
pip install geopy
|
||||
pip install maidenhead
|
||||
pip install beautifulsoup4
|
||||
pip install dadjokes
|
||||
pip install schedule
|
||||
pip install wikipedia
|
||||
```
|
||||
|
||||
For open-meteo use:
|
||||
|
||||
```sh
|
||||
pip install openmeteo_requests
|
||||
pip install retry_requests
|
||||
pip install numpy
|
||||
```
|
||||
|
||||
For the Ollama LLM:
|
||||
|
||||
```sh
|
||||
pip install googlesearch-python
|
||||
```
|
||||
|
||||
To enable emoji in the Debian console, install the fonts:
|
||||
|
||||
```sh
|
||||
sudo apt-get install fonts-noto-color-emoji
|
||||
```
|
||||
|
||||
+83
-16
@@ -25,39 +25,51 @@ port = /dev/ttyUSB0
|
||||
[general]
|
||||
# if False will respond on all channels but the default channel
|
||||
respond_by_dm_only = True
|
||||
# Allows auto-ping feature in a channel, False forces DM
|
||||
autoPingInChannel = False
|
||||
# defaultChannel is the meshtastic default public channel, e.g. LongFast (if none use -1)
|
||||
defaultChannel = 0
|
||||
# ignoreDefaultChannel, the bot will ignore the default channel set above
|
||||
ignoreDefaultChannel = False
|
||||
|
||||
# motd is reset to this value on boot
|
||||
motd = Thanks for using MeshBOT! Have a good day!
|
||||
welcome_message = MeshBot, here for you like a friend who is not. Try sending: ping @foo or, cmd
|
||||
|
||||
# whoami
|
||||
whoami = True
|
||||
|
||||
# enable or disable the Joke module
|
||||
DadJokes = True
|
||||
DadJokesEmoji = False
|
||||
|
||||
# enable or disable the Solar module
|
||||
spaceWeather = True
|
||||
|
||||
# enable or disable the wikipedia search module
|
||||
wikipedia = True
|
||||
|
||||
# Enable ollama LLM see more at https://ollama.com
|
||||
ollama = False
|
||||
# Ollama model to use (defaults to gemma2:2b)
|
||||
# ollamaModel = llama3.1
|
||||
# server instance to use (defaults to local machine install)
|
||||
ollamaHostName = http://localhost:11434
|
||||
|
||||
# StoreForward Enabled and Limits
|
||||
StoreForward = True
|
||||
StoreLimit = 3
|
||||
|
||||
# history command
|
||||
enableCmdHistory = True
|
||||
# command history ignore list ex: 2813308004,4258675309
|
||||
lheardCmdIgnoreNodes =
|
||||
|
||||
# 24 hour clock
|
||||
zuluTime = False
|
||||
# wait time for URL requests
|
||||
urlTimeout = 10
|
||||
|
||||
# logging to file of the non Bot messages
|
||||
LogMessagesToFile = False
|
||||
# Logging of system messages to file
|
||||
@@ -65,20 +77,17 @@ SyslogToFile = True
|
||||
# Number of log files to keep in days, 0 to keep all
|
||||
log_backup_count = 32
|
||||
|
||||
[games]
|
||||
# if hop limit for the user exceeds this value, the message will be dropped
|
||||
game_hop_limit = 5
|
||||
# enable or disable the games module(s)
|
||||
dopeWars = True
|
||||
lemonade = True
|
||||
blackjack = True
|
||||
videopoker = True
|
||||
mastermind = True
|
||||
golfsim = True
|
||||
[emergencyHandler]
|
||||
# enable or disable the emergency response handler
|
||||
enabled = False
|
||||
# channel to send a message to when the emergency handler is triggered
|
||||
alert_channel = 2
|
||||
alert_interface = 1
|
||||
|
||||
[sentry]
|
||||
# detect anyone close to the bot
|
||||
SentryEnabled = True
|
||||
emailSentryAlerts = False
|
||||
# radius in meters to detect someone close to the bot
|
||||
SentryRadius = 100
|
||||
# channel to send a message to when the watchdog is triggered
|
||||
@@ -99,26 +108,73 @@ bbslink_enabled = False
|
||||
# list of whitelisted nodes numbers ex: 2813308004,4258675309 empty list allows all
|
||||
bbslink_whitelist =
|
||||
|
||||
[smtp]
|
||||
# enable or disable the SMTP module
|
||||
enableSMTP = False
|
||||
# enable or disable the IMAP module for inbound email
|
||||
enableImap = False
|
||||
# list of Sysop Emails seperate with commas
|
||||
sysopEmails =
|
||||
SMTP_SERVER = smtp.gmail.com
|
||||
# 587 SMTP over TLS/STARTTLS, 25 legacy SMTP, 465 SMTP over SSL
|
||||
SMTP_PORT = 587
|
||||
# Sender email: be mindful of public access, don't use your personal email
|
||||
FROM_EMAIL = none@gmail.com
|
||||
SMTP_AUTH = True
|
||||
SMTP_USERNAME = none@gmail.com
|
||||
SMTP_PASSWORD = none
|
||||
EMAIL_SUBJECT = Meshtastic✉️
|
||||
|
||||
# IMAP not implimented yet
|
||||
IMAP_SERVER = imap.gmail.com
|
||||
# 993 IMAP over TLS/SSL, 143 legacy IMAP
|
||||
IMAP_PORT = 993
|
||||
# IMAP login usually same as SMTP
|
||||
IMAP_USERNAME = none@gmail.com
|
||||
IMAP_PASSWORD = none
|
||||
IMAP_FOLDER = inbox
|
||||
|
||||
|
||||
# location module
|
||||
[location]
|
||||
enabled = True
|
||||
lat = 48.50
|
||||
lon = -123.0
|
||||
|
||||
# Default to metric units rather than imperial
|
||||
useMetric = False
|
||||
|
||||
# repeaterList lookup location (rbook / artsci)
|
||||
repeaterLookup = rbook
|
||||
|
||||
# NOAA weather forecast days, the first two rows are today and tonight
|
||||
NOAAforecastDuration = 4
|
||||
# number of weather alerts to display
|
||||
NOAAalertCount = 2
|
||||
# use Open-Meteo API for weather data not NOAA useful for non US locations
|
||||
UseMeteoWxAPI = False
|
||||
# Default to metric units rather than imperial
|
||||
useMetric = False
|
||||
# repeaterList lookup location (rbook / artsci)
|
||||
repeaterLookup = rbook
|
||||
|
||||
# EAS Alert Broadcast
|
||||
wxAlertBroadcastEnabled = False
|
||||
# EAS Alert Broadcast Channels
|
||||
wxAlertBroadcastCh = 2
|
||||
|
||||
# FEMA IPAWS/CAP Alert Broadcast
|
||||
femaAlertBroadcastEnabled = False
|
||||
# FEMA IPAWS/CAP Alert Broadcast Channels
|
||||
femaAlertBroadcastCh = 2
|
||||
# Ignore any headline that includes the word Test
|
||||
ignoreFEMAtest = True
|
||||
# comma separated list of codes (e.g., SAME,FIPS,ZIP) trigger local alert.
|
||||
# find your SAME https://www.weather.gov/nwr/counties
|
||||
mySAME = 053029,053073
|
||||
|
||||
# Satalite Pass Prediction
|
||||
# Register for free API https://www.n2yo.com/login/
|
||||
n2yoAPIKey =
|
||||
# NORAD list https://www.n2yo.com/satellites/
|
||||
satList = 25544,7530
|
||||
|
||||
# repeater module
|
||||
[repeater]
|
||||
enabled = False
|
||||
@@ -153,12 +209,23 @@ broadcastCh = 2
|
||||
enable_read_news = False
|
||||
news_file_path = news.txt
|
||||
|
||||
[games]
|
||||
# if hop limit for the user exceeds this value, the message will be dropped
|
||||
game_hop_limit = 5
|
||||
# enable or disable the games module(s)
|
||||
dopeWars = True
|
||||
lemonade = True
|
||||
blackjack = True
|
||||
videopoker = True
|
||||
mastermind = True
|
||||
golfsim = True
|
||||
|
||||
[messagingSettings]
|
||||
# delay in seconds for response to avoid message collision
|
||||
responseDelay = 0.7
|
||||
responseDelay = 1.2
|
||||
# delay in seconds for splits in messages to avoid message collision
|
||||
splitDelay = 0.0
|
||||
# message chunk size for sending at high success rate
|
||||
# message chunk size for sending at high success rate, chunkr allows exceeding by 3 characters
|
||||
MESSAGE_CHUNK_SIZE = 160
|
||||
# Request Acknowledgement of message OTA
|
||||
wantAck = False
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/bin/bash
|
||||
# instruction set the meshing-around docker container
|
||||
|
||||
# Substitute environment variables in the config file
|
||||
envsubst < /app/config.ini > /app/config.tmp && mv /app/config.tmp /app/config.ini
|
||||
|
||||
@@ -23,6 +23,27 @@ except Exception as e:
|
||||
except Exception as e:
|
||||
bbs_dm = "System: data/bbsdm.pkl not found"
|
||||
|
||||
try:
|
||||
with open('../data/email_db.pickle', 'rb') as f:
|
||||
email_db = pickle.load(f)
|
||||
except Exception as e:
|
||||
try:
|
||||
with open('data/email_db.pickle', 'rb') as f:
|
||||
email_db = pickle.load(f)
|
||||
except Exception as e:
|
||||
email_db = "System: data/email_db.pickle not found"
|
||||
|
||||
try:
|
||||
with open('../data/sms_db.pickle', 'rb') as f:
|
||||
sms_db = pickle.load(f)
|
||||
except Exception as e:
|
||||
try:
|
||||
with open('data/sms_db.pickle', 'rb') as f:
|
||||
sms_db = pickle.load(f)
|
||||
except Exception as e:
|
||||
sms_db = "System: data/sms_db.pickle not found"
|
||||
|
||||
|
||||
# Game HS tables
|
||||
try:
|
||||
with open('../data/lemonstand.pkl', 'rb') as f:
|
||||
@@ -90,6 +111,10 @@ print ("System: bbs_messages")
|
||||
print (bbs_messages)
|
||||
print ("\nSystem: bbs_dm")
|
||||
print (bbs_dm)
|
||||
print ("\nSystem: email_db")
|
||||
print (email_db)
|
||||
print ("\nSystem: sms_db")
|
||||
print (sms_db)
|
||||
print (f"\n\nGame HS tables\n")
|
||||
print (f"lemon:{lemon_score}")
|
||||
print (f"dopewar:{dopewar_score}")
|
||||
|
||||
@@ -11,6 +11,7 @@ seen=set()
|
||||
pattern = re.compile(r'ZCZC.*?NWS-')
|
||||
|
||||
# alternate regex for parsing multimon-ng output
|
||||
# provided by https://github.com/A-c0rN
|
||||
#reg = r"^.*?(NNNN|ZCZC)(?:-([A-Za-z0-9]{3})-([A-Za-z0-9]{3})-((?:-?[0-9]{6})+)\+([0-9]{4})-([0-9]{7})-(.{8})-)?.*?$"
|
||||
#prog = re.compile(reg, re.MULTILINE)
|
||||
#groups = prog.match(sameData).groups()
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# # Simulate meshing-around de K7MHI 2024
|
||||
from modules.log import * # Import the logger
|
||||
from modules.log import * # Import the logger; ### --> If you are reading this put the script in the project root <-- ###
|
||||
import time
|
||||
import random
|
||||
|
||||
|
||||
+72
-38
@@ -1,31 +1,51 @@
|
||||
#!/bin/bash
|
||||
# meshing-around install helper script
|
||||
|
||||
# install.sh
|
||||
cd "$(dirname "$0")"
|
||||
program_path=$(pwd)
|
||||
cp etc/pong_bot.tmp etc/pong_bot.service
|
||||
cp etc/mesh_bot.tmp etc/mesh_bot.service
|
||||
cp etc/mesh_bot_reporting.tmp etc/mesh_bot_reporting.service
|
||||
|
||||
printf "\n########################"
|
||||
printf "\nMeshing Around Installer\n"
|
||||
printf "\nThis script will install the Meshing Around bot and its dependencies works best in debian/ubuntu\n"
|
||||
printf "\nChecking for dependencies\n"
|
||||
printf "########################\n"
|
||||
printf "\nThis script will try and install the Meshing Around Bot and its dependencies."
|
||||
printf "Installer works best in raspian/debian/ubuntu, if there is a problem, try running the installer again.\n"
|
||||
printf "\nChecking for dependencies...\n"
|
||||
|
||||
# Check and install dependencies
|
||||
if ! command -v python3 &> /dev/null
|
||||
then
|
||||
printf "python3 not found, trying 'apt-get install python3 python3-pip'\n"
|
||||
sudo apt-get install python3 python3-pip
|
||||
fi
|
||||
if ! command -v pip &> /dev/null
|
||||
then
|
||||
printf "pip not found, trying 'apt-get install python3-pip'\n"
|
||||
sudo apt-get install python3-pip
|
||||
fi
|
||||
|
||||
# double check for python3 and pip
|
||||
if ! command -v python3 &> /dev/null
|
||||
then
|
||||
printf "python3 not found, please install python3 with your OS\n"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v pip &> /dev/null
|
||||
then
|
||||
printf "pip not found, please install pip with your OS\n"
|
||||
exit 1
|
||||
fi
|
||||
printf "\nDependencies installed\n"
|
||||
|
||||
# add user to groups for serial access
|
||||
printf "\nAdding user to dialout and tty groups for serial access\n"
|
||||
printf "\nAdding user to dialout, bluetooth, and tty groups for serial access\n"
|
||||
sudo usermod -a -G dialout $USER
|
||||
sudo usermod -a -G tty $USER
|
||||
sudo usermod -a -G bluetooth $USER
|
||||
|
||||
# check for pip
|
||||
if ! command -v pip &> /dev/null
|
||||
then
|
||||
printf "pip not found, please install pip with your OS\n"
|
||||
sudo apt-get install python3-pip
|
||||
else
|
||||
printf "python pip found\n"
|
||||
fi
|
||||
# copy service files
|
||||
cp etc/pong_bot.tmp etc/pong_bot.service
|
||||
cp etc/mesh_bot.tmp etc/mesh_bot.service
|
||||
cp etc/mesh_bot_reporting.tmp etc/mesh_bot_reporting.service
|
||||
|
||||
# generate config file, check if it exists
|
||||
if [ -f config.ini ]; then
|
||||
@@ -34,34 +54,42 @@ if [ -f config.ini ]; then
|
||||
fi
|
||||
|
||||
cp config.template config.ini
|
||||
printf "\nConfig file generated\n"
|
||||
printf "\nConfig files generated!\n"
|
||||
|
||||
|
||||
# set virtual environment and install dependencies
|
||||
printf "\nMeshing Around Installer\n"
|
||||
|
||||
echo "Do you want to install the bot in a virtual environment? (y/n)"
|
||||
printf "\nDo you want to install the bot in a python virtual environment? (y/n)"
|
||||
read venv
|
||||
|
||||
if [ $venv == "y" ]; then
|
||||
# set virtual environment
|
||||
if ! python3 -m venv --help &> /dev/null; then
|
||||
printf "Python3 venv module not found, please install python3-venv with your OS\n"
|
||||
printf "Python3/venv error, please install python3-venv with your OS\n"
|
||||
exit 1
|
||||
else
|
||||
echo "The Following could be messy, or take some time on slower devices."
|
||||
echo "Creating virtual environment..."
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
#check if python3 has venv module
|
||||
if [ -f venv/bin/activate ]; then
|
||||
printf "\nFpund virtual environment for python\n"
|
||||
printf "\nFound virtual environment for python\n"
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
else
|
||||
printf "\nVirtual environment not found, trying `sudo apt-get install python3-venv`\n"
|
||||
sudo apt-get install python3-venv
|
||||
printf "\nPython3 venv module not found, please install python3-venv with your OS if not already done. re-run the script\n"
|
||||
fi
|
||||
# create virtual environment
|
||||
python3 -m venv venv
|
||||
|
||||
# double check for python3-venv
|
||||
if [ -f venv/bin/activate ]; then
|
||||
printf "\nFound virtual environment for python\n"
|
||||
source venv/bin/activate
|
||||
else
|
||||
printf "\nPython3 venv module not found, please install python3-venv with your OS\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "\nVirtual environment created\n"
|
||||
|
||||
# config service files for virtual environment
|
||||
replace="s|python3 mesh_bot.py|/usr/bin/bash launch.sh mesh|g"
|
||||
sed -i "$replace" etc/mesh_bot.service
|
||||
@@ -105,14 +133,6 @@ sed -i $replace etc/mesh_bot_reporting.service
|
||||
sudo systemctl daemon-reload
|
||||
printf "\n service files updated\n"
|
||||
|
||||
# ask if emoji font should be installed for linux
|
||||
echo "Do you want to install the emoji font for debian/ubuntu linux? (y/n)"
|
||||
read emoji
|
||||
if [ $emoji == "y" ]; then
|
||||
sudo apt-get install -y fonts-noto-color-emoji
|
||||
echo "Emoji font installed!, reboot to load the font"
|
||||
fi
|
||||
|
||||
if [ $bot == "pong" ]; then
|
||||
# install service for pong bot
|
||||
sudo cp etc/pong_bot.service /etc/systemd/system/
|
||||
@@ -132,11 +152,19 @@ if [ $bot == "n" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
printf "\nOptionally if you want to install the LLM Ollama compnents we will execute the following commands\n"
|
||||
# ask if emoji font should be installed for linux
|
||||
printf "\nDo you want to install the emoji font for debian/ubuntu linux? (y/n)"
|
||||
read emoji
|
||||
if [ $emoji == "y" ]; then
|
||||
sudo apt-get install -y fonts-noto-color-emoji
|
||||
echo "Emoji font installed!, reboot to load the font"
|
||||
fi
|
||||
|
||||
printf "\nOptionally if you want to install the multi gig LLM Ollama compnents we will execute the following commands\n"
|
||||
printf "\ncurl -fsSL https://ollama.com/install.sh | sh\n"
|
||||
|
||||
# ask if the user wants to install the LLM Ollama components
|
||||
echo "Do you want to install the LLM Ollama components? (y/n)"
|
||||
printf "\nDo you want to install the LLM Ollama components? (y/n)"
|
||||
read ollama
|
||||
if [ $ollama == "y" ]; then
|
||||
curl -fsSL https://ollama.com/install.sh | sh
|
||||
@@ -150,10 +178,16 @@ if [ $ollama == "y" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Good time to reboot? (y/n)"
|
||||
if [ $venv == "y" ]; then
|
||||
printf "\nFor running in virtual, launch bot with './launch.sh mesh' in path $program_path\n"
|
||||
fi
|
||||
|
||||
printf "\nGood time to reboot? (y/n)"
|
||||
read reboot
|
||||
if [ $reboot == "y" ]; then
|
||||
sudo reboot
|
||||
fi
|
||||
|
||||
printf "\nInstallation complete!\n"
|
||||
exit 0
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/bin/bash
|
||||
# This script launches the meshing-around bot or the report generator in python virtual environment
|
||||
|
||||
# launch.sh
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
+215
-30
@@ -15,7 +15,9 @@ restrictedResponse = "🤖only available in a Direct Message📵" # "" for none
|
||||
|
||||
# Global Variables
|
||||
cmdHistory = [] # list to hold the last commands
|
||||
seenNodes = [] # list to hold the last seen nodes
|
||||
DEBUGpacket = False # Debug print the packet rx
|
||||
DEBUGhops = False # Debug print hop info and bad hop count packets
|
||||
|
||||
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
|
||||
global cmdHistory
|
||||
@@ -37,11 +39,15 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
"bbspost": lambda: handle_bbspost(message, message_from_id, deviceID),
|
||||
"bbsread": lambda: handle_bbsread(message),
|
||||
"blackjack": lambda: handleBlackJack(message, message_from_id, deviceID),
|
||||
"clearsms": lambda: handle_sms(message_from_id, message),
|
||||
"cmd": lambda: help_message,
|
||||
"cq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"cqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"cqcqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"cmd": lambda: help_message,
|
||||
"dopewars": lambda: handleDopeWars(message, message_from_id, deviceID),
|
||||
"ea": lambda: handle_fema_alerts(message, message_from_id, deviceID),
|
||||
"ealert": lambda: handle_fema_alerts(message, message_from_id, deviceID),
|
||||
"email:": lambda: handle_email(message_from_id, message),
|
||||
"games": lambda: gamesCmdList,
|
||||
"globalthermonuclearwar": lambda: handle_gTnW(),
|
||||
"golfsim": lambda: handleGolf(message, message_from_id, deviceID),
|
||||
@@ -59,7 +65,11 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
"pong": lambda: "🏓PING!!🛜",
|
||||
"readnews": lambda: read_news(),
|
||||
"rlist": lambda: handle_repeaterQuery(message_from_id, deviceID, channel_number),
|
||||
"satpass": lambda: handle_satpass(message_from_id, deviceID, channel_number, message),
|
||||
"setemail": lambda: handle_email(message_from_id, message),
|
||||
"setsms": lambda: handle_sms( message_from_id, message),
|
||||
"sitrep": lambda: handle_lheard(message, message_from_id, deviceID, isDM),
|
||||
"sms:": lambda: handle_sms(message_from_id, message),
|
||||
"solar": lambda: drap_xray_conditions() + "\n" + solar_conditions(),
|
||||
"sun": lambda: handle_sun(message_from_id, deviceID, channel_number),
|
||||
"test": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
@@ -68,14 +78,24 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
"videopoker": lambda: handleVideoPoker(message, message_from_id, deviceID),
|
||||
"whereami": lambda: handle_whereami(message_from_id, deviceID, channel_number),
|
||||
"whoami": lambda: handle_whoami(message_from_id, deviceID, hop, snr, rssi, pkiStatus),
|
||||
"whois": lambda: handle_whois(message, deviceID, channel_number, message_from_id),
|
||||
"wiki:": lambda: handle_wiki(message, isDM),
|
||||
"wiki?": lambda: handle_wiki(message, isDM),
|
||||
"wx": lambda: handle_wxc(message_from_id, deviceID, 'wx'),
|
||||
"wxalert": lambda: handle_wxalert(message_from_id, deviceID, message),
|
||||
"wxa": lambda: handle_wxalert(message_from_id, deviceID, message),
|
||||
"wxalert": lambda: handle_wxalert(message_from_id, deviceID, message),
|
||||
"wxc": lambda: handle_wxc(message_from_id, deviceID, 'wxc'),
|
||||
"📍": lambda: handle_whoami(message_from_id, deviceID, hop, snr, rssi, pkiStatus),
|
||||
"🔔": lambda: handle_alertBell(message_from_id, deviceID, message),
|
||||
# any value from system.py:trap_list_emergency will trigger the emergency function
|
||||
"112": lambda: handle_emergency(message_from_id, deviceID, message),
|
||||
"911": lambda: handle_emergency(message_from_id, deviceID, message),
|
||||
"999": lambda: handle_emergency(message_from_id, deviceID, message),
|
||||
"ambulance": lambda: handle_emergency(message_from_id, deviceID, message),
|
||||
"emergency": lambda: handle_emergency(message_from_id, deviceID, message),
|
||||
"fire": lambda: handle_emergency(message_from_id, deviceID, message),
|
||||
"police": lambda: handle_emergency(message_from_id, deviceID, message),
|
||||
"rescue": lambda: handle_emergency(message_from_id, deviceID, message),
|
||||
}
|
||||
|
||||
# set the command handler
|
||||
@@ -152,6 +172,7 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
|
||||
msg = msg + " #" + message.split("#")[1]
|
||||
type = type + " #" + message.split("#")[1]
|
||||
|
||||
|
||||
# check for multi ping request
|
||||
if " " in message:
|
||||
# if stop multi ping
|
||||
@@ -161,27 +182,35 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
|
||||
multiPingList.pop(i)
|
||||
msg = "🛑 auto-ping"
|
||||
|
||||
# if 3 or more entries (2 or more active), throttle the multi-ping for congestion
|
||||
if len(multiPingList) > 2:
|
||||
msg = "🚫⛔️ auto-ping, service busy. ⏳Try again soon."
|
||||
pingCount = -1
|
||||
else:
|
||||
# set inital pingCount
|
||||
try:
|
||||
pingCount = int(message.split(" ")[1])
|
||||
if pingCount == 123 or pingCount == 1234:
|
||||
pingCount = 1
|
||||
if pingCount > 51:
|
||||
pingCount = 50
|
||||
except:
|
||||
# disabled in channel
|
||||
if autoPingInChannel and not isDM:
|
||||
# if 3 or more entries (2 or more active), throttle the multi-ping for congestion
|
||||
if len(multiPingList) > 2:
|
||||
msg = "🚫⛔️ auto-ping, service busy. ⏳Try again soon."
|
||||
pingCount = -1
|
||||
|
||||
if pingCount > 1:
|
||||
multiPingList.append({'message_from_id': message_from_id, 'count': pingCount + 1, 'type': type, 'deviceID': deviceID, 'channel_number': channel_number, 'startCount': pingCount})
|
||||
if type == "🎙TEST":
|
||||
msg = f"🛜Initalizing BufferTest, using chunks of about {int(maxBuffer // pingCount)}, max length {maxBuffer} in {pingCount} messages"
|
||||
else:
|
||||
msg = f"🚦Initalizing {pingCount} auto-ping"
|
||||
# set inital pingCount
|
||||
try:
|
||||
pingCount = int(message.split(" ")[1])
|
||||
if pingCount == 123 or pingCount == 1234:
|
||||
pingCount = 1
|
||||
if pingCount > 51:
|
||||
pingCount = 50
|
||||
except:
|
||||
pingCount = -1
|
||||
|
||||
if pingCount > 1:
|
||||
multiPingList.append({'message_from_id': message_from_id, 'count': pingCount + 1, 'type': type, 'deviceID': deviceID, 'channel_number': channel_number, 'startCount': pingCount})
|
||||
if type == "🎙TEST":
|
||||
msg = f"🛜Initalizing BufferTest, using chunks of about {int(maxBuffer // pingCount)}, max length {maxBuffer} in {pingCount} messages"
|
||||
else:
|
||||
msg = f"🚦Initalizing {pingCount} auto-ping"
|
||||
else:
|
||||
msg = "🔊AutoPing via DM only⛔️"
|
||||
|
||||
# if not a DM add the username to the beginning of msg
|
||||
if not isDM:
|
||||
msg = get_name_from_number(message_from_id) + msg
|
||||
|
||||
return msg
|
||||
|
||||
@@ -189,6 +218,34 @@ def handle_alertBell(message_from_id, deviceID, message):
|
||||
msg = ["the only prescription is more 🐮🔔🐄🛎️", "what this 🤖 needs is more 🐮🔔🐄🛎️", "🎤ring my bell🛎️🔔🎶"]
|
||||
return random.choice(msg)
|
||||
|
||||
def handle_emergency(message_from_id, deviceID, message):
|
||||
# if user in bbs_ban_list return
|
||||
if str(message_from_id) in bbs_ban_list:
|
||||
# silent discard
|
||||
logger.warning(f"System: {message_from_id} on spam list, no emergency responder alert sent")
|
||||
return ''
|
||||
# trgger alert to emergency_responder_alert_channel
|
||||
if message_from_id != 0:
|
||||
if deviceID == 1: rxNode = myNodeNum1
|
||||
elif deviceID == 2: rxNode = myNodeNum2
|
||||
nodeLocation = get_node_location(message_from_id, deviceID)
|
||||
# if default location is returned set to Unknown
|
||||
if nodeLocation[0] == latitudeValue and nodeLocation[1] == longitudeValue:
|
||||
nodeLocation = ["?", "?"]
|
||||
nodeInfo = f"{get_name_from_number(message_from_id, 'short', deviceID)} detected by {get_name_from_number(rxNode, 'short', deviceID)} lastGPS {nodeLocation[0]}, {nodeLocation[1]}"
|
||||
msg = f"🔔🚨Intercepted Possible Emergency Assistance needed for: {nodeInfo}"
|
||||
# alert the emergency_responder_alert_channel
|
||||
time.sleep(responseDelay)
|
||||
send_message(msg, emergency_responder_alert_channel, 0, emergency_responder_alert_interface)
|
||||
logger.warning(f"System: {message_from_id} Emergency Assistance Requested in {message}")
|
||||
# send the message out via email/sms
|
||||
if enableSMTP:
|
||||
for user in sysopEmails:
|
||||
send_email(user, f"Emergency Assistance Requested by {nodeInfo} in {message}", message_from_id)
|
||||
# respond to the user
|
||||
time.sleep(responseDelay + 2)
|
||||
return EMERGENCY_RESPONSE
|
||||
|
||||
def handle_motd(message, message_from_id, isDM):
|
||||
global MOTD
|
||||
isAdmin = False
|
||||
@@ -248,6 +305,34 @@ llmRunCounter = 0
|
||||
llmTotalRuntime = []
|
||||
llmLocationTable = [{'nodeID': 1234567890, 'location': 'No Location'},]
|
||||
|
||||
def handle_satpass(message_from_id, deviceID, channel_number, message):
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
passes = ''
|
||||
satList = satListConfig
|
||||
message = message.lower()
|
||||
|
||||
# if user has a NORAD ID in the message
|
||||
if "satpass " in message:
|
||||
try:
|
||||
userList = message.split("satpass ")[1].split(" ")[0]
|
||||
#split userList and make into satList overrided the config.ini satList
|
||||
satList = userList.split(",")
|
||||
except:
|
||||
return "example use:🛰️satpass 25544,33591"
|
||||
|
||||
# Detailed satellite pass
|
||||
for bird in satList:
|
||||
satPass = getNextSatellitePass(bird, str(location[0]), str(location[1]))
|
||||
if satPass:
|
||||
# append to passes
|
||||
passes = passes + satPass + "\n"
|
||||
# remove the last newline
|
||||
passes = passes[:-1]
|
||||
|
||||
if passes == '':
|
||||
passes = "No 🛰️ anytime soon"
|
||||
return passes
|
||||
|
||||
def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel):
|
||||
global llmRunCounter, llmLocationTable, llmTotalRuntime, cmdHistory
|
||||
location_name = 'no location provided'
|
||||
@@ -576,6 +661,15 @@ def handle_wxc(message_from_id, deviceID, cmd):
|
||||
weather = get_weather(str(location[0]), str(location[1]))
|
||||
return weather
|
||||
|
||||
def handle_fema_alerts(message, message_from_id, deviceID):
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
if message.lower().startswith("ealert"):
|
||||
# Detailed alert
|
||||
return getIpawsAlert(str(location[0]), str(location[1]))
|
||||
else:
|
||||
# Headlines only
|
||||
return getIpawsAlert(str(location[0]), str(location[1]), shortAlerts=True)
|
||||
|
||||
def handle_bbspost(message, message_from_id, deviceID):
|
||||
if "$" in message and not "example:" in message:
|
||||
subject = message.split("$")[1].split("#")[0]
|
||||
@@ -657,6 +751,9 @@ def handle_lheard(message, nodeid, deviceID, isDM):
|
||||
else:
|
||||
# trim the last \n
|
||||
bot_response = bot_response[:-1]
|
||||
|
||||
# get count of nodes heard
|
||||
bot_response += f"\n👀In Mesh: {len(seenNodes)}"
|
||||
|
||||
# bot_response += getNodeTelemetry(deviceID)
|
||||
return bot_response
|
||||
@@ -760,6 +857,46 @@ def handle_whoami(message_from_id, deviceID, hop, snr, rssi, pkiStatus):
|
||||
msg = "Error in whoami"
|
||||
return msg
|
||||
|
||||
def handle_whois(message, deviceID, channel_number, message_from_id):
|
||||
#return data on a node name or number
|
||||
if "?" in message:
|
||||
return message.split("?")[0].title() + " command returns information on a node."
|
||||
else:
|
||||
# get the nodeID from the message
|
||||
msg = ''
|
||||
node = ''
|
||||
# find the requested node in db
|
||||
if " " in message:
|
||||
node = message.split(" ")[1]
|
||||
if node.startswith("!") and len(node) == 9:
|
||||
# mesh !hex
|
||||
try:
|
||||
node = int(node.strip("!"),16)
|
||||
except ValueError as e:
|
||||
node = 0
|
||||
elif node.isalpha() or not node.isnumeric():
|
||||
# try short name
|
||||
node = get_num_from_short_name(node, deviceID)
|
||||
|
||||
# get details on the node
|
||||
for i in range(len(seenNodes)):
|
||||
if seenNodes[i]['nodeID'] == int(node):
|
||||
msg = f"Node: {seenNodes[i]['nodeID']} is {get_name_from_number(seenNodes[i]['nodeID'], 'long', deviceID)}\n"
|
||||
msg += f"Last 👀: {time.ctime(seenNodes[i]['lastSeen'])} "
|
||||
break
|
||||
|
||||
if msg == '':
|
||||
msg = "Provide a valid node number or short name"
|
||||
else:
|
||||
# if the user is an admin show the channel and interface and location
|
||||
if str(message_from_id) in bbs_admin_list:
|
||||
location = get_node_location(seenNodes[i]['nodeID'], deviceID, channel_number)
|
||||
msg += f"Ch: {seenNodes[i]['channel']}, Int: {seenNodes[i]['rxInterface']}"
|
||||
msg += f"Lat: {location[0]}, Lon: {location[1]}\n"
|
||||
if location != [latitudeValue, longitudeValue]:
|
||||
msg += f"Loc: {where_am_i(str(location[0]), str(location[1]))}"
|
||||
return msg
|
||||
|
||||
def check_and_play_game(tracker, message_from_id, message_string, rxNode, channel_number, game_name, handle_game_func):
|
||||
global llm_enabled
|
||||
|
||||
@@ -800,6 +937,7 @@ def checkPlayingGame(message_from_id, message_string, rxNode, channel_number):
|
||||
return playingGame
|
||||
|
||||
def onReceive(packet, interface):
|
||||
global seenNodes
|
||||
# Priocess the incoming packet, handles the responses to the packet with auto_response()
|
||||
# Sends the packet to the correct handler for processing
|
||||
|
||||
@@ -809,6 +947,8 @@ def onReceive(packet, interface):
|
||||
# Valies assinged to the packet
|
||||
rxNode, message_from_id, snr, rssi, hop, hop_away, channel_number = 0, 0, 0, 0, 0, 0, 0
|
||||
pkiStatus = (False, 'ABC')
|
||||
replyIDset = False
|
||||
emojiSeen = False
|
||||
isDM = False
|
||||
|
||||
if DEBUGpacket:
|
||||
@@ -818,7 +958,6 @@ def onReceive(packet, interface):
|
||||
# Debug print the packet for debugging
|
||||
logger.debug(f"Packet Received\n {packet} \n END of packet \n")
|
||||
|
||||
|
||||
# set the value for the incomming interface
|
||||
if rxType == 'SerialInterface':
|
||||
rxInterface = interface.__dict__.get('devPath', 'unknown')
|
||||
@@ -844,9 +983,15 @@ def onReceive(packet, interface):
|
||||
if packet.get('channel'):
|
||||
channel_number = packet.get('channel', 0)
|
||||
|
||||
# set the message_from_id
|
||||
message_from_id = packet['from']
|
||||
|
||||
# if message_from_id is not in the seenNodes list add it
|
||||
if not any(node['nodeID'] == message_from_id for node in seenNodes):
|
||||
seenNodes.append({'nodeID': message_from_id, 'rxInterface': rxNode, 'channel': channel_number, 'welcome': False, 'lastSeen': time.time()})
|
||||
|
||||
# BBS DM MAIL CHECKER
|
||||
if bbs_enabled and 'decoded' in packet:
|
||||
message_from_id = packet['from']
|
||||
|
||||
msg = bbs_check_dm(message_from_id)
|
||||
if msg:
|
||||
@@ -862,7 +1007,6 @@ def onReceive(packet, interface):
|
||||
if 'decoded' in packet and packet['decoded']['portnum'] == 'TEXT_MESSAGE_APP':
|
||||
message_bytes = packet['decoded']['payload']
|
||||
message_string = message_bytes.decode('utf-8')
|
||||
message_from_id = packet['from']
|
||||
|
||||
# get the signal strength and snr if available
|
||||
if packet.get('rxSnr') or packet.get('rxRssi'):
|
||||
@@ -871,7 +1015,15 @@ def onReceive(packet, interface):
|
||||
|
||||
# check if the packet has a publicKey flag use it
|
||||
if packet.get('publicKey'):
|
||||
pkiStatus = (packet.get('pkiEncrypted', False), packet.get('publicKey', 'ABC'))
|
||||
pkiStatus = packet.get('pkiEncrypted', False), packet.get('publicKey', 'ABC')
|
||||
|
||||
# check if the packet has replyId flag // currently unused in the code
|
||||
if packet.get('replyId'):
|
||||
replyIDset = packet.get('replyId', False)
|
||||
|
||||
# check if the packet has emoji flag set it // currently unused in the code
|
||||
if packet.get('emoji'):
|
||||
emojiSeen = packet.get('emoji', False)
|
||||
|
||||
# check if the packet has a hop count flag use it
|
||||
if packet.get('hopsAway'):
|
||||
@@ -887,10 +1039,18 @@ def onReceive(packet, interface):
|
||||
hop_start = packet.get('hopStart', 0)
|
||||
else:
|
||||
hop_start = 0
|
||||
|
||||
|
||||
if DEBUGhops:
|
||||
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_start == hop_limit:
|
||||
hop = "Direct"
|
||||
hop_count = 0
|
||||
elif hop_start == 0 and hop_limit > 0:
|
||||
hop = "MQTT"
|
||||
hop_count = 0
|
||||
else:
|
||||
# set hop to Direct if the message was sent directly otherwise set the hop count
|
||||
if hop_away > 0:
|
||||
@@ -938,10 +1098,26 @@ def onReceive(packet, interface):
|
||||
else:
|
||||
# respond with welcome message on DM
|
||||
logger.warning(f"Device:{rxNode} Ignoring DM: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
send_message(welcome_message, channel_number, message_from_id, rxNode)
|
||||
|
||||
# if seenNodes list is not marked as welcomed send welcome message
|
||||
if not any(node['nodeID'] == message_from_id and node['welcome'] == True for node in seenNodes):
|
||||
# send welcome message
|
||||
send_message(welcome_message, channel_number, message_from_id, rxNode)
|
||||
time.sleep(responseDelay)
|
||||
# mark the node as welcomed
|
||||
for node in seenNodes:
|
||||
if node['nodeID'] == message_from_id:
|
||||
node['welcome'] = True
|
||||
else:
|
||||
if dad_jokes_enabled:
|
||||
# respond with a dad joke on DM
|
||||
send_message(tell_joke(), channel_number, message_from_id, rxNode)
|
||||
else:
|
||||
# respond with help message on DM
|
||||
send_message(help_message, channel_number, message_from_id, rxNode)
|
||||
|
||||
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-'))
|
||||
else:
|
||||
@@ -1064,6 +1240,13 @@ async def start_rx():
|
||||
logger.debug(f"System: File Monitor News Reader Enabled for {news_file_path}")
|
||||
if wxAlertBroadcastEnabled:
|
||||
logger.debug(f"System: Weather Alert Broadcast Enabled on channels {wxAlertBroadcastChannel}")
|
||||
if emergency_responder_enabled:
|
||||
logger.debug(f"System: Emergency Responder Enabled on channels {emergency_responder_alert_channel} for interface {emergency_responder_alert_interface}")
|
||||
if enableSMTP:
|
||||
if enableImap:
|
||||
logger.debug(f"System: SMTP Email Alerting Enabled using IMAP")
|
||||
else:
|
||||
logger.debug(f"System: SMTP Email Alerting Enabled")
|
||||
if scheduler_enabled:
|
||||
# Examples of using the scheduler, Times here are in 24hr format
|
||||
# https://schedule.readthedocs.io/en/stable/
|
||||
@@ -1115,8 +1298,10 @@ async def main():
|
||||
hamlibTask = asyncio.create_task(handleSignalWatcher())
|
||||
|
||||
await asyncio.gather(meshRxTask, watchdogTask)
|
||||
await asyncio.gather(hamlibTask)
|
||||
await asyncio.gather(fileMonTask)
|
||||
if radio_detection_enabled:
|
||||
await asyncio.gather(hamlibTask)
|
||||
if file_monitor_enabled:
|
||||
await asyncio.gather(fileMonTask)
|
||||
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
|
||||
@@ -20,6 +20,17 @@ def read_news():
|
||||
# read the news file on demand
|
||||
return read_file(news_file_path)
|
||||
|
||||
def write_news(content, append=False):
|
||||
# write the news file on demand
|
||||
try:
|
||||
with open(news_file_path, 'a' if append else 'w') as f:
|
||||
f.write(content)
|
||||
logger.info(f"FileMon: Updated {news_file_path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"FileMon: Error writing file: {news_file_path}")
|
||||
return False
|
||||
|
||||
async def watch_file():
|
||||
|
||||
if not os.path.exists(file_monitor_file_path):
|
||||
|
||||
@@ -159,7 +159,7 @@ def get_found_items(nodeID):
|
||||
if dwInventoryDb[i].get('userID') == nodeID:
|
||||
dwInventoryDb[i]['inventory'] = inventory
|
||||
dwInventoryDb[i]['amount'] = amount
|
||||
msg = "💊You found " + str(qty) + " of " + str(my_drugs[found])
|
||||
msg = f"💊You found {qty} {my_drugs[found].name}"
|
||||
else:
|
||||
# rolls to see how much cash the user finds
|
||||
cash_found = random.randint(1, 977)
|
||||
@@ -241,6 +241,8 @@ def buy_func(nodeID, price_list, choice=0, value='0'):
|
||||
buy_amount = cash // price_list[drug_choice]
|
||||
if buy_amount > 100 - inventory:
|
||||
buy_amount = 100 - inventory
|
||||
if buy_amount == 0:
|
||||
return "You don\'t have any empty inventory slots.🎒"
|
||||
# set the buy amount to the max if the user enters m
|
||||
buy_amount = int(buy_amount)
|
||||
|
||||
|
||||
+21
-22
@@ -1,27 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
# LLM Module for meshing-around
|
||||
# This module is used to interact with Ollama to generate responses to user input
|
||||
# This module is used to interact with LLM API to generate responses to user input
|
||||
# K7MHI Kelly Keeton 2024
|
||||
from modules.log import *
|
||||
|
||||
# Ollama Client
|
||||
# https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server
|
||||
from ollama import Client as OllamaClient
|
||||
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)
|
||||
|
||||
# LLM System Variables
|
||||
ollamaClient = OllamaClient(host=ollamaHostName)
|
||||
ollamaAPI = ollamaHostName + "/api/generate"
|
||||
openaiAPI = "https://api.openai.com/v1/completions" # not used, if you do push a enhancement!
|
||||
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
|
||||
@@ -193,20 +198,26 @@ def llm_query(input, nodeID=0, location_name=None):
|
||||
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")
|
||||
else:
|
||||
# Build the query from the template
|
||||
modelPrompt = meshBotAI.format(input=input, context='\n'.join(googleResults), location_name=location_name, llmModel=llmModel, history=history)
|
||||
# Query the model without RAG context
|
||||
result = ollamaClient.generate(model=llmModel, prompt=modelPrompt)
|
||||
|
||||
# Condense the result to just needed
|
||||
if isinstance(result, dict):
|
||||
result = result.get("response")
|
||||
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", "")
|
||||
else:
|
||||
raise Exception(f"HTTP Error: {result.status_code}")
|
||||
|
||||
#logger.debug(f"System: LLM Response: " + result.strip().replace('\n', ' '))
|
||||
except Exception as e:
|
||||
logger.warning(f"System: LLM failure: {e}")
|
||||
return "I am having trouble processing your request, please try again later."
|
||||
return "⛔️I am having trouble processing your request, please try again later."
|
||||
|
||||
# cleanup for message output
|
||||
response = result.strip().replace('\n', ' ')
|
||||
@@ -217,15 +228,3 @@ def llm_query(input, nodeID=0, location_name=None):
|
||||
llmChat_history[nodeID] = [input, response]
|
||||
|
||||
return response
|
||||
|
||||
# import subprocess
|
||||
# def get_ollama_cpu():
|
||||
# try:
|
||||
# psOutput = subprocess.run(['ollama', 'ps'], capture_output=True, text=True)
|
||||
# if "GPU" in psOutput.stdout:
|
||||
# logger.debug(f"System: Ollama process with GPU")
|
||||
# else:
|
||||
# logger.debug(f"System: Ollama process with CPU, query time will be slower")
|
||||
# except Exception as e:
|
||||
# logger.debug(f"System: Ollama process not found, {e}")
|
||||
# return False
|
||||
|
||||
+147
-29
@@ -9,7 +9,7 @@ import bs4 as bs # pip install beautifulsoup4
|
||||
import xml.dom.minidom
|
||||
from modules.log import *
|
||||
|
||||
trap_list_location = ("whereami", "tide", "moon", "wx", "wxc", "wxa", "wxalert", "rlist")
|
||||
trap_list_location = ("whereami", "tide", "moon", "wx", "wxc", "wxa", "wxalert", "rlist", "ea", "ealert")
|
||||
|
||||
def where_am_i(lat=0, lon=0, short=False, zip=False):
|
||||
whereIam = ""
|
||||
@@ -255,7 +255,7 @@ def get_weather(lat=0, lon=0, unit=0):
|
||||
# extract data from rows
|
||||
for row in rows:
|
||||
# shrink the text
|
||||
line = abbreviate_weather(row.text)
|
||||
line = abbreviate_noaa(row.text)
|
||||
# only grab a few days of weather
|
||||
if len(weather.split("\n")) < forecastDuration:
|
||||
weather += line + "\n"
|
||||
@@ -278,23 +278,23 @@ def get_weather(lat=0, lon=0, unit=0):
|
||||
|
||||
return weather
|
||||
|
||||
def abbreviate_weather(row):
|
||||
def abbreviate_noaa(row):
|
||||
# replace long strings with shorter ones for display
|
||||
replacements = {
|
||||
"Monday": "Mon ",
|
||||
"Tuesday": "Tue ",
|
||||
"Wednesday": "Wed ",
|
||||
"Thursday": "Thu ",
|
||||
"Friday": "Fri ",
|
||||
"Saturday": "Sat ",
|
||||
"Sunday": "Sunday ",
|
||||
"Today": "Today ",
|
||||
"Night": "Night ",
|
||||
"Tonight": "Tonight ",
|
||||
"Tomorrow": "Tomorrow ",
|
||||
"Day": "Day ",
|
||||
"This Afternoon": "Afternoon ",
|
||||
"Overnight": "Overnight ",
|
||||
"monday": "Mon ",
|
||||
"tuesday": "Tue ",
|
||||
"wednesday": "Wed ",
|
||||
"thursday": "Thu ",
|
||||
"friday": "Fri ",
|
||||
"saturday": "Sat ",
|
||||
"sunday": "Sun ",
|
||||
"today": "Today ",
|
||||
"night": "Night ",
|
||||
"tonight": "Tonight ",
|
||||
"tomorrow": "Tomorrow ",
|
||||
"day": "Day ",
|
||||
"this afternoon": "Afternoon ",
|
||||
"overnight": "Overnight ",
|
||||
"northwest": "NW",
|
||||
"northeast": "NE",
|
||||
"southwest": "SW",
|
||||
@@ -303,25 +303,33 @@ def abbreviate_weather(row):
|
||||
"south": "S",
|
||||
"east": "E",
|
||||
"west": "W",
|
||||
"Northwest": "NW",
|
||||
"Northeast": "NE",
|
||||
"Southwest": "SW",
|
||||
"Southeast": "SE",
|
||||
"North": "N",
|
||||
"South": "S",
|
||||
"East": "E",
|
||||
"West": "W",
|
||||
"precipitation": "precip",
|
||||
"showers": "shwrs",
|
||||
"thunderstorms": "t-storms",
|
||||
"thunderstorm": "t-storm",
|
||||
"quarters": "qtrs",
|
||||
"quarter": "qtr"
|
||||
"quarter": "qtr",
|
||||
"january": "Jan",
|
||||
"february": "Feb",
|
||||
"march": "Mar",
|
||||
"april": "Apr",
|
||||
"may": "May",
|
||||
"june": "Jun",
|
||||
"july": "Jul",
|
||||
"august": "Aug",
|
||||
"september": "Sep",
|
||||
"october": "Oct",
|
||||
"november": "Nov",
|
||||
"december": "Dec",
|
||||
"degrees": "°",
|
||||
"percent": "%",
|
||||
"department": "Dept.",
|
||||
}
|
||||
|
||||
line = row
|
||||
for key, value in replacements.items():
|
||||
line = line.replace(key, value)
|
||||
# case insensitive replace
|
||||
line = line.replace(key, value).replace(key.capitalize(), value).replace(key.upper(), value)
|
||||
|
||||
return line
|
||||
|
||||
@@ -367,7 +375,7 @@ def getWeatherAlerts(lat=0, lon=0, useDefaultLatLon=False):
|
||||
alert_num = 0
|
||||
alert_num = len(alerts.split("\n"))
|
||||
|
||||
alerts = abbreviate_weather(alerts)
|
||||
alerts = abbreviate_noaa(alerts)
|
||||
|
||||
# return the first ALERT_COUNT alerts
|
||||
data = "\n".join(alerts.split("\n")[:numWxAlerts]), alert_num
|
||||
@@ -427,7 +435,7 @@ def getActiveWeatherAlertsDetail(lat=0, lon=0):
|
||||
"\n***\n"
|
||||
)
|
||||
|
||||
alerts = abbreviate_weather(alerts)
|
||||
alerts = abbreviate_noaa(alerts)
|
||||
|
||||
# trim the alerts to the first ALERT_COUNT
|
||||
alerts = alerts.split("\n***\n")[:numWxAlerts]
|
||||
@@ -442,3 +450,113 @@ def getActiveWeatherAlertsDetail(lat=0, lon=0):
|
||||
alerts = "\n".join(alerts)
|
||||
|
||||
return alerts
|
||||
|
||||
def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
# get the latest IPAWS alert from FEMA
|
||||
alert = ''
|
||||
alerts = []
|
||||
|
||||
# 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:
|
||||
alert_data = requests.get(alert_url, timeout=urlTimeoutSeconds)
|
||||
if not alert_data.ok:
|
||||
logger.warning("System: iPAWS fetching IPAWS alerts from FEMA")
|
||||
return ERROR_FETCHING_DATA
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning("System: iPAWS fetching IPAWS alerts from FEMA")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
# main feed bulletins
|
||||
alertxml = xml.dom.minidom.parseString(alert_data.text)
|
||||
|
||||
# extract alerts from main feed
|
||||
for entry in alertxml.getElementsByTagName("entry"):
|
||||
link = entry.getElementsByTagName("link")[0].getAttribute("href")
|
||||
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:
|
||||
#logger.warning(f"System: iPAWS Error fetching linked alert data from {link}")
|
||||
continue
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning(f"System: iPAWS Error fetching embedded alert data from {link}")
|
||||
continue
|
||||
|
||||
# this alert is a full CAP alert
|
||||
linked_xml = xml.dom.minidom.parseString(linked_data.text)
|
||||
|
||||
for info in linked_xml.getElementsByTagName("info"):
|
||||
# extract values from XML
|
||||
sameVal = "NONE"
|
||||
geocode_value = "NONE"
|
||||
description = ""
|
||||
try:
|
||||
eventCode_table = info.getElementsByTagName("eventCode")[0]
|
||||
alertType = eventCode_table.getElementsByTagName("valueName")[0].childNodes[0].nodeValue
|
||||
alertCode = eventCode_table.getElementsByTagName("value")[0].childNodes[0].nodeValue
|
||||
headline = info.getElementsByTagName("headline")[0].childNodes[0].nodeValue
|
||||
# use headline if no description
|
||||
if info.getElementsByTagName("description") and info.getElementsByTagName("description")[0].childNodes:
|
||||
description = info.getElementsByTagName("description")[0].childNodes[0].nodeValue
|
||||
else:
|
||||
logger.debug(f"System: report this to discord - iPAWS No description for alert: {headline}")
|
||||
description = headline
|
||||
|
||||
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.warning(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):
|
||||
# ignore the FEMA test alerts
|
||||
if ignoreFEMAtest:
|
||||
if "Test" in headline:
|
||||
logger.debug(f"System: Ignoring FEMA Test Alert: {headline} for {areaDesc}")
|
||||
continue
|
||||
|
||||
# add to alerts list
|
||||
alerts.append({
|
||||
'alertType': alertType,
|
||||
'alertCode': alertCode,
|
||||
'headline': headline,
|
||||
'areaDesc': areaDesc,
|
||||
'geocode_type': geocode_type,
|
||||
'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}")
|
||||
|
||||
# return the numWxAlerts of alerts
|
||||
if len(alerts) > 0:
|
||||
for alertItem in alerts[:numWxAlerts]:
|
||||
if shortAlerts:
|
||||
alert += abbreviate_noaa(f"🚨FEMA Alert: {alertItem['headline']}")
|
||||
else:
|
||||
alert += abbreviate_noaa(f"🚨FEMA Alert: {alertItem['headline']}\n{alertItem['description']}")
|
||||
# add a newline if not the last alert
|
||||
if alertItem != alerts[:numWxAlerts][-1]:
|
||||
alert += "\n"
|
||||
else:
|
||||
alert = NO_ALERTS
|
||||
|
||||
return alert
|
||||
|
||||
+19
-1
@@ -74,4 +74,22 @@ if log_messages_to_file:
|
||||
file_handler = TimedRotatingFileHandler('logs/messages.log', when='midnight', backupCount=log_backup_count)
|
||||
file_handler.setLevel(logging.INFO) # INFO used for messages to disk
|
||||
file_handler.setFormatter(logging.Formatter(msgLogFormat))
|
||||
msgLogger.addHandler(file_handler)
|
||||
msgLogger.addHandler(file_handler)
|
||||
|
||||
# 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
|
||||
+63
-23
@@ -6,8 +6,9 @@ import configparser
|
||||
NO_DATA_NOGPS = "No location data: does your device have GPS?"
|
||||
ERROR_FETCHING_DATA = "error fetching data"
|
||||
WELCOME_MSG = 'MeshBot, here for you like a friend who is not. Try sending: ping @foo or, CMD? for more'
|
||||
EMERGENCY_RESPONSE = "MeshBot detected a possible request for Emergency Assistance and alerted a wider audience."
|
||||
MOTD = 'Thanks for using MeshBOT! Have a good day!'
|
||||
NO_ALERTS = "No weather alerts found."
|
||||
NO_ALERTS = "No alerts found."
|
||||
|
||||
# setup the global variables
|
||||
SITREP_NODE_COUNT = 3 # number of nodes to report in the sitrep
|
||||
@@ -40,46 +41,53 @@ except Exception as e:
|
||||
if config.sections() == []:
|
||||
print(f"System: Error reading config file: {config_file} is empty or does not exist.")
|
||||
config['interface'] = {'type': 'serial', 'port': "/dev/ttyACM0", 'hostname': '', 'mac': ''}
|
||||
config['general'] = {'respond_by_dm_only': 'True', 'defaultChannel': '0', 'motd': MOTD,
|
||||
'welcome_message': WELCOME_MSG, 'zuluTime': 'False'}
|
||||
config['general'] = {'respond_by_dm_only': 'True', 'defaultChannel': '0', 'motd': MOTD, 'welcome_message': WELCOME_MSG, 'zuluTime': 'False'}
|
||||
config.write(open(config_file, 'w'))
|
||||
print (f"System: Config file created, check {config_file} or review the config.template")
|
||||
|
||||
if 'sentry' not in config:
|
||||
config['sentry'] = {'SentryEnabled': 'False', 'SentryChannel': '2', 'SentryHoldoff': '9', 'sentryIgnoreList': '', 'SentryRadius': '100'}
|
||||
config.write(open(config_file, 'w'))
|
||||
config['sentry'] = {'SentryEnabled': 'False', 'SentryChannel': '2', 'SentryHoldoff': '9', 'sentryIgnoreList': '', 'SentryRadius': '100'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'location' not in config:
|
||||
config['location'] = {'enabled': 'True', 'lat': '48.50', 'lon': '-123.0', 'UseMeteoWxAPI': 'False', 'useMetric': 'False', 'NOAAforecastDuration': '4', 'NOAAalertCount': '2', 'NOAAalertsEnabled': 'True', 'wxAlertBroadcastEnabled': 'False', 'wxAlertBroadcastChannel': '2', 'repeaterLookup': 'rbook'}
|
||||
config.write(open(config_file, 'w'))
|
||||
config['location'] = {'enabled': 'True', 'lat': '48.50', 'lon': '-123.0', 'UseMeteoWxAPI': 'False', 'useMetric': 'False', 'NOAAforecastDuration': '4', 'NOAAalertCount': '2', 'NOAAalertsEnabled': 'True', 'wxAlertBroadcastEnabled': 'False', 'wxAlertBroadcastChannel': '2', 'repeaterLookup': 'rbook'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'bbs' not in config:
|
||||
config['bbs'] = {'enabled': 'False', 'bbsdb': 'data/bbsdb.pkl', 'bbs_ban_list': '', 'bbs_admin_list': ''}
|
||||
config.write(open(config_file, 'w'))
|
||||
config['bbs'] = {'enabled': 'False', 'bbsdb': 'data/bbsdb.pkl', 'bbs_ban_list': '', 'bbs_admin_list': ''}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'repeater' not in config:
|
||||
config['repeater'] = {'enabled': 'False', 'repeater_channels': ''}
|
||||
config.write(open(config_file, 'w'))
|
||||
config['repeater'] = {'enabled': 'False', 'repeater_channels': ''}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'radioMon' not in config:
|
||||
config['radioMon'] = {'enabled': 'False', 'rigControlServerAddress': 'localhost:4532', 'sigWatchBrodcastCh': '2', 'signalDetectionThreshold': '-10', 'signalHoldTime': '10', 'signalCooldown': '5', 'signalCycleLimit': '5'}
|
||||
config.write(open(config_file, 'w'))
|
||||
config['radioMon'] = {'enabled': 'False', 'rigControlServerAddress': 'localhost:4532', 'sigWatchBrodcastCh': '2', 'signalDetectionThreshold': '-10', 'signalHoldTime': '10', 'signalCooldown': '5', 'signalCycleLimit': '5'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'games' not in config:
|
||||
config['games'] = {'dopeWars': 'True', 'lemonade': 'True', 'blackjack': 'True', 'videoPoker': 'True'}
|
||||
config.write(open(config_file, 'w'))
|
||||
config['games'] = {'dopeWars': 'True', 'lemonade': 'True', 'blackjack': 'True', 'videoPoker': 'True'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'messagingSettings' not in config:
|
||||
config['messagingSettings'] = {'responseDelay': '0.7', 'splitDelay': '0', 'MESSAGE_CHUNK_SIZE': '160'}
|
||||
config.write(open(config_file, 'w'))
|
||||
config['messagingSettings'] = {'responseDelay': '0.7', 'splitDelay': '0', 'MESSAGE_CHUNK_SIZE': '160'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'fileMon' not in config:
|
||||
config['fileMon'] = {'enabled': 'False', 'file_path': 'alert.txt', 'broadcastCh': '2'}
|
||||
config.write(open(config_file, 'w'))
|
||||
config['fileMon'] = {'enabled': 'False', 'file_path': 'alert.txt', 'broadcastCh': '2'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'scheduler' not in config:
|
||||
config['scheduler'] = {'enabled': 'False'}
|
||||
config.write(open(config_file, 'w'))
|
||||
config['scheduler'] = {'enabled': 'False'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'emergencyHandler' not in config:
|
||||
config['emergencyHandler'] = {'enabled': 'False', 'alert_channel': '2', 'alert_interface': '1', 'email': ''}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'smtp' not in config:
|
||||
config['smtp'] = {'sysopEmails': '', 'enableSMTP': 'False', 'enableImap': 'False'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
# interface1 settings
|
||||
interface1_type = config['interface'].get('type', 'serial')
|
||||
@@ -97,7 +105,7 @@ if 'interface2' in config:
|
||||
else:
|
||||
interface2_enabled = False
|
||||
|
||||
# variables
|
||||
# variables from the config.ini file
|
||||
try:
|
||||
# general
|
||||
useDMForResponse = config['general'].getboolean('respond_by_dm_only', True)
|
||||
@@ -114,6 +122,7 @@ try:
|
||||
welcome_message = (f"{welcome_message}").replace('\\n', '\n') # allow for newlines in the welcome message
|
||||
motd_enabled = config['general'].getboolean('motdEnabled', True)
|
||||
MOTD = config['general'].get('motd', MOTD)
|
||||
autoPingInChannel = config['general'].getboolean('autoPingInChannel', False)
|
||||
enableCmdHistory = config['general'].getboolean('enableCmdHistory', True)
|
||||
lheardCmdIgnoreNode = config['general'].get('lheardCmdIgnoreNode', '').split(',')
|
||||
whoami_enabled = config['general'].getboolean('whoami', True)
|
||||
@@ -125,12 +134,19 @@ try:
|
||||
llmModel = config['general'].get('ollamaModel', 'gemma2:2b') # default gemma2:2b
|
||||
ollamaHostName = config['general'].get('ollamaHostName', 'http://localhost:11434') # default localhost
|
||||
|
||||
# emergency response
|
||||
emergency_responder_enabled = config['emergencyHandler'].getboolean('enabled', False)
|
||||
emergency_responder_alert_channel = config['emergencyHandler'].getint('alert_channel', 2) # default 2
|
||||
emergency_responder_alert_interface = config['emergencyHandler'].getint('alert_interface', 1) # default 1
|
||||
emergency_responder_email = config['emergencyHandler'].get('email', '').split(',')
|
||||
|
||||
# sentry
|
||||
sentry_enabled = config['sentry'].getboolean('SentryEnabled', False) # default False
|
||||
secure_channel = config['sentry'].getint('SentryChannel', 2) # default 2
|
||||
sentry_holdoff = config['sentry'].getint('SentryHoldoff', 9) # default 9
|
||||
sentryIgnoreList = config['sentry'].get('sentryIgnoreList', '').split(',')
|
||||
sentry_radius = config['sentry'].getint('SentryRadius', 100) # default 100 meters
|
||||
email_sentry_alerts = config['sentry'].getboolean('emailSentryAlerts', False) # default False
|
||||
|
||||
# location
|
||||
location_enabled = config['location'].getboolean('enabled', True)
|
||||
@@ -142,7 +158,14 @@ try:
|
||||
numWxAlerts = config['location'].getint('NOAAalertCount', 2) # default 2 alerts
|
||||
wxAlertsEnabled = config['location'].getboolean('NOAAalertsEnabled', True) # default True not enabled yet
|
||||
repeater_lookup = config['location'].get('repeaterLookup', 'rbook') # default repeater lookup source
|
||||
mySAME = config['location'].get('mySAME', '').split(',') # default empty
|
||||
ipawsPIN = config['location'].get('ipawsPIN', '000000') # default 000000
|
||||
femaAlertBroadcastEnabled = config['location'].getboolean('femaAlertBroadcastEnabled', False) # default False
|
||||
femaAlertBroadcastCh = config['location'].get('femaAlertBroadcastCh', '2').split(',') # default Channel 2
|
||||
wxAlertBroadcastEnabled = config['location'].getboolean('wxAlertBroadcastEnabled', False) # default False
|
||||
ignoreFEMAtest = config['location'].getboolean('ignoreFEMAtest', True) # default True
|
||||
n2yoAPIKey = config['location'].get('n2yoAPIKey', '') # default empty
|
||||
satListConfig = config['location'].get('satList', '25544').split(',') # default 25544 ISS
|
||||
# brodcast channel for weather alerts
|
||||
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh')
|
||||
if wxAlertBroadcastChannel:
|
||||
@@ -150,7 +173,7 @@ try:
|
||||
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh').split(',')
|
||||
else:
|
||||
wxAlertBroadcastChannel = config['location'].getint('wxAlertBroadcastCh', 2) # default 2
|
||||
|
||||
|
||||
# bbs
|
||||
bbs_enabled = config['bbs'].getboolean('enabled', False)
|
||||
bbsdb = config['bbs'].get('bbsdb', 'data/bbsdb.pkl')
|
||||
@@ -159,6 +182,23 @@ try:
|
||||
bbs_link_enabled = config['bbs'].getboolean('bbslink_enabled', False)
|
||||
bbs_link_whitelist = config['bbs'].get('bbslink_whitelist', '').split(',')
|
||||
|
||||
# E-Mail Settings
|
||||
sysopEmails = config['smtp'].get('sysopEmails', '').split(',')
|
||||
enableSMTP = config['smtp'].getboolean('enableSMTP', False)
|
||||
enableImap = config['smtp'].getboolean('enableImap', False)
|
||||
SMTP_SERVER = config['smtp'].get('SMTP_SERVER', 'smtp.gmail.com')
|
||||
SMTP_PORT = config['smtp'].getint('SMTP_PORT', 587)
|
||||
FROM_EMAIL = config['smtp'].get('FROM_EMAIL', 'none@gmail.com')
|
||||
SMTP_AUTH = config['smtp'].getboolean('SMTP_AUTH', True)
|
||||
SMTP_USERNAME = config['smtp'].get('SMTP_USERNAME', FROM_EMAIL)
|
||||
SMTP_PASSWORD = config['smtp'].get('SMTP_PASSWORD', 'password')
|
||||
EMAIL_SUBJECT = config['smtp'].get('EMAIL_SUBJECT', 'Meshtastic✉️')
|
||||
IMAP_SERVER = config['smtp'].get('IMAP_SERVER', 'imap.gmail.com')
|
||||
IMAP_PORT = config['smtp'].getint('IMAP_PORT', 993)
|
||||
IMAP_USERNAME = config['smtp'].get('IMAP_USERNAME', SMTP_USERNAME)
|
||||
IMAP_PASSWORD = config['smtp'].get('IMAP_PASSWORD', SMTP_PASSWORD)
|
||||
IMAP_FOLDER = config['smtp'].get('IMAP_FOLDER', 'inbox')
|
||||
|
||||
# repeater
|
||||
repeater_enabled = config['repeater'].getboolean('enabled', False)
|
||||
repeater_channels = config['repeater'].get('repeater_channels', '').split(',')
|
||||
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
# SMTP module for the meshing-around bot
|
||||
# 2024 Idea and code bits from https://github.com/tremmert81
|
||||
# https://avtech.com/articles/138/list-of-email-to-sms-addresses/
|
||||
# 2024 Kelly Keeton K7MHI
|
||||
|
||||
from modules.log import *
|
||||
import pickle
|
||||
import time
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
|
||||
# System variables
|
||||
trap_list_smtp = ("email:", "setemail", "sms:", "setsms", "clearsms")
|
||||
smtpThrottle = {}
|
||||
SMTP_TIMEOUT = 10
|
||||
|
||||
if enableImap:
|
||||
# Import IMAP library
|
||||
import imaplib
|
||||
import email
|
||||
|
||||
# Send email
|
||||
def send_email(to_email, message, nodeID=0):
|
||||
global smtpThrottle
|
||||
|
||||
# Clean up email address
|
||||
to_email = to_email.strip()
|
||||
|
||||
# Basic email validation
|
||||
if "@" not in to_email or "." not in to_email:
|
||||
logger.warning(f"System: Invalid email address format: {to_email}")
|
||||
return False
|
||||
|
||||
# throttle email to prevent abuse
|
||||
if to_email in smtpThrottle:
|
||||
if smtpThrottle[to_email] > time.time() - 120:
|
||||
logger.warning("System: Email throttled for " + to_email[:-6])
|
||||
return "⛔️Email throttled, try again later"
|
||||
smtpThrottle[to_email] = time.time()
|
||||
|
||||
# check if email is in the ban list
|
||||
if nodeID in bbs_ban_list:
|
||||
logger.warning("System: Email blocked for " + str(nodeID))
|
||||
return "⛔️Email throttled, try again later"
|
||||
# Send email
|
||||
try:
|
||||
# Create message
|
||||
msg = MIMEMultipart()
|
||||
msg['From'] = FROM_EMAIL
|
||||
msg['To'] = to_email
|
||||
msg['Subject'] = EMAIL_SUBJECT
|
||||
msg.attach(MIMEText(message, 'plain'))
|
||||
|
||||
# Connect to SMTP server
|
||||
server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT, timeout=SMTP_TIMEOUT)
|
||||
try:
|
||||
# login /auth
|
||||
if SMTP_PORT == 587:
|
||||
server.starttls()
|
||||
if SMTP_AUTH:
|
||||
server.login(SMTP_USERNAME, SMTP_PASSWORD)
|
||||
except Exception as e:
|
||||
logger.warning(f"System: Failed to login to SMTP server: {str(e)}")
|
||||
return
|
||||
|
||||
# Send email; this command will hold the program until the email is sent
|
||||
server.send_message(msg)
|
||||
server.quit()
|
||||
|
||||
logger.info("System: Email sent to: " + to_email[:-6])
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"System: Failed to send email: {str(e)}")
|
||||
return False
|
||||
|
||||
def check_email(nodeID, sysop=False):
|
||||
if not enableImap:
|
||||
return
|
||||
|
||||
try:
|
||||
# Connect to IMAP server
|
||||
mail = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT, timeout=SMTP_TIMEOUT)
|
||||
mail.login(IMAP_USERNAME, IMAP_PASSWORD)
|
||||
mail.select(IMAP_FOLDER)
|
||||
|
||||
# Search for new emails
|
||||
status, data = mail.search(None, 'UNSEEN')
|
||||
if status == 'OK':
|
||||
for num in data[0].split():
|
||||
status, data = mail.fetch(num, '(RFC822)')
|
||||
if status == 'OK':
|
||||
email_message = email.message_from_bytes(data[0][1])
|
||||
email_from = email_message['from']
|
||||
email_subject = email_message['subject']
|
||||
email_body = ""
|
||||
|
||||
if not sysop:
|
||||
# Check if email is whitelisted by particpant in the mesh
|
||||
for address in sms_db[nodeID]:
|
||||
if address in email_from:
|
||||
email_body = email_message.get_payload()
|
||||
logger.info("System: Email received from: " + email_from[:-6] + " for " + str(nodeID))
|
||||
return email_body.strip()
|
||||
else:
|
||||
# Check if email is from sysop
|
||||
for address in sysopEmails:
|
||||
if address in email_from:
|
||||
email_body = email_message.get_payload()
|
||||
logger.info("System: SysOp Email received from: " + email_from[:-6] + " for sysop")
|
||||
return email_body.strip()
|
||||
|
||||
except Exception as e:
|
||||
logger.warning("System: Failed to check email: " + str(e))
|
||||
return False
|
||||
|
||||
# initalize email db
|
||||
email_db = {}
|
||||
try:
|
||||
with open('data/email_db.pickle', 'rb') as f:
|
||||
email_db = pickle.load(f)
|
||||
except:
|
||||
logger.warning("System: Email db not found, creating a new one")
|
||||
with open('data/email_db.pickle', 'wb') as f:
|
||||
pickle.dump(email_db, f)
|
||||
|
||||
def store_email(nodeID, email):
|
||||
global email_db
|
||||
|
||||
# if not in db, add it
|
||||
logger.debug("System: Setting E-Mail for " + str(nodeID))
|
||||
email_db[nodeID] = email
|
||||
|
||||
# save to a pickle for persistence, this is a simple db, be mindful of risk
|
||||
with open('data/email_db.pickle', 'wb') as f:
|
||||
pickle.dump(email_db, f)
|
||||
f.close()
|
||||
return True
|
||||
|
||||
|
||||
# initalize SMS db
|
||||
sms_db = [{'nodeID': 0, 'sms':[]}]
|
||||
try:
|
||||
with open('data/sms_db.pickle', 'rb') as f:
|
||||
sms_db = pickle.load(f)
|
||||
except:
|
||||
logger.warning("System: SMS db not found, creating a new one")
|
||||
with open('data/sms_db.pickle', 'wb') as f:
|
||||
pickle.dump(sms_db, f)
|
||||
|
||||
def store_sms(nodeID, sms):
|
||||
global sms_db
|
||||
try:
|
||||
logger.debug("System: Setting SMS for " + str(nodeID))
|
||||
# if not in db, add it
|
||||
if nodeID not in sms_db:
|
||||
sms_db.append({'nodeID': nodeID, 'sms': sms})
|
||||
else:
|
||||
# if in db, update it
|
||||
for item in sms_db:
|
||||
if item['nodeID'] == nodeID:
|
||||
item['sms'].append(sms)
|
||||
|
||||
# save to a pickle for persistence, this is a simple db, be mindful of risk
|
||||
with open('data/sms_db.pickle', 'wb') as f:
|
||||
pickle.dump(sms_db, f)
|
||||
f.close()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning("System: Failed to store SMS: " + str(e))
|
||||
return False
|
||||
|
||||
def handle_sms(nodeID, message):
|
||||
global sms_db
|
||||
# if clearsms, remove all sms for node
|
||||
if message.lower().startswith("clearsms"):
|
||||
if any(item['nodeID'] == nodeID for item in sms_db):
|
||||
# remove record from db for nodeID
|
||||
sms_db = [item for item in sms_db if item['nodeID'] != nodeID]
|
||||
# update the pickle
|
||||
with open('data/sms_db.pickle', 'wb') as f:
|
||||
pickle.dump(sms_db, f)
|
||||
f.close()
|
||||
return "📲 address cleared"
|
||||
return "📲No address to clear"
|
||||
|
||||
# send SMS to SMS in db. if none ask for one
|
||||
if message.lower().startswith("setsms"):
|
||||
message = message.split(" ", 1)
|
||||
if len(message[1]) < 5:
|
||||
return "?📲setsms: example@phone.co"
|
||||
if "@" not in message[1] and "." not in message[1]:
|
||||
return "📲Please provide a valid email address"
|
||||
if store_sms(nodeID, message[1]):
|
||||
return "📲SMS address set 📪"
|
||||
else:
|
||||
return "⛔️Failed to set address"
|
||||
|
||||
if message.lower().startswith("sms:"):
|
||||
message = message.split(" ", 1)
|
||||
if any(item['nodeID'] == nodeID for item in sms_db):
|
||||
count = 0
|
||||
# for all dict items maching nodeID in sms_db send sms
|
||||
for item in sms_db:
|
||||
if item['nodeID'] == nodeID:
|
||||
smsEmail = item['sms']
|
||||
logger.info("System: Sending SMS for " + str(nodeID) + " to " + smsEmail[:-6])
|
||||
if send_email(smsEmail, message[1], nodeID):
|
||||
count += 1
|
||||
else:
|
||||
return "⛔️Failed to send SMS"
|
||||
return "📲SMS sent " + str(count) + " addresses 📤"
|
||||
else:
|
||||
return "📲No address set, use 📲setsms"
|
||||
|
||||
return "Error: ⛔️ not understood. use:setsms example@phone.co"
|
||||
|
||||
def handle_email(nodeID, message):
|
||||
global email_db
|
||||
try:
|
||||
# send email to email in db. if none ask for one
|
||||
if message.lower().startswith("setemail"):
|
||||
message = message.split(" ", 1)
|
||||
if len(message) < 2:
|
||||
return "📧Please provide an email address"
|
||||
email_addr = message[1].strip()
|
||||
if "@" not in email_addr or "." not in email_addr:
|
||||
return "📧Please provide a valid email address"
|
||||
if store_email(nodeID, email_addr):
|
||||
return "📧Email address set 📪"
|
||||
return "Error: ⛔️ Failed to set email address"
|
||||
|
||||
if message.lower().startswith("email:"):
|
||||
parts = message.split(" ", 1)
|
||||
if len(parts) < 2:
|
||||
return "Error: ⛔️ format should be: email: message or, email: address@example.com #message"
|
||||
|
||||
content = parts[1].strip()
|
||||
|
||||
# Check if this is a direct email with address
|
||||
if "@" in content and "#" in content:
|
||||
# Split into email and message
|
||||
addr_msg = content.split("#", 1)
|
||||
if len(addr_msg) != 2:
|
||||
return "Error: ⛔️ Message format should be: email: address@example.com #message"
|
||||
|
||||
to_email = addr_msg[0].strip()
|
||||
message_body = addr_msg[1].strip()
|
||||
|
||||
logger.info(f"System: Sending email for {nodeID} to {to_email}")
|
||||
if send_email(to_email, message_body, nodeID):
|
||||
return "📧Email-sent 📤"
|
||||
return "⛔️Failed to send email"
|
||||
|
||||
# Using stored email address
|
||||
elif nodeID in email_db:
|
||||
logger.info(f"System: Sending email for {nodeID} to stored address")
|
||||
if send_email(email_db[nodeID], content, nodeID):
|
||||
return "📧Email-sent 📤"
|
||||
return "⛔️Failed to send email"
|
||||
|
||||
return "Error: ⛔️ no email on file. use: setemail"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"System: Email handling error: {str(e)}")
|
||||
return "⛔️Failed to process email command"
|
||||
@@ -9,7 +9,7 @@ import ephem # pip install pyephem
|
||||
from datetime import timedelta
|
||||
from modules.log import *
|
||||
|
||||
trap_list_solarconditions = ("sun", "solar", "hfcond")
|
||||
trap_list_solarconditions = ("sun", "solar", "hfcond", "satpass")
|
||||
|
||||
def hf_band_conditions():
|
||||
# ham radio HF band conditions
|
||||
@@ -140,3 +140,43 @@ def get_moon(lat=0, lon=0):
|
||||
+ "\nFullMoon:" + moon_table['next_full_moon'] + "\nNewMoon:" + moon_table['next_new_moon']
|
||||
|
||||
return moon_data
|
||||
|
||||
def getNextSatellitePass(satellite, lat=0, lon=0):
|
||||
pass_data = ''
|
||||
# get the next satellite pass for a given satellite
|
||||
visualPassAPI = "https://api.n2yo.com/rest/v1/satellite/visualpasses/"
|
||||
if lat == 0 and lon == 0:
|
||||
lat = latitudeValue
|
||||
lon = longitudeValue
|
||||
# API URL
|
||||
if n2yoAPIKey == '':
|
||||
logger.error("System: Missing API key free at https://www.n2yo.com/login/")
|
||||
return "not configured, bug your sysop"
|
||||
url = visualPassAPI + str(satellite) + "/" + str(lat) + "/" + str(lon) + "/0/2/300/" + "&apiKey=" + n2yoAPIKey
|
||||
# get the next pass data
|
||||
try:
|
||||
if not int(satellite):
|
||||
raise Exception("Invalid satellite number")
|
||||
next_pass_data = requests.get(url, timeout=urlTimeoutSeconds)
|
||||
if(next_pass_data.ok):
|
||||
pass_json = next_pass_data.json()
|
||||
if 'info' in pass_json and 'passescount' in pass_json['info'] and pass_json['info']['passescount'] > 0:
|
||||
satname = pass_json['info']['satname']
|
||||
pass_time = pass_json['passes'][0]['startUTC']
|
||||
pass_duration = pass_json['passes'][0]['duration']
|
||||
pass_maxEl = pass_json['passes'][0]['maxEl']
|
||||
pass_rise_time = datetime.fromtimestamp(pass_time).strftime('%a %d %I:%M%p')
|
||||
pass_startAzCompass = pass_json['passes'][0]['startAzCompass']
|
||||
pass_set_time = datetime.fromtimestamp(pass_time + pass_duration).strftime('%a %d %I:%M%p')
|
||||
pass__endAzCompass = pass_json['passes'][0]['endAzCompass']
|
||||
pass_data = f"{satname} @{pass_rise_time} Az:{pass_startAzCompass} for{getPrettyTime(pass_duration)}, MaxEl:{pass_maxEl}° Set@{pass_set_time} Az:{pass__endAzCompass}"
|
||||
elif pass_json['info']['passescount'] == 0:
|
||||
satname = pass_json['info']['satname']
|
||||
pass_data = f"{satname} has no upcoming passes"
|
||||
else:
|
||||
logger.error(f"System: Error fetching satellite pass data {satellite}")
|
||||
pass_data = ERROR_FETCHING_DATA
|
||||
except Exception as e:
|
||||
logger.warning(f"System: User supplied value {satellite} unknown or invalid")
|
||||
pass_data = "Provide NORAD# example use:🛰️satpass 25544,33591"
|
||||
return pass_data
|
||||
+52
-30
@@ -13,7 +13,7 @@ from modules.log import *
|
||||
|
||||
# Global Variables
|
||||
trap_list = ("cmd","cmd?") # default trap list
|
||||
help_message = "Bot CMD?:\n"
|
||||
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}]
|
||||
@@ -38,17 +38,30 @@ if motd_enabled:
|
||||
trap_list = trap_list + trap_list_motd
|
||||
help_message = help_message + ", motd"
|
||||
|
||||
# SMTP Configuration
|
||||
if enableSMTP:
|
||||
from modules.smtp import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + trap_list_smtp
|
||||
help_message = help_message + ", email:, sms:"
|
||||
|
||||
# Emergency Responder Configuration
|
||||
if emergency_responder_enabled:
|
||||
trap_list_emergency = ("emergency", "911", "112", "999", "police", "fire", "ambulance", "rescue")
|
||||
trap_list = trap_list + trap_list_emergency
|
||||
|
||||
# whoami Configuration
|
||||
if whoami_enabled:
|
||||
trap_list_whoami = ("whoami", "📍")
|
||||
trap_list_whoami = ("whoami", "📍", "whois")
|
||||
trap_list = trap_list + trap_list_whoami
|
||||
help_message = help_message + ", whoami"
|
||||
|
||||
# Solar Conditions Configuration
|
||||
if solar_conditions_enabled:
|
||||
from modules.solarconditions import * # from the spudgunman/meshing-around repo
|
||||
from modules.space import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + trap_list_solarconditions # items hfcond, solar, sun, moon
|
||||
help_message = help_message + ", sun, hfcond, solar, moon"
|
||||
if n2yoAPIKey != "":
|
||||
help_message = help_message + ", satpass"
|
||||
else:
|
||||
hf_band_conditions = False
|
||||
|
||||
@@ -68,7 +81,7 @@ if location_enabled:
|
||||
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, tide, ealert"
|
||||
|
||||
# BBS Configuration
|
||||
if bbs_enabled:
|
||||
@@ -180,6 +193,14 @@ if file_monitor_enabled or read_news_enabled:
|
||||
trap_list = trap_list + trap_list_filemon # items readnews
|
||||
help_message = help_message + ", readmail"
|
||||
|
||||
# clean up the help message
|
||||
help_message = help_message.split(", ")
|
||||
help_message.sort()
|
||||
if len(help_message) > 20:
|
||||
# split in half for formatting
|
||||
help_message = help_message[:len(help_message)//2] + ["\nCMD?"] + help_message[len(help_message)//2:]
|
||||
help_message = ", ".join(help_message)
|
||||
|
||||
# BLE dual interface prevention
|
||||
if interface1_type == 'ble' and interface2_type == 'ble':
|
||||
logger.critical(f"System: BLE Interface1 and Interface2 cannot both be BLE. Exiting")
|
||||
@@ -427,22 +448,39 @@ def messageChunker(message):
|
||||
else:
|
||||
# split the part into chunks
|
||||
current_chunk = ''
|
||||
sentences = part.split('. ')
|
||||
sentences = []
|
||||
sentence = ''
|
||||
for char in part:
|
||||
sentence += char
|
||||
if char in '.!?':
|
||||
sentences.append(sentence.strip())
|
||||
sentence = ''
|
||||
if sentence:
|
||||
sentences.append(sentence.strip())
|
||||
|
||||
for sentence in sentences:
|
||||
sentence = sentence.strip()
|
||||
sentence = sentence.replace(' ', ' ')
|
||||
# remove empty sentences
|
||||
if not sentence:
|
||||
continue
|
||||
# remove junk sentences and append to the previous sentence this may exceed the MESSAGE_CHUNK_SIZE by 3
|
||||
if len(sentence) < 4:
|
||||
if current_chunk:
|
||||
current_chunk += sentence
|
||||
else:
|
||||
current_chunk = sentence
|
||||
continue
|
||||
|
||||
# if sentence is too long, split it by words
|
||||
if len(current_chunk) + len(sentence) > MESSAGE_CHUNK_SIZE:
|
||||
message_list.append(current_chunk)
|
||||
if current_chunk:
|
||||
message_list.append(current_chunk)
|
||||
current_chunk = sentence
|
||||
else:
|
||||
if current_chunk:
|
||||
current_chunk += ' '
|
||||
current_chunk += sentence
|
||||
current_chunk += ' ' + sentence
|
||||
else:
|
||||
current_chunk = sentence
|
||||
if current_chunk:
|
||||
message_list.append(current_chunk)
|
||||
|
||||
@@ -485,7 +523,7 @@ def send_message(message, ch, nodeid=0, nodeInt=1, bypassChuncking=False):
|
||||
if nodeid == 0:
|
||||
# Send to channel
|
||||
if wantAck:
|
||||
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + f"req.ACK " + "Chunker{chunkOf} SendingChannel: " + CustomFormatter.white + m.replace('\n', ' '))
|
||||
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + f"req.ACK " + f"Chunker{chunkOf} SendingChannel: " + CustomFormatter.white + m.replace('\n', ' '))
|
||||
interface.sendText(text=m, channelIndex=ch, wantAck=True)
|
||||
else:
|
||||
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + f"Chunker{chunkOf} SendingChannel: " + CustomFormatter.white + m.replace('\n', ' '))
|
||||
@@ -556,23 +594,6 @@ def get_wikipedia_summary(search_term):
|
||||
|
||||
return summary
|
||||
|
||||
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
|
||||
|
||||
def messageTrap(msg):
|
||||
# Check if the message contains a trap word, this is the first filter for listning to messages
|
||||
# after this the message is passed to the command_handler in the bot.py which is switch case filter for applying word to function
|
||||
@@ -1015,14 +1036,16 @@ async def handleSentinel(deviceID=1):
|
||||
|
||||
if handleSentinel_loop >= sentry_holdoff and handleSentinel_spotted != enemySpotted:
|
||||
# check the positionMetadata for nodeID and get metadata
|
||||
if positionMetadata and closest_nodes[0]['id'] in positionMetadata:
|
||||
if closest_nodes and positionMetadata and closest_nodes[0]['id'] in positionMetadata:
|
||||
metadata = positionMetadata[closest_nodes[0]['id']]
|
||||
if metadata.get('precisionBits') is not None:
|
||||
resolution = metadata.get('precisionBits')
|
||||
|
||||
|
||||
logger.warning(f"System: {enemySpotted} is close to your location on Interface1 Accuracy is {resolution}bits")
|
||||
send_message(f"Sentry{deviceID}: {enemySpotted}", secure_channel, 0, deviceID)
|
||||
if enableSMTP and email_sentry_alerts:
|
||||
for email in sysopEmails:
|
||||
send_email(email, f"Sentry{deviceID}: {enemySpotted}")
|
||||
handleSentinel_loop = 0
|
||||
handleSentinel_spotted = enemySpotted
|
||||
else:
|
||||
@@ -1098,4 +1121,3 @@ async def watchdog():
|
||||
except Exception as e:
|
||||
logger.error(f"System: retrying interface2: {e}")
|
||||
|
||||
|
||||
|
||||
+202
-117
@@ -8,24 +8,29 @@ from pubsub import pub # pip install pubsub
|
||||
from modules.log import *
|
||||
from modules.system import *
|
||||
|
||||
# Global Variables
|
||||
cmdHistory = [] # list to hold the last commands
|
||||
DEBUGpacket = False # Debug print the packet rx
|
||||
|
||||
def auto_response(message, snr, rssi, hop, message_from_id, channel_number, deviceID):
|
||||
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
|
||||
# Auto response to messages
|
||||
message_lower = message.lower()
|
||||
bot_response = "I'm sorry, I'm afraid I can't do that."
|
||||
|
||||
command_handler = {
|
||||
"ping": lambda: handle_ping(message, hop, snr, rssi),
|
||||
"pong": lambda: "🏓Ping!!",
|
||||
"motd": lambda: handle_motd(message, MOTD),
|
||||
"ack": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"cmd": lambda: help_message,
|
||||
"cmd?": lambda: help_message,
|
||||
"cq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"cqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"cqcqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"lheard": lambda: handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2),
|
||||
"motd": lambda: handle_motd(message, MOTD),
|
||||
"ping": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"pong": lambda: "🏓PING!!🛜",
|
||||
"sitrep": lambda: handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2),
|
||||
"ack": lambda: handle_ack(hop, snr, rssi),
|
||||
"testing": lambda: handle_testing(hop, snr, rssi),
|
||||
"test": lambda: handle_testing(hop, snr, rssi),
|
||||
"test": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"testing": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
}
|
||||
cmds = [] # list to hold the commands found in the message
|
||||
for key in command_handler:
|
||||
@@ -44,17 +49,88 @@ def auto_response(message, snr, rssi, hop, message_from_id, channel_number, devi
|
||||
|
||||
return bot_response
|
||||
|
||||
def handle_ping(message, hop, snr, rssi):
|
||||
if "@" in message:
|
||||
if hop == "Direct":
|
||||
return "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}" + " and copy: " + message.split("@")[1]
|
||||
else:
|
||||
return "🏓PONG, " + hop + " and copy: " + message.split("@")[1]
|
||||
def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number):
|
||||
global multiPing
|
||||
if "?" in message and isDM:
|
||||
return message.split("?")[0].title() + " command returns SNR and RSSI, or hopcount from your message. Try adding e.g. @place or #tag"
|
||||
|
||||
msg = ""
|
||||
type = ''
|
||||
|
||||
if "ping" in message.lower():
|
||||
msg = "🏓PONG\n"
|
||||
type = "🏓PING"
|
||||
elif "test" in message.lower() or "testing" in message.lower():
|
||||
msg = random.choice(["🎙Testing 1,2,3\n", "🎙Testing\n",\
|
||||
"🎙Testing, testing\n",\
|
||||
"🎙Ah-wun, ah-two...\n", "🎙Is this thing on?\n",\
|
||||
"🎙Roger that!\n",])
|
||||
type = "🎙TEST"
|
||||
elif "ack" in message.lower():
|
||||
msg = random.choice(["✋ACK-ACK!\n", "✋Ack to you!\n"])
|
||||
type = "✋ACK"
|
||||
elif "cqcq" in message.lower() or "cq" in message.lower() or "cqcqcq" in message.lower():
|
||||
if deviceID == 1:
|
||||
myname = get_name_from_number(myNodeNum1, 'short', 1)
|
||||
elif deviceID == 2:
|
||||
myname = get_name_from_number(myNodeNum2, 'short', 2)
|
||||
msg = f"QSP QSL OM DE {myname} K\n"
|
||||
else:
|
||||
if hop == "Direct":
|
||||
return "🏓PONG, " + f"SNR:{snr} RSSI:{rssi}"
|
||||
msg = "🔊 Can you hear me now?"
|
||||
|
||||
if hop == "Direct":
|
||||
msg = msg + f"SNR:{snr} RSSI:{rssi}"
|
||||
else:
|
||||
msg = msg + hop
|
||||
|
||||
if "@" in message:
|
||||
msg = msg + " @" + message.split("@")[1]
|
||||
type = type + " @" + message.split("@")[1]
|
||||
elif "#" in message:
|
||||
msg = msg + " #" + message.split("#")[1]
|
||||
type = type + " #" + message.split("#")[1]
|
||||
|
||||
|
||||
# check for multi ping request
|
||||
if " " in message:
|
||||
# if stop multi ping
|
||||
if "stop" in message.lower():
|
||||
for i in range(0, len(multiPingList)):
|
||||
if multiPingList[i].get('message_from_id') == message_from_id:
|
||||
multiPingList.pop(i)
|
||||
msg = "🛑 auto-ping"
|
||||
|
||||
# disabled in channel
|
||||
if autoPingInChannel and not isDM:
|
||||
# if 3 or more entries (2 or more active), throttle the multi-ping for congestion
|
||||
if len(multiPingList) > 2:
|
||||
msg = "🚫⛔️ auto-ping, service busy. ⏳Try again soon."
|
||||
pingCount = -1
|
||||
else:
|
||||
# set inital pingCount
|
||||
try:
|
||||
pingCount = int(message.split(" ")[1])
|
||||
if pingCount == 123 or pingCount == 1234:
|
||||
pingCount = 1
|
||||
if pingCount > 51:
|
||||
pingCount = 50
|
||||
except:
|
||||
pingCount = -1
|
||||
|
||||
if pingCount > 1:
|
||||
multiPingList.append({'message_from_id': message_from_id, 'count': pingCount + 1, 'type': type, 'deviceID': deviceID, 'channel_number': channel_number, 'startCount': pingCount})
|
||||
if type == "🎙TEST":
|
||||
msg = f"🛜Initalizing BufferTest, using chunks of about {int(maxBuffer // pingCount)}, max length {maxBuffer} in {pingCount} messages"
|
||||
else:
|
||||
msg = f"🚦Initalizing {pingCount} auto-ping"
|
||||
else:
|
||||
return "🏓PONG, " + hop
|
||||
msg = "🔊AutoPing via DM only⛔️"
|
||||
|
||||
# if not a DM add the username to the beginning of msg
|
||||
if not isDM:
|
||||
msg = get_name_from_number(message_from_id) + msg
|
||||
|
||||
return msg
|
||||
|
||||
def handle_motd(message):
|
||||
global MOTD
|
||||
@@ -65,64 +141,37 @@ def handle_motd(message):
|
||||
else:
|
||||
return MOTD
|
||||
|
||||
def handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2):
|
||||
bot_response = "Last heard:\n" + str(get_node_list(1))
|
||||
chutil1 = interface1.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
|
||||
chutil1 = "{:.2f}".format(chutil1)
|
||||
if interface2_enabled:
|
||||
bot_response += "Port2:\n" + str(get_node_list(2))
|
||||
chutil2 = interface2.nodes.get(decimal_to_hex(myNodeNum2), {}).get("deviceMetrics", {}).get("channelUtilization", 0)
|
||||
chutil2 = "{:.2f}".format(chutil2)
|
||||
def handle_lheard(message, nodeid, deviceID, isDM):
|
||||
if "?" in message and isDM:
|
||||
return message.split("?")[0].title() + " command returns a list of the nodes that have been heard recently"
|
||||
|
||||
# display last heard nodes add to response
|
||||
bot_response = "Last Heard\n"
|
||||
bot_response += str(get_node_list(1))
|
||||
|
||||
# show last users of the bot with the cmdHistory list
|
||||
history = handle_history(message, nodeid, deviceID, isDM, lheard=True)
|
||||
if history:
|
||||
bot_response += f'LastSeen\n{history}'
|
||||
else:
|
||||
# trim the last \n
|
||||
bot_response = bot_response[:-1]
|
||||
|
||||
# bot_response += getNodeTelemetry(deviceID)
|
||||
return bot_response
|
||||
|
||||
def handle_ack(hop, snr, rssi):
|
||||
if hop == "Direct":
|
||||
return "✋ACK-ACK! " + f"SNR:{snr} RSSI:{rssi}"
|
||||
else:
|
||||
return "✋ACK-ACK! " + hop
|
||||
|
||||
def handle_testing(hop, snr, rssi):
|
||||
if hop == "Direct":
|
||||
return "🎙Testing 1,2,3 " + f"SNR:{snr} RSSI:{rssi}"
|
||||
else:
|
||||
return "🎙Testing 1,2,3 " + hop
|
||||
|
||||
def onDisconnect(interface):
|
||||
global retry_int1, retry_int2
|
||||
rxType = type(interface).__name__
|
||||
if rxType == 'SerialInterface':
|
||||
rxInterface = interface.__dict__.get('devPath', 'unknown')
|
||||
logger.critical(f"System: Lost Connection to Device {rxInterface}")
|
||||
if port1 in rxInterface:
|
||||
retry_int1 = True
|
||||
elif interface2_enabled and port2 in rxInterface:
|
||||
retry_int2 = True
|
||||
|
||||
if rxType == 'TCPInterface':
|
||||
rxHost = interface.__dict__.get('hostname', 'unknown')
|
||||
logger.critical(f"System: Lost Connection to Device {rxHost}")
|
||||
if hostname1 in rxHost and interface1_type == 'tcp':
|
||||
retry_int1 = True
|
||||
elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp':
|
||||
retry_int2 = True
|
||||
|
||||
if rxType == 'BLEInterface':
|
||||
logger.critical(f"System: Lost Connection to Device BLE")
|
||||
if interface1_type == 'ble':
|
||||
retry_int1 = True
|
||||
elif interface2_enabled and interface2_type == 'ble':
|
||||
retry_int2 = True
|
||||
|
||||
def onReceive(packet, interface):
|
||||
# extract interface defailts from interface object
|
||||
# Priocess the incoming packet, handles the responses to the packet with auto_response()
|
||||
# Sends the packet to the correct handler for processing
|
||||
|
||||
# extract interface details from inbound packet
|
||||
rxType = type(interface).__name__
|
||||
rxNode = 0
|
||||
message_from_id = 0
|
||||
snr = 0
|
||||
rssi = 0
|
||||
hop = 0
|
||||
hop_away = 0
|
||||
|
||||
|
||||
# Valies assinged to the packet
|
||||
rxNode, message_from_id, snr, rssi, hop, hop_away, channel_number = 0, 0, 0, 0, 0, 0, 0
|
||||
pkiStatus = (False, 'ABC')
|
||||
isDM = False
|
||||
|
||||
if DEBUGpacket:
|
||||
# Debug print the interface object
|
||||
for item in interface.__dict__.items(): intDebug = f"{item}\n"
|
||||
@@ -130,6 +179,7 @@ def onReceive(packet, interface):
|
||||
# Debug print the packet for debugging
|
||||
logger.debug(f"Packet Received\n {packet} \n END of packet \n")
|
||||
|
||||
# set the value for the incomming interface
|
||||
if rxType == 'SerialInterface':
|
||||
rxInterface = interface.__dict__.get('devPath', 'unknown')
|
||||
if port1 in rxInterface:
|
||||
@@ -150,42 +200,47 @@ def onReceive(packet, interface):
|
||||
elif interface2_enabled and interface2_type == 'ble':
|
||||
rxNode = 2
|
||||
|
||||
# check for a message packet and process it
|
||||
# check if the packet has a channel flag use it
|
||||
if packet.get('channel'):
|
||||
channel_number = packet.get('channel', 0)
|
||||
|
||||
# handle TEXT_MESSAGE_APP
|
||||
try:
|
||||
if 'decoded' in packet and packet['decoded']['portnum'] == 'TEXT_MESSAGE_APP':
|
||||
message_bytes = packet['decoded']['payload']
|
||||
message_string = message_bytes.decode('utf-8')
|
||||
message_from_id = packet['from']
|
||||
try:
|
||||
snr = packet['rxSnr']
|
||||
rssi = packet['rxRssi']
|
||||
except KeyError:
|
||||
snr = 0
|
||||
rssi = 0
|
||||
|
||||
if packet.get('channel'):
|
||||
channel_number = packet['channel']
|
||||
else:
|
||||
channel_number = publicChannel
|
||||
|
||||
# get the signal strength and snr if available
|
||||
if packet.get('rxSnr') or packet.get('rxRssi'):
|
||||
snr = packet.get('rxSnr', 0)
|
||||
rssi = packet.get('rxRssi', 0)
|
||||
|
||||
# check if the packet has a publicKey flag use it
|
||||
if packet.get('publicKey'):
|
||||
pkiStatus = (packet.get('pkiEncrypted', False), packet.get('publicKey', 'ABC'))
|
||||
|
||||
# check if the packet has a hop count flag use it
|
||||
if packet.get('hopsAway'):
|
||||
hop_away = packet['hopsAway']
|
||||
hop_away = packet.get('hopsAway', 0)
|
||||
else:
|
||||
# if the packet does not have a hop count try other methods
|
||||
hop_away = 0
|
||||
if packet.get('hopLimit'):
|
||||
hop_limit = packet['hopLimit']
|
||||
hop_limit = packet.get('hopLimit', 0)
|
||||
else:
|
||||
hop_limit = 0
|
||||
|
||||
if packet.get('hopStart'):
|
||||
hop_start = packet['hopStart']
|
||||
hop_start = packet.get('hopStart', 0)
|
||||
else:
|
||||
hop_start = 0
|
||||
|
||||
if hop_start == hop_limit:
|
||||
hop = "Direct"
|
||||
hop_count = 0
|
||||
elif hop_start == 0 and hop_limit > 0:
|
||||
hop = "MQTT"
|
||||
hop_count = 0
|
||||
else:
|
||||
# set hop to Direct if the message was sent directly otherwise set the hop count
|
||||
if hop_away > 0:
|
||||
@@ -196,46 +251,53 @@ def onReceive(packet, interface):
|
||||
|
||||
hop = f"{hop_count} hops"
|
||||
|
||||
if message_string == help_message or message_string == welcome_message or "CMD?:" in message_string:
|
||||
if help_message in message_string or welcome_message in message_string or "CMD?:" in message_string:
|
||||
# ignore help and welcome messages
|
||||
logger.warning(f"Got Own Welcome/Help header. From: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
return
|
||||
|
||||
|
||||
# If the packet is a DM (Direct Message) respond to it, otherwise validate its a message for us on the channel
|
||||
if packet['to'] == myNodeNum1 or packet['to'] == myNodeNum2:
|
||||
# message is DM to us
|
||||
|
||||
isDM = True
|
||||
# check if the message contains a trap word, DMs are always responded to
|
||||
if messageTrap(message_string):
|
||||
if (messageTrap(message_string) and not llm_enabled) or messageTrap(message_string.split()[0]):
|
||||
# log the message to the message log
|
||||
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
|
||||
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
|
||||
else:
|
||||
# respond with welcome message on DM
|
||||
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, message_from_id, rxNode)
|
||||
else:
|
||||
logger.warning(f"Device:{rxNode} Ignoring DM: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
send_message(welcome_message, channel_number, message_from_id, rxNode)
|
||||
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | {message_string}")
|
||||
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-'))
|
||||
else:
|
||||
# message is on a channel
|
||||
if messageTrap(message_string):
|
||||
# message is for bot to respond to
|
||||
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "Received: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
|
||||
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
if useDMForResponse:
|
||||
# respond to channel message via direct message
|
||||
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
|
||||
if ignoreDefaultChannel and channel_number == publicChannel:
|
||||
logger.debug(f"System: ignoreDefaultChannel CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)}")
|
||||
else:
|
||||
# or respond to channel message on the channel itself
|
||||
if channel_number == publicChannel and antiSpam:
|
||||
# warning user spamming default channel
|
||||
logger.error(f"System: AntiSpam protection, sending DM to: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
|
||||
# message is for bot to respond to
|
||||
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "ReceivedChannel: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
|
||||
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
if useDMForResponse:
|
||||
# respond to channel message via direct message
|
||||
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, message_from_id, rxNode)
|
||||
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, message_from_id, rxNode)
|
||||
else:
|
||||
# respond to channel message on the channel itself
|
||||
send_message(auto_response(message_string, snr, rssi, hop, message_from_id, channel_number, rxNode), channel_number, 0, rxNode)
|
||||
# or respond to channel message on the channel itself
|
||||
if channel_number == publicChannel and antiSpam:
|
||||
# warning user spamming default channel
|
||||
logger.warning(f"System: AntiSpam protection, sending DM to: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
|
||||
# respond to channel message via direct message
|
||||
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, message_from_id, rxNode)
|
||||
else:
|
||||
# respond to channel message on the channel itself
|
||||
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, 0, rxNode)
|
||||
|
||||
else:
|
||||
# message is not for bot to respond to
|
||||
# ignore the message but add it to the message history list
|
||||
@@ -269,10 +331,12 @@ def onReceive(packet, interface):
|
||||
elif rxNode == 2:
|
||||
logger.debug(f"Repeating message on Device1 Channel:{channel_number}")
|
||||
send_message(rMsg, channel_number, 0, 1)
|
||||
else:
|
||||
# Evaluate non TEXT_MESSAGE_APP packets
|
||||
consumeMetadata(packet, rxNode)
|
||||
except KeyError as e:
|
||||
logger.critical(f"System: Error processing packet: {e} Device:{rxNode}")
|
||||
print(packet) # print the packet for debugging
|
||||
print("END of packet \n")
|
||||
logger.debug(f"System: Error Packet = {packet}")
|
||||
|
||||
async def start_rx():
|
||||
print (CustomFormatter.bold_white + f"\nMeshtastic Autoresponder Bot CTL+C to exit\n" + CustomFormatter.reset)
|
||||
@@ -286,16 +350,32 @@ async def start_rx():
|
||||
f"{get_name_from_number(myNodeNum2, 'short', 2)}. NodeID: {myNodeNum2}, {decimal_to_hex(myNodeNum2)}")
|
||||
if log_messages_to_file:
|
||||
logger.debug("System: Logging Messages to disk")
|
||||
if syslog_to_file:
|
||||
logger.debug("System: Logging System Logs to disk")
|
||||
if solar_conditions_enabled:
|
||||
logger.debug("System: Celestial Telemetry Enabled")
|
||||
if motd_enabled:
|
||||
logger.debug(f"System: MOTD Enabled using {MOTD}")
|
||||
if sentry_enabled:
|
||||
logger.debug("System: Sentry Enabled")
|
||||
logger.debug(f"System: Sentry Mode Enabled {sentry_radius}m radius reporting to channel:{secure_channel}")
|
||||
if store_forward_enabled:
|
||||
logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}")
|
||||
if useDMForResponse:
|
||||
logger.debug("System: Respond by DM only")
|
||||
logger.debug(f"System: Respond by DM only")
|
||||
if repeater_enabled and interface2_enabled:
|
||||
logger.debug(f"System: Repeater Enabled for Channels: {repeater_channels}")
|
||||
if radio_detection_enabled:
|
||||
logger.debug(f"System: Radio Detection Enabled using rigctld at {rigControlServerAddress} brodcasting to channels: {sigWatchBroadcastCh} for {get_freq_common_name(get_hamlib('f'))}")
|
||||
if file_monitor_enabled:
|
||||
logger.debug(f"System: File Monitor Enabled for {file_monitor_file_path}, broadcasting to channels: {file_monitor_broadcastCh}")
|
||||
if read_news_enabled:
|
||||
logger.debug(f"System: File Monitor News Reader Enabled for {news_file_path}")
|
||||
if scheduler_enabled:
|
||||
# Examples of using the scheduler, Times here are in 24hr format
|
||||
# https://schedule.readthedocs.io/en/stable/
|
||||
|
||||
# Reminder Scheduler is enabled every Monday at noon send a log message
|
||||
schedule.every().monday.at("12:00").do(lambda: logger.info("System: Scheduled Broadcast Reminder"))
|
||||
logger.debug("System: Starting the broadcast scheduler")
|
||||
await BroadcastScheduler()
|
||||
|
||||
# here we go loopty loo
|
||||
while True:
|
||||
@@ -306,14 +386,19 @@ async def start_rx():
|
||||
async def main():
|
||||
meshRxTask = asyncio.create_task(start_rx())
|
||||
watchdogTask = asyncio.create_task(watchdog())
|
||||
await asyncio.wait([meshRxTask, watchdogTask])
|
||||
if file_monitor_enabled:
|
||||
fileMonTask: asyncio.Task = asyncio.create_task(handleFileWatcher())
|
||||
|
||||
await asyncio.gather(meshRxTask, watchdogTask)
|
||||
if file_monitor_enabled:
|
||||
await asyncio.gather(fileMonTask)
|
||||
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
try:
|
||||
asyncLoop = asyncio.new_event_loop()
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
exit_handler()
|
||||
pass
|
||||
|
||||
# EOF
|
||||
|
||||
@@ -3,7 +3,6 @@ pubsub
|
||||
datetime
|
||||
pyephem
|
||||
requests
|
||||
geopy
|
||||
maidenhead
|
||||
beautifulsoup4
|
||||
dadjokes
|
||||
@@ -13,5 +12,4 @@ numpy
|
||||
geopy
|
||||
schedule
|
||||
wikipedia
|
||||
ollama
|
||||
googlesearch-python
|
||||
|
||||
Reference in New Issue
Block a user