Files
RFP_Infinity-Vis/Infinity_Vis_1/infinity_vis_1/models.py

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]