CI / backend (pull_request) Canceled after 0s
CI / shell (pull_request) Canceled after 0s
CI / frontend (pull_request) Canceled after 0s
CI / arm64-smoke (pull_request) Canceled after 0s
CI / backend (push) Canceled after 0s
CI / shell (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / arm64-smoke (push) Canceled after 0s
153 lines
5.6 KiB
Python
153 lines
5.6 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
from typing import Literal
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
|
|
from app.core.config import get_settings
|
|
from app.core.database import SessionLocal
|
|
from app.dmx.backends import (
|
|
ArtNetDmxBackend,
|
|
ArtNetNode,
|
|
OlaDmxBackend,
|
|
SimulatorDmxBackend,
|
|
discover_artnet_nodes,
|
|
)
|
|
from app.dmx.engine import DmxEngine
|
|
from app.models.entities import Setting
|
|
|
|
|
|
DMX_OUTPUT_SETTING_KEY = "dmx_output"
|
|
BackendKind = Literal["simulator", "ola", "artnet"]
|
|
|
|
|
|
class DmxOutputService:
|
|
def __init__(
|
|
self,
|
|
engine: DmxEngine,
|
|
session_factory: async_sessionmaker[AsyncSession] = SessionLocal,
|
|
) -> None:
|
|
self.engine = engine
|
|
self._session_factory = session_factory
|
|
self._settings = get_settings()
|
|
self._config = self._default_config()
|
|
self._artnet_nodes: list[ArtNetNode] = []
|
|
|
|
async def startup(self) -> None:
|
|
await self._load_config()
|
|
self.engine.configure_backend(self._build_backend(self._config))
|
|
|
|
async def get_config(self) -> dict[str, object]:
|
|
return {
|
|
**self._config,
|
|
"artnet_nodes": [self._serialize_node(node) for node in self._artnet_nodes],
|
|
}
|
|
|
|
async def save_config(self, payload: dict[str, object]) -> dict[str, object]:
|
|
normalized = self._normalize_config(payload)
|
|
self._config = normalized
|
|
await self._persist_config(normalized)
|
|
backend = self._build_backend(normalized)
|
|
if self.engine.is_running:
|
|
await self.engine.replace_backend(backend)
|
|
else:
|
|
self.engine.configure_backend(backend)
|
|
return await self.get_config()
|
|
|
|
async def discover_artnet(self, timeout_s: float = 1.0) -> dict[str, object]:
|
|
nodes = await self._discover(timeout_s)
|
|
self._artnet_nodes = nodes
|
|
return {
|
|
"items": [self._serialize_node(node) for node in nodes],
|
|
"count": len(nodes),
|
|
}
|
|
|
|
async def _discover(self, timeout_s: float) -> list[ArtNetNode]:
|
|
return await self._to_thread_discovery(timeout_s)
|
|
|
|
async def _to_thread_discovery(self, timeout_s: float) -> list[ArtNetNode]:
|
|
import asyncio
|
|
|
|
return await asyncio.to_thread(discover_artnet_nodes, timeout_s)
|
|
|
|
async def _load_config(self) -> None:
|
|
async with self._session_factory() as session:
|
|
setting = await session.get(Setting, DMX_OUTPUT_SETTING_KEY)
|
|
if setting is None or not isinstance(setting.value, dict):
|
|
return
|
|
self._config = self._normalize_config(setting.value)
|
|
|
|
async def _persist_config(self, config: dict[str, object]) -> None:
|
|
async with self._session_factory() as session:
|
|
setting = await session.get(Setting, DMX_OUTPUT_SETTING_KEY)
|
|
if setting is None:
|
|
setting = Setting(
|
|
key=DMX_OUTPUT_SETTING_KEY,
|
|
value=config,
|
|
updated_at=datetime.now(UTC),
|
|
)
|
|
session.add(setting)
|
|
else:
|
|
setting.value = config
|
|
setting.updated_at = datetime.now(UTC)
|
|
await session.commit()
|
|
|
|
def _default_config(self) -> dict[str, object]:
|
|
backend: BackendKind = "simulator" if self._settings.simulator_enabled else "ola"
|
|
return {
|
|
"backend": backend,
|
|
"universe": self._settings.ola_universe,
|
|
"output_port": self._settings.ola_output_port or "",
|
|
"target_host": "",
|
|
}
|
|
|
|
def _normalize_config(self, payload: dict[str, object]) -> dict[str, object]:
|
|
backend = str(payload.get("backend", self._default_config()["backend"])).lower()
|
|
if backend not in {"simulator", "ola", "artnet"}:
|
|
backend = "simulator"
|
|
universe = int(payload.get("universe", self._settings.ola_universe) or self._settings.ola_universe)
|
|
output_port = str(payload.get("output_port", "") or "").strip()
|
|
target_host = str(payload.get("target_host", "") or "").strip()
|
|
if backend == "artnet" and not target_host:
|
|
target_host = "255.255.255.255"
|
|
if backend == "simulator":
|
|
output_port = "simulator"
|
|
return {
|
|
"backend": backend,
|
|
"universe": max(1, min(63999, universe)),
|
|
"output_port": output_port,
|
|
"target_host": target_host,
|
|
}
|
|
|
|
def _build_backend(self, config: dict[str, object]):
|
|
backend = str(config["backend"])
|
|
universe = int(config["universe"])
|
|
output_port = str(config.get("output_port", "") or "") or None
|
|
if backend == "ola":
|
|
return OlaDmxBackend(
|
|
universe=universe,
|
|
output_port=output_port,
|
|
send_timeout_s=self._settings.ola_send_timeout_ms / 1000,
|
|
)
|
|
if backend == "artnet":
|
|
target_host = str(config.get("target_host", "") or "255.255.255.255")
|
|
return ArtNetDmxBackend(
|
|
universe=universe,
|
|
target_host=target_host,
|
|
output_port=output_port or target_host,
|
|
)
|
|
return SimulatorDmxBackend(universe=universe)
|
|
|
|
def _serialize_node(self, node: ArtNetNode) -> dict[str, object]:
|
|
return {
|
|
"ip": node.ip,
|
|
"short_name": node.short_name,
|
|
"long_name": node.long_name,
|
|
"label": node.label,
|
|
"net": node.net,
|
|
"sub_switch": node.sub_switch,
|
|
"port_count": node.port_count,
|
|
"raw_port_address": node.raw_port_address,
|
|
}
|