diff --git a/wyoming_satellite/__main__.py b/wyoming_satellite/__main__.py index 31acf48..582b186 100644 --- a/wyoming_satellite/__main__.py +++ b/wyoming_satellite/__main__.py @@ -381,26 +381,31 @@ class SatelliteEventHandler(AsyncEventHandler): self.client_id = str(time.monotonic_ns()) self.satellite = satellite - _LOGGER.debug("Client connected: %s", self.client_id) - self.satellite.set_server(self.client_id, self.writer) - # ------------------------------------------------------------------------- async def handle_event(self, event: Event) -> bool: """Handle events from the server.""" - if self.satellite.server_id != self.client_id: - # New connection - return False - if Describe.is_type(event.type): await self.write_event(self.wyoming_info_event) - _LOGGER.debug("Sent info to client: %s", self.client_id) return True + if self.satellite.server_id is None: + # Take over after a problem occurred + self.satellite.set_server(self.client_id, self.writer) + elif self.satellite.server_id != self.client_id: + # New connection + _LOGGER.debug("Connection cancelled: %s", self.client_id) + return False + await self.satellite.event_from_server(event) return True + async def disconnect(self) -> None: + """Server disconnect.""" + if self.satellite.server_id == self.client_id: + self.satellite.clear_server() + # ----------------------------------------------------------------------------- diff --git a/wyoming_satellite/satellite.py b/wyoming_satellite/satellite.py index d009531..60d0da9 100644 --- a/wyoming_satellite/satellite.py +++ b/wyoming_satellite/satellite.py @@ -111,6 +111,13 @@ class SatelliteBase: """Set event writer.""" self.server_id = server_id self._writer = writer + _LOGGER.debug("Server set: %s", server_id) + + def clear_server(self) -> None: + """Remove writer.""" + self.server_id = None + self._writer = None + _LOGGER.debug("Server disconnected") async def event_to_server(self, event: Event) -> None: """Send an event to the server.""" @@ -119,9 +126,13 @@ class SatelliteBase: try: await async_write_event(event, self._writer) - except Exception: - _LOGGER.exception("Unexpected error sending event to server") - self._writer = None + except Exception as err: + self.clear_server() + + if isinstance(err, ConnectionResetError): + _LOGGER.warning("Server disconnected unexpectedly") + else: + _LOGGER.exception("Unexpected error sending event to server") # ------------------------------------------------------------------------- @@ -442,6 +453,12 @@ class SatelliteBase: ).event() await snd_client.write_event(event) + + if self.settings.snd.disconnect_after_stop and AudioStop.is_type( + event.type + ): + await _disconnect() + snd_client = None # reconnect on next event except asyncio.CancelledError: break except Exception: diff --git a/wyoming_satellite/settings.py b/wyoming_satellite/settings.py index e6a50e2..765a9fd 100644 --- a/wyoming_satellite/settings.py +++ b/wyoming_satellite/settings.py @@ -6,68 +6,134 @@ from typing import List, Optional @dataclass(frozen=True) class ServiceSettings(ABC): + """Base class for service settings.""" + uri: Optional[str] = None + """tcp://ip-address:port""" + command: Optional[List[str]] = None + """program + args""" + reconnect_seconds: float = 3.0 + """Seconds before reconnection attempt is made.""" @property def enabled(self) -> bool: + """True if service is enabled.""" return bool(self.uri or self.command) @dataclass(frozen=True) class MicSettings(ServiceSettings): + """Microphone service settings.""" + volume_multiplier: float = 1.0 + """Multiplier for raw audio data (1.0 = no change)""" + auto_gain: int = 0 + """Automatic gain control (0-31 dbFS)""" + noise_suppression: int = 0 + """Level of noise suppression (0-4, 4 is max)""" + rate: int = 16000 + """Sample rate of mic audio (hertz)""" + width: int = 2 + """Sample width of mic audio (bytes)""" + channels: int = 1 + """Sample channels in mic audio""" + samples_per_chunk: int = 1024 + """Samples to read at a time from mic command""" @property def needs_webrtc(self) -> bool: + """True if webrtc audio enhancements are needed.""" return self.enabled and ((self.auto_gain > 0) or (self.noise_suppression > 0)) @property def needs_processing(self) -> bool: + """True if some audio pre-processing is needed for mic audio.""" return self.enabled and ((self.volume_multiplier != 1.0) or self.needs_webrtc) @dataclass(frozen=True) class SndSettings(ServiceSettings): + """Sound output service settings.""" + volume_multiplier: float = 1.0 + """Multiplier for raw audio data (1.0 = no change)""" + awake_wav: Optional[str] = None + """Path to WAV file played after wake word detection.""" + done_wav: Optional[str] = None + """Path to WAV file played after voice command is recognized.""" + rate: int = 22050 + """Sample rate of output audio (hertz)""" + width: int = 2 + """Sample width of output audio (bytes)""" + channels: int = 1 + """Sample channels in output audio""" + samples_per_chunk: int = 1024 + """Samples to write at a time to snd command""" + + disconnect_after_stop: bool = True + """True if snd service should be disconnected after AudioStop.""" @property def needs_processing(self) -> bool: + """True if some pre-processing is needed for output audio.""" return self.enabled and (self.volume_multiplier != 1.0) @dataclass(frozen=True) class WakeSettings(ServiceSettings): + """Wake word service settings.""" + names: Optional[List[str]] = None + """List of wake word names to listen for.""" + rate: int = 16000 + """Sample rate of wake word audio (hertz)""" + width: int = 2 + """Sample width of wake word audio (bytes)""" + channels: int = 1 + """Sample channels in wake word audio""" @dataclass(frozen=True) class VadSettings: + """Voice activity detector settings.""" + enabled: bool = False + """True if VAD should be used.""" + threshold: float = 0.5 + """Silence threshold (0-1, 1 is speech)""" + trigger_level: int = 1 + """Number of chunks to cross threshold before activation.""" + buffer_seconds: float = 2.0 + """Seconds of audio before activation to keep/stream""" + wake_word_timeout: Optional[float] = 5.0 + """Seconds before going back to sleep if wake word is not detected.""" @dataclass(frozen=True) class EventSettings(ServiceSettings): + """External event service settings.""" + startup: Optional[List[str]] = None streaming_start: Optional[List[str]] = None streaming_stop: Optional[List[str]] = None @@ -84,6 +150,8 @@ class EventSettings(ServiceSettings): @dataclass(frozen=True) class SatelliteSettings: + """Wyoming satellite settings.""" + mic: MicSettings vad: VadSettings wake: WakeSettings