Add Art-Net / sACN (E1.31) DMX device support

Full-stack implementation of DMX output for stage lighting and LED controllers:
- DMXClient with Art-Net and sACN packet builders, multi-universe splitting
- DMXDeviceProvider with manual_led_count capability and URL parsing
- Device store, API schemas, routes wired with dmx_protocol/start_universe/start_channel
- Frontend: add/settings modals with DMX fields, IconSelect protocol picker
- Fix add device modal dirty check on type change (re-snapshot after switch)
- i18n keys for DMX in en/ru/zh locales

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 16:46:40 +03:00
parent 18c886cbc5
commit ff24ec95e6
18 changed files with 607 additions and 7 deletions

View File

@@ -54,6 +54,9 @@ def _device_to_response(device) -> DeviceResponse:
zone_mode=device.zone_mode,
capabilities=sorted(get_device_capabilities(device.device_type)),
tags=getattr(device, 'tags', []),
dmx_protocol=getattr(device, 'dmx_protocol', 'artnet'),
dmx_start_universe=getattr(device, 'dmx_start_universe', 0),
dmx_start_channel=getattr(device, 'dmx_start_channel', 1),
created_at=device.created_at,
updated_at=device.updated_at,
)
@@ -129,6 +132,9 @@ async def create_device(
rgbw=device_data.rgbw or False,
zone_mode=device_data.zone_mode or "combined",
tags=device_data.tags,
dmx_protocol=device_data.dmx_protocol or "artnet",
dmx_start_universe=device_data.dmx_start_universe or 0,
dmx_start_channel=device_data.dmx_start_channel or 1,
)
# WS devices: auto-set URL to ws://{device_id}
@@ -313,6 +319,9 @@ async def update_device(
rgbw=update_data.rgbw,
zone_mode=update_data.zone_mode,
tags=update_data.tags,
dmx_protocol=update_data.dmx_protocol,
dmx_start_universe=update_data.dmx_start_universe,
dmx_start_channel=update_data.dmx_start_channel,
)
# Sync connection info in processor manager

View File

@@ -19,6 +19,10 @@ class DeviceCreate(BaseModel):
rgbw: Optional[bool] = Field(None, description="RGBW mode (mock devices)")
zone_mode: Optional[str] = Field(None, description="OpenRGB zone mode: combined or separate")
tags: List[str] = Field(default_factory=list, description="User-defined tags")
# DMX (Art-Net / sACN) fields
dmx_protocol: Optional[str] = Field(None, description="DMX protocol: artnet or sacn")
dmx_start_universe: Optional[int] = Field(None, ge=0, le=32767, description="DMX start universe")
dmx_start_channel: Optional[int] = Field(None, ge=1, le=512, description="DMX start channel (1-512)")
class DeviceUpdate(BaseModel):
@@ -34,6 +38,9 @@ class DeviceUpdate(BaseModel):
rgbw: Optional[bool] = Field(None, description="RGBW mode (mock devices)")
zone_mode: Optional[str] = Field(None, description="OpenRGB zone mode: combined or separate")
tags: Optional[List[str]] = None
dmx_protocol: Optional[str] = Field(None, description="DMX protocol: artnet or sacn")
dmx_start_universe: Optional[int] = Field(None, ge=0, le=32767, description="DMX start universe")
dmx_start_channel: Optional[int] = Field(None, ge=1, le=512, description="DMX start channel (1-512)")
class CalibrationLineSchema(BaseModel):
@@ -128,6 +135,9 @@ class DeviceResponse(BaseModel):
zone_mode: str = Field(default="combined", description="OpenRGB zone mode: combined or separate")
capabilities: List[str] = Field(default_factory=list, description="Device type capabilities")
tags: List[str] = Field(default_factory=list, description="User-defined tags")
dmx_protocol: str = Field(default="artnet", description="DMX protocol: artnet or sacn")
dmx_start_universe: int = Field(default=0, description="DMX start universe")
dmx_start_channel: int = Field(default=1, description="DMX start channel (1-512)")
created_at: datetime = Field(description="Creation timestamp")
updated_at: datetime = Field(description="Last update timestamp")