diff --git a/config.template b/config.template index 666c252..75b97a8 100644 --- a/config.template +++ b/config.template @@ -294,6 +294,10 @@ signalHoldTime = 10 # the following are combined to reset the monitor signalCooldown = 5 signalCycleLimit = 5 +# enable VOX detection using default input +voxDetectionEnabled = False +# description to use in the alert message +voxDescription = VOX [fileMon] filemon_enabled = False diff --git a/mesh_bot.py b/mesh_bot.py index bd23e2f..f4e9541 100755 --- a/mesh_bot.py +++ b/mesh_bot.py @@ -1895,6 +1895,9 @@ async def main(): if radio_detection_enabled: tasks.append(asyncio.create_task(handleSignalWatcher(), name="hamlib")) + + if voxDetectionEnabled: + tasks.append(asyncio.create_task(voxMonitor(), name="vox_detection")) logger.debug(f"System: Starting {len(tasks)} async tasks") diff --git a/modules/radio.py b/modules/radio.py index 7268ef1..4008bde 100644 --- a/modules/radio.py +++ b/modules/radio.py @@ -7,6 +7,16 @@ import socket import asyncio from modules.log import * +voxHoldTime = signalHoldTime +previousVoxState = False + +if voxDetectionEnabled: + import sounddevice as sd # pip install sounddevice sudo apt install portaudio19-dev + from vosk import Model, KaldiRecognizer # pip install vosk + import json + q = asyncio.Queue() + + def get_hamlib(msg="f"): try: rigControlSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -133,6 +143,11 @@ def get_sig_strength(): strength = get_hamlib('l STRENGTH') return strength +def vox_callback(indata, frames, time, status): + if status: + logger.warning(f"RadioMon: VOX input status: {status}") + q.put(bytes(indata)) + async def signalWatcher(): global previousStrength global signalCycle @@ -157,4 +172,38 @@ async def signalWatcher(): signalCycle = 0 previousStrength = -40 + +def make_vox_callback(loop, q): + def vox_callback(indata, frames, time, status): + if status: + logger.warning(f"RadioMon: VOX input status: {status}") + try: + loop.call_soon_threadsafe(q.put_nowait, bytes(indata)) + except RuntimeError: + pass + return vox_callback + +async def voxMonitor(): + global previousVoxState, voxMsgQueue + try: + model = Model(lang="en-us") + device_info = sd.query_devices(None, 'input') + samplerate = 16000 + logger.debug(f"RadioMon: VOX monitor started on device {device_info['name']} with samplerate {samplerate}") + rec = KaldiRecognizer(model, samplerate) + loop = asyncio.get_running_loop() + callback = make_vox_callback(loop, q) + with sd.RawInputStream(samplerate=samplerate, blocksize=8000, dtype='int16', channels=1, callback=callback): + while True: + data = await q.get() + if rec.AcceptWaveform(data): + result = rec.Result() + text = json.loads(result).get("text", "") + if text and text != "huh": + logger.info(f"🎙️Detected {voxDescription}: {text}") + voxMsgQueue.append(f"🎙️Detected {voxDescription}: {text}") + await asyncio.sleep(0.5) + except Exception as e: + logger.error(f"RadioMon: Error in VOX monitor: {e}") + # end of file \ No newline at end of file diff --git a/modules/settings.py b/modules/settings.py index 9078f21..c24e296 100644 --- a/modules/settings.py +++ b/modules/settings.py @@ -32,6 +32,7 @@ surveyTracker, tictactoeTracker, hamtestTracker, hangmanTracker, golfTracker, ma cmdHistory = [] # list to hold the command history for lheard and history commands msg_history = [] # list to hold the message history for the messages command max_bytes = 200 # Meshtastic has ~237 byte limit, use conservative 200 bytes for message content +voxMsgQueue = [] # queue for VOX detected messages # Read the config file, if it does not exist, create basic config file config = configparser.ConfigParser() @@ -360,10 +361,13 @@ try: radio_detection_enabled = config['radioMon'].getboolean('enabled', False) rigControlServerAddress = config['radioMon'].get('rigControlServerAddress', 'localhost:4532') # default localhost:4532 sigWatchBroadcastCh = config['radioMon'].get('sigWatchBroadcastCh', '2').split(',') # default Channel 2 + sigWatchBroadcastInterface = config['radioMon'].getint('sigWatchBroadcastInterface', 1) # default interface 1 signalDetectionThreshold = config['radioMon'].getint('signalDetectionThreshold', -10) # default -10 dBm 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 + voxDetectionEnabled = config['radioMon'].getboolean('voxDetectionEnabled', False) # default VOX detection disabled + voxDescription = config['radioMon'].get('voxDescription', 'VOX') # default VOX detected audio message # file monitor file_monitor_enabled = config['fileMon'].getboolean('filemon_enabled', False) diff --git a/modules/system.py b/modules/system.py index 0a0a2a9..26106ec 100644 --- a/modules/system.py +++ b/modules/system.py @@ -285,6 +285,9 @@ if checklist_enabled: if radio_detection_enabled: from modules.radio import * # from the spudgunman/meshing-around repo +if voxDetectionEnabled: + from modules.radio import * # from the spudgunman/meshing-around repo + # File Monitor Configuration if file_monitor_enabled or read_news_enabled or bee_enabled: from modules.filemon import * # from the spudgunman/meshing-around repo @@ -1640,24 +1643,14 @@ async def handleSignalWatcher(): if type(sigWatchBroadcastCh) is list: for ch in sigWatchBroadcastCh: if antiSpam and ch != publicChannel: - send_message(msg, int(ch), 0, 1) + send_message(msg, int(ch), 0, sigWatchBroadcastInterface) time.sleep(responseDelay) - if multiple_interface: - for i in range(2, 10): - if globals().get(f'interface{i}_enabled'): - send_message(msg, int(ch), 0, i) - time.sleep(responseDelay) else: logger.warning(f"System: antiSpam prevented Alert from Hamlib {msg}") else: if antiSpam and sigWatchBroadcastCh != publicChannel: - send_message(msg, int(sigWatchBroadcastCh), 0, 1) + send_message(msg, int(sigWatchBroadcastCh), 0, sigWatchBroadcastInterface) time.sleep(responseDelay) - if multiple_interface: - for i in range(2, 10): - if globals().get(f'interface{i}_enabled'): - send_message(msg, int(sigWatchBroadcastCh), 0, i) - time.sleep(responseDelay) else: logger.warning(f"System: antiSpam prevented Alert from Hamlib {msg}") @@ -1795,11 +1788,30 @@ async def handleSentinel(deviceID): else: handleSentinel_loop += 1 +async def process_vox_queue(): + # process the voxMsgQueue + global voxMsgQueue + items_to_process = voxMsgQueue[:] + voxMsgQueue.clear() + if len(items_to_process) > 0: + logger.debug(f"System: Processing {len(items_to_process)} items in voxMsgQueue") + for item in items_to_process: + message = item + for channel in sigWatchBroadcastCh: + if antiSpam and int(channel) != publicChannel: + send_message(message, int(channel), 0, sigWatchBroadcastInterface) + time.sleep(responseDelay) + async def watchdog(): global telemetryData, retry_int1, retry_int2, retry_int3, retry_int4, retry_int5, retry_int6, retry_int7, retry_int8, retry_int9 + logger.debug("System: Watchdog started") while True: await asyncio.sleep(20) + # perform memory cleanup every 10 minutes + if datetime.now().minute % 10 == 0: + cleanup_memory() + # check all interfaces for i in range(1, 10): interface = globals().get(f'interface{i}') @@ -1834,16 +1846,16 @@ async def watchdog(): # check for noisy telemetry if noisyNodeLogging: noisyTelemetryCheck() + + # vox queue processing + if voxDetectionEnabled: + await process_vox_queue() # check the load_bbsdm flag to reload the BBS messages from disk if bbs_enabled and bbsAPI_enabled: load_bbsdm() load_bbsdb() - # perform memory cleanup every 10 minutes - if datetime.now().minute % 10 == 0: - cleanup_memory() - def exit_handler(): # Close the interface and save the BBS messages logger.debug(f"System: Closing Autoresponder")