docs: переименовать план в websocket-server-plan.md

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-05-20 15:58:42 +03:00
co-authored by Qwen-Coder
parent 5d59246902
commit 6e06120b55
+656
View File
@@ -0,0 +1,656 @@
# План: 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 актуализирован
### 🔄 В процессе
- [ ] **Этап 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 |
---
**Конец документа**