Files
RadioPhotonic_PCB_PC_software/laser_control/models.py
2026-04-26 18:39:55 +03:00

129 lines
3.8 KiB
Python

"""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 = ""