Update docs and screenshots
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
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
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user