Phase 6: strip signature-restating docstrings; correct README/SETUP

- Collapsed Args:/Returns:/Raises: blocks that only restated the
  signature (364 lines): tektronix_base 48% -> ~20% doc density,
  helios_laser and uc480_camera likewise. Only docstrings whose entire
  body was those sections were touched.
- Preserved verbatim the comments that carry hardware knowledge the code
  can't express: uc480's USB split-transaction contention note (with its
  measured fps), the IS_ALLOW_STARTER_FW_UPLOAD segfault explanation, the
  QImage-copy rationale, and tektronix's NUMFRAMESACQuired warning.
- README: project structure, quick start, and every usage example now
  describe code that exists (they referenced hardware/bbd202.py,
  CoherentHOPSLaser, get_curve_binary, and 'python -m scanengine.app',
  none of which do). Added a headless-scan example and a read-a-scan-file
  example, since reuse without the GUI is the point of the refactor.
- SETUP: structure section defers to README instead of keeping a second
  stale copy; documents the vendored uEye SDK and the Genesis quarantine.
- ruff is now clean repo-wide: fixed the remaining raise-from, unused
  loop variables, placeholder f-strings, and a non-strict zip; the
  widget-layout semicolon idiom is an explicit config ignore rather than
  22 standing warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Thomas Ales
