diff --git a/docs/development/multi-event-server-plan.md b/docs/development/multi-event-server-plan.md index 807771b..a1ce0db 100644 --- a/docs/development/multi-event-server-plan.md +++ b/docs/development/multi-event-server-plan.md @@ -1,4 +1,4 @@ -# План: Множественные подключения event сервисов к Wyoming Satellite +# План: WebSocket сервер для Wyoming Satellite **Дата:** 2026-05-20 **Статус:** В разработке @@ -20,12 +20,11 @@ ### 🔄 В процессе -- [ ] **Этап 2: settings.py** — добавить server_enabled, server_host, server_port -- [ ] **Этап 3: satellite.py** — event server + broadcast метод -- [ ] **Этап 4: __main__.py** — CLI аргументы для event server -- [ ] **Этап 5: test_event_client.py** — тестовый клиент -- [ ] **Этап 6: Тестирование** — проверка работы -- [ ] **Этап 7: Документация** — обновление docs/ +- [ ] **Этап WS-1: settings.py** — добавить websocket_enabled, websocket_host, websocket_port +- [ ] **Этап WS-2: satellite.py** — встроенный WebSocket сервер +- [ ] **Этап WS-3: __main__.py** — CLI аргументы для WebSocket +- [ ] **Этап WS-4: Тестирование** — проверка WebSocket клиентов +- [ ] **Этап WS-5: Документация** — обновление docs/ ### ⏳ Ожидается @@ -34,45 +33,9 @@ --- -## Стратегия разработки - -### Изоляция разработки - -**Текущая версия (production):** -- Директория: `~/projects/Satellite` -- Ветка: `master` -- Статус: ✅ Рабочая версия, используется в production - -**Версия для разработки:** -- Директория: `~/projects/satellite-dev` -- Ветка: `satellite-dev` -- Статус: 🚧 Экспериментальная версия -- Окружение: `.venv/` в корне проекта - -### Преимущества такого подхода - -- ✅ **Полная изоляция** — эксперименты не влияют на рабочую систему -- ✅ **Быстрый откат** — просто переключаемся на `~/projects/Satellite` -- ✅ **Чистая история** — merge в master только после тестирования -- ✅ **Параллельная работа** — можно продолжать фиксить баги в Satellite пока идёт разработка - -### Быстрый старт для разработки - -```bash -cd ~/projects/satellite-dev - -# Настройка окружения (один раз) -./setup_dev.sh - -# Запуск для тестирования -./start_satellite.sh wake -``` - ---- - ## Архитектура -### Текущее состояние +### Текущее состояние (production) ``` ┌─────────────────┐ ┌─────────────────┐ @@ -81,37 +44,45 @@ cd ~/projects/satellite-dev │ Client) │ │ Server) │ └─────────────────┘ └────────┬────────┘ │ - │ 10500 (один клиент!) + │ 10500 (outgoing!) ▼ ┌─────────────────┐ │ Event Service │ - │ (клиент) │ + │ (LED, MQTT) │ └─────────────────┘ ``` -**Проблема:** Satellite поддерживает **только одного** event клиента через `event.uri` +**Ограничение:** Satellite **подключается** к event службе, но не может принимать входящие подключения ### Целевое состояние ``` -┌─────────────────┐ ┌─────────────────┐ -│ Home Assistant │ ──────► │ Satellite │ -│ (Wyoming │ 10700 │ (Wyoming │ -│ Client) │ │ Server) │ -└─────────────────┘ └────────┬────────┘ - │ - ┌───────────────┼───────────────┐ - │ │ │ - │ 10600 (event │ │ - │ server) │ │ - ▼ ▼ ▼ - ┌─────────┐ ┌──────────┐ ┌──────────┐ - │ LED │ │ MQTT │ │ Logger │ - │ Service │ │ Service │ │ Service │ - └─────────┘ └──────────┘ └──────────┘ +┌─────────────────┐ ┌──────────────────────────┐ +│ Home Assistant │ ──────► │ Wyoming Satellite │ +│ (Wyoming │ 10700 │ (единый процесс) │ +│ Client) │ │ │ +└─────────────────┘ │ ┌────────────────────┐ │ + │ │ Event CLIENT │ │ + │ │ (outgoing :10500) │ │ + │ │ → LED, MQTT │ │ + │ └────────────────────┘ │ + │ ┌────────────────────┐ │ + │ │ WebSocket SERVER │ │ + │ │ (incoming :8675) │ │ + │ │ ← Browser, UI │ │ + │ └────────────────────┘ │ + └──────────────────────────┘ ``` -**Решение:** Event сервер на satellite для множественных подключений +**Принцип:** +- ✅ **Event CLIENT** (`--event-uri`) — satellite **подключается** к внешней службе (LED, MQTT) +- ✅ **WebSocket SERVER** (`--websocket-enabled`) — встроенный сервер, клиенты **подключаются к нему** + +**Преимущества:** +- ✅ **Не нужен отдельный процесс** — websocket-service встроен в satellite +- ✅ **Прямая трансляция событий** — без промежуточных очередей +- ✅ **Масштабируемость** — неограниченное количество WebSocket клиентов +- ✅ **Обратная совместимость** — `--event-uri` продолжает работать --- @@ -158,198 +129,147 @@ python3 -c "from wyoming_satellite.satellite import SatelliteBase; print('OK')" --- -### Этап 2: Модификация `wyoming_satellite/settings.py` (1 час) 🔄 В ПРОЦЕССЕ +### Этап WS-1: Модификация `wyoming_satellite/settings.py` (30 мин) 🔄 В ПРОЦЕССЕ -**Добавить настройки event сервера:** +**Добавить настройки WebSocket сервера:** ```python @dataclass(frozen=True) class EventSettings(ServiceSettings): - """External event service settings.""" + # ... существующие поля ... + + # === НОВЫЕ ПОЛЯ: WebSocket server === + websocket_enabled: bool = False + """True если WebSocket сервер должен быть запущен.""" - startup: Optional[List[str]] = None - streaming_start: Optional[List[str]] = None - streaming_stop: Optional[List[str]] = None - detect: Optional[List[str]] = None - detection: Optional[List[str]] = None - played: Optional[List[str]] = None - transcript: Optional[List[str]] = None - stt_start: Optional[List[str]] = None - stt_stop: Optional[List[str]] = None - synthesize: Optional[List[str]] = None - tts_start: Optional[List[str]] = None - tts_stop: Optional[List[str]] = None - error: Optional[List[str]] = None - connected: Optional[List[str]] = None - disconnected: Optional[List[str]] = None - - # === НОВЫЕ ПОЛЯ: Event server === - server_enabled: bool = False - """True если event сервер должен быть запущен.""" - - server_host: str = "0.0.0.0" - """Host для event сервера.""" - - server_port: int = 10600 - """Port для event сервера.""" + websocket_host: str = "0.0.0.0" + """Host для WebSocket сервера.""" + + websocket_port: int = 8675 + """Port для WebSocket сервера.""" ``` -**Итого изменений:** ~20 строк +**Итого изменений:** ~10 строк --- -### Этап 3: Модификация `wyoming_satellite/satellite.py` (3-4 часа) ⏳ ОЖИДАЕТСЯ +### Этап WS-2: Модификация `wyoming_satellite/satellite.py` (2-3 часа) ⏳ ОЖИДАЕТСЯ -#### 3.1 Добавить атрибуты для event сервера +#### Зависимость -В `SatelliteBase.__init__()` после существующих атрибутов: +`websockets>=10.0` уже установлен в `requirements.txt` ✅ + +#### 2.1 Добавить атрибуты для WebSocket сервера + +В `SatelliteBase.__init__()` после `_event_task`: ```python -# Event server (новый) -self._event_server: Optional[asyncio.Server] = None -self._event_clients: List[asyncio.StreamWriter] = [] -self._event_clients_lock = asyncio.Lock() +# WebSocket server (новый) +self._websocket_server: Optional[websockets.server.Serve] = None +self._websocket_clients: Set[websockets.server.WebSocketServerProtocol] = set() +self._websocket_lock = asyncio.Lock() ``` -**Импорт:** `List` из `typing` (если ещё не импортирован) +**Импорт:** `Set` из `typing`, `websockets` -#### 3.2 Метод для рассылки событий всем клиентам - -После существующих методов добавить: +#### 2.2 Метод для рассылки в WebSocket ```python -async def _broadcast_to_clients(self, event: Event) -> None: +async def _broadcast_to_websocket(self, event: Event) -> None: """ - Отправить событие всем подключённым event клиентам. - + Отправить событие всем WebSocket клиентам. + Мёртвые подключения автоматически удаляются. """ - async with self._event_clients_lock: - if not self._event_clients: + import json + + async with self._websocket_lock: + if not self._websocket_clients: return - + dead_clients = [] - for i, writer in enumerate(self._event_clients): + message = json.dumps( + {"type": event.type, "data": event.data or {}}, + ensure_ascii=False + ) + + for ws in self._websocket_clients: try: - await async_write_event(event, writer) + await ws.send(message) except Exception: _LOGGER.debug( - f"Failed to send event to client {i}, removing" + f"Failed to send event to WebSocket client, removing" ) - dead_clients.append(i) - + dead_clients.append(ws) + # Удалить мёртвые подключения - for i in reversed(dead_clients): - self._event_clients.pop(i) + for ws in dead_clients: + self._websocket_clients.discard(ws) ``` -#### 3.3 Заменить `forward_event()` - -**Было:** +#### 2.3 WebSocket сервер ```python -async def forward_event(self, event: Event) -> None: - """Forward an event to the event service.""" - if self._event_queue is not None: - self._event_queue.put_nowait(event) -``` - -**Стало:** - -```python -async def forward_event(self, event: Event) -> None: +async def start_websocket_server(self) -> None: """ - Forward an event to all event consumers. - - Sends to: - - Legacy event service (if configured via event.uri) - - All connected event server clients - """ - # Legacy event service (через TCP клиент) - if self._event_queue is not None: - self._event_queue.put_nowait(event) - - # Event server клиенты - await self._broadcast_to_clients(event) -``` + Запустить WebSocket сервер для внешних клиентов. -#### 3.4 Event сервер для подключений - -Добавить методы после `_event_task_proc()`: - -```python -async def start_event_server(self) -> None: + Клиенты могут подключаться к порту websocket_port и получать все события. """ - Запустить сервер для event клиентов. - - Клиенты могут подключаться к порту server_port и получать все события. - """ - async def handle_client( - reader: asyncio.StreamReader, - writer: asyncio.StreamWriter - ) -> None: - client_addr = writer.get_extra_info('peername') - _LOGGER.info(f"Event client connected: {client_addr}") - - async with self._event_clients_lock: - self._event_clients.append(writer) - + 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: # Ждать отключения клиента - await reader.read(1) - except asyncio.IncompleteReadError: + async for message in websocket: + # Игнорировать входящие сообщения (только рассылка) + _LOGGER.debug(f"Received from WebSocket: {message}") + except websockets.exceptions.ConnectionClosed: pass # Нормальное отключение except Exception as exc: - _LOGGER.debug(f"Event client error: {exc}") + _LOGGER.debug(f"WebSocket client error: {exc}") finally: - async with self._event_clients_lock: - if writer in self._event_clients: - self._event_clients.remove(writer) - - try: - writer.close() - await writer.wait_closed() - except Exception: - pass - - _LOGGER.info(f"Event client disconnected: {client_addr}") - - self._event_server = await asyncio.start_server( - handle_client, - self.settings.event.server_host, - self.settings.event.server_port - ) - - _LOGGER.info( - f"Event server listening on " - f"{self.settings.event.server_host}:{self.settings.event.server_port}" + 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 ) -async def stop_event_server(self) -> None: + _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: """ - Остановить event сервер и закрыть все подключения. + Остановить WebSocket сервер и закрыть все подключения. """ - _LOGGER.info("Stopping event server...") - - if self._event_server: - self._event_server.close() - await self._event_server.wait_closed() - self._event_server = None - + _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._event_clients_lock: - for writer in self._event_clients: - writer.close() - try: - await writer.wait_closed() - except Exception: - pass - self._event_clients.clear() - - _LOGGER.info("Event server stopped") + 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") ``` -#### 3.5 Интеграция в жизненный цикл +#### 2.4 Интеграция в жизненный цикл **В `_start()`:** @@ -358,11 +278,11 @@ async def _start(self) -> None: """Connect to services.""" self.state = State.STARTING await self._connect_to_services() - - # Запустить event сервер (новый) - if self.settings.event.server_enabled: - await self.start_event_server() - + + # Запустить WebSocket сервер (новый) + if self.settings.event.websocket_enabled: + await self.start_websocket_server() + self.state = State.STARTED await self.started() ``` @@ -374,317 +294,187 @@ async def _stop(self) -> None: """Disconnect from services.""" self.server_id = None self._writer = None - - # Остановить event сервер (новый) - if self._event_server: - await self.stop_event_server() - + + # Остановить WebSocket сервер (новый) + if self._websocket_server: + await self.stop_websocket_server() + await self._disconnect_from_services() self._disable_ping() self.state = State.STOPPED ``` -**Итого изменений:** ~200 строк +#### 2.5 Обновить `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 строк --- -### Этап 4: Модификация `wyoming_satellite/__main__.py` (30 мин) ⏳ ОЖИДАЕТСЯ +### Этап WS-3: Модификация `wyoming_satellite/__main__.py` (20 мин) ⏳ ОЖИДАЕТСЯ -#### 4.1 Добавить CLI аргументы +#### 3.1 Добавить CLI аргументы После существующих аргументов (перед `args = parser.parse_args()`): ```python -# Event server +# WebSocket server parser.add_argument( - "--event-server-enabled", + "--websocket-enabled", action="store_true", - help="Enable event server for multiple event clients (default: disabled)" + help="Enable WebSocket server for external clients (default: disabled)" ) parser.add_argument( - "--event-server-host", + "--websocket-host", default="0.0.0.0", - help="Host for event server (default: 0.0.0.0)" + help="Host for WebSocket server (default: 0.0.0.0)" ) parser.add_argument( - "--event-server-port", + "--websocket-port", type=int, - default=10600, - help="Port for event server (default: 10600)" + default=8675, + help="Port for WebSocket server (default: 8675)" ) ``` -#### 4.2 Передать настройки в SatelliteSettings +#### 3.2 Передать настройки в EventSettings В существующем `EventSettings(...)`: ```python event=EventSettings( uri=args.event_uri, - command=split_command(args.event_command), - startup=split_command(args.startup_command), - streaming_start=split_command(args.streaming_start_command), - streaming_stop=split_command(args.streaming_stop_command), - detect=split_command(args.detect_command), - detection=split_command(args.detection_command), - played=split_command(args.tts_played_command), - transcript=split_command(args.transcript_command), - stt_start=split_command(args.stt_start_command), - stt_stop=split_command(args.stt_stop_command), - synthesize=split_command(args.synthesize_command), - tts_start=split_command(args.tts_start_command), - tts_stop=split_command(args.tts_stop_command), - error=split_command(args.error_command), - connected=split_command(args.connected_command), - disconnected=split_command(args.disconnected_command), + # ... существующие поля ... # === НОВЫЕ ПОЛЯ === - server_enabled=args.event_server_enabled, - server_host=args.event_server_host, - server_port=args.event_server_port, + websocket_enabled=args.websocket_enabled, + websocket_host=args.websocket_host, + websocket_port=args.websocket_port, ), ``` -**Итого изменений:** ~30 строк +**Итого изменений:** ~20 строк --- -### Этап 5: Тестовый event клиент (1 час) ⏳ ОЖИДАЕТСЯ +### Этап WS-4: Тестирование WebSocket (1 час) ⏳ ОЖИДАЕТСЯ -#### 5.1 Создать `examples/test_event_client.py` - -```python -#!/usr/bin/env python3 -""" -Test event client для подключения к Wyoming Satellite. - -Подключается к event server и получает все события. -Используется для тестирования и отладки. - -Пример использования: - python3 test_event_client.py --uri tcp://127.0.0.1:10600 -""" - -import argparse -import asyncio -import logging -import signal -import sys -from datetime import datetime - -from wyoming.event import Event -from wyoming.client import AsyncClient - -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s [%(levelname)s] %(name)s: %(message)s" -) -logger = logging.getLogger("test_event_client") - -stop_event = asyncio.Event() - -def signal_handler(sig, frame): - logger.info(f"Получен сигнал {sig}, завершаю...") - stop_event.set() - -async def main(args): - logger.info(f"Connecting to {args.uri}...") - - client = AsyncClient.from_uri(args.uri) - - try: - await client.connect() - logger.info("✅ Connected!") - - event_count = 0 - while not stop_event.is_set(): - try: - event = await asyncio.wait_for( - client.read_event(), - timeout=5.0 - ) - - if event is None: - logger.warning("⚠️ Server disconnected") - break - - event_count += 1 - event_type = event.get("type", "unknown") - logger.info(f"📨 [{event_count}] {event_type}") - - if args.verbose: - logger.debug(f" Data: {event}") - - except asyncio.TimeoutError: - # Проверка stop_event - continue - except Exception as exc: - logger.error(f"❌ Error reading event: {exc}") - break - - logger.info(f"Всего получено событий: {event_count}") - - except Exception as exc: - logger.error(f"❌ Error: {exc}") - finally: - try: - await client.disconnect() - except Exception: - pass - logger.info("👋 Disconnected") - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - description="Test event client для Wyoming Satellite" - ) - parser.add_argument( - "--uri", - default="tcp://127.0.0.1:10600", - help="URI для подключения (default: tcp://127.0.0.1:10600)" - ) - parser.add_argument( - "--verbose", - action="store_true", - help="Выводить полные данные событий" - ) - args = parser.parse_args() - - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - - try: - asyncio.run(main(args)) - except KeyboardInterrupt: - pass -``` - -#### 5.2 Сделать исполняемым +#### 4.1 Запустить satellite с WebSocket ```bash -chmod +x examples/test_event_client.py -``` - ---- - -### Этап 6: Тестирование (2-3 часа) ⏳ ОЖИДАЕТСЯ - -#### 6.1 Запустить satellite с event сервером - -```bash -cd ~/projects/Satellite -source ~/.venv_satellite/bin/activate +cd ~/projects/satellite-dev +source .venv/bin/activate python3 -m wyoming_satellite \ --uri tcp://0.0.0.0:10700 \ - --event-server-enabled \ - --event-server-port 10600 \ + --event-uri tcp://127.0.0.1:10500 \ + --websocket-enabled \ + --websocket-port 8675 \ --debug ``` **Ожидаемый вывод:** ``` INFO: Ready -INFO: Event server listening on 0.0.0.0:10600 +INFO: WebSocket server listening on 0.0.0.0:8675 INFO: Слушаем tcp://0.0.0.0:10700 ``` -#### 6.2 Проверить подключения +#### 4.2 Проверить подключения -**Терминал 1: Home Assistant (порт 10700)** +**Терминал 1: test_client.py** ```bash -# Эмуляция подключения HA (или реальное HA) -# HA должен подключиться автоматически через Zeroconf +source .venv/bin/activate +python3 examples/websocket-service/test_client.py \ + --uri ws://127.0.0.1:8675 ``` -**Терминал 2: test_event_client #1 (порт 10600)** -```bash -source ~/.venv_satellite/bin/activate -python3 examples/test_event_client.py --uri tcp://127.0.0.1:10600 +**Терминал 2: Браузер** +``` +Открыть examples/websocket-service/client.html ``` -**Терминал 3: test_event_client #2 (порт 10600)** +**Терминал 3: wyoming-led.py (event client)** ```bash -source ~/.venv_satellite/bin/activate -python3 examples/test_event_client.py --uri tcp://127.0.0.1:10600 +source .venv/bin/activate +python3 examples/wyoming-led.py --host 127.0.0.1 --port 10500 ``` -**Терминал 4:原有 wyoming-led.py (порт 10500)** -```bash -source ~/.venv_satellite/bin/activate -cd examples -python3 wyoming-led.py --host 127.0.0.1 --port 10500 -``` - -#### 6.3 Проверить логи +#### 4.3 Проверить логи **В логе satellite:** ``` -INFO: Event client connected: ('127.0.0.1', XXXX) # клиент 1 -INFO: Event client connected: ('127.0.0.1', XXXX) # клиент 2 +INFO: WebSocket client connected: ('127.0.0.1', XXXX) INFO: Connected to server # Home Assistant ``` **Проверить:** -- ✅ Все 3 клиента получают события -- ✅ Home Assistant продолжает работать (порт 10700) +- ✅ WebSocket клиент получает события +- ✅ wyoming-led.py работает через `--event-uri` +- ✅ Home Assistant работает (порт 10700) - ✅ При отключении клиента satellite не падает -- ✅ wyoming-led.py работает через старый порт 10500 - -#### 6.4 Тест на устойчивость - -```bash -# Отключить клиента 1 (Ctrl+C) -# Проверить что клиент 2 продолжает работать - -# Отключить wyoming-led.py -# Проверить что event клиенты работают - -# Отключить Home Assistant -# Проверить что event клиенты работают -``` --- -### Этап 7: Обновление документации (30 мин) ⏳ ОЖИДАЕТСЯ +### Этап WS-5: Обновление документации (20 мин) ⏳ ОЖИДАЕТСЯ -#### 7.1 `docs/integration/mqtt-led.md` +#### 5.1 `docs/integration/mqtt-led.md` Добавить секцию после "Архитектура USB контроллера": ```markdown -## Event Server (множественные подключения) +## WebSocket сервер (внешние клиенты) -Начиная с версии 2026.06, Wyoming Satellite поддерживает event server для -множественных подключений. +Wyoming Satellite поддерживает встроенный WebSocket сервер для внешних клиентов. ### Преимущества -- ✅ **Масштабируемость:** Неограниченное количество event клиентов -- ✅ **Надёжность:** Падение клиента не влияет на satellite -- ✅ **Гибкость:** Каждый клиент выбирает какие события обрабатывать +- ✅ **Не нужен отдельный процесс** — websocket-service встроен в satellite +- ✅ **Прямая трансляция событий** — без промежуточных очередей +- ✅ **Масштабируемость** — неограниченное количество WebSocket клиентов ### Запуск ```bash python3 -m wyoming_satellite \ --uri tcp://0.0.0.0:10700 \ - --event-server-enabled \ - --event-server-port 10600 + --event-uri tcp://127.0.0.1:10500 \ + --websocket-enabled \ + --websocket-port 8675 ``` -### Подключение клиента +### Подключение из браузера -```python -from wyoming.client import AsyncClient +```javascript +const ws = new WebSocket('ws://localhost:8675'); -client = AsyncClient.from_uri("tcp://127.0.0.1:10600") -await client.connect() - -while True: - event = await client.read_event() - # Обработка события +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`: @@ -692,96 +482,25 @@ while True: ```bash python3 -m wyoming_satellite \ --event-uri tcp://127.0.0.1:10500 \ - --event-server-enabled \ - --event-server-port 10600 + --websocket-enabled \ + --websocket-port 8675 ``` Оба метода работают параллельно! ``` -#### 7.2 `docs/deployment/systemd-services.md` - -Обновить секцию `wyoming-event.service`: - -```markdown -### Event Server (новый) - -Для поддержки множественных event клиентов добавьте в сервис: - -```ini -ExecStart=/home/sirmrak/projects/.venv_satellite/bin/python3 \ - /home/sirmrak/projects/Satellite/wyoming_satellite/__main__.py \ - --uri tcp://0.0.0.0:10700 \ - --event-server-enabled \ - --event-server-port 10600 -``` - -Клиенты подключаются к порту 10600: - -```bash -python3 examples/test_event_client.py --uri tcp://127.0.0.1:10600 -``` -``` - -#### 7.3 `DEPLOY.md` - -Добавить секцию после "LED управление": - -```markdown -## Event Server (множественные подключения) - -Для подключения нескольких event сервисов одновременно: - -```bash -python3 -m wyoming_satellite \ - --event-server-enabled \ - --event-server-port 10600 -``` - -**Примеры использования:** - -- LED управление + MQTT уведомления + логирование в базу -- Несколько независимых обработчиков событий -- Тестовые клиенты для отладки - -**Документация:** [Event Server](docs/integration/mqtt-led.md#event-server) -``` - ---- - -### Этап 8: Merge в master (после успешного тестирования) ⏳ ОЖИДАЕТСЯ - -```bash -# Проверка что всё работает -git status -git diff master - -# Merge -git checkout master -git pull origin master -git merge feature/multi-event-server - -# Push -git push origin master - -# Удалить ветку (опционально) -git branch -d feature/multi-event-server -``` - --- ## Итоговые изменения | Файл | Тип | Изменения | Строк | |------|-----|-----------|-------| -| `wyoming_satellite/settings.py` | Изменён | EventSettings + server настройки | ~20 | -| `wyoming_satellite/satellite.py` | Изменён | Event server + broadcast | ~200 | -| `wyoming_satellite/__main__.py` | Изменён | CLI аргументы | ~30 | -| `examples/test_event_client.py` | Новый | Тестовый клиент | ~80 | -| `docs/integration/mqtt-led.md` | Изменён | Документация event server | ~40 | -| `docs/deployment/systemd-services.md` | Изменён | Обновление сервисов | ~20 | -| `DEPLOY.md` | Изменён | Quick start | ~15 | -| **ВСЕГО** | | | **~405 строк** | +| `wyoming_satellite/settings.py` | Изменён | EventSettings + WebSocket | ~10 | +| `wyoming_satellite/satellite.py` | Изменён | WebSocket server + broadcast | ~150 | +| `wyoming_satellite/__main__.py` | Изменён | CLI аргументы | ~20 | +| `examples/websocket-service/*` | Существующий | Готовые клиенты | — | +| `docs/integration/mqtt-led.md` | Изменён | Документация WebSocket | ~50 | +| **ВСЕГО** | | | **~230 строк** | --- @@ -789,11 +508,11 @@ git branch -d feature/multi-event-server | Риск | Вероятность | Влияние | Решение | |------|-------------|---------|---------| -| Конфликт портов (10600 занят) | Низкая | Среднее | Проверка `lsof -i :10600`, смена порта | +| Конфликт портов (8675 занят) | Низкая | Среднее | Проверка `lsof -i :8675`, смена порта | | Утечка памяти (клиенты) | Средняя | Высокое | Логирование подключений, таймауты | | Падение satellite | Низкая | Критичное | Тестирование в ветке, backup файлов | | Home Assistant отключится | Низкая | Критичное | Порт 10700 не меняется, обратная совместимость | -| Event клиенты не получают события | Средняя | Среднее | Проверка `_broadcast_to_clients()`, логирование | +| Event client не работает | Низкая | Критичное | `--event-uri` не меняется, проверять логи | ### План Б: Откат @@ -825,55 +544,47 @@ sudo systemctl restart wyoming-satelite-vosk.service - [x] Лишние файлы удалены (Docker, test.wav, старые скрипты) - [x] `python3 -c "from wyoming_satellite.satellite import SatelliteBase; print('OK')"` работает -### 🔄 Этап 2: settings.py (в процессе) +### 🔄 Этап WS-1: settings.py (в процессе) -- [ ] `server_enabled: bool` добавлено -- [ ] `server_host: str` добавлено -- [ ] `server_port: int` добавлено -- [ ] `python3 -c "from wyoming_satellite.settings import EventSettings; print(EventSettings.server_enabled)"` работает +- [ ] `websocket_enabled: bool` добавлено +- [ ] `websocket_host: str` добавлено +- [ ] `websocket_port: int` добавлено +- [ ] `python3 -c "from wyoming_satellite.settings import EventSettings; print(EventSettings.websocket_enabled)"` работает -### ⏳ Этап 3: satellite.py (ожидается) +### ⏳ Этап WS-2: satellite.py (ожидается) -- [ ] `_event_server: Optional[asyncio.Server]` добавлено -- [ ] `_event_clients: List[asyncio.StreamWriter]` добавлено -- [ ] `_event_clients_lock` добавлено -- [ ] Метод `_broadcast_to_clients()` реализован -- [ ] Метод `start_event_server()` реализован -- [ ] Метод `stop_event_server()` реализован +- [ ] `_websocket_server: Optional[asyncio.Server]` добавлено +- [ ] `_websocket_clients: Set[...]` добавлено +- [ ] `_websocket_lock` добавлено +- [ ] Метод `_broadcast_to_websocket()` реализован +- [ ] Метод `start_websocket_server()` реализован +- [ ] Метод `stop_websocket_server()` реализован - [ ] Интеграция в `_start()` выполнена - [ ] Интеграция в `_stop()` выполнена - [ ] `python3 -m py_compile wyoming_satellite/satellite.py` проходит -### ⏳ Этап 4: __main__.py (ожидается) +### ⏳ Этап WS-3: __main__.py (ожидается) -- [ ] `--event-server-enabled` аргумент добавлен -- [ ] `--event-server-host` аргумент добавлен -- [ ] `--event-server-port` аргумент добавлен +- [ ] `--websocket-enabled` аргумент добавлен +- [ ] `--websocket-host` аргумент добавлен +- [ ] `--websocket-port` аргумент добавлен - [ ] Передача настроек в `EventSettings()` настроена - [ ] `python3 -m wyoming_satellite --help` показывает новые аргументы -### ⏳ Этап 5: test_event_client.py (ожидается) +### ⏳ Этап WS-4: Тестирование (ожидается) -- [ ] `examples/test_event_client.py` создан -- [ ] Клиент подключается к порту 10600 -- [ ] События отображаются в логе -- [ ] Обработка отключения сервера работает - -### ⏳ Этап 6: Тестирование (ожидается) - -- [ ] Satellite запускается с `--event-server-enabled` -- [ ] Multiple clients can connect (2+ клиентов) +- [ ] Satellite запускается с `--websocket-enabled` +- [ ] WebSocket клиент подключается к порту 8675 - [ ] Все клиенты получают события - [ ] Home Assistant работает (порт 10700) - [ ] wyoming-led.py работает (порт 10500) - [ ] При отключении клиента satellite не падает - [ ] Утечек памяти нет (через 10 минут работы) -### ⏳ Этап 7: Документация (ожидается) +### ⏳ Этап WS-5: Документация (ожидается) - [ ] `docs/integration/mqtt-led.md` обновлена -- [ ] `docs/deployment/systemd-services.md` обновлён -- [ ] `DEPLOY.md` обновлён +- [ ] Примеры в документации работают ### ⏳ Перед merge @@ -885,7 +596,7 @@ sudo systemctl restart wyoming-satelite-vosk.service ## Приложения -### A. Пример события в event клиенте +### A. Пример события в WebSocket клиенте ```json { @@ -914,7 +625,7 @@ sudo systemctl restart wyoming-satelite-vosk.service ```bash # Проверка портов -lsof -i :10600 +lsof -i :8675 lsof -i :10700 lsof -i :10500 @@ -925,19 +636,19 @@ netstat -tlnp | grep python journalctl -u wyoming-satelite-vosk.service -f # Тест подключения -telnet 127.0.0.1 10600 +telnet 127.0.0.1 8675 ``` ### C. Сравнение подходов -| Аспект | Старый (push) | Новый (pull) | -|--------|---------------|--------------| -| Подключения | 1 satellite → 1 event | N clients → 1 satellite | -| Отказоустойчивость | Падение event → satellite работает | Падение client → satellite работает | -| Масштабируемость | Ограничено 1 сервисом | Неограниченно клиентов | -| Сложность | Проще | Сложнее (управление клиентами) | -| Гибкость | Низкая | Высокая (каждый клиент выбирает события) | -| Обратная совместимость | ✅ Работает | ✅ Работает через event.uri | +| Аспект | Старый (отдельный сервис) | Новый (встроенный) | +|--------|---------------------------|--------------------| +| Процессы | 2 (satellite + websocket-service) | 1 (satellite) | +| Подключения | satellite → websocket → clients | clients → satellite | +| Отказоустойчивость | Падение websocket → переподключение | Падение client → satellite работает | +| Масштабируемость | Ограничено | Неограниченно клиентов | +| Сложность | Выше (два сервиса) | Ниже (один сервис) | +| Обратная совместимость | ✅ Работает через event.uri | ✅ Работает через event.uri | ---