diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md new file mode 100644 index 0000000..39c606f --- /dev/null +++ b/KNOWN_ISSUES.md @@ -0,0 +1,36 @@ +# Known issues requiring on-rig verification + +Questions that cannot be answered from the code alone. Check these the next +time the hardware is available; each one gates a small code change. + +## uC480 camera: gain/exposure during active capture + +The driver used to carry an (unused) `_capture_paused` context manager whose +docstring claimed many IDS cameras return `IS_CANT_COMMUNICATE_WITH_DRIVER` +(17) or `IS_NO_SUCCESS` (-1) when gain/exposure commands are issued during +active capture. `set_exposure()` and `set_gain()` never used it, and the +helper was deleted in the Phase-1 cleanup. + +**Bench check:** with live streaming running, move the exposure and gain +sliders in `camera_test_app.py` and watch the log for those error codes. +If they appear, the setters need a stop-live/apply/restart sequence +(re-create the helper around the two call sites in +[uc480_camera.py](hardware/uc480_camera.py)). + +## Genesis laser: forked protocol implementations disagree + +`hardware/genesis_core.py` and the reference implementation +`tools/genesis_laser_gui.py` disagree on ADC command bytes, LDD enable +polarity, shutter semantics, filtering, and scaling. Do not modify either +until the checklist in [docs/genesis_verification.md](docs/genesis_verification.md) +has been run on the bench. + +## `lib/ueye_loader.so` — still needed? + +`lib/ueye_loader.c` is an `LD_PRELOAD` shim that dlopens +`/usr/lib/libueye_api.so` — yet nothing in the repo references it, and the +vendored SDK copy is `lib/libueye_api64.so.3.82` (a different file). On the +rig, check whether the camera apps run without the shim; if they do, delete +`lib/ueye_loader.{c,so}`. Either way, record in SETUP.md where +`libueye_api64.so.3.82` came from (IDS SDK version) and how the loader is +meant to be used. diff --git a/bbd202_test_app.py b/bbd202_test_app.py index f3ef352..11f10dd 100755 --- a/bbd202_test_app.py +++ b/bbd202_test_app.py @@ -12,10 +12,10 @@ import time from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QLabel, QLineEdit, QPushButton, QComboBox, QDoubleSpinBox, - QStatusBar, QMessageBox, QGridLayout, QCheckBox, QFrame + QStatusBar, QMessageBox, QGridLayout ) from PyQt6.QtCore import Qt, QThread, pyqtSignal, QObject, QTimer -from PyQt6.QtGui import QFont, QKeySequence, QShortcut +from PyQt6.QtGui import QFont from hardware.pybbd202 import ThorlabsServoDriver, AXIS_X, AXIS_Y from hardware.pybbd202.apt_constants import TriggerBitsServo diff --git a/camera_test_app.py b/camera_test_app.py index 9abff0e..93371b1 100755 --- a/camera_test_app.py +++ b/camera_test_app.py @@ -10,9 +10,9 @@ import logging from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QLabel, QPushButton, QDoubleSpinBox, QSpinBox, - QStatusBar, QSizePolicy + QSizePolicy ) -from PyQt6.QtCore import Qt, QTimer +from PyQt6.QtCore import Qt from PyQt6.QtGui import QPixmap, QImage from hardware.uc480_camera import UC480Camera, CameraStreamThread diff --git a/docs/genesis_verification.md b/docs/genesis_verification.md new file mode 100644 index 0000000..3cce6bf --- /dev/null +++ b/docs/genesis_verification.md @@ -0,0 +1,25 @@ +# Genesis laser — hardware verification checklist + +`hardware/genesis_core.py` was extracted from `tools/genesis_laser_gui.py`, +but the extraction changed behavior in ways only the bench can adjudicate. +Until every row below is resolved, **both files stay in the repo unchanged**: +`genesis_laser_gui.py` is the reference implementation, `genesis_core.py` +(+ `tools/genesis_laser_control.py`) is the intended successor. + +Run these with the Genesis laser connected, interlock chain accessible, and +a front-panel/manual reference for current and temperature readouts. + +| # | Divergence | Bench test | Resolution | +|---|---|---|---| +| 1 | **ADS7828 command byte.** Reference passes raw command bytes (`0x84`, `0xe4`, `0x94` — `genesis_laser_gui.py:73-77`); core synthesizes `0x80 \| (ch<<4) \| 0x0c` → `0x8C` for ch0 (`genesis_core.py:413`), different PD1/PD0 power-down bits. | Read the same ADC channel through both implementations; compare against the front-panel current readout. Also check for settling differences right after power-up. | Keep whichever matches the panel; fix the other. | +| 2 | **LDD enable polarity.** Reference `get_ldd_enable()` returns `not bool(value & 0x01)` ("Inverted logic", `genesis_laser_gui.py:602-612`); core returns the un-inverted bit (`genesis_core.py:585-599`). Same register, opposite answers. | With emission verifiably OFF (keyswitch off), read LDD status via both. Exactly one will say "disabled". | Adopt the polarity that matches reality; document the register semantics inline. | +| 3 | **Shutter: manual or bit-controlled?** Reference docs say "this laser has a MANUAL shutter" and `emergency_stop()` deliberately leaves it alone; core `set_shutter()` toggles a PCA9555 bit and `emergency_stop()`/`enter_safe_state()` rely on it. | Toggle `set_shutter()` from core with the beam blocked; observe whether anything physical actuates. | If the bit is inert, remove `set_shutter` and fix the safe-state functions; if real, correct the `tools/` docs. | +| 4 | **ADC filtering dropped.** Reference reads 3× and takes median (`i2c_read_discard_high_low`, `genesis_laser_gui.py:341-364`) or retries until two reads agree; core does single unfiltered reads. | Log ~100 consecutive current readings through core; if the spread is more than display noise, filtering was load-bearing. | Port the median-of-3 helper into `genesis_core.I2CProtocol`. | +| 5 | **Scaling dropped.** Reference converts to Amps/Watts (`AMPS_FULLSCALE * ADC_TO_VOLTS`); core returns raw 0–4095 counts. | Compare a scaled reading against the front panel. | Port the scaling constants + conversion into core. | +| 6 | **Temperatures + power monitoring dropped.** `get_main_temp` / `get_etalon_temp` / `get_shg_temp` / `get_power_actual` exist only in the reference. | Confirm each channel's reading is sane vs. front panel. | Port the four getters into core. | +| 7 | **`pre_flight_check()` dropped.** Reference validates remote-enable + keyswitch + interlock before emission. | n/a — code review + one interlock-open test. | Port into core; call it from `genesis_laser_control.py` before enabling. | + +When all rows are resolved: port the verified behavior into +`genesis_core.py`, update `tools/genesis_laser_control.py`, delete +`tools/genesis_laser_gui.py`, and remove this checklist plus the warning +header in `genesis_core.py`. diff --git a/hardware/genesis_core.py b/hardware/genesis_core.py index d3c82aa..2d5ab97 100755 --- a/hardware/genesis_core.py +++ b/hardware/genesis_core.py @@ -2,6 +2,16 @@ Genesis SLM MX 532 Laser Core Hardware Control Module ====================================================== +.. warning:: + QUARANTINED — do not modify semantics or dedupe against + ``tools/genesis_laser_gui.py`` until the bench checklist in + ``docs/genesis_verification.md`` has been run. This module was + extracted from that GUI but diverges from it in ways only hardware can + adjudicate: ADS7828 command byte (0x84 vs 0x8C), LDD enable polarity + (inverted vs not), shutter semantics (manual vs bit-controlled), + dropped median-of-3 ADC filtering, dropped Amps/Watts scaling, dropped + temperature reads and pre-flight check. + This module provides low-level hardware control for the Genesis SLM MX 532 laser using NXP I2C-over-serial protocol. It contains reusable classes for serial communication, I2C protocol handling, device control, and laser operations. @@ -34,7 +44,6 @@ from typing import Optional, List from enum import IntEnum import serial -from serial.tools import list_ports # ============================================================================ diff --git a/hardware/pybbd202/__init__.py b/hardware/pybbd202/__init__.py index 030d3b7..c5aaea8 100755 --- a/hardware/pybbd202/__init__.py +++ b/hardware/pybbd202/__init__.py @@ -1,9 +1,7 @@ """pybbd202 - Thorlabs BBD202 servo stage driver (pyserial-based)""" from .bbd20x import ThorlabsServoDriver -from .apt_constants import TriggerBitsServo, StatusBits # Axis address constants AXIS_X = 0x21 AXIS_Y = 0x22 -CONTROLLER = 0x11 diff --git a/hardware/pybbd202/apt_constants.py b/hardware/pybbd202/apt_constants.py index 882ad6b..c0a1959 100755 --- a/hardware/pybbd202/apt_constants.py +++ b/hardware/pybbd202/apt_constants.py @@ -32,16 +32,6 @@ class StatusBits(IntFlag): MOT_SB_COMMUTATIONERROR | MOT_SB_OVERLOAD | MOT_SB_ERROR | MOT_SB_INSTRERROR) -class TriggerBitsStepper(IntFlag): - TRIGIN_ENABLE = 0x01, - TRIGOUT_ENABLE = 0x02, - TRIGOUT_MODEFOLLOW = 0x04, - TRIGOUT_MODEMOVEEND = 0x08, - TRIG_RELMOVE = 0x10, - TRIG_ABSMOVE = 0x20, - TRIG_HOMEMOVE = 0x40, - TRIGOUT_NOTRIGIN = 0x80 - class TriggerBitsServo(IntFlag): TRIGIN_HIGH = 0x01 TRIGIN_RELMOVE = 0x02 diff --git a/hardware/pybbd202/apt_messages.py b/hardware/pybbd202/apt_messages.py index a96704f..2706140 100755 --- a/hardware/pybbd202/apt_messages.py +++ b/hardware/pybbd202/apt_messages.py @@ -4,7 +4,6 @@ Version 1 ''' import struct -from .apt_constants import StatusBits as sb class APTProtocol(): ADDRESSES = { 'HOST_PC': 0x01, 'CONTROLLER': 0x11, diff --git a/hardware/pybbd202/bbd20x.py b/hardware/pybbd202/bbd20x.py index 2c7b9cc..f02dbe4 100755 --- a/hardware/pybbd202/bbd20x.py +++ b/hardware/pybbd202/bbd20x.py @@ -288,25 +288,6 @@ class ThorlabsServoDriver(): self.am_moving[ch] = False return - def _update0x0212(self, msg): - ''' - _update0x0212 - internal function that listens for CHANENABLESTATE - messages. - ''' - if msg['source'] == 0x21: - ch = 0 - elif msg['source'] == 0x22: - ch = 1 - else: - raise ValueError("Wherever this message came from, it's WRONG!") - - if msg['enable_state'] == 0x01: - self.am_enabled[ch] = True # enabled - elif msg['enable_state'] == 0x02: - self.am_enabled[ch] = False # disabled - else: - raise ValueError("Am I a joke to you? WTF did this even come from?!") - # ── Axis control ───────────────────────────────────────────── def enable_axis(self, axis): @@ -473,38 +454,6 @@ class ThorlabsServoDriver(): destination=axis, source=0x01) return TriggerBitsServo(result['mode']) - def set_trigger_trigin_high(self, axis): - '''Set trigger input to logic high.''' - self.set_trigger(axis, TriggerBitsServo.TRIGIN_HIGH) - - def set_trigger_trigin_relmove(self, axis): - '''Set trigger input to initiate a relative move.''' - self.set_trigger(axis, TriggerBitsServo.TRIGIN_RELMOVE) - - def set_trigger_trigin_absmove(self, axis): - '''Set trigger input to initiate an absolute move.''' - self.set_trigger(axis, TriggerBitsServo.TRIGIN_ABSMOVE) - - def set_trigger_trigin_homemove(self, axis): - '''Set trigger input to initiate a home move.''' - self.set_trigger(axis, TriggerBitsServo.TRIGIN_HOMEMOVE) - - def set_trigger_trigout_high(self, axis): - '''Set trigger output to logic high.''' - self.set_trigger(axis, TriggerBitsServo.TRIGOUT_HIGH) - - def set_trigger_trigout_inmotion(self, axis): - '''Set trigger output high while axis is in motion.''' - self.set_trigger(axis, TriggerBitsServo.TRIGOUT_INMOTION) - - def set_trigger_trigout_motioncomplete(self, axis): - '''Set trigger output to pulse when motion completes.''' - self.set_trigger(axis, TriggerBitsServo.TRIGOUT_MOTIONCOMPLETE) - - def set_trigger_trigout_maxvelocity(self, axis): - '''Set trigger output to pulse at max velocity.''' - self.set_trigger(axis, TriggerBitsServo.TRIGOUT_MAXVELOCITY) - def set_trigger_trigout_maxv(self, axis): '''Set trigger output high + pulse at max velocity (TRIGOUT_MAXV).''' self.set_trigger(axis, TriggerBitsServo.TRIGOUT_MAXV) diff --git a/hardware/t3r_protocol.py b/hardware/t3r_protocol.py index ab37229..32abb80 100755 --- a/hardware/t3r_protocol.py +++ b/hardware/t3r_protocol.py @@ -193,14 +193,6 @@ def set_position(ch: int, position: int) -> bytes: return build_frame(CMD_SET_POSITION, struct.pack(" bytes: - return build_frame(CMD_READ_REG, struct.pack(" bytes: - return build_frame(CMD_WRITE_REG, struct.pack(" Optional[QImage]: """ Capture a single frame from the camera. @@ -535,24 +511,6 @@ class UC480Camera(QObject): logger.error(f"Failed to set framerate: {ret}") return False - def get_framerate(self) -> Optional[float]: - """ - Get current framerate. - - Returns: - Framerate in fps, or None if failed - """ - if not self.is_initialized: - return None - - fps = ueye.c_double() - ret = ueye.is_GetFramesPerSecond(self.h_cam, fps) - - if ret == ueye.IS_SUCCESS: - return fps.value - else: - return None - def set_gain(self, master_gain: int) -> bool: """ Set camera master gain. diff --git a/helios_test_app.py b/helios_test_app.py index b112aa0..358ce86 100755 --- a/helios_test_app.py +++ b/helios_test_app.py @@ -6,15 +6,13 @@ Simple PyQt6 GUI for testing and controlling the Helios laser. import sys import logging -from typing import Optional -from enum import Enum from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QLabel, QLineEdit, QPushButton, QComboBox, QSpinBox, - QStatusBar, QMessageBox, QTabWidget, QTextEdit + QMessageBox, QTabWidget, QTextEdit ) -from PyQt6.QtCore import Qt, QThread, pyqtSignal, QObject +from PyQt6.QtCore import QThread, pyqtSignal, QObject from PyQt6.QtGui import QFont from hardware.helios_laser import HeliosLaser, PulseMode diff --git a/sc3_aui_app.py b/sc3_aui_app.py index ac3b9e5..28d50a5 100755 --- a/sc3_aui_app.py +++ b/sc3_aui_app.py @@ -16,10 +16,9 @@ import threading from threading import Thread import numpy as np -import serial from PyQt6 import uic from PyQt6.QtCore import QObject, QThread, QTimer, Qt, pyqtSignal, pyqtSlot -from PyQt6.QtGui import QImage, QPixmap, QPainter, QColor, QPen +from PyQt6.QtGui import QImage, QPixmap from PyQt6.QtWidgets import ( QApplication, QDialog, QDialogButtonBox, QFileDialog, QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QMainWindow, QMessageBox, QPushButton, @@ -93,7 +92,6 @@ SCAN_RAMP_MM = SCAN_VELOCITY_MM_S**2 / (2.0 * SCAN_ACCEL_MM_S2) SCAN_RAMP_BUFFER_MM = 1.0 SCOPE_SAMPLE_RATE = 6.25e9 # 6.25 GS/s → 160 ps/sample SCOPE_TRIG_LEVEL_V = 0.500 -T3R_BAUD = 115200 # GR-axis rotation defaults (used by ScanWorker between angles) GR_MICROSTEPS = 8 # microsteps/full-step on GR axis (ch3) diff --git a/sras_scan_manager.py b/sras_scan_manager.py index 65933c7..ad3e78c 100755 --- a/sras_scan_manager.py +++ b/sras_scan_manager.py @@ -18,7 +18,7 @@ Only format version 6 is supported. import argparse import struct import sys -from dataclasses import dataclass, field +from dataclasses import dataclass from datetime import datetime from pathlib import Path diff --git a/t3r_control_panel.py b/t3r_control_panel.py index 9bd2b74..7b9784e 100755 --- a/t3r_control_panel.py +++ b/t3r_control_panel.py @@ -19,7 +19,7 @@ Usage:: from __future__ import annotations -from PyQt6.QtCore import Qt, QTimer +from PyQt6.QtCore import Qt from PyQt6.QtGui import QFont from PyQt6.QtWidgets import ( QCheckBox, QComboBox, QDialog, QDoubleSpinBox, QFrame, QGridLayout, diff --git a/tools/genesis_laser_control.py b/tools/genesis_laser_control.py index cfdab4c..41098a8 100755 --- a/tools/genesis_laser_control.py +++ b/tools/genesis_laser_control.py @@ -31,26 +31,19 @@ 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 + QMessageBox, QStatusBar ) -from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QSettings -from PyQt6.QtGui import QFont, QPalette, QColor +from PyQt6.QtCore import Qt, QTimer, QSettings +from PyQt6.QtGui import QFont # Import core hardware control classes from hardware.genesis_core import ( - I2CAddress, PCA9555Register, ControlBitmask, SerialComm, I2CProtocol, I2CDevices, LaserControl ) diff --git a/tools/genesis_laser_gui.py b/tools/genesis_laser_gui.py index e84de04..2e01f1d 100755 --- a/tools/genesis_laser_gui.py +++ b/tools/genesis_laser_gui.py @@ -39,7 +39,6 @@ from PyQt6.QtWidgets import ( QLineEdit, QTextEdit, QCheckBox, QMessageBox, QGroupBox, QGridLayout ) from PyQt6.QtCore import QObject, pyqtSignal, QTimer, Qt -from PyQt6.QtGui import QPalette, QColor # ============================================================================ # CONSTANTS