"""Public domain models used by the controller and GUI layers.""" from dataclasses import dataclass, field from datetime import datetime from enum import IntEnum, IntFlag from typing import Any from .constants import ( VOLT_3V3_MAX, VOLT_3V3_MIN, VOLT_5V_MAX, VOLT_5V_MIN, VOLT_7V_MAX, VOLT_7V_MIN, ) class DeviceState(IntFlag): """Bit-mask of device error flags returned by the firmware status packet.""" OK = 0x0000 SD_ERROR = 0x0001 UART_ERROR = 0x0002 UART_DECODE_ERROR = 0x0004 TEC1_ERROR = 0x0008 TEC2_ERROR = 0x0010 DEFAULT_ERROR = 0x0020 AD9102_ERROR = 0x0080 @dataclass(slots=True) class Measurements: """Latest live telemetry frame decoded from the board.""" current1: float current2: float temp1: float temp2: float adc_pf3_voltage: float = 0.0 adc_pf4_voltage: float = 0.0 temp_ext1: float | None = None temp_ext2: float | None = None voltage_3v3: float = 0.0 voltage_5v1: float = 0.0 voltage_5v2: float = 0.0 voltage_7v0: float = 0.0 message_id: int | None = None timestamp: datetime = field(default_factory=datetime.now) def to_dict(self) -> dict[str, Any]: """Return a JSON-friendly representation.""" return { "current1": self.current1, "current2": self.current2, "adc_pf3_voltage": self.adc_pf3_voltage, "adc_pf4_voltage": self.adc_pf4_voltage, "temp1": self.temp1, "temp2": self.temp2, "temp_ext1": self.temp_ext1, "temp_ext2": self.temp_ext2, "voltage_3v3": self.voltage_3v3, "voltage_5v1": self.voltage_5v1, "voltage_5v2": self.voltage_5v2, "voltage_7v0": self.voltage_7v0, "message_id": self.message_id, "timestamp": self.timestamp.isoformat(), } def check_power_rails(self) -> dict[str, bool]: """Check nominal supply rails against static tolerances.""" return { "3v3": VOLT_3V3_MIN <= self.voltage_3v3 <= VOLT_3V3_MAX, "5v1": VOLT_5V_MIN <= self.voltage_5v1 <= VOLT_5V_MAX, "5v2": VOLT_5V_MIN <= self.voltage_5v2 <= VOLT_5V_MAX, "7v0": VOLT_7V_MIN <= self.voltage_7v0 <= VOLT_7V_MAX, } @dataclass(slots=True) class DeviceStatus: """Decoded two-byte status response from the board.""" state: DeviceState = DeviceState.OK detail: int = 0 measurements: Measurements | None = None is_connected: bool = False last_command_id: int | None = None error_message: str | None = None @property def has_error(self) -> bool: """Return True when any firmware error bit is set.""" return self.state != DeviceState.OK @property def is_ok(self) -> bool: """Convenience alias for the common no-error case.""" return not self.has_error @property def active_errors(self) -> list[str]: """Return the names of all active error flags.""" return [ flag.name for flag in DeviceState if flag is not DeviceState.OK and (self.state & flag) == flag ] def to_dict(self) -> dict[str, Any]: """Return a JSON-friendly representation.""" return { "state_mask": int(self.state), "state_names": self.active_errors, "detail": self.detail, "measurements": self.measurements.to_dict() if self.measurements else None, "is_connected": self.is_connected, "last_command_id": self.last_command_id, "error_message": self.error_message, "has_error": self.has_error, } class SweepTarget(IntEnum): """Output driven by a firmware staircase sweep.""" LASER2_CURRENT = 0 STM32_DAC = 1 @dataclass(frozen=True, slots=True) class SweepSettings: """Firmware sweep: min..max in fixed steps, then a pause at min before repeating.""" target: SweepTarget enabled: bool min_code: int max_code: int step_code: int step_period_us: int pause_ms: int @dataclass(slots=True) class ProfileSaveRequest: """Rendered profile payload that should be persisted on the device SD card.""" profile_name: str profile_text: str waveform_text: str = ""