30 KiB
План: Множественные подключения event сервисов к Wyoming Satellite
Дата: 2026-05-20
Статус: В разработке
Ветка: satellite-dev
Директория для разработки: ~/projects/satellite-dev
Чеклист выполнения
✅ Выполнено
- Ветка создана —
satellite-dev(переименована изfeature/multi-event-server) - Окружение настроено —
.venv/в корне проекта - Docker удалён — файлы Dockerfile, docker-compose.yml, скрипты
- Лишние файлы удалены — test.wav, start.sh, start-dev.sh, .projectile
- setup_dev.sh создан — быстрая настройка окружения
- Документация обновлена — plan.md актуализирован
🔄 В процессе
- Этап 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/
⏳ Ожидается
- Merge в master
- Production развёртывание
Стратегия разработки
Изоляция разработки
Текущая версия (production):
- Директория:
~/projects/Satellite - Ветка:
master - Статус: ✅ Рабочая версия, используется в production
Версия для разработки:
- Директория:
~/projects/satellite-dev - Ветка:
satellite-dev - Статус: 🚧 Экспериментальная версия
- Окружение:
.venv/в корне проекта
Преимущества такого подхода
- ✅ Полная изоляция — эксперименты не влияют на рабочую систему
- ✅ Быстрый откат — просто переключаемся на
~/projects/Satellite - ✅ Чистая история — merge в master только после тестирования
- ✅ Параллельная работа — можно продолжать фиксить баги в Satellite пока идёт разработка
Быстрый старт для разработки
cd ~/projects/satellite-dev
# Настройка окружения (один раз)
./setup_dev.sh
# Запуск для тестирования
./start_satellite.sh wake
Архитектура
Текущее состояние
┌─────────────────┐ ┌─────────────────┐
│ Home Assistant │ ──────► │ Satellite │
│ (Wyoming │ 10700 │ (Wyoming │
│ Client) │ │ Server) │
└─────────────────┘ └────────┬────────┘
│
│ 10500 (один клиент!)
▼
┌─────────────────┐
│ Event Service │
│ (клиент) │
└─────────────────┘
Проблема: Satellite поддерживает только одного event клиента через event.uri
Целевое состояние
┌─────────────────┐ ┌─────────────────┐
│ Home Assistant │ ──────► │ Satellite │
│ (Wyoming │ 10700 │ (Wyoming │
│ Client) │ │ Server) │
└─────────────────┘ └────────┬────────┘
│
┌───────────────┼───────────────┐
│ │ │
│ 10600 (event │ │
│ server) │ │
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌──────────┐
│ LED │ │ MQTT │ │ Logger │
│ Service │ │ Service │ │ Service │
└─────────┘ └──────────┘ └──────────┘
Решение: Event сервер на satellite для множественных подключений
Этапы реализации
Этап 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')"
Итого: Окружение готово, можно приступать к реализации.
Этап 2: Модификация wyoming_satellite/settings.py (1 час) 🔄 В ПРОЦЕССЕ
Добавить настройки event сервера:
@dataclass(frozen=True)
class EventSettings(ServiceSettings):
"""External event service settings."""
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 сервера."""
Итого изменений: ~20 строк
Этап 3: Модификация wyoming_satellite/satellite.py (3-4 часа) ⏳ ОЖИДАЕТСЯ
3.1 Добавить атрибуты для event сервера
В SatelliteBase.__init__() после существующих атрибутов:
# Event server (новый)
self._event_server: Optional[asyncio.Server] = None
self._event_clients: List[asyncio.StreamWriter] = []
self._event_clients_lock = asyncio.Lock()
Импорт: List из typing (если ещё не импортирован)
3.2 Метод для рассылки событий всем клиентам
После существующих методов добавить:
async def _broadcast_to_clients(self, event: Event) -> None:
"""
Отправить событие всем подключённым event клиентам.
Мёртвые подключения автоматически удаляются.
"""
async with self._event_clients_lock:
if not self._event_clients:
return
dead_clients = []
for i, writer in enumerate(self._event_clients):
try:
await async_write_event(event, writer)
except Exception:
_LOGGER.debug(
f"Failed to send event to client {i}, removing"
)
dead_clients.append(i)
# Удалить мёртвые подключения
for i in reversed(dead_clients):
self._event_clients.pop(i)
3.3 Заменить forward_event()
Было:
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)
Стало:
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 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)
3.4 Event сервер для подключений
Добавить методы после _event_task_proc():
async def start_event_server(self) -> None:
"""
Запустить сервер для 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)
try:
# Ждать отключения клиента
await reader.read(1)
except asyncio.IncompleteReadError:
pass # Нормальное отключение
except Exception as exc:
_LOGGER.debug(f"Event 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 def stop_event_server(self) -> None:
"""
Остановить event сервер и закрыть все подключения.
"""
_LOGGER.info("Stopping event server...")
if self._event_server:
self._event_server.close()
await self._event_server.wait_closed()
self._event_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")
3.5 Интеграция в жизненный цикл
В _start():
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()
self.state = State.STARTED
await self.started()
В _stop():
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()
await self._disconnect_from_services()
self._disable_ping()
self.state = State.STOPPED
Итого изменений: ~200 строк
Этап 4: Модификация wyoming_satellite/__main__.py (30 мин) ⏳ ОЖИДАЕТСЯ
4.1 Добавить CLI аргументы
После существующих аргументов (перед args = parser.parse_args()):
# Event server
parser.add_argument(
"--event-server-enabled",
action="store_true",
help="Enable event server for multiple event clients (default: disabled)"
)
parser.add_argument(
"--event-server-host",
default="0.0.0.0",
help="Host for event server (default: 0.0.0.0)"
)
parser.add_argument(
"--event-server-port",
type=int,
default=10600,
help="Port for event server (default: 10600)"
)
4.2 Передать настройки в SatelliteSettings
В существующем EventSettings(...):
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,
),
Итого изменений: ~30 строк
Этап 5: Тестовый event клиент (1 час) ⏳ ОЖИДАЕТСЯ
5.1 Создать examples/test_event_client.py
#!/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 Сделать исполняемым
chmod +x examples/test_event_client.py
Этап 6: Тестирование (2-3 часа) ⏳ ОЖИДАЕТСЯ
6.1 Запустить satellite с event сервером
cd ~/projects/Satellite
source ~/.venv_satellite/bin/activate
python3 -m wyoming_satellite \
--uri tcp://0.0.0.0:10700 \
--event-server-enabled \
--event-server-port 10600 \
--debug
Ожидаемый вывод:
INFO: Ready
INFO: Event server listening on 0.0.0.0:10600
INFO: Слушаем tcp://0.0.0.0:10700
6.2 Проверить подключения
Терминал 1: Home Assistant (порт 10700)
# Эмуляция подключения HA (или реальное HA)
# HA должен подключиться автоматически через Zeroconf
Терминал 2: test_event_client #1 (порт 10600)
source ~/.venv_satellite/bin/activate
python3 examples/test_event_client.py --uri tcp://127.0.0.1:10600
Терминал 3: test_event_client #2 (порт 10600)
source ~/.venv_satellite/bin/activate
python3 examples/test_event_client.py --uri tcp://127.0.0.1:10600
Терминал 4:原有 wyoming-led.py (порт 10500)
source ~/.venv_satellite/bin/activate
cd examples
python3 wyoming-led.py --host 127.0.0.1 --port 10500
6.3 Проверить логи
В логе satellite:
INFO: Event client connected: ('127.0.0.1', XXXX) # клиент 1
INFO: Event client connected: ('127.0.0.1', XXXX) # клиент 2
INFO: Connected to server # Home Assistant
Проверить:
- ✅ Все 3 клиента получают события
- ✅ Home Assistant продолжает работать (порт 10700)
- ✅ При отключении клиента satellite не падает
- ✅ wyoming-led.py работает через старый порт 10500
6.4 Тест на устойчивость
# Отключить клиента 1 (Ctrl+C)
# Проверить что клиент 2 продолжает работать
# Отключить wyoming-led.py
# Проверить что event клиенты работают
# Отключить Home Assistant
# Проверить что event клиенты работают
Этап 7: Обновление документации (30 мин) ⏳ ОЖИДАЕТСЯ
7.1 docs/integration/mqtt-led.md
Добавить секцию после "Архитектура USB контроллера":
## Event Server (множественные подключения)
Начиная с версии 2026.06, Wyoming Satellite поддерживает event server для
множественных подключений.
### Преимущества
- ✅ **Масштабируемость:** Неограниченное количество event клиентов
- ✅ **Надёжность:** Падение клиента не влияет на satellite
- ✅ **Гибкость:** Каждый клиент выбирает какие события обрабатывать
### Запуск
```bash
python3 -m wyoming_satellite \
--uri tcp://0.0.0.0:10700 \
--event-server-enabled \
--event-server-port 10600
Подключение клиента
from wyoming.client import AsyncClient
client = AsyncClient.from_uri("tcp://127.0.0.1:10600")
await client.connect()
while True:
event = await client.read_event()
# Обработка события
Обратная совместимость
Старый wyoming-led.py продолжает работать через --event-uri:
python3 -m wyoming_satellite \
--event-uri tcp://127.0.0.1:10500 \
--event-server-enabled \
--event-server-port 10600
Оба метода работают параллельно!
#### 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:
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
---
### Этап 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 строк |
Риски и план Б
| Риск | Вероятность | Влияние | Решение |
|---|---|---|---|
| Конфликт портов (10600 занят) | Низкая | Среднее | Проверка lsof -i :10600, смена порта |
| Утечка памяти (клиенты) | Средняя | Высокое | Логирование подключений, таймауты |
| Падение satellite | Низкая | Критичное | Тестирование в ветке, backup файлов |
| Home Assistant отключится | Низкая | Критичное | Порт 10700 не меняется, обратная совместимость |
| Event клиенты не получают события | Средняя | Среднее | Проверка _broadcast_to_clients(), логирование |
План Б: Откат
Если что-то идёт не так:
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')"работает
🔄 Этап 2: settings.py (в процессе)
server_enabled: boolдобавленоserver_host: strдобавленоserver_port: intдобавленоpython3 -c "from wyoming_satellite.settings import EventSettings; print(EventSettings.server_enabled)"работает
⏳ Этап 3: satellite.py (ожидается)
_event_server: Optional[asyncio.Server]добавлено_event_clients: List[asyncio.StreamWriter]добавлено_event_clients_lockдобавлено- Метод
_broadcast_to_clients()реализован - Метод
start_event_server()реализован - Метод
stop_event_server()реализован - Интеграция в
_start()выполнена - Интеграция в
_stop()выполнена python3 -m py_compile wyoming_satellite/satellite.pyпроходит
⏳ Этап 4: __main__.py (ожидается)
--event-server-enabledаргумент добавлен--event-server-hostаргумент добавлен--event-server-portаргумент добавлен- Передача настроек в
EventSettings()настроена python3 -m wyoming_satellite --helpпоказывает новые аргументы
⏳ Этап 5: test_event_client.py (ожидается)
examples/test_event_client.pyсоздан- Клиент подключается к порту 10600
- События отображаются в логе
- Обработка отключения сервера работает
⏳ Этап 6: Тестирование (ожидается)
- Satellite запускается с
--event-server-enabled - Multiple clients can connect (2+ клиентов)
- Все клиенты получают события
- Home Assistant работает (порт 10700)
- wyoming-led.py работает (порт 10500)
- При отключении клиента satellite не падает
- Утечек памяти нет (через 10 минут работы)
⏳ Этап 7: Документация (ожидается)
docs/integration/mqtt-led.mdобновленаdocs/deployment/systemd-services.mdобновлёнDEPLOY.mdобновлён
⏳ Перед merge
- Все тесты пройдены
- Документация актуальна
- Ветка называется
satellite-dev
Приложения
A. Пример события в event клиенте
{
"type": "voice-started",
"data": {}
}
{
"type": "voice-stopped",
"data": {}
}
{
"type": "transcript",
"data": {
"text": "привет дроид"
}
}
B. Диагностика
# Проверка портов
lsof -i :10600
lsof -i :10700
lsof -i :10500
# Проверка подключений
netstat -tlnp | grep python
# Логи satellite
journalctl -u wyoming-satelite-vosk.service -f
# Тест подключения
telnet 127.0.0.1 10600
C. Сравнение подходов
| Аспект | Старый (push) | Новый (pull) |
|---|---|---|
| Подключения | 1 satellite → 1 event | N clients → 1 satellite |
| Отказоустойчивость | Падение event → satellite работает | Падение client → satellite работает |
| Масштабируемость | Ограничено 1 сервисом | Неограниченно клиентов |
| Сложность | Проще | Сложнее (управление клиентами) |
| Гибкость | Низкая | Высокая (каждый клиент выбирает события) |
| Обратная совместимость | ✅ Работает | ✅ Работает через event.uri |
Конец документа