67aabde4b6
- 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>
822 lines
30 KiB
Python
Executable File
822 lines
30 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Genesis SLM MX 532 Laser Control Application
|
|
=============================================
|
|
|
|
This application provides comprehensive control of a Genesis SLM MX 532 laser
|
|
over serial port using NXP I2C-over-serial protocol.
|
|
|
|
Protocol Overview:
|
|
-----------------
|
|
The laser uses NXP I2C-over-serial protocol with packet format:
|
|
[0x53] [I2C_ADDR] [LENGTH] [COMMAND_BYTES] [DATA_BYTES] [0x50]
|
|
|
|
For reads:
|
|
[0x53] [ADDR_WRITE] [CMD_LEN] [CMD] [0x53] [ADDR_READ] [DATA_LEN] [0x50]
|
|
|
|
I2C Devices:
|
|
-----------
|
|
- X9119 digital potentiometer at 0x152 - laser current control
|
|
- PCA9555 I/O expander at 0x14a - digital I/O
|
|
- AD5254 digital potentiometer at 0x158 - limits control
|
|
- ADS7828 ADC at 0x190 - sensor readings
|
|
- M24C64 EEPROM at 0x2a4 - configuration storage
|
|
|
|
Usage:
|
|
------
|
|
python genesis_laser_control.py
|
|
|
|
Author: Claude
|
|
Date: 2026-01-24
|
|
"""
|
|
|
|
import sys
|
|
|
|
from serial.tools import list_ports
|
|
from PyQt6.QtWidgets import (
|
|
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
|
QTabWidget, QLabel, QSlider, QPushButton, QSpinBox, QCheckBox,
|
|
QComboBox, QTextEdit, QLineEdit, QGroupBox, QGridLayout,
|
|
QMessageBox, QStatusBar
|
|
)
|
|
from PyQt6.QtCore import Qt, QTimer, QSettings
|
|
from PyQt6.QtGui import QFont
|
|
|
|
# Import core hardware control classes
|
|
from hardware.genesis_core import (
|
|
SerialComm, I2CProtocol, I2CDevices, LaserControl
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# Main Window GUI
|
|
# ============================================================================
|
|
|
|
class MainWindow(QMainWindow):
|
|
"""Main application window with tabbed interface"""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
# Initialize backend
|
|
self.serial_comm = SerialComm()
|
|
self.i2c_protocol = I2CProtocol(self.serial_comm)
|
|
self.i2c_devices = I2CDevices(self.i2c_protocol)
|
|
self.laser_control = LaserControl(self.i2c_devices)
|
|
|
|
# Settings for persistence
|
|
self.settings = QSettings("Genesis", "LaserControl")
|
|
|
|
# Monitoring timer
|
|
self.monitor_timer = QTimer()
|
|
self.monitor_timer.timeout.connect(self.update_monitoring)
|
|
self.monitor_enabled = False
|
|
|
|
# Initialize UI
|
|
self.init_ui()
|
|
self.apply_stylesheet()
|
|
|
|
# Restore settings
|
|
self.restore_settings()
|
|
|
|
# Initial state: all controls disabled
|
|
self.update_control_states()
|
|
|
|
def init_ui(self):
|
|
"""Initialize user interface"""
|
|
self.setWindowTitle("Genesis SLM MX 532 Laser Control")
|
|
self.setMinimumSize(800, 600)
|
|
|
|
# Central widget with tabs
|
|
central_widget = QWidget()
|
|
self.setCentralWidget(central_widget)
|
|
|
|
layout = QVBoxLayout(central_widget)
|
|
|
|
# Tab widget
|
|
self.tabs = QTabWidget()
|
|
layout.addWidget(self.tabs)
|
|
|
|
# Create tabs
|
|
self.tab_basic = self.create_basic_controls_tab()
|
|
self.tab_monitoring = self.create_monitoring_tab()
|
|
self.tab_advanced = self.create_advanced_tab()
|
|
self.tab_config = self.create_configuration_tab()
|
|
|
|
self.tabs.addTab(self.tab_basic, "Basic Controls")
|
|
self.tabs.addTab(self.tab_monitoring, "Monitoring")
|
|
self.tabs.addTab(self.tab_advanced, "Advanced")
|
|
self.tabs.addTab(self.tab_config, "Configuration")
|
|
|
|
# Status bar
|
|
self.status_bar = QStatusBar()
|
|
self.setStatusBar(self.status_bar)
|
|
self.status_bar.showMessage("Disconnected")
|
|
|
|
def create_basic_controls_tab(self) -> QWidget:
|
|
"""Create basic controls tab"""
|
|
tab = QWidget()
|
|
layout = QVBoxLayout(tab)
|
|
|
|
# Current control group
|
|
current_group = QGroupBox("Current Control")
|
|
current_layout = QGridLayout()
|
|
|
|
current_layout.addWidget(QLabel("Current Command (0-1023):"), 0, 0)
|
|
|
|
self.current_slider = QSlider(Qt.Orientation.Horizontal)
|
|
self.current_slider.setMinimum(0)
|
|
self.current_slider.setMaximum(1023)
|
|
self.current_slider.setValue(0)
|
|
self.current_slider.setToolTip("Set laser current (0-1023)")
|
|
self.current_slider.valueChanged.connect(self.on_current_changed)
|
|
current_layout.addWidget(self.current_slider, 0, 1)
|
|
|
|
self.current_spinbox = QSpinBox()
|
|
self.current_spinbox.setMinimum(0)
|
|
self.current_spinbox.setMaximum(1023)
|
|
self.current_spinbox.setValue(0)
|
|
self.current_spinbox.valueChanged.connect(self.current_slider.setValue)
|
|
current_layout.addWidget(self.current_spinbox, 0, 2)
|
|
|
|
self.current_percent_label = QLabel("0.0%")
|
|
current_layout.addWidget(self.current_percent_label, 0, 3)
|
|
|
|
current_group.setLayout(current_layout)
|
|
layout.addWidget(current_group)
|
|
|
|
# Power control group
|
|
power_group = QGroupBox("Power Control")
|
|
power_layout = QGridLayout()
|
|
|
|
power_layout.addWidget(QLabel("Power Command (0-1023):"), 0, 0)
|
|
|
|
self.power_slider = QSlider(Qt.Orientation.Horizontal)
|
|
self.power_slider.setMinimum(0)
|
|
self.power_slider.setMaximum(1023)
|
|
self.power_slider.setValue(0)
|
|
self.power_slider.setToolTip("Set laser power command (0-1023)")
|
|
self.power_slider.valueChanged.connect(self.on_power_changed)
|
|
power_layout.addWidget(self.power_slider, 0, 1)
|
|
|
|
self.power_spinbox = QSpinBox()
|
|
self.power_spinbox.setMinimum(0)
|
|
self.power_spinbox.setMaximum(1023)
|
|
self.power_spinbox.setValue(0)
|
|
self.power_spinbox.valueChanged.connect(self.power_slider.setValue)
|
|
power_layout.addWidget(self.power_spinbox, 0, 2)
|
|
|
|
power_group.setLayout(power_layout)
|
|
layout.addWidget(power_group)
|
|
|
|
# Toggle controls group
|
|
toggle_group = QGroupBox("Digital Controls")
|
|
toggle_layout = QGridLayout()
|
|
|
|
self.shutter_btn = QPushButton("Shutter: CLOSED")
|
|
self.shutter_btn.setCheckable(True)
|
|
self.shutter_btn.setToolTip("Open/close laser shutter")
|
|
self.shutter_btn.clicked.connect(self.on_shutter_toggled)
|
|
toggle_layout.addWidget(self.shutter_btn, 0, 0)
|
|
|
|
self.keyswitch_btn = QPushButton("Keyswitch: OFF")
|
|
self.keyswitch_btn.setCheckable(True)
|
|
self.keyswitch_btn.setToolTip("Enable/disable keyswitch")
|
|
self.keyswitch_btn.clicked.connect(self.on_keyswitch_toggled)
|
|
toggle_layout.addWidget(self.keyswitch_btn, 0, 1)
|
|
|
|
self.remote_enable_btn = QPushButton("Remote: DISABLED")
|
|
self.remote_enable_btn.setCheckable(True)
|
|
self.remote_enable_btn.setToolTip("Enable/disable remote control")
|
|
self.remote_enable_btn.clicked.connect(self.on_remote_enable_toggled)
|
|
toggle_layout.addWidget(self.remote_enable_btn, 1, 0)
|
|
|
|
self.analog_enable_btn = QPushButton("Analog Input: DISABLED")
|
|
self.analog_enable_btn.setCheckable(True)
|
|
self.analog_enable_btn.setToolTip("Enable/disable analog input")
|
|
self.analog_enable_btn.clicked.connect(self.on_analog_enable_toggled)
|
|
toggle_layout.addWidget(self.analog_enable_btn, 1, 1)
|
|
|
|
self.current_mode_btn = QPushButton("Current Mode: OFF")
|
|
self.current_mode_btn.setCheckable(True)
|
|
self.current_mode_btn.setToolTip("Enable/disable current mode")
|
|
self.current_mode_btn.clicked.connect(self.on_current_mode_toggled)
|
|
toggle_layout.addWidget(self.current_mode_btn, 2, 0)
|
|
|
|
toggle_group.setLayout(toggle_layout)
|
|
layout.addWidget(toggle_group)
|
|
|
|
# Emergency stop
|
|
self.emergency_stop_btn = QPushButton("EMERGENCY STOP")
|
|
self.emergency_stop_btn.setStyleSheet("background-color: #ff0000; color: white; font-weight: bold; font-size: 14pt;")
|
|
self.emergency_stop_btn.clicked.connect(self.on_emergency_stop)
|
|
self.emergency_stop_btn.setEnabled(True) # Always enabled
|
|
layout.addWidget(self.emergency_stop_btn)
|
|
|
|
layout.addStretch()
|
|
|
|
return tab
|
|
|
|
def create_monitoring_tab(self) -> QWidget:
|
|
"""Create monitoring tab"""
|
|
tab = QWidget()
|
|
layout = QVBoxLayout(tab)
|
|
|
|
# Auto-refresh control
|
|
refresh_layout = QHBoxLayout()
|
|
self.auto_refresh_check = QCheckBox("Auto-refresh")
|
|
self.auto_refresh_check.stateChanged.connect(self.on_auto_refresh_toggled)
|
|
refresh_layout.addWidget(self.auto_refresh_check)
|
|
|
|
refresh_layout.addWidget(QLabel("Interval (ms):"))
|
|
self.refresh_interval_spin = QSpinBox()
|
|
self.refresh_interval_spin.setMinimum(100)
|
|
self.refresh_interval_spin.setMaximum(5000)
|
|
self.refresh_interval_spin.setValue(500)
|
|
self.refresh_interval_spin.setSingleStep(100)
|
|
refresh_layout.addWidget(self.refresh_interval_spin)
|
|
|
|
self.refresh_now_btn = QPushButton("Refresh Now")
|
|
self.refresh_now_btn.clicked.connect(self.update_monitoring)
|
|
refresh_layout.addWidget(self.refresh_now_btn)
|
|
|
|
refresh_layout.addStretch()
|
|
layout.addLayout(refresh_layout)
|
|
|
|
# Readings group
|
|
readings_group = QGroupBox("Sensor Readings")
|
|
readings_layout = QGridLayout()
|
|
|
|
readings_layout.addWidget(QLabel("Actual Current:"), 0, 0)
|
|
self.current_actual_label = QLabel("---")
|
|
readings_layout.addWidget(self.current_actual_label, 0, 1)
|
|
|
|
readings_layout.addWidget(QLabel("Interlock Status:"), 1, 0)
|
|
self.interlock_label = QLabel("---")
|
|
readings_layout.addWidget(self.interlock_label, 1, 1)
|
|
|
|
readings_layout.addWidget(QLabel("LDD Enable:"), 2, 0)
|
|
self.ldd_enable_label = QLabel("---")
|
|
readings_layout.addWidget(self.ldd_enable_label, 2, 1)
|
|
|
|
readings_layout.addWidget(QLabel("PS Glue In:"), 3, 0)
|
|
self.psglue_in_label = QLabel("---")
|
|
readings_layout.addWidget(self.psglue_in_label, 3, 1)
|
|
|
|
readings_layout.addWidget(QLabel("PS Glue Out:"), 4, 0)
|
|
self.psglue_out_label = QLabel("---")
|
|
readings_layout.addWidget(self.psglue_out_label, 4, 1)
|
|
|
|
readings_layout.addWidget(QLabel("Head DIO:"), 5, 0)
|
|
self.head_dio_label = QLabel("---")
|
|
readings_layout.addWidget(self.head_dio_label, 5, 1)
|
|
|
|
readings_group.setLayout(readings_layout)
|
|
layout.addWidget(readings_group)
|
|
|
|
# Laser info group
|
|
info_group = QGroupBox("Laser Information")
|
|
info_layout = QGridLayout()
|
|
|
|
info_layout.addWidget(QLabel("Model:"), 0, 0)
|
|
self.laser_model_label = QLabel("Genesis SLM MX 532")
|
|
info_layout.addWidget(self.laser_model_label, 0, 1)
|
|
|
|
info_layout.addWidget(QLabel("Wavelength:"), 1, 0)
|
|
self.wavelength_label = QLabel("532 nm")
|
|
info_layout.addWidget(self.wavelength_label, 1, 1)
|
|
|
|
info_group.setLayout(info_layout)
|
|
layout.addWidget(info_group)
|
|
|
|
layout.addStretch()
|
|
|
|
return tab
|
|
|
|
def create_advanced_tab(self) -> QWidget:
|
|
"""Create advanced tab"""
|
|
tab = QWidget()
|
|
layout = QVBoxLayout(tab)
|
|
|
|
# Raw I2C packet sender
|
|
packet_group = QGroupBox("Raw I2C Packet Sender")
|
|
packet_layout = QGridLayout()
|
|
|
|
packet_layout.addWidget(QLabel("I2C Address (hex):"), 0, 0)
|
|
self.i2c_addr_edit = QLineEdit("52")
|
|
self.i2c_addr_edit.setMaximumWidth(100)
|
|
packet_layout.addWidget(self.i2c_addr_edit, 0, 1)
|
|
|
|
packet_layout.addWidget(QLabel("Command (hex):"), 1, 0)
|
|
self.i2c_cmd_edit = QLineEdit("a0")
|
|
self.i2c_cmd_edit.setMaximumWidth(200)
|
|
packet_layout.addWidget(self.i2c_cmd_edit, 1, 1)
|
|
|
|
packet_layout.addWidget(QLabel("Data (hex):"), 2, 0)
|
|
self.i2c_data_edit = QLineEdit("00 00")
|
|
packet_layout.addWidget(self.i2c_data_edit, 2, 1)
|
|
|
|
self.send_packet_btn = QPushButton("Send Packet")
|
|
self.send_packet_btn.clicked.connect(self.on_send_raw_packet)
|
|
packet_layout.addWidget(self.send_packet_btn, 3, 0, 1, 2)
|
|
|
|
self.packet_response_label = QLabel("Response: ---")
|
|
packet_layout.addWidget(self.packet_response_label, 4, 0, 1, 2)
|
|
|
|
packet_group.setLayout(packet_layout)
|
|
layout.addWidget(packet_group)
|
|
|
|
# Packet log
|
|
log_group = QGroupBox("Packet Capture Log")
|
|
log_layout = QVBoxLayout()
|
|
|
|
log_controls = QHBoxLayout()
|
|
self.clear_log_btn = QPushButton("Clear Log")
|
|
self.clear_log_btn.clicked.connect(self.on_clear_log)
|
|
log_controls.addWidget(self.clear_log_btn)
|
|
log_controls.addStretch()
|
|
log_layout.addLayout(log_controls)
|
|
|
|
self.packet_log_text = QTextEdit()
|
|
self.packet_log_text.setReadOnly(True)
|
|
self.packet_log_text.setFont(QFont("Courier", 9))
|
|
log_layout.addWidget(self.packet_log_text)
|
|
|
|
log_group.setLayout(log_layout)
|
|
layout.addWidget(log_group)
|
|
|
|
# Update log periodically
|
|
self.log_timer = QTimer()
|
|
self.log_timer.timeout.connect(self.update_packet_log)
|
|
self.log_timer.start(1000) # Update every second
|
|
|
|
return tab
|
|
|
|
def create_configuration_tab(self) -> QWidget:
|
|
"""Create configuration tab"""
|
|
tab = QWidget()
|
|
layout = QVBoxLayout(tab)
|
|
|
|
# Serial port configuration
|
|
serial_group = QGroupBox("Serial Port Configuration")
|
|
serial_layout = QGridLayout()
|
|
|
|
serial_layout.addWidget(QLabel("Port:"), 0, 0)
|
|
self.port_combo = QComboBox()
|
|
self.refresh_ports()
|
|
serial_layout.addWidget(self.port_combo, 0, 1)
|
|
|
|
self.refresh_ports_btn = QPushButton("Refresh Ports")
|
|
self.refresh_ports_btn.clicked.connect(self.refresh_ports)
|
|
serial_layout.addWidget(self.refresh_ports_btn, 0, 2)
|
|
|
|
serial_layout.addWidget(QLabel("Baud Rate:"), 1, 0)
|
|
self.baudrate_combo = QComboBox()
|
|
self.baudrate_combo.addItems(["9600", "19200", "38400", "57600", "115200"])
|
|
self.baudrate_combo.setCurrentText("9600")
|
|
serial_layout.addWidget(self.baudrate_combo, 1, 1)
|
|
|
|
self.connect_btn = QPushButton("Connect")
|
|
self.connect_btn.clicked.connect(self.on_connect_clicked)
|
|
serial_layout.addWidget(self.connect_btn, 2, 0, 1, 3)
|
|
|
|
serial_group.setLayout(serial_layout)
|
|
layout.addWidget(serial_group)
|
|
|
|
# DTR/RTS control
|
|
control_group = QGroupBox("Serial Control Lines")
|
|
control_layout = QVBoxLayout()
|
|
|
|
self.dtr_check = QCheckBox("DTR (Data Terminal Ready)")
|
|
self.dtr_check.stateChanged.connect(self.on_dtr_changed)
|
|
control_layout.addWidget(self.dtr_check)
|
|
|
|
self.rts_check = QCheckBox("RTS (Request To Send)")
|
|
self.rts_check.stateChanged.connect(self.on_rts_changed)
|
|
control_layout.addWidget(self.rts_check)
|
|
|
|
control_group.setLayout(control_layout)
|
|
layout.addWidget(control_group)
|
|
|
|
# About section
|
|
about_group = QGroupBox("About")
|
|
about_layout = QVBoxLayout()
|
|
|
|
about_text = QLabel(
|
|
"Genesis SLM MX 532 Laser Control\n\n"
|
|
"Protocol: NXP I2C-over-serial\n"
|
|
"Baud Rate: 9600 (default)\n\n"
|
|
"This application provides comprehensive control of the Genesis laser\n"
|
|
"using I2C devices over serial communication."
|
|
)
|
|
about_layout.addWidget(about_text)
|
|
|
|
about_group.setLayout(about_layout)
|
|
layout.addWidget(about_group)
|
|
|
|
layout.addStretch()
|
|
|
|
return tab
|
|
|
|
# ------------------------------------------------------------------------
|
|
# Event Handlers - Basic Controls
|
|
# ------------------------------------------------------------------------
|
|
|
|
def on_current_changed(self, value: int):
|
|
"""Handle current slider change"""
|
|
self.current_spinbox.setValue(value)
|
|
percent = (value / 1023.0) * 100.0
|
|
self.current_percent_label.setText(f"{percent:.1f}%")
|
|
|
|
if self.serial_comm.is_connected():
|
|
if self.laser_control.set_current(value):
|
|
self.status_bar.showMessage(f"Current set to {value} ({percent:.1f}%)", 2000)
|
|
|
|
def on_power_changed(self, value: int):
|
|
"""Handle power slider change"""
|
|
self.power_spinbox.setValue(value)
|
|
|
|
if self.serial_comm.is_connected():
|
|
if self.laser_control.set_power_cmd(value):
|
|
self.status_bar.showMessage(f"Power command set to {value}", 2000)
|
|
|
|
def on_shutter_toggled(self, checked: bool):
|
|
"""Handle shutter toggle"""
|
|
if checked and self.current_slider.value() > 0:
|
|
reply = QMessageBox.question(
|
|
self,
|
|
"Confirm Shutter Open",
|
|
"Current is set above 0. Are you sure you want to open the shutter?",
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
|
)
|
|
if reply == QMessageBox.StandardButton.No:
|
|
self.shutter_btn.setChecked(False)
|
|
return
|
|
|
|
if self.laser_control.set_shutter(checked):
|
|
self.shutter_btn.setText("Shutter: OPEN" if checked else "Shutter: CLOSED")
|
|
self.status_bar.showMessage(f"Shutter {'opened' if checked else 'closed'}", 2000)
|
|
else:
|
|
self.shutter_btn.setChecked(not checked)
|
|
|
|
def on_keyswitch_toggled(self, checked: bool):
|
|
"""Handle keyswitch toggle"""
|
|
if self.laser_control.set_keyswitch(checked):
|
|
self.keyswitch_btn.setText("Keyswitch: ON" if checked else "Keyswitch: OFF")
|
|
self.status_bar.showMessage(f"Keyswitch {'enabled' if checked else 'disabled'}", 2000)
|
|
else:
|
|
self.keyswitch_btn.setChecked(not checked)
|
|
|
|
def on_remote_enable_toggled(self, checked: bool):
|
|
"""Handle remote enable toggle"""
|
|
if self.laser_control.set_remote_enable(checked):
|
|
self.remote_enable_btn.setText("Remote: ENABLED" if checked else "Remote: DISABLED")
|
|
self.status_bar.showMessage(f"Remote control {'enabled' if checked else 'disabled'}", 2000)
|
|
else:
|
|
self.remote_enable_btn.setChecked(not checked)
|
|
|
|
def on_analog_enable_toggled(self, checked: bool):
|
|
"""Handle analog enable toggle"""
|
|
if self.laser_control.set_analog_enable(checked):
|
|
self.analog_enable_btn.setText("Analog Input: ENABLED" if checked else "Analog Input: DISABLED")
|
|
self.status_bar.showMessage(f"Analog input {'enabled' if checked else 'disabled'}", 2000)
|
|
else:
|
|
self.analog_enable_btn.setChecked(not checked)
|
|
|
|
def on_current_mode_toggled(self, checked: bool):
|
|
"""Handle current mode toggle"""
|
|
if self.laser_control.set_current_mode(checked):
|
|
self.current_mode_btn.setText("Current Mode: ON" if checked else "Current Mode: OFF")
|
|
self.status_bar.showMessage(f"Current mode {'enabled' if checked else 'disabled'}", 2000)
|
|
else:
|
|
self.current_mode_btn.setChecked(not checked)
|
|
|
|
def on_emergency_stop(self):
|
|
"""Handle emergency stop button"""
|
|
if self.laser_control.emergency_stop():
|
|
# Reset UI to safe state
|
|
self.current_slider.setValue(0)
|
|
self.power_slider.setValue(0)
|
|
self.shutter_btn.setChecked(False)
|
|
self.shutter_btn.setText("Shutter: CLOSED")
|
|
self.keyswitch_btn.setChecked(False)
|
|
self.keyswitch_btn.setText("Keyswitch: OFF")
|
|
|
|
self.status_bar.showMessage("EMERGENCY STOP ACTIVATED", 5000)
|
|
QMessageBox.warning(self, "Emergency Stop", "Emergency stop activated!\nLaser is now in safe state.")
|
|
|
|
# ------------------------------------------------------------------------
|
|
# Event Handlers - Monitoring
|
|
# ------------------------------------------------------------------------
|
|
|
|
def on_auto_refresh_toggled(self, state):
|
|
"""Handle auto-refresh toggle"""
|
|
if state:
|
|
interval = self.refresh_interval_spin.value()
|
|
self.monitor_timer.start(interval)
|
|
self.monitor_enabled = True
|
|
else:
|
|
self.monitor_timer.stop()
|
|
self.monitor_enabled = False
|
|
|
|
def update_monitoring(self):
|
|
"""Update monitoring readings"""
|
|
if not self.serial_comm.is_connected():
|
|
return
|
|
|
|
# Read actual current
|
|
current = self.laser_control.get_current_actual()
|
|
if current is not None:
|
|
self.current_actual_label.setText(f"{current} counts")
|
|
else:
|
|
self.current_actual_label.setText("Error reading")
|
|
|
|
# Read interlock status
|
|
interlock = self.laser_control.get_interlock_status()
|
|
if interlock is not None:
|
|
if interlock:
|
|
self.interlock_label.setText("OK")
|
|
self.interlock_label.setStyleSheet("color: green; font-weight: bold;")
|
|
else:
|
|
self.interlock_label.setText("FAULT")
|
|
self.interlock_label.setStyleSheet("color: red; font-weight: bold;")
|
|
else:
|
|
self.interlock_label.setText("Error reading")
|
|
self.interlock_label.setStyleSheet("")
|
|
|
|
# Read LDD enable
|
|
ldd = self.laser_control.get_ldd_enable_status()
|
|
if ldd is not None:
|
|
self.ldd_enable_label.setText("Enabled" if ldd else "Disabled")
|
|
else:
|
|
self.ldd_enable_label.setText("Error reading")
|
|
|
|
# Read PS glue status
|
|
psglue_in = self.laser_control.get_psglue_in_status()
|
|
if psglue_in is not None:
|
|
self.psglue_in_label.setText(f"0x{psglue_in:02x}")
|
|
else:
|
|
self.psglue_in_label.setText("Error reading")
|
|
|
|
psglue_out = self.laser_control.get_psglue_out_status()
|
|
if psglue_out is not None:
|
|
self.psglue_out_label.setText(f"0x{psglue_out:02x}")
|
|
else:
|
|
self.psglue_out_label.setText("Error reading")
|
|
|
|
# Read head DIO
|
|
head_dio = self.laser_control.get_head_dio_status()
|
|
if head_dio is not None:
|
|
self.head_dio_label.setText(f"0x{head_dio:02x}")
|
|
else:
|
|
self.head_dio_label.setText("Error reading")
|
|
|
|
# ------------------------------------------------------------------------
|
|
# Event Handlers - Advanced
|
|
# ------------------------------------------------------------------------
|
|
|
|
def on_send_raw_packet(self):
|
|
"""Handle raw packet send"""
|
|
if not self.serial_comm.is_connected():
|
|
QMessageBox.warning(self, "Not Connected", "Please connect to serial port first.")
|
|
return
|
|
|
|
try:
|
|
# Parse inputs
|
|
addr = int(self.i2c_addr_edit.text(), 16)
|
|
cmd_hex = self.i2c_cmd_edit.text().replace(" ", "")
|
|
cmd = bytes.fromhex(cmd_hex)
|
|
|
|
data_hex = self.i2c_data_edit.text().replace(" ", "")
|
|
data = bytes.fromhex(data_hex) if data_hex else b''
|
|
|
|
# Send packet
|
|
if self.i2c_protocol.write(addr, cmd, data):
|
|
self.packet_response_label.setText("Response: Packet sent successfully")
|
|
self.status_bar.showMessage("Raw packet sent", 2000)
|
|
else:
|
|
self.packet_response_label.setText("Response: Send failed")
|
|
|
|
except ValueError as e:
|
|
QMessageBox.warning(self, "Invalid Input", f"Invalid hex value: {e}")
|
|
|
|
def on_clear_log(self):
|
|
"""Handle clear log button"""
|
|
self.serial_comm.clear_packet_log()
|
|
self.packet_log_text.clear()
|
|
|
|
def update_packet_log(self):
|
|
"""Update packet log display"""
|
|
if not self.serial_comm.is_connected():
|
|
return
|
|
|
|
log_entries = self.serial_comm.get_packet_log(100)
|
|
if log_entries:
|
|
self.packet_log_text.setPlainText("\n".join(log_entries))
|
|
# Scroll to bottom
|
|
scrollbar = self.packet_log_text.verticalScrollBar()
|
|
scrollbar.setValue(scrollbar.maximum())
|
|
|
|
# ------------------------------------------------------------------------
|
|
# Event Handlers - Configuration
|
|
# ------------------------------------------------------------------------
|
|
|
|
def on_connect_clicked(self):
|
|
"""Handle connect/disconnect button"""
|
|
if self.serial_comm.is_connected():
|
|
# Disconnect
|
|
self.laser_control.enter_safe_state()
|
|
self.serial_comm.disconnect()
|
|
self.connect_btn.setText("Connect")
|
|
self.status_bar.showMessage("Disconnected")
|
|
self.update_control_states()
|
|
else:
|
|
# Connect
|
|
port = self.port_combo.currentText()
|
|
baudrate = int(self.baudrate_combo.currentText())
|
|
|
|
if self.serial_comm.connect(port, baudrate):
|
|
self.connect_btn.setText("Disconnect")
|
|
self.status_bar.showMessage(f"Connected to {port} at {baudrate} baud")
|
|
self.update_control_states()
|
|
else:
|
|
QMessageBox.critical(self, "Connection Error", f"Failed to connect to {port}")
|
|
|
|
def on_dtr_changed(self, state):
|
|
"""Handle DTR checkbox change"""
|
|
if self.serial_comm.is_connected():
|
|
self.serial_comm.port.dtr = bool(state)
|
|
|
|
def on_rts_changed(self, state):
|
|
"""Handle RTS checkbox change"""
|
|
if self.serial_comm.is_connected():
|
|
self.serial_comm.port.rts = bool(state)
|
|
|
|
def refresh_ports(self):
|
|
"""Refresh available serial ports"""
|
|
self.port_combo.clear()
|
|
ports = list_ports.comports()
|
|
for port in ports:
|
|
self.port_combo.addItem(port.device)
|
|
|
|
# Add default if no ports found
|
|
if self.port_combo.count() == 0:
|
|
self.port_combo.addItem("/dev/ttyUSB0")
|
|
|
|
# ------------------------------------------------------------------------
|
|
# UI Update Functions
|
|
# ------------------------------------------------------------------------
|
|
|
|
def update_control_states(self):
|
|
"""Enable/disable controls based on connection state"""
|
|
connected = self.serial_comm.is_connected()
|
|
|
|
# Basic controls
|
|
self.current_slider.setEnabled(connected)
|
|
self.current_spinbox.setEnabled(connected)
|
|
self.power_slider.setEnabled(connected)
|
|
self.power_spinbox.setEnabled(connected)
|
|
self.shutter_btn.setEnabled(connected)
|
|
self.keyswitch_btn.setEnabled(connected)
|
|
self.remote_enable_btn.setEnabled(connected)
|
|
self.analog_enable_btn.setEnabled(connected)
|
|
self.current_mode_btn.setEnabled(connected)
|
|
|
|
# Monitoring controls
|
|
self.auto_refresh_check.setEnabled(connected)
|
|
self.refresh_now_btn.setEnabled(connected)
|
|
|
|
# Advanced controls
|
|
self.send_packet_btn.setEnabled(connected)
|
|
|
|
# Configuration controls
|
|
self.port_combo.setEnabled(not connected)
|
|
self.baudrate_combo.setEnabled(not connected)
|
|
self.dtr_check.setEnabled(connected)
|
|
self.rts_check.setEnabled(connected)
|
|
|
|
def apply_stylesheet(self):
|
|
"""Apply custom stylesheet for professional look"""
|
|
self.setStyleSheet("""
|
|
QGroupBox {
|
|
font-weight: bold;
|
|
border: 2px solid #cccccc;
|
|
border-radius: 5px;
|
|
margin-top: 10px;
|
|
padding-top: 10px;
|
|
}
|
|
|
|
QGroupBox::title {
|
|
subcontrol-origin: margin;
|
|
left: 10px;
|
|
padding: 0 5px 0 5px;
|
|
}
|
|
|
|
QPushButton {
|
|
padding: 5px 10px;
|
|
border-radius: 3px;
|
|
background-color: #e0e0e0;
|
|
}
|
|
|
|
QPushButton:hover {
|
|
background-color: #d0d0d0;
|
|
}
|
|
|
|
QPushButton:pressed {
|
|
background-color: #c0c0c0;
|
|
}
|
|
|
|
QPushButton:disabled {
|
|
background-color: #f0f0f0;
|
|
color: #a0a0a0;
|
|
}
|
|
|
|
QPushButton:checked {
|
|
background-color: #4CAF50;
|
|
color: white;
|
|
}
|
|
|
|
QSlider::groove:horizontal {
|
|
border: 1px solid #999999;
|
|
height: 8px;
|
|
background: #e0e0e0;
|
|
margin: 2px 0;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
QSlider::handle:horizontal {
|
|
background: #4CAF50;
|
|
border: 1px solid #5c5c5c;
|
|
width: 18px;
|
|
margin: -5px 0;
|
|
border-radius: 9px;
|
|
}
|
|
""")
|
|
|
|
# ------------------------------------------------------------------------
|
|
# Settings Persistence
|
|
# ------------------------------------------------------------------------
|
|
|
|
def save_settings(self):
|
|
"""Save window geometry and settings"""
|
|
self.settings.setValue("geometry", self.saveGeometry())
|
|
self.settings.setValue("port", self.port_combo.currentText())
|
|
self.settings.setValue("baudrate", self.baudrate_combo.currentText())
|
|
|
|
def restore_settings(self):
|
|
"""Restore window geometry and settings"""
|
|
geometry = self.settings.value("geometry")
|
|
if geometry:
|
|
self.restoreGeometry(geometry)
|
|
|
|
port = self.settings.value("port")
|
|
if port:
|
|
index = self.port_combo.findText(port)
|
|
if index >= 0:
|
|
self.port_combo.setCurrentIndex(index)
|
|
|
|
baudrate = self.settings.value("baudrate")
|
|
if baudrate:
|
|
index = self.baudrate_combo.findText(baudrate)
|
|
if index >= 0:
|
|
self.baudrate_combo.setCurrentIndex(index)
|
|
|
|
def closeEvent(self, event):
|
|
"""Handle window close event"""
|
|
if self.serial_comm.is_connected():
|
|
reply = QMessageBox.question(
|
|
self,
|
|
"Confirm Exit",
|
|
"Laser is connected. Enter safe state and disconnect?",
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
|
)
|
|
if reply == QMessageBox.StandardButton.Yes:
|
|
self.laser_control.enter_safe_state()
|
|
self.serial_comm.disconnect()
|
|
else:
|
|
event.ignore()
|
|
return
|
|
|
|
self.save_settings()
|
|
event.accept()
|
|
|
|
|
|
# ============================================================================
|
|
# Main Entry Point
|
|
# ============================================================================
|
|
|
|
def main():
|
|
"""Main application entry point"""
|
|
app = QApplication(sys.argv)
|
|
app.setApplicationName("Genesis Laser Control")
|
|
app.setOrganizationName("Genesis")
|
|
|
|
window = MainWindow()
|
|
window.show()
|
|
|
|
sys.exit(app.exec())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|