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 (
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
+2 -2
View File
@@ -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
+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
======================================================
.. 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
# ============================================================================
-2
View File
@@ -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
-10
View File
@@ -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
-1
View File
@@ -4,7 +4,6 @@
Version 1
'''
import struct
from .apt_constants import StatusBits as sb
class APTProtocol():
ADDRESSES = { 'HOST_PC': 0x01, 'CONTROLLER': 0x11,
-51
View File
@@ -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)
-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))
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).
# ---------------------------------------------------------------------------
@@ -261,13 +253,6 @@ class Position:
position: int
@dataclass
class Reg:
ch: int
reg: int
value: int
def decode_pong(p: bytes):
if len(p) < 4:
return None
@@ -304,13 +289,6 @@ def decode_position(p: bytes):
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):
"""MOTION_DONE / STOPPED share the (ch, position) layout."""
return decode_position(p)
-42
View File
@@ -14,7 +14,6 @@ from PyQt6.QtCore import QThread, pyqtSignal, QObject
from PyQt6.QtGui import QImage
import logging
import threading
from contextlib import contextmanager
from typing import List, Optional, Tuple
logger = logging.getLogger(__name__)
@@ -331,29 +330,6 @@ class UC480Camera(QObject):
logger.info("Video capture stopped")
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]:
"""
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.
+2 -4
View File
@@ -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
+1 -3
View File
@@ -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)
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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,
+3 -10
View File
@@ -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
)
-1
View File
@@ -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