diff --git a/README.md b/README.md index f9c2148..6adee97 100644 --- a/README.md +++ b/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,9 @@ 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. +### File Monitor Alerts +- **File Mon**: Monitor a flat file for changes, brodcast the contents of the message to mesh group. This could be used to monitor NOAA OTA EAS System and offgrid send these alerts or any others to the mesh. + ### Data Reporting - **HTML Generator**: Visualize bot traffic and data flows with a built-in HTML generator for [data reporting](logs/README.md). @@ -118,7 +122,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] @@ -213,6 +217,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. diff --git a/config.template b/config.template index 851e1f2..fc52ca6 100644 --- a/config.template +++ b/config.template @@ -131,6 +131,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 diff --git a/mesh_bot.py b/mesh_bot.py index 419f5d5..3fd7a2e 100755 --- a/mesh_bot.py +++ b/mesh_bot.py @@ -1037,6 +1037,8 @@ 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}") if scheduler_enabled: # Examples of using the scheduler, Times here are in 24hr format # https://schedule.readthedocs.io/en/stable/ @@ -1078,11 +1080,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: diff --git a/modules/filemon.py b/modules/filemon.py new file mode 100644 index 0000000..4546fa1 --- /dev/null +++ b/modules/filemon.py @@ -0,0 +1,29 @@ +# 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 + return content + await asyncio.sleep(1) # Check every \ No newline at end of file diff --git a/modules/settings.py b/modules/settings.py index 6751aa2..b8858b7 100644 --- a/modules/settings.py +++ b/modules/settings.py @@ -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 @@ -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', '') @@ -151,7 +156,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) diff --git a/modules/system.py b/modules/system.py index 88bf26f..c6a5ee7 100644 --- a/modules/system.py +++ b/modules/system.py @@ -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") @@ -813,7 +817,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 +847,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 +924,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): @@ -983,5 +1016,3 @@ async def watchdog(): await retry_interface(2) except Exception as e: logger.error(f"System: retrying interface2: {e}") - -