Merge pull request #86 from SpudGunMan/lab

Enhancements 🦃
This commit is contained in:
Kelly
2024-11-27 23:01:45 -08:00
committed by GitHub
10 changed files with 125 additions and 37 deletions
+26 -10
View File
@@ -37,8 +37,12 @@ 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. This could be used to monitor NOAA OTA EAS System and offgrid send these alerts or any others to the mesh. Or to parse data from any other tools capable of writing information to file.
- **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).
@@ -215,15 +219,30 @@ file_path = alert.txt
broadcastCh = 2,4
```
#### NOAA EAS
- [dsame3](https://github.com/jamieden/dsame3)
- this can be used with a rtl-sdr to capture alerts
- has a sample .ogg file for testing alerts
- TODO: fork this and have copy which will just dump the needed data right away?
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 - > raw_NOAA_Alert.txt
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
@@ -281,8 +300,6 @@ For the Ollama LLM:
```sh
pip install ollama
pip install langchain
pip install langchain-ollama
pip install googlesearch-python
```
@@ -325,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 | |
@@ -373,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)
+6
View File
@@ -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]
+3 -3
View File
@@ -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):
+3 -3
View File
@@ -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):
+2
View File
@@ -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)
+12 -10
View File
@@ -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
@@ -1039,6 +1039,8 @@ async def start_rx():
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/
+23 -2
View File
@@ -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 = ""
+3 -2
View File
@@ -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)
+9 -1
View File
@@ -51,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:
@@ -102,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)
@@ -137,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)
+38 -6
View File
@@ -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
@@ -573,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)):
@@ -589,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__
@@ -931,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:
@@ -944,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)
@@ -976,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:
@@ -1005,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:
@@ -1016,3 +1046,5 @@ async def watchdog():
await retry_interface(2)
except Exception as e:
logger.error(f"System: retrying interface2: {e}")