Initial commit: merge nuescan, pymso, pybbd202, and pypewpewhops into scanengine-3

- Merged four separate hardware control projects into unified platform
- Created unified requirements.txt with all dependencies
- Added comprehensive .gitignore
- Added project overview README

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Thomas Ales [M S E]
2026-01-16 20:11:31 -06:00
commit fc43fbe4b0
65 changed files with 19127 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
"""
Dialog controllers for nueScan application
"""
+71
View File
@@ -0,0 +1,71 @@
"""
Genesis Laser Settings Dialog
Configures Genesis scanning laser parameters
"""
import os
from PyQt6 import uic
from PyQt6.QtWidgets import QDialog
class GenesisDialog(QDialog):
"""Dialog for configuring Genesis laser settings"""
def __init__(self, parent=None):
super().__init__(parent)
# Load UI file
ui_path = os.path.join(
os.path.dirname(os.path.dirname(__file__)),
'nuescan_genesis_dialog.ui'
)
uic.loadUi(ui_path, self)
self.setWindowTitle("Genesis Laser Settings")
# Initialize with default values
self._load_default_settings()
# Connect signals
self._connect_signals()
def _connect_signals(self):
"""Connect dialog signals"""
# LineEdit text changed
self.le_genesis_power_mw.textChanged.connect(self.on_power_changed)
# Dialog buttons are auto-connected by Qt Designer
def _load_default_settings(self):
"""Load default Genesis settings"""
self.le_genesis_power_mw.setText("100.0") # Default 100mW
def on_power_changed(self, text):
"""Handle scanning power change"""
print(f"DEBUG: Genesis power changed to: {text}")
def get_settings(self):
"""
Get current Genesis settings as a dictionary
Returns:
dict: Genesis laser settings
"""
try:
power_mw = float(self.le_genesis_power_mw.text())
except ValueError:
power_mw = 0.0
return {
'power_mw': power_mw
}
def set_settings(self, settings):
"""
Set Genesis settings from a dictionary
Args:
settings (dict): Genesis laser settings
"""
if 'power_mw' in settings:
self.le_genesis_power_mw.setText(str(settings['power_mw']))
+169
View File
@@ -0,0 +1,169 @@
"""
Helios Device Settings Dialog
Configures Helios laser parameters and COM port
Copyright (C) 2025 Thomas Ales
Licensed under GNU General Public License v2.0
"""
import os
from PyQt6 import uic
from PyQt6.QtWidgets import QDialog, QMessageBox
from hardware.helios_driver import HeliosDriver
class HeliosDialog(QDialog):
"""Dialog for configuring Helios device settings"""
def __init__(self, parent=None):
super().__init__(parent)
# Load UI file
ui_path = os.path.join(
os.path.dirname(os.path.dirname(__file__)),
'nuescan_helios_dialog.ui'
)
uic.loadUi(ui_path, self)
self.setWindowTitle("Helios Device Settings")
# Initialize with default values
self._load_default_settings()
# Connect signals
self._connect_signals()
# Populate COM ports
self._populate_com_ports()
def _connect_signals(self):
"""Connect dialog signals"""
# ComboBox value changed
self.cb_helios_port.currentIndexChanged.connect(self.on_port_changed)
# LineEdit text changed
self.le_helios_frequency.textChanged.connect(self.on_frequency_changed)
self.le_helios_current.textChanged.connect(self.on_current_changed)
# Dialog buttons are auto-connected by Qt Designer
def _load_default_settings(self):
"""Load default Helios settings"""
self.le_helios_frequency.setText("10000") # Default 10kHz
self.le_helios_current.setText("500") # Default 500mA
def _populate_com_ports(self):
"""Populate available COM ports"""
# Get available ports from system
ports = HeliosDriver.list_available_ports()
if ports:
self.cb_helios_port.addItems(ports)
print(f"DEBUG: Found {len(ports)} available COM ports")
else:
# No ports found
self.cb_helios_port.addItem("No ports found")
print("WARNING: No COM ports found")
def refresh_com_ports(self):
"""Refresh the COM port list"""
current_port = self.cb_helios_port.currentText()
self.cb_helios_port.clear()
self._populate_com_ports()
# Try to restore previous selection
index = self.cb_helios_port.findText(current_port)
if index >= 0:
self.cb_helios_port.setCurrentIndex(index)
def on_port_changed(self, index):
"""Handle COM port selection change"""
port = self.cb_helios_port.currentText()
print(f"DEBUG: Helios port changed to: {port}")
def on_frequency_changed(self, text):
"""Handle frequency change"""
print(f"DEBUG: Helios frequency changed to: {text}")
def on_current_changed(self, text):
"""Handle current change"""
print(f"DEBUG: Helios current changed to: {text}")
def get_settings(self):
"""
Get current Helios settings as a dictionary
Validates input ranges before returning.
Returns:
dict: Helios device settings, or None if validation fails
"""
# Validate frequency
try:
frequency_hz = float(self.le_helios_frequency.text())
# Convert to period to check valid range (8000-60000 ns)
# Valid frequencies: ~16.7 kHz to 125 kHz
if frequency_hz < 16666 or frequency_hz > 125000:
QMessageBox.warning(
self, "Invalid Frequency",
f"Frequency must be between 16.7 kHz and 125 kHz\n"
f"(Period: 8000-60000 ns)\n\n"
f"Entered: {frequency_hz/1000:.1f} kHz"
)
return None
except ValueError:
QMessageBox.warning(
self, "Invalid Frequency",
"Please enter a valid frequency value in Hz"
)
return None
# Validate current
try:
current_ma = float(self.le_helios_current.text())
if current_ma < 0 or current_ma > 7000:
QMessageBox.warning(
self, "Invalid Current",
f"Current must be between 0 and 7000 mA\n\n"
f"Entered: {current_ma} mA"
)
return None
except ValueError:
QMessageBox.warning(
self, "Invalid Current",
"Please enter a valid current value in mA"
)
return None
# Validate COM port selection
com_port = self.cb_helios_port.currentText()
if not com_port or com_port == "No ports found":
QMessageBox.warning(
self, "No Port Selected",
"Please select a valid COM port"
)
return None
return {
'com_port': com_port,
'frequency_hz': frequency_hz,
'current_ma': current_ma
}
def set_settings(self, settings):
"""
Set Helios settings from a dictionary
Args:
settings (dict): Helios device settings
"""
if 'com_port' in settings:
index = self.cb_helios_port.findText(settings['com_port'])
if index >= 0:
self.cb_helios_port.setCurrentIndex(index)
if 'frequency_hz' in settings:
self.le_helios_frequency.setText(str(settings['frequency_hz']))
if 'current_ma' in settings:
self.le_helios_current.setText(str(settings['current_ma']))
+126
View File
@@ -0,0 +1,126 @@
"""
nueScan - Oscilloscope Dialog Controller
Handles all UI interactions for the oscilloscope dialog
Copyright (C) 2025 Thomas Ales
Licensed under GNU General Public License v2.0
"""
import os
from PyQt6 import uic
from PyQt6.QtWidgets import QDialog, QMessageBox
class OscopeDialog(QDialog):
"""Oscope dialog for nueScan application"""
def __init__(self, parent, microscope):
super().__init__(parent)
# Store hardware controllers
self.microscope = microscope
# Load UI file
ui_path = os.path.join(os.path.dirname(__file__), '..', 'nuescan_oscope_dialog.ui')
uic.loadUi(ui_path, self)
# Set window title
self.setWindowTitle("nueScan - Oscilloscope Settings")
# Connect all UI signals
self._connect_signals()
# Initialize UI state
self._initialize_ui()
def _connect_signals(self):
"""Connect all UI signals to handler methods"""
# ===== ComboBox Value Changed Handlers =====
self.cb_set_trig_channel.currentIndexChanged.connect(self.on_trigger_channel_changed)
self.cb_set_saw_channel.currentIndexChanged.connect(self.on_saw_channel_changed)
self.cb_set_bias_a_ch.currentIndexChanged.connect(self.on_bias_a_channel_changed)
self.cb_set_bias_b_ch.currentIndexChanged.connect(self.on_bias_b_channel_changed)
# ===== LineEdit Text Changed Handlers =====
self.le_set_trigger_voltage.textChanged.connect(self.on_trigger_voltage_changed)
self.le_set_sample_thresh_voltage.textChanged.connect(self.on_sample_thresh_voltage_changed)
self.le_set_pd_trig_voltage.textChanged.connect(self.on_pd_trig_voltage_changed)
self.le_oscope_visa_address.textChanged.connect(self.on_oscope_visa_address_changed)
# ===== Button Click Handlers =====
self.btn_test_scope_connection.clicked.connect(self.on_test_scope_connection_clicked)
self.btn_save_scope_settings.clicked.connect(self.on_save_scope_settings_clicked)
self.btn_cancel_scope_settings.clicked.connect(self.on_cancel_scope_settings_clicked)
def _initialize_ui(self):
"""Initialize UI with default values"""
# Populate combo boxes with dummy data
self._populate_combo_boxes()
def _populate_combo_boxes(self):
"""Populate all combo boxes with initial values"""
# Oscilloscope channels
channels = ["CH1", "CH2", "CH3", "CH4"]
self.cb_set_trig_channel.addItems(channels)
self.cb_set_saw_channel.addItems(channels)
self.cb_set_bias_a_ch.addItems(channels)
self.cb_set_bias_b_ch.addItems(channels)
# ==================== ComboBox Change Handlers ====================
def on_trigger_channel_changed(self, index):
"""Handle Phototrigger channel change"""
channel = self.cb_set_trig_channel.currentText()
print(f"DEBUG: Phototrigger channel changed to: {channel}")
def on_bias_a_channel_changed(self, index):
"""Handle Bias A channel change"""
channel = self.cb_set_bias_a_ch.currentText()
print(f"DEBUG: Bias A channel changed to: {channel}")
def on_bias_b_channel_changed(self, index):
"""Handle Bias B channel change"""
channel = self.cb_set_bias_b_ch.currentText()
print(f"DEBUG: Bias B channel changed to: {channel}")
def on_saw_channel_changed(self, index):
"""Handle RF/SAW channel change"""
channel = self.cb_set_saw_channel.currentText()
print(f"DEBUG: RF/SAW channel changed to: {channel}")
# ==================== LineEdit Text Change Handlers ====================
def on_pd_trig_voltage_changed(self, text):
"""Handle PD Trigger voltage change"""
print(f"DEBUG: PD Trigger voltage changed to: {text}")
def on_trigger_voltage_changed(self, text):
"""Handle Sample Min Bias voltage change"""
print(f"DEBUG: Sample Min Bias voltage changed to: {text}")
def on_sample_thresh_voltage_changed(self, text):
"""Handle Sample Min Bias voltage change (placeholder)"""
print(f"DEBUG: Sample threshold voltage changed to: {text}")
def on_oscope_visa_address_changed(self, text):
"""Handle oscilloscope VISA address change"""
print(f"DEBUG: Oscilloscope VISA address changed to: {text}")
# ==================== Button Click Handlers ====================
def on_test_scope_connection_clicked(self):
"""Test the oscilloscope connection"""
print("DEBUG: Test oscope connection clicked")
def on_save_scope_settings_clicked(self):
"""Save the oscilloscope settings"""
print("DEBUG: Save oscope settings clicked")
self.accept()
def on_cancel_scope_settings_clicked(self):
"""Cancel the oscilloscope settings changes"""
print("DEBUG: Cancel oscope settings clicked")
self.reject()
+163
View File
@@ -0,0 +1,163 @@
"""
Scan Active Dialog
Displays real-time scanning progress and status
"""
import os
from PyQt6 import uic
from PyQt6.QtWidgets import QDialog
from PyQt6.QtCore import QTimer
class ScanActiveDialog(QDialog):
"""Dialog for displaying active scan progress"""
def __init__(self, parent=None):
super().__init__(parent)
# Load UI file
ui_path = os.path.join(
os.path.dirname(os.path.dirname(__file__)),
'nuescan_scan_active_dialog.ui'
)
uic.loadUi(ui_path, self)
self.setWindowTitle("Scan in Progress")
# Make dialog modal
self.setModal(True)
# Initialize state
self.scan_cancelled = False
# Connect signals
self._connect_signals()
# Initialize progress
self._initialize_progress()
# Demo timer (for testing progress updates)
self._demo_timer = QTimer()
self._demo_timer.timeout.connect(self._demo_update)
self._demo_progress = 0
def _connect_signals(self):
"""Connect dialog signals"""
self.pb_cancel_scan.clicked.connect(self.on_cancel_clicked)
def _initialize_progress(self):
"""Initialize progress bars and status"""
self.pbar_total_scan.setValue(0)
self.pbar_this_scan.setValue(0)
self.l_status_current_scan.setText("1")
self.l_status_total_scans.setText("1")
self.l_status_current_row.setText("0")
self.l_status_total_rows.setText("0")
self.l_est_time_done.setText("Calculating...")
def on_cancel_clicked(self):
"""Handle cancel button click"""
print("DEBUG: Scan cancelled by user")
self.scan_cancelled = True
self.reject()
# ==================== Progress Update Methods ====================
def update_total_progress(self, current, total):
"""
Update the total scan progress bar
Args:
current (int): Current scan number
total (int): Total number of scans
"""
if total > 0:
percentage = int((current / total) * 100)
self.pbar_total_scan.setValue(percentage)
def update_current_scan_progress(self, current, total):
"""
Update the current scan progress bar
Args:
current (int): Current row number
total (int): Total number of rows
"""
if total > 0:
percentage = int((current / total) * 100)
self.pbar_this_scan.setValue(percentage)
def update_status(self, scan_num, total_scans, row_num, total_rows, time_remaining):
"""
Update scan status information
Args:
scan_num (int): Current scan number
total_scans (int): Total number of scans
row_num (int): Current row number
total_rows (int): Total number of rows
time_remaining (str): Estimated time remaining (formatted string)
"""
self.l_status_current_scan.setText(str(scan_num))
self.l_status_total_scans.setText(str(total_scans))
self.l_status_current_row.setText(str(row_num))
self.l_status_total_rows.setText(str(total_rows))
self.l_est_time_done.setText(f"{time_remaining} remaining...")
def start_demo_progress(self):
"""
Start a demo progress animation (for testing)
Remove this method in production
"""
self._demo_progress = 0
self._demo_timer.start(100) # Update every 100ms
def _demo_update(self):
"""
Demo progress update (for testing)
Remove this method in production
"""
self._demo_progress += 1
# Simulate scan progress
total_scans = 5
rows_per_scan = 100
total_steps = total_scans * rows_per_scan
current_scan = (self._demo_progress // rows_per_scan) + 1
current_row = (self._demo_progress % rows_per_scan)
if current_scan > total_scans:
self._demo_timer.stop()
self.accept()
return
# Update progress
self.update_total_progress(current_scan - 1, total_scans)
self.update_current_scan_progress(current_row, rows_per_scan)
# Calculate time remaining (demo)
remaining_steps = total_steps - self._demo_progress
seconds_remaining = remaining_steps * 0.1 # 0.1s per step
hours = int(seconds_remaining // 3600)
minutes = int((seconds_remaining % 3600) // 60)
seconds = int(seconds_remaining % 60)
time_str = f"{hours:02d}:{minutes:02d}:{seconds:02d}"
self.update_status(
current_scan,
total_scans,
current_row,
rows_per_scan,
time_str
)
def is_cancelled(self):
"""
Check if scan was cancelled
Returns:
bool: True if cancelled, False otherwise
"""
return self.scan_cancelled
+101
View File
@@ -0,0 +1,101 @@
"""
nueScan - Status Dialog Controller
Handles all UI interactions for the status dialog
Copyright (C) 2025 Thomas Ales
Licensed under GNU General Public License v2.0
"""
import os
from PyQt6 import uic
from PyQt6.QtWidgets import QDialog
class StatusDialog(QDialog):
"""Status dialog for nueScan application"""
def __init__(self, parent, thorlabs_stage, t3r_device, microscope):
super().__init__(parent)
# Store hardware controllers
self.thorlabs_stage = thorlabs_stage
self.t3r_device = t3r_device
self.microscope = microscope
# Load UI file
ui_path = os.path.join(os.path.dirname(__file__), '..', 'nuescan_status_dialog.ui')
uic.loadUi(ui_path, self)
# Set window title
self.setWindowTitle("nueScan - Status Indicators")
def update_all_status(self):
"""Update all status labels with current hardware states"""
self._update_stage_status()
self._update_t3r_status()
self._update_microscope_status()
self._update_transfer_system_status()
def _update_stage_status(self):
"""Update ThorLabs stage status indicators"""
status = self.thorlabs_stage.get_status()
self.l_is_mls_connected.setText("Yes" if status['connected'] else "No")
self.l_is_mls_x_home.setText("Yes" if status['x_homed'] else "No")
self.l_is_mls_y_home.setText("Yes" if status['y_homed'] else "No")
self.l_is_mls_ready.setText("Yes" if status['ready'] else "No")
self.l_is_mls_scanning.setText("Yes" if status['scanning'] else "No")
def _update_t3r_status(self):
"""Update T3R device status indicators"""
status = self.t3r_device.get_status()
self.l_is_t3r_connected.setText("Yes" if status['connected'] else "No")
self.l_is_t3r_homed.setText("Yes" if status['homed'] else "No")
self.l_is_t3r_ready.setText("Yes" if status['ready'] else "No")
def _update_microscope_status(self):
"""Update microscope (Genesis/Helios) status indicators"""
status = self.microscope.get_status()
# Helios status
self.l_is_helios_ready.setText("Yes" if status['helios_ready'] else "No")
self.l_is_helios_interlocked.setText("Yes" if status['helios_interlocked'] else "No")
# Genesis status
self.l_is_genesis_ready.setText("Yes" if status['genesis_ready'] else "No")
self.l_is_genesis_interlocked.setText("Yes" if status['genesis_interlocked'] else "No")
def _update_transfer_system_status(self):
"""Update Robo-met.3D transfer system status indicators"""
# Stub implementation - would read from actual I/O
# These represent digital I/O states
io_states = self._read_transfer_io_states()
# SRAS outputs
self.l_sras_ok.setText("High (1)" if io_states['sras_ok'] else "Low (0)")
self.l_sras_ctl.setText("High (1)" if io_states['sras_ctl'] else "Low (0)")
self.l_sras_done.setText("High (1)" if io_states['sras_done'] else "Low (0)")
self.l_sras_error.setText("High (1)" if io_states['sras_error'] else "Low (0)")
# R3D inputs
self.l_r3d_estop_ok.setText("High (1)" if io_states['r3d_estop'] else "Low (0)")
self.l_r3d_rtl.setText("High (1)" if io_states['r3d_ready_to_load'] else "Low (0)")
self.l_r3d_rts.setText("High (1)" if io_states['r3d_ready_to_start'] else "Low (0)")
self.l_r3d_spare.setText("High (1)" if io_states['r3d_spare'] else "Low (0)")
def _read_transfer_io_states(self):
"""
Stub method to read transfer system I/O states
In production, this would read from actual hardware I/O
"""
return {
'sras_ok': False,
'sras_ctl': False,
'sras_done': False,
'sras_error': False,
'r3d_estop': True, # Active low, so True = OK
'r3d_ready_to_load': False,
'r3d_ready_to_start': False,
'r3d_spare': False
}