# План: WebSocket сервер для Wyoming Satellite **Дата:** 2026-05-20 **Статус:** В разработке **Ветка:** `satellite-dev` **Директория для разработки:** `~/projects/satellite-dev` **Файл плана:** `docs/development/websocket-server-plan.md` --- ## Чеклист выполнения ### ✅ Выполнено - [x] **Ветка создана** — `satellite-dev` (переименована из `feature/multi-event-server`) - [x] **Окружение настроено** — `.venv/` в корне проекта - [x] **Docker удалён** — файлы Dockerfile, docker-compose.yml, скрипты - [x] **Лишние файлы удалены** — test.wav, start.sh, start-dev.sh, .projectile - [x] **setup_dev.sh создан** — быстрая настройка окружения - [x] **Документация обновлена** — plan.md актуализирован - [x] **Этап WS-1: settings.py** — websocket_enabled, websocket_host, websocket_port - [x] **Этап WS-2: satellite.py** — WebSocket сервер + runtime атрибуты - [x] **Этап WS-2A: Протокол команд** — handle_websocket_command() - [x] **Этап WS-3: __main__.py** — CLI аргументы --websocket-enabled/host/port ### 🔄 В процессе - [ ] **Этап WS-4: Тестирование** — проверка WebSocket клиентов - [ ] **Этап WS-5: Документация** — обновление docs/ ### ✅ Выполнено - [x] **Этап WS-6: Audio level** — трансляция RMS громкости микрофона ### ⏳ Ожидается - [ ] Merge в master - [ ] Production развёртывание --- ## Архитектура ### Текущее состояние (production) ``` ┌─────────────────┐ ┌─────────────────┐ │ Home Assistant │ ──────► │ Satellite │ │ (Wyoming │ 10700 │ (Wyoming │ │ Client) │ │ Server) │ └─────────────────┘ └────────┬────────┘ │ │ 10500 (outgoing!) ▼ ┌─────────────────┐ │ Event Service │ │ (LED, MQTT) │ └─────────────────┘ ``` **Ограничение:** Satellite **подключается** к event службе, но не может принимать входящие подключения ### Целевое состояние ``` ┌─────────────────┐ ┌──────────────────────────┐ │ Home Assistant │ ──────► │ Wyoming Satellite │ │ (Wyoming │ 10700 │ (единый процесс) │ │ Client) │ │ │ └─────────────────┘ │ ┌────────────────────┐ │ │ │ Event CLIENT │ │ │ │ (outgoing :10500) │ │ │ │ → LED, MQTT │ │ │ └────────────────────┘ │ │ ┌────────────────────┐ │ │ │ WebSocket SERVER │ │ │ │ (incoming :8675) │ │ │ │ ← Browser, UI │ │ │ └────────────────────┘ │ └──────────────────────────┘ ``` **Принцип:** - ✅ **Event CLIENT** (`--event-uri`) — satellite **подключается** к внешней службе (LED, MQTT) - ✅ **WebSocket SERVER** (`--websocket-enabled`) — встроенный сервер, клиенты **подключаются к нему** **Преимущества:** - ✅ **Не нужен отдельный процесс** — websocket-service встроен в satellite - ✅ **Прямая трансляция событий** — без промежуточных очередей - ✅ **Масштабируемость** — неограниченное количество WebSocket клиентов - ✅ **Обратная совместимость** — `--event-uri` продолжает работать --- ## Этапы реализации ### Этап 1: Подготовка ✅ ВЫПОЛНЕН #### 1.1 Проверка окружения ✅ ```bash # Production версия (не трогать!) cd ~/projects/Satellite git status # Должен быть clean # Development версия cd ~/projects/satellite-dev git status # Должен быть на satellite-dev ``` #### 1.2 Настройка venv для satellite-dev ✅ ```bash # Автоматическая настройка cd ~/projects/satellite-dev ./setup_dev.sh # Или вручную: python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt ``` #### 1.3 Проверка что всё работает ✅ ```bash cd ~/projects/satellite-dev source .venv/bin/activate # Проверка импорта python3 -c "from wyoming_satellite.satellite import SatelliteBase; print('OK')" ``` **Итого:** Окружение готово, можно приступать к реализации. --- ### Этап WS-1: Модификация `wyoming_satellite/settings.py` (30 мин) 🔄 В ПРОЦЕССЕ **Добавить настройки WebSocket сервера:** ```python @dataclass(frozen=True) class EventSettings(ServiceSettings): # ... существующие поля ... # === НОВЫЕ ПОЛЯ: WebSocket server === websocket_enabled: bool = False """True если WebSocket сервер должен быть запущен.""" websocket_host: str = "0.0.0.0" """Host для WebSocket сервера.""" websocket_port: int = 8675 """Port для WebSocket сервера.""" ``` **Итого изменений:** ~10 строк --- ### Этап WS-2: Модификация `wyoming_satellite/satellite.py` (2-3 часа) ⏳ ОЖИДАЕТСЯ #### Зависимость `websockets>=10.0` уже установлен в `requirements.txt` ✅ #### 2.1 Добавить атрибуты для WebSocket сервера В `SatelliteBase.__init__()` после `_event_task`: ```python # WebSocket server (новый) self._websocket_server: Optional[websockets.server.Serve] = None self._websocket_clients: Set[websockets.server.WebSocketServerProtocol] = set() self._websocket_lock = asyncio.Lock() ``` **Импорт:** `Set` из `typing`, `websockets` #### 2.2 Метод для рассылки в WebSocket ```python async def _broadcast_to_websocket(self, event: Event) -> None: """ Отправить событие всем WebSocket клиентам. Мёртвые подключения автоматически удаляются. """ import json async with self._websocket_lock: if not self._websocket_clients: return dead_clients = [] message = json.dumps( {"type": event.type, "data": event.data or {}}, ensure_ascii=False ) for ws in self._websocket_clients: try: await ws.send(message) except Exception: _LOGGER.debug( f"Failed to send event to WebSocket client, removing" ) dead_clients.append(ws) # Удалить мёртвые подключения for ws in dead_clients: self._websocket_clients.discard(ws) ``` #### 2.3 WebSocket сервер ```python async def start_websocket_server(self) -> None: """ Запустить WebSocket сервер для внешних клиентов. Клиенты могут подключаться к порту websocket_port и получать все события. """ async def handle_websocket(websocket, path): client_addr = websocket.remote_address _LOGGER.info(f"WebSocket client connected: {client_addr}") async with self._websocket_lock: self._websocket_clients.add(websocket) try: # Ждать команды от клиента async for message in websocket: try: data = json.loads(message) command = data.get('command') command_data = data.get('data', {}) if command: _LOGGER.debug(f"Received command: {command}") await self.handle_websocket_command(command, command_data) else: _LOGGER.debug(f"Received message (no command): {data}") except json.JSONDecodeError: _LOGGER.warning(f"Invalid JSON from WebSocket: {message}") except Exception as exc: _LOGGER.error(f"Error processing command: {exc}") except websockets.exceptions.ConnectionClosed: pass # Нормальное отключение except Exception as exc: _LOGGER.debug(f"WebSocket client error: {exc}") finally: async with self._websocket_lock: self._websocket_clients.discard(websocket) _LOGGER.info(f"WebSocket client disconnected: {client_addr}") self._websocket_server = await websockets.serve( handle_websocket, self.settings.event.websocket_host, self.settings.event.websocket_port ) _LOGGER.info( f"WebSocket server listening on " f"{self.settings.event.websocket_host}:{self.settings.event.websocket_port}" ) async def stop_websocket_server(self) -> None: """ Остановить WebSocket сервер и закрыть все подключения. """ _LOGGER.info("Stopping WebSocket server...") if self._websocket_server: self._websocket_server.close() await self._websocket_server.wait_closed() self._websocket_server = None # Закрыть все подключения async with self._websocket_lock: for ws in self._websocket_clients: await ws.close(1000, "Server shutting down") self._websocket_clients.clear() _LOGGER.info("WebSocket server stopped") ``` #### 2.4 Обработчик команд ```python async def handle_websocket_command(self, command: str, data: dict) -> None: """ Обработать команду от WebSocket клиента. Поддерживаемые команды: - set_volume: Изменить громкость - mute: Заглушить микрофон - unmute: Включить микрофон - play_sound: Воспроизвести WAV файл - set_led_brightness: Яркость LED - set_led_color: Цвет LED - trigger_event: Отправить событие в Home Assistant """ import json try: if command == "set_volume": # Изменить громкость (0.0 - 1.0) level = float(data.get("level", 1.0)) level = max(0.0, min(1.0, level)) # Clamp 0-1 # Сохранить в settings (потребуется добавить mutable settings) _LOGGER.info(f"Setting volume to {level}") # Примечание: settings frozen, нужно будет изменить архитектуру elif command == "mute": _LOGGER.info("Muting microphone") self.microphone_muted = True elif command == "unmute": _LOGGER.info("Unmuting microphone") self.microphone_muted = False elif command == "play_sound": # Воспроизвести WAV файл wav_path = data.get("path") if wav_path: _LOGGER.info(f"Playing sound: {wav_path}") await self._play_wav(wav_path, mute_microphone=False) else: _LOGGER.warning("play_sound command missing 'path'") elif command == "set_led_brightness": # Отправить команду в LED сервис (через event.uri) brightness = int(data.get("brightness", 128)) _LOGGER.info(f"Setting LED brightness to {brightness}") # Отправить событие в event service elif command == "set_led_color": # Установить цвет LED r = int(data.get("r", 0)) g = int(data.get("g", 0)) b = int(data.get("b", 0)) _LOGGER.info(f"Setting LED color to RGB({r}, {g}, {b})") # Отправить событие в event service elif command == "trigger_event": # Отправить событие в Home Assistant event_type = data.get("event_type") event_data = data.get("data", {}) if event_type: _LOGGER.info(f"Triggering event: {event_type}") # Отправить RunPipeline или другое событие else: _LOGGER.warning("trigger_event missing 'event_type'") else: _LOGGER.warning(f"Unknown command: {command}") except Exception as exc: _LOGGER.error(f"Error handling command {command}: {exc}") ``` #### 2.5 Интеграция в жизненный цикл **В `_start()`:** ```python async def _start(self) -> None: """Connect to services.""" self.state = State.STARTING await self._connect_to_services() # Запустить WebSocket сервер (новый) if self.settings.event.websocket_enabled: await self.start_websocket_server() self.state = State.STARTED await self.started() ``` **В `_stop()`:** ```python async def _stop(self) -> None: """Disconnect from services.""" self.server_id = None self._writer = None # Остановить WebSocket сервер (новый) if self._websocket_server: await self.stop_websocket_server() await self._disconnect_from_services() self._disable_ping() self.state = State.STOPPED ``` #### 2.6 Обновить `forward_event()` ```python async def forward_event(self, event: Event) -> None: """ Forward an event to all event consumers. Sends to: - Legacy event service (if configured via event.uri) - All connected WebSocket clients """ # Legacy event service (через TCP клиент) if self._event_queue is not None: self._event_queue.put_nowait(event) # WebSocket клиенты await self._broadcast_to_websocket(event) ``` **Итого изменений:** ~150 строк --- ### Этап WS-2A: Протокол WebSocket команд (30 мин) ⏳ ОЖИДАЕТСЯ **Формат сообщений:** ```json { "command": "имя_команды", "data": { "ключ": "значение" } } ``` **Поддерживаемые команды:** | Команда | Данные | Описание | |---------|--------|----------| | `set_volume` | `{"level": 0.0-1.0}` | Изменить громкость | | `mute` | `{}` | Заглушить микрофон | | `unmute` | `{}` | Включить микрофон | | `play_sound` | `{"path": "/path/to.wav"}` | Воспроизвести WAV | | `set_led_brightness` | `{"brightness": 0-255}` | Яркость LED | | `set_led_color` | `{"r": 255, "g": 0, "b": 0}` | Цвет LED (RGB) | | `trigger_event` | `{"event_type": "...", "data": {...}}` | Событие в HA | **Примеры:** ```javascript // Установить громкость 70% ws.send(JSON.stringify({ "command": "set_volume", "data": {"level": 0.7} })); // Заглушить микрофон ws.send(JSON.stringify({ "command": "mute", "data": {} })); // Воспроизвести звук ws.send(JSON.stringify({ "command": "play_sound", "data": {"path": "/sounds/awake.wav"} })); // Установить цвет LED (красный) ws.send(JSON.stringify({ "command": "set_led_color", "data": {"r": 255, "g": 0, "b": 0} })); // Отправить событие в Home Assistant ws.send(JSON.stringify({ "command": "trigger_event", "data": { "event_type": "wyoming_satellite.custom", "data": {"custom_data": "value"} } })); ``` **Ответы сервера:** Сервер отправляет события в формате: ```json { "type": "voice-started", "data": {} } ``` --- ### Этап WS-3: Модификация `wyoming_satellite/__main__.py` (20 мин) ⏳ ОЖИДАЕТСЯ #### 3.1 Добавить CLI аргументы После существующих аргументов (перед `args = parser.parse_args()`): ```python # WebSocket server parser.add_argument( "--websocket-enabled", action="store_true", help="Enable WebSocket server for external clients (default: disabled)" ) parser.add_argument( "--websocket-host", default="0.0.0.0", help="Host for WebSocket server (default: 0.0.0.0)" ) parser.add_argument( "--websocket-port", type=int, default=8675, help="Port for WebSocket server (default: 8675)" ) ``` #### 3.2 Передать настройки в EventSettings В существующем `EventSettings(...)`: ```python event=EventSettings( uri=args.event_uri, # ... существующие поля ... # === НОВЫЕ ПОЛЯ === websocket_enabled=args.websocket_enabled, websocket_host=args.websocket_host, websocket_port=args.websocket_port, ), ``` **Итого изменений:** ~20 строк --- ### Этап WS-4: Тестирование WebSocket (1 час) ⏳ ОЖИДАЕТСЯ #### 4.1 Запустить satellite с WebSocket ```bash cd ~/projects/satellite-dev source .venv/bin/activate python3 -m wyoming_satellite \ --uri tcp://0.0.0.0:10700 \ --event-uri tcp://127.0.0.1:10500 \ --websocket-enabled \ --websocket-port 8675 \ --debug ``` **Ожидаемый вывод:** ``` INFO: Ready INFO: WebSocket server listening on 0.0.0.0:8675 INFO: Слушаем tcp://0.0.0.0:10700 ``` #### 4.2 Проверить подключения **Терминал 1: test_client.py** ```bash source .venv/bin/activate python3 examples/websocket-service/test_client.py \ --uri ws://127.0.0.1:8675 ``` **Терминал 2: Браузер** ``` Открыть examples/websocket-service/client.html ``` **Терминал 3: wyoming-led.py (event client)** ```bash source .venv/bin/activate python3 examples/wyoming-led.py --host 127.0.0.1 --port 10500 ``` #### 4.3 Проверить логи **В логе satellite:** ``` INFO: WebSocket client connected: ('127.0.0.1', XXXX) INFO: Connected to server # Home Assistant ``` **Проверить:** - ✅ WebSocket клиент получает события - ✅ wyoming-led.py работает через `--event-uri` - ✅ Home Assistant работает (порт 10700) - ✅ При отключении клиента satellite не падает --- ### Этап WS-5: Обновление документации (20 мин) ⏳ ОЖИДАЕТСЯ #### 5.1 `docs/integration/mqtt-led.md` Добавить секцию после "Архитектура USB контроллера": ```markdown ## WebSocket сервер (внешние клиенты) Wyoming Satellite поддерживает встроенный WebSocket сервер для внешних клиентов. ### Преимущества - ✅ **Не нужен отдельный процесс** — websocket-service встроен в satellite - ✅ **Прямая трансляция событий** — без промежуточных очередей - ✅ **Масштабируемость** — неограниченное количество WebSocket клиентов ### Запуск ```bash python3 -m wyoming_satellite \ --uri tcp://0.0.0.0:10700 \ --event-uri tcp://127.0.0.1:10500 \ --websocket-enabled \ --websocket-port 8675 ``` ### Подключение из браузера ```javascript const ws = new WebSocket('ws://localhost:8675'); ws.onmessage = (event) => { const data = JSON.parse(event.data); console.log('Событие:', data.type, data.data); }; ``` ### Примеры - `examples/websocket-service/client.html` — браузерный клиент - `examples/websocket-service/test_client.py` — Python тест клиент ### Обратная совместимость Старый `wyoming-led.py` продолжает работать через `--event-uri`: ```bash python3 -m wyoming_satellite \ --event-uri tcp://127.0.0.1:10500 \ --websocket-enabled \ --websocket-port 8675 ``` Оба метода работают параллельно! ``` --- ### Этап WS-6: Audio level — RMS громкость микрофона (30 мин) ✅ ВЫПОЛНЕНО **Трансляция уровня громкости в WebSocket для визуализации.** #### 6.1 Формат события ```json { "type": "audio-level", "data": { "level": 0.42 // 0.0 - 1.0, RMS нормализованный } } ``` **Частота:** 10 раз в секунду (100 мс) #### 6.2 Модификация `wyoming_satellite/settings.py` Добавить настройку: ```python @dataclass(frozen=True) class EventSettings(ServiceSettings): # ... существующие поля ... # Audio level monitoring audio_level_enabled: bool = False """True если нужно вычислять и отправлять RMS громкость микрофона.""" audio_level_interval_ms: int = 100 """Интервал отправки audio-level событий (миллисекунды).""" ``` #### 6.3 Модификация `wyoming_satellite/satellite.py` **Утилита для расчёта RMS:** ```python def _calculate_rms(self, audio_bytes: bytes) -> float: """ Вычислить RMS громкость (0.0 - 1.0). Использует тот же алгоритм что и WebRtcVadEnergy из vad.py """ pcm = array.array("h") pcm.frombytes(audio_bytes) if not pcm: return 0.0 rms = math.sqrt(sum(int(s) * int(s) for s in pcm) / len(pcm)) return rms / 32767.0 # Нормализация для 16-bit ``` **Атрибуты в `__init__()`:** ```python self._last_mic_level: float = 0.0 self._audio_level_task: Optional[asyncio.Task] = None ``` **Вычисление в `event_from_mic()`:** ```python async def event_from_mic( self, event: Event, audio_bytes: Optional[bytes] = None ) -> None: """Called when an event is received from the mic service.""" # Вычислить RMS для аудио чанка if AudioChunk.is_type(event.type) and audio_bytes: self._last_mic_level = self._calculate_rms(audio_bytes) # ... остальной код ... ``` **Фоновая задача отправки:** ```python async def _send_audio_level(self) -> None: """ Периодически отправлять audio-level события в WebSocket. """ from wyoming.event import Event as WyomingEvent interval = self.settings.event.audio_level_interval_ms / 1000.0 while self.is_running: await asyncio.sleep(interval) if self.settings.event.audio_level_enabled and self._websocket_clients: event = WyomingEvent( "audio-level", {"level": self._last_mic_level} ).event() await self._broadcast_to_websocket(event) ``` **Интеграция в `_start()` / `_stop()`:** ```python async def _start(self) -> None: # ... существующий код ... # Запустить задачу audio level (новый) if self.settings.event.audio_level_enabled: self._audio_level_task = asyncio.create_task( self._send_audio_level(), name="audio_level" ) async def _stop(self) -> None: # ... существующий код ... # Остановить задачу audio level if self._audio_level_task: self._audio_level_task.cancel() self._audio_level_task = None ``` #### 6.4 Модификация `wyoming_satellite/__main__.py` CLI аргументы: ```python parser.add_argument( "--audio-level-enabled", action="store_true", help="Enable audio level monitoring and send audio-level events to WebSocket" ) parser.add_argument( "--audio-level-interval-ms", type=int, default=100, help="Interval for audio-level events in milliseconds (default: 100)" ) ``` Передача в настройки: ```python event=EventSettings( # ... существующие поля ... # Audio level audio_level_enabled=args.audio_level_enabled, audio_level_interval_ms=args.audio_level_interval_ms, ) ``` #### 6.5 Пример использования в браузере ```html
``` #### 6.6 Запуск с audio level ```bash python3 -m wyoming_satellite \ --uri tcp://0.0.0.0:10700 \ --websocket-enabled \ --websocket-port 8675 \ --audio-level-enabled \ --audio-level-interval-ms 100 ``` **Итого изменений:** ~40 строк --- ## Итоговые изменения | Файл | Тип | Изменения | Строк | |------|-----|-----------|-------| | `wyoming_satellite/settings.py` | Изменён | EventSettings + WebSocket | ~10 | | `wyoming_satellite/satellite.py` | Изменён | WebSocket server + broadcast + commands | ~200 | | `wyoming_satellite/__main__.py` | Изменён | CLI аргументы | ~20 | | `examples/websocket-service/*` | Существующий | Готовые клиенты | — | | `docs/integration/mqtt-led.md` | Изменён | Документация WebSocket + commands | ~80 | | **ВСЕГО** | | | **~310 строк** | --- ## Риски и план Б | Риск | Вероятность | Влияние | Решение | |------|-------------|---------|---------| | Конфликт портов (8675 занят) | Низкая | Среднее | Проверка `lsof -i :8675`, смена порта | | Утечка памяти (клиенты) | Средняя | Высокое | Логирование подключений, таймауты | | Падение satellite | Низкая | Критичное | Тестирование в ветке, backup файлов | | Home Assistant отключится | Низкая | Критичное | Порт 10700 не меняется, обратная совместимость | | Event client не работает | Низкая | Критичное | `--event-uri` не меняется, проверять логи | ### План Б: Откат Если что-то идёт не так: ```bash cd ~/projects/Satellite git checkout master # Восстановить файлы cp wyoming_satellite/satellite.py.backup wyoming_satellite/satellite.py cp wyoming_satellite/settings.py.backup wyoming_satellite/settings.py cp wyoming_satellite/__main__.py.backup wyoming_satellite/__main__.py # Перезапустить сервисы sudo systemctl daemon-reload sudo systemctl restart wyoming-satelite-vosk.service ``` --- ## Контрольный список ### ✅ Этап 1: Подготовка (выполнено) - [x] Ветка создана — `satellite-dev` - [x] Окружение настроено — `.venv/` в корне проекта - [x] `setup_dev.sh` создан - [x] Лишние файлы удалены (Docker, test.wav, старые скрипты) - [x] `python3 -c "from wyoming_satellite.satellite import SatelliteBase; print('OK')"` работает ### ✅ Этап WS-1: settings.py (выполнено) - [x] `websocket_enabled: bool` добавлено - [x] `websocket_host: str` добавлено - [x] `websocket_port: int` добавлено - [x] `python3 -c "from wyoming_satellite.settings import EventSettings; print(EventSettings.websocket_enabled)"` работает ### ✅ Этап WS-2: satellite.py (выполнено) - [x] `_websocket_server: Optional[asyncio.Server]` добавлено - [x] `_websocket_clients: Set[...]` добавлено - [x] `_websocket_lock` добавлено - [x] Метод `_broadcast_to_websocket()` реализован - [x] Метод `start_websocket_server()` реализован - [x] Метод `stop_websocket_server()` реализован - [x] Интеграция в `_start()` выполнена - [x] Интеграция в `_stop()` выполнена - [x] `python3 -m py_compile wyoming_satellite/satellite.py` проходит ### ✅ Этап WS-2A: Протокол команд (выполнено) - [x] `handle_websocket_command()` реализован - [x] Команда `set_volume` работает - [x] Команда `mute`/`unmute` работает - [x] Команда `play_sound` работает - [x] Команда `set_led_*` работает - [x] Команда `trigger_event` работает - [x] Обработка ошибок реализована ### ✅ Этап WS-3: __main__.py (выполнено) - [x] `--websocket-enabled` аргумент добавлен - [x] `--websocket-host` аргумент добавлен - [x] `--websocket-port` аргумент добавлен - [x] Передача настроек в `EventSettings()` настроена - [x] `python3 -m wyoming_satellite --help` показывает новые аргументы ### 🔄 Этап WS-4: Тестирование (в процессе) - [ ] Satellite запускается с `--websocket-enabled` - [ ] WebSocket клиент подключается к порту 8675 - [ ] Все клиенты получают события - [ ] Home Assistant работает (порт 10700) - [ ] wyoming-led.py работает (порт 10500) - [ ] При отключении клиента satellite не падает - [ ] Утечек памяти нет (через 10 минут работы) ### ⏳ Этап WS-5: Документация (ожидается) - [ ] `docs/integration/mqtt-led.md` обновлена - [ ] Примеры в документации работают ### ⏳ Перед merge - [ ] Все тесты пройдены - [ ] Документация актуальна - [ ] Ветка называется `satellite-dev` --- ## Приложения ### A. Пример события в WebSocket клиенте ```json { "type": "voice-started", "data": {} } ``` ```json { "type": "voice-stopped", "data": {} } ``` ```json { "type": "transcript", "data": { "text": "привет дроид" } } ``` ### B. Диагностика ```bash # Проверка портов lsof -i :8675 lsof -i :10700 lsof -i :10500 # Проверка подключений netstat -tlnp | grep python # Логи satellite journalctl -u wyoming-satelite-vosk.service -f # Тест подключения telnet 127.0.0.1 8675 ``` ### C. Сравнение подходов | Аспект | Старый (отдельный сервис) | Новый (встроенный) | |--------|---------------------------|--------------------| | Процессы | 2 (satellite + websocket-service) | 1 (satellite) | | Подключения | satellite → websocket → clients | clients → satellite | | Отказоустойчивость | Падение websocket → переподключение | Падение client → satellite работает | | Масштабируемость | Ограничено | Неограниченно клиентов | | Сложность | Выше (два сервиса) | Ниже (один сервис) | | Обратная совместимость | ✅ Работает через event.uri | ✅ Работает через event.uri | --- **Конец документа**