"""Control-panel state: the data shape read from the Arduino over serial. This is the serial-contract payload shared with sub-project 4 (firmware). It models the full panel from design §6/§7: the 7-way content dial, the four experience knobs (0..4), and the two intensity levels (volume, brightness). The wire framing itself is the separate 3<->4 serial contract; this module is the *decoded* form. """ from __future__ import annotations from dataclasses import dataclass, fields # The seven positions of the content dial (design §6). CONTENT_POSITIONS = frozenset( {"off", "white_noise", "music", "audio_track", "video", "music_video", "audio_video"} ) KNOB_FIELDS = ("left", "right", "dark", "light", "volume", "brightness") KNOB_MIN = 0 KNOB_MAX = 4 class ControlsError(ValueError): """Raised when a Controls payload is structurally invalid.""" @dataclass(frozen=True) class Controls: content: str left: int right: int dark: int light: int volume: int brightness: int def validate_controls(c: Controls) -> None: """Raise ControlsError if the payload is structurally invalid.""" if c.content not in CONTENT_POSITIONS: raise ControlsError( f"invalid content position {c.content!r}; " f"expected one of {sorted(CONTENT_POSITIONS)}" ) for name in KNOB_FIELDS: value = getattr(c, name) if isinstance(value, bool) or not isinstance(value, int): raise ControlsError(f"knob {name} must be an int, got {value!r}") if not (KNOB_MIN <= value <= KNOB_MAX): raise ControlsError( f"knob {name}={value} out of range {KNOB_MIN}..{KNOB_MAX}" ) def parse_controls(data: dict) -> Controls: """Build a validated Controls from a decoded mapping, rejecting unknown or missing keys.""" known = {f.name for f in fields(Controls)} unknown = set(data) - known if unknown: raise ControlsError(f"unknown keys: {sorted(unknown)}") missing = known - set(data) if missing: raise ControlsError(f"missing keys: {sorted(missing)}") c = Controls(**data) validate_controls(c) return c