Phase 1c: prune remaining dead functions, unused imports, quarantine Genesis

- uc480_camera: drop never-called _capture_paused/get_framerate (the
  hardware question _capture_paused encoded is now in KNOWN_ISSUES.md)
- t3r_protocol: drop read_reg/write_reg/decode_reg/Reg (commands never
  wired into the driver)
- bbd20x: drop _update0x0212 (never dispatched) and 8 of 9 unused
  trigger convenience wrappers; apt_constants: drop TriggerBitsStepper
  (servo-only rig)
- ruff --fix: 35 unused imports across all apps; drop unused T3R_BAUD
- genesis_core.py: quarantine warning header; docs/genesis_verification.md
  bench checklist for the 7 divergences vs tools/genesis_laser_gui.py

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Thomas Ales
2026-07-28 10:08:48 -05:00
parent 5148f0bca2
commit 67aabde4b6
17 changed files with 83 additions and 153 deletions
+36
View File
@@ -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.
+2 -2
View File
@@ -12,10 +12,10 @@ import time
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QGroupBox, QLabel, QLineEdit, QPushButton, QComboBox, QDoubleSpinBox, 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.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 import ThorlabsServoDriver, AXIS_X, AXIS_Y
from hardware.pybbd202.apt_constants import TriggerBitsServo from hardware.pybbd202.apt_constants import TriggerBitsServo
+2 -2
View File
@@ -10,9 +10,9 @@ import logging
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QGroupBox, QLabel, QPushButton, QDoubleSpinBox, QSpinBox, 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 PyQt6.QtGui import QPixmap, QImage
from hardware.uc480_camera import UC480Camera, CameraStreamThread from hardware.uc480_camera import UC480Camera, CameraStreamThread
+25
View File
@@ -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`.
+10 -1
View File
@@ -2,6 +2,16 @@
Genesis SLM MX 532 Laser Core Hardware Control Module 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 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 using NXP I2C-over-serial protocol. It contains reusable classes for serial
communication, I2C protocol handling, device control, and laser operations. communication, I2C protocol handling, device control, and laser operations.
@@ -34,7 +44,6 @@ from typing import Optional, List
from enum import IntEnum from enum import IntEnum
import serial import serial
from serial.tools import list_ports
# ============================================================================ # ============================================================================
-2
View File
@@ -1,9 +1,7 @@
"""pybbd202 - Thorlabs BBD202 servo stage driver (pyserial-based)""" """pybbd202 - Thorlabs BBD202 servo stage driver (pyserial-based)"""
from .bbd20x import ThorlabsServoDriver from .bbd20x import ThorlabsServoDriver
from .apt_constants import TriggerBitsServo, StatusBits
# Axis address constants # Axis address constants
AXIS_X = 0x21 AXIS_X = 0x21
AXIS_Y = 0x22 AXIS_Y = 0x22
CONTROLLER = 0x11
-10
View File
@@ -32,16 +32,6 @@ class StatusBits(IntFlag):
MOT_SB_COMMUTATIONERROR | MOT_SB_OVERLOAD | MOT_SB_COMMUTATIONERROR | MOT_SB_OVERLOAD |
MOT_SB_ERROR | MOT_SB_INSTRERROR) 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): class TriggerBitsServo(IntFlag):
TRIGIN_HIGH = 0x01 TRIGIN_HIGH = 0x01
TRIGIN_RELMOVE = 0x02 TRIGIN_RELMOVE = 0x02
-1
View File
@@ -4,7 +4,6 @@
Version 1 Version 1
''' '''
import struct import struct
from .apt_constants import StatusBits as sb
class APTProtocol(): class APTProtocol():
ADDRESSES = { 'HOST_PC': 0x01, 'CONTROLLER': 0x11, ADDRESSES = { 'HOST_PC': 0x01, 'CONTROLLER': 0x11,
-51
View File
@@ -288,25 +288,6 @@ class ThorlabsServoDriver():
self.am_moving[ch] = False self.am_moving[ch] = False
return 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 ───────────────────────────────────────────── # ── Axis control ─────────────────────────────────────────────
def enable_axis(self, axis): def enable_axis(self, axis):
@@ -473,38 +454,6 @@ class ThorlabsServoDriver():
destination=axis, source=0x01) destination=axis, source=0x01)
return TriggerBitsServo(result['mode']) 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): def set_trigger_trigout_maxv(self, axis):
'''Set trigger output high + pulse at max velocity (TRIGOUT_MAXV).''' '''Set trigger output high + pulse at max velocity (TRIGOUT_MAXV).'''
self.set_trigger(axis, TriggerBitsServo.TRIGOUT_MAXV) self.set_trigger(axis, TriggerBitsServo.TRIGOUT_MAXV)
-22
View File
@@ -193,14 +193,6 @@ def set_position(ch: int, position: int) -> bytes:
return build_frame(CMD_SET_POSITION, struct.pack("<Bi", ch, position)) return build_frame(CMD_SET_POSITION, struct.pack("<Bi", ch, position))
def read_reg(ch: int, reg: int) -> bytes:
return build_frame(CMD_READ_REG, struct.pack("<BB", ch, reg))
def write_reg(ch: int, reg: int, value: int) -> bytes:
return build_frame(CMD_WRITE_REG, struct.pack("<BBI", ch, reg, value))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Response / event decoders. Each returns a dataclass (or None on bad length). # Response / event decoders. Each returns a dataclass (or None on bad length).
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -261,13 +253,6 @@ class Position:
position: int position: int
@dataclass
class Reg:
ch: int
reg: int
value: int
def decode_pong(p: bytes): def decode_pong(p: bytes):
if len(p) < 4: if len(p) < 4:
return None return None
@@ -304,13 +289,6 @@ def decode_position(p: bytes):
return Position(ch, pos) return Position(ch, pos)
def decode_reg(p: bytes):
if len(p) < 6:
return None
ch, reg, value = struct.unpack_from("<BBI", p, 0)
return Reg(ch, reg, value)
def decode_event_position(p: bytes): def decode_event_position(p: bytes):
"""MOTION_DONE / STOPPED share the (ch, position) layout.""" """MOTION_DONE / STOPPED share the (ch, position) layout."""
return decode_position(p) return decode_position(p)
-42
View File
@@ -14,7 +14,6 @@ from PyQt6.QtCore import QThread, pyqtSignal, QObject
from PyQt6.QtGui import QImage from PyQt6.QtGui import QImage
import logging import logging
import threading import threading
from contextlib import contextmanager
from typing import List, Optional, Tuple from typing import List, Optional, Tuple
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -331,29 +330,6 @@ class UC480Camera(QObject):
logger.info("Video capture stopped") logger.info("Video capture stopped")
return True return True
@contextmanager
def _capture_paused(self):
"""
Context manager that temporarily stops live video while a camera
parameter is being changed, then restarts it. Many IDS cameras
return IS_CANT_COMMUNICATE_WITH_DRIVER (17) or IS_NO_SUCCESS (-1)
when gain/exposure commands are issued during active capture.
"""
with self._settings_lock:
was_capturing = self.is_capturing
if was_capturing:
ueye.is_StopLiveVideo(self.h_cam, ueye.IS_WAIT)
self.is_capturing = False
try:
yield
finally:
if was_capturing:
ret = ueye.is_CaptureVideo(self.h_cam, ueye.IS_DONT_WAIT)
if ret == ueye.IS_SUCCESS:
self.is_capturing = True
else:
logger.error(f"Failed to restart capture after settings change: {ret}")
def get_frame(self) -> Optional[QImage]: def get_frame(self) -> Optional[QImage]:
""" """
Capture a single frame from the camera. Capture a single frame from the camera.
@@ -535,24 +511,6 @@ class UC480Camera(QObject):
logger.error(f"Failed to set framerate: {ret}") logger.error(f"Failed to set framerate: {ret}")
return False 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: def set_gain(self, master_gain: int) -> bool:
""" """
Set camera master gain. Set camera master gain.
+2 -4
View File
@@ -6,15 +6,13 @@ Simple PyQt6 GUI for testing and controlling the Helios laser.
import sys import sys
import logging import logging
from typing import Optional
from enum import Enum
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QGroupBox, QLabel, QLineEdit, QPushButton, QComboBox, QSpinBox, 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 PyQt6.QtGui import QFont
from hardware.helios_laser import HeliosLaser, PulseMode from hardware.helios_laser import HeliosLaser, PulseMode
+1 -3
View File
@@ -16,10 +16,9 @@ import threading
from threading import Thread from threading import Thread
import numpy as np import numpy as np
import serial
from PyQt6 import uic from PyQt6 import uic
from PyQt6.QtCore import QObject, QThread, QTimer, Qt, pyqtSignal, pyqtSlot 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 ( from PyQt6.QtWidgets import (
QApplication, QDialog, QDialogButtonBox, QFileDialog, QHBoxLayout, QLabel, QApplication, QDialog, QDialogButtonBox, QFileDialog, QHBoxLayout, QLabel,
QListWidget, QListWidgetItem, QMainWindow, QMessageBox, QPushButton, 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 SCAN_RAMP_BUFFER_MM = 1.0
SCOPE_SAMPLE_RATE = 6.25e9 # 6.25 GS/s → 160 ps/sample SCOPE_SAMPLE_RATE = 6.25e9 # 6.25 GS/s → 160 ps/sample
SCOPE_TRIG_LEVEL_V = 0.500 SCOPE_TRIG_LEVEL_V = 0.500
T3R_BAUD = 115200
# GR-axis rotation defaults (used by ScanWorker between angles) # GR-axis rotation defaults (used by ScanWorker between angles)
GR_MICROSTEPS = 8 # microsteps/full-step on GR axis (ch3) GR_MICROSTEPS = 8 # microsteps/full-step on GR axis (ch3)
+1 -1
View File
@@ -18,7 +18,7 @@ Only format version 6 is supported.
import argparse import argparse
import struct import struct
import sys import sys
from dataclasses import dataclass, field from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
+1 -1
View File
@@ -19,7 +19,7 @@ Usage::
from __future__ import annotations from __future__ import annotations
from PyQt6.QtCore import Qt, QTimer from PyQt6.QtCore import Qt
from PyQt6.QtGui import QFont from PyQt6.QtGui import QFont
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QCheckBox, QComboBox, QDialog, QDoubleSpinBox, QFrame, QGridLayout, QCheckBox, QComboBox, QDialog, QDoubleSpinBox, QFrame, QGridLayout,
+3 -10
View File
@@ -31,26 +31,19 @@ Date: 2026-01-24
""" """
import sys 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 serial.tools import list_ports
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QTabWidget, QLabel, QSlider, QPushButton, QSpinBox, QCheckBox, QTabWidget, QLabel, QSlider, QPushButton, QSpinBox, QCheckBox,
QComboBox, QTextEdit, QLineEdit, QGroupBox, QGridLayout, QComboBox, QTextEdit, QLineEdit, QGroupBox, QGridLayout,
QMessageBox, QStatusBar, QProgressBar QMessageBox, QStatusBar
) )
from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QSettings from PyQt6.QtCore import Qt, QTimer, QSettings
from PyQt6.QtGui import QFont, QPalette, QColor from PyQt6.QtGui import QFont
# Import core hardware control classes # Import core hardware control classes
from hardware.genesis_core import ( from hardware.genesis_core import (
I2CAddress, PCA9555Register, ControlBitmask,
SerialComm, I2CProtocol, I2CDevices, LaserControl SerialComm, I2CProtocol, I2CDevices, LaserControl
) )
-1
View File
@@ -39,7 +39,6 @@ from PyQt6.QtWidgets import (
QLineEdit, QTextEdit, QCheckBox, QMessageBox, QGroupBox, QGridLayout QLineEdit, QTextEdit, QCheckBox, QMessageBox, QGroupBox, QGridLayout
) )
from PyQt6.QtCore import QObject, pyqtSignal, QTimer, Qt from PyQt6.QtCore import QObject, pyqtSignal, QTimer, Qt
from PyQt6.QtGui import QPalette, QColor
# ============================================================================ # ============================================================================
# CONSTANTS # CONSTANTS