"""Public domain models used by the controller and GUI layers.""" from dataclasses import dataclass, field from datetime import datetime from enum import 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 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 to6_counter_lsb: int | None = None to6_counter_msb: 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, "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, "to6_counter_lsb": self.to6_counter_lsb, "to6_counter_msb": self.to6_counter_msb, "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, } @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 = ""