pre local llm test

This commit is contained in:
Thomas Ales [M S E]
2026-05-28 18:29:24 -05:00
parent a0e0151b5d
commit 57f50a733d
5 changed files with 433 additions and 33 deletions
+2 -1
View File
@@ -3,5 +3,6 @@
"bbd_port": "/dev/ttyAPT",
"oscope_ip": "192.168.100.105",
"laser_freq_hz": 20000.0,
"save_dir": "/opt/scanengine-3/scans"
"save_dir": "/opt/scanengine-3/scans",
"helios_port": "/dev/ttyUSB0"
}
+17 -10
View File
@@ -280,21 +280,28 @@ class HeliosLaser:
logger.error(f"Invalid response for LDS: {response}")
return None
def get_power_mw(self) -> Optional[float]:
"""
Get laser output power in mW.
Returns:
Power in mW or None if error
"""
response = self._query("HMP")
def _query_millicelsius(self, command: str) -> Optional[float]:
"""Query a temperature register (returns milli-°C) and convert to °C."""
response = self._query(command)
if response:
try:
return float(response)
return int(response) / 1000.0
except ValueError:
logger.error(f"Invalid response for HMP: {response}")
logger.error(f"Invalid response for {command}: {response}")
return None
def get_diode_temp_c(self) -> Optional[float]:
"""Diode temperature in °C (LTA, 5000–50000 milli-°C)."""
return self._query_millicelsius("LTA")
def get_power_stage_temp_c(self) -> Optional[float]:
"""Power stage temperature in °C (LTT, 5000–65355 milli-°C)."""
return self._query_millicelsius("LTT")
def get_qswitch_temp_c(self) -> Optional[float]:
"""Q-switch temperature in °C (EOA, 5000–50000 milli-°C)."""
return self._query_millicelsius("EOA")
def get_controller_serial(self) -> Optional[str]:
"""
Get controller serial number.
+18
View File
@@ -60,6 +60,24 @@
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_stream">
<item>
<widget class="QPushButton" name="uc480_start_btn">
<property name="text">
<string>Start Stream</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="uc480_stop_btn">
<property name="text">
<string>Stop Stream</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="uc480_close_window_btn">
<property name="text">
+10
View File
@@ -897,6 +897,16 @@
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="show_helios_toggle">
<property name="text">
<string>Show Helios Panel</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="start_scan_btn">
<property name="text">
+386 -22
View File
@@ -26,6 +26,7 @@ from PyQt6.QtWidgets import (
ROOT = Path(__file__).parent
sys.path.insert(0, str(ROOT))
from hardware.helios_laser import HeliosLaser
from hardware.pybbd202 import AXIS_X, AXIS_Y, ThorlabsServoDriver
from hardware.tektronix_base import TektronixOscilloscopeBase
from hardware.uc480_camera import CameraStreamThread, UC480Camera
@@ -34,11 +35,12 @@ from hardware.uc480_camera import CameraStreamThread, UC480Camera
_AUI_DEFAULTS_PATH = ROOT / "aui_defaults.json"
_AUI_DEFAULTS_FALLBACK = {
"t3r_port": "/dev/ttyUSB0",
"bbd_port": "/dev/ttyUSB1",
"oscope_ip": "192.168.0.1",
"t3r_port": "/dev/ttyUSB0",
"bbd_port": "/dev/ttyUSB1",
"oscope_ip": "192.168.0.1",
"laser_freq_hz": 2000,
"save_dir": str(ROOT / "scans"),
"save_dir": str(ROOT / "scans"),
"helios_port": "/dev/ttyUSB2",
}
def _load_aui_defaults() -> dict:
@@ -66,6 +68,7 @@ DEFAULT_BBD_PORT = _aui["bbd_port"]
DEFAULT_OSCOPE_IP = _aui["oscope_ip"]
DEFAULT_LASER_FREQ_HZ = float(_aui["laser_freq_hz"])
DEFAULT_SAVE_DIR = _aui["save_dir"]
DEFAULT_HELIOS_PORT = _aui.get("helios_port", "/dev/ttyUSB2")
# ── Scan constants ───────────────────────────────────────────────────────────
@@ -512,7 +515,7 @@ class OscopeWorker(QObject):
# ── CH1 — RF Acoustic Packet ──────────────────────────────────────────
s.set_channel_label_name(1, "RF Acoustic Packet")
s.set_channel_scale(1, 0.05) # 100 mV/div
s.set_channel_scale(1, 0.07) # 70 mV/div
s.set_channel_position(1, 0.0) # 0 divs
s.set_channel_termination(1, 50) # 50 Ohm
s.set_channel_coupling(1, "DC")
@@ -536,7 +539,7 @@ class OscopeWorker(QObject):
# ── CH4 — Bias B ──────────────────────────────────────────────────────
s.set_channel_label_name(4, "Bias - B")
s.set_channel_scale(4, 0.1) # 50 mV/div
s.set_channel_scale(4, 0.1) # 100 mV/div
s.set_channel_position(4, -2.72) # -2.72 divs
s.set_channel_termination(4, 1000000) # 1 Mohm
s.set_channel_coupling(4, "DC")
@@ -562,11 +565,167 @@ class OscopeWorker(QObject):
self._cmd_q.put({"type": "stop"})
# ── Helios laser worker ───────────────────────────────────────────────────────
class HeliosWorker(QObject):
"""Manages HeliosLaser in a worker thread using a command queue."""
connected = pyqtSignal()
disconnected = pyqtSignal()
connection_failed = pyqtSignal(str)
enabled_updated = pyqtSignal(bool)
current_updated = pyqtSignal(int)
diode_temp_updated = pyqtSignal(float)
pstage_temp_updated = pyqtSignal(float)
qswitch_temp_updated = pyqtSignal(float)
status_registers_updated = pyqtSignal(object, object, object) # LER, LCE, CCE (int|None)
error_occurred = pyqtSignal(str)
def __init__(self):
super().__init__()
self._laser: HeliosLaser | None = None
self._cmd_q: queue.Queue = queue.Queue()
self._running = False
self.is_connected = False
@pyqtSlot()
def run(self):
self._running = True
while self._running:
try:
cmd = self._cmd_q.get(timeout=0.1)
self._dispatch(cmd)
except queue.Empty:
pass
if self._laser:
try:
self._laser.disconnect()
except Exception:
pass
def _dispatch(self, cmd: dict):
t = cmd["type"]
if t == "connect":
self._do_connect(cmd["port"])
elif t == "disconnect":
self._do_disconnect()
elif t == "set_enable":
self._do_set_enable(cmd["enable"])
elif t == "set_current":
self._do_set_current(cmd["current_ma"])
elif t == "poll_status":
self._do_poll_status()
elif t == "stop":
self._running = False
def _do_connect(self, port: str):
try:
self._laser = HeliosLaser(timeout=1.0)
if not self._laser.connect(port):
self._laser = None
self.connection_failed.emit("Failed to open serial port")
return
self.is_connected = True
self.connected.emit()
self._do_poll_status()
except Exception as e:
self._laser = None
self.connection_failed.emit(str(e))
def _do_disconnect(self):
if self._laser:
try:
self._laser.set_laser_enable(False)
except Exception:
pass
try:
self._laser.disconnect()
except Exception:
pass
self._laser = None
self.is_connected = False
self.disconnected.emit()
def _do_set_enable(self, enable: bool):
if not self._laser:
return
try:
self._laser.set_laser_enable(enable)
self.enabled_updated.emit(self._laser.is_laser_enabled())
except Exception as e:
self.error_occurred.emit(str(e))
def _do_set_current(self, current_ma: int):
if not self._laser:
return
try:
self._laser.set_current_ma(current_ma)
self.current_updated.emit(current_ma)
except Exception as e:
self.error_occurred.emit(str(e))
def _do_poll_status(self):
if not self._laser or not self._laser.is_connected:
return
try:
ler, lce, cce = self._laser.get_status_registers()
self.status_registers_updated.emit(ler, lce, cce)
except Exception:
pass
try:
self.enabled_updated.emit(self._laser.is_laser_enabled())
except Exception:
pass
try:
current = self._laser.get_current_ma()
if current is not None:
self.current_updated.emit(current)
except Exception:
pass
try:
t = self._laser.get_diode_temp_c()
if t is not None:
self.diode_temp_updated.emit(t)
except Exception:
pass
try:
t = self._laser.get_power_stage_temp_c()
if t is not None:
self.pstage_temp_updated.emit(t)
except Exception:
pass
try:
t = self._laser.get_qswitch_temp_c()
if t is not None:
self.qswitch_temp_updated.emit(t)
except Exception:
pass
def queue_connect(self, port: str):
self._cmd_q.put({"type": "connect", "port": port})
def queue_disconnect(self):
self._cmd_q.put({"type": "disconnect"})
def queue_set_enable(self, enable: bool):
self._cmd_q.put({"type": "set_enable", "enable": enable})
def queue_set_current(self, current_ma: int):
self._cmd_q.put({"type": "set_current", "current_ma": current_ma})
def queue_poll_status(self):
self._cmd_q.put({"type": "poll_status"})
def stop_worker(self):
self._cmd_q.put({"type": "stop"})
# ── Camera window popup ───────────────────────────────────────────────────────
class CameraWindow(QWidget):
"""Camera display popup — auto-connects on show, auto-disconnects on close."""
closed = pyqtSignal()
def __init__(self, parent: QWidget | None = None):
super().__init__(parent, Qt.WindowType.Window)
uic.loadUi(ROOT / "sc3-aui-camera.ui", self)
@@ -590,18 +749,25 @@ class CameraWindow(QWidget):
self.uc480_exposure_slider.valueChanged.connect(self._set_exposure)
self.uc480_gain_slider.valueChanged.connect(self._set_gain)
self.uc480_start_btn.clicked.connect(self._start_stream)
self.uc480_stop_btn.clicked.connect(self._stop_stream)
self.uc480_close_window_btn.clicked.connect(self.close)
self._update_controls()
def showEvent(self, event):
super().showEvent(event)
self._start_camera()
self._connect_camera()
self._start_stream()
def closeEvent(self, event):
self._stop_camera()
self._stop_stream()
self._disconnect_camera()
super().closeEvent(event)
self.closed.emit()
def _start_camera(self):
if self._stream is not None:
def _connect_camera(self):
if self._camera is not None:
return
try:
self._camera = UC480Camera()
@@ -610,22 +776,33 @@ class CameraWindow(QWidget):
self.uc480_exposure_slider.blockSignals(True)
self.uc480_exposure_slider.setValue(max(1, round(exp_ms * 10)))
self.uc480_exposure_slider.blockSignals(False)
self._stream = CameraStreamThread(self._camera)
self._stream.frame_ready.connect(self._on_frame)
self._stream.start()
except Exception as e:
QMessageBox.warning(self, "Camera Error", f"Could not open camera:\n{e}")
self._camera = None
self._frame_label.setText(f"Camera error:\n{e}")
self._update_controls()
def _stop_camera(self):
if self._stream:
self._stream.stop()
self._stream = None
def _disconnect_camera(self):
if self._camera:
try:
self._camera.cleanup()
except Exception:
pass
self._camera = None
self._update_controls()
def _start_stream(self):
if self._stream is not None or self._camera is None:
return
self._stream = CameraStreamThread(self._camera)
self._stream.frame_ready.connect(self._on_frame)
self._stream.start()
self._update_controls()
def _stop_stream(self):
if self._stream:
self._stream.stop()
self._stream = None
self._update_controls()
def _on_frame(self, image: QImage):
px = QPixmap.fromImage(image).scaled(
@@ -650,6 +827,162 @@ class CameraWindow(QWidget):
except Exception:
pass
def _update_controls(self):
camera_ok = self._camera is not None
streaming = self._stream is not None
self.uc480_start_btn.setEnabled(camera_ok and not streaming)
self.uc480_stop_btn.setEnabled(streaming)
self.uc480_exposure_slider.setEnabled(camera_ok)
self.uc480_gain_slider.setEnabled(camera_ok)
# ── Helios laser panel ────────────────────────────────────────────────────────
class HeliosWindow(QWidget):
"""Helios laser control panel — shown/hidden with a toggle in the main window."""
closed = pyqtSignal()
def __init__(self, worker: HeliosWorker, parent: QWidget | None = None):
super().__init__(parent, Qt.WindowType.Window)
uic.loadUi(ROOT / "sc3-aui-helios.ui", self)
self.setWindowTitle("Helios Laser")
self._worker = worker
self._poll_timer = QTimer(self)
self._poll_timer.setInterval(1000)
self._poll_timer.timeout.connect(self._worker.queue_poll_status)
self._wire_signals()
self._update_controls(False)
self._set_emission_indicator(False)
def closeEvent(self, event):
self._poll_timer.stop()
if self._worker.is_connected:
self._worker.queue_disconnect()
super().closeEvent(event)
self.closed.emit()
def _wire_signals(self):
self.helios_connect_toggle.toggled.connect(self._on_connect_toggle)
self._worker.connected.connect(self._on_connected)
self._worker.disconnected.connect(self._on_disconnected)
self._worker.connection_failed.connect(self._on_connection_failed)
self.helios_enable_btn.toggled.connect(self._on_enable_toggle)
self._worker.enabled_updated.connect(self._on_enabled_updated)
self.helios_set_current_btn.clicked.connect(self._on_set_current)
self._worker.current_updated.connect(self._on_current_updated)
self._worker.diode_temp_updated.connect(
lambda t: self.helios_temp_diode_label.setText(f"{t:.1f} °C")
)
self._worker.pstage_temp_updated.connect(
lambda t: self.helios_temp_pstage_label.setText(f"{t:.1f} °C")
)
self._worker.qswitch_temp_updated.connect(
lambda t: self.helios_temp_qswitch_label.setText(f"{t:.1f} °C")
)
self._worker.status_registers_updated.connect(self._on_status_registers)
self._worker.error_occurred.connect(
lambda m: self.helios_status_label.setText(m)
)
def _on_connect_toggle(self, checked: bool):
if checked:
self.helios_connect_toggle.setText("Connecting…")
self.helios_connect_toggle.setEnabled(False)
port = self.helios_port_edit.text().strip()
d = _load_aui_defaults()
d["helios_port"] = port
_save_aui_defaults(d)
self._worker.queue_connect(port)
else:
self._poll_timer.stop()
self._worker.queue_disconnect()
def _on_connected(self):
self.helios_connect_toggle.setEnabled(True)
self.helios_connect_toggle.setText("Disconnect")
self.helios_status_label.setText("Connected")
self._update_controls(True)
self._poll_timer.start()
def _on_disconnected(self):
self._poll_timer.stop()
self.helios_connect_toggle.blockSignals(True)
self.helios_connect_toggle.setChecked(False)
self.helios_connect_toggle.setText("Connect")
self.helios_connect_toggle.setEnabled(True)
self.helios_connect_toggle.blockSignals(False)
self.helios_status_label.setText("Disconnected")
self._update_controls(False)
def _on_connection_failed(self, msg: str):
self.helios_connect_toggle.blockSignals(True)
self.helios_connect_toggle.setChecked(False)
self.helios_connect_toggle.setText("Connect")
self.helios_connect_toggle.setEnabled(True)
self.helios_connect_toggle.blockSignals(False)
self.helios_status_label.setText(f"Failed: {msg}")
self._update_controls(False)
def _set_emission_indicator(self, enabled: bool):
if enabled:
self.helios_emission_indicator.setStyleSheet(
"background-color: #ff2222; border-radius: 10px;"
)
else:
self.helios_emission_indicator.setStyleSheet(
"background-color: #444444; border-radius: 10px;"
)
def _on_enable_toggle(self, checked: bool):
self.helios_enable_btn.setText("Disable Laser" if checked else "Enable Laser")
self._worker.queue_set_enable(checked)
def _on_enabled_updated(self, enabled: bool):
self.helios_enable_btn.blockSignals(True)
self.helios_enable_btn.setChecked(enabled)
self.helios_enable_btn.setText("Disable Laser" if enabled else "Enable Laser")
self.helios_enable_btn.blockSignals(False)
self._set_emission_indicator(enabled)
def _on_set_current(self):
self._worker.queue_set_current(self.helios_current_spin.value())
def _on_current_updated(self, current_ma: int):
self.helios_current_spin.blockSignals(True)
self.helios_current_spin.setValue(current_ma)
self.helios_current_spin.blockSignals(False)
def _reset_temperatures(self):
self.helios_temp_diode_label.setText("--- °C")
self.helios_temp_pstage_label.setText("--- °C")
self.helios_temp_qswitch_label.setText("--- °C")
def _on_status_registers(self, ler, lce, cce):
self.helios_ler_label.setText(f"0x{ler:04X}" if ler is not None else "---")
self.helios_lce_label.setText(f"0x{lce:04X}" if lce is not None else "---")
self.helios_cce_label.setText(f"0x{cce:04X}" if cce is not None else "---")
def _update_controls(self, connected: bool):
self.helios_enable_btn.setEnabled(connected)
self.helios_current_spin.setEnabled(connected)
self.helios_set_current_btn.setEnabled(connected)
if not connected:
self.helios_enable_btn.blockSignals(True)
self.helios_enable_btn.setChecked(False)
self.helios_enable_btn.setText("Enable Laser")
self.helios_enable_btn.blockSignals(False)
self._set_emission_indicator(False)
self._reset_temperatures()
self.helios_ler_label.setText("---")
self.helios_lce_label.setText("---")
self.helios_cce_label.setText("---")
# ── Scan progress popup ───────────────────────────────────────────────────────
@@ -1013,8 +1346,14 @@ class MainWindow(QMainWindow):
self._oscope_worker.moveToThread(self._oscope_thread)
self._oscope_thread.started.connect(self._oscope_worker.run)
self._helios_thread = QThread(self)
self._helios_worker = HeliosWorker()
self._helios_worker.moveToThread(self._helios_thread)
self._helios_thread.started.connect(self._helios_worker.run)
# ── Popup windows ─────────────────────────────────────────────────────
self._camera_win = CameraWindow()
self._helios_win = HeliosWindow(self._helios_worker)
self._scan_progress = ScanProgressWindow()
self._scan_worker: ScanWorker | None = None
@@ -1038,6 +1377,7 @@ class MainWindow(QMainWindow):
self._t3r_thread.start()
self._bbd_thread.start()
self._oscope_thread.start()
self._helios_thread.start()
# ── UI initialisation ─────────────────────────────────────────────────────
@@ -1045,6 +1385,7 @@ class MainWindow(QMainWindow):
self.t3r_comport_edit.setText(DEFAULT_T3R_PORT)
self.bbd202_comport_edit.setText(DEFAULT_BBD_PORT)
self.oscope_ip_edit.setText(DEFAULT_OSCOPE_IP)
self._helios_win.helios_port_edit.setText(DEFAULT_HELIOS_PORT)
self.t3r_jog_speed_edit.setText(str(GR_MOVE_SPEED_HZ))
self.lineEdit_10.setText(str(BBD_DEFAULT_JOG_MM))
@@ -1131,8 +1472,17 @@ class MainWindow(QMainWindow):
# Camera
self.show_camera_toggle.toggled.connect(self._on_camera_toggle)
self._camera_win.uc480_close_window_btn.clicked.connect(
lambda: self.show_camera_toggle.setChecked(False)
self._camera_win.closed.connect(
lambda: self._set_toggle(self.show_camera_toggle, False, "Show Camera Window")
)
# Helios
self.show_helios_toggle.toggled.connect(self._on_helios_toggle)
self._helios_win.closed.connect(
lambda: self._set_toggle(self.show_helios_toggle, False, "Show Helios Panel")
)
self._helios_win.helios_close_btn.clicked.connect(
lambda: self.show_helios_toggle.setChecked(False)
)
# Scan
@@ -1345,6 +1695,16 @@ class MainWindow(QMainWindow):
self.show_camera_toggle.setText("Show Camera Window")
self._camera_win.close()
# ── Helios toggle ─────────────────────────────────────────────────────────
def _on_helios_toggle(self, checked: bool):
if checked:
self.show_helios_toggle.setText("Hide Helios Panel")
self._helios_win.show()
else:
self.show_helios_toggle.setText("Show Helios Panel")
self._helios_win.close()
# ── Scan ──────────────────────────────────────────────────────────────────
def _on_browse_save_dir(self):
@@ -1459,7 +1819,10 @@ class MainWindow(QMainWindow):
self._bbd_worker.queue_disconnect()
if self._oscope_worker.is_connected:
self._oscope_worker.queue_disconnect()
if self._helios_worker.is_connected:
self._helios_worker.queue_disconnect()
self._camera_win.close()
self._helios_win.close()
QTimer.singleShot(1500, QApplication.instance().quit)
# ── Helpers ───────────────────────────────────────────────────────────────
@@ -1478,9 +1841,10 @@ class MainWindow(QMainWindow):
def closeEvent(self, event):
self._camera_win.close()
self._scan_progress.close()
for w in (self._t3r_worker, self._bbd_worker, self._oscope_worker):
self._helios_win.close()
for w in (self._t3r_worker, self._bbd_worker, self._oscope_worker, self._helios_worker):
w.stop_worker()
for t in (self._t3r_thread, self._bbd_thread, self._oscope_thread):
for t in (self._t3r_thread, self._bbd_thread, self._oscope_thread, self._helios_thread):
t.quit()
t.wait(2000)
super().closeEvent(event)