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.
This commit is contained in:
imonlinux
2025-10-12 12:57:11 -07:00
committed by GitHub
parent 53b8fc4da9
commit e0bef20398
+66 -7
View File
@@ -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