Add new PyQt UI
This commit is contained in:
@ -1,219 +1,128 @@
|
||||
"""
|
||||
Data models for laser control module.
|
||||
"""Public domain models used by the controller and GUI layers."""
|
||||
|
||||
Provides dataclasses and enums for structured data representation
|
||||
throughout the laser control system.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
from typing import Optional, Dict, Any
|
||||
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
|
||||
|
||||
|
||||
class VariationType(IntEnum):
|
||||
"""Types of parameter variation modes."""
|
||||
MANUAL = 0x00
|
||||
CHANGE_CURRENT_LD1 = 0x01
|
||||
CHANGE_CURRENT_LD2 = 0x02
|
||||
CHANGE_TEMPERATURE_LD1 = 0x03
|
||||
CHANGE_TEMPERATURE_LD2 = 0x04
|
||||
|
||||
|
||||
class DeviceState(IntEnum):
|
||||
"""Device operational states."""
|
||||
IDLE = 0x0000
|
||||
RUNNING = 0x0001
|
||||
BUSY = 0x0002
|
||||
ERROR = 0x00FF
|
||||
ERROR_OVERHEAT = 0x0100
|
||||
ERROR_POWER = 0x0200
|
||||
ERROR_COMMUNICATION = 0x0400
|
||||
ERROR_INVALID_COMMAND = 0x0800
|
||||
|
||||
|
||||
@dataclass
|
||||
class ManualModeParams:
|
||||
"""Parameters for manual control mode."""
|
||||
temp1: float # Temperature for laser 1 (°C)
|
||||
temp2: float # Temperature for laser 2 (°C)
|
||||
current1: float # Current for laser 1 (mA)
|
||||
current2: float # Current for laser 2 (mA)
|
||||
pi_coeff1_p: float = 1.0 # PI controller proportional coefficient for laser 1
|
||||
pi_coeff1_i: float = 0.5 # PI controller integral coefficient for laser 1
|
||||
pi_coeff2_p: float = 1.0 # PI controller proportional coefficient for laser 2
|
||||
pi_coeff2_i: float = 0.5 # PI controller integral coefficient for laser 2
|
||||
|
||||
def to_dict(self) -> Dict[str, float]:
|
||||
"""Convert to dictionary representation."""
|
||||
return {
|
||||
'temp1': self.temp1,
|
||||
'temp2': self.temp2,
|
||||
'current1': self.current1,
|
||||
'current2': self.current2,
|
||||
'pi_coeff1_p': self.pi_coeff1_p,
|
||||
'pi_coeff1_i': self.pi_coeff1_i,
|
||||
'pi_coeff2_p': self.pi_coeff2_p,
|
||||
'pi_coeff2_i': self.pi_coeff2_i
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class VariationParams:
|
||||
"""Parameters for variation mode."""
|
||||
variation_type: VariationType
|
||||
# Static parameters (fixed during variation)
|
||||
static_temp1: float
|
||||
static_temp2: float
|
||||
static_current1: float
|
||||
static_current2: float
|
||||
# Variation range
|
||||
min_value: float # Minimum value for varied parameter
|
||||
max_value: float # Maximum value for varied parameter
|
||||
step: float # Step size for variation
|
||||
# Time parameters
|
||||
time_step: int # Time step in microseconds (20-100)
|
||||
delay_time: int # Delay between measurements in milliseconds (3-10)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary representation."""
|
||||
return {
|
||||
'variation_type': self.variation_type.value,
|
||||
'static_temp1': self.static_temp1,
|
||||
'static_temp2': self.static_temp2,
|
||||
'static_current1': self.static_current1,
|
||||
'static_current2': self.static_current2,
|
||||
'min_value': self.min_value,
|
||||
'max_value': self.max_value,
|
||||
'step': self.step,
|
||||
'time_step': self.time_step,
|
||||
'delay_time': self.delay_time
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(slots=True)
|
||||
class Measurements:
|
||||
"""Real-time measurements from the device."""
|
||||
# Photodiode currents
|
||||
current1: float # Photodiode current for laser 1 (mA)
|
||||
current2: float # Photodiode current for laser 2 (mA)
|
||||
# Temperatures
|
||||
temp1: float # Temperature of laser 1 (°C)
|
||||
temp2: float # Temperature of laser 2 (°C)
|
||||
temp_ext1: Optional[float] = None # External thermistor 1 temperature (°C)
|
||||
temp_ext2: Optional[float] = None # External thermistor 2 temperature (°C)
|
||||
# Power supply voltages
|
||||
voltage_3v3: float = 0.0 # 3.3V rail voltage
|
||||
voltage_5v1: float = 0.0 # 5V rail 1 voltage
|
||||
voltage_5v2: float = 0.0 # 5V rail 2 voltage
|
||||
voltage_7v0: float = 0.0 # 7V rail voltage
|
||||
# Metadata
|
||||
timestamp: Optional[datetime] = None
|
||||
message_id: Optional[int] = None
|
||||
to6_counter_lsb: Optional[int] = None
|
||||
to6_counter_msb: Optional[int] = None
|
||||
"""Latest live telemetry frame decoded from the board."""
|
||||
|
||||
def __post_init__(self):
|
||||
"""Set timestamp if not provided."""
|
||||
if self.timestamp is None:
|
||||
self.timestamp = datetime.now()
|
||||
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]:
|
||||
"""Convert to dictionary representation."""
|
||||
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,
|
||||
'timestamp': self.timestamp.isoformat() if self.timestamp else None,
|
||||
'message_id': self.message_id
|
||||
"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 if power supply voltages are within acceptable range."""
|
||||
def check_power_rails(self) -> dict[str, bool]:
|
||||
"""Check nominal supply rails against static tolerances."""
|
||||
return {
|
||||
'3v3': 3.1 <= self.voltage_3v3 <= 3.5,
|
||||
'5v1': 4.8 <= self.voltage_5v1 <= 5.3,
|
||||
'5v2': 4.8 <= self.voltage_5v2 <= 5.3,
|
||||
'7v0': 6.5 <= self.voltage_7v0 <= 7.5
|
||||
"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
|
||||
@dataclass(slots=True)
|
||||
class DeviceStatus:
|
||||
"""Complete device status information."""
|
||||
state: DeviceState
|
||||
measurements: Optional[Measurements] = None
|
||||
"""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: Optional[int] = None
|
||||
error_message: Optional[str] = None
|
||||
|
||||
@property
|
||||
def is_idle(self) -> bool:
|
||||
"""Check if device is idle."""
|
||||
return self.state == DeviceState.IDLE
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
"""Check if device is running a task."""
|
||||
return self.state == DeviceState.RUNNING
|
||||
last_command_id: int | None = None
|
||||
error_message: str | None = None
|
||||
|
||||
@property
|
||||
def has_error(self) -> bool:
|
||||
"""Check if device has any error."""
|
||||
return self.state >= DeviceState.ERROR
|
||||
"""Return True when any firmware error bit is set."""
|
||||
return self.state != DeviceState.OK
|
||||
|
||||
@property
|
||||
def error_type(self) -> Optional[str]:
|
||||
"""Get human-readable error type."""
|
||||
if not self.has_error:
|
||||
return None
|
||||
def is_ok(self) -> bool:
|
||||
"""Convenience alias for the common no-error case."""
|
||||
return not self.has_error
|
||||
|
||||
error_map = {
|
||||
DeviceState.ERROR_OVERHEAT: "Overheating",
|
||||
DeviceState.ERROR_POWER: "Power supply issue",
|
||||
DeviceState.ERROR_COMMUNICATION: "Communication error",
|
||||
DeviceState.ERROR_INVALID_COMMAND: "Invalid command"
|
||||
}
|
||||
return error_map.get(self.state, "Unknown 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]:
|
||||
"""Convert to dictionary representation."""
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""Return a JSON-friendly representation."""
|
||||
return {
|
||||
'state': self.state.value,
|
||||
'state_name': self.state.name,
|
||||
'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,
|
||||
'is_idle': self.is_idle,
|
||||
'is_running': self.is_running,
|
||||
'has_error': self.has_error,
|
||||
'error_type': self.error_type
|
||||
"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
|
||||
class CalibrationData:
|
||||
"""Calibration data for device sensors."""
|
||||
# Temperature calibration coefficients
|
||||
temp1_offset: float = 0.0
|
||||
temp1_scale: float = 1.0
|
||||
temp2_offset: float = 0.0
|
||||
temp2_scale: float = 1.0
|
||||
# Current calibration coefficients
|
||||
current1_offset: float = 0.0
|
||||
current1_scale: float = 1.0
|
||||
current2_offset: float = 0.0
|
||||
current2_scale: float = 1.0
|
||||
# Voltage calibration
|
||||
voltage_3v3_scale: float = 1.0
|
||||
voltage_5v1_scale: float = 1.0
|
||||
voltage_5v2_scale: float = 1.0
|
||||
voltage_7v0_scale: float = 1.0
|
||||
@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 = ""
|
||||
|
||||
Reference in New Issue
Block a user