vox detection

This commit is contained in:
SpudGunMan
2025-10-11 20:44:03 -07:00
parent 96447b166f
commit f4734c5b87
5 changed files with 88 additions and 16 deletions

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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)

View File

@@ -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")