Files
RadioPhotonic_PCB_PC_software/laser_control/models.py
T
2026-10-09 19:45:30 +03:00

149 lines
4.2 KiB
Python

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