Files
satellite/docs/development/websocket-server-plan.md
T
sirmrak b794f4cafb docs: обновить статус плана — готово к тестированию
- Статус: В разработке → 🔄 Тестирование
- Добавлено: WebSocket события работают (synthesize, audio-start/stop, played)
- Добавлено: TEST сервисы созданы
- Добавлено: Критический баг исправлен (detach_kernel_driver)
- Требуется тестирование: Audio level monitoring, WebSocket команды
2026-05-20 22:13:45 +03:00

34 KiB
Raw Blame History

План: WebSocket сервер для Wyoming Satellite

Дата: 2026-05-20 Статус: 🔄 Тестирование Ветка: satellite-dev Директория для разработки: ~/projects/satellite-dev Файл плана: docs/development/websocket-server-plan.md


Чеклист выполнения

✅ Выполнено

  • Ветка создана — satellite-dev (переименована из feature/multi-event-server)
  • Окружение настроено — .venv/ в корне проекта
  • Docker удалён — файлы Dockerfile, docker-compose.yml, скрипты
  • Лишние файлы удалены — test.wav, start.sh, start-dev.sh, .projectile
  • setup_dev.sh создан — быстрая настройка окружения
  • Этап WS-1: settings.py — websocket_enabled, websocket_host, websocket_port
  • Этап WS-2: satellite.py — WebSocket сервер + runtime атрибуты
  • Этап WS-2A: Протокол команд — handle_websocket_command() реализован
  • Этап WS-3: __main__.py — CLI аргументы --websocket-enabled/host/port
  • TEST сервисы — созданы для безопасного тестирования
  • Критический баг исправлен — убран detach_kernel_driver() (audio больше не отваливается)
  • WebSocket события — события приходят в браузер (synthesize, audio-start/stop, played)

🔄 Требуется тестирование

  • Этап WS-4: Audio level monitoring — проверить что audio-level события приходят (10 раз в секунду)
  • Этап WS-4: WebSocket команды — проверить set_volume, mute, play_sound

⏳ Ожидается

  • Этап WS-5: Документация — обновить docs/integration/mqtt-led.md
  • 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 Проверка окружения ✅

# Production версия (не трогать!)
cd ~/projects/Satellite
git status  # Должен быть clean

# Development версия
cd ~/projects/satellite-dev
git status  # Должен быть на satellite-dev

1.2 Настройка venv для satellite-dev ✅

# Автоматическая настройка
cd ~/projects/satellite-dev
./setup_dev.sh

# Или вручную:
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

1.3 Проверка что всё работает ✅

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 сервера:

@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:

# 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

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 сервер

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 Обработчик команд

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():

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():

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()

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 мин) ⏳ ОЖИДАЕТСЯ

Формат сообщений:

{
  "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

Примеры:

// Установить громкость 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"}
  }
}));

Ответы сервера:

Сервер отправляет события в формате:

{
  "type": "voice-started",
  "data": {}
}

Этап WS-3: Модификация wyoming_satellite/__main__.py (20 мин) ⏳ ОЖИДАЕТСЯ

3.1 Добавить CLI аргументы

После существующих аргументов (перед args = parser.parse_args()):

# 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(...):

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

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

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)

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 контроллера":

## 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

Подключение из браузера

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:

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

Добавить настройку:

@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:

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__():

self._last_mic_level: float = 0.0
self._audio_level_task: Optional[asyncio.Task] = None

Вычисление в event_from_mic():

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)
    
    # ... остальной код ...

Фоновая задача отправки:

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():

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 аргументы:

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)"
)

Передача в настройки:

event=EventSettings(
    # ... существующие поля ...
    
    # Audio level
    audio_level_enabled=args.audio_level_enabled,
    audio_level_interval_ms=args.audio_level_interval_ms,
)

6.5 Пример использования в браузере

<div id="level-meter" style="width: 100%; height: 20px; background: #333;">
  <div id="level-bar" style="width: 0%; height: 100%; background: #0f0;"></div>
</div>

<script>
const ws = new WebSocket('ws://localhost:8675');
const levelBar = document.getElementById('level-bar');

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  if (data.type === 'audio-level') {
    const percent = data.level * 100;
    levelBar.style.width = percent + '%';
    
    // Цвет от уровня
    if (data.level > 0.7) {
      levelBar.style.background = '#f00';  // Громко
    } else if (data.level > 0.3) {
      levelBar.style.background = '#ff0';  // Средне
    } else {
      levelBar.style.background = '#0f0';  // Тихо
    }
  }
};
</script>

6.6 Запуск с audio level

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 не меняется, проверять логи

План Б: Откат

Если что-то идёт не так:

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: Подготовка (выполнено)

  • Ветка создана — satellite-dev
  • Окружение настроено — .venv/ в корне проекта
  • setup_dev.sh создан
  • Лишние файлы удалены (Docker, test.wav, старые скрипты)
  • 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-2A: Протокол команд (выполнено)

  • handle_websocket_command() реализован
  • Команда set_volume работает
  • Команда mute/unmute работает
  • Команда play_sound работает
  • Команда set_led_* работает
  • Команда trigger_event работает
  • Обработка ошибок реализована

✅ Этап 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 клиенте

{
  "type": "voice-started",
  "data": {}
}
{
  "type": "voice-stopped",
  "data": {}
}
{
  "type": "transcript",
  "data": {
    "text": "привет дроид"
  }
}

B. Диагностика

# Проверка портов
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

Конец документа