feat: Multi-event server support + USB LED controller
New features: - wyoming_satellite/xvf_usb.py — прямой USB доступ к XVF3800 без subprocess - docs/development/multi-event-server-plan.md — план разработки event server - examples/respeaker_usb_mic_array.py — исходный файл с LED анимациями Documentation updates: - DEPLOY.md — добавлена секция про LED управление и event server план - docs/integration/mqtt-led.md — обновлена документация по USB и event server - docs/deployment/systemd-services.md — добавлен event server раздел Refactoring: - examples/wyoming-led.py — замена subprocess на XVF3800Controller - install_service.sh — удалён (устарел, используется install_systemd_services.sh) Bug fixes: - wyoming-led.py — яркость DoA эффекта через масштабирование цвета - systemd сервис — добавлена поддержка wyoming_satellite.xvf_usb модуля
This commit is contained in:
+82
-52
@@ -7,7 +7,6 @@ Wyoming LED Control with MQTT Discovery for Home Assistant
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import subprocess
|
||||
import sys
|
||||
import signal
|
||||
import json
|
||||
@@ -22,6 +21,13 @@ from wyoming.snd import Played
|
||||
from wyoming.vad import VoiceStarted, VoiceStopped
|
||||
from wyoming.wake import Detection
|
||||
|
||||
# Импорт USB контроллера
|
||||
from wyoming_satellite.xvf_usb import (
|
||||
XVF3800Controller,
|
||||
EFFECT_OFF, EFFECT_BREATH, EFFECT_RAINBOW, EFFECT_DOA,
|
||||
rgb_to_bgr_int, scale_color
|
||||
)
|
||||
|
||||
# ====================== НАСТРОЙКИ ======================
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
@@ -31,7 +37,6 @@ logging.basicConfig(
|
||||
logger = logging.getLogger("XVF3800 LED Service")
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
XVF_HOST_PATH = str(SCRIPT_DIR / "xvf_host.py")
|
||||
CONFIG_FILE = SCRIPT_DIR / "led_brightness_config.json"
|
||||
|
||||
MQTT_BROKER = "192.168.200.100"
|
||||
@@ -68,6 +73,7 @@ mqtt_client = None
|
||||
current_state = "idle"
|
||||
last_mqtt_brightness = None
|
||||
ha_disabled = False
|
||||
led_controller: XVF3800Controller | None = None
|
||||
|
||||
|
||||
# ====================== CONFIG ======================
|
||||
@@ -94,15 +100,19 @@ def save_brightness_config():
|
||||
|
||||
|
||||
# ====================== LED ======================
|
||||
def run_xvf_command(command: str, values=None):
|
||||
args = [sys.executable, XVF_HOST_PATH, command]
|
||||
if values:
|
||||
args.extend(["--values"] + [str(v) for v in values])
|
||||
def init_leds() -> bool:
|
||||
"""Инициализация USB контроллера."""
|
||||
global led_controller
|
||||
try:
|
||||
subprocess.run(args, capture_output=True, text=True, check=True, timeout=5)
|
||||
return True
|
||||
led_controller = XVF3800Controller()
|
||||
if led_controller.is_connected():
|
||||
logger.info("✅ XVF3800 USB подключён")
|
||||
return True
|
||||
else:
|
||||
logger.warning("⚠️ XVF3800 не найден (USB)")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"❌ LED command error {command}: {e}")
|
||||
logger.error(f"❌ Ошибка инициализации LED: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -113,58 +123,74 @@ def get_effective_brightness(state: str) -> int:
|
||||
|
||||
|
||||
def set_led_state(state: str, skip_publish=False):
|
||||
"""Установка LED состояния через USB контроллер."""
|
||||
global current_state
|
||||
current_state = state
|
||||
brightness = get_effective_brightness(state)
|
||||
|
||||
if led_controller is None:
|
||||
logger.warning("⚠️ LED контроллер не инициализирован")
|
||||
if not skip_publish:
|
||||
publish_state(state)
|
||||
publish_satellite_state(state)
|
||||
return
|
||||
|
||||
if state == "listening":
|
||||
run_xvf_command("LED_EFFECT", [1])
|
||||
run_xvf_command("LED_COLOR", ["0x00BCD4"])
|
||||
run_xvf_command("LED_BRIGHTNESS", [brightness])
|
||||
run_xvf_command("LED_SPEED", [2])
|
||||
# Бирюзовое дыхание
|
||||
led_controller.set_led_effect(EFFECT_BREATH)
|
||||
led_controller.set_led_color(rgb_to_bgr_int(0, 188, 212)) # 0x00BCD4
|
||||
led_controller.set_led_brightness(brightness)
|
||||
led_controller.set_led_speed(2)
|
||||
logger.info(f"🔵 Listening: бирюзовое дыхание ({brightness})")
|
||||
|
||||
elif state == "wake":
|
||||
run_xvf_command("LED_EFFECT", [1])
|
||||
run_xvf_command("LED_COLOR", ["0xFF0000"])
|
||||
run_xvf_command("LED_BRIGHTNESS", [brightness])
|
||||
# Красное дыхание
|
||||
led_controller.set_led_effect(EFFECT_BREATH)
|
||||
led_controller.set_led_color(rgb_to_bgr_int(255, 0, 0)) # 0xFF0000
|
||||
led_controller.set_led_brightness(brightness)
|
||||
logger.info(f"🔴 Wake: красный ({brightness})")
|
||||
|
||||
elif state == "thinking":
|
||||
run_xvf_command("LED_EFFECT", [2])
|
||||
run_xvf_command("LED_BRIGHTNESS", [brightness])
|
||||
run_xvf_command("LED_SPEED", [4])
|
||||
# Радуга
|
||||
led_controller.set_led_effect(EFFECT_RAINBOW)
|
||||
led_controller.set_led_brightness(brightness)
|
||||
led_controller.set_led_speed(4)
|
||||
logger.info(f"🌈 Thinking: радуга ({brightness})")
|
||||
|
||||
elif state == "speaking":
|
||||
run_xvf_command("LED_EFFECT", [1])
|
||||
run_xvf_command("LED_COLOR", ["0x4CAF50"])
|
||||
run_xvf_command("LED_BRIGHTNESS", [brightness])
|
||||
# Зелёное дыхание
|
||||
led_controller.set_led_effect(EFFECT_BREATH)
|
||||
led_controller.set_led_color(rgb_to_bgr_int(76, 175, 80)) # 0x4CAF50
|
||||
led_controller.set_led_brightness(brightness)
|
||||
logger.info(f"🟢 Speaking: зелёный ({brightness})")
|
||||
|
||||
elif state == "idle":
|
||||
# DoA эффект: яркость управляется через значения цвета, а не LED_BRIGHTNESS
|
||||
# Масштабируем цвета по яркости (0-255)
|
||||
base_r = int(0x30 * brightness / 255)
|
||||
base_g = int(0x30 * brightness / 255)
|
||||
base_b = int(0x30 * brightness / 255)
|
||||
doa_r = int(0x00 * brightness / 255)
|
||||
doa_g = int(0xBC * brightness / 255)
|
||||
doa_b = int(0xD4 * brightness / 255)
|
||||
base_color = f"0x{base_b:02X}{base_g:02X}{base_r:02X}"
|
||||
doa_color = f"0x{doa_b:02X}{doa_g:02X}{doa_r:02X}"
|
||||
run_xvf_command("LED_EFFECT", [4])
|
||||
run_xvf_command("LED_DOA_COLOR", [base_color, doa_color])
|
||||
logger.info(f"⚪ Idle: DoA (brightness={brightness}, base={base_color}, doa={doa_color})")
|
||||
# DoA эффект: яркость управляется через значения цвета
|
||||
base_rgb = scale_color((0x30, 0x30, 0x30), brightness)
|
||||
doa_rgb = scale_color((0x00, 0xBC, 0xD4), brightness)
|
||||
base_color = rgb_to_bgr_int(*base_rgb)
|
||||
doa_color = rgb_to_bgr_int(*doa_rgb)
|
||||
led_controller.set_led_effect(EFFECT_DOA)
|
||||
led_controller.set_led_doa_color(base_color, doa_color)
|
||||
logger.info(f"⚪ Idle: DoA (brightness={brightness}, base=0x{base_color:06X}, doa=0x{doa_color:06X})")
|
||||
|
||||
elif state == "paused":
|
||||
run_xvf_command("LED_EFFECT", [1])
|
||||
run_xvf_command("LED_COLOR", ["0x404040"])
|
||||
run_xvf_command("LED_BRIGHTNESS", [brightness])
|
||||
# Серое дыхание
|
||||
led_controller.set_led_effect(EFFECT_BREATH)
|
||||
led_controller.set_led_color(rgb_to_bgr_int(64, 64, 64)) # 0x404040
|
||||
led_controller.set_led_brightness(brightness)
|
||||
logger.info(f"⚫ Paused: серый ({brightness})")
|
||||
|
||||
elif state == "error":
|
||||
run_xvf_command("LED_EFFECT", [1])
|
||||
run_xvf_command("LED_COLOR", ["0xFF00FF"])
|
||||
run_xvf_command("LED_BRIGHTNESS", [brightness])
|
||||
# Маджента дыхание
|
||||
led_controller.set_led_effect(EFFECT_BREATH)
|
||||
led_controller.set_led_color(rgb_to_bgr_int(255, 0, 255)) # 0xFF00FF
|
||||
led_controller.set_led_brightness(brightness)
|
||||
logger.info(f"🟣 Error: маджента ({brightness})")
|
||||
|
||||
elif state == "off":
|
||||
run_xvf_command("LED_EFFECT", [0])
|
||||
run_xvf_command("LED_BRIGHTNESS", [0])
|
||||
# Выключить
|
||||
led_controller.set_led_effect(EFFECT_OFF)
|
||||
logger.info("⬛ Off: выключено")
|
||||
|
||||
if not skip_publish:
|
||||
@@ -350,8 +376,8 @@ def on_mqtt_message(client, userdata, msg):
|
||||
ha_state = payload.upper()
|
||||
if ha_state == "OFF":
|
||||
ha_disabled = True
|
||||
run_xvf_command("LED_EFFECT", [0])
|
||||
run_xvf_command("LED_BRIGHTNESS", [0])
|
||||
if led_controller:
|
||||
led_controller.set_led_effect(EFFECT_OFF)
|
||||
publish_state("off")
|
||||
logger.info("⬛ LED OFF")
|
||||
elif ha_state == "ON":
|
||||
@@ -413,20 +439,24 @@ class LedEventHandler(AsyncEventHandler):
|
||||
|
||||
|
||||
def cleanup_leds():
|
||||
"""Очистка: выключаем LED и закрываем USB."""
|
||||
logger.info("🧹 Очистка: выключаем LED...")
|
||||
try:
|
||||
run_xvf_command("LED_EFFECT", [0])
|
||||
run_xvf_command("LED_BRIGHTNESS", [0])
|
||||
logger.info("✅ LED выключены")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка очистки: {e}")
|
||||
if led_controller:
|
||||
try:
|
||||
led_controller.set_led_effect(EFFECT_OFF)
|
||||
led_controller.close()
|
||||
logger.info("✅ LED выключены, USB закрыт")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка очистки: {e}")
|
||||
else:
|
||||
logger.info("ℹ️ LED контроллер не был инициализирован")
|
||||
|
||||
|
||||
async def main(args):
|
||||
logger.info(f"🚀 Запуск на {args.host}:{args.port}")
|
||||
load_brightness_config()
|
||||
init_leds()
|
||||
start_mqtt_client()
|
||||
run_xvf_command("VERSION")
|
||||
set_led_state("idle")
|
||||
|
||||
uri = f"tcp://{args.host}:{args.port}"
|
||||
|
||||
Reference in New Issue
Block a user