From e0bef20398c538a006a1389bfb981711ff989e30 Mon Sep 17 00:00:00 2001 From: imonlinux <39863321+imonlinux@users.noreply.github.com> Date: Sun, 12 Oct 2025 12:57:11 -0700 Subject: [PATCH] Implement MQTT for Advanced LVA Controls Adds command-line arguments (--mqtt-host, --mqtt-port, etc., and --num-leds) to configure the MQTT connection and initial LED setup. Instantiates and starts the MqttController on application startup if an MQTT host is provided. Includes the MicMuteHandler class, which listens for mute commands, updates the global mic_muted state, and clears the audio queue on unmute to prevent processing stale audio. Updates the process_audio loop to respect the mic_muted flag. --- linux_voice_assistant/__main__.py | 73 ++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 7 deletions(-) diff --git a/linux_voice_assistant/__main__.py b/linux_voice_assistant/__main__.py index cd6692a..ab523be 100644 --- a/linux_voice_assistant/__main__.py +++ b/linux_voice_assistant/__main__.py @@ -12,8 +12,9 @@ from typing import Dict, List, Optional, Union import numpy as np import sounddevice as sd -from .event_bus import EventBus +from .event_bus import EventBus, EventHandler, subscribe from .led_controller import LedController +from .mqtt_controller import MqttController from .microwakeword import MicroWakeWord, MicroWakeWordFeatures from .models import AvailableWakeWord, Preferences, ServerState, WakeWordType from .mpv_player import MpvMediaPlayer @@ -34,6 +35,34 @@ if is_arm(): else: _LIB_DIR = _REPO_DIR / "lib" / "linux_amd64" +# ----------------------------------------------------------------------------- + +class MicMuteHandler(EventHandler): + """Event handler for mic mute switch.""" + def __init__(self, state, mqtt_controller: Optional[MqttController]): + super().__init__(state) + self.mqtt_controller = mqtt_controller + + @subscribe + def set_mic_mute(self, data: dict): + is_muted = data.get("state", False) + if self.state.mic_muted != is_muted: + self.state.mic_muted = is_muted + _LOGGER.debug("Mic muted = %s", is_muted) + + if self.mqtt_controller: + self.mqtt_controller.publish_mute_state(is_muted) + + if is_muted: + self.state.event_bus.publish("mic_muted") + else: + _LOGGER.debug("Clearing stale audio queue...") + while not self.state.audio_queue.empty(): + try: + self.state.audio_queue.get_nowait() + except Queue.Empty: + break + self.state.event_bus.publish("mic_unmuted") # ----------------------------------------------------------------------------- @@ -107,6 +136,17 @@ async def main() -> None: default=12, help="GPIO pin for LED data (for Grove)", ) + # --- ADDED NUM_LEDS ARGUMENT --- + parser.add_argument( + "--num-leds", + type=int, + default=3, + help="Number of LEDs in the strip", + ) + parser.add_argument("--mqtt-host", help="MQTT broker host") + parser.add_argument("--mqtt-port", type=int, default=1883, help="MQTT broker port") + parser.add_argument("--mqtt-username", help="MQTT broker username") + parser.add_argument("--mqtt-password", help="MQTT broker password") parser.add_argument( "--debug", action="store_true", help="Print DEBUG messages to console" ) @@ -135,9 +175,8 @@ async def main() -> None: trained_languages=model_config.get("trained_languages", []), config_path=model_config_path, ) - _LOGGER.debug("Available wake words: %s", list(sorted(available_wake_words.keys()))) - + preferences_path = Path(args.preferences_file) if preferences_path.exists(): _LOGGER.debug("Loading preferences: %s", preferences_path) @@ -147,9 +186,12 @@ async def main() -> None: else: preferences = Preferences() + # Set number of LEDs from command line default, then override with saved preference + preferences.num_leds = getattr(preferences, 'num_leds', args.num_leds) + + libtensorflowlite_c_path = _LIB_DIR / "libtensorflowlite_c.so" _LOGGER.debug("libtensorflowlite_c path: %s", libtensorflowlite_c_path) - wake_models: Dict[str, Union[MicroWakeWord, OpenWakeWord]] = {} if preferences.active_wake_words: for wake_word_id in preferences.active_wake_words: @@ -164,7 +206,6 @@ async def main() -> None: wake_word = available_wake_words[wake_word_id] _LOGGER.debug("Loading wake model: %s", wake_word_id) wake_models[wake_word_id] = wake_word.load(libtensorflowlite_c_path) - stop_model: Optional[MicroWakeWord] = None for wake_word_dir in wake_word_dirs: stop_config_path = wake_word_dir / f"{args.stop_model}.json" @@ -175,9 +216,7 @@ async def main() -> None: stop_config_path, libtensorflowlite_c_path ) break - assert stop_model is not None - music_player = MpvMediaPlayer( device=args.audio_output_device, initial_volume=preferences.volume_level ) @@ -212,7 +251,21 @@ async def main() -> None: interface=args.led_interface, clock_pin=args.led_clock_pin, data_pin=args.led_data_pin, + num_leds=state.preferences.num_leds, # Pass number of LEDs ) + + mqtt_controller: Optional[MqttController] = None + if args.mqtt_host: + mqtt_controller = MqttController( + state=state, + host=args.mqtt_host, + port=args.mqtt_port, + username=args.mqtt_username, + password=args.mqtt_password, + ) + mqtt_controller.start() + + mic_mute_handler = MicMuteHandler(state, mqtt_controller) process_audio_thread = threading.Thread( target=process_audio, args=(state,), daemon=True @@ -245,6 +298,8 @@ async def main() -> None: except KeyboardInterrupt: pass finally: + if mqtt_controller: + mqtt_controller.stop() state.audio_queue.put_nowait(None) process_audio_thread.join() @@ -266,6 +321,10 @@ def process_audio(state: ServerState): try: while True: + if state.mic_muted: + time.sleep(0.1) + continue + audio_chunk = state.audio_queue.get() if audio_chunk is None: break