Все настроено

This commit is contained in:
2026-05-14 10:19:01 +03:00
parent da3799abcd
commit 8f2ff703db
11 changed files with 1038 additions and 8 deletions
+9
View File
@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(/home/sirmrak/projects/.venv_satellite/bin/pip *)",
"Bash(find *)"
]
},
"$version": 4
}
+8
View File
@@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(/home/sirmrak/projects/.venv_satellite/bin/pip *)"
]
},
"$version": 4
}
+10
View File
@@ -0,0 +1,10 @@
{
"listening": 120,
"wake": 200,
"thinking": 150,
"speaking": 140,
"idle": 80,
"paused": 40,
"error": 255,
"global_multiplier": 0.6392156862745098
}
+520
View File
@@ -0,0 +1,520 @@
#!/usr/bin/env python3
"""
Controls the LEDs on the ReSpeaker Mic Array via xvf_host.py
С поддержкой MQTT Discovery для Home Assistant (без эхо-петли)
"""
import argparse
import asyncio
import logging
import subprocess
import os
import sys
import signal
import json
from functools import partial
from pathlib import Path
import paho.mqtt.client as mqtt
from wyoming.event import Event
from wyoming.satellite import (
SatelliteConnected,
SatelliteDisconnected,
StreamingStarted,
StreamingStopped,
)
from wyoming.server import AsyncEventHandler, AsyncServer
from wyoming.snd import Played
from wyoming.vad import VoiceStarted, VoiceStopped
from wyoming.wake import Detection
# 🔹 Настройка логирования
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
logger = logging.getLogger("XVF3800 LED Service via xvf_host")
# 🔹 Пути
SCRIPT_DIR = Path(__file__).resolve().parent
XVF_HOST_PATH = str(SCRIPT_DIR / "xvf_host.py")
CONFIG_FILE = SCRIPT_DIR / "led_brightness_config.json"
# 🔹 MQTT конфигурация
MQTT_BROKER = "192.168.200.100"
MQTT_PORT = 1883
MQTT_USER = "mqtt"
MQTT_PASS = "mqtt"
# 🔹 Разделённые топики (команды ≠ состояния)
MQTT_TOPIC_COMMAND = "wyoming/led/command" # HA → Скрипт (команды)
MQTT_TOPIC_BRIGHTNESS_SET = "wyoming/led/brightness/set" # HA → Скрипт (установка)
MQTT_TOPIC_BRIGHTNESS_STATE = "wyoming/led/brightness/state" # Скрипт → HA (отчёт)
MQTT_TOPIC_STATE = "wyoming/led/state" # Скрипт → HA (состояние)
MQTT_TOPIC_AVAILABILITY = "wyoming/led/availability" # Статус онлайн/офлайн
MQTT_CLIENT_ID = "wyoming-led-service"
# 🔹 MQTT Discovery
MQTT_DISCOVERY_PREFIX = "homeassistant"
MQTT_DISCOVERY_DEVICE = {
"identifiers": ["wyoming-led-repeaker"],
"name": "Wyoming LED ReSpeaker",
"manufacturer": "ReSpeaker",
"model": "XVF3800 USB 4-MIC Array",
"sw_version": "1.0",
"connections": [["mac", "unknown"]]
}
# 🔹 Глобальные настройки
BRIGHTNESS_CONFIG = {
"listening": 120, "wake": 200, "thinking": 150,
"speaking": 140, "idle": 80, "paused": 40, "error": 255,
"global_multiplier": 1.0
}
# 🔹 Глобальные переменные
shutdown_event = asyncio.Event()
mqtt_client = None
current_state = "idle"
last_mqtt_brightness = None # 🔹 Защита от эха: последнее значение из MQTT
# ============================================================
# 💾 Настройки яркости
# ============================================================
def load_brightness_config():
global BRIGHTNESS_CONFIG
if CONFIG_FILE.exists():
try:
with open(CONFIG_FILE, "r") as f:
saved = json.load(f)
BRIGHTNESS_CONFIG.update(saved)
logger.info(f"📂 Загружены настройки из {CONFIG_FILE}")
except Exception as e:
logger.warning(f"⚠️ Ошибка загрузки конфига: {e}")
else:
save_brightness_config()
def save_brightness_config():
try:
with open(CONFIG_FILE, "w") as f:
json.dump(BRIGHTNESS_CONFIG, f, indent=2)
logger.debug(f"💾 Сохранены настройки яркости")
except Exception as e:
logger.error(f"❌ Ошибка сохранения: {e}")
# ============================================================
# 🎛️ Управление 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])
try:
result = subprocess.run(args, capture_output=True, text=True, check=True, timeout=5)
if result.stdout.strip():
logger.debug(f"{command} output: {result.stdout.strip()}")
return True
except Exception as e:
logger.error(f"❌ Ошибка {command}: {e}")
return False
def get_effective_brightness(state: str) -> int:
base = BRIGHTNESS_CONFIG.get(state, 100)
multiplier = BRIGHTNESS_CONFIG.get("global_multiplier", 1.0)
return int(base * multiplier)
def set_led_state(state: str, skip_publish=False):
"""Устанавливает состояние LED. Если skip_publish=True — не публикуем в MQTT (избегаем эха)"""
global current_state
current_state = state
brightness = get_effective_brightness(state)
if state == "listening":
run_xvf_command("LED_EFFECT", [0])
run_xvf_command("LED_EFFECT", [1])
run_xvf_command("LED_COLOR", ["0x00BCD4"])
run_xvf_command("LED_BRIGHTNESS", [brightness])
run_xvf_command("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])
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])
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])
run_xvf_command("LED_SPEED", [2])
logger.info(f"🟢 Speaking: зелёный ({brightness})")
elif state == "idle":
run_xvf_command("LED_EFFECT", [4])
run_xvf_command("LED_DOA_COLOR", ["0x303030", "0x00BCD4"])
run_xvf_command("LED_BRIGHTNESS", [brightness])
logger.info(f"⚪ Idle: DoA ({brightness})")
elif state == "paused":
run_xvf_command("LED_EFFECT", [1])
run_xvf_command("LED_COLOR", ["0x404040"])
run_xvf_command("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])
logger.info(f"🟣 Error: маджента ({brightness})")
elif state == "off":
run_xvf_command("LED_EFFECT", [0])
run_xvf_command("LED_BRIGHTNESS", [0])
logger.info("⬛ Off: выключено")
else:
run_xvf_command("LED_EFFECT", [0])
logger.info("⬛ Unknown")
# 🔹 Публикуем состояние только если не скипаем
if not skip_publish:
publish_state(state, brightness)
# ============================================================
# 🔍 MQTT Discovery
# ============================================================
def publish_discovery_config():
if not mqtt_client:
return
try:
# 🔹 Number (слайдер) — читаем из STATE, пишем в SET
discovery_number = {
"name": "Wyoming LED Brightness",
"unique_id": "wyoming_led_brightness",
"state_topic": MQTT_TOPIC_BRIGHTNESS_STATE, # ← Читаем отчёт
"command_topic": MQTT_TOPIC_BRIGHTNESS_SET, # ← Пишем команду
"min": 0, "max": 255,
"icon": "mdi:brightness-6",
"availability_topic": MQTT_TOPIC_AVAILABILITY,
"payload_available": "online",
"payload_not_available": "offline",
"device": MQTT_DISCOVERY_DEVICE,
"object_id": "wyoming_led_brightness"
}
# 🔹 Sensor (состояние)
discovery_sensor = {
"name": "Wyoming LED State",
"unique_id": "wyoming_led_state",
"state_topic": MQTT_TOPIC_STATE,
"value_template": "{{ value_json.state }}",
"icon": "mdi:led-strip",
"availability_topic": MQTT_TOPIC_AVAILABILITY,
"payload_available": "online",
"payload_not_available": "offline",
"device": MQTT_DISCOVERY_DEVICE,
"json_attributes_topic": MQTT_TOPIC_STATE,
"object_id": "wyoming_led_state"
}
# 🔹 Light (полноценная лампа)
discovery_light = {
"name": "Wyoming LED",
"unique_id": "wyoming_led_light",
"state_topic": MQTT_TOPIC_STATE,
"command_topic": MQTT_TOPIC_COMMAND,
"brightness_state_topic": MQTT_TOPIC_BRIGHTNESS_STATE,
"brightness_command_topic": MQTT_TOPIC_BRIGHTNESS_SET,
"brightness_scale": 255,
"availability_topic": MQTT_TOPIC_AVAILABILITY,
"payload_available": "online",
"payload_not_available": "offline",
"payload_on": '{"type": "set_state", "state": "speaking"}',
"payload_off": '{"type": "set_state", "state": "idle"}',
"device": MQTT_DISCOVERY_DEVICE,
"object_id": "wyoming_led",
"schema": "json"
}
# Публикуем с retain=True для автообнаружения
mqtt_client.publish(f"{MQTT_DISCOVERY_PREFIX}/number/wyoming_led/config",
json.dumps(discovery_number), retain=True)
mqtt_client.publish(f"{MQTT_DISCOVERY_PREFIX}/sensor/wyoming_led/config",
json.dumps(discovery_sensor), retain=True)
mqtt_client.publish(f"{MQTT_DISCOVERY_PREFIX}/light/wyoming_led/config",
json.dumps(discovery_light), retain=True)
logger.info("🔍 MQTT Discovery: конфигурации опубликованы")
except Exception as e:
logger.error(f"❌ Ошибка Discovery: {e}")
# ============================================================
# 📡 MQTT функции
# ============================================================
def on_mqtt_connect(client, userdata, flags, rc, properties=None):
if rc == 0:
logger.info(f"✅ MQTT подключён к {MQTT_BROKER}:{MQTT_PORT}")
client.publish(MQTT_TOPIC_AVAILABILITY, "online", retain=True)
# 🔹 Подписываемся ТОЛЬКО на командные топики (не на state!)
client.subscribe(MQTT_TOPIC_COMMAND)
client.subscribe(MQTT_TOPIC_BRIGHTNESS_SET)
publish_discovery_config()
logger.info(f"📡 Подписки: {MQTT_TOPIC_COMMAND}, {MQTT_TOPIC_BRIGHTNESS_SET}")
else:
logger.error(f"❌ MQTT ошибка подключения: {rc}")
def on_mqtt_disconnect(client, userdata, rc, properties=None):
logger.warning(f"⚠️ MQTT отключён (код: {rc})")
def on_mqtt_message(client, userdata, msg):
"""Обработчик входящих MQTT сообщений"""
global last_mqtt_brightness
try:
topic = msg.topic
payload = msg.payload.decode('utf-8')
logger.debug(f"📩 MQTT {topic}: {payload}")
# 🔹 Обработка яркости (только из _SET топика!)
if topic == MQTT_TOPIC_BRIGHTNESS_SET:
brightness = int(payload)
brightness = max(0, min(255, brightness))
# 🔹 Защита от эха: если значение не изменилось — игнорируем
if brightness == last_mqtt_brightness:
logger.debug(f"⏭️ Эхо: яркость {brightness} уже обработана")
return
last_mqtt_brightness = brightness
BRIGHTNESS_CONFIG["global_multiplier"] = brightness / 255.0
save_brightness_config()
logger.info(f"🔆 Яркость: {brightness} (×{BRIGHTNESS_CONFIG['global_multiplier']:.2f})")
# 🔹 Применяем к текущему состоянию, но НЕ публикуем обратно (skip_publish=True)
set_led_state(current_state, skip_publish=True)
# 🔹 Публикуем отчёт в отдельный _STATE топик (без эха)
publish_brightness(brightness)
# 🔹 Обработка команд
elif topic == MQTT_TOPIC_COMMAND:
command = json.loads(payload)
cmd_type = command.get("type", "")
if cmd_type == "set_brightness":
brightness = max(0, min(255, command.get("value", 100)))
if brightness != last_mqtt_brightness:
last_mqtt_brightness = brightness
BRIGHTNESS_CONFIG["global_multiplier"] = brightness / 255.0
save_brightness_config()
publish_brightness(brightness)
set_led_state(current_state, skip_publish=True)
logger.info(f"🔆 Яркость из команды: {brightness}")
elif cmd_type == "set_state":
state = command.get("state", "idle")
set_led_state(state, skip_publish=False) # Можно публиковать, это новое состояние
logger.info(f"🎨 Состояние: {state}")
elif cmd_type == "set_effect":
run_xvf_command("LED_EFFECT", [command.get("effect", 0)])
run_xvf_command("LED_COLOR", [command.get("color", "0x000000")])
run_xvf_command("LED_BRIGHTNESS", [command.get("brightness", 100)])
logger.info(f"🎨 Эффект применён")
except json.JSONDecodeError:
logger.warning(f"⚠️ Неверный JSON: {payload}")
except Exception as e:
logger.error(f"❌ Ошибка MQTT: {e}")
def publish_state(state: str, brightness: int):
"""Публикует состояние в топик состояния"""
if mqtt_client:
try:
payload = json.dumps({
"state": state,
"brightness": brightness,
"timestamp": asyncio.get_event_loop().time()
})
mqtt_client.publish(MQTT_TOPIC_STATE, payload, retain=False) # ← Не retain!
except Exception as e:
logger.debug(f"⚠️ Ошибка публикации state: {e}")
def publish_brightness(brightness: int):
"""Публикует яркость в топик состояния (отчёт, не команда!)"""
if mqtt_client:
try:
# 🔹 Публикуем в _STATE, а не в _SET! И без retain, чтобы не триггерить эхо
mqtt_client.publish(MQTT_TOPIC_BRIGHTNESS_STATE, str(brightness), retain=False)
except Exception as e:
logger.debug(f"⚠️ Ошибка публикации brightness: {e}")
def start_mqtt_client():
global mqtt_client
mqtt_client = mqtt.Client(
callback_api_version=mqtt.CallbackAPIVersion.VERSION2,
client_id=MQTT_CLIENT_ID,
protocol=mqtt.MQTTv311
)
mqtt_client.username_pw_set(MQTT_USER, MQTT_PASS)
mqtt_client.on_connect = on_mqtt_connect
mqtt_client.on_disconnect = on_mqtt_disconnect
mqtt_client.on_message = on_mqtt_message
mqtt_client.will_set(MQTT_TOPIC_AVAILABILITY, "offline", retain=True, qos=1)
try:
mqtt_client.connect(MQTT_BROKER, MQTT_PORT, keepalive=60)
mqtt_client.loop_start()
logger.info("📡 MQTT клиент запущен")
except Exception as e:
logger.error(f"❌ Ошибка подключения MQTT: {e}")
return mqtt_client
def stop_mqtt_client():
global mqtt_client
if mqtt_client:
try:
mqtt_client.publish(MQTT_TOPIC_AVAILABILITY, "offline", retain=True)
mqtt_client.loop_stop()
mqtt_client.disconnect()
logger.info("📡 MQTT остановлен")
except Exception as e:
logger.error(f"❌ Остановка MQTT: {e}")
# ============================================================
# 🧹 Очистка
# ============================================================
def cleanup_leds():
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}")
# ============================================================
# 🎯 Wyoming Handler
# ============================================================
class LedEventHandler(AsyncEventHandler):
async def handle_event(self, event: Event) -> bool:
event_type = event.type
logger.debug(f"🔍 Событие: {event_type}")
if Detection.is_type(event_type):
logger.info("🎯 Detection → wake")
set_led_state("wake")
elif VoiceStarted.is_type(event_type):
logger.info("🎤 VoiceStarted → listening")
set_led_state("listening")
elif VoiceStopped.is_type(event_type):
logger.info("💭 VoiceStopped → thinking")
set_led_state("thinking")
elif event_type == "audio-start":
logger.info("🔊 audio-start → speaking")
set_led_state("speaking")
elif Played.is_type(event_type):
logger.info("✅ Played → idle")
set_led_state("idle")
elif event_type in ["transcribe", "transcript", "synthesize", "intent"]:
logger.debug(f"🧠 {event_type} → thinking")
elif event_type == "pause-satellite":
logger.info("⏸️ pause → paused")
set_led_state("paused")
elif SatelliteConnected.is_type(event_type) or event_type == "satellite-connected":
logger.info("🔌 connected → idle")
set_led_state("idle")
elif event_type == "error":
logger.warning("⚠️ error")
set_led_state("error")
elif event_type in ["run-pipeline", "run-satellite", "streaming-started",
"streaming-stopped", "audio-stop", "satellite-disconnected"]:
logger.debug(f"⏭️ {event_type} — игнор")
else:
logger.debug(f"❓ Неизвестно: {event_type}")
return True
async def on_connected(self) -> None:
logger.info("✅ TCP подключён")
async def on_disconnected(self) -> None:
logger.info("❌ TCP отключён")
set_led_state("idle")
# ============================================================
# 🚀 Main
# ============================================================
async def main(args):
logger.info(f"🚀 Запуск на {args.host}:{args.port}")
load_brightness_config()
start_mqtt_client()
run_xvf_command("VERSION")
set_led_state("idle")
uri = f"tcp://{args.host}:{args.port}"
server = AsyncServer.from_uri(uri)
logger.info(f"📡 Слушаем {uri}")
await server.run(partial(LedEventHandler))
def signal_handler(sig, frame):
logger.info(f"\n🛑 Сигнал {sig}")
cleanup_leds()
stop_mqtt_client()
sys.exit(0)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="LED service with MQTT Discovery (no echo)")
parser.add_argument("--host", default="127.0.0.1")
parser.add_argument("--port", type=int, default=10701)
args = parser.parse_args()
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
try:
logger.info("💡 Ctrl+C для остановки")
asyncio.run(main(args))
except KeyboardInterrupt:
logger.info("⌨️ KeyboardInterrupt")
cleanup_leds()
stop_mqtt_client()
except Exception as e:
logger.error(f"❌ Ошибка: {e}")
cleanup_leds()
stop_mqtt_client()
sys.exit(1)
finally:
logger.info("👋 Завершено")
+412
View File
@@ -0,0 +1,412 @@
import sys
import struct
import argparse
import usb.core
import usb.util
import libusb_package
import time
CONTROL_SUCCESS = 0
SERVICER_COMMAND_RETRY = 64
# name, resid, cmdid, length, type, description
PARAMETERS = {
# APPLICATION_SERVICER_RESID commands
"VERSION": (48, 0, 3, "ro", "uint8", "The version number of the firmware. The format is VERSION_MAJOR VERSION_MINOR VERSION_PATCH"),
"BLD_MSG": (48, 1, 50, "ro", "char", "Retrieve the build message built in to the firmware, normally the build configuration name"),
"BLD_HOST": (48, 2, 30, "ro", "char", "Retrieve details of the CI build host used to build the firmware"),
"BLD_REPO_HASH": (48, 3, 40, "ro", "char", "Retrieve the GIT hash of the sw_xvf3800 repo used to build the firmware"),
"BLD_MODIFIED": (48, 4, 6, "ro", "char", "Show whether or not the current firmware repo has been modified from the official release. Requires use of a GIT repo"),
"BOOT_STATUS": (48, 5, 3, "ro", "char", "Shows whether or not the firmware has been booted via SPI or JTAG/FLASH"),
"TEST_CORE_BURN": (48, 6, 1, "rw", "uint8", "Set to enable core burn to exercise worst case timing. This will reboot the chip, reset all parameters to default and significantly increase power consumption."),
"REBOOT": (48, 7, 1, "wo", "uint8", "Set to any value to reboot the chip and reset all parameters to default."),
"USB_BIT_DEPTH": (48, 8, 2, "rw", "uint8", "Only relevant for the UA device variant. For the UA device, set or get the USB bit depth IN, OUT to either 16, 24 or 32. Setting will reboot the chip, resetting all other parameters to default. If issued to the INT device, a set is ignored and the device is not rebooted while a get always returns 0 as both IN and OUT bit depths."),
"SAVE_CONFIGURATION": (48, 9, 1, "wo", "uint8", "Set to any value to save the current configuration to flash."),
"CLEAR_CONFIGURATION": (48, 10, 1, "wo", "uint8", "Set to any value to clear the current configuration and revert to the default configuration."),
# AEC_RESID commands
"SHF_BYPASS": (33, 70, 1, "rw", "uint8", "AEC bypass"),
"AEC_NUM_MICS": (33, 71, 1, "ro", "int32", "Number of microphone inputs into the AEC"),
"AEC_NUM_FARENDS": (33, 72, 1, "ro", "int32", "Number of farend inputs into the AEC"),
"AEC_MIC_ARRAY_TYPE": (33, 73, 1, "ro", "int32", "Microphone array type (1 - linear, 2 - squarecular)"),
"AEC_MIC_ARRAY_GEO": (33, 74, 12, "ro", "float", "Microphone array geometry. Each microphone is represented by 3 XYZ coordinates in m."),
"AEC_AZIMUTH_VALUES": (33, 75, 4, "ro", "radians", "Azimuth values in radians - beam 1, beam 2, free-running beam, 3 - auto-select beam"),
"TEST_AEC_DISABLE_CONTROL": (33, 76, 1, "wo", "uint32", "Disable Control processing in the AEC resource. Only to be used for internal testing! Do not use in normal operation. When disabled, there's no way to bring back control without restarting the device."),
"AEC_CURRENT_IDLE_TIME": (33, 77, 1, "ro", "uint32", "AEC processing current idle time in 10ns ticks"),
"AEC_MIN_IDLE_TIME": (33, 78, 1, "ro", "uint32", "AEC processing minimum idle time in 10ns ticks"),
"AEC_RESET_MIN_IDLE_TIME": (33, 79, 1, "wo", "uint32", "Reset the AEC minimum idle time. The value of the argument passed is ignored."),
"AEC_SPENERGY_VALUES": (33, 80, 4, "ro", "float", "Speech energy level values for each beam. Any value above 0 indicates speech. Higher values indicates louder or closer speech, however noise, echo and reverb can cause the energy level to decrease. 0 - beam 1, 1 - beam 2, 2 - free-running beam, 3 - auto-select beam"),
"AEC_FIXEDBEAMSAZIMUTH_VALUES": (33, 81, 2, "rw", "radians", "Azimuth values in radians for beams in fixed mode - fixed beam 1, fixed beam 2"),
"AEC_FIXEDBEAMSELEVATION_VALUES": (33, 82, 2, "rw", "radians", "Elevation angles in radians for the beams in fixed mode. 0 - fixed beam 1, 1 - fixed beam 2"),
"AEC_FIXEDBEAMSGATING": (33, 83, 1, "rw", "uint8", "Enables/disables gating for beams in fixed mode. When enabled, speech energy is used to determine which beam should be active; inactive beams are silenced."),
"SPECIAL_CMD_AEC_FAR_MIC_INDEX": (33, 90, 2, "wo", "int32", "Set Far and Mic index for AEC filter read. Also used as a trigger command that starts a new filter read"),
"SPECIAL_CMD_AEC_FILTER_COEFF_START_OFFSET": (33, 91, 1, "rw", "int32", "start offset to get or set the filter coefficients from"),
"SPECIAL_CMD_AEC_FILTER_COEFFS": (33, 92, 15, "rw", "float", "get or set AEC filter coefficients"),
"SPECIAL_CMD_AEC_FILTER_LENGTH": (33, 93, 1, "ro", "int32", "get AEC filter length"),
"AEC_FILTER_CMD_ABORT": (33, 94, 1, "wo", "int32", "Reset the special command state machine. Used for safely exiting from an AEC filter read/write command sequence that has gone wrong."),
"AEC_AECPATHCHANGE": (33, 0, 1, "ro", "int32", "AEC Path Change Detection. Valid range: 0,1 (false,true)"),
"AEC_HPFONOFF": (33, 1, 1, "rw", "int32", "High-pass Filter on microphone signals. Valid range: 0,1,2,3,4 (0:Off, 1:on70, 2:on125, 3:on150, 4:on180)"),
"AEC_AECSILENCELEVEL": (33, 2, 2, "rw", "float", "Power threshold for signal detection in adaptive filter.(set,cur), Valid range (set): [0.0 .. 1.0] Valid range (cur): 0.05*set, 1e-6f or set."),
"AEC_AECCONVERGED": (33, 3, 1, "ro", "int32", "Flag indicating whether AEC is converged. Valid range: 0,1 (false,true)"),
"AEC_AECEMPHASISONOFF": (33, 4, 1, "rw", "int32", "Pre-emphasis and de-emphasis filtering for AEC. Valid range: 0,1,2 (off,on,on_eq)on: Emphasis filter for speech signals without modification of far-end ref. on_eq: Emphasis filter for far-end reference signals where the low frequencies are boosted by e.g. an equalizer."),
"AEC_FAR_EXTGAIN": (33, 5, 1, "rw", "float", "External gain in dB applied to the far-end reference signals. Valid range: [-inf .. inf]"),
"AEC_PCD_COUPLINGI": (33, 6, 1, "rw", "float", "Sensitivity parameter for PCD. Valid range: [0.0 .. 1.0]PCD can be disabled by setting a value outside the range"),
"AEC_PCD_MINTHR": (33, 7, 1, "rw", "float", "Minimum threshold value used in PCD. Valid range: [0.0 .. 0.02]"),
"AEC_PCD_MAXTHR": (33, 8, 1, "rw", "float", "Maximum threshold value used in PCD. Valid range: [0.025 .. 0.2]"),
"AEC_RT60": (33, 9, 1, "ro", "float", "Current RT60 estimate. Valid range: [0.250 .. 0.900] (seconds) A negative value indicates that the RT60 estimation is invalid."),
"AEC_ASROUTONOFF": (33, 35, 1, "rw", "int32", "Enables the automatic speech recognition (ASR) output: if set to 0, the AEC residuals are output, one channel per microphone, if set to 1, the ASR processed output is used, where each channel is associated with a beam from the beamformer. Valid range: 0,1 (off, on)"),
"AEC_ASROUTGAIN": (33, 36, 1, "rw", "float", "Fixed gain applied to qsr output, i.e., only applied if ASROUTONOFF==1.. Valid range: [0.0 .. 1000.0] (linear gain factor)"),
"AEC_FIXEDBEAMSONOFF": (33, 37, 1, "rw", "int32", "Enables or disables fixed focused beam mode. Valid range: 0,1 (off, on)"),
"AEC_FIXEDBEAMNOISETHR": (33, 38, 2, "rw", "float", "Threshold value for updating the noise canceller when fixed beam mode is enabled. A higher value indicates that the noise canceller may update when the free running beam is close to the fixed beam. A lower value indicates that the noise canceller may update when the free running beam is further away from the focused beam.[BECLEAR_NUMBER_OF_BEAMS], Valid range: [0.0 .. 1.0]"),
# AUDIO_MGR_RESID commands
"AUDIO_MGR_MIC_GAIN": (35, 0, 1, "rw", "float", "Audio Mgr pre SHF microphone gain"),
"AUDIO_MGR_REF_GAIN": (35, 1, 1, "rw", "float", "Audio Mgr pre SHF reference gain"),
"AUDIO_MGR_CURRENT_IDLE_TIME": (35, 2, 1, "ro", "int32", "Get audio manager current idle time in 10ns ticks"),
"AUDIO_MGR_MIN_IDLE_TIME": (35, 3, 1, "ro", "int32", "Get audio manager min idle time in 10ns ticks"),
"AUDIO_MGR_RESET_MIN_IDLE_TIME": (35, 4, 1, "wo", "int32", "Reset audio manager min idle time. The value of the argument passed is ignored."),
"MAX_CONTROL_TIME": (35, 5, 1, "ro", "int32", "Get audio manager max control time"),
"RESET_MAX_CONTROL_TIME": (35, 6, 1, "wo", "int32", "Reset audio manager max control time. The value of the argument passed is ignored."),
"I2S_CURRENT_IDLE_TIME": (35, 7, 1, "ro", "int32", "Get I2S current idle time in 10ns ticks"),
"I2S_MIN_IDLE_TIME": (35, 8, 1, "ro", "int32", "Get I2S min idle time in 10ns ticks"),
"I2S_RESET_MIN_IDLE_TIME": (35, 9, 1, "wo", "int32", "I2S reset idle time. The value of the argument passed is ignored."),
"I2S_INPUT_PACKED": (35, 10, 1, "rw", "uint8", "Will expect packed input on both I2S or USB channels if this is not 0. Note, could take up to 3 samples to take effect."),
"AUDIO_MGR_SELECTED_AZIMUTHS": (35, 11, 2, "ro", "radians", "This returns the azimuths determined by the beam_selection function in post_shf_dsp.c. The default implementation will provide a processed DoA at index 0 and the DoA of the auto select beam at index 1. The processed DoA uses speech energy to select from the DoA on each fixed beam to provide the direction of a speaker. NAN will be returned if neither of the fixed beams contain speech. Versions 2.0.0 and earlier of XVF3800 had a different default implementation of beam_selection which returned the DoA associated with the AUDIO_MGR_SELECTED_CHANNELS"),
"AUDIO_MGR_SELECTED_CHANNELS": (35, 12, 2, "rw", "uint8", "Default implementation of post processing will use this to select which channels should be output to MUX_USER_CHOSEN_CHANNELS. Note that a customer implementation of the beam selection stage could override this command. How this channel selection aligns with actual output depends on the mux configuration"),
"AUDIO_MGR_OP_PACKED": (35, 13, 2, "rw", "uint8", "<L>, <R>; Sets/gets packing status for L and R output channels"),
"AUDIO_MGR_OP_UPSAMPLE": (35, 14, 2, "rw", "uint8", "<L>, <R>; Sets/gets upsample status for L and R output channels, where appropriate"),
"AUDIO_MGR_OP_L": (35, 15, 2, "rw", "uint8", "<category>, <source>; Sets category and source for L output channel. Equivalent to AUDIO_MGR_OP_L_PK0"),
"AUDIO_MGR_OP_L_PK0": (35, 16, 2, "rw", "uint8", "<category>, <source>; Sets category and source for first (of three) sources on the L channel in packed mode. Equivalent to AUDIO_MGR_OP_L"),
"AUDIO_MGR_OP_L_PK1": (35, 17, 2, "rw", "uint8", "Sets category and source for second (of three) sources on the L channel in packed mode"),
"AUDIO_MGR_OP_L_PK2": (35, 18, 2, "rw", "uint8", "Sets category and source for third (of three) sources on the L channel in packed mode"),
"AUDIO_MGR_OP_R": (35, 19, 2, "rw", "uint8", "<category>, <source>; Sets category and source for R output channel. Equivalent to AUDIO_MGR_OP_R_PK0"),
"AUDIO_MGR_OP_R_PK0": (35, 20, 2, "rw", "uint8", "<category>, <source>; Sets category and source for first (of three) sources on the R channel in packed mode. Equivalent to AUDIO_MGR_OP_R"),
"AUDIO_MGR_OP_R_PK1": (35, 21, 2, "rw", "uint8", "Sets category and source for second (of three) sources on the R channel in packed mode"),
"AUDIO_MGR_OP_R_PK2": (35, 22, 2, "rw", "uint8", "Sets category and source for third (of three) sources on the R channel in packed mode"),
"AUDIO_MGR_OP_ALL": (35, 23, 12, "rw", "uint8", "Sets category and source for all 3 sources on L channel and all 3 sources for R channel. Equivalent to AUDIO_MGR_OP_[L,R]_PK[0,1,2] with successive pairs of arguments"),
"I2S_INACTIVE": (35, 24, 1, "ro", "uint8", "Returns whether the main audio loop is exchanging samples with I2S (0). If not (1), I2S is inactive"),
"AUDIO_MGR_FAR_END_DSP_ENABLE": (35, 25, 1, "rw", "uint8", "Enables/disables XVF3800 far-end DSP (if implemented). Write a 1 to enable, 0 to disable"),
"AUDIO_MGR_SYS_DELAY": (35, 26, 1, "rw", "int32", "Delay, measured in samples, that is applied to the reference signal before passing to SHF algorithm"),
"I2S_DAC_DSP_ENABLE": (35, 27, 1, "rw", "uint8", "Indicates if the DAC performs DSP on the far-end reference signal. If enabled (1), use the I2S signal as input to the audio pipeline, even in the UA configuration"),
# GPO_SERVICER_RESID commands
"GPO_READ_VALUES": (20, 0, 5, "ro", "uint8", "Get current logic level of all GPO pins, in order of Pin X0D11, X0D30, X0D31, X0D33 and X0D39."),
"GPO_WRITE_VALUE": (20, 1, 2, "wo", "uint8", "Set current logic level of selected GPO pin. Supports Pin X0D11, X0D30, X0D31, X0D33 and X0D39."),
"GPO_PORT_PIN_INDEX": (20, 2, 2, "rw", "uint32", "GPO port index and pin index that the following commands would be directed to"),
"GPO_PIN_VAL": (20, 3, 3, "wo", "uint8", "value to write to one pin of a GPO port. Payload specifies port_index, pin_index and value to write to the pin"),
"GPO_PIN_ACTIVE_LEVEL": (20, 4, 1, "rw", "uint32", "Active level of the port/pin specified by the GPO_PORT_PIN_INDEX command. 1 = ACTIVE_HIGH, 0 = ACTIVE_LOW"),
"LED_EFFECT": (20, 12, 1, "rw", "uint8", "Set the LED effect mode, 0 = off, 1 = breath, 2 = rainbow, 3 = single color, 4 = doa, 5 = ring"),
"LED_BRIGHTNESS": (20, 13, 1, "rw", "uint8", "Set the brightness value of the LED for the breath and rainbow mode"),
"LED_GAMMIFY": (20, 14, 1, "rw", "uint8", "Set to enable gamma correction, 0 = disable, 1 = enable"),
"LED_SPEED": (20, 15, 1, "rw", "uint8", "Set the effect speed of breath and rainbow mode"),
"LED_COLOR": (20, 16, 1, "rw", "uint32", "Set the LED color of breath mode and single color mode"),
"LED_DOA_COLOR": (20, 17, 2, "rw", "uint32", "Set the LED color of doa mode, the first value is the base color and the second value is the doa color"),
"DOA_VALUE": (20, 18, 2, "ro", "uint16", "Get the doa value and if speech is detected, payload[0] = doa value, from 0 to 359, payload[1] = 1 if speech is detected, 0 if not"),
"LED_RING_COLOR": (20, 19, 12, "rw", "uint32", "Set the LED color of ring mode, each value is the color of one LED"),
# PP_RESID commands
"PP_CURRENT_IDLE_TIME": (17, 70, 1, "ro", "uint32", "PP processing current idle time in 10ns ticks"),
"PP_MIN_IDLE_TIME": (17, 71, 1, "ro", "uint32", "PP processing minimum idle time in 10ns ticks"),
"PP_RESET_MIN_IDLE_TIME": (17, 72, 1, "wo", "uint32", "Reset the PP minimum idle time. The value of the argument passed is ignored."),
"PP_NL_MODEL_CMD_ABORT": (17, 94, 1, "wo", "int32", "Reset the special command state machine. Used for safely exiting from a NL model read/write sequence that has gone wrong."),
"PP_EQUALIZATION_CMD_ABORT": (17, 100, 1, "wo", "int32", "Reset the special command state machine. Used for safely exiting from an equalization read/write sequence that has gone wrong."),
"PP_AGCONOFF": (17, 10, 1, "rw", "int32", "Automatic Gain Control. Valid range: 0,1 (off,on)"),
"PP_AGCMAXGAIN": (17, 11, 1, "rw", "float", "Maximum AGC gain factor. Valid range: [1.0 .. 1000.0] (linear gain factor)"),
"PP_AGCDESIREDLEVEL": (17, 12, 1, "rw", "float", "Target power level of the output signal. Valid range: [1e-8 .. 1.0] (power level)"),
"PP_AGCGAIN": (17, 13, 1, "rw", "float", "Current AGC gain factor. Valid range: [1.0 .. 1000.0] (linear gain factor)"),
"PP_AGCTIME": (17, 14, 1, "rw", "float", "Ramp-up/down time-constant. Valid range: [0.5 .. 4.0] (seconds)"),
"PP_AGCFASTTIME": (17, 15, 1, "rw", "float", "Ramp down time-constant in case of peaks. Valid range: [0.05 .. 4.0] (seconds)"),
"PP_AGCALPHAFASTGAIN": (17, 16, 1, "rw", "float", "Gain threshold enabling fast alpha mode. Valid range: [0.0 .. 1000.0] (linear gain factor)"),
"PP_AGCALPHASLOW": (17, 17, 1, "rw", "float", "Slow memory parameter for speech power. Valid range [0.0 .. 1.0]"),
"PP_AGCALPHAFAST": (17, 18, 1, "rw", "float", "Fast memory parameter for speech power. Valid range [0.0 .. 1.0]"),
"PP_LIMITONOFF": (17, 19, 1, "rw", "int32", "Limiter on communication output. Valid range: 0,1 (off,on)"),
"PP_LIMITPLIMIT": (17, 20, 1, "rw", "float", "Maximum limiter power. Valid range: [1e-8 .. 1.0] (power level)"),
"PP_MIN_NS": (17, 21, 1, "rw", "float", "Gain-floor for stationary noise suppression. Valid range: [0.0 .. 1.0]"),
"PP_MIN_NN": (17, 22, 1, "rw", "float", "Gain-floor for non-stationary noise suppression. Valid range: [0.0 .. 1.0]"),
"PP_ECHOONOFF": (17, 23, 1, "rw", "int32", "Echo suppression. Valid range: 0,1 (off,on)"),
"PP_GAMMA_E": (17, 24, 1, "rw", "float", "Over-subtraction factor of echo (direct and early components). Valid range: [0.0 .. 2.0]"),
"PP_GAMMA_ETAIL": (17, 25, 1, "rw", "float", "Over-subtraction factor of echo (tail components). Valid range: [0.0 .. 2.0]"),
"PP_GAMMA_ENL": (17, 26, 1, "rw", "float", "Over-subtraction factor of non-linear echo. Valid range: [0.0 .. 5.0]"),
"PP_NLATTENONOFF": (17, 27, 1, "rw", "int32", "Non-Linear echo attenuation. Valid range: 0,1 (off,on)"),
"PP_NLAEC_MODE": (17, 28, 1, "rw", "int32", "Non-Linear AEC training mode. Valid range: 0,1,2 (normal,train,train2)"),
"PP_MGSCALE": (17, 29, 3, "rw", "float", "Minimum gain scale for acoustic echo suppression.(max,min,cur), Valid range (max,min): [(1.0,0.0) .. (1e5,max)] Valid range (cur): min or max."),
"PP_FMIN_SPEINDEX": (17, 30, 1, "rw", "float", "In case of double talk, frequencies below SPEINDEX are more suppressed than frequencies above SPEINDEX. The actual suppression is depending on the setting of DTSENSITIVE. The parameter is not a taste parameter but needs to be tuned for a specific device. Valid range: [0.0 .. 7999.0]"),
"PP_DTSENSITIVE": (17, 31, 1, "rw", "int32", "Tradeoff between echo suppression and doubletalk performance. A lower value prefers high echo suppression (possibly at the cost of less doubletalk performance), a higher value prefers better doubletalk performance (possibly at the cost of good echo suppression). Good doubletalk performance is only possible for hardware without much non-linearities. When the value has 2 digits, for robustness an extra near-end speech detector is used. Valid range: [0 .. 5, 10 .. 15]"),
"PP_ATTNS_MODE": (17, 32, 1, "rw", "int32", "Additional reduction of AGC gain during non-speech. Valid range: 0,1 (off, on)"),
"PP_ATTNS_NOMINAL": (17, 33, 1, "rw", "float", "Amount of additional reduction during non-speech at nominal speech level. Valid range: [0.0 .. 1.0]"),
"PP_ATTNS_SLOPE": (17, 34, 1, "rw", "float", "Determines the extra amount of suppression during non-speech when the AGC level increases at lower speech level. The extra attenuation is given by (agcgain_nominal/agcgain_current)^attns_slope. With a value of 1.0 the amount of noise in the output remains approximately the same, independent of the agc gain. Valid range: [0.0 .. 5.0]"),
}
class ReSpeaker:
TIMEOUT = 100000
def __init__(self, dev):
self.dev = dev
def write(self, name, data_list):
try:
data = PARAMETERS[name]
except KeyError:
return
if data[3] == "ro":
raise ValueError('{} is read-only'.format(name))
if len(data_list) != data[2]:
raise ValueError('{} value count is not {}'.format(name, data[2]))
windex = data[0] # resid
wvalue = data[1] # cmdid
data_cnt = data[2] # cnt
data_type = data[4] # data type
payload = []
if data_type == 'float' or data_type == 'radians':
for i in range(data_cnt):
payload += struct.pack(b'f', float(data_list[i]))
elif data_type == 'char':
# For char arrays, convert string to bytes
payload = bytearray(data_list, 'utf-8') if isinstance(data_list, str) else bytearray(data_list)
elif data_type == 'uint8':
for i in range(data_cnt):
payload += data_list[i].to_bytes(1, byteorder='little')
elif data_type == 'uint32' or data_type == 'int32':
for i in range(data_cnt):
payload += struct.pack(b'I' if data_type == 'uint32' else b'i', data_list[i])
else:
# Default to int32 for other types
for i in range(data_cnt):
payload += struct.pack(b'i', data_list[i])
print("WriteCMD: cmdid: {}, resid: {}, payload: {}".format(wvalue, windex, payload))
self.dev.ctrl_transfer(
usb.util.CTRL_OUT | usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_RECIPIENT_DEVICE,
0, wvalue, windex, payload, self.TIMEOUT)
def read(self, name):
try:
data = PARAMETERS[name]
except KeyError:
return
read_attempts = 1
windex = data[0] # resid
wvalue = 0x80 | data[1] # cmdid
data_cnt = data[2] # cnt
data_type = data[4] # data type
if data_type == 'uint8' or data_type == 'char':
length = data_cnt + 1 # 1 byte for status
elif data_type == 'float' or data_type == 'radians' or data_type == 'uint32' or data_type == 'int32':
length = data_cnt * 4 + 1 # 1 byte for status
elif data_type == 'uint16':
length = data_cnt * 2 + 1 # 1 byte for status
response = self.dev.ctrl_transfer(
usb.util.CTRL_IN | usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_RECIPIENT_DEVICE,
0, wvalue, windex, length, self.TIMEOUT)
while True:
if read_attempts > 100:
raise ValueError('Read attempt exceeds 100 times')
if response[0] == CONTROL_SUCCESS:
break
elif response[0] == SERVICER_COMMAND_RETRY:
read_attempts += 1
response = self.dev.ctrl_transfer(
usb.util.CTRL_IN | usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_RECIPIENT_DEVICE,
0, wvalue, windex, length, self.TIMEOUT)
else:
raise ValueError('Unknown status code: {}'.format(response[0]))
time.sleep(0.01)
print("ReadCMD: cmdid: {}, resid: {}, payload: {}".format(wvalue, windex, response.tolist()))
if data_type == 'uint8':
byte_data = response.tobytes()
match_str = '<'
for i in range(data_cnt):
match_str += 'B'
result = struct.unpack(match_str, byte_data[1:])
elif data_type == 'char':
# For char arrays, convert bytes to string
byte_data = response.tobytes()
# Remove status byte and null terminators
result = byte_data[1:].rstrip(b'\x00').decode('utf-8', errors='ignore')
elif data_type == 'radians' or data_type == 'float':
byte_data = response.tobytes()
match_str = '<'
for i in range(data_cnt):
match_str += 'f'
result = struct.unpack(match_str, byte_data[1:])
elif data_type == 'uint32':
byte_data = response.tobytes()
match_str = '<'
for i in range(data_cnt):
match_str += 'I'
result = struct.unpack(match_str, byte_data[1:])
elif data_type == 'int32':
byte_data = response.tobytes()
match_str = '<'
for i in range(data_cnt):
match_str += 'i'
result = struct.unpack(match_str, byte_data[1:])
elif data_type == 'uint16':
byte_data = response.tobytes()
match_str = '<'
for i in range(data_cnt):
match_str += 'H'
result = struct.unpack(match_str, byte_data[1:])
return result
def close(self):
"""
close the interface
"""
usb.util.dispose_resources(self.dev)
def find(vid=0x2886, pid=0x001A):
if sys.platform.startswith('win'):
dev = libusb_package.find(idVendor=vid, idProduct=pid)
else:
dev = usb.core.find(idVendor=vid, idProduct=pid)
if not dev:
return
return ReSpeaker(dev)
def parse_value(value_str):
"""Parse a value string that can be decimal, hex (0x prefix), or hex ($ prefix)"""
try:
# Try hex formats first (0x prefix or $ prefix)
if value_str.startswith(('0x', '0X')):
return int(value_str, 16)
elif value_str.startswith('$'):
return int(value_str[1:], 16)
else:
# Try as decimal float first, then decimal int
try:
return float(value_str)
except ValueError:
return int(value_str)
except ValueError:
raise ValueError(f"Cannot parse '{value_str}' as a number (supports decimal and hex formats like 0xFF or $FF)")
def case_insensitive_command(value):
"""Convert command to uppercase and validate it exists"""
if not isinstance(value, str):
raise argparse.ArgumentTypeError(f"Command must be a string, got {type(value)}")
upper_value = value.upper()
# Check if the uppercase version exists in PARAMETERS
if upper_value not in PARAMETERS:
# Provide helpful error message with similar commands
available_commands = list(PARAMETERS.keys())
similar_commands = [cmd for cmd in available_commands if upper_value in cmd.upper()]
if similar_commands:
raise argparse.ArgumentTypeError(
f"Invalid command '{value}'. Did you mean one of: {similar_commands}? "
)
else:
raise argparse.ArgumentTypeError(
f"Invalid command '{value}'. "
)
return upper_value
def list_commands():
"""Display all supported commands with their detailed information."""
print("\n=== Supported Commands ===\n")
print(f"{'Command':<30} {'RESID':<6} {'CMDID':<6} {'Length':<7} {'Type':<12} {'Access':<8} {'Description'}")
print("-" * 120)
for name, info in PARAMETERS.items():
resid, cmdid, length, param_type, access, description = info
print(f"{name:<30} {resid:<6} {cmdid:<6} {length:<7} {param_type:<12} {access:<8} {description}")
def main():
parser = argparse.ArgumentParser(description='ReSpeaker Host Control Script')
parser.add_argument('-l', '--list', action='store_true',
help='list all supported commands with detailed information')
parser.add_argument('COMMAND', nargs='?', type=case_insensitive_command,
help='command to execute (e.g., VERSION, DOA_VALUE, etc.)')
parser.add_argument('--vid', type=lambda x: int(x, 0), default=0x2886,
help='usb vendor ID (default: 0x2886)')
parser.add_argument('--pid', type=lambda x: int(x, 0), default=0x001A,
help='usb product ID (default: 0x001A)')
parser.add_argument('--values', nargs='+', type=parse_value,
help='values for write commands (only for write operations). Supports decimal (123, 1.5) and hex (0x7B, $7B) formats')
args = parser.parse_args()
# Handle list command
if args.list:
list_commands()
sys.exit(0)
dev = find(vid=args.vid, pid=args.pid)
if not dev:
print('No device found')
sys.exit(1)
try:
if args.values:
if PARAMETERS[args.COMMAND][3] == "ro":
print(f"Error: {args.COMMAND} is read-only and cannot be written to")
sys.exit(1)
# Convert values to appropriate types based on parameter type
param_type = PARAMETERS[args.COMMAND][4]
if param_type == "float" or param_type == "radians":
args.values = [float(v) for v in args.values]
else:
args.values = [int(v) for v in args.values]
if PARAMETERS[args.COMMAND][2] != len(args.values):
print(f"Error: {args.COMMAND} value count is {PARAMETERS[args.COMMAND][2]}, but {len(args.values)} values provided")
sys.exit(1)
dev.write(args.COMMAND, args.values)
else:
if PARAMETERS[args.COMMAND][3] == "wo":
print(f"Error: {args.COMMAND} is write-only and cannot be read")
resid, cmdid, length, param_type, access, description = PARAMETERS[args.COMMAND]
print(f"{'Command':<30} {'RESID':<6} {'CMDID':<6} {'Length':<7} {'Type':<12} {'Access':<8} {'Description'}")
print("-" * 120)
print(f"{args.COMMAND:<30} {resid:<6} {cmdid:<6} {length:<7} {param_type:<12} {access:<8} {description}")
sys.exit(1)
result = dev.read(args.COMMAND)
if args.COMMAND == "LED_COLOR" or args.COMMAND == "LED_DOA_COLOR" or args.COMMAND == "LED_RING_COLOR":
formatted_result = []
for val in result:
formatted_result.append(f"0x{val:06X}")
print(f"{args.COMMAND}: [{', '.join(formatted_result)}]")
else:
formatted_result = []
for val in result:
if isinstance(val, float):
formatted_result.append(f"{val:.3f}")
elif isinstance(val, str):
formatted_result.append(f"'{val}'")
else:
formatted_result.append(str(val))
print(f"{args.COMMAND}: [{', '.join(formatted_result)}]")
print(f"Done!")
except Exception as e:
print(f"Error executing command {args.COMMAND}: {e}")
sys.exit(1)
finally:
dev.close()
if __name__ == '__main__':
main()
+37
View File
@@ -0,0 +1,37 @@
[Unit]
Description=Wyoming Event Service with LED control
After=network.target
[Service]
Type=simple
# 🔹 Запуск от имени вашего пользователя
User=sirmrak
Group=sirmrak
# 🔹 Рабочая директория — где лежит wyoming-led.py
WorkingDirectory=/home/sirmrak/projects/Satellite/examples
# 🔹 Прямой запуск через python из виртуального окружения (без обёрток!)
ExecStart=/home/sirmrak/projects/.venv_satellite/bin/python3 /home/sirmrak/projects/Satellite/examples/wyoming-led.py --host 127.0.0.1 --port 10500
# 🔹 Критически важно: передаём окружение
Environment="HOME=/home/sirmrak"
Environment="USER=sirmrak"
Environment="PATH=/home/sirmrak/projects/.venv_satellite/bin:/usr/local/bin:/usr/bin:/bin"
Environment="VIRTUAL_ENV=/home/sirmrak/projects/.venv_satellite"
# 🔹 Разрешаем доступ к устройству (на всякий случай)
PrivateDevices=false
# 🔹 Перезапуск при ошибке
Restart=on-failure
RestartSec=3
# 🔹 Логирование в journal
StandardOutput=journal
StandardError=journal
SyslogIdentifier=wyoming-event
[Install]
WantedBy=multi-user.target
+15
View File
@@ -0,0 +1,15 @@
[Unit]
Description=Wyoming Satellite Service (Wake Mode)
Requires=network.target
[Service]
Type=simple
User=sirmrak
WorkingDirectory=/home/sirmrak/projects/Satellite
ExecStart=/home/sirmrak/projects/Satellite/start_satellite.sh
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
+19
View File
@@ -0,0 +1,19 @@
[Unit]
Description=voskWakeWord
[Service]
Type=simple
User=sirmrak
ExecStart=/home/sirmrak/projects/.venv_satellite/bin/python \
/home/sirmrak/projects/Satellite/local/vosk-wake-dev/script/run \
--uri tcp://0.0.0.0:10400 \
--model-path /home/sirmrak/projects/models/vosk-model-small-ru-0.22 \
--rate 16000 \
--wake-word "привет дройд"
WorkingDirectory=/home/sirmrak/projects/Satellite/local/vosk-wake-dev
Restart=always
RestartSec=1
[Install]
WantedBy=default.target
+2 -2
View File
@@ -1,6 +1,6 @@
/home/sirmrak/wyoming-satellite/.venv/bin/python local/vosk-wake-dev/script/run \ /home/sirmrak/projects/.venv_satellite/bin/python local/vosk-wake-dev/script/run \
--uri tcp://0.0.0.0:10400 \ --uri tcp://0.0.0.0:10400 \
--model-path /home/sirmrak/models/vosk-model-small-ru-0.22 \ --model-path /home/sirmrak/projects/models/vosk-model-small-ru-0.22 \
--rate 16000 \ --rate 16000 \
--wake-word "привет дроид" #\ --wake-word "привет дроид" #\
# --debug # --debug
+6 -6
View File
@@ -6,7 +6,7 @@ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
export PYTHONPATH="${SCRIPT_DIR}" export PYTHONPATH="${SCRIPT_DIR}"
# Путь к Python-окружению # Путь к Python-окружению
PY="/home/sirmrak/wyoming-satellite/.venv/bin/python" PY="/home/sirmrak/projects/.venv_satellite/bin/python"
# Режим запуска: wake|vad (по умолчанию wake) # Режим запуска: wake|vad (по умолчанию wake)
MODE="${1:-wake}" MODE="${1:-wake}"
@@ -15,11 +15,11 @@ BASE_ARGS=(
--debug --debug
--name 'Wyoming Pi Satellite' --name 'Wyoming Pi Satellite'
--uri tcp://0.0.0.0:10700 --uri tcp://0.0.0.0:10700
--mic-command 'arecord -D plughw:CARD=seeed2micvoicec,DEV=0 -q -r 16000 -c 1 -f S16_LE -t raw' --mic-command 'arecord -D plughw:CARD=Array,DEV=0 -q -r 16000 -c 1 -f S16_LE -t raw'
--snd-command 'aplay -D plughw:CARD=0,DEV=0 -q -r 22050 -c 1 -f S16_LE -t raw' --snd-command 'aplay -D plughw:CARD=Array,DEV=0 -q -r 22050 -c 1 -f S16_LE -t raw'
--awake-wav /home/sirmrak/wyoming-satellite/sounds/awake.wav --awake-wav /home/sirmrak/projects/Satellite/sounds/awake.wav
--mic-noise-suppression 2 #--mic-noise-suppression 2
--mic-auto-gain 20 #--mic-auto-gain 20
--event-uri tcp://127.0.0.1:10500 --event-uri tcp://127.0.0.1:10500
) )
View File