Files
scanengine-3/helios_test_app.py
T
Thomas Ales 67aabde4b6 Phase 1c: prune remaining dead functions, unused imports, quarantine Genesis
- uc480_camera: drop never-called _capture_paused/get_framerate (the
  hardware question _capture_paused encoded is now in KNOWN_ISSUES.md)
- t3r_protocol: drop read_reg/write_reg/decode_reg/Reg (commands never
  wired into the driver)
- bbd20x: drop _update0x0212 (never dispatched) and 8 of 9 unused
  trigger convenience wrappers; apt_constants: drop TriggerBitsStepper
  (servo-only rig)
- ruff --fix: 35 unused imports across all apps; drop unused T3R_BAUD
- genesis_core.py: quarantine warning header; docs/genesis_verification.md
  bench checklist for the 7 divergences vs tools/genesis_laser_gui.py

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 10:08:48 -05:00

984 lines
37 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Helios Laser Test Application
Simple PyQt6 GUI for testing and controlling the Helios laser.
"""
import sys
import logging
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QGroupBox, QLabel, QLineEdit, QPushButton, QComboBox, QSpinBox,
QMessageBox, QTabWidget, QTextEdit
)
from PyQt6.QtCore import QThread, pyqtSignal, QObject
from PyQt6.QtGui import QFont
from hardware.helios_laser import HeliosLaser, PulseMode
# ---------------------------------------------------------------------------
# Status register bit definitions (Tables 8-1, 8-2, 8-3 — Helios manual)
# Each entry: bit_number -> (severity, description, comment)
# severity: 'C' = critical error, 'S' = status, 'I' = input error, '' = none
# ---------------------------------------------------------------------------
_LER_FLAGS = {
0: ('C', 'Controller temperature failure (resonator/SHG/q-switch)',
'Check CCE register for details'),
1: ('S', 'Trigger input active',
'High when trigger signal applied or laser in continuous pulsing'),
2: ('I', 'Command error',
'Unknown command sent to controller'),
3: ('C', 'Laser disable pin open (utility connector)',
'Shuts down pump diodes; reset LER 0 required to restart'),
4: ('C', 'Internal hardware failure',
'Contact Coherent'),
5: ('C', 'Over voltage laser diode',
'Check for open circuit or voltage spikes'),
6: ('C', 'Internal hardware failure',
'Contact Coherent'),
7: ('C', 'Controller temperature failure at pump diodes',
'Check LCE register for details'),
8: ('S', 'Laser start delay (60 s warmup)',
'Laser cannot be started yet; status error LED flashing'),
9: ('C', 'Internal hardware failure',
'Check environment for strong EMI; contact Coherent'),
10: ('C', 'Internal hardware failure',
'Check environment for strong EMI; contact Coherent'),
11: ('C', 'Internal hardware failure',
'Check environment for strong EMI; contact Coherent'),
12: ('S', 'Slave controller error (remote input)',
'Valid only for master controller coupled with a slave'),
13: ('', 'Laserhead not found',
'Head not connected / not found; check EMI; ignore for double-electronic slave'),
14: ('', 'Laserhead I\u00b2C acknowledge error',
'Check environment for strong EMI; ignore for double-electronic slave'),
15: ('S', 'Range-Error (not critical)',
'Input value out of range'),
}
_LCE_FLAGS = {
0: ('C', 'Pump diode over/under temperature',
'Limit exceeded (<10\u00b0C or >60\u00b0C); check head cooling'),
1: ('C', 'Internal hardware failure',
'Contact Coherent'),
2: ('C', 'Pump diode temperature out of range',
'Actual temp >2\u00b0C off setpoint for >1 min'),
3: ('C', 'Pump diode current critical',
'Current set too close to current limit'),
4: ('C', 'Pump diode temperature out of limit',
'Pump diode temperature is out of limit'),
5: ('S', 'Door switch open',
'Close utility connector pin 2 permanently to pin 9 (GND)'),
7: ('C', 'Pump diode NTC error',
'Invalid temperature measured or NTC broken'),
8: ('C', 'Laser diode power stage over temperature',
'Temp <10\u00b0C or >65\u00b0C at controller; check controller cooling'),
9: ('C', 'Internal hardware failure',
'Check environment for strong EMI; contact Coherent'),
10: ('C', 'Internal hardware failure',
'Check environment for strong EMI; contact Coherent'),
11: ('C', 'Internal hardware failure',
'Check environment for strong EMI; contact Coherent'),
15: ('S', 'Range-Error (not critical)',
'Input value out of range'),
}
_CCE_FLAGS = {
0: ('C', 'Resonator/SHG under/over temperature',
'Limit exceeded (<10\u00b0C or >60\u00b0C); temperature controller deactivated'),
1: ('C', 'Resonator/SHG NTC failure',
'Temperature sensor broken or disconnected'),
2: ('C', 'Resonator/SHG temperature out of range',
'Actual temp >2\u00b0C off setpoint for >1 min'),
3: ('C', 'Q-switch ADC / temperature readout failure',
'Internal hardware error or no NTC connected'),
4: ('C', 'Q-switch temperature out of range',
'Actual temp >2\u00b0C off setpoint for >1 min'),
5: ('C', 'Q-switch under/over temperature',
'Limit exceeded (<10\u00b0C or >60\u00b0C); temperature controller deactivated'),
7: ('C', 'Q-switch NTC failure',
'Internal hardware error or no NTC connected'),
8: ('C', 'Internal hardware failure',
'Contact Coherent'),
15: ('S', 'Range-Error (not critical)',
'Input value out of range'),
}
_SEVERITY_LABEL = {'C': '[CRIT]', 'S': '[STAT]', 'I': '[INPT]', '': '[INFO]'}
def _decode_register(flags_dict: dict, value: int) -> list:
"""Return list of (bit, severity, description, comment) for each set bit."""
active = []
for bit, (sev, desc, comment) in flags_dict.items():
if value & (1 << bit):
active.append((bit, sev, desc, comment))
return active
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class LaserWorker(QObject):
"""Worker thread for laser operations to prevent UI blocking."""
# Signals
operation_complete = pyqtSignal(bool, str) # success, message
frequency_updated = pyqtSignal(int)
current_updated = pyqtSignal(int)
power_updated = pyqtSignal(float)
enabled_updated = pyqtSignal(bool)
serial_updated = pyqtSignal(str, str) # controller_sn, head_sn
status_registers_updated = pyqtSignal(object, object, object) # ler, lce, cce (int or None)
remote_enable_updated = pyqtSignal(object) # bool or None
raw_response_received = pyqtSignal(str, str) # command, response
def __init__(self, laser: HeliosLaser):
super().__init__()
self.laser = laser
def set_frequency(self, freq: int):
try:
success = self.laser.set_frequency_hz(freq)
msg = f"Frequency set to {freq} Hz" if success else "Failed to set frequency"
self.operation_complete.emit(success, msg)
except Exception as e:
self.operation_complete.emit(False, str(e))
def set_current(self, current: int):
try:
success = self.laser.set_current_ma(current)
msg = f"Current set to {current} mA" if success else "Failed to set current"
self.operation_complete.emit(success, msg)
except Exception as e:
self.operation_complete.emit(False, str(e))
def set_pulse_mode(self, mode: int):
try:
pulse_mode = PulseMode(mode)
success = self.laser.set_pulse_mode(pulse_mode)
msg = f"Pulse mode set to {pulse_mode.name}" if success else "Failed to set pulse mode"
self.operation_complete.emit(success, msg)
except Exception as e:
self.operation_complete.emit(False, str(e))
def set_laser_enable(self, enable: bool):
try:
success = self.laser.set_laser_enable(enable)
state = "enabled" if enable else "disabled"
msg = f"Laser {state}" if success else f"Failed to {state} laser"
self.operation_complete.emit(success, msg)
except Exception as e:
self.operation_complete.emit(False, str(e))
def query_frequency(self):
try:
freq = self.laser.get_frequency_hz()
if freq is not None:
self.frequency_updated.emit(freq)
self.operation_complete.emit(True, f"Frequency: {freq} Hz")
else:
self.operation_complete.emit(False, "Failed to query frequency")
except Exception as e:
self.operation_complete.emit(False, str(e))
def query_current(self):
try:
current = self.laser.get_current_ma()
if current is not None:
self.current_updated.emit(current)
self.operation_complete.emit(True, f"Current: {current} mA")
else:
self.operation_complete.emit(False, "Failed to query current")
except Exception as e:
self.operation_complete.emit(False, str(e))
def query_power(self):
try:
power = self.laser.get_power_mw()
if power is not None:
self.power_updated.emit(power)
self.operation_complete.emit(True, f"Power: {power:.2f} mW")
else:
self.operation_complete.emit(False, "Failed to query power")
except Exception as e:
self.operation_complete.emit(False, str(e))
def query_enabled(self):
try:
enabled = self.laser.is_laser_enabled()
self.enabled_updated.emit(enabled)
state = "enabled" if enabled else "disabled"
self.operation_complete.emit(True, f"Laser is {state}")
except Exception as e:
self.operation_complete.emit(False, str(e))
def query_serials(self):
try:
controller_sn = self.laser.get_controller_serial()
head_sn = self.laser.get_head_serial()
if controller_sn and head_sn:
self.serial_updated.emit(controller_sn, head_sn)
msg = f"Controller: {controller_sn}, Head: {head_sn}"
self.operation_complete.emit(True, msg)
else:
self.operation_complete.emit(False, "Failed to query serial numbers")
except Exception as e:
self.operation_complete.emit(False, str(e))
def query_status_registers(self):
try:
ler, lce, cce = self.laser.get_status_registers()
self.status_registers_updated.emit(ler, lce, cce)
self.operation_complete.emit(True, f"Status: LER={ler} LCE={lce} CCE={cce}")
except Exception as e:
self.operation_complete.emit(False, str(e))
def query_remote_enable(self):
try:
state = self.laser.get_remote_enable()
self.remote_enable_updated.emit(state)
if state is not None:
self.operation_complete.emit(True, f"Remote enable (LRE): {'ON' if state else 'OFF'}")
else:
self.operation_complete.emit(False, "Failed to query remote enable")
except Exception as e:
self.operation_complete.emit(False, str(e))
def set_remote_enable(self, enable: bool):
try:
success = self.laser.set_remote_enable(enable)
state = "ON" if enable else "OFF"
msg = f"Remote enable (LRE) set to {state}" if success else "Failed to set remote enable"
self.operation_complete.emit(success, msg)
except Exception as e:
self.operation_complete.emit(False, str(e))
def do_reset_faults(self):
try:
success = self.laser.reset_faults()
msg = "Fault reset sequence sent (CCE 0 → LCE 0 → LER 0)" if success else "Failed to send reset sequence"
self.operation_complete.emit(success, msg)
except Exception as e:
self.operation_complete.emit(False, str(e))
def do_ler_reset(self):
try:
success = self.laser._send_command("LER 0")
msg = "LER 0 sent" if success else "Failed to send LER 0"
self.operation_complete.emit(success, msg)
except Exception as e:
self.operation_complete.emit(False, str(e))
def send_raw(self, cmd: str):
try:
response = self.laser.send_raw_command(cmd)
if response is not None:
self.raw_response_received.emit(cmd, response)
self.operation_complete.emit(True, f"TX: {cmd!r} RX: {response!r}")
else:
self.operation_complete.emit(False, f"No response for: {cmd!r}")
except Exception as e:
self.operation_complete.emit(False, str(e))
class HeliosTestApp(QMainWindow):
"""Main application window for Helios laser testing."""
def __init__(self):
super().__init__()
self.laser = HeliosLaser()
self.worker = None
self.worker_thread = None
self.init_ui()
self.update_port_list()
def init_ui(self):
"""Initialize the user interface."""
self.setWindowTitle("Helios Laser Test Application")
self.setGeometry(100, 100, 900, 700)
# Main widget and layout
main_widget = QWidget()
self.setCentralWidget(main_widget)
main_layout = QVBoxLayout(main_widget)
# Connection group
connection_group = self.create_connection_group()
main_layout.addWidget(connection_group)
# Tabs for control and monitoring
tabs = QTabWidget()
tabs.addTab(self.create_control_tab(), "Control")
tabs.addTab(self.create_monitor_tab(), "Monitor")
tabs.addTab(self.create_terminal_tab(), "Terminal")
main_layout.addWidget(tabs)
# Status bar
self.statusBar().showMessage("Disconnected")
def create_connection_group(self) -> QGroupBox:
"""Create the connection control group."""
group = QGroupBox("Connection")
layout = QHBoxLayout()
# Port selection
layout.addWidget(QLabel("Serial Port:"))
self.combo_port = QComboBox()
self.combo_port.setMinimumWidth(150)
layout.addWidget(self.combo_port)
# Refresh ports button
btn_refresh = QPushButton("Refresh Ports")
btn_refresh.clicked.connect(self.update_port_list)
layout.addWidget(btn_refresh)
# Connect button
self.btn_connect = QPushButton("Connect")
self.btn_connect.clicked.connect(self.toggle_connection)
self.btn_connect.setMinimumWidth(100)
layout.addWidget(self.btn_connect)
# Connection status
self.lbl_status = QLabel("Status: Disconnected")
font = self.lbl_status.font()
font.setBold(True)
self.lbl_status.setFont(font)
layout.addWidget(self.lbl_status)
layout.addStretch()
group.setLayout(layout)
return group
def create_control_tab(self) -> QWidget:
"""Create the control tab."""
widget = QWidget()
layout = QVBoxLayout(widget)
# Frequency control
freq_group = QGroupBox("Frequency Control")
freq_layout = QHBoxLayout()
freq_layout.addWidget(QLabel("Frequency (Hz):"))
self.spin_frequency = QSpinBox()
self.spin_frequency.setRange(16700, 125000)
self.spin_frequency.setValue(50000)
self.spin_frequency.setSingleStep(1000)
freq_layout.addWidget(self.spin_frequency)
btn_set_freq = QPushButton("Set Frequency")
btn_set_freq.clicked.connect(self.on_set_frequency)
btn_set_freq.setMaximumWidth(150)
freq_layout.addWidget(btn_set_freq)
btn_get_freq = QPushButton("Query")
btn_get_freq.clicked.connect(self.on_query_frequency)
btn_get_freq.setMaximumWidth(100)
freq_layout.addWidget(btn_get_freq)
freq_layout.addStretch()
freq_group.setLayout(freq_layout)
layout.addWidget(freq_group)
# Current control
current_group = QGroupBox("Current Control")
current_layout = QHBoxLayout()
current_layout.addWidget(QLabel("Current (mA):"))
self.spin_current = QSpinBox()
self.spin_current.setRange(0, 2000)
self.spin_current.setValue(1000)
self.spin_current.setSingleStep(100)
current_layout.addWidget(self.spin_current)
btn_set_current = QPushButton("Set Current")
btn_set_current.clicked.connect(self.on_set_current)
btn_set_current.setMaximumWidth(150)
current_layout.addWidget(btn_set_current)
btn_get_current = QPushButton("Query")
btn_get_current.clicked.connect(self.on_query_current)
btn_get_current.setMaximumWidth(100)
current_layout.addWidget(btn_get_current)
current_layout.addStretch()
current_group.setLayout(current_layout)
layout.addWidget(current_group)
# Pulse mode control
mode_group = QGroupBox("Pulse Mode Control")
mode_layout = QHBoxLayout()
mode_layout.addWidget(QLabel("Pulse Mode:"))
self.combo_mode = QComboBox()
self.combo_mode.addItem("Single Pulse", PulseMode.SINGLE_PULSE.value)
self.combo_mode.addItem("Continuous Gating", PulseMode.CONTINUOUS_GATING.value)
self.combo_mode.addItem("Continuous Pulsing", PulseMode.CONTINUOUS_PULSING.value)
mode_layout.addWidget(self.combo_mode)
btn_set_mode = QPushButton("Set Mode")
btn_set_mode.clicked.connect(self.on_set_mode)
btn_set_mode.setMaximumWidth(150)
mode_layout.addWidget(btn_set_mode)
mode_layout.addStretch()
mode_group.setLayout(mode_layout)
layout.addWidget(mode_group)
# Laser enable/disable
enable_group = QGroupBox("Laser Control")
enable_layout = QHBoxLayout()
self.btn_enable = QPushButton("Enable Laser")
self.btn_enable.setStyleSheet("background-color: lightgreen")
self.btn_enable.clicked.connect(self.on_enable_laser)
self.btn_enable.setMaximumWidth(150)
enable_layout.addWidget(self.btn_enable)
self.btn_disable = QPushButton("Disable Laser")
self.btn_disable.setStyleSheet("background-color: lightcoral")
self.btn_disable.clicked.connect(self.on_disable_laser)
self.btn_disable.setMaximumWidth(150)
enable_layout.addWidget(self.btn_disable)
btn_check_enabled = QPushButton("Query Status")
btn_check_enabled.clicked.connect(self.on_query_enabled)
btn_check_enabled.setMaximumWidth(150)
enable_layout.addWidget(btn_check_enabled)
self.lbl_enabled = QLabel("Status: Unknown")
font = self.lbl_enabled.font()
font.setBold(True)
self.lbl_enabled.setFont(font)
enable_layout.addWidget(self.lbl_enabled)
enable_layout.addStretch()
enable_group.setLayout(enable_layout)
layout.addWidget(enable_group)
# Status & Safety
safety_group = QGroupBox("Status && Safety")
safety_layout = QVBoxLayout()
# Row 1: status registers + query/reset buttons
reg_row = QHBoxLayout()
btn_query_status = QPushButton("Query Status")
btn_query_status.clicked.connect(self.on_query_status)
btn_query_status.setMaximumWidth(130)
reg_row.addWidget(btn_query_status)
btn_reset_faults = QPushButton("Reset Faults")
btn_reset_faults.setStyleSheet("background-color: #FFD700")
btn_reset_faults.clicked.connect(self.on_reset_faults)
btn_reset_faults.setMaximumWidth(130)
reg_row.addWidget(btn_reset_faults)
btn_ler_reset = QPushButton("LER 0 Reset")
btn_ler_reset.setStyleSheet("background-color: #FFA500")
btn_ler_reset.setToolTip("Send LER 0 only (clears controller error register)")
btn_ler_reset.clicked.connect(self.on_ler_reset)
btn_ler_reset.setMaximumWidth(130)
reg_row.addWidget(btn_ler_reset)
reg_row.addSpacing(20)
reg_row.addWidget(QLabel("LER:"))
self.lbl_ler = QLabel("—")
bold_font = self.lbl_ler.font()
bold_font.setBold(True)
self.lbl_ler.setFont(bold_font)
reg_row.addWidget(self.lbl_ler)
reg_row.addWidget(QLabel("LCE:"))
self.lbl_lce = QLabel("—")
self.lbl_lce.setFont(bold_font)
reg_row.addWidget(self.lbl_lce)
reg_row.addWidget(QLabel("CCE:"))
self.lbl_cce = QLabel("—")
self.lbl_cce.setFont(bold_font)
reg_row.addWidget(self.lbl_cce)
reg_row.addSpacing(20)
reg_row.addWidget(QLabel("Interlock:"))
self.lbl_interlock = QLabel("UNKNOWN")
self.lbl_interlock.setFont(bold_font)
self.lbl_interlock.setMinimumWidth(80)
reg_row.addWidget(self.lbl_interlock)
reg_row.addStretch()
safety_layout.addLayout(reg_row)
# Row 2: remote enable (LRE / pin 8)
lre_row = QHBoxLayout()
btn_lre_on = QPushButton("Remote Enable ON")
btn_lre_on.setStyleSheet("background-color: lightgreen")
btn_lre_on.clicked.connect(lambda: self.on_set_remote_enable(True))
btn_lre_on.setMaximumWidth(160)
lre_row.addWidget(btn_lre_on)
btn_lre_off = QPushButton("Remote Enable OFF")
btn_lre_off.setStyleSheet("background-color: lightcoral")
btn_lre_off.clicked.connect(lambda: self.on_set_remote_enable(False))
btn_lre_off.setMaximumWidth(160)
lre_row.addWidget(btn_lre_off)
btn_lre_query = QPushButton("Query LRE")
btn_lre_query.clicked.connect(self.on_query_remote_enable)
btn_lre_query.setMaximumWidth(120)
lre_row.addWidget(btn_lre_query)
lre_row.addSpacing(20)
lre_row.addWidget(QLabel("Enable Pin (LRE):"))
self.lbl_lre = QLabel("UNKNOWN")
self.lbl_lre.setFont(bold_font)
self.lbl_lre.setMinimumWidth(80)
lre_row.addWidget(self.lbl_lre)
lre_row.addStretch()
safety_layout.addLayout(lre_row)
# Row 3: decoded register flags
self.text_register_decode = QTextEdit()
self.text_register_decode.setReadOnly(True)
self.text_register_decode.setMinimumHeight(110)
self.text_register_decode.setMaximumHeight(160)
self.text_register_decode.setPlaceholderText(
"Register flags will appear here after querying status…"
)
mono_font = QFont("Monospace")
mono_font.setStyleHint(QFont.StyleHint.Monospace)
self.text_register_decode.setFont(mono_font)
safety_layout.addWidget(self.text_register_decode)
safety_group.setLayout(safety_layout)
layout.addWidget(safety_group)
layout.addStretch()
return widget
def create_monitor_tab(self) -> QWidget:
"""Create the monitoring/information tab."""
widget = QWidget()
layout = QVBoxLayout(widget)
# System information
info_group = QGroupBox("System Information")
info_layout = QHBoxLayout()
btn_query_all = QPushButton("Query All")
btn_query_all.clicked.connect(self.on_query_all)
info_layout.addWidget(btn_query_all)
info_layout.addWidget(QLabel("Controller SN:"))
self.lbl_controller_sn = QLineEdit()
self.lbl_controller_sn.setReadOnly(True)
info_layout.addWidget(self.lbl_controller_sn)
info_layout.addWidget(QLabel("Head SN:"))
self.lbl_head_sn = QLineEdit()
self.lbl_head_sn.setReadOnly(True)
info_layout.addWidget(self.lbl_head_sn)
info_group.setLayout(info_layout)
layout.addWidget(info_group)
# Power monitoring
power_group = QGroupBox("Power Monitoring")
power_layout = QHBoxLayout()
btn_get_power = QPushButton("Query Power")
btn_get_power.clicked.connect(self.on_query_power)
power_layout.addWidget(btn_get_power)
power_layout.addWidget(QLabel("Output Power:"))
self.lbl_power = QLineEdit()
self.lbl_power.setReadOnly(True)
self.lbl_power.setMaximumWidth(150)
power_layout.addWidget(self.lbl_power)
power_layout.addWidget(QLabel("mW"))
power_layout.addStretch()
power_group.setLayout(power_layout)
layout.addWidget(power_group)
# Log/Message display
log_group = QGroupBox("Messages")
log_layout = QVBoxLayout()
self.text_log = QTextEdit()
self.text_log.setReadOnly(True)
self.text_log.setMaximumHeight(300)
log_layout.addWidget(self.text_log)
log_group.setLayout(log_layout)
layout.addWidget(log_group)
layout.addStretch()
return widget
def create_terminal_tab(self) -> QWidget:
"""Create the raw command terminal tab."""
widget = QWidget()
layout = QVBoxLayout(widget)
# Input row
cmd_group = QGroupBox("Raw Command")
cmd_layout = QHBoxLayout()
cmd_layout.addWidget(QLabel("Command:"))
self.le_raw_cmd = QLineEdit()
self.le_raw_cmd.setPlaceholderText("e.g. LDF or LDS 1000 or LER 0")
self.le_raw_cmd.returnPressed.connect(self.on_send_raw)
cmd_layout.addWidget(self.le_raw_cmd)
btn_send_raw = QPushButton("Send")
btn_send_raw.setMaximumWidth(80)
btn_send_raw.clicked.connect(self.on_send_raw)
cmd_layout.addWidget(btn_send_raw)
btn_clear_terminal = QPushButton("Clear")
btn_clear_terminal.setMaximumWidth(70)
btn_clear_terminal.clicked.connect(lambda: self.text_terminal.clear())
cmd_layout.addWidget(btn_clear_terminal)
cmd_group.setLayout(cmd_layout)
layout.addWidget(cmd_group)
# Terminal output
term_group = QGroupBox("Response Log")
term_layout = QVBoxLayout()
self.text_terminal = QTextEdit()
self.text_terminal.setReadOnly(True)
mono_font = QFont("Monospace")
mono_font.setStyleHint(QFont.StyleHint.Monospace)
self.text_terminal.setFont(mono_font)
self.text_terminal.setPlaceholderText(
"TX/RX pairs will appear here.\n"
"Commands are sent as-is with a trailing CR.\n"
"No ? suffix needed — send the command name to query (e.g. LDF)."
)
term_layout.addWidget(self.text_terminal)
term_group.setLayout(term_layout)
layout.addWidget(term_group)
return widget
def update_port_list(self):
"""Update the available serial ports."""
self.combo_port.clear()
try:
ports = HeliosLaser.list_available_ports()
if ports:
self.combo_port.addItems(ports)
else:
self.combo_port.addItem("No ports found")
except Exception as e:
self.combo_port.addItem(f"Error: {e}")
logger.error(f"Error listing ports: {e}")
def toggle_connection(self):
"""Toggle connection to the laser."""
if self.laser.is_connected:
self.disconnect_laser()
else:
self.connect_laser()
def connect_laser(self):
"""Connect to the laser."""
port = self.combo_port.currentText()
if not port or "No ports" in port or "Error" in port:
QMessageBox.warning(self, "Connection Error", "No valid port selected")
return
if self.laser.connect(port):
self.lbl_status.setText(f"Status: Connected to {port}")
self.lbl_status.setStyleSheet("color: green")
self.btn_connect.setText("Disconnect")
self.combo_port.setEnabled(False)
# Create worker for async operations
self.worker = LaserWorker(self.laser)
self.worker_thread = QThread()
self.worker.moveToThread(self.worker_thread)
self.worker.operation_complete.connect(self.on_operation_complete)
self.worker.frequency_updated.connect(self.on_frequency_updated)
self.worker.current_updated.connect(self.on_current_updated)
self.worker.power_updated.connect(self.on_power_updated)
self.worker.enabled_updated.connect(self.on_enabled_updated)
self.worker.serial_updated.connect(self.on_serial_updated)
self.worker.status_registers_updated.connect(self.on_status_registers_updated)
self.worker.remote_enable_updated.connect(self.on_remote_enable_updated)
self.worker.raw_response_received.connect(self.on_raw_response)
self.worker_thread.start()
self.statusBar().showMessage(f"Connected to {port}")
logger.info(f"Connected to {port}")
else:
QMessageBox.critical(self, "Connection Error", f"Failed to connect to {port}")
def disconnect_laser(self):
"""Disconnect from the laser."""
if self.worker_thread:
self.worker_thread.quit()
self.worker_thread.wait()
self.laser.disconnect()
self.lbl_status.setText("Status: Disconnected")
self.lbl_status.setStyleSheet("color: red")
self.btn_connect.setText("Connect")
self.combo_port.setEnabled(True)
self.statusBar().showMessage("Disconnected")
logger.info("Disconnected from laser")
def on_set_frequency(self):
"""Set the laser frequency."""
if not self.laser.is_connected:
QMessageBox.warning(self, "Error", "Not connected to laser")
return
freq = self.spin_frequency.value()
self.worker.set_frequency(freq)
def on_query_frequency(self):
"""Query the laser frequency."""
if not self.laser.is_connected:
QMessageBox.warning(self, "Error", "Not connected to laser")
return
self.worker.query_frequency()
def on_set_current(self):
"""Set the laser current."""
if not self.laser.is_connected:
QMessageBox.warning(self, "Error", "Not connected to laser")
return
current = self.spin_current.value()
self.worker.set_current(current)
def on_query_current(self):
"""Query the laser current."""
if not self.laser.is_connected:
QMessageBox.warning(self, "Error", "Not connected to laser")
return
self.worker.query_current()
def on_set_mode(self):
"""Set the laser pulse mode."""
if not self.laser.is_connected:
QMessageBox.warning(self, "Error", "Not connected to laser")
return
mode = self.combo_mode.currentData()
self.worker.set_pulse_mode(mode)
def on_enable_laser(self):
"""Enable the laser."""
if not self.laser.is_connected:
QMessageBox.warning(self, "Error", "Not connected to laser")
return
self.worker.set_laser_enable(True)
def on_disable_laser(self):
"""Disable the laser."""
if not self.laser.is_connected:
QMessageBox.warning(self, "Error", "Not connected to laser")
return
self.worker.set_laser_enable(False)
def on_query_enabled(self):
"""Query if laser is enabled."""
if not self.laser.is_connected:
QMessageBox.warning(self, "Error", "Not connected to laser")
return
self.worker.query_enabled()
def on_query_power(self):
"""Query the laser output power."""
if not self.laser.is_connected:
QMessageBox.warning(self, "Error", "Not connected to laser")
return
self.worker.query_power()
def on_query_all(self):
"""Query all laser parameters."""
if not self.laser.is_connected:
QMessageBox.warning(self, "Error", "Not connected to laser")
return
self.worker.query_serials()
self.worker.query_frequency()
self.worker.query_current()
self.worker.query_power()
self.worker.query_enabled()
self.worker.query_status_registers()
self.worker.query_remote_enable()
def on_query_status(self):
"""Query LER/LCE/CCE status registers."""
if not self.laser.is_connected:
QMessageBox.warning(self, "Error", "Not connected to laser")
return
self.worker.query_status_registers()
def on_reset_faults(self):
"""Send the fault reset sequence."""
if not self.laser.is_connected:
QMessageBox.warning(self, "Error", "Not connected to laser")
return
self.worker.do_reset_faults()
def on_query_remote_enable(self):
"""Query the remote enable (LRE) state."""
if not self.laser.is_connected:
QMessageBox.warning(self, "Error", "Not connected to laser")
return
self.worker.query_remote_enable()
def on_set_remote_enable(self, enable: bool):
"""Set the remote enable (LRE) state."""
if not self.laser.is_connected:
QMessageBox.warning(self, "Error", "Not connected to laser")
return
self.worker.set_remote_enable(enable)
def on_ler_reset(self):
"""Send LER 0 only."""
if not self.laser.is_connected:
QMessageBox.warning(self, "Error", "Not connected to laser")
return
self.worker.do_ler_reset()
def on_send_raw(self):
"""Send the raw command from the terminal input."""
if not self.laser.is_connected:
QMessageBox.warning(self, "Error", "Not connected to laser")
return
cmd = self.le_raw_cmd.text().strip()
if not cmd:
return
self.worker.send_raw(cmd)
def on_raw_response(self, cmd: str, response: str):
"""Display raw TX/RX pair in the terminal log."""
self.text_terminal.append(f"TX: {cmd}")
self.text_terminal.append(f"RX: {response if response else '<no response>'}")
self.text_terminal.append("")
self.text_terminal.verticalScrollBar().setValue(
self.text_terminal.verticalScrollBar().maximum()
)
def on_operation_complete(self, success: bool, message: str):
"""Handle operation completion."""
self.log_message(message)
if not success:
QMessageBox.warning(self, "Operation Failed", message)
def on_frequency_updated(self, freq: int):
"""Update frequency display."""
self.spin_frequency.setValue(freq)
def on_current_updated(self, current: int):
"""Update current display."""
self.spin_current.setValue(current)
def on_power_updated(self, power: float):
"""Update power display."""
self.lbl_power.setText(f"{power:.2f}")
def on_enabled_updated(self, enabled: bool):
"""Update enabled status display."""
state = "Enabled" if enabled else "Disabled"
color = "green" if enabled else "red"
self.lbl_enabled.setText(f"Status: {state}")
self.lbl_enabled.setStyleSheet(f"color: {color}")
def on_serial_updated(self, controller_sn: str, head_sn: str):
"""Update serial number displays."""
self.lbl_controller_sn.setText(controller_sn)
self.lbl_head_sn.setText(head_sn)
def on_status_registers_updated(self, ler, lce, cce):
"""Update status register displays, interlock indicator, and decoded flags."""
def _fmt(val):
return str(val) if val is not None else "ERR"
self.lbl_ler.setText(_fmt(ler))
self.lbl_lce.setText(_fmt(lce))
self.lbl_cce.setText(_fmt(cce))
# Interlock fault: any non-zero register value indicates an active fault
if any(v is None for v in (ler, lce, cce)):
self.lbl_interlock.setText("UNKNOWN")
self.lbl_interlock.setStyleSheet("color: gray")
elif any(v != 0 for v in (ler, lce, cce)):
self.lbl_interlock.setText("FAULT")
self.lbl_interlock.setStyleSheet("color: red; font-weight: bold")
else:
self.lbl_interlock.setText("OK")
self.lbl_interlock.setStyleSheet("color: green; font-weight: bold")
# Decode and display individual flags
lines = []
for reg_name, value, flags_dict in (
("LER", ler, _LER_FLAGS),
("LCE", lce, _LCE_FLAGS),
("CCE", cce, _CCE_FLAGS),
):
if value is None:
lines.append(f"{reg_name}: <read error>")
continue
active = _decode_register(flags_dict, value)
if not active:
lines.append(f"{reg_name} (raw={value}): OK — no flags set")
else:
lines.append(f"{reg_name} (raw={value}):")
for bit, sev, desc, comment in active:
label = _SEVERITY_LABEL.get(sev, '[ ]')
lines.append(f" {label} bit {bit:2d} ({1 << bit:>5}): {desc}")
lines.append(f" → {comment}")
self.text_register_decode.setPlainText("\n".join(lines))
def on_remote_enable_updated(self, state):
"""Update remote enable (LRE) indicator."""
if state is None:
self.lbl_lre.setText("UNKNOWN")
self.lbl_lre.setStyleSheet("color: gray")
elif state:
self.lbl_lre.setText("ACTIVE")
self.lbl_lre.setStyleSheet("color: green; font-weight: bold")
else:
self.lbl_lre.setText("INACTIVE")
self.lbl_lre.setStyleSheet("color: red; font-weight: bold")
def log_message(self, message: str):
"""Add message to log."""
self.text_log.append(message)
# Auto-scroll to bottom
self.text_log.verticalScrollBar().setValue(
self.text_log.verticalScrollBar().maximum()
)
def closeEvent(self, event):
"""Handle application close."""
if self.laser.is_connected:
self.disconnect_laser()
if self.worker_thread and self.worker_thread.isRunning():
self.worker_thread.quit()
self.worker_thread.wait()
event.accept()
def main():
"""Run the application."""
app = QApplication(sys.argv)
window = HeliosTestApp()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()