Add new PyQt UI

This commit is contained in:
Ayzen
2026-04-26 18:39:55 +03:00
parent c92745d2bc
commit 0ec504ffa9
33 changed files with 3284 additions and 3789 deletions

View File

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