Files
satellite/docs/development/multi-event-server-plan.md
T

656 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# План: WebSocket сервер для Wyoming Satellite
**Дата:** 2026-05-20
**Статус:** В разработке
**Ветка:** `satellite-dev`
**Директория для разработки:** `~/projects/satellite-dev`
---
## Чеклист выполнения
### ✅ Выполнено
- [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 актуализирован
### 🔄 В процессе
- [ ] **Этап 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/
### ⏳ Ожидается
- [ ] 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:
# Игнорировать входящие сообщения (только рассылка)
_LOGGER.debug(f"Received from WebSocket: {message}")
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 Интеграция в жизненный цикл
**В `_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.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 строк
---
### Этап 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
```
Оба метода работают параллельно!
```
---
## Итоговые изменения
| Файл | Тип | Изменения | Строк |
|------|-----|-----------|-------|
| `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 строк** |
---
## Риски и план Б
| Риск | Вероятность | Влияние | Решение |
|------|-------------|---------|---------|
| Конфликт портов (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 (в процессе)
- [ ] `websocket_enabled: bool` добавлено
- [ ] `websocket_host: str` добавлено
- [ ] `websocket_port: int` добавлено
- [ ] `python3 -c "from wyoming_satellite.settings import EventSettings; print(EventSettings.websocket_enabled)"` работает
### ⏳ Этап WS-2: satellite.py (ожидается)
- [ ] `_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` проходит
### ⏳ Этап WS-3: __main__.py (ожидается)
- [ ] `--websocket-enabled` аргумент добавлен
- [ ] `--websocket-host` аргумент добавлен
- [ ] `--websocket-port` аргумент добавлен
- [ ] Передача настроек в `EventSettings()` настроена
- [ ] `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 |
---
**Конец документа**