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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user