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:
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
@@ -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
|
||||
|
||||
|
||||
# ============================================================================
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
Version 1
|
||||
'''
|
||||
import struct
|
||||
from .apt_constants import StatusBits as sb
|
||||
|
||||
class APTProtocol():
|
||||
ADDRESSES = { 'HOST_PC': 0x01, 'CONTROLLER': 0x11,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user