when'd i last commit this pos?
This commit is contained in:
Executable
+828
@@ -0,0 +1,828 @@
|
||||
#!/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
|
||||
import time
|
||||
import struct
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Tuple
|
||||
from enum import IntEnum
|
||||
|
||||
import serial
|
||||
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, QProgressBar
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QSettings
|
||||
from PyQt6.QtGui import QFont, QPalette, QColor
|
||||
|
||||
# Import core hardware control classes
|
||||
from hardware.genesis_core import (
|
||||
I2CAddress, PCA9555Register, ControlBitmask,
|
||||
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()
|
||||
Executable
+1445
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user