2026-07-28 11:27:36 -05:00
parent 44febe34b8
commit 709dc529df
10 changed files with 264 additions and 584 deletions
+177 -93
View File
@@ -9,7 +9,7 @@ scanengine-3 is a unified platform for scanning acoustic microscopy and precisio
### Key Features ### Key Features
- **Stage Control**: ThorLabs BBD202/BBD203 motor controller with 3-axis positioning - **Stage Control**: ThorLabs BBD202/BBD203 motor controller with 3-axis positioning
- **Laser Systems**: Helios and Coherent HOPS laser control - **Laser Systems**: Helios pulsed laser and Genesis CW laser control
- **Data Acquisition**: Tektronix oscilloscope integration with fast-frame support - **Data Acquisition**: Tektronix oscilloscope integration with fast-frame support
- **Scan Planning**: Automated raster scan generation and execution - **Scan Planning**: Automated raster scan generation and execution
- **Real-time Monitoring**: Live status updates and progress tracking - **Real-time Monitoring**: Live status updates and progress tracking
@@ -30,11 +30,6 @@ scanengine-3 is a unified platform for scanning acoustic microscopy and precisio
- Multiple pulse modes - Multiple pulse modes
- Temperature and power monitoring - Temperature and power monitoring
- **Coherent HOPS Laser**
- I2C/FTDI interface
- Power and modulation control
- Temperature monitoring
### Data Acquisition ### Data Acquisition
- **Tektronix MSO/DPO Series Oscilloscopes** - **Tektronix MSO/DPO Series Oscilloscopes**
- Direct socket communication (no VISA overhead) - Direct socket communication (no VISA overhead)
@@ -42,69 +37,69 @@ scanengine-3 is a unified platform for scanning acoustic microscopy and precisio
- Multi-channel waveform capture - Multi-channel waveform capture
- Configurable triggering - Configurable triggering
### Microscope Systems ### Rotation / Focus
- **Genesis Microscope** (stub implementation) - **T3R four-channel stepper controller**
- **T3R Timing Device** (stub implementation) - Focus axis plus the GR rotation stage (12.5:1 gear train)
- Custom binary framing protocol over USB serial
## Project Structure ## Project Structure
The codebase is split so that everything needed to run a scan is importable
without PyQt6 or any vendor SDK — `core/` is the headless engine, `gui/` is
the shared Qt layer, and the root scripts are entry points.
``` ```
scanengine-3/ scanengine-3/
├── scanengine/ # Main application package ├── core/ # Headless: no PyQt6, no vendor SDKs
│ ├── __init__.py │ ├── scan_engine.py # ScanEngine — full acquisition sequence
│ ├── app.py # Main application entry point │ ├── scan_geometry.py # ScanPlan, rotated-bbox planning, limits
│ ├── main_launcher.ui # Main launcher UI │ ├── scan_resume.py # Resume planning (frontier rule)
│ ├── new_scan_wizard.ui # Scan wizard UI │ ├── scope_sras.py # Oscilloscope SCPI policy for SRAS
│ └── options.ui # Options dialog UI │ ├── rotation.py # GR rotation axis settings + moves
│ ├── sras_format.py # v6 .sras writer/reader (memory-mapped)
│ ├── sras_analysis.py # Image reducers + SAW matched filter
│ └── config.py # ScanDefaults ⇄ aui_defaults.json
│ │
├── hardware/ # Hardware driver package ├── hardware/ # Device drivers (Qt-free)
│ ├── __init__.py │ ├── serial_util.py # Shared 8N1 open + port enumeration
│ ├── bbd202.py # ThorLabs stage controller │ ├── t3r_driver.py # T3R stepper controller
│ ├── uc480_camera.py # IDS/ThorLabs camera │ ├── t3r_protocol.py # T3R frame encode/decode
│ ├── tektronix_base.py # Tektronix oscilloscope │ ├── helios_laser.py # Helios pulsed laser
│ ├── coherent_hops_laser.py # Coherent HOPS laser │ ├── tektronix_base.py # Tektronix oscilloscope (raw SCPI)
│ └── genesis_core.py # Genesis laser core logic │ ├── uc480_camera.py # IDS/ThorLabs uEye camera (returns QImage)
│ ├── genesis_core.py # Genesis laser — QUARANTINED, see below
│ └── pybbd202/ # ThorLabs BBD202 stage (APT protocol)
│ │
├── scanning/ # Scan planning package ├── gui/ # Shared PyQt6 layer
│ ├── __init__.py │ ├── scan_bridge.py # QtScanController over core.scan_engine
│ ├── sc3_scan_model.py # Scan model │ ├── qt_t3r.py # Qt adapter over the T3R driver
│ └── stage_scan_plan_generator.py # Scan path planning │ ├── qt_workers.py # QueueWorker / PollingQueueWorker bases
│ └── widgets.py # ConnectionBar, LogConsole, PortSelector…
│ │
├── tools/ # Standalone executable tools ├── sc3_aui_app.py # Main acquisition application
│ ├── genesis_laser_control.py # Standalone Genesis app ├── sras_viewer.py # Scan data viewer
│ └── genesis_laser_gui.py # Alternative Genesis GUI ├── sras_scan_manager.py # CLI: inspect/export/delete angles
├── t3r_control_panel.py # T3R panel (used by the main app)
├── helios_test_app.py # Per-device test benches
├── bbd202_test_app.py
├── camera_test_app.py
├── sc3-aui-*.ui # Qt Designer files loaded at runtime
│ │
├── tests/ # Test files ├── tests/ # pytest suite
│ ├── __init__.py │ ├── golden/ # v6 .sras + geometry fixtures
│ ├── test_camera_integration.py │ ├── fakes.py # Recording fake stage/scope/rotator
│ ├── test_genesis_connection.py │ └── test_*.py
│ ├── test_genesis_protocol.py
│ ├── test_rotated_aoi.py
│ └── test_temperature_scaling.py
│ │
├── docs/ # Documentation ├── docs/
│ ├── hardware/ # Hardware documentation │ ├── hardware/ # Driver notes
│ │ ├── BBD203_CONNECTION_GUIDE.md │ ├── protocols/ # Vendor protocol PDFs
│ │ ├── BBD203_Communications_Protocol.md │ └── genesis_verification.md # Bench checklist (see KNOWN_ISSUES.md)
│ │ ├── BBD203_DRIVER_README.md
│ │ ├── HELIOS_DRIVER_README.md
│ │ ├── GENESIS_LASER_README.md
│ │ └── laser_control_implementation_guide.md
│ └── protocols/ # Protocol specifications
│ ├── apt_communications_protocol.pdf
│ ├── helios_comms_protocol.pdf
│ └── thorlabs_mls_protocol.pdf
│ │
├── lib/ # Binary libraries (not in git) ├── lib/ # Vendored IDS uEye SDK (not in git)
│ ├── libueye_api64.so.3.82 ├── aui_defaults.json # Persisted ports / scope IP / save dir
│ ├── ueye_loader.c ├── scan_format.md # .sras binary format specification
│ └── ueye_loader.so ├── KNOWN_ISSUES.md # Open questions needing the hardware
│ └── requirements.txt
├── config.json # System configuration
├── requirements.txt # Python dependencies
├── README.md # This file
├── SETUP.md # Setup instructions
└── LICENSE # License file
``` ```
## Quick Start ## Quick Start
@@ -126,14 +121,32 @@ pip install -r requirements.txt
### Running the Application ### Running the Application
```bash ```bash
# Main GUI application # Main acquisition application
python -m scanengine.app python sc3_aui_app.py
# Scan data viewer
python sras_viewer.py
# Inspect / export / delete angles in a .sras file
python sras_scan_manager.py path/to/scan.sras
# Per-device test benches
python helios_test_app.py
python bbd202_test_app.py
python camera_test_app.py
# Genesis laser control tool # Genesis laser control tool
python tools/genesis_laser_control.py python tools/genesis_laser_control.py
```
# Alternative Genesis laser GUI ### Running the tests
python tools/genesis_laser_gui.py
The suite is hardware-free: fake drivers and committed fixtures stand in
for the rig.
```bash
pip install pytest ruff
python -m pytest tests/ -q
``` ```
## Dependencies ## Dependencies
@@ -143,66 +156,137 @@ python tools/genesis_laser_gui.py
- **pyvisa** (>=1.13.0) - VISA instrument control - **pyvisa** (>=1.13.0) - VISA instrument control
- **pyvisa-py** (>=0.7.0) - Pure Python VISA backend - **pyvisa-py** (>=0.7.0) - Pure Python VISA backend
- **pyftdi** (>=0.54.0) - FTDI USB device support - **pyftdi** (>=0.54.0) - FTDI USB device support
- **numpy** (>=1.20.0) - Array processing
- **scipy** (>=1.10) - Signal processing (viewer SAW pipeline)
- **matplotlib** (>=3.7) - Plotting (viewer, live scan preview)
- **pyueye** (>=4.95.0) - IDS uEye camera SDK bindings (camera only)
## Known hardware caveats
`hardware/genesis_core.py` is quarantined: it diverges from the reference
implementation in `tools/genesis_laser_gui.py` in ways that need the laser
on the bench to settle. See [KNOWN_ISSUES.md](KNOWN_ISSUES.md) and
[docs/genesis_verification.md](docs/genesis_verification.md) before
changing either file.
## Usage Examples ## Usage Examples
### Stage Control ### Running a scan without any GUI
The acquisition sequence lives in `core.scan_engine` and takes plain
drivers plus callbacks, so a script (or a future simpler GUI) can drive the
identical scan the main app runs:
```python ```python
from hardware.bbd202 import BBD202Controller from pathlib import Path
from core.scan_engine import ScanCallbacks, ScanEngine
from core.scan_geometry import build_plan
from core.rotation import RotationAxis
from hardware.pybbd202 import ThorlabsServoDriver
from hardware.tektronix_base import TektronixOscilloscopeBase
from hardware.t3r_driver import T3RDriver
# BBD202/BBD203 controller example plan = build_plan(x_start=10.0, y_start=10.0, x_delta=20.0, y_delta=10.0,
controller = BBD202Controller() num_angles=3, row_spacing=0.25,
controller.connect("/dev/ttyUSB0") # Serial port laser_freq_hz=20000.0, velocity_mm_s=100.0)
# Use controller for stage operations
stage = ThorlabsServoDriver(); stage.connect("/dev/ttyUSB0")
scope = TektronixOscilloscopeBase("192.168.100.105"); scope.connect()
t3r = T3RDriver(); t3r.open("/dev/ttyACM0")
engine = ScanEngine(stage, scope, RotationAxis(t3r), plan,
Path("/data/SRAS/demo.sras"),
callbacks=ScanCallbacks(on_status=print,
prompt=lambda t, m: input(f"{t}: {m} ")))
result = engine.run() # blocking; engine.abort() is thread-safe
print(f"wrote {result.rows_written} rows to {result.path}")
``` ```
### Oscilloscope Acquisition ### Reading a scan file
`SrasFile` memory-maps the data block, so opening a multi-gigabyte scan
costs only the pages actually touched:
```python ```python
from core.sras_format import SrasFile
from core.sras_analysis import CH4_IDX, ChannelCalibration, compute_dc_image
with SrasFile("/data/SRAS/demo.sras") as sras:
print(sras.header.n_angles, "angles")
for st in sras.angle_status(): # handles aborted/partial files
print(f" angle {st.index}: {st.n_rows_available}/{st.n_rows} rows ({st.status})")
view = sras.load_angle(0) # (rows, channels, frames, samples)
calib = ChannelCalibration.from_preambles(sras.preambles)
dc_mv = calib.adc_to_mv(compute_dc_image(view, CH4_IDX), CH4_IDX)
```
### Stage control
```python
from hardware.pybbd202 import AXIS_X, AXIS_Y, ThorlabsServoDriver
stage = ThorlabsServoDriver()
stage.connect("/dev/ttyUSB0") # raises if no bay responds
stage.enable_axis(AXIS_X)
stage.home_axis(AXIS_X, timeout=120.0)
stage.move_axis_absolute(AXIS_X, 25.0, timeout=30.0)
```
### Oscilloscope acquisition
```python
from core.scope_sras import configure_acquisition, configure_channels
from hardware.tektronix_base import TektronixOscilloscopeBase from hardware.tektronix_base import TektronixOscilloscopeBase
scope = TektronixOscilloscopeBase() scope = TektronixOscilloscopeBase("192.168.100.105", port=4000)
scope.connect("192.168.1.100", 4000) scope.connect()
scope.set_acquire_mode("SAMPLE") configure_channels(scope) # standard SRAS front-end setup
waveform = scope.get_curve_binary(1) # Channel 1 samples_per_frame = configure_acquisition(scope)
``` ```
### Laser Control ### Laser control
```python ```python
from hardware.coherent_hops_laser import CoherentHOPSLaser from hardware.helios_laser import HeliosLaser
laser = CoherentHOPSLaser() laser = HeliosLaser()
laser.connect() laser.connect("/dev/ttyUSB1")
laser.set_power_level(50.0) # 50% power laser.set_current_ma(1200)
laser.enable_output(True) laser.set_laser_enable(True)
print(laser.get_diode_temp_c(), "°C")
laser.disconnect() # always explicit — no __del__
``` ```
### Camera Control ### Camera control
```python ```python
from hardware.uc480_camera import UC480Camera from hardware.uc480_camera import UC480Camera, find_camera_bus_conflicts
camera = UC480Camera(camera_id=0) find_camera_bus_conflicts() # warns about USB bus contention
camera = UC480Camera(camera_id=1)
camera.initialize() camera.initialize()
camera.start_capture() camera.start_capture()
# Camera operations
``` ```
## Configuration ## Configuration
### Stage Settings ### Persisted settings
Stage configuration is stored in `~/.nuescan/stage_settings.json`: `aui_defaults.json` holds the ports, scope IP, and save directory the main
- Velocity and acceleration profiles app last used. It is read and written through `core.config.ScanDefaults`,
- Trigger configuration which always writes every field — see KNOWN_ISSUES.md history for why
- Axis limits and safety parameters partial writes were a problem.
### Serial Port Configuration ### Fixed acquisition settings
Hardware devices are accessed via: Scan velocity, laser frequency, sample rate, and the ramp geometry are
- **BBD202/203**: USB with automatic serial number detection constants in `core/scan_engine.py` and `core/scope_sras.py`, not user
- **Helios**: RS-232 serial port (9600 baud, 8N1) settings; a `.sras` file records them so resume can refuse a mismatch.
- **HOPS Laser**: FTDI USB (I2C interface)
### Serial port configuration
- **BBD202**: USB serial, APT protocol (`/dev/ttyUSB*`)
- **T3R**: USB serial, custom binary framing (`/dev/ttyACM*`)
- **Helios**: RS-232 (9600 baud, 8N1)
- **Genesis**: USB serial, I2C-over-serial
- **Oscilloscope**: Ethernet/LXI (TCP socket on port 4000) - **Oscilloscope**: Ethernet/LXI (TCP socket on port 4000)
## Development ## Development
+25 -65
View File
@@ -91,11 +91,10 @@ lsusb | grep -i thorlabs
**First-time setup:** **First-time setup:**
```bash ```bash
# Run the stage test application # Run the stage test application
python stage_test_app.py python bbd202_test_app.py
# Enter your BBD203 serial number # Set the serial port, click Connect (it now fails loudly if no bay
# Click "Connect" to test the connection # responds), then Home to verify operation.
# Use "Home All Axes" to verify operation
``` ```
### Helios Laser System ### Helios Laser System
@@ -132,10 +131,15 @@ python -c "from pyftdi.ftdi import Ftdi; Ftdi.show_devices()"
**First-time setup:** **First-time setup:**
```bash ```bash
# Test laser connection # Test the Genesis laser connection
python -c "from hardware.coherent_hops_laser import CoherentHOPSLaser; laser = CoherentHOPSLaser(); print('Connected:', laser.connect())" python tools/genesis_laser_control.py
``` ```
> Before changing any Genesis code, read
> [docs/genesis_verification.md](docs/genesis_verification.md) — the two
> implementations in the repo disagree on ADC scaling, LDD polarity, and
> shutter behaviour, and only the bench can settle it.
### Tektronix Oscilloscope ### Tektronix Oscilloscope
**Connection:** **Connection:**
@@ -228,65 +232,21 @@ Main window settings (geometry, last used values) are stored in Qt settings:
## Project Structure ## Project Structure
``` See the tree in [README.md](README.md#project-structure). In short: `core/`
scanengine-3/ is the headless scan engine and file format (no PyQt6, no vendor SDKs),
│ `hardware/` holds the Qt-free device drivers, `gui/` the shared PyQt6
├── scanengine/ # Main application package adapters and widgets, and the root `*.py` files are the runnable apps.
│ ├── __init__.py
│ ├── app.py # Main application entry point ## Vendored camera SDK (`lib/`)
│ ├── main_launcher.ui # Main launcher UI
│ ├── new_scan_wizard.ui # Scan wizard UI `lib/` is gitignored, so a fresh clone does not have it. The IDS uEye
│ └── options.ui # Options dialog UI runtime (`libueye_api64.so.3.82`) must come from the IDS SDK installation
│ matching the camera firmware on this rig.
├── hardware/ # Hardware driver package
│ ├── __init__.py `lib/ueye_loader.{c,so}` is an `LD_PRELOAD` shim that dlopens
│ ├── bbd202.py # ThorLabs stage controller `/usr/lib/libueye_api.so` before Python starts. Nothing in the repo
│ ├── uc480_camera.py # IDS/ThorLabs camera references it and no launcher sets `LD_PRELOAD`, so whether it is still
│ ├── tektronix_base.py # Tektronix oscilloscope needed is an open question — see [KNOWN_ISSUES.md](KNOWN_ISSUES.md).
│ ├── coherent_hops_laser.py # Coherent HOPS laser
│ └── genesis_core.py # Genesis laser core logic
│
├── scanning/ # Scan planning package
│ ├── __init__.py
│ ├── sc3_scan_model.py # Scan model
│ └── stage_scan_plan_generator.py # Scan path planning
│
├── tools/ # Standalone executable tools
│ ├── genesis_laser_control.py # Standalone Genesis app
│ └── genesis_laser_gui.py # Alternative Genesis GUI
│
├── tests/ # Test files
│ ├── __init__.py
│ ├── test_camera_integration.py
│ ├── test_genesis_connection.py
│ ├── test_genesis_protocol.py
│ ├── test_rotated_aoi.py
│ └── test_temperature_scaling.py
│
├── docs/ # Documentation
│ ├── hardware/ # Hardware documentation
│ │ ├── BBD203_CONNECTION_GUIDE.md
│ │ ├── BBD203_Communications_Protocol.md
│ │ ├── BBD203_DRIVER_README.md
│ │ ├── HELIOS_DRIVER_README.md
│ │ ├── GENESIS_LASER_README.md
│ │ └── laser_control_implementation_guide.md
│ └── protocols/ # Protocol specifications
│ ├── apt_communications_protocol.pdf
│ ├── helios_comms_protocol.pdf
│ └── thorlabs_mls_protocol.pdf
│
├── lib/ # Binary libraries (not in git)
│ ├── libueye_api64.so.3.82
│ ├── ueye_loader.c
│ └── ueye_loader.so
│
├── config.json # System configuration
├── requirements.txt # Python dependencies
├── README.md # Project overview
├── SETUP.md # This file
└── LICENSE # License file
```
## Troubleshooting ## Troubleshooting
+8 -70
View File
@@ -29,13 +29,7 @@ class HeliosLaser:
""" """
def __init__(self, port: str = None, timeout: float = 1.0): def __init__(self, port: str = None, timeout: float = 1.0):
""" """Initialize Helios laser driver."""
Initialize Helios laser driver.
Args:
port: Serial port (e.g., '/dev/ttyUSB0' or 'COM5')
timeout: Serial timeout in seconds
"""
self.port = port self.port = port
self.timeout = timeout self.timeout = timeout
self.serial = None self.serial = None
@@ -48,15 +42,7 @@ class HeliosLaser:
return list_port_devices() return list_port_devices()
def connect(self, port: str = None) -> bool: def connect(self, port: str = None) -> bool:
""" """Connect to the Helios laser."""
Connect to the Helios laser.
Args:
port: Serial port (uses stored port if None)
Returns:
True if connection successful
"""
if port: if port:
self.port = port self.port = port
@@ -91,15 +77,7 @@ class HeliosLaser:
self.serial = None self.serial = None
def _send_command(self, command: str) -> bool: def _send_command(self, command: str) -> bool:
""" """Send a command to the laser."""
Send a command to the laser.
Args:
command: ASCII command string (without CR)
Returns:
True if sent successfully
"""
if not self.is_connected or not self.serial: if not self.is_connected or not self.serial:
logger.error("Not connected to laser") logger.error("Not connected to laser")
return False return False
@@ -164,15 +142,7 @@ class HeliosLaser:
return None return None
def set_frequency_hz(self, frequency: int) -> bool: def set_frequency_hz(self, frequency: int) -> bool:
""" """Set laser pulse frequency in Hz."""
Set laser pulse frequency in Hz.
Args:
frequency: Frequency in Hz (16700 - 125000)
Returns:
True if successful
"""
if not (16700 <= frequency <= 125000): if not (16700 <= frequency <= 125000):
logger.error(f"Frequency {frequency} Hz out of range (16700-125000)") logger.error(f"Frequency {frequency} Hz out of range (16700-125000)")
return False return False
@@ -189,15 +159,7 @@ class HeliosLaser:
return self._send_command(command) return self._send_command(command)
def set_current_ma(self, current: int) -> bool: def set_current_ma(self, current: int) -> bool:
""" """Set pump diode current in mA."""
Set pump diode current in mA.
Args:
current: Current in mA (0 - 2000 for this model)
Returns:
True if successful
"""
if not (0 <= current <= 2000): if not (0 <= current <= 2000):
logger.error(f"Current {current} mA out of range (0-2000)") logger.error(f"Current {current} mA out of range (0-2000)")
return False return False
@@ -206,28 +168,12 @@ class HeliosLaser:
return self._send_command(command) return self._send_command(command)
def set_pulse_mode(self, mode: PulseMode) -> bool: def set_pulse_mode(self, mode: PulseMode) -> bool:
""" """Set pulse mode."""
Set pulse mode.
Args:
mode: PulseMode enumeration value
Returns:
True if successful
"""
command = f"LDG {mode.value}" command = f"LDG {mode.value}"
return self._send_command(command) return self._send_command(command)
def set_laser_enable(self, enable: bool) -> bool: def set_laser_enable(self, enable: bool) -> bool:
""" """Enable or disable laser emission."""
Enable or disable laser emission.
Args:
enable: True to enable, False to disable
Returns:
True if successful
"""
command = f"LDO {1 if enable else 0}" command = f"LDO {1 if enable else 0}"
success = self._send_command(command) success = self._send_command(command)
@@ -329,15 +275,7 @@ class HeliosLaser:
return None return None
def set_remote_enable(self, enable: bool) -> bool: def set_remote_enable(self, enable: bool) -> bool:
""" """Set the remote enable state (LRE - utility connector pin 8)."""
Set the remote enable state (LRE - utility connector pin 8).
Args:
enable: True to activate remote enable, False to deactivate
Returns:
True if successful
"""
command = f"LRE {1 if enable else 0}" command = f"LRE {1 if enable else 0}"
return self._send_command(command) return self._send_command(command)
+1 -2
View File
@@ -280,8 +280,7 @@ class APTProtocol():
raise ValueError(f"No data fields have been defined for {msg_spec['name']}!") raise ValueError(f"No data fields have been defined for {msg_spec['name']}!")
unpacked_payload = struct.unpack(fmt_string, payload) unpacked_payload = struct.unpack(fmt_string, payload)
data = {field: value for field, value in zip(data_fields, data = dict(zip(data_fields, unpacked_payload, strict=True))
unpacked_payload)}
data['destination'] = dest data['destination'] = dest
data['source'] = src data['source'] = src
+1 -1
View File
@@ -344,7 +344,7 @@ class ThorlabsServoDriver():
power up. Default timeout is 60s, but 20-30s is fine as well if power up. Default timeout is 60s, but 20-30s is fine as well if
you're in that much of a hurry. you're in that much of a hurry.
''' '''
ch = self._channel_for(axis) self._channel_for(axis) # validate the axis address
self.send_and_wait(0x0443, timeout=timeout, retries=0, chan_ident=1, self.send_and_wait(0x0443, timeout=timeout, retries=0, chan_ident=1,
destination=axis, source=0x01) destination=axis, source=0x01)
+30 -262
View File
@@ -93,13 +93,7 @@ class TektronixOscilloscopeBase:
self._connected = False self._connected = False
def connect(self): def connect(self):
""" """Establish TCP socket connection to the oscilloscope."""
Establish TCP socket connection to the oscilloscope.
Raises:
ValueError: If resource_name is not provided
ConnectionError: If connection fails
"""
if not self.resource_name: if not self.resource_name:
raise ValueError("resource_name (IP address/hostname) must be provided") raise ValueError("resource_name (IP address/hostname) must be provided")
@@ -114,7 +108,8 @@ class TektronixOscilloscopeBase:
except socket.error as e: except socket.error as e:
self.socket = None self.socket = None
self._connected = False self._connected = False
raise ConnectionError(f"Failed to connect to {self.resource_name}:{self.port} - {e}") raise ConnectionError(
f"Failed to connect to {self.resource_name}:{self.port} - {e}") from e
def disconnect(self): def disconnect(self):
""" """
@@ -130,18 +125,7 @@ class TektronixOscilloscopeBase:
self._connected = False self._connected = False
def _normalize_channel(self, channel): def _normalize_channel(self, channel):
""" """Normalize channel input to integer (1-4)."""
Normalize channel input to integer (1-4).
Args:
channel: Channel as int (1-4) or string ('CH1'-'CH4')
Returns:
int: Channel number (1-4)
Raises:
ValueError: If channel is invalid
"""
if isinstance(channel, str): if isinstance(channel, str):
channel_upper = channel.upper() channel_upper = channel.upper()
if not (channel_upper.startswith('CH') and len(channel_upper) == 3 and channel_upper[2].isdigit()): if not (channel_upper.startswith('CH') and len(channel_upper) == 3 and channel_upper[2].isdigit()):
@@ -154,27 +138,13 @@ class TektronixOscilloscopeBase:
return channel return channel
def set_acquire_mode(self, mode): def set_acquire_mode(self, mode):
""" """Set the acquisition mode."""
Set the acquisition mode.
Args:
mode: Acquisition mode to set (case-insensitive). Valid options:
'SAMple' or 'SAM' or 'SAMPLE' - Sample mode (default)
'PEAKdetect' or 'PEAK' or 'PEAKDETECT' - Peak detect mode
'HIRes' or 'HIR' or 'HIRES' - High resolution mode
'AVErage' or 'AVE' or 'AVERAGE' - Average mode
'ENVelope' or 'ENV' or 'ENVELOPE' - Envelope mode
Raises:
ValueError: If mode is not valid
RuntimeError: If not connected
"""
# Normalize mode to uppercase for comparison # Normalize mode to uppercase for comparison
mode_upper = mode.upper() mode_upper = mode.upper()
# Check if mode is valid (any short or long form) # Check if mode is valid (any short or long form)
valid = False valid = False
for long_form, variants in self.ACQUIRE_MODES.items(): for variants in self.ACQUIRE_MODES.values():
if mode_upper in [v.upper() for v in variants]: if mode_upper in [v.upper() for v in variants]:
valid = True valid = True
break break
@@ -189,30 +159,12 @@ class TektronixOscilloscopeBase:
self.write(f"ACQuire:MODe {mode}") self.write(f"ACQuire:MODe {mode}")
def get_fastframe_state(self): def get_fastframe_state(self):
""" """Query the current FastFrame state."""
Query the current FastFrame state.
Returns:
int: 0 if FastFrame is off, 1 if active
Raises:
RuntimeError: If not connected
"""
response = self.query("HORizontal:FASTframe:STATE?") response = self.query("HORizontal:FASTframe:STATE?")
return int(response) return int(response)
def set_fastframe_state(self, state): def set_fastframe_state(self, state):
""" """Enable or disable FastFrame mode."""
Enable or disable FastFrame mode.
Args:
state: FastFrame state (0 = off, 1 = active)
Can be int (0/1) or bool (False/True)
Raises:
ValueError: If state is not valid
RuntimeError: If not connected
"""
# Convert boolean to int if needed # Convert boolean to int if needed
if isinstance(state, bool): if isinstance(state, bool):
state = 1 if state else 0 state = 1 if state else 0
@@ -224,16 +176,7 @@ class TektronixOscilloscopeBase:
self.write(f"HORizontal:FASTframe:STATE {state}") self.write(f"HORizontal:FASTframe:STATE {state}")
def set_fastframe_count(self, count): def set_fastframe_count(self, count):
""" """Set the number of FastFrame frames."""
Set the number of FastFrame frames.
Args:
count: Number of frames to capture (must be positive integer)
Raises:
ValueError: If count is not valid
RuntimeError: If not connected
"""
# Validate count # Validate count
if not isinstance(count, int) or count <= 0: if not isinstance(count, int) or count <= 0:
raise ValueError(f"Invalid frame count: {count}. Must be a positive integer") raise ValueError(f"Invalid frame count: {count}. Must be a positive integer")
@@ -241,29 +184,12 @@ class TektronixOscilloscopeBase:
self.write(f"HORizontal:FASTframe:COUNt {count}") self.write(f"HORizontal:FASTframe:COUNt {count}")
def get_record_length(self): def get_record_length(self):
""" """Query the current horizontal record length."""
Query the current horizontal record length.
Returns:
int: Current record length in samples
Raises:
RuntimeError: If not connected
"""
response = self.query("HORizontal:MODe:RECOrdlength?") response = self.query("HORizontal:MODe:RECOrdlength?")
return int(response) return int(response)
def set_sample_rate(self, rate): def set_sample_rate(self, rate):
""" """Set the horizontal sample rate."""
Set the horizontal sample rate.
Args:
rate: Sample rate in samples per second (must be positive number)
Raises:
ValueError: If rate is not valid
RuntimeError: If not connected
"""
# Validate rate # Validate rate
if not isinstance(rate, (int, float)) or rate <= 0: if not isinstance(rate, (int, float)) or rate <= 0:
raise ValueError(f"Invalid sample rate: {rate}. Must be a positive number") raise ValueError(f"Invalid sample rate: {rate}. Must be a positive number")
@@ -271,24 +197,12 @@ class TektronixOscilloscopeBase:
self.write(f"HORizontal:MODe:SAMPLERate {rate}") self.write(f"HORizontal:MODe:SAMPLERate {rate}")
def set_trigger_slope(self, slope): def set_trigger_slope(self, slope):
""" """Set the trigger slope."""
Set the trigger slope.
Args:
slope: Trigger slope (case-insensitive). Valid options:
'RISe' or 'RISE' - Rising edge
'FALL' - Falling edge
'EITher' or 'EITHER' - Either edge
Raises:
ValueError: If slope is not valid
RuntimeError: If not connected
"""
slope_upper = slope.upper() slope_upper = slope.upper()
# Check if slope is valid # Check if slope is valid
valid = False valid = False
for long_form, variants in self.TRIGGER_SLOPE.items(): for variants in self.TRIGGER_SLOPE.values():
if slope_upper in [v.upper() for v in variants]: if slope_upper in [v.upper() for v in variants]:
valid = True valid = True
break break
@@ -303,18 +217,7 @@ class TektronixOscilloscopeBase:
self.write(f"TRIGger:A:EDGE:SLOpe {slope}") self.write(f"TRIGger:A:EDGE:SLOpe {slope}")
def set_trigger_source(self, source): def set_trigger_source(self, source):
""" """Set the trigger source."""
Set the trigger source.
Args:
source: Trigger source channel. Valid options:
'CH1', 'CH2', 'CH3', 'CH4' (case-insensitive)
Can also pass as integer 1-4
Raises:
ValueError: If source is not valid
RuntimeError: If not connected
"""
# Convert integer to channel string if needed # Convert integer to channel string if needed
if isinstance(source, int): if isinstance(source, int):
if source < 1 or source > 4: if source < 1 or source > 4:
@@ -333,17 +236,7 @@ class TektronixOscilloscopeBase:
self.write(f"TRIGger:A:EDGE:SOUrce {source}") self.write(f"TRIGger:A:EDGE:SOUrce {source}")
def set_trigger_level(self, channel, level): def set_trigger_level(self, channel, level):
""" """Set the trigger level for a specific channel."""
Set the trigger level for a specific channel.
Args:
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
level: Trigger level in volts
Raises:
ValueError: If channel or level is not valid
RuntimeError: If not connected
"""
# Convert to channel number if string # Convert to channel number if string
if isinstance(channel, str): if isinstance(channel, str):
channel_upper = channel.upper() channel_upper = channel.upper()
@@ -362,23 +255,12 @@ class TektronixOscilloscopeBase:
self.write(f"TRIGger:A:LEVel:CH{channel} {level}") self.write(f"TRIGger:A:LEVel:CH{channel} {level}")
def set_trigger_mode(self, mode): def set_trigger_mode(self, mode):
""" """Set the trigger mode."""
Set the trigger mode.
Args:
mode: Trigger mode (case-insensitive). Valid options:
'AUTO' - Auto trigger mode
'NORMal' or 'NORMAL' - Normal trigger mode
Raises:
ValueError: If mode is not valid
RuntimeError: If not connected
"""
mode_upper = mode.upper() mode_upper = mode.upper()
# Check if mode is valid # Check if mode is valid
valid = False valid = False
for long_form, variants in self.TRIGGER_MODE.items(): for variants in self.TRIGGER_MODE.values():
if mode_upper in [v.upper() for v in variants]: if mode_upper in [v.upper() for v in variants]:
valid = True valid = True
break break
@@ -395,38 +277,18 @@ class TektronixOscilloscopeBase:
# ========== Channel Control Methods ========== # ========== Channel Control Methods ==========
def set_channel_bandwidth(self, channel, bandwidth): def set_channel_bandwidth(self, channel, bandwidth):
""" """Set the bandwidth for a specific channel."""
Set the bandwidth for a specific channel.
Args:
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
bandwidth: Bandwidth setting (depends on scope model)
Raises:
ValueError: If channel is not valid
RuntimeError: If not connected
"""
channel = self._normalize_channel(channel) channel = self._normalize_channel(channel)
self.write(f"CH{channel}:BANdwidth {bandwidth}") self.write(f"CH{channel}:BANdwidth {bandwidth}")
def set_channel_coupling(self, channel, coupling): def set_channel_coupling(self, channel, coupling):
""" """Set the coupling mode for a specific channel."""
Set the coupling mode for a specific channel.
Args:
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
coupling: Coupling mode (AC or DC, case-insensitive)
Raises:
ValueError: If channel or coupling is not valid
RuntimeError: If not connected
"""
channel = self._normalize_channel(channel) channel = self._normalize_channel(channel)
coupling_upper = coupling.upper() coupling_upper = coupling.upper()
# Validate coupling # Validate coupling
valid = False valid = False
for long_form, variants in self.CHANNEL_COUPLING.items(): for variants in self.CHANNEL_COUPLING.items():
if coupling_upper in [v.upper() for v in variants]: if coupling_upper in [v.upper() for v in variants]:
valid = True valid = True
break break
@@ -441,17 +303,7 @@ class TektronixOscilloscopeBase:
self.write(f"CH{channel}:COUPling {coupling}") self.write(f"CH{channel}:COUPling {coupling}")
def set_channel_label_name(self, channel, name): def set_channel_label_name(self, channel, name):
""" """Set the label name for a specific channel."""
Set the label name for a specific channel.
Args:
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
name: Label name as string
Raises:
ValueError: If channel is not valid
RuntimeError: If not connected
"""
channel = self._normalize_channel(channel) channel = self._normalize_channel(channel)
if not isinstance(name, str): if not isinstance(name, str):
@@ -461,17 +313,7 @@ class TektronixOscilloscopeBase:
self.write(f'CH{channel}:LABel:NAMe "{name}"') self.write(f'CH{channel}:LABel:NAMe "{name}"')
def set_channel_position(self, channel, position): def set_channel_position(self, channel, position):
""" """Set the vertical position for a specific channel."""
Set the vertical position for a specific channel.
Args:
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
position: Vertical position in divisions (can be negative or positive)
Raises:
ValueError: If channel or position is not valid
RuntimeError: If not connected
"""
channel = self._normalize_channel(channel) channel = self._normalize_channel(channel)
if not isinstance(position, (int, float)): if not isinstance(position, (int, float)):
@@ -480,17 +322,7 @@ class TektronixOscilloscopeBase:
self.write(f"CH{channel}:POSition {position}") self.write(f"CH{channel}:POSition {position}")
def set_channel_scale(self, channel, scale): def set_channel_scale(self, channel, scale):
""" """Set the vertical scale for a specific channel."""
Set the vertical scale for a specific channel.
Args:
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
scale: Vertical scale in volts per division (must be positive)
Raises:
ValueError: If channel or scale is not valid
RuntimeError: If not connected
"""
channel = self._normalize_channel(channel) channel = self._normalize_channel(channel)
if not isinstance(scale, (int, float)) or scale <= 0: if not isinstance(scale, (int, float)) or scale <= 0:
@@ -499,17 +331,7 @@ class TektronixOscilloscopeBase:
self.write(f"CH{channel}:SCAle {scale}") self.write(f"CH{channel}:SCAle {scale}")
def set_channel_termination(self, channel, termination): def set_channel_termination(self, channel, termination):
""" """Set the termination for a specific channel."""
Set the termination for a specific channel.
Args:
channel: Channel number (1-4) or channel string ('CH1'-'CH4')
termination: Termination in ohms (50 or 1000000)
Raises:
ValueError: If channel or termination is not valid
RuntimeError: If not connected
"""
channel = self._normalize_channel(channel) channel = self._normalize_channel(channel)
if termination not in self.CHANNEL_TERMINATION: if termination not in self.CHANNEL_TERMINATION:
@@ -521,18 +343,7 @@ class TektronixOscilloscopeBase:
# ========== Waveform Transfer Methods ========== # ========== Waveform Transfer Methods ==========
def set_data_source(self, source): def set_data_source(self, source):
""" """Set the data source for waveform transfer."""
Set the data source for waveform transfer.
Args:
source: Data source channel. Valid options:
'CH1', 'CH2', 'CH3', 'CH4' (case-insensitive)
Can also pass as integer 1-4
Raises:
ValueError: If source is not valid
RuntimeError: If not connected
"""
# Convert integer to channel string if needed # Convert integer to channel string if needed
if isinstance(source, int): if isinstance(source, int):
if source < 1 or source > 4: if source < 1 or source > 4:
@@ -551,15 +362,7 @@ class TektronixOscilloscopeBase:
self.write(f"DATa:SOUrce {source}") self.write(f"DATa:SOUrce {source}")
def query_wfmoutpre(self): def query_wfmoutpre(self):
""" """Query all waveform output preamble parameters."""
Query all waveform output preamble parameters.
Returns:
str: Complete waveform preamble string with all parameters
Raises:
RuntimeError: If not connected
"""
return self.query("WFMOutpre?") return self.query("WFMOutpre?")
def transfer_curve(self): def transfer_curve(self):
@@ -627,7 +430,7 @@ class TektronixOscilloscopeBase:
# fewer frames than configured — reading the configured count would block. # fewer frames than configured — reading the configured count would block.
acquired = int(self.query("ACQuire:NUMFRAMESACQuired?")) acquired = int(self.query("ACQuire:NUMFRAMESACQuired?"))
if acquired <= 0: if acquired <= 0:
raise RuntimeError(f"Scope acquired 0 FastFrame frames — no data to read") raise RuntimeError("Scope acquired 0 FastFrame frames — no data to read")
frame_count = acquired frame_count = acquired
# Send a single CURVe? query - scope will return all frames # Send a single CURVe? query - scope will return all frames
@@ -653,21 +456,7 @@ class TektronixOscilloscopeBase:
return waveforms return waveforms
def parse_curve_data(self, curve_bytes, byte_count=1, signed=True, byte_order='MSB'): def parse_curve_data(self, curve_bytes, byte_count=1, signed=True, byte_order='MSB'):
""" """Parse raw curve data into integer array."""
Parse raw curve data into integer array.
Args:
curve_bytes: Raw binary curve data
byte_count: Number of bytes per sample (1 or 2)
signed: True for signed integer, False for unsigned
byte_order: 'MSB' or 'LSB' for byte order
Returns:
list: List of integer values from the curve data
Raises:
ValueError: If parameters are invalid
"""
if byte_count not in (1, 2): if byte_count not in (1, 2):
raise ValueError(f"Invalid byte count: {byte_count}. Must be 1 or 2") raise ValueError(f"Invalid byte count: {byte_count}. Must be 1 or 2")
@@ -709,16 +498,7 @@ class TektronixOscilloscopeBase:
return values return values
def write(self, command): def write(self, command):
""" """Send a raw SCPI command to the instrument."""
Send a raw SCPI command to the instrument.
Args:
command: SCPI command string
Raises:
RuntimeError: If not connected
socket.error: If communication fails
"""
if not self._connected or not self.socket: if not self._connected or not self.socket:
raise RuntimeError("Not connected to instrument") raise RuntimeError("Not connected to instrument")
@@ -728,19 +508,7 @@ class TektronixOscilloscopeBase:
self.socket.sendall(command.encode('ascii')) self.socket.sendall(command.encode('ascii'))
def query(self, command): def query(self, command):
""" """Send a SCPI query and return the response."""
Send a SCPI query and return the response.
Args:
command: SCPI query command string
Returns:
str: Response from the instrument (stripped of trailing newline)
Raises:
RuntimeError: If not connected
socket.error: If communication fails
"""
if not self._connected or not self.socket: if not self._connected or not self.socket:
raise RuntimeError("Not connected to instrument") raise RuntimeError("Not connected to instrument")
+15 -84
View File
@@ -91,12 +91,7 @@ class UC480Camera(QObject):
error_occurred = pyqtSignal(str) # Emitted when an error occurs error_occurred = pyqtSignal(str) # Emitted when an error occurs
def __init__(self, camera_id: int = 1): def __init__(self, camera_id: int = 1):
""" """Initialize the uC480 camera driver."""
Initialize the uC480 camera driver.
Args:
camera_id: Camera ID (1-based; use is_GetCameraList to find IDs)
"""
super().__init__() super().__init__()
self.camera_id = camera_id self.camera_id = camera_id
@@ -131,12 +126,7 @@ class UC480Camera(QObject):
self._settings_lock = threading.Lock() self._settings_lock = threading.Lock()
def initialize(self) -> bool: def initialize(self) -> bool:
""" """Initialize the camera and allocate memory."""
Initialize the camera and allocate memory.
Returns:
True if successful, False otherwise
"""
try: try:
# Initialize camera. After is_ExitCamera the UI124x series # Initialize camera. After is_ExitCamera the UI124x series
# resets and re-enumerates on USB (firmware reload), so retry # resets and re-enumerates on USB (firmware reload), so retry
@@ -264,12 +254,7 @@ class UC480Camera(QObject):
logger.error(f"is_ExitCamera failed: {ret} — camera handle may still be held by daemon") logger.error(f"is_ExitCamera failed: {ret} — camera handle may still be held by daemon")
def start_capture(self) -> bool: def start_capture(self) -> bool:
""" """Start continuous video capture."""
Start continuous video capture.
Returns:
True if successful, False otherwise
"""
if not self.is_initialized: if not self.is_initialized:
logger.error("Camera not initialized") logger.error("Camera not initialized")
return False return False
@@ -311,12 +296,7 @@ class UC480Camera(QObject):
return ret == ueye.IS_SUCCESS return ret == ueye.IS_SUCCESS
def stop_capture(self) -> bool: def stop_capture(self) -> bool:
""" """Stop continuous video capture."""
Stop continuous video capture.
Returns:
True if successful, False otherwise
"""
if not self.is_capturing: if not self.is_capturing:
return True return True
@@ -331,12 +311,7 @@ class UC480Camera(QObject):
return True return True
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.
Returns:
QImage if successful, None otherwise
"""
if not self.is_initialized: if not self.is_initialized:
logger.error("Camera not initialized") logger.error("Camera not initialized")
return None return None
@@ -377,15 +352,7 @@ class UC480Camera(QObject):
return None return None
def set_exposure(self, exposure_ms: float) -> bool: def set_exposure(self, exposure_ms: float) -> bool:
""" """Set camera exposure time."""
Set camera exposure time.
Args:
exposure_ms: Exposure time in milliseconds
Returns:
True if successful, False otherwise
"""
if not self.is_initialized: if not self.is_initialized:
return False return False
@@ -405,12 +372,7 @@ class UC480Camera(QObject):
return False return False
def get_exposure(self) -> Optional[float]: def get_exposure(self) -> Optional[float]:
""" """Get current exposure time."""
Get current exposure time.
Returns:
Exposure time in milliseconds, or None if failed
"""
if not self.is_initialized: if not self.is_initialized:
return None return None
@@ -428,12 +390,7 @@ class UC480Camera(QObject):
return None return None
def get_pixel_clock_range(self) -> Optional[Tuple[int, int, int]]: def get_pixel_clock_range(self) -> Optional[Tuple[int, int, int]]:
""" """Query the sensor's supported pixel clock range."""
Query the sensor's supported pixel clock range.
Returns:
(min_mhz, max_mhz, increment_mhz), or None if the query failed
"""
if not self.is_initialized: if not self.is_initialized:
return None return None
@@ -452,15 +409,7 @@ class UC480Camera(QObject):
return None return None
def set_pixel_clock(self, pixel_clock_mhz: int) -> bool: def set_pixel_clock(self, pixel_clock_mhz: int) -> bool:
""" """Set camera pixel clock."""
Set camera pixel clock.
Args:
pixel_clock_mhz: Pixel clock in MHz
Returns:
True if successful, False otherwise
"""
if not self.is_initialized: if not self.is_initialized:
return False return False
@@ -512,15 +461,7 @@ class UC480Camera(QObject):
return False return False
def set_gain(self, master_gain: int) -> bool: def set_gain(self, master_gain: int) -> bool:
""" """Set camera master gain."""
Set camera master gain.
Args:
master_gain: Gain value (0-100)
Returns:
True if successful, False otherwise
"""
if not self.is_initialized: if not self.is_initialized:
return False return False
@@ -541,9 +482,9 @@ class UC480Camera(QObject):
return True return True
elif ret == ueye.IS_CANT_COMMUNICATE_WITH_DRIVER: elif ret == ueye.IS_CANT_COMMUNICATE_WITH_DRIVER:
logger.error( logger.error(
f"Hardware gain not supported by this camera model " "Hardware gain not supported by this camera model "
f"(IS_CANT_COMMUNICATE_WITH_DRIVER). " "(IS_CANT_COMMUNICATE_WITH_DRIVER). "
f"Consider using gain boost instead." "Consider using gain boost instead."
) )
return False return False
else: else:
@@ -551,12 +492,7 @@ class UC480Camera(QObject):
return False return False
def get_sensor_info(self) -> dict: def get_sensor_info(self) -> dict:
""" """Get camera sensor information."""
Get camera sensor information.
Returns:
Dictionary with sensor information
"""
if not self.is_initialized: if not self.is_initialized:
return {} return {}
@@ -582,12 +518,7 @@ class CameraStreamThread(QThread):
error_occurred = pyqtSignal(str) error_occurred = pyqtSignal(str)
def __init__(self, camera: UC480Camera): def __init__(self, camera: UC480Camera):
""" """Initialize the camera stream thread."""
Initialize the camera stream thread.
Args:
camera: UC480Camera instance
"""
super().__init__() super().__init__()
self.camera = camera self.camera = camera
self.running = False self.running = False
+5 -5
View File
@@ -9,11 +9,11 @@ select = ["F", "E7", "E9", "B"]
ignore = [ ignore = [
"E731", # lambda assignment — used deliberately for short Qt slot glue "E731", # lambda assignment — used deliberately for short Qt slot glue
"E741", # ambiguous single-letter names — used in math-heavy geometry code "E741", # ambiguous single-letter names — used in math-heavy geometry code
"E702", # `w = QLabel(); w.setFont(f)` on one line — the widget-layout idiom here
] ]
[lint.per-file-ignores] [lint.per-file-ignores]
# Wildcard re-exports are part of this package's (legacy) public surface # Deliberate package re-exports
# until Phase 1 empties it. "hardware/pybbd202/__init__.py" = ["F401"]
"hardware/__init__.py" = ["F401", "F403"] # Quarantined pending hardware verification (docs/genesis_verification.md)
"hardware/pybbd202/__init__.py" = ["F401", "F403"] "tools/genesis_laser_gui.py" = ["B007"]
"scanning/__init__.py" = ["F401", "F403"]
+1 -1
View File
@@ -13,7 +13,7 @@ from threading import Thread
import numpy as np import numpy as np
from PyQt6 import uic from PyQt6 import uic
from PyQt6.QtCore import QObject, QThread, QTimer, Qt, pyqtSignal from PyQt6.QtCore import QThread, QTimer, Qt, pyqtSignal
from PyQt6.QtGui import QImage, QPixmap 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,
+1 -1
View File
@@ -643,7 +643,7 @@ class T3RControlPanel(QDialog):
self.conn_lbl.setText("opening…") self.conn_lbl.setText("opening…")
self.connect_btn.setText("Disconnect") self.connect_btn.setText("Disconnect")
self.port_combo.setEnabled(False) self.port_combo.setEnabled(False)
self._log(f"Port opened, sending PING…", "evt") self._log("Port opened, sending PING…", "evt")
def _on_handshake_ok(self, proto_ver: int, fw_ver: int, num_ch: int): def _on_handshake_ok(self, proto_ver: int, fw_ver: int, num_ch: int):
self.conn_lbl.setText("connected") self.conn_lbl.setText("connected")