#!/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()