Skip to content

Commit

Permalink
Merge pull request #86 from SpudGunMan/lab
Browse files Browse the repository at this point in the history
Enhancements 🦃
  • Loading branch information
SpudGunMan authored Nov 28, 2024
2 parents f94f329 + c21a67d commit eeab9f3
Show file tree
Hide file tree
Showing 10 changed files with 125 additions and 37 deletions.
36 changes: 26 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -281,8 +300,6 @@ For the Ollama LLM:

```sh
pip install ollama
pip install langchain
pip install langchain-ollama
pip install googlesearch-python
```

Expand Down Expand Up @@ -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 | |
Expand Down Expand Up @@ -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 changes: 6 additions & 0 deletions config.template
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
6 changes: 3 additions & 3 deletions etc/report_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand Down
6 changes: 3 additions & 3 deletions etc/report_generator5.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand Down
2 changes: 2 additions & 0 deletions logs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
22 changes: 12 additions & 10 deletions mesh_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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,
Expand All @@ -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),
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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/
Expand Down
25 changes: 23 additions & 2 deletions modules/locationdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 = ""
Expand Down
5 changes: 3 additions & 2 deletions modules/log.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from logging.handlers import TimedRotatingFileHandler
import re
from datetime import datetime
from modules.settings import *
Expand Down Expand Up @@ -63,14 +64,14 @@ def format(self, record):

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)
10 changes: 9 additions & 1 deletion modules/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
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:
Expand Down Expand Up @@ -102,6 +102,7 @@
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)
Expand Down Expand Up @@ -137,6 +138,13 @@
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)
Expand Down
Loading

0 comments on commit eeab9f3

Please sign in to comment.