mirror of
https://github.com/SpudGunMan/meshing-around.git
synced 2026-03-28 17:32:36 +01:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eeab9f3fb1 | ||
|
|
c21a67d1cf | ||
|
|
afe48a44da | ||
|
|
7e4822e4ec | ||
|
|
705ab6a980 | ||
|
|
963b29eea4 | ||
|
|
b3f889c4c7 | ||
|
|
545b4891b4 | ||
|
|
c89f14b3c2 | ||
|
|
c416b00383 | ||
|
|
669a891eeb | ||
|
|
520d58b262 | ||
|
|
24dff868ff | ||
|
|
cf45bb5060 | ||
|
|
0f9064f2c3 | ||
|
|
f94f329b1f | ||
|
|
dc4560081d | ||
|
|
b42cd0e6dc | ||
|
|
bbe1e45541 | ||
|
|
2c61db1215 | ||
|
|
fde2bb94d9 | ||
|
|
436a43d3ad | ||
|
|
6b2a6f3a83 | ||
|
|
8e5773115c |
57
README.md
57
README.md
@@ -19,6 +19,7 @@ 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
|
||||
|
||||
### 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.
|
||||
@@ -36,6 +37,13 @@ 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
|
||||
|
||||
### File Monitor Alerts
|
||||
- **File Mon**: Monitor a flat/text file for changes, brodcast the contents of the message to mesh channel.
|
||||
|
||||
### Data Reporting
|
||||
- **HTML Generator**: Visualize bot traffic and data flows with a built-in HTML generator for [data reporting](logs/README.md).
|
||||
|
||||
@@ -118,7 +126,7 @@ defaultChannel = 0
|
||||
```
|
||||
|
||||
### Location Settings
|
||||
The weather forecasting defaults to NOAA, but for locations outside the USA, you can set `UseMeteoWxAPI` "Go to definition") 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 used for Sentry.
|
||||
|
||||
```ini
|
||||
[location]
|
||||
@@ -202,6 +210,41 @@ llmContext_fromGoogle = True # enable context from google search results helps w
|
||||
googleSearchResults = 3 # number of google search results to include in the context more results = more compute time
|
||||
```
|
||||
|
||||
### File Monitoring
|
||||
Some dev notes for ideas of use
|
||||
```ini
|
||||
[fileMon]
|
||||
enabled = True
|
||||
file_path = alert.txt
|
||||
broadcastCh = 2,4
|
||||
```
|
||||
#### 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
|
||||
```
|
||||
|
||||
To Monitor EAS with no internet connection see the following notes
|
||||
|
||||
- [EAS2Text](https://github.com/A-c0rN/EAS2Text)
|
||||
- depends on [multimon-ng](https://github.com/EliasOenal/multimon-ng) or [direwolf](https://github.com/wb2osz/direwolf)
|
||||
- [dsame3](https://github.com/jamieden/dsame3) // recomend not using anything but the sample file for basic work
|
||||
- this can be used with a rtl-sdr to capture alerts
|
||||
- has a sample .ogg file for testing alerts
|
||||
|
||||
The following example shell command can pipe the data using [etc/eas_alert_parser.py](etc/eas_alert_parser.py) to alert.txt
|
||||
```bash
|
||||
sox -t ogg WXR-RWT.ogg -esigned-integer -b16 -r 22050 -t raw - | multimon-ng -a EAS -v 1 -t raw - | python eas_alert_parser.py
|
||||
```
|
||||
The following example shell command will pipe rtl_sdr to alert.txt
|
||||
```bash
|
||||
rtl_fm -f 162425000 -s 22050 | multimon-ng -t raw -a EAS /dev/stdin | python eas_alert_parser.py
|
||||
```
|
||||
|
||||
### Scheduler
|
||||
The Scheduler is enabled in the `settings.py` by setting `scheduler_enabled = True`. The actions and settings are via code only at this time. See mesh_bot.py around line [425](https://github.com/SpudGunMan/meshing-around/blob/22983133ee4db3df34f66699f565e506de296197/mesh_bot.py#L425-L435) to edit the schedule. See [schedule documentation](https://schedule.readthedocs.io/en/stable/) for more.
|
||||
|
||||
@@ -213,6 +256,13 @@ schedule.every().day.at("08:00").do(lambda: send_message(handle_wxc(0, 1, 'wx'),
|
||||
schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now", 2, 0, 1))
|
||||
```
|
||||
|
||||
#### BBS Link
|
||||
The scheduler also handles the BBL Link Brodcast message
|
||||
```python
|
||||
# Send bbslink looking for peers every other day at 10:00 using send_message function to channel 8 on device 1
|
||||
schedule.every(2).days.at("10:00").do(lambda: send_message("bbslink MeshBot looking for peers", 8, 0, 1))
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -250,8 +300,6 @@ For the Ollama LLM:
|
||||
|
||||
```sh
|
||||
pip install ollama
|
||||
pip install langchain
|
||||
pip install langchain-ollama
|
||||
pip install googlesearch-python
|
||||
```
|
||||
|
||||
@@ -294,6 +342,7 @@ 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 | ✅ |
|
||||
|
||||
### Data Lookup
|
||||
| Command | Description | |
|
||||
@@ -342,5 +391,3 @@ I used ideas and snippets from other responder bots and want to call them out!
|
||||
|
||||
### Tools
|
||||
- **Node Backup Management**: [Node Slurper](https://github.com/SpudGunMan/node-slurper)
|
||||
|
||||
|
||||
|
||||
@@ -61,6 +61,8 @@ urlTimeout = 10
|
||||
LogMessagesToFile = False
|
||||
# Logging of system messages to file
|
||||
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
|
||||
@@ -107,6 +109,10 @@ UseMeteoWxAPI = False
|
||||
useMetric = False
|
||||
# repeaterList lookup location (rbook / artsci)
|
||||
repeaterLookup = rbook
|
||||
# EAS Alert Broadcast
|
||||
wxAlertBroadcastEnabled = False
|
||||
# EAS Alert Broadcast Channels
|
||||
wxAlertBroadcastCh = 2
|
||||
|
||||
# repeater module
|
||||
[repeater]
|
||||
@@ -131,6 +137,11 @@ signalHoldTime = 10
|
||||
signalCooldown = 5
|
||||
signalCycleLimit = 5
|
||||
|
||||
[fileMon]
|
||||
enabled = False
|
||||
file_path = alert.txt
|
||||
broadcastCh = 2
|
||||
|
||||
[messagingSettings]
|
||||
# delay in seconds for response to avoid message collision
|
||||
responseDelay = 0.7
|
||||
|
||||
40
etc/eas_alert_parser.py
Normal file
40
etc/eas_alert_parser.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# Super sloppy multimon-ng output cleaner for processing by EAS2Text
|
||||
# I maed dis, sorta, mostly just mashed code I found or that chatGPT hallucinated
|
||||
# by Mike O'Connell/skrrt, no licence or whatever just be chill yo
|
||||
# enhanced by sheer.cold
|
||||
|
||||
import re
|
||||
from EAS2Text import EAS2Text
|
||||
|
||||
buff=[] # store messages for writing
|
||||
seen=set()
|
||||
pattern = re.compile(r'ZCZC.*?NWS-')
|
||||
while True:
|
||||
try:
|
||||
# Handle piped input
|
||||
line=input().strip()
|
||||
except EOFError:
|
||||
break
|
||||
# only want EAS lines
|
||||
if line.startswith("EAS:") or line.startswith("EAS (part):"):
|
||||
content=line.split(maxsplit=1)[1]
|
||||
if content=="NNNN": # end of EAS message
|
||||
# write if we have something
|
||||
if buff:
|
||||
print("writing")
|
||||
with open("alert.txt","w") as fh:
|
||||
fh.write('\n'.join(buff))
|
||||
# prepare for new data
|
||||
buff.clear()
|
||||
seen.clear()
|
||||
elif content in seen:
|
||||
# don't need repeats'
|
||||
continue
|
||||
else:
|
||||
# check for national weather service
|
||||
match=pattern.search(content)
|
||||
if match:
|
||||
seen.add(content)
|
||||
msg=EAS2Text(content).EASText
|
||||
print("got message", msg)
|
||||
buff.append(msg)
|
||||
@@ -372,7 +372,7 @@ def get_database_info():
|
||||
elif 'bbsdm' in file:
|
||||
bbsdm = pickle.load(f)
|
||||
except Exception as e:
|
||||
print(f"Error reading database file: {str(e)}")
|
||||
print(f"Warning issue reading database file: {str(e)}")
|
||||
if 'lemonstand' in file:
|
||||
lemon_score = "no data"
|
||||
elif 'dopewar' in file:
|
||||
@@ -922,8 +922,8 @@ def generate_database_html(database_info):
|
||||
|
||||
def main():
|
||||
log_dir = LOG_PATH
|
||||
today = datetime.now().strftime('%Y_%m_%d')
|
||||
log_file = f'meshbot{today}.log'
|
||||
today = datetime.now().strftime('%Y-%m-%d')
|
||||
log_file = f'meshbot.log'
|
||||
log_path = os.path.join(log_dir, log_file)
|
||||
|
||||
if not os.path.exists(log_path):
|
||||
|
||||
@@ -381,7 +381,7 @@ def get_database_info():
|
||||
elif 'bbsdm' in file:
|
||||
bbsdm = pickle.load(f)
|
||||
except Exception as e:
|
||||
print(f"Error reading database file: {str(e)}")
|
||||
print(f"Warning issue reading database file: {str(e)}")
|
||||
if 'lemonstand' in file:
|
||||
lemon_score = "no data"
|
||||
elif 'dopewar' in file:
|
||||
@@ -1217,8 +1217,8 @@ def generate_database_html(database_info):
|
||||
def main():
|
||||
# Log file
|
||||
log_dir = LOG_PATH
|
||||
today = datetime.now().strftime('%Y_%m_%d')
|
||||
log_file = f'meshbot{today}.log'
|
||||
today = datetime.now().strftime('%Y-%m-%d')
|
||||
log_file = f'meshbot.log'
|
||||
log_path = os.path.join(log_dir, log_file)
|
||||
|
||||
if not os.path.exists(log_path):
|
||||
|
||||
@@ -14,6 +14,8 @@ Logging messages to disk or 'Syslog' to disk uses the python native logging func
|
||||
LogMessagesToFile = False
|
||||
# Logging of system messages to file, needed for reporting engine
|
||||
SyslogToFile = True
|
||||
# Number of log files to keep in days, 0 to keep all
|
||||
log_backup_count = 32
|
||||
```
|
||||
|
||||
To change the stdout (what you see on the console) logging level (default is DEBUG) see the following example, line is in [../modules/log.py](../modules/log.py)
|
||||
|
||||
34
mesh_bot.py
34
mesh_bot.py
@@ -25,7 +25,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
|
||||
# Command List
|
||||
default_commands = {
|
||||
"ack": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM),
|
||||
"ack": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"ask:": lambda: handle_llm(message_from_id, channel_number, deviceID, message, publicChannel),
|
||||
"askai": lambda: handle_llm(message_from_id, channel_number, deviceID, message, publicChannel),
|
||||
"bbslink": lambda: bbs_sync_posts(message, message_from_id, deviceID),
|
||||
@@ -37,9 +37,9 @@ 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),
|
||||
"cq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM),
|
||||
"cqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM),
|
||||
"cqcqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM),
|
||||
"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),
|
||||
"games": lambda: gamesCmdList,
|
||||
@@ -54,15 +54,15 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
"messages": lambda: handle_messages(message, deviceID, channel_number, msg_history, publicChannel, isDM),
|
||||
"moon": lambda: handle_moon(message_from_id, deviceID, channel_number),
|
||||
"motd": lambda: handle_motd(message, message_from_id, isDM),
|
||||
"ping": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM),
|
||||
"pinging": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM),
|
||||
"ping": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"pinging": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"pong": lambda: "🏓PING!!🛜",
|
||||
"rlist": lambda: handle_repeaterQuery(message_from_id, deviceID, channel_number),
|
||||
"sitrep": lambda: handle_lheard(message, message_from_id, deviceID, isDM),
|
||||
"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),
|
||||
"testing": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM),
|
||||
"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),
|
||||
"tide": lambda: handle_tide(message_from_id, deviceID, channel_number),
|
||||
"videopoker": lambda: handleVideoPoker(message, message_from_id, deviceID),
|
||||
"whereami": lambda: handle_whereami(message_from_id, deviceID, channel_number),
|
||||
@@ -110,7 +110,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
time.sleep(responseDelay)
|
||||
return bot_response
|
||||
|
||||
def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM):
|
||||
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"
|
||||
@@ -169,7 +169,7 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM):
|
||||
pingCount = -1
|
||||
|
||||
if pingCount > 1:
|
||||
multiPingList.append({'message_from_id': message_from_id, 'count': pingCount + 1, 'type': type, 'deviceID': deviceID})
|
||||
multiPingList.append({'message_from_id': message_from_id, 'count': pingCount + 1, 'type': type, 'deviceID': deviceID, 'channel_number': channel_number})
|
||||
msg = f"🚦Initalizing {pingCount} auto-ping"
|
||||
|
||||
return msg
|
||||
@@ -1037,6 +1037,10 @@ async def start_rx():
|
||||
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 wxAlertBroadcastEnabled:
|
||||
logger.debug(f"System: Weather Alert Broadcast Enabled on channels {wxAlertBroadcastChannel}")
|
||||
if scheduler_enabled:
|
||||
# Examples of using the scheduler, Times here are in 24hr format
|
||||
# https://schedule.readthedocs.io/en/stable/
|
||||
@@ -1078,11 +1082,15 @@ async def start_rx():
|
||||
async def main():
|
||||
meshRxTask = asyncio.create_task(start_rx())
|
||||
watchdogTask = asyncio.create_task(watchdog())
|
||||
if file_monitor_enabled:
|
||||
fileMonTask: asyncio.Task = asyncio.create_task(handleFileWatcher())
|
||||
if radio_detection_enabled:
|
||||
hamlibTask = asyncio.create_task(handleSignalWatcher())
|
||||
await asyncio.wait([meshRxTask, watchdogTask, hamlibTask])
|
||||
else:
|
||||
await asyncio.wait([meshRxTask, watchdogTask])
|
||||
|
||||
await asyncio.gather(meshRxTask, watchdogTask)
|
||||
await asyncio.gather(hamlibTask)
|
||||
await asyncio.gather(fileMonTask)
|
||||
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
try:
|
||||
|
||||
32
modules/filemon.py
Normal file
32
modules/filemon.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# File monitor module for the meshing-around bot
|
||||
# 2024 Kelly Keeton K7MHI
|
||||
|
||||
from modules.log import *
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
async def watch_file():
|
||||
def read_file(file_monitor_file_path):
|
||||
try:
|
||||
with open(file_monitor_file_path, 'r') as f:
|
||||
content = f.read()
|
||||
return content
|
||||
except Exception as e:
|
||||
logger.warning(f"FileMon: Error reading file: {file_monitor_file_path}")
|
||||
return None
|
||||
|
||||
if not os.path.exists(file_monitor_file_path):
|
||||
return None
|
||||
else:
|
||||
last_modified_time = os.path.getmtime(file_monitor_file_path)
|
||||
while True:
|
||||
current_modified_time = os.path.getmtime(file_monitor_file_path)
|
||||
if current_modified_time != last_modified_time:
|
||||
# File has been modified
|
||||
content = read_file(file_monitor_file_path)
|
||||
last_modified_time = current_modified_time
|
||||
# Cleanup the content
|
||||
content = content.replace('\n', ' ').replace('\r', '').strip()
|
||||
if content:
|
||||
return content
|
||||
await asyncio.sleep(1) # Check every
|
||||
@@ -325,11 +325,15 @@ def abbreviate_weather(row):
|
||||
|
||||
return line
|
||||
|
||||
def getWeatherAlerts(lat=0, lon=0):
|
||||
def getWeatherAlerts(lat=0, lon=0, useDefaultLatLon=False):
|
||||
# get weather alerts from NOAA limited to ALERT_COUNT with the total number of alerts found
|
||||
alerts = ""
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
if float(lat) == 0 and float(lon) == 0 and not useDefaultLatLon:
|
||||
return NO_DATA_NOGPS
|
||||
else:
|
||||
if useDefaultLatLon:
|
||||
lat = latitudeValue
|
||||
lon = longitudeValue
|
||||
|
||||
alert_url = "https://api.weather.gov/alerts/active.atom?point=" + str(lat) + "," + str(lon)
|
||||
#alert_url = "https://api.weather.gov/alerts/active.atom?area=WA"
|
||||
@@ -369,6 +373,23 @@ def getWeatherAlerts(lat=0, lon=0):
|
||||
data = "\n".join(alerts.split("\n")[:numWxAlerts]), alert_num
|
||||
return data
|
||||
|
||||
wxAlertCache = ""
|
||||
def alertBrodcast():
|
||||
# get the latest weather alerts and broadcast them if there are any
|
||||
global wxAlertCache
|
||||
currentAlert = getWeatherAlerts(latitudeValue, longitudeValue)
|
||||
|
||||
if currentAlert[0] == ERROR_FETCHING_DATA or currentAlert == NO_DATA_NOGPS or currentAlert == NO_ALERTS:
|
||||
wxAlertCache = ""
|
||||
return False
|
||||
# broadcast the alerts send to wxBrodcastCh
|
||||
elif currentAlert[0] != wxAlertCache:
|
||||
logger.debug("Location:Broadcasting weather alerts")
|
||||
wxAlertCache = currentAlert[0]
|
||||
return currentAlert
|
||||
|
||||
return False
|
||||
|
||||
def getActiveWeatherAlertsDetail(lat=0, lon=0):
|
||||
# get the latest details of weather alerts from NOAA
|
||||
alerts = ""
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
import re
|
||||
from datetime import datetime
|
||||
from modules.settings import *
|
||||
@@ -63,14 +64,14 @@ logger.addHandler(stdout_handler)
|
||||
|
||||
if syslog_to_file:
|
||||
# Create file handler for logging to a file
|
||||
file_handler_sys = logging.FileHandler('logs/meshbot{}.log'.format(today.strftime('%Y_%m_%d')))
|
||||
file_handler_sys = TimedRotatingFileHandler('logs/meshbot.log', when='midnight', backupCount=log_backup_count)
|
||||
file_handler_sys.setLevel(logging.DEBUG) # DEBUG used by default for system logs to disk
|
||||
file_handler_sys.setFormatter(plainFormatter(logFormat))
|
||||
logger.addHandler(file_handler_sys)
|
||||
|
||||
if log_messages_to_file:
|
||||
# Create file handler for logging to a file
|
||||
file_handler = logging.FileHandler('logs/messages{}.log'.format(today.strftime('%Y_%m_%d')))
|
||||
file_handler = TimedRotatingFileHandler('logs/messages.log', when='midnight', backupCount=log_backup_count)
|
||||
file_handler.setLevel(logging.INFO) # INFO used for messages to disk
|
||||
file_handler.setFormatter(logging.Formatter(msgLogFormat))
|
||||
msgLogger.addHandler(file_handler)
|
||||
@@ -19,6 +19,7 @@ antiSpam = True # anti-spam feature to prevent flooding public channel
|
||||
ping_enabled = True # ping feature to respond to pings, ack's etc.
|
||||
sitrep_enabled = True # sitrep feature to respond to sitreps
|
||||
lastHamLibAlert = 0 # last alert from hamlib
|
||||
lastFileAlert = 0 # last alert from file monitor
|
||||
max_retry_count1 = 4 # max retry count for interface 1
|
||||
max_retry_count2 = 4 # max retry count for interface 2
|
||||
retry_int1 = False
|
||||
@@ -50,7 +51,7 @@ if 'sentry' not in config:
|
||||
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'}
|
||||
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:
|
||||
@@ -73,6 +74,10 @@ if 'messagingSettings' not in config:
|
||||
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'))
|
||||
|
||||
# interface1 settings
|
||||
interface1_type = config['interface'].get('type', 'serial')
|
||||
port1 = config['interface'].get('port', '')
|
||||
@@ -97,6 +102,7 @@ try:
|
||||
ignoreDefaultChannel = config['general'].getboolean('ignoreDefaultChannel', False)
|
||||
zuluTime = config['general'].getboolean('zuluTime', False) # aka 24 hour time
|
||||
log_messages_to_file = config['general'].getboolean('LogMessagesToFile', False) # default off
|
||||
log_backup_count = config['general'].getint('LogBackupCount', 32) # default 32 days
|
||||
syslog_to_file = config['general'].getboolean('SyslogToFile', True) # default on
|
||||
urlTimeoutSeconds = config['general'].getint('urlTimeout', 10) # default 10 seconds
|
||||
store_forward_enabled = config['general'].getboolean('StoreForward', True)
|
||||
@@ -132,6 +138,13 @@ 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
|
||||
wxAlertBroadcastEnabled = config['location'].getboolean('wxAlertBroadcastEnabled', False) # default False
|
||||
# brodcast channel for weather alerts
|
||||
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh')
|
||||
if ',' in wxAlertBroadcastChannel:
|
||||
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh').split(',')
|
||||
else:
|
||||
wxAlertBroadcastChannel = config['location'].getint('wxAlertBroadcastCh', 2) # default 2
|
||||
|
||||
# bbs
|
||||
bbs_enabled = config['bbs'].getboolean('enabled', False)
|
||||
@@ -151,7 +164,12 @@ try:
|
||||
signalHoldTime = config['radioMon'].getint('signalHoldTime', 10) # default 10 seconds
|
||||
signalCooldown = config['radioMon'].getint('signalCooldown', 5) # default 1 second
|
||||
signalCycleLimit = config['radioMon'].getint('signalCycleLimit', 5) # default 5 cycles, used with SIGNAL_COOLDOWN
|
||||
|
||||
|
||||
# file monitor
|
||||
file_monitor_enabled = config['fileMon'].getboolean('enabled', False)
|
||||
file_monitor_file_path = config['fileMon'].get('file_path', 'alert.txt') # default alert.txt
|
||||
file_monitor_broadcastCh = config['fileMon'].getint('broadcastCh', 2) # default 2
|
||||
|
||||
# games
|
||||
game_hop_limit = config['messagingSettings'].getint('game_hop_limit', 5) # default 3 hops
|
||||
dopewars_enabled = config['games'].getboolean('dopeWars', True)
|
||||
|
||||
@@ -15,7 +15,7 @@ trap_list = ("cmd","cmd?") # default trap list
|
||||
help_message = "Bot CMD?:\n"
|
||||
asyncLoop = asyncio.new_event_loop()
|
||||
games_enabled = False
|
||||
multiPingList = [{'message_from_id': 0, 'count': 0, 'type': '', 'deviceID': 0}]
|
||||
multiPingList = [{'message_from_id': 0, 'count': 0, 'type': '', 'deviceID': 0, 'channel_number': 0}]
|
||||
|
||||
|
||||
# Ping Configuration
|
||||
@@ -174,6 +174,10 @@ if store_forward_enabled:
|
||||
if radio_detection_enabled:
|
||||
from modules.radio import * # from the spudgunman/meshing-around repo
|
||||
|
||||
# File Monitor Configuration
|
||||
if file_monitor_enabled:
|
||||
from modules.filemon import * # from the spudgunman/meshing-around repo
|
||||
|
||||
# BLE dual interface prevention
|
||||
if interface1_type == 'ble' and interface2_type == 'ble':
|
||||
logger.critical(f"System: BLE Interface1 and Interface2 cannot both be BLE. Exiting")
|
||||
@@ -569,15 +573,18 @@ def handleMultiPing(nodeID=0, deviceID=1):
|
||||
for i in range(len(mPlCpy)):
|
||||
message_id_from = mPlCpy[i]['message_from_id']
|
||||
count = mPlCpy[i]['count']
|
||||
type = mPlCpy[i]['type']
|
||||
type = mPlCpy[i]['type'].strip()
|
||||
deviceID = mPlCpy[i]['deviceID']
|
||||
channel_number = mPlCpy[i]['channel_number']
|
||||
|
||||
if count > 1 and deviceID == 1:
|
||||
if count > 1:
|
||||
count -= 1
|
||||
# update count in the list
|
||||
multiPingList[i]['count'] = count
|
||||
for i in range(len(multiPingList)):
|
||||
if multiPingList[i]['message_from_id'] == message_id_from:
|
||||
multiPingList[i]['count'] = count
|
||||
|
||||
send_message(f"🔂{count} {type}", publicChannel, message_id_from, 1)
|
||||
send_message(f"🔂{count} {type}", channel_number, message_id_from, deviceID)
|
||||
if count < 2:
|
||||
# remove the item from the list
|
||||
for j in range(len(multiPingList)):
|
||||
@@ -585,6 +592,24 @@ def handleMultiPing(nodeID=0, deviceID=1):
|
||||
multiPingList.pop(j)
|
||||
break
|
||||
|
||||
|
||||
def handleWxBroadcast(deviceID=1):
|
||||
# only allow API call every 30 minutes
|
||||
clock = datetime.now()
|
||||
if clock.minute % 30 != 0:
|
||||
return False
|
||||
|
||||
# check for alerts
|
||||
alert = alertBrodcast()
|
||||
if alert:
|
||||
msg = f"🚨 {alert[1]} EAS ALERTs: {alert[0]}"
|
||||
if isinstance(wxAlertBroadcastChannel, list):
|
||||
for channel in wxAlertBroadcastChannel:
|
||||
send_message(msg, int(channel), 0, deviceID)
|
||||
else:
|
||||
send_message(msg, wxAlertBroadcastChannel, 0, deviceID)
|
||||
return True
|
||||
|
||||
def onDisconnect(interface):
|
||||
global retry_int1, retry_int2
|
||||
rxType = type(interface).__name__
|
||||
@@ -813,7 +838,7 @@ async def BroadcastScheduler():
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def handleSignalWatcher():
|
||||
global lastHamLibAlert, antiSpam, sigWatchBroadcastCh
|
||||
global lastHamLibAlert
|
||||
# monitor rigctld for signal strength and frequency
|
||||
while True:
|
||||
msg = await signalWatcher()
|
||||
@@ -843,6 +868,36 @@ async def handleSignalWatcher():
|
||||
await asyncio.sleep(1)
|
||||
pass
|
||||
|
||||
async def handleFileWatcher():
|
||||
global lastFileAlert
|
||||
# monitor the file system for changes
|
||||
while True:
|
||||
msg = await watch_file()
|
||||
if msg != ERROR_FETCHING_DATA and msg is not None:
|
||||
logger.debug(f"System: Detected Alert from FileWatcher on file {file_monitor_file_path}")
|
||||
|
||||
# check we are not spammig the channel limit messages to once per minute
|
||||
if time.time() - lastFileAlert > 60:
|
||||
lastFileAlert = time.time()
|
||||
# if fileWatchBroadcastCh list contains multiple channels, broadcast to all
|
||||
if type(file_monitor_broadcastCh) is list:
|
||||
for ch in file_monitor_broadcastCh:
|
||||
if antiSpam and ch != publicChannel:
|
||||
send_message(msg, int(ch), 0, 1)
|
||||
if interface2_enabled:
|
||||
send_message(msg, int(ch), 0, 2)
|
||||
else:
|
||||
logger.warning(f"System: antiSpam prevented Alert from FileWatcher")
|
||||
else:
|
||||
if antiSpam and file_monitor_broadcastCh != publicChannel:
|
||||
send_message(msg, int(file_monitor_broadcastCh), 0, 1)
|
||||
if interface2_enabled:
|
||||
send_message(msg, int(file_monitor_broadcastCh), 0, 2)
|
||||
else:
|
||||
logger.warning(f"System: antiSpam prevented Alert from FileWatcher")
|
||||
|
||||
await asyncio.sleep(1)
|
||||
pass
|
||||
|
||||
async def retry_interface(nodeID=1):
|
||||
global interface1, interface2, retry_int1, retry_int2, max_retry_count1, max_retry_count2
|
||||
@@ -890,7 +945,6 @@ async def retry_interface(nodeID=1):
|
||||
logger.error(f"System: Error Opening interface{nodeID} on: {e}")
|
||||
|
||||
|
||||
|
||||
handleSentinel_spotted = ""
|
||||
handleSentinel_loop = 0
|
||||
async def handleSentinel(deviceID=1):
|
||||
@@ -898,6 +952,7 @@ async def handleSentinel(deviceID=1):
|
||||
# Locate Closest Nodes and report them to a secure channel
|
||||
# async function for possibly demanding back location data
|
||||
enemySpotted = ""
|
||||
resolution = "unknown"
|
||||
closest_nodes = get_closest_nodes(deviceID)
|
||||
if closest_nodes != ERROR_FETCHING_DATA and closest_nodes:
|
||||
if closest_nodes[0]['id'] is not None:
|
||||
@@ -911,7 +966,9 @@ async def handleSentinel(deviceID=1):
|
||||
# check the positionMetadata for nodeID and get metadata
|
||||
if positionMetadata and closest_nodes[0]['id'] in positionMetadata:
|
||||
metadata = positionMetadata[closest_nodes[0]['id']]
|
||||
resolution = metadata.get('precisionBits', 'na')
|
||||
if metadata.get('precisionBits') is not None:
|
||||
resolution = metadata.get('precisionBits')
|
||||
|
||||
|
||||
logger.warning(f"System: {enemySpotted} is close to your location on Interface1 Accuracy is {resolution}bits")
|
||||
send_message(f"Sentry{deviceID}: {enemySpotted}", secure_channel, 0, deviceID)
|
||||
@@ -943,6 +1000,9 @@ async def watchdog():
|
||||
# multiPing handler
|
||||
handleMultiPing(0,1)
|
||||
|
||||
if wxAlertBroadcastEnabled:
|
||||
handleWxBroadcast(1)
|
||||
|
||||
# Telemetry data
|
||||
int1Data = displayNodeTelemetry(0, 1)
|
||||
if int1Data != -1 and telemetryData[0]['lastAlert1'] != int1Data:
|
||||
@@ -972,6 +1032,9 @@ async def watchdog():
|
||||
# multiPing handler
|
||||
handleMultiPing(0,2)
|
||||
|
||||
if wxAlertBroadcastEnabled:
|
||||
handleWxBroadcast(2)
|
||||
|
||||
# Telemetry data
|
||||
int2Data = displayNodeTelemetry(0, 2)
|
||||
if int2Data != -1 and telemetryData[0]['lastAlert2'] != int2Data:
|
||||
|
||||
@@ -14,6 +14,4 @@ geopy
|
||||
schedule
|
||||
wikipedia
|
||||
ollama
|
||||
langchain
|
||||
langchain-ollama
|
||||
googlesearch-python
|
||||
|
||||
Reference in New Issue
Block a user