165 lines
4.4 KiB
Python
165 lines
4.4 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Literal
|
|
|
|
|
|
RGB = tuple[int, int, int]
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class SegmentSpec:
|
|
name: str
|
|
side: str
|
|
start_channel: int
|
|
led_count: int
|
|
reverse: bool = False
|
|
orientation_rad: float = 0.0
|
|
x0: float = 0.0
|
|
y0: float = 0.0
|
|
x1: float = 0.0
|
|
y1: float = 0.0
|
|
|
|
@property
|
|
def byte_offset(self) -> int:
|
|
return max(0, self.start_channel - 1)
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class TileSpec:
|
|
tile_id: str
|
|
row: int
|
|
col: int
|
|
led_total: int
|
|
controller_ip: str = ""
|
|
screen_name: str = ""
|
|
controller_host: str = ""
|
|
controller_name: str = ""
|
|
controller_mac: str = ""
|
|
universe: int = 0
|
|
subnet: int = 0
|
|
enabled: bool = True
|
|
brightness: float = 1.0
|
|
x0: float = 0.0
|
|
y0: float = 0.0
|
|
x1: float = 0.0
|
|
y1: float = 0.0
|
|
segments: list[SegmentSpec] = field(default_factory=list)
|
|
|
|
@property
|
|
def controller_payload_bytes(self) -> int:
|
|
return max(0, self.led_total) * 3
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class ControllerTileRoute:
|
|
tile_id: str
|
|
controller_offset_bytes: int
|
|
segment_offsets: tuple[tuple[int, int], ...]
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class ControllerSpec:
|
|
host: str
|
|
payload_bytes: int
|
|
tiles: tuple[ControllerTileRoute, ...]
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class MappingSpec:
|
|
name: str = "Infinity Vis 1"
|
|
rows: int = 3
|
|
cols: int = 6
|
|
tiles: list[TileSpec] = field(default_factory=list)
|
|
file_path: Path | None = None
|
|
protocol: str = "ddp"
|
|
subnet: int = 0
|
|
color_format: str = "rgb"
|
|
tile_behavior: str = "solid_color_per_tile"
|
|
global_gamma: float = 2.2
|
|
|
|
def ordered_tiles(self) -> list[TileSpec]:
|
|
return sorted(self.tiles, key=lambda tile: (tile.row, tile.col, tile.tile_id))
|
|
|
|
def tile_lookup(self) -> dict[str, TileSpec]:
|
|
return {tile.tile_id: tile for tile in self.tiles}
|
|
|
|
def build_controllers(self) -> list[ControllerSpec]:
|
|
groups: dict[str, list[TileSpec]] = {}
|
|
for tile in self.ordered_tiles():
|
|
if not tile.enabled:
|
|
continue
|
|
host = tile.controller_ip.strip()
|
|
if not host:
|
|
continue
|
|
groups.setdefault(host, []).append(tile)
|
|
|
|
controllers: list[ControllerSpec] = []
|
|
for host, tiles in groups.items():
|
|
running_offset = 0
|
|
routes: list[ControllerTileRoute] = []
|
|
for tile in tiles:
|
|
segment_offsets = tuple(
|
|
(running_offset + segment.byte_offset, segment.led_count)
|
|
for segment in sorted(tile.segments, key=lambda item: (item.start_channel, item.name))
|
|
)
|
|
routes.append(
|
|
ControllerTileRoute(
|
|
tile_id=tile.tile_id,
|
|
controller_offset_bytes=running_offset,
|
|
segment_offsets=segment_offsets,
|
|
)
|
|
)
|
|
running_offset += tile.controller_payload_bytes
|
|
controllers.append(
|
|
ControllerSpec(
|
|
host=host,
|
|
payload_bytes=running_offset,
|
|
tiles=tuple(routes),
|
|
)
|
|
)
|
|
return sorted(controllers, key=lambda item: item.host)
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class PatternParams:
|
|
brightness: float = 1.0
|
|
tempo_multiplier: float = 1.0
|
|
direction: str = "left_to_right"
|
|
checker_mode: str = "classic"
|
|
scan_style: str = "line"
|
|
angle: float = 0.0
|
|
on_width: float = 1.0
|
|
off_width: float = 1.0
|
|
strobe_mode: str = "global"
|
|
stopwatch_mode: str = "sync"
|
|
color_mode: str = "dual"
|
|
primary_color: str = "#4D7CFF"
|
|
secondary_color: str = "#0E1630"
|
|
palette: str = "Laser Club"
|
|
symmetry: str = "none"
|
|
center_pulse_mode: str = "expand"
|
|
step_size: float = 1.0
|
|
block_size: float = 1.0
|
|
pixel_group_size: float = 1.0
|
|
strobe_duty_cycle: float = 0.5
|
|
randomness: float = 0.35
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class SceneState:
|
|
pattern_id: str = "solid"
|
|
params: PatternParams = field(default_factory=PatternParams)
|
|
tempo_bpm: float = 120.0
|
|
utility_mode: Literal["none", "blackout", "single_tile", "identify"] = "none"
|
|
selected_tile_id: str | None = None
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class FrameSnapshot:
|
|
timestamp: float
|
|
pattern_id: str
|
|
tile_colors: list[RGB]
|
|
controller_payloads: dict[str, bytes]
|