mirror of
https://github.com/SpudGunMan/meshing-around.git
synced 2026-05-04 20:42:16 +02:00
Compare commits
106 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
207
README.md
207
README.md
@@ -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,7 +24,8 @@ 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.
|
||||
@@ -41,13 +43,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,7 +59,7 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
- **Message Chunking**: Automatically chunk messages over 160 characters to ensure higher delivery success across hops.
|
||||
|
||||
## Getting Started
|
||||
This project is developed on Linux (specifically a Raspberry Pi) but should work on any platform where the [Meshtastic protobuf API](https://meshtastic.org/docs/software/python/cli/) modules are supported, and with any compatible [Meshtastic](https://meshtastic.org/docs/getting-started/) hardware.
|
||||
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
|
||||
|
||||
@@ -73,7 +76,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 +125,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 +171,72 @@ 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
|
||||
UNDER DEV..
|
||||
|
||||
This uses the SAME code to locate the bot and alerts.
|
||||
|
||||
```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,56 +339,12 @@ 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
|
||||
```
|
||||
|
||||
## Full list of commands for the bot
|
||||
|
||||
### Networking
|
||||
@@ -356,8 +367,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 +380,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 | |
|
||||
@@ -378,8 +395,6 @@ sudo apt-get install fonts-noto-color-emoji
|
||||
| `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 | ✅ |
|
||||
|
||||
|
||||
|
||||
### Games (via DM)
|
||||
| Command | Description | |
|
||||
|---------|-------------|-
|
||||
@@ -412,10 +427,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
|
||||
```
|
||||
|
||||
@@ -29,35 +29,45 @@ respond_by_dm_only = True
|
||||
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 +75,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,6 +106,33 @@ 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
|
||||
@@ -118,6 +152,15 @@ repeaterLookup = rbook
|
||||
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
|
||||
|
||||
# repeater module
|
||||
[repeater]
|
||||
@@ -153,12 +196,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
|
||||
|
||||
@@ -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()
|
||||
|
||||
120
mesh_bot.py
120
mesh_bot.py
@@ -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,10 @@ 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),
|
||||
"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),
|
||||
@@ -71,11 +80,20 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
"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
|
||||
@@ -189,6 +207,29 @@ 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):
|
||||
# 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
|
||||
@@ -576,6 +617,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 +707,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
|
||||
@@ -800,6 +853,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 +863,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 +874,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 +899,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 +923,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 +931,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,7 +955,12 @@ 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
|
||||
@@ -938,10 +1011,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 +1153,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 +1211,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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,101 @@ 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
|
||||
ipawsPIN = "000000"
|
||||
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
|
||||
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
|
||||
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
|
||||
description = info.getElementsByTagName("description")[0].childNodes[0].nodeValue
|
||||
|
||||
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
|
||||
sameVal = "NONE"
|
||||
if geocode_type == "SAME":
|
||||
sameVal = geocode_value
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -125,12 +133,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 +157,11 @@ 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
|
||||
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
|
||||
# brodcast channel for weather alerts
|
||||
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh')
|
||||
if wxAlertBroadcastChannel:
|
||||
@@ -150,7 +169,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 +178,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
modules/smtp.py
Normal file
266
modules/smtp.py
Normal file
@@ -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"
|
||||
@@ -38,6 +38,17 @@ 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", "📍")
|
||||
@@ -68,7 +79,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:
|
||||
@@ -427,22 +438,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)
|
||||
|
||||
@@ -1023,6 +1051,9 @@ async def handleSentinel(deviceID=1):
|
||||
|
||||
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:
|
||||
|
||||
307
pong_bot.py
307
pong_bot.py
@@ -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,79 @@ 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"
|
||||
|
||||
# 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:
|
||||
return "🏓PONG, " + hop
|
||||
# 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"
|
||||
|
||||
return msg
|
||||
|
||||
def handle_motd(message):
|
||||
global MOTD
|
||||
@@ -65,64 +132,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 +170,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 +191,44 @@ 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
|
||||
else:
|
||||
# set hop to Direct if the message was sent directly otherwise set the hop count
|
||||
if hop_away > 0:
|
||||
@@ -196,46 +239,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 +319,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 +338,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 +374,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