when'd i last commit this pos?
This commit is contained in:
@@ -1,57 +1,246 @@
|
|||||||
# scanengine-3
|
# scanengine-3
|
||||||
|
|
||||||
A unified scanning and instrumentation control platform combining multiple hardware control modules.
|
SRAS Scanning and Instrumentation Control Platform
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
scanengine-3 merges four previously separate projects into a single, cohesive platform:
|
scanengine-3 is a unified platform for scanning acoustic microscopy and precision instrumentation control. It integrates multiple hardware control modules into a single cohesive PyQt6-based application.
|
||||||
|
|
||||||
- **nuescan**: SRAS scan planning and control software with PyQt6 GUI
|
### Key Features
|
||||||
- **pymso**: Tektronix oscilloscope control and data acquisition
|
|
||||||
- **pybbd202**: Thorlabs BBD203/MLS203 motor controller driver
|
- **Stage Control**: ThorLabs BBD202/BBD203 motor controller with 3-axis positioning
|
||||||
- **pypewpewhops**: Coherent HOPS laser control via I2C
|
- **Laser Systems**: Helios and Coherent HOPS laser control
|
||||||
|
- **Data Acquisition**: Tektronix oscilloscope integration with fast-frame support
|
||||||
|
- **Scan Planning**: Automated raster scan generation and execution
|
||||||
|
- **Real-time Monitoring**: Live status updates and progress tracking
|
||||||
|
|
||||||
|
## Hardware Components
|
||||||
|
|
||||||
|
### Motion Control
|
||||||
|
- **ThorLabs BBD202/BBD203 Motor Controller**
|
||||||
|
- 3-channel APT protocol driver
|
||||||
|
- Precision positioning with encoder feedback
|
||||||
|
- Programmable velocity and acceleration
|
||||||
|
- Trigger output support for synchronized data acquisition
|
||||||
|
|
||||||
|
### Laser Systems
|
||||||
|
- **Helios Laser System**
|
||||||
|
- Frequency control (16.7-125 kHz)
|
||||||
|
- Current control (0-7000 mA)
|
||||||
|
- Multiple pulse modes
|
||||||
|
- Temperature and power monitoring
|
||||||
|
|
||||||
|
- **Coherent HOPS Laser**
|
||||||
|
- I2C/FTDI interface
|
||||||
|
- Power and modulation control
|
||||||
|
- Temperature monitoring
|
||||||
|
|
||||||
|
### Data Acquisition
|
||||||
|
- **Tektronix MSO/DPO Series Oscilloscopes**
|
||||||
|
- Direct socket communication (no VISA overhead)
|
||||||
|
- Fast-frame acquisition for high-speed scanning
|
||||||
|
- Multi-channel waveform capture
|
||||||
|
- Configurable triggering
|
||||||
|
|
||||||
|
### Microscope Systems
|
||||||
|
- **Genesis Microscope** (stub implementation)
|
||||||
|
- **T3R Timing Device** (stub implementation)
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
scanengine-3/
|
scanengine-3/
|
||||||
├── nuescan/ # Main scan control application with GUI
|
├── scanengine/ # Main application package
|
||||||
├── pymso/ # Oscilloscope control module
|
│ ├── __init__.py
|
||||||
├── pybbd202/ # Stage controller driver
|
│ ├── app.py # Main application entry point
|
||||||
├── pypewpewhops/ # Laser control module
|
│ ├── main_launcher.ui # Main launcher UI
|
||||||
├── requirements.txt # Unified dependencies
|
│ ├── new_scan_wizard.ui # Scan wizard UI
|
||||||
└── README.md # This file
|
│ └── options.ui # Options dialog UI
|
||||||
|
│
|
||||||
|
├── hardware/ # Hardware driver package
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── bbd202.py # ThorLabs stage controller
|
||||||
|
│ ├── uc480_camera.py # IDS/ThorLabs camera
|
||||||
|
│ ├── tektronix_base.py # Tektronix oscilloscope
|
||||||
|
│ ├── 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 # This file
|
||||||
|
├── SETUP.md # Setup instructions
|
||||||
|
└── LICENSE # License file
|
||||||
```
|
```
|
||||||
|
|
||||||
## Installation
|
## Quick Start
|
||||||
|
|
||||||
1. Create a virtual environment:
|
### Installation
|
||||||
```bash
|
|
||||||
python -m venv venv
|
|
||||||
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Install dependencies:
|
```bash
|
||||||
```bash
|
# Clone or navigate to project directory
|
||||||
pip install -r requirements.txt
|
cd scanengine-3
|
||||||
```
|
|
||||||
|
# Create virtual environment (recommended)
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running the Application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Main GUI application
|
||||||
|
python -m scanengine.app
|
||||||
|
|
||||||
|
# Genesis laser control tool
|
||||||
|
python tools/genesis_laser_control.py
|
||||||
|
|
||||||
|
# Alternative Genesis laser GUI
|
||||||
|
python tools/genesis_laser_gui.py
|
||||||
|
```
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
- **PyQt6**: GUI framework for nuescan
|
- **PyQt6** (>=6.4.0) - GUI framework
|
||||||
- **pyserial**: Serial communication for hardware interfaces
|
- **pyserial** (>=3.5) - Serial communication
|
||||||
- **pyvisa/pyvisa-py**: VISA instrument control for oscilloscopes
|
- **pyvisa** (>=1.13.0) - VISA instrument control
|
||||||
- **pyftdi**: FTDI device support for laser and stage controllers
|
- **pyvisa-py** (>=0.7.0) - Pure Python VISA backend
|
||||||
|
- **pyftdi** (>=0.54.0) - FTDI USB device support
|
||||||
|
|
||||||
## Usage
|
## Usage Examples
|
||||||
|
|
||||||
Refer to the README files in each subdirectory for specific usage instructions:
|
### Stage Control
|
||||||
|
|
||||||
- `nuescan/README.md` - Main application usage
|
```python
|
||||||
- `pymso/` - Oscilloscope control examples
|
from hardware.bbd202 import BBD202Controller
|
||||||
- `pybbd202/README.md` - Stage controller documentation
|
|
||||||
- `pypewpewhops/README.md` - Laser control documentation
|
# BBD202/BBD203 controller example
|
||||||
|
controller = BBD202Controller()
|
||||||
|
controller.connect("/dev/ttyUSB0") # Serial port
|
||||||
|
# Use controller for stage operations
|
||||||
|
```
|
||||||
|
|
||||||
|
### Oscilloscope Acquisition
|
||||||
|
|
||||||
|
```python
|
||||||
|
from hardware.tektronix_base import TektronixOscilloscopeBase
|
||||||
|
|
||||||
|
scope = TektronixOscilloscopeBase()
|
||||||
|
scope.connect("192.168.1.100", 4000)
|
||||||
|
scope.set_acquire_mode("SAMPLE")
|
||||||
|
waveform = scope.get_curve_binary(1) # Channel 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Laser Control
|
||||||
|
|
||||||
|
```python
|
||||||
|
from hardware.coherent_hops_laser import CoherentHOPSLaser
|
||||||
|
|
||||||
|
laser = CoherentHOPSLaser()
|
||||||
|
laser.connect()
|
||||||
|
laser.set_power_level(50.0) # 50% power
|
||||||
|
laser.enable_output(True)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Camera Control
|
||||||
|
|
||||||
|
```python
|
||||||
|
from hardware.uc480_camera import UC480Camera
|
||||||
|
|
||||||
|
camera = UC480Camera(camera_id=0)
|
||||||
|
camera.initialize()
|
||||||
|
camera.start_capture()
|
||||||
|
# Camera operations
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Stage Settings
|
||||||
|
Stage configuration is stored in `~/.nuescan/stage_settings.json`:
|
||||||
|
- Velocity and acceleration profiles
|
||||||
|
- Trigger configuration
|
||||||
|
- Axis limits and safety parameters
|
||||||
|
|
||||||
|
### Serial Port Configuration
|
||||||
|
Hardware devices are accessed via:
|
||||||
|
- **BBD202/203**: USB with automatic serial number detection
|
||||||
|
- **Helios**: RS-232 serial port (9600 baud, 8N1)
|
||||||
|
- **HOPS Laser**: FTDI USB (I2C interface)
|
||||||
|
- **Oscilloscope**: Ethernet/LXI (TCP socket on port 4000)
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Adding New Hardware
|
||||||
|
|
||||||
|
1. Create driver module in `hardware/` directory
|
||||||
|
2. Implement connection, control, and status methods
|
||||||
|
3. Add UI elements to main window or create new dialog
|
||||||
|
4. Connect signals in `main_window.py`
|
||||||
|
|
||||||
|
### Testing Without Hardware
|
||||||
|
|
||||||
|
All hardware modules include stub implementations or simulation modes. The GUI can be developed and tested without physical devices connected.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Detailed documentation available in project subdirectories:
|
||||||
|
|
||||||
|
- [BBD202/203 Driver Guide](docs/hardware/BBD203_DRIVER_README.md)
|
||||||
|
- [BBD202/203 Connection Guide](docs/hardware/BBD203_CONNECTION_GUIDE.md)
|
||||||
|
- [BBD202/203 Communications Protocol](docs/hardware/BBD203_Communications_Protocol.md)
|
||||||
|
- [Helios Laser Guide](docs/hardware/HELIOS_DRIVER_README.md)
|
||||||
|
- [Genesis Laser Guide](docs/hardware/GENESIS_LASER_README.md)
|
||||||
|
- [Laser Control Implementation Guide](docs/hardware/laser_control_implementation_guide.md)
|
||||||
|
- [Setup Instructions](SETUP.md)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Each module retains its original license. See individual subdirectories for license information.
|
Copyright (C) 2025 Thomas Ales
|
||||||
|
Licensed under GNU General Public License v2.0
|
||||||
|
|
||||||
|
See LICENSE file for full license text.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues, questions, or contributions, please refer to the project documentation or contact the development team.
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
scanengine-3 v0.1.0 - Initial unified release
|
||||||
|
|||||||
@@ -0,0 +1,428 @@
|
|||||||
|
# scanengine-3 Setup Guide
|
||||||
|
|
||||||
|
Complete installation and configuration guide for the SRAS scanning platform.
|
||||||
|
|
||||||
|
## System Requirements
|
||||||
|
|
||||||
|
### Software Requirements
|
||||||
|
- Python 3.8 or higher (3.10+ recommended)
|
||||||
|
- pip package manager
|
||||||
|
- Git (for version control)
|
||||||
|
|
||||||
|
### Operating Systems
|
||||||
|
- Linux (primary development platform)
|
||||||
|
- Windows 10/11
|
||||||
|
- macOS (limited testing)
|
||||||
|
|
||||||
|
### Hardware Requirements
|
||||||
|
Optional - application runs in simulation mode without hardware:
|
||||||
|
- USB ports for ThorLabs BBD202/203 and FTDI devices
|
||||||
|
- Serial (RS-232) port or USB-to-serial adapter for Helios laser
|
||||||
|
- Network connection for Tektronix oscilloscope (Ethernet/LXI)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### 1. Create Virtual Environment
|
||||||
|
|
||||||
|
Using a virtual environment is strongly recommended to isolate dependencies.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Navigate to project directory
|
||||||
|
cd scanengine-3
|
||||||
|
|
||||||
|
# Create virtual environment
|
||||||
|
python -m venv venv
|
||||||
|
|
||||||
|
# Activate virtual environment
|
||||||
|
# On Linux/macOS:
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# On Windows:
|
||||||
|
venv\Scripts\activate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Install Python Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install all required packages
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Or install individually:
|
||||||
|
pip install PyQt6>=6.4.0
|
||||||
|
pip install pyserial>=3.5
|
||||||
|
pip install pyvisa>=1.13.0
|
||||||
|
pip install pyvisa-py>=0.7.0
|
||||||
|
pip install pyftdi>=0.54.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Verify Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test Python imports
|
||||||
|
python -c "import PyQt6; import serial; import pyvisa; print('Dependencies OK')"
|
||||||
|
|
||||||
|
# List connected serial devices (optional)
|
||||||
|
python -c "import serial.tools.list_ports; print(list(serial.tools.list_ports.comports()))"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hardware Setup
|
||||||
|
|
||||||
|
### ThorLabs BBD202/BBD203 Motor Controller
|
||||||
|
|
||||||
|
**Connection:**
|
||||||
|
1. Connect BBD202/203 controller to PC via USB
|
||||||
|
2. Power on the controller
|
||||||
|
3. Note the serial number printed on the device (8 digits)
|
||||||
|
|
||||||
|
**Linux-specific:**
|
||||||
|
```bash
|
||||||
|
# Add user to dialout group for serial access
|
||||||
|
sudo usermod -a -G dialout $USER
|
||||||
|
# Log out and back in for changes to take effect
|
||||||
|
|
||||||
|
# Verify USB connection
|
||||||
|
lsusb | grep -i thorlabs
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows-specific:**
|
||||||
|
- Install ThorLabs APT software to get USB drivers
|
||||||
|
- Verify device appears in Device Manager under "Ports (COM & LPT)"
|
||||||
|
|
||||||
|
**First-time setup:**
|
||||||
|
```bash
|
||||||
|
# Run the stage test application
|
||||||
|
python stage_test_app.py
|
||||||
|
|
||||||
|
# Enter your BBD203 serial number
|
||||||
|
# Click "Connect" to test the connection
|
||||||
|
# Use "Home All Axes" to verify operation
|
||||||
|
```
|
||||||
|
|
||||||
|
### Helios Laser System
|
||||||
|
|
||||||
|
**Connection:**
|
||||||
|
1. Connect Helios laser to RS-232 serial port
|
||||||
|
2. Configure serial settings: 9600 baud, 8 data bits, no parity, 1 stop bit (8N1)
|
||||||
|
3. Note the COM port name (e.g., COM3 on Windows, /dev/ttyUSB0 on Linux)
|
||||||
|
|
||||||
|
**Linux-specific:**
|
||||||
|
```bash
|
||||||
|
# Identify serial port
|
||||||
|
ls -l /dev/ttyUSB* /dev/ttyS*
|
||||||
|
|
||||||
|
# Test connection (optional, if helios driver available)
|
||||||
|
# python -c "from hardware.helios_driver import HeliosDriver; d = HeliosDriver('/dev/ttyUSB0'); print('Connected:', d.connect())"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Coherent HOPS Laser
|
||||||
|
|
||||||
|
**Connection:**
|
||||||
|
1. Connect HOPS laser to PC via FTDI USB cable
|
||||||
|
2. Laser communicates over I2C protocol through FTDI interface
|
||||||
|
|
||||||
|
**Driver installation:**
|
||||||
|
```bash
|
||||||
|
# Linux: Install libftdi (if not already present)
|
||||||
|
sudo apt install libftdi1-dev # Debian/Ubuntu
|
||||||
|
sudo dnf install libftdi-devel # Fedora
|
||||||
|
|
||||||
|
# Verify FTDI device
|
||||||
|
python -c "from pyftdi.ftdi import Ftdi; Ftdi.show_devices()"
|
||||||
|
```
|
||||||
|
|
||||||
|
**First-time setup:**
|
||||||
|
```bash
|
||||||
|
# Test laser connection
|
||||||
|
python -c "from hardware.coherent_hops_laser import CoherentHOPSLaser; laser = CoherentHOPSLaser(); print('Connected:', laser.connect())"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tektronix Oscilloscope
|
||||||
|
|
||||||
|
**Connection:**
|
||||||
|
1. Connect oscilloscope to network via Ethernet
|
||||||
|
2. Configure oscilloscope IP address (static recommended)
|
||||||
|
3. Enable LXI server on oscilloscope (Utility → I/O → Network → LXI)
|
||||||
|
|
||||||
|
**Network configuration:**
|
||||||
|
```bash
|
||||||
|
# Verify connectivity
|
||||||
|
ping <oscilloscope-ip>
|
||||||
|
|
||||||
|
# Test connection
|
||||||
|
python -c "from hardware.tektronix_base import TektronixOscilloscopeBase; scope = TektronixOscilloscopeBase(); scope.connect('<oscilloscope-ip>', 4000); print('Connected')"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the Application
|
||||||
|
|
||||||
|
### Main Application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run main application
|
||||||
|
python -m scanengine.app
|
||||||
|
```
|
||||||
|
|
||||||
|
**On first launch:**
|
||||||
|
1. Main window opens with scan launcher interface
|
||||||
|
2. Configure system settings before starting scans
|
||||||
|
3. Use "Options" to configure hardware connections
|
||||||
|
|
||||||
|
### Genesis Laser Control Tools
|
||||||
|
|
||||||
|
For standalone Genesis laser control:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Full-featured Genesis laser control app
|
||||||
|
python tools/genesis_laser_control.py
|
||||||
|
|
||||||
|
# Alternative Genesis GUI
|
||||||
|
python tools/genesis_laser_gui.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Current and power control
|
||||||
|
- Shutter and keyswitch control
|
||||||
|
- Real-time monitoring
|
||||||
|
- Interlock status
|
||||||
|
- Temperature readings
|
||||||
|
- Raw I2C packet interface
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Stage Settings
|
||||||
|
|
||||||
|
Stage configuration is automatically saved to:
|
||||||
|
```
|
||||||
|
~/.nuescan/stage_settings.json (Linux/macOS)
|
||||||
|
%USERPROFILE%\.nuescan\stage_settings.json (Windows)
|
||||||
|
```
|
||||||
|
|
||||||
|
Settings include:
|
||||||
|
- Velocity profiles per axis
|
||||||
|
- Acceleration profiles
|
||||||
|
- Trigger output configuration
|
||||||
|
- Last used serial number
|
||||||
|
|
||||||
|
**Manual editing:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"x_axis": {
|
||||||
|
"velocity": 2.0,
|
||||||
|
"acceleration": 5.0,
|
||||||
|
"trigger": {
|
||||||
|
"mode": 1,
|
||||||
|
"polarity": 0,
|
||||||
|
"start_pos_fwd": 0.0,
|
||||||
|
"interval_fwd": 1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Application Settings
|
||||||
|
|
||||||
|
Main window settings (geometry, last used values) are stored in Qt settings:
|
||||||
|
```
|
||||||
|
~/.config/SRAS/nueScan.conf (Linux)
|
||||||
|
%APPDATA%\SRAS\nueScan.ini (Windows)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
scanengine-3/
|
||||||
|
│
|
||||||
|
├── scanengine/ # Main application package
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── app.py # Main application entry point
|
||||||
|
│ ├── main_launcher.ui # Main launcher UI
|
||||||
|
│ ├── new_scan_wizard.ui # Scan wizard UI
|
||||||
|
│ └── options.ui # Options dialog UI
|
||||||
|
│
|
||||||
|
├── hardware/ # Hardware driver package
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── bbd202.py # ThorLabs stage controller
|
||||||
|
│ ├── uc480_camera.py # IDS/ThorLabs camera
|
||||||
|
│ ├── tektronix_base.py # Tektronix oscilloscope
|
||||||
|
│ ├── 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
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**1. Import Errors**
|
||||||
|
```
|
||||||
|
ModuleNotFoundError: No module named 'PyQt6'
|
||||||
|
```
|
||||||
|
**Solution:** Ensure virtual environment is activated and dependencies are installed
|
||||||
|
```bash
|
||||||
|
source venv/bin/activate # or venv\Scripts\activate on Windows
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Serial Port Access Denied (Linux)**
|
||||||
|
```
|
||||||
|
PermissionError: [Errno 13] Permission denied: '/dev/ttyUSB0'
|
||||||
|
```
|
||||||
|
**Solution:** Add user to dialout group
|
||||||
|
```bash
|
||||||
|
sudo usermod -a -G dialout $USER
|
||||||
|
# Log out and back in
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. BBD202/203 Not Detected**
|
||||||
|
- Verify USB cable is connected and device is powered on
|
||||||
|
- Check serial number is correct (8 digits, case-sensitive)
|
||||||
|
- On Windows, verify ThorLabs APT drivers are installed
|
||||||
|
- Try different USB port
|
||||||
|
|
||||||
|
**4. Oscilloscope Connection Failed**
|
||||||
|
- Verify network connectivity with `ping`
|
||||||
|
- Ensure oscilloscope LXI server is enabled
|
||||||
|
- Check firewall settings (port 4000 must be open)
|
||||||
|
- Verify IP address is correct
|
||||||
|
|
||||||
|
**5. PyQt6 UI Loading Errors**
|
||||||
|
```
|
||||||
|
uic.loadUi() failed to load .ui file
|
||||||
|
```
|
||||||
|
**Solution:** Ensure .ui files are in same directory as main script, or check file paths
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
|
||||||
|
Enable verbose logging:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Add to __main__.py before creating QApplication
|
||||||
|
import logging
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
```
|
||||||
|
|
||||||
|
All hardware modules print status messages:
|
||||||
|
- `DEBUG:` - Detailed operation information
|
||||||
|
- `INFO:` - Normal operations
|
||||||
|
- `WARNING:` - Potential issues
|
||||||
|
- `ERROR:` - Operation failures
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### UI Modifications
|
||||||
|
|
||||||
|
1. Edit `.ui` files using Qt Designer:
|
||||||
|
```bash
|
||||||
|
designer nuescan_mainwindow.ui
|
||||||
|
```
|
||||||
|
|
||||||
|
2. UI files are loaded dynamically at runtime - no compilation needed
|
||||||
|
|
||||||
|
3. Access UI elements in code:
|
||||||
|
```python
|
||||||
|
self.ui.buttonName.clicked.connect(self.handler_method)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding New Hardware
|
||||||
|
|
||||||
|
1. Create driver module in `hardware/` directory
|
||||||
|
2. Implement required methods:
|
||||||
|
- `connect()` / `disconnect()`
|
||||||
|
- `is_connected()`
|
||||||
|
- `get_status()`
|
||||||
|
3. Add to main window or create dedicated dialog
|
||||||
|
4. Update UI to include new hardware section
|
||||||
|
|
||||||
|
### Testing Without Hardware
|
||||||
|
|
||||||
|
All hardware drivers support operation without physical devices:
|
||||||
|
- Stage: Simulated position and status
|
||||||
|
- Lasers: Accept commands without hardware validation
|
||||||
|
- Oscilloscope: Can be tested with scope simulator
|
||||||
|
|
||||||
|
Run application normally - missing hardware will log warnings but won't prevent startup.
|
||||||
|
|
||||||
|
## Performance Optimization
|
||||||
|
|
||||||
|
### Fast Data Acquisition
|
||||||
|
|
||||||
|
For high-speed scanning with oscilloscope:
|
||||||
|
1. Use wired Ethernet (not Wi-Fi)
|
||||||
|
2. Set oscilloscope to 1 Gb Ethernet if available
|
||||||
|
3. Enable binary data transfer format
|
||||||
|
4. Use fast-frame mode for multi-point scans
|
||||||
|
|
||||||
|
### Stage Movement Optimization
|
||||||
|
|
||||||
|
For optimal scan performance:
|
||||||
|
1. Home all axes before starting scan
|
||||||
|
2. Set appropriate velocity limits (2-5 mm/s typical)
|
||||||
|
3. Configure trigger output for synchronized acquisition
|
||||||
|
4. Use continuous motion scans when possible
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
- [ThorLabs APT Protocol Manual](docs/protocols/apt_communications_protocol.pdf)
|
||||||
|
- [Helios Communication Protocol](docs/protocols/helios_comms_protocol.pdf)
|
||||||
|
- [ThorLabs MLS Protocol](docs/protocols/thorlabs_mls_protocol.pdf)
|
||||||
|
- [BBD202/203 Driver Documentation](docs/hardware/BBD203_DRIVER_README.md)
|
||||||
|
- [BBD202/203 Connection Guide](docs/hardware/BBD203_CONNECTION_GUIDE.md)
|
||||||
|
- [Helios Driver Documentation](docs/hardware/HELIOS_DRIVER_README.md)
|
||||||
|
- [Genesis Laser Documentation](docs/hardware/GENESIS_LASER_README.md)
|
||||||
|
- [Laser Control Implementation Guide](docs/hardware/laser_control_implementation_guide.md)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Copyright (C) 2025 Thomas Ales
|
||||||
|
Licensed under GNU General Public License v2.0
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions:
|
||||||
|
1. Check troubleshooting section above
|
||||||
|
2. Review hardware-specific documentation
|
||||||
|
3. Examine console output for error messages
|
||||||
|
4. Contact development team
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
scanengine-3 v0.1.0
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"detection_laser": {
|
||||||
|
"scan_power_mw": "125"
|
||||||
|
},
|
||||||
|
"generation_laser": {
|
||||||
|
"com_port": "/dev/ttyUSB0",
|
||||||
|
"frequency_hz": "20000",
|
||||||
|
"pump_diode_current_ma": "750",
|
||||||
|
"focusing_frequency_hz": "20000",
|
||||||
|
"focusing_pump_current_ma": "300"
|
||||||
|
},
|
||||||
|
"scanning_stage": {
|
||||||
|
"scan_velocity_mm_s": "200",
|
||||||
|
"scan_acceleration_mm_s2": "500",
|
||||||
|
"x_trigger_mode": 5,
|
||||||
|
"y_trigger_mode": 0,
|
||||||
|
"optical_axis_x_mm": "55",
|
||||||
|
"optical_axis_y_mm": "37.5"
|
||||||
|
},
|
||||||
|
"t3r": {
|
||||||
|
"com_port": "/dev/ttyUSB0"
|
||||||
|
},
|
||||||
|
"oscilloscope": {
|
||||||
|
"socket_address": "192.168.0.1",
|
||||||
|
"scratch_directory": "/opt/",
|
||||||
|
"save_location": "pc"
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable → Regular
@@ -0,0 +1,161 @@
|
|||||||
|
# Genesis SLM MX 532 Laser Control Application
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This application provides comprehensive control of the Genesis SLM MX 532 laser over serial port using the NXP I2C-over-serial protocol. It features a professional PyQt6 GUI with multiple tabs for basic controls, monitoring, advanced operations, and configuration.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Tab 1: Basic Controls
|
||||||
|
- **Current Control**: Slider and numeric input for laser current (0-1023) with percentage display
|
||||||
|
- **Power Control**: Slider for power command setting
|
||||||
|
- **Digital Controls**: Toggle buttons for:
|
||||||
|
- Shutter (Open/Closed)
|
||||||
|
- Keyswitch (On/Off)
|
||||||
|
- Remote Enable (On/Off)
|
||||||
|
- Analog Input Enable (On/Off)
|
||||||
|
- Current Mode (On/Off)
|
||||||
|
- **Emergency Stop**: Red button that immediately closes shutter, sets current to 0, and disables keyswitch
|
||||||
|
|
||||||
|
### Tab 2: Monitoring
|
||||||
|
- Real-time sensor readings with auto-refresh capability (configurable interval)
|
||||||
|
- Displays:
|
||||||
|
- Actual current reading from ADC
|
||||||
|
- Interlock status (visual color-coded indicator)
|
||||||
|
- LDD (Laser Diode Driver) enable status
|
||||||
|
- Power supply glue input/output status
|
||||||
|
- Head DIO status
|
||||||
|
- Laser information display (model and wavelength)
|
||||||
|
|
||||||
|
### Tab 3: Advanced
|
||||||
|
- **Raw I2C Packet Sender**: Send custom I2C commands with hex input
|
||||||
|
- **Packet Capture Log**: Real-time log of all transmitted and received packets with timestamps
|
||||||
|
- Useful for debugging and development
|
||||||
|
|
||||||
|
### Tab 4: Configuration
|
||||||
|
- Serial port selection with auto-detection
|
||||||
|
- Baud rate configuration (default: 9600)
|
||||||
|
- Connect/Disconnect control
|
||||||
|
- DTR/RTS control line settings
|
||||||
|
- About section with protocol information
|
||||||
|
|
||||||
|
## Safety Features
|
||||||
|
|
||||||
|
1. **All controls disabled until connected** - Prevents accidental commands
|
||||||
|
2. **Shutter confirmation dialog** - Warns if opening shutter with current > 0
|
||||||
|
3. **Emergency stop** - Always enabled, immediately enters safe state
|
||||||
|
4. **Auto-safe state on disconnect** - Laser enters safe state when disconnecting
|
||||||
|
5. **Settings persistence** - Window geometry and last settings are saved
|
||||||
|
|
||||||
|
## Protocol Details
|
||||||
|
|
||||||
|
The application uses the NXP I2C-over-serial protocol with this packet format:
|
||||||
|
|
||||||
|
**Write packet:**
|
||||||
|
```
|
||||||
|
[0x53] [I2C_ADDR] [LENGTH] [COMMAND_BYTES] [DATA_BYTES] [0x50]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Read packet:**
|
||||||
|
```
|
||||||
|
[0x53] [ADDR_WRITE] [CMD_LEN] [CMD] [0x53] [ADDR_READ] [DATA_LEN] [0x50]
|
||||||
|
```
|
||||||
|
|
||||||
|
### I2C Devices
|
||||||
|
|
||||||
|
- **X9119** (0x52/0x53) - Digital potentiometer for current control
|
||||||
|
- **PCA9555** (various addresses) - I/O expanders for digital control
|
||||||
|
- **ADS7828** (0x90/0x91) - ADC for sensor readings
|
||||||
|
- **AD5254** (0x58) - Digital potentiometer for limits
|
||||||
|
- **M24C64** (0xa4/0xa5) - EEPROM for configuration storage
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Ensure dependencies are installed:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Connect the laser to your computer via serial port (typically /dev/ttyUSB0 on Linux)
|
||||||
|
|
||||||
|
3. Run the application:
|
||||||
|
```bash
|
||||||
|
python genesis_laser_control.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. **Launch the application**
|
||||||
|
```bash
|
||||||
|
python genesis_laser_control.py
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Connect to the laser:**
|
||||||
|
- Go to the "Configuration" tab
|
||||||
|
- Select the correct serial port from the dropdown
|
||||||
|
- Verify baud rate is set to 9600 (default)
|
||||||
|
- Click "Connect"
|
||||||
|
|
||||||
|
3. **Basic operation:**
|
||||||
|
- Enable remote control using the "Remote: DISABLED" button
|
||||||
|
- Enable keyswitch if required
|
||||||
|
- Adjust current using the slider
|
||||||
|
- Open shutter when ready (confirmation dialog will appear if current > 0)
|
||||||
|
|
||||||
|
4. **Monitoring:**
|
||||||
|
- Go to the "Monitoring" tab
|
||||||
|
- Enable "Auto-refresh" to continuously update readings
|
||||||
|
- Adjust refresh interval as needed (default: 500ms)
|
||||||
|
|
||||||
|
5. **Emergency stop:**
|
||||||
|
- Click the red "EMERGENCY STOP" button on any tab
|
||||||
|
- This immediately closes shutter, sets current to 0, and disables keyswitch
|
||||||
|
|
||||||
|
6. **Disconnecting:**
|
||||||
|
- Click "Disconnect" in the Configuration tab
|
||||||
|
- Laser will automatically enter safe state before disconnecting
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Connection Issues
|
||||||
|
- Verify serial port permissions: `sudo usermod -a -G dialout $USER` (logout and login)
|
||||||
|
- Check cable connection and power
|
||||||
|
- Verify correct port in Configuration tab
|
||||||
|
- Try refreshing ports list
|
||||||
|
|
||||||
|
### Communication Errors
|
||||||
|
- Check packet log in Advanced tab for detailed TX/RX
|
||||||
|
- Verify baud rate is 9600
|
||||||
|
- Ensure no other programs are using the serial port
|
||||||
|
- Try power cycling the laser
|
||||||
|
|
||||||
|
### Interlock Faults
|
||||||
|
- Check physical interlock connections
|
||||||
|
- Verify interlock status in Monitoring tab
|
||||||
|
- Ensure all safety covers are in place
|
||||||
|
|
||||||
|
## Code Structure
|
||||||
|
|
||||||
|
The application is organized into several classes:
|
||||||
|
|
||||||
|
- **SerialComm**: Low-level serial port communication
|
||||||
|
- **I2CProtocol**: NXP packet construction and parsing
|
||||||
|
- **I2CDevices**: I2C device-specific functions (PCA9555, X9119, ADS7828)
|
||||||
|
- **LaserControl**: High-level laser control operations
|
||||||
|
- **MainWindow**: PyQt6 GUI and user interaction
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
To modify the application:
|
||||||
|
|
||||||
|
1. **Adding new I2C devices**: Extend the `I2CDevices` class
|
||||||
|
2. **Adding new controls**: Add methods to `LaserControl` and corresponding UI elements
|
||||||
|
3. **Custom commands**: Use the Raw I2C Packet Sender in the Advanced tab for testing
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
See LICENSE file for details.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions, please refer to the main project documentation.
|
||||||
@@ -0,0 +1,554 @@
|
|||||||
|
# Genesis SLM MX 532 Laser Control - PyQt6 Implementation Guide
|
||||||
|
|
||||||
|
## Protocol Overview
|
||||||
|
The laser uses NXP I2C-over-serial protocol on /dev/ttyUSB0 at 9600 8N1.
|
||||||
|
|
||||||
|
### Packet Format
|
||||||
|
**Write:**
|
||||||
|
```
|
||||||
|
[0x53] [I2C_ADDR_WRITE] [LENGTH] [COMMAND_BYTES] [DATA_BYTES] [0x50]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Read:**
|
||||||
|
```
|
||||||
|
[0x53] [ADDR_WRITE] [CMD_LEN] [CMD] [0x53] [ADDR_READ] [DATA_LEN] [0x50]
|
||||||
|
```
|
||||||
|
|
||||||
|
## I2C Device Map
|
||||||
|
|
||||||
|
| Device | Address | Write | Read | Purpose |
|
||||||
|
|--------|---------|-------|------|---------|
|
||||||
|
| X9119 (current) | 0x152 | 0x52 | 0x53 | Laser current control |
|
||||||
|
| X9119 (photo) | 0x152 | 0x52 | 0x53 | Power feedback control |
|
||||||
|
| PCA9555 (PS DIO) | 0x140 | 0x40 | 0x41 | Power supply digital I/O |
|
||||||
|
| PCA9555 (Head DIO) | 0x144 | 0x44 | 0x45 | Head digital I/O |
|
||||||
|
| PCA9555 (PS Glue In) | 0x148 | 0x48 | 0x49 | Power supply glue logic input |
|
||||||
|
| PCA9555 (PS Glue Out) | 0x14a | 0x4a | 0x4b | Power supply glue logic output |
|
||||||
|
| AD5254 (current limit) | 0x158 | 0x58 | 0x59 | Current limit potentiometer |
|
||||||
|
| AD5254 (photo limit) | 0x158 | 0x58 | 0x59 | Photo limit potentiometer |
|
||||||
|
| ADS7828 (ADC) | 0x190 | 0x90 | 0x91 | Analog sensor readings |
|
||||||
|
| M24C64 (EEPROM) | 0x2a4 | 0xa4 | 0xa5 | Configuration storage |
|
||||||
|
| STM32 (MCU) | 0x2ae | 0xae | 0xaf | Microcontroller |
|
||||||
|
| DS1682 (hour meter) | 0x1d6 | 0xd6 | 0xd7 | Hour meter |
|
||||||
|
|
||||||
|
## Control Commands (from SendI2cCommand)
|
||||||
|
|
||||||
|
### SET Commands with Vtable Offsets
|
||||||
|
|
||||||
|
| Command | Offset | Device | Details |
|
||||||
|
|---------|--------|--------|---------|
|
||||||
|
| PCMD= | 0xf8 | X9119 @ 0x152 | Power command, cmd 0xa0, 0-1023 |
|
||||||
|
| PMEM= | 0x100 | X9119 @ 0x152 | Power memory (writes to NV) |
|
||||||
|
| CCMD= | 0xe8 | X9119 @ 0x152 | Current command, cmd 0xa0, 0-1023 |
|
||||||
|
| SHCMD= | 0x54 | PCA9555 @ 0x14a | Shutter, port 0, bit 0x01 |
|
||||||
|
| KSWCMD= | 0x60 | PCA9555 @ 0x14a | Keyswitch, port 0, bit 0x20 |
|
||||||
|
| CMODECMD= | 0x84 | PCA9555 @ 0x14a | Current mode, port 0, bit 0x04 |
|
||||||
|
| ANACMD= | 0x70 | PCA9555 @ 0x14a | Analog enable, port 0, bit 0x10 |
|
||||||
|
| REM= | 0x7c | PCA9555 @ 0x14a | Remote enable, port 0, bit 0x08 |
|
||||||
|
|
||||||
|
### QUERY Commands with Vtable Offsets
|
||||||
|
|
||||||
|
| Command | Offset | Returns |
|
||||||
|
|---------|--------|---------|
|
||||||
|
| ?TMAINCMD | 0xdc | Main temperature command |
|
||||||
|
| ?MAIND | 0xe0 | Main diode current |
|
||||||
|
| ?PSGLUEIN | 0x44 | PS glue input (PCA9555 @ 0x148) |
|
||||||
|
| ?PSGLUEOUT | 0x48 | PS glue output (PCA9555 @ 0x14a) |
|
||||||
|
| ?HEADDIO | 0x4c | Head DIO (PCA9555 @ 0x144) |
|
||||||
|
| ?WAVELENGTH | 0x14 | Wavelength reading |
|
||||||
|
| ?LASERMODEL | 0x10 | Model string |
|
||||||
|
| ?POWERUNITS | 0x110 | Power units string |
|
||||||
|
|
||||||
|
## Device-Specific Commands
|
||||||
|
|
||||||
|
### X9119 Digital Potentiometer
|
||||||
|
- **Write wiper:** Command 0xa0, 2 bytes (10-bit value 0-1023)
|
||||||
|
- **Valid range:** 0-1023
|
||||||
|
- **Error message:** "Valid code range is 0-1023"
|
||||||
|
|
||||||
|
### PCA9555 I/O Expander Registers
|
||||||
|
| Register | Address | Purpose |
|
||||||
|
|----------|---------|---------|
|
||||||
|
| Input Port 0 | 0x00 | Read input state |
|
||||||
|
| Input Port 1 | 0x01 | Read input state |
|
||||||
|
| Output Port 0 | 0x02 | Write output state |
|
||||||
|
| Output Port 1 | 0x03 | Write output state |
|
||||||
|
| Polarity Inv 0 | 0x04 | Invert polarity |
|
||||||
|
| Polarity Inv 1 | 0x05 | Invert polarity |
|
||||||
|
| Config Port 0 | 0x06 | Direction (0=out, 1=in) |
|
||||||
|
| Config Port 1 | 0x07 | Direction (0=out, 1=in) |
|
||||||
|
|
||||||
|
### PCA9555 @ 0x14a (Power Supply Glue Out) Bit Assignments
|
||||||
|
| Bit | Mask | Function |
|
||||||
|
|-----|------|----------|
|
||||||
|
| 0 | 0x01 | Shutter command |
|
||||||
|
| 2 | 0x04 | Current mode command |
|
||||||
|
| 3 | 0x08 | Remote enable command |
|
||||||
|
| 4 | 0x10 | Analog input enable |
|
||||||
|
| 5 | 0x20 | Keyswitch command |
|
||||||
|
|
||||||
|
### ADS7828 ADC Commands
|
||||||
|
- **Current actual:** Channel 0x84
|
||||||
|
- **Photo actual (alt):** Channel 0xe4
|
||||||
|
- **Main temp actual:** Channel 0x94
|
||||||
|
|
||||||
|
## Read Strategies (Critical!)
|
||||||
|
|
||||||
|
The DLL uses three different read strategies:
|
||||||
|
|
||||||
|
### 1. ReadOne - Single Read
|
||||||
|
```python
|
||||||
|
def i2c_read_one(addr, cmd, data_len):
|
||||||
|
"""Single read, no filtering. Use for digital I/O."""
|
||||||
|
return nxp_read(addr, cmd, 1 if cmd <= 0xFF else 2, data_len)
|
||||||
|
```
|
||||||
|
**Use for:** Digital I/O states (PCA9555 reads)
|
||||||
|
|
||||||
|
### 2. ReadMatchTwo - Match Until Consistent
|
||||||
|
```python
|
||||||
|
def i2c_read_match_two(addr, cmd, data_len, max_attempts=5):
|
||||||
|
"""Read until 2 values match (up to 5 attempts). Most reliable."""
|
||||||
|
readings = []
|
||||||
|
for attempt in range(max_attempts):
|
||||||
|
value = nxp_read(addr, cmd, 1 if cmd <= 0xFF else 2, data_len)
|
||||||
|
readings.append(value)
|
||||||
|
if readings.count(value) >= 2:
|
||||||
|
return value
|
||||||
|
time.sleep(0.01)
|
||||||
|
return readings[0]
|
||||||
|
```
|
||||||
|
**Use for:** EEPROM reads, configuration values
|
||||||
|
|
||||||
|
### 3. ReadDiscardHighLow - Median Filter
|
||||||
|
```python
|
||||||
|
def i2c_read_discard_high_low(addr, cmd, data_len):
|
||||||
|
"""Read 3 times, return median. Filters noise."""
|
||||||
|
readings = []
|
||||||
|
for _ in range(3):
|
||||||
|
value = nxp_read(addr, cmd, 1 if cmd <= 0xFF else 2, data_len)
|
||||||
|
readings.append(value)
|
||||||
|
time.sleep(0.01)
|
||||||
|
return sorted(readings)[1] # Median
|
||||||
|
```
|
||||||
|
**Use for:** ADC readings (current, temperature, voltage)
|
||||||
|
|
||||||
|
## Implementation Requirements
|
||||||
|
|
||||||
|
### Core Protocol Functions
|
||||||
|
|
||||||
|
```python
|
||||||
|
def nxp_write(i2c_addr_write, cmd, data, data_len):
|
||||||
|
"""
|
||||||
|
Build NXP I2C write packet.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
i2c_addr_write: I2C device address (write bit cleared)
|
||||||
|
cmd: Command byte(s) - int for 1 byte, int for 2 bytes
|
||||||
|
data: Data value to write
|
||||||
|
data_len: 1, 2, or 4 bytes
|
||||||
|
|
||||||
|
Packet: [0x53][addr][len][cmd...][data...][0x50]
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def nxp_read(i2c_addr_write, cmd, cmd_len, data_len):
|
||||||
|
"""
|
||||||
|
Build NXP I2C read packet.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
i2c_addr_write: I2C device address (write bit cleared)
|
||||||
|
cmd: Command byte(s)
|
||||||
|
cmd_len: 1 or 2 bytes
|
||||||
|
data_len: Number of bytes to read
|
||||||
|
|
||||||
|
Packet: [0x53][addr][cmd_len][cmd...][0x53][addr|0x01][data_len][0x50]
|
||||||
|
Returns: Bytes read from device
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def pca9555_read_port(addr, port):
|
||||||
|
"""Read PCA9555 port register (0x00-0x07)."""
|
||||||
|
return i2c_read_one(addr, port, 1)
|
||||||
|
|
||||||
|
def pca9555_write_port(addr, port, value):
|
||||||
|
"""Write PCA9555 port register."""
|
||||||
|
nxp_write(addr, port, value, 1)
|
||||||
|
|
||||||
|
def pca9555_set_bit(addr, port, bitmask, state):
|
||||||
|
"""
|
||||||
|
Set or clear specific bit on PCA9555.
|
||||||
|
Reads current value, modifies bit, writes back.
|
||||||
|
"""
|
||||||
|
current = pca9555_read_port(addr, 0x02 + port) # Output port
|
||||||
|
if state:
|
||||||
|
new_value = current | bitmask
|
||||||
|
else:
|
||||||
|
new_value = current & ~bitmask
|
||||||
|
pca9555_write_port(addr, 0x02 + port, new_value)
|
||||||
|
|
||||||
|
def x9119_write_wiper(addr, value):
|
||||||
|
"""Set X9119 wiper position (0-1023)."""
|
||||||
|
if value > 1023:
|
||||||
|
raise ValueError("X9119 value must be 0-1023")
|
||||||
|
nxp_write(addr, 0xa0, value, 2)
|
||||||
|
|
||||||
|
def ads7828_read(addr, channel):
|
||||||
|
"""Read ADS7828 ADC channel."""
|
||||||
|
return i2c_read_discard_high_low(addr, channel, 2)
|
||||||
|
```
|
||||||
|
|
||||||
|
### High-Level Control Functions
|
||||||
|
|
||||||
|
```python
|
||||||
|
def set_current(value):
|
||||||
|
"""CCMD= - Set laser current (0-1023)."""
|
||||||
|
x9119_write_wiper(0x52, value)
|
||||||
|
|
||||||
|
def set_power_cmd(value):
|
||||||
|
"""PCMD= - Set power command (0-1023)."""
|
||||||
|
x9119_write_wiper(0x52, value) # Same device, different context
|
||||||
|
|
||||||
|
def set_shutter(state):
|
||||||
|
"""SHCMD= - Control shutter (True=open, False=closed)."""
|
||||||
|
pca9555_set_bit(0x4a, 0, 0x01, state)
|
||||||
|
|
||||||
|
def set_keyswitch(state):
|
||||||
|
"""KSWCMD= - Control keyswitch (True=on, False=off)."""
|
||||||
|
pca9555_set_bit(0x4a, 0, 0x20, state)
|
||||||
|
|
||||||
|
def set_remote_enable(state):
|
||||||
|
"""REM= - Enable remote control (True=enabled)."""
|
||||||
|
pca9555_set_bit(0x4a, 0, 0x08, state)
|
||||||
|
|
||||||
|
def set_analog_enable(state):
|
||||||
|
"""ANACMD= - Enable analog input (True=enabled)."""
|
||||||
|
pca9555_set_bit(0x4a, 0, 0x10, state)
|
||||||
|
|
||||||
|
def set_current_mode(state):
|
||||||
|
"""CMODECMD= - Set current mode (True=enabled)."""
|
||||||
|
pca9555_set_bit(0x4a, 0, 0x04, state)
|
||||||
|
|
||||||
|
def get_current_actual():
|
||||||
|
"""Read actual current from ADC."""
|
||||||
|
raw = ads7828_read(0x90, 0x84)
|
||||||
|
# TODO: Scale based on AmpsFullscale
|
||||||
|
return raw * 0.000244140625 # Placeholder scaling
|
||||||
|
|
||||||
|
def get_main_temp():
|
||||||
|
"""Read main crystal temperature."""
|
||||||
|
return ads7828_read(0x90, 0x94)
|
||||||
|
|
||||||
|
def get_interlock_status():
|
||||||
|
"""Check interlock status."""
|
||||||
|
value = pca9555_read_port(0x48, 0x00) # Input port 0
|
||||||
|
return bool(value & 0x01)
|
||||||
|
|
||||||
|
def get_ldd_enable():
|
||||||
|
"""Check if laser diode driver is enabled."""
|
||||||
|
value = pca9555_read_port(0x40, 0x00)
|
||||||
|
return not bool(value & 0x01) # Inverted logic
|
||||||
|
```
|
||||||
|
|
||||||
|
## GUI Structure
|
||||||
|
|
||||||
|
### Tab 1: Basic Controls
|
||||||
|
- **Current Control**
|
||||||
|
- Slider: 0-1023
|
||||||
|
- Spinbox: Numeric input
|
||||||
|
- Label: Percentage (0-100%)
|
||||||
|
- Button: Set to 0 (quick disable)
|
||||||
|
|
||||||
|
- **Power Command**
|
||||||
|
- Slider: 0-1023
|
||||||
|
- Spinbox: Numeric input
|
||||||
|
|
||||||
|
- **Digital Controls (Toggle Buttons)**
|
||||||
|
- Shutter (Open/Closed) - Red when open
|
||||||
|
- Keyswitch (On/Off)
|
||||||
|
- Remote Enable (On/Off)
|
||||||
|
- Analog Input (On/Off)
|
||||||
|
- Current Mode (On/Off)
|
||||||
|
|
||||||
|
- **Emergency Stop**
|
||||||
|
- Large red button
|
||||||
|
- Always enabled (even when disconnected)
|
||||||
|
- Actions: Close shutter, current=0, keyswitch=off
|
||||||
|
|
||||||
|
- **Status Display**
|
||||||
|
- Actual current reading (live)
|
||||||
|
- Connection status indicator
|
||||||
|
|
||||||
|
### Tab 2: Monitoring
|
||||||
|
- **Real-Time Readings** (auto-refresh 500ms)
|
||||||
|
- Current Actual: [value] A
|
||||||
|
- Main Temperature: [value] °C
|
||||||
|
- SHG Temperature: [value] °C
|
||||||
|
- Interlock Status: [OK/FAULT] (colored indicator)
|
||||||
|
- LDD Enable: [ON/OFF]
|
||||||
|
|
||||||
|
- **System Info** (read once on connect)
|
||||||
|
- Laser Model: [from EEPROM]
|
||||||
|
- Wavelength: [value] nm
|
||||||
|
- Serial Number: [from EEPROM]
|
||||||
|
|
||||||
|
- **Controls**
|
||||||
|
- Refresh Rate slider (100ms - 2000ms)
|
||||||
|
- Enable/Disable auto-refresh checkbox
|
||||||
|
- Manual refresh button
|
||||||
|
|
||||||
|
### Tab 3: Advanced
|
||||||
|
- **Raw I2C Interface**
|
||||||
|
- Device Address (hex): [input]
|
||||||
|
- Command (hex): [input]
|
||||||
|
- Data (hex): [input]
|
||||||
|
- Data Length: [dropdown: 1/2/4 bytes]
|
||||||
|
- Buttons: [Write] [Read]
|
||||||
|
- Response display (hex)
|
||||||
|
|
||||||
|
- **Packet Monitor**
|
||||||
|
- Scrolling log of all TX/RX
|
||||||
|
- Format: `[HH:MM:SS.mmm] TX: 53 52 02 a0 01 00 50`
|
||||||
|
- Clear button
|
||||||
|
- Export to file button
|
||||||
|
|
||||||
|
- **EEPROM Tools**
|
||||||
|
- Warning label (red): "Caution: Can damage laser!"
|
||||||
|
- Read Location (hex): [input]
|
||||||
|
- Read Length: [input]
|
||||||
|
- [Read] button
|
||||||
|
- Data display
|
||||||
|
- Write protection checkbox
|
||||||
|
|
||||||
|
### Tab 4: Configuration
|
||||||
|
- **Serial Port**
|
||||||
|
- Dropdown: Auto-detect available ports
|
||||||
|
- Baud Rate: [dropdown: 9600 default]
|
||||||
|
- Data Bits: 8 (fixed)
|
||||||
|
- Parity: None (fixed)
|
||||||
|
- Stop Bits: 1 (fixed)
|
||||||
|
|
||||||
|
- **Connection**
|
||||||
|
- [Connect]/[Disconnect] button (toggle)
|
||||||
|
- DTR checkbox
|
||||||
|
- RTS checkbox
|
||||||
|
- Status: [Connected/Disconnected]
|
||||||
|
|
||||||
|
- **Debug Options**
|
||||||
|
- Show raw sensor values (before filtering) checkbox
|
||||||
|
- Packet timeout (ms): [input]
|
||||||
|
- Retry count: [input]
|
||||||
|
|
||||||
|
## Safety Features
|
||||||
|
|
||||||
|
### Pre-Flight Checks
|
||||||
|
Before opening shutter, verify:
|
||||||
|
1. Remote enable is ON
|
||||||
|
2. Keyswitch is ON
|
||||||
|
3. Interlock is OK
|
||||||
|
4. Current is set to safe value
|
||||||
|
5. Show confirmation dialog
|
||||||
|
|
||||||
|
### Automatic Safety
|
||||||
|
- All controls disabled until serial connected
|
||||||
|
- Emergency stop always enabled
|
||||||
|
- Auto-disable on serial errors
|
||||||
|
- Auto-close shutter on disconnect
|
||||||
|
- Set current to 0 on disconnect
|
||||||
|
- Watchdog timer - if no commands for 5 seconds, safe state
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
- Catch all serial exceptions
|
||||||
|
- Display errors in status bar
|
||||||
|
- Log errors with timestamps
|
||||||
|
- Retry logic for critical operations
|
||||||
|
- Timeout handling (default 1 second)
|
||||||
|
|
||||||
|
## Code Organization
|
||||||
|
|
||||||
|
### Class Structure
|
||||||
|
```
|
||||||
|
LaserControlApp (QMainWindow)
|
||||||
|
├── SerialComm (QObject)
|
||||||
|
│ ├── Methods: connect(), disconnect(), write_packet(), read_packet()
|
||||||
|
│ └── Signals: connected, disconnected, error, data_received
|
||||||
|
│
|
||||||
|
├── I2CProtocol (QObject)
|
||||||
|
│ ├── Low-level: nxp_write(), nxp_read()
|
||||||
|
│ ├── Device-specific: pca9555_*, x9119_*, ads7828_*
|
||||||
|
│ └── Read strategies: read_one(), read_match_two(), read_discard_high_low()
|
||||||
|
│
|
||||||
|
├── LaserControl (QObject)
|
||||||
|
│ ├── High-level: set_current(), set_shutter(), etc.
|
||||||
|
│ ├── Monitoring: get_current_actual(), get_temps(), etc.
|
||||||
|
│ └── Safety: emergency_stop(), safe_state(), pre_flight_check()
|
||||||
|
│
|
||||||
|
└── UI Tabs
|
||||||
|
├── BasicControlTab (QWidget)
|
||||||
|
├── MonitoringTab (QWidget)
|
||||||
|
├── AdvancedTab (QWidget)
|
||||||
|
└── ConfigTab (QWidget)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Constants (at top of file)
|
||||||
|
```python
|
||||||
|
# I2C Addresses (7-bit, shifted)
|
||||||
|
ADDR_X9119_CURRENT = 0x52
|
||||||
|
ADDR_PCA9555_PS_DIO = 0x40
|
||||||
|
ADDR_PCA9555_HEAD_DIO = 0x44
|
||||||
|
ADDR_PCA9555_PS_GLUE_IN = 0x48
|
||||||
|
ADDR_PCA9555_PS_GLUE_OUT = 0x4a
|
||||||
|
ADDR_AD5254 = 0x58
|
||||||
|
ADDR_ADS7828 = 0x90
|
||||||
|
ADDR_EEPROM = 0xa4
|
||||||
|
ADDR_STM32 = 0xae
|
||||||
|
|
||||||
|
# PCA9555 Bit Masks (0x14a)
|
||||||
|
BIT_SHUTTER = 0x01
|
||||||
|
BIT_CURRENT_MODE = 0x04
|
||||||
|
BIT_REMOTE_ENABLE = 0x08
|
||||||
|
BIT_ANALOG_ENABLE = 0x10
|
||||||
|
BIT_KEYSWITCH = 0x20
|
||||||
|
|
||||||
|
# X9119 Commands
|
||||||
|
CMD_X9119_WRITE_WIPER = 0xa0
|
||||||
|
|
||||||
|
# ADS7828 Channels
|
||||||
|
CHAN_CURRENT_ACTUAL = 0x84
|
||||||
|
CHAN_PHOTO_ACTUAL = 0xe4
|
||||||
|
CHAN_MAIN_TEMP = 0x94
|
||||||
|
|
||||||
|
# Serial Protocol
|
||||||
|
NXP_START_BYTE = 0x53
|
||||||
|
NXP_STOP_BYTE = 0x50
|
||||||
|
```
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
|
||||||
|
Use QSS for professional appearance:
|
||||||
|
|
||||||
|
```python
|
||||||
|
STYLESHEET = """
|
||||||
|
QMainWindow {
|
||||||
|
background-color: #2b2b2b;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton {
|
||||||
|
background-color: #3c3c3c;
|
||||||
|
color: white;
|
||||||
|
border: 1px solid #555;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: #4c4c4c;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton:pressed {
|
||||||
|
background-color: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton#emergency {
|
||||||
|
background-color: #cc0000;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton#emergency:hover {
|
||||||
|
background-color: #ff0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
QLabel#status_ok {
|
||||||
|
color: #00ff00;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
QLabel#status_warning {
|
||||||
|
color: #ffaa00;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
QLabel#status_error {
|
||||||
|
color: #ff0000;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSlider::groove:horizontal {
|
||||||
|
background: #3c3c3c;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSlider::handle:horizontal {
|
||||||
|
background: #5c5c5c;
|
||||||
|
width: 16px;
|
||||||
|
margin: -4px 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
Before Monday demo:
|
||||||
|
- [ ] Serial connection/disconnection works
|
||||||
|
- [ ] Current slider sets X9119 correctly
|
||||||
|
- [ ] Shutter opens/closes (verify with read-back)
|
||||||
|
- [ ] Emergency stop works from any state
|
||||||
|
- [ ] Sensor readings update (current, temp)
|
||||||
|
- [ ] Error handling doesn't crash app
|
||||||
|
- [ ] All safety interlocks functional
|
||||||
|
- [ ] Confirm dialog before shutter open
|
||||||
|
- [ ] Auto-disable on disconnect
|
||||||
|
- [ ] Packet monitor shows correct data
|
||||||
|
|
||||||
|
## README Section for Top of File
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""
|
||||||
|
Genesis SLM MX 532 Laser Control Application
|
||||||
|
=============================================
|
||||||
|
|
||||||
|
This application controls a Genesis SLM MX 532 laser via I2C-over-serial protocol.
|
||||||
|
|
||||||
|
PROTOCOL OVERVIEW:
|
||||||
|
- Serial: /dev/ttyUSB0 @ 9600 8N1
|
||||||
|
- Protocol: NXP I2C tunneled over serial
|
||||||
|
- Packet format: [0x53][addr][len][cmd][data][0x50]
|
||||||
|
|
||||||
|
SAFETY WARNINGS:
|
||||||
|
- Always close shutter before adjusting current
|
||||||
|
- Verify interlock status before opening shutter
|
||||||
|
- Use emergency stop if anything looks wrong
|
||||||
|
- Never bypass safety interlocks
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
1. Connect serial port in Configuration tab
|
||||||
|
2. Enable Remote and Keyswitch
|
||||||
|
3. Set current to desired level
|
||||||
|
4. Open shutter (with confirmation)
|
||||||
|
5. Monitor temperature and current
|
||||||
|
6. Close shutter when done
|
||||||
|
|
||||||
|
For more information, see protocol documentation.
|
||||||
|
|
||||||
|
Author: [Your Name]
|
||||||
|
Date: [Date]
|
||||||
|
License: [License]
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes for Implementation
|
||||||
|
|
||||||
|
- All I2C addresses are 7-bit and need write bit cleared (& 0xFE) for writes
|
||||||
|
- Read addresses are write address | 0x01
|
||||||
|
- Multi-byte data is big-endian
|
||||||
|
- Always validate inputs before sending to hardware
|
||||||
|
- Use type hints throughout
|
||||||
|
- Add docstrings to all functions
|
||||||
|
- Log all I2C transactions for debugging
|
||||||
|
- Include unit tests for packet construction
|
||||||
|
- Make sure emergency stop can interrupt any operation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**CRITICAL:** Test thoroughly before Monday! The laser is expensive - safety first!
|
||||||
Executable → Regular
@@ -0,0 +1,6 @@
|
|||||||
|
"""Hardware driver modules for ScanEngine-3"""
|
||||||
|
from .bbd202 import *
|
||||||
|
from .uc480_camera import *
|
||||||
|
from .tektronix_base import *
|
||||||
|
from .coherent_hops_laser import *
|
||||||
|
from .genesis_core import *
|
||||||
@@ -58,6 +58,9 @@ class MsgId(IntEnum):
|
|||||||
MOT_GET_MOVEABSPARAMS = 0x0452
|
MOT_GET_MOVEABSPARAMS = 0x0452
|
||||||
MOT_MOVE_STOP = 0x0465
|
MOT_MOVE_STOP = 0x0465
|
||||||
MOT_MOVE_STOPPED = 0x0466
|
MOT_MOVE_STOPPED = 0x0466
|
||||||
|
MOT_SET_TRIGGER = 0x0500
|
||||||
|
MOT_REQ_TRIGGER = 0x0501
|
||||||
|
MOT_GET_TRIGGER = 0x0502
|
||||||
|
|
||||||
|
|
||||||
class ChannelEnableState(IntEnum):
|
class ChannelEnableState(IntEnum):
|
||||||
@@ -78,6 +81,16 @@ class StopMode(IntEnum):
|
|||||||
CONTROLLED = 0x02
|
CONTROLLED = 0x02
|
||||||
|
|
||||||
|
|
||||||
|
class TriggerMode(IntEnum):
|
||||||
|
"""Trigger mode values."""
|
||||||
|
DISABLED = 0x00
|
||||||
|
IN_OUT_RELATIVE_MOVE = 0x01
|
||||||
|
IN_OUT_ABSOLUTE_MOVE = 0x02
|
||||||
|
IN_OUT_HOME = 0x03
|
||||||
|
IN_OUT_STOP = 0x04
|
||||||
|
OUT_ONLY = 0x10
|
||||||
|
|
||||||
|
|
||||||
class MotorStatusBits(IntFlag):
|
class MotorStatusBits(IntFlag):
|
||||||
"""Motor status bit flags."""
|
"""Motor status bit flags."""
|
||||||
CWHARDLIMIT = 0x00000001 # Clockwise hard limit triggered
|
CWHARDLIMIT = 0x00000001 # Clockwise hard limit triggered
|
||||||
@@ -183,9 +196,10 @@ class MotionController:
|
|||||||
self._status_bits = {} # dest -> int (updated on every MOVE_COMPLETED or MOVE_STOPPED)
|
self._status_bits = {} # dest -> int (updated on every MOVE_COMPLETED or MOVE_STOPPED)
|
||||||
self._status_bits_lock = threading.Lock()
|
self._status_bits_lock = threading.Lock()
|
||||||
|
|
||||||
# CRITICAL: Controller stops responding after ~50 commands without periodic ACK
|
# TX queue for serialized command sending - all outgoing data goes through this queue
|
||||||
self._ack_thread: Optional[threading.Thread] = None
|
self._tx_queue: queue.Queue = queue.Queue()
|
||||||
self._ack_running = False
|
self._tx_thread: Optional[threading.Thread] = None
|
||||||
|
self._tx_running = False
|
||||||
|
|
||||||
self._rx_thread: Optional[threading.Thread] = None
|
self._rx_thread: Optional[threading.Thread] = None
|
||||||
self._rx_running = False
|
self._rx_running = False
|
||||||
@@ -197,12 +211,32 @@ class MotionController:
|
|||||||
self._waiters: dict[int, tuple[threading.Event, list]] = {}
|
self._waiters: dict[int, tuple[threading.Event, list]] = {}
|
||||||
self._waiter_lock = threading.Lock()
|
self._waiter_lock = threading.Lock()
|
||||||
|
|
||||||
|
# Pending move tracking for high-level motion control
|
||||||
|
self._pending_moves = {} # dest -> target_position (float)
|
||||||
|
self._pending_moves_lock = threading.Lock()
|
||||||
|
|
||||||
|
# Last error tracking
|
||||||
|
self._last_error: Optional[str] = None
|
||||||
|
self._last_error_lock = threading.Lock()
|
||||||
|
|
||||||
|
# Auto-ACK control - when True, ACKs are sent automatically in RX loop
|
||||||
|
# Set to False via stop_status_ack() for manual ACK mode during scanning
|
||||||
|
self._auto_ack_enabled = True
|
||||||
|
self._status_update_count_x = 0 # Track X status updates
|
||||||
|
self._status_update_count_y = 0 # Track Y status updates
|
||||||
|
|
||||||
def connect(self, enable_updates: bool = True) -> None:
|
def connect(self, enable_updates: bool = True) -> None:
|
||||||
"""
|
"""
|
||||||
Open connection to the controller.
|
Open connection to the controller.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
enable_updates: If True, enable status update messages and start listener thread
|
enable_updates: If True, enable status update messages. ACKs are sent
|
||||||
|
reactively when status updates are received.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
When enable_updates=True, the controller will send periodic status updates.
|
||||||
|
ACKs are automatically sent in response to each status update through
|
||||||
|
the TX queue, ensuring proper serialization with other commands.
|
||||||
"""
|
"""
|
||||||
self.ftdi.open_from_url(self.url)
|
self.ftdi.open_from_url(self.url)
|
||||||
self.ftdi.set_baudrate(self.baudrate)
|
self.ftdi.set_baudrate(self.baudrate)
|
||||||
@@ -213,33 +247,47 @@ class MotionController:
|
|||||||
self._connected = True
|
self._connected = True
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# Start TX thread for serialized command sending
|
||||||
|
self._tx_running = True
|
||||||
|
self._tx_thread = threading.Thread(target=self._tx_loop, daemon=True)
|
||||||
|
self._tx_thread.start()
|
||||||
|
|
||||||
self._rx_running = True
|
self._rx_running = True
|
||||||
self._rx_thread = threading.Thread(target=self._rx_loop, daemon=True)
|
self._rx_thread = threading.Thread(target=self._rx_loop, daemon=True)
|
||||||
self._rx_thread.start()
|
self._rx_thread.start()
|
||||||
|
|
||||||
if enable_updates:
|
if enable_updates:
|
||||||
self._send_raw(self._build_short_message(MsgId.HW_START_UPDATEMSGS))
|
# Send to both axis channels to enable updates from each
|
||||||
|
self._send_raw(self._build_short_message(MsgId.HW_START_UPDATEMSGS, dest=self.DEST_X_AXIS))
|
||||||
|
self._send_raw(self._build_short_message(MsgId.HW_START_UPDATEMSGS, dest=self.DEST_Y_AXIS))
|
||||||
else:
|
else:
|
||||||
self._send_raw(self._build_short_message(MsgId.HW_STOP_UPDATEMSGS))
|
self._send_raw(self._build_short_message(MsgId.HW_STOP_UPDATEMSGS, dest=self.DEST_X_AXIS))
|
||||||
|
self._send_raw(self._build_short_message(MsgId.HW_STOP_UPDATEMSGS, dest=self.DEST_Y_AXIS))
|
||||||
|
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
||||||
def disconnect(self) -> None:
|
def disconnect(self) -> None:
|
||||||
"""Close connection to the controller."""
|
"""Close connection to the controller."""
|
||||||
if self._connected:
|
if self._connected:
|
||||||
self.stop_status_ack()
|
|
||||||
|
|
||||||
self._rx_running = False
|
self._rx_running = False
|
||||||
if self._rx_thread:
|
if self._rx_thread:
|
||||||
self._rx_thread.join(timeout=1.0)
|
self._rx_thread.join(timeout=1.0)
|
||||||
self._rx_thread = None
|
self._rx_thread = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._send_raw(self._build_short_message(MsgId.HW_DISCONNECT))
|
# Send disconnect directly to FTDI since we're shutting down TX thread
|
||||||
|
with self._rx_lock:
|
||||||
|
self.ftdi.write_data(self._build_short_message(MsgId.HW_DISCONNECT))
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Stop TX thread after sending disconnect
|
||||||
|
self._tx_running = False
|
||||||
|
if self._tx_thread:
|
||||||
|
self._tx_thread.join(timeout=1.0)
|
||||||
|
self._tx_thread = None
|
||||||
|
|
||||||
self.ftdi.close()
|
self.ftdi.close()
|
||||||
self._connected = False
|
self._connected = False
|
||||||
self._hw_info = None
|
self._hw_info = None
|
||||||
@@ -257,11 +305,25 @@ class MotionController:
|
|||||||
return header + data
|
return header + data
|
||||||
|
|
||||||
def _send_raw(self, data: bytes) -> int:
|
def _send_raw(self, data: bytes) -> int:
|
||||||
"""Send raw bytes to the controller."""
|
"""Queue raw bytes to be sent to the controller via the TX thread."""
|
||||||
if not self._connected:
|
if not self._connected:
|
||||||
raise ConnectionError("Not connected to controller")
|
raise ConnectionError("Not connected to controller")
|
||||||
with self._rx_lock:
|
self._tx_queue.put(data)
|
||||||
return self.ftdi.write_data(data)
|
return len(data)
|
||||||
|
|
||||||
|
def _tx_loop(self) -> None:
|
||||||
|
"""TX thread - processes all outgoing commands from the queue."""
|
||||||
|
while self._tx_running:
|
||||||
|
try:
|
||||||
|
data = self._tx_queue.get(timeout=0.1)
|
||||||
|
if data is not None:
|
||||||
|
with self._rx_lock:
|
||||||
|
self.ftdi.write_data(data)
|
||||||
|
except queue.Empty:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
if self._tx_running:
|
||||||
|
print(f"TX error: {e}")
|
||||||
|
|
||||||
def _rx_loop(self) -> None:
|
def _rx_loop(self) -> None:
|
||||||
"""Receiver thread loop - continuously reads and parses messages."""
|
"""Receiver thread loop - continuously reads and parses messages."""
|
||||||
@@ -359,6 +421,48 @@ class MotionController:
|
|||||||
self._encoder_counts[msg.source] = encoder_counts
|
self._encoder_counts[msg.source] = encoder_counts
|
||||||
self._stage_positions[msg.source] = position_mm
|
self._stage_positions[msg.source] = position_mm
|
||||||
|
|
||||||
|
# Process status updates - parse position and status data
|
||||||
|
if msg.msg_id == MsgId.MOT_GET_USTATUSUPDATE:
|
||||||
|
# Track per-axis counts
|
||||||
|
if msg.source == 0x21:
|
||||||
|
self._status_update_count_x += 1
|
||||||
|
count = self._status_update_count_x
|
||||||
|
elif msg.source == 0x22:
|
||||||
|
self._status_update_count_y += 1
|
||||||
|
count = self._status_update_count_y
|
||||||
|
else:
|
||||||
|
count = 0
|
||||||
|
# Debug: print every 10th status update with position
|
||||||
|
if count > 0 and count % 10 == 0:
|
||||||
|
axis = "X" if msg.source == 0x21 else "Y"
|
||||||
|
if len(msg.data) >= 14:
|
||||||
|
pos_counts = struct.unpack('<i', msg.data[2:6])[0]
|
||||||
|
status_bits = struct.unpack('<I', msg.data[10:14])[0]
|
||||||
|
pos_mm = pos_counts / self.ENCODER_COUNTS_PER_MM
|
||||||
|
moving = "MOVING" if (status_bits & 0x00000030) else "idle"
|
||||||
|
print(f" [STATUS] {axis}#{count} pos={pos_mm:.3f}mm {moving} (0x{status_bits:08X})")
|
||||||
|
# Parse status update data (14 bytes: chan_ident + position + velocity + motor_current + status_bits)
|
||||||
|
if len(msg.data) >= 14:
|
||||||
|
position_counts = struct.unpack('<i', msg.data[2:6])[0]
|
||||||
|
position_mm = position_counts / self.ENCODER_COUNTS_PER_MM
|
||||||
|
status_bits = struct.unpack('<I', msg.data[10:14])[0]
|
||||||
|
with self._position_lock:
|
||||||
|
self._encoder_counts[msg.source] = position_counts
|
||||||
|
self._stage_positions[msg.source] = position_mm
|
||||||
|
with self._status_bits_lock:
|
||||||
|
self._status_bits[msg.source] = status_bits
|
||||||
|
|
||||||
|
# Send ACK for every status update
|
||||||
|
if self._auto_ack_enabled:
|
||||||
|
ack_msg = self._build_short_message(
|
||||||
|
MsgId.MOT_ACK_USTATUSUPDATE,
|
||||||
|
param1=0x01, # chan_ident
|
||||||
|
param2=0x00,
|
||||||
|
dest=0x11, # Generic USB destination
|
||||||
|
source=0x01
|
||||||
|
)
|
||||||
|
self._tx_queue.put(ack_msg)
|
||||||
|
|
||||||
# MOT_MOVE_HOMED is a short message - positions auto-reset to 0
|
# MOT_MOVE_HOMED is a short message - positions auto-reset to 0
|
||||||
if msg.msg_id == MsgId.MOT_MOVE_HOMED:
|
if msg.msg_id == MsgId.MOT_MOVE_HOMED:
|
||||||
with self._position_lock:
|
with self._position_lock:
|
||||||
@@ -860,6 +964,58 @@ class MotionController:
|
|||||||
with self._position_lock:
|
with self._position_lock:
|
||||||
return self._stage_positions.get(self.DEST_Y_AXIS)
|
return self._stage_positions.get(self.DEST_Y_AXIS)
|
||||||
|
|
||||||
|
def poll_status(self) -> None:
|
||||||
|
"""Request status updates from all axes (non-blocking)."""
|
||||||
|
# Request status from both axes
|
||||||
|
self.send_command(MsgId.MOT_REQ_USTATUSUPDATE, param1=0x01, param2=0x00,
|
||||||
|
dest=self.DEST_X_AXIS, source=0x01)
|
||||||
|
self.send_command(MsgId.MOT_REQ_USTATUSUPDATE, param1=0x01, param2=0x00,
|
||||||
|
dest=self.DEST_Y_AXIS, source=0x01)
|
||||||
|
|
||||||
|
def request_status_update(self, dest: int) -> None:
|
||||||
|
"""Request status update from specific axis (non-blocking)."""
|
||||||
|
self.send_command(MsgId.MOT_REQ_USTATUSUPDATE, param1=0x01, param2=0x00,
|
||||||
|
dest=dest, source=0x01)
|
||||||
|
|
||||||
|
def check_for_errors(self) -> Optional[str]:
|
||||||
|
"""Check if any axis has error flags set and store as last error."""
|
||||||
|
error_msg = None
|
||||||
|
with self._status_bits_lock:
|
||||||
|
x_bits = self._status_bits.get(self.DEST_X_AXIS, 0)
|
||||||
|
y_bits = self._status_bits.get(self.DEST_Y_AXIS, 0)
|
||||||
|
|
||||||
|
if x_bits & MotorStatusBits.ERROR:
|
||||||
|
error_msg = "X-axis error detected"
|
||||||
|
elif y_bits & MotorStatusBits.ERROR:
|
||||||
|
error_msg = "Y-axis error detected"
|
||||||
|
|
||||||
|
if error_msg:
|
||||||
|
with self._last_error_lock:
|
||||||
|
self._last_error = error_msg
|
||||||
|
|
||||||
|
return error_msg
|
||||||
|
|
||||||
|
def clear_last_error(self) -> None:
|
||||||
|
"""Clear the stored last error message."""
|
||||||
|
with self._last_error_lock:
|
||||||
|
self._last_error = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_error(self) -> Optional[str]:
|
||||||
|
"""Get the last error message that was detected."""
|
||||||
|
with self._last_error_lock:
|
||||||
|
return self._last_error
|
||||||
|
|
||||||
|
def is_move_pending(self) -> bool:
|
||||||
|
"""Check if any moves are pending."""
|
||||||
|
with self._pending_moves_lock:
|
||||||
|
return len(self._pending_moves) > 0
|
||||||
|
|
||||||
|
def get_pending_targets(self) -> dict:
|
||||||
|
"""Get pending move targets as dict mapping dest -> target_position."""
|
||||||
|
with self._pending_moves_lock:
|
||||||
|
return dict(self._pending_moves)
|
||||||
|
|
||||||
# Velocity parameter properties
|
# Velocity parameter properties
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -1289,12 +1445,23 @@ class MotionController:
|
|||||||
Once started, the controller will periodically send status update messages
|
Once started, the controller will periodically send status update messages
|
||||||
containing position, velocity, and status information. These can be captured
|
containing position, velocity, and status information. These can be captured
|
||||||
by registering a callback for the update message type.
|
by registering a callback for the update message type.
|
||||||
|
|
||||||
|
Note: On BBD202/BBD203, HW_START_UPDATEMSGS must be sent to each axis
|
||||||
|
channel (0x21, 0x22) rather than the generic destination (0x11).
|
||||||
"""
|
"""
|
||||||
|
# Send to both axis channels to enable updates from each
|
||||||
self.send_command(
|
self.send_command(
|
||||||
MsgId.HW_START_UPDATEMSGS,
|
MsgId.HW_START_UPDATEMSGS,
|
||||||
param1=0x00,
|
param1=0x00,
|
||||||
param2=0x00,
|
param2=0x00,
|
||||||
dest=0x11, # Generic destination
|
dest=self.DEST_X_AXIS, # 0x21
|
||||||
|
source=0x01
|
||||||
|
)
|
||||||
|
self.send_command(
|
||||||
|
MsgId.HW_START_UPDATEMSGS,
|
||||||
|
param1=0x00,
|
||||||
|
param2=0x00,
|
||||||
|
dest=self.DEST_Y_AXIS, # 0x22
|
||||||
source=0x01
|
source=0x01
|
||||||
)
|
)
|
||||||
print("Started automatic status update messages")
|
print("Started automatic status update messages")
|
||||||
@@ -1315,83 +1482,42 @@ class MotionController:
|
|||||||
)
|
)
|
||||||
print("Stopped automatic status update messages")
|
print("Stopped automatic status update messages")
|
||||||
|
|
||||||
def _ack_loop(self) -> None:
|
|
||||||
"""
|
|
||||||
Background thread that sends periodic ACK messages.
|
|
||||||
|
|
||||||
CRITICAL: The controller will stop responding after ~50 commands if ACK
|
|
||||||
messages are not sent at least once per second.
|
|
||||||
"""
|
|
||||||
while self._ack_running:
|
|
||||||
try:
|
|
||||||
self.send_command(
|
|
||||||
MsgId.MOT_ACK_USTATUSUPDATE,
|
|
||||||
param1=0x00,
|
|
||||||
param2=0x00,
|
|
||||||
dest=0x11, # Generic destination
|
|
||||||
source=0x01
|
|
||||||
)
|
|
||||||
# Sleep for 1 second before next ACK
|
|
||||||
time.sleep(1.0)
|
|
||||||
except Exception as e:
|
|
||||||
if self._ack_running:
|
|
||||||
print(f"ACK loop error: {e}")
|
|
||||||
time.sleep(1.0)
|
|
||||||
|
|
||||||
def start_status_ack(self) -> None:
|
def start_status_ack(self) -> None:
|
||||||
"""
|
"""
|
||||||
Start the periodic status ACK thread.
|
Enable automatic ACK mode.
|
||||||
|
|
||||||
CRITICAL: This MUST be called to prevent the controller from stopping
|
When enabled, ACKs are sent automatically in the RX loop whenever
|
||||||
responses after ~50 commands. The ACK is sent every 1 second.
|
a status update message is received from the controller.
|
||||||
|
|
||||||
|
Use this after scanning operations that require manual ACK mode.
|
||||||
"""
|
"""
|
||||||
if self._ack_running:
|
self._auto_ack_enabled = True
|
||||||
return # Already running
|
|
||||||
|
|
||||||
self._ack_running = True
|
|
||||||
self._ack_thread = threading.Thread(target=self._ack_loop, daemon=True)
|
|
||||||
self._ack_thread.start()
|
|
||||||
print("Started periodic status ACK (every 1 second)")
|
|
||||||
|
|
||||||
def stop_status_ack(self) -> None:
|
def stop_status_ack(self) -> None:
|
||||||
"""Stop the periodic status ACK thread."""
|
|
||||||
if self._ack_running:
|
|
||||||
self._ack_running = False
|
|
||||||
if self._ack_thread:
|
|
||||||
self._ack_thread.join(timeout=2.0)
|
|
||||||
self._ack_thread = None
|
|
||||||
print("Stopped periodic status ACK")
|
|
||||||
|
|
||||||
def request_status_update(self, dest: int) -> None:
|
|
||||||
"""
|
"""
|
||||||
Request a status update from the specified axis.
|
Disable automatic ACK mode (switch to manual ACK mode).
|
||||||
|
|
||||||
This sends MGMSG_MOT_REQ_USTATUSUPDATE which causes the controller to
|
When disabled, ACKs are NOT sent automatically. You must call
|
||||||
respond with MGMSG_MOT_GET_USTATUSUPDATE (0x0491).
|
ack_status_update() manually after each move completes.
|
||||||
|
|
||||||
Args:
|
Use this during scanning operations for more precise control over
|
||||||
dest: Destination address (0x21 for X-axis, 0x22 for Y-axis)
|
when ACKs are sent. The snake test pattern is:
|
||||||
|
mc.stop_status_ack()
|
||||||
Raises:
|
for each move:
|
||||||
ValueError: If dest is invalid
|
mc.move_to_fast(...)
|
||||||
|
while not mc.poll_until_idle(...):
|
||||||
|
...
|
||||||
|
mc.ack_status_update() # Manual ACK after move completes
|
||||||
|
mc.start_status_ack()
|
||||||
"""
|
"""
|
||||||
if dest not in (self.DEST_X_AXIS, self.DEST_Y_AXIS):
|
self._auto_ack_enabled = False
|
||||||
raise ValueError(f"Invalid destination: 0x{dest:02X}. Must be 0x21 (X-axis) or 0x22 (Y-axis)")
|
|
||||||
|
|
||||||
self.send_command(
|
|
||||||
MsgId.MOT_REQ_USTATUSUPDATE,
|
|
||||||
param1=0x01,
|
|
||||||
param2=0x00,
|
|
||||||
dest=dest,
|
|
||||||
source=0x01
|
|
||||||
)
|
|
||||||
|
|
||||||
def ack_status_update(self) -> None:
|
def ack_status_update(self) -> None:
|
||||||
"""
|
"""
|
||||||
Send a single status update ACK.
|
Send a single status update ACK manually.
|
||||||
|
|
||||||
Normally you should use start_status_ack() to run the ACK automatically.
|
Note: ACKs are now sent automatically in response to status update messages.
|
||||||
This method is for manual ACK control if needed.
|
This method is retained for debugging or manual control if needed.
|
||||||
"""
|
"""
|
||||||
self.send_command(
|
self.send_command(
|
||||||
MsgId.MOT_ACK_USTATUSUPDATE,
|
MsgId.MOT_ACK_USTATUSUPDATE,
|
||||||
@@ -1821,6 +1947,38 @@ class MotionController:
|
|||||||
if waiter_key in self._waiters:
|
if waiter_key in self._waiters:
|
||||||
del self._waiters[waiter_key]
|
del self._waiters[waiter_key]
|
||||||
|
|
||||||
|
# Trigger configuration methods
|
||||||
|
|
||||||
|
def set_trigger(self, dest: int, trigger_mode: int, polarity: int = 0x01) -> None:
|
||||||
|
"""
|
||||||
|
Configure trigger output for an axis.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dest: Destination address (0x21 for X-axis, 0x22 for Y-axis)
|
||||||
|
trigger_mode: Trigger mode from TriggerMode enum
|
||||||
|
polarity: Trigger polarity (0x01 = active high, 0x02 = active low)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If dest is invalid
|
||||||
|
"""
|
||||||
|
if dest not in (self.DEST_X_AXIS, self.DEST_Y_AXIS):
|
||||||
|
raise ValueError(f"Invalid destination: 0x{dest:02X}. Must be 0x21 (X-axis) or 0x22 (Y-axis)")
|
||||||
|
|
||||||
|
# Build 14-byte trigger configuration packet
|
||||||
|
data = struct.pack('<HBBBBBBBBBBBB',
|
||||||
|
0x0001, # Channel ID (bytes 6-7)
|
||||||
|
trigger_mode, # Trigger mode (byte 8)
|
||||||
|
polarity, # Polarity (byte 9)
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0 # Reserved bytes 10-19
|
||||||
|
)
|
||||||
|
|
||||||
|
self.send_command(
|
||||||
|
MsgId.MOT_SET_TRIGGER,
|
||||||
|
data=data,
|
||||||
|
dest=dest,
|
||||||
|
source=0x01
|
||||||
|
)
|
||||||
|
|
||||||
# Move relative parameter methods
|
# Move relative parameter methods
|
||||||
|
|
||||||
def set_move_rel_params(self, dest: int, relative_distance: float) -> None:
|
def set_move_rel_params(self, dest: int, relative_distance: float) -> None:
|
||||||
@@ -2259,6 +2417,294 @@ class MotionController:
|
|||||||
self.stop_move(self.DEST_Y_AXIS, stop_mode, False)
|
self.stop_move(self.DEST_Y_AXIS, stop_mode, False)
|
||||||
return {'x': None, 'y': None}
|
return {'x': None, 'y': None}
|
||||||
|
|
||||||
|
# High-level motion control methods
|
||||||
|
|
||||||
|
def poll_positions(self) -> None:
|
||||||
|
"""
|
||||||
|
Request position updates for both X and Y axes.
|
||||||
|
|
||||||
|
This actively queries the controller for the current positions of both axes.
|
||||||
|
The positions are stored internally and can be accessed via position_x and position_y properties.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.get_position(self.DEST_X_AXIS, timeout=1.5)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error polling X position: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.get_position(self.DEST_Y_AXIS, timeout=1.5)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error polling Y position: {e}")
|
||||||
|
|
||||||
|
def move_to_fast(self, x: Optional[float] = None, y: Optional[float] = None) -> None:
|
||||||
|
"""
|
||||||
|
Move to absolute position(s) using fast non-blocking moves.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x: Target X position in mm (None to leave X unchanged)
|
||||||
|
y: Target Y position in mm (None to leave Y unchanged)
|
||||||
|
"""
|
||||||
|
# Track pending moves
|
||||||
|
with self._pending_moves_lock:
|
||||||
|
if x is not None:
|
||||||
|
self._pending_moves[self.DEST_X_AXIS] = x
|
||||||
|
if y is not None:
|
||||||
|
self._pending_moves[self.DEST_Y_AXIS] = y
|
||||||
|
|
||||||
|
if y is not None:
|
||||||
|
y_counts = int(y * self.ENCODER_COUNTS_PER_MM)
|
||||||
|
y_data = struct.pack('<Hi', 0x0001, y_counts)
|
||||||
|
self.send_command(
|
||||||
|
MsgId.MOT_MOVE_ABSOLUTE,
|
||||||
|
data=y_data,
|
||||||
|
dest=self.DEST_Y_AXIS,
|
||||||
|
source=0x01
|
||||||
|
)
|
||||||
|
|
||||||
|
# Delay between commands - controller may need time to process
|
||||||
|
if x is not None and y is not None:
|
||||||
|
time.sleep(0.100) # 100ms delay
|
||||||
|
|
||||||
|
if x is not None:
|
||||||
|
x_counts = int(x * self.ENCODER_COUNTS_PER_MM)
|
||||||
|
x_data = struct.pack('<Hi', 0x0001, x_counts)
|
||||||
|
self.send_command(
|
||||||
|
MsgId.MOT_MOVE_ABSOLUTE,
|
||||||
|
data=x_data,
|
||||||
|
dest=self.DEST_X_AXIS,
|
||||||
|
source=0x01
|
||||||
|
)
|
||||||
|
|
||||||
|
def move_and_wait_for_completed(self, x: float = None, y: float = None,
|
||||||
|
timeout: float = 30.0) -> bool:
|
||||||
|
"""
|
||||||
|
Move to position and wait for MOT_MOVE_COMPLETED message(s).
|
||||||
|
|
||||||
|
This method sends absolute move commands and polls for MOT_MOVE_COMPLETED
|
||||||
|
messages by checking the _move_completed dictionary which is updated by
|
||||||
|
the RX thread when completion messages arrive.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x: Target X position in mm, or None to skip X axis
|
||||||
|
y: Target Y position in mm, or None to skip Y axis
|
||||||
|
timeout: Maximum wait time in seconds (default 30s)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if all moves completed successfully, False if timeout
|
||||||
|
"""
|
||||||
|
if x is None and y is None:
|
||||||
|
return True # Nothing to do
|
||||||
|
|
||||||
|
# Determine which axes we're moving
|
||||||
|
axes_to_move = []
|
||||||
|
if x is not None:
|
||||||
|
axes_to_move.append(self.DEST_X_AXIS)
|
||||||
|
if y is not None:
|
||||||
|
axes_to_move.append(self.DEST_Y_AXIS)
|
||||||
|
|
||||||
|
# Clear any previous completion data for axes we're about to move
|
||||||
|
with self._move_completed_lock:
|
||||||
|
for dest in axes_to_move:
|
||||||
|
if dest in self._move_completed:
|
||||||
|
del self._move_completed[dest]
|
||||||
|
|
||||||
|
# Track pending moves
|
||||||
|
with self._pending_moves_lock:
|
||||||
|
if x is not None:
|
||||||
|
self._pending_moves[self.DEST_X_AXIS] = x
|
||||||
|
if y is not None:
|
||||||
|
self._pending_moves[self.DEST_Y_AXIS] = y
|
||||||
|
|
||||||
|
move_type = "X+Y" if (x is not None and y is not None) else "X-only" if x is not None else "Y-only"
|
||||||
|
print(f" [move_and_wait] {move_type}: x={x}, y={y}")
|
||||||
|
|
||||||
|
# Send Y move first if needed
|
||||||
|
if y is not None:
|
||||||
|
y_counts = int(y * self.ENCODER_COUNTS_PER_MM)
|
||||||
|
y_data = struct.pack('<Hi', 0x0001, y_counts)
|
||||||
|
self.send_command(
|
||||||
|
MsgId.MOT_MOVE_ABSOLUTE,
|
||||||
|
data=y_data,
|
||||||
|
dest=self.DEST_Y_AXIS,
|
||||||
|
source=0x01
|
||||||
|
)
|
||||||
|
|
||||||
|
# Small delay between commands if moving both axes
|
||||||
|
if x is not None and y is not None:
|
||||||
|
time.sleep(0.020)
|
||||||
|
|
||||||
|
# Send X move if needed
|
||||||
|
if x is not None:
|
||||||
|
x_counts = int(x * self.ENCODER_COUNTS_PER_MM)
|
||||||
|
x_data = struct.pack('<Hi', 0x0001, x_counts)
|
||||||
|
self.send_command(
|
||||||
|
MsgId.MOT_MOVE_ABSOLUTE,
|
||||||
|
data=x_data,
|
||||||
|
dest=self.DEST_X_AXIS,
|
||||||
|
source=0x01
|
||||||
|
)
|
||||||
|
|
||||||
|
# Poll for MOT_MOVE_COMPLETED messages
|
||||||
|
start_time = time.time()
|
||||||
|
completed_axes = set()
|
||||||
|
|
||||||
|
while len(completed_axes) < len(axes_to_move):
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
if elapsed > timeout:
|
||||||
|
missing = [("X" if d == self.DEST_X_AXIS else "Y") for d in axes_to_move if d not in completed_axes]
|
||||||
|
print(f" [move_and_wait] Timeout waiting for {', '.join(missing)} MOT_MOVE_COMPLETED")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check for completions
|
||||||
|
with self._move_completed_lock:
|
||||||
|
for dest in axes_to_move:
|
||||||
|
if dest not in completed_axes and dest in self._move_completed:
|
||||||
|
axis_name = "X" if dest == self.DEST_X_AXIS else "Y"
|
||||||
|
print(f" [move_and_wait] {axis_name}-axis move completed")
|
||||||
|
completed_axes.add(dest)
|
||||||
|
# Clear pending move
|
||||||
|
with self._pending_moves_lock:
|
||||||
|
if dest in self._pending_moves:
|
||||||
|
del self._pending_moves[dest]
|
||||||
|
|
||||||
|
if len(completed_axes) < len(axes_to_move):
|
||||||
|
time.sleep(0.010) # 10ms poll interval
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def poll_until_idle(self, tolerance: float = 0.005, timeout: float = 0.1) -> bool:
|
||||||
|
"""
|
||||||
|
Poll positions and check if all pending moves are complete within tolerance.
|
||||||
|
|
||||||
|
This method checks if the stage has settled at the target positions for all pending moves.
|
||||||
|
A move is considered complete when the current position is within 'tolerance' of the target.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tolerance: Position tolerance in mm (default 0.005mm = 5 microns)
|
||||||
|
timeout: Time to spend polling in this call (default 0.1s)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if all pending moves are complete (within tolerance), False otherwise
|
||||||
|
"""
|
||||||
|
# Get current pending targets (snapshot)
|
||||||
|
with self._pending_moves_lock:
|
||||||
|
pending = dict(self._pending_moves)
|
||||||
|
|
||||||
|
if not pending:
|
||||||
|
# No pending moves
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check if all axes are within tolerance
|
||||||
|
all_settled = True
|
||||||
|
completed_axes = []
|
||||||
|
|
||||||
|
# Query positions using get_position which properly waits for responses
|
||||||
|
# Use short timeout to avoid blocking too long
|
||||||
|
query_timeout = min(timeout, 0.5)
|
||||||
|
|
||||||
|
for dest, target in pending.items():
|
||||||
|
try:
|
||||||
|
# Get current position with timeout
|
||||||
|
current = self.get_position(dest, timeout=query_timeout)
|
||||||
|
|
||||||
|
if current is None:
|
||||||
|
# Position query failed or timed out - not settled
|
||||||
|
all_settled = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
error = abs(current - target)
|
||||||
|
|
||||||
|
if error <= tolerance:
|
||||||
|
# This axis is settled
|
||||||
|
completed_axes.append(dest)
|
||||||
|
else:
|
||||||
|
# Still moving
|
||||||
|
all_settled = False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Position query failed - assume not settled
|
||||||
|
print(f"Error querying position for axis 0x{dest:02X}: {e}")
|
||||||
|
all_settled = False
|
||||||
|
|
||||||
|
# Remove completed axes from pending list
|
||||||
|
if completed_axes:
|
||||||
|
with self._pending_moves_lock:
|
||||||
|
for dest in completed_axes:
|
||||||
|
if dest in self._pending_moves:
|
||||||
|
del self._pending_moves[dest]
|
||||||
|
|
||||||
|
return all_settled
|
||||||
|
|
||||||
|
def poll_until_idle_passive(self, tolerance: float = 0.005, debug: bool = False) -> bool:
|
||||||
|
"""
|
||||||
|
Check if all pending moves are complete using cached positions from automatic updates.
|
||||||
|
|
||||||
|
Unlike poll_until_idle(), this does NOT send position request commands. It only
|
||||||
|
reads the cached positions that are updated from automatic status updates
|
||||||
|
(MOT_GET_USTATUSUPDATE). This avoids command traffic that might interfere with
|
||||||
|
move execution.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tolerance: Position tolerance in mm (default 0.005mm = 5 microns)
|
||||||
|
debug: If True, print debug info about positions
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if all pending moves are complete (within tolerance), False otherwise
|
||||||
|
"""
|
||||||
|
# Get current pending targets (snapshot)
|
||||||
|
with self._pending_moves_lock:
|
||||||
|
pending = dict(self._pending_moves)
|
||||||
|
|
||||||
|
if not pending:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check if all axes are within tolerance using cached positions
|
||||||
|
all_settled = True
|
||||||
|
completed_axes = []
|
||||||
|
|
||||||
|
for dest, target in pending.items():
|
||||||
|
# Get cached position (updated by automatic status updates)
|
||||||
|
with self._position_lock:
|
||||||
|
current = self._stage_positions.get(dest)
|
||||||
|
|
||||||
|
axis_name = "X" if dest == self.DEST_X_AXIS else "Y"
|
||||||
|
|
||||||
|
if current is None:
|
||||||
|
# No cached position yet - not settled
|
||||||
|
if debug:
|
||||||
|
print(f" [poll] {axis_name}: no cached position")
|
||||||
|
all_settled = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
error = abs(current - target)
|
||||||
|
|
||||||
|
if debug:
|
||||||
|
print(f" [poll] {axis_name}: current={current:.3f}, target={target:.3f}, error={error*1000:.1f}um")
|
||||||
|
|
||||||
|
if error <= tolerance:
|
||||||
|
completed_axes.append(dest)
|
||||||
|
else:
|
||||||
|
all_settled = False
|
||||||
|
|
||||||
|
# Remove completed axes from pending list
|
||||||
|
if completed_axes:
|
||||||
|
with self._pending_moves_lock:
|
||||||
|
for dest in completed_axes:
|
||||||
|
if dest in self._pending_moves:
|
||||||
|
del self._pending_moves[dest]
|
||||||
|
|
||||||
|
return all_settled
|
||||||
|
|
||||||
|
def clear_pending_moves(self) -> None:
|
||||||
|
"""
|
||||||
|
Clear all pending move targets.
|
||||||
|
|
||||||
|
This clears the internal tracking of pending moves. It does NOT stop the motors -
|
||||||
|
use stop_all_axes() if you want to halt motion.
|
||||||
|
"""
|
||||||
|
with self._pending_moves_lock:
|
||||||
|
self._pending_moves.clear()
|
||||||
|
|
||||||
# Status bit decoding methods
|
# Status bit decoding methods
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"""
|
||||||
|
Coherent HOPS Laser Driver - Stub Module
|
||||||
|
This is a temporary stub to allow testing camera integration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class CoherentHOPSLaser:
|
||||||
|
"""Stub class for Coherent HOPS Laser"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DummyLaser:
|
||||||
|
"""Dummy laser for testing without hardware"""
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
"""Simulate connection"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
"""Simulate disconnection"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_hardware_id(self):
|
||||||
|
"""Return simulated hardware ID"""
|
||||||
|
return "SIM-12345"
|
||||||
|
|
||||||
|
def get_laser_model(self):
|
||||||
|
"""Return simulated model"""
|
||||||
|
return "Genesis Simulator"
|
||||||
|
|
||||||
|
def get_interlock_status(self):
|
||||||
|
"""Return simulated interlock status"""
|
||||||
|
return "OK"
|
||||||
|
|
||||||
|
def get_key_switch_status(self):
|
||||||
|
"""Return simulated key switch status"""
|
||||||
|
return "ON"
|
||||||
|
|
||||||
|
def get_temperature_main(self):
|
||||||
|
"""Return simulated main temperature"""
|
||||||
|
return 25.5
|
||||||
|
|
||||||
|
def get_temperature_eta(self):
|
||||||
|
"""Return simulated ETA temperature"""
|
||||||
|
return 26.3
|
||||||
@@ -0,0 +1,669 @@
|
|||||||
|
"""
|
||||||
|
Genesis SLM MX 532 Laser Core Hardware Control Module
|
||||||
|
======================================================
|
||||||
|
|
||||||
|
This module provides low-level hardware control for the Genesis SLM MX 532 laser
|
||||||
|
using NXP I2C-over-serial protocol. It contains reusable classes for serial
|
||||||
|
communication, I2C protocol handling, device control, and laser operations.
|
||||||
|
|
||||||
|
This module is GUI-independent and can be used by both command-line and GUI applications.
|
||||||
|
|
||||||
|
Protocol Overview:
|
||||||
|
-----------------
|
||||||
|
The laser uses NXP I2C-over-serial protocol with packet format:
|
||||||
|
[0x53] [I2C_ADDR] [LENGTH] [COMMAND_BYTES] [DATA_BYTES] [0x50]
|
||||||
|
|
||||||
|
For reads:
|
||||||
|
[0x53] [ADDR_WRITE] [CMD_LEN] [CMD] [0x53] [ADDR_READ] [DATA_LEN] [0x50]
|
||||||
|
|
||||||
|
I2C Devices:
|
||||||
|
-----------
|
||||||
|
- X9119 digital potentiometer at 0x52 - laser current control
|
||||||
|
- PCA9555 I/O expander at 0x4a - digital I/O
|
||||||
|
- AD5254 digital potentiometer at 0x58 - limits control
|
||||||
|
- ADS7828 ADC at 0x90 - sensor readings
|
||||||
|
- M24C64 EEPROM at 0xa4 - configuration storage
|
||||||
|
|
||||||
|
Author: Claude
|
||||||
|
Date: 2026-01-24
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional, List
|
||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
|
import serial
|
||||||
|
from serial.tools import list_ports
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Constants - I2C Addresses
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
class I2CAddress(IntEnum):
|
||||||
|
"""I2C device addresses (7-bit addresses shifted left by 1)"""
|
||||||
|
# X9119 Digital Potentiometer (current control)
|
||||||
|
X9119_CURRENT_WRITE = 0x52
|
||||||
|
X9119_CURRENT_READ = 0x53
|
||||||
|
|
||||||
|
# PCA9555 I/O Expanders
|
||||||
|
PCA9555_DIO_WRITE = 0x4a # Main control I/O at 0x14a
|
||||||
|
PCA9555_DIO_READ = 0x4b
|
||||||
|
PCA9555_PSGLUE_WRITE = 0x48 # Power supply glue at 0x148
|
||||||
|
PCA9555_PSGLUE_READ = 0x49
|
||||||
|
PCA9555_HEAD_WRITE = 0x44 # Head DIO at 0x144
|
||||||
|
PCA9555_HEAD_READ = 0x45
|
||||||
|
PCA9555_LDD_WRITE = 0x40 # LDD control at 0x140
|
||||||
|
PCA9555_LDD_READ = 0x41
|
||||||
|
|
||||||
|
# AD5254 Digital Potentiometer (limits)
|
||||||
|
AD5254_WRITE = 0x58
|
||||||
|
|
||||||
|
# ADS7828 ADC (sensor readings)
|
||||||
|
ADS7828_WRITE = 0x90
|
||||||
|
ADS7828_READ = 0x91
|
||||||
|
|
||||||
|
# M24C64 EEPROM (configuration)
|
||||||
|
EEPROM_WRITE = 0xa4
|
||||||
|
EEPROM_READ = 0xa5
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Constants - PCA9555 Register Addresses
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
class PCA9555Register(IntEnum):
|
||||||
|
"""PCA9555 I/O expander register addresses"""
|
||||||
|
INPUT_PORT_0 = 0x00
|
||||||
|
INPUT_PORT_1 = 0x01
|
||||||
|
OUTPUT_PORT_0 = 0x02
|
||||||
|
OUTPUT_PORT_1 = 0x03
|
||||||
|
POLARITY_INV_0 = 0x04
|
||||||
|
POLARITY_INV_1 = 0x05
|
||||||
|
CONFIG_PORT_0 = 0x06
|
||||||
|
CONFIG_PORT_1 = 0x07
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Constants - Control Bitmasks
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
class ControlBitmask(IntEnum):
|
||||||
|
"""Bitmasks for PCA9555 port 0 control bits"""
|
||||||
|
SHUTTER = 0x01 # Bit 0
|
||||||
|
CURRENT_MODE = 0x04 # Bit 2
|
||||||
|
REMOTE_ENABLE = 0x08 # Bit 3
|
||||||
|
ANALOG_ENABLE = 0x10 # Bit 4
|
||||||
|
KEYSWITCH = 0x20 # Bit 5
|
||||||
|
INTERLOCK = 0x01 # Bit 0 (on different PCA9555)
|
||||||
|
LDD_ENABLE = 0x01 # Bit 0 (on LDD PCA9555)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Serial Communication Layer
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
class SerialComm:
|
||||||
|
"""Handles low-level serial port communication"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.port: Optional[serial.Serial] = None
|
||||||
|
self.port_name: str = "/dev/ttyUSB0"
|
||||||
|
self.baudrate: int = 9600
|
||||||
|
self.timeout: float = 1.0
|
||||||
|
self.packet_log: List[str] = []
|
||||||
|
|
||||||
|
def connect(self, port_name: str, baudrate: int = 9600) -> bool:
|
||||||
|
"""
|
||||||
|
Connect to serial port
|
||||||
|
|
||||||
|
Args:
|
||||||
|
port_name: Serial port device name
|
||||||
|
baudrate: Baud rate (default 9600)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.port_name = port_name
|
||||||
|
self.baudrate = baudrate
|
||||||
|
self.port = serial.Serial(
|
||||||
|
port=port_name,
|
||||||
|
baudrate=baudrate,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
parity=serial.PARITY_NONE,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
timeout=self.timeout
|
||||||
|
)
|
||||||
|
time.sleep(0.1) # Allow port to stabilize
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Serial connection error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
"""Disconnect from serial port"""
|
||||||
|
if self.port and self.port.is_open:
|
||||||
|
self.port.close()
|
||||||
|
self.port = None
|
||||||
|
|
||||||
|
def is_connected(self) -> bool:
|
||||||
|
"""Check if serial port is connected"""
|
||||||
|
return self.port is not None and self.port.is_open
|
||||||
|
|
||||||
|
def write(self, data: bytes) -> bool:
|
||||||
|
"""
|
||||||
|
Write data to serial port
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Bytes to write
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
if not self.is_connected():
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.port.write(data)
|
||||||
|
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
||||||
|
hex_str = " ".join(f"{b:02x}" for b in data)
|
||||||
|
self.packet_log.append(f"[{timestamp}] TX: {hex_str}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Serial write error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def read(self, size: int) -> Optional[bytes]:
|
||||||
|
"""
|
||||||
|
Read data from serial port
|
||||||
|
|
||||||
|
Args:
|
||||||
|
size: Number of bytes to read
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Bytes read or None on error
|
||||||
|
"""
|
||||||
|
if not self.is_connected():
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = self.port.read(size)
|
||||||
|
if data:
|
||||||
|
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
||||||
|
hex_str = " ".join(f"{b:02x}" for b in data)
|
||||||
|
self.packet_log.append(f"[{timestamp}] RX: {hex_str}")
|
||||||
|
return data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Serial read error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
"""Flush serial port buffers"""
|
||||||
|
if self.is_connected():
|
||||||
|
self.port.reset_input_buffer()
|
||||||
|
self.port.reset_output_buffer()
|
||||||
|
|
||||||
|
def get_packet_log(self, last_n: int = 100) -> List[str]:
|
||||||
|
"""Get last N entries from packet log"""
|
||||||
|
return self.packet_log[-last_n:]
|
||||||
|
|
||||||
|
def clear_packet_log(self):
|
||||||
|
"""Clear packet log"""
|
||||||
|
self.packet_log.clear()
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# I2C Protocol Layer
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
class I2CProtocol:
|
||||||
|
"""Handles NXP I2C-over-serial protocol packet construction"""
|
||||||
|
|
||||||
|
NXP_START = 0x53
|
||||||
|
NXP_STOP = 0x50
|
||||||
|
|
||||||
|
def __init__(self, serial_comm: SerialComm):
|
||||||
|
self.serial = serial_comm
|
||||||
|
|
||||||
|
def write(self, i2c_addr_write: int, cmd: bytes, data: bytes = b'') -> bool:
|
||||||
|
"""
|
||||||
|
Build and send NXP I2C write packet
|
||||||
|
|
||||||
|
Packet format: [0x53] [I2C_ADDR] [LENGTH] [COMMAND] [DATA] [0x50]
|
||||||
|
|
||||||
|
Args:
|
||||||
|
i2c_addr_write: I2C write address
|
||||||
|
cmd: Command bytes
|
||||||
|
data: Data bytes (optional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
if not isinstance(cmd, bytes):
|
||||||
|
cmd = bytes([cmd])
|
||||||
|
if not isinstance(data, bytes):
|
||||||
|
data = bytes(data)
|
||||||
|
|
||||||
|
length = len(cmd) + len(data)
|
||||||
|
packet = bytes([self.NXP_START, i2c_addr_write, length]) + cmd + data + bytes([self.NXP_STOP])
|
||||||
|
|
||||||
|
return self.serial.write(packet)
|
||||||
|
|
||||||
|
def read(self, i2c_addr_write: int, cmd: bytes, cmd_len: int, data_len: int) -> Optional[bytes]:
|
||||||
|
"""
|
||||||
|
Build and send NXP I2C read packet, return data
|
||||||
|
|
||||||
|
Packet format for read:
|
||||||
|
[0x53] [ADDR_WRITE] [CMD_LEN] [CMD] [0x53] [ADDR_READ] [DATA_LEN] [0x50]
|
||||||
|
|
||||||
|
Args:
|
||||||
|
i2c_addr_write: I2C write address
|
||||||
|
cmd: Command bytes
|
||||||
|
cmd_len: Length of command
|
||||||
|
data_len: Expected data length to read
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Data bytes read or None on error
|
||||||
|
"""
|
||||||
|
if not isinstance(cmd, bytes):
|
||||||
|
cmd = bytes([cmd])
|
||||||
|
|
||||||
|
i2c_addr_read = i2c_addr_write | 0x01 # Set read bit
|
||||||
|
|
||||||
|
# First part: write command
|
||||||
|
packet_write = bytes([self.NXP_START, i2c_addr_write, cmd_len]) + cmd
|
||||||
|
|
||||||
|
# Second part: read data
|
||||||
|
packet_read = bytes([self.NXP_START, i2c_addr_read, data_len, self.NXP_STOP])
|
||||||
|
|
||||||
|
packet = packet_write + packet_read
|
||||||
|
|
||||||
|
if not self.serial.write(packet):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Read response
|
||||||
|
# Expected response: [data_bytes]
|
||||||
|
time.sleep(0.05) # Give device time to respond
|
||||||
|
response = self.serial.read(data_len)
|
||||||
|
|
||||||
|
return response if response and len(response) == data_len else None
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# I2C Device Functions
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
class I2CDevices:
|
||||||
|
"""High-level I2C device control functions"""
|
||||||
|
|
||||||
|
def __init__(self, protocol: I2CProtocol):
|
||||||
|
self.protocol = protocol
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
# PCA9555 I/O Expander Functions
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def pca9555_read_port(self, addr_write: int, port: int) -> Optional[int]:
|
||||||
|
"""
|
||||||
|
Read PCA9555 port register
|
||||||
|
|
||||||
|
Args:
|
||||||
|
addr_write: I2C write address
|
||||||
|
port: Register address (0-7)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Port value (0-255) or None on error
|
||||||
|
"""
|
||||||
|
data = self.protocol.read(addr_write, bytes([port]), 1, 1)
|
||||||
|
return data[0] if data else None
|
||||||
|
|
||||||
|
def pca9555_write_port(self, addr_write: int, port: int, value: int) -> bool:
|
||||||
|
"""
|
||||||
|
Write PCA9555 port register
|
||||||
|
|
||||||
|
Args:
|
||||||
|
addr_write: I2C write address
|
||||||
|
port: Register address (0-7)
|
||||||
|
value: Value to write (0-255)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
return self.protocol.write(addr_write, bytes([port]), bytes([value]))
|
||||||
|
|
||||||
|
def pca9555_set_bit(self, addr_write: int, port: int, bitmask: int, state: bool) -> bool:
|
||||||
|
"""
|
||||||
|
Set or clear specific bit on PCA9555
|
||||||
|
|
||||||
|
Args:
|
||||||
|
addr_write: I2C write address
|
||||||
|
port: Register address
|
||||||
|
bitmask: Bit mask (e.g., 0x01 for bit 0)
|
||||||
|
state: True to set bit, False to clear
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
# Read current value
|
||||||
|
current = self.pca9555_read_port(addr_write, port)
|
||||||
|
if current is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Modify bit
|
||||||
|
if state:
|
||||||
|
new_value = current | bitmask
|
||||||
|
else:
|
||||||
|
new_value = current & ~bitmask
|
||||||
|
|
||||||
|
# Write back
|
||||||
|
return self.pca9555_write_port(addr_write, port, new_value)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
# X9119 Digital Potentiometer Functions
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def x9119_write_wiper(self, addr_write: int, value: int) -> bool:
|
||||||
|
"""
|
||||||
|
Set X9119 wiper position
|
||||||
|
|
||||||
|
Args:
|
||||||
|
addr_write: I2C write address
|
||||||
|
value: Wiper position (0-1023, 10-bit)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
# X9119 write wiper command: 0xa0 followed by 2 bytes (10-bit value)
|
||||||
|
# Value is split into two bytes: MSB contains upper 2 bits, LSB contains lower 8 bits
|
||||||
|
value = max(0, min(1023, value)) # Clamp to valid range
|
||||||
|
|
||||||
|
msb = (value >> 8) & 0x03 # Upper 2 bits
|
||||||
|
lsb = value & 0xFF # Lower 8 bits
|
||||||
|
|
||||||
|
cmd = bytes([0xa0])
|
||||||
|
data = bytes([msb, lsb])
|
||||||
|
|
||||||
|
return self.protocol.write(addr_write, cmd, data)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
# ADS7828 ADC Functions
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def ads7828_read(self, addr_write: int, channel: int) -> Optional[int]:
|
||||||
|
"""
|
||||||
|
Read ADS7828 ADC channel
|
||||||
|
|
||||||
|
Args:
|
||||||
|
addr_write: I2C write address
|
||||||
|
channel: Channel to read (0-7)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
12-bit ADC value (0-4095) or None on error
|
||||||
|
"""
|
||||||
|
# ADS7828 command byte format:
|
||||||
|
# Bit 7: SD (single-ended=1)
|
||||||
|
# Bits 6-4: Channel select
|
||||||
|
# Bit 3: PD1 (power-down mode)
|
||||||
|
# Bit 2: PD0 (power-down mode)
|
||||||
|
# Bits 1-0: Don't care
|
||||||
|
|
||||||
|
# For single-ended read with internal reference on
|
||||||
|
cmd_byte = 0x80 | ((channel & 0x07) << 4) | 0x0c
|
||||||
|
|
||||||
|
data = self.protocol.read(addr_write, bytes([cmd_byte]), 1, 2)
|
||||||
|
if not data or len(data) != 2:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Combine two bytes into 12-bit value
|
||||||
|
value = (data[0] << 8) | data[1]
|
||||||
|
value = (value >> 4) & 0x0FFF # Extract 12-bit value
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Laser Control Layer
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
class LaserControl:
|
||||||
|
"""High-level laser control functions"""
|
||||||
|
|
||||||
|
def __init__(self, devices: I2CDevices):
|
||||||
|
self.devices = devices
|
||||||
|
self._safe_state_active = False
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
# SET Commands
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def set_current(self, value: int) -> bool:
|
||||||
|
"""
|
||||||
|
Set laser current command (CCMD=)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: Current value (0-1023)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
return self.devices.x9119_write_wiper(I2CAddress.X9119_CURRENT_WRITE, value)
|
||||||
|
|
||||||
|
def set_power_cmd(self, value: int) -> bool:
|
||||||
|
"""
|
||||||
|
Set power command (PCMD=)
|
||||||
|
|
||||||
|
Note: Uses same device as current control, different channel
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: Power value (0-1023)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
return self.devices.x9119_write_wiper(I2CAddress.X9119_CURRENT_WRITE, value)
|
||||||
|
|
||||||
|
def set_shutter(self, state: bool) -> bool:
|
||||||
|
"""
|
||||||
|
Set shutter state (SHCMD=)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
state: True for open, False for closed
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
return self.devices.pca9555_set_bit(
|
||||||
|
I2CAddress.PCA9555_DIO_WRITE,
|
||||||
|
PCA9555Register.OUTPUT_PORT_0,
|
||||||
|
ControlBitmask.SHUTTER,
|
||||||
|
state
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_keyswitch(self, state: bool) -> bool:
|
||||||
|
"""
|
||||||
|
Set keyswitch state (KSWCMD=)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
state: True for on, False for off
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
return self.devices.pca9555_set_bit(
|
||||||
|
I2CAddress.PCA9555_DIO_WRITE,
|
||||||
|
PCA9555Register.OUTPUT_PORT_0,
|
||||||
|
ControlBitmask.KEYSWITCH,
|
||||||
|
state
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_current_mode(self, state: bool) -> bool:
|
||||||
|
"""
|
||||||
|
Set current mode (CMODECMD=)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
state: True for on, False for off
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
return self.devices.pca9555_set_bit(
|
||||||
|
I2CAddress.PCA9555_DIO_WRITE,
|
||||||
|
PCA9555Register.OUTPUT_PORT_0,
|
||||||
|
ControlBitmask.CURRENT_MODE,
|
||||||
|
state
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_analog_enable(self, state: bool) -> bool:
|
||||||
|
"""
|
||||||
|
Set analog input enable (ANACMD=)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
state: True for enabled, False for disabled
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
return self.devices.pca9555_set_bit(
|
||||||
|
I2CAddress.PCA9555_DIO_WRITE,
|
||||||
|
PCA9555Register.OUTPUT_PORT_0,
|
||||||
|
ControlBitmask.ANALOG_ENABLE,
|
||||||
|
state
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_remote_enable(self, state: bool) -> bool:
|
||||||
|
"""
|
||||||
|
Set remote enable (REM=)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
state: True for enabled, False for disabled
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
return self.devices.pca9555_set_bit(
|
||||||
|
I2CAddress.PCA9555_DIO_WRITE,
|
||||||
|
PCA9555Register.OUTPUT_PORT_0,
|
||||||
|
ControlBitmask.REMOTE_ENABLE,
|
||||||
|
state
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
# GET/Query Commands
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def get_current_actual(self) -> Optional[float]:
|
||||||
|
"""
|
||||||
|
Get actual current reading
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Current in arbitrary units (0-4095) or None on error
|
||||||
|
"""
|
||||||
|
# Read from ADS7828, channel based on command 0x84
|
||||||
|
# Command 0x84 suggests channel 0
|
||||||
|
value = self.devices.ads7828_read(I2CAddress.ADS7828_WRITE, 0)
|
||||||
|
return value if value is not None else None
|
||||||
|
|
||||||
|
def get_interlock_status(self) -> Optional[bool]:
|
||||||
|
"""
|
||||||
|
Get interlock status
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if interlock OK, False if fault, None on error
|
||||||
|
"""
|
||||||
|
value = self.devices.pca9555_read_port(
|
||||||
|
I2CAddress.PCA9555_PSGLUE_WRITE,
|
||||||
|
PCA9555Register.INPUT_PORT_0
|
||||||
|
)
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Bit 0 indicates interlock status
|
||||||
|
return bool(value & ControlBitmask.INTERLOCK)
|
||||||
|
|
||||||
|
def get_ldd_enable_status(self) -> Optional[bool]:
|
||||||
|
"""
|
||||||
|
Get laser diode driver enable status
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if enabled, False if disabled, None on error
|
||||||
|
"""
|
||||||
|
value = self.devices.pca9555_read_port(
|
||||||
|
I2CAddress.PCA9555_LDD_WRITE,
|
||||||
|
PCA9555Register.INPUT_PORT_0
|
||||||
|
)
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return bool(value & ControlBitmask.LDD_ENABLE)
|
||||||
|
|
||||||
|
def get_psglue_in_status(self) -> Optional[int]:
|
||||||
|
"""
|
||||||
|
Get power supply glue input status
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Port value or None on error
|
||||||
|
"""
|
||||||
|
return self.devices.pca9555_read_port(
|
||||||
|
I2CAddress.PCA9555_PSGLUE_WRITE,
|
||||||
|
PCA9555Register.INPUT_PORT_0
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_psglue_out_status(self) -> Optional[int]:
|
||||||
|
"""
|
||||||
|
Get power supply glue output status
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Port value or None on error
|
||||||
|
"""
|
||||||
|
return self.devices.pca9555_read_port(
|
||||||
|
I2CAddress.PCA9555_DIO_WRITE,
|
||||||
|
PCA9555Register.OUTPUT_PORT_0
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_head_dio_status(self) -> Optional[int]:
|
||||||
|
"""
|
||||||
|
Get head DIO status
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Port value or None on error
|
||||||
|
"""
|
||||||
|
return self.devices.pca9555_read_port(
|
||||||
|
I2CAddress.PCA9555_HEAD_WRITE,
|
||||||
|
PCA9555Register.INPUT_PORT_0
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
# Safety Functions
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def emergency_stop(self) -> bool:
|
||||||
|
"""
|
||||||
|
Emergency stop: close shutter, set current to 0, disable keyswitch
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if all operations successful, False otherwise
|
||||||
|
"""
|
||||||
|
success = True
|
||||||
|
success &= self.set_shutter(False)
|
||||||
|
success &= self.set_current(0)
|
||||||
|
success &= self.set_keyswitch(False)
|
||||||
|
self._safe_state_active = True
|
||||||
|
return success
|
||||||
|
|
||||||
|
def enter_safe_state(self) -> bool:
|
||||||
|
"""
|
||||||
|
Enter safe state (similar to emergency stop but also disables remote)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
success = True
|
||||||
|
success &= self.set_shutter(False)
|
||||||
|
success &= self.set_current(0)
|
||||||
|
success &= self.set_power_cmd(0)
|
||||||
|
success &= self.set_keyswitch(False)
|
||||||
|
success &= self.set_remote_enable(False)
|
||||||
|
self._safe_state_active = True
|
||||||
|
return success
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
"""
|
||||||
|
Helios Laser System Driver
|
||||||
|
Basic implementation for controlling the Helios pulsed laser.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
from typing import Optional, List
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PulseMode(Enum):
|
||||||
|
"""Helios pulse mode enumeration"""
|
||||||
|
SINGLE_PULSE = 0
|
||||||
|
CONTINUOUS_GATING = 1
|
||||||
|
CONTINUOUS_PULSING = 2
|
||||||
|
|
||||||
|
|
||||||
|
class HeliosLaser:
|
||||||
|
"""
|
||||||
|
Driver for Helios pulsed laser system.
|
||||||
|
|
||||||
|
Communication: RS-232, 9600 baud, 8N1
|
||||||
|
Commands are ASCII strings terminated with CR
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, port: str = None, timeout: float = 1.0):
|
||||||
|
"""
|
||||||
|
Initialize Helios laser driver.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
port: Serial port (e.g., '/dev/ttyUSB0' or 'COM5')
|
||||||
|
timeout: Serial timeout in seconds
|
||||||
|
"""
|
||||||
|
self.port = port
|
||||||
|
self.timeout = timeout
|
||||||
|
self.serial = None
|
||||||
|
self.is_connected = False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def list_available_ports() -> List[str]:
|
||||||
|
"""List available serial ports"""
|
||||||
|
import serial.tools.list_ports
|
||||||
|
ports = serial.tools.list_ports.comports()
|
||||||
|
return [port.device for port in ports]
|
||||||
|
|
||||||
|
def connect(self, port: str = None) -> bool:
|
||||||
|
"""
|
||||||
|
Connect to the Helios laser.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
port: Serial port (uses stored port if None)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if connection successful
|
||||||
|
"""
|
||||||
|
if port:
|
||||||
|
self.port = port
|
||||||
|
|
||||||
|
if not self.port:
|
||||||
|
logger.error("No port specified")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.serial = serial.Serial(
|
||||||
|
port=self.port,
|
||||||
|
baudrate=9600,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
parity=serial.PARITY_NONE,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
timeout=self.timeout
|
||||||
|
)
|
||||||
|
time.sleep(0.1) # Allow time for connection to stabilize
|
||||||
|
self.is_connected = True
|
||||||
|
logger.info(f"Connected to Helios laser on {self.port}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to connect to Helios laser: {e}")
|
||||||
|
self.is_connected = False
|
||||||
|
return False
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
"""Disconnect from the laser"""
|
||||||
|
if self.serial and self.serial.is_open:
|
||||||
|
try:
|
||||||
|
# Disable laser before disconnecting
|
||||||
|
self.set_laser_enable(False)
|
||||||
|
self.serial.close()
|
||||||
|
logger.info("Disconnected from Helios laser")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error during disconnect: {e}")
|
||||||
|
|
||||||
|
self.is_connected = False
|
||||||
|
self.serial = None
|
||||||
|
|
||||||
|
def _send_command(self, command: str) -> bool:
|
||||||
|
"""
|
||||||
|
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:
|
||||||
|
logger.error("Not connected to laser")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
cmd_bytes = (command + '\r').encode('ascii')
|
||||||
|
self.serial.write(cmd_bytes)
|
||||||
|
logger.debug(f"Sent command: {command}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to send command '{command}': {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _query(self, command: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Send a query and read response.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command: ASCII query command (without CR)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response string or None if error
|
||||||
|
"""
|
||||||
|
if not self._send_command(command):
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = self.serial.readline().decode('ascii').strip()
|
||||||
|
logger.debug(f"Query '{command}' response: {response}")
|
||||||
|
return response
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to read response for '{command}': {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set_frequency_hz(self, frequency: int) -> bool:
|
||||||
|
"""
|
||||||
|
Set laser pulse frequency in Hz.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frequency: Frequency in Hz (16700 - 125000)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful
|
||||||
|
"""
|
||||||
|
if not (16700 <= frequency <= 125000):
|
||||||
|
logger.error(f"Frequency {frequency} Hz out of range (16700-125000)")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Convert frequency to period in nanoseconds
|
||||||
|
period_ns = int(1e9 / frequency)
|
||||||
|
|
||||||
|
command = f"FP={period_ns}"
|
||||||
|
return self._send_command(command)
|
||||||
|
|
||||||
|
def set_current_ma(self, current: int) -> bool:
|
||||||
|
"""
|
||||||
|
Set pump diode current in mA.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current: Current in mA (0 - 7000)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful
|
||||||
|
"""
|
||||||
|
if not (0 <= current <= 7000):
|
||||||
|
logger.error(f"Current {current} mA out of range (0-7000)")
|
||||||
|
return False
|
||||||
|
|
||||||
|
command = f"PC={current}"
|
||||||
|
return self._send_command(command)
|
||||||
|
|
||||||
|
def set_pulse_mode(self, mode: PulseMode) -> bool:
|
||||||
|
"""
|
||||||
|
Set pulse mode.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mode: PulseMode enumeration value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful
|
||||||
|
"""
|
||||||
|
command = f"PM={mode.value}"
|
||||||
|
return self._send_command(command)
|
||||||
|
|
||||||
|
def set_laser_enable(self, enable: bool) -> bool:
|
||||||
|
"""
|
||||||
|
Enable or disable laser emission.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
enable: True to enable, False to disable
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful
|
||||||
|
"""
|
||||||
|
command = f"LE={1 if enable else 0}"
|
||||||
|
success = self._send_command(command)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
state = "enabled" if enable else "disabled"
|
||||||
|
logger.info(f"Laser {state}")
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
def is_laser_enabled(self) -> bool:
|
||||||
|
"""
|
||||||
|
Check if laser is currently enabled.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if laser is enabled
|
||||||
|
"""
|
||||||
|
response = self._query("LE?")
|
||||||
|
if response:
|
||||||
|
try:
|
||||||
|
return int(response) == 1
|
||||||
|
except ValueError:
|
||||||
|
logger.error(f"Invalid response for LE?: {response}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_frequency_hz(self) -> Optional[int]:
|
||||||
|
"""
|
||||||
|
Get current laser frequency in Hz.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Frequency in Hz or None if error
|
||||||
|
"""
|
||||||
|
response = self._query("FP?")
|
||||||
|
if response:
|
||||||
|
try:
|
||||||
|
period_ns = int(response)
|
||||||
|
return int(1e9 / period_ns)
|
||||||
|
except (ValueError, ZeroDivisionError):
|
||||||
|
logger.error(f"Invalid response for FP?: {response}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_current_ma(self) -> Optional[int]:
|
||||||
|
"""
|
||||||
|
Get current pump diode current in mA.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Current in mA or None if error
|
||||||
|
"""
|
||||||
|
response = self._query("PC?")
|
||||||
|
if response:
|
||||||
|
try:
|
||||||
|
return int(response)
|
||||||
|
except ValueError:
|
||||||
|
logger.error(f"Invalid response for PC?: {response}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_power_mw(self) -> Optional[float]:
|
||||||
|
"""
|
||||||
|
Get laser output power in mW.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Power in mW or None if error
|
||||||
|
"""
|
||||||
|
response = self._query("PO?")
|
||||||
|
if response:
|
||||||
|
try:
|
||||||
|
return float(response)
|
||||||
|
except ValueError:
|
||||||
|
logger.error(f"Invalid response for PO?: {response}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_controller_serial(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Get controller serial number.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Serial number string or None if error
|
||||||
|
"""
|
||||||
|
return self._query("SN?")
|
||||||
|
|
||||||
|
def get_head_serial(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Get laser head serial number.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Serial number string or None if error
|
||||||
|
"""
|
||||||
|
return self._query("HSN?")
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
"""Destructor - ensure cleanup"""
|
||||||
|
self.disconnect()
|
||||||
@@ -0,0 +1,475 @@
|
|||||||
|
"""
|
||||||
|
uC480 Camera Driver
|
||||||
|
Driver for IDS/Thorlabs uEye uC480 cameras using pyueye library.
|
||||||
|
Provides camera control, live streaming, and image capture capabilities.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from pyueye import ueye
|
||||||
|
from PyQt6.QtCore import QThread, pyqtSignal, QObject
|
||||||
|
from PyQt6.QtGui import QImage
|
||||||
|
import logging
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class UC480Camera(QObject):
|
||||||
|
"""
|
||||||
|
Driver class for uC480 camera.
|
||||||
|
Handles initialization, configuration, and image acquisition.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Signals
|
||||||
|
frame_ready = pyqtSignal(QImage) # Emitted when a new frame is captured
|
||||||
|
error_occurred = pyqtSignal(str) # Emitted when an error occurs
|
||||||
|
|
||||||
|
def __init__(self, camera_id: int = 0):
|
||||||
|
"""
|
||||||
|
Initialize the uC480 camera driver.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
camera_id: Camera ID (0 for first available camera)
|
||||||
|
"""
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.camera_id = camera_id
|
||||||
|
self.h_cam = ueye.HIDS(camera_id)
|
||||||
|
self.is_initialized = False
|
||||||
|
self.is_capturing = False
|
||||||
|
|
||||||
|
# Memory and image info
|
||||||
|
self.mem_ptr = ueye.c_mem_p()
|
||||||
|
self.mem_id = ueye.int()
|
||||||
|
self.pitch = ueye.INT()
|
||||||
|
|
||||||
|
# Camera info
|
||||||
|
self.sensor_info = ueye.SENSORINFO()
|
||||||
|
self.cam_info = ueye.CAMINFO()
|
||||||
|
self.rect_aoi = ueye.IS_RECT()
|
||||||
|
|
||||||
|
# Image dimensions
|
||||||
|
self.width = 0
|
||||||
|
self.height = 0
|
||||||
|
self.bits_per_pixel = 24 # Default to 24-bit color
|
||||||
|
self.bytes_per_pixel = 3
|
||||||
|
self.color_mode = ueye.IS_CM_BGR8_PACKED
|
||||||
|
|
||||||
|
def initialize(self) -> bool:
|
||||||
|
"""
|
||||||
|
Initialize the camera and allocate memory.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Initialize camera
|
||||||
|
ret = ueye.is_InitCamera(self.h_cam, None)
|
||||||
|
if ret != ueye.IS_SUCCESS:
|
||||||
|
logger.error(f"Failed to initialize camera: {ret}")
|
||||||
|
self.error_occurred.emit(f"Failed to initialize camera: {ret}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Get sensor info
|
||||||
|
ret = ueye.is_GetSensorInfo(self.h_cam, self.sensor_info)
|
||||||
|
if ret != ueye.IS_SUCCESS:
|
||||||
|
logger.error(f"Failed to get sensor info: {ret}")
|
||||||
|
self.cleanup()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Get camera info
|
||||||
|
ret = ueye.is_GetCameraInfo(self.h_cam, self.cam_info)
|
||||||
|
if ret != ueye.IS_SUCCESS:
|
||||||
|
logger.error(f"Failed to get camera info: {ret}")
|
||||||
|
self.cleanup()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Set color mode
|
||||||
|
ret = ueye.is_SetColorMode(self.h_cam, self.color_mode)
|
||||||
|
if ret != ueye.IS_SUCCESS:
|
||||||
|
logger.error(f"Failed to set color mode: {ret}")
|
||||||
|
self.cleanup()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Get maximum image size
|
||||||
|
self.width = self.sensor_info.nMaxWidth.value
|
||||||
|
self.height = self.sensor_info.nMaxHeight.value
|
||||||
|
|
||||||
|
# Set Area of Interest (AOI) to maximum size
|
||||||
|
self.rect_aoi.s32X = ueye.int(0)
|
||||||
|
self.rect_aoi.s32Y = ueye.int(0)
|
||||||
|
self.rect_aoi.s32Width = ueye.int(self.width)
|
||||||
|
self.rect_aoi.s32Height = ueye.int(self.height)
|
||||||
|
|
||||||
|
ret = ueye.is_AOI(self.h_cam, ueye.IS_AOI_IMAGE_SET_AOI, self.rect_aoi, ueye.sizeof(self.rect_aoi))
|
||||||
|
if ret != ueye.IS_SUCCESS:
|
||||||
|
logger.error(f"Failed to set AOI: {ret}")
|
||||||
|
self.cleanup()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Allocate image memory
|
||||||
|
ret = ueye.is_AllocImageMem(
|
||||||
|
self.h_cam,
|
||||||
|
self.width,
|
||||||
|
self.height,
|
||||||
|
self.bits_per_pixel,
|
||||||
|
self.mem_ptr,
|
||||||
|
self.mem_id
|
||||||
|
)
|
||||||
|
if ret != ueye.IS_SUCCESS:
|
||||||
|
logger.error(f"Failed to allocate image memory: {ret}")
|
||||||
|
self.cleanup()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Set active memory
|
||||||
|
ret = ueye.is_SetImageMem(self.h_cam, self.mem_ptr, self.mem_id)
|
||||||
|
if ret != ueye.IS_SUCCESS:
|
||||||
|
logger.error(f"Failed to set active memory: {ret}")
|
||||||
|
self.cleanup()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Get pitch (bytes per line)
|
||||||
|
ret = ueye.is_GetImageMemPitch(self.h_cam, self.pitch)
|
||||||
|
if ret != ueye.IS_SUCCESS:
|
||||||
|
logger.error(f"Failed to get pitch: {ret}")
|
||||||
|
self.cleanup()
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.is_initialized = True
|
||||||
|
logger.info(f"Camera initialized: {self.width}x{self.height}, {self.bits_per_pixel}bpp")
|
||||||
|
|
||||||
|
# Set default settings
|
||||||
|
self.set_exposure(10.0) # 10ms default exposure
|
||||||
|
self.set_pixel_clock(30) # 30MHz default pixel clock
|
||||||
|
self.set_framerate(30.0) # 30fps default
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Exception during camera initialization: {e}")
|
||||||
|
self.error_occurred.emit(f"Exception during initialization: {e}")
|
||||||
|
self.cleanup()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
"""Release camera resources."""
|
||||||
|
if self.is_capturing:
|
||||||
|
self.stop_capture()
|
||||||
|
|
||||||
|
if self.mem_ptr:
|
||||||
|
ueye.is_FreeImageMem(self.h_cam, self.mem_ptr, self.mem_id)
|
||||||
|
self.mem_ptr = None
|
||||||
|
|
||||||
|
if self.is_initialized:
|
||||||
|
ueye.is_ExitCamera(self.h_cam)
|
||||||
|
self.is_initialized = False
|
||||||
|
logger.info("Camera resources released")
|
||||||
|
|
||||||
|
def start_capture(self) -> bool:
|
||||||
|
"""
|
||||||
|
Start continuous video capture.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
if not self.is_initialized:
|
||||||
|
logger.error("Camera not initialized")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.is_capturing:
|
||||||
|
logger.warning("Camera already capturing")
|
||||||
|
return True
|
||||||
|
|
||||||
|
ret = ueye.is_CaptureVideo(self.h_cam, ueye.IS_DONT_WAIT)
|
||||||
|
if ret != ueye.IS_SUCCESS:
|
||||||
|
logger.error(f"Failed to start capture: {ret}")
|
||||||
|
self.error_occurred.emit(f"Failed to start capture: {ret}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.is_capturing = True
|
||||||
|
logger.info("Video capture started")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def stop_capture(self) -> bool:
|
||||||
|
"""
|
||||||
|
Stop continuous video capture.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
if not self.is_capturing:
|
||||||
|
return True
|
||||||
|
|
||||||
|
ret = ueye.is_StopLiveVideo(self.h_cam, ueye.IS_WAIT)
|
||||||
|
if ret != ueye.IS_SUCCESS:
|
||||||
|
logger.error(f"Failed to stop capture: {ret}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.is_capturing = False
|
||||||
|
logger.info("Video capture stopped")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_frame(self) -> Optional[QImage]:
|
||||||
|
"""
|
||||||
|
Capture a single frame from the camera.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
QImage if successful, None otherwise
|
||||||
|
"""
|
||||||
|
if not self.is_initialized:
|
||||||
|
logger.error("Camera not initialized")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Create numpy array from image memory
|
||||||
|
try:
|
||||||
|
array = ueye.get_data(
|
||||||
|
self.mem_ptr,
|
||||||
|
self.width,
|
||||||
|
self.height,
|
||||||
|
self.bits_per_pixel,
|
||||||
|
self.pitch,
|
||||||
|
copy=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reshape to image dimensions
|
||||||
|
frame = np.reshape(array, (self.height, self.width, self.bytes_per_pixel))
|
||||||
|
|
||||||
|
# Convert to QImage (BGR to RGB)
|
||||||
|
height, width, channel = frame.shape
|
||||||
|
bytes_per_line = self.bytes_per_pixel * width
|
||||||
|
|
||||||
|
# Convert BGR to RGB
|
||||||
|
rgb_frame = frame[:, :, ::-1].copy()
|
||||||
|
|
||||||
|
q_image = QImage(
|
||||||
|
rgb_frame.data,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
bytes_per_line,
|
||||||
|
QImage.Format.Format_RGB888
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make a copy since the numpy array will be deleted
|
||||||
|
return q_image.copy()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get frame: {e}")
|
||||||
|
self.error_occurred.emit(f"Failed to get frame: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set_exposure(self, exposure_ms: float) -> bool:
|
||||||
|
"""
|
||||||
|
Set camera exposure time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
exposure_ms: Exposure time in milliseconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
if not self.is_initialized:
|
||||||
|
return False
|
||||||
|
|
||||||
|
exposure = ueye.c_double(exposure_ms)
|
||||||
|
ret = ueye.is_Exposure(
|
||||||
|
self.h_cam,
|
||||||
|
ueye.IS_EXPOSURE_CMD_SET_EXPOSURE,
|
||||||
|
exposure,
|
||||||
|
ueye.sizeof(exposure)
|
||||||
|
)
|
||||||
|
|
||||||
|
if ret == ueye.IS_SUCCESS:
|
||||||
|
logger.debug(f"Exposure set to {exposure_ms}ms")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(f"Failed to set exposure: {ret}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_exposure(self) -> Optional[float]:
|
||||||
|
"""
|
||||||
|
Get current exposure time.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Exposure time in milliseconds, or None if failed
|
||||||
|
"""
|
||||||
|
if not self.is_initialized:
|
||||||
|
return None
|
||||||
|
|
||||||
|
exposure = ueye.c_double()
|
||||||
|
ret = ueye.is_Exposure(
|
||||||
|
self.h_cam,
|
||||||
|
ueye.IS_EXPOSURE_CMD_GET_EXPOSURE,
|
||||||
|
exposure,
|
||||||
|
ueye.sizeof(exposure)
|
||||||
|
)
|
||||||
|
|
||||||
|
if ret == ueye.IS_SUCCESS:
|
||||||
|
return exposure.value
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set_pixel_clock(self, pixel_clock_mhz: int) -> bool:
|
||||||
|
"""
|
||||||
|
Set camera pixel clock.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pixel_clock_mhz: Pixel clock in MHz
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
if not self.is_initialized:
|
||||||
|
return False
|
||||||
|
|
||||||
|
ret = ueye.is_PixelClock(
|
||||||
|
self.h_cam,
|
||||||
|
ueye.IS_PIXELCLOCK_CMD_SET,
|
||||||
|
ueye.c_uint(pixel_clock_mhz),
|
||||||
|
ueye.sizeof(ueye.c_uint)
|
||||||
|
)
|
||||||
|
|
||||||
|
if ret == ueye.IS_SUCCESS:
|
||||||
|
logger.debug(f"Pixel clock set to {pixel_clock_mhz}MHz")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(f"Failed to set pixel clock: {ret}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def set_framerate(self, fps: float) -> bool:
|
||||||
|
"""
|
||||||
|
Set camera framerate.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fps: Frames per second
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
if not self.is_initialized:
|
||||||
|
return False
|
||||||
|
|
||||||
|
new_fps = ueye.c_double(fps)
|
||||||
|
actual_fps = ueye.c_double()
|
||||||
|
ret = ueye.is_SetFrameRate(self.h_cam, new_fps, actual_fps)
|
||||||
|
|
||||||
|
if ret == ueye.IS_SUCCESS:
|
||||||
|
logger.debug(f"Framerate set to {fps}fps")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(f"Failed to set framerate: {ret}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_framerate(self) -> Optional[float]:
|
||||||
|
"""
|
||||||
|
Get current framerate.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Framerate in fps, or None if failed
|
||||||
|
"""
|
||||||
|
if not self.is_initialized:
|
||||||
|
return None
|
||||||
|
|
||||||
|
fps = ueye.c_double()
|
||||||
|
ret = ueye.is_GetFramesPerSecond(self.h_cam, fps)
|
||||||
|
|
||||||
|
if ret == ueye.IS_SUCCESS:
|
||||||
|
return fps.value
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set_gain(self, master_gain: int) -> bool:
|
||||||
|
"""
|
||||||
|
Set camera master gain.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
master_gain: Gain value (0-100)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
if not self.is_initialized:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if master_gain < 0 or master_gain > 100:
|
||||||
|
logger.error(f"Gain value {master_gain} out of range (0-100)")
|
||||||
|
return False
|
||||||
|
|
||||||
|
ret = ueye.is_SetHardwareGain(
|
||||||
|
self.h_cam,
|
||||||
|
master_gain,
|
||||||
|
ueye.IS_IGNORE_PARAMETER,
|
||||||
|
ueye.IS_IGNORE_PARAMETER,
|
||||||
|
ueye.IS_IGNORE_PARAMETER
|
||||||
|
)
|
||||||
|
|
||||||
|
if ret == ueye.IS_SUCCESS:
|
||||||
|
logger.debug(f"Master gain set to {master_gain}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(f"Failed to set gain: {ret}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_sensor_info(self) -> dict:
|
||||||
|
"""
|
||||||
|
Get camera sensor information.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with sensor information
|
||||||
|
"""
|
||||||
|
if not self.is_initialized:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'sensor_name': self.sensor_info.strSensorName.decode('utf-8'),
|
||||||
|
'max_width': self.sensor_info.nMaxWidth.value,
|
||||||
|
'max_height': self.sensor_info.nMaxHeight.value,
|
||||||
|
'color_mode': self.sensor_info.nColorMode.value,
|
||||||
|
'pixel_size': self.sensor_info.wPixelSize.value / 100.0, # in µm
|
||||||
|
}
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
"""Destructor - ensure cleanup."""
|
||||||
|
self.cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
class CameraStreamThread(QThread):
|
||||||
|
"""
|
||||||
|
Thread for continuous camera frame acquisition and streaming.
|
||||||
|
"""
|
||||||
|
|
||||||
|
frame_ready = pyqtSignal(QImage)
|
||||||
|
error_occurred = pyqtSignal(str)
|
||||||
|
|
||||||
|
def __init__(self, camera: UC480Camera):
|
||||||
|
"""
|
||||||
|
Initialize the camera stream thread.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
camera: UC480Camera instance
|
||||||
|
"""
|
||||||
|
super().__init__()
|
||||||
|
self.camera = camera
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Main thread loop for frame acquisition."""
|
||||||
|
self.running = True
|
||||||
|
|
||||||
|
if not self.camera.start_capture():
|
||||||
|
self.error_occurred.emit("Failed to start camera capture")
|
||||||
|
return
|
||||||
|
|
||||||
|
while self.running:
|
||||||
|
frame = self.camera.get_frame()
|
||||||
|
if frame is not None:
|
||||||
|
self.frame_ready.emit(frame)
|
||||||
|
else:
|
||||||
|
# Small delay on error to prevent CPU spinning
|
||||||
|
self.msleep(10)
|
||||||
|
|
||||||
|
self.camera.stop_capture()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Stop the streaming thread."""
|
||||||
|
self.running = False
|
||||||
|
self.wait()
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
# Byte-compiled / optimized / DLL files
|
|
||||||
__pycache__/
|
|
||||||
*.py[codz]
|
|
||||||
*$py.class
|
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
share/python-wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
MANIFEST
|
|
||||||
|
|
||||||
# PyInstaller
|
|
||||||
# Usually these files are written by a python script from a template
|
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
||||||
*.manifest
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
|
||||||
pip-log.txt
|
|
||||||
pip-delete-this-directory.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
htmlcov/
|
|
||||||
.tox/
|
|
||||||
.nox/
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
*.py.cover
|
|
||||||
.hypothesis/
|
|
||||||
.pytest_cache/
|
|
||||||
cover/
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
*.log
|
|
||||||
local_settings.py
|
|
||||||
db.sqlite3
|
|
||||||
db.sqlite3-journal
|
|
||||||
|
|
||||||
# Flask stuff:
|
|
||||||
instance/
|
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy stuff:
|
|
||||||
.scrapy
|
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
.pybuilder/
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Jupyter Notebook
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# IPython
|
|
||||||
profile_default/
|
|
||||||
ipython_config.py
|
|
||||||
|
|
||||||
# pyenv
|
|
||||||
# For a library or package, you might want to ignore these files since the code is
|
|
||||||
# intended to run in multiple environments; otherwise, check them in:
|
|
||||||
# .python-version
|
|
||||||
|
|
||||||
# pipenv
|
|
||||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
||||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
||||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
||||||
# install all needed dependencies.
|
|
||||||
#Pipfile.lock
|
|
||||||
|
|
||||||
# UV
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
||||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
||||||
# commonly ignored for libraries.
|
|
||||||
#uv.lock
|
|
||||||
|
|
||||||
# poetry
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
||||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
||||||
# commonly ignored for libraries.
|
|
||||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
||||||
#poetry.lock
|
|
||||||
#poetry.toml
|
|
||||||
|
|
||||||
# pdm
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
||||||
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
||||||
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
||||||
#pdm.lock
|
|
||||||
#pdm.toml
|
|
||||||
.pdm-python
|
|
||||||
.pdm-build/
|
|
||||||
|
|
||||||
# pixi
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
||||||
#pixi.lock
|
|
||||||
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
||||||
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
||||||
.pixi
|
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
||||||
__pypackages__/
|
|
||||||
|
|
||||||
# Celery stuff
|
|
||||||
celerybeat-schedule
|
|
||||||
celerybeat.pid
|
|
||||||
|
|
||||||
# SageMath parsed files
|
|
||||||
*.sage.py
|
|
||||||
|
|
||||||
# Environments
|
|
||||||
.env
|
|
||||||
.envrc
|
|
||||||
.venv
|
|
||||||
env/
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
env.bak/
|
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# Spyder project settings
|
|
||||||
.spyderproject
|
|
||||||
.spyproject
|
|
||||||
|
|
||||||
# Rope project settings
|
|
||||||
.ropeproject
|
|
||||||
|
|
||||||
# mkdocs documentation
|
|
||||||
/site
|
|
||||||
|
|
||||||
# mypy
|
|
||||||
.mypy_cache/
|
|
||||||
.dmypy.json
|
|
||||||
dmypy.json
|
|
||||||
|
|
||||||
# Pyre type checker
|
|
||||||
.pyre/
|
|
||||||
|
|
||||||
# pytype static type analyzer
|
|
||||||
.pytype/
|
|
||||||
|
|
||||||
# Cython debug symbols
|
|
||||||
cython_debug/
|
|
||||||
|
|
||||||
# PyCharm
|
|
||||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
||||||
#.idea/
|
|
||||||
|
|
||||||
# Abstra
|
|
||||||
# Abstra is an AI-powered process automation framework.
|
|
||||||
# Ignore directories containing user credentials, local state, and settings.
|
|
||||||
# Learn more at https://abstra.io/docs
|
|
||||||
.abstra/
|
|
||||||
|
|
||||||
# Visual Studio Code
|
|
||||||
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
||||||
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
||||||
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
||||||
# you could uncomment the following to ignore the entire vscode folder
|
|
||||||
# .vscode/
|
|
||||||
|
|
||||||
# Ruff stuff:
|
|
||||||
.ruff_cache/
|
|
||||||
|
|
||||||
# PyPI configuration file
|
|
||||||
.pypirc
|
|
||||||
|
|
||||||
# Cursor
|
|
||||||
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
|
||||||
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
|
||||||
# refer to https://docs.cursor.com/context/ignore-files
|
|
||||||
.cursorignore
|
|
||||||
.cursorindexingignore
|
|
||||||
|
|
||||||
# Marimo
|
|
||||||
marimo/_static/
|
|
||||||
marimo/_lsp/
|
|
||||||
__marimo__/
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
# nuescan
|
|
||||||
SRAS Scan Planning and Control Software
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
# nueScan Setup Guide
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
- Python 3.8 or higher
|
|
||||||
- pip package manager
|
|
||||||
|
|
||||||
### Install Dependencies
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
## Running the Application
|
|
||||||
|
|
||||||
### Method 1: Run as module
|
|
||||||
```bash
|
|
||||||
python -m nuescan
|
|
||||||
```
|
|
||||||
|
|
||||||
### Method 2: Run __main__.py directly
|
|
||||||
```bash
|
|
||||||
python __main__.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
nuescan/
|
|
||||||
├── __main__.py # Application entry point
|
|
||||||
├── main_window.py # Main window controller
|
|
||||||
├── requirements.txt # Python dependencies
|
|
||||||
├── SETUP.md # This file
|
|
||||||
│
|
|
||||||
├── dialogs/ # Dialog controllers
|
|
||||||
│ ├── __init__.py
|
|
||||||
│ ├── genesis_dialog.py # Genesis settings dialog
|
|
||||||
│ ├── helios_dialog.py # Helios settings dialog
|
|
||||||
│ └── scan_active_dialog.py # Scan progress dialog
|
|
||||||
│
|
|
||||||
├── hardware/ # Hardware interface stubs
|
|
||||||
│ ├── __init__.py
|
|
||||||
│ ├── thorlabs_stage.py # ThorLabs MLS stage controller
|
|
||||||
│ ├── t3r_device.py # T3R-SL device controller
|
|
||||||
│ └── microscope.py # Genesis/Helios controller
|
|
||||||
│
|
|
||||||
└── *.ui # Qt Designer UI files
|
|
||||||
```
|
|
||||||
|
|
||||||
## Hardware Interfaces
|
|
||||||
|
|
||||||
### Current Implementation Status
|
|
||||||
|
|
||||||
Hardware module implementation status:
|
|
||||||
- **ThorLabs BBD203 Stage**: ✅ **FULLY IMPLEMENTED** - Production ready
|
|
||||||
- **Helios Laser System**: ✅ **FULLY IMPLEMENTED** - Production ready
|
|
||||||
- **Genesis Microscope**: Stub implementation for development
|
|
||||||
- **T3R-SL Device**: Stub implementation for development
|
|
||||||
|
|
||||||
#### ThorLabs MLS Stage (BBD203 Motor Controller)
|
|
||||||
- **File**: `hardware/thorlabs_stage.py`
|
|
||||||
- **Purpose**: 3-axis positioning control via BBD203 controller
|
|
||||||
- **Connection**: USB with serial number auto-detection
|
|
||||||
- **Status**: Full implementation - ready for real hardware
|
|
||||||
- **Protocol**: APT binary protocol v42.1
|
|
||||||
- **Usage**: Enter BBD203 serial number in UI, driver auto-finds USB port
|
|
||||||
|
|
||||||
#### Helios Laser System
|
|
||||||
- **Files**: `hardware/helios_driver.py`, `hardware/helios_protocol.py`
|
|
||||||
- **Purpose**: Laser control with frequency, current, and pulse mode settings
|
|
||||||
- **Connection**: RS-232 serial (9600 baud, 8N1)
|
|
||||||
- **Status**: Full implementation - ready for real hardware
|
|
||||||
- **Protocol**: ASCII-based RS-232 protocol
|
|
||||||
- **Features**:
|
|
||||||
- Frequency control (16.7-125 kHz)
|
|
||||||
- Current control (0-7000 mA)
|
|
||||||
- Pulse mode control (single, gating, continuous)
|
|
||||||
- Temperature monitoring (4 sensors)
|
|
||||||
- Power monitoring
|
|
||||||
- Status register with error detection
|
|
||||||
- **Usage**: Configure via Helios Settings dialog, COM port selected from dropdown
|
|
||||||
|
|
||||||
#### Genesis Microscope
|
|
||||||
- **File**: `hardware/microscope.py` (Genesis methods)
|
|
||||||
- **Purpose**: Laser scanning microscope system
|
|
||||||
- **Connection**: USB/Serial (not implemented)
|
|
||||||
- **Status**: Stub - simulates microscope connection and status
|
|
||||||
|
|
||||||
#### T3R-SL Device
|
|
||||||
- **File**: `hardware/t3r_device.py`
|
|
||||||
- **Purpose**: Timing and trigger control
|
|
||||||
- **Connection**: USB/Serial (COM port)
|
|
||||||
- **Status**: Stub - simulates device connection and status
|
|
||||||
|
|
||||||
### Implementing Real Hardware Support
|
|
||||||
|
|
||||||
To add actual hardware support, modify the stub methods in the respective hardware files:
|
|
||||||
|
|
||||||
1. Add real serial communication using `pyserial`
|
|
||||||
2. Implement manufacturer-specific protocols
|
|
||||||
3. Add error handling and timeout logic
|
|
||||||
4. Implement actual status polling from devices
|
|
||||||
|
|
||||||
## UI Event Connections
|
|
||||||
|
|
||||||
All UI elements are connected to handler methods:
|
|
||||||
|
|
||||||
### Buttons
|
|
||||||
- ThorLabs stage connect/disconnect
|
|
||||||
- COM port refresh and connect
|
|
||||||
- Genesis/Helios settings dialogs
|
|
||||||
- Begin scanning
|
|
||||||
- Advanced oscilloscope settings
|
|
||||||
|
|
||||||
### ComboBoxes
|
|
||||||
- COM port selection
|
|
||||||
- Number of scans
|
|
||||||
- Row spacing
|
|
||||||
- Oscilloscope channel selections
|
|
||||||
|
|
||||||
### Text Inputs
|
|
||||||
- Stage serial number
|
|
||||||
- Scan coordinates (X/Y start, delta)
|
|
||||||
- Trigger voltage
|
|
||||||
- VISA address
|
|
||||||
|
|
||||||
## Development Notes
|
|
||||||
|
|
||||||
### Adding New Hardware
|
|
||||||
1. Create new controller class in `hardware/` directory
|
|
||||||
2. Import and instantiate in `main_window.py`
|
|
||||||
3. Add status update methods
|
|
||||||
4. Connect to UI elements as needed
|
|
||||||
|
|
||||||
### Modifying UI
|
|
||||||
1. Edit `.ui` files with Qt Designer
|
|
||||||
2. UI elements are accessed by their object names
|
|
||||||
3. Connections are made in `_connect_signals()` method
|
|
||||||
|
|
||||||
### Debug Output
|
|
||||||
All stub methods print debug information to console. Look for:
|
|
||||||
- `DEBUG:` - Function calls and state changes
|
|
||||||
- `INFO:` - Successful operations
|
|
||||||
- `WARNING:` - Potential issues
|
|
||||||
- `ERROR:` - Operation failures
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
The application can be run without any hardware connected. All hardware interfaces will simulate proper responses.
|
|
||||||
|
|
||||||
### Test Progress Dialog
|
|
||||||
To test the scan progress dialog with simulated progress:
|
|
||||||
1. Configure scan parameters
|
|
||||||
2. Click "Begin Scan"
|
|
||||||
3. The dialog will show with demo progress animation
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
Copyright (C) 2025 Thomas Ales
|
|
||||||
Licensed under GNU General Public License v2.0
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
nueScan - SRAS Scan Planning and Control Software
|
|
||||||
Entry point for the application
|
|
||||||
|
|
||||||
Copyright (C) 2025 Thomas Ales
|
|
||||||
Licensed under GNU General Public License v2.0
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from PyQt6.QtWidgets import QApplication
|
|
||||||
from main_window import NueScanMainWindow
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Main entry point for nueScan application"""
|
|
||||||
app = QApplication(sys.argv)
|
|
||||||
app.setApplicationName("nueScan")
|
|
||||||
app.setOrganizationName("SRAS")
|
|
||||||
app.setApplicationVersion("0.1.0")
|
|
||||||
|
|
||||||
# Create and show main window
|
|
||||||
main_window = NueScanMainWindow()
|
|
||||||
main_window.show()
|
|
||||||
|
|
||||||
sys.exit(app.exec())
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Dialog controllers for nueScan application
|
|
||||||
"""
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
"""
|
|
||||||
Genesis Laser Settings Dialog
|
|
||||||
Configures Genesis scanning laser parameters
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from PyQt6 import uic
|
|
||||||
from PyQt6.QtWidgets import QDialog
|
|
||||||
|
|
||||||
|
|
||||||
class GenesisDialog(QDialog):
|
|
||||||
"""Dialog for configuring Genesis laser settings"""
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
|
|
||||||
# Load UI file
|
|
||||||
ui_path = os.path.join(
|
|
||||||
os.path.dirname(os.path.dirname(__file__)),
|
|
||||||
'nuescan_genesis_dialog.ui'
|
|
||||||
)
|
|
||||||
uic.loadUi(ui_path, self)
|
|
||||||
|
|
||||||
self.setWindowTitle("Genesis Laser Settings")
|
|
||||||
|
|
||||||
# Initialize with default values
|
|
||||||
self._load_default_settings()
|
|
||||||
|
|
||||||
# Connect signals
|
|
||||||
self._connect_signals()
|
|
||||||
|
|
||||||
def _connect_signals(self):
|
|
||||||
"""Connect dialog signals"""
|
|
||||||
# LineEdit text changed
|
|
||||||
self.le_genesis_power_mw.textChanged.connect(self.on_power_changed)
|
|
||||||
|
|
||||||
# Dialog buttons are auto-connected by Qt Designer
|
|
||||||
|
|
||||||
def _load_default_settings(self):
|
|
||||||
"""Load default Genesis settings"""
|
|
||||||
self.le_genesis_power_mw.setText("100.0") # Default 100mW
|
|
||||||
|
|
||||||
def on_power_changed(self, text):
|
|
||||||
"""Handle scanning power change"""
|
|
||||||
print(f"DEBUG: Genesis power changed to: {text}")
|
|
||||||
|
|
||||||
def get_settings(self):
|
|
||||||
"""
|
|
||||||
Get current Genesis settings as a dictionary
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Genesis laser settings
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
power_mw = float(self.le_genesis_power_mw.text())
|
|
||||||
except ValueError:
|
|
||||||
power_mw = 0.0
|
|
||||||
|
|
||||||
return {
|
|
||||||
'power_mw': power_mw
|
|
||||||
}
|
|
||||||
|
|
||||||
def set_settings(self, settings):
|
|
||||||
"""
|
|
||||||
Set Genesis settings from a dictionary
|
|
||||||
|
|
||||||
Args:
|
|
||||||
settings (dict): Genesis laser settings
|
|
||||||
"""
|
|
||||||
if 'power_mw' in settings:
|
|
||||||
self.le_genesis_power_mw.setText(str(settings['power_mw']))
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
"""
|
|
||||||
Helios Device Settings Dialog
|
|
||||||
Configures Helios laser parameters and COM port
|
|
||||||
|
|
||||||
Copyright (C) 2025 Thomas Ales
|
|
||||||
Licensed under GNU General Public License v2.0
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from PyQt6 import uic
|
|
||||||
from PyQt6.QtWidgets import QDialog, QMessageBox
|
|
||||||
from hardware.helios_driver import HeliosDriver
|
|
||||||
|
|
||||||
|
|
||||||
class HeliosDialog(QDialog):
|
|
||||||
"""Dialog for configuring Helios device settings"""
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
|
|
||||||
# Load UI file
|
|
||||||
ui_path = os.path.join(
|
|
||||||
os.path.dirname(os.path.dirname(__file__)),
|
|
||||||
'nuescan_helios_dialog.ui'
|
|
||||||
)
|
|
||||||
uic.loadUi(ui_path, self)
|
|
||||||
|
|
||||||
self.setWindowTitle("Helios Device Settings")
|
|
||||||
|
|
||||||
# Initialize with default values
|
|
||||||
self._load_default_settings()
|
|
||||||
|
|
||||||
# Connect signals
|
|
||||||
self._connect_signals()
|
|
||||||
|
|
||||||
# Populate COM ports
|
|
||||||
self._populate_com_ports()
|
|
||||||
|
|
||||||
def _connect_signals(self):
|
|
||||||
"""Connect dialog signals"""
|
|
||||||
# ComboBox value changed
|
|
||||||
self.cb_helios_port.currentIndexChanged.connect(self.on_port_changed)
|
|
||||||
|
|
||||||
# LineEdit text changed
|
|
||||||
self.le_helios_frequency.textChanged.connect(self.on_frequency_changed)
|
|
||||||
self.le_helios_current.textChanged.connect(self.on_current_changed)
|
|
||||||
|
|
||||||
# Dialog buttons are auto-connected by Qt Designer
|
|
||||||
|
|
||||||
def _load_default_settings(self):
|
|
||||||
"""Load default Helios settings"""
|
|
||||||
self.le_helios_frequency.setText("10000") # Default 10kHz
|
|
||||||
self.le_helios_current.setText("500") # Default 500mA
|
|
||||||
|
|
||||||
def _populate_com_ports(self):
|
|
||||||
"""Populate available COM ports"""
|
|
||||||
# Get available ports from system
|
|
||||||
ports = HeliosDriver.list_available_ports()
|
|
||||||
|
|
||||||
if ports:
|
|
||||||
self.cb_helios_port.addItems(ports)
|
|
||||||
print(f"DEBUG: Found {len(ports)} available COM ports")
|
|
||||||
else:
|
|
||||||
# No ports found
|
|
||||||
self.cb_helios_port.addItem("No ports found")
|
|
||||||
print("WARNING: No COM ports found")
|
|
||||||
|
|
||||||
def refresh_com_ports(self):
|
|
||||||
"""Refresh the COM port list"""
|
|
||||||
current_port = self.cb_helios_port.currentText()
|
|
||||||
self.cb_helios_port.clear()
|
|
||||||
self._populate_com_ports()
|
|
||||||
|
|
||||||
# Try to restore previous selection
|
|
||||||
index = self.cb_helios_port.findText(current_port)
|
|
||||||
if index >= 0:
|
|
||||||
self.cb_helios_port.setCurrentIndex(index)
|
|
||||||
|
|
||||||
def on_port_changed(self, index):
|
|
||||||
"""Handle COM port selection change"""
|
|
||||||
port = self.cb_helios_port.currentText()
|
|
||||||
print(f"DEBUG: Helios port changed to: {port}")
|
|
||||||
|
|
||||||
def on_frequency_changed(self, text):
|
|
||||||
"""Handle frequency change"""
|
|
||||||
print(f"DEBUG: Helios frequency changed to: {text}")
|
|
||||||
|
|
||||||
def on_current_changed(self, text):
|
|
||||||
"""Handle current change"""
|
|
||||||
print(f"DEBUG: Helios current changed to: {text}")
|
|
||||||
|
|
||||||
def get_settings(self):
|
|
||||||
"""
|
|
||||||
Get current Helios settings as a dictionary
|
|
||||||
|
|
||||||
Validates input ranges before returning.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Helios device settings, or None if validation fails
|
|
||||||
"""
|
|
||||||
# Validate frequency
|
|
||||||
try:
|
|
||||||
frequency_hz = float(self.le_helios_frequency.text())
|
|
||||||
# Convert to period to check valid range (8000-60000 ns)
|
|
||||||
# Valid frequencies: ~16.7 kHz to 125 kHz
|
|
||||||
if frequency_hz < 16666 or frequency_hz > 125000:
|
|
||||||
QMessageBox.warning(
|
|
||||||
self, "Invalid Frequency",
|
|
||||||
f"Frequency must be between 16.7 kHz and 125 kHz\n"
|
|
||||||
f"(Period: 8000-60000 ns)\n\n"
|
|
||||||
f"Entered: {frequency_hz/1000:.1f} kHz"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
except ValueError:
|
|
||||||
QMessageBox.warning(
|
|
||||||
self, "Invalid Frequency",
|
|
||||||
"Please enter a valid frequency value in Hz"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Validate current
|
|
||||||
try:
|
|
||||||
current_ma = float(self.le_helios_current.text())
|
|
||||||
if current_ma < 0 or current_ma > 7000:
|
|
||||||
QMessageBox.warning(
|
|
||||||
self, "Invalid Current",
|
|
||||||
f"Current must be between 0 and 7000 mA\n\n"
|
|
||||||
f"Entered: {current_ma} mA"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
except ValueError:
|
|
||||||
QMessageBox.warning(
|
|
||||||
self, "Invalid Current",
|
|
||||||
"Please enter a valid current value in mA"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Validate COM port selection
|
|
||||||
com_port = self.cb_helios_port.currentText()
|
|
||||||
if not com_port or com_port == "No ports found":
|
|
||||||
QMessageBox.warning(
|
|
||||||
self, "No Port Selected",
|
|
||||||
"Please select a valid COM port"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
return {
|
|
||||||
'com_port': com_port,
|
|
||||||
'frequency_hz': frequency_hz,
|
|
||||||
'current_ma': current_ma
|
|
||||||
}
|
|
||||||
|
|
||||||
def set_settings(self, settings):
|
|
||||||
"""
|
|
||||||
Set Helios settings from a dictionary
|
|
||||||
|
|
||||||
Args:
|
|
||||||
settings (dict): Helios device settings
|
|
||||||
"""
|
|
||||||
if 'com_port' in settings:
|
|
||||||
index = self.cb_helios_port.findText(settings['com_port'])
|
|
||||||
if index >= 0:
|
|
||||||
self.cb_helios_port.setCurrentIndex(index)
|
|
||||||
|
|
||||||
if 'frequency_hz' in settings:
|
|
||||||
self.le_helios_frequency.setText(str(settings['frequency_hz']))
|
|
||||||
|
|
||||||
if 'current_ma' in settings:
|
|
||||||
self.le_helios_current.setText(str(settings['current_ma']))
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
"""
|
|
||||||
nueScan - Oscilloscope Dialog Controller
|
|
||||||
Handles all UI interactions for the oscilloscope dialog
|
|
||||||
|
|
||||||
Copyright (C) 2025 Thomas Ales
|
|
||||||
Licensed under GNU General Public License v2.0
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from PyQt6 import uic
|
|
||||||
from PyQt6.QtWidgets import QDialog, QMessageBox
|
|
||||||
|
|
||||||
|
|
||||||
class OscopeDialog(QDialog):
|
|
||||||
"""Oscope dialog for nueScan application"""
|
|
||||||
|
|
||||||
def __init__(self, parent, microscope):
|
|
||||||
super().__init__(parent)
|
|
||||||
|
|
||||||
# Store hardware controllers
|
|
||||||
self.microscope = microscope
|
|
||||||
|
|
||||||
# Load UI file
|
|
||||||
ui_path = os.path.join(os.path.dirname(__file__), '..', 'nuescan_oscope_dialog.ui')
|
|
||||||
uic.loadUi(ui_path, self)
|
|
||||||
|
|
||||||
# Set window title
|
|
||||||
self.setWindowTitle("nueScan - Oscilloscope Settings")
|
|
||||||
|
|
||||||
# Connect all UI signals
|
|
||||||
self._connect_signals()
|
|
||||||
|
|
||||||
# Initialize UI state
|
|
||||||
self._initialize_ui()
|
|
||||||
|
|
||||||
def _connect_signals(self):
|
|
||||||
"""Connect all UI signals to handler methods"""
|
|
||||||
# ===== ComboBox Value Changed Handlers =====
|
|
||||||
self.cb_set_trig_channel.currentIndexChanged.connect(self.on_trigger_channel_changed)
|
|
||||||
self.cb_set_saw_channel.currentIndexChanged.connect(self.on_saw_channel_changed)
|
|
||||||
self.cb_set_bias_a_ch.currentIndexChanged.connect(self.on_bias_a_channel_changed)
|
|
||||||
self.cb_set_bias_b_ch.currentIndexChanged.connect(self.on_bias_b_channel_changed)
|
|
||||||
|
|
||||||
# ===== LineEdit Text Changed Handlers =====
|
|
||||||
self.le_set_trigger_voltage.textChanged.connect(self.on_trigger_voltage_changed)
|
|
||||||
self.le_set_sample_thresh_voltage.textChanged.connect(self.on_sample_thresh_voltage_changed)
|
|
||||||
self.le_set_pd_trig_voltage.textChanged.connect(self.on_pd_trig_voltage_changed)
|
|
||||||
self.le_oscope_visa_address.textChanged.connect(self.on_oscope_visa_address_changed)
|
|
||||||
|
|
||||||
|
|
||||||
# ===== Button Click Handlers =====
|
|
||||||
self.btn_test_scope_connection.clicked.connect(self.on_test_scope_connection_clicked)
|
|
||||||
self.btn_save_scope_settings.clicked.connect(self.on_save_scope_settings_clicked)
|
|
||||||
self.btn_cancel_scope_settings.clicked.connect(self.on_cancel_scope_settings_clicked)
|
|
||||||
|
|
||||||
def _initialize_ui(self):
|
|
||||||
"""Initialize UI with default values"""
|
|
||||||
# Populate combo boxes with dummy data
|
|
||||||
self._populate_combo_boxes()
|
|
||||||
|
|
||||||
def _populate_combo_boxes(self):
|
|
||||||
"""Populate all combo boxes with initial values"""
|
|
||||||
# Oscilloscope channels
|
|
||||||
channels = ["CH1", "CH2", "CH3", "CH4"]
|
|
||||||
self.cb_set_trig_channel.addItems(channels)
|
|
||||||
self.cb_set_saw_channel.addItems(channels)
|
|
||||||
self.cb_set_bias_a_ch.addItems(channels)
|
|
||||||
self.cb_set_bias_b_ch.addItems(channels)
|
|
||||||
|
|
||||||
|
|
||||||
# ==================== ComboBox Change Handlers ====================
|
|
||||||
|
|
||||||
def on_trigger_channel_changed(self, index):
|
|
||||||
"""Handle Phototrigger channel change"""
|
|
||||||
channel = self.cb_set_trig_channel.currentText()
|
|
||||||
print(f"DEBUG: Phototrigger channel changed to: {channel}")
|
|
||||||
|
|
||||||
def on_bias_a_channel_changed(self, index):
|
|
||||||
"""Handle Bias A channel change"""
|
|
||||||
channel = self.cb_set_bias_a_ch.currentText()
|
|
||||||
print(f"DEBUG: Bias A channel changed to: {channel}")
|
|
||||||
|
|
||||||
def on_bias_b_channel_changed(self, index):
|
|
||||||
"""Handle Bias B channel change"""
|
|
||||||
channel = self.cb_set_bias_b_ch.currentText()
|
|
||||||
print(f"DEBUG: Bias B channel changed to: {channel}")
|
|
||||||
|
|
||||||
def on_saw_channel_changed(self, index):
|
|
||||||
"""Handle RF/SAW channel change"""
|
|
||||||
channel = self.cb_set_saw_channel.currentText()
|
|
||||||
print(f"DEBUG: RF/SAW channel changed to: {channel}")
|
|
||||||
|
|
||||||
|
|
||||||
# ==================== LineEdit Text Change Handlers ====================
|
|
||||||
|
|
||||||
def on_pd_trig_voltage_changed(self, text):
|
|
||||||
"""Handle PD Trigger voltage change"""
|
|
||||||
print(f"DEBUG: PD Trigger voltage changed to: {text}")
|
|
||||||
|
|
||||||
def on_trigger_voltage_changed(self, text):
|
|
||||||
"""Handle Sample Min Bias voltage change"""
|
|
||||||
print(f"DEBUG: Sample Min Bias voltage changed to: {text}")
|
|
||||||
|
|
||||||
def on_sample_thresh_voltage_changed(self, text):
|
|
||||||
"""Handle Sample Min Bias voltage change (placeholder)"""
|
|
||||||
print(f"DEBUG: Sample threshold voltage changed to: {text}")
|
|
||||||
|
|
||||||
def on_oscope_visa_address_changed(self, text):
|
|
||||||
"""Handle oscilloscope VISA address change"""
|
|
||||||
print(f"DEBUG: Oscilloscope VISA address changed to: {text}")
|
|
||||||
|
|
||||||
# ==================== Button Click Handlers ====================
|
|
||||||
|
|
||||||
def on_test_scope_connection_clicked(self):
|
|
||||||
"""Test the oscilloscope connection"""
|
|
||||||
print("DEBUG: Test oscope connection clicked")
|
|
||||||
|
|
||||||
def on_save_scope_settings_clicked(self):
|
|
||||||
"""Save the oscilloscope settings"""
|
|
||||||
print("DEBUG: Save oscope settings clicked")
|
|
||||||
self.accept()
|
|
||||||
|
|
||||||
def on_cancel_scope_settings_clicked(self):
|
|
||||||
"""Cancel the oscilloscope settings changes"""
|
|
||||||
print("DEBUG: Cancel oscope settings clicked")
|
|
||||||
self.reject()
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
"""
|
|
||||||
Scan Active Dialog
|
|
||||||
Displays real-time scanning progress and status
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from PyQt6 import uic
|
|
||||||
from PyQt6.QtWidgets import QDialog
|
|
||||||
from PyQt6.QtCore import QTimer
|
|
||||||
|
|
||||||
|
|
||||||
class ScanActiveDialog(QDialog):
|
|
||||||
"""Dialog for displaying active scan progress"""
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
|
|
||||||
# Load UI file
|
|
||||||
ui_path = os.path.join(
|
|
||||||
os.path.dirname(os.path.dirname(__file__)),
|
|
||||||
'nuescan_scan_active_dialog.ui'
|
|
||||||
)
|
|
||||||
uic.loadUi(ui_path, self)
|
|
||||||
|
|
||||||
self.setWindowTitle("Scan in Progress")
|
|
||||||
|
|
||||||
# Make dialog modal
|
|
||||||
self.setModal(True)
|
|
||||||
|
|
||||||
# Initialize state
|
|
||||||
self.scan_cancelled = False
|
|
||||||
|
|
||||||
# Connect signals
|
|
||||||
self._connect_signals()
|
|
||||||
|
|
||||||
# Initialize progress
|
|
||||||
self._initialize_progress()
|
|
||||||
|
|
||||||
# Demo timer (for testing progress updates)
|
|
||||||
self._demo_timer = QTimer()
|
|
||||||
self._demo_timer.timeout.connect(self._demo_update)
|
|
||||||
self._demo_progress = 0
|
|
||||||
|
|
||||||
def _connect_signals(self):
|
|
||||||
"""Connect dialog signals"""
|
|
||||||
self.pb_cancel_scan.clicked.connect(self.on_cancel_clicked)
|
|
||||||
|
|
||||||
def _initialize_progress(self):
|
|
||||||
"""Initialize progress bars and status"""
|
|
||||||
self.pbar_total_scan.setValue(0)
|
|
||||||
self.pbar_this_scan.setValue(0)
|
|
||||||
self.l_status_current_scan.setText("1")
|
|
||||||
self.l_status_total_scans.setText("1")
|
|
||||||
self.l_status_current_row.setText("0")
|
|
||||||
self.l_status_total_rows.setText("0")
|
|
||||||
self.l_est_time_done.setText("Calculating...")
|
|
||||||
|
|
||||||
def on_cancel_clicked(self):
|
|
||||||
"""Handle cancel button click"""
|
|
||||||
print("DEBUG: Scan cancelled by user")
|
|
||||||
self.scan_cancelled = True
|
|
||||||
self.reject()
|
|
||||||
|
|
||||||
# ==================== Progress Update Methods ====================
|
|
||||||
|
|
||||||
def update_total_progress(self, current, total):
|
|
||||||
"""
|
|
||||||
Update the total scan progress bar
|
|
||||||
|
|
||||||
Args:
|
|
||||||
current (int): Current scan number
|
|
||||||
total (int): Total number of scans
|
|
||||||
"""
|
|
||||||
if total > 0:
|
|
||||||
percentage = int((current / total) * 100)
|
|
||||||
self.pbar_total_scan.setValue(percentage)
|
|
||||||
|
|
||||||
def update_current_scan_progress(self, current, total):
|
|
||||||
"""
|
|
||||||
Update the current scan progress bar
|
|
||||||
|
|
||||||
Args:
|
|
||||||
current (int): Current row number
|
|
||||||
total (int): Total number of rows
|
|
||||||
"""
|
|
||||||
if total > 0:
|
|
||||||
percentage = int((current / total) * 100)
|
|
||||||
self.pbar_this_scan.setValue(percentage)
|
|
||||||
|
|
||||||
def update_status(self, scan_num, total_scans, row_num, total_rows, time_remaining):
|
|
||||||
"""
|
|
||||||
Update scan status information
|
|
||||||
|
|
||||||
Args:
|
|
||||||
scan_num (int): Current scan number
|
|
||||||
total_scans (int): Total number of scans
|
|
||||||
row_num (int): Current row number
|
|
||||||
total_rows (int): Total number of rows
|
|
||||||
time_remaining (str): Estimated time remaining (formatted string)
|
|
||||||
"""
|
|
||||||
self.l_status_current_scan.setText(str(scan_num))
|
|
||||||
self.l_status_total_scans.setText(str(total_scans))
|
|
||||||
self.l_status_current_row.setText(str(row_num))
|
|
||||||
self.l_status_total_rows.setText(str(total_rows))
|
|
||||||
self.l_est_time_done.setText(f"{time_remaining} remaining...")
|
|
||||||
|
|
||||||
def start_demo_progress(self):
|
|
||||||
"""
|
|
||||||
Start a demo progress animation (for testing)
|
|
||||||
Remove this method in production
|
|
||||||
"""
|
|
||||||
self._demo_progress = 0
|
|
||||||
self._demo_timer.start(100) # Update every 100ms
|
|
||||||
|
|
||||||
def _demo_update(self):
|
|
||||||
"""
|
|
||||||
Demo progress update (for testing)
|
|
||||||
Remove this method in production
|
|
||||||
"""
|
|
||||||
self._demo_progress += 1
|
|
||||||
|
|
||||||
# Simulate scan progress
|
|
||||||
total_scans = 5
|
|
||||||
rows_per_scan = 100
|
|
||||||
total_steps = total_scans * rows_per_scan
|
|
||||||
|
|
||||||
current_scan = (self._demo_progress // rows_per_scan) + 1
|
|
||||||
current_row = (self._demo_progress % rows_per_scan)
|
|
||||||
|
|
||||||
if current_scan > total_scans:
|
|
||||||
self._demo_timer.stop()
|
|
||||||
self.accept()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Update progress
|
|
||||||
self.update_total_progress(current_scan - 1, total_scans)
|
|
||||||
self.update_current_scan_progress(current_row, rows_per_scan)
|
|
||||||
|
|
||||||
# Calculate time remaining (demo)
|
|
||||||
remaining_steps = total_steps - self._demo_progress
|
|
||||||
seconds_remaining = remaining_steps * 0.1 # 0.1s per step
|
|
||||||
hours = int(seconds_remaining // 3600)
|
|
||||||
minutes = int((seconds_remaining % 3600) // 60)
|
|
||||||
seconds = int(seconds_remaining % 60)
|
|
||||||
|
|
||||||
time_str = f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
|
||||||
|
|
||||||
self.update_status(
|
|
||||||
current_scan,
|
|
||||||
total_scans,
|
|
||||||
current_row,
|
|
||||||
rows_per_scan,
|
|
||||||
time_str
|
|
||||||
)
|
|
||||||
|
|
||||||
def is_cancelled(self):
|
|
||||||
"""
|
|
||||||
Check if scan was cancelled
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if cancelled, False otherwise
|
|
||||||
"""
|
|
||||||
return self.scan_cancelled
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
"""
|
|
||||||
nueScan - Status Dialog Controller
|
|
||||||
Handles all UI interactions for the status dialog
|
|
||||||
|
|
||||||
Copyright (C) 2025 Thomas Ales
|
|
||||||
Licensed under GNU General Public License v2.0
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from PyQt6 import uic
|
|
||||||
from PyQt6.QtWidgets import QDialog
|
|
||||||
|
|
||||||
|
|
||||||
class StatusDialog(QDialog):
|
|
||||||
"""Status dialog for nueScan application"""
|
|
||||||
|
|
||||||
def __init__(self, parent, thorlabs_stage, t3r_device, microscope):
|
|
||||||
super().__init__(parent)
|
|
||||||
|
|
||||||
# Store hardware controllers
|
|
||||||
self.thorlabs_stage = thorlabs_stage
|
|
||||||
self.t3r_device = t3r_device
|
|
||||||
self.microscope = microscope
|
|
||||||
|
|
||||||
# Load UI file
|
|
||||||
ui_path = os.path.join(os.path.dirname(__file__), '..', 'nuescan_status_dialog.ui')
|
|
||||||
uic.loadUi(ui_path, self)
|
|
||||||
|
|
||||||
# Set window title
|
|
||||||
self.setWindowTitle("nueScan - Status Indicators")
|
|
||||||
|
|
||||||
def update_all_status(self):
|
|
||||||
"""Update all status labels with current hardware states"""
|
|
||||||
self._update_stage_status()
|
|
||||||
self._update_t3r_status()
|
|
||||||
self._update_microscope_status()
|
|
||||||
self._update_transfer_system_status()
|
|
||||||
|
|
||||||
def _update_stage_status(self):
|
|
||||||
"""Update ThorLabs stage status indicators"""
|
|
||||||
status = self.thorlabs_stage.get_status()
|
|
||||||
|
|
||||||
self.l_is_mls_connected.setText("Yes" if status['connected'] else "No")
|
|
||||||
self.l_is_mls_x_home.setText("Yes" if status['x_homed'] else "No")
|
|
||||||
self.l_is_mls_y_home.setText("Yes" if status['y_homed'] else "No")
|
|
||||||
self.l_is_mls_ready.setText("Yes" if status['ready'] else "No")
|
|
||||||
self.l_is_mls_scanning.setText("Yes" if status['scanning'] else "No")
|
|
||||||
|
|
||||||
def _update_t3r_status(self):
|
|
||||||
"""Update T3R device status indicators"""
|
|
||||||
status = self.t3r_device.get_status()
|
|
||||||
|
|
||||||
self.l_is_t3r_connected.setText("Yes" if status['connected'] else "No")
|
|
||||||
self.l_is_t3r_homed.setText("Yes" if status['homed'] else "No")
|
|
||||||
self.l_is_t3r_ready.setText("Yes" if status['ready'] else "No")
|
|
||||||
|
|
||||||
def _update_microscope_status(self):
|
|
||||||
"""Update microscope (Genesis/Helios) status indicators"""
|
|
||||||
status = self.microscope.get_status()
|
|
||||||
|
|
||||||
# Helios status
|
|
||||||
self.l_is_helios_ready.setText("Yes" if status['helios_ready'] else "No")
|
|
||||||
self.l_is_helios_interlocked.setText("Yes" if status['helios_interlocked'] else "No")
|
|
||||||
|
|
||||||
# Genesis status
|
|
||||||
self.l_is_genesis_ready.setText("Yes" if status['genesis_ready'] else "No")
|
|
||||||
self.l_is_genesis_interlocked.setText("Yes" if status['genesis_interlocked'] else "No")
|
|
||||||
|
|
||||||
def _update_transfer_system_status(self):
|
|
||||||
"""Update Robo-met.3D transfer system status indicators"""
|
|
||||||
# Stub implementation - would read from actual I/O
|
|
||||||
# These represent digital I/O states
|
|
||||||
io_states = self._read_transfer_io_states()
|
|
||||||
|
|
||||||
# SRAS outputs
|
|
||||||
self.l_sras_ok.setText("High (1)" if io_states['sras_ok'] else "Low (0)")
|
|
||||||
self.l_sras_ctl.setText("High (1)" if io_states['sras_ctl'] else "Low (0)")
|
|
||||||
self.l_sras_done.setText("High (1)" if io_states['sras_done'] else "Low (0)")
|
|
||||||
self.l_sras_error.setText("High (1)" if io_states['sras_error'] else "Low (0)")
|
|
||||||
|
|
||||||
# R3D inputs
|
|
||||||
self.l_r3d_estop_ok.setText("High (1)" if io_states['r3d_estop'] else "Low (0)")
|
|
||||||
self.l_r3d_rtl.setText("High (1)" if io_states['r3d_ready_to_load'] else "Low (0)")
|
|
||||||
self.l_r3d_rts.setText("High (1)" if io_states['r3d_ready_to_start'] else "Low (0)")
|
|
||||||
self.l_r3d_spare.setText("High (1)" if io_states['r3d_spare'] else "Low (0)")
|
|
||||||
|
|
||||||
def _read_transfer_io_states(self):
|
|
||||||
"""
|
|
||||||
Stub method to read transfer system I/O states
|
|
||||||
In production, this would read from actual hardware I/O
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
'sras_ok': False,
|
|
||||||
'sras_ctl': False,
|
|
||||||
'sras_done': False,
|
|
||||||
'sras_error': False,
|
|
||||||
'r3d_estop': True, # Active low, so True = OK
|
|
||||||
'r3d_ready_to_load': False,
|
|
||||||
'r3d_ready_to_start': False,
|
|
||||||
'r3d_spare': False
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
"""
|
|
||||||
Hardware communication modules for nueScan
|
|
||||||
Handles communication with ThorLabs stage, T3R device, and microscopes
|
|
||||||
"""
|
|
||||||
@@ -1,928 +0,0 @@
|
|||||||
"""
|
|
||||||
ThorLabs BBD203 3-Channel Motor Controller Driver
|
|
||||||
Complete implementation of the APT protocol for BBD203
|
|
||||||
|
|
||||||
Copyright (C) 2025 Thomas Ales
|
|
||||||
Licensed under GNU General Public License v2.0
|
|
||||||
"""
|
|
||||||
|
|
||||||
import serial
|
|
||||||
import serial.tools.list_ports
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
from typing import Dict, List, Optional, Callable, Tuple
|
|
||||||
from queue import Queue, Empty
|
|
||||||
|
|
||||||
from hardware.bbd203_protocol import (
|
|
||||||
APTProtocol, APTMessage, MessageID, StatusBits, TriggerMode, Destination
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BBD203Channel:
|
|
||||||
"""Represents a single channel on the BBD203"""
|
|
||||||
|
|
||||||
def __init__(self, channel_num: int):
|
|
||||||
"""
|
|
||||||
Initialize channel
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel_num: Channel number (1, 2, or 3)
|
|
||||||
"""
|
|
||||||
self.channel_num = channel_num
|
|
||||||
self.enabled = False
|
|
||||||
self.homed = False
|
|
||||||
self.position_mm = 0.0
|
|
||||||
self.encoder_count = 0
|
|
||||||
self.status_bits = 0
|
|
||||||
self.moving = False
|
|
||||||
self.homing = False
|
|
||||||
self.error = False
|
|
||||||
|
|
||||||
def update_from_status(self, position: int, encoder: int, status: int,
|
|
||||||
protocol: APTProtocol):
|
|
||||||
"""Update channel state from status update"""
|
|
||||||
self.position_mm = protocol.apt_to_position(position)
|
|
||||||
self.encoder_count = encoder
|
|
||||||
self.status_bits = status
|
|
||||||
|
|
||||||
# Parse status bits
|
|
||||||
self.homed = bool(status & StatusBits.HOMED)
|
|
||||||
self.homing = bool(status & StatusBits.HOMING)
|
|
||||||
self.enabled = bool(status & StatusBits.MOTOR_ENABLED)
|
|
||||||
self.error = bool(status & StatusBits.MOTION_ERROR)
|
|
||||||
|
|
||||||
# Check if moving
|
|
||||||
self.moving = bool(status & (
|
|
||||||
StatusBits.IN_MOTION_FORWARD |
|
|
||||||
StatusBits.IN_MOTION_REVERSE |
|
|
||||||
StatusBits.JOGGING_FORWARD |
|
|
||||||
StatusBits.JOGGING_REVERSE |
|
|
||||||
StatusBits.HOMING
|
|
||||||
))
|
|
||||||
|
|
||||||
def is_ready(self) -> bool:
|
|
||||||
"""Check if channel is ready for operation"""
|
|
||||||
return self.enabled and self.homed and not self.error
|
|
||||||
|
|
||||||
|
|
||||||
class BBD203Driver:
|
|
||||||
"""
|
|
||||||
Complete driver for ThorLabs BBD203 3-Channel Motor Controller
|
|
||||||
|
|
||||||
Features:
|
|
||||||
- 3 independent motor channels
|
|
||||||
- Binary APT protocol communication
|
|
||||||
- Automatic status updates
|
|
||||||
- Thread-safe operation
|
|
||||||
- Position and velocity control
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, encoder_counts_per_mm: int = 20000, timeout: float = 1.0):
|
|
||||||
"""
|
|
||||||
Initialize BBD203 driver
|
|
||||||
|
|
||||||
Args:
|
|
||||||
encoder_counts_per_mm: Encoder resolution (default: 20000 for MLS203)
|
|
||||||
timeout: Serial communication timeout in seconds
|
|
||||||
"""
|
|
||||||
self.protocol = APTProtocol(encoder_counts_per_mm)
|
|
||||||
self.timeout = timeout
|
|
||||||
|
|
||||||
# Serial connection
|
|
||||||
self._serial: Optional[serial.Serial] = None
|
|
||||||
self._port_name = ""
|
|
||||||
self._connected = False
|
|
||||||
|
|
||||||
# Channels
|
|
||||||
self.channels = {
|
|
||||||
1: BBD203Channel(1),
|
|
||||||
2: BBD203Channel(2),
|
|
||||||
3: BBD203Channel(3)
|
|
||||||
}
|
|
||||||
|
|
||||||
# Communication thread
|
|
||||||
self._rx_thread: Optional[threading.Thread] = None
|
|
||||||
self._stop_thread = threading.Event()
|
|
||||||
self._rx_queue = Queue()
|
|
||||||
|
|
||||||
# Callbacks for asynchronous events
|
|
||||||
self._move_complete_callbacks: Dict[int, List[Callable]] = {1: [], 2: [], 3: []}
|
|
||||||
self._home_complete_callbacks: Dict[int, List[Callable]] = {1: [], 2: [], 3: []}
|
|
||||||
|
|
||||||
# Hardware info
|
|
||||||
self._hw_info = {}
|
|
||||||
|
|
||||||
# ==================== Connection Management ====================
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def list_available_ports() -> List[str]:
|
|
||||||
"""
|
|
||||||
List available serial ports
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list: Available port names
|
|
||||||
"""
|
|
||||||
ports = serial.tools.list_ports.comports()
|
|
||||||
return [port.device for port in ports]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def list_thorlabs_devices() -> List[Dict[str, str]]:
|
|
||||||
"""
|
|
||||||
List all ThorLabs APT devices connected via USB
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list: List of dictionaries containing device information
|
|
||||||
Each dict has: 'serial', 'port', 'description', 'vid', 'pid'
|
|
||||||
"""
|
|
||||||
thorlabs_devices = []
|
|
||||||
ports = serial.tools.list_ports.comports()
|
|
||||||
|
|
||||||
# ThorLabs devices typically use FTDI chips
|
|
||||||
# Common VID/PID combinations:
|
|
||||||
# - FTDI: VID=0x0403, various PIDs
|
|
||||||
thorlabs_vids = [0x0403] # FTDI vendor ID
|
|
||||||
|
|
||||||
for port in ports:
|
|
||||||
# Check if this is a ThorLabs device by VID
|
|
||||||
if port.vid in thorlabs_vids:
|
|
||||||
device_info = {
|
|
||||||
'serial': port.serial_number or 'Unknown',
|
|
||||||
'port': port.device,
|
|
||||||
'description': port.description or 'Unknown',
|
|
||||||
'manufacturer': port.manufacturer or 'Unknown',
|
|
||||||
'vid': f"0x{port.vid:04X}" if port.vid else 'Unknown',
|
|
||||||
'pid': f"0x{port.pid:04X}" if port.pid else 'Unknown'
|
|
||||||
}
|
|
||||||
thorlabs_devices.append(device_info)
|
|
||||||
print(f"DEBUG: Found ThorLabs device - Serial: {device_info['serial']}, "
|
|
||||||
f"Port: {device_info['port']}")
|
|
||||||
|
|
||||||
return thorlabs_devices
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def find_device_by_serial(serial_number: str) -> Optional[str]:
|
|
||||||
"""
|
|
||||||
Find ThorLabs device by serial number and return its port
|
|
||||||
|
|
||||||
Args:
|
|
||||||
serial_number: Device serial number (e.g., '83123456')
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: COM port name if found, None otherwise
|
|
||||||
"""
|
|
||||||
devices = BBD203Driver.list_thorlabs_devices()
|
|
||||||
|
|
||||||
for device in devices:
|
|
||||||
if device['serial'] == serial_number:
|
|
||||||
print(f"INFO: Found device {serial_number} on port {device['port']}")
|
|
||||||
return device['port']
|
|
||||||
|
|
||||||
print(f"WARNING: Device with serial number {serial_number} not found")
|
|
||||||
print(f"Available devices: {[d['serial'] for d in devices]}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def connect_by_serial(self, serial_number: str, baudrate: int = 115200) -> bool:
|
|
||||||
"""
|
|
||||||
Connect to BBD203 controller by serial number (auto-find port)
|
|
||||||
|
|
||||||
This is the preferred connection method - automatically finds the
|
|
||||||
device by serial number over USB, similar to Kinesis library.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
serial_number: Device serial number (e.g., '83123456')
|
|
||||||
baudrate: Baud rate (default: 115200)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if connection successful
|
|
||||||
|
|
||||||
Example:
|
|
||||||
driver.connect_by_serial('83123456')
|
|
||||||
"""
|
|
||||||
# Find device port by serial number
|
|
||||||
port = self.find_device_by_serial(serial_number)
|
|
||||||
|
|
||||||
if port is None:
|
|
||||||
print(f"ERROR: Could not find BBD203 with serial number {serial_number}")
|
|
||||||
print("Available ThorLabs devices:")
|
|
||||||
for device in self.list_thorlabs_devices():
|
|
||||||
print(f" Serial: {device['serial']}, Port: {device['port']}, "
|
|
||||||
f"Description: {device['description']}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Connect using the found port
|
|
||||||
return self.connect(port, baudrate)
|
|
||||||
|
|
||||||
def connect(self, port: str, baudrate: int = 115200) -> bool:
|
|
||||||
"""
|
|
||||||
Connect to BBD203 controller by port name
|
|
||||||
|
|
||||||
Note: It's recommended to use connect_by_serial() instead, which
|
|
||||||
automatically finds the device by serial number.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
port: Serial port name (e.g., 'COM3' or '/dev/ttyUSB0')
|
|
||||||
baudrate: Baud rate (default: 115200)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if connection successful
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
print(f"INFO: Connecting to BBD203 on {port}")
|
|
||||||
|
|
||||||
self._serial = serial.Serial(
|
|
||||||
port=port,
|
|
||||||
baudrate=baudrate,
|
|
||||||
bytesize=serial.EIGHTBITS,
|
|
||||||
parity=serial.PARITY_NONE,
|
|
||||||
stopbits=serial.STOPBITS_ONE,
|
|
||||||
timeout=self.timeout,
|
|
||||||
rtscts=False, # Disable hardware flow control
|
|
||||||
xonxoff=False # Disable software flow control
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set DTR and RTS for ThorLabs FTDI devices
|
|
||||||
# For BBD203, RTS should be LOW to enable communication
|
|
||||||
self._serial.dtr = False
|
|
||||||
self._serial.rts = False
|
|
||||||
|
|
||||||
self._port_name = port
|
|
||||||
self._connected = True
|
|
||||||
|
|
||||||
# Give controller time to initialize after connection
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
# Start receive thread
|
|
||||||
self._stop_thread.clear()
|
|
||||||
self._rx_thread = threading.Thread(target=self._receive_loop, daemon=True)
|
|
||||||
self._rx_thread.start()
|
|
||||||
|
|
||||||
# Initialize controller
|
|
||||||
time.sleep(0.5) # Allow thread to start and controller to be ready
|
|
||||||
|
|
||||||
# Request hardware info
|
|
||||||
self._send_command(self.protocol.cmd_req_hw_info())
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
# Start automatic status updates
|
|
||||||
self._send_command(self.protocol.cmd_start_update_msgs())
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
print(f"INFO: Successfully connected to BBD203 on {port}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
except serial.SerialException as e:
|
|
||||||
print(f"ERROR: Failed to connect to {port}: {e}")
|
|
||||||
self._connected = False
|
|
||||||
return False
|
|
||||||
|
|
||||||
def disconnect(self) -> bool:
|
|
||||||
"""
|
|
||||||
Disconnect from BBD203 controller
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if disconnection successful
|
|
||||||
"""
|
|
||||||
if not self._connected:
|
|
||||||
return True
|
|
||||||
|
|
||||||
try:
|
|
||||||
print("INFO: Disconnecting from BBD203")
|
|
||||||
|
|
||||||
# Stop status updates
|
|
||||||
self._send_command(self.protocol.cmd_stop_update_msgs())
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
# Stop receive thread
|
|
||||||
self._stop_thread.set()
|
|
||||||
if self._rx_thread:
|
|
||||||
self._rx_thread.join(timeout=2.0)
|
|
||||||
|
|
||||||
# Close serial port
|
|
||||||
if self._serial and self._serial.is_open:
|
|
||||||
self._serial.close()
|
|
||||||
|
|
||||||
self._connected = False
|
|
||||||
print("INFO: Disconnected from BBD203")
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"ERROR: Error during disconnect: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_connected(self) -> bool:
|
|
||||||
"""Check if controller is connected"""
|
|
||||||
return self._connected and self._serial and self._serial.is_open
|
|
||||||
|
|
||||||
# ==================== Communication Methods ====================
|
|
||||||
|
|
||||||
def _send_command(self, cmd: bytes) -> bool:
|
|
||||||
"""
|
|
||||||
Send command to controller
|
|
||||||
|
|
||||||
Args:
|
|
||||||
cmd: Command bytes to send
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if send successful
|
|
||||||
"""
|
|
||||||
if not self.is_connected():
|
|
||||||
print("ERROR: Cannot send command - not connected")
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
print(f"DEBUG: Sending {len(cmd)} bytes: {cmd.hex()}")
|
|
||||||
self._serial.write(cmd)
|
|
||||||
self._serial.flush() # Ensure data is sent
|
|
||||||
return True
|
|
||||||
except serial.SerialException as e:
|
|
||||||
print(f"ERROR: Failed to send command: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _receive_loop(self):
|
|
||||||
"""Background thread to receive messages from controller"""
|
|
||||||
buffer = bytearray()
|
|
||||||
print("DEBUG: Receive thread started")
|
|
||||||
|
|
||||||
while not self._stop_thread.is_set():
|
|
||||||
try:
|
|
||||||
if self._serial.in_waiting > 0:
|
|
||||||
data = self._serial.read(self._serial.in_waiting)
|
|
||||||
print(f"DEBUG: Received {len(data)} bytes: {data.hex()}")
|
|
||||||
buffer.extend(data)
|
|
||||||
|
|
||||||
# Process complete messages
|
|
||||||
while len(buffer) >= 6:
|
|
||||||
# Parse header
|
|
||||||
msg_id, data_len, dest, source = APTMessage.parse_header(buffer)
|
|
||||||
|
|
||||||
# Determine total message length
|
|
||||||
if data_len == 0 or data_len > 255:
|
|
||||||
# Header-only message
|
|
||||||
msg_len = 6
|
|
||||||
else:
|
|
||||||
# Message with data
|
|
||||||
msg_len = 6 + data_len
|
|
||||||
|
|
||||||
# Wait for complete message
|
|
||||||
if len(buffer) < msg_len:
|
|
||||||
break
|
|
||||||
|
|
||||||
# Extract message
|
|
||||||
msg = bytes(buffer[:msg_len])
|
|
||||||
buffer = buffer[msg_len:]
|
|
||||||
|
|
||||||
# Process message
|
|
||||||
self._process_message(msg_id, msg)
|
|
||||||
|
|
||||||
else:
|
|
||||||
time.sleep(0.001) # Small delay to prevent busy waiting
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
if not self._stop_thread.is_set():
|
|
||||||
print(f"ERROR: Exception in receive loop: {e}")
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
def _process_message(self, msg_id: int, msg: bytes):
|
|
||||||
"""
|
|
||||||
Process received message
|
|
||||||
|
|
||||||
Args:
|
|
||||||
msg_id: Message ID
|
|
||||||
msg: Complete message bytes
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
print(f"DEBUG: Processing message ID 0x{msg_id:04X}, len={len(msg)}, data={msg.hex()}")
|
|
||||||
|
|
||||||
if msg_id == MessageID.MGMSG_MOT_GET_STATUSUPDATE:
|
|
||||||
# Status update
|
|
||||||
channel, position, encoder, status = APTMessage.parse_status_update(msg)
|
|
||||||
# Determine which channel this is for (from destination byte)
|
|
||||||
dest = msg[4]
|
|
||||||
channel_num = dest - 0x20 # 0x21->1, 0x22->2, 0x23->3
|
|
||||||
|
|
||||||
if channel_num in self.channels:
|
|
||||||
self.channels[channel_num].update_from_status(
|
|
||||||
position, encoder, status, self.protocol
|
|
||||||
)
|
|
||||||
|
|
||||||
elif msg_id == MessageID.MGMSG_MOT_MOVE_COMPLETED:
|
|
||||||
# Move completed
|
|
||||||
dest = msg[4]
|
|
||||||
channel_num = dest - 0x20
|
|
||||||
|
|
||||||
if channel_num in self.channels:
|
|
||||||
self.channels[channel_num].moving = False
|
|
||||||
|
|
||||||
# Call callbacks
|
|
||||||
for callback in self._move_complete_callbacks.get(channel_num, []):
|
|
||||||
callback(channel_num)
|
|
||||||
|
|
||||||
elif msg_id == MessageID.MGMSG_MOT_MOVE_HOMED:
|
|
||||||
# Homing completed
|
|
||||||
dest = msg[4]
|
|
||||||
channel_num = dest - 0x20
|
|
||||||
|
|
||||||
if channel_num in self.channels:
|
|
||||||
self.channels[channel_num].homed = True
|
|
||||||
self.channels[channel_num].homing = False
|
|
||||||
|
|
||||||
# Call callbacks
|
|
||||||
for callback in self._home_complete_callbacks.get(channel_num, []):
|
|
||||||
callback(channel_num)
|
|
||||||
|
|
||||||
elif msg_id == MessageID.MGMSG_MOT_MOVE_STOPPED:
|
|
||||||
# Motion stopped
|
|
||||||
dest = msg[4]
|
|
||||||
channel_num = dest - 0x20
|
|
||||||
|
|
||||||
if channel_num in self.channels:
|
|
||||||
self.channels[channel_num].moving = False
|
|
||||||
|
|
||||||
elif msg_id == MessageID.MGMSG_MOD_GET_CHANENABLESTATE:
|
|
||||||
# Channel enable state
|
|
||||||
channel, enabled = APTMessage.parse_channel_enable_state(msg)
|
|
||||||
print(f"DEBUG: Received CHANENABLESTATE - parsed channel={channel}, enabled={enabled}, msg={msg.hex()}")
|
|
||||||
|
|
||||||
# Use the channel number from the parsed message
|
|
||||||
if channel in self.channels:
|
|
||||||
self.channels[channel].enabled = enabled
|
|
||||||
print(f"DEBUG: Set channel {channel} enabled={enabled}")
|
|
||||||
|
|
||||||
elif msg_id == MessageID.MGMSG_HW_RESPONSE:
|
|
||||||
# Hardware response (error or acknowledgement)
|
|
||||||
print(f"DEBUG: Received HW_RESPONSE: {msg.hex()}")
|
|
||||||
|
|
||||||
elif msg_id == MessageID.MGMSG_HW_GET_INFO:
|
|
||||||
# Hardware info
|
|
||||||
print(f"DEBUG: Received hardware info")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"ERROR: Failed to process message {msg_id:04X}: {e}")
|
|
||||||
|
|
||||||
def _set_and_verify_enable(self, channel: int, enable: bool, retries: int = 3) -> bool:
|
|
||||||
"""
|
|
||||||
Set channel enable state and verify it was set correctly
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1, 2, or 3)
|
|
||||||
enable: True to enable, False to disable
|
|
||||||
retries: Number of retry attempts
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if value was set and verified
|
|
||||||
"""
|
|
||||||
for attempt in range(retries):
|
|
||||||
# Send enable command
|
|
||||||
cmd = self.protocol.cmd_enable_channel(channel, enable)
|
|
||||||
if not self._send_command(cmd):
|
|
||||||
continue
|
|
||||||
|
|
||||||
time.sleep(0.5) # Wait for controller to process
|
|
||||||
|
|
||||||
# Request channel enable state to verify
|
|
||||||
req_cmd = self.protocol.cmd_req_channel_enable_state(channel)
|
|
||||||
self._send_command(req_cmd)
|
|
||||||
time.sleep(0.5) # Wait for response
|
|
||||||
|
|
||||||
# Check if state matches expected
|
|
||||||
if self.channels[channel].enabled == enable:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if attempt < retries - 1:
|
|
||||||
print(f"DEBUG: Enable verification failed for channel {channel}, "
|
|
||||||
f"retrying ({attempt + 1}/{retries})")
|
|
||||||
time.sleep(0.2)
|
|
||||||
|
|
||||||
print(f"ERROR: Failed to set and verify enable state for channel {channel} "
|
|
||||||
f"after {retries} attempts")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# ==================== Channel Control ====================
|
|
||||||
|
|
||||||
def enable_channel(self, channel: int, enable: bool = True) -> bool:
|
|
||||||
"""
|
|
||||||
Enable or disable a motor channel
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1, 2, or 3)
|
|
||||||
enable: True to enable, False to disable
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if command sent successfully
|
|
||||||
"""
|
|
||||||
if channel not in [1, 2, 3]:
|
|
||||||
print(f"ERROR: Invalid channel number: {channel}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
action = "Enabling" if enable else "Disabling"
|
|
||||||
print(f"DEBUG: {action} channel {channel}")
|
|
||||||
|
|
||||||
# Use set and verify to ensure command was processed
|
|
||||||
return self._set_and_verify_enable(channel, enable)
|
|
||||||
|
|
||||||
def identify(self, channel: int) -> bool:
|
|
||||||
"""
|
|
||||||
Flash front panel LEDs to identify controller
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1, 2, or 3)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if command sent successfully
|
|
||||||
"""
|
|
||||||
print(f"DEBUG: Identifying channel {channel}")
|
|
||||||
cmd = self.protocol.cmd_identify(channel)
|
|
||||||
return self._send_command(cmd)
|
|
||||||
|
|
||||||
# ==================== Homing ====================
|
|
||||||
|
|
||||||
def home_channel(self, channel: int, wait: bool = False, timeout: float = 30.0) -> bool:
|
|
||||||
"""
|
|
||||||
Home a motor channel
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1, 2, or 3)
|
|
||||||
wait: If True, block until homing complete
|
|
||||||
timeout: Timeout in seconds if waiting
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if homing initiated (or completed if wait=True)
|
|
||||||
"""
|
|
||||||
if channel not in [1, 2, 3]:
|
|
||||||
print(f"ERROR: Invalid channel number: {channel}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print(f"DEBUG: Homing channel {channel}")
|
|
||||||
|
|
||||||
self.channels[channel].homing = True
|
|
||||||
self.channels[channel].homed = False
|
|
||||||
|
|
||||||
cmd = self.protocol.cmd_move_home(channel)
|
|
||||||
if not self._send_command(cmd):
|
|
||||||
return False
|
|
||||||
|
|
||||||
if wait:
|
|
||||||
# Wait for homing to complete
|
|
||||||
start_time = time.time()
|
|
||||||
while time.time() - start_time < timeout:
|
|
||||||
if self.channels[channel].homed and not self.channels[channel].homing:
|
|
||||||
print(f"INFO: Channel {channel} homing completed")
|
|
||||||
return True
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
print(f"ERROR: Homing timeout for channel {channel}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def home_all_channels(self, wait: bool = False, timeout: float = 30.0) -> bool:
|
|
||||||
"""
|
|
||||||
Home all enabled channels
|
|
||||||
|
|
||||||
Args:
|
|
||||||
wait: If True, block until all homing complete
|
|
||||||
timeout: Timeout in seconds if waiting
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if all homing operations successful
|
|
||||||
"""
|
|
||||||
success = True
|
|
||||||
for channel in [1, 2, 3]:
|
|
||||||
if self.channels[channel].enabled:
|
|
||||||
if not self.home_channel(channel, wait=False):
|
|
||||||
success = False
|
|
||||||
|
|
||||||
if wait:
|
|
||||||
start_time = time.time()
|
|
||||||
while time.time() - start_time < timeout:
|
|
||||||
all_homed = all(
|
|
||||||
ch.homed for ch in self.channels.values() if ch.enabled
|
|
||||||
)
|
|
||||||
if all_homed:
|
|
||||||
print("INFO: All channels homed successfully")
|
|
||||||
return True
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
print("ERROR: Timeout waiting for all channels to home")
|
|
||||||
return False
|
|
||||||
|
|
||||||
return success
|
|
||||||
|
|
||||||
# ==================== Motion Control ====================
|
|
||||||
|
|
||||||
def move_absolute(self, channel: int, position_mm: float,
|
|
||||||
wait: bool = False, timeout: float = 30.0) -> bool:
|
|
||||||
"""
|
|
||||||
Move to absolute position
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1, 2, or 3)
|
|
||||||
position_mm: Target position in mm
|
|
||||||
wait: If True, block until move complete
|
|
||||||
timeout: Timeout in seconds if waiting
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if move initiated (or completed if wait=True)
|
|
||||||
"""
|
|
||||||
if channel not in [1, 2, 3]:
|
|
||||||
print(f"ERROR: Invalid channel number: {channel}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not self.channels[channel].is_ready():
|
|
||||||
print(f"ERROR: Channel {channel} not ready for movement")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print(f"DEBUG: Moving channel {channel} to {position_mm} mm")
|
|
||||||
|
|
||||||
self.channels[channel].moving = True
|
|
||||||
|
|
||||||
cmd = self.protocol.cmd_move_absolute(channel, position_mm)
|
|
||||||
if not self._send_command(cmd):
|
|
||||||
return False
|
|
||||||
|
|
||||||
if wait:
|
|
||||||
# Wait for move to complete
|
|
||||||
start_time = time.time()
|
|
||||||
while time.time() - start_time < timeout:
|
|
||||||
if not self.channels[channel].moving:
|
|
||||||
print(f"INFO: Channel {channel} move completed")
|
|
||||||
return True
|
|
||||||
time.sleep(0.01)
|
|
||||||
|
|
||||||
print(f"ERROR: Move timeout for channel {channel}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def move_relative(self, channel: int, distance_mm: float,
|
|
||||||
wait: bool = False, timeout: float = 30.0) -> bool:
|
|
||||||
"""
|
|
||||||
Move relative distance
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1, 2, or 3)
|
|
||||||
distance_mm: Distance to move in mm (positive or negative)
|
|
||||||
wait: If True, block until move complete
|
|
||||||
timeout: Timeout in seconds if waiting
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if move initiated (or completed if wait=True)
|
|
||||||
"""
|
|
||||||
if channel not in [1, 2, 3]:
|
|
||||||
print(f"ERROR: Invalid channel number: {channel}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not self.channels[channel].is_ready():
|
|
||||||
print(f"ERROR: Channel {channel} not ready for movement")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print(f"DEBUG: Moving channel {channel} by {distance_mm} mm")
|
|
||||||
|
|
||||||
self.channels[channel].moving = True
|
|
||||||
|
|
||||||
cmd = self.protocol.cmd_move_relative(channel, distance_mm)
|
|
||||||
if not self._send_command(cmd):
|
|
||||||
return False
|
|
||||||
|
|
||||||
if wait:
|
|
||||||
# Wait for move to complete
|
|
||||||
start_time = time.time()
|
|
||||||
while time.time() - start_time < timeout:
|
|
||||||
if not self.channels[channel].moving:
|
|
||||||
print(f"INFO: Channel {channel} move completed")
|
|
||||||
return True
|
|
||||||
time.sleep(0.01)
|
|
||||||
|
|
||||||
print(f"ERROR: Move timeout for channel {channel}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def stop(self, channel: int, immediate: bool = True) -> bool:
|
|
||||||
"""
|
|
||||||
Stop motion
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1, 2, or 3), or 0 for all channels
|
|
||||||
immediate: If True, stop immediately; if False, decelerate
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if stop command sent successfully
|
|
||||||
"""
|
|
||||||
if channel == 0:
|
|
||||||
# Stop all channels
|
|
||||||
success = True
|
|
||||||
for ch in [1, 2, 3]:
|
|
||||||
if not self.stop(ch, immediate):
|
|
||||||
success = False
|
|
||||||
return success
|
|
||||||
|
|
||||||
if channel not in [1, 2, 3]:
|
|
||||||
print(f"ERROR: Invalid channel number: {channel}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print(f"DEBUG: Stopping channel {channel}")
|
|
||||||
|
|
||||||
cmd = self.protocol.cmd_move_stop(channel, immediate)
|
|
||||||
return self._send_command(cmd)
|
|
||||||
|
|
||||||
# ==================== Parameter Setting ====================
|
|
||||||
|
|
||||||
def set_velocity_params(self, channel: int, max_vel_mm_s: float,
|
|
||||||
accel_mm_s2: float) -> bool:
|
|
||||||
"""
|
|
||||||
Set velocity and acceleration parameters
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1, 2, or 3)
|
|
||||||
max_vel_mm_s: Maximum velocity in mm/s
|
|
||||||
accel_mm_s2: Acceleration in mm/s²
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if parameters set successfully
|
|
||||||
"""
|
|
||||||
if channel not in [1, 2, 3]:
|
|
||||||
print(f"ERROR: Invalid channel number: {channel}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print(f"DEBUG: Setting velocity params for channel {channel}: "
|
|
||||||
f"vel={max_vel_mm_s} mm/s, accel={accel_mm_s2} mm/s²")
|
|
||||||
|
|
||||||
cmd = self.protocol.cmd_set_velocity_params(channel, max_vel_mm_s, accel_mm_s2)
|
|
||||||
if self._send_command(cmd):
|
|
||||||
time.sleep(0.1) # Wait for controller to process
|
|
||||||
|
|
||||||
# Request status update to confirm parameters were accepted
|
|
||||||
self.request_status_update(channel)
|
|
||||||
time.sleep(0.1) # Wait for status response
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
# ==================== Status and Position ====================
|
|
||||||
|
|
||||||
def get_position(self, channel: int) -> Optional[float]:
|
|
||||||
"""
|
|
||||||
Get current position of channel
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1, 2, or 3)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: Current position in mm, or None if unavailable
|
|
||||||
"""
|
|
||||||
if channel not in [1, 2, 3]:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return self.channels[channel].position_mm
|
|
||||||
|
|
||||||
def get_channel_status(self, channel: int) -> Optional[Dict]:
|
|
||||||
"""
|
|
||||||
Get detailed status of channel
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1, 2, or 3)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Channel status dictionary
|
|
||||||
"""
|
|
||||||
if channel not in [1, 2, 3]:
|
|
||||||
return None
|
|
||||||
|
|
||||||
ch = self.channels[channel]
|
|
||||||
return {
|
|
||||||
'channel': channel,
|
|
||||||
'enabled': ch.enabled,
|
|
||||||
'homed': ch.homed,
|
|
||||||
'homing': ch.homing,
|
|
||||||
'moving': ch.moving,
|
|
||||||
'error': ch.error,
|
|
||||||
'ready': ch.is_ready(),
|
|
||||||
'position_mm': ch.position_mm,
|
|
||||||
'encoder_count': ch.encoder_count,
|
|
||||||
'status_bits': ch.status_bits
|
|
||||||
}
|
|
||||||
|
|
||||||
def request_status_update(self, channel: int) -> bool:
|
|
||||||
"""
|
|
||||||
Request immediate status update for channel
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1, 2, or 3)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if request sent successfully
|
|
||||||
"""
|
|
||||||
if channel not in [1, 2, 3]:
|
|
||||||
return False
|
|
||||||
|
|
||||||
cmd = self.protocol.cmd_req_status_update(channel)
|
|
||||||
return self._send_command(cmd)
|
|
||||||
|
|
||||||
# ==================== Callbacks ====================
|
|
||||||
|
|
||||||
def register_move_complete_callback(self, channel: int, callback: Callable):
|
|
||||||
"""Register callback for move complete event"""
|
|
||||||
if channel in [1, 2, 3]:
|
|
||||||
self._move_complete_callbacks[channel].append(callback)
|
|
||||||
|
|
||||||
def register_home_complete_callback(self, channel: int, callback: Callable):
|
|
||||||
"""Register callback for home complete event"""
|
|
||||||
if channel in [1, 2, 3]:
|
|
||||||
self._home_complete_callbacks[channel].append(callback)
|
|
||||||
|
|
||||||
# ==================== Trigger Configuration ====================
|
|
||||||
|
|
||||||
def set_trigger_mode(self, channel: int, mode: int, polarity: int = 0x01,
|
|
||||||
start_pos_fwd: float = 0.0, start_pos_rev: float = 0.0,
|
|
||||||
interval_fwd: float = 0.0, interval_rev: float = 0.0) -> bool:
|
|
||||||
"""
|
|
||||||
Set trigger configuration for a channel
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1, 2, or 3)
|
|
||||||
mode: Trigger mode (TriggerMode enum value)
|
|
||||||
polarity: Trigger polarity (0x01 = active high, 0x02 = active low)
|
|
||||||
start_pos_fwd: Start position for forward trigger (mm)
|
|
||||||
start_pos_rev: Start position for reverse trigger (mm)
|
|
||||||
interval_fwd: Interval for forward trigger (mm)
|
|
||||||
interval_rev: Interval for reverse trigger (mm)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if trigger configuration set successfully
|
|
||||||
|
|
||||||
Example:
|
|
||||||
# Disable trigger
|
|
||||||
driver.set_trigger_mode(1, TriggerMode.DISABLED)
|
|
||||||
|
|
||||||
# Enable trigger output on motion
|
|
||||||
driver.set_trigger_mode(1, TriggerMode.OUT_ONLY)
|
|
||||||
|
|
||||||
# Trigger at specific positions
|
|
||||||
driver.set_trigger_mode(1, TriggerMode.OUT_POSITION,
|
|
||||||
start_pos_fwd=10.0, interval_fwd=1.0)
|
|
||||||
"""
|
|
||||||
if channel not in [1, 2, 3]:
|
|
||||||
print(f"ERROR: Invalid channel number: {channel}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print(f"DEBUG: Setting trigger mode for channel {channel}: mode={mode}")
|
|
||||||
|
|
||||||
cmd = self.protocol.cmd_set_trigger(
|
|
||||||
channel, mode, polarity, start_pos_fwd, start_pos_rev,
|
|
||||||
interval_fwd, interval_rev
|
|
||||||
)
|
|
||||||
|
|
||||||
if self._send_command(cmd):
|
|
||||||
time.sleep(0.1) # Wait for controller to process
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_trigger_config(self, channel: int) -> Optional[Dict]:
|
|
||||||
"""
|
|
||||||
Get current trigger configuration for a channel
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1, 2, or 3)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Trigger configuration or None if unavailable
|
|
||||||
"""
|
|
||||||
if channel not in [1, 2, 3]:
|
|
||||||
return None
|
|
||||||
|
|
||||||
cmd = self.protocol.cmd_req_trigger(channel)
|
|
||||||
if not self._send_command(cmd):
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Note: In a complete implementation, would wait for response
|
|
||||||
# For now, returning None as response handling would need queue
|
|
||||||
print("WARNING: get_trigger_config not fully implemented (requires response queue)")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# ==================== Digital I/O ====================
|
|
||||||
|
|
||||||
def set_digital_outputs(self, output_bits: int) -> bool:
|
|
||||||
"""
|
|
||||||
Set digital output states
|
|
||||||
|
|
||||||
Args:
|
|
||||||
output_bits: Bit pattern for outputs (0x00 to 0xFF)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if digital outputs set successfully
|
|
||||||
|
|
||||||
Note:
|
|
||||||
Digital outputs share pins with trigger outputs. Ensure
|
|
||||||
trigger mode is disabled before using digital outputs.
|
|
||||||
"""
|
|
||||||
if not (0 <= output_bits <= 0xFF):
|
|
||||||
print(f"ERROR: Invalid output bits: {output_bits}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print(f"DEBUG: Setting digital outputs: 0x{output_bits:02X}")
|
|
||||||
|
|
||||||
cmd = self.protocol.cmd_set_digital_outputs(output_bits)
|
|
||||||
if self._send_command(cmd):
|
|
||||||
time.sleep(0.05)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
@@ -1,532 +0,0 @@
|
|||||||
"""
|
|
||||||
ThorLabs BBD203 APT Protocol Handler
|
|
||||||
Binary message protocol for BBD203 motor controller
|
|
||||||
|
|
||||||
Copyright (C) 2025 Thomas Ales
|
|
||||||
Licensed under GNU General Public License v2.0
|
|
||||||
"""
|
|
||||||
|
|
||||||
import struct
|
|
||||||
from enum import IntEnum
|
|
||||||
from typing import Tuple, Optional, List
|
|
||||||
|
|
||||||
|
|
||||||
# Message IDs
|
|
||||||
class MessageID(IntEnum):
|
|
||||||
"""APT Protocol Message IDs for BBD203"""
|
|
||||||
|
|
||||||
# Module Control
|
|
||||||
MGMSG_MOD_IDENTIFY = 0x0223
|
|
||||||
MGMSG_MOD_SET_CHANENABLESTATE = 0x0210
|
|
||||||
MGMSG_MOD_REQ_CHANENABLESTATE = 0x0211
|
|
||||||
MGMSG_MOD_GET_CHANENABLESTATE = 0x0212
|
|
||||||
|
|
||||||
# Hardware Control
|
|
||||||
MGMSG_HW_DISCONNECT = 0x0002
|
|
||||||
MGMSG_HW_RESPONSE = 0x0080
|
|
||||||
MGMSG_HW_RICHRESPONSE = 0x0081
|
|
||||||
MGMSG_HW_START_UPDATEMSGS = 0x0011
|
|
||||||
MGMSG_HW_STOP_UPDATEMSGS = 0x0012
|
|
||||||
MGMSG_HW_REQ_INFO = 0x0005
|
|
||||||
MGMSG_HW_GET_INFO = 0x0006
|
|
||||||
|
|
||||||
# Motor Control - Basic
|
|
||||||
MGMSG_MOT_SET_POSCOUNTER = 0x0410
|
|
||||||
MGMSG_MOT_REQ_POSCOUNTER = 0x0411
|
|
||||||
MGMSG_MOT_GET_POSCOUNTER = 0x0412
|
|
||||||
MGMSG_MOT_SET_ENCCOUNTER = 0x0409
|
|
||||||
MGMSG_MOT_REQ_ENCCOUNTER = 0x040A
|
|
||||||
MGMSG_MOT_GET_ENCCOUNTER = 0x040B
|
|
||||||
|
|
||||||
# Motor Control - Homing
|
|
||||||
MGMSG_MOT_SET_HOMEPARAMS = 0x0440
|
|
||||||
MGMSG_MOT_REQ_HOMEPARAMS = 0x0441
|
|
||||||
MGMSG_MOT_GET_HOMEPARAMS = 0x0442
|
|
||||||
MGMSG_MOT_MOVE_HOME = 0x0443
|
|
||||||
MGMSG_MOT_MOVE_HOMED = 0x0444
|
|
||||||
|
|
||||||
# Motor Control - Movement
|
|
||||||
MGMSG_MOT_SET_MOVERELPARAMS = 0x0445
|
|
||||||
MGMSG_MOT_REQ_MOVERELPARAMS = 0x0446
|
|
||||||
MGMSG_MOT_GET_MOVERELPARAMS = 0x0447
|
|
||||||
MGMSG_MOT_MOVE_RELATIVE = 0x0448
|
|
||||||
MGMSG_MOT_SET_MOVEABSPARAMS = 0x0450
|
|
||||||
MGMSG_MOT_REQ_MOVEABSPARAMS = 0x0451
|
|
||||||
MGMSG_MOT_GET_MOVEABSPARAMS = 0x0452
|
|
||||||
MGMSG_MOT_MOVE_ABSOLUTE = 0x0453
|
|
||||||
MGMSG_MOT_MOVE_COMPLETED = 0x0464
|
|
||||||
MGMSG_MOT_MOVE_VELOCITY = 0x0457
|
|
||||||
MGMSG_MOT_MOVE_STOP = 0x0465
|
|
||||||
MGMSG_MOT_MOVE_STOPPED = 0x0466
|
|
||||||
|
|
||||||
# Motor Control - Velocity
|
|
||||||
MGMSG_MOT_SET_VELPARAMS = 0x0413
|
|
||||||
MGMSG_MOT_REQ_VELPARAMS = 0x0414
|
|
||||||
MGMSG_MOT_GET_VELPARAMS = 0x0415
|
|
||||||
|
|
||||||
# Motor Control - Status
|
|
||||||
MGMSG_MOT_REQ_STATUSUPDATE = 0x0480
|
|
||||||
MGMSG_MOT_GET_STATUSUPDATE = 0x0481
|
|
||||||
MGMSG_MOT_REQ_STATUSBITS = 0x0429
|
|
||||||
MGMSG_MOT_GET_STATUSBITS = 0x042A
|
|
||||||
|
|
||||||
# Digital I/O and Trigger
|
|
||||||
MGMSG_RACK_SET_DIGOUTPUTS = 0x0228
|
|
||||||
MGMSG_RACK_REQ_DIGOUTPUTS = 0x0229
|
|
||||||
MGMSG_RACK_GET_DIGOUTPUTS = 0x0230
|
|
||||||
MGMSG_MOT_SET_TRIGGER = 0x0500
|
|
||||||
MGMSG_MOT_REQ_TRIGGER = 0x0501
|
|
||||||
MGMSG_MOT_GET_TRIGGER = 0x0502
|
|
||||||
|
|
||||||
|
|
||||||
# Destination addresses
|
|
||||||
class Destination(IntEnum):
|
|
||||||
"""BBD203 Destination addresses"""
|
|
||||||
USB = 0x50
|
|
||||||
ALL_CHANNELS = 0x11
|
|
||||||
CHANNEL_1 = 0x21
|
|
||||||
CHANNEL_2 = 0x22
|
|
||||||
CHANNEL_3 = 0x23
|
|
||||||
|
|
||||||
|
|
||||||
# Source addresses
|
|
||||||
class Source(IntEnum):
|
|
||||||
"""Source addresses"""
|
|
||||||
HOST = 0x01
|
|
||||||
|
|
||||||
|
|
||||||
# Status bits
|
|
||||||
class StatusBits(IntEnum):
|
|
||||||
"""Motor status bit definitions"""
|
|
||||||
HOMING = 0x00000200
|
|
||||||
HOMED = 0x00000400
|
|
||||||
TRACKING = 0x00001000
|
|
||||||
SETTLED = 0x00002000
|
|
||||||
MOTION_ERROR = 0x00004000
|
|
||||||
MOTOR_ENABLED = 0x80000000
|
|
||||||
FORWARD_LIMIT = 0x00000001
|
|
||||||
REVERSE_LIMIT = 0x00000002
|
|
||||||
IN_MOTION_FORWARD = 0x00000010
|
|
||||||
IN_MOTION_REVERSE = 0x00000020
|
|
||||||
JOGGING_FORWARD = 0x00000040
|
|
||||||
JOGGING_REVERSE = 0x00000080
|
|
||||||
|
|
||||||
|
|
||||||
# Trigger modes
|
|
||||||
class TriggerMode(IntEnum):
|
|
||||||
"""Trigger mode definitions"""
|
|
||||||
DISABLED = 0x00
|
|
||||||
IN_OUT_RELATIVE_MOVE = 0x01
|
|
||||||
IN_OUT_ABSOLUTE_MOVE = 0x02
|
|
||||||
IN_OUT_HOME = 0x03
|
|
||||||
IN_OUT_STOP = 0x04
|
|
||||||
OUT_ONLY = 0x10
|
|
||||||
OUT_POSITION = 0x11
|
|
||||||
|
|
||||||
|
|
||||||
class APTMessage:
|
|
||||||
"""
|
|
||||||
APT Protocol Message Builder and Parser
|
|
||||||
Handles construction and parsing of binary APT messages
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_header_only(msg_id: int, param1: int, param2: int,
|
|
||||||
dest: int, source: int = Source.HOST) -> bytes:
|
|
||||||
"""
|
|
||||||
Build a 6-byte header-only message
|
|
||||||
|
|
||||||
Args:
|
|
||||||
msg_id: Message ID (16-bit)
|
|
||||||
param1: Parameter 1 (8-bit)
|
|
||||||
param2: Parameter 2 (8-bit)
|
|
||||||
dest: Destination address
|
|
||||||
source: Source address (default: HOST)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bytes: 6-byte message
|
|
||||||
"""
|
|
||||||
return struct.pack('<HBBBB', msg_id, param1, param2, dest, source)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_with_data(msg_id: int, dest: int, data: bytes,
|
|
||||||
source: int = Source.HOST) -> bytes:
|
|
||||||
"""
|
|
||||||
Build a message with data packet
|
|
||||||
|
|
||||||
Args:
|
|
||||||
msg_id: Message ID (16-bit)
|
|
||||||
dest: Destination address
|
|
||||||
data: Data packet bytes
|
|
||||||
source: Source address (default: HOST)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bytes: Complete message (header + data)
|
|
||||||
"""
|
|
||||||
data_len = len(data)
|
|
||||||
header = struct.pack('<HHBB', msg_id, data_len, dest, source)
|
|
||||||
return header + data
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_header(data: bytes) -> Tuple[int, int, int, int, int]:
|
|
||||||
"""
|
|
||||||
Parse message header
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data: At least 6 bytes of message data
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple: (msg_id, data_len, dest, source, has_data)
|
|
||||||
"""
|
|
||||||
if len(data) < 6:
|
|
||||||
raise ValueError("Insufficient data for header")
|
|
||||||
|
|
||||||
msg_id, byte2, byte3, dest, source = struct.unpack('<HBBBB', data[:6])
|
|
||||||
|
|
||||||
# Determine if this is header-only or has data
|
|
||||||
# Header-only messages use bytes 2-3 as parameters
|
|
||||||
# Messages with data use bytes 2-3 as data length
|
|
||||||
data_len = (byte3 << 8) | byte2
|
|
||||||
|
|
||||||
return msg_id, data_len, dest, source
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_position_counter(data: bytes) -> Tuple[int, int]:
|
|
||||||
"""Parse MGMSG_MOT_GET_POSCOUNTER response"""
|
|
||||||
if len(data) < 12:
|
|
||||||
raise ValueError("Insufficient data for position counter")
|
|
||||||
|
|
||||||
_, channel, position = struct.unpack('<HHI', data[6:12])
|
|
||||||
return channel, position
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_encoder_counter(data: bytes) -> Tuple[int, int]:
|
|
||||||
"""Parse MGMSG_MOT_GET_ENCCOUNTER response"""
|
|
||||||
if len(data) < 12:
|
|
||||||
raise ValueError("Insufficient data for encoder counter")
|
|
||||||
|
|
||||||
_, channel, encoder = struct.unpack('<HHI', data[6:12])
|
|
||||||
return channel, encoder
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_status_update(data: bytes) -> Tuple[int, int, int, int]:
|
|
||||||
"""
|
|
||||||
Parse MGMSG_MOT_GET_STATUSUPDATE response
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple: (channel, position, enc_count, status_bits)
|
|
||||||
"""
|
|
||||||
if len(data) < 20:
|
|
||||||
raise ValueError("Insufficient data for status update")
|
|
||||||
|
|
||||||
# Skip 6-byte header, parse data packet
|
|
||||||
channel, position, enc_count, status = struct.unpack('<HIII', data[6:20])
|
|
||||||
return channel, position, enc_count, status
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_velocity_params(data: bytes) -> Tuple[int, int, int, int]:
|
|
||||||
"""
|
|
||||||
Parse MGMSG_MOT_GET_VELPARAMS response
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple: (channel, min_vel, max_vel, accel)
|
|
||||||
"""
|
|
||||||
if len(data) < 20:
|
|
||||||
raise ValueError("Insufficient data for velocity params")
|
|
||||||
|
|
||||||
channel, min_vel, max_vel, accel = struct.unpack('<HIII', data[6:20])
|
|
||||||
return channel, min_vel, max_vel, accel
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_channel_enable_state(data: bytes) -> Tuple[int, bool]:
|
|
||||||
"""Parse MGMSG_MOD_GET_CHANENABLESTATE response"""
|
|
||||||
if len(data) < 6:
|
|
||||||
raise ValueError("Insufficient data for channel enable state")
|
|
||||||
|
|
||||||
# Header only message, params in bytes 2-3
|
|
||||||
_, enable_state, channel, _, _ = struct.unpack('<HBBBB', data[:6])
|
|
||||||
return channel, (enable_state == 0x01)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_trigger_config(data: bytes) -> Tuple[int, int, int, int, int, int, int]:
|
|
||||||
"""
|
|
||||||
Parse MGMSG_MOT_GET_TRIGGER response
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple: (channel, trigger_mode, polarity, start_pos_fwd, start_pos_rev,
|
|
||||||
interval_fwd, interval_rev, num_pulses, pulse_width, num_cycles)
|
|
||||||
"""
|
|
||||||
if len(data) < 28:
|
|
||||||
raise ValueError("Insufficient data for trigger config")
|
|
||||||
|
|
||||||
# Parse data packet (22 bytes starting at byte 6)
|
|
||||||
channel, mode, polarity, start_fwd, start_rev, interval_fwd, interval_rev = \
|
|
||||||
struct.unpack('<HBBIIIi', data[6:28])
|
|
||||||
|
|
||||||
# Extended parameters if available
|
|
||||||
num_pulses = 0
|
|
||||||
pulse_width = 0
|
|
||||||
num_cycles = 0
|
|
||||||
if len(data) >= 40:
|
|
||||||
num_pulses, pulse_width, num_cycles = struct.unpack('<III', data[28:40])
|
|
||||||
|
|
||||||
return (channel, mode, polarity, start_fwd, start_rev,
|
|
||||||
interval_fwd, interval_rev, num_pulses, pulse_width, num_cycles)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_digital_outputs(data: bytes) -> Tuple[int, int]:
|
|
||||||
"""Parse MGMSG_RACK_GET_DIGOUTPUTS response"""
|
|
||||||
if len(data) < 6:
|
|
||||||
raise ValueError("Insufficient data for digital outputs")
|
|
||||||
|
|
||||||
# Header only message, params in bytes 2-3
|
|
||||||
_, output_state, _, _, _ = struct.unpack('<HBBBB', data[:6])
|
|
||||||
return output_state
|
|
||||||
|
|
||||||
|
|
||||||
class APTProtocol:
|
|
||||||
"""
|
|
||||||
High-level APT Protocol interface for BBD203
|
|
||||||
Provides methods to build common command messages
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Scaling constants
|
|
||||||
T_SAMPLE = 102.4e-6 # Controller sample time
|
|
||||||
VELOCITY_SCALE = int(T_SAMPLE * 65536)
|
|
||||||
ACCEL_SCALE = int((T_SAMPLE ** 2) * 65536)
|
|
||||||
|
|
||||||
def __init__(self, encoder_counts_per_mm: int = 20000):
|
|
||||||
"""
|
|
||||||
Initialize APT Protocol handler
|
|
||||||
|
|
||||||
Args:
|
|
||||||
encoder_counts_per_mm: Encoder resolution (default: 20000 for MLS203)
|
|
||||||
"""
|
|
||||||
self.enc_cnt = encoder_counts_per_mm
|
|
||||||
|
|
||||||
def position_to_apt(self, pos_mm: float) -> int:
|
|
||||||
"""Convert position in mm to APT units"""
|
|
||||||
return int(pos_mm * self.enc_cnt)
|
|
||||||
|
|
||||||
def apt_to_position(self, apt_units: int) -> float:
|
|
||||||
"""Convert APT units to position in mm"""
|
|
||||||
return apt_units / self.enc_cnt
|
|
||||||
|
|
||||||
def velocity_to_apt(self, vel_mm_s: float) -> int:
|
|
||||||
"""Convert velocity in mm/s to APT units"""
|
|
||||||
return int(self.enc_cnt * self.T_SAMPLE * 65536 * vel_mm_s)
|
|
||||||
|
|
||||||
def apt_to_velocity(self, apt_units: int) -> float:
|
|
||||||
"""Convert APT units to velocity in mm/s"""
|
|
||||||
return apt_units / (self.enc_cnt * self.T_SAMPLE * 65536)
|
|
||||||
|
|
||||||
def accel_to_apt(self, accel_mm_s2: float) -> int:
|
|
||||||
"""Convert acceleration in mm/s² to APT units"""
|
|
||||||
return int(self.enc_cnt * (self.T_SAMPLE ** 2) * 65536 * accel_mm_s2)
|
|
||||||
|
|
||||||
def apt_to_accel(self, apt_units: int) -> float:
|
|
||||||
"""Convert APT units to acceleration in mm/s²"""
|
|
||||||
return apt_units / (self.enc_cnt * (self.T_SAMPLE ** 2) * 65536)
|
|
||||||
|
|
||||||
# Command builders
|
|
||||||
|
|
||||||
def cmd_identify(self, channel: int) -> bytes:
|
|
||||||
"""Build identify command (flash LEDs)"""
|
|
||||||
dest = Destination.CHANNEL_1 + (channel - 1)
|
|
||||||
return APTMessage.build_header_only(
|
|
||||||
MessageID.MGMSG_MOD_IDENTIFY, 0x00, 0x00, dest
|
|
||||||
)
|
|
||||||
|
|
||||||
def cmd_enable_channel(self, channel: int, enable: bool = True) -> bytes:
|
|
||||||
"""Build enable/disable channel command"""
|
|
||||||
dest = Destination.CHANNEL_1 + (channel - 1)
|
|
||||||
state = 0x01 if enable else 0x02
|
|
||||||
return APTMessage.build_header_only(
|
|
||||||
MessageID.MGMSG_MOD_SET_CHANENABLESTATE, state, channel, dest
|
|
||||||
)
|
|
||||||
|
|
||||||
def cmd_req_channel_enable_state(self, channel: int) -> bytes:
|
|
||||||
"""Build request channel enable state command"""
|
|
||||||
dest = Destination.CHANNEL_1 + (channel - 1)
|
|
||||||
return APTMessage.build_header_only(
|
|
||||||
MessageID.MGMSG_MOD_REQ_CHANENABLESTATE, 0x01, 0x00, dest
|
|
||||||
)
|
|
||||||
|
|
||||||
def cmd_start_update_msgs(self) -> bytes:
|
|
||||||
"""Build start automatic status updates command"""
|
|
||||||
return APTMessage.build_header_only(
|
|
||||||
MessageID.MGMSG_HW_START_UPDATEMSGS, 0x00, 0x00, Destination.USB
|
|
||||||
)
|
|
||||||
|
|
||||||
def cmd_stop_update_msgs(self) -> bytes:
|
|
||||||
"""Build stop automatic status updates command"""
|
|
||||||
return APTMessage.build_header_only(
|
|
||||||
MessageID.MGMSG_HW_STOP_UPDATEMSGS, 0x00, 0x00, Destination.USB
|
|
||||||
)
|
|
||||||
|
|
||||||
def cmd_req_hw_info(self) -> bytes:
|
|
||||||
"""Build request hardware info command"""
|
|
||||||
return APTMessage.build_header_only(
|
|
||||||
MessageID.MGMSG_HW_REQ_INFO, 0x00, 0x00, Destination.USB
|
|
||||||
)
|
|
||||||
|
|
||||||
def cmd_move_home(self, channel: int) -> bytes:
|
|
||||||
"""Build move home command"""
|
|
||||||
dest = Destination.CHANNEL_1 + (channel - 1)
|
|
||||||
return APTMessage.build_header_only(
|
|
||||||
MessageID.MGMSG_MOT_MOVE_HOME, 0x01, 0x00, dest
|
|
||||||
)
|
|
||||||
|
|
||||||
def cmd_move_absolute(self, channel: int, position_mm: float) -> bytes:
|
|
||||||
"""Build move absolute command"""
|
|
||||||
dest = Destination.CHANNEL_1 + (channel - 1)
|
|
||||||
pos_apt = self.position_to_apt(position_mm)
|
|
||||||
data = struct.pack('<HI', channel, pos_apt)
|
|
||||||
return APTMessage.build_with_data(
|
|
||||||
MessageID.MGMSG_MOT_MOVE_ABSOLUTE, dest, data
|
|
||||||
)
|
|
||||||
|
|
||||||
def cmd_move_relative(self, channel: int, distance_mm: float) -> bytes:
|
|
||||||
"""Build move relative command"""
|
|
||||||
dest = Destination.CHANNEL_1 + (channel - 1)
|
|
||||||
dist_apt = self.position_to_apt(distance_mm)
|
|
||||||
data = struct.pack('<Hi', channel, dist_apt)
|
|
||||||
return APTMessage.build_with_data(
|
|
||||||
MessageID.MGMSG_MOT_MOVE_RELATIVE, dest, data
|
|
||||||
)
|
|
||||||
|
|
||||||
def cmd_move_stop(self, channel: int, immediate: bool = True) -> bytes:
|
|
||||||
"""Build stop motion command"""
|
|
||||||
dest = Destination.CHANNEL_1 + (channel - 1)
|
|
||||||
stop_mode = 0x01 if immediate else 0x02
|
|
||||||
return APTMessage.build_header_only(
|
|
||||||
MessageID.MGMSG_MOT_MOVE_STOP, 0x01, stop_mode, dest
|
|
||||||
)
|
|
||||||
|
|
||||||
def cmd_set_velocity_params(self, channel: int, max_vel_mm_s: float,
|
|
||||||
accel_mm_s2: float) -> bytes:
|
|
||||||
"""Build set velocity parameters command"""
|
|
||||||
dest = Destination.CHANNEL_1 + (channel - 1)
|
|
||||||
max_vel_apt = self.velocity_to_apt(max_vel_mm_s)
|
|
||||||
accel_apt = self.accel_to_apt(accel_mm_s2)
|
|
||||||
|
|
||||||
data = struct.pack('<HIII',
|
|
||||||
channel, # Channel number
|
|
||||||
0, # Min velocity (0)
|
|
||||||
max_vel_apt, # Max velocity
|
|
||||||
accel_apt # Acceleration
|
|
||||||
)
|
|
||||||
return APTMessage.build_with_data(
|
|
||||||
MessageID.MGMSG_MOT_SET_VELPARAMS, dest, data
|
|
||||||
)
|
|
||||||
|
|
||||||
def cmd_req_velocity_params(self, channel: int) -> bytes:
|
|
||||||
"""Build request velocity parameters command"""
|
|
||||||
dest = Destination.CHANNEL_1 + (channel - 1)
|
|
||||||
return APTMessage.build_header_only(
|
|
||||||
MessageID.MGMSG_MOT_REQ_VELPARAMS, 0x01, 0x00, dest
|
|
||||||
)
|
|
||||||
|
|
||||||
def cmd_req_position(self, channel: int) -> bytes:
|
|
||||||
"""Build request position counter command"""
|
|
||||||
dest = Destination.CHANNEL_1 + (channel - 1)
|
|
||||||
return APTMessage.build_header_only(
|
|
||||||
MessageID.MGMSG_MOT_REQ_POSCOUNTER, 0x01, 0x00, dest
|
|
||||||
)
|
|
||||||
|
|
||||||
def cmd_req_encoder(self, channel: int) -> bytes:
|
|
||||||
"""Build request encoder counter command"""
|
|
||||||
dest = Destination.CHANNEL_1 + (channel - 1)
|
|
||||||
return APTMessage.build_header_only(
|
|
||||||
MessageID.MGMSG_MOT_REQ_ENCCOUNTER, 0x01, 0x00, dest
|
|
||||||
)
|
|
||||||
|
|
||||||
def cmd_req_status_update(self, channel: int) -> bytes:
|
|
||||||
"""Build request status update command"""
|
|
||||||
dest = Destination.CHANNEL_1 + (channel - 1)
|
|
||||||
return APTMessage.build_header_only(
|
|
||||||
MessageID.MGMSG_MOT_REQ_STATUSUPDATE, 0x01, 0x00, dest
|
|
||||||
)
|
|
||||||
|
|
||||||
def cmd_req_status_bits(self, channel: int) -> bytes:
|
|
||||||
"""Build request status bits command"""
|
|
||||||
dest = Destination.CHANNEL_1 + (channel - 1)
|
|
||||||
return APTMessage.build_header_only(
|
|
||||||
MessageID.MGMSG_MOT_REQ_STATUSBITS, 0x01, 0x00, dest
|
|
||||||
)
|
|
||||||
|
|
||||||
def cmd_set_position_counter(self, channel: int, position_mm: float) -> bytes:
|
|
||||||
"""Build set position counter command"""
|
|
||||||
dest = Destination.CHANNEL_1 + (channel - 1)
|
|
||||||
pos_apt = self.position_to_apt(position_mm)
|
|
||||||
data = struct.pack('<HI', channel, pos_apt)
|
|
||||||
return APTMessage.build_with_data(
|
|
||||||
MessageID.MGMSG_MOT_SET_POSCOUNTER, dest, data
|
|
||||||
)
|
|
||||||
|
|
||||||
def cmd_set_trigger(self, channel: int, mode: int, polarity: int = 0x01,
|
|
||||||
start_pos_fwd: float = 0.0, start_pos_rev: float = 0.0,
|
|
||||||
interval_fwd: float = 0.0, interval_rev: float = 0.0) -> bytes:
|
|
||||||
"""
|
|
||||||
Build set trigger configuration command
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Channel number (1, 2, or 3)
|
|
||||||
mode: Trigger mode (TriggerMode enum value)
|
|
||||||
polarity: Trigger polarity (0x01 = active high, 0x02 = active low)
|
|
||||||
start_pos_fwd: Start position for forward trigger (mm)
|
|
||||||
start_pos_rev: Start position for reverse trigger (mm)
|
|
||||||
interval_fwd: Interval for forward trigger (mm)
|
|
||||||
interval_rev: Interval for reverse trigger (mm)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bytes: Complete trigger configuration command
|
|
||||||
"""
|
|
||||||
dest = Destination.CHANNEL_1 + (channel - 1)
|
|
||||||
|
|
||||||
# Convert positions to APT units
|
|
||||||
start_fwd_apt = self.position_to_apt(start_pos_fwd)
|
|
||||||
start_rev_apt = self.position_to_apt(start_pos_rev)
|
|
||||||
interval_fwd_apt = self.position_to_apt(interval_fwd)
|
|
||||||
interval_rev_apt = int(self.position_to_apt(interval_rev)) # Signed
|
|
||||||
|
|
||||||
data = struct.pack('<HBBIIIi',
|
|
||||||
channel, # Channel number
|
|
||||||
mode, # Trigger mode
|
|
||||||
polarity, # Polarity
|
|
||||||
start_fwd_apt, # Start position forward
|
|
||||||
start_rev_apt, # Start position reverse
|
|
||||||
interval_fwd_apt, # Interval forward
|
|
||||||
interval_rev_apt # Interval reverse (signed)
|
|
||||||
)
|
|
||||||
|
|
||||||
return APTMessage.build_with_data(
|
|
||||||
MessageID.MGMSG_MOT_SET_TRIGGER, dest, data
|
|
||||||
)
|
|
||||||
|
|
||||||
def cmd_req_trigger(self, channel: int) -> bytes:
|
|
||||||
"""Build request trigger configuration command"""
|
|
||||||
dest = Destination.CHANNEL_1 + (channel - 1)
|
|
||||||
return APTMessage.build_header_only(
|
|
||||||
MessageID.MGMSG_MOT_REQ_TRIGGER, 0x01, 0x00, dest
|
|
||||||
)
|
|
||||||
|
|
||||||
def cmd_set_digital_outputs(self, output_bits: int) -> bytes:
|
|
||||||
"""
|
|
||||||
Build set digital outputs command
|
|
||||||
|
|
||||||
Args:
|
|
||||||
output_bits: Bit pattern for digital outputs (0x00 to 0xFF)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bytes: Digital output command
|
|
||||||
"""
|
|
||||||
return APTMessage.build_header_only(
|
|
||||||
MessageID.MGMSG_RACK_SET_DIGOUTPUTS, output_bits, 0x00, Destination.USB
|
|
||||||
)
|
|
||||||
|
|
||||||
def cmd_req_digital_outputs(self) -> bytes:
|
|
||||||
"""Build request digital outputs command"""
|
|
||||||
return APTMessage.build_header_only(
|
|
||||||
MessageID.MGMSG_RACK_REQ_DIGOUTPUTS, 0x00, 0x00, Destination.USB
|
|
||||||
)
|
|
||||||
@@ -1,627 +0,0 @@
|
|||||||
"""
|
|
||||||
Helios Laser Driver
|
|
||||||
Complete RS-232 driver for Helios laser systems
|
|
||||||
|
|
||||||
Copyright (C) 2025 Thomas Ales
|
|
||||||
Licensed under GNU General Public License v2.0
|
|
||||||
"""
|
|
||||||
|
|
||||||
import serial
|
|
||||||
import serial.tools.list_ports
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
from typing import Dict, List, Optional, Tuple
|
|
||||||
|
|
||||||
from hardware.helios_protocol import (
|
|
||||||
HeliosProtocol, HeliosCommand, HeliosStatus, PulseMode
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class HeliosDriver:
|
|
||||||
"""
|
|
||||||
Complete driver for Helios laser system
|
|
||||||
|
|
||||||
Features:
|
|
||||||
- RS-232 communication at 9600 baud
|
|
||||||
- All protocol commands supported
|
|
||||||
- Thread-safe operation
|
|
||||||
- Temperature monitoring
|
|
||||||
- Status monitoring
|
|
||||||
- Power monitoring
|
|
||||||
"""
|
|
||||||
|
|
||||||
# RS-232 Settings (from protocol document)
|
|
||||||
BAUDRATE = 9600
|
|
||||||
DATABITS = 8
|
|
||||||
PARITY = 'N'
|
|
||||||
STOPBITS = 1
|
|
||||||
|
|
||||||
def __init__(self, timeout: float = 2.0):
|
|
||||||
"""
|
|
||||||
Initialize Helios driver
|
|
||||||
|
|
||||||
Args:
|
|
||||||
timeout: Serial communication timeout in seconds
|
|
||||||
"""
|
|
||||||
self.protocol = HeliosProtocol()
|
|
||||||
self.timeout = timeout
|
|
||||||
|
|
||||||
# Serial connection
|
|
||||||
self._serial: Optional[serial.Serial] = None
|
|
||||||
self._port_name = ""
|
|
||||||
self._connected = False
|
|
||||||
|
|
||||||
# Communication lock for thread safety
|
|
||||||
self._comm_lock = threading.Lock()
|
|
||||||
|
|
||||||
# Cached state
|
|
||||||
self._laser_enabled = False
|
|
||||||
self._pulse_mode = PulseMode.CONTINUOUS_PULSING
|
|
||||||
self._frequency_hz = 10000.0
|
|
||||||
self._current_ma = 500.0
|
|
||||||
self._controller_serial = ""
|
|
||||||
self._head_serial = ""
|
|
||||||
|
|
||||||
# Temperature monitoring (in °C)
|
|
||||||
self._pump_temp_c = 0.0
|
|
||||||
self._resonator_temp_c = 0.0
|
|
||||||
self._qswitch_temp_c = 0.0
|
|
||||||
self._power_stage_temp_c = 0.0
|
|
||||||
|
|
||||||
# Status
|
|
||||||
self._status = HeliosStatus(0)
|
|
||||||
self._operation_hours = 0.0
|
|
||||||
self._power_mw = 0.0
|
|
||||||
|
|
||||||
print("INFO: Helios Laser driver initialized")
|
|
||||||
|
|
||||||
# ==================== Connection Management ====================
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def list_available_ports() -> List[str]:
|
|
||||||
"""
|
|
||||||
List available serial ports
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list: Available port names
|
|
||||||
"""
|
|
||||||
ports = serial.tools.list_ports.comports()
|
|
||||||
return [port.device for port in ports]
|
|
||||||
|
|
||||||
def connect(self, port: str) -> bool:
|
|
||||||
"""
|
|
||||||
Connect to Helios laser
|
|
||||||
|
|
||||||
Args:
|
|
||||||
port: Serial port name (e.g., 'COM3', '/dev/ttyUSB0')
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if connection successful
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
print(f"INFO: Connecting to Helios laser on {port}")
|
|
||||||
|
|
||||||
self._serial = serial.Serial(
|
|
||||||
port=port,
|
|
||||||
baudrate=self.BAUDRATE,
|
|
||||||
bytesize=self.DATABITS,
|
|
||||||
parity=self.PARITY,
|
|
||||||
stopbits=self.STOPBITS,
|
|
||||||
timeout=self.timeout
|
|
||||||
)
|
|
||||||
|
|
||||||
self._port_name = port
|
|
||||||
self._connected = True
|
|
||||||
|
|
||||||
# Read serial numbers
|
|
||||||
time.sleep(0.5)
|
|
||||||
self._controller_serial = self.query_controller_serial()
|
|
||||||
time.sleep(0.5)
|
|
||||||
self._head_serial = self.query_head_serial()
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
# Read initial state
|
|
||||||
self._update_cached_state()
|
|
||||||
|
|
||||||
print(f"INFO: Connected to Helios laser on {port}")
|
|
||||||
print(f" Controller S/N: {self._controller_serial}")
|
|
||||||
print(f" Head S/N: {self._head_serial}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
except serial.SerialException as e:
|
|
||||||
print(f"ERROR: Failed to connect to {port}: {e}")
|
|
||||||
self._connected = False
|
|
||||||
return False
|
|
||||||
|
|
||||||
def disconnect(self) -> bool:
|
|
||||||
"""
|
|
||||||
Disconnect from Helios laser
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if disconnection successful
|
|
||||||
"""
|
|
||||||
if not self._connected:
|
|
||||||
return True
|
|
||||||
|
|
||||||
try:
|
|
||||||
print("INFO: Disconnecting from Helios laser")
|
|
||||||
|
|
||||||
# Turn off laser before disconnecting
|
|
||||||
self.set_laser_enable(False)
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
# Close serial port
|
|
||||||
if self._serial and self._serial.is_open:
|
|
||||||
self._serial.close()
|
|
||||||
|
|
||||||
self._connected = False
|
|
||||||
print("INFO: Disconnected from Helios laser")
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"ERROR: Error during disconnect: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_connected(self) -> bool:
|
|
||||||
"""Check if laser is connected"""
|
|
||||||
return self._connected and self._serial and self._serial.is_open
|
|
||||||
|
|
||||||
# ==================== Communication Methods ====================
|
|
||||||
|
|
||||||
def _send_command(self, command: bytes) -> bool:
|
|
||||||
"""
|
|
||||||
Send command to laser (no response expected)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
command: Command bytes to send
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if send successful
|
|
||||||
"""
|
|
||||||
if not self.is_connected():
|
|
||||||
print("ERROR: Cannot send command - not connected")
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
with self._comm_lock:
|
|
||||||
self._serial.write(command)
|
|
||||||
self._serial.flush()
|
|
||||||
return True
|
|
||||||
except serial.SerialException as e:
|
|
||||||
print(f"ERROR: Failed to send command: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _query(self, command: bytes) -> Optional[str]:
|
|
||||||
"""
|
|
||||||
Send query and read response
|
|
||||||
|
|
||||||
Args:
|
|
||||||
command: Query command bytes
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Response string, or None if error
|
|
||||||
"""
|
|
||||||
if not self.is_connected():
|
|
||||||
print("ERROR: Cannot query - not connected")
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
with self._comm_lock:
|
|
||||||
# Clear input buffer
|
|
||||||
self._serial.reset_input_buffer()
|
|
||||||
|
|
||||||
# Send query
|
|
||||||
self._serial.write(command)
|
|
||||||
self._serial.flush()
|
|
||||||
|
|
||||||
# Read response (terminated by CR)
|
|
||||||
response = self._serial.read_until(b'\r')
|
|
||||||
|
|
||||||
if not response:
|
|
||||||
print("ERROR: No response from laser")
|
|
||||||
return None
|
|
||||||
|
|
||||||
return HeliosCommand.parse_response(response)
|
|
||||||
|
|
||||||
except serial.SerialException as e:
|
|
||||||
print(f"ERROR: Query failed: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _set_and_verify(self, set_cmd: bytes, query_cmd: bytes,
|
|
||||||
expected_value: str, retries: int = 3) -> bool:
|
|
||||||
"""
|
|
||||||
Set a value and verify it was set correctly
|
|
||||||
|
|
||||||
Args:
|
|
||||||
set_cmd: Command to set value
|
|
||||||
query_cmd: Command to query value
|
|
||||||
expected_value: Expected response
|
|
||||||
retries: Number of retry attempts
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if value was set and verified
|
|
||||||
"""
|
|
||||||
for attempt in range(retries):
|
|
||||||
# Send set command
|
|
||||||
if not self._send_command(set_cmd):
|
|
||||||
continue
|
|
||||||
|
|
||||||
time.sleep(0.5) # Wait 500ms for laser to process (per documentation)
|
|
||||||
|
|
||||||
# Query to verify
|
|
||||||
response = self._query(query_cmd)
|
|
||||||
if response and response == expected_value:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if attempt < retries - 1:
|
|
||||||
print(f"DEBUG: Verification failed, retrying ({attempt + 1}/{retries})")
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
print(f"ERROR: Failed to set and verify value after {retries} attempts")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# ==================== Laser Control ====================
|
|
||||||
|
|
||||||
def set_laser_enable(self, enabled: bool) -> bool:
|
|
||||||
"""
|
|
||||||
Enable or disable laser
|
|
||||||
|
|
||||||
Args:
|
|
||||||
enabled: True to enable, False to disable
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if command successful
|
|
||||||
"""
|
|
||||||
print(f"DEBUG: {'Enabling' if enabled else 'Disabling'} laser")
|
|
||||||
|
|
||||||
cmd = self.protocol.cmd_set_laser_enable(enabled)
|
|
||||||
query_cmd = self.protocol.cmd_query_laser_enable()
|
|
||||||
expected = "1" if enabled else "0"
|
|
||||||
|
|
||||||
if self._set_and_verify(cmd, query_cmd, expected):
|
|
||||||
self._laser_enabled = enabled
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_laser_enabled(self) -> bool:
|
|
||||||
"""Check if laser is currently enabled"""
|
|
||||||
return self._laser_enabled
|
|
||||||
|
|
||||||
def query_laser_enable(self) -> bool:
|
|
||||||
"""Query laser enable state from hardware"""
|
|
||||||
cmd = self.protocol.cmd_query_laser_enable()
|
|
||||||
response = self._query(cmd)
|
|
||||||
if response:
|
|
||||||
self._laser_enabled = (response == "1")
|
|
||||||
return self._laser_enabled
|
|
||||||
return False
|
|
||||||
|
|
||||||
# ==================== Pulse Mode Control ====================
|
|
||||||
|
|
||||||
def set_pulse_mode(self, mode: PulseMode) -> bool:
|
|
||||||
"""
|
|
||||||
Set pulse mode
|
|
||||||
|
|
||||||
Args:
|
|
||||||
mode: PulseMode enum value
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if command successful
|
|
||||||
"""
|
|
||||||
print(f"DEBUG: Setting pulse mode to {mode.name}")
|
|
||||||
|
|
||||||
cmd = self.protocol.cmd_set_pulse_mode(mode)
|
|
||||||
query_cmd = self.protocol.cmd_query_pulse_mode()
|
|
||||||
expected = str(mode.value)
|
|
||||||
|
|
||||||
if self._set_and_verify(cmd, query_cmd, expected):
|
|
||||||
self._pulse_mode = mode
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_pulse_mode(self) -> PulseMode:
|
|
||||||
"""Get current pulse mode"""
|
|
||||||
return self._pulse_mode
|
|
||||||
|
|
||||||
# ==================== Frequency Control ====================
|
|
||||||
|
|
||||||
def set_frequency_hz(self, freq_hz: float) -> bool:
|
|
||||||
"""
|
|
||||||
Set laser frequency in Hz
|
|
||||||
|
|
||||||
Args:
|
|
||||||
freq_hz: Frequency in Hz (16.7 kHz to 125 kHz)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if command successful
|
|
||||||
"""
|
|
||||||
print(f"DEBUG: Setting laser frequency to {freq_hz} Hz")
|
|
||||||
|
|
||||||
try:
|
|
||||||
cmd = self.protocol.cmd_set_frequency_hz(freq_hz)
|
|
||||||
period_ns = self.protocol.frequency_to_period_ns(freq_hz)
|
|
||||||
query_cmd = self.protocol.cmd_query_frequency()
|
|
||||||
expected = str(period_ns)
|
|
||||||
|
|
||||||
if self._set_and_verify(cmd, query_cmd, expected):
|
|
||||||
self._frequency_hz = freq_hz
|
|
||||||
return True
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"ERROR: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_frequency_hz(self) -> float:
|
|
||||||
"""Get current frequency in Hz"""
|
|
||||||
return self._frequency_hz
|
|
||||||
|
|
||||||
def query_frequency_hz(self) -> Optional[float]:
|
|
||||||
"""Query frequency from hardware (returns Hz)"""
|
|
||||||
cmd = self.protocol.cmd_query_frequency()
|
|
||||||
response = self._query(cmd)
|
|
||||||
if response:
|
|
||||||
try:
|
|
||||||
period_ns = int(response)
|
|
||||||
freq_hz = self.protocol.period_ns_to_frequency(period_ns)
|
|
||||||
self._frequency_hz = freq_hz
|
|
||||||
return freq_hz
|
|
||||||
except (ValueError, ZeroDivisionError) as e:
|
|
||||||
print(f"ERROR: Failed to parse frequency: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# ==================== Current Control ====================
|
|
||||||
|
|
||||||
def set_current_ma(self, current_ma: float) -> bool:
|
|
||||||
"""
|
|
||||||
Set laser diode current in mA
|
|
||||||
|
|
||||||
Args:
|
|
||||||
current_ma: Current in mA (0-7000)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if command successful
|
|
||||||
"""
|
|
||||||
print(f"DEBUG: Setting laser current to {current_ma} mA")
|
|
||||||
|
|
||||||
try:
|
|
||||||
cmd = self.protocol.cmd_set_current_ma(current_ma)
|
|
||||||
query_cmd = self.protocol.cmd_query_current()
|
|
||||||
expected = str(int(current_ma))
|
|
||||||
|
|
||||||
if self._set_and_verify(cmd, query_cmd, expected):
|
|
||||||
self._current_ma = current_ma
|
|
||||||
return True
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"ERROR: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_current_ma(self) -> float:
|
|
||||||
"""Get current setting in mA"""
|
|
||||||
return self._current_ma
|
|
||||||
|
|
||||||
# ==================== Temperature Monitoring ====================
|
|
||||||
|
|
||||||
def query_pump_temperature_c(self) -> Optional[float]:
|
|
||||||
"""Query pump diode temperature in °C"""
|
|
||||||
cmd = self.protocol.cmd_query_pump_temp()
|
|
||||||
response = self._query(cmd)
|
|
||||||
if response:
|
|
||||||
try:
|
|
||||||
temp_mc = int(response)
|
|
||||||
temp_c = self.protocol.millicelsius_to_celsius(temp_mc)
|
|
||||||
self._pump_temp_c = temp_c
|
|
||||||
return temp_c
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"ERROR: Failed to parse temperature: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def query_resonator_temperature_c(self) -> Optional[float]:
|
|
||||||
"""Query resonator/SHG temperature in °C"""
|
|
||||||
cmd = self.protocol.cmd_query_resonator_temp()
|
|
||||||
response = self._query(cmd)
|
|
||||||
if response:
|
|
||||||
try:
|
|
||||||
temp_mc = int(response)
|
|
||||||
temp_c = self.protocol.millicelsius_to_celsius(temp_mc)
|
|
||||||
self._resonator_temp_c = temp_c
|
|
||||||
return temp_c
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"ERROR: Failed to parse temperature: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def query_qswitch_temperature_c(self) -> Optional[float]:
|
|
||||||
"""Query q-switch temperature in °C"""
|
|
||||||
cmd = self.protocol.cmd_query_qswitch_temp()
|
|
||||||
response = self._query(cmd)
|
|
||||||
if response:
|
|
||||||
try:
|
|
||||||
temp_mc = int(response)
|
|
||||||
temp_c = self.protocol.millicelsius_to_celsius(temp_mc)
|
|
||||||
self._qswitch_temp_c = temp_c
|
|
||||||
return temp_c
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"ERROR: Failed to parse temperature: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def query_power_stage_temperature_c(self) -> Optional[float]:
|
|
||||||
"""Query controller power stage temperature in °C"""
|
|
||||||
cmd = self.protocol.cmd_query_power_stage_temp()
|
|
||||||
response = self._query(cmd)
|
|
||||||
if response:
|
|
||||||
try:
|
|
||||||
temp_mc = int(response)
|
|
||||||
temp_c = self.protocol.millicelsius_to_celsius(temp_mc)
|
|
||||||
self._power_stage_temp_c = temp_c
|
|
||||||
return temp_c
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"ERROR: Failed to parse temperature: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def query_all_temperatures(self) -> Dict[str, float]:
|
|
||||||
"""
|
|
||||||
Query all temperatures
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Temperature readings in °C
|
|
||||||
"""
|
|
||||||
temps = {}
|
|
||||||
temps['pump'] = self.query_pump_temperature_c()
|
|
||||||
time.sleep(0.5)
|
|
||||||
temps['resonator'] = self.query_resonator_temperature_c()
|
|
||||||
time.sleep(0.5)
|
|
||||||
temps['qswitch'] = self.query_qswitch_temperature_c()
|
|
||||||
time.sleep(0.5)
|
|
||||||
temps['power_stage'] = self.query_power_stage_temperature_c()
|
|
||||||
return temps
|
|
||||||
|
|
||||||
# ==================== Status and Monitoring ====================
|
|
||||||
|
|
||||||
def query_status(self) -> HeliosStatus:
|
|
||||||
"""Query status register"""
|
|
||||||
cmd = self.protocol.cmd_query_status()
|
|
||||||
response = self._query(cmd)
|
|
||||||
if response:
|
|
||||||
try:
|
|
||||||
status_value = int(response)
|
|
||||||
self._status = HeliosStatus(status_value)
|
|
||||||
return self._status
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"ERROR: Failed to parse status: {e}")
|
|
||||||
return self._status
|
|
||||||
|
|
||||||
def clear_status(self) -> bool:
|
|
||||||
"""Clear status register"""
|
|
||||||
cmd = self.protocol.cmd_clear_status()
|
|
||||||
return self._send_command(cmd)
|
|
||||||
|
|
||||||
def clear_errors(self) -> bool:
|
|
||||||
"""Clear controller errors"""
|
|
||||||
cmd = self.protocol.cmd_clear_errors()
|
|
||||||
return self._send_command(cmd)
|
|
||||||
|
|
||||||
def query_power_monitor_mw(self) -> Optional[float]:
|
|
||||||
"""Query laser power monitor in mW"""
|
|
||||||
cmd = self.protocol.cmd_query_power_monitor()
|
|
||||||
response = self._query(cmd)
|
|
||||||
if response:
|
|
||||||
try:
|
|
||||||
power_mw = float(response)
|
|
||||||
self._power_mw = power_mw
|
|
||||||
return power_mw
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"ERROR: Failed to parse power: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def query_operation_hours(self) -> Optional[float]:
|
|
||||||
"""Query laser diode operation time in hours"""
|
|
||||||
cmd = self.protocol.cmd_query_operation_time()
|
|
||||||
response = self._query(cmd)
|
|
||||||
if response:
|
|
||||||
try:
|
|
||||||
hours = float(response)
|
|
||||||
self._operation_hours = hours
|
|
||||||
return hours
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"ERROR: Failed to parse operation time: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# ==================== Serial Numbers ====================
|
|
||||||
|
|
||||||
def query_controller_serial(self) -> str:
|
|
||||||
"""Query controller serial number"""
|
|
||||||
cmd = self.protocol.cmd_query_controller_serial()
|
|
||||||
response = self._query(cmd)
|
|
||||||
if response:
|
|
||||||
self._controller_serial = response
|
|
||||||
return response
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def query_head_serial(self) -> str:
|
|
||||||
"""Query laser head serial number"""
|
|
||||||
cmd = self.protocol.cmd_query_head_serial()
|
|
||||||
response = self._query(cmd)
|
|
||||||
if response:
|
|
||||||
self._head_serial = response
|
|
||||||
return response
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def get_controller_serial(self) -> str:
|
|
||||||
"""Get cached controller serial number"""
|
|
||||||
return self._controller_serial
|
|
||||||
|
|
||||||
def get_head_serial(self) -> str:
|
|
||||||
"""Get cached head serial number"""
|
|
||||||
return self._head_serial
|
|
||||||
|
|
||||||
# ==================== Factory Reset ====================
|
|
||||||
|
|
||||||
def restore_factory_settings(self) -> bool:
|
|
||||||
"""
|
|
||||||
Restore factory settings
|
|
||||||
|
|
||||||
WARNING: This will reset all parameters to factory defaults.
|
|
||||||
Laser must be disabled before calling this method.
|
|
||||||
After calling, wait 2 seconds before power cycling.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if command sent successfully
|
|
||||||
"""
|
|
||||||
if self._laser_enabled:
|
|
||||||
print("ERROR: Laser must be disabled before factory reset")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print("WARNING: Restoring factory settings")
|
|
||||||
cmd = self.protocol.cmd_restore_factory()
|
|
||||||
if self._send_command(cmd):
|
|
||||||
print("INFO: Factory settings restored. Wait 2s before power cycle.")
|
|
||||||
time.sleep(2)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
# ==================== State Management ====================
|
|
||||||
|
|
||||||
def _update_cached_state(self):
|
|
||||||
"""Update all cached state from hardware"""
|
|
||||||
self.query_laser_enable()
|
|
||||||
time.sleep(0.5)
|
|
||||||
self.query_frequency_hz()
|
|
||||||
time.sleep(0.5)
|
|
||||||
# Current is write-only in some modes, skip query
|
|
||||||
self.query_status()
|
|
||||||
|
|
||||||
def get_status(self) -> Dict:
|
|
||||||
"""
|
|
||||||
Get complete laser status
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Comprehensive status dictionary
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
'connected': self.is_connected(),
|
|
||||||
'laser_enabled': self._laser_enabled,
|
|
||||||
'pulse_mode': self._pulse_mode.name,
|
|
||||||
'frequency_hz': self._frequency_hz,
|
|
||||||
'current_ma': self._current_ma,
|
|
||||||
'power_mw': self._power_mw,
|
|
||||||
'operation_hours': self._operation_hours,
|
|
||||||
'temperatures': {
|
|
||||||
'pump_c': self._pump_temp_c,
|
|
||||||
'resonator_c': self._resonator_temp_c,
|
|
||||||
'qswitch_c': self._qswitch_temp_c,
|
|
||||||
'power_stage_c': self._power_stage_temp_c
|
|
||||||
},
|
|
||||||
'status_value': self._status.value,
|
|
||||||
'has_errors': self._status.has_errors(),
|
|
||||||
'controller_serial': self._controller_serial,
|
|
||||||
'head_serial': self._head_serial
|
|
||||||
}
|
|
||||||
|
|
||||||
def update_status(self):
|
|
||||||
"""Update status information from hardware"""
|
|
||||||
if not self.is_connected():
|
|
||||||
return
|
|
||||||
|
|
||||||
self.query_status()
|
|
||||||
time.sleep(0.5)
|
|
||||||
self.query_power_monitor_mw()
|
|
||||||
time.sleep(0.5)
|
|
||||||
self.query_all_temperatures()
|
|
||||||
@@ -1,302 +0,0 @@
|
|||||||
"""
|
|
||||||
Helios Laser Protocol Handler
|
|
||||||
ASCII-based RS-232 communication protocol for Helios laser systems
|
|
||||||
|
|
||||||
Copyright (C) 2025 Thomas Ales
|
|
||||||
Licensed under GNU General Public License v2.0
|
|
||||||
"""
|
|
||||||
|
|
||||||
from enum import IntEnum
|
|
||||||
from typing import Union, Optional
|
|
||||||
|
|
||||||
|
|
||||||
class PulseMode(IntEnum):
|
|
||||||
"""Helios pulse mode settings"""
|
|
||||||
SINGLE_PULSE = 1
|
|
||||||
GATING = 4
|
|
||||||
CONTINUOUS_PULSING = 14
|
|
||||||
|
|
||||||
|
|
||||||
class HeliosCommand:
|
|
||||||
"""
|
|
||||||
Helios laser command constants and builders
|
|
||||||
|
|
||||||
All commands are ASCII strings terminated with carriage return <CR>
|
|
||||||
Format: COMMAND value<CR> for setting
|
|
||||||
COMMAND<CR> for querying
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Command constants
|
|
||||||
LDO = "LDO" # Laser enabled (0/1)
|
|
||||||
LDG = "LDG" # Pulse mode (1/4/14)
|
|
||||||
LDF = "LDF" # Period between pulses (ns)
|
|
||||||
LRE = "LRE" # Laser remote enable (0/1) - Single electronic only
|
|
||||||
LDS = "LDS" # Laser diode pulse current (mA)
|
|
||||||
LTA = "LTA" # Actual pump diode temperature (m°C)
|
|
||||||
LMA = "LMA" # Actual resonator/SHG temperature (mA)
|
|
||||||
EOA = "EOA" # Actual q-switch temperature (m°C)
|
|
||||||
ELT = "ELT" # Pump diode temp control deviation (m°C)
|
|
||||||
ELM = "ELM" # Resonator/SHG temp control deviation (m°C)
|
|
||||||
EEO = "EEO" # Q-switch temp control deviation (m°C)
|
|
||||||
LTT = "LTT" # Controller power stage temperature (m°C)
|
|
||||||
LER = "LER" # Status register (read)
|
|
||||||
LCE = "LCE" # Clear status register
|
|
||||||
CCE = "CCE" # Clear controller errors
|
|
||||||
CSR = "CSR" # Controller serial number
|
|
||||||
HSR = "HSR" # Laser head serial number
|
|
||||||
HTR = "HTR" # Laser diode operation time (hours)
|
|
||||||
HPR = "HPR" # Restore factory settings
|
|
||||||
HMP = "HMP" # Laser power monitor (mW)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_command(command: str, value: Optional[Union[int, float]] = None) -> bytes:
|
|
||||||
"""
|
|
||||||
Build a Helios command string
|
|
||||||
|
|
||||||
Args:
|
|
||||||
command: Command string (e.g., "LDO", "LDF")
|
|
||||||
value: Optional value to set (None for query)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bytes: Command ready to send over serial
|
|
||||||
|
|
||||||
Example:
|
|
||||||
build_command("LDO", 1) -> b"LDO 1\r"
|
|
||||||
build_command("LDO") -> b"LDO\r"
|
|
||||||
"""
|
|
||||||
if value is not None:
|
|
||||||
cmd_str = f"{command} {value}\r"
|
|
||||||
else:
|
|
||||||
cmd_str = f"{command}\r"
|
|
||||||
return cmd_str.encode('ascii')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_response(response: bytes) -> str:
|
|
||||||
"""
|
|
||||||
Parse response from Helios laser
|
|
||||||
|
|
||||||
Args:
|
|
||||||
response: Raw bytes from serial port
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Parsed response string (stripped of CR/LF)
|
|
||||||
"""
|
|
||||||
return response.decode('ascii').strip()
|
|
||||||
|
|
||||||
|
|
||||||
class HeliosStatus:
|
|
||||||
"""
|
|
||||||
Helios status register decoder
|
|
||||||
|
|
||||||
Status is sum of multiple bit flags:
|
|
||||||
Example: 1*2^0 + 0*2^1 + 1*2^2 = 5
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Status bit definitions (from LER/LCE/CCE commands)
|
|
||||||
# These are example flags - actual flags depend on controller model
|
|
||||||
# Refer to "Troubleshooting" section in manual for complete list
|
|
||||||
|
|
||||||
def __init__(self, status_value: int):
|
|
||||||
"""
|
|
||||||
Initialize status decoder
|
|
||||||
|
|
||||||
Args:
|
|
||||||
status_value: Numeric status value from controller
|
|
||||||
"""
|
|
||||||
self.value = status_value
|
|
||||||
self.flags = self._decode_flags(status_value)
|
|
||||||
|
|
||||||
def _decode_flags(self, value: int) -> list:
|
|
||||||
"""Decode status value into list of active bit positions"""
|
|
||||||
flags = []
|
|
||||||
bit_pos = 0
|
|
||||||
while value > 0:
|
|
||||||
if value & 1:
|
|
||||||
flags.append(bit_pos)
|
|
||||||
value >>= 1
|
|
||||||
bit_pos += 1
|
|
||||||
return flags
|
|
||||||
|
|
||||||
def has_errors(self) -> bool:
|
|
||||||
"""Check if any error flags are set"""
|
|
||||||
return self.value > 0
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"Status: {self.value} (flags: {self.flags})"
|
|
||||||
|
|
||||||
|
|
||||||
class HeliosProtocol:
|
|
||||||
"""
|
|
||||||
High-level Helios protocol handler
|
|
||||||
Provides parameter validation and unit conversions
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Parameter ranges (from protocol document)
|
|
||||||
RANGE_LDO = (0, 1)
|
|
||||||
RANGE_LDG = (0, 14)
|
|
||||||
RANGE_LDF = (8000, 60000) # ns
|
|
||||||
RANGE_LRE = (0, 1)
|
|
||||||
RANGE_LDS = (0, 7000) # mA
|
|
||||||
RANGE_LTA = (5000, 50000) # m°C
|
|
||||||
RANGE_LMA = (0, 4000) # mA (seems like error in doc, should be m°C)
|
|
||||||
RANGE_EOA = (5000, 50000) # m°C
|
|
||||||
RANGE_ELT = (-32768, 32767) # m°C
|
|
||||||
RANGE_ELM = (-32768, 32767) # m°C
|
|
||||||
RANGE_EEO = (-32768, 32767) # m°C
|
|
||||||
RANGE_LTT = (5000, 65535) # m°C
|
|
||||||
RANGE_HTR = (0, 65535) # hours
|
|
||||||
RANGE_HMP = (0, 5000) # mW
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Initialize protocol handler"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Temperature conversions (m°C <-> °C)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def celsius_to_millicelsius(temp_c: float) -> int:
|
|
||||||
"""Convert temperature from °C to m°C (milli-Celsius)"""
|
|
||||||
return int(temp_c * 1000)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def millicelsius_to_celsius(temp_mc: int) -> float:
|
|
||||||
"""Convert temperature from m°C to °C"""
|
|
||||||
return temp_mc / 1000.0
|
|
||||||
|
|
||||||
# Frequency conversions (Hz <-> ns period)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def frequency_to_period_ns(freq_hz: float) -> int:
|
|
||||||
"""
|
|
||||||
Convert frequency in Hz to period in nanoseconds
|
|
||||||
|
|
||||||
Args:
|
|
||||||
freq_hz: Frequency in Hz
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int: Period in nanoseconds
|
|
||||||
|
|
||||||
Example:
|
|
||||||
50000 Hz -> 20000 ns (50 kHz)
|
|
||||||
"""
|
|
||||||
if freq_hz <= 0:
|
|
||||||
raise ValueError("Frequency must be positive")
|
|
||||||
period_ns = int(1e9 / freq_hz)
|
|
||||||
return period_ns
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def period_ns_to_frequency(period_ns: int) -> float:
|
|
||||||
"""
|
|
||||||
Convert period in nanoseconds to frequency in Hz
|
|
||||||
|
|
||||||
Args:
|
|
||||||
period_ns: Period in nanoseconds
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: Frequency in Hz
|
|
||||||
"""
|
|
||||||
if period_ns <= 0:
|
|
||||||
raise ValueError("Period must be positive")
|
|
||||||
freq_hz = 1e9 / period_ns
|
|
||||||
return freq_hz
|
|
||||||
|
|
||||||
# Command builders with validation
|
|
||||||
|
|
||||||
def cmd_set_laser_enable(self, enabled: bool) -> bytes:
|
|
||||||
"""Build command to enable/disable laser"""
|
|
||||||
value = 1 if enabled else 0
|
|
||||||
return HeliosCommand.build_command(HeliosCommand.LDO, value)
|
|
||||||
|
|
||||||
def cmd_query_laser_enable(self) -> bytes:
|
|
||||||
"""Build query for laser enable state"""
|
|
||||||
return HeliosCommand.build_command(HeliosCommand.LDO)
|
|
||||||
|
|
||||||
def cmd_set_pulse_mode(self, mode: PulseMode) -> bytes:
|
|
||||||
"""Build command to set pulse mode"""
|
|
||||||
if mode not in [PulseMode.SINGLE_PULSE, PulseMode.GATING,
|
|
||||||
PulseMode.CONTINUOUS_PULSING]:
|
|
||||||
raise ValueError(f"Invalid pulse mode: {mode}")
|
|
||||||
return HeliosCommand.build_command(HeliosCommand.LDG, mode)
|
|
||||||
|
|
||||||
def cmd_query_pulse_mode(self) -> bytes:
|
|
||||||
"""Build query for pulse mode"""
|
|
||||||
return HeliosCommand.build_command(HeliosCommand.LDG)
|
|
||||||
|
|
||||||
def cmd_set_frequency_hz(self, freq_hz: float) -> bytes:
|
|
||||||
"""
|
|
||||||
Build command to set laser frequency (Hz)
|
|
||||||
Converts to period in ns internally
|
|
||||||
"""
|
|
||||||
period_ns = self.frequency_to_period_ns(freq_hz)
|
|
||||||
if not (self.RANGE_LDF[0] <= period_ns <= self.RANGE_LDF[1]):
|
|
||||||
raise ValueError(f"Frequency results in period {period_ns}ns, "
|
|
||||||
f"valid range: {self.RANGE_LDF[0]}-{self.RANGE_LDF[1]}ns")
|
|
||||||
return HeliosCommand.build_command(HeliosCommand.LDF, period_ns)
|
|
||||||
|
|
||||||
def cmd_query_frequency(self) -> bytes:
|
|
||||||
"""Build query for laser frequency (returns period in ns)"""
|
|
||||||
return HeliosCommand.build_command(HeliosCommand.LDF)
|
|
||||||
|
|
||||||
def cmd_set_current_ma(self, current_ma: float) -> bytes:
|
|
||||||
"""Build command to set laser diode current (mA)"""
|
|
||||||
if not (self.RANGE_LDS[0] <= current_ma <= self.RANGE_LDS[1]):
|
|
||||||
raise ValueError(f"Current {current_ma}mA outside valid range: "
|
|
||||||
f"{self.RANGE_LDS[0]}-{self.RANGE_LDS[1]}mA")
|
|
||||||
return HeliosCommand.build_command(HeliosCommand.LDS, int(current_ma))
|
|
||||||
|
|
||||||
def cmd_query_current(self) -> bytes:
|
|
||||||
"""Build query for laser diode current"""
|
|
||||||
return HeliosCommand.build_command(HeliosCommand.LDS)
|
|
||||||
|
|
||||||
def cmd_query_pump_temp(self) -> bytes:
|
|
||||||
"""Build query for actual pump diode temperature"""
|
|
||||||
return HeliosCommand.build_command(HeliosCommand.LTA)
|
|
||||||
|
|
||||||
def cmd_query_resonator_temp(self) -> bytes:
|
|
||||||
"""Build query for actual resonator/SHG temperature"""
|
|
||||||
return HeliosCommand.build_command(HeliosCommand.LMA)
|
|
||||||
|
|
||||||
def cmd_query_qswitch_temp(self) -> bytes:
|
|
||||||
"""Build query for actual q-switch temperature"""
|
|
||||||
return HeliosCommand.build_command(HeliosCommand.EOA)
|
|
||||||
|
|
||||||
def cmd_query_power_stage_temp(self) -> bytes:
|
|
||||||
"""Build query for controller power stage temperature"""
|
|
||||||
return HeliosCommand.build_command(HeliosCommand.LTT)
|
|
||||||
|
|
||||||
def cmd_query_status(self) -> bytes:
|
|
||||||
"""Build query for status register"""
|
|
||||||
return HeliosCommand.build_command(HeliosCommand.LER)
|
|
||||||
|
|
||||||
def cmd_clear_status(self) -> bytes:
|
|
||||||
"""Build command to clear status register"""
|
|
||||||
return HeliosCommand.build_command(HeliosCommand.LCE, 0)
|
|
||||||
|
|
||||||
def cmd_clear_errors(self) -> bytes:
|
|
||||||
"""Build command to clear controller errors"""
|
|
||||||
return HeliosCommand.build_command(HeliosCommand.CCE, 0)
|
|
||||||
|
|
||||||
def cmd_query_controller_serial(self) -> bytes:
|
|
||||||
"""Build query for controller serial number"""
|
|
||||||
return HeliosCommand.build_command(HeliosCommand.CSR)
|
|
||||||
|
|
||||||
def cmd_query_head_serial(self) -> bytes:
|
|
||||||
"""Build query for laser head serial number"""
|
|
||||||
return HeliosCommand.build_command(HeliosCommand.HSR)
|
|
||||||
|
|
||||||
def cmd_query_operation_time(self) -> bytes:
|
|
||||||
"""Build query for laser diode operation time (hours)"""
|
|
||||||
return HeliosCommand.build_command(HeliosCommand.HTR)
|
|
||||||
|
|
||||||
def cmd_query_power_monitor(self) -> bytes:
|
|
||||||
"""Build query for laser power monitor (mW)"""
|
|
||||||
return HeliosCommand.build_command(HeliosCommand.HMP)
|
|
||||||
|
|
||||||
def cmd_restore_factory(self) -> bytes:
|
|
||||||
"""
|
|
||||||
Build command to restore factory settings
|
|
||||||
|
|
||||||
WARNING: Laser must be disabled (LDO 0) before this command
|
|
||||||
After sending, wait 2 seconds before rebooting/power cycling
|
|
||||||
"""
|
|
||||||
return HeliosCommand.build_command(HeliosCommand.HPR)
|
|
||||||
@@ -1,387 +0,0 @@
|
|||||||
"""
|
|
||||||
Microscope Controller
|
|
||||||
Handles communication with Genesis and Helios microscope/laser systems
|
|
||||||
|
|
||||||
Genesis: Laser scanning microscope system (stub)
|
|
||||||
Helios: Laser control system with frequency and current control (full implementation)
|
|
||||||
Both systems communicate via USB/Serial interfaces
|
|
||||||
|
|
||||||
Copyright (C) 2025 Thomas Ales
|
|
||||||
Licensed under GNU General Public License v2.0
|
|
||||||
"""
|
|
||||||
|
|
||||||
import time
|
|
||||||
from typing import Dict, Optional
|
|
||||||
from hardware.helios_driver import HeliosDriver, PulseMode
|
|
||||||
|
|
||||||
|
|
||||||
class MicroscopeController:
|
|
||||||
"""
|
|
||||||
Controller for Genesis and Helios microscope systems
|
|
||||||
|
|
||||||
Provides methods for:
|
|
||||||
- System connection and initialization
|
|
||||||
- Interlock status monitoring
|
|
||||||
- Parameter configuration
|
|
||||||
- Safety checks
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Initialize microscope controller"""
|
|
||||||
# Genesis state (stub)
|
|
||||||
self._genesis_connected = False
|
|
||||||
self._genesis_ready = False
|
|
||||||
self._genesis_interlocked = False
|
|
||||||
self._genesis_power_mw = 0.0
|
|
||||||
|
|
||||||
# Helios driver (full implementation)
|
|
||||||
self._helios_driver = HeliosDriver()
|
|
||||||
self._helios_port = None
|
|
||||||
|
|
||||||
# System interlock (master safety)
|
|
||||||
self._system_interlocked = False
|
|
||||||
|
|
||||||
print("INFO: Microscope controller initialized")
|
|
||||||
print(" - Genesis: Stub implementation")
|
|
||||||
print(" - Helios: Full RS-232 driver")
|
|
||||||
|
|
||||||
# ==================== Genesis Methods ====================
|
|
||||||
|
|
||||||
def connect_genesis(self) -> bool:
|
|
||||||
"""
|
|
||||||
Connect to Genesis laser scanning microscope
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if connection successful
|
|
||||||
"""
|
|
||||||
print("DEBUG: Connecting to Genesis microscope")
|
|
||||||
|
|
||||||
# Stub implementation
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
self._genesis_connected = True
|
|
||||||
self._genesis_ready = True
|
|
||||||
self._genesis_interlocked = True # Assume interlocks OK
|
|
||||||
|
|
||||||
print("INFO: Genesis microscope connected")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def disconnect_genesis(self) -> bool:
|
|
||||||
"""
|
|
||||||
Disconnect from Genesis microscope
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if disconnection successful
|
|
||||||
"""
|
|
||||||
print("DEBUG: Disconnecting from Genesis microscope")
|
|
||||||
|
|
||||||
self._genesis_connected = False
|
|
||||||
self._genesis_ready = False
|
|
||||||
|
|
||||||
print("INFO: Genesis microscope disconnected")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def apply_genesis_settings(self, settings: Dict) -> bool:
|
|
||||||
"""
|
|
||||||
Apply Genesis configuration settings
|
|
||||||
|
|
||||||
Args:
|
|
||||||
settings: Dictionary containing Genesis parameters
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if settings applied successfully
|
|
||||||
"""
|
|
||||||
print("DEBUG: Applying Genesis settings")
|
|
||||||
print(f" Power: {settings.get('power_mw', 0)} mW")
|
|
||||||
|
|
||||||
if not self._genesis_connected:
|
|
||||||
print("ERROR: Genesis not connected")
|
|
||||||
return False
|
|
||||||
|
|
||||||
self._genesis_power_mw = settings.get('power_mw', 0.0)
|
|
||||||
|
|
||||||
# Stub - would send commands to actual hardware
|
|
||||||
time.sleep(0.05)
|
|
||||||
|
|
||||||
print("INFO: Genesis settings applied")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_genesis_status(self) -> Dict[str, any]:
|
|
||||||
"""
|
|
||||||
Get Genesis microscope status
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Genesis status information
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
'connected': self._genesis_connected,
|
|
||||||
'ready': self._genesis_ready,
|
|
||||||
'interlocked': self._genesis_interlocked,
|
|
||||||
'power_mw': self._genesis_power_mw
|
|
||||||
}
|
|
||||||
|
|
||||||
# ==================== Helios Methods ====================
|
|
||||||
|
|
||||||
def connect_helios(self, port: str) -> bool:
|
|
||||||
"""
|
|
||||||
Connect to Helios laser system
|
|
||||||
|
|
||||||
Args:
|
|
||||||
port: COM port for Helios device
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if connection successful
|
|
||||||
"""
|
|
||||||
print(f"DEBUG: Connecting to Helios on {port}")
|
|
||||||
|
|
||||||
if not self._helios_driver.connect(port):
|
|
||||||
return False
|
|
||||||
|
|
||||||
self._helios_port = port
|
|
||||||
print(f"INFO: Helios connected on {port}")
|
|
||||||
print(f" Controller S/N: {self._helios_driver.get_controller_serial()}")
|
|
||||||
print(f" Head S/N: {self._helios_driver.get_head_serial()}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def disconnect_helios(self) -> bool:
|
|
||||||
"""
|
|
||||||
Disconnect from Helios laser system
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if disconnection successful
|
|
||||||
"""
|
|
||||||
print("DEBUG: Disconnecting from Helios")
|
|
||||||
return self._helios_driver.disconnect()
|
|
||||||
|
|
||||||
def is_helios_connected(self) -> bool:
|
|
||||||
"""Check if Helios is connected"""
|
|
||||||
return self._helios_driver.is_connected()
|
|
||||||
|
|
||||||
def apply_helios_settings(self, settings: Dict) -> bool:
|
|
||||||
"""
|
|
||||||
Apply Helios configuration settings
|
|
||||||
|
|
||||||
This method connects and configures the Helios laser with the
|
|
||||||
specified parameters from the settings dialog.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
settings: Dictionary containing:
|
|
||||||
- com_port: COM port name
|
|
||||||
- frequency_hz: Laser frequency in Hz
|
|
||||||
- current_ma: Laser diode current in mA
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if settings applied successfully
|
|
||||||
"""
|
|
||||||
if settings is None:
|
|
||||||
print("ERROR: No settings provided")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print("DEBUG: Applying Helios settings")
|
|
||||||
print(f" Port: {settings.get('com_port', 'N/A')}")
|
|
||||||
print(f" Frequency: {settings.get('frequency_hz', 0)} Hz")
|
|
||||||
print(f" Current: {settings.get('current_ma', 0)} mA")
|
|
||||||
|
|
||||||
# Connect if not already connected or port changed
|
|
||||||
port = settings.get('com_port')
|
|
||||||
if not port:
|
|
||||||
print("ERROR: No COM port specified")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not self.is_helios_connected() or port != self._helios_port:
|
|
||||||
if self.is_helios_connected():
|
|
||||||
self.disconnect_helios()
|
|
||||||
if not self.connect_helios(port):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Set frequency
|
|
||||||
freq_hz = settings.get('frequency_hz', 0.0)
|
|
||||||
if freq_hz > 0:
|
|
||||||
if not self._helios_driver.set_frequency_hz(freq_hz):
|
|
||||||
print("ERROR: Failed to set frequency")
|
|
||||||
return False
|
|
||||||
time.sleep(0.05)
|
|
||||||
|
|
||||||
# Set current
|
|
||||||
current_ma = settings.get('current_ma', 0.0)
|
|
||||||
if current_ma > 0:
|
|
||||||
if not self._helios_driver.set_current_ma(current_ma):
|
|
||||||
print("ERROR: Failed to set current")
|
|
||||||
return False
|
|
||||||
time.sleep(0.05)
|
|
||||||
|
|
||||||
# Set to continuous pulsing mode by default
|
|
||||||
if not self._helios_driver.set_pulse_mode(PulseMode.CONTINUOUS_PULSING):
|
|
||||||
print("WARNING: Failed to set pulse mode")
|
|
||||||
|
|
||||||
print("INFO: Helios settings applied successfully")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def helios_enable_laser(self, enabled: bool) -> bool:
|
|
||||||
"""
|
|
||||||
Enable or disable Helios laser
|
|
||||||
|
|
||||||
Args:
|
|
||||||
enabled: True to enable, False to disable
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if command successful
|
|
||||||
"""
|
|
||||||
if not self.is_helios_connected():
|
|
||||||
print("ERROR: Helios not connected")
|
|
||||||
return False
|
|
||||||
|
|
||||||
return self._helios_driver.set_laser_enable(enabled)
|
|
||||||
|
|
||||||
def is_helios_laser_enabled(self) -> bool:
|
|
||||||
"""Check if Helios laser is currently enabled"""
|
|
||||||
if not self.is_helios_connected():
|
|
||||||
return False
|
|
||||||
return self._helios_driver.is_laser_enabled()
|
|
||||||
|
|
||||||
def get_helios_status(self) -> Dict[str, any]:
|
|
||||||
"""
|
|
||||||
Get Helios laser system status
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Helios status information
|
|
||||||
"""
|
|
||||||
if not self.is_helios_connected():
|
|
||||||
return {
|
|
||||||
'connected': False,
|
|
||||||
'ready': False,
|
|
||||||
'interlocked': False,
|
|
||||||
'port': None,
|
|
||||||
'frequency_hz': 0.0,
|
|
||||||
'current_ma': 0.0,
|
|
||||||
'laser_enabled': False,
|
|
||||||
'power_mw': 0.0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get comprehensive status from driver
|
|
||||||
status = self._helios_driver.get_status()
|
|
||||||
|
|
||||||
return {
|
|
||||||
'connected': status['connected'],
|
|
||||||
'ready': not status['has_errors'],
|
|
||||||
'interlocked': not status['has_errors'], # Use error state as interlock
|
|
||||||
'port': self._helios_port,
|
|
||||||
'frequency_hz': status['frequency_hz'],
|
|
||||||
'current_ma': status['current_ma'],
|
|
||||||
'laser_enabled': status['laser_enabled'],
|
|
||||||
'power_mw': status['power_mw'],
|
|
||||||
'operation_hours': status['operation_hours'],
|
|
||||||
'controller_serial': status['controller_serial'],
|
|
||||||
'head_serial': status['head_serial']
|
|
||||||
}
|
|
||||||
|
|
||||||
def helios_update_status(self):
|
|
||||||
"""Update Helios status from hardware"""
|
|
||||||
if self.is_helios_connected():
|
|
||||||
self._helios_driver.update_status()
|
|
||||||
|
|
||||||
# ==================== Combined Status Methods ====================
|
|
||||||
|
|
||||||
def get_status(self) -> Dict[str, any]:
|
|
||||||
"""
|
|
||||||
Get complete microscope system status
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Combined status for Genesis, Helios, and interlocks
|
|
||||||
"""
|
|
||||||
# Get Helios status from driver
|
|
||||||
helios_status = self.get_helios_status()
|
|
||||||
helios_ready = helios_status.get('ready', False)
|
|
||||||
helios_interlocked = helios_status.get('interlocked', False)
|
|
||||||
|
|
||||||
return {
|
|
||||||
# System-wide
|
|
||||||
'interlocked': self._system_interlocked or (
|
|
||||||
self._genesis_interlocked and helios_interlocked
|
|
||||||
),
|
|
||||||
|
|
||||||
# Genesis
|
|
||||||
'genesis_ready': self._genesis_ready,
|
|
||||||
'genesis_interlocked': self._genesis_interlocked,
|
|
||||||
|
|
||||||
# Helios
|
|
||||||
'helios_ready': helios_ready,
|
|
||||||
'helios_interlocked': helios_interlocked
|
|
||||||
}
|
|
||||||
|
|
||||||
def check_interlocks(self) -> bool:
|
|
||||||
"""
|
|
||||||
Check all safety interlocks
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if all interlocks are satisfied
|
|
||||||
"""
|
|
||||||
# Check Genesis interlocks
|
|
||||||
if self._genesis_connected and not self._genesis_interlocked:
|
|
||||||
print("WARNING: Genesis interlock not satisfied")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Check Helios interlocks
|
|
||||||
if self.is_helios_connected():
|
|
||||||
helios_status = self.get_helios_status()
|
|
||||||
if not helios_status.get('interlocked', False):
|
|
||||||
print("WARNING: Helios interlock not satisfied")
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
# ==================== Scan Preparation ====================
|
|
||||||
|
|
||||||
def prepare_for_scan(self, params: Dict) -> bool:
|
|
||||||
"""
|
|
||||||
Prepare microscope systems for scanning
|
|
||||||
|
|
||||||
Args:
|
|
||||||
params: Scan parameters dictionary
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if preparation successful
|
|
||||||
"""
|
|
||||||
print("DEBUG: Preparing microscope systems for scan")
|
|
||||||
|
|
||||||
# Check interlocks
|
|
||||||
if not self.check_interlocks():
|
|
||||||
print("ERROR: Interlock check failed")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Verify systems are ready
|
|
||||||
if self._genesis_connected and not self._genesis_ready:
|
|
||||||
print("ERROR: Genesis not ready")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.is_helios_connected():
|
|
||||||
helios_status = self.get_helios_status()
|
|
||||||
if not helios_status.get('ready', False):
|
|
||||||
print("ERROR: Helios not ready")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Configure for scan
|
|
||||||
# Stub implementation
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
print("INFO: Microscope systems ready for scan")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def emergency_stop(self) -> bool:
|
|
||||||
"""
|
|
||||||
Emergency stop all microscope operations
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if stop successful
|
|
||||||
"""
|
|
||||||
print("WARNING: Emergency stop triggered")
|
|
||||||
|
|
||||||
# Disable all systems
|
|
||||||
if self._genesis_connected:
|
|
||||||
self._genesis_ready = False
|
|
||||||
|
|
||||||
if self.is_helios_connected():
|
|
||||||
# Disable Helios laser immediately
|
|
||||||
self._helios_driver.set_laser_enable(False)
|
|
||||||
|
|
||||||
return True
|
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
"""
|
|
||||||
ThorLabs Stage Settings Management
|
|
||||||
Handles saving and loading of stage configuration parameters
|
|
||||||
|
|
||||||
Copyright (C) 2025 Thomas Ales
|
|
||||||
Licensed under GNU General Public License v2.0
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from typing import Dict, Optional
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
class StageSettings:
|
|
||||||
"""
|
|
||||||
Manages stage configuration settings including velocity, acceleration,
|
|
||||||
and trigger I/O configuration
|
|
||||||
"""
|
|
||||||
|
|
||||||
DEFAULT_SETTINGS = {
|
|
||||||
'velocity': {
|
|
||||||
'x_axis': 1.0, # mm/s
|
|
||||||
'y_axis': 1.0, # mm/s
|
|
||||||
'z_axis': 1.0, # mm/s
|
|
||||||
},
|
|
||||||
'acceleration': {
|
|
||||||
'x_axis': 5.0, # mm/s²
|
|
||||||
'y_axis': 5.0, # mm/s²
|
|
||||||
'z_axis': 5.0, # mm/s²
|
|
||||||
},
|
|
||||||
'trigger': {
|
|
||||||
'x_axis': {
|
|
||||||
'mode': 0x00, # Disabled
|
|
||||||
'polarity': 0x01, # Active high
|
|
||||||
'start_pos_fwd': 0.0,
|
|
||||||
'start_pos_rev': 0.0,
|
|
||||||
'interval_fwd': 0.0,
|
|
||||||
'interval_rev': 0.0
|
|
||||||
},
|
|
||||||
'y_axis': {
|
|
||||||
'mode': 0x00, # Disabled
|
|
||||||
'polarity': 0x01, # Active high
|
|
||||||
'start_pos_fwd': 0.0,
|
|
||||||
'start_pos_rev': 0.0,
|
|
||||||
'interval_fwd': 0.0,
|
|
||||||
'interval_rev': 0.0
|
|
||||||
},
|
|
||||||
'z_axis': {
|
|
||||||
'mode': 0x00, # Disabled
|
|
||||||
'polarity': 0x01, # Active high
|
|
||||||
'start_pos_fwd': 0.0,
|
|
||||||
'start_pos_rev': 0.0,
|
|
||||||
'interval_fwd': 0.0,
|
|
||||||
'interval_rev': 0.0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'digital_io': {
|
|
||||||
'output_bits': 0x00
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, settings_file: Optional[str] = None):
|
|
||||||
"""
|
|
||||||
Initialize stage settings manager
|
|
||||||
|
|
||||||
Args:
|
|
||||||
settings_file: Path to settings file (default: ~/.nuescan/stage_settings.json)
|
|
||||||
"""
|
|
||||||
if settings_file is None:
|
|
||||||
# Default to user home directory
|
|
||||||
home = Path.home()
|
|
||||||
settings_dir = home / '.nuescan'
|
|
||||||
settings_dir.mkdir(exist_ok=True)
|
|
||||||
settings_file = str(settings_dir / 'stage_settings.json')
|
|
||||||
|
|
||||||
self.settings_file = settings_file
|
|
||||||
self.settings = self.DEFAULT_SETTINGS.copy()
|
|
||||||
|
|
||||||
# Load existing settings if available
|
|
||||||
self.load()
|
|
||||||
|
|
||||||
def load(self) -> bool:
|
|
||||||
"""
|
|
||||||
Load settings from file
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if settings loaded successfully, False otherwise
|
|
||||||
"""
|
|
||||||
if not os.path.exists(self.settings_file):
|
|
||||||
print(f"INFO: Settings file not found, using defaults: {self.settings_file}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(self.settings_file, 'r') as f:
|
|
||||||
loaded_settings = json.load(f)
|
|
||||||
|
|
||||||
# Merge with defaults to ensure all keys exist
|
|
||||||
self._merge_settings(loaded_settings)
|
|
||||||
|
|
||||||
print(f"INFO: Loaded stage settings from {self.settings_file}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"ERROR: Failed to load settings from {self.settings_file}: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def save(self) -> bool:
|
|
||||||
"""
|
|
||||||
Save settings to file
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if settings saved successfully, False otherwise
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Ensure directory exists
|
|
||||||
os.makedirs(os.path.dirname(self.settings_file), exist_ok=True)
|
|
||||||
|
|
||||||
with open(self.settings_file, 'w') as f:
|
|
||||||
json.dump(self.settings, f, indent=4)
|
|
||||||
|
|
||||||
print(f"INFO: Saved stage settings to {self.settings_file}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"ERROR: Failed to save settings to {self.settings_file}: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _merge_settings(self, loaded_settings: Dict):
|
|
||||||
"""Merge loaded settings with defaults"""
|
|
||||||
# Velocity
|
|
||||||
if 'velocity' in loaded_settings:
|
|
||||||
self.settings['velocity'].update(loaded_settings['velocity'])
|
|
||||||
|
|
||||||
# Acceleration
|
|
||||||
if 'acceleration' in loaded_settings:
|
|
||||||
self.settings['acceleration'].update(loaded_settings['acceleration'])
|
|
||||||
|
|
||||||
# Trigger
|
|
||||||
if 'trigger' in loaded_settings:
|
|
||||||
for axis in ['x_axis', 'y_axis', 'z_axis']:
|
|
||||||
if axis in loaded_settings['trigger']:
|
|
||||||
self.settings['trigger'][axis].update(loaded_settings['trigger'][axis])
|
|
||||||
|
|
||||||
# Digital I/O
|
|
||||||
if 'digital_io' in loaded_settings:
|
|
||||||
self.settings['digital_io'].update(loaded_settings['digital_io'])
|
|
||||||
|
|
||||||
# ==================== Velocity Settings ====================
|
|
||||||
|
|
||||||
def get_velocity(self, axis: str) -> float:
|
|
||||||
"""Get velocity for specific axis (x_axis, y_axis, z_axis)"""
|
|
||||||
return self.settings['velocity'].get(axis, 1.0)
|
|
||||||
|
|
||||||
def set_velocity(self, axis: str, velocity: float):
|
|
||||||
"""Set velocity for specific axis"""
|
|
||||||
if axis in ['x_axis', 'y_axis', 'z_axis']:
|
|
||||||
self.settings['velocity'][axis] = velocity
|
|
||||||
|
|
||||||
def get_all_velocities(self) -> Dict[str, float]:
|
|
||||||
"""Get all axis velocities"""
|
|
||||||
return self.settings['velocity'].copy()
|
|
||||||
|
|
||||||
def set_all_velocities(self, x: float, y: float, z: float):
|
|
||||||
"""Set all axis velocities"""
|
|
||||||
self.settings['velocity']['x_axis'] = x
|
|
||||||
self.settings['velocity']['y_axis'] = y
|
|
||||||
self.settings['velocity']['z_axis'] = z
|
|
||||||
|
|
||||||
# ==================== Acceleration Settings ====================
|
|
||||||
|
|
||||||
def get_acceleration(self, axis: str) -> float:
|
|
||||||
"""Get acceleration for specific axis"""
|
|
||||||
return self.settings['acceleration'].get(axis, 5.0)
|
|
||||||
|
|
||||||
def set_acceleration(self, axis: str, acceleration: float):
|
|
||||||
"""Set acceleration for specific axis"""
|
|
||||||
if axis in ['x_axis', 'y_axis', 'z_axis']:
|
|
||||||
self.settings['acceleration'][axis] = acceleration
|
|
||||||
|
|
||||||
def get_all_accelerations(self) -> Dict[str, float]:
|
|
||||||
"""Get all axis accelerations"""
|
|
||||||
return self.settings['acceleration'].copy()
|
|
||||||
|
|
||||||
def set_all_accelerations(self, x: float, y: float, z: float):
|
|
||||||
"""Set all axis accelerations"""
|
|
||||||
self.settings['acceleration']['x_axis'] = x
|
|
||||||
self.settings['acceleration']['y_axis'] = y
|
|
||||||
self.settings['acceleration']['z_axis'] = z
|
|
||||||
|
|
||||||
# ==================== Trigger Settings ====================
|
|
||||||
|
|
||||||
def get_trigger_config(self, axis: str) -> Dict:
|
|
||||||
"""Get trigger configuration for specific axis"""
|
|
||||||
return self.settings['trigger'].get(axis, {}).copy()
|
|
||||||
|
|
||||||
def set_trigger_config(self, axis: str, mode: int, polarity: int = 0x01,
|
|
||||||
start_pos_fwd: float = 0.0, start_pos_rev: float = 0.0,
|
|
||||||
interval_fwd: float = 0.0, interval_rev: float = 0.0):
|
|
||||||
"""Set trigger configuration for specific axis"""
|
|
||||||
if axis in ['x_axis', 'y_axis', 'z_axis']:
|
|
||||||
self.settings['trigger'][axis] = {
|
|
||||||
'mode': mode,
|
|
||||||
'polarity': polarity,
|
|
||||||
'start_pos_fwd': start_pos_fwd,
|
|
||||||
'start_pos_rev': start_pos_rev,
|
|
||||||
'interval_fwd': interval_fwd,
|
|
||||||
'interval_rev': interval_rev
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_trigger_mode(self, axis: str) -> int:
|
|
||||||
"""Get trigger mode for specific axis"""
|
|
||||||
return self.settings['trigger'].get(axis, {}).get('mode', 0x00)
|
|
||||||
|
|
||||||
def set_trigger_mode(self, axis: str, mode: int):
|
|
||||||
"""Set trigger mode for specific axis"""
|
|
||||||
if axis in ['x_axis', 'y_axis', 'z_axis']:
|
|
||||||
if axis not in self.settings['trigger']:
|
|
||||||
self.settings['trigger'][axis] = self.DEFAULT_SETTINGS['trigger'][axis].copy()
|
|
||||||
self.settings['trigger'][axis]['mode'] = mode
|
|
||||||
|
|
||||||
# ==================== Digital I/O Settings ====================
|
|
||||||
|
|
||||||
def get_digital_outputs(self) -> int:
|
|
||||||
"""Get digital output bits"""
|
|
||||||
return self.settings['digital_io'].get('output_bits', 0x00)
|
|
||||||
|
|
||||||
def set_digital_outputs(self, output_bits: int):
|
|
||||||
"""Set digital output bits"""
|
|
||||||
self.settings['digital_io']['output_bits'] = output_bits
|
|
||||||
|
|
||||||
# ==================== Utility Methods ====================
|
|
||||||
|
|
||||||
def reset_to_defaults(self):
|
|
||||||
"""Reset all settings to defaults"""
|
|
||||||
self.settings = self.DEFAULT_SETTINGS.copy()
|
|
||||||
|
|
||||||
def get_all_settings(self) -> Dict:
|
|
||||||
"""Get copy of all settings"""
|
|
||||||
return json.loads(json.dumps(self.settings)) # Deep copy via JSON
|
|
||||||
|
|
||||||
def update_from_dict(self, settings_dict: Dict):
|
|
||||||
"""Update settings from dictionary"""
|
|
||||||
self._merge_settings(settings_dict)
|
|
||||||
@@ -1,259 +0,0 @@
|
|||||||
"""
|
|
||||||
T3R-SL Device Controller
|
|
||||||
Handles communication with T3R-SL device via USB/Serial
|
|
||||||
|
|
||||||
The T3R-SL is a specialized instrument control device that provides
|
|
||||||
timing, triggering, and coordination for the SRAS scanning system.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import time
|
|
||||||
from typing import Dict, List, Optional
|
|
||||||
|
|
||||||
|
|
||||||
class T3RDevice:
|
|
||||||
"""
|
|
||||||
Controller for T3R-SL device
|
|
||||||
|
|
||||||
Provides methods for:
|
|
||||||
- Connecting/disconnecting
|
|
||||||
- Device initialization and homing
|
|
||||||
- Status monitoring
|
|
||||||
- Trigger and timing control
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Initialize T3R device controller"""
|
|
||||||
self._connected = False
|
|
||||||
self._port = None
|
|
||||||
self._homed = False
|
|
||||||
self._ready = False
|
|
||||||
|
|
||||||
# Device state
|
|
||||||
self._initialized = False
|
|
||||||
self._error_state = False
|
|
||||||
|
|
||||||
print("INFO: T3R-SL Device controller initialized (stub)")
|
|
||||||
|
|
||||||
def get_available_ports(self) -> List[str]:
|
|
||||||
"""
|
|
||||||
Get list of available COM ports
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list: Available port names
|
|
||||||
"""
|
|
||||||
# Stub implementation - return dummy ports
|
|
||||||
# In production, would scan for actual serial ports
|
|
||||||
return [
|
|
||||||
"COM1", "COM2", "COM3", "COM4", "COM5",
|
|
||||||
"/dev/ttyUSB0", "/dev/ttyUSB1", "/dev/ttyUSB2"
|
|
||||||
]
|
|
||||||
|
|
||||||
def connect(self, port: str) -> bool:
|
|
||||||
"""
|
|
||||||
Connect to T3R device on specified port
|
|
||||||
|
|
||||||
Args:
|
|
||||||
port: COM port name
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if connection successful
|
|
||||||
"""
|
|
||||||
print(f"DEBUG: Connecting to T3R device on {port}")
|
|
||||||
|
|
||||||
# Stub implementation
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
self._port = port
|
|
||||||
self._connected = True
|
|
||||||
self._ready = False # Need to initialize after connect
|
|
||||||
|
|
||||||
print(f"INFO: Connected to T3R device on {port}")
|
|
||||||
|
|
||||||
# Auto-initialize
|
|
||||||
return self._initialize()
|
|
||||||
|
|
||||||
def disconnect(self) -> bool:
|
|
||||||
"""
|
|
||||||
Disconnect from T3R device
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if disconnection successful
|
|
||||||
"""
|
|
||||||
print("DEBUG: Disconnecting from T3R device")
|
|
||||||
|
|
||||||
self._connected = False
|
|
||||||
self._homed = False
|
|
||||||
self._ready = False
|
|
||||||
self._initialized = False
|
|
||||||
|
|
||||||
print("INFO: Disconnected from T3R device")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def is_connected(self) -> bool:
|
|
||||||
"""Check if device is connected"""
|
|
||||||
return self._connected
|
|
||||||
|
|
||||||
def _initialize(self) -> bool:
|
|
||||||
"""
|
|
||||||
Initialize T3R device after connection
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if initialization successful
|
|
||||||
"""
|
|
||||||
print("DEBUG: Initializing T3R device")
|
|
||||||
|
|
||||||
if not self._connected:
|
|
||||||
print("ERROR: Cannot initialize - device not connected")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Stub - simulate initialization
|
|
||||||
time.sleep(0.2)
|
|
||||||
|
|
||||||
self._initialized = True
|
|
||||||
self._error_state = False
|
|
||||||
|
|
||||||
# Perform homing
|
|
||||||
return self.home()
|
|
||||||
|
|
||||||
def home(self) -> bool:
|
|
||||||
"""
|
|
||||||
Home/zero T3R device
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if homing successful
|
|
||||||
"""
|
|
||||||
print("DEBUG: Homing T3R device")
|
|
||||||
|
|
||||||
if not self._connected or not self._initialized:
|
|
||||||
print("ERROR: Cannot home - device not initialized")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Stub - simulate homing
|
|
||||||
time.sleep(0.3)
|
|
||||||
|
|
||||||
self._homed = True
|
|
||||||
self._ready = True
|
|
||||||
|
|
||||||
print("INFO: T3R device homed successfully")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_status(self) -> Dict[str, bool]:
|
|
||||||
"""
|
|
||||||
Get current device status
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Status information
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
'connected': self._connected,
|
|
||||||
'initialized': self._initialized,
|
|
||||||
'homed': self._homed,
|
|
||||||
'ready': self._ready,
|
|
||||||
'error': self._error_state
|
|
||||||
}
|
|
||||||
|
|
||||||
def prepare_for_scan(self, params: Dict) -> bool:
|
|
||||||
"""
|
|
||||||
Prepare T3R device for scanning operation
|
|
||||||
|
|
||||||
Args:
|
|
||||||
params: Scan parameters dictionary
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if preparation successful
|
|
||||||
"""
|
|
||||||
print("DEBUG: Preparing T3R device for scan")
|
|
||||||
print(f" Number of scans: {params.get('num_scans', 1)}")
|
|
||||||
print(f" Trigger voltage: {params.get('trigger_voltage', 0)}V")
|
|
||||||
|
|
||||||
if not self._ready:
|
|
||||||
print("ERROR: T3R device not ready for scanning")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Configure device for scan parameters
|
|
||||||
# Stub implementation
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
print("INFO: T3R device ready for scanning")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def trigger_acquisition(self) -> bool:
|
|
||||||
"""
|
|
||||||
Trigger a data acquisition event
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if trigger successful
|
|
||||||
"""
|
|
||||||
if not self._ready:
|
|
||||||
print("ERROR: Cannot trigger - device not ready")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print("DEBUG: Triggering acquisition")
|
|
||||||
# Stub - would send trigger command
|
|
||||||
return True
|
|
||||||
|
|
||||||
def read_position(self) -> Optional[float]:
|
|
||||||
"""
|
|
||||||
Read current position from T3R device
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: Current position value, or None if error
|
|
||||||
"""
|
|
||||||
if not self._ready:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Stub - return dummy position
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
def set_timing_parameters(self, acquisition_time: float,
|
|
||||||
delay_time: float) -> bool:
|
|
||||||
"""
|
|
||||||
Set timing parameters for acquisition
|
|
||||||
|
|
||||||
Args:
|
|
||||||
acquisition_time: Acquisition window time in seconds
|
|
||||||
delay_time: Delay before acquisition in seconds
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if parameters set successfully
|
|
||||||
"""
|
|
||||||
print(f"DEBUG: Setting timing - acq: {acquisition_time}s, delay: {delay_time}s")
|
|
||||||
|
|
||||||
if not self._connected:
|
|
||||||
print("ERROR: Device not connected")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Stub implementation
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_error_status(self) -> Dict[str, any]:
|
|
||||||
"""
|
|
||||||
Get detailed error status
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Error status information
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
'has_error': self._error_state,
|
|
||||||
'error_code': 0,
|
|
||||||
'error_message': 'No error'
|
|
||||||
}
|
|
||||||
|
|
||||||
def reset(self) -> bool:
|
|
||||||
"""
|
|
||||||
Reset T3R device to initial state
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if reset successful
|
|
||||||
"""
|
|
||||||
print("DEBUG: Resetting T3R device")
|
|
||||||
|
|
||||||
if not self._connected:
|
|
||||||
return False
|
|
||||||
|
|
||||||
self._error_state = False
|
|
||||||
self._homed = False
|
|
||||||
self._ready = False
|
|
||||||
|
|
||||||
# Re-initialize
|
|
||||||
return self._initialize()
|
|
||||||
@@ -1,651 +0,0 @@
|
|||||||
"""
|
|
||||||
ThorLabs MLS Stage Controller
|
|
||||||
Handles communication with ThorLabs MLS 3-axis positioning stage via BBD203 motor controller
|
|
||||||
|
|
||||||
The ThorLabs MLS stage provides precision X/Y/Z positioning for scanning operations.
|
|
||||||
This implementation uses the BBD203 3-channel motor controller with the APT protocol.
|
|
||||||
|
|
||||||
Channel Mapping:
|
|
||||||
- Channel 1: X-axis
|
|
||||||
- Channel 2: Y-axis
|
|
||||||
- Channel 3: Z-axis (optional)
|
|
||||||
|
|
||||||
Copyright (C) 2025 Thomas Ales
|
|
||||||
Licensed under GNU General Public License v2.0
|
|
||||||
"""
|
|
||||||
|
|
||||||
import time
|
|
||||||
from typing import Dict, Optional
|
|
||||||
|
|
||||||
from hardware.bbd203_driver import BBD203Driver
|
|
||||||
from hardware.bbd203_protocol import TriggerMode
|
|
||||||
from hardware.stage_settings import StageSettings
|
|
||||||
|
|
||||||
|
|
||||||
class ThorLabsStage:
|
|
||||||
"""
|
|
||||||
Controller for ThorLabs MLS positioning stage using BBD203 motor controller
|
|
||||||
|
|
||||||
Provides methods for:
|
|
||||||
- Connecting/disconnecting from stage
|
|
||||||
- Homing axes
|
|
||||||
- Position control and readout
|
|
||||||
- Status monitoring
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Channel mapping
|
|
||||||
X_AXIS = 1
|
|
||||||
Y_AXIS = 2
|
|
||||||
Z_AXIS = 3
|
|
||||||
|
|
||||||
def __init__(self, encoder_counts_per_mm: int = 20000, settings_file: Optional[str] = None):
|
|
||||||
"""
|
|
||||||
Initialize ThorLabs stage controller
|
|
||||||
|
|
||||||
Args:
|
|
||||||
encoder_counts_per_mm: Encoder resolution (default: 20000 for MLS203)
|
|
||||||
settings_file: Path to settings file (default: ~/.nuescan/stage_settings.json)
|
|
||||||
"""
|
|
||||||
self._driver = BBD203Driver(encoder_counts_per_mm)
|
|
||||||
self._port = None
|
|
||||||
|
|
||||||
# Settings manager
|
|
||||||
self.settings = StageSettings(settings_file)
|
|
||||||
|
|
||||||
# Scanning state
|
|
||||||
self._scanning = False
|
|
||||||
|
|
||||||
# Default velocity and acceleration (can be overridden by settings)
|
|
||||||
self._default_velocity = 1.0 # mm/s
|
|
||||||
self._default_accel = 5.0 # mm/s²
|
|
||||||
|
|
||||||
print("INFO: ThorLabs Stage controller initialized (BBD203 driver)")
|
|
||||||
|
|
||||||
def connect(self, serial_number: str, baudrate: int = 115200) -> bool:
|
|
||||||
"""
|
|
||||||
Connect to ThorLabs stage via BBD203 controller using serial number
|
|
||||||
|
|
||||||
The serial number is printed on the BBD203 controller (e.g., '83123456').
|
|
||||||
The driver will automatically find the USB device and connect.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
serial_number: BBD203 device serial number
|
|
||||||
baudrate: Baud rate (default: 115200)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if connection successful
|
|
||||||
"""
|
|
||||||
print(f"DEBUG: Connecting to BBD203/MLS Stage with serial number {serial_number}")
|
|
||||||
|
|
||||||
# Connect by serial number - driver will auto-find the port
|
|
||||||
if not self._driver.connect_by_serial(serial_number, baudrate):
|
|
||||||
return False
|
|
||||||
|
|
||||||
self._port = serial_number # Store serial for reference
|
|
||||||
|
|
||||||
# Enable all channels
|
|
||||||
time.sleep(0.2)
|
|
||||||
self._driver.enable_channel(self.X_AXIS, True)
|
|
||||||
time.sleep(0.1)
|
|
||||||
self._driver.enable_channel(self.Y_AXIS, True)
|
|
||||||
time.sleep(0.1)
|
|
||||||
self._driver.enable_channel(self.Z_AXIS, True)
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
# Apply startup settings from configuration
|
|
||||||
self.apply_startup_settings()
|
|
||||||
|
|
||||||
print("INFO: Stage connected and channels enabled")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def disconnect(self) -> bool:
|
|
||||||
"""
|
|
||||||
Disconnect from ThorLabs stage
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if disconnection successful
|
|
||||||
"""
|
|
||||||
self._scanning = False
|
|
||||||
return self._driver.disconnect()
|
|
||||||
|
|
||||||
def is_connected(self) -> bool:
|
|
||||||
"""Check if stage is connected"""
|
|
||||||
return self._driver.is_connected()
|
|
||||||
|
|
||||||
# ==================== Homing ====================
|
|
||||||
|
|
||||||
def home_all_axes(self, wait: bool = True, timeout: float = 60.0) -> bool:
|
|
||||||
"""
|
|
||||||
Home all axes (X, Y, Z)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
wait: If True, block until homing complete
|
|
||||||
timeout: Timeout in seconds
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if homing successful
|
|
||||||
"""
|
|
||||||
print("DEBUG: Homing all axes")
|
|
||||||
|
|
||||||
if not self.is_connected():
|
|
||||||
print("ERROR: Cannot home - stage not connected")
|
|
||||||
return False
|
|
||||||
|
|
||||||
return self._driver.home_all_channels(wait=wait, timeout=timeout)
|
|
||||||
|
|
||||||
def home_axis(self, axis: str, wait: bool = True, timeout: float = 30.0) -> bool:
|
|
||||||
"""
|
|
||||||
Home a specific axis
|
|
||||||
|
|
||||||
Args:
|
|
||||||
axis: Axis to home ('X', 'Y', or 'Z')
|
|
||||||
wait: If True, block until homing complete
|
|
||||||
timeout: Timeout in seconds
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if homing successful
|
|
||||||
"""
|
|
||||||
axis = axis.upper()
|
|
||||||
if axis not in ['X', 'Y', 'Z']:
|
|
||||||
print(f"ERROR: Invalid axis: {axis}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
channel = {'X': self.X_AXIS, 'Y': self.Y_AXIS, 'Z': self.Z_AXIS}[axis]
|
|
||||||
|
|
||||||
print(f"DEBUG: Homing {axis} axis (channel {channel})")
|
|
||||||
|
|
||||||
return self._driver.home_channel(channel, wait=wait, timeout=timeout)
|
|
||||||
|
|
||||||
# ==================== Motion Control ====================
|
|
||||||
|
|
||||||
def move_absolute(self, x: Optional[float] = None,
|
|
||||||
y: Optional[float] = None,
|
|
||||||
z: Optional[float] = None,
|
|
||||||
wait: bool = False) -> bool:
|
|
||||||
"""
|
|
||||||
Move to absolute position
|
|
||||||
|
|
||||||
Args:
|
|
||||||
x: X position in mm (None to leave unchanged)
|
|
||||||
y: Y position in mm (None to leave unchanged)
|
|
||||||
z: Z position in mm (None to leave unchanged)
|
|
||||||
wait: If True, block until move complete
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if move successful
|
|
||||||
"""
|
|
||||||
if not self.is_connected():
|
|
||||||
print("ERROR: Stage not connected")
|
|
||||||
return False
|
|
||||||
|
|
||||||
success = True
|
|
||||||
|
|
||||||
# Move each axis that was specified
|
|
||||||
if x is not None:
|
|
||||||
if not self._driver.move_absolute(self.X_AXIS, x, wait=wait):
|
|
||||||
success = False
|
|
||||||
|
|
||||||
if y is not None:
|
|
||||||
if not self._driver.move_absolute(self.Y_AXIS, y, wait=wait):
|
|
||||||
success = False
|
|
||||||
|
|
||||||
if z is not None:
|
|
||||||
if not self._driver.move_absolute(self.Z_AXIS, z, wait=wait):
|
|
||||||
success = False
|
|
||||||
|
|
||||||
return success
|
|
||||||
|
|
||||||
def move_relative(self, dx: float = 0.0, dy: float = 0.0, dz: float = 0.0,
|
|
||||||
wait: bool = False) -> bool:
|
|
||||||
"""
|
|
||||||
Move relative to current position
|
|
||||||
|
|
||||||
Args:
|
|
||||||
dx: X displacement in mm
|
|
||||||
dy: Y displacement in mm
|
|
||||||
dz: Z displacement in mm
|
|
||||||
wait: If True, block until move complete
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if move successful
|
|
||||||
"""
|
|
||||||
if not self.is_connected():
|
|
||||||
print("ERROR: Stage not connected")
|
|
||||||
return False
|
|
||||||
|
|
||||||
success = True
|
|
||||||
|
|
||||||
if dx != 0.0:
|
|
||||||
if not self._driver.move_relative(self.X_AXIS, dx, wait=wait):
|
|
||||||
success = False
|
|
||||||
|
|
||||||
if dy != 0.0:
|
|
||||||
if not self._driver.move_relative(self.Y_AXIS, dy, wait=wait):
|
|
||||||
success = False
|
|
||||||
|
|
||||||
if dz != 0.0:
|
|
||||||
if not self._driver.move_relative(self.Z_AXIS, dz, wait=wait):
|
|
||||||
success = False
|
|
||||||
|
|
||||||
return success
|
|
||||||
|
|
||||||
def stop_all(self, immediate: bool = True) -> bool:
|
|
||||||
"""
|
|
||||||
Stop all motion
|
|
||||||
|
|
||||||
Args:
|
|
||||||
immediate: If True, stop immediately; if False, decelerate
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if stop successful
|
|
||||||
"""
|
|
||||||
return self._driver.stop(0, immediate) # Channel 0 = all channels
|
|
||||||
|
|
||||||
# ==================== Position and Status ====================
|
|
||||||
|
|
||||||
def get_position(self) -> Dict[str, float]:
|
|
||||||
"""
|
|
||||||
Get current position
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Current X, Y, Z positions in mm
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
'x': self._driver.get_position(self.X_AXIS) or 0.0,
|
|
||||||
'y': self._driver.get_position(self.Y_AXIS) or 0.0,
|
|
||||||
'z': self._driver.get_position(self.Z_AXIS) or 0.0
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_status(self) -> Dict[str, bool]:
|
|
||||||
"""
|
|
||||||
Get current stage status
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Status information compatible with main_window expectations
|
|
||||||
"""
|
|
||||||
x_status = self._driver.get_channel_status(self.X_AXIS) or {}
|
|
||||||
y_status = self._driver.get_channel_status(self.Y_AXIS) or {}
|
|
||||||
z_status = self._driver.get_channel_status(self.Z_AXIS) or {}
|
|
||||||
|
|
||||||
# Determine if any axis is moving
|
|
||||||
moving = (x_status.get('moving', False) or
|
|
||||||
y_status.get('moving', False) or
|
|
||||||
z_status.get('moving', False))
|
|
||||||
|
|
||||||
# Determine if stage is ready (all enabled axes are homed)
|
|
||||||
x_ready = x_status.get('enabled', False) and x_status.get('homed', False)
|
|
||||||
y_ready = y_status.get('enabled', False) and y_status.get('homed', False)
|
|
||||||
ready = x_ready and y_ready # Z is optional
|
|
||||||
|
|
||||||
return {
|
|
||||||
'connected': self.is_connected(),
|
|
||||||
'x_homed': x_status.get('homed', False),
|
|
||||||
'y_homed': y_status.get('homed', False),
|
|
||||||
'z_homed': z_status.get('homed', False),
|
|
||||||
'ready': ready,
|
|
||||||
'scanning': self._scanning,
|
|
||||||
'moving': moving
|
|
||||||
}
|
|
||||||
|
|
||||||
# ==================== Velocity Control ====================
|
|
||||||
|
|
||||||
def set_velocity(self, velocity_mm_s: float, accel_mm_s2: float,
|
|
||||||
axis: Optional[str] = None) -> bool:
|
|
||||||
"""
|
|
||||||
Set velocity and acceleration parameters
|
|
||||||
|
|
||||||
Args:
|
|
||||||
velocity_mm_s: Maximum velocity in mm/s
|
|
||||||
accel_mm_s2: Acceleration in mm/s²
|
|
||||||
axis: Specific axis ('X', 'Y', 'Z'), or None for all axes
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if parameters set successfully
|
|
||||||
"""
|
|
||||||
if axis:
|
|
||||||
axis = axis.upper()
|
|
||||||
if axis not in ['X', 'Y', 'Z']:
|
|
||||||
print(f"ERROR: Invalid axis: {axis}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
channel = {'X': self.X_AXIS, 'Y': self.Y_AXIS, 'Z': self.Z_AXIS}[axis]
|
|
||||||
return self._driver.set_velocity_params(channel, velocity_mm_s, accel_mm_s2)
|
|
||||||
else:
|
|
||||||
# Set for all axes
|
|
||||||
success = True
|
|
||||||
for channel in [self.X_AXIS, self.Y_AXIS, self.Z_AXIS]:
|
|
||||||
if not self._driver.set_velocity_params(channel, velocity_mm_s, accel_mm_s2):
|
|
||||||
success = False
|
|
||||||
time.sleep(0.05)
|
|
||||||
return success
|
|
||||||
|
|
||||||
# ==================== Scan Support ====================
|
|
||||||
|
|
||||||
def prepare_for_scan(self, params: Dict) -> bool:
|
|
||||||
"""
|
|
||||||
Prepare stage for scanning operation
|
|
||||||
|
|
||||||
Args:
|
|
||||||
params: Scan parameters dictionary
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if preparation successful
|
|
||||||
"""
|
|
||||||
print("DEBUG: Preparing stage for scan")
|
|
||||||
|
|
||||||
status = self.get_status()
|
|
||||||
if not status['ready']:
|
|
||||||
print("ERROR: Stage not ready for scanning")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Move to start position
|
|
||||||
x_start = params.get('x_start', 0)
|
|
||||||
y_start = params.get('y_start', 0)
|
|
||||||
|
|
||||||
print(f" Moving to scan start: X={x_start} mm, Y={y_start} mm")
|
|
||||||
|
|
||||||
if not self.move_absolute(x=x_start, y=y_start, wait=True):
|
|
||||||
print("ERROR: Failed to move to start position")
|
|
||||||
return False
|
|
||||||
|
|
||||||
self._scanning = True
|
|
||||||
print("INFO: Stage ready for scanning")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def stop_scan(self) -> bool:
|
|
||||||
"""
|
|
||||||
Stop current scan operation
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if stop successful
|
|
||||||
"""
|
|
||||||
print("DEBUG: Stopping scan")
|
|
||||||
self._scanning = False
|
|
||||||
return self.stop_all(immediate=True)
|
|
||||||
|
|
||||||
# ==================== Utility Methods ====================
|
|
||||||
|
|
||||||
def identify(self, channel: Optional[int] = None) -> bool:
|
|
||||||
"""
|
|
||||||
Flash front panel LEDs to identify controller
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel: Specific channel (1, 2, 3), or None for all
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if command sent successfully
|
|
||||||
"""
|
|
||||||
if channel is None:
|
|
||||||
# Identify all channels
|
|
||||||
for ch in [1, 2, 3]:
|
|
||||||
self._driver.identify(ch)
|
|
||||||
time.sleep(0.1)
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return self._driver.identify(channel)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def list_available_ports():
|
|
||||||
"""
|
|
||||||
List available serial ports (deprecated - use list_devices instead)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list: Available port names
|
|
||||||
"""
|
|
||||||
return BBD203Driver.list_available_ports()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def list_devices():
|
|
||||||
"""
|
|
||||||
List all connected ThorLabs BBD203 devices
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list: List of dicts with device info including 'serial' and 'port'
|
|
||||||
"""
|
|
||||||
return BBD203Driver.list_thorlabs_devices()
|
|
||||||
|
|
||||||
# ==================== Settings Management ====================
|
|
||||||
|
|
||||||
def apply_startup_settings(self) -> bool:
|
|
||||||
"""
|
|
||||||
Apply saved settings to the stage on startup
|
|
||||||
|
|
||||||
This includes:
|
|
||||||
- Velocity parameters for all axes
|
|
||||||
- Acceleration parameters for all axes
|
|
||||||
- Trigger configuration for all axes
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if all settings applied successfully
|
|
||||||
"""
|
|
||||||
print("INFO: Applying startup settings to stage")
|
|
||||||
|
|
||||||
success = True
|
|
||||||
|
|
||||||
# Apply velocity and acceleration settings
|
|
||||||
velocities = self.settings.get_all_velocities()
|
|
||||||
accelerations = self.settings.get_all_accelerations()
|
|
||||||
|
|
||||||
print(f" Velocity settings: X={velocities['x_axis']} mm/s, "
|
|
||||||
f"Y={velocities['y_axis']} mm/s, Z={velocities['z_axis']} mm/s")
|
|
||||||
print(f" Acceleration settings: X={accelerations['x_axis']} mm/s², "
|
|
||||||
f"Y={accelerations['y_axis']} mm/s², Z={accelerations['z_axis']} mm/s²")
|
|
||||||
|
|
||||||
# Set velocity/acceleration for each axis
|
|
||||||
if not self._driver.set_velocity_params(
|
|
||||||
self.X_AXIS, velocities['x_axis'], accelerations['x_axis']
|
|
||||||
):
|
|
||||||
success = False
|
|
||||||
time.sleep(0.05)
|
|
||||||
|
|
||||||
if not self._driver.set_velocity_params(
|
|
||||||
self.Y_AXIS, velocities['y_axis'], accelerations['y_axis']
|
|
||||||
):
|
|
||||||
success = False
|
|
||||||
time.sleep(0.05)
|
|
||||||
|
|
||||||
if not self._driver.set_velocity_params(
|
|
||||||
self.Z_AXIS, velocities['z_axis'], accelerations['z_axis']
|
|
||||||
):
|
|
||||||
success = False
|
|
||||||
time.sleep(0.05)
|
|
||||||
|
|
||||||
# Apply trigger configuration for each axis
|
|
||||||
for axis_name, channel in [('x_axis', self.X_AXIS),
|
|
||||||
('y_axis', self.Y_AXIS),
|
|
||||||
('z_axis', self.Z_AXIS)]:
|
|
||||||
trigger_config = self.settings.get_trigger_config(axis_name)
|
|
||||||
|
|
||||||
if not self._driver.set_trigger_mode(
|
|
||||||
channel,
|
|
||||||
trigger_config['mode'],
|
|
||||||
trigger_config['polarity'],
|
|
||||||
trigger_config['start_pos_fwd'],
|
|
||||||
trigger_config['start_pos_rev'],
|
|
||||||
trigger_config['interval_fwd'],
|
|
||||||
trigger_config['interval_rev']
|
|
||||||
):
|
|
||||||
success = False
|
|
||||||
time.sleep(0.05)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
print("INFO: All startup settings applied successfully")
|
|
||||||
else:
|
|
||||||
print("WARNING: Some startup settings failed to apply")
|
|
||||||
|
|
||||||
return success
|
|
||||||
|
|
||||||
def save_current_settings(self) -> bool:
|
|
||||||
"""
|
|
||||||
Save current settings to file
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if saved successfully
|
|
||||||
"""
|
|
||||||
return self.settings.save()
|
|
||||||
|
|
||||||
def reload_settings(self) -> bool:
|
|
||||||
"""
|
|
||||||
Reload settings from file
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if reloaded successfully
|
|
||||||
"""
|
|
||||||
return self.settings.load()
|
|
||||||
|
|
||||||
def configure_velocity(self, x: Optional[float] = None,
|
|
||||||
y: Optional[float] = None,
|
|
||||||
z: Optional[float] = None,
|
|
||||||
save: bool = True) -> bool:
|
|
||||||
"""
|
|
||||||
Configure velocity for one or more axes
|
|
||||||
|
|
||||||
Args:
|
|
||||||
x: X-axis velocity in mm/s (None to keep current)
|
|
||||||
y: Y-axis velocity in mm/s (None to keep current)
|
|
||||||
z: Z-axis velocity in mm/s (None to keep current)
|
|
||||||
save: Save settings to file after updating
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if configuration successful
|
|
||||||
"""
|
|
||||||
success = True
|
|
||||||
|
|
||||||
if x is not None:
|
|
||||||
self.settings.set_velocity('x_axis', x)
|
|
||||||
accel = self.settings.get_acceleration('x_axis')
|
|
||||||
if self.is_connected():
|
|
||||||
success &= self._driver.set_velocity_params(self.X_AXIS, x, accel)
|
|
||||||
time.sleep(0.05)
|
|
||||||
|
|
||||||
if y is not None:
|
|
||||||
self.settings.set_velocity('y_axis', y)
|
|
||||||
accel = self.settings.get_acceleration('y_axis')
|
|
||||||
if self.is_connected():
|
|
||||||
success &= self._driver.set_velocity_params(self.Y_AXIS, y, accel)
|
|
||||||
time.sleep(0.05)
|
|
||||||
|
|
||||||
if z is not None:
|
|
||||||
self.settings.set_velocity('z_axis', z)
|
|
||||||
accel = self.settings.get_acceleration('z_axis')
|
|
||||||
if self.is_connected():
|
|
||||||
success &= self._driver.set_velocity_params(self.Z_AXIS, z, accel)
|
|
||||||
time.sleep(0.05)
|
|
||||||
|
|
||||||
if save:
|
|
||||||
self.settings.save()
|
|
||||||
|
|
||||||
return success
|
|
||||||
|
|
||||||
def configure_acceleration(self, x: Optional[float] = None,
|
|
||||||
y: Optional[float] = None,
|
|
||||||
z: Optional[float] = None,
|
|
||||||
save: bool = True) -> bool:
|
|
||||||
"""
|
|
||||||
Configure acceleration for one or more axes
|
|
||||||
|
|
||||||
Args:
|
|
||||||
x: X-axis acceleration in mm/s² (None to keep current)
|
|
||||||
y: Y-axis acceleration in mm/s² (None to keep current)
|
|
||||||
z: Z-axis acceleration in mm/s² (None to keep current)
|
|
||||||
save: Save settings to file after updating
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if configuration successful
|
|
||||||
"""
|
|
||||||
success = True
|
|
||||||
|
|
||||||
if x is not None:
|
|
||||||
self.settings.set_acceleration('x_axis', x)
|
|
||||||
vel = self.settings.get_velocity('x_axis')
|
|
||||||
if self.is_connected():
|
|
||||||
success &= self._driver.set_velocity_params(self.X_AXIS, vel, x)
|
|
||||||
time.sleep(0.05)
|
|
||||||
|
|
||||||
if y is not None:
|
|
||||||
self.settings.set_acceleration('y_axis', y)
|
|
||||||
vel = self.settings.get_velocity('y_axis')
|
|
||||||
if self.is_connected():
|
|
||||||
success &= self._driver.set_velocity_params(self.Y_AXIS, vel, y)
|
|
||||||
time.sleep(0.05)
|
|
||||||
|
|
||||||
if z is not None:
|
|
||||||
self.settings.set_acceleration('z_axis', z)
|
|
||||||
vel = self.settings.get_velocity('z_axis')
|
|
||||||
if self.is_connected():
|
|
||||||
success &= self._driver.set_velocity_params(self.Z_AXIS, vel, z)
|
|
||||||
time.sleep(0.05)
|
|
||||||
|
|
||||||
if save:
|
|
||||||
self.settings.save()
|
|
||||||
|
|
||||||
return success
|
|
||||||
|
|
||||||
def configure_trigger(self, axis: str, mode: int,
|
|
||||||
polarity: int = 0x01,
|
|
||||||
start_pos_fwd: float = 0.0,
|
|
||||||
start_pos_rev: float = 0.0,
|
|
||||||
interval_fwd: float = 0.0,
|
|
||||||
interval_rev: float = 0.0,
|
|
||||||
save: bool = True) -> bool:
|
|
||||||
"""
|
|
||||||
Configure trigger for specific axis
|
|
||||||
|
|
||||||
Args:
|
|
||||||
axis: Axis name ('X', 'Y', or 'Z')
|
|
||||||
mode: Trigger mode (TriggerMode enum value)
|
|
||||||
polarity: Trigger polarity (0x01 = active high, 0x02 = active low)
|
|
||||||
start_pos_fwd: Start position for forward trigger (mm)
|
|
||||||
start_pos_rev: Start position for reverse trigger (mm)
|
|
||||||
interval_fwd: Interval for forward trigger (mm)
|
|
||||||
interval_rev: Interval for reverse trigger (mm)
|
|
||||||
save: Save settings to file after updating
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if configuration successful
|
|
||||||
"""
|
|
||||||
axis = axis.upper()
|
|
||||||
if axis not in ['X', 'Y', 'Z']:
|
|
||||||
print(f"ERROR: Invalid axis: {axis}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
axis_name = f"{axis.lower()}_axis"
|
|
||||||
channel = {'X': self.X_AXIS, 'Y': self.Y_AXIS, 'Z': self.Z_AXIS}[axis]
|
|
||||||
|
|
||||||
# Update settings
|
|
||||||
self.settings.set_trigger_config(
|
|
||||||
axis_name, mode, polarity,
|
|
||||||
start_pos_fwd, start_pos_rev,
|
|
||||||
interval_fwd, interval_rev
|
|
||||||
)
|
|
||||||
|
|
||||||
# Apply to hardware if connected
|
|
||||||
success = True
|
|
||||||
if self.is_connected():
|
|
||||||
success = self._driver.set_trigger_mode(
|
|
||||||
channel, mode, polarity,
|
|
||||||
start_pos_fwd, start_pos_rev,
|
|
||||||
interval_fwd, interval_rev
|
|
||||||
)
|
|
||||||
|
|
||||||
if save:
|
|
||||||
self.settings.save()
|
|
||||||
|
|
||||||
return success
|
|
||||||
|
|
||||||
def get_detailed_status(self) -> Dict:
|
|
||||||
"""
|
|
||||||
Get detailed status of all channels
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Detailed status information
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
'connected': self.is_connected(),
|
|
||||||
'scanning': self._scanning,
|
|
||||||
'x_axis': self._driver.get_channel_status(self.X_AXIS),
|
|
||||||
'y_axis': self._driver.get_channel_status(self.Y_AXIS),
|
|
||||||
'z_axis': self._driver.get_channel_status(self.Z_AXIS),
|
|
||||||
'position': self.get_position(),
|
|
||||||
'settings': self.settings.get_all_settings()
|
|
||||||
}
|
|
||||||
@@ -1,411 +0,0 @@
|
|||||||
"""
|
|
||||||
nueScan - Main Window Controller
|
|
||||||
Handles all UI interactions and coordinates hardware communication
|
|
||||||
|
|
||||||
Copyright (C) 2025 Thomas Ales
|
|
||||||
Licensed under GNU General Public License v2.0
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from PyQt6 import uic
|
|
||||||
from PyQt6.QtWidgets import QMainWindow, QMessageBox
|
|
||||||
from PyQt6.QtCore import QTimer
|
|
||||||
|
|
||||||
# Import dialog controllers
|
|
||||||
from dialogs.genesis_dialog import GenesisDialog
|
|
||||||
from dialogs.helios_dialog import HeliosDialog
|
|
||||||
from dialogs.scan_active_dialog import ScanActiveDialog
|
|
||||||
from dialogs.status_dialog import StatusDialog
|
|
||||||
from dialogs.oscope_dialog import OscopeDialog
|
|
||||||
|
|
||||||
# Import hardware controllers
|
|
||||||
from hardware.thorlabs_stage import ThorLabsStage
|
|
||||||
from hardware.t3r_device import T3RDevice
|
|
||||||
from hardware.microscope import MicroscopeController
|
|
||||||
|
|
||||||
|
|
||||||
class NueScanMainWindow(QMainWindow):
|
|
||||||
"""Main window for nueScan application"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
# Load UI file
|
|
||||||
ui_path = os.path.join(os.path.dirname(__file__), 'nuescan_mainwindow.ui')
|
|
||||||
uic.loadUi(ui_path, self)
|
|
||||||
|
|
||||||
# Set window title
|
|
||||||
self.setWindowTitle("nueScan - SRAS Scan Planning and Control")
|
|
||||||
|
|
||||||
# Initialize hardware controllers
|
|
||||||
self.thorlabs_stage = ThorLabsStage()
|
|
||||||
self.t3r_device = T3RDevice()
|
|
||||||
self.microscope = MicroscopeController()
|
|
||||||
|
|
||||||
# Initialize dialogs (create on demand)
|
|
||||||
self.genesis_dialog = None
|
|
||||||
self.helios_dialog = None
|
|
||||||
self.scan_active_dialog = None
|
|
||||||
self.status_dialog = StatusDialog(self, self.thorlabs_stage, self.t3r_device, self.microscope)
|
|
||||||
self.oscope_dialog = OscopeDialog(self, self.microscope)
|
|
||||||
|
|
||||||
# Status update timer
|
|
||||||
self.status_timer = QTimer()
|
|
||||||
self.status_timer.timeout.connect(self._update_all_status)
|
|
||||||
self.status_timer.start(100) # Update every 100ms
|
|
||||||
|
|
||||||
# Connect all UI signals
|
|
||||||
self._connect_signals()
|
|
||||||
|
|
||||||
# Initialize UI state
|
|
||||||
self._initialize_ui()
|
|
||||||
|
|
||||||
def _connect_signals(self):
|
|
||||||
"""Connect all UI signals to handler methods"""
|
|
||||||
|
|
||||||
# ===== Button Click Handlers =====
|
|
||||||
self.btn_toggle_mls.clicked.connect(self.on_toggle_mls_clicked)
|
|
||||||
self.btn_refresh_com.clicked.connect(self.on_refresh_com_clicked)
|
|
||||||
self.button_connect_com.clicked.connect(self.on_connect_com_clicked)
|
|
||||||
self.btn_begin_scanning.clicked.connect(self.on_begin_scanning_clicked)
|
|
||||||
self.btn_toggle_status_window.clicked.connect(self.on_toggle_status_window_clicked)
|
|
||||||
self.actionShow_Oscope_Settings.triggered.connect(self.on_show_oscope_settings_clicked)
|
|
||||||
self.actionDigital_IO_State.triggered.connect(self.on_show_digital_io_state_clicked)
|
|
||||||
self.actionMLS203_Information.triggered.connect(self.on_show_mls203_information_clicked)
|
|
||||||
self.actionTransfer_System_Editor.triggered.connect(self.on_show_transfer_system_editor_clicked)
|
|
||||||
self.actionHelios_Settings.triggered.connect(self.on_show_helios_settings_clicked)
|
|
||||||
self.actionGenesis_Settings.triggered.connect(self.on_show_genesis_settings_clicked)
|
|
||||||
|
|
||||||
# ===== LineEdit Text Changed Handlers =====
|
|
||||||
self.le_stage_serial.textChanged.connect(self.on_stage_serial_changed)
|
|
||||||
self.le_x_start_coord.textChanged.connect(self.on_x_start_coord_changed)
|
|
||||||
self.le_x_delta.textChanged.connect(self.on_x_delta_changed)
|
|
||||||
self.le_y_start_coord.textChanged.connect(self.on_y_start_coord_changed)
|
|
||||||
self.le_y_delta.textChanged.connect(self.on_y_delta_changed)
|
|
||||||
self.le_file_suffix.textChanged.connect(self.on_file_suffix_changed)
|
|
||||||
|
|
||||||
# ===== ComboBox Value Changed Handlers =====
|
|
||||||
self.cb_num_scans.currentIndexChanged.connect(self.on_num_scans_changed)
|
|
||||||
self.cb_row_spacing.currentIndexChanged.connect(self.on_row_spacing_changed)
|
|
||||||
|
|
||||||
def _initialize_ui(self):
|
|
||||||
"""Initialize UI with default values"""
|
|
||||||
# Set placeholder text for stage serial
|
|
||||||
self.le_stage_serial.setPlaceholderText("Enter BBD203 serial (e.g., 83123456)")
|
|
||||||
|
|
||||||
# Refresh COM ports
|
|
||||||
self.on_refresh_com_clicked()
|
|
||||||
|
|
||||||
# ==================== Button Click Handlers ====================
|
|
||||||
|
|
||||||
def on_toggle_status_window_clicked(self):
|
|
||||||
"""Toggle the visibility of the status window"""
|
|
||||||
if self.status_dialog.isVisible():
|
|
||||||
self.status_dialog.hide()
|
|
||||||
else:
|
|
||||||
self.status_dialog.show()
|
|
||||||
|
|
||||||
def on_show_oscope_settings_clicked(self):
|
|
||||||
"""Toggle the visibility of the oscope settings window"""
|
|
||||||
if self.oscope_dialog.isVisible():
|
|
||||||
self.oscope_dialog.hide()
|
|
||||||
else:
|
|
||||||
self.oscope_dialog.show()
|
|
||||||
|
|
||||||
def on_show_digital_io_state_clicked(self):
|
|
||||||
"""Show the digital IO state dialog"""
|
|
||||||
print("DEBUG: Show digital IO state clicked")
|
|
||||||
|
|
||||||
def on_show_mls203_information_clicked(self):
|
|
||||||
"""Show the MLS203 information dialog"""
|
|
||||||
print("DEBUG: Show MLS203 information clicked")
|
|
||||||
|
|
||||||
def on_show_transfer_system_editor_clicked(self):
|
|
||||||
"""Show the transfer system editor dialog"""
|
|
||||||
print("DEBUG: Show transfer system editor clicked")
|
|
||||||
|
|
||||||
def on_toggle_mls_clicked(self):
|
|
||||||
"""Handle ThorLabs MLS stage connect/disconnect"""
|
|
||||||
print("DEBUG: MLS toggle button clicked")
|
|
||||||
if self.thorlabs_stage.is_connected():
|
|
||||||
self.thorlabs_stage.disconnect()
|
|
||||||
self.btn_toggle_mls.setText("Connect")
|
|
||||||
else:
|
|
||||||
serial_number = self.le_stage_serial.text().strip()
|
|
||||||
if not serial_number:
|
|
||||||
QMessageBox.warning(
|
|
||||||
self, "No Serial Number",
|
|
||||||
"Please enter the BBD203 serial number.\n\n"
|
|
||||||
"The serial number is printed on the controller label\n"
|
|
||||||
"(e.g., '83123456')."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
print(f"INFO: Attempting to connect to BBD203 serial: {serial_number}")
|
|
||||||
success = self.thorlabs_stage.connect(serial_number)
|
|
||||||
if success:
|
|
||||||
self.btn_toggle_mls.setText("Disconnect")
|
|
||||||
QMessageBox.information(
|
|
||||||
self, "Connected",
|
|
||||||
f"Successfully connected to BBD203 controller\n"
|
|
||||||
f"Serial: {serial_number}\n\n"
|
|
||||||
f"All channels enabled. Ready to home axes."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Show available devices
|
|
||||||
devices = self.thorlabs_stage.list_devices()
|
|
||||||
if devices:
|
|
||||||
device_list = "\n".join([
|
|
||||||
f" Serial: {d['serial']} ({d['description']})"
|
|
||||||
for d in devices
|
|
||||||
])
|
|
||||||
msg = (f"Failed to connect to BBD203 with serial: {serial_number}\n\n"
|
|
||||||
f"Available ThorLabs devices:\n{device_list}")
|
|
||||||
else:
|
|
||||||
msg = (f"Failed to connect to BBD203 with serial: {serial_number}\n\n"
|
|
||||||
f"No ThorLabs devices found.\n"
|
|
||||||
f"Check USB connection and driver installation.")
|
|
||||||
|
|
||||||
QMessageBox.warning(self, "Connection Error", msg)
|
|
||||||
|
|
||||||
def on_refresh_com_clicked(self):
|
|
||||||
"""Refresh available COM ports"""
|
|
||||||
print("DEBUG: Refresh COM ports clicked")
|
|
||||||
self.combo_com_ports.clear()
|
|
||||||
ports = self.t3r_device.get_available_ports()
|
|
||||||
self.combo_com_ports.addItems(ports)
|
|
||||||
|
|
||||||
def on_connect_com_clicked(self):
|
|
||||||
"""Connect to selected COM port"""
|
|
||||||
print("DEBUG: Connect COM button clicked")
|
|
||||||
port = self.combo_com_ports.currentText()
|
|
||||||
if port:
|
|
||||||
success = self.t3r_device.connect(port)
|
|
||||||
if success:
|
|
||||||
self.button_connect_com.setText("Disconnect")
|
|
||||||
else:
|
|
||||||
QMessageBox.warning(self, "Connection Error", f"Failed to connect to {port}")
|
|
||||||
else:
|
|
||||||
QMessageBox.warning(self, "No Port Selected", "Please select a COM port")
|
|
||||||
|
|
||||||
def on_show_helios_settings_clicked(self):
|
|
||||||
"""Show Helios settings dialog"""
|
|
||||||
print("DEBUG: Show Helios settings clicked")
|
|
||||||
if not self.helios_dialog:
|
|
||||||
self.helios_dialog = HeliosDialog(self)
|
|
||||||
|
|
||||||
if self.helios_dialog.exec():
|
|
||||||
# User clicked OK, apply settings
|
|
||||||
settings = self.helios_dialog.get_settings()
|
|
||||||
self.microscope.apply_helios_settings(settings)
|
|
||||||
print(f"DEBUG: Applied Helios settings: {settings}")
|
|
||||||
|
|
||||||
def on_show_genesis_settings_clicked(self):
|
|
||||||
"""Show Genesis settings dialog"""
|
|
||||||
print("DEBUG: Show Genesis settings clicked")
|
|
||||||
if not self.genesis_dialog:
|
|
||||||
self.genesis_dialog = GenesisDialog(self)
|
|
||||||
|
|
||||||
if self.genesis_dialog.exec():
|
|
||||||
# User clicked OK, apply settings
|
|
||||||
settings = self.genesis_dialog.get_settings()
|
|
||||||
self.microscope.apply_genesis_settings(settings)
|
|
||||||
print(f"DEBUG: Applied Genesis settings: {settings}")
|
|
||||||
|
|
||||||
|
|
||||||
def on_begin_scanning_clicked(self):
|
|
||||||
"""Start the scanning process"""
|
|
||||||
print("DEBUG: Begin scanning clicked")
|
|
||||||
|
|
||||||
# Validate that all systems are ready
|
|
||||||
if not self._validate_scan_ready():
|
|
||||||
return
|
|
||||||
|
|
||||||
# Create and show scan active dialog
|
|
||||||
if not self.scan_active_dialog:
|
|
||||||
self.scan_active_dialog = ScanActiveDialog(self)
|
|
||||||
|
|
||||||
# Start the scan
|
|
||||||
self._start_scan()
|
|
||||||
|
|
||||||
# Show progress dialog
|
|
||||||
self.scan_active_dialog.exec()
|
|
||||||
|
|
||||||
# ==================== ComboBox Change Handlers ====================
|
|
||||||
|
|
||||||
def on_num_scans_changed(self, index):
|
|
||||||
"""Handle number of scans change"""
|
|
||||||
num_scans = self.cb_num_scans.currentText()
|
|
||||||
print(f"DEBUG: Number of scans changed to: {num_scans}")
|
|
||||||
self._recalculate_scan_parameters()
|
|
||||||
|
|
||||||
def on_row_spacing_changed(self, index):
|
|
||||||
"""Handle row spacing change"""
|
|
||||||
spacing = self.cb_row_spacing.currentText()
|
|
||||||
print(f"DEBUG: Row spacing changed to: {spacing}")
|
|
||||||
self._recalculate_scan_parameters()
|
|
||||||
|
|
||||||
# ==================== LineEdit Text Changed Handlers ====================
|
|
||||||
|
|
||||||
def on_stage_serial_changed(self, text):
|
|
||||||
"""Handle stage serial number change"""
|
|
||||||
print(f"DEBUG: Stage serial changed to: {text}")
|
|
||||||
|
|
||||||
def on_x_start_coord_changed(self, text):
|
|
||||||
"""Handle X start coordinate change"""
|
|
||||||
print(f"DEBUG: X start coordinate changed to: {text}")
|
|
||||||
self._recalculate_scan_parameters()
|
|
||||||
|
|
||||||
def on_x_delta_changed(self, text):
|
|
||||||
"""Handle X delta change"""
|
|
||||||
print(f"DEBUG: X delta changed to: {text}")
|
|
||||||
self._recalculate_scan_parameters()
|
|
||||||
|
|
||||||
def on_y_start_coord_changed(self, text):
|
|
||||||
"""Handle Y start coordinate change"""
|
|
||||||
print(f"DEBUG: Y start coordinate changed to: {text}")
|
|
||||||
self._recalculate_scan_parameters()
|
|
||||||
|
|
||||||
def on_y_delta_changed(self, text):
|
|
||||||
"""Handle Y delta change"""
|
|
||||||
print(f"DEBUG: Y delta changed to: {text}")
|
|
||||||
self._recalculate_scan_parameters()
|
|
||||||
|
|
||||||
def on_file_suffix_changed(self, text):
|
|
||||||
"""Handle file suffix change"""
|
|
||||||
print(f"DEBUG: File suffix changed to: {text}")
|
|
||||||
|
|
||||||
# ==================== Status Update Methods ====================
|
|
||||||
|
|
||||||
def _update_all_status(self):
|
|
||||||
"""Update all status labels with current hardware states"""
|
|
||||||
self.status_dialog.update_all_status()
|
|
||||||
|
|
||||||
# ==================== Scan Management Methods ====================
|
|
||||||
|
|
||||||
def _validate_scan_ready(self):
|
|
||||||
"""Validate that all systems are ready for scanning"""
|
|
||||||
if not self.thorlabs_stage.is_connected():
|
|
||||||
QMessageBox.warning(
|
|
||||||
self, "Not Ready", "ThorLabs stage is not connected")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not self.t3r_device.is_connected():
|
|
||||||
QMessageBox.warning(self, "Not Ready", "T3R device is not connected")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Add more validation as needed
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _start_scan(self):
|
|
||||||
"""Initialize and start the scanning process"""
|
|
||||||
print("DEBUG: Starting scan process")
|
|
||||||
|
|
||||||
# Collect scan parameters
|
|
||||||
params = self._collect_scan_parameters()
|
|
||||||
|
|
||||||
# Initialize hardware for scanning
|
|
||||||
self.thorlabs_stage.prepare_for_scan(params)
|
|
||||||
self.t3r_device.prepare_for_scan(params)
|
|
||||||
self.microscope.prepare_for_scan(params)
|
|
||||||
|
|
||||||
# Start the scan (would be implemented in actual hardware controllers)
|
|
||||||
print(f"DEBUG: Scan parameters: {params}")
|
|
||||||
|
|
||||||
def _collect_scan_parameters(self):
|
|
||||||
"""Collect all scan parameters from UI"""
|
|
||||||
try:
|
|
||||||
params = {
|
|
||||||
'x_start': float(self.le_x_start_coord.text() or 0),
|
|
||||||
'x_delta': float(self.le_x_delta.text() or 0),
|
|
||||||
'y_start': float(self.le_y_start_coord.text() or 0),
|
|
||||||
'y_delta': float(self.le_y_delta.text() or 0),
|
|
||||||
'num_scans': int(self.cb_num_scans.currentText() or 1),
|
|
||||||
'row_spacing': float(self.cb_row_spacing.currentText() or 0.1),
|
|
||||||
'file_suffix': self.le_file_suffix.text(),
|
|
||||||
'pd_trig_voltage': float(self.oscope_dialog.le_set_pd_trig_voltage.text() or 0),
|
|
||||||
'sample_min_bias_voltage': float(self.oscope_dialog.le_set_sample_thresh_voltage.text() or 0),
|
|
||||||
'trigger_voltage': float(self.oscope_dialog.le_set_trigger_voltage.text() or 0),
|
|
||||||
'visa_address': self.oscope_dialog.le_oscope_visa_address.text(),
|
|
||||||
'phototrigger_channel': self.oscope_dialog.cb_set_trig_channel.currentText(),
|
|
||||||
'bias_a_channel': self.oscope_dialog.cb_set_bias_a_ch.currentText(),
|
|
||||||
'bias_b_channel': self.oscope_dialog.cb_set_bias_b_ch.currentText(),
|
|
||||||
'rf_saw_channel': self.oscope_dialog.cb_set_saw_channel.currentText()
|
|
||||||
}
|
|
||||||
except ValueError:
|
|
||||||
params = {
|
|
||||||
'x_start': 0, 'x_delta': 0, 'y_start': 0, 'y_delta': 0,
|
|
||||||
'num_scans': 1, 'row_spacing': 0.1, 'file_suffix': '',
|
|
||||||
'pd_trig_voltage': 0, 'sample_min_bias_voltage': 0, 'trigger_voltage': 0,
|
|
||||||
'visa_address': '', 'phototrigger_channel': 'CH1', 'bias_a_channel': 'CH1',
|
|
||||||
'bias_b_channel': 'CH1', 'rf_saw_channel': 'CH1'
|
|
||||||
}
|
|
||||||
|
|
||||||
return params
|
|
||||||
|
|
||||||
def _recalculate_scan_parameters(self):
|
|
||||||
"""Recalculate and update scan statistics"""
|
|
||||||
params = self._collect_scan_parameters()
|
|
||||||
|
|
||||||
# Calculate points per row (stub calculation)
|
|
||||||
if params['x_delta'] > 0:
|
|
||||||
points_per_row = int(abs(params['x_start']) / params['x_delta'])
|
|
||||||
else:
|
|
||||||
points_per_row = 0
|
|
||||||
|
|
||||||
# Calculate rows per scan (stub calculation)
|
|
||||||
if params['row_spacing'] > 0 and params['y_delta'] > 0:
|
|
||||||
rows_per_scan = int(abs(params['y_delta']) / params['row_spacing'])
|
|
||||||
else:
|
|
||||||
rows_per_scan = 0
|
|
||||||
|
|
||||||
# Calculate totals
|
|
||||||
scans_in_set = params['num_scans']
|
|
||||||
total_records = points_per_row * rows_per_scan * scans_in_set
|
|
||||||
total_points = total_records
|
|
||||||
|
|
||||||
# Estimate file size (1KB per point as example)
|
|
||||||
estimated_size_gb = (total_points * 1024) / (1024 ** 3)
|
|
||||||
|
|
||||||
# Update labels
|
|
||||||
self.l_scan_ppr.setText(str(points_per_row))
|
|
||||||
self.l_scan_rps.setText(str(rows_per_scan))
|
|
||||||
self.l_scan_sis.setText(str(scans_in_set))
|
|
||||||
self.l_scan_total_records.setText(str(total_records))
|
|
||||||
self.l_scan_total_points.setText(str(total_points))
|
|
||||||
self.l_scan_estimated_size.setText(f"{estimated_size_gb:.2f}GB")
|
|
||||||
|
|
||||||
# Calculate angle spacing
|
|
||||||
if scans_in_set > 1:
|
|
||||||
angle_spacing = 360.0 / scans_in_set
|
|
||||||
else:
|
|
||||||
angle_spacing = 0
|
|
||||||
self.l_scan_angle_spacing.setText(f"{angle_spacing:.2f}°")
|
|
||||||
|
|
||||||
# ==================== Progress Update Methods ====================
|
|
||||||
|
|
||||||
def update_total_progress(self, current, total):
|
|
||||||
"""
|
|
||||||
Update total scan progress
|
|
||||||
Called from scan control logic to update progress bar
|
|
||||||
"""
|
|
||||||
if self.scan_active_dialog:
|
|
||||||
self.scan_active_dialog.update_total_progress(current, total)
|
|
||||||
|
|
||||||
def update_current_scan_progress(self, current, total):
|
|
||||||
"""
|
|
||||||
Update current scan progress
|
|
||||||
Called from scan control logic to update progress bar
|
|
||||||
"""
|
|
||||||
if self.scan_active_dialog:
|
|
||||||
self.scan_active_dialog.update_current_scan_progress(current, total)
|
|
||||||
|
|
||||||
def update_scan_status(self, scan_num, total_scans, row_num, total_rows, time_remaining):
|
|
||||||
"""
|
|
||||||
Update scan status information
|
|
||||||
Called from scan control logic
|
|
||||||
"""
|
|
||||||
if self.scan_active_dialog:
|
|
||||||
self.scan_active_dialog.update_status(
|
|
||||||
scan_num, total_scans, row_num, total_rows, time_remaining
|
|
||||||
)
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<ui version="4.0">
|
|
||||||
<class>Dialog</class>
|
|
||||||
<widget class="QDialog" name="Dialog">
|
|
||||||
<property name="geometry">
|
|
||||||
<rect>
|
|
||||||
<x>0</x>
|
|
||||||
<y>0</y>
|
|
||||||
<width>400</width>
|
|
||||||
<height>168</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<property name="windowTitle">
|
|
||||||
<string>Dialog</string>
|
|
||||||
</property>
|
|
||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
|
||||||
<item>
|
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
|
||||||
<item row="0" column="0">
|
|
||||||
<widget class="QLabel" name="label">
|
|
||||||
<property name="text">
|
|
||||||
<string>Scanning Power [mW]:</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="1">
|
|
||||||
<widget class="QLineEdit" name="le_genesis_power_mw"/>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QDialogButtonBox" name="buttonBox">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="standardButtons">
|
|
||||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
<resources/>
|
|
||||||
<connections>
|
|
||||||
<connection>
|
|
||||||
<sender>buttonBox</sender>
|
|
||||||
<signal>accepted()</signal>
|
|
||||||
<receiver>Dialog</receiver>
|
|
||||||
<slot>accept()</slot>
|
|
||||||
<hints>
|
|
||||||
<hint type="sourcelabel">
|
|
||||||
<x>248</x>
|
|
||||||
<y>254</y>
|
|
||||||
</hint>
|
|
||||||
<hint type="destinationlabel">
|
|
||||||
<x>157</x>
|
|
||||||
<y>274</y>
|
|
||||||
</hint>
|
|
||||||
</hints>
|
|
||||||
</connection>
|
|
||||||
<connection>
|
|
||||||
<sender>buttonBox</sender>
|
|
||||||
<signal>rejected()</signal>
|
|
||||||
<receiver>Dialog</receiver>
|
|
||||||
<slot>reject()</slot>
|
|
||||||
<hints>
|
|
||||||
<hint type="sourcelabel">
|
|
||||||
<x>316</x>
|
|
||||||
<y>260</y>
|
|
||||||
</hint>
|
|
||||||
<hint type="destinationlabel">
|
|
||||||
<x>286</x>
|
|
||||||
<y>274</y>
|
|
||||||
</hint>
|
|
||||||
</hints>
|
|
||||||
</connection>
|
|
||||||
</connections>
|
|
||||||
</ui>
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<ui version="4.0">
|
|
||||||
<class>Dialog</class>
|
|
||||||
<widget class="QDialog" name="Dialog">
|
|
||||||
<property name="geometry">
|
|
||||||
<rect>
|
|
||||||
<x>0</x>
|
|
||||||
<y>0</y>
|
|
||||||
<width>400</width>
|
|
||||||
<height>173</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<property name="windowTitle">
|
|
||||||
<string>Dialog</string>
|
|
||||||
</property>
|
|
||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
|
||||||
<item>
|
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
|
||||||
<item row="2" column="1">
|
|
||||||
<widget class="QLineEdit" name="le_helios_current"/>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="1">
|
|
||||||
<widget class="QLineEdit" name="le_helios_frequency"/>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="0">
|
|
||||||
<widget class="QLabel" name="label">
|
|
||||||
<property name="text">
|
|
||||||
<string>Helios Frequency [Hz]:</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="0">
|
|
||||||
<widget class="QLabel" name="label_2">
|
|
||||||
<property name="text">
|
|
||||||
<string>Laser Current [mA]:</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="0">
|
|
||||||
<widget class="QLabel" name="label_3">
|
|
||||||
<property name="text">
|
|
||||||
<string>Helios COM Port:</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="1">
|
|
||||||
<widget class="QComboBox" name="cb_helios_port"/>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QDialogButtonBox" name="buttonBox">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="standardButtons">
|
|
||||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
<resources/>
|
|
||||||
<connections>
|
|
||||||
<connection>
|
|
||||||
<sender>buttonBox</sender>
|
|
||||||
<signal>accepted()</signal>
|
|
||||||
<receiver>Dialog</receiver>
|
|
||||||
<slot>accept()</slot>
|
|
||||||
<hints>
|
|
||||||
<hint type="sourcelabel">
|
|
||||||
<x>248</x>
|
|
||||||
<y>254</y>
|
|
||||||
</hint>
|
|
||||||
<hint type="destinationlabel">
|
|
||||||
<x>157</x>
|
|
||||||
<y>274</y>
|
|
||||||
</hint>
|
|
||||||
</hints>
|
|
||||||
</connection>
|
|
||||||
<connection>
|
|
||||||
<sender>buttonBox</sender>
|
|
||||||
<signal>rejected()</signal>
|
|
||||||
<receiver>Dialog</receiver>
|
|
||||||
<slot>reject()</slot>
|
|
||||||
<hints>
|
|
||||||
<hint type="sourcelabel">
|
|
||||||
<x>316</x>
|
|
||||||
<y>260</y>
|
|
||||||
</hint>
|
|
||||||
<hint type="destinationlabel">
|
|
||||||
<x>286</x>
|
|
||||||
<y>274</y>
|
|
||||||
</hint>
|
|
||||||
</hints>
|
|
||||||
</connection>
|
|
||||||
</connections>
|
|
||||||
</ui>
|
|
||||||
@@ -1,484 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<ui version="4.0">
|
|
||||||
<class>nueScanWindow</class>
|
|
||||||
<widget class="QMainWindow" name="nueScanWindow">
|
|
||||||
<property name="geometry">
|
|
||||||
<rect>
|
|
||||||
<x>0</x>
|
|
||||||
<y>0</y>
|
|
||||||
<width>830</width>
|
|
||||||
<height>681</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<property name="windowTitle">
|
|
||||||
<string>MainWindow</string>
|
|
||||||
</property>
|
|
||||||
<widget class="QWidget" name="centralwidget">
|
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
|
||||||
<item row="0" column="0">
|
|
||||||
<layout class="QGridLayout" name="topGridLayout">
|
|
||||||
<item row="0" column="0">
|
|
||||||
<widget class="QLabel" name="label">
|
|
||||||
<property name="text">
|
|
||||||
<string>ThorLABS MLS Stage Serial:</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="1" colspan="2">
|
|
||||||
<widget class="QLineEdit" name="le_stage_serial"/>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="3" colspan="2">
|
|
||||||
<widget class="QPushButton" name="btn_toggle_mls">
|
|
||||||
<property name="text">
|
|
||||||
<string>Connect</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="5" colspan="2">
|
|
||||||
<widget class="QPushButton" name="btn_toggle_status_window">
|
|
||||||
<property name="text">
|
|
||||||
<string>Status</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="0">
|
|
||||||
<widget class="QLabel" name="label_2">
|
|
||||||
<property name="text">
|
|
||||||
<string>T3R COM Port:</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="1" colspan="2">
|
|
||||||
<widget class="QComboBox" name="combo_com_ports"/>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="3" colspan="2">
|
|
||||||
<widget class="QPushButton" name="btn_refresh_com">
|
|
||||||
<property name="text">
|
|
||||||
<string>Refresh Serial Devices</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="5" colspan="2">
|
|
||||||
<widget class="QPushButton" name="button_connect_com">
|
|
||||||
<property name="text">
|
|
||||||
<string>Connect</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="0" colspan="7">
|
|
||||||
<widget class="Line" name="line">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="3" column="0" colspan="2">
|
|
||||||
<widget class="QLabel" name="label_44">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>14</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Scan Details and Settings:</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignRight|Qt::AlignTrailing</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="0">
|
|
||||||
<widget class="QLabel" name="label_50">
|
|
||||||
<property name="text">
|
|
||||||
<string>Coordinate and Spacing Settings:</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="5" column="1">
|
|
||||||
<widget class="QLabel" name="label_45">
|
|
||||||
<property name="text">
|
|
||||||
<string>X-Begin [mm]</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="6" column="1">
|
|
||||||
<widget class="QLineEdit" name="le_x_start_coord"/>
|
|
||||||
</item>
|
|
||||||
<item row="5" column="2">
|
|
||||||
<widget class="QLabel" name="label_46">
|
|
||||||
<property name="text">
|
|
||||||
<string>X-Delta [mm]</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="6" column="2">
|
|
||||||
<widget class="QLineEdit" name="le_x_delta"/>
|
|
||||||
</item>
|
|
||||||
<item row="5" column="3">
|
|
||||||
<widget class="QLabel" name="label_47">
|
|
||||||
<property name="text">
|
|
||||||
<string>Y-Begin [mm]</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="6" column="3">
|
|
||||||
<widget class="QLineEdit" name="le_y_start_coord"/>
|
|
||||||
</item>
|
|
||||||
<item row="5" column="4">
|
|
||||||
<widget class="QLabel" name="label_48">
|
|
||||||
<property name="text">
|
|
||||||
<string>Y-Delta [mm]</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="6" column="4">
|
|
||||||
<widget class="QLineEdit" name="le_y_delta"/>
|
|
||||||
</item>
|
|
||||||
<item row="5" column="5">
|
|
||||||
<widget class="QLabel" name="label_49">
|
|
||||||
<property name="text">
|
|
||||||
<string>Row
|
|
||||||
Spacing [mm]:</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="6" column="5">
|
|
||||||
<widget class="QComboBox" name="cb_row_spacing"/>
|
|
||||||
</item>
|
|
||||||
<item row="7" column="0">
|
|
||||||
<widget class="QLabel" name="label_52">
|
|
||||||
<property name="text">
|
|
||||||
<string>Angular Settings:</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="8" column="1">
|
|
||||||
<widget class="QLabel" name="label_53">
|
|
||||||
<property name="text">
|
|
||||||
<string># of Scans:</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="9" column="1">
|
|
||||||
<widget class="QComboBox" name="cb_num_scans"/>
|
|
||||||
</item>
|
|
||||||
<item row="8" column="2">
|
|
||||||
<widget class="QLabel" name="label_54">
|
|
||||||
<property name="text">
|
|
||||||
<string>Equivalent
|
|
||||||
Angular Spacing</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="9" column="2">
|
|
||||||
<widget class="QLabel" name="l_scan_angle_spacing">
|
|
||||||
<property name="text">
|
|
||||||
<string>0</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="10" column="0">
|
|
||||||
<widget class="QLabel" name="label_58">
|
|
||||||
<property name="text">
|
|
||||||
<string>File Suffix:</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignRight|Qt::AlignTrailing</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="11" column="1" colspan="3">
|
|
||||||
<widget class="QLineEdit" name="le_file_suffix"/>
|
|
||||||
</item>
|
|
||||||
<item row="12" column="0" colspan="7">
|
|
||||||
<widget class="Line" name="line_2">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="13" column="0" colspan="2">
|
|
||||||
<widget class="QLabel" name="label_100">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>14</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Timing and Size Information:</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignRight|Qt::AlignTrailing</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="14" column="1">
|
|
||||||
<widget class="QLabel" name="label_173">
|
|
||||||
<property name="text">
|
|
||||||
<string>Points Per Row</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="15" column="1">
|
|
||||||
<widget class="QLabel" name="l_scan_ppr">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>14</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>0</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="14" column="2">
|
|
||||||
<widget class="QLabel" name="label_174">
|
|
||||||
<property name="text">
|
|
||||||
<string>Rows Per Scan</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="15" column="2">
|
|
||||||
<widget class="QLabel" name="l_scan_rps">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>14</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>0</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="14" column="3">
|
|
||||||
<widget class="QLabel" name="label_175">
|
|
||||||
<property name="text">
|
|
||||||
<string>Scans In Set</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="15" column="3">
|
|
||||||
<widget class="QLabel" name="l_scan_sis">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>14</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>0</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="14" column="4">
|
|
||||||
<widget class="QLabel" name="label_176">
|
|
||||||
<property name="text">
|
|
||||||
<string>Total Records</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="15" column="4">
|
|
||||||
<widget class="QLabel" name="l_scan_total_records">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>14</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>0</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="14" column="5">
|
|
||||||
<widget class="QLabel" name="label_177">
|
|
||||||
<property name="text">
|
|
||||||
<string>Total Points
|
|
||||||
Captured</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="15" column="5">
|
|
||||||
<widget class="QLabel" name="l_scan_total_points">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>14</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>0</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="14" column="6">
|
|
||||||
<widget class="QLabel" name="label_178">
|
|
||||||
<property name="text">
|
|
||||||
<string>Current Size
|
|
||||||
On Disk</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="15" column="6">
|
|
||||||
<widget class="QLabel" name="l_scan_estimated_size">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>14</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>0GB</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="16" column="0" colspan="7">
|
|
||||||
<widget class="Line" name="line_3">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="17" column="4" colspan="3">
|
|
||||||
<widget class="QPushButton" name="btn_begin_scanning">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>20</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Begin Scan</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
<widget class="QMenuBar" name="neuScanMBar">
|
|
||||||
<property name="geometry">
|
|
||||||
<rect>
|
|
||||||
<x>0</x>
|
|
||||||
<y>0</y>
|
|
||||||
<width>830</width>
|
|
||||||
<height>30</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<widget class="QMenu" name="menuFile">
|
|
||||||
<property name="title">
|
|
||||||
<string>File</string>
|
|
||||||
</property>
|
|
||||||
<addaction name="actionShow_Oscope_Settings"/>
|
|
||||||
</widget>
|
|
||||||
<widget class="QMenu" name="menuView">
|
|
||||||
<property name="title">
|
|
||||||
<string>View</string>
|
|
||||||
</property>
|
|
||||||
<addaction name="actionDigital_IO_State"/>
|
|
||||||
<addaction name="separator"/>
|
|
||||||
<addaction name="actionMLS203_Information"/>
|
|
||||||
<addaction name="actionTransfer_System_Editor"/>
|
|
||||||
<addaction name="separator"/>
|
|
||||||
<addaction name="actionHelios_Settings"/>
|
|
||||||
<addaction name="actionGenesis_Settings"/>
|
|
||||||
</widget>
|
|
||||||
<addaction name="menuFile"/>
|
|
||||||
<addaction name="menuView"/>
|
|
||||||
</widget>
|
|
||||||
<widget class="QStatusBar" name="neuScanSBar"/>
|
|
||||||
<action name="actionShow_Oscope_Settings">
|
|
||||||
<property name="text">
|
|
||||||
<string>Show Oscope Settings</string>
|
|
||||||
</property>
|
|
||||||
</action>
|
|
||||||
<action name="actionDigital_IO_State">
|
|
||||||
<property name="text">
|
|
||||||
<string>Digital IO State</string>
|
|
||||||
</property>
|
|
||||||
</action>
|
|
||||||
<action name="actionMLS203_Information">
|
|
||||||
<property name="text">
|
|
||||||
<string>MLS203 Information</string>
|
|
||||||
</property>
|
|
||||||
</action>
|
|
||||||
<action name="actionTransfer_System_Editor">
|
|
||||||
<property name="text">
|
|
||||||
<string>Transfer System Editor</string>
|
|
||||||
</property>
|
|
||||||
</action>
|
|
||||||
<action name="actionHelios_Settings">
|
|
||||||
<property name="text">
|
|
||||||
<string>Helios Settings</string>
|
|
||||||
</property>
|
|
||||||
</action>
|
|
||||||
<action name="actionGenesis_Settings">
|
|
||||||
<property name="text">
|
|
||||||
<string>Genesis Settings</string>
|
|
||||||
</property>
|
|
||||||
</action>
|
|
||||||
</widget>
|
|
||||||
<resources/>
|
|
||||||
<connections/>
|
|
||||||
</ui>
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<ui version="4.0">
|
|
||||||
<class>OscopeDialog</class>
|
|
||||||
<widget class="QDialog" name="OscopeDialog">
|
|
||||||
<property name="geometry">
|
|
||||||
<rect>
|
|
||||||
<x>0</x>
|
|
||||||
<y>0</y>
|
|
||||||
<width>466</width>
|
|
||||||
<height>358</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<property name="windowTitle">
|
|
||||||
<string>Oscilloscope Settings</string>
|
|
||||||
</property>
|
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
|
||||||
<item row="0" column="0">
|
|
||||||
<layout class="QGridLayout" name="topGridLayout">
|
|
||||||
<item row="0" column="0" colspan="3">
|
|
||||||
<widget class="QLabel" name="label_57">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>16</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Oscilloscope Settings</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignmentFlag::AlignBottom|Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="0">
|
|
||||||
<widget class="QLabel" name="label_60">
|
|
||||||
<property name="text">
|
|
||||||
<string>Phototrigger
|
|
||||||
Channel</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignmentFlag::AlignBottom|Qt::AlignmentFlag::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="0">
|
|
||||||
<widget class="QComboBox" name="cb_set_trig_channel"/>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="1">
|
|
||||||
<widget class="QLabel" name="label_59">
|
|
||||||
<property name="text">
|
|
||||||
<string>Bias A
|
|
||||||
Channel</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignmentFlag::AlignBottom|Qt::AlignmentFlag::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="1">
|
|
||||||
<widget class="QComboBox" name="cb_set_bias_a_ch"/>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="2">
|
|
||||||
<widget class="QLabel" name="label_61">
|
|
||||||
<property name="text">
|
|
||||||
<string>Bias B
|
|
||||||
Channel</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignmentFlag::AlignBottom|Qt::AlignmentFlag::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="2">
|
|
||||||
<widget class="QComboBox" name="cb_set_bias_b_ch"/>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="3">
|
|
||||||
<widget class="QLabel" name="label_96">
|
|
||||||
<property name="text">
|
|
||||||
<string>RF/SAW
|
|
||||||
Channel</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignmentFlag::AlignBottom|Qt::AlignmentFlag::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="3">
|
|
||||||
<widget class="QComboBox" name="cb_set_saw_channel"/>
|
|
||||||
</item>
|
|
||||||
<item row="3" column="0">
|
|
||||||
<widget class="QLabel" name="label_97">
|
|
||||||
<property name="text">
|
|
||||||
<string>PD Trigger
|
|
||||||
Voltage [V]:</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignmentFlag::AlignBottom|Qt::AlignmentFlag::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="0">
|
|
||||||
<widget class="QLineEdit" name="le_set_pd_trig_voltage"/>
|
|
||||||
</item>
|
|
||||||
<item row="3" column="1">
|
|
||||||
<widget class="QLabel" name="label_trigger_voltage">
|
|
||||||
<property name="text">
|
|
||||||
<string>Trigger
|
|
||||||
Voltage [V]:</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignmentFlag::AlignBottom|Qt::AlignmentFlag::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="1">
|
|
||||||
<widget class="QLineEdit" name="le_set_trigger_voltage"/>
|
|
||||||
</item>
|
|
||||||
<item row="3" column="2">
|
|
||||||
<widget class="QLabel" name="label_99">
|
|
||||||
<property name="text">
|
|
||||||
<string>Sample
|
|
||||||
Min Bias [V]:</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignmentFlag::AlignBottom|Qt::AlignmentFlag::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="2">
|
|
||||||
<widget class="QLineEdit" name="le_set_sample_thresh_voltage"/>
|
|
||||||
</item>
|
|
||||||
<item row="5" column="0">
|
|
||||||
<widget class="QLabel" name="label_98">
|
|
||||||
<property name="text">
|
|
||||||
<string>VISA Address:</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="5" column="1" colspan="3">
|
|
||||||
<widget class="QLineEdit" name="le_oscope_visa_address"/>
|
|
||||||
</item>
|
|
||||||
<item row="5" column="4">
|
|
||||||
<widget class="QPushButton" name="btn_test_scope_connection">
|
|
||||||
<property name="text">
|
|
||||||
<string>Test
|
|
||||||
Connect</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="6" column="3">
|
|
||||||
<widget class="QPushButton" name="btn_save_scope_settings">
|
|
||||||
<property name="text">
|
|
||||||
<string>Save</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="6" column="4">
|
|
||||||
<widget class="QPushButton" name="btn_cancel_scope_settings">
|
|
||||||
<property name="text">
|
|
||||||
<string>Cancel</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
<resources/>
|
|
||||||
<connections/>
|
|
||||||
</ui>
|
|
||||||
@@ -1,275 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<ui version="4.0">
|
|
||||||
<class>Dialog</class>
|
|
||||||
<widget class="QDialog" name="Dialog">
|
|
||||||
<property name="geometry">
|
|
||||||
<rect>
|
|
||||||
<x>0</x>
|
|
||||||
<y>0</y>
|
|
||||||
<width>858</width>
|
|
||||||
<height>298</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<property name="windowTitle">
|
|
||||||
<string>Dialog</string>
|
|
||||||
</property>
|
|
||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
|
||||||
<item>
|
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
|
||||||
<item row="0" column="0" colspan="8">
|
|
||||||
<widget class="QLabel" name="label">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>24</pointsize>
|
|
||||||
<bold>true</bold>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>SCANNING...</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="10" column="1" colspan="7">
|
|
||||||
<widget class="QPushButton" name="pb_cancel_scan">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>14</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Cancel Scan</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="4">
|
|
||||||
<widget class="QLabel" name="label_4">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>12</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>of</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="3" column="0" colspan="8">
|
|
||||||
<widget class="QProgressBar" name="pbar_total_scan">
|
|
||||||
<property name="value">
|
|
||||||
<number>24</number>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="7" column="1">
|
|
||||||
<spacer name="horizontalSpacer_3">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>40</width>
|
|
||||||
<height>20</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
<item row="7" column="6" colspan="2">
|
|
||||||
<spacer name="horizontalSpacer_4">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>40</width>
|
|
||||||
<height>20</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="2">
|
|
||||||
<widget class="QLabel" name="label_2">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>16</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Scan</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="5">
|
|
||||||
<widget class="QLabel" name="l_status_total_scans">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>16</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>09</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="7" column="3">
|
|
||||||
<widget class="QLabel" name="l_status_current_row">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>16</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>000</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="7" column="5">
|
|
||||||
<widget class="QLabel" name="l_status_total_rows">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>16</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>000</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="6" column="1" colspan="7">
|
|
||||||
<widget class="QProgressBar" name="pbar_this_scan">
|
|
||||||
<property name="value">
|
|
||||||
<number>24</number>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="3">
|
|
||||||
<widget class="QLabel" name="l_status_current_scan">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>16</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>01</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="7" column="4">
|
|
||||||
<widget class="QLabel" name="label_8">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>12</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>of</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="7" column="2">
|
|
||||||
<widget class="QLabel" name="label_7">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>16</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Row</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="5" column="3">
|
|
||||||
<spacer name="verticalSpacer">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Vertical</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>20</width>
|
|
||||||
<height>40</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="0" colspan="8">
|
|
||||||
<widget class="QLabel" name="l_est_time_done">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>16</pointsize>
|
|
||||||
<bold>false</bold>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>00:00:00 remaining....</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="1">
|
|
||||||
<spacer name="horizontalSpacer">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>40</width>
|
|
||||||
<height>20</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="7">
|
|
||||||
<spacer name="horizontalSpacer_2">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>40</width>
|
|
||||||
<height>20</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
<item row="8" column="3">
|
|
||||||
<spacer name="verticalSpacer_2">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Vertical</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>20</width>
|
|
||||||
<height>40</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
<resources/>
|
|
||||||
<connections/>
|
|
||||||
</ui>
|
|
||||||
@@ -1,473 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<ui version="4.0">
|
|
||||||
<class>StatusDialog</class>
|
|
||||||
<widget class="QDialog" name="StatusDialog">
|
|
||||||
<property name="geometry">
|
|
||||||
<rect>
|
|
||||||
<x>0</x>
|
|
||||||
<y>0</y>
|
|
||||||
<width>835</width>
|
|
||||||
<height>260</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<property name="windowTitle">
|
|
||||||
<string>Status Indicators</string>
|
|
||||||
</property>
|
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
|
||||||
<item row="0" column="0">
|
|
||||||
<layout class="QGridLayout" name="statusGridLayout">
|
|
||||||
<item row="0" column="0">
|
|
||||||
<widget class="QLabel" name="label_3">
|
|
||||||
<property name="text">
|
|
||||||
<string>Stage Status Information:</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignRight|Qt::AlignTrailing</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="1">
|
|
||||||
<widget class="QLabel" name="label_14">
|
|
||||||
<property name="text">
|
|
||||||
<string>isConnected?</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="1">
|
|
||||||
<widget class="QLabel" name="l_is_mls_connected">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>14</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Yes</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="2">
|
|
||||||
<widget class="QLabel" name="label_9">
|
|
||||||
<property name="text">
|
|
||||||
<string>isXHomed?</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="2">
|
|
||||||
<widget class="QLabel" name="l_is_mls_x_home">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>14</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Yes</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="3">
|
|
||||||
<widget class="QLabel" name="label_11">
|
|
||||||
<property name="text">
|
|
||||||
<string>isYHomed?</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="3">
|
|
||||||
<widget class="QLabel" name="l_is_mls_y_home">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>14</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Yes</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="4">
|
|
||||||
<widget class="QLabel" name="label_12">
|
|
||||||
<property name="text">
|
|
||||||
<string>isReady?</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="4">
|
|
||||||
<widget class="QLabel" name="l_is_mls_ready">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>14</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Yes</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="5">
|
|
||||||
<widget class="QLabel" name="label_13">
|
|
||||||
<property name="text">
|
|
||||||
<string>isScanning?</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="5">
|
|
||||||
<widget class="QLabel" name="l_is_mls_scanning">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>14</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Yes</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="3" column="0">
|
|
||||||
<widget class="QLabel" name="label_16">
|
|
||||||
<property name="text">
|
|
||||||
<string>T3R-SL Status Information:</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignRight|Qt::AlignTrailing</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="1">
|
|
||||||
<widget class="QLabel" name="label_17">
|
|
||||||
<property name="text">
|
|
||||||
<string>isConnected?</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="5" column="1">
|
|
||||||
<widget class="QLabel" name="l_is_t3r_connected">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>14</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Yes</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="2">
|
|
||||||
<widget class="QLabel" name="label_19">
|
|
||||||
<property name="text">
|
|
||||||
<string>isHomed?</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="5" column="2">
|
|
||||||
<widget class="QLabel" name="l_is_t3r_homed">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>14</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Yes</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="3">
|
|
||||||
<widget class="QLabel" name="label_20">
|
|
||||||
<property name="text">
|
|
||||||
<string>isReady?</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignHCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="5" column="3">
|
|
||||||
<widget class="QLabel" name="l_is_t3r_ready">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<family>Sans Serif</family>
|
|
||||||
<pointsize>14</pointsize>
|
|
||||||
<italic>false</italic>
|
|
||||||
<bold>false</bold>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="styleSheet">
|
|
||||||
<string notr="true">font: 14pt "Sans Serif";</string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Yes</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="6" column="0">
|
|
||||||
<widget class="QLabel" name="label_100">
|
|
||||||
<property name="text">
|
|
||||||
<string>Microscope Status Information:</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignRight|Qt::AlignTrailing</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="7" column="2">
|
|
||||||
<widget class="QLabel" name="l_is_helios_ready">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>14</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Yes</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="7" column="3">
|
|
||||||
<widget class="QLabel" name="l_is_helios_interlocked">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>14</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Yes</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="7" column="4">
|
|
||||||
<widget class="QLabel" name="l_is_genesis_ready">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>14</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Yes</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="7" column="5">
|
|
||||||
<widget class="QLabel" name="l_is_genesis_interlocked">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>14</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Yes</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="8" column="0">
|
|
||||||
<widget class="QLabel" name="label_157">
|
|
||||||
<property name="text">
|
|
||||||
<string>Transfer System Information:</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignBottom|Qt::AlignRight|Qt::AlignTrailing</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="9" column="1">
|
|
||||||
<widget class="QLabel" name="label_158">
|
|
||||||
<property name="text">
|
|
||||||
<string>Outputs To
|
|
||||||
Robo-met.3D</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="10" column="2">
|
|
||||||
<widget class="QLabel" name="l_sras_ok">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>14</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Low (0)</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="10" column="3">
|
|
||||||
<widget class="QLabel" name="l_sras_ctl">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>14</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Low (0)</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="10" column="4">
|
|
||||||
<widget class="QLabel" name="l_sras_done">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>14</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Low (0)</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="10" column="5">
|
|
||||||
<widget class="QLabel" name="l_sras_error">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>14</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Low (0)</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="11" column="1">
|
|
||||||
<widget class="QLabel" name="label_159">
|
|
||||||
<property name="text">
|
|
||||||
<string>Inputs from
|
|
||||||
Robo-met.3D</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="12" column="2">
|
|
||||||
<widget class="QLabel" name="l_r3d_estop_ok">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>14</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Low (0)</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="12" column="3">
|
|
||||||
<widget class="QLabel" name="l_r3d_rtl">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>14</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Low (0)</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="12" column="4">
|
|
||||||
<widget class="QLabel" name="l_r3d_rts">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>14</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Low (0)</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="12" column="5">
|
|
||||||
<widget class="QLabel" name="l_r3d_spare">
|
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<pointsize>14</pointsize>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Low (0)</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
<resources/>
|
|
||||||
<connections/>
|
|
||||||
</ui>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# nueScan - SRAS Scan Planning and Control Software
|
|
||||||
# Python Dependencies
|
|
||||||
|
|
||||||
# GUI Framework
|
|
||||||
PyQt6>=6.4.0
|
|
||||||
|
|
||||||
# Serial Communication (for hardware interfaces)
|
|
||||||
pyserial>=3.5
|
|
||||||
|
|
||||||
# VISA instrument control (for oscilloscope)
|
|
||||||
pyvisa>=1.13.0
|
|
||||||
pyvisa-py>=0.7.0
|
|
||||||
|
|
||||||
# Optional: For enhanced serial port detection
|
|
||||||
# pyserial-asyncio>=0.6
|
|
||||||
|
|
||||||
# Development dependencies (optional)
|
|
||||||
# pytest>=7.0.0
|
|
||||||
# pytest-qt>=4.0.0
|
|
||||||
@@ -1,797 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
ThorLabs Stage Test Application
|
|
||||||
Qt6-based GUI for testing and verifying the BBD203/MLS stage driver functionality
|
|
||||||
|
|
||||||
Copyright (C) 2025 Thomas Ales
|
|
||||||
Licensed under GNU General Public License v2.0
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
from PyQt6.QtWidgets import (
|
|
||||||
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
|
||||||
QGroupBox, QPushButton, QLabel, QLineEdit, QComboBox, QTextEdit,
|
|
||||||
QSpinBox, QDoubleSpinBox, QCheckBox, QGridLayout, QMessageBox, QTabWidget
|
|
||||||
)
|
|
||||||
from PyQt6.QtCore import QTimer, Qt
|
|
||||||
from PyQt6.QtGui import QFont
|
|
||||||
|
|
||||||
from hardware.thorlabs_stage import ThorLabsStage
|
|
||||||
from hardware.bbd203_protocol import TriggerMode
|
|
||||||
|
|
||||||
|
|
||||||
class StageTestWindow(QMainWindow):
|
|
||||||
"""Main window for stage testing application"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.stage = ThorLabsStage()
|
|
||||||
self.status_timer = QTimer()
|
|
||||||
self.status_timer.timeout.connect(self.update_status)
|
|
||||||
|
|
||||||
self.init_ui()
|
|
||||||
self.refresh_devices()
|
|
||||||
|
|
||||||
def init_ui(self):
|
|
||||||
"""Initialize the user interface"""
|
|
||||||
self.setWindowTitle("ThorLabs Stage Test Application")
|
|
||||||
self.setGeometry(100, 100, 900, 700)
|
|
||||||
|
|
||||||
# Central widget
|
|
||||||
central_widget = QWidget()
|
|
||||||
self.setCentralWidget(central_widget)
|
|
||||||
|
|
||||||
# Main layout
|
|
||||||
main_layout = QVBoxLayout(central_widget)
|
|
||||||
|
|
||||||
# Title
|
|
||||||
title_label = QLabel("ThorLabs BBD203/MLS Stage Driver Test")
|
|
||||||
title_font = QFont()
|
|
||||||
title_font.setPointSize(16)
|
|
||||||
title_font.setBold(True)
|
|
||||||
title_label.setFont(title_font)
|
|
||||||
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
||||||
main_layout.addWidget(title_label)
|
|
||||||
|
|
||||||
# Connection section
|
|
||||||
main_layout.addWidget(self.create_connection_group())
|
|
||||||
|
|
||||||
# Tabbed interface for different sections
|
|
||||||
tab_widget = QTabWidget()
|
|
||||||
|
|
||||||
# Control tab
|
|
||||||
control_widget = QWidget()
|
|
||||||
control_layout = QVBoxLayout(control_widget)
|
|
||||||
|
|
||||||
controls_layout = QHBoxLayout()
|
|
||||||
controls_layout.addWidget(self.create_homing_group())
|
|
||||||
controls_layout.addWidget(self.create_motion_group())
|
|
||||||
control_layout.addLayout(controls_layout)
|
|
||||||
|
|
||||||
control_layout.addWidget(self.create_status_group())
|
|
||||||
|
|
||||||
tab_widget.addTab(control_widget, "Control")
|
|
||||||
|
|
||||||
# Settings tab
|
|
||||||
settings_widget = QWidget()
|
|
||||||
settings_layout = QVBoxLayout(settings_widget)
|
|
||||||
settings_layout.addWidget(self.create_settings_group())
|
|
||||||
tab_widget.addTab(settings_widget, "Settings")
|
|
||||||
|
|
||||||
main_layout.addWidget(tab_widget)
|
|
||||||
|
|
||||||
# Log section
|
|
||||||
main_layout.addWidget(self.create_log_group())
|
|
||||||
|
|
||||||
# Start status updates
|
|
||||||
self.status_timer.start(200) # Update every 200ms
|
|
||||||
|
|
||||||
def create_connection_group(self) -> QGroupBox:
|
|
||||||
"""Create connection control group"""
|
|
||||||
group = QGroupBox("Connection")
|
|
||||||
layout = QGridLayout()
|
|
||||||
|
|
||||||
# Device selection
|
|
||||||
layout.addWidget(QLabel("Device:"), 0, 0)
|
|
||||||
self.device_combo = QComboBox()
|
|
||||||
layout.addWidget(self.device_combo, 0, 1, 1, 2)
|
|
||||||
|
|
||||||
self.refresh_btn = QPushButton("Refresh Devices")
|
|
||||||
self.refresh_btn.clicked.connect(self.refresh_devices)
|
|
||||||
layout.addWidget(self.refresh_btn, 0, 3)
|
|
||||||
|
|
||||||
# Serial number entry
|
|
||||||
layout.addWidget(QLabel("Serial Number:"), 1, 0)
|
|
||||||
self.serial_edit = QLineEdit()
|
|
||||||
self.serial_edit.setPlaceholderText("e.g., 83123456")
|
|
||||||
layout.addWidget(self.serial_edit, 1, 1, 1, 2)
|
|
||||||
|
|
||||||
# Baudrate
|
|
||||||
layout.addWidget(QLabel("Baudrate:"), 2, 0)
|
|
||||||
self.baudrate_combo = QComboBox()
|
|
||||||
self.baudrate_combo.addItems(["115200", "9600", "19200", "38400", "57600"])
|
|
||||||
self.baudrate_combo.setCurrentText("115200")
|
|
||||||
layout.addWidget(self.baudrate_combo, 2, 1)
|
|
||||||
|
|
||||||
# Connect/Disconnect buttons
|
|
||||||
self.connect_btn = QPushButton("Connect")
|
|
||||||
self.connect_btn.clicked.connect(self.connect_stage)
|
|
||||||
layout.addWidget(self.connect_btn, 2, 2)
|
|
||||||
|
|
||||||
self.disconnect_btn = QPushButton("Disconnect")
|
|
||||||
self.disconnect_btn.clicked.connect(self.disconnect_stage)
|
|
||||||
self.disconnect_btn.setEnabled(False)
|
|
||||||
layout.addWidget(self.disconnect_btn, 2, 3)
|
|
||||||
|
|
||||||
# Identify button
|
|
||||||
self.identify_btn = QPushButton("Identify (Flash LEDs)")
|
|
||||||
self.identify_btn.clicked.connect(self.identify_stage)
|
|
||||||
self.identify_btn.setEnabled(False)
|
|
||||||
layout.addWidget(self.identify_btn, 3, 0, 1, 4)
|
|
||||||
|
|
||||||
group.setLayout(layout)
|
|
||||||
return group
|
|
||||||
|
|
||||||
def create_homing_group(self) -> QGroupBox:
|
|
||||||
"""Create homing control group"""
|
|
||||||
group = QGroupBox("Homing")
|
|
||||||
layout = QVBoxLayout()
|
|
||||||
|
|
||||||
# Home all button
|
|
||||||
self.home_all_btn = QPushButton("Home All Axes")
|
|
||||||
self.home_all_btn.clicked.connect(self.home_all)
|
|
||||||
self.home_all_btn.setEnabled(False)
|
|
||||||
layout.addWidget(self.home_all_btn)
|
|
||||||
|
|
||||||
# Individual axis homing
|
|
||||||
axis_layout = QHBoxLayout()
|
|
||||||
|
|
||||||
self.home_x_btn = QPushButton("Home X")
|
|
||||||
self.home_x_btn.clicked.connect(lambda: self.home_axis('X'))
|
|
||||||
self.home_x_btn.setEnabled(False)
|
|
||||||
axis_layout.addWidget(self.home_x_btn)
|
|
||||||
|
|
||||||
self.home_y_btn = QPushButton("Home Y")
|
|
||||||
self.home_y_btn.clicked.connect(lambda: self.home_axis('Y'))
|
|
||||||
self.home_y_btn.setEnabled(False)
|
|
||||||
axis_layout.addWidget(self.home_y_btn)
|
|
||||||
|
|
||||||
self.home_z_btn = QPushButton("Home Z")
|
|
||||||
self.home_z_btn.clicked.connect(lambda: self.home_axis('Z'))
|
|
||||||
self.home_z_btn.setEnabled(False)
|
|
||||||
axis_layout.addWidget(self.home_z_btn)
|
|
||||||
|
|
||||||
layout.addLayout(axis_layout)
|
|
||||||
|
|
||||||
group.setLayout(layout)
|
|
||||||
return group
|
|
||||||
|
|
||||||
def create_motion_group(self) -> QGroupBox:
|
|
||||||
"""Create motion control group"""
|
|
||||||
group = QGroupBox("Motion Control")
|
|
||||||
layout = QGridLayout()
|
|
||||||
|
|
||||||
# Absolute move controls
|
|
||||||
layout.addWidget(QLabel("Absolute Move (mm):"), 0, 0, 1, 3)
|
|
||||||
|
|
||||||
layout.addWidget(QLabel("X:"), 1, 0)
|
|
||||||
self.abs_x_spin = QDoubleSpinBox()
|
|
||||||
self.abs_x_spin.setRange(-100, 100)
|
|
||||||
self.abs_x_spin.setDecimals(3)
|
|
||||||
self.abs_x_spin.setSingleStep(0.1)
|
|
||||||
layout.addWidget(self.abs_x_spin, 1, 1)
|
|
||||||
|
|
||||||
layout.addWidget(QLabel("Y:"), 2, 0)
|
|
||||||
self.abs_y_spin = QDoubleSpinBox()
|
|
||||||
self.abs_y_spin.setRange(-100, 100)
|
|
||||||
self.abs_y_spin.setDecimals(3)
|
|
||||||
self.abs_y_spin.setSingleStep(0.1)
|
|
||||||
layout.addWidget(self.abs_y_spin, 2, 1)
|
|
||||||
|
|
||||||
layout.addWidget(QLabel("Z:"), 3, 0)
|
|
||||||
self.abs_z_spin = QDoubleSpinBox()
|
|
||||||
self.abs_z_spin.setRange(-100, 100)
|
|
||||||
self.abs_z_spin.setDecimals(3)
|
|
||||||
self.abs_z_spin.setSingleStep(0.1)
|
|
||||||
layout.addWidget(self.abs_z_spin, 3, 1)
|
|
||||||
|
|
||||||
self.move_abs_btn = QPushButton("Move Absolute")
|
|
||||||
self.move_abs_btn.clicked.connect(self.move_absolute)
|
|
||||||
self.move_abs_btn.setEnabled(False)
|
|
||||||
layout.addWidget(self.move_abs_btn, 4, 0, 1, 2)
|
|
||||||
|
|
||||||
# Relative move controls
|
|
||||||
layout.addWidget(QLabel("Relative Move (mm):"), 5, 0, 1, 3)
|
|
||||||
|
|
||||||
layout.addWidget(QLabel("dX:"), 6, 0)
|
|
||||||
self.rel_x_spin = QDoubleSpinBox()
|
|
||||||
self.rel_x_spin.setRange(-10, 10)
|
|
||||||
self.rel_x_spin.setDecimals(3)
|
|
||||||
self.rel_x_spin.setSingleStep(0.1)
|
|
||||||
layout.addWidget(self.rel_x_spin, 6, 1)
|
|
||||||
|
|
||||||
layout.addWidget(QLabel("dY:"), 7, 0)
|
|
||||||
self.rel_y_spin = QDoubleSpinBox()
|
|
||||||
self.rel_y_spin.setRange(-10, 10)
|
|
||||||
self.rel_y_spin.setDecimals(3)
|
|
||||||
self.rel_y_spin.setSingleStep(0.1)
|
|
||||||
layout.addWidget(self.rel_y_spin, 7, 1)
|
|
||||||
|
|
||||||
layout.addWidget(QLabel("dZ:"), 8, 0)
|
|
||||||
self.rel_z_spin = QDoubleSpinBox()
|
|
||||||
self.rel_z_spin.setRange(-10, 10)
|
|
||||||
self.rel_z_spin.setDecimals(3)
|
|
||||||
self.rel_z_spin.setSingleStep(0.1)
|
|
||||||
layout.addWidget(self.rel_z_spin, 8, 1)
|
|
||||||
|
|
||||||
self.move_rel_btn = QPushButton("Move Relative")
|
|
||||||
self.move_rel_btn.clicked.connect(self.move_relative)
|
|
||||||
self.move_rel_btn.setEnabled(False)
|
|
||||||
layout.addWidget(self.move_rel_btn, 9, 0, 1, 2)
|
|
||||||
|
|
||||||
# Stop button
|
|
||||||
self.stop_btn = QPushButton("STOP ALL")
|
|
||||||
self.stop_btn.clicked.connect(self.stop_all)
|
|
||||||
self.stop_btn.setEnabled(False)
|
|
||||||
self.stop_btn.setStyleSheet("background-color: #ff4444; color: white; font-weight: bold;")
|
|
||||||
layout.addWidget(self.stop_btn, 10, 0, 1, 2)
|
|
||||||
|
|
||||||
group.setLayout(layout)
|
|
||||||
return group
|
|
||||||
|
|
||||||
def create_settings_group(self) -> QGroupBox:
|
|
||||||
"""Create settings configuration group"""
|
|
||||||
group = QGroupBox("Stage Settings")
|
|
||||||
layout = QVBoxLayout()
|
|
||||||
|
|
||||||
# Velocity settings
|
|
||||||
vel_group = QGroupBox("Velocity (mm/s)")
|
|
||||||
vel_layout = QGridLayout()
|
|
||||||
|
|
||||||
vel_layout.addWidget(QLabel("X Axis:"), 0, 0)
|
|
||||||
self.vel_x_spin = QDoubleSpinBox()
|
|
||||||
self.vel_x_spin.setRange(0.01, 10.0)
|
|
||||||
self.vel_x_spin.setDecimals(3)
|
|
||||||
self.vel_x_spin.setSingleStep(0.1)
|
|
||||||
self.vel_x_spin.setValue(1.0)
|
|
||||||
vel_layout.addWidget(self.vel_x_spin, 0, 1)
|
|
||||||
|
|
||||||
vel_layout.addWidget(QLabel("Y Axis:"), 1, 0)
|
|
||||||
self.vel_y_spin = QDoubleSpinBox()
|
|
||||||
self.vel_y_spin.setRange(0.01, 10.0)
|
|
||||||
self.vel_y_spin.setDecimals(3)
|
|
||||||
self.vel_y_spin.setSingleStep(0.1)
|
|
||||||
self.vel_y_spin.setValue(1.0)
|
|
||||||
vel_layout.addWidget(self.vel_y_spin, 1, 1)
|
|
||||||
|
|
||||||
vel_layout.addWidget(QLabel("Z Axis:"), 2, 0)
|
|
||||||
self.vel_z_spin = QDoubleSpinBox()
|
|
||||||
self.vel_z_spin.setRange(0.01, 10.0)
|
|
||||||
self.vel_z_spin.setDecimals(3)
|
|
||||||
self.vel_z_spin.setSingleStep(0.1)
|
|
||||||
self.vel_z_spin.setValue(1.0)
|
|
||||||
vel_layout.addWidget(self.vel_z_spin, 2, 1)
|
|
||||||
|
|
||||||
self.apply_vel_btn = QPushButton("Apply Velocity")
|
|
||||||
self.apply_vel_btn.clicked.connect(self.apply_velocity_settings)
|
|
||||||
self.apply_vel_btn.setEnabled(False)
|
|
||||||
vel_layout.addWidget(self.apply_vel_btn, 3, 0, 1, 2)
|
|
||||||
|
|
||||||
vel_group.setLayout(vel_layout)
|
|
||||||
layout.addWidget(vel_group)
|
|
||||||
|
|
||||||
# Acceleration settings
|
|
||||||
accel_group = QGroupBox("Acceleration (mm/s²)")
|
|
||||||
accel_layout = QGridLayout()
|
|
||||||
|
|
||||||
accel_layout.addWidget(QLabel("X Axis:"), 0, 0)
|
|
||||||
self.accel_x_spin = QDoubleSpinBox()
|
|
||||||
self.accel_x_spin.setRange(0.1, 100.0)
|
|
||||||
self.accel_x_spin.setDecimals(2)
|
|
||||||
self.accel_x_spin.setSingleStep(1.0)
|
|
||||||
self.accel_x_spin.setValue(5.0)
|
|
||||||
accel_layout.addWidget(self.accel_x_spin, 0, 1)
|
|
||||||
|
|
||||||
accel_layout.addWidget(QLabel("Y Axis:"), 1, 0)
|
|
||||||
self.accel_y_spin = QDoubleSpinBox()
|
|
||||||
self.accel_y_spin.setRange(0.1, 100.0)
|
|
||||||
self.accel_y_spin.setDecimals(2)
|
|
||||||
self.accel_y_spin.setSingleStep(1.0)
|
|
||||||
self.accel_y_spin.setValue(5.0)
|
|
||||||
accel_layout.addWidget(self.accel_y_spin, 1, 1)
|
|
||||||
|
|
||||||
accel_layout.addWidget(QLabel("Z Axis:"), 2, 0)
|
|
||||||
self.accel_z_spin = QDoubleSpinBox()
|
|
||||||
self.accel_z_spin.setRange(0.1, 100.0)
|
|
||||||
self.accel_z_spin.setDecimals(2)
|
|
||||||
self.accel_z_spin.setSingleStep(1.0)
|
|
||||||
self.accel_z_spin.setValue(5.0)
|
|
||||||
accel_layout.addWidget(self.accel_z_spin, 2, 1)
|
|
||||||
|
|
||||||
self.apply_accel_btn = QPushButton("Apply Acceleration")
|
|
||||||
self.apply_accel_btn.clicked.connect(self.apply_acceleration_settings)
|
|
||||||
self.apply_accel_btn.setEnabled(False)
|
|
||||||
accel_layout.addWidget(self.apply_accel_btn, 3, 0, 1, 2)
|
|
||||||
|
|
||||||
accel_group.setLayout(accel_layout)
|
|
||||||
layout.addWidget(accel_group)
|
|
||||||
|
|
||||||
# Trigger settings
|
|
||||||
trigger_group = QGroupBox("Trigger Configuration")
|
|
||||||
trigger_layout = QGridLayout()
|
|
||||||
|
|
||||||
trigger_layout.addWidget(QLabel("Axis:"), 0, 0)
|
|
||||||
self.trigger_axis_combo = QComboBox()
|
|
||||||
self.trigger_axis_combo.addItems(["X", "Y", "Z"])
|
|
||||||
trigger_layout.addWidget(self.trigger_axis_combo, 0, 1)
|
|
||||||
|
|
||||||
trigger_layout.addWidget(QLabel("Mode:"), 1, 0)
|
|
||||||
self.trigger_mode_combo = QComboBox()
|
|
||||||
self.trigger_mode_combo.addItems([
|
|
||||||
"Disabled",
|
|
||||||
"In/Out Relative Move",
|
|
||||||
"In/Out Absolute Move",
|
|
||||||
"In/Out Home",
|
|
||||||
"In/Out Stop",
|
|
||||||
"Out Only",
|
|
||||||
"Out Position"
|
|
||||||
])
|
|
||||||
trigger_layout.addWidget(self.trigger_mode_combo, 1, 1)
|
|
||||||
|
|
||||||
trigger_layout.addWidget(QLabel("Polarity:"), 2, 0)
|
|
||||||
self.trigger_polarity_combo = QComboBox()
|
|
||||||
self.trigger_polarity_combo.addItems(["Active High", "Active Low"])
|
|
||||||
trigger_layout.addWidget(self.trigger_polarity_combo, 2, 1)
|
|
||||||
|
|
||||||
self.apply_trigger_btn = QPushButton("Apply Trigger Settings")
|
|
||||||
self.apply_trigger_btn.clicked.connect(self.apply_trigger_settings)
|
|
||||||
self.apply_trigger_btn.setEnabled(False)
|
|
||||||
trigger_layout.addWidget(self.apply_trigger_btn, 3, 0, 1, 2)
|
|
||||||
|
|
||||||
trigger_group.setLayout(trigger_layout)
|
|
||||||
layout.addWidget(trigger_group)
|
|
||||||
|
|
||||||
# Save/Load buttons
|
|
||||||
buttons_layout = QHBoxLayout()
|
|
||||||
|
|
||||||
self.load_settings_btn = QPushButton("Load Settings")
|
|
||||||
self.load_settings_btn.clicked.connect(self.load_settings)
|
|
||||||
buttons_layout.addWidget(self.load_settings_btn)
|
|
||||||
|
|
||||||
self.save_settings_btn = QPushButton("Save Settings")
|
|
||||||
self.save_settings_btn.clicked.connect(self.save_settings)
|
|
||||||
self.save_settings_btn.setEnabled(False)
|
|
||||||
buttons_layout.addWidget(self.save_settings_btn)
|
|
||||||
|
|
||||||
layout.addLayout(buttons_layout)
|
|
||||||
|
|
||||||
group.setLayout(layout)
|
|
||||||
return group
|
|
||||||
|
|
||||||
def create_status_group(self) -> QGroupBox:
|
|
||||||
"""Create status display group"""
|
|
||||||
group = QGroupBox("Status")
|
|
||||||
layout = QGridLayout()
|
|
||||||
|
|
||||||
# Connection status
|
|
||||||
layout.addWidget(QLabel("Connected:"), 0, 0)
|
|
||||||
self.connected_label = QLabel("No")
|
|
||||||
self.connected_label.setStyleSheet("font-weight: bold; color: red;")
|
|
||||||
layout.addWidget(self.connected_label, 0, 1)
|
|
||||||
|
|
||||||
# Homed status
|
|
||||||
layout.addWidget(QLabel("X Homed:"), 1, 0)
|
|
||||||
self.x_homed_label = QLabel("No")
|
|
||||||
layout.addWidget(self.x_homed_label, 1, 1)
|
|
||||||
|
|
||||||
layout.addWidget(QLabel("Y Homed:"), 2, 0)
|
|
||||||
self.y_homed_label = QLabel("No")
|
|
||||||
layout.addWidget(self.y_homed_label, 2, 1)
|
|
||||||
|
|
||||||
layout.addWidget(QLabel("Z Homed:"), 3, 0)
|
|
||||||
self.z_homed_label = QLabel("No")
|
|
||||||
layout.addWidget(self.z_homed_label, 3, 1)
|
|
||||||
|
|
||||||
# Position
|
|
||||||
layout.addWidget(QLabel("X Position:"), 1, 2)
|
|
||||||
self.x_pos_label = QLabel("0.000 mm")
|
|
||||||
layout.addWidget(self.x_pos_label, 1, 3)
|
|
||||||
|
|
||||||
layout.addWidget(QLabel("Y Position:"), 2, 2)
|
|
||||||
self.y_pos_label = QLabel("0.000 mm")
|
|
||||||
layout.addWidget(self.y_pos_label, 2, 3)
|
|
||||||
|
|
||||||
layout.addWidget(QLabel("Z Position:"), 3, 2)
|
|
||||||
self.z_pos_label = QLabel("0.000 mm")
|
|
||||||
layout.addWidget(self.z_pos_label, 3, 3)
|
|
||||||
|
|
||||||
# Ready/Moving status
|
|
||||||
layout.addWidget(QLabel("Stage Ready:"), 4, 0)
|
|
||||||
self.ready_label = QLabel("No")
|
|
||||||
layout.addWidget(self.ready_label, 4, 1)
|
|
||||||
|
|
||||||
layout.addWidget(QLabel("Moving:"), 4, 2)
|
|
||||||
self.moving_label = QLabel("No")
|
|
||||||
layout.addWidget(self.moving_label, 4, 3)
|
|
||||||
|
|
||||||
group.setLayout(layout)
|
|
||||||
return group
|
|
||||||
|
|
||||||
def create_log_group(self) -> QGroupBox:
|
|
||||||
"""Create log display group"""
|
|
||||||
group = QGroupBox("Log")
|
|
||||||
layout = QVBoxLayout()
|
|
||||||
|
|
||||||
self.log_text = QTextEdit()
|
|
||||||
self.log_text.setReadOnly(True)
|
|
||||||
self.log_text.setMaximumHeight(150)
|
|
||||||
layout.addWidget(self.log_text)
|
|
||||||
|
|
||||||
# Clear log button
|
|
||||||
clear_btn = QPushButton("Clear Log")
|
|
||||||
clear_btn.clicked.connect(self.log_text.clear)
|
|
||||||
layout.addWidget(clear_btn)
|
|
||||||
|
|
||||||
group.setLayout(layout)
|
|
||||||
return group
|
|
||||||
|
|
||||||
# ==================== Connection Methods ====================
|
|
||||||
|
|
||||||
def refresh_devices(self):
|
|
||||||
"""Refresh list of available devices"""
|
|
||||||
self.log("Searching for ThorLabs devices...")
|
|
||||||
devices = ThorLabsStage.list_devices()
|
|
||||||
|
|
||||||
self.device_combo.clear()
|
|
||||||
|
|
||||||
if devices:
|
|
||||||
for device in devices:
|
|
||||||
label = f"{device['serial']} - {device['port']} ({device['description']})"
|
|
||||||
self.device_combo.addItem(label, device['serial'])
|
|
||||||
self.log(f"Found: {label}")
|
|
||||||
|
|
||||||
# Auto-fill serial number from first device
|
|
||||||
if self.device_combo.count() > 0:
|
|
||||||
self.serial_edit.setText(self.device_combo.currentData())
|
|
||||||
else:
|
|
||||||
self.log("No ThorLabs devices found")
|
|
||||||
|
|
||||||
def connect_stage(self):
|
|
||||||
"""Connect to stage"""
|
|
||||||
serial = self.serial_edit.text().strip()
|
|
||||||
if not serial:
|
|
||||||
self.log("ERROR: Please enter a serial number")
|
|
||||||
return
|
|
||||||
|
|
||||||
baudrate = int(self.baudrate_combo.currentText())
|
|
||||||
|
|
||||||
self.log(f"Connecting to device {serial} at {baudrate} baud...")
|
|
||||||
|
|
||||||
if self.stage.connect(serial, baudrate):
|
|
||||||
self.log("Successfully connected to stage")
|
|
||||||
self.connected_label.setText("Yes")
|
|
||||||
self.connected_label.setStyleSheet("font-weight: bold; color: green;")
|
|
||||||
|
|
||||||
# Enable controls
|
|
||||||
self.connect_btn.setEnabled(False)
|
|
||||||
self.disconnect_btn.setEnabled(True)
|
|
||||||
self.identify_btn.setEnabled(True)
|
|
||||||
self.home_all_btn.setEnabled(True)
|
|
||||||
self.home_x_btn.setEnabled(True)
|
|
||||||
self.home_y_btn.setEnabled(True)
|
|
||||||
self.home_z_btn.setEnabled(True)
|
|
||||||
self.move_abs_btn.setEnabled(True)
|
|
||||||
self.move_rel_btn.setEnabled(True)
|
|
||||||
self.stop_btn.setEnabled(True)
|
|
||||||
self.apply_vel_btn.setEnabled(True)
|
|
||||||
self.apply_accel_btn.setEnabled(True)
|
|
||||||
self.apply_trigger_btn.setEnabled(True)
|
|
||||||
self.save_settings_btn.setEnabled(True)
|
|
||||||
|
|
||||||
# Load current settings into UI
|
|
||||||
self.load_settings_to_ui()
|
|
||||||
else:
|
|
||||||
self.log("ERROR: Failed to connect to stage")
|
|
||||||
|
|
||||||
def disconnect_stage(self):
|
|
||||||
"""Disconnect from stage"""
|
|
||||||
self.log("Disconnecting from stage...")
|
|
||||||
|
|
||||||
if self.stage.disconnect():
|
|
||||||
self.log("Disconnected successfully")
|
|
||||||
self.connected_label.setText("No")
|
|
||||||
self.connected_label.setStyleSheet("font-weight: bold; color: red;")
|
|
||||||
|
|
||||||
# Disable controls
|
|
||||||
self.connect_btn.setEnabled(True)
|
|
||||||
self.disconnect_btn.setEnabled(False)
|
|
||||||
self.identify_btn.setEnabled(False)
|
|
||||||
self.home_all_btn.setEnabled(False)
|
|
||||||
self.home_x_btn.setEnabled(False)
|
|
||||||
self.home_y_btn.setEnabled(False)
|
|
||||||
self.home_z_btn.setEnabled(False)
|
|
||||||
self.move_abs_btn.setEnabled(False)
|
|
||||||
self.move_rel_btn.setEnabled(False)
|
|
||||||
self.stop_btn.setEnabled(False)
|
|
||||||
self.apply_vel_btn.setEnabled(False)
|
|
||||||
self.apply_accel_btn.setEnabled(False)
|
|
||||||
self.apply_trigger_btn.setEnabled(False)
|
|
||||||
self.save_settings_btn.setEnabled(False)
|
|
||||||
else:
|
|
||||||
self.log("ERROR: Failed to disconnect")
|
|
||||||
|
|
||||||
def identify_stage(self):
|
|
||||||
"""Flash LEDs to identify controller"""
|
|
||||||
self.log("Flashing LEDs for identification...")
|
|
||||||
if self.stage.identify():
|
|
||||||
self.log("Identification command sent")
|
|
||||||
else:
|
|
||||||
self.log("ERROR: Failed to send identify command")
|
|
||||||
|
|
||||||
# ==================== Homing Methods ====================
|
|
||||||
|
|
||||||
def home_all(self):
|
|
||||||
"""Home all axes"""
|
|
||||||
self.log("Homing all axes...")
|
|
||||||
if self.stage.home_all_axes(wait=False):
|
|
||||||
self.log("Homing started for all axes")
|
|
||||||
else:
|
|
||||||
self.log("ERROR: Failed to start homing")
|
|
||||||
|
|
||||||
def home_axis(self, axis: str):
|
|
||||||
"""Home specific axis"""
|
|
||||||
self.log(f"Homing {axis} axis...")
|
|
||||||
if self.stage.home_axis(axis, wait=False):
|
|
||||||
self.log(f"{axis} axis homing started")
|
|
||||||
else:
|
|
||||||
self.log(f"ERROR: Failed to home {axis} axis")
|
|
||||||
|
|
||||||
# ==================== Motion Methods ====================
|
|
||||||
|
|
||||||
def move_absolute(self):
|
|
||||||
"""Move to absolute position"""
|
|
||||||
x = self.abs_x_spin.value()
|
|
||||||
y = self.abs_y_spin.value()
|
|
||||||
z = self.abs_z_spin.value()
|
|
||||||
|
|
||||||
self.log(f"Moving to absolute position: X={x}, Y={y}, Z={z}")
|
|
||||||
|
|
||||||
if self.stage.move_absolute(x=x, y=y, z=z, wait=False):
|
|
||||||
self.log("Absolute move started")
|
|
||||||
else:
|
|
||||||
self.log("ERROR: Failed to start absolute move")
|
|
||||||
|
|
||||||
def move_relative(self):
|
|
||||||
"""Move relative distance"""
|
|
||||||
dx = self.rel_x_spin.value()
|
|
||||||
dy = self.rel_y_spin.value()
|
|
||||||
dz = self.rel_z_spin.value()
|
|
||||||
|
|
||||||
self.log(f"Moving relative: dX={dx}, dY={dy}, dZ={dz}")
|
|
||||||
|
|
||||||
if self.stage.move_relative(dx=dx, dy=dy, dz=dz, wait=False):
|
|
||||||
self.log("Relative move started")
|
|
||||||
else:
|
|
||||||
self.log("ERROR: Failed to start relative move")
|
|
||||||
|
|
||||||
def stop_all(self):
|
|
||||||
"""Stop all motion"""
|
|
||||||
self.log("STOPPING ALL MOTION")
|
|
||||||
if self.stage.stop_all(immediate=True):
|
|
||||||
self.log("Stop command sent")
|
|
||||||
else:
|
|
||||||
self.log("ERROR: Failed to send stop command")
|
|
||||||
|
|
||||||
# ==================== Status Update ====================
|
|
||||||
|
|
||||||
def update_status(self):
|
|
||||||
"""Update status display"""
|
|
||||||
if not self.stage.is_connected():
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Get status
|
|
||||||
status = self.stage.get_status()
|
|
||||||
position = self.stage.get_position()
|
|
||||||
|
|
||||||
# Update homed status
|
|
||||||
self.x_homed_label.setText("Yes" if status.get('x_homed') else "No")
|
|
||||||
self.x_homed_label.setStyleSheet(
|
|
||||||
"color: green;" if status.get('x_homed') else "color: red;"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.y_homed_label.setText("Yes" if status.get('y_homed') else "No")
|
|
||||||
self.y_homed_label.setStyleSheet(
|
|
||||||
"color: green;" if status.get('y_homed') else "color: red;"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.z_homed_label.setText("Yes" if status.get('z_homed') else "No")
|
|
||||||
self.z_homed_label.setStyleSheet(
|
|
||||||
"color: green;" if status.get('z_homed') else "color: red;"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update position
|
|
||||||
self.x_pos_label.setText(f"{position.get('x', 0.0):.3f} mm")
|
|
||||||
self.y_pos_label.setText(f"{position.get('y', 0.0):.3f} mm")
|
|
||||||
self.z_pos_label.setText(f"{position.get('z', 0.0):.3f} mm")
|
|
||||||
|
|
||||||
# Update ready/moving status
|
|
||||||
self.ready_label.setText("Yes" if status.get('ready') else "No")
|
|
||||||
self.ready_label.setStyleSheet(
|
|
||||||
"color: green; font-weight: bold;" if status.get('ready')
|
|
||||||
else "color: orange;"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.moving_label.setText("Yes" if status.get('moving') else "No")
|
|
||||||
self.moving_label.setStyleSheet(
|
|
||||||
"color: orange; font-weight: bold;" if status.get('moving')
|
|
||||||
else "color: green;"
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.log(f"ERROR: Failed to update status: {e}")
|
|
||||||
|
|
||||||
# ==================== Settings Methods ====================
|
|
||||||
|
|
||||||
def load_settings_to_ui(self):
|
|
||||||
"""Load current settings from stage into UI"""
|
|
||||||
if not self.stage.is_connected():
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Get current settings
|
|
||||||
velocities = self.stage.settings.get_all_velocities()
|
|
||||||
accelerations = self.stage.settings.get_all_accelerations()
|
|
||||||
|
|
||||||
# Update velocity spinboxes
|
|
||||||
self.vel_x_spin.setValue(velocities['x_axis'])
|
|
||||||
self.vel_y_spin.setValue(velocities['y_axis'])
|
|
||||||
self.vel_z_spin.setValue(velocities['z_axis'])
|
|
||||||
|
|
||||||
# Update acceleration spinboxes
|
|
||||||
self.accel_x_spin.setValue(accelerations['x_axis'])
|
|
||||||
self.accel_y_spin.setValue(accelerations['y_axis'])
|
|
||||||
self.accel_z_spin.setValue(accelerations['z_axis'])
|
|
||||||
|
|
||||||
# Update trigger settings for X axis (default)
|
|
||||||
trigger_config = self.stage.settings.get_trigger_config('x_axis')
|
|
||||||
self.trigger_mode_combo.setCurrentIndex(trigger_config.get('mode', 0))
|
|
||||||
|
|
||||||
polarity = trigger_config.get('polarity', 0x01)
|
|
||||||
self.trigger_polarity_combo.setCurrentIndex(0 if polarity == 0x01 else 1)
|
|
||||||
|
|
||||||
self.log("Settings loaded into UI")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.log(f"ERROR: Failed to load settings to UI: {e}")
|
|
||||||
|
|
||||||
def apply_velocity_settings(self):
|
|
||||||
"""Apply velocity settings to stage"""
|
|
||||||
self.log("Applying velocity settings...")
|
|
||||||
|
|
||||||
x = self.vel_x_spin.value()
|
|
||||||
y = self.vel_y_spin.value()
|
|
||||||
z = self.vel_z_spin.value()
|
|
||||||
|
|
||||||
if self.stage.configure_velocity(x=x, y=y, z=z, save=False):
|
|
||||||
self.log(f"Velocity settings applied: X={x}, Y={y}, Z={z} mm/s")
|
|
||||||
else:
|
|
||||||
self.log("ERROR: Failed to apply velocity settings")
|
|
||||||
|
|
||||||
def apply_acceleration_settings(self):
|
|
||||||
"""Apply acceleration settings to stage"""
|
|
||||||
self.log("Applying acceleration settings...")
|
|
||||||
|
|
||||||
x = self.accel_x_spin.value()
|
|
||||||
y = self.accel_y_spin.value()
|
|
||||||
z = self.accel_z_spin.value()
|
|
||||||
|
|
||||||
if self.stage.configure_acceleration(x=x, y=y, z=z, save=False):
|
|
||||||
self.log(f"Acceleration settings applied: X={x}, Y={y}, Z={z} mm/s²")
|
|
||||||
else:
|
|
||||||
self.log("ERROR: Failed to apply acceleration settings")
|
|
||||||
|
|
||||||
def apply_trigger_settings(self):
|
|
||||||
"""Apply trigger settings to stage"""
|
|
||||||
self.log("Applying trigger settings...")
|
|
||||||
|
|
||||||
axis = self.trigger_axis_combo.currentText()
|
|
||||||
mode_index = self.trigger_mode_combo.currentIndex()
|
|
||||||
|
|
||||||
# Map mode index to TriggerMode enum
|
|
||||||
mode_map = {
|
|
||||||
0: TriggerMode.DISABLED,
|
|
||||||
1: TriggerMode.IN_OUT_RELATIVE_MOVE,
|
|
||||||
2: TriggerMode.IN_OUT_ABSOLUTE_MOVE,
|
|
||||||
3: TriggerMode.IN_OUT_HOME,
|
|
||||||
4: TriggerMode.IN_OUT_STOP,
|
|
||||||
5: TriggerMode.OUT_ONLY,
|
|
||||||
6: TriggerMode.OUT_POSITION
|
|
||||||
}
|
|
||||||
mode = mode_map.get(mode_index, TriggerMode.DISABLED)
|
|
||||||
|
|
||||||
# Get polarity
|
|
||||||
polarity = 0x01 if self.trigger_polarity_combo.currentIndex() == 0 else 0x02
|
|
||||||
|
|
||||||
if self.stage.configure_trigger(axis, mode, polarity=polarity, save=False):
|
|
||||||
mode_name = self.trigger_mode_combo.currentText()
|
|
||||||
pol_name = self.trigger_polarity_combo.currentText()
|
|
||||||
self.log(f"Trigger settings applied: {axis} axis, {mode_name}, {pol_name}")
|
|
||||||
else:
|
|
||||||
self.log("ERROR: Failed to apply trigger settings")
|
|
||||||
|
|
||||||
def save_settings(self):
|
|
||||||
"""Save current settings to file"""
|
|
||||||
self.log("Saving settings to file...")
|
|
||||||
|
|
||||||
if self.stage.save_current_settings():
|
|
||||||
self.log("Settings saved successfully")
|
|
||||||
QMessageBox.information(self, "Settings Saved",
|
|
||||||
"Stage settings have been saved successfully.")
|
|
||||||
else:
|
|
||||||
self.log("ERROR: Failed to save settings")
|
|
||||||
QMessageBox.warning(self, "Save Failed",
|
|
||||||
"Failed to save stage settings.")
|
|
||||||
|
|
||||||
def load_settings(self):
|
|
||||||
"""Load settings from file"""
|
|
||||||
self.log("Loading settings from file...")
|
|
||||||
|
|
||||||
if self.stage.reload_settings():
|
|
||||||
self.log("Settings loaded successfully")
|
|
||||||
|
|
||||||
# Update UI with loaded settings
|
|
||||||
if self.stage.is_connected():
|
|
||||||
self.load_settings_to_ui()
|
|
||||||
|
|
||||||
# Apply to hardware if connected
|
|
||||||
if self.stage.is_connected():
|
|
||||||
self.stage.apply_startup_settings()
|
|
||||||
|
|
||||||
QMessageBox.information(self, "Settings Loaded",
|
|
||||||
"Stage settings have been loaded successfully.")
|
|
||||||
else:
|
|
||||||
self.log("WARNING: No settings file found, using defaults")
|
|
||||||
QMessageBox.information(self, "No Settings Found",
|
|
||||||
"No settings file found. Using default values.")
|
|
||||||
|
|
||||||
# ==================== Logging ====================
|
|
||||||
|
|
||||||
def log(self, message: str):
|
|
||||||
"""Add message to log"""
|
|
||||||
timestamp = time.strftime("%H:%M:%S")
|
|
||||||
self.log_text.append(f"[{timestamp}] {message}")
|
|
||||||
# Auto-scroll to bottom
|
|
||||||
scrollbar = self.log_text.verticalScrollBar()
|
|
||||||
scrollbar.setValue(scrollbar.maximum())
|
|
||||||
|
|
||||||
def closeEvent(self, event):
|
|
||||||
"""Handle window close event"""
|
|
||||||
if self.stage.is_connected():
|
|
||||||
reply = QMessageBox.question(
|
|
||||||
self, 'Disconnect Stage',
|
|
||||||
'Stage is still connected. Disconnect before closing?',
|
|
||||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
||||||
QMessageBox.StandardButton.Yes
|
|
||||||
)
|
|
||||||
|
|
||||||
if reply == QMessageBox.StandardButton.Yes:
|
|
||||||
self.stage.disconnect()
|
|
||||||
event.accept()
|
|
||||||
else:
|
|
||||||
event.ignore()
|
|
||||||
else:
|
|
||||||
event.accept()
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Main application entry point"""
|
|
||||||
app = QApplication(sys.argv)
|
|
||||||
window = StageTestWindow()
|
|
||||||
window.show()
|
|
||||||
sys.exit(app.exec())
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2026 Thomas K Ales
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,668 +0,0 @@
|
|||||||
# BBD202/203 Motion Controller Library
|
|
||||||
|
|
||||||
Python library for controlling Thorlabs BBD202/BBD203 motion controllers via FTDI interface using the APT protocol.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Full control of X and Y axes
|
|
||||||
- Absolute and relative positioning
|
|
||||||
- Configurable velocity and acceleration
|
|
||||||
- Automatic position tracking
|
|
||||||
- Status monitoring with convenient properties
|
|
||||||
- Thread-safe operation
|
|
||||||
- Context manager support for automatic cleanup
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
The FTDI D2XX driver is required for this code to work correctly. You will need to disable linux's
|
|
||||||
ftdi_sio module in order to use it. I am not sure why, and I have no intent of diagnosing it.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install pyftdi
|
|
||||||
```
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
```python
|
|
||||||
from bbd203_controller import MotionController
|
|
||||||
|
|
||||||
# Connect using context manager (automatic cleanup)
|
|
||||||
with MotionController() as mc:
|
|
||||||
# Print hardware information
|
|
||||||
print(f"Model: {mc.get_model()}")
|
|
||||||
print(f"Serial: {mc.get_serial_number()}")
|
|
||||||
print(f"Firmware: {mc.get_firmware_version()}")
|
|
||||||
|
|
||||||
# Enable and home X-axis
|
|
||||||
mc.set_channel_enable_state(mc.DEST_X_AXIS, enabled=True)
|
|
||||||
mc.home_x_axis(timeout=20.0)
|
|
||||||
|
|
||||||
# Move to absolute position
|
|
||||||
mc.set_velocity_params(mc.DEST_X_AXIS,
|
|
||||||
min_velocity=0.0,
|
|
||||||
acceleration=100.0,
|
|
||||||
max_velocity=50.0)
|
|
||||||
mc.set_move_abs_params(mc.DEST_X_AXIS, absolute_position=25.0)
|
|
||||||
result = mc.move_absolute(mc.DEST_X_AXIS, timeout=30.0)
|
|
||||||
|
|
||||||
print(f"Final position: {result['position']:.3f} mm")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Connection Management
|
|
||||||
|
|
||||||
### Basic Connection
|
|
||||||
|
|
||||||
```python
|
|
||||||
from bbd203_controller import MotionController
|
|
||||||
|
|
||||||
# Manual connection
|
|
||||||
mc = MotionController()
|
|
||||||
mc.connect()
|
|
||||||
|
|
||||||
# Use the controller...
|
|
||||||
|
|
||||||
mc.disconnect()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using Context Manager (Recommended)
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Automatic connection and cleanup
|
|
||||||
with MotionController() as mc:
|
|
||||||
# Use the controller...
|
|
||||||
pass # Automatically disconnects when exiting context
|
|
||||||
```
|
|
||||||
|
|
||||||
### Custom FTDI URL
|
|
||||||
|
|
||||||
```python
|
|
||||||
mc = MotionController(url='ftdi://0x0403:0xfaf0/1', baudrate=115200)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Axis Control
|
|
||||||
|
|
||||||
### Enabling Axes
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Enable X-axis
|
|
||||||
mc.set_channel_enable_state(mc.DEST_X_AXIS, enabled=True)
|
|
||||||
|
|
||||||
# Enable Y-axis
|
|
||||||
mc.set_channel_enable_state(mc.DEST_Y_AXIS, enabled=True)
|
|
||||||
|
|
||||||
# Check if enabled
|
|
||||||
is_enabled = mc.get_channel_enable_state(mc.DEST_X_AXIS)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Homing
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Home X-axis (blocks until complete)
|
|
||||||
if mc.home_x_axis(timeout=20.0):
|
|
||||||
print(f"X-axis homed at position: {mc.position_x} mm")
|
|
||||||
else:
|
|
||||||
print("Homing timed out")
|
|
||||||
|
|
||||||
# Home Y-axis
|
|
||||||
mc.home_y_axis(timeout=20.0)
|
|
||||||
```
|
|
||||||
|
|
||||||
After homing, the position automatically resets to 0 mm.
|
|
||||||
|
|
||||||
## Motion Control
|
|
||||||
|
|
||||||
### Setting Velocity Parameters
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Set velocity parameters for X-axis
|
|
||||||
mc.set_velocity_params(
|
|
||||||
mc.DEST_X_AXIS,
|
|
||||||
min_velocity=0.0, # mm/s
|
|
||||||
acceleration=100.0, # mm/s²
|
|
||||||
max_velocity=50.0 # mm/s
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get current velocity parameters
|
|
||||||
params = mc.get_velocity_params(mc.DEST_X_AXIS)
|
|
||||||
print(f"Max velocity: {params['max_velocity']:.2f} mm/s")
|
|
||||||
print(f"Acceleration: {params['acceleration']:.2f} mm/s²")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Setting Acceleration Only
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Change just the acceleration, preserving velocity settings
|
|
||||||
mc.set_acceleration(mc.DEST_X_AXIS, 75.0)
|
|
||||||
|
|
||||||
# Get just the acceleration value
|
|
||||||
accel = mc.get_acceleration(mc.DEST_X_AXIS)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Absolute Moves
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Move to absolute position
|
|
||||||
mc.set_move_abs_params(mc.DEST_X_AXIS, absolute_position=30.0)
|
|
||||||
result = mc.move_absolute(mc.DEST_X_AXIS, timeout=30.0)
|
|
||||||
|
|
||||||
if result:
|
|
||||||
print(f"Moved to: {result['position']:.3f} mm")
|
|
||||||
print(f"Status: 0x{result['status_bits']:08X}")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Relative Moves
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Move relative to current position
|
|
||||||
mc.set_move_rel_params(mc.DEST_X_AXIS, relative_distance=5.0)
|
|
||||||
result = mc.move_relative(mc.DEST_X_AXIS, timeout=30.0)
|
|
||||||
|
|
||||||
# Move backwards
|
|
||||||
mc.set_move_rel_params(mc.DEST_X_AXIS, relative_distance=-2.5)
|
|
||||||
mc.move_relative(mc.DEST_X_AXIS, timeout=30.0)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Stopping Motion
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Controlled stop (gradual deceleration)
|
|
||||||
mc.stop_x_axis(stop_mode=mc.StopMode.CONTROLLED, wait_for_stopped=True)
|
|
||||||
|
|
||||||
# Immediate stop
|
|
||||||
mc.stop_x_axis(stop_mode=mc.StopMode.IMMEDIATE, wait_for_stopped=True)
|
|
||||||
|
|
||||||
# Stop both axes simultaneously
|
|
||||||
results = mc.stop_all_axes(stop_mode=mc.StopMode.CONTROLLED)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Position Tracking
|
|
||||||
|
|
||||||
### Reading Current Position
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Query position from controller (blocking)
|
|
||||||
position = mc.get_position(mc.DEST_X_AXIS, timeout=5.0)
|
|
||||||
print(f"X position: {position:.3f} mm")
|
|
||||||
|
|
||||||
# Access cached position (non-blocking)
|
|
||||||
x_pos = mc.position_x
|
|
||||||
y_pos = mc.position_y
|
|
||||||
|
|
||||||
# Get encoder counts (raw values)
|
|
||||||
x_counts = mc.encoder_count_x
|
|
||||||
```
|
|
||||||
|
|
||||||
## Status Monitoring
|
|
||||||
|
|
||||||
### Using Status Properties
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Check various status flags
|
|
||||||
print(f"X-axis enabled: {mc.is_enabled_x}")
|
|
||||||
print(f"X-axis homed: {mc.is_homed_x}")
|
|
||||||
print(f"X-axis in motion: {mc.is_in_motion_x}")
|
|
||||||
print(f"X-axis settled: {mc.is_settled_x}")
|
|
||||||
print(f"X-axis has errors: {mc.has_errors_x}")
|
|
||||||
print(f"Power OK: {mc.power_ok_x}")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Decoding Status Bits
|
|
||||||
|
|
||||||
```python
|
|
||||||
result = mc.move_absolute(mc.DEST_X_AXIS, timeout=30.0)
|
|
||||||
|
|
||||||
if result:
|
|
||||||
status_bits = result['status_bits']
|
|
||||||
|
|
||||||
# Get human-readable description
|
|
||||||
description = MotionController.get_status_description(status_bits)
|
|
||||||
print(description)
|
|
||||||
|
|
||||||
# Check for errors
|
|
||||||
if MotionController.has_errors(status_bits):
|
|
||||||
print("ERROR: Motion completed with errors!")
|
|
||||||
|
|
||||||
# Check motion state
|
|
||||||
if MotionController.is_settled(status_bits):
|
|
||||||
print("Stage is settled at target position")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Available Status Checks
|
|
||||||
|
|
||||||
- `is_enabled_x` / `is_enabled_y` - Motor output enabled
|
|
||||||
- `is_homed_x` / `is_homed_y` - Axis has been homed
|
|
||||||
- `is_homing_x` / `is_homing_y` - Currently homing
|
|
||||||
- `is_in_motion_x` / `is_in_motion_y` - Currently moving
|
|
||||||
- `is_settled_x` / `is_settled_y` - Settled at target
|
|
||||||
- `is_tracking_x` / `is_tracking_y` - Within tracking window
|
|
||||||
- `is_connected_x` / `is_connected_y` - Motor recognized
|
|
||||||
- `has_errors_x` / `has_errors_y` - Any error condition
|
|
||||||
- `power_ok_x` / `power_ok_y` - Power supply OK
|
|
||||||
- `is_active_x` / `is_active_y` - Executing motion command
|
|
||||||
- `at_cw_limit_x` / `at_cw_limit_y` - At clockwise limit
|
|
||||||
- `at_ccw_limit_x` / `at_ccw_limit_y` - At counter-clockwise limit
|
|
||||||
|
|
||||||
## Multi-Axis Operations
|
|
||||||
|
|
||||||
### Simultaneous Moves (Using Threading)
|
|
||||||
|
|
||||||
```python
|
|
||||||
import threading
|
|
||||||
|
|
||||||
def move_x():
|
|
||||||
mc.set_move_abs_params(mc.DEST_X_AXIS, 50.0)
|
|
||||||
mc.move_absolute(mc.DEST_X_AXIS, timeout=30.0)
|
|
||||||
|
|
||||||
def move_y():
|
|
||||||
mc.set_move_abs_params(mc.DEST_Y_AXIS, 30.0)
|
|
||||||
mc.move_absolute(mc.DEST_Y_AXIS, timeout=30.0)
|
|
||||||
|
|
||||||
# Start both moves in parallel
|
|
||||||
x_thread = threading.Thread(target=move_x)
|
|
||||||
y_thread = threading.Thread(target=move_y)
|
|
||||||
|
|
||||||
x_thread.start()
|
|
||||||
y_thread.start()
|
|
||||||
|
|
||||||
# Wait for both to complete
|
|
||||||
x_thread.join()
|
|
||||||
y_thread.join()
|
|
||||||
|
|
||||||
print(f"Final position: ({mc.position_x:.2f}, {mc.position_y:.2f}) mm")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Hardware Information
|
|
||||||
|
|
||||||
```python
|
|
||||||
with MotionController() as mc:
|
|
||||||
# Individual fields
|
|
||||||
print(f"Serial Number: {mc.get_serial_number()}")
|
|
||||||
print(f"Model: {mc.get_model()}")
|
|
||||||
print(f"Firmware: {mc.get_firmware_version()}")
|
|
||||||
print(f"Hardware Version: {mc.get_hw_version()}")
|
|
||||||
print(f"Number of Channels: {mc.get_num_channels()}")
|
|
||||||
|
|
||||||
# All info at once
|
|
||||||
info = mc.get_hw_info()
|
|
||||||
for key, value in info.items():
|
|
||||||
print(f"{key}: {value}")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Complete Examples
|
|
||||||
|
|
||||||
### Example 1: Simple Linear Move
|
|
||||||
|
|
||||||
```python
|
|
||||||
from bbd203_controller import MotionController
|
|
||||||
import time
|
|
||||||
|
|
||||||
with MotionController() as mc:
|
|
||||||
# Enable and home X-axis
|
|
||||||
mc.set_channel_enable_state(mc.DEST_X_AXIS, enabled=True)
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
print("Homing X-axis...")
|
|
||||||
mc.home_x_axis(timeout=20.0)
|
|
||||||
print(f"Homed at {mc.position_x} mm")
|
|
||||||
|
|
||||||
# Set velocity for smooth motion
|
|
||||||
mc.set_velocity_params(mc.DEST_X_AXIS, 0.0, 50.0, 25.0)
|
|
||||||
|
|
||||||
# Move to 40mm
|
|
||||||
print("Moving to 40mm...")
|
|
||||||
mc.set_move_abs_params(mc.DEST_X_AXIS, 40.0)
|
|
||||||
result = mc.move_absolute(mc.DEST_X_AXIS, timeout=30.0)
|
|
||||||
|
|
||||||
if result and not mc.has_errors_x:
|
|
||||||
print(f"Successfully moved to {result['position']:.3f} mm")
|
|
||||||
else:
|
|
||||||
print("Move failed or has errors")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example 2: Square Pattern with Two Axes
|
|
||||||
|
|
||||||
```python
|
|
||||||
from bbd203_controller import MotionController
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
|
|
||||||
def move_to_position(mc, x, y, label):
|
|
||||||
"""Move to (x, y) with both axes moving simultaneously."""
|
|
||||||
print(f"Moving to {label}: ({x}, {y}) mm")
|
|
||||||
|
|
||||||
# Set parameters for both axes
|
|
||||||
mc.set_move_abs_params(mc.DEST_X_AXIS, x)
|
|
||||||
mc.set_move_abs_params(mc.DEST_Y_AXIS, y)
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
# Execute moves in parallel
|
|
||||||
results = [None, None]
|
|
||||||
|
|
||||||
def move_x():
|
|
||||||
results[0] = mc.move_absolute(mc.DEST_X_AXIS, timeout=30.0)
|
|
||||||
|
|
||||||
def move_y():
|
|
||||||
results[1] = mc.move_absolute(mc.DEST_Y_AXIS, timeout=30.0)
|
|
||||||
|
|
||||||
x_thread = threading.Thread(target=move_x)
|
|
||||||
y_thread = threading.Thread(target=move_y)
|
|
||||||
|
|
||||||
x_thread.start()
|
|
||||||
y_thread.start()
|
|
||||||
x_thread.join()
|
|
||||||
y_thread.join()
|
|
||||||
|
|
||||||
if results[0] and results[1]:
|
|
||||||
print(f" Reached ({results[0]['position']:.2f}, {results[1]['position']:.2f}) mm")
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
with MotionController() as mc:
|
|
||||||
# Enable both axes
|
|
||||||
mc.set_channel_enable_state(mc.DEST_X_AXIS, enabled=True)
|
|
||||||
mc.set_channel_enable_state(mc.DEST_Y_AXIS, enabled=True)
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
# Home both axes
|
|
||||||
print("Homing axes...")
|
|
||||||
mc.home_x_axis(timeout=20.0)
|
|
||||||
mc.home_y_axis(timeout=20.0)
|
|
||||||
|
|
||||||
# Set velocity for both axes
|
|
||||||
velocity = 50.0
|
|
||||||
acceleration = 100.0
|
|
||||||
mc.set_velocity_params(mc.DEST_X_AXIS, 0.0, acceleration, velocity)
|
|
||||||
mc.set_velocity_params(mc.DEST_Y_AXIS, 0.0, acceleration, velocity)
|
|
||||||
|
|
||||||
# Define 20mm square centered at (55, 37.5)
|
|
||||||
center_x, center_y = 55.0, 37.5
|
|
||||||
half_size = 10.0
|
|
||||||
|
|
||||||
waypoints = [
|
|
||||||
(center_x - half_size, center_y - half_size, "Bottom Left"),
|
|
||||||
(center_x + half_size, center_y - half_size, "Bottom Right"),
|
|
||||||
(center_x + half_size, center_y + half_size, "Top Right"),
|
|
||||||
(center_x - half_size, center_y + half_size, "Top Left"),
|
|
||||||
(center_x, center_y, "Center"),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Execute square pattern
|
|
||||||
for x, y, label in waypoints:
|
|
||||||
if not move_to_position(mc, x, y, label):
|
|
||||||
print(f"Failed at {label}")
|
|
||||||
break
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
print("Square pattern complete!")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example 3: Velocity Ramping Test
|
|
||||||
|
|
||||||
```python
|
|
||||||
from bbd203_controller import MotionController
|
|
||||||
import time
|
|
||||||
|
|
||||||
with MotionController() as mc:
|
|
||||||
mc.set_channel_enable_state(mc.DEST_X_AXIS, enabled=True)
|
|
||||||
time.sleep(0.5)
|
|
||||||
mc.home_x_axis(timeout=20.0)
|
|
||||||
|
|
||||||
# Test at different velocities
|
|
||||||
test_velocities = [10.0, 25.0, 50.0, 100.0]
|
|
||||||
move_distance = 20.0
|
|
||||||
|
|
||||||
for velocity in test_velocities:
|
|
||||||
print(f"\n--- Testing at {velocity} mm/s ---")
|
|
||||||
|
|
||||||
# Set velocity parameters
|
|
||||||
mc.set_velocity_params(mc.DEST_X_AXIS, 0.0, 100.0, velocity)
|
|
||||||
|
|
||||||
# Move forward
|
|
||||||
mc.set_move_abs_params(mc.DEST_X_AXIS, move_distance)
|
|
||||||
start_time = time.time()
|
|
||||||
result = mc.move_absolute(mc.DEST_X_AXIS, timeout=30.0)
|
|
||||||
elapsed = time.time() - start_time
|
|
||||||
|
|
||||||
if result:
|
|
||||||
print(f" Moved {move_distance}mm in {elapsed:.2f}s")
|
|
||||||
print(f" Average speed: {move_distance/elapsed:.2f} mm/s")
|
|
||||||
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
# Move back to start
|
|
||||||
mc.set_move_abs_params(mc.DEST_X_AXIS, 0.0)
|
|
||||||
mc.move_absolute(mc.DEST_X_AXIS, timeout=30.0)
|
|
||||||
time.sleep(0.5)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example 4: Position Monitoring During Move
|
|
||||||
|
|
||||||
```python
|
|
||||||
from bbd203_controller import MotionController
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
|
|
||||||
with MotionController() as mc:
|
|
||||||
mc.set_channel_enable_state(mc.DEST_X_AXIS, enabled=True)
|
|
||||||
time.sleep(0.5)
|
|
||||||
mc.home_x_axis(timeout=20.0)
|
|
||||||
|
|
||||||
# Set slow velocity for visible monitoring
|
|
||||||
mc.set_velocity_params(mc.DEST_X_AXIS, 0.0, 50.0, 10.0)
|
|
||||||
|
|
||||||
# Start move in background thread
|
|
||||||
move_complete = threading.Event()
|
|
||||||
|
|
||||||
def do_move():
|
|
||||||
mc.set_move_abs_params(mc.DEST_X_AXIS, 50.0)
|
|
||||||
mc.move_absolute(mc.DEST_X_AXIS, timeout=60.0)
|
|
||||||
move_complete.set()
|
|
||||||
|
|
||||||
move_thread = threading.Thread(target=do_move)
|
|
||||||
move_thread.start()
|
|
||||||
|
|
||||||
# Monitor position while moving
|
|
||||||
print("Position monitoring:")
|
|
||||||
while not move_complete.is_set():
|
|
||||||
# Request current position
|
|
||||||
pos = mc.get_position(mc.DEST_X_AXIS, timeout=1.0)
|
|
||||||
if pos is not None:
|
|
||||||
print(f" Current position: {pos:.3f} mm, "
|
|
||||||
f"In motion: {mc.is_in_motion_x}, "
|
|
||||||
f"Settled: {mc.is_settled_x}")
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
move_thread.join()
|
|
||||||
print(f"Move complete! Final position: {mc.position_x:.3f} mm")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Constants and Enumerations
|
|
||||||
|
|
||||||
### Axis Destinations
|
|
||||||
|
|
||||||
```python
|
|
||||||
mc.DEST_CONTROLLER # 0x11 - Controller/motherboard
|
|
||||||
mc.DEST_X_AXIS # 0x21 - X-axis
|
|
||||||
mc.DEST_Y_AXIS # 0x22 - Y-axis
|
|
||||||
```
|
|
||||||
|
|
||||||
### Stop Modes
|
|
||||||
|
|
||||||
```python
|
|
||||||
from bbd203_controller import StopMode
|
|
||||||
|
|
||||||
StopMode.IMMEDIATE # 1 - Instant stop
|
|
||||||
StopMode.CONTROLLED # 2 - Controlled deceleration (default)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Jog Modes
|
|
||||||
|
|
||||||
```python
|
|
||||||
from bbd203_controller import JogMode
|
|
||||||
|
|
||||||
JogMode.CONTINUOUS # 1 - Continuous jogging
|
|
||||||
JogMode.SINGLE_STEP # 2 - Single step jogging
|
|
||||||
```
|
|
||||||
|
|
||||||
### Channel Enable States
|
|
||||||
|
|
||||||
```python
|
|
||||||
from bbd203_controller import ChannelEnableState
|
|
||||||
|
|
||||||
ChannelEnableState.DISABLED # 0x02
|
|
||||||
ChannelEnableState.ENABLED # 0x01
|
|
||||||
```
|
|
||||||
|
|
||||||
## Scaling Factors
|
|
||||||
|
|
||||||
The library handles all unit conversions automatically:
|
|
||||||
|
|
||||||
- **Position**: 20,000 encoder counts per mm
|
|
||||||
- **Velocity**: 13,421.77 counts per mm/s
|
|
||||||
- **Acceleration**: 13.744 counts per mm/s²
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
```python
|
|
||||||
from bbd203_controller import MotionController
|
|
||||||
|
|
||||||
try:
|
|
||||||
with MotionController() as mc:
|
|
||||||
# Invalid axis destination
|
|
||||||
mc.set_channel_enable_state(0x99, enabled=True)
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"ValueError: {e}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
with MotionController() as mc:
|
|
||||||
mc.set_channel_enable_state(mc.DEST_X_AXIS, enabled=True)
|
|
||||||
mc.home_x_axis(timeout=5.0) # Too short timeout
|
|
||||||
|
|
||||||
if not mc.is_homed_x:
|
|
||||||
print("Homing failed - axis not homed")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error: {e}")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Advanced Features
|
|
||||||
|
|
||||||
### Message Callbacks
|
|
||||||
|
|
||||||
```python
|
|
||||||
from bbd203_controller import MotionController, AptMessage, MsgId
|
|
||||||
|
|
||||||
def on_move_stopped(msg: AptMessage):
|
|
||||||
print(f"Axis stopped unexpectedly!")
|
|
||||||
print(f"Source: 0x{msg.source:02X}")
|
|
||||||
|
|
||||||
with MotionController() as mc:
|
|
||||||
# Register callback for stop events
|
|
||||||
mc.register_callback(MsgId.MOT_MOVE_STOPPED, on_move_stopped)
|
|
||||||
|
|
||||||
# Your motion code here...
|
|
||||||
|
|
||||||
# Unregister when done
|
|
||||||
mc.unregister_callback(MsgId.MOT_MOVE_STOPPED, on_move_stopped)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Direct Message Access
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Wait for a specific message type
|
|
||||||
msg = mc.wait_for_message(MsgId.MOT_MOVE_COMPLETED, timeout=30.0)
|
|
||||||
|
|
||||||
# Get next message from queue
|
|
||||||
msg = mc.get_message(timeout=0.1)
|
|
||||||
|
|
||||||
# Get all queued messages
|
|
||||||
messages = mc.get_all_messages()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Manual Connection Control
|
|
||||||
|
|
||||||
```python
|
|
||||||
mc = MotionController()
|
|
||||||
|
|
||||||
# Connect with updates disabled
|
|
||||||
mc.connect(enable_updates=False)
|
|
||||||
|
|
||||||
# Manually start/stop status updates
|
|
||||||
mc.start_update_messages()
|
|
||||||
# ...
|
|
||||||
mc.stop_update_messages()
|
|
||||||
|
|
||||||
mc.disconnect()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tips for Linear Scans
|
|
||||||
|
|
||||||
For performing linear scans at controlled speeds:
|
|
||||||
|
|
||||||
1. **Set velocity parameters** before each scan to ensure consistent motion
|
|
||||||
2. **Use absolute moves** with pre-calculated waypoints for accuracy
|
|
||||||
3. **For continuous scanning**: Execute moves sequentially without waiting
|
|
||||||
4. **For synchronized 2-axis moves**: Use threading (see examples above)
|
|
||||||
5. **Monitor position** during moves if needed for data acquisition timing
|
|
||||||
|
|
||||||
### Simple 1D Linear Scan
|
|
||||||
|
|
||||||
```python
|
|
||||||
with MotionController() as mc:
|
|
||||||
mc.set_channel_enable_state(mc.DEST_X_AXIS, enabled=True)
|
|
||||||
mc.home_x_axis(timeout=20.0)
|
|
||||||
|
|
||||||
# Scan parameters
|
|
||||||
start_pos = 10.0 # mm
|
|
||||||
end_pos = 90.0 # mm
|
|
||||||
step_size = 2.0 # mm
|
|
||||||
scan_speed = 20.0 # mm/s
|
|
||||||
|
|
||||||
# Set velocity for consistent speed
|
|
||||||
mc.set_velocity_params(mc.DEST_X_AXIS, 0.0, 100.0, scan_speed)
|
|
||||||
|
|
||||||
# Execute scan
|
|
||||||
position = start_pos
|
|
||||||
while position <= end_pos:
|
|
||||||
mc.set_move_abs_params(mc.DEST_X_AXIS, position)
|
|
||||||
result = mc.move_absolute(mc.DEST_X_AXIS, timeout=30.0)
|
|
||||||
|
|
||||||
if result:
|
|
||||||
# Acquire data at this position
|
|
||||||
print(f"Scan point at {result['position']:.3f} mm")
|
|
||||||
# Your data acquisition code here...
|
|
||||||
|
|
||||||
position += step_size
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Controller Not Responding
|
|
||||||
|
|
||||||
If the controller stops responding after many commands:
|
|
||||||
- The library automatically sends ACK messages every second
|
|
||||||
- This is handled internally and should not require user intervention
|
|
||||||
|
|
||||||
### Moves Timing Out
|
|
||||||
|
|
||||||
- Increase the `timeout` parameter on move commands
|
|
||||||
- Check that velocity and acceleration are set appropriately
|
|
||||||
- Ensure the axis is enabled and homed
|
|
||||||
|
|
||||||
### Position Inaccurate After Homing
|
|
||||||
|
|
||||||
- Position automatically resets to 0 mm after homing completes
|
|
||||||
- Always wait for homing to complete before issuing move commands
|
|
||||||
- Check `is_homed_x` / `is_homed_y` properties to verify
|
|
||||||
|
|
||||||
### Unexpected Stops
|
|
||||||
|
|
||||||
- Register a callback for `MsgId.MOT_MOVE_STOPPED` to detect stop events
|
|
||||||
- Check error flags in status bits
|
|
||||||
- Ensure no limit switches are being triggered
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
## Author
|
|
||||||
|
|
||||||
Generated for BBD202/BBD203 motion controller control via APT protocol.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
msodev/
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test script for acquisition mode get/set functions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from tektronix_base import TektronixOscilloscopeBase
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Test acquisition mode functions on oscilloscope at 192.168.10.105"""
|
|
||||||
|
|
||||||
scope_ip = "192.168.10.105"
|
|
||||||
|
|
||||||
print(f"Connecting to oscilloscope at {scope_ip}...")
|
|
||||||
|
|
||||||
scope = TektronixOscilloscopeBase(resource_name=scope_ip)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Connect to scope
|
|
||||||
scope.connect()
|
|
||||||
print(f"✓ Connected to {scope.identify()}\n")
|
|
||||||
|
|
||||||
# Get current acquisition mode
|
|
||||||
print("Getting current acquisition mode...")
|
|
||||||
current_mode = scope.get_acquire_mode()
|
|
||||||
print(f"✓ Current acquisition mode: {current_mode}\n")
|
|
||||||
|
|
||||||
# Test setting different acquisition modes
|
|
||||||
print("Testing acquisition mode changes:")
|
|
||||||
test_modes = ['SAMple', 'HIRes', 'AVErage', 'PEAKdetect', 'ENVelope']
|
|
||||||
|
|
||||||
for mode in test_modes:
|
|
||||||
print(f"\n Setting mode to: {mode}")
|
|
||||||
scope.set_acquire_mode(mode)
|
|
||||||
|
|
||||||
# Verify the change
|
|
||||||
actual_mode = scope.get_acquire_mode()
|
|
||||||
if actual_mode == mode:
|
|
||||||
print(f" ✓ Verified: {actual_mode}")
|
|
||||||
else:
|
|
||||||
print(f" ✗ Mismatch: expected {mode}, got {actual_mode}")
|
|
||||||
|
|
||||||
# Restore original mode
|
|
||||||
print(f"\nRestoring original mode: {current_mode}")
|
|
||||||
scope.set_acquire_mode(current_mode)
|
|
||||||
print(f"✓ Restored to: {scope.get_acquire_mode()}")
|
|
||||||
|
|
||||||
# Test invalid mode
|
|
||||||
print("\nTesting invalid mode (should raise ValueError)...")
|
|
||||||
try:
|
|
||||||
scope.set_acquire_mode("INVALID")
|
|
||||||
print("✗ ERROR: Should have raised ValueError!")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"✓ Correctly raised ValueError: {e}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Error: {type(e).__name__}: {e}")
|
|
||||||
finally:
|
|
||||||
if scope.is_connected:
|
|
||||||
scope.disconnect()
|
|
||||||
print("\n✓ Disconnected from oscilloscope")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test script for channel control functionality.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from tektronix_base import TektronixOscilloscopeBase
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Test channel control functions on oscilloscope at 192.168.10.105"""
|
|
||||||
|
|
||||||
scope_ip = "192.168.10.105"
|
|
||||||
|
|
||||||
print(f"Connecting to oscilloscope at {scope_ip}...")
|
|
||||||
|
|
||||||
scope = TektronixOscilloscopeBase(resource_name=scope_ip)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Connect to scope
|
|
||||||
scope.connect()
|
|
||||||
print(f"Connected to {scope.identify()}\n")
|
|
||||||
|
|
||||||
# Test with CH1
|
|
||||||
test_channel = 1
|
|
||||||
print(f"=== Testing Channel {test_channel} ===\n")
|
|
||||||
|
|
||||||
# Query all channel parameters
|
|
||||||
print("Querying all channel parameters...")
|
|
||||||
all_params = scope.query_channel(test_channel)
|
|
||||||
print(f"CH{test_channel} parameters: {all_params[:100]}...\n")
|
|
||||||
|
|
||||||
# Get current settings to restore later
|
|
||||||
print("Getting current channel settings...")
|
|
||||||
current_bandwidth = scope.get_channel_bandwidth(test_channel)
|
|
||||||
current_coupling = scope.get_channel_coupling(test_channel)
|
|
||||||
current_termination = scope.get_channel_termination(test_channel)
|
|
||||||
current_scale = scope.get_channel_scale(test_channel)
|
|
||||||
current_offset = scope.get_channel_offset(test_channel)
|
|
||||||
current_position = scope.get_channel_position(test_channel)
|
|
||||||
current_label_name = scope.get_channel_label_name(test_channel)
|
|
||||||
current_label_color = scope.get_channel_label_color(test_channel)
|
|
||||||
current_label_font_size = scope.get_channel_label_font_size(test_channel)
|
|
||||||
current_label_font_type = scope.get_channel_label_font_type(test_channel)
|
|
||||||
current_label_xpos = scope.get_channel_label_xpos(test_channel)
|
|
||||||
current_label_ypos = scope.get_channel_label_ypos(test_channel)
|
|
||||||
|
|
||||||
print(f" Current bandwidth: {current_bandwidth}")
|
|
||||||
print(f" Current coupling: {current_coupling}")
|
|
||||||
print(f" Current termination: {current_termination} ohms")
|
|
||||||
print(f" Current scale: {current_scale} V/div")
|
|
||||||
print(f" Current offset: {current_offset} V")
|
|
||||||
print(f" Current position: {current_position} divisions")
|
|
||||||
print(f" Current label name: {current_label_name}")
|
|
||||||
print(f" Current label color: {current_label_color}")
|
|
||||||
print(f" Current label font size: {current_label_font_size} pt")
|
|
||||||
print(f" Current label font type: {current_label_font_type}")
|
|
||||||
print(f" Current label X position: {current_label_xpos} px")
|
|
||||||
print(f" Current label Y position: {current_label_ypos} px\n")
|
|
||||||
|
|
||||||
# Test coupling modes
|
|
||||||
print("Testing coupling modes:")
|
|
||||||
for coupling in ['DC', 'AC']:
|
|
||||||
print(f" Setting coupling to {coupling}...")
|
|
||||||
scope.set_channel_coupling(test_channel, coupling)
|
|
||||||
actual = scope.get_channel_coupling(test_channel)
|
|
||||||
print(f" Actual coupling: {actual}")
|
|
||||||
|
|
||||||
# Test termination
|
|
||||||
print("\nTesting termination settings:")
|
|
||||||
for term in [50, 1000000]:
|
|
||||||
print(f" Setting termination to {term} ohms...")
|
|
||||||
scope.set_channel_termination(test_channel, term)
|
|
||||||
actual = scope.get_channel_termination(test_channel)
|
|
||||||
print(f" Actual termination: {actual} ohms")
|
|
||||||
|
|
||||||
# Test vertical scale
|
|
||||||
print("\nTesting vertical scale:")
|
|
||||||
test_scales = [0.1, 0.5, 1.0, 2.0]
|
|
||||||
for scale in test_scales:
|
|
||||||
print(f" Setting scale to {scale} V/div...")
|
|
||||||
scope.set_channel_scale(test_channel, scale)
|
|
||||||
actual = scope.get_channel_scale(test_channel)
|
|
||||||
print(f" Actual scale: {actual} V/div")
|
|
||||||
|
|
||||||
# Test vertical offset
|
|
||||||
print("\nTesting vertical offset:")
|
|
||||||
test_offsets = [0.0, 0.5, -0.5, 1.0]
|
|
||||||
for offset in test_offsets:
|
|
||||||
print(f" Setting offset to {offset} V...")
|
|
||||||
scope.set_channel_offset(test_channel, offset)
|
|
||||||
actual = scope.get_channel_offset(test_channel)
|
|
||||||
print(f" Actual offset: {actual} V")
|
|
||||||
|
|
||||||
# Test vertical position
|
|
||||||
print("\nTesting vertical position:")
|
|
||||||
test_positions = [0.0, 1.0, -1.0, 2.5]
|
|
||||||
for position in test_positions:
|
|
||||||
print(f" Setting position to {position} divisions...")
|
|
||||||
scope.set_channel_position(test_channel, position)
|
|
||||||
actual = scope.get_channel_position(test_channel)
|
|
||||||
print(f" Actual position: {actual} divisions")
|
|
||||||
|
|
||||||
# Test label name
|
|
||||||
print("\nTesting label name:")
|
|
||||||
test_names = ["Test Signal", "CH1-Custom", "Probe Input"]
|
|
||||||
for name in test_names:
|
|
||||||
print(f" Setting label to '{name}'...")
|
|
||||||
scope.set_channel_label_name(test_channel, name)
|
|
||||||
actual = scope.get_channel_label_name(test_channel)
|
|
||||||
print(f" Actual label: {actual}")
|
|
||||||
|
|
||||||
# Test label color
|
|
||||||
print("\nTesting label color:")
|
|
||||||
test_colors = ["#FF0000", "#00FF00", "#0000FF", "#FFFF00"]
|
|
||||||
for color in test_colors:
|
|
||||||
print(f" Setting color to {color}...")
|
|
||||||
scope.set_channel_label_color(test_channel, color)
|
|
||||||
actual = scope.get_channel_label_color(test_channel)
|
|
||||||
print(f" Actual color: {actual}")
|
|
||||||
|
|
||||||
# Test label font size
|
|
||||||
print("\nTesting label font size:")
|
|
||||||
test_sizes = [10, 12, 14, 16]
|
|
||||||
for size in test_sizes:
|
|
||||||
print(f" Setting font size to {size} pt...")
|
|
||||||
scope.set_channel_label_font_size(test_channel, size)
|
|
||||||
actual = scope.get_channel_label_font_size(test_channel)
|
|
||||||
print(f" Actual font size: {actual} pt")
|
|
||||||
|
|
||||||
# Test label font type
|
|
||||||
print("\nTesting label font type:")
|
|
||||||
test_fonts = ["Arial", "Helvetica", "Courier"]
|
|
||||||
for font in test_fonts:
|
|
||||||
print(f" Setting font to {font}...")
|
|
||||||
scope.set_channel_label_font_type(test_channel, font)
|
|
||||||
actual = scope.get_channel_label_font_type(test_channel)
|
|
||||||
print(f" Actual font: {actual}")
|
|
||||||
|
|
||||||
# Test label position
|
|
||||||
print("\nTesting label X position:")
|
|
||||||
test_xpos = [100, 200, 300]
|
|
||||||
for xpos in test_xpos:
|
|
||||||
print(f" Setting X position to {xpos} px...")
|
|
||||||
scope.set_channel_label_xpos(test_channel, xpos)
|
|
||||||
actual = scope.get_channel_label_xpos(test_channel)
|
|
||||||
print(f" Actual X position: {actual} px")
|
|
||||||
|
|
||||||
print("\nTesting label Y position:")
|
|
||||||
test_ypos = [50, 100, 150]
|
|
||||||
for ypos in test_ypos:
|
|
||||||
print(f" Setting Y position to {ypos} px...")
|
|
||||||
scope.set_channel_label_ypos(test_channel, ypos)
|
|
||||||
actual = scope.get_channel_label_ypos(test_channel)
|
|
||||||
print(f" Actual Y position: {actual} px")
|
|
||||||
|
|
||||||
# Test using string channel format
|
|
||||||
print("\nTesting with string channel format ('CH2'):")
|
|
||||||
print(" Setting CH2 coupling to AC...")
|
|
||||||
scope.set_channel_coupling('CH2', 'AC')
|
|
||||||
actual = scope.get_channel_coupling('CH2')
|
|
||||||
print(f" Actual CH2 coupling: {actual}")
|
|
||||||
|
|
||||||
print(" Setting CH2 scale to 0.5 V/div...")
|
|
||||||
scope.set_channel_scale('CH2', 0.5)
|
|
||||||
actual = scope.get_channel_scale('CH2')
|
|
||||||
print(f" Actual CH2 scale: {actual} V/div")
|
|
||||||
|
|
||||||
# Restore original settings
|
|
||||||
print(f"\nRestoring original settings for CH{test_channel}...")
|
|
||||||
scope.set_channel_coupling(test_channel, current_coupling)
|
|
||||||
scope.set_channel_termination(test_channel, current_termination)
|
|
||||||
scope.set_channel_scale(test_channel, current_scale)
|
|
||||||
scope.set_channel_offset(test_channel, current_offset)
|
|
||||||
scope.set_channel_position(test_channel, current_position)
|
|
||||||
scope.set_channel_label_name(test_channel, current_label_name)
|
|
||||||
scope.set_channel_label_color(test_channel, current_label_color)
|
|
||||||
scope.set_channel_label_font_size(test_channel, current_label_font_size)
|
|
||||||
scope.set_channel_label_font_type(test_channel, current_label_font_type)
|
|
||||||
scope.set_channel_label_xpos(test_channel, current_label_xpos)
|
|
||||||
scope.set_channel_label_ypos(test_channel, current_label_ypos)
|
|
||||||
print(f" Restored coupling: {scope.get_channel_coupling(test_channel)}")
|
|
||||||
print(f" Restored termination: {scope.get_channel_termination(test_channel)} ohms")
|
|
||||||
print(f" Restored scale: {scope.get_channel_scale(test_channel)} V/div")
|
|
||||||
|
|
||||||
# Test invalid inputs
|
|
||||||
print("\n=== Testing Error Handling ===\n")
|
|
||||||
|
|
||||||
print("Testing invalid channel number (should raise ValueError)...")
|
|
||||||
try:
|
|
||||||
scope.get_channel_coupling(5)
|
|
||||||
print(" ERROR: Should have raised ValueError!")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f" Correctly raised ValueError: {e}")
|
|
||||||
|
|
||||||
print("\nTesting invalid channel string (should raise ValueError)...")
|
|
||||||
try:
|
|
||||||
scope.get_channel_coupling('CH5')
|
|
||||||
print(" ERROR: Should have raised ValueError!")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f" Correctly raised ValueError: {e}")
|
|
||||||
|
|
||||||
print("\nTesting invalid coupling (should raise ValueError)...")
|
|
||||||
try:
|
|
||||||
scope.set_channel_coupling(1, 'INVALID')
|
|
||||||
print(" ERROR: Should have raised ValueError!")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f" Correctly raised ValueError: {e}")
|
|
||||||
|
|
||||||
print("\nTesting invalid termination (should raise ValueError)...")
|
|
||||||
try:
|
|
||||||
scope.set_channel_termination(1, 75)
|
|
||||||
print(" ERROR: Should have raised ValueError!")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f" Correctly raised ValueError: {e}")
|
|
||||||
|
|
||||||
print("\nTesting invalid scale (should raise ValueError)...")
|
|
||||||
try:
|
|
||||||
scope.set_channel_scale(1, -1.0)
|
|
||||||
print(" ERROR: Should have raised ValueError!")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f" Correctly raised ValueError: {e}")
|
|
||||||
|
|
||||||
print("\nTesting invalid color format (should raise ValueError)...")
|
|
||||||
try:
|
|
||||||
scope.set_channel_label_color(1, "FF0000") # Missing #
|
|
||||||
print(" ERROR: Should have raised ValueError!")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f" Correctly raised ValueError: {e}")
|
|
||||||
|
|
||||||
print("\nTesting invalid font size (should raise ValueError)...")
|
|
||||||
try:
|
|
||||||
scope.set_channel_label_font_size(1, -5)
|
|
||||||
print(" ERROR: Should have raised ValueError!")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f" Correctly raised ValueError: {e}")
|
|
||||||
|
|
||||||
print("\nTesting invalid X position (should raise ValueError)...")
|
|
||||||
try:
|
|
||||||
scope.set_channel_label_xpos(1, -10)
|
|
||||||
print(" ERROR: Should have raised ValueError!")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f" Correctly raised ValueError: {e}")
|
|
||||||
|
|
||||||
print("\n=== All tests completed successfully! ===")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error: {type(e).__name__}: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
finally:
|
|
||||||
if scope.is_connected:
|
|
||||||
scope.disconnect()
|
|
||||||
print("\nDisconnected from oscilloscope")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test script to connect to a Tektronix oscilloscope via raw sockets.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from tektronix_base import TektronixOscilloscopeBase
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Test connection to oscilloscope at 192.168.10.105"""
|
|
||||||
|
|
||||||
scope_ip = "192.168.10.105"
|
|
||||||
scope_port = 4000
|
|
||||||
|
|
||||||
print(f"Attempting to connect to oscilloscope at {scope_ip}:{scope_port}...")
|
|
||||||
|
|
||||||
scope = TektronixOscilloscopeBase(resource_name=scope_ip, port=scope_port, timeout=5.0)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Attempt connection
|
|
||||||
scope.connect()
|
|
||||||
print(f"✓ Successfully connected to {scope_ip}:{scope_port}")
|
|
||||||
print(f"✓ Connection status: {scope.is_connected}")
|
|
||||||
|
|
||||||
# Get instrument identification
|
|
||||||
print("\nQuerying instrument identification...")
|
|
||||||
idn = scope.identify()
|
|
||||||
print(f"✓ Instrument ID: {idn}")
|
|
||||||
|
|
||||||
# Test a simple query
|
|
||||||
print("\nTesting SCPI query...")
|
|
||||||
response = scope.query("*OPT?")
|
|
||||||
print(f"✓ Installed options: {response}")
|
|
||||||
|
|
||||||
# Test a write command
|
|
||||||
print("\nTesting SCPI write command...")
|
|
||||||
scope.write("*CLS")
|
|
||||||
print("✓ Cleared status registers")
|
|
||||||
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"✗ Configuration error: {e}")
|
|
||||||
except ConnectionError as e:
|
|
||||||
print(f"✗ Connection failed: {e}")
|
|
||||||
print("\nTroubleshooting tips:")
|
|
||||||
print(" - Verify the oscilloscope IP address is correct")
|
|
||||||
print(" - Check network connectivity (try: ping 192.168.10.105)")
|
|
||||||
print(" - Ensure the oscilloscope has LXI/socket server enabled")
|
|
||||||
print(" - Verify port 4000 is correct (check scope network settings)")
|
|
||||||
except RuntimeError as e:
|
|
||||||
print(f"✗ Runtime error: {e}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Unexpected error: {type(e).__name__}: {e}")
|
|
||||||
finally:
|
|
||||||
# Always disconnect
|
|
||||||
if scope.is_connected:
|
|
||||||
scope.disconnect()
|
|
||||||
print("\n✓ Disconnected from oscilloscope")
|
|
||||||
else:
|
|
||||||
print("\n✗ Not connected")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test script for FastFrame functionality.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from tektronix_base import TektronixOscilloscopeBase
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Test FastFrame functions on oscilloscope at 192.168.10.105"""
|
|
||||||
|
|
||||||
scope_ip = "192.168.10.105"
|
|
||||||
|
|
||||||
print(f"Connecting to oscilloscope at {scope_ip}...")
|
|
||||||
|
|
||||||
scope = TektronixOscilloscopeBase(resource_name=scope_ip)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Connect to scope
|
|
||||||
scope.connect()
|
|
||||||
print(f"✓ Connected to {scope.identify()}\n")
|
|
||||||
|
|
||||||
# Get current FastFrame state
|
|
||||||
print("Getting current FastFrame state...")
|
|
||||||
current_state = scope.get_fastframe_state()
|
|
||||||
print(f"✓ Current FastFrame state: {current_state} ({'active' if current_state else 'off'})\n")
|
|
||||||
|
|
||||||
# Get current frame count
|
|
||||||
print("Getting current frame count...")
|
|
||||||
current_count = scope.get_fastframe_count()
|
|
||||||
print(f"✓ Current frame count: {current_count}\n")
|
|
||||||
|
|
||||||
# Test enabling FastFrame
|
|
||||||
print("Enabling FastFrame...")
|
|
||||||
scope.set_fastframe_state(1)
|
|
||||||
state = scope.get_fastframe_state()
|
|
||||||
print(f"✓ FastFrame state: {state} ({'active' if state else 'off'})\n")
|
|
||||||
|
|
||||||
# Test setting frame count
|
|
||||||
print("Setting frame count to 100...")
|
|
||||||
scope.set_fastframe_count(100)
|
|
||||||
count = scope.get_fastframe_count()
|
|
||||||
print(f"✓ Frame count: {count}\n")
|
|
||||||
|
|
||||||
# Test using boolean for state
|
|
||||||
print("Testing boolean state (True)...")
|
|
||||||
scope.set_fastframe_state(True)
|
|
||||||
state = scope.get_fastframe_state()
|
|
||||||
print(f"✓ FastFrame state: {state} ({'active' if state else 'off'})\n")
|
|
||||||
|
|
||||||
print("Testing boolean state (False)...")
|
|
||||||
scope.set_fastframe_state(False)
|
|
||||||
state = scope.get_fastframe_state()
|
|
||||||
print(f"✓ FastFrame state: {state} ({'active' if state else 'off'})\n")
|
|
||||||
|
|
||||||
# Test different frame counts
|
|
||||||
print("Testing different frame counts:")
|
|
||||||
test_counts = [10, 50, 200, 500]
|
|
||||||
for test_count in test_counts:
|
|
||||||
print(f" Setting count to {test_count}...")
|
|
||||||
scope.set_fastframe_count(test_count)
|
|
||||||
actual_count = scope.get_fastframe_count()
|
|
||||||
if actual_count == test_count:
|
|
||||||
print(f" ✓ Verified: {actual_count}")
|
|
||||||
else:
|
|
||||||
print(f" ✗ Mismatch: expected {test_count}, got {actual_count}")
|
|
||||||
|
|
||||||
# Restore original settings
|
|
||||||
print(f"\nRestoring original settings...")
|
|
||||||
scope.set_fastframe_state(current_state)
|
|
||||||
scope.set_fastframe_count(current_count)
|
|
||||||
print(f"✓ Restored FastFrame state: {scope.get_fastframe_state()}")
|
|
||||||
print(f"✓ Restored frame count: {scope.get_fastframe_count()}")
|
|
||||||
|
|
||||||
# Test invalid inputs
|
|
||||||
print("\nTesting invalid state (should raise ValueError)...")
|
|
||||||
try:
|
|
||||||
scope.set_fastframe_state(2)
|
|
||||||
print("✗ ERROR: Should have raised ValueError!")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"✓ Correctly raised ValueError: {e}")
|
|
||||||
|
|
||||||
print("\nTesting invalid frame count (should raise ValueError)...")
|
|
||||||
try:
|
|
||||||
scope.set_fastframe_count(-1)
|
|
||||||
print("✗ ERROR: Should have raised ValueError!")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"✓ Correctly raised ValueError: {e}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Error: {type(e).__name__}: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
finally:
|
|
||||||
if scope.is_connected:
|
|
||||||
scope.disconnect()
|
|
||||||
print("\n✓ Disconnected from oscilloscope")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,292 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test script for FastFrame waveform acquisition.
|
|
||||||
|
|
||||||
Tests acquiring 1000 FastFrame records from a 1MHz square wave on CH1.
|
|
||||||
Signal: 1MHz square wave, +250mV to -250mV
|
|
||||||
Records: 1000 frames, 2500 points each
|
|
||||||
"""
|
|
||||||
|
|
||||||
import time
|
|
||||||
from tektronix_base import TektronixOscilloscopeBase
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Test FastFrame acquisition with 1MHz square wave on CH1"""
|
|
||||||
|
|
||||||
scope_ip = "192.168.10.105"
|
|
||||||
|
|
||||||
print(f"Connecting to oscilloscope at {scope_ip}...")
|
|
||||||
|
|
||||||
scope = TektronixOscilloscopeBase(resource_name=scope_ip, timeout=20.0)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Connect to scope
|
|
||||||
scope.connect()
|
|
||||||
print(f"Connected to {scope.identify()}\n")
|
|
||||||
|
|
||||||
print("=== Configuring Oscilloscope ===\n")
|
|
||||||
|
|
||||||
# Configure CH1 for the signal
|
|
||||||
print("Configuring CH1...")
|
|
||||||
print(" Setting vertical scale to 100mV/div...")
|
|
||||||
scope.set_channel_scale(1, 0.1) # 100mV/div for ±250mV signal
|
|
||||||
print(" Setting vertical offset to 0V...")
|
|
||||||
scope.set_channel_offset(1, 0.0)
|
|
||||||
print(" Setting coupling to DC...")
|
|
||||||
scope.set_channel_coupling(1, 'DC')
|
|
||||||
print(" Setting termination to 50 ohms...")
|
|
||||||
scope.set_channel_termination(1, 50)
|
|
||||||
|
|
||||||
# Verify settings
|
|
||||||
actual_scale = scope.get_channel_scale(1)
|
|
||||||
actual_offset = scope.get_channel_offset(1)
|
|
||||||
actual_coupling = scope.get_channel_coupling(1)
|
|
||||||
actual_term = scope.get_channel_termination(1)
|
|
||||||
print(f" Verified: {actual_scale}V/div, {actual_offset}V offset, {actual_coupling} coupling, {actual_term}Ω\n")
|
|
||||||
|
|
||||||
# Configure horizontal timebase
|
|
||||||
# For 1MHz square wave (1µs period), let's capture ~4 cycles (4µs)
|
|
||||||
# 4µs over 10 divisions = 400ns/div
|
|
||||||
print("Configuring horizontal timebase...")
|
|
||||||
print(" Setting time scale to 400ns/div (4µs total for ~4 cycles of 1MHz)...")
|
|
||||||
scope.set_time_scale(400e-9) # 400ns/div
|
|
||||||
print(" Setting record length to 2500 points...")
|
|
||||||
scope.set_record_length(2500)
|
|
||||||
|
|
||||||
# Verify settings
|
|
||||||
actual_time_scale = scope.get_time_scale()
|
|
||||||
actual_record_length = scope.get_record_length()
|
|
||||||
actual_sample_rate = scope.get_sample_rate()
|
|
||||||
print(f" Verified: {actual_time_scale*1e9:.0f}ns/div, {actual_record_length} points")
|
|
||||||
print(f" Sample rate: {actual_sample_rate/1e6:.1f} MS/s\n")
|
|
||||||
|
|
||||||
# Configure trigger
|
|
||||||
print("Configuring trigger...")
|
|
||||||
print(" Setting trigger source to CH1...")
|
|
||||||
scope.set_trigger_source('CH1')
|
|
||||||
print(" Setting trigger level to +100mV...")
|
|
||||||
scope.set_trigger_level(1, 0.1) # +100mV
|
|
||||||
print(" Setting trigger slope to rising...")
|
|
||||||
scope.set_trigger_slope('RISE')
|
|
||||||
print(" Setting trigger mode to NORMAL...")
|
|
||||||
scope.set_trigger_mode('NORMAL')
|
|
||||||
print(" Setting trigger coupling to DC...")
|
|
||||||
scope.set_trigger_coupling('DC')
|
|
||||||
|
|
||||||
# Verify trigger settings
|
|
||||||
actual_trigger_source = scope.get_trigger_source()
|
|
||||||
actual_trigger_level = scope.get_trigger_level(1)
|
|
||||||
actual_trigger_slope = scope.get_trigger_slope()
|
|
||||||
actual_trigger_mode = scope.get_trigger_mode()
|
|
||||||
print(f" Verified: {actual_trigger_source}, {actual_trigger_level}V, {actual_trigger_slope}, {actual_trigger_mode}\n")
|
|
||||||
|
|
||||||
# Configure FastFrame
|
|
||||||
print("Configuring FastFrame...")
|
|
||||||
print(" Enabling FastFrame...")
|
|
||||||
scope.set_fastframe_state(True)
|
|
||||||
print(" Setting frame count to 1000...")
|
|
||||||
scope.set_fastframe_count(1000)
|
|
||||||
|
|
||||||
# Verify FastFrame settings
|
|
||||||
ff_state = scope.get_fastframe_state()
|
|
||||||
ff_count = scope.get_fastframe_count()
|
|
||||||
print(f" Verified: FastFrame {'enabled' if ff_state else 'disabled'}, {ff_count} frames\n")
|
|
||||||
|
|
||||||
# Configure waveform transfer
|
|
||||||
print("Configuring waveform transfer...")
|
|
||||||
scope.set_data_encoding('RIBinary')
|
|
||||||
scope.set_wfmoutpre_encoding('BINary')
|
|
||||||
scope.set_wfmoutpre_byte_count(1)
|
|
||||||
scope.set_wfmoutpre_byte_order('MSB')
|
|
||||||
scope.set_data_source('CH1')
|
|
||||||
|
|
||||||
# Verify configuration
|
|
||||||
actual_encoding = scope.get_data_encoding()
|
|
||||||
actual_source = scope.get_data_source()
|
|
||||||
print(f" Verified: {actual_encoding} encoding, source {actual_source}")
|
|
||||||
print(" Data transfer configured for 8-bit signed binary\n")
|
|
||||||
|
|
||||||
print("=== Acquiring FastFrame Records ===\n")
|
|
||||||
|
|
||||||
# Wait for FastFrame settings to take effect
|
|
||||||
print("Waiting for FastFrame configuration to settle...")
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
# Start acquisition - this arms the scope and begins acquiring triggered frames
|
|
||||||
print("Starting acquisition (arming scope)...")
|
|
||||||
scope.write("ACQuire:STATE RUN")
|
|
||||||
print(" Scope is now armed and acquiring triggered events")
|
|
||||||
|
|
||||||
# Wait and poll for acquisition to complete
|
|
||||||
print(f" Waiting for {ff_count} triggers to be acquired...")
|
|
||||||
print(" (Polling acquisition state...)")
|
|
||||||
|
|
||||||
# Poll for up to 10 seconds
|
|
||||||
for i in range(100):
|
|
||||||
time.sleep(0.1)
|
|
||||||
state = scope.query("ACQuire:STATE?")
|
|
||||||
if i % 10 == 0: # Print every second
|
|
||||||
print(f" Polling... state: {state.strip()}")
|
|
||||||
# Check if we've acquired enough frames
|
|
||||||
if i > 20: # After 2 seconds minimum
|
|
||||||
break
|
|
||||||
|
|
||||||
# Stop acquisition
|
|
||||||
print(" Stopping acquisition...")
|
|
||||||
scope.write("ACQuire:STATE STOP")
|
|
||||||
|
|
||||||
# Verify we have frames
|
|
||||||
time.sleep(0.2)
|
|
||||||
print("✓ Acquisition stopped, scope should now have all 1000 frames\n")
|
|
||||||
|
|
||||||
# Clear any leftover data in the receive buffer
|
|
||||||
print("Clearing receive buffer...")
|
|
||||||
scope.socket.setblocking(False)
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
junk = scope.socket.recv(4096)
|
|
||||||
if not junk:
|
|
||||||
break
|
|
||||||
print(f" Cleared {len(junk)} bytes of junk data")
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
scope.socket.setblocking(True)
|
|
||||||
print(" Buffer cleared\n")
|
|
||||||
|
|
||||||
# Test transferring a single frame first
|
|
||||||
print("Testing single frame transfer first...")
|
|
||||||
try:
|
|
||||||
scope.set_fastframe_selected(1)
|
|
||||||
print(" Selected frame 1")
|
|
||||||
test_curve = scope.transfer_curve()
|
|
||||||
print(f" ✓ Successfully transferred {len(test_curve)} bytes")
|
|
||||||
test_waveform = scope.parse_curve_data(test_curve, byte_count=1, signed=True, byte_order='MSB')
|
|
||||||
print(f" ✓ Parsed {len(test_waveform)} samples")
|
|
||||||
print(f" First 10 values: {test_waveform[:10]}\n")
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ✗ Single frame test failed: {e}")
|
|
||||||
print(" Cannot proceed with bulk transfer\n")
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Now transfer all 1000 frames
|
|
||||||
print(f"Transferring {ff_count} frames from scope...")
|
|
||||||
print("This may take a while...\n")
|
|
||||||
|
|
||||||
start_time = time.time()
|
|
||||||
all_waveforms = []
|
|
||||||
|
|
||||||
for frame_num in range(1, ff_count + 1):
|
|
||||||
# Show progress every 100 frames
|
|
||||||
if frame_num % 100 == 0 or frame_num == 1:
|
|
||||||
elapsed = time.time() - start_time
|
|
||||||
if frame_num > 1:
|
|
||||||
rate = frame_num / elapsed
|
|
||||||
eta = (ff_count - frame_num) / rate
|
|
||||||
print(f" Frame {frame_num}/{ff_count} - Elapsed: {elapsed:.1f}s - Rate: {rate:.1f} frames/s - ETA: {eta:.1f}s")
|
|
||||||
else:
|
|
||||||
print(f" Frame {frame_num}/{ff_count}...")
|
|
||||||
|
|
||||||
# Select this frame
|
|
||||||
scope.set_fastframe_selected(frame_num)
|
|
||||||
|
|
||||||
# Verify frame selection for first frame
|
|
||||||
if frame_num == 1:
|
|
||||||
actual_frame = scope.get_fastframe_selected()
|
|
||||||
if actual_frame != frame_num:
|
|
||||||
print(f" Warning: Frame mismatch - requested {frame_num}, got {actual_frame}")
|
|
||||||
|
|
||||||
# Transfer curve data directly with longer timeout
|
|
||||||
old_timeout = scope.socket.gettimeout()
|
|
||||||
scope.socket.settimeout(30.0)
|
|
||||||
|
|
||||||
try:
|
|
||||||
curve_bytes = scope.transfer_curve()
|
|
||||||
waveform = scope.parse_curve_data(curve_bytes, byte_count=1, signed=True, byte_order='MSB')
|
|
||||||
all_waveforms.append(waveform)
|
|
||||||
finally:
|
|
||||||
scope.socket.settimeout(old_timeout)
|
|
||||||
|
|
||||||
end_time = time.time()
|
|
||||||
total_time = end_time - start_time
|
|
||||||
|
|
||||||
print(f"\n✓ Acquired all {ff_count} frames in {total_time:.2f} seconds")
|
|
||||||
print(f" Average rate: {ff_count / total_time:.2f} frames/second")
|
|
||||||
print(f" Average time per frame: {total_time / ff_count * 1000:.2f} ms\n")
|
|
||||||
|
|
||||||
# Analyze the acquired data
|
|
||||||
print("=== Waveform Statistics ===\n")
|
|
||||||
|
|
||||||
# Check that all frames have the expected length
|
|
||||||
frame_lengths = [len(wf) for wf in all_waveforms]
|
|
||||||
print(f"Frame lengths: {min(frame_lengths)} to {max(frame_lengths)} points")
|
|
||||||
|
|
||||||
# Calculate statistics for first frame
|
|
||||||
first_frame = all_waveforms[0]
|
|
||||||
print(f"\nFirst frame (frame 1):")
|
|
||||||
print(f" Samples: {len(first_frame)}")
|
|
||||||
print(f" Min ADC value: {min(first_frame)}")
|
|
||||||
print(f" Max ADC value: {max(first_frame)}")
|
|
||||||
print(f" Average ADC value: {sum(first_frame) / len(first_frame):.2f}")
|
|
||||||
print(f" First 20 values: {first_frame[:20]}")
|
|
||||||
|
|
||||||
# Calculate statistics for middle frame
|
|
||||||
mid_frame_idx = ff_count // 2
|
|
||||||
mid_frame = all_waveforms[mid_frame_idx]
|
|
||||||
print(f"\nMiddle frame (frame {mid_frame_idx + 1}):")
|
|
||||||
print(f" Samples: {len(mid_frame)}")
|
|
||||||
print(f" Min ADC value: {min(mid_frame)}")
|
|
||||||
print(f" Max ADC value: {max(mid_frame)}")
|
|
||||||
print(f" Average ADC value: {sum(mid_frame) / len(mid_frame):.2f}")
|
|
||||||
|
|
||||||
# Calculate statistics for last frame
|
|
||||||
last_frame = all_waveforms[-1]
|
|
||||||
print(f"\nLast frame (frame {ff_count}):")
|
|
||||||
print(f" Samples: {len(last_frame)}")
|
|
||||||
print(f" Min ADC value: {min(last_frame)}")
|
|
||||||
print(f" Max ADC value: {max(last_frame)}")
|
|
||||||
print(f" Average ADC value: {sum(last_frame) / len(last_frame):.2f}")
|
|
||||||
|
|
||||||
# Calculate overall statistics
|
|
||||||
all_values = [val for wf in all_waveforms for val in wf]
|
|
||||||
print(f"\nOverall statistics (all {ff_count} frames, {len(all_values)} total samples):")
|
|
||||||
print(f" Min ADC value: {min(all_values)}")
|
|
||||||
print(f" Max ADC value: {max(all_values)}")
|
|
||||||
print(f" Average ADC value: {sum(all_values) / len(all_values):.2f}")
|
|
||||||
print(f" Total data transferred: {len(all_values)} bytes")
|
|
||||||
print(f" Transfer rate: {len(all_values) / total_time / 1024 / 1024:.2f} MB/s")
|
|
||||||
|
|
||||||
# Check for square wave characteristics
|
|
||||||
print(f"\nSquare wave detection:")
|
|
||||||
# A square wave should have values clustered around two levels
|
|
||||||
positive_samples = sum(1 for v in first_frame if v > 0)
|
|
||||||
negative_samples = sum(1 for v in first_frame if v < 0)
|
|
||||||
zero_samples = sum(1 for v in first_frame if v == 0)
|
|
||||||
print(f" Frame 1: {positive_samples} positive, {negative_samples} negative, {zero_samples} zero samples")
|
|
||||||
|
|
||||||
# Estimate duty cycle from first frame
|
|
||||||
if len(first_frame) > 0:
|
|
||||||
duty_cycle = (positive_samples / len(first_frame)) * 100
|
|
||||||
print(f" Estimated duty cycle: {duty_cycle:.1f}%")
|
|
||||||
|
|
||||||
print("\n=== FastFrame Acquisition Test Completed Successfully! ===")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"\nError: {type(e).__name__}: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
finally:
|
|
||||||
if scope.is_connected:
|
|
||||||
# Try to disable FastFrame before disconnecting
|
|
||||||
try:
|
|
||||||
print("\nDisabling FastFrame...")
|
|
||||||
scope.set_fastframe_state(False)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
scope.disconnect()
|
|
||||||
print("Disconnected from oscilloscope")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test script to verify FastFrame packed transfer hypothesis.
|
|
||||||
|
|
||||||
Tests if all FastFrame records come in a single CURVe? transfer,
|
|
||||||
similar to WFMv3 file format.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import time
|
|
||||||
from tektronix_base import TektronixOscilloscopeBase
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Test FastFrame packed transfer"""
|
|
||||||
|
|
||||||
scope_ip = "192.168.10.105"
|
|
||||||
num_frames = 1000
|
|
||||||
target_record_length = 5000
|
|
||||||
|
|
||||||
print(f"Connecting to oscilloscope at {scope_ip}...")
|
|
||||||
|
|
||||||
scope = TektronixOscilloscopeBase(resource_name=scope_ip, timeout=20.0)
|
|
||||||
|
|
||||||
try:
|
|
||||||
scope.connect()
|
|
||||||
print(f"Connected to {scope.identify()}\n")
|
|
||||||
|
|
||||||
# Configure CH1 for 1MHz square wave ±250mV
|
|
||||||
print("Configuring CH1...")
|
|
||||||
scope.set_channel_scale(1, 0.1) # 100mV/div
|
|
||||||
scope.set_channel_offset(1, 0.0)
|
|
||||||
scope.set_channel_coupling(1, 'DC')
|
|
||||||
scope.set_channel_termination(1, 50)
|
|
||||||
|
|
||||||
# Configure horizontal for 5000 point record length
|
|
||||||
# At 6.25 GS/s: 5000 points / 6.25e9 = 800ns window = 80ns/div
|
|
||||||
print("Configuring horizontal for 5000 point record length...")
|
|
||||||
scope.set_record_length(target_record_length)
|
|
||||||
scope.set_time_scale(80e-9) # 80ns/div = 800ns window at 6.25 GS/s
|
|
||||||
|
|
||||||
# Check actual settings
|
|
||||||
actual_record_length = scope.get_record_length()
|
|
||||||
actual_time_scale = scope.get_time_scale()
|
|
||||||
actual_sample_rate = scope.get_sample_rate()
|
|
||||||
print(f" Record length: {actual_record_length}")
|
|
||||||
print(f" Time scale: {actual_time_scale*1e9:.0f} ns/div")
|
|
||||||
print(f" Sample rate: {actual_sample_rate/1e6:.1f} MS/s")
|
|
||||||
|
|
||||||
# Configure trigger
|
|
||||||
print("Configuring trigger...")
|
|
||||||
scope.set_trigger_source('CH1')
|
|
||||||
scope.set_trigger_level(1, 0.1)
|
|
||||||
scope.set_trigger_slope('RISE')
|
|
||||||
scope.set_trigger_mode('NORMAL')
|
|
||||||
|
|
||||||
# Configure FastFrame
|
|
||||||
print(f"Configuring FastFrame for {num_frames} frames...")
|
|
||||||
scope.set_fastframe_state(True)
|
|
||||||
scope.set_fastframe_count(num_frames)
|
|
||||||
|
|
||||||
# Check record length AFTER enabling FastFrame
|
|
||||||
actual_record_length_ff = scope.get_record_length()
|
|
||||||
actual_sample_rate_ff = scope.get_sample_rate()
|
|
||||||
print(f" Record length after FastFrame: {actual_record_length_ff}")
|
|
||||||
print(f" Sample rate after FastFrame: {actual_sample_rate_ff/1e6:.1f} MS/s")
|
|
||||||
|
|
||||||
ff_state = scope.get_fastframe_state()
|
|
||||||
ff_count = scope.get_fastframe_count()
|
|
||||||
print(f" FastFrame: {'enabled' if ff_state else 'disabled'}, {ff_count} frames\n")
|
|
||||||
|
|
||||||
# Configure waveform transfer
|
|
||||||
print("Configuring waveform transfer...")
|
|
||||||
scope.set_data_encoding('RIBinary')
|
|
||||||
scope.set_wfmoutpre_encoding('BINary')
|
|
||||||
scope.set_wfmoutpre_byte_count(1)
|
|
||||||
scope.set_wfmoutpre_byte_order('MSB')
|
|
||||||
scope.set_data_source('CH1')
|
|
||||||
|
|
||||||
# Acquire data
|
|
||||||
print("\nAcquiring FastFrame data...")
|
|
||||||
time.sleep(0.5)
|
|
||||||
scope.write("ACQuire:STATE RUN")
|
|
||||||
|
|
||||||
# Wait for triggers (no polling - just wait)
|
|
||||||
print(" Waiting for triggers...")
|
|
||||||
time.sleep(3)
|
|
||||||
|
|
||||||
scope.write("ACQuire:STATE STOP")
|
|
||||||
time.sleep(0.5)
|
|
||||||
print(" Acquisition stopped\n")
|
|
||||||
|
|
||||||
# Transfer all frames individually
|
|
||||||
print(f"\n=== Transferring {num_frames} Frames Individually ===\n")
|
|
||||||
|
|
||||||
all_waveforms = []
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
for frame_num in range(1, num_frames + 1):
|
|
||||||
# Progress every 100 frames
|
|
||||||
if frame_num == 1 or frame_num % 100 == 0:
|
|
||||||
elapsed = time.time() - start_time
|
|
||||||
if frame_num > 1:
|
|
||||||
rate = frame_num / elapsed
|
|
||||||
eta = (num_frames - frame_num) / rate
|
|
||||||
print(f" Frame {frame_num}/{num_frames} - {elapsed:.1f}s elapsed - {rate:.1f} fps - ETA {eta:.1f}s")
|
|
||||||
else:
|
|
||||||
print(f" Frame {frame_num}/{num_frames}...")
|
|
||||||
|
|
||||||
# Select frame and wait for it to take effect
|
|
||||||
scope.set_fastframe_selected(frame_num)
|
|
||||||
time.sleep(0.01) # Small delay for command processing
|
|
||||||
|
|
||||||
# Transfer curve data
|
|
||||||
curve_bytes = scope.transfer_curve()
|
|
||||||
waveform = scope.parse_curve_data(curve_bytes, byte_count=1, signed=True, byte_order='MSB')
|
|
||||||
all_waveforms.append(waveform)
|
|
||||||
|
|
||||||
end_time = time.time()
|
|
||||||
total_time = end_time - start_time
|
|
||||||
|
|
||||||
print(f"\n=== Transfer Complete ===")
|
|
||||||
print(f"Frames transferred: {len(all_waveforms)}")
|
|
||||||
print(f"Points per frame: {len(all_waveforms[0])}")
|
|
||||||
print(f"Total time: {total_time:.2f}s")
|
|
||||||
print(f"Rate: {num_frames / total_time:.1f} frames/s")
|
|
||||||
print(f"Total data: {sum(len(wf) for wf in all_waveforms)} samples")
|
|
||||||
print(f"Throughput: {sum(len(wf) for wf in all_waveforms) / total_time / 1e6:.2f} MS/s")
|
|
||||||
|
|
||||||
# Show statistics for selected frames
|
|
||||||
print(f"\n=== Frame Statistics ===")
|
|
||||||
for frame_idx in [0, num_frames//2, num_frames-1]:
|
|
||||||
wf = all_waveforms[frame_idx]
|
|
||||||
print(f"\nFrame {frame_idx + 1}:")
|
|
||||||
print(f" Samples: {len(wf)}")
|
|
||||||
print(f" Min: {min(wf)}, Max: {max(wf)}, Avg: {sum(wf)/len(wf):.1f}")
|
|
||||||
print(f" First 10: {wf[:10]}")
|
|
||||||
|
|
||||||
print("\n=== Test Complete ===")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"\nError: {type(e).__name__}: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
finally:
|
|
||||||
if scope.is_connected:
|
|
||||||
try:
|
|
||||||
scope.set_fastframe_state(False)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
scope.disconnect()
|
|
||||||
print("\nDisconnected")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Simple FastFrame test - minimal setup.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import time
|
|
||||||
from tektronix_base import TektronixOscilloscopeBase
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
scope_ip = "192.168.10.105"
|
|
||||||
|
|
||||||
print(f"Connecting to {scope_ip}...")
|
|
||||||
scope = TektronixOscilloscopeBase(resource_name=scope_ip, timeout=20.0)
|
|
||||||
|
|
||||||
try:
|
|
||||||
scope.connect()
|
|
||||||
print(f"Connected: {scope.identify()}\n")
|
|
||||||
|
|
||||||
# Configure CH1 (same as failing test)
|
|
||||||
print("Configuring CH1...")
|
|
||||||
scope.set_channel_scale(1, 0.1)
|
|
||||||
scope.set_channel_offset(1, 0.0)
|
|
||||||
scope.set_channel_coupling(1, 'DC')
|
|
||||||
scope.set_channel_termination(1, 50)
|
|
||||||
|
|
||||||
# Configure horizontal (same as failing test)
|
|
||||||
print("Configuring horizontal...")
|
|
||||||
scope.set_record_length(5000)
|
|
||||||
scope.set_time_scale(80e-9)
|
|
||||||
actual_record_length = scope.get_record_length()
|
|
||||||
actual_time_scale = scope.get_time_scale()
|
|
||||||
actual_sample_rate = scope.get_sample_rate()
|
|
||||||
print(f" Record: {actual_record_length}, Time: {actual_time_scale*1e9:.0f}ns/div, Rate: {actual_sample_rate/1e6:.1f}MS/s")
|
|
||||||
|
|
||||||
# Configure trigger (same as failing test)
|
|
||||||
print("Configuring trigger...")
|
|
||||||
scope.set_trigger_source('CH1')
|
|
||||||
scope.set_trigger_level(1, 0.1)
|
|
||||||
scope.set_trigger_slope('RISE')
|
|
||||||
scope.set_trigger_mode('NORMAL')
|
|
||||||
scope.set_trigger_coupling('DC')
|
|
||||||
|
|
||||||
# Enable FastFrame with 1000 frames
|
|
||||||
print("Enabling FastFrame (1000 frames)...")
|
|
||||||
scope.set_fastframe_state(True)
|
|
||||||
scope.set_fastframe_count(1000)
|
|
||||||
|
|
||||||
ff_state = scope.get_fastframe_state()
|
|
||||||
ff_count = scope.get_fastframe_count()
|
|
||||||
print(f" State: {ff_state}, Count: {ff_count}")
|
|
||||||
|
|
||||||
# Configure data transfer (same as failing test)
|
|
||||||
print("Configuring data transfer...")
|
|
||||||
scope.set_data_encoding('RIBinary')
|
|
||||||
scope.set_wfmoutpre_encoding('BINary')
|
|
||||||
scope.set_wfmoutpre_byte_count(1)
|
|
||||||
scope.set_wfmoutpre_byte_order('MSB')
|
|
||||||
scope.set_data_source('CH1')
|
|
||||||
|
|
||||||
# Acquire
|
|
||||||
print("Acquiring...")
|
|
||||||
scope.write("ACQuire:STATE RUN")
|
|
||||||
time.sleep(2) # Wait for triggers
|
|
||||||
scope.write("ACQuire:STATE STOP")
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
# Check record length
|
|
||||||
record_len = scope.get_record_length()
|
|
||||||
print(f"Record length: {record_len}")
|
|
||||||
|
|
||||||
# Test: Send single CURVe? and read ALL frames
|
|
||||||
print(f"\nTesting: Send one CURVe? and read all {ff_count} frames...")
|
|
||||||
scope.set_fastframe_selected(1) # Start from frame 1
|
|
||||||
scope.write("CURVe?")
|
|
||||||
|
|
||||||
all_waveforms = []
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
for frame_num in range(1, ff_count + 1):
|
|
||||||
if frame_num == 1 or frame_num % 100 == 0:
|
|
||||||
elapsed = time.time() - start_time
|
|
||||||
if frame_num > 1:
|
|
||||||
rate = frame_num / elapsed
|
|
||||||
print(f" Frame {frame_num}/{ff_count} - {rate:.1f} fps")
|
|
||||||
else:
|
|
||||||
print(f" Frame {frame_num}/{ff_count}...")
|
|
||||||
|
|
||||||
# Read one frame using read_raw (no new CURVe? command)
|
|
||||||
curve_bytes = scope.read_raw()
|
|
||||||
waveform = scope.parse_curve_data(curve_bytes, byte_count=1, signed=True)
|
|
||||||
all_waveforms.append(waveform)
|
|
||||||
|
|
||||||
end_time = time.time()
|
|
||||||
total_time = end_time - start_time
|
|
||||||
|
|
||||||
print(f"\nTransferred {len(all_waveforms)} frames in {total_time:.2f}s")
|
|
||||||
print(f"Rate: {ff_count / total_time:.1f} frames/s")
|
|
||||||
print(f"Points per frame: {len(all_waveforms[0])}")
|
|
||||||
print(f"First frame: min={min(all_waveforms[0])}, max={max(all_waveforms[0])}")
|
|
||||||
print(f"Last frame: min={min(all_waveforms[-1])}, max={max(all_waveforms[-1])}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"\nError: {type(e).__name__}: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
finally:
|
|
||||||
if scope.is_connected:
|
|
||||||
try:
|
|
||||||
scope.set_fastframe_state(False)
|
|
||||||
print("FastFrame disabled")
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
scope.disconnect()
|
|
||||||
print("Disconnected")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test script for horizontal mode control functionality.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from tektronix_base import TektronixOscilloscopeBase
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Test horizontal mode functions on oscilloscope at 192.168.10.105"""
|
|
||||||
|
|
||||||
scope_ip = "192.168.10.105"
|
|
||||||
|
|
||||||
print(f"Connecting to oscilloscope at {scope_ip}...")
|
|
||||||
|
|
||||||
scope = TektronixOscilloscopeBase(resource_name=scope_ip)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Connect to scope
|
|
||||||
scope.connect()
|
|
||||||
print(f"✓ Connected to {scope.identify()}\n")
|
|
||||||
|
|
||||||
# Get current horizontal settings
|
|
||||||
print("Getting current horizontal settings...")
|
|
||||||
current_record_length = scope.get_record_length()
|
|
||||||
current_sample_rate = scope.get_sample_rate()
|
|
||||||
current_time_scale = scope.get_time_scale()
|
|
||||||
print(f"✓ Current record length: {current_record_length} samples")
|
|
||||||
print(f"✓ Current sample rate: {current_sample_rate} S/s")
|
|
||||||
print(f"✓ Current time scale: {current_time_scale} s/div\n")
|
|
||||||
|
|
||||||
# Test setting record length
|
|
||||||
print("Testing record length changes:")
|
|
||||||
test_lengths = [1000, 10000, 100000]
|
|
||||||
for length in test_lengths:
|
|
||||||
print(f" Setting record length to {length}...")
|
|
||||||
scope.set_record_length(length)
|
|
||||||
actual_length = scope.get_record_length()
|
|
||||||
print(f" ✓ Actual record length: {actual_length}")
|
|
||||||
|
|
||||||
# Test setting sample rate
|
|
||||||
print("\nTesting sample rate changes:")
|
|
||||||
test_rates = [1e6, 10e6, 100e6] # 1 MS/s, 10 MS/s, 100 MS/s
|
|
||||||
for rate in test_rates:
|
|
||||||
print(f" Setting sample rate to {rate:.0f} S/s...")
|
|
||||||
scope.set_sample_rate(rate)
|
|
||||||
actual_rate = scope.get_sample_rate()
|
|
||||||
print(f" ✓ Actual sample rate: {actual_rate:.0f} S/s")
|
|
||||||
|
|
||||||
# Test setting time scale
|
|
||||||
print("\nTesting time scale changes:")
|
|
||||||
test_scales = [1e-6, 10e-6, 100e-6, 1e-3] # 1 µs/div, 10 µs/div, 100 µs/div, 1 ms/div
|
|
||||||
for scale in test_scales:
|
|
||||||
print(f" Setting time scale to {scale:.6f} s/div...")
|
|
||||||
scope.set_time_scale(scale)
|
|
||||||
actual_scale = scope.get_time_scale()
|
|
||||||
print(f" ✓ Actual time scale: {actual_scale:.6f} s/div")
|
|
||||||
|
|
||||||
# Restore original settings
|
|
||||||
print(f"\nRestoring original settings...")
|
|
||||||
scope.set_record_length(current_record_length)
|
|
||||||
scope.set_sample_rate(current_sample_rate)
|
|
||||||
scope.set_time_scale(current_time_scale)
|
|
||||||
print(f"✓ Restored record length: {scope.get_record_length()}")
|
|
||||||
print(f"✓ Restored sample rate: {scope.get_sample_rate()}")
|
|
||||||
print(f"✓ Restored time scale: {scope.get_time_scale()}")
|
|
||||||
|
|
||||||
# Test invalid inputs
|
|
||||||
print("\nTesting invalid record length (should raise ValueError)...")
|
|
||||||
try:
|
|
||||||
scope.set_record_length(-1)
|
|
||||||
print("✗ ERROR: Should have raised ValueError!")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"✓ Correctly raised ValueError: {e}")
|
|
||||||
|
|
||||||
print("\nTesting invalid sample rate (should raise ValueError)...")
|
|
||||||
try:
|
|
||||||
scope.set_sample_rate(0)
|
|
||||||
print("✗ ERROR: Should have raised ValueError!")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"✓ Correctly raised ValueError: {e}")
|
|
||||||
|
|
||||||
print("\nTesting invalid time scale (should raise ValueError)...")
|
|
||||||
try:
|
|
||||||
scope.set_time_scale(-1.0)
|
|
||||||
print("✗ ERROR: Should have raised ValueError!")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"✓ Correctly raised ValueError: {e}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Error: {type(e).__name__}: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
finally:
|
|
||||||
if scope.is_connected:
|
|
||||||
scope.disconnect()
|
|
||||||
print("\n✓ Disconnected from oscilloscope")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Simple curve transfer test - no FastFrame.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import time
|
|
||||||
from tektronix_base import TektronixOscilloscopeBase
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
scope_ip = "192.168.10.105"
|
|
||||||
|
|
||||||
print(f"Connecting to {scope_ip}...")
|
|
||||||
scope = TektronixOscilloscopeBase(resource_name=scope_ip, timeout=20.0)
|
|
||||||
|
|
||||||
try:
|
|
||||||
scope.connect()
|
|
||||||
print(f"Connected: {scope.identify()}\n")
|
|
||||||
|
|
||||||
# Make sure FastFrame is OFF
|
|
||||||
print("Disabling FastFrame...")
|
|
||||||
scope.set_fastframe_state(False)
|
|
||||||
|
|
||||||
# Configure simple acquisition
|
|
||||||
print("Configuring acquisition...")
|
|
||||||
scope.set_data_encoding('RIBinary')
|
|
||||||
scope.set_data_source('CH1')
|
|
||||||
|
|
||||||
# Single acquisition
|
|
||||||
print("Running single acquisition...")
|
|
||||||
scope.write("ACQuire:STATE RUN")
|
|
||||||
time.sleep(0.5)
|
|
||||||
scope.write("ACQuire:STATE STOP")
|
|
||||||
time.sleep(0.2)
|
|
||||||
|
|
||||||
# Transfer curve
|
|
||||||
print("Transferring curve data...")
|
|
||||||
curve_bytes = scope.transfer_curve()
|
|
||||||
print(f"Received {len(curve_bytes)} bytes")
|
|
||||||
|
|
||||||
# Parse
|
|
||||||
waveform = scope.parse_curve_data(curve_bytes, byte_count=1, signed=True)
|
|
||||||
print(f"Parsed {len(waveform)} samples")
|
|
||||||
print(f"Min: {min(waveform)}, Max: {max(waveform)}")
|
|
||||||
print(f"First 10: {waveform[:10]}")
|
|
||||||
|
|
||||||
print("\nSuccess!")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"\nError: {type(e).__name__}: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
finally:
|
|
||||||
if scope.is_connected:
|
|
||||||
scope.disconnect()
|
|
||||||
print("Disconnected")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Simple test to debug query timeout issues.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from tektronix_base import TektronixOscilloscopeBase
|
|
||||||
import time
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Test simple queries with the oscilloscope"""
|
|
||||||
|
|
||||||
scope_ip = "192.168.10.105"
|
|
||||||
|
|
||||||
print(f"Connecting to oscilloscope at {scope_ip}...")
|
|
||||||
|
|
||||||
scope = TektronixOscilloscopeBase(resource_name=scope_ip, timeout=10.0)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Connect
|
|
||||||
scope.connect()
|
|
||||||
print("✓ Connected")
|
|
||||||
|
|
||||||
# Wait a moment to ensure connection is stable
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
# Try a simple query
|
|
||||||
print("\nSending *IDN? query...")
|
|
||||||
idn = scope.query("*IDN?")
|
|
||||||
print(f"✓ Response: {idn}")
|
|
||||||
|
|
||||||
# Try acquisition mode query
|
|
||||||
print("\nSending ACQuire:MODe? query...")
|
|
||||||
mode = scope.query("ACQuire:MODe?")
|
|
||||||
print(f"✓ Current mode: {mode}")
|
|
||||||
|
|
||||||
# Set mode to sample using short form
|
|
||||||
print("\nSetting mode to SAM (short form)...")
|
|
||||||
scope.set_acquire_mode("SAM")
|
|
||||||
|
|
||||||
# Verify
|
|
||||||
mode = scope.get_acquire_mode()
|
|
||||||
print(f"✓ Mode is now: {mode}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Error: {type(e).__name__}: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
finally:
|
|
||||||
if scope.is_connected:
|
|
||||||
scope.disconnect()
|
|
||||||
print("\n✓ Disconnected")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Simple test to verify basic waveform acquisition works.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from tektronix_base import TektronixOscilloscopeBase
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Test single waveform acquisition from CH1"""
|
|
||||||
|
|
||||||
scope_ip = "192.168.10.105"
|
|
||||||
print(f"Connecting to {scope_ip}...")
|
|
||||||
|
|
||||||
scope = TektronixOscilloscopeBase(resource_name=scope_ip, timeout=15.0)
|
|
||||||
|
|
||||||
try:
|
|
||||||
scope.connect()
|
|
||||||
print(f"✓ Connected to {scope.identify()}\n")
|
|
||||||
|
|
||||||
# Check current settings
|
|
||||||
print("Current settings:")
|
|
||||||
print(f" Data encoding: {scope.get_data_encoding()}")
|
|
||||||
print(f" Data source: {scope.get_data_source()}")
|
|
||||||
print(f" Record length: {scope.get_record_length()}")
|
|
||||||
print(f" FastFrame state: {scope.get_fastframe_state()}\n")
|
|
||||||
|
|
||||||
# Ensure FastFrame is off
|
|
||||||
if scope.get_fastframe_state():
|
|
||||||
print("Disabling FastFrame...")
|
|
||||||
scope.set_fastframe_state(False)
|
|
||||||
|
|
||||||
# Set data source to CH1
|
|
||||||
print("Setting data source to CH1...")
|
|
||||||
scope.set_data_source('CH1')
|
|
||||||
|
|
||||||
# Try to acquire a single waveform
|
|
||||||
print("\nAttempting to acquire waveform from CH1...")
|
|
||||||
print("(This will timeout if the scope isn't responding properly)\n")
|
|
||||||
|
|
||||||
waveform = scope.acquire_waveform('CH1')
|
|
||||||
|
|
||||||
print(f"✓ SUCCESS! Acquired {len(waveform)} samples")
|
|
||||||
print(f" First 10 values: {waveform[:10]}")
|
|
||||||
print(f" Min: {min(waveform)}, Max: {max(waveform)}")
|
|
||||||
print(f" Average: {sum(waveform)/len(waveform):.2f}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"\n✗ Error: {type(e).__name__}: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
finally:
|
|
||||||
if scope.is_connected:
|
|
||||||
scope.disconnect()
|
|
||||||
print("\n✓ Disconnected")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test script for transfer_fastframe() method.
|
|
||||||
|
|
||||||
Tests bulk FastFrame transfer with 1MHz square wave on CH1.
|
|
||||||
Signal: 1MHz square wave, +125mV to -125mV
|
|
||||||
"""
|
|
||||||
|
|
||||||
import time
|
|
||||||
from tektronix_base import TektronixOscilloscopeBase
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
scope_ip = "192.168.10.105"
|
|
||||||
num_frames = 1000
|
|
||||||
|
|
||||||
print(f"Connecting to {scope_ip}...")
|
|
||||||
scope = TektronixOscilloscopeBase(resource_name=scope_ip, timeout=30.0)
|
|
||||||
|
|
||||||
try:
|
|
||||||
scope.connect()
|
|
||||||
print(f"Connected: {scope.identify()}\n")
|
|
||||||
|
|
||||||
# Configure CH1
|
|
||||||
print("Configuring CH1...")
|
|
||||||
scope.set_channel_scale(1, 0.1) # 100mV/div
|
|
||||||
scope.set_channel_offset(1, 0.0)
|
|
||||||
scope.set_channel_coupling(1, 'DC')
|
|
||||||
scope.set_channel_termination(1, 50)
|
|
||||||
|
|
||||||
# Configure horizontal
|
|
||||||
print("Configuring horizontal...")
|
|
||||||
scope.set_record_length(5000)
|
|
||||||
scope.set_time_scale(80e-9) # 80ns/div
|
|
||||||
|
|
||||||
# Configure trigger
|
|
||||||
print("Configuring trigger...")
|
|
||||||
scope.set_trigger_source('CH1')
|
|
||||||
scope.set_trigger_level(1, 0.05) # 50mV
|
|
||||||
scope.set_trigger_slope('RISE')
|
|
||||||
scope.set_trigger_mode('NORMAL')
|
|
||||||
|
|
||||||
# Configure FastFrame
|
|
||||||
print(f"Configuring FastFrame ({num_frames} frames)...")
|
|
||||||
scope.set_fastframe_state(True)
|
|
||||||
scope.set_fastframe_count(num_frames)
|
|
||||||
|
|
||||||
# Verify settings
|
|
||||||
ff_count = scope.get_fastframe_count()
|
|
||||||
record_len = scope.get_record_length()
|
|
||||||
print(f" Frames: {ff_count}, Record length: {record_len}")
|
|
||||||
|
|
||||||
# Configure data transfer
|
|
||||||
print("Configuring data transfer...")
|
|
||||||
scope.set_data_encoding('RIBinary')
|
|
||||||
scope.set_data_source('CH1')
|
|
||||||
|
|
||||||
# Acquire
|
|
||||||
print("\nAcquiring FastFrame data...")
|
|
||||||
scope.write("ACQuire:STATE RUN")
|
|
||||||
time.sleep(2) # Wait for triggers
|
|
||||||
scope.write("ACQuire:STATE STOP")
|
|
||||||
time.sleep(0.5)
|
|
||||||
print(" Acquisition complete")
|
|
||||||
|
|
||||||
# Transfer all frames using the new method
|
|
||||||
print(f"\nTransferring {ff_count} frames using transfer_fastframe()...")
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
waveforms = scope.transfer_fastframe(parse=True, byte_count=1, signed=True)
|
|
||||||
|
|
||||||
end_time = time.time()
|
|
||||||
total_time = end_time - start_time
|
|
||||||
|
|
||||||
# Results
|
|
||||||
print(f"\n=== Transfer Complete ===")
|
|
||||||
print(f"Frames received: {len(waveforms)}")
|
|
||||||
print(f"Points per frame: {len(waveforms[0])}")
|
|
||||||
print(f"Total samples: {sum(len(wf) for wf in waveforms)}")
|
|
||||||
print(f"Transfer time: {total_time:.3f}s")
|
|
||||||
print(f"Frame rate: {len(waveforms) / total_time:.1f} frames/s")
|
|
||||||
print(f"Sample rate: {sum(len(wf) for wf in waveforms) / total_time / 1e6:.2f} MS/s")
|
|
||||||
|
|
||||||
# Waveform statistics
|
|
||||||
print(f"\n=== Waveform Statistics ===")
|
|
||||||
print(f"Frame 1: min={min(waveforms[0])}, max={max(waveforms[0])}, avg={sum(waveforms[0])/len(waveforms[0]):.1f}")
|
|
||||||
print(f"Frame {len(waveforms)//2}: min={min(waveforms[len(waveforms)//2])}, max={max(waveforms[len(waveforms)//2])}")
|
|
||||||
print(f"Frame {len(waveforms)}: min={min(waveforms[-1])}, max={max(waveforms[-1])}")
|
|
||||||
|
|
||||||
# Check for square wave
|
|
||||||
first_frame = waveforms[0]
|
|
||||||
positive = sum(1 for v in first_frame if v > 0)
|
|
||||||
negative = sum(1 for v in first_frame if v < 0)
|
|
||||||
print(f"\nSquare wave check (frame 1): {positive} positive, {negative} negative samples")
|
|
||||||
print(f"Duty cycle estimate: {positive / len(first_frame) * 100:.1f}%")
|
|
||||||
|
|
||||||
print("\n=== Success! ===")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"\nError: {type(e).__name__}: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
finally:
|
|
||||||
if scope.is_connected:
|
|
||||||
try:
|
|
||||||
scope.set_fastframe_state(False)
|
|
||||||
print("\nFastFrame disabled")
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
scope.disconnect()
|
|
||||||
print("Disconnected")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test script for trigger control functionality.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from tektronix_base import TektronixOscilloscopeBase
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Test trigger functions on oscilloscope at 192.168.10.105"""
|
|
||||||
|
|
||||||
scope_ip = "192.168.10.105"
|
|
||||||
|
|
||||||
print(f"Connecting to oscilloscope at {scope_ip}...")
|
|
||||||
|
|
||||||
scope = TektronixOscilloscopeBase(resource_name=scope_ip)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Connect to scope
|
|
||||||
scope.connect()
|
|
||||||
print(f"✓ Connected to {scope.identify()}\n")
|
|
||||||
|
|
||||||
# Get current trigger settings
|
|
||||||
print("Getting current trigger settings...")
|
|
||||||
current_coupling = scope.get_trigger_coupling()
|
|
||||||
current_slope = scope.get_trigger_slope()
|
|
||||||
current_source = scope.get_trigger_source()
|
|
||||||
current_mode = scope.get_trigger_mode()
|
|
||||||
print(f"✓ Current trigger coupling: {current_coupling}")
|
|
||||||
print(f"✓ Current trigger slope: {current_slope}")
|
|
||||||
print(f"✓ Current trigger source: {current_source}")
|
|
||||||
print(f"✓ Current trigger mode: {current_mode}")
|
|
||||||
|
|
||||||
# Get trigger level for current source
|
|
||||||
# Extract channel number from source
|
|
||||||
if current_source.startswith('CH'):
|
|
||||||
channel = int(current_source[2])
|
|
||||||
current_level = scope.get_trigger_level(channel)
|
|
||||||
print(f"✓ Current trigger level for {current_source}: {current_level} V\n")
|
|
||||||
else:
|
|
||||||
current_level = 0.0
|
|
||||||
channel = 1
|
|
||||||
print(f" (Source is {current_source}, not a channel)\n")
|
|
||||||
|
|
||||||
# Test trigger coupling
|
|
||||||
print("Testing trigger coupling modes:")
|
|
||||||
test_couplings = ['DC', 'HFRej', 'LFRej', 'NOISErej']
|
|
||||||
for coupling in test_couplings:
|
|
||||||
print(f" Setting coupling to {coupling}...")
|
|
||||||
scope.set_trigger_coupling(coupling)
|
|
||||||
actual_coupling = scope.get_trigger_coupling()
|
|
||||||
print(f" ✓ Actual coupling: {actual_coupling}")
|
|
||||||
|
|
||||||
# Test trigger slope
|
|
||||||
print("\nTesting trigger slope modes:")
|
|
||||||
test_slopes = ['RISe', 'FALL', 'EITher']
|
|
||||||
for slope in test_slopes:
|
|
||||||
print(f" Setting slope to {slope}...")
|
|
||||||
scope.set_trigger_slope(slope)
|
|
||||||
actual_slope = scope.get_trigger_slope()
|
|
||||||
print(f" ✓ Actual slope: {actual_slope}")
|
|
||||||
|
|
||||||
# Test trigger source
|
|
||||||
print("\nTesting trigger source selection:")
|
|
||||||
test_sources = ['CH1', 'CH2', 'CH3', 'CH4']
|
|
||||||
for source in test_sources:
|
|
||||||
print(f" Setting source to {source}...")
|
|
||||||
scope.set_trigger_source(source)
|
|
||||||
actual_source = scope.get_trigger_source()
|
|
||||||
print(f" ✓ Actual source: {actual_source}")
|
|
||||||
|
|
||||||
# Test trigger source with integer
|
|
||||||
print("\nTesting trigger source with integer (2)...")
|
|
||||||
scope.set_trigger_source(2)
|
|
||||||
actual_source = scope.get_trigger_source()
|
|
||||||
print(f"✓ Actual source: {actual_source}")
|
|
||||||
|
|
||||||
# Test trigger level
|
|
||||||
print("\nTesting trigger level settings:")
|
|
||||||
test_levels = [0.0, 0.5, 1.0, -0.5, 2.5]
|
|
||||||
for level in test_levels:
|
|
||||||
print(f" Setting CH1 trigger level to {level} V...")
|
|
||||||
scope.set_trigger_level(1, level)
|
|
||||||
actual_level = scope.get_trigger_level(1)
|
|
||||||
print(f" ✓ Actual level: {actual_level} V")
|
|
||||||
|
|
||||||
# Test trigger level with channel string
|
|
||||||
print("\nTesting trigger level with channel string ('CH2')...")
|
|
||||||
scope.set_trigger_level('CH2', 1.5)
|
|
||||||
actual_level = scope.get_trigger_level('CH2')
|
|
||||||
print(f"✓ Actual level: {actual_level} V")
|
|
||||||
|
|
||||||
# Test trigger mode
|
|
||||||
print("\nTesting trigger modes:")
|
|
||||||
test_modes = ['AUTO', 'NORMal']
|
|
||||||
for mode in test_modes:
|
|
||||||
print(f" Setting trigger mode to {mode}...")
|
|
||||||
scope.set_trigger_mode(mode)
|
|
||||||
actual_mode = scope.get_trigger_mode()
|
|
||||||
print(f" ✓ Actual mode: {actual_mode}")
|
|
||||||
|
|
||||||
# Restore original settings
|
|
||||||
print(f"\nRestoring original trigger settings...")
|
|
||||||
scope.set_trigger_coupling(current_coupling)
|
|
||||||
scope.set_trigger_slope(current_slope)
|
|
||||||
scope.set_trigger_source(current_source)
|
|
||||||
scope.set_trigger_mode(current_mode)
|
|
||||||
if current_source.startswith('CH'):
|
|
||||||
scope.set_trigger_level(channel, current_level)
|
|
||||||
print(f"✓ Restored trigger coupling: {scope.get_trigger_coupling()}")
|
|
||||||
print(f"✓ Restored trigger slope: {scope.get_trigger_slope()}")
|
|
||||||
print(f"✓ Restored trigger source: {scope.get_trigger_source()}")
|
|
||||||
print(f"✓ Restored trigger mode: {scope.get_trigger_mode()}")
|
|
||||||
|
|
||||||
# Test invalid inputs
|
|
||||||
print("\nTesting invalid coupling (should raise ValueError)...")
|
|
||||||
try:
|
|
||||||
scope.set_trigger_coupling("INVALID")
|
|
||||||
print("✗ ERROR: Should have raised ValueError!")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"✓ Correctly raised ValueError: {e}")
|
|
||||||
|
|
||||||
print("\nTesting invalid slope (should raise ValueError)...")
|
|
||||||
try:
|
|
||||||
scope.set_trigger_slope("INVALID")
|
|
||||||
print("✗ ERROR: Should have raised ValueError!")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"✓ Correctly raised ValueError: {e}")
|
|
||||||
|
|
||||||
print("\nTesting invalid source (should raise ValueError)...")
|
|
||||||
try:
|
|
||||||
scope.set_trigger_source("CH5")
|
|
||||||
print("✗ ERROR: Should have raised ValueError!")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"✓ Correctly raised ValueError: {e}")
|
|
||||||
|
|
||||||
print("\nTesting invalid channel for trigger level (should raise ValueError)...")
|
|
||||||
try:
|
|
||||||
scope.set_trigger_level(5, 0.0)
|
|
||||||
print("✗ ERROR: Should have raised ValueError!")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"✓ Correctly raised ValueError: {e}")
|
|
||||||
|
|
||||||
print("\nTesting invalid trigger mode (should raise ValueError)...")
|
|
||||||
try:
|
|
||||||
scope.set_trigger_mode("INVALID")
|
|
||||||
print("✗ ERROR: Should have raised ValueError!")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"✓ Correctly raised ValueError: {e}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Error: {type(e).__name__}: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
finally:
|
|
||||||
if scope.is_connected:
|
|
||||||
scope.disconnect()
|
|
||||||
print("\n✓ Disconnected from oscilloscope")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,273 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test script for waveform transfer functionality.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from tektronix_base import TektronixOscilloscopeBase
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Test waveform transfer functions on oscilloscope at 192.168.10.105"""
|
|
||||||
|
|
||||||
scope_ip = "192.168.10.105"
|
|
||||||
|
|
||||||
print(f"Connecting to oscilloscope at {scope_ip}...")
|
|
||||||
|
|
||||||
scope = TektronixOscilloscopeBase(resource_name=scope_ip)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Connect to scope
|
|
||||||
scope.connect()
|
|
||||||
print(f"Connected to {scope.identify()}\n")
|
|
||||||
|
|
||||||
# Save current settings
|
|
||||||
print("Saving current waveform transfer settings...")
|
|
||||||
current_data_encoding = scope.get_data_encoding()
|
|
||||||
current_data_source = scope.get_data_source()
|
|
||||||
current_wfmoutpre_encoding = scope.get_wfmoutpre_encoding()
|
|
||||||
current_byte_count = scope.get_wfmoutpre_byte_count()
|
|
||||||
current_byte_order = scope.get_wfmoutpre_byte_order()
|
|
||||||
print(f" Current data encoding: {current_data_encoding}")
|
|
||||||
print(f" Current data source: {current_data_source}")
|
|
||||||
print(f" Current WFMOutpre encoding: {current_wfmoutpre_encoding}")
|
|
||||||
print(f" Current byte count: {current_byte_count}")
|
|
||||||
print(f" Current byte order: {current_byte_order}\n")
|
|
||||||
|
|
||||||
# Test setting data encoding
|
|
||||||
print("Testing data encoding settings:")
|
|
||||||
test_encodings = ['ASCIi', 'RIBinary', 'RPBinary']
|
|
||||||
for encoding in test_encodings:
|
|
||||||
print(f" Setting data encoding to {encoding}...")
|
|
||||||
scope.set_data_encoding(encoding)
|
|
||||||
actual = scope.get_data_encoding()
|
|
||||||
print(f" Actual encoding: {actual}")
|
|
||||||
|
|
||||||
# Test setting data source
|
|
||||||
print("\nTesting data source selection:")
|
|
||||||
for source in ['CH1', 'CH2', 'CH3', 'CH4']:
|
|
||||||
print(f" Setting data source to {source}...")
|
|
||||||
scope.set_data_source(source)
|
|
||||||
actual = scope.get_data_source()
|
|
||||||
print(f" Actual source: {actual}")
|
|
||||||
|
|
||||||
# Test with integer source
|
|
||||||
print("\nTesting data source with integer (2)...")
|
|
||||||
scope.set_data_source(2)
|
|
||||||
actual = scope.get_data_source()
|
|
||||||
print(f" Actual source: {actual}")
|
|
||||||
|
|
||||||
# Test waveform preamble encoding
|
|
||||||
print("\nTesting waveform preamble encoding:")
|
|
||||||
for encoding in ['BINary', 'ASCii']:
|
|
||||||
print(f" Setting WFMOutpre encoding to {encoding}...")
|
|
||||||
scope.set_wfmoutpre_encoding(encoding)
|
|
||||||
actual = scope.get_wfmoutpre_encoding()
|
|
||||||
print(f" Actual encoding: {actual}")
|
|
||||||
|
|
||||||
# Test byte count
|
|
||||||
print("\nTesting byte count settings:")
|
|
||||||
for byte_count in [1, 2]:
|
|
||||||
print(f" Setting byte count to {byte_count}...")
|
|
||||||
scope.set_wfmoutpre_byte_count(byte_count)
|
|
||||||
actual = scope.get_wfmoutpre_byte_count()
|
|
||||||
print(f" Actual byte count: {actual}")
|
|
||||||
|
|
||||||
# Test byte order
|
|
||||||
print("\nTesting byte order settings:")
|
|
||||||
for byte_order in ['MSB', 'LSB']:
|
|
||||||
print(f" Setting byte order to {byte_order}...")
|
|
||||||
scope.set_wfmoutpre_byte_order(byte_order)
|
|
||||||
actual = scope.get_wfmoutpre_byte_order()
|
|
||||||
print(f" Actual byte order: {actual}")
|
|
||||||
|
|
||||||
# Query complete waveform preamble
|
|
||||||
print("\nQuerying complete waveform preamble...")
|
|
||||||
preamble = scope.query_wfmoutpre()
|
|
||||||
print(f" Preamble (first 100 chars): {preamble[:100]}...")
|
|
||||||
|
|
||||||
# Set up for binary waveform acquisition
|
|
||||||
print("\nConfiguring for binary waveform transfer:")
|
|
||||||
print(" Setting data encoding to RIBinary...")
|
|
||||||
scope.set_data_encoding('RIBinary')
|
|
||||||
print(" Setting WFMOutpre encoding to BINary...")
|
|
||||||
scope.set_wfmoutpre_encoding('BINary')
|
|
||||||
print(" Setting byte count to 1...")
|
|
||||||
scope.set_wfmoutpre_byte_count(1)
|
|
||||||
print(" Setting byte order to MSB...")
|
|
||||||
scope.set_wfmoutpre_byte_order('MSB')
|
|
||||||
print(" Setting data source to CH1...")
|
|
||||||
scope.set_data_source('CH1')
|
|
||||||
print(" Configuration complete")
|
|
||||||
|
|
||||||
# Transfer curve data
|
|
||||||
print("\nTransferring curve data from CH1...")
|
|
||||||
curve_data = scope.transfer_curve()
|
|
||||||
print(f" Received {len(curve_data)} bytes of curve data")
|
|
||||||
print(f" First 10 bytes (raw): {list(curve_data[:10])}")
|
|
||||||
|
|
||||||
# Parse the curve data
|
|
||||||
print("\nParsing curve data...")
|
|
||||||
values = scope.parse_curve_data(curve_data, byte_count=1, signed=True, byte_order='MSB')
|
|
||||||
print(f" Parsed {len(values)} samples")
|
|
||||||
print(f" First 10 values: {values[:10]}")
|
|
||||||
print(f" Min value: {min(values)}")
|
|
||||||
print(f" Max value: {max(values)}")
|
|
||||||
print(f" Average value: {sum(values) / len(values):.2f}")
|
|
||||||
|
|
||||||
# Transfer complete waveform (preamble + curve)
|
|
||||||
print("\nTransferring complete waveform (WAVFrm?)...")
|
|
||||||
preamble_str, curve_bytes = scope.transfer_waveform()
|
|
||||||
print(f" Received preamble: {preamble_str[:100]}...")
|
|
||||||
print(f" Received {len(curve_bytes)} bytes of curve data")
|
|
||||||
|
|
||||||
# Parse this curve data too
|
|
||||||
waveform_values = scope.parse_curve_data(curve_bytes, byte_count=1, signed=True, byte_order='MSB')
|
|
||||||
print(f" Parsed {len(waveform_values)} samples from complete waveform")
|
|
||||||
|
|
||||||
# Test high-level acquire_waveform method
|
|
||||||
print("\nTesting high-level acquire_waveform method:")
|
|
||||||
print(" Acquiring waveform from CH1...")
|
|
||||||
waveform = scope.acquire_waveform('CH1')
|
|
||||||
print(f" Acquired {len(waveform)} samples")
|
|
||||||
print(f" First 10 values: {waveform[:10]}")
|
|
||||||
print(f" Min: {min(waveform)}, Max: {max(waveform)}, Avg: {sum(waveform)/len(waveform):.2f}")
|
|
||||||
|
|
||||||
print("\n Acquiring waveform from CH2 (using integer)...")
|
|
||||||
try:
|
|
||||||
waveform2 = scope.acquire_waveform(2)
|
|
||||||
print(f" Acquired {len(waveform2)} samples from CH2")
|
|
||||||
print(f" First 10 values: {waveform2[:10]}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f" Could not acquire from CH2 (channel may not be active): {e}")
|
|
||||||
|
|
||||||
# Test FastFrame support
|
|
||||||
print("\nTesting FastFrame frame selection:")
|
|
||||||
|
|
||||||
# Check if FastFrame is currently enabled
|
|
||||||
try:
|
|
||||||
ff_state = scope.get_fastframe_state()
|
|
||||||
ff_count = scope.get_fastframe_count()
|
|
||||||
print(f" Current FastFrame state: {ff_state} ({'active' if ff_state else 'off'})")
|
|
||||||
print(f" Current frame count: {ff_count}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f" Could not query FastFrame state (skipping FastFrame tests): {e}")
|
|
||||||
ff_state = None
|
|
||||||
|
|
||||||
if ff_state is not None and ff_state:
|
|
||||||
# FastFrame is active, test frame selection
|
|
||||||
print(" FastFrame is active, testing frame selection...")
|
|
||||||
current_frame = scope.get_fastframe_selected()
|
|
||||||
print(f" Current selected frame: {current_frame}")
|
|
||||||
|
|
||||||
# Try selecting different frames
|
|
||||||
for frame in [1, min(5, ff_count), ff_count]:
|
|
||||||
print(f" Selecting frame {frame}...")
|
|
||||||
scope.set_fastframe_selected(frame)
|
|
||||||
actual = scope.get_fastframe_selected()
|
|
||||||
print(f" Selected frame: {actual}")
|
|
||||||
|
|
||||||
# Acquire waveform from this frame
|
|
||||||
print(f" Acquiring waveform from frame {frame}...")
|
|
||||||
frame_waveform = scope.acquire_waveform('CH1', frame_number=frame)
|
|
||||||
print(f" Acquired {len(frame_waveform)} samples")
|
|
||||||
print(f" First 5 values: {frame_waveform[:5]}")
|
|
||||||
|
|
||||||
# Restore original frame
|
|
||||||
scope.set_fastframe_selected(current_frame)
|
|
||||||
elif ff_state is not None:
|
|
||||||
print(" FastFrame is not active, enabling it temporarily...")
|
|
||||||
scope.set_fastframe_state(True)
|
|
||||||
scope.set_fastframe_count(10)
|
|
||||||
print(" FastFrame enabled with 10 frames")
|
|
||||||
|
|
||||||
# Test frame selection
|
|
||||||
for frame in [1, 5, 10]:
|
|
||||||
print(f" Selecting frame {frame}...")
|
|
||||||
scope.set_fastframe_selected(frame)
|
|
||||||
actual = scope.get_fastframe_selected()
|
|
||||||
print(f" Selected frame: {actual}")
|
|
||||||
|
|
||||||
# Restore FastFrame state
|
|
||||||
scope.set_fastframe_state(False)
|
|
||||||
print(" FastFrame disabled (restored)")
|
|
||||||
|
|
||||||
# Restore original settings
|
|
||||||
print("\nRestoring original waveform transfer settings...")
|
|
||||||
try:
|
|
||||||
scope.set_data_encoding(current_data_encoding)
|
|
||||||
scope.set_data_source(current_data_source)
|
|
||||||
scope.set_wfmoutpre_encoding(current_wfmoutpre_encoding)
|
|
||||||
scope.set_wfmoutpre_byte_count(current_byte_count)
|
|
||||||
scope.set_wfmoutpre_byte_order(current_byte_order)
|
|
||||||
print(" Settings restored")
|
|
||||||
except Exception as e:
|
|
||||||
print(f" Could not restore settings (connection may be in bad state): {e}")
|
|
||||||
|
|
||||||
# Test error handling
|
|
||||||
print("\n=== Testing Error Handling ===\n")
|
|
||||||
|
|
||||||
# Skip error handling tests if connection is already bad
|
|
||||||
try:
|
|
||||||
# Quick connectivity check
|
|
||||||
scope.query("*OPC?")
|
|
||||||
except Exception:
|
|
||||||
print("Connection appears to be in bad state, skipping error handling tests\n")
|
|
||||||
print("=== Waveform transfer tests completed (with some skipped due to connection issues) ===")
|
|
||||||
return
|
|
||||||
|
|
||||||
print("Testing invalid data encoding (should raise ValueError)...")
|
|
||||||
try:
|
|
||||||
scope.set_data_encoding('INVALID')
|
|
||||||
print(" ERROR: Should have raised ValueError!")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f" Correctly raised ValueError: {e}")
|
|
||||||
|
|
||||||
print("\nTesting invalid data source (should raise ValueError)...")
|
|
||||||
try:
|
|
||||||
scope.set_data_source('CH5')
|
|
||||||
print(" ERROR: Should have raised ValueError!")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f" Correctly raised ValueError: {e}")
|
|
||||||
|
|
||||||
print("\nTesting invalid byte count (should raise ValueError)...")
|
|
||||||
try:
|
|
||||||
scope.set_wfmoutpre_byte_count(3)
|
|
||||||
print(" ERROR: Should have raised ValueError!")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f" Correctly raised ValueError: {e}")
|
|
||||||
|
|
||||||
print("\nTesting invalid byte order (should raise ValueError)...")
|
|
||||||
try:
|
|
||||||
scope.set_wfmoutpre_byte_order('INVALID')
|
|
||||||
print(" ERROR: Should have raised ValueError!")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f" Correctly raised ValueError: {e}")
|
|
||||||
|
|
||||||
print("\nTesting invalid FastFrame frame number (should raise ValueError)...")
|
|
||||||
try:
|
|
||||||
scope.set_fastframe_selected(-1)
|
|
||||||
print(" ERROR: Should have raised ValueError!")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f" Correctly raised ValueError: {e}")
|
|
||||||
|
|
||||||
print("\nTesting invalid channel in acquire_waveform (should raise ValueError)...")
|
|
||||||
try:
|
|
||||||
scope.acquire_waveform(5)
|
|
||||||
print(" ERROR: Should have raised ValueError!")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f" Correctly raised ValueError: {e}")
|
|
||||||
|
|
||||||
print("\n=== All waveform transfer tests completed successfully! ===")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"\nError: {type(e).__name__}: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
finally:
|
|
||||||
if scope.is_connected:
|
|
||||||
scope.disconnect()
|
|
||||||
print("\nDisconnected from oscilloscope")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Simple focused test for waveform transfer functionality.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from tektronix_base import TektronixOscilloscopeBase
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Simple test of core waveform transfer on CH1"""
|
|
||||||
|
|
||||||
scope_ip = "192.168.10.105"
|
|
||||||
|
|
||||||
print(f"Connecting to oscilloscope at {scope_ip}...")
|
|
||||||
|
|
||||||
scope = TektronixOscilloscopeBase(resource_name=scope_ip, timeout=10.0)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Connect to scope
|
|
||||||
scope.connect()
|
|
||||||
print(f"Connected to {scope.identify()}\n")
|
|
||||||
|
|
||||||
# Test 1: Configure and transfer using low-level methods
|
|
||||||
print("=== Test 1: Low-level waveform transfer ===")
|
|
||||||
print("Configuring data transfer settings...")
|
|
||||||
scope.set_data_encoding('RIBinary')
|
|
||||||
scope.set_wfmoutpre_encoding('BINary')
|
|
||||||
scope.set_wfmoutpre_byte_count(1)
|
|
||||||
scope.set_wfmoutpre_byte_order('MSB')
|
|
||||||
scope.set_data_source('CH1')
|
|
||||||
|
|
||||||
print("Transferring curve data from CH1...")
|
|
||||||
curve_data = scope.transfer_curve()
|
|
||||||
print(f" Received {len(curve_data)} bytes")
|
|
||||||
|
|
||||||
print("Parsing curve data...")
|
|
||||||
values = scope.parse_curve_data(curve_data, byte_count=1, signed=True, byte_order='MSB')
|
|
||||||
print(f" Parsed {len(values)} samples")
|
|
||||||
print(f" First 10 values: {values[:10]}")
|
|
||||||
print(f" Min: {min(values)}, Max: {max(values)}, Avg: {sum(values)/len(values):.2f}")
|
|
||||||
|
|
||||||
# Test 2: Use high-level acquire_waveform method
|
|
||||||
print("\n=== Test 2: High-level waveform acquisition ===")
|
|
||||||
print("Acquiring waveform from CH1...")
|
|
||||||
waveform = scope.acquire_waveform('CH1')
|
|
||||||
print(f" Acquired {len(waveform)} samples")
|
|
||||||
print(f" First 10 values: {waveform[:10]}")
|
|
||||||
print(f" Min: {min(waveform)}, Max: {max(waveform)}, Avg: {sum(waveform)/len(waveform):.2f}")
|
|
||||||
|
|
||||||
# Test 3: Query waveform preamble
|
|
||||||
print("\n=== Test 3: Waveform preamble ===")
|
|
||||||
preamble = scope.query_wfmoutpre()
|
|
||||||
print(f" Preamble: {preamble[:150]}...")
|
|
||||||
|
|
||||||
# Test 4: Transfer complete waveform (preamble + curve)
|
|
||||||
print("\n=== Test 4: Complete waveform transfer ===")
|
|
||||||
print("Transferring complete waveform...")
|
|
||||||
preamble_str, curve_bytes = scope.transfer_waveform()
|
|
||||||
print(f" Preamble length: {len(preamble_str)} chars")
|
|
||||||
print(f" Curve data: {len(curve_bytes)} bytes")
|
|
||||||
wf_values = scope.parse_curve_data(curve_bytes, byte_count=1, signed=True, byte_order='MSB')
|
|
||||||
print(f" Parsed {len(wf_values)} samples")
|
|
||||||
|
|
||||||
print("\n=== All tests completed successfully! ===")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"\nError: {type(e).__name__}: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
finally:
|
|
||||||
if scope.is_connected:
|
|
||||||
scope.disconnect()
|
|
||||||
print("\nDisconnected from oscilloscope")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,315 +0,0 @@
|
|||||||
# Coherent HOPS Laser I2C Protocol Documentation
|
|
||||||
|
|
||||||
This document describes the I2C communication protocol used to control Coherent HOPS laser systems, extracted from the CohrHopsDemo v2.0.7 codebase.
|
|
||||||
|
|
||||||
## Hardware Overview
|
|
||||||
|
|
||||||
### FTDI Interface
|
|
||||||
- **Chip**: FT2232C (dual-channel USB)
|
|
||||||
- **Protocol**: I2C via MPSSE (Multi-Protocol Synchronous Serial Engine)
|
|
||||||
- **Library**: CohrFTCI2C.dll (Windows), use libftdi/libmpsse on Linux
|
|
||||||
|
|
||||||
### I2C Configuration
|
|
||||||
| Parameter | Value/Range |
|
|
||||||
|-----------|-------------|
|
|
||||||
| Clock Divisor | 0 - 65535 |
|
|
||||||
| Modes | STANDARD, FAST |
|
|
||||||
| Control Bytes | 1 - 255 |
|
|
||||||
| Data Bytes | 1 - 65535 |
|
|
||||||
|
|
||||||
### I2C Slave
|
|
||||||
- **Device**: NXP microcontroller
|
|
||||||
- **Role**: Intermediary between FTDI and laser hardware
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## I2C Library Functions
|
|
||||||
|
|
||||||
These are the low-level FTDI I2C functions (from CohrFTCI2C.dll):
|
|
||||||
|
|
||||||
| Function | Purpose |
|
|
||||||
|----------|---------|
|
|
||||||
| `I2C_GetNumDevices` | Enumerate connected I2C devices |
|
|
||||||
| `I2C_GetDeviceNameLocID` | Get device location identifier |
|
|
||||||
| `I2C_GetDeviceNameSerialNumber` | Get device serial number |
|
|
||||||
| `I2C_Open` | Open I2C device |
|
|
||||||
| `I2C_OpenEx` | Extended open with options |
|
|
||||||
| `I2C_OpenSerialNumber` | Open device by serial number |
|
|
||||||
| `I2C_InitDevice` | Initialize MPSSE interface |
|
|
||||||
| `I2C_SetMode` | Set STANDARD or FAST mode |
|
|
||||||
| `I2C_GetClock` | Get current clock divisor |
|
|
||||||
| `I2C_SetClock` | Set clock divisor |
|
|
||||||
| `I2C_SetLoopback` | Enable/disable loopback testing |
|
|
||||||
| `I2C_Write` | Write control + data bytes |
|
|
||||||
| `I2C_Read` | Read data bytes |
|
|
||||||
| `I2C_ReadAlt` | Alternative read function |
|
|
||||||
| `I2C_Close` | Close I2C device |
|
|
||||||
| `I2C_GetErrorCodeString` | Get error descriptions |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## NXP Slave Operations
|
|
||||||
|
|
||||||
The NXP microcontroller provides these I2C operations:
|
|
||||||
|
|
||||||
| Method | Purpose |
|
|
||||||
|--------|---------|
|
|
||||||
| `NXP::Write` | Write data to I2C slave |
|
|
||||||
| `NXP::Read` | Read data from I2C slave |
|
|
||||||
| `NXP::WriteRegister` | Write to internal registers |
|
|
||||||
| `NXP::ReadRegister` | Read from internal registers |
|
|
||||||
| `NXP::WriteGPIO` | Control GPIO outputs |
|
|
||||||
| `NXP::ReadGPIO` | Read GPIO inputs |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## I2C Transaction Format
|
|
||||||
|
|
||||||
### Write Operation
|
|
||||||
```
|
|
||||||
1. WriteControlBuffer: I2C slave address + W bit (0)
|
|
||||||
2. WriteDataBuffer: Register address + data
|
|
||||||
- BYTE mode: Single byte writes
|
|
||||||
- PAGE mode: Multi-byte writes
|
|
||||||
```
|
|
||||||
|
|
||||||
### Read Operation
|
|
||||||
```
|
|
||||||
1. WriteControlBuffer: I2C slave address + R bit (1)
|
|
||||||
2. ReadDataBuffer: Receive response
|
|
||||||
- BYTE mode: Single byte reads
|
|
||||||
- BLOCK mode: Multi-byte reads
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## High-Level Command Interface
|
|
||||||
|
|
||||||
Commands are sent via `CohrHOPS_SendCommand()` using the format `?COMMAND` for queries.
|
|
||||||
|
|
||||||
### System Information Commands
|
|
||||||
|
|
||||||
| Command | Purpose | Example Response |
|
|
||||||
|---------|---------|------------------|
|
|
||||||
| `?HID` | Query Hardware ID | Device identifier |
|
|
||||||
| `?HTYPE` | Query Head Type | Head variant |
|
|
||||||
| `?HBDREV` | Query Head Board Revision | PCB revision |
|
|
||||||
| `?HEADDIO` | Query Head Digital I/O | DIO configuration |
|
|
||||||
| `?LASERMODEL` | Query Laser Model | G532, Tina, Mini00, MiniX |
|
|
||||||
| `?POWERUNITS` | Query Power Units | mW, W, etc. |
|
|
||||||
| `?WAVELENGTH` | Query Wavelength | 532nm, etc. |
|
|
||||||
|
|
||||||
### Temperature Monitoring
|
|
||||||
|
|
||||||
| Command | Purpose |
|
|
||||||
|---------|---------|
|
|
||||||
| `?TMAIN` | Main Heatsink Temperature |
|
|
||||||
| `?TBRF` | BRF (Birefringent Filter) Temperature |
|
|
||||||
| `?TSHG` | SHG (Second Harmonic Generator) Temperature |
|
|
||||||
| `?TTHG` | THG (Third Harmonic Generator) Temperature |
|
|
||||||
| `?TETA` | ETA Temperature |
|
|
||||||
|
|
||||||
### Temperature Control (Setpoints)
|
|
||||||
|
|
||||||
| Command | Purpose |
|
|
||||||
|---------|---------|
|
|
||||||
| `?TMAINCMD` | Get/Set Main Temperature Setpoint |
|
|
||||||
| `?TBRFCMD` | Get/Set BRF Temperature Setpoint |
|
|
||||||
| `?TSHGCMD` | Get/Set SHG Temperature Setpoint |
|
|
||||||
| `?TTHGCMD` | Get/Set THG Temperature Setpoint |
|
|
||||||
| `?TETACMD` | Get/Set ETA Temperature Setpoint |
|
|
||||||
|
|
||||||
### Temperature Data
|
|
||||||
|
|
||||||
| Command | Purpose |
|
|
||||||
|---------|---------|
|
|
||||||
| `?MAIND` | Main Temperature Data |
|
|
||||||
| `?BRFD` | BRF Temperature Data |
|
|
||||||
| `?SHGD` | SHG Temperature Data |
|
|
||||||
| `?THGD` | THG Temperature Data |
|
|
||||||
| `?ETAD` | ETA Temperature Data |
|
|
||||||
|
|
||||||
### Power Control
|
|
||||||
|
|
||||||
| Command | Purpose |
|
|
||||||
|---------|---------|
|
|
||||||
| `?PCMD` | Get/Set Power Command |
|
|
||||||
| `?PMEM` | Query Power Memory (stored settings) |
|
|
||||||
| `?PLIM` | Query Power Limits |
|
|
||||||
|
|
||||||
### Current Control
|
|
||||||
|
|
||||||
| Command | Purpose |
|
|
||||||
|---------|---------|
|
|
||||||
| `?CCMD` | Get/Set Current Command |
|
|
||||||
| `?CLIM` | Query Current Limits |
|
|
||||||
| `?CMODE` | Get/Set Control Mode |
|
|
||||||
| `?CMODECMD` | Get/Set Control Mode Command |
|
|
||||||
|
|
||||||
### Digital I/O
|
|
||||||
|
|
||||||
| Command | Purpose |
|
|
||||||
|---------|---------|
|
|
||||||
| `?PSDIO` | Power Supply Digital I/O |
|
|
||||||
| `?PSGLUEIN` | Power Supply Glue Logic Input |
|
|
||||||
| `?PSGLUEOUT` | Power Supply Glue Logic Output |
|
|
||||||
|
|
||||||
### Monitoring & Status
|
|
||||||
|
|
||||||
| Command | Purpose |
|
|
||||||
|---------|---------|
|
|
||||||
| `?ANA` | Query Analog Values |
|
|
||||||
| `?ANACMD` | Get/Set Analog Command |
|
|
||||||
| `?KSW` | Key Switch Status |
|
|
||||||
| `?KSWCMD` | Get/Set Key Switch Command |
|
|
||||||
| `?FAN` | Fan Status/Control |
|
|
||||||
| `?INT` | Interlock Status |
|
|
||||||
| `?REM` | Remote Control Status |
|
|
||||||
| `?EEH` | EEPROM Header |
|
|
||||||
|
|
||||||
### Configuration Registers
|
|
||||||
|
|
||||||
| Command | Purpose |
|
|
||||||
|---------|---------|
|
|
||||||
| `?CFG0` | Configuration Register 0 |
|
|
||||||
| `?CFG1` | Configuration Register 1 |
|
|
||||||
| `?CFG2` | Configuration Register 2 |
|
|
||||||
| `?CFG3` | Configuration Register 3 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Supported Laser Models
|
|
||||||
|
|
||||||
| Model | Description |
|
|
||||||
|-------|-------------|
|
|
||||||
| G532 | 532nm Green Laser |
|
|
||||||
| Tina | Proprietary Model |
|
|
||||||
| Mini00 | Compact Variant |
|
|
||||||
| MiniX | Extended Mini Variant |
|
|
||||||
| CommonLaser | Base Implementation |
|
|
||||||
| DummyLaser | Test/Simulation |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Linux Implementation Guide
|
|
||||||
|
|
||||||
### Required Libraries
|
|
||||||
|
|
||||||
For Linux implementation, use one of:
|
|
||||||
- **libftdi** + **libmpsse** - Direct FTDI MPSSE control
|
|
||||||
- **pylibftdi** - Python bindings for libftdi
|
|
||||||
- Standard Linux I2C (`/dev/i2c-*`) if FTDI exposes as I2C adapter
|
|
||||||
|
|
||||||
### Installation (Debian/Ubuntu)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt install libftdi-dev libmpsse-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Basic Implementation Steps
|
|
||||||
|
|
||||||
1. **Initialize FTDI Device**
|
|
||||||
```c
|
|
||||||
// Find and open FT2232C device
|
|
||||||
ftdi_init(&ftdi);
|
|
||||||
ftdi_usb_open(&ftdi, 0x0403, 0x6010); // FTDI VID/PID
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Configure MPSSE for I2C**
|
|
||||||
```c
|
|
||||||
// Enable MPSSE mode
|
|
||||||
ftdi_set_bitmode(&ftdi, 0, BITMODE_MPSSE);
|
|
||||||
|
|
||||||
// Set I2C clock speed
|
|
||||||
// Clock = 60MHz / ((1 + divisor) * 2)
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Send I2C Commands**
|
|
||||||
```c
|
|
||||||
// Write command to laser
|
|
||||||
i2c_write(slave_addr, "?HID", 4);
|
|
||||||
|
|
||||||
// Read response
|
|
||||||
i2c_read(slave_addr, buffer, sizeof(buffer));
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example: Query Laser Model
|
|
||||||
|
|
||||||
```c
|
|
||||||
#include <ftdi.h>
|
|
||||||
#include <mpsse.h>
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
struct mpsse_context *i2c;
|
|
||||||
char response[256];
|
|
||||||
|
|
||||||
// Open I2C at 100kHz
|
|
||||||
i2c = MPSSE(I2C, ONE_HUNDRED_KHZ, MSB);
|
|
||||||
|
|
||||||
if (i2c && i2c->open) {
|
|
||||||
// Send query command
|
|
||||||
Start(i2c);
|
|
||||||
Write(i2c, "?LASERMODEL", 11);
|
|
||||||
Stop(i2c);
|
|
||||||
|
|
||||||
// Read response
|
|
||||||
Start(i2c);
|
|
||||||
char *data = Read(i2c, 256);
|
|
||||||
Stop(i2c);
|
|
||||||
|
|
||||||
printf("Laser Model: %s\n", data);
|
|
||||||
free(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
Close(i2c);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
### Common Errors
|
|
||||||
|
|
||||||
| Error | Description |
|
|
||||||
|-------|-------------|
|
|
||||||
| Timeout after control byte | No ACK received after sending slave address |
|
|
||||||
| Timeout after data byte | No ACK received after sending data |
|
|
||||||
| MPSSE sync failure | Failed to synchronize FTDI MPSSE interface |
|
|
||||||
|
|
||||||
### Recovery
|
|
||||||
|
|
||||||
1. Reset MPSSE interface
|
|
||||||
2. Re-initialize I2C
|
|
||||||
3. Check physical connections
|
|
||||||
4. Verify I2C slave address
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Protocol Notes
|
|
||||||
|
|
||||||
- Commands use ASCII text format
|
|
||||||
- Query commands start with `?`
|
|
||||||
- Set commands likely use `=` followed by value
|
|
||||||
- Responses are ASCII strings
|
|
||||||
- Temperature values likely in degrees Celsius
|
|
||||||
- Power values use units from `?POWERUNITS` response
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Source Files Reference
|
|
||||||
|
|
||||||
| File | Purpose |
|
|
||||||
|------|---------|
|
|
||||||
| `CohrHOPS.dll` | Main laser control library |
|
|
||||||
| `CohrFTCI2C.dll` | FTDI I2C bridge library |
|
|
||||||
| `main.c` | Demo application |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Additional Resources
|
|
||||||
|
|
||||||
- FTDI MPSSE Documentation: https://ftdichip.com/software-examples/mpsse-projects/
|
|
||||||
- libmpsse: https://github.com/devttys0/libmpsse
|
|
||||||
- Linux I2C: https://www.kernel.org/doc/html/latest/i2c/
|
|
||||||
@@ -1,283 +0,0 @@
|
|||||||
# PyPewPewHOPS - Coherent HOPS Laser Control Library
|
|
||||||
|
|
||||||
Python library for controlling Coherent HOPS laser systems via I2C protocol through an FTDI FT2232C USB interface.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Complete implementation of all documented I2C commands
|
|
||||||
- Support for system information queries
|
|
||||||
- Temperature monitoring and control for all sensors
|
|
||||||
- Power and current control
|
|
||||||
- Digital I/O operations
|
|
||||||
- Configuration register access
|
|
||||||
- Context manager support for safe resource handling
|
|
||||||
- Dummy laser simulator for development without hardware
|
|
||||||
- Comprehensive error handling
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### Requirements
|
|
||||||
|
|
||||||
- Python 3.7+
|
|
||||||
- FTDI FT2232C USB device
|
|
||||||
- libftdi library (for Linux)
|
|
||||||
|
|
||||||
### Install Dependencies
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
### Linux Setup
|
|
||||||
|
|
||||||
On Linux, you may need to install libftdi:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Debian/Ubuntu
|
|
||||||
sudo apt install libftdi-dev
|
|
||||||
|
|
||||||
# Fedora
|
|
||||||
sudo dnf install libftdi-devel
|
|
||||||
```
|
|
||||||
|
|
||||||
You may also need to add your user to the appropriate group:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo usermod -a -G dialout $USER
|
|
||||||
sudo usermod -a -G plugdev $USER
|
|
||||||
```
|
|
||||||
|
|
||||||
Then log out and log back in for the changes to take effect.
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
### Using the Simulator (No Hardware)
|
|
||||||
|
|
||||||
```python
|
|
||||||
from coherent_hops_laser import DummyLaser
|
|
||||||
|
|
||||||
with DummyLaser() as laser:
|
|
||||||
# Query system information
|
|
||||||
info = laser.get_system_info()
|
|
||||||
print(f"Model: {info.laser_model}")
|
|
||||||
print(f"Wavelength: {info.wavelength}")
|
|
||||||
|
|
||||||
# Monitor temperatures
|
|
||||||
temps = laser.get_all_temperatures()
|
|
||||||
print(f"Main temperature: {temps.main}°C")
|
|
||||||
|
|
||||||
# Control power
|
|
||||||
laser.set_power_command(100.0)
|
|
||||||
power = laser.get_power_command()
|
|
||||||
print(f"Power set to: {power} mW")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using Real Hardware
|
|
||||||
|
|
||||||
```python
|
|
||||||
from coherent_hops_laser import CoherentHOPSLaser, I2CMode
|
|
||||||
|
|
||||||
# Initialize laser controller
|
|
||||||
laser = CoherentHOPSLaser(
|
|
||||||
slave_address=0x50, # I2C slave address
|
|
||||||
i2c_mode=I2CMode.STANDARD # 100 kHz
|
|
||||||
)
|
|
||||||
|
|
||||||
# Connect to FTDI device
|
|
||||||
laser.connect('ftdi://ftdi:2232/1')
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Query laser model
|
|
||||||
model = laser.get_laser_model()
|
|
||||||
print(f"Laser Model: {model}")
|
|
||||||
|
|
||||||
# Get all temperatures
|
|
||||||
temps = laser.get_all_temperatures()
|
|
||||||
print(f"Temperatures: {temps}")
|
|
||||||
|
|
||||||
# Set power
|
|
||||||
laser.set_power_command(50.0)
|
|
||||||
|
|
||||||
# Check control mode
|
|
||||||
mode = laser.get_control_mode()
|
|
||||||
print(f"Control Mode: {mode}")
|
|
||||||
|
|
||||||
finally:
|
|
||||||
laser.disconnect()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using Context Manager
|
|
||||||
|
|
||||||
```python
|
|
||||||
from coherent_hops_laser import CoherentHOPSLaser
|
|
||||||
|
|
||||||
with CoherentHOPSLaser() as laser:
|
|
||||||
laser.connect()
|
|
||||||
|
|
||||||
# Your laser control code here
|
|
||||||
info = laser.get_system_info()
|
|
||||||
print(info)
|
|
||||||
|
|
||||||
# Automatically disconnects
|
|
||||||
```
|
|
||||||
|
|
||||||
## Available Commands
|
|
||||||
|
|
||||||
### System Information
|
|
||||||
|
|
||||||
- `get_hardware_id()` - Hardware ID
|
|
||||||
- `get_head_type()` - Head type
|
|
||||||
- `get_head_board_revision()` - PCB revision
|
|
||||||
- `get_laser_model()` - Laser model (G532, Tina, Mini00, MiniX)
|
|
||||||
- `get_power_units()` - Power units (mW, W)
|
|
||||||
- `get_wavelength()` - Wavelength (e.g., 532nm)
|
|
||||||
- `get_system_info()` - All system info at once
|
|
||||||
|
|
||||||
### Temperature Monitoring
|
|
||||||
|
|
||||||
- `get_temperature_main()` - Main heatsink temperature
|
|
||||||
- `get_temperature_brf()` - BRF temperature
|
|
||||||
- `get_temperature_shg()` - SHG temperature
|
|
||||||
- `get_temperature_thg()` - THG temperature
|
|
||||||
- `get_temperature_eta()` - ETA temperature
|
|
||||||
- `get_all_temperatures()` - All temperatures at once
|
|
||||||
|
|
||||||
### Temperature Control
|
|
||||||
|
|
||||||
- `get_temperature_setpoint_main()` / `set_temperature_setpoint_main(temp)`
|
|
||||||
- `get_temperature_setpoint_brf()` / `set_temperature_setpoint_brf(temp)`
|
|
||||||
- `get_temperature_setpoint_shg()` / `set_temperature_setpoint_shg(temp)`
|
|
||||||
- `get_temperature_setpoint_thg()` / `set_temperature_setpoint_thg(temp)`
|
|
||||||
- `get_temperature_setpoint_eta()` / `set_temperature_setpoint_eta(temp)`
|
|
||||||
|
|
||||||
### Power Control
|
|
||||||
|
|
||||||
- `get_power_command()` / `set_power_command(power)` - Get/set power
|
|
||||||
- `get_power_memory()` - Stored power settings
|
|
||||||
- `get_power_limits()` - Power limits
|
|
||||||
|
|
||||||
### Current Control
|
|
||||||
|
|
||||||
- `get_current_command()` / `set_current_command(current)` - Get/set current
|
|
||||||
- `get_current_limits()` - Current limits
|
|
||||||
- `get_control_mode()` / `set_control_mode(mode)` - Control mode (POWER/CURRENT)
|
|
||||||
|
|
||||||
### Status Monitoring
|
|
||||||
|
|
||||||
- `get_key_switch_status()` - Key switch status
|
|
||||||
- `get_fan_status()` / `set_fan_control(value)` - Fan control
|
|
||||||
- `get_interlock_status()` - Interlock status
|
|
||||||
- `get_remote_control_status()` - Remote control status
|
|
||||||
- `get_analog_values()` - Analog sensor values
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
|
|
||||||
- `get_config_register_0()` / `set_config_register_0(value)`
|
|
||||||
- `get_config_register_1()` / `set_config_register_1(value)`
|
|
||||||
- `get_config_register_2()` / `set_config_register_2(value)`
|
|
||||||
- `get_config_register_3()` / `set_config_register_3(value)`
|
|
||||||
|
|
||||||
See the [API documentation](LASER_I2C_PROTOCOL.md) for complete command reference.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
Run the example script:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Simulation mode (no hardware)
|
|
||||||
python3 example_usage.py 1
|
|
||||||
|
|
||||||
# Real hardware mode
|
|
||||||
python3 example_usage.py 2
|
|
||||||
|
|
||||||
# Continuous monitoring
|
|
||||||
python3 example_usage.py 3
|
|
||||||
```
|
|
||||||
|
|
||||||
Or run the built-in test:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 coherent_hops_laser.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## Continuous Monitoring Example
|
|
||||||
|
|
||||||
```python
|
|
||||||
from coherent_hops_laser import CoherentHOPSLaser
|
|
||||||
import time
|
|
||||||
|
|
||||||
with CoherentHOPSLaser() as laser:
|
|
||||||
laser.connect()
|
|
||||||
|
|
||||||
while True:
|
|
||||||
temps = laser.get_all_temperatures()
|
|
||||||
power = laser.get_power_command()
|
|
||||||
|
|
||||||
print(f"Main: {temps.main:.1f}°C Power: {power:.1f}mW")
|
|
||||||
time.sleep(1)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Cannot find FTDI device
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check if device is connected
|
|
||||||
lsusb | grep FTDI
|
|
||||||
|
|
||||||
# Should show something like:
|
|
||||||
# Bus 001 Device 005: ID 0403:6010 Future Technology Devices International, Ltd FT2232C
|
|
||||||
```
|
|
||||||
|
|
||||||
### Permission denied
|
|
||||||
|
|
||||||
Add your user to the dialout/plugdev group:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo usermod -a -G dialout $USER
|
|
||||||
sudo usermod -a -G plugdev $USER
|
|
||||||
```
|
|
||||||
|
|
||||||
Then log out and back in.
|
|
||||||
|
|
||||||
### I2C communication errors
|
|
||||||
|
|
||||||
- Verify correct slave address (default: 0x50)
|
|
||||||
- Check I2C speed (try I2CMode.STANDARD instead of FAST)
|
|
||||||
- Verify physical connections
|
|
||||||
- Check for other devices on the bus
|
|
||||||
|
|
||||||
### Import errors
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install pyftdi
|
|
||||||
pip install pyftdi
|
|
||||||
|
|
||||||
# If that fails, try:
|
|
||||||
pip install --user pyftdi
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
- **CoherentHOPSLaser**: Main class for real hardware control
|
|
||||||
- **DummyLaser**: Simulator for development without hardware
|
|
||||||
- **I2CController**: Low-level FTDI I2C communication (from pyftdi)
|
|
||||||
- **LaserInfo / TemperatureStatus**: Data classes for structured responses
|
|
||||||
|
|
||||||
## Safety Notes
|
|
||||||
|
|
||||||
- Always verify power levels before enabling laser output
|
|
||||||
- Monitor temperatures during operation
|
|
||||||
- Check interlock status before operation
|
|
||||||
- Use appropriate laser safety equipment
|
|
||||||
- Follow all manufacturer safety guidelines
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This implementation is based on the Coherent HOPS Demo v2.0.7 protocol documentation.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- FTDI MPSSE Documentation: https://ftdichip.com/software-examples/mpsse-projects/
|
|
||||||
- PyFTDI: https://github.com/eblot/pyftdi
|
|
||||||
- Original Protocol Documentation: [LASER_I2C_PROTOCOL.md](LASER_I2C_PROTOCOL.md)
|
|
||||||
@@ -1,773 +0,0 @@
|
|||||||
"""
|
|
||||||
Coherent HOPS Laser I2C Control Library
|
|
||||||
|
|
||||||
This module provides a Python interface to control Coherent HOPS laser systems
|
|
||||||
via I2C protocol through an FTDI FT2232C USB interface.
|
|
||||||
|
|
||||||
Dependencies:
|
|
||||||
pip install pyftdi
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
from coherent_hops_laser import CoherentHOPSLaser
|
|
||||||
|
|
||||||
laser = CoherentHOPSLaser()
|
|
||||||
laser.connect()
|
|
||||||
|
|
||||||
# Query system information
|
|
||||||
model = laser.get_laser_model()
|
|
||||||
wavelength = laser.get_wavelength()
|
|
||||||
|
|
||||||
# Monitor temperatures
|
|
||||||
main_temp = laser.get_temperature_main()
|
|
||||||
|
|
||||||
# Control power
|
|
||||||
laser.set_power_command(100.0) # Set power in mW or W
|
|
||||||
|
|
||||||
laser.disconnect()
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Optional, Union, List
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from enum import Enum
|
|
||||||
import time
|
|
||||||
import logging
|
|
||||||
|
|
||||||
try:
|
|
||||||
from pyftdi.i2c import I2cController, I2cNackError
|
|
||||||
except ImportError:
|
|
||||||
raise ImportError(
|
|
||||||
"pyftdi library is required. Install with: pip install pyftdi"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Configure logging
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class I2CMode(Enum):
|
|
||||||
"""I2C communication modes"""
|
|
||||||
STANDARD = 100000 # 100 kHz
|
|
||||||
FAST = 400000 # 400 kHz
|
|
||||||
|
|
||||||
|
|
||||||
class ControlMode(Enum):
|
|
||||||
"""Laser control modes"""
|
|
||||||
POWER = "POWER"
|
|
||||||
CURRENT = "CURRENT"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class LaserInfo:
|
|
||||||
"""Laser system information"""
|
|
||||||
hardware_id: Optional[str] = None
|
|
||||||
head_type: Optional[str] = None
|
|
||||||
head_board_revision: Optional[str] = None
|
|
||||||
laser_model: Optional[str] = None
|
|
||||||
power_units: Optional[str] = None
|
|
||||||
wavelength: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class TemperatureStatus:
|
|
||||||
"""Temperature monitoring data"""
|
|
||||||
main: Optional[float] = None
|
|
||||||
brf: Optional[float] = None
|
|
||||||
shg: Optional[float] = None
|
|
||||||
thg: Optional[float] = None
|
|
||||||
eta: Optional[float] = None
|
|
||||||
|
|
||||||
|
|
||||||
class CoherentHOPSLaser:
|
|
||||||
"""
|
|
||||||
Main interface class for Coherent HOPS laser control via I2C.
|
|
||||||
|
|
||||||
This class provides high-level methods for all documented laser commands
|
|
||||||
including system queries, temperature control, power/current management,
|
|
||||||
and digital I/O operations.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Default I2C slave address for NXP microcontroller
|
|
||||||
DEFAULT_SLAVE_ADDRESS = 0x50
|
|
||||||
|
|
||||||
# FTDI USB VID/PID for FT2232C
|
|
||||||
FTDI_VID = 0x0403
|
|
||||||
FTDI_PID = 0x6010
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
slave_address: int = DEFAULT_SLAVE_ADDRESS,
|
|
||||||
i2c_mode: I2CMode = I2CMode.STANDARD,
|
|
||||||
timeout: float = 1.0
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Initialize the laser controller.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
slave_address: I2C slave address of the NXP microcontroller
|
|
||||||
i2c_mode: I2C communication speed mode
|
|
||||||
timeout: Command timeout in seconds
|
|
||||||
"""
|
|
||||||
self.slave_address = slave_address
|
|
||||||
self.i2c_mode = i2c_mode
|
|
||||||
self.timeout = timeout
|
|
||||||
|
|
||||||
self._i2c_controller = I2cController()
|
|
||||||
self._i2c_slave = None
|
|
||||||
self._connected = False
|
|
||||||
|
|
||||||
def connect(self, url: str = 'ftdi://ftdi:2232/1') -> None:
|
|
||||||
"""
|
|
||||||
Connect to the FTDI I2C device.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
url: FTDI device URL (default: first FT2232C device, channel 1)
|
|
||||||
Examples:
|
|
||||||
- 'ftdi://ftdi:2232/1' - First FT2232 device, channel 1
|
|
||||||
- 'ftdi://ftdi:2232:SERIAL/1' - Device with specific serial number
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
IOError: If connection fails
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Configure I2C controller
|
|
||||||
self._i2c_controller.configure(url, frequency=self.i2c_mode.value)
|
|
||||||
|
|
||||||
# Get I2C slave interface
|
|
||||||
self._i2c_slave = self._i2c_controller.get_port(self.slave_address)
|
|
||||||
|
|
||||||
self._connected = True
|
|
||||||
logger.info(f"Connected to laser at I2C address 0x{self.slave_address:02X}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to connect to I2C device: {e}")
|
|
||||||
raise IOError(f"I2C connection failed: {e}")
|
|
||||||
|
|
||||||
def disconnect(self) -> None:
|
|
||||||
"""Disconnect from the I2C device."""
|
|
||||||
if self._connected:
|
|
||||||
self._i2c_controller.terminate()
|
|
||||||
self._connected = False
|
|
||||||
logger.info("Disconnected from laser")
|
|
||||||
|
|
||||||
def _ensure_connected(self) -> None:
|
|
||||||
"""Verify device is connected before operations."""
|
|
||||||
if not self._connected:
|
|
||||||
raise RuntimeError("Not connected. Call connect() first.")
|
|
||||||
|
|
||||||
def _send_command(self, command: str, value: Optional[str] = None) -> str:
|
|
||||||
"""
|
|
||||||
Send a command to the laser and read response.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
command: Command string (e.g., 'HID', 'LASERMODEL')
|
|
||||||
value: Optional value for set commands
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Response string from the laser
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
I2cNackError: If I2C communication fails
|
|
||||||
TimeoutError: If response timeout occurs
|
|
||||||
"""
|
|
||||||
self._ensure_connected()
|
|
||||||
|
|
||||||
# Format command: query = ?COMMAND, set = COMMAND=VALUE
|
|
||||||
if value is not None:
|
|
||||||
cmd_str = f"{command}={value}"
|
|
||||||
else:
|
|
||||||
cmd_str = f"?{command}"
|
|
||||||
|
|
||||||
cmd_bytes = cmd_str.encode('ascii')
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Write command
|
|
||||||
self._i2c_slave.write(cmd_bytes)
|
|
||||||
|
|
||||||
# Small delay for laser to process
|
|
||||||
time.sleep(0.01)
|
|
||||||
|
|
||||||
# Read response (max 256 bytes)
|
|
||||||
response = self._i2c_slave.read(256)
|
|
||||||
|
|
||||||
# Decode and strip null bytes and whitespace
|
|
||||||
result = response.decode('ascii', errors='ignore').rstrip('\x00').strip()
|
|
||||||
|
|
||||||
logger.debug(f"Command: {cmd_str} -> Response: {result}")
|
|
||||||
return result
|
|
||||||
|
|
||||||
except I2cNackError as e:
|
|
||||||
logger.error(f"I2C NACK error for command {cmd_str}: {e}")
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Communication error for command {cmd_str}: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# System Information Commands
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
def get_hardware_id(self) -> str:
|
|
||||||
"""Query Hardware ID."""
|
|
||||||
return self._send_command('HID')
|
|
||||||
|
|
||||||
def get_head_type(self) -> str:
|
|
||||||
"""Query Head Type."""
|
|
||||||
return self._send_command('HTYPE')
|
|
||||||
|
|
||||||
def get_head_board_revision(self) -> str:
|
|
||||||
"""Query Head Board Revision."""
|
|
||||||
return self._send_command('HBDREV')
|
|
||||||
|
|
||||||
def get_head_digital_io(self) -> str:
|
|
||||||
"""Query Head Digital I/O configuration."""
|
|
||||||
return self._send_command('HEADDIO')
|
|
||||||
|
|
||||||
def get_laser_model(self) -> str:
|
|
||||||
"""
|
|
||||||
Query Laser Model.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Model name (e.g., 'G532', 'Tina', 'Mini00', 'MiniX')
|
|
||||||
"""
|
|
||||||
return self._send_command('LASERMODEL')
|
|
||||||
|
|
||||||
def get_power_units(self) -> str:
|
|
||||||
"""
|
|
||||||
Query Power Units.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Power units (e.g., 'mW', 'W')
|
|
||||||
"""
|
|
||||||
return self._send_command('POWERUNITS')
|
|
||||||
|
|
||||||
def get_wavelength(self) -> str:
|
|
||||||
"""
|
|
||||||
Query Wavelength.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Wavelength (e.g., '532nm')
|
|
||||||
"""
|
|
||||||
return self._send_command('WAVELENGTH')
|
|
||||||
|
|
||||||
def get_system_info(self) -> LaserInfo:
|
|
||||||
"""
|
|
||||||
Query all system information.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
LaserInfo dataclass with all system parameters
|
|
||||||
"""
|
|
||||||
return LaserInfo(
|
|
||||||
hardware_id=self.get_hardware_id(),
|
|
||||||
head_type=self.get_head_type(),
|
|
||||||
head_board_revision=self.get_head_board_revision(),
|
|
||||||
laser_model=self.get_laser_model(),
|
|
||||||
power_units=self.get_power_units(),
|
|
||||||
wavelength=self.get_wavelength()
|
|
||||||
)
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# Temperature Monitoring
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
def get_temperature_main(self) -> float:
|
|
||||||
"""
|
|
||||||
Get Main Heatsink Temperature.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Temperature in degrees Celsius
|
|
||||||
"""
|
|
||||||
response = self._send_command('TMAIN')
|
|
||||||
return float(response)
|
|
||||||
|
|
||||||
def get_temperature_brf(self) -> float:
|
|
||||||
"""
|
|
||||||
Get BRF (Birefringent Filter) Temperature.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Temperature in degrees Celsius
|
|
||||||
"""
|
|
||||||
response = self._send_command('TBRF')
|
|
||||||
return float(response)
|
|
||||||
|
|
||||||
def get_temperature_shg(self) -> float:
|
|
||||||
"""
|
|
||||||
Get SHG (Second Harmonic Generator) Temperature.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Temperature in degrees Celsius
|
|
||||||
"""
|
|
||||||
response = self._send_command('TSHG')
|
|
||||||
return float(response)
|
|
||||||
|
|
||||||
def get_temperature_thg(self) -> float:
|
|
||||||
"""
|
|
||||||
Get THG (Third Harmonic Generator) Temperature.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Temperature in degrees Celsius
|
|
||||||
"""
|
|
||||||
response = self._send_command('TTHG')
|
|
||||||
return float(response)
|
|
||||||
|
|
||||||
def get_temperature_eta(self) -> float:
|
|
||||||
"""
|
|
||||||
Get ETA Temperature.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Temperature in degrees Celsius
|
|
||||||
"""
|
|
||||||
response = self._send_command('TETA')
|
|
||||||
return float(response)
|
|
||||||
|
|
||||||
def get_all_temperatures(self) -> TemperatureStatus:
|
|
||||||
"""
|
|
||||||
Query all temperature sensors.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
TemperatureStatus dataclass with all temperature readings
|
|
||||||
"""
|
|
||||||
return TemperatureStatus(
|
|
||||||
main=self.get_temperature_main(),
|
|
||||||
brf=self.get_temperature_brf(),
|
|
||||||
shg=self.get_temperature_shg(),
|
|
||||||
thg=self.get_temperature_thg(),
|
|
||||||
eta=self.get_temperature_eta()
|
|
||||||
)
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# Temperature Control (Setpoints)
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
def get_temperature_setpoint_main(self) -> float:
|
|
||||||
"""Get Main Temperature Setpoint."""
|
|
||||||
response = self._send_command('TMAINCMD')
|
|
||||||
return float(response)
|
|
||||||
|
|
||||||
def set_temperature_setpoint_main(self, temperature: float) -> None:
|
|
||||||
"""Set Main Temperature Setpoint."""
|
|
||||||
self._send_command('TMAINCMD', str(temperature))
|
|
||||||
|
|
||||||
def get_temperature_setpoint_brf(self) -> float:
|
|
||||||
"""Get BRF Temperature Setpoint."""
|
|
||||||
response = self._send_command('TBRFCMD')
|
|
||||||
return float(response)
|
|
||||||
|
|
||||||
def set_temperature_setpoint_brf(self, temperature: float) -> None:
|
|
||||||
"""Set BRF Temperature Setpoint."""
|
|
||||||
self._send_command('TBRFCMD', str(temperature))
|
|
||||||
|
|
||||||
def get_temperature_setpoint_shg(self) -> float:
|
|
||||||
"""Get SHG Temperature Setpoint."""
|
|
||||||
response = self._send_command('TSHGCMD')
|
|
||||||
return float(response)
|
|
||||||
|
|
||||||
def set_temperature_setpoint_shg(self, temperature: float) -> None:
|
|
||||||
"""Set SHG Temperature Setpoint."""
|
|
||||||
self._send_command('TSHGCMD', str(temperature))
|
|
||||||
|
|
||||||
def get_temperature_setpoint_thg(self) -> float:
|
|
||||||
"""Get THG Temperature Setpoint."""
|
|
||||||
response = self._send_command('TTHGCMD')
|
|
||||||
return float(response)
|
|
||||||
|
|
||||||
def set_temperature_setpoint_thg(self, temperature: float) -> None:
|
|
||||||
"""Set THG Temperature Setpoint."""
|
|
||||||
self._send_command('TTHGCMD', str(temperature))
|
|
||||||
|
|
||||||
def get_temperature_setpoint_eta(self) -> float:
|
|
||||||
"""Get ETA Temperature Setpoint."""
|
|
||||||
response = self._send_command('TETACMD')
|
|
||||||
return float(response)
|
|
||||||
|
|
||||||
def set_temperature_setpoint_eta(self, temperature: float) -> None:
|
|
||||||
"""Set ETA Temperature Setpoint."""
|
|
||||||
self._send_command('TETACMD', str(temperature))
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# Temperature Data
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
def get_temperature_data_main(self) -> str:
|
|
||||||
"""Get Main Temperature Data."""
|
|
||||||
return self._send_command('MAIND')
|
|
||||||
|
|
||||||
def get_temperature_data_brf(self) -> str:
|
|
||||||
"""Get BRF Temperature Data."""
|
|
||||||
return self._send_command('BRFD')
|
|
||||||
|
|
||||||
def get_temperature_data_shg(self) -> str:
|
|
||||||
"""Get SHG Temperature Data."""
|
|
||||||
return self._send_command('SHGD')
|
|
||||||
|
|
||||||
def get_temperature_data_thg(self) -> str:
|
|
||||||
"""Get THG Temperature Data."""
|
|
||||||
return self._send_command('THGD')
|
|
||||||
|
|
||||||
def get_temperature_data_eta(self) -> str:
|
|
||||||
"""Get ETA Temperature Data."""
|
|
||||||
return self._send_command('ETAD')
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# Power Control
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
def get_power_command(self) -> float:
|
|
||||||
"""
|
|
||||||
Get Power Command value.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Power value in units from get_power_units()
|
|
||||||
"""
|
|
||||||
response = self._send_command('PCMD')
|
|
||||||
return float(response)
|
|
||||||
|
|
||||||
def set_power_command(self, power: float) -> None:
|
|
||||||
"""
|
|
||||||
Set Power Command value.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
power: Power value in units from get_power_units()
|
|
||||||
"""
|
|
||||||
self._send_command('PCMD', str(power))
|
|
||||||
|
|
||||||
def get_power_memory(self) -> str:
|
|
||||||
"""Query Power Memory (stored settings)."""
|
|
||||||
return self._send_command('PMEM')
|
|
||||||
|
|
||||||
def get_power_limits(self) -> str:
|
|
||||||
"""Query Power Limits."""
|
|
||||||
return self._send_command('PLIM')
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# Current Control
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
def get_current_command(self) -> float:
|
|
||||||
"""
|
|
||||||
Get Current Command value.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Current value in Amperes
|
|
||||||
"""
|
|
||||||
response = self._send_command('CCMD')
|
|
||||||
return float(response)
|
|
||||||
|
|
||||||
def set_current_command(self, current: float) -> None:
|
|
||||||
"""
|
|
||||||
Set Current Command value.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
current: Current value in Amperes
|
|
||||||
"""
|
|
||||||
self._send_command('CCMD', str(current))
|
|
||||||
|
|
||||||
def get_current_limits(self) -> str:
|
|
||||||
"""Query Current Limits."""
|
|
||||||
return self._send_command('CLIM')
|
|
||||||
|
|
||||||
def get_control_mode(self) -> str:
|
|
||||||
"""
|
|
||||||
Get Control Mode.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Control mode (e.g., 'POWER' or 'CURRENT')
|
|
||||||
"""
|
|
||||||
return self._send_command('CMODE')
|
|
||||||
|
|
||||||
def set_control_mode(self, mode: Union[str, ControlMode]) -> None:
|
|
||||||
"""
|
|
||||||
Set Control Mode.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
mode: Control mode ('POWER' or 'CURRENT', or ControlMode enum)
|
|
||||||
"""
|
|
||||||
if isinstance(mode, ControlMode):
|
|
||||||
mode = mode.value
|
|
||||||
self._send_command('CMODE', mode)
|
|
||||||
|
|
||||||
def get_control_mode_command(self) -> str:
|
|
||||||
"""Get Control Mode Command."""
|
|
||||||
return self._send_command('CMODECMD')
|
|
||||||
|
|
||||||
def set_control_mode_command(self, mode: str) -> None:
|
|
||||||
"""Set Control Mode Command."""
|
|
||||||
self._send_command('CMODECMD', mode)
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# Digital I/O
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
def get_ps_digital_io(self) -> str:
|
|
||||||
"""Get Power Supply Digital I/O status."""
|
|
||||||
return self._send_command('PSDIO')
|
|
||||||
|
|
||||||
def get_ps_glue_input(self) -> str:
|
|
||||||
"""Get Power Supply Glue Logic Input status."""
|
|
||||||
return self._send_command('PSGLUEIN')
|
|
||||||
|
|
||||||
def get_ps_glue_output(self) -> str:
|
|
||||||
"""Get Power Supply Glue Logic Output status."""
|
|
||||||
return self._send_command('PSGLUEOUT')
|
|
||||||
|
|
||||||
def set_ps_glue_output(self, value: str) -> None:
|
|
||||||
"""Set Power Supply Glue Logic Output."""
|
|
||||||
self._send_command('PSGLUEOUT', value)
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# Monitoring & Status
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
def get_analog_values(self) -> str:
|
|
||||||
"""Query Analog Values."""
|
|
||||||
return self._send_command('ANA')
|
|
||||||
|
|
||||||
def get_analog_command(self) -> str:
|
|
||||||
"""Get Analog Command."""
|
|
||||||
return self._send_command('ANACMD')
|
|
||||||
|
|
||||||
def set_analog_command(self, value: str) -> None:
|
|
||||||
"""Set Analog Command."""
|
|
||||||
self._send_command('ANACMD', value)
|
|
||||||
|
|
||||||
def get_key_switch_status(self) -> str:
|
|
||||||
"""Get Key Switch Status."""
|
|
||||||
return self._send_command('KSW')
|
|
||||||
|
|
||||||
def get_key_switch_command(self) -> str:
|
|
||||||
"""Get Key Switch Command."""
|
|
||||||
return self._send_command('KSWCMD')
|
|
||||||
|
|
||||||
def set_key_switch_command(self, value: str) -> None:
|
|
||||||
"""Set Key Switch Command."""
|
|
||||||
self._send_command('KSWCMD', value)
|
|
||||||
|
|
||||||
def get_fan_status(self) -> str:
|
|
||||||
"""Get Fan Status/Control."""
|
|
||||||
return self._send_command('FAN')
|
|
||||||
|
|
||||||
def set_fan_control(self, value: str) -> None:
|
|
||||||
"""Set Fan Control."""
|
|
||||||
self._send_command('FAN', value)
|
|
||||||
|
|
||||||
def get_interlock_status(self) -> str:
|
|
||||||
"""Get Interlock Status."""
|
|
||||||
return self._send_command('INT')
|
|
||||||
|
|
||||||
def get_remote_control_status(self) -> str:
|
|
||||||
"""Get Remote Control Status."""
|
|
||||||
return self._send_command('REM')
|
|
||||||
|
|
||||||
def get_eeprom_header(self) -> str:
|
|
||||||
"""Get EEPROM Header."""
|
|
||||||
return self._send_command('EEH')
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# Configuration Registers
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
def get_config_register_0(self) -> str:
|
|
||||||
"""Get Configuration Register 0."""
|
|
||||||
return self._send_command('CFG0')
|
|
||||||
|
|
||||||
def set_config_register_0(self, value: str) -> None:
|
|
||||||
"""Set Configuration Register 0."""
|
|
||||||
self._send_command('CFG0', value)
|
|
||||||
|
|
||||||
def get_config_register_1(self) -> str:
|
|
||||||
"""Get Configuration Register 1."""
|
|
||||||
return self._send_command('CFG1')
|
|
||||||
|
|
||||||
def set_config_register_1(self, value: str) -> None:
|
|
||||||
"""Set Configuration Register 1."""
|
|
||||||
self._send_command('CFG1', value)
|
|
||||||
|
|
||||||
def get_config_register_2(self) -> str:
|
|
||||||
"""Get Configuration Register 2."""
|
|
||||||
return self._send_command('CFG2')
|
|
||||||
|
|
||||||
def set_config_register_2(self, value: str) -> None:
|
|
||||||
"""Set Configuration Register 2."""
|
|
||||||
self._send_command('CFG2', value)
|
|
||||||
|
|
||||||
def get_config_register_3(self) -> str:
|
|
||||||
"""Get Configuration Register 3."""
|
|
||||||
return self._send_command('CFG3')
|
|
||||||
|
|
||||||
def set_config_register_3(self, value: str) -> None:
|
|
||||||
"""Set Configuration Register 3."""
|
|
||||||
self._send_command('CFG3', value)
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# Context Manager Support
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
"""Context manager entry."""
|
|
||||||
if not self._connected:
|
|
||||||
self.connect()
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
||||||
"""Context manager exit."""
|
|
||||||
self.disconnect()
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class DummyLaser(CoherentHOPSLaser):
|
|
||||||
"""
|
|
||||||
Simulated laser for testing without hardware.
|
|
||||||
|
|
||||||
This class provides dummy responses for all commands to enable
|
|
||||||
software development and testing without physical hardware.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Initialize dummy laser (no I2C connection needed)."""
|
|
||||||
super().__init__()
|
|
||||||
self._connected = True # Simulate connection
|
|
||||||
|
|
||||||
# Simulated state
|
|
||||||
self._power_cmd = 50.0
|
|
||||||
self._current_cmd = 1.5
|
|
||||||
self._control_mode = 'POWER'
|
|
||||||
self._temps = {
|
|
||||||
'main': 25.0,
|
|
||||||
'brf': 30.0,
|
|
||||||
'shg': 35.0,
|
|
||||||
'thg': 32.0,
|
|
||||||
'eta': 28.0
|
|
||||||
}
|
|
||||||
self._temp_setpoints = {
|
|
||||||
'main': 25.0,
|
|
||||||
'brf': 30.0,
|
|
||||||
'shg': 35.0,
|
|
||||||
'thg': 32.0,
|
|
||||||
'eta': 28.0
|
|
||||||
}
|
|
||||||
|
|
||||||
def connect(self, url: str = 'dummy') -> None:
|
|
||||||
"""Dummy connection (always succeeds)."""
|
|
||||||
self._connected = True
|
|
||||||
logger.info("Connected to DummyLaser (simulation mode)")
|
|
||||||
|
|
||||||
def _send_command(self, command: str, value: Optional[str] = None) -> str:
|
|
||||||
"""Simulate command responses."""
|
|
||||||
logger.debug(f"DummyLaser command: {command}, value: {value}")
|
|
||||||
|
|
||||||
# Handle set commands
|
|
||||||
if value is not None:
|
|
||||||
if command == 'PCMD':
|
|
||||||
self._power_cmd = float(value)
|
|
||||||
return value
|
|
||||||
elif command == 'CCMD':
|
|
||||||
self._current_cmd = float(value)
|
|
||||||
return value
|
|
||||||
elif command == 'CMODE':
|
|
||||||
self._control_mode = value
|
|
||||||
return value
|
|
||||||
elif 'CMD' in command and 'T' in command:
|
|
||||||
# Temperature setpoint
|
|
||||||
key = command.replace('CMD', '').replace('T', '').lower()
|
|
||||||
if key in self._temp_setpoints:
|
|
||||||
self._temp_setpoints[key] = float(value)
|
|
||||||
return value
|
|
||||||
return 'OK'
|
|
||||||
|
|
||||||
# Handle query commands
|
|
||||||
responses = {
|
|
||||||
'HID': 'HOPS-12345',
|
|
||||||
'HTYPE': 'Standard',
|
|
||||||
'HBDREV': 'Rev 2.1',
|
|
||||||
'HEADDIO': '0xFF',
|
|
||||||
'LASERMODEL': 'G532',
|
|
||||||
'POWERUNITS': 'mW',
|
|
||||||
'WAVELENGTH': '532nm',
|
|
||||||
'TMAIN': str(self._temps['main']),
|
|
||||||
'TBRF': str(self._temps['brf']),
|
|
||||||
'TSHG': str(self._temps['shg']),
|
|
||||||
'TTHG': str(self._temps['thg']),
|
|
||||||
'TETA': str(self._temps['eta']),
|
|
||||||
'TMAINCMD': str(self._temp_setpoints['main']),
|
|
||||||
'TBRFCMD': str(self._temp_setpoints['brf']),
|
|
||||||
'TSHGCMD': str(self._temp_setpoints['shg']),
|
|
||||||
'TTHGCMD': str(self._temp_setpoints['thg']),
|
|
||||||
'TETACMD': str(self._temp_setpoints['eta']),
|
|
||||||
'MAIND': 'MainTempData',
|
|
||||||
'BRFD': 'BRFTempData',
|
|
||||||
'SHGD': 'SHGTempData',
|
|
||||||
'THGD': 'THGTempData',
|
|
||||||
'ETAD': 'ETATempData',
|
|
||||||
'PCMD': str(self._power_cmd),
|
|
||||||
'PMEM': '100',
|
|
||||||
'PLIM': '0-200',
|
|
||||||
'CCMD': str(self._current_cmd),
|
|
||||||
'CLIM': '0-5',
|
|
||||||
'CMODE': self._control_mode,
|
|
||||||
'CMODECMD': self._control_mode,
|
|
||||||
'PSDIO': '0x00',
|
|
||||||
'PSGLUEIN': '0x00',
|
|
||||||
'PSGLUEOUT': '0x00',
|
|
||||||
'ANA': '0,0,0,0',
|
|
||||||
'ANACMD': '0',
|
|
||||||
'KSW': 'ON',
|
|
||||||
'KSWCMD': 'ON',
|
|
||||||
'FAN': 'AUTO',
|
|
||||||
'INT': 'OK',
|
|
||||||
'REM': 'ENABLED',
|
|
||||||
'EEH': 'EEPROM_V1',
|
|
||||||
'CFG0': '0x00',
|
|
||||||
'CFG1': '0x00',
|
|
||||||
'CFG2': '0x00',
|
|
||||||
'CFG3': '0x00',
|
|
||||||
}
|
|
||||||
|
|
||||||
return responses.get(command, 'UNKNOWN')
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
"""Example usage and testing."""
|
|
||||||
|
|
||||||
# Test with dummy laser
|
|
||||||
print("=== Testing with DummyLaser ===\n")
|
|
||||||
|
|
||||||
with DummyLaser() as laser:
|
|
||||||
# System info
|
|
||||||
print("System Information:")
|
|
||||||
info = laser.get_system_info()
|
|
||||||
print(f" Model: {info.laser_model}")
|
|
||||||
print(f" Wavelength: {info.wavelength}")
|
|
||||||
print(f" Power Units: {info.power_units}")
|
|
||||||
print(f" Hardware ID: {info.hardware_id}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Temperature monitoring
|
|
||||||
print("Temperature Status:")
|
|
||||||
temps = laser.get_all_temperatures()
|
|
||||||
print(f" Main: {temps.main}°C")
|
|
||||||
print(f" BRF: {temps.brf}°C")
|
|
||||||
print(f" SHG: {temps.shg}°C")
|
|
||||||
print(f" THG: {temps.thg}°C")
|
|
||||||
print(f" ETA: {temps.eta}°C")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Power control
|
|
||||||
print("Power Control:")
|
|
||||||
current_power = laser.get_power_command()
|
|
||||||
print(f" Current Power: {current_power} {info.power_units}")
|
|
||||||
laser.set_power_command(75.0)
|
|
||||||
new_power = laser.get_power_command()
|
|
||||||
print(f" New Power: {new_power} {info.power_units}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Control mode
|
|
||||||
print("Control Mode:")
|
|
||||||
mode = laser.get_control_mode()
|
|
||||||
print(f" Current Mode: {mode}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
print("\n=== For real hardware, use: ===")
|
|
||||||
print("laser = CoherentHOPSLaser()")
|
|
||||||
print("laser.connect('ftdi://ftdi:2232/1')")
|
|
||||||
print("# ... perform operations ...")
|
|
||||||
print("laser.disconnect()")
|
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Example usage of the Coherent HOPS Laser control library.
|
|
||||||
|
|
||||||
This script demonstrates how to use the library to control a real laser system.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from coherent_hops_laser import CoherentHOPSLaser, DummyLaser, I2CMode
|
|
||||||
import time
|
|
||||||
|
|
||||||
|
|
||||||
def demo_system_info(laser):
|
|
||||||
"""Demonstrate system information queries."""
|
|
||||||
print("=" * 60)
|
|
||||||
print("SYSTEM INFORMATION")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
info = laser.get_system_info()
|
|
||||||
print(f"Hardware ID: {info.hardware_id}")
|
|
||||||
print(f"Laser Model: {info.laser_model}")
|
|
||||||
print(f"Wavelength: {info.wavelength}")
|
|
||||||
print(f"Power Units: {info.power_units}")
|
|
||||||
print(f"Head Type: {info.head_type}")
|
|
||||||
print(f"Board Revision: {info.head_board_revision}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
def demo_temperature_monitoring(laser):
|
|
||||||
"""Demonstrate temperature monitoring."""
|
|
||||||
print("=" * 60)
|
|
||||||
print("TEMPERATURE MONITORING")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
temps = laser.get_all_temperatures()
|
|
||||||
print(f"Main Heatsink: {temps.main:.2f}°C")
|
|
||||||
print(f"BRF (Birefringent): {temps.brf:.2f}°C")
|
|
||||||
print(f"SHG (2nd Harmonic): {temps.shg:.2f}°C")
|
|
||||||
print(f"THG (3rd Harmonic): {temps.thg:.2f}°C")
|
|
||||||
print(f"ETA: {temps.eta:.2f}°C")
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
def demo_temperature_control(laser):
|
|
||||||
"""Demonstrate temperature setpoint control."""
|
|
||||||
print("=" * 60)
|
|
||||||
print("TEMPERATURE CONTROL")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
# Read current setpoints
|
|
||||||
print("Current Setpoints:")
|
|
||||||
print(f" Main: {laser.get_temperature_setpoint_main():.2f}°C")
|
|
||||||
print(f" BRF: {laser.get_temperature_setpoint_brf():.2f}°C")
|
|
||||||
print(f" SHG: {laser.get_temperature_setpoint_shg():.2f}°C")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Example: Set a new setpoint (commented out for safety)
|
|
||||||
# print("Setting Main temperature setpoint to 26.0°C...")
|
|
||||||
# laser.set_temperature_setpoint_main(26.0)
|
|
||||||
# print(f" New setpoint: {laser.get_temperature_setpoint_main():.2f}°C")
|
|
||||||
print("(Temperature setpoint modification disabled in demo)")
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
def demo_power_control(laser):
|
|
||||||
"""Demonstrate power control."""
|
|
||||||
print("=" * 60)
|
|
||||||
print("POWER CONTROL")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
units = laser.get_power_units()
|
|
||||||
current_power = laser.get_power_command()
|
|
||||||
print(f"Current Power: {current_power} {units}")
|
|
||||||
|
|
||||||
power_limits = laser.get_power_limits()
|
|
||||||
print(f"Power Limits: {power_limits}")
|
|
||||||
|
|
||||||
power_memory = laser.get_power_memory()
|
|
||||||
print(f"Power Memory: {power_memory}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Example: Set power (commented out for safety)
|
|
||||||
# print("Setting power to 100.0 mW...")
|
|
||||||
# laser.set_power_command(100.0)
|
|
||||||
# print(f" New power: {laser.get_power_command()} {units}")
|
|
||||||
print("(Power modification disabled in demo)")
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
def demo_current_control(laser):
|
|
||||||
"""Demonstrate current control."""
|
|
||||||
print("=" * 60)
|
|
||||||
print("CURRENT CONTROL")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
current = laser.get_current_command()
|
|
||||||
print(f"Current Command: {current} A")
|
|
||||||
|
|
||||||
limits = laser.get_current_limits()
|
|
||||||
print(f"Current Limits: {limits}")
|
|
||||||
|
|
||||||
mode = laser.get_control_mode()
|
|
||||||
print(f"Control Mode: {mode}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
def demo_status_monitoring(laser):
|
|
||||||
"""Demonstrate status monitoring."""
|
|
||||||
print("=" * 60)
|
|
||||||
print("STATUS MONITORING")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
print(f"Key Switch: {laser.get_key_switch_status()}")
|
|
||||||
print(f"Fan Status: {laser.get_fan_status()}")
|
|
||||||
print(f"Interlock: {laser.get_interlock_status()}")
|
|
||||||
print(f"Remote Control: {laser.get_remote_control_status()}")
|
|
||||||
print(f"Analog Values: {laser.get_analog_values()}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
def demo_configuration(laser):
|
|
||||||
"""Demonstrate configuration register access."""
|
|
||||||
print("=" * 60)
|
|
||||||
print("CONFIGURATION REGISTERS")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
print(f"Config Register 0: {laser.get_config_register_0()}")
|
|
||||||
print(f"Config Register 1: {laser.get_config_register_1()}")
|
|
||||||
print(f"Config Register 2: {laser.get_config_register_2()}")
|
|
||||||
print(f"Config Register 3: {laser.get_config_register_3()}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
def main_dummy_demo():
|
|
||||||
"""Run demo with simulated hardware."""
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
print("COHERENT HOPS LASER CONTROL - SIMULATION MODE")
|
|
||||||
print("=" * 60 + "\n")
|
|
||||||
|
|
||||||
with DummyLaser() as laser:
|
|
||||||
demo_system_info(laser)
|
|
||||||
demo_temperature_monitoring(laser)
|
|
||||||
demo_temperature_control(laser)
|
|
||||||
demo_power_control(laser)
|
|
||||||
demo_current_control(laser)
|
|
||||||
demo_status_monitoring(laser)
|
|
||||||
demo_configuration(laser)
|
|
||||||
|
|
||||||
# Demonstrate power control
|
|
||||||
print("=" * 60)
|
|
||||||
print("POWER CONTROL DEMONSTRATION (Simulation)")
|
|
||||||
print("=" * 60)
|
|
||||||
print(f"Initial power: {laser.get_power_command()} mW")
|
|
||||||
laser.set_power_command(125.0)
|
|
||||||
print(f"After setting to 125.0 mW: {laser.get_power_command()} mW")
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
def main_real_hardware():
|
|
||||||
"""Run demo with real hardware."""
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
print("COHERENT HOPS LASER CONTROL - REAL HARDWARE")
|
|
||||||
print("=" * 60 + "\n")
|
|
||||||
|
|
||||||
# Configure for your specific setup
|
|
||||||
FTDI_URL = 'ftdi://ftdi:2232/1' # Adjust if needed
|
|
||||||
SLAVE_ADDRESS = 0x50 # Default NXP slave address
|
|
||||||
I2C_FREQUENCY = I2CMode.STANDARD # or I2CMode.FAST
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Connect to laser
|
|
||||||
laser = CoherentHOPSLaser(
|
|
||||||
slave_address=SLAVE_ADDRESS,
|
|
||||||
i2c_mode=I2C_FREQUENCY
|
|
||||||
)
|
|
||||||
|
|
||||||
print(f"Connecting to FTDI device at {FTDI_URL}...")
|
|
||||||
laser.connect(FTDI_URL)
|
|
||||||
print("Connected successfully!\n")
|
|
||||||
|
|
||||||
# Run demos
|
|
||||||
demo_system_info(laser)
|
|
||||||
demo_temperature_monitoring(laser)
|
|
||||||
demo_status_monitoring(laser)
|
|
||||||
demo_power_control(laser)
|
|
||||||
demo_current_control(laser)
|
|
||||||
|
|
||||||
# Clean disconnect
|
|
||||||
laser.disconnect()
|
|
||||||
print("Disconnected successfully.")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error: {e}")
|
|
||||||
print("\nTroubleshooting:")
|
|
||||||
print("1. Check FTDI device is connected (lsusb | grep FTDI)")
|
|
||||||
print("2. Verify user permissions (add user to 'dialout' or 'plugdev' group)")
|
|
||||||
print("3. Check FTDI URL matches your device")
|
|
||||||
print("4. Try: sudo python3 example_usage.py (not recommended long-term)")
|
|
||||||
|
|
||||||
|
|
||||||
def continuous_monitoring_example():
|
|
||||||
"""Example of continuous temperature and power monitoring."""
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
print("CONTINUOUS MONITORING EXAMPLE")
|
|
||||||
print("=" * 60 + "\n")
|
|
||||||
|
|
||||||
with DummyLaser() as laser:
|
|
||||||
print("Monitoring laser parameters (5 iterations)...")
|
|
||||||
print("Press Ctrl+C to stop\n")
|
|
||||||
|
|
||||||
try:
|
|
||||||
for i in range(5):
|
|
||||||
temps = laser.get_all_temperatures()
|
|
||||||
power = laser.get_power_command()
|
|
||||||
mode = laser.get_control_mode()
|
|
||||||
|
|
||||||
print(f"[{i+1}] T_main={temps.main:.1f}°C "
|
|
||||||
f"T_shg={temps.shg:.1f}°C "
|
|
||||||
f"Power={power:.1f}mW "
|
|
||||||
f"Mode={mode}")
|
|
||||||
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
print("\nMonitoring stopped.")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
import sys
|
|
||||||
|
|
||||||
print("Coherent HOPS Laser Control - Example Usage\n")
|
|
||||||
print("Available demos:")
|
|
||||||
print(" 1. Simulation mode (no hardware required)")
|
|
||||||
print(" 2. Real hardware mode")
|
|
||||||
print(" 3. Continuous monitoring example")
|
|
||||||
print()
|
|
||||||
|
|
||||||
if len(sys.argv) > 1:
|
|
||||||
choice = sys.argv[1]
|
|
||||||
else:
|
|
||||||
choice = input("Select demo (1/2/3) [default: 1]: ").strip() or "1"
|
|
||||||
|
|
||||||
if choice == "1":
|
|
||||||
main_dummy_demo()
|
|
||||||
print("\nContinuous monitoring demo:")
|
|
||||||
continuous_monitoring_example()
|
|
||||||
elif choice == "2":
|
|
||||||
main_real_hardware()
|
|
||||||
elif choice == "3":
|
|
||||||
continuous_monitoring_example()
|
|
||||||
else:
|
|
||||||
print("Invalid choice. Use 1, 2, or 3.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
print("Demo complete!")
|
|
||||||
print("=" * 60)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
pyftdi>=0.54.0
|
|
||||||
+4
-14
@@ -1,19 +1,9 @@
|
|||||||
# scanengine-3 - Unified Requirements
|
|
||||||
# Combined dependencies from nuescan, pymso, pybbd202, and pypewpewhops
|
|
||||||
|
|
||||||
# GUI Framework (from nuescan)
|
|
||||||
PyQt6>=6.4.0
|
PyQt6>=6.4.0
|
||||||
|
|
||||||
# Serial Communication (from nuescan)
|
|
||||||
pyserial>=3.5
|
pyserial>=3.5
|
||||||
|
|
||||||
# VISA instrument control (from nuescan - for oscilloscope/pymso)
|
|
||||||
pyvisa>=1.13.0
|
pyvisa>=1.13.0
|
||||||
pyvisa-py>=0.7.0
|
pyvisa-py>=0.7.0
|
||||||
|
|
||||||
# FTDI device support (from pypewpewhops and pybbd202)
|
|
||||||
pyftdi>=0.54.0
|
pyftdi>=0.54.0
|
||||||
|
pytest>=7.0.0
|
||||||
# Development dependencies (optional)
|
pytest-qt>=4.0.0
|
||||||
# pytest>=7.0.0
|
pyueye>=4.95.0
|
||||||
# pytest-qt>=4.0.0
|
numpy>=1.20.0
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
"""ScanEngine-3 main application package"""
|
||||||
|
__version__ = "3.0.0"
|
||||||
+3432
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,270 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>JogStageDialog</class>
|
||||||
|
<widget class="QDialog" name="JogStageDialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>400</width>
|
||||||
|
<height>400</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Jog Stage</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_title">
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<pointsize>16</pointsize>
|
||||||
|
<weight>75</weight>
|
||||||
|
<bold>true</bold>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Stage Jogging Control</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_position">
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<pointsize>12</pointsize>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Position: X=0.00mm, Y=0.00mm</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="verticalSpacer_top">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Orientation::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QPushButton" name="btn_jog_y_plus">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>80</width>
|
||||||
|
<height>60</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<pointsize>14</pointsize>
|
||||||
|
<weight>75</weight>
|
||||||
|
<bold>true</bold>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>+Y</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QPushButton" name="btn_jog_x_plus">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>80</width>
|
||||||
|
<height>60</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<pointsize>14</pointsize>
|
||||||
|
<weight>75</weight>
|
||||||
|
<bold>true</bold>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>+X</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_center">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_speed">
|
||||||
|
<property name="text">
|
||||||
|
<string>Jog Speed (mm/s):</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="le_jog_speed">
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>100</width>
|
||||||
|
<height>16777215</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>20.0</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_step">
|
||||||
|
<property name="text">
|
||||||
|
<string>Step Size (mm):</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="le_step_size">
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>100</width>
|
||||||
|
<height>16777215</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>1.0</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="2">
|
||||||
|
<widget class="QPushButton" name="btn_jog_x_minus">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>80</width>
|
||||||
|
<height>60</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<pointsize>14</pointsize>
|
||||||
|
<weight>75</weight>
|
||||||
|
<bold>true</bold>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>-X</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<widget class="QPushButton" name="btn_jog_y_minus">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>80</width>
|
||||||
|
<height>60</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<pointsize>14</pointsize>
|
||||||
|
<weight>75</weight>
|
||||||
|
<bold>true</bold>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>-Y</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="verticalSpacer_bottom">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Orientation::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_status">
|
||||||
|
<property name="text">
|
||||||
|
<string>Status: Disconnected</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_buttons">
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="btn_connect">
|
||||||
|
<property name="text">
|
||||||
|
<string>Connect</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="btn_home">
|
||||||
|
<property name="text">
|
||||||
|
<string>Home All Axes</string>
|
||||||
|
</property>
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Orientation::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="btn_close">
|
||||||
|
<property name="text">
|
||||||
|
<string>Close</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>MainWindow</class>
|
||||||
|
<widget class="QMainWindow" name="MainWindow">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>653</width>
|
||||||
|
<height>360</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<family>Bahnschrift</family>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Scanengin3 | v3.1.0 | build: nottagain</string>
|
||||||
|
</property>
|
||||||
|
<widget class="QWidget" name="centralwidget">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
|
<item>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<family>Bahnschrift</family>
|
||||||
|
<pointsize>26</pointsize>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Scanengine Task Launcher:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="verticalSpacer_2">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Orientation::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_2">
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<family>Bahnschrift</family>
|
||||||
|
<pointsize>14</pointsize>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Choose a workflow:</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="pb_start_new_scan">
|
||||||
|
<property name="text">
|
||||||
|
<string>Begin a New Scan</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="pb_continue_scan">
|
||||||
|
<property name="text">
|
||||||
|
<string>Continue an Existing Scan</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="pb_open_options">
|
||||||
|
<property name="text">
|
||||||
|
<string>Configure System / Set Default Values</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="verticalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Orientation::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<widget class="QStatusBar" name="statusbar"/>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
||||||
@@ -0,0 +1,426 @@
|
|||||||
|
"""
|
||||||
|
Motion Controller Worker Thread
|
||||||
|
|
||||||
|
Handles all motion control operations in a separate thread to keep the UI responsive.
|
||||||
|
Provides async command queueing and position updates via Qt signals.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from PyQt6 import QtCore
|
||||||
|
from hardware.bbd202 import MotionController
|
||||||
|
import queue
|
||||||
|
import time
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
|
||||||
|
|
||||||
|
class MotionCommand:
|
||||||
|
"""Represents a motion command"""
|
||||||
|
def __init__(self, cmd_type: str, **kwargs):
|
||||||
|
self.cmd_type = cmd_type
|
||||||
|
self.params = kwargs
|
||||||
|
|
||||||
|
|
||||||
|
class MotionWorker(QtCore.QObject):
|
||||||
|
"""
|
||||||
|
Worker object for handling motion control in a separate thread.
|
||||||
|
|
||||||
|
Signals:
|
||||||
|
connected: Emitted when controller connects successfully
|
||||||
|
disconnected: Emitted when controller disconnects
|
||||||
|
connection_failed: Emitted when connection fails (error_msg: str)
|
||||||
|
position_updated: Emitted when position changes (x: float, y: float)
|
||||||
|
homed_status: Emitted with home status (x_homed: bool, y_homed: bool)
|
||||||
|
move_completed: Emitted when a move completes (axis: str)
|
||||||
|
error_occurred: Emitted when an error occurs (error_msg: str)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Signals
|
||||||
|
connected = QtCore.pyqtSignal()
|
||||||
|
disconnected = QtCore.pyqtSignal()
|
||||||
|
connection_failed = QtCore.pyqtSignal(str)
|
||||||
|
position_updated = QtCore.pyqtSignal(float, float) # x, y in mm
|
||||||
|
homed_status = QtCore.pyqtSignal(bool, bool) # x_homed, y_homed
|
||||||
|
motion_status = QtCore.pyqtSignal(bool, bool) # x_moving, y_moving
|
||||||
|
move_completed = QtCore.pyqtSignal(str) # axis name
|
||||||
|
error_occurred = QtCore.pyqtSignal(str) # error message
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.controller: Optional[MotionController] = None
|
||||||
|
self.is_connected = False
|
||||||
|
self.command_queue = queue.Queue()
|
||||||
|
self.running = True
|
||||||
|
|
||||||
|
# Default parameters
|
||||||
|
self.jog_speed = 20.0 # mm/s
|
||||||
|
self.acceleration = 50.0 # mm/s^2
|
||||||
|
self.step_size = 1.0 # mm
|
||||||
|
|
||||||
|
# Position tracking
|
||||||
|
self.last_x = None
|
||||||
|
self.last_y = None
|
||||||
|
|
||||||
|
# Status tracking
|
||||||
|
self.last_x_homed = None
|
||||||
|
self.last_y_homed = None
|
||||||
|
self.last_x_moving = None
|
||||||
|
self.last_y_moving = None
|
||||||
|
|
||||||
|
# Position update throttling (active requests can be slow)
|
||||||
|
self.last_position_update_time = 0
|
||||||
|
self.position_update_interval = 0.2 # seconds between position requests
|
||||||
|
|
||||||
|
# Flag to pause polling during scanning (scan worker handles its own position queries)
|
||||||
|
self.scanning_active = False
|
||||||
|
|
||||||
|
@QtCore.pyqtSlot()
|
||||||
|
def run(self):
|
||||||
|
"""Main worker loop - processes commands from queue"""
|
||||||
|
print("Motion worker thread started")
|
||||||
|
|
||||||
|
while self.running:
|
||||||
|
try:
|
||||||
|
# Check for commands with timeout to allow periodic position updates
|
||||||
|
try:
|
||||||
|
cmd = self.command_queue.get(timeout=0.05) # 50ms timeout
|
||||||
|
self.process_command(cmd)
|
||||||
|
except queue.Empty:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Periodically update position and status if connected
|
||||||
|
# Skip updates during scanning - scan worker handles its own position queries
|
||||||
|
if self.is_connected and self.controller and not self.scanning_active:
|
||||||
|
self.update_position()
|
||||||
|
self.update_home_status()
|
||||||
|
self.update_motion_status()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in motion worker loop: {e}")
|
||||||
|
self.error_occurred.emit(str(e))
|
||||||
|
|
||||||
|
# Cleanup on exit
|
||||||
|
if self.controller:
|
||||||
|
try:
|
||||||
|
self.controller.disconnect()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print("Motion worker thread stopped")
|
||||||
|
|
||||||
|
def process_command(self, cmd: MotionCommand):
|
||||||
|
"""Process a motion command"""
|
||||||
|
try:
|
||||||
|
if cmd.cmd_type == 'connect':
|
||||||
|
self.do_connect()
|
||||||
|
elif cmd.cmd_type == 'disconnect':
|
||||||
|
self.do_disconnect()
|
||||||
|
elif cmd.cmd_type == 'jog':
|
||||||
|
self.do_jog(cmd.params['axis'], cmd.params['direction'])
|
||||||
|
elif cmd.cmd_type == 'home':
|
||||||
|
self.do_home(cmd.params['axis'])
|
||||||
|
elif cmd.cmd_type == 'set_velocity':
|
||||||
|
self.do_set_velocity(cmd.params['speed'], cmd.params['accel'])
|
||||||
|
elif cmd.cmd_type == 'set_step_size':
|
||||||
|
self.step_size = cmd.params['step_size']
|
||||||
|
elif cmd.cmd_type == 'set_axis_enable':
|
||||||
|
self.do_set_axis_enable(cmd.params['axis'], cmd.params['enabled'])
|
||||||
|
elif cmd.cmd_type == 'stop':
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing command {cmd.cmd_type}: {e}")
|
||||||
|
self.error_occurred.emit(f"Command '{cmd.cmd_type}' failed: {str(e)}")
|
||||||
|
|
||||||
|
def do_connect(self):
|
||||||
|
"""Connect to the motion controller"""
|
||||||
|
try:
|
||||||
|
self.controller = MotionController()
|
||||||
|
self.controller.connect() # ACKs are sent reactively when enable_updates=True
|
||||||
|
|
||||||
|
# Enable channels
|
||||||
|
self.controller.set_channel_enable_state(self.controller.DEST_X_AXIS, True)
|
||||||
|
self.controller.set_channel_enable_state(self.controller.DEST_Y_AXIS, True)
|
||||||
|
|
||||||
|
# Request status updates for both axes to populate status bits (including homed state)
|
||||||
|
# This is necessary because status bits are not sent automatically after connect
|
||||||
|
self.controller.request_status_update(self.controller.DEST_X_AXIS)
|
||||||
|
self.controller.request_status_update(self.controller.DEST_Y_AXIS)
|
||||||
|
# Wait a moment for the asynchronous status update responses
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
# Set initial velocity parameters
|
||||||
|
for dest in [self.controller.DEST_X_AXIS, self.controller.DEST_Y_AXIS]:
|
||||||
|
self.controller.set_velocity_params(
|
||||||
|
dest,
|
||||||
|
min_velocity=0.0,
|
||||||
|
acceleration=self.acceleration,
|
||||||
|
max_velocity=self.jog_speed
|
||||||
|
)
|
||||||
|
|
||||||
|
self.is_connected = True
|
||||||
|
# Force initial updates (they will be emitted because last values are None)
|
||||||
|
self.update_position()
|
||||||
|
self.update_home_status()
|
||||||
|
self.update_motion_status()
|
||||||
|
self.connected.emit()
|
||||||
|
|
||||||
|
print("Motion controller connected successfully")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to connect to motion controller: {e}")
|
||||||
|
self.connection_failed.emit(str(e))
|
||||||
|
|
||||||
|
def do_disconnect(self):
|
||||||
|
"""Disconnect from the motion controller"""
|
||||||
|
if self.controller:
|
||||||
|
try:
|
||||||
|
self.controller.disconnect()
|
||||||
|
print("Motion controller disconnected")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error during disconnect: {e}")
|
||||||
|
|
||||||
|
self.controller = None
|
||||||
|
self.is_connected = False
|
||||||
|
self.disconnected.emit()
|
||||||
|
|
||||||
|
def do_jog(self, axis: str, direction: int):
|
||||||
|
"""Execute a jog move"""
|
||||||
|
if not self.is_connected or not self.controller:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Determine destination
|
||||||
|
dest = self.controller.DEST_X_AXIS if axis == 'x' else self.controller.DEST_Y_AXIS
|
||||||
|
|
||||||
|
# Calculate relative distance
|
||||||
|
distance = self.step_size * direction
|
||||||
|
|
||||||
|
# Set relative move parameters
|
||||||
|
self.controller.set_move_rel_params(dest, distance)
|
||||||
|
|
||||||
|
# Execute the move (non-blocking - we don't wait for completion)
|
||||||
|
# Use a very short timeout since we're doing continuous jogging
|
||||||
|
self.controller.move_relative(dest, timeout=0.5)
|
||||||
|
|
||||||
|
# Update position
|
||||||
|
self.update_position()
|
||||||
|
|
||||||
|
self.move_completed.emit(axis)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Don't emit errors for timeout - that's expected during continuous jog
|
||||||
|
if "timeout" not in str(e).lower():
|
||||||
|
print(f"Jog error: {e}")
|
||||||
|
self.error_occurred.emit(f"Jog failed: {str(e)}")
|
||||||
|
|
||||||
|
def do_home(self, axis: str):
|
||||||
|
"""Home an axis"""
|
||||||
|
if not self.is_connected or not self.controller:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
dest = self.controller.DEST_X_AXIS if axis == 'x' else self.controller.DEST_Y_AXIS
|
||||||
|
|
||||||
|
print(f"Homing {axis.upper()} axis...")
|
||||||
|
self.controller.home_axis(dest, timeout=20.0)
|
||||||
|
|
||||||
|
# Update position and status after homing
|
||||||
|
self.update_position()
|
||||||
|
self.update_home_status()
|
||||||
|
|
||||||
|
print(f"{axis.upper()} axis homed successfully")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Home error: {e}")
|
||||||
|
self.error_occurred.emit(f"Homing {axis.upper()} failed: {str(e)}")
|
||||||
|
|
||||||
|
def do_set_velocity(self, speed: float, accel: float):
|
||||||
|
"""Set velocity parameters"""
|
||||||
|
if not self.is_connected or not self.controller:
|
||||||
|
self.jog_speed = speed
|
||||||
|
self.acceleration = accel
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.jog_speed = speed
|
||||||
|
self.acceleration = accel
|
||||||
|
|
||||||
|
for dest in [self.controller.DEST_X_AXIS, self.controller.DEST_Y_AXIS]:
|
||||||
|
self.controller.set_velocity_params(
|
||||||
|
dest,
|
||||||
|
min_velocity=0.0,
|
||||||
|
acceleration=self.acceleration,
|
||||||
|
max_velocity=self.jog_speed
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Set velocity error: {e}")
|
||||||
|
|
||||||
|
def do_set_axis_enable(self, axis: str, enabled: bool):
|
||||||
|
"""Enable or disable an axis for manual movement"""
|
||||||
|
if not self.is_connected or not self.controller:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
dest = self.controller.DEST_X_AXIS if axis == 'x' else self.controller.DEST_Y_AXIS
|
||||||
|
self.controller.set_channel_enable_state(dest, enabled)
|
||||||
|
state_str = "enabled" if enabled else "disabled"
|
||||||
|
print(f"{axis.upper()} axis {state_str}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Set axis enable error: {e}")
|
||||||
|
self.error_occurred.emit(f"Failed to {'enable' if enabled else 'disable'} {axis.upper()} axis: {str(e)}")
|
||||||
|
|
||||||
|
def update_position(self):
|
||||||
|
"""Update current position and emit signal if changed"""
|
||||||
|
if not self.is_connected or not self.controller:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Throttle position requests to avoid slowing down the main loop
|
||||||
|
current_time = time.time()
|
||||||
|
if current_time - self.last_position_update_time < self.position_update_interval:
|
||||||
|
return
|
||||||
|
self.last_position_update_time = current_time
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Actively request positions from the controller instead of relying on cached values
|
||||||
|
# This ensures we always have up-to-date position data
|
||||||
|
# Use longer timeout (1.5s) to accommodate high-speed scanning at 200mm/s
|
||||||
|
x_pos = self.controller.get_position(self.controller.DEST_X_AXIS, timeout=1.5)
|
||||||
|
y_pos = self.controller.get_position(self.controller.DEST_Y_AXIS, timeout=1.5)
|
||||||
|
|
||||||
|
if x_pos is not None and y_pos is not None:
|
||||||
|
# Always emit on first update, or if position changed significantly (> 0.001mm)
|
||||||
|
if (self.last_x is None or self.last_y is None or
|
||||||
|
abs(x_pos - self.last_x) > 0.001 or abs(y_pos - self.last_y) > 0.001):
|
||||||
|
self.last_x = x_pos
|
||||||
|
self.last_y = y_pos
|
||||||
|
print(f"Position update: X={x_pos:.3f}mm, Y={y_pos:.3f}mm")
|
||||||
|
self.position_updated.emit(x_pos, y_pos)
|
||||||
|
# else: silently skip incomplete position data during busy scanning
|
||||||
|
|
||||||
|
except RuntimeError as e:
|
||||||
|
# RuntimeError indicates actual hardware error (overtemp, encoder fault, etc.)
|
||||||
|
print(f"CRITICAL: Motor error detected: {e}")
|
||||||
|
self.error_occurred.emit(str(e))
|
||||||
|
# Stop requesting position updates to avoid spam
|
||||||
|
self.is_connected = False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error updating position: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
def update_home_status(self):
|
||||||
|
"""Update home status and emit signal if changed"""
|
||||||
|
if not self.is_connected or not self.controller:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
x_homed = self.controller.is_homed_x
|
||||||
|
y_homed = self.controller.is_homed_y
|
||||||
|
|
||||||
|
# Only emit if status changed
|
||||||
|
if x_homed != self.last_x_homed or y_homed != self.last_y_homed:
|
||||||
|
self.last_x_homed = x_homed
|
||||||
|
self.last_y_homed = y_homed
|
||||||
|
self.homed_status.emit(x_homed, y_homed)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error updating home status: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
def update_motion_status(self):
|
||||||
|
"""Update motion status and emit signal if changed.
|
||||||
|
|
||||||
|
NOTE: On BBD202 firmware v2.1.5, the is_in_motion_x/y properties
|
||||||
|
do NOT reliably detect motion - they may always return False even
|
||||||
|
during active movement. This is a known firmware limitation.
|
||||||
|
|
||||||
|
For scan execution, use the ScanWorker.move_and_wait() method which
|
||||||
|
uses position-based motion detection instead of status bits.
|
||||||
|
|
||||||
|
This UI status display is best-effort only.
|
||||||
|
"""
|
||||||
|
if not self.is_connected or not self.controller:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Actively poll for status updates
|
||||||
|
self.controller.poll_status()
|
||||||
|
|
||||||
|
# Check for any error conditions
|
||||||
|
error_msg = self.controller.check_for_errors()
|
||||||
|
if error_msg:
|
||||||
|
print(f"Motor error detected: {error_msg}")
|
||||||
|
self.error_occurred.emit(error_msg)
|
||||||
|
self.controller.clear_last_error()
|
||||||
|
|
||||||
|
# Read the cached status (may not accurately reflect motion on some firmware)
|
||||||
|
x_moving = self.controller.is_in_motion_x
|
||||||
|
y_moving = self.controller.is_in_motion_y
|
||||||
|
|
||||||
|
# Also check if there are pending moves (more reliable)
|
||||||
|
if self.controller.is_move_pending():
|
||||||
|
# If there are pending moves, we're likely still moving
|
||||||
|
# This provides a backup indication when status bits fail
|
||||||
|
pending = self.controller.get_pending_targets()
|
||||||
|
if self.controller.DEST_X_AXIS in pending:
|
||||||
|
x_moving = True
|
||||||
|
if self.controller.DEST_Y_AXIS in pending:
|
||||||
|
y_moving = True
|
||||||
|
|
||||||
|
# Only emit if status changed
|
||||||
|
if x_moving != self.last_x_moving or y_moving != self.last_y_moving:
|
||||||
|
self.last_x_moving = x_moving
|
||||||
|
self.last_y_moving = y_moving
|
||||||
|
self.motion_status.emit(x_moving, y_moving)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error updating motion status: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
# Slot methods for queuing commands
|
||||||
|
@QtCore.pyqtSlot()
|
||||||
|
def queue_connect(self):
|
||||||
|
"""Queue a connect command"""
|
||||||
|
self.command_queue.put(MotionCommand('connect'))
|
||||||
|
|
||||||
|
@QtCore.pyqtSlot()
|
||||||
|
def queue_disconnect(self):
|
||||||
|
"""Queue a disconnect command"""
|
||||||
|
self.command_queue.put(MotionCommand('disconnect'))
|
||||||
|
|
||||||
|
@QtCore.pyqtSlot(str, int)
|
||||||
|
def queue_jog(self, axis: str, direction: int):
|
||||||
|
"""Queue a jog command"""
|
||||||
|
self.command_queue.put(MotionCommand('jog', axis=axis, direction=direction))
|
||||||
|
|
||||||
|
@QtCore.pyqtSlot(str)
|
||||||
|
def queue_home(self, axis: str):
|
||||||
|
"""Queue a home command"""
|
||||||
|
self.command_queue.put(MotionCommand('home', axis=axis))
|
||||||
|
|
||||||
|
@QtCore.pyqtSlot(float, float)
|
||||||
|
def queue_set_velocity(self, speed: float, accel: float):
|
||||||
|
"""Queue a set velocity command"""
|
||||||
|
self.command_queue.put(MotionCommand('set_velocity', speed=speed, accel=accel))
|
||||||
|
|
||||||
|
@QtCore.pyqtSlot(float)
|
||||||
|
def queue_set_step_size(self, step_size: float):
|
||||||
|
"""Queue a set step size command"""
|
||||||
|
self.command_queue.put(MotionCommand('set_step_size', step_size=step_size))
|
||||||
|
|
||||||
|
@QtCore.pyqtSlot(str, bool)
|
||||||
|
def queue_set_axis_enable(self, axis: str, enabled: bool):
|
||||||
|
"""Queue a command to enable or disable an axis"""
|
||||||
|
self.command_queue.put(MotionCommand('set_axis_enable', axis=axis, enabled=enabled))
|
||||||
|
|
||||||
|
@QtCore.pyqtSlot()
|
||||||
|
def stop(self):
|
||||||
|
"""Stop the worker thread"""
|
||||||
|
# Set running to False immediately so the main loop can exit
|
||||||
|
# even if it's blocked waiting for a response from the controller
|
||||||
|
self.running = False
|
||||||
|
# Also queue a stop command to ensure the command_queue.get() returns
|
||||||
|
self.command_queue.put(MotionCommand('stop'))
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,743 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>Dialog</class>
|
||||||
|
<widget class="QDialog" name="Dialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>681</width>
|
||||||
|
<height>471</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<family>Bahnschrift</family>
|
||||||
|
<pointsize>12</pointsize>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Dialog</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||||
|
<item>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QTabWidget" name="options_tab_widget">
|
||||||
|
<property name="currentIndex">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<widget class="QWidget" name="genesis_config">
|
||||||
|
<attribute name="title">
|
||||||
|
<string>Detection Laser</string>
|
||||||
|
</attribute>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||||
|
<item>
|
||||||
|
<layout class="QVBoxLayout" name="vl_genconfig_main">
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer_2">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Orientation::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_2">
|
||||||
|
<property name="text">
|
||||||
|
<string>Scanning Power [mW]:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="le_detection_scanpower"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer_3">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Orientation::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Test Connection:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_4">
|
||||||
|
<property name="text">
|
||||||
|
<string>???</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="pb_test_genesis_connection">
|
||||||
|
<property name="text">
|
||||||
|
<string>Query Laser / Test Connection</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||||
|
<item>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_5">
|
||||||
|
<property name="text">
|
||||||
|
<string>Laser SN:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="l_detection_serialnum">
|
||||||
|
<property name="text">
|
||||||
|
<string>???</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_16">
|
||||||
|
<property name="text">
|
||||||
|
<string>Laser Model:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="l_detection_modelname">
|
||||||
|
<property name="text">
|
||||||
|
<string>???</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_7">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_9">
|
||||||
|
<property name="text">
|
||||||
|
<string>Interlock State:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="l_detection_interlock">
|
||||||
|
<property name="text">
|
||||||
|
<string>???</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_14">
|
||||||
|
<property name="text">
|
||||||
|
<string>Keyswitch State:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="l_detection_keyswitch">
|
||||||
|
<property name="text">
|
||||||
|
<string>???</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_3">
|
||||||
|
<property name="text">
|
||||||
|
<string>Main Heatsink Temp</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="l_detection_heatsink_temp">
|
||||||
|
<property name="text">
|
||||||
|
<string>???</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_12">
|
||||||
|
<property name="text">
|
||||||
|
<string>ETA Temp</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="l_detection_eta_temp">
|
||||||
|
<property name="text">
|
||||||
|
<string>???</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<widget class="QWidget" name="helios_config">
|
||||||
|
<attribute name="title">
|
||||||
|
<string>Generation Laser</string>
|
||||||
|
</attribute>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_9">
|
||||||
|
<item>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_8">
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_17">
|
||||||
|
<property name="text">
|
||||||
|
<string>Communication Port:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="le_generation_comport"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="pb_autodetect_genlaser">
|
||||||
|
<property name="text">
|
||||||
|
<string>Autodetect</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_focusing_params">
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<pointsize>11</pointsize>
|
||||||
|
<weight>75</weight>
|
||||||
|
<bold>true</bold>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Focusing Parameters:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_focusing_freq">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_focusing_frequency">
|
||||||
|
<property name="text">
|
||||||
|
<string>Focusing Frequency [Hz]:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="le_generation_focusing_frequency"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_focusing_current">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_focusing_current">
|
||||||
|
<property name="text">
|
||||||
|
<string>Focusing Pump Current [mA]:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="le_pumpdiode_focusing_current"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="verticalSpacer_focusing">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Orientation::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeType">
|
||||||
|
<enum>QSizePolicy::Policy::Fixed</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_scanning_params">
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<pointsize>11</pointsize>
|
||||||
|
<weight>75</weight>
|
||||||
|
<bold>true</bold>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Scanning Parameters:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_8">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_18">
|
||||||
|
<property name="text">
|
||||||
|
<string>Scanning Frequency [Hz]:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="le_generation_frequency"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_9">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_19">
|
||||||
|
<property name="text">
|
||||||
|
<string>Scanning Pump Current [mA]:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="le_pumpdiode_current"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_10">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_20">
|
||||||
|
<property name="text">
|
||||||
|
<string>Effective Scan Direction Pixel Size:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="l_calculated_pixel_size_laser">
|
||||||
|
<property name="text">
|
||||||
|
<string>mm px</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="le_generation_reset">
|
||||||
|
<property name="text">
|
||||||
|
<string>Reset Laser (Required after interlock enable)</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<widget class="QWidget" name="mls_config">
|
||||||
|
<attribute name="title">
|
||||||
|
<string>Scanning Stage</string>
|
||||||
|
</attribute>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_11">
|
||||||
|
<item>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_10">
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_13">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_22">
|
||||||
|
<property name="text">
|
||||||
|
<string>Scan Velocity [mm/s]:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="le_scan_velocity"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_15">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_23">
|
||||||
|
<property name="text">
|
||||||
|
<string>Scan Acceleration [mm/s2]:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="le_scan_accel"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_17">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_24">
|
||||||
|
<property name="text">
|
||||||
|
<string>X-Axis Trigger Mode</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QComboBox" name="combo_x_trigmode"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_19">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_25">
|
||||||
|
<property name="text">
|
||||||
|
<string>Y-Axis Trigger Mode</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QComboBox" name="combo_y_trigmode"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_14">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_26">
|
||||||
|
<property name="text">
|
||||||
|
<string>Optical Axis X Coordinate [mm]:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="le_optical_xcoord"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_27">
|
||||||
|
<property name="text">
|
||||||
|
<string>Optical Axis Y Coordinate [mm]:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="le_optical_ycoord"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<widget class="QWidget" name="t3r_config">
|
||||||
|
<attribute name="title">
|
||||||
|
<string>T3R</string>
|
||||||
|
</attribute>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_20">
|
||||||
|
<item>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_12">
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_21">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_28">
|
||||||
|
<property name="text">
|
||||||
|
<string>Comminucation Port:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="le_t3r_comport"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="pb_t3r_autodetect">
|
||||||
|
<property name="text">
|
||||||
|
<string>Autodetect</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_25">
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="pb_t3r_test_connection">
|
||||||
|
<property name="text">
|
||||||
|
<string>Test Connection</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_24">
|
||||||
|
<item>
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="label_29">
|
||||||
|
<property name="text">
|
||||||
|
<string>Firmware Version</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="2">
|
||||||
|
<widget class="QLabel" name="label_33">
|
||||||
|
<property name="text">
|
||||||
|
<string>FPGA Present?</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="2">
|
||||||
|
<widget class="QLabel" name="l_t3r_fpga_active">
|
||||||
|
<property name="text">
|
||||||
|
<string>???</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="QLabel" name="l_t3r_homed">
|
||||||
|
<property name="text">
|
||||||
|
<string>???</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel" name="l_t3r_fwversion">
|
||||||
|
<property name="text">
|
||||||
|
<string>???</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QLabel" name="label_31">
|
||||||
|
<property name="text">
|
||||||
|
<string>Homed Status</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="3">
|
||||||
|
<widget class="QLabel" name="label_35">
|
||||||
|
<property name="text">
|
||||||
|
<string>FPGA MultiDivider
|
||||||
|
Enabled?</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="4">
|
||||||
|
<widget class="QLabel" name="label_36">
|
||||||
|
<property name="text">
|
||||||
|
<string>FPGA RowPacking
|
||||||
|
Enabled?</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="3">
|
||||||
|
<widget class="QLabel" name="l_t3r_fpga_multidivider">
|
||||||
|
<property name="text">
|
||||||
|
<string>???</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="4">
|
||||||
|
<widget class="QLabel" name="l_t3r_fpga_rowpack">
|
||||||
|
<property name="text">
|
||||||
|
<string>???</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<widget class="QWidget" name="scope_config">
|
||||||
|
<attribute name="title">
|
||||||
|
<string>Oscilloscope</string>
|
||||||
|
</attribute>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_26">
|
||||||
|
<item>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_13">
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_28">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_39">
|
||||||
|
<property name="text">
|
||||||
|
<string>Oscilloscope Socket Address:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="le_oscope_socket_addr"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_30">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_40">
|
||||||
|
<property name="text">
|
||||||
|
<string>Scratch Directory:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="le_data_scratchdir"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="pushButton_5">
|
||||||
|
<property name="text">
|
||||||
|
<string>Browse</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_29">
|
||||||
|
<item>
|
||||||
|
<widget class="QRadioButton" name="rdo_savetopc">
|
||||||
|
<property name="text">
|
||||||
|
<string>Save to PC</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QRadioButton" name="rdo_savetoscope">
|
||||||
|
<property name="text">
|
||||||
|
<string>Save to Oscilloscope</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_27">
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="pb_test_scope">
|
||||||
|
<property name="text">
|
||||||
|
<string>Test Connection</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Orientation::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="pb_updateconfig">
|
||||||
|
<property name="text">
|
||||||
|
<string>Update Configuration</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="pb_cancelconfig">
|
||||||
|
<property name="text">
|
||||||
|
<string>Cancel</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""Scan planning and modeling modules"""
|
||||||
|
from .sc3_scan_model import SC3ScanModel
|
||||||
|
from .stage_scan_plan_generator import *
|
||||||
@@ -0,0 +1,638 @@
|
|||||||
|
"""
|
||||||
|
Scan Model Class.
|
||||||
|
Holds the scan configuration, and computes the
|
||||||
|
required start and end points for various specified angles.
|
||||||
|
Python implementation of SC3ScanModel.cs
|
||||||
|
"""
|
||||||
|
|
||||||
|
import math
|
||||||
|
from decimal import Decimal, InvalidOperation
|
||||||
|
from typing import List, Optional
|
||||||
|
import csv
|
||||||
|
|
||||||
|
|
||||||
|
class SC3ScanModel:
|
||||||
|
"""
|
||||||
|
Scan Model for generating scan paths and rotated scans.
|
||||||
|
Manages scan configuration and computes scan coordinates at various angles.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# Private variables
|
||||||
|
self._x_origin: Decimal = Decimal('0.0')
|
||||||
|
self._y_origin: Decimal = Decimal('0.0')
|
||||||
|
self._x_delta: Decimal = Decimal('0.0')
|
||||||
|
self._y_delta: Decimal = Decimal('0.0')
|
||||||
|
self._row_spacing: Decimal = Decimal('0.0')
|
||||||
|
self._laser_frequency: Decimal = Decimal('2000.0') # Default: 2000 Hz
|
||||||
|
self._scan_velocity: Decimal = Decimal('100.0') # Default: 100 mm/s
|
||||||
|
self._scan_acceleration: Decimal = Decimal('0.0')
|
||||||
|
self._scan_angles: int = 0
|
||||||
|
self._points_required: int = 0
|
||||||
|
self._rows_required: int = 0
|
||||||
|
self._points_per_line: int = 0
|
||||||
|
|
||||||
|
# Optical axis centerline in stage coordinates
|
||||||
|
# Stage: MLS203-1
|
||||||
|
self._optical_x_origin: Decimal = Decimal('55.0')
|
||||||
|
self._optical_y_origin: Decimal = Decimal('37.5')
|
||||||
|
|
||||||
|
# Data storage
|
||||||
|
self._scan_coordinates: List[List[Decimal]] = []
|
||||||
|
self._scan_velocities: List[List[Decimal]] = []
|
||||||
|
self._scan_accelerations: List[List[Decimal]] = []
|
||||||
|
self._rotated_coordinates: List[List[List[Decimal]]] = []
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
self._deg2rad: float = math.pi / 180.0
|
||||||
|
|
||||||
|
# Properties
|
||||||
|
@property
|
||||||
|
def x_origin(self) -> Decimal:
|
||||||
|
"""X coordinate of scan origin (mm)"""
|
||||||
|
return self._x_origin
|
||||||
|
|
||||||
|
@x_origin.setter
|
||||||
|
def x_origin(self, value: Decimal):
|
||||||
|
try:
|
||||||
|
self._x_origin = Decimal(str(value))
|
||||||
|
except (ValueError, InvalidOperation) as e:
|
||||||
|
raise ValueError(f"Invalid x_origin value: {value}") from e
|
||||||
|
|
||||||
|
@property
|
||||||
|
def y_origin(self) -> Decimal:
|
||||||
|
"""Y coordinate of scan origin (mm)"""
|
||||||
|
return self._y_origin
|
||||||
|
|
||||||
|
@y_origin.setter
|
||||||
|
def y_origin(self, value: Decimal):
|
||||||
|
try:
|
||||||
|
self._y_origin = Decimal(str(value))
|
||||||
|
except (ValueError, InvalidOperation) as e:
|
||||||
|
raise ValueError(f"Invalid y_origin value: {value}") from e
|
||||||
|
|
||||||
|
@property
|
||||||
|
def x_delta(self) -> Decimal:
|
||||||
|
"""Total X distance to scan (mm)"""
|
||||||
|
return self._x_delta
|
||||||
|
|
||||||
|
@x_delta.setter
|
||||||
|
def x_delta(self, value: Decimal):
|
||||||
|
try:
|
||||||
|
val = Decimal(str(value))
|
||||||
|
if val < 0:
|
||||||
|
raise ValueError("x_delta must be non-negative")
|
||||||
|
self._x_delta = val
|
||||||
|
self.calculate_points_per_line()
|
||||||
|
self.calculate_points_required()
|
||||||
|
except (ValueError, InvalidOperation) as e:
|
||||||
|
raise ValueError(f"Invalid x_delta value: {value}") from e
|
||||||
|
|
||||||
|
@property
|
||||||
|
def y_delta(self) -> Decimal:
|
||||||
|
"""Total Y distance to scan (mm)"""
|
||||||
|
return self._y_delta
|
||||||
|
|
||||||
|
@y_delta.setter
|
||||||
|
def y_delta(self, value: Decimal):
|
||||||
|
try:
|
||||||
|
val = Decimal(str(value))
|
||||||
|
if val < 0:
|
||||||
|
raise ValueError("y_delta must be non-negative")
|
||||||
|
self._y_delta = val
|
||||||
|
self.calculate_rows_required()
|
||||||
|
self.calculate_points_required()
|
||||||
|
except (ValueError, InvalidOperation) as e:
|
||||||
|
raise ValueError(f"Invalid y_delta value: {value}") from e
|
||||||
|
|
||||||
|
@property
|
||||||
|
def row_spacing(self) -> Decimal:
|
||||||
|
"""Spacing between scan rows (mm)"""
|
||||||
|
return self._row_spacing
|
||||||
|
|
||||||
|
@row_spacing.setter
|
||||||
|
def row_spacing(self, value: Decimal):
|
||||||
|
try:
|
||||||
|
val = Decimal(str(value))
|
||||||
|
if val < 0:
|
||||||
|
raise ValueError("row_spacing must be non-negative")
|
||||||
|
self._row_spacing = val
|
||||||
|
self.calculate_rows_required()
|
||||||
|
self.calculate_points_required()
|
||||||
|
except (ValueError, InvalidOperation) as e:
|
||||||
|
raise ValueError(f"Invalid row_spacing value: {value}") from e
|
||||||
|
|
||||||
|
@property
|
||||||
|
def laser_frequency(self) -> Decimal:
|
||||||
|
"""Laser pulse frequency (Hz)"""
|
||||||
|
return self._laser_frequency
|
||||||
|
|
||||||
|
@laser_frequency.setter
|
||||||
|
def laser_frequency(self, value: Decimal):
|
||||||
|
try:
|
||||||
|
val = Decimal(str(value))
|
||||||
|
if val <= 0:
|
||||||
|
raise ValueError("laser_frequency must be positive")
|
||||||
|
self._laser_frequency = val
|
||||||
|
self.calculate_points_per_line()
|
||||||
|
self.calculate_points_required()
|
||||||
|
except (ValueError, InvalidOperation) as e:
|
||||||
|
raise ValueError(f"Invalid laser_frequency value: {value}") from e
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scan_velocity(self) -> Decimal:
|
||||||
|
"""Scan velocity (mm/s)"""
|
||||||
|
return self._scan_velocity
|
||||||
|
|
||||||
|
@scan_velocity.setter
|
||||||
|
def scan_velocity(self, value: Decimal):
|
||||||
|
try:
|
||||||
|
val = Decimal(str(value))
|
||||||
|
if val <= 0:
|
||||||
|
raise ValueError("scan_velocity must be positive")
|
||||||
|
self._scan_velocity = val
|
||||||
|
self.calculate_points_per_line()
|
||||||
|
self.calculate_points_required()
|
||||||
|
except (ValueError, InvalidOperation) as e:
|
||||||
|
raise ValueError(f"Invalid scan_velocity value: {value}") from e
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scan_acceleration(self) -> Decimal:
|
||||||
|
"""Scan acceleration (mm/s²)"""
|
||||||
|
return self._scan_acceleration
|
||||||
|
|
||||||
|
@scan_acceleration.setter
|
||||||
|
def scan_acceleration(self, value: Decimal):
|
||||||
|
try:
|
||||||
|
val = Decimal(str(value))
|
||||||
|
if val < 0:
|
||||||
|
raise ValueError("scan_acceleration must be non-negative")
|
||||||
|
self._scan_acceleration = val
|
||||||
|
except (ValueError, InvalidOperation) as e:
|
||||||
|
raise ValueError(f"Invalid scan_acceleration value: {value}") from e
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scan_angles(self) -> int:
|
||||||
|
"""Number of scan angles to compute"""
|
||||||
|
return self._scan_angles
|
||||||
|
|
||||||
|
@scan_angles.setter
|
||||||
|
def scan_angles(self, value: int):
|
||||||
|
if value < 0:
|
||||||
|
raise ValueError("scan_angles must be non-negative")
|
||||||
|
self._scan_angles = value
|
||||||
|
# Only compute rotated scans if we have base scan coordinates
|
||||||
|
if self._scan_coordinates:
|
||||||
|
self.compute_rotated_scans()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def points_required(self) -> int:
|
||||||
|
"""Total number of points required for a single angle scan (computed)"""
|
||||||
|
return self._points_required
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rows_required(self) -> int:
|
||||||
|
"""Number of rows required for the scan (computed)"""
|
||||||
|
return self._rows_required
|
||||||
|
|
||||||
|
@property
|
||||||
|
def points_per_line(self) -> int:
|
||||||
|
"""Number of points per scan line (computed)"""
|
||||||
|
return self._points_per_line
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scan_coordinates(self) -> List[List[Decimal]]:
|
||||||
|
"""List of scan coordinates [x_start, y_start, x_end, y_end] in mm"""
|
||||||
|
return self._scan_coordinates
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scan_velocities(self) -> List[List[Decimal]]:
|
||||||
|
"""List of velocity vectors [vx, vy] in mm/s for each angle"""
|
||||||
|
return self._scan_velocities
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scan_accelerations(self) -> List[List[Decimal]]:
|
||||||
|
"""List of acceleration vectors [ax, ay] in mm/s² for each angle"""
|
||||||
|
return self._scan_accelerations
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rotated_coordinates(self) -> List[List[List[Decimal]]]:
|
||||||
|
"""List of rotated scan coordinates for each angle in mm"""
|
||||||
|
return self._rotated_coordinates
|
||||||
|
|
||||||
|
@property
|
||||||
|
def optical_x_origin(self) -> Decimal:
|
||||||
|
"""X coordinate of optical axis origin in mm (read-only, MLS203-1 stage)"""
|
||||||
|
return self._optical_x_origin
|
||||||
|
|
||||||
|
@property
|
||||||
|
def optical_y_origin(self) -> Decimal:
|
||||||
|
"""Y coordinate of optical axis origin in mm (read-only, MLS203-1 stage)"""
|
||||||
|
return self._optical_y_origin
|
||||||
|
|
||||||
|
# Calculation Methods
|
||||||
|
def calculate_points_per_line(self):
|
||||||
|
"""
|
||||||
|
Calculates the number of data points per scan line based on
|
||||||
|
x_delta, scan_velocity, and laser_frequency.
|
||||||
|
|
||||||
|
Formula: points = (distance / velocity) * frequency
|
||||||
|
"""
|
||||||
|
if self._scan_velocity != 0:
|
||||||
|
self._points_per_line = int(
|
||||||
|
(self._x_delta / self._scan_velocity) * self._laser_frequency
|
||||||
|
)
|
||||||
|
|
||||||
|
def calculate_points_required(self):
|
||||||
|
"""
|
||||||
|
Calculates the total number of data points for a complete single-angle scan.
|
||||||
|
Also triggers computation of the zero-angle scan coordinates.
|
||||||
|
|
||||||
|
Formula: total_points = points_per_line * rows_required
|
||||||
|
"""
|
||||||
|
if self._rows_required != 0:
|
||||||
|
self._points_required = self._points_per_line * self._rows_required
|
||||||
|
self.compute_zero_scan()
|
||||||
|
|
||||||
|
def calculate_rows_required(self):
|
||||||
|
"""
|
||||||
|
Calculates the number of scan rows needed based on y_delta and row_spacing.
|
||||||
|
|
||||||
|
Formula: rows = ceil(y_delta / row_spacing)
|
||||||
|
"""
|
||||||
|
if self._row_spacing == 0:
|
||||||
|
self._rows_required = 0
|
||||||
|
else:
|
||||||
|
self._rows_required = int(math.ceil(self._y_delta / self._row_spacing))
|
||||||
|
|
||||||
|
def compute_zero_scan(self):
|
||||||
|
"""
|
||||||
|
Computes the zero-angle (reference) scan coordinates.
|
||||||
|
Each coordinate is [x_start, y_start, x_end, y_end].
|
||||||
|
"""
|
||||||
|
# Ignore the zero-row case
|
||||||
|
if self._rows_required == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
y_offset = Decimal('0.0')
|
||||||
|
self._scan_coordinates.clear()
|
||||||
|
|
||||||
|
for row in range(self._rows_required + 1):
|
||||||
|
# Calculate x/y origin/delta for each needed row
|
||||||
|
y_offset = Decimal(row) * self._row_spacing
|
||||||
|
|
||||||
|
coords = [
|
||||||
|
self._x_origin, # x_start
|
||||||
|
self._y_origin + y_offset, # y_start
|
||||||
|
self._x_origin + self._x_delta, # x_end
|
||||||
|
self._y_origin + y_offset # y_end (same as y_start for horizontal scan)
|
||||||
|
]
|
||||||
|
self._scan_coordinates.append(coords)
|
||||||
|
|
||||||
|
def _rotate_point(self, x: Decimal, y: Decimal, cosine: Decimal, sine: Decimal) -> tuple:
|
||||||
|
"""
|
||||||
|
Rotate a point around the optical axis origin.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x, y: Point coordinates to rotate
|
||||||
|
cosine, sine: Precomputed cos and sin of rotation angle
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (rotated_x, rotated_y)
|
||||||
|
"""
|
||||||
|
# Rotation transformation:
|
||||||
|
# Xr = (X - Xo)*cos(a) + (Y - Yo)*sin(a) + Xo
|
||||||
|
# Yr = -(X - Xo)*sin(a) + (Y - Yo)*cos(a) + Yo
|
||||||
|
x_rot = ((x - self._optical_x_origin) * cosine +
|
||||||
|
(y - self._optical_y_origin) * sine +
|
||||||
|
self._optical_x_origin)
|
||||||
|
y_rot = (-(x - self._optical_x_origin) * sine +
|
||||||
|
(y - self._optical_y_origin) * cosine +
|
||||||
|
self._optical_y_origin)
|
||||||
|
return (x_rot, y_rot)
|
||||||
|
|
||||||
|
def _compute_rotated_aoi_bbox(self, angle_rad: float) -> tuple:
|
||||||
|
"""
|
||||||
|
Compute the bounding box of the rotated area of interest.
|
||||||
|
|
||||||
|
Rotates the four corners of the AoI rectangle and finds the
|
||||||
|
min/max extents to create a bounding box.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
angle_rad: Rotation angle in radians
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (min_x, min_y, max_x, max_y) as Decimals
|
||||||
|
"""
|
||||||
|
cosine = Decimal(str(math.cos(angle_rad)))
|
||||||
|
sine = Decimal(str(math.sin(angle_rad)))
|
||||||
|
|
||||||
|
# Define the four corners of the AoI rectangle
|
||||||
|
corners = [
|
||||||
|
(self._x_origin, self._y_origin),
|
||||||
|
(self._x_origin + self._x_delta, self._y_origin),
|
||||||
|
(self._x_origin + self._x_delta, self._y_origin + self._y_delta),
|
||||||
|
(self._x_origin, self._y_origin + self._y_delta)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Rotate all corners
|
||||||
|
rotated_corners = []
|
||||||
|
for x, y in corners:
|
||||||
|
x_rot, y_rot = self._rotate_point(x, y, cosine, sine)
|
||||||
|
rotated_corners.append((x_rot, y_rot))
|
||||||
|
|
||||||
|
# Find bounding box extents
|
||||||
|
x_coords = [corner[0] for corner in rotated_corners]
|
||||||
|
y_coords = [corner[1] for corner in rotated_corners]
|
||||||
|
|
||||||
|
return (min(x_coords), min(y_coords), max(x_coords), max(y_coords))
|
||||||
|
|
||||||
|
def compute_rotated_scans(self):
|
||||||
|
"""
|
||||||
|
Computes rotated scan coordinates by rotating the area of interest (AoI)
|
||||||
|
and generating horizontal (+x direction) scans through the bounding box
|
||||||
|
of the rotated AoI.
|
||||||
|
|
||||||
|
The rotation covers 0 to 180 degrees with spacing determined by scan_angles.
|
||||||
|
For each angle:
|
||||||
|
1. Rotate the AoI rectangle around the optical axis origin
|
||||||
|
2. Compute the bounding box of the rotated rectangle
|
||||||
|
3. Generate horizontal scan lines through the bounding box
|
||||||
|
"""
|
||||||
|
if self._rows_required == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Compute spacing between scans
|
||||||
|
if self._scan_angles == 0:
|
||||||
|
angle_spacing = 180
|
||||||
|
else:
|
||||||
|
angle_spacing = 180 / self._scan_angles
|
||||||
|
|
||||||
|
self._rotated_coordinates.clear()
|
||||||
|
|
||||||
|
# Iterate through each required angle from 0 to 180 degrees
|
||||||
|
i = 0
|
||||||
|
while i < 180:
|
||||||
|
current_angle_radians = i * (math.pi / 180.0)
|
||||||
|
|
||||||
|
# Get bounding box of rotated AoI
|
||||||
|
min_x, min_y, max_x, max_y = self._compute_rotated_aoi_bbox(current_angle_radians)
|
||||||
|
|
||||||
|
# Calculate the y extent of the bounding box
|
||||||
|
y_extent = max_y - min_y
|
||||||
|
|
||||||
|
# Determine number of rows needed for this bounding box
|
||||||
|
if self._row_spacing == 0:
|
||||||
|
num_rows = 0
|
||||||
|
else:
|
||||||
|
num_rows = int(math.ceil(y_extent / self._row_spacing))
|
||||||
|
|
||||||
|
temp_list = []
|
||||||
|
|
||||||
|
# Generate horizontal scan lines through the bounding box
|
||||||
|
for row in range(num_rows + 1):
|
||||||
|
y_offset = Decimal(row) * self._row_spacing
|
||||||
|
y_pos = min_y + y_offset
|
||||||
|
|
||||||
|
# Create horizontal scan line at this y position
|
||||||
|
scan_line = [
|
||||||
|
min_x, # x_start
|
||||||
|
y_pos, # y_start
|
||||||
|
max_x, # x_end
|
||||||
|
y_pos # y_end (same as y_start for horizontal scan)
|
||||||
|
]
|
||||||
|
temp_list.append(scan_line)
|
||||||
|
|
||||||
|
self._rotated_coordinates.append(temp_list)
|
||||||
|
i += int(round(angle_spacing))
|
||||||
|
|
||||||
|
def compute_kinematics(self, offset: int = 0):
|
||||||
|
"""
|
||||||
|
Computes velocity and acceleration component vectors for each scan angle.
|
||||||
|
|
||||||
|
For each angle, decomposes the scalar velocity and acceleration into
|
||||||
|
X and Y components based on the scan direction angle.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
offset: Angle offset in degrees (default: 0)
|
||||||
|
"""
|
||||||
|
if self._scan_angles == 0:
|
||||||
|
scan_increment = 180
|
||||||
|
else:
|
||||||
|
scan_increment = 180 // self._scan_angles
|
||||||
|
|
||||||
|
self._scan_velocities.clear()
|
||||||
|
self._scan_accelerations.clear()
|
||||||
|
|
||||||
|
for i in range(self._scan_angles):
|
||||||
|
deg_angle = scan_increment * i + offset
|
||||||
|
angle_rad = deg_angle * self._deg2rad
|
||||||
|
|
||||||
|
# Compute velocity components: V = V_mag * [cos(θ), sin(θ)]
|
||||||
|
velocities = [
|
||||||
|
Decimal(str(math.cos(angle_rad))) * self._scan_velocity,
|
||||||
|
Decimal(str(math.sin(angle_rad))) * self._scan_velocity
|
||||||
|
]
|
||||||
|
|
||||||
|
# Compute acceleration components: A = A_mag * [cos(θ), sin(θ)]
|
||||||
|
accels = [
|
||||||
|
Decimal(str(math.cos(angle_rad))) * self._scan_acceleration,
|
||||||
|
Decimal(str(math.sin(angle_rad))) * self._scan_acceleration
|
||||||
|
]
|
||||||
|
|
||||||
|
self._scan_velocities.append(velocities)
|
||||||
|
self._scan_accelerations.append(accels)
|
||||||
|
|
||||||
|
# Export Methods
|
||||||
|
def export_zero_scan_csv(self, filename: Optional[str] = None) -> str:
|
||||||
|
"""
|
||||||
|
Export the zero-angle scan coordinates to a CSV file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: Output filename. If None, generates from row count.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The filename that was written
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If no scan coordinates have been computed
|
||||||
|
IOError: If file cannot be written
|
||||||
|
"""
|
||||||
|
if not self._scan_coordinates:
|
||||||
|
raise ValueError("No scan coordinates available. Configure scan parameters first.")
|
||||||
|
|
||||||
|
if filename is None:
|
||||||
|
filename = f"scantest-{self._rows_required}rows.csv"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(filename, 'w', newline='') as f:
|
||||||
|
writer = csv.writer(f)
|
||||||
|
for coords in self._scan_coordinates:
|
||||||
|
writer.writerow([str(c) for c in coords])
|
||||||
|
except IOError as e:
|
||||||
|
raise IOError(f"Failed to write file {filename}: {e}") from e
|
||||||
|
|
||||||
|
return filename
|
||||||
|
|
||||||
|
def export_rotated_scan_csv(self, angle_index: int, filename: Optional[str] = None) -> str:
|
||||||
|
"""
|
||||||
|
Export a specific rotated scan to CSV.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
angle_index: Index of the angle to export (0-based)
|
||||||
|
filename: Output filename. If None, generates from angle and row count.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The filename that was written
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If angle_index is invalid or no rotated coordinates exist
|
||||||
|
IOError: If file cannot be written
|
||||||
|
"""
|
||||||
|
if not self._rotated_coordinates:
|
||||||
|
raise ValueError("No rotated coordinates available. Set scan_angles first.")
|
||||||
|
|
||||||
|
if angle_index < 0 or angle_index >= len(self._rotated_coordinates):
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid angle_index {angle_index}. Must be 0-{len(self._rotated_coordinates)-1}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if filename is None:
|
||||||
|
angle_deg = angle_index * (180 // self._scan_angles if self._scan_angles > 0 else 180)
|
||||||
|
filename = f"scantest-{angle_deg:03d}deg-{self._rows_required}rows.csv"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(filename, 'w', newline='') as f:
|
||||||
|
writer = csv.writer(f)
|
||||||
|
for coords in self._rotated_coordinates[angle_index]:
|
||||||
|
writer.writerow([str(c) for c in coords])
|
||||||
|
except IOError as e:
|
||||||
|
raise IOError(f"Failed to write file {filename}: {e}") from e
|
||||||
|
|
||||||
|
return filename
|
||||||
|
|
||||||
|
def export_all_rotated_scans_csv(self, output_dir: str = ".") -> List[str]:
|
||||||
|
"""
|
||||||
|
Export all rotated scans to separate CSV files.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
output_dir: Directory to write files to (default: current directory)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of filenames that were written
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If no rotated coordinates exist
|
||||||
|
IOError: If files cannot be written
|
||||||
|
"""
|
||||||
|
if not self._rotated_coordinates:
|
||||||
|
raise ValueError("No rotated coordinates available. Set scan_angles first.")
|
||||||
|
|
||||||
|
import os
|
||||||
|
filenames = []
|
||||||
|
|
||||||
|
for angle_index in range(len(self._rotated_coordinates)):
|
||||||
|
angle_deg = angle_index * (180 // self._scan_angles if self._scan_angles > 0 else 180)
|
||||||
|
filename = f"scantest-{angle_deg:03d}deg-{self._rows_required}rows.csv"
|
||||||
|
filepath = os.path.join(output_dir, filename)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(filepath, 'w', newline='') as f:
|
||||||
|
writer = csv.writer(f)
|
||||||
|
for coords in self._rotated_coordinates[angle_index]:
|
||||||
|
writer.writerow([str(c) for c in coords])
|
||||||
|
filenames.append(filepath)
|
||||||
|
except IOError as e:
|
||||||
|
raise IOError(f"Failed to write file {filepath}: {e}") from e
|
||||||
|
|
||||||
|
return filenames
|
||||||
|
|
||||||
|
def export_kinematics_csv(self, filename: Optional[str] = None) -> str:
|
||||||
|
"""
|
||||||
|
Export velocity and acceleration data to CSV.
|
||||||
|
Format: vx, vy, ax, ay for each angle.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: Output filename. If None, generates from row count.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The filename that was written
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If no kinematics data has been computed
|
||||||
|
IOError: If file cannot be written
|
||||||
|
"""
|
||||||
|
if not self._scan_velocities or not self._scan_accelerations:
|
||||||
|
raise ValueError("No kinematics data available. Call compute_kinematics() first.")
|
||||||
|
|
||||||
|
if filename is None:
|
||||||
|
filename = f"kinematics-{self._rows_required}rows.csv"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(filename, 'w', newline='') as f:
|
||||||
|
writer = csv.writer(f)
|
||||||
|
# Optional: write header
|
||||||
|
writer.writerow(['vx', 'vy', 'ax', 'ay'])
|
||||||
|
for i in range(len(self._scan_velocities)):
|
||||||
|
row = [
|
||||||
|
str(self._scan_velocities[i][0]),
|
||||||
|
str(self._scan_velocities[i][1]),
|
||||||
|
str(self._scan_accelerations[i][0]),
|
||||||
|
str(self._scan_accelerations[i][1])
|
||||||
|
]
|
||||||
|
writer.writerow(row)
|
||||||
|
except IOError as e:
|
||||||
|
raise IOError(f"Failed to write file {filename}: {e}") from e
|
||||||
|
|
||||||
|
return filename
|
||||||
|
|
||||||
|
# Utility Methods
|
||||||
|
def get_angle_list(self) -> List[int]:
|
||||||
|
"""
|
||||||
|
Get the list of scan angles in degrees.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of angles in degrees for the configured scan_angles
|
||||||
|
"""
|
||||||
|
if self._scan_angles == 0:
|
||||||
|
return []
|
||||||
|
|
||||||
|
scan_increment = 180 // self._scan_angles
|
||||||
|
return [scan_increment * i for i in range(self._scan_angles)]
|
||||||
|
|
||||||
|
def get_scan_info(self) -> dict:
|
||||||
|
"""
|
||||||
|
Get a dictionary with current scan configuration and computed values.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing scan parameters and computed values
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'x_origin': float(self._x_origin),
|
||||||
|
'y_origin': float(self._y_origin),
|
||||||
|
'x_delta': float(self._x_delta),
|
||||||
|
'y_delta': float(self._y_delta),
|
||||||
|
'row_spacing': float(self._row_spacing),
|
||||||
|
'laser_frequency': float(self._laser_frequency),
|
||||||
|
'scan_velocity': float(self._scan_velocity),
|
||||||
|
'scan_acceleration': float(self._scan_acceleration),
|
||||||
|
'scan_angles': self._scan_angles,
|
||||||
|
'points_per_line': self._points_per_line,
|
||||||
|
'rows_required': self._rows_required,
|
||||||
|
'points_required': self._points_required,
|
||||||
|
'optical_x_origin': float(self._optical_x_origin),
|
||||||
|
'optical_y_origin': float(self._optical_y_origin),
|
||||||
|
'num_scan_coordinates': len(self._scan_coordinates),
|
||||||
|
'num_rotated_angles': len(self._rotated_coordinates),
|
||||||
|
'angle_list': self.get_angle_list(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
"""String representation of the scan model."""
|
||||||
|
return (
|
||||||
|
f"SC3ScanModel("
|
||||||
|
f"origin=({self._x_origin},{self._y_origin}), "
|
||||||
|
f"delta=({self._x_delta},{self._y_delta}), "
|
||||||
|
f"rows={self._rows_required}, "
|
||||||
|
f"angles={self._scan_angles})"
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Test modules for ScanEngine-3"""
|
||||||
@@ -0,0 +1,569 @@
|
|||||||
|
"""
|
||||||
|
BBD202 Stage Diagnostic Tests
|
||||||
|
|
||||||
|
Diagnostic tests for the Thorlabs MLS203/BBD202 motion controller to:
|
||||||
|
1. Check if axes are properly homed
|
||||||
|
2. Move in a 15mm square pattern while outputting position data
|
||||||
|
3. Detect stage hangs through position monitoring
|
||||||
|
4. Output response information for debugging
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from typing import Optional
|
||||||
|
from hardware.bbd202 import MotionController, MotorStatusBits
|
||||||
|
|
||||||
|
|
||||||
|
def format_status_bits(bits: Optional[int]) -> str:
|
||||||
|
"""Format status bits as a readable string with key flags."""
|
||||||
|
if bits is None:
|
||||||
|
return "None (no status received)"
|
||||||
|
|
||||||
|
flags = []
|
||||||
|
status = MotorStatusBits(bits)
|
||||||
|
|
||||||
|
# Key flags to check
|
||||||
|
if MotorStatusBits.HOMED in status:
|
||||||
|
flags.append("HOMED")
|
||||||
|
if MotorStatusBits.HOMING in status:
|
||||||
|
flags.append("HOMING")
|
||||||
|
if MotorStatusBits.ENABLED in status:
|
||||||
|
flags.append("ENABLED")
|
||||||
|
if MotorStatusBits.SETTLED in status:
|
||||||
|
flags.append("SETTLED")
|
||||||
|
if MotorStatusBits.TRACKING in status:
|
||||||
|
flags.append("TRACKING")
|
||||||
|
if MotorStatusBits.INMOTIONCW in status:
|
||||||
|
flags.append("INMOTIONCW")
|
||||||
|
if MotorStatusBits.INMOTIONCCW in status:
|
||||||
|
flags.append("INMOTIONCCW")
|
||||||
|
if MotorStatusBits.POWEROK in status:
|
||||||
|
flags.append("POWEROK")
|
||||||
|
if MotorStatusBits.ERROR in status:
|
||||||
|
flags.append("ERROR")
|
||||||
|
if MotorStatusBits.POSITIONERROR in status:
|
||||||
|
flags.append("POSITIONERROR")
|
||||||
|
if MotorStatusBits.OVERTEMP in status:
|
||||||
|
flags.append("OVERTEMP")
|
||||||
|
if MotorStatusBits.COMMUTATIONERROR in status:
|
||||||
|
flags.append("COMMUTATIONERROR")
|
||||||
|
|
||||||
|
flag_str = ", ".join(flags) if flags else "NO_FLAGS"
|
||||||
|
return f"0x{bits:08X} [{flag_str}]"
|
||||||
|
|
||||||
|
|
||||||
|
def test_homing_status(mc: MotionController) -> bool:
|
||||||
|
"""
|
||||||
|
Test 1: Check if both axes are properly homed.
|
||||||
|
|
||||||
|
Returns True if both axes are homed, False otherwise.
|
||||||
|
"""
|
||||||
|
print("\n" + "="*70)
|
||||||
|
print("TEST 1: Checking Homing Status")
|
||||||
|
print("="*70)
|
||||||
|
|
||||||
|
# Request fresh status from both axes
|
||||||
|
print("\nRequesting status update from both axes...")
|
||||||
|
mc.request_status_update(mc.DEST_X_AXIS)
|
||||||
|
mc.request_status_update(mc.DEST_Y_AXIS)
|
||||||
|
time.sleep(0.2) # Allow time for response
|
||||||
|
|
||||||
|
# Also poll status to ensure we have latest
|
||||||
|
mc.poll_status()
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# Check X-axis
|
||||||
|
x_bits = mc.status_bits_x
|
||||||
|
x_homed = mc.is_homed_x
|
||||||
|
print(f"\nX-Axis Status:")
|
||||||
|
print(f" Raw status bits: {format_status_bits(x_bits)}")
|
||||||
|
print(f" is_homed_x: {x_homed}")
|
||||||
|
|
||||||
|
# Check Y-axis
|
||||||
|
y_bits = mc.status_bits_y
|
||||||
|
y_homed = mc.is_homed_y
|
||||||
|
print(f"\nY-Axis Status:")
|
||||||
|
print(f" Raw status bits: {format_status_bits(y_bits)}")
|
||||||
|
print(f" is_homed_y: {y_homed}")
|
||||||
|
|
||||||
|
# Check for errors
|
||||||
|
last_err = mc.last_error
|
||||||
|
if last_err:
|
||||||
|
print(f"\n** Last Error: {last_err}")
|
||||||
|
|
||||||
|
error_check = mc.check_for_errors()
|
||||||
|
if error_check:
|
||||||
|
print(f"** Error condition detected: {error_check}")
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
print(f"\n--- Homing Status Summary ---")
|
||||||
|
if x_homed and y_homed:
|
||||||
|
print("PASS: Both axes are homed")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("FAIL: One or both axes are NOT homed")
|
||||||
|
if not x_homed:
|
||||||
|
print(" -> X-axis needs homing")
|
||||||
|
if not y_homed:
|
||||||
|
print(" -> Y-axis needs homing")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def test_square_pattern(mc: MotionController, side_length: float = 15.0) -> bool:
|
||||||
|
"""
|
||||||
|
Test 2: Move the stage in a square pattern.
|
||||||
|
|
||||||
|
Moves in a 15mm square starting from current position, outputting
|
||||||
|
position data at each step to detect hangs.
|
||||||
|
|
||||||
|
Pattern: Start -> +X -> +Y -> -X -> -Y (back to start)
|
||||||
|
|
||||||
|
Uses the new wait_until_settled() method for reliable motion detection.
|
||||||
|
"""
|
||||||
|
print("\n" + "="*70)
|
||||||
|
print(f"TEST 2: Square Pattern Movement ({side_length}mm)")
|
||||||
|
print("="*70)
|
||||||
|
print("Using wait_until_settled() for motion detection")
|
||||||
|
|
||||||
|
# Get starting position
|
||||||
|
mc.poll_positions()
|
||||||
|
time.sleep(0.1)
|
||||||
|
start_x = mc.position_x
|
||||||
|
start_y = mc.position_y
|
||||||
|
|
||||||
|
if start_x is None or start_y is None:
|
||||||
|
print("ERROR: Could not get initial position")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"\nStarting position: X={start_x:.4f}mm, Y={start_y:.4f}mm")
|
||||||
|
|
||||||
|
# Define the square corners (relative to start)
|
||||||
|
corners = [
|
||||||
|
(start_x + side_length, start_y), # Move +X
|
||||||
|
(start_x + side_length, start_y + side_length), # Move +Y
|
||||||
|
(start_x, start_y + side_length), # Move -X
|
||||||
|
(start_x, start_y), # Move -Y (back to start)
|
||||||
|
]
|
||||||
|
|
||||||
|
corner_names = ["+X edge", "+Y edge (diagonal)", "-X edge", "Return to start"]
|
||||||
|
|
||||||
|
all_passed = True
|
||||||
|
|
||||||
|
for i, (target_x, target_y) in enumerate(corners):
|
||||||
|
print(f"\n--- Move {i+1}/4: {corner_names[i]} ---")
|
||||||
|
print(f"Target: X={target_x:.4f}mm, Y={target_y:.4f}mm")
|
||||||
|
|
||||||
|
# Clear any previous error
|
||||||
|
mc.clear_last_error()
|
||||||
|
|
||||||
|
# Record start time
|
||||||
|
move_start = time.time()
|
||||||
|
|
||||||
|
# Issue move command (non-blocking)
|
||||||
|
mc.move_to_fast(x=target_x, y=target_y)
|
||||||
|
|
||||||
|
# Use poll_until_idle() in a loop to show progress
|
||||||
|
print(f"\n{'Time':>8} {'X_pos':>12} {'Y_pos':>12} {'X_err':>10} {'Y_err':>10} {'Pending'}")
|
||||||
|
print("-" * 70)
|
||||||
|
|
||||||
|
sample_count = 0
|
||||||
|
while not mc.poll_until_idle(tolerance=0.005, timeout=0.05):
|
||||||
|
elapsed = time.time() - move_start
|
||||||
|
|
||||||
|
curr_x = mc.position_x
|
||||||
|
curr_y = mc.position_y
|
||||||
|
|
||||||
|
if curr_x is not None and curr_y is not None:
|
||||||
|
x_err = curr_x - target_x
|
||||||
|
y_err = curr_y - target_y
|
||||||
|
|
||||||
|
# Print every 5th sample
|
||||||
|
if sample_count % 5 == 0:
|
||||||
|
pending = mc.get_pending_targets()
|
||||||
|
pending_str = ", ".join([f"{'X' if d==0x21 else 'Y'}" for d in pending.keys()])
|
||||||
|
print(f"{elapsed:>7.3f}s {curr_x:>12.4f} {curr_y:>12.4f} {x_err:>+10.4f} {y_err:>+10.4f} {pending_str:>8}")
|
||||||
|
|
||||||
|
sample_count += 1
|
||||||
|
|
||||||
|
# Timeout check
|
||||||
|
if elapsed > 30.0:
|
||||||
|
print(f"\n** TIMEOUT after 30s - stage may be hung!")
|
||||||
|
all_passed = False
|
||||||
|
mc.clear_pending_moves()
|
||||||
|
break
|
||||||
|
|
||||||
|
# Move complete - show final status
|
||||||
|
elapsed = time.time() - move_start
|
||||||
|
curr_x = mc.position_x
|
||||||
|
curr_y = mc.position_y
|
||||||
|
|
||||||
|
if curr_x is not None and curr_y is not None:
|
||||||
|
x_err = curr_x - target_x
|
||||||
|
y_err = curr_y - target_y
|
||||||
|
print(f"{elapsed:>7.3f}s {curr_x:>12.4f} {curr_y:>12.4f} {x_err:>+10.4f} {y_err:>+10.4f} ** DONE **")
|
||||||
|
print(f"\nMove completed in {elapsed:.3f}s")
|
||||||
|
print(f"Final position: X={curr_x:.4f}mm, Y={curr_y:.4f}mm")
|
||||||
|
print(f"Position error: X={abs(x_err)*1000:.1f}um, Y={abs(y_err)*1000:.1f}um")
|
||||||
|
|
||||||
|
# Check for errors after move
|
||||||
|
err = mc.check_for_errors()
|
||||||
|
if err:
|
||||||
|
print(f"** Error after move: {err}")
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
last_err = mc.last_error
|
||||||
|
if last_err:
|
||||||
|
print(f"** Last error: {last_err}")
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
# Short delay between moves
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# Final summary
|
||||||
|
print(f"\n--- Square Pattern Summary ---")
|
||||||
|
mc.poll_positions()
|
||||||
|
time.sleep(0.1)
|
||||||
|
final_x = mc.position_x
|
||||||
|
final_y = mc.position_y
|
||||||
|
if final_x is not None and final_y is not None:
|
||||||
|
total_x_err = abs(final_x - start_x)
|
||||||
|
total_y_err = abs(final_y - start_y)
|
||||||
|
print(f"Final position: X={final_x:.4f}mm, Y={final_y:.4f}mm")
|
||||||
|
print(f"Start position: X={start_x:.4f}mm, Y={start_y:.4f}mm")
|
||||||
|
print(f"Return error: X={total_x_err*1000:.1f}um, Y={total_y_err*1000:.1f}um")
|
||||||
|
|
||||||
|
if total_x_err > 0.05 or total_y_err > 0.05:
|
||||||
|
print("FAIL: Did not return to start position accurately (>50um)")
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
if all_passed:
|
||||||
|
print("PASS: Square pattern completed successfully")
|
||||||
|
else:
|
||||||
|
print("FAIL: Issues detected during square pattern")
|
||||||
|
|
||||||
|
return all_passed
|
||||||
|
|
||||||
|
|
||||||
|
def print_diagnostic_info(mc: MotionController):
|
||||||
|
"""Print comprehensive diagnostic information about the controller."""
|
||||||
|
print("\n" + "="*70)
|
||||||
|
print("CONTROLLER DIAGNOSTIC INFO")
|
||||||
|
print("="*70)
|
||||||
|
|
||||||
|
try:
|
||||||
|
hw_info = mc.get_hw_info()
|
||||||
|
print(f"\nHardware Info:")
|
||||||
|
print(f" Serial Number: {hw_info['serial_number']}")
|
||||||
|
print(f" Model: {hw_info['model']}")
|
||||||
|
print(f" Firmware: {hw_info['firmware_version']}")
|
||||||
|
print(f" HW Version: {hw_info['hw_version']}")
|
||||||
|
print(f" Channels: {hw_info['num_channels']}")
|
||||||
|
print(f" Notes: {hw_info['notes']}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Error getting hardware info: {e}")
|
||||||
|
|
||||||
|
# Request and display velocity params
|
||||||
|
print(f"\nVelocity Parameters:")
|
||||||
|
mc.send_command(0x0414, param1=0x01, dest=mc.DEST_X_AXIS) # REQ_VELPARAMS
|
||||||
|
mc.send_command(0x0414, param1=0x01, dest=mc.DEST_Y_AXIS)
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
x_vel = mc.velocity_params_x
|
||||||
|
y_vel = mc.velocity_params_y
|
||||||
|
if x_vel:
|
||||||
|
print(f" X-Axis: max_vel={x_vel['max_velocity']:.2f}mm/s, accel={x_vel['acceleration']:.2f}mm/s²")
|
||||||
|
if y_vel:
|
||||||
|
print(f" Y-Axis: max_vel={y_vel['max_velocity']:.2f}mm/s, accel={y_vel['acceleration']:.2f}mm/s²")
|
||||||
|
|
||||||
|
|
||||||
|
def test_bay_and_channel_status(mc: MotionController) -> dict:
|
||||||
|
"""
|
||||||
|
Test bay occupancy and channel enable states.
|
||||||
|
|
||||||
|
Returns dict with diagnostic info about each axis.
|
||||||
|
"""
|
||||||
|
print("\n" + "="*70)
|
||||||
|
print("BAY AND CHANNEL DIAGNOSTICS")
|
||||||
|
print("="*70)
|
||||||
|
|
||||||
|
results = {'x': {}, 'y': {}}
|
||||||
|
|
||||||
|
# Query bay status using MGMSG_RACK_REQ_BAYUSED (0x0060)
|
||||||
|
print("\n--- Querying Bay Status (RACK_REQ_BAYUSED 0x0060) ---")
|
||||||
|
print("Sending to controller (0x11)...")
|
||||||
|
|
||||||
|
# Send bay request to controller
|
||||||
|
mc.send_command(0x0060, param1=0x00, param2=0x00, dest=0x11, source=0x01)
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
# Check for response in queue
|
||||||
|
msgs = mc.get_all_messages()
|
||||||
|
bay_response = None
|
||||||
|
for msg in msgs:
|
||||||
|
print(f" Received: msg_id=0x{msg.msg_id:04X}, source=0x{msg.source:02X}, "
|
||||||
|
f"param1=0x{msg.param1:02X}, param2=0x{msg.param2:02X}, data={msg.data.hex() if msg.data else 'none'}")
|
||||||
|
if msg.msg_id == 0x0061: # RACK_GET_BAYUSED
|
||||||
|
bay_response = msg
|
||||||
|
|
||||||
|
if bay_response:
|
||||||
|
# param1 contains bay_ident (which bay), param2 contains state
|
||||||
|
print(f"\nBay status response: bay={bay_response.param1}, state=0x{bay_response.param2:02X}")
|
||||||
|
else:
|
||||||
|
print(" No RACK_GET_BAYUSED response received")
|
||||||
|
|
||||||
|
# Check channel enable state for X-axis (0x21)
|
||||||
|
print("\n--- X-Axis (Bay 1 / dest=0x21) Channel Status ---")
|
||||||
|
print(f" Destination address: 0x{mc.DEST_X_AXIS:02X}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
x_enabled = mc.get_channel_enable_state(mc.DEST_X_AXIS, timeout=2.0)
|
||||||
|
print(f" Channel enabled: {x_enabled}")
|
||||||
|
results['x']['enabled'] = x_enabled
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Error querying channel state: {e}")
|
||||||
|
results['x']['enabled'] = None
|
||||||
|
|
||||||
|
# Get X position to verify communication
|
||||||
|
x_pos = mc.get_position(mc.DEST_X_AXIS, timeout=2.0)
|
||||||
|
print(f" Current position: {x_pos:.4f}mm" if x_pos is not None else " Position query failed!")
|
||||||
|
results['x']['position'] = x_pos
|
||||||
|
results['x']['communicating'] = x_pos is not None
|
||||||
|
|
||||||
|
# Check channel enable state for Y-axis (0x22)
|
||||||
|
print("\n--- Y-Axis (Bay 2 / dest=0x22) Channel Status ---")
|
||||||
|
print(f" Destination address: 0x{mc.DEST_Y_AXIS:02X}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
y_enabled = mc.get_channel_enable_state(mc.DEST_Y_AXIS, timeout=2.0)
|
||||||
|
print(f" Channel enabled: {y_enabled}")
|
||||||
|
results['y']['enabled'] = y_enabled
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Error querying channel state: {e}")
|
||||||
|
results['y']['enabled'] = None
|
||||||
|
|
||||||
|
# Get Y position to verify communication
|
||||||
|
y_pos = mc.get_position(mc.DEST_Y_AXIS, timeout=2.0)
|
||||||
|
print(f" Current position: {y_pos:.4f}mm" if y_pos is not None else " Position query failed!")
|
||||||
|
results['y']['position'] = y_pos
|
||||||
|
results['y']['communicating'] = y_pos is not None
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def test_y_axis_move_verbose(mc: MotionController) -> bool:
|
||||||
|
"""
|
||||||
|
Test Y-axis movement using the new wait_until_settled() method.
|
||||||
|
|
||||||
|
Sends a small Y-axis move and monitors progress.
|
||||||
|
"""
|
||||||
|
print("\n" + "="*70)
|
||||||
|
print("Y-AXIS VERBOSE MOVE TEST")
|
||||||
|
print("="*70)
|
||||||
|
print("Using wait_until_settled() for motion detection")
|
||||||
|
|
||||||
|
# Get current Y position
|
||||||
|
mc.poll_positions()
|
||||||
|
time.sleep(0.1)
|
||||||
|
start_y = mc.position_y
|
||||||
|
|
||||||
|
if start_y is None:
|
||||||
|
print("ERROR: Cannot get Y position")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"\nCurrent Y position: {start_y:.4f}mm")
|
||||||
|
|
||||||
|
# Target: move 5mm in Y
|
||||||
|
target_y = start_y + 5.0
|
||||||
|
print(f"Target Y position: {target_y:.4f}mm")
|
||||||
|
|
||||||
|
# Issue move command using move_to_fast (which tracks pending moves)
|
||||||
|
print("\n--- Issuing Y-axis move via move_to_fast() ---")
|
||||||
|
mc.move_to_fast(y=target_y)
|
||||||
|
|
||||||
|
# Show pending moves
|
||||||
|
pending = mc.get_pending_targets()
|
||||||
|
print(f" Pending moves: {pending}")
|
||||||
|
print(f" is_move_pending(): {mc.is_move_pending()}")
|
||||||
|
|
||||||
|
# Monitor using poll_until_idle
|
||||||
|
print("\n--- Monitoring Y-axis with poll_until_idle() ---")
|
||||||
|
print(f"{'Time':>6} {'Y_pos':>10} {'Y_err':>10} {'Pending'}")
|
||||||
|
print("-" * 45)
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
movement_detected = False
|
||||||
|
|
||||||
|
while not mc.poll_until_idle(tolerance=0.005, timeout=0.05):
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
curr_y = mc.position_y
|
||||||
|
|
||||||
|
if curr_y is not None:
|
||||||
|
y_err = curr_y - target_y
|
||||||
|
pending = "Y" if mc.is_move_pending() else "-"
|
||||||
|
print(f"{elapsed:>5.2f}s {curr_y:>10.4f} {y_err:>+10.4f} {pending:>8}")
|
||||||
|
|
||||||
|
if abs(curr_y - start_y) > 0.01:
|
||||||
|
movement_detected = True
|
||||||
|
|
||||||
|
if elapsed > 10.0:
|
||||||
|
print("\n** TIMEOUT after 10s **")
|
||||||
|
mc.clear_pending_moves()
|
||||||
|
break
|
||||||
|
|
||||||
|
# Final status
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
final_y = mc.position_y
|
||||||
|
print(f"\n--- Y-Axis Move Summary ---")
|
||||||
|
print(f"Start position: {start_y:.4f}mm")
|
||||||
|
print(f"Target position: {target_y:.4f}mm")
|
||||||
|
print(f"Final position: {final_y:.4f}mm" if final_y else "Final position: unknown")
|
||||||
|
print(f"Move time: {elapsed:.3f}s")
|
||||||
|
|
||||||
|
if final_y is not None:
|
||||||
|
distance_moved = abs(final_y - start_y)
|
||||||
|
print(f"Distance moved: {distance_moved:.4f}mm")
|
||||||
|
|
||||||
|
if distance_moved < 0.01:
|
||||||
|
print("\n** FAIL: Y-axis did not move at all! **")
|
||||||
|
print("Possible causes:")
|
||||||
|
print(" 1. Y-axis channel is disabled")
|
||||||
|
print(" 2. Y-axis motor driver issue")
|
||||||
|
print(" 3. Command not reaching Y-axis bay")
|
||||||
|
print(" 4. Mechanical obstruction")
|
||||||
|
return False
|
||||||
|
elif abs(final_y - target_y) < 0.05:
|
||||||
|
print("\n** PASS: Y-axis moved to target **")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"\n** PARTIAL: Y-axis moved but not to target **")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return movement_detected
|
||||||
|
|
||||||
|
|
||||||
|
def test_enable_and_move_y(mc: MotionController) -> bool:
|
||||||
|
"""
|
||||||
|
Explicitly enable Y-axis channel and attempt movement.
|
||||||
|
"""
|
||||||
|
print("\n" + "="*70)
|
||||||
|
print("ENABLE Y-AXIS AND MOVE TEST")
|
||||||
|
print("="*70)
|
||||||
|
|
||||||
|
# First, explicitly enable Y-axis channel
|
||||||
|
print("\n--- Enabling Y-axis channel (MOD_SET_CHANENABLESTATE) ---")
|
||||||
|
print(f" Sending to dest=0x{mc.DEST_Y_AXIS:02X}, enable=True")
|
||||||
|
|
||||||
|
try:
|
||||||
|
mc.set_channel_enable_state(mc.DEST_Y_AXIS, enabled=True)
|
||||||
|
time.sleep(0.2)
|
||||||
|
print(" Enable command sent")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Error sending enable: {e}")
|
||||||
|
|
||||||
|
# Verify it's enabled
|
||||||
|
try:
|
||||||
|
y_enabled = mc.get_channel_enable_state(mc.DEST_Y_AXIS, timeout=2.0)
|
||||||
|
print(f" Channel enabled state: {y_enabled}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Error checking state: {e}")
|
||||||
|
|
||||||
|
# Request fresh status
|
||||||
|
mc.request_status_update(mc.DEST_Y_AXIS)
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
y_bits = mc.status_bits_y
|
||||||
|
print(f" Status bits: {format_status_bits(y_bits)}")
|
||||||
|
|
||||||
|
# Now try the verbose move test
|
||||||
|
return test_y_axis_move_verbose(mc)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("="*70)
|
||||||
|
print("BBD202 Stage Diagnostic Test")
|
||||||
|
print("="*70)
|
||||||
|
|
||||||
|
mc = None
|
||||||
|
try:
|
||||||
|
# Connect to controller
|
||||||
|
print("\nConnecting to BBD202 controller...")
|
||||||
|
mc = MotionController()
|
||||||
|
mc.connect(enable_updates=True)
|
||||||
|
print("Connected!")
|
||||||
|
|
||||||
|
# Print diagnostic info
|
||||||
|
print_diagnostic_info(mc)
|
||||||
|
|
||||||
|
# Run bay and channel diagnostics
|
||||||
|
channel_results = test_bay_and_channel_status(mc)
|
||||||
|
|
||||||
|
# Run homing status test
|
||||||
|
homed = test_homing_status(mc)
|
||||||
|
|
||||||
|
if not homed:
|
||||||
|
print("\n" + "-"*70)
|
||||||
|
response = input("Axes not homed. Home now? (y/n): ").strip().lower()
|
||||||
|
if response == 'y':
|
||||||
|
print("\nHoming X-axis...")
|
||||||
|
x_result = mc.home_x_axis(timeout=30.0)
|
||||||
|
print(f"X-axis homing: {'SUCCESS' if x_result else 'FAILED'}")
|
||||||
|
|
||||||
|
print("\nHoming Y-axis...")
|
||||||
|
y_result = mc.home_y_axis(timeout=30.0)
|
||||||
|
print(f"Y-axis homing: {'SUCCESS' if y_result else 'FAILED'}")
|
||||||
|
|
||||||
|
if not (x_result and y_result):
|
||||||
|
print("\nHoming failed. Cannot continue with movement test.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Re-check homing status
|
||||||
|
test_homing_status(mc)
|
||||||
|
else:
|
||||||
|
print("\nSkipping movement tests (axes not homed)")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Run Y-axis verbose test first to diagnose the issue
|
||||||
|
y_axis_ok = test_enable_and_move_y(mc)
|
||||||
|
|
||||||
|
# If Y-axis failed, skip the square pattern
|
||||||
|
if not y_axis_ok:
|
||||||
|
print("\n** Y-axis movement failed - skipping square pattern test **")
|
||||||
|
square_ok = False
|
||||||
|
else:
|
||||||
|
# Run square pattern test
|
||||||
|
square_ok = test_square_pattern(mc, side_length=15.0)
|
||||||
|
|
||||||
|
# Final summary
|
||||||
|
print("\n" + "="*70)
|
||||||
|
print("DIAGNOSTIC SUMMARY")
|
||||||
|
print("="*70)
|
||||||
|
print(f"X-Axis Channel: {'OK' if channel_results['x'].get('communicating') else 'FAIL'}")
|
||||||
|
print(f"Y-Axis Channel: {'OK' if channel_results['y'].get('communicating') else 'FAIL'}")
|
||||||
|
print(f"Homing Status: {'PASS' if homed else 'FAIL'}")
|
||||||
|
print(f"Y-Axis Move: {'PASS' if y_axis_ok else 'FAIL'}")
|
||||||
|
print(f"Square Pattern: {'PASS' if square_ok else 'FAIL/SKIPPED'}")
|
||||||
|
|
||||||
|
if homed and y_axis_ok and square_ok:
|
||||||
|
print("\nAll tests PASSED")
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
print("\nSome tests FAILED - see details above")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n\nTest interrupted by user")
|
||||||
|
return 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nError during test: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return 1
|
||||||
|
finally:
|
||||||
|
if mc is not None:
|
||||||
|
print("\nDisconnecting from controller...")
|
||||||
|
mc.disconnect()
|
||||||
|
print("Disconnected")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
"""
|
||||||
|
BBD202 Snake Test
|
||||||
|
|
||||||
|
Quick test to verify the BBD202 stage driver with a snake scan pattern.
|
||||||
|
Scans a 20mm x 20mm area with 2mm row spacing in a back-and-forth pattern.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from hardware.bbd202 import MotionController, MotorStatusBits
|
||||||
|
|
||||||
|
|
||||||
|
def snake_test(mc: MotionController, width: float = 20.0, height: float = 20.0, row_spacing: float = 2.0) -> bool:
|
||||||
|
"""
|
||||||
|
Execute a snake scan pattern.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mc: MotionController instance
|
||||||
|
width: Scan width in mm (X direction)
|
||||||
|
height: Scan height in mm (Y direction)
|
||||||
|
row_spacing: Distance between rows in mm
|
||||||
|
|
||||||
|
Pattern:
|
||||||
|
Start (0,0) -> Move +X to (width, 0) -> Move +Y by row_spacing ->
|
||||||
|
Move -X to (0, row_spacing) -> Move +Y by row_spacing ->
|
||||||
|
... repeat until height is covered
|
||||||
|
"""
|
||||||
|
print("\n" + "="*70)
|
||||||
|
print(f"SNAKE TEST: {width}mm x {height}mm area, {row_spacing}mm row spacing")
|
||||||
|
print("="*70)
|
||||||
|
|
||||||
|
# Get starting position
|
||||||
|
mc.poll_positions()
|
||||||
|
time.sleep(0.1)
|
||||||
|
start_x = mc.position_x
|
||||||
|
start_y = mc.position_y
|
||||||
|
|
||||||
|
if start_x is None or start_y is None:
|
||||||
|
print("ERROR: Could not get initial position")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"\nStarting position: X={start_x:.4f}mm, Y={start_y:.4f}mm")
|
||||||
|
|
||||||
|
# Calculate number of rows
|
||||||
|
num_rows = int(height / row_spacing) + 1
|
||||||
|
print(f"Number of rows: {num_rows}")
|
||||||
|
|
||||||
|
# Generate snake path waypoints
|
||||||
|
waypoints = []
|
||||||
|
for row in range(num_rows):
|
||||||
|
y_pos = start_y + row * row_spacing
|
||||||
|
|
||||||
|
if row % 2 == 0:
|
||||||
|
# Even row: move left to right
|
||||||
|
waypoints.append((start_x + width, y_pos, f"Row {row+1}/{num_rows} (+X)"))
|
||||||
|
else:
|
||||||
|
# Odd row: move right to left
|
||||||
|
waypoints.append((start_x, y_pos, f"Row {row+1}/{num_rows} (-X)"))
|
||||||
|
|
||||||
|
print(f"Total waypoints: {len(waypoints)}")
|
||||||
|
|
||||||
|
all_passed = True
|
||||||
|
total_distance = 0.0
|
||||||
|
total_time = 0.0
|
||||||
|
|
||||||
|
# Execute the snake pattern
|
||||||
|
for i, (target_x, target_y, description) in enumerate(waypoints):
|
||||||
|
print(f"\n--- Move {i+1}/{len(waypoints)}: {description} ---")
|
||||||
|
print(f"Target: X={target_x:.4f}mm, Y={target_y:.4f}mm")
|
||||||
|
|
||||||
|
# Record start time and position
|
||||||
|
move_start = time.time()
|
||||||
|
mc.poll_positions()
|
||||||
|
time.sleep(0.02)
|
||||||
|
curr_x = mc.position_x
|
||||||
|
curr_y = mc.position_y
|
||||||
|
|
||||||
|
if curr_x is not None and curr_y is not None:
|
||||||
|
distance = ((target_x - curr_x)**2 + (target_y - curr_y)**2)**0.5
|
||||||
|
total_distance += distance
|
||||||
|
|
||||||
|
# Clear any previous error
|
||||||
|
mc.clear_last_error()
|
||||||
|
|
||||||
|
# Issue move command
|
||||||
|
mc.move_to_fast(x=target_x, y=target_y)
|
||||||
|
|
||||||
|
# Wait for move to complete with periodic progress updates
|
||||||
|
sample_count = 0
|
||||||
|
max_err = 0.0
|
||||||
|
|
||||||
|
while not mc.poll_until_idle(tolerance=0.005, timeout=0.05):
|
||||||
|
elapsed = time.time() - move_start
|
||||||
|
curr_x = mc.position_x
|
||||||
|
curr_y = mc.position_y
|
||||||
|
|
||||||
|
if curr_x is not None and curr_y is not None:
|
||||||
|
x_err = curr_x - target_x
|
||||||
|
y_err = curr_y - target_y
|
||||||
|
total_err = (x_err**2 + y_err**2)**0.5
|
||||||
|
max_err = max(max_err, total_err)
|
||||||
|
|
||||||
|
# Print every 10th sample
|
||||||
|
if sample_count % 10 == 0:
|
||||||
|
print(f" {elapsed:>5.2f}s X={curr_x:>8.4f} Y={curr_y:>8.4f} err={total_err*1000:>6.1f}um", end='\r')
|
||||||
|
|
||||||
|
sample_count += 1
|
||||||
|
|
||||||
|
# Timeout check
|
||||||
|
if elapsed > 30.0:
|
||||||
|
print(f"\n** TIMEOUT after 30s - stage may be hung!")
|
||||||
|
all_passed = False
|
||||||
|
mc.clear_pending_moves()
|
||||||
|
break
|
||||||
|
|
||||||
|
# Move complete
|
||||||
|
elapsed = time.time() - move_start
|
||||||
|
total_time += elapsed
|
||||||
|
|
||||||
|
mc.poll_positions()
|
||||||
|
time.sleep(0.02)
|
||||||
|
final_x = mc.position_x
|
||||||
|
final_y = mc.position_y
|
||||||
|
|
||||||
|
if final_x is not None and final_y is not None:
|
||||||
|
x_err = final_x - target_x
|
||||||
|
y_err = final_y - target_y
|
||||||
|
total_err = (x_err**2 + y_err**2)**0.5
|
||||||
|
print(f" {elapsed:>5.2f}s X={final_x:>8.4f} Y={final_y:>8.4f} err={total_err*1000:>6.1f}um ** DONE **")
|
||||||
|
|
||||||
|
if total_err > 0.05: # 50um tolerance
|
||||||
|
print(f" ** WARNING: Position error exceeds 50um: {total_err*1000:.1f}um")
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
# Check for errors after move
|
||||||
|
err = mc.check_for_errors()
|
||||||
|
if err:
|
||||||
|
print(f" ** Error after move: {err}")
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
last_err = mc.last_error
|
||||||
|
if last_err:
|
||||||
|
print(f" ** Last error: {last_err}")
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
# Return to start position
|
||||||
|
print(f"\n--- Returning to start position ---")
|
||||||
|
move_start = time.time()
|
||||||
|
mc.move_to_fast(x=start_x, y=start_y)
|
||||||
|
|
||||||
|
while not mc.poll_until_idle(tolerance=0.005, timeout=0.05):
|
||||||
|
if time.time() - move_start > 30.0:
|
||||||
|
print("** TIMEOUT returning to start")
|
||||||
|
mc.clear_pending_moves()
|
||||||
|
break
|
||||||
|
|
||||||
|
elapsed = time.time() - move_start
|
||||||
|
total_time += elapsed
|
||||||
|
|
||||||
|
# Final summary
|
||||||
|
print(f"\n{'='*70}")
|
||||||
|
print("SNAKE TEST SUMMARY")
|
||||||
|
print(f"{'='*70}")
|
||||||
|
|
||||||
|
mc.poll_positions()
|
||||||
|
time.sleep(0.1)
|
||||||
|
final_x = mc.position_x
|
||||||
|
final_y = mc.position_y
|
||||||
|
|
||||||
|
if final_x is not None and final_y is not None:
|
||||||
|
return_x_err = abs(final_x - start_x)
|
||||||
|
return_y_err = abs(final_y - start_y)
|
||||||
|
return_err = (return_x_err**2 + return_y_err**2)**0.5
|
||||||
|
|
||||||
|
print(f"Start position: X={start_x:.4f}mm, Y={start_y:.4f}mm")
|
||||||
|
print(f"Final position: X={final_x:.4f}mm, Y={final_y:.4f}mm")
|
||||||
|
print(f"Return error: {return_err*1000:.1f}um (X={return_x_err*1000:.1f}um, Y={return_y_err*1000:.1f}um)")
|
||||||
|
print(f"Total distance: {total_distance:.2f}mm")
|
||||||
|
print(f"Total time: {total_time:.2f}s")
|
||||||
|
print(f"Average speed: {total_distance/total_time:.2f}mm/s")
|
||||||
|
print(f"Waypoints: {len(waypoints)}")
|
||||||
|
|
||||||
|
if return_err > 0.05: # 50um tolerance
|
||||||
|
print(f"\n** FAIL: Did not return to start position (error: {return_err*1000:.1f}um > 50um)")
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
if all_passed:
|
||||||
|
print(f"\n** PASS: Snake test completed successfully **")
|
||||||
|
else:
|
||||||
|
print(f"\n** FAIL: Issues detected during snake test **")
|
||||||
|
|
||||||
|
return all_passed
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("="*70)
|
||||||
|
print("BBD202 Snake Test")
|
||||||
|
print("="*70)
|
||||||
|
|
||||||
|
mc = None
|
||||||
|
try:
|
||||||
|
# Connect to controller
|
||||||
|
print("\nConnecting to BBD202 controller...")
|
||||||
|
mc = MotionController()
|
||||||
|
mc.connect(enable_updates=True)
|
||||||
|
print("Connected!")
|
||||||
|
|
||||||
|
# Brief hardware info
|
||||||
|
try:
|
||||||
|
hw_info = mc.get_hw_info()
|
||||||
|
print(f"\nHardware: {hw_info['model']} (S/N: {hw_info['serial_number']})")
|
||||||
|
print(f"Firmware: {hw_info['firmware_version']}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Could not get hardware info: {e}")
|
||||||
|
|
||||||
|
# Check homing status
|
||||||
|
print("\n--- Checking Homing Status ---")
|
||||||
|
mc.request_status_update(mc.DEST_X_AXIS)
|
||||||
|
mc.request_status_update(mc.DEST_Y_AXIS)
|
||||||
|
time.sleep(0.2)
|
||||||
|
mc.poll_status()
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
x_homed = mc.is_homed_x
|
||||||
|
y_homed = mc.is_homed_y
|
||||||
|
|
||||||
|
print(f"X-axis homed: {x_homed}")
|
||||||
|
print(f"Y-axis homed: {y_homed}")
|
||||||
|
|
||||||
|
if not (x_homed and y_homed):
|
||||||
|
print("\n** Axes not homed - will home now **")
|
||||||
|
|
||||||
|
print("\nHoming X-axis...")
|
||||||
|
x_result = mc.home_x_axis(timeout=30.0)
|
||||||
|
print(f"X-axis: {'SUCCESS' if x_result else 'FAILED'}")
|
||||||
|
|
||||||
|
print("\nHoming Y-axis...")
|
||||||
|
y_result = mc.home_y_axis(timeout=30.0)
|
||||||
|
print(f"Y-axis: {'SUCCESS' if y_result else 'FAILED'}")
|
||||||
|
|
||||||
|
if not (x_result and y_result):
|
||||||
|
print("\n** Homing failed. Cannot continue. **")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Run the snake test
|
||||||
|
success = snake_test(mc, width=20.0, height=20.0, row_spacing=2.0)
|
||||||
|
|
||||||
|
return 0 if success else 1
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n\nTest interrupted by user")
|
||||||
|
return 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n** ERROR: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return 1
|
||||||
|
finally:
|
||||||
|
if mc is not None:
|
||||||
|
print("\nDisconnecting from controller...")
|
||||||
|
mc.disconnect()
|
||||||
|
print("Disconnected")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
"""
|
||||||
|
BBD202 Snake Scan Test - 20x20mm Area
|
||||||
|
|
||||||
|
Scans a 20mm x 20mm area centered at X=55, Y=37.5 in a snake pattern.
|
||||||
|
Row spacing: 0.5mm
|
||||||
|
Movement order: X+, Y+, X-, Y+ (snake pattern)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import argparse
|
||||||
|
from hardware.bbd202 import MotionController, MotorStatusBits
|
||||||
|
|
||||||
|
|
||||||
|
# Scan parameters
|
||||||
|
CENTER_X = 55.0 # mm
|
||||||
|
CENTER_Y = 37.5 # mm
|
||||||
|
SCAN_WIDTH = 20.0 # mm (X direction)
|
||||||
|
SCAN_HEIGHT = 20.0 # mm (Y direction)
|
||||||
|
ROW_SPACING = 0.5 # mm (Y step between rows)
|
||||||
|
|
||||||
|
# Calculated bounds
|
||||||
|
START_X = CENTER_X - SCAN_WIDTH / 2 # 45.0 mm
|
||||||
|
END_X = CENTER_X + SCAN_WIDTH / 2 # 65.0 mm
|
||||||
|
START_Y = CENTER_Y - SCAN_HEIGHT / 2 # 27.5 mm
|
||||||
|
END_Y = CENTER_Y + SCAN_HEIGHT / 2 # 47.5 mm
|
||||||
|
|
||||||
|
|
||||||
|
def snake_scan(mc: MotionController) -> bool:
|
||||||
|
"""
|
||||||
|
Execute a snake scan pattern over the 20x20mm area.
|
||||||
|
|
||||||
|
Pattern (X+, Y+, X-, Y+):
|
||||||
|
Row 0: X=45 -> X=65 (X+)
|
||||||
|
Step: Y += 0.5 (Y+)
|
||||||
|
Row 1: X=65 -> X=45 (X-)
|
||||||
|
Step: Y += 0.5 (Y+)
|
||||||
|
... repeat until Y reaches 47.5mm
|
||||||
|
"""
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print("SNAKE SCAN TEST: 20mm x 20mm")
|
||||||
|
print("=" * 70)
|
||||||
|
print(f"Center: X={CENTER_X:.1f}mm, Y={CENTER_Y:.1f}mm")
|
||||||
|
print(f"Bounds: X=[{START_X:.1f}, {END_X:.1f}]mm, Y=[{START_Y:.1f}, {END_Y:.1f}]mm")
|
||||||
|
print(f"Row spacing: {ROW_SPACING}mm")
|
||||||
|
print(f"Pattern: X+, Y+, X-, Y+ (snake)")
|
||||||
|
|
||||||
|
# Calculate number of rows
|
||||||
|
num_rows = int(SCAN_HEIGHT / ROW_SPACING) + 1
|
||||||
|
print(f"Total rows: {num_rows}")
|
||||||
|
|
||||||
|
# Stop automatic ACK to prevent interference with move commands
|
||||||
|
# We'll send manual ACKs after each move completes
|
||||||
|
mc.stop_status_ack()
|
||||||
|
print("Using manual ACK mode for scan")
|
||||||
|
|
||||||
|
# First, move to the starting position
|
||||||
|
print(f"\n--- Moving to start position ({START_X:.1f}, {START_Y:.1f}) ---")
|
||||||
|
mc.move_to_fast(x=START_X, y=START_Y)
|
||||||
|
|
||||||
|
move_start = time.time()
|
||||||
|
while not mc.poll_until_idle(tolerance=0.005, timeout=0.05):
|
||||||
|
if time.time() - move_start > 30.0:
|
||||||
|
print("** TIMEOUT moving to start position!")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Verify starting position
|
||||||
|
mc.poll_positions()
|
||||||
|
time.sleep(0.1)
|
||||||
|
curr_x = mc.position_x
|
||||||
|
curr_y = mc.position_y
|
||||||
|
print(f"At start: X={curr_x:.4f}mm, Y={curr_y:.4f}mm")
|
||||||
|
|
||||||
|
# Generate snake path waypoints
|
||||||
|
waypoints = []
|
||||||
|
for row in range(num_rows):
|
||||||
|
y_pos = START_Y + row * ROW_SPACING
|
||||||
|
|
||||||
|
if row % 2 == 0:
|
||||||
|
# Even row: move X+ (left to right)
|
||||||
|
waypoints.append((END_X, y_pos, f"Row {row + 1}/{num_rows} X+ (Y={y_pos:.1f})"))
|
||||||
|
else:
|
||||||
|
# Odd row: move X- (right to left)
|
||||||
|
waypoints.append((START_X, y_pos, f"Row {row + 1}/{num_rows} X- (Y={y_pos:.1f})"))
|
||||||
|
|
||||||
|
print(f"\nTotal waypoints: {len(waypoints)}")
|
||||||
|
print("-" * 70)
|
||||||
|
|
||||||
|
all_passed = True
|
||||||
|
total_distance = 0.0
|
||||||
|
total_time = 0.0
|
||||||
|
scan_start_time = time.time()
|
||||||
|
|
||||||
|
# Execute the snake pattern
|
||||||
|
for i, (target_x, target_y, description) in enumerate(waypoints):
|
||||||
|
# Record start position for distance calculation
|
||||||
|
mc.poll_positions()
|
||||||
|
time.sleep(0.02)
|
||||||
|
prev_x = mc.position_x
|
||||||
|
prev_y = mc.position_y
|
||||||
|
|
||||||
|
if prev_x is not None and prev_y is not None:
|
||||||
|
distance = ((target_x - prev_x) ** 2 + (target_y - prev_y) ** 2) ** 0.5
|
||||||
|
total_distance += distance
|
||||||
|
|
||||||
|
# Clear any previous error
|
||||||
|
mc.clear_last_error()
|
||||||
|
|
||||||
|
# Issue move command
|
||||||
|
move_start = time.time()
|
||||||
|
mc.move_to_fast(x=target_x, y=target_y)
|
||||||
|
|
||||||
|
# Wait for move to complete
|
||||||
|
while not mc.poll_until_idle(tolerance=0.005, timeout=0.05):
|
||||||
|
elapsed = time.time() - move_start
|
||||||
|
|
||||||
|
# Timeout check
|
||||||
|
if elapsed > 30.0:
|
||||||
|
print(f"\n** TIMEOUT on {description} after 30s!")
|
||||||
|
all_passed = False
|
||||||
|
mc.clear_pending_moves()
|
||||||
|
break
|
||||||
|
|
||||||
|
# Move complete - send ACK to keep controller responsive
|
||||||
|
mc.ack_status_update()
|
||||||
|
|
||||||
|
elapsed = time.time() - move_start
|
||||||
|
total_time += elapsed
|
||||||
|
|
||||||
|
mc.poll_positions()
|
||||||
|
time.sleep(0.02)
|
||||||
|
final_x = mc.position_x
|
||||||
|
final_y = mc.position_y
|
||||||
|
|
||||||
|
if final_x is not None and final_y is not None:
|
||||||
|
x_err = final_x - target_x
|
||||||
|
y_err = final_y - target_y
|
||||||
|
total_err = (x_err ** 2 + y_err ** 2) ** 0.5
|
||||||
|
|
||||||
|
# Print progress (compact format)
|
||||||
|
status = "OK" if total_err < 0.05 else "ERR"
|
||||||
|
print(f" {description:35} -> X={final_x:7.3f} Y={final_y:7.3f} err={total_err * 1000:5.1f}um [{status}]")
|
||||||
|
|
||||||
|
if total_err > 0.05: # 50um tolerance
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
# Check for errors after move
|
||||||
|
err = mc.check_for_errors()
|
||||||
|
if err:
|
||||||
|
print(f" ** Error: {err}")
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
last_err = mc.last_error
|
||||||
|
if last_err:
|
||||||
|
print(f" ** Last error: {last_err}")
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
scan_elapsed = time.time() - scan_start_time
|
||||||
|
|
||||||
|
# Return to start position
|
||||||
|
print(f"\n--- Returning to start position ({START_X:.1f}, {START_Y:.1f}) ---")
|
||||||
|
move_start = time.time()
|
||||||
|
mc.move_to_fast(x=START_X, y=START_Y)
|
||||||
|
|
||||||
|
while not mc.poll_until_idle(tolerance=0.005, timeout=0.05):
|
||||||
|
if time.time() - move_start > 30.0:
|
||||||
|
print("** TIMEOUT returning to start")
|
||||||
|
mc.clear_pending_moves()
|
||||||
|
break
|
||||||
|
|
||||||
|
# Final summary
|
||||||
|
print(f"\n{'=' * 70}")
|
||||||
|
print("SNAKE SCAN SUMMARY")
|
||||||
|
print(f"{'=' * 70}")
|
||||||
|
|
||||||
|
mc.poll_positions()
|
||||||
|
time.sleep(0.1)
|
||||||
|
final_x = mc.position_x
|
||||||
|
final_y = mc.position_y
|
||||||
|
|
||||||
|
if final_x is not None and final_y is not None:
|
||||||
|
return_x_err = abs(final_x - START_X)
|
||||||
|
return_y_err = abs(final_y - START_Y)
|
||||||
|
return_err = (return_x_err ** 2 + return_y_err ** 2) ** 0.5
|
||||||
|
|
||||||
|
print(f"Scan area: {SCAN_WIDTH:.1f}mm x {SCAN_HEIGHT:.1f}mm")
|
||||||
|
print(f"Center: X={CENTER_X:.1f}mm, Y={CENTER_Y:.1f}mm")
|
||||||
|
print(f"Row spacing: {ROW_SPACING}mm")
|
||||||
|
print(f"Rows completed: {num_rows}")
|
||||||
|
print(f"Total distance: {total_distance:.2f}mm")
|
||||||
|
print(f"Scan time: {scan_elapsed:.2f}s")
|
||||||
|
print(f"Average speed: {total_distance / scan_elapsed:.2f}mm/s")
|
||||||
|
print(f"Return error: {return_err * 1000:.1f}um (X={return_x_err * 1000:.1f}um, Y={return_y_err * 1000:.1f}um)")
|
||||||
|
|
||||||
|
if return_err > 0.05:
|
||||||
|
print(f"\n** FAIL: Return position error exceeds 50um")
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
# Restart automatic ACK
|
||||||
|
mc.start_status_ack()
|
||||||
|
|
||||||
|
if all_passed:
|
||||||
|
print(f"\n** PASS: Snake scan completed successfully **")
|
||||||
|
else:
|
||||||
|
print(f"\n** FAIL: Issues detected during snake scan **")
|
||||||
|
|
||||||
|
return all_passed
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="BBD202 Snake Scan Test - 20x20mm Area")
|
||||||
|
parser.add_argument("--auto-home", action="store_true",
|
||||||
|
help="Automatically home axes if not homed (no prompt)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
print("=" * 70)
|
||||||
|
print("BBD202 Snake Scan Test - 20x20mm Area")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
mc = None
|
||||||
|
try:
|
||||||
|
# Connect to controller
|
||||||
|
print("\nConnecting to BBD202 controller...")
|
||||||
|
mc = MotionController()
|
||||||
|
mc.connect(enable_updates=True)
|
||||||
|
print("Connected!")
|
||||||
|
|
||||||
|
# Brief hardware info
|
||||||
|
try:
|
||||||
|
hw_info = mc.get_hw_info()
|
||||||
|
print(f"\nHardware: {hw_info['model']} (S/N: {hw_info['serial_number']})")
|
||||||
|
print(f"Firmware: {hw_info['firmware_version']}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Could not get hardware info: {e}")
|
||||||
|
|
||||||
|
# Check homing status
|
||||||
|
print("\n--- Checking Homing Status ---")
|
||||||
|
mc.request_status_update(mc.DEST_X_AXIS)
|
||||||
|
mc.request_status_update(mc.DEST_Y_AXIS)
|
||||||
|
time.sleep(0.2)
|
||||||
|
mc.poll_status()
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
x_homed = mc.is_homed_x
|
||||||
|
y_homed = mc.is_homed_y
|
||||||
|
|
||||||
|
print(f"X-axis homed: {x_homed}")
|
||||||
|
print(f"Y-axis homed: {y_homed}")
|
||||||
|
|
||||||
|
if not (x_homed and y_homed):
|
||||||
|
print("\n** Axes not homed! **")
|
||||||
|
|
||||||
|
# Check if we should auto-home or prompt
|
||||||
|
should_home = args.auto_home
|
||||||
|
if not should_home:
|
||||||
|
try:
|
||||||
|
response = input("Home axes now? (y/n): ").strip().lower()
|
||||||
|
should_home = response == 'y'
|
||||||
|
except EOFError:
|
||||||
|
print("Non-interactive mode detected. Use --auto-home flag.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if should_home:
|
||||||
|
print("\nHoming X-axis...")
|
||||||
|
x_result = mc.home_x_axis(timeout=30.0)
|
||||||
|
print(f"X-axis: {'SUCCESS' if x_result else 'FAILED'}")
|
||||||
|
|
||||||
|
print("\nHoming Y-axis...")
|
||||||
|
y_result = mc.home_y_axis(timeout=30.0)
|
||||||
|
print(f"Y-axis: {'SUCCESS' if y_result else 'FAILED'}")
|
||||||
|
|
||||||
|
if not (x_result and y_result):
|
||||||
|
print("\n** Homing failed. Cannot continue. **")
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
print("\nCannot run snake scan without homing. Exiting.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Run the snake scan
|
||||||
|
success = snake_scan(mc)
|
||||||
|
|
||||||
|
return 0 if success else 1
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n\nTest interrupted by user")
|
||||||
|
return 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n** ERROR: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return 1
|
||||||
|
finally:
|
||||||
|
if mc is not None:
|
||||||
|
print("\nDisconnecting from controller...")
|
||||||
|
mc.disconnect()
|
||||||
|
print("Disconnected")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for camera integration with the scan wizard.
|
||||||
|
Tests the camera driver and integration with the UI.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from PyQt6 import QtWidgets
|
||||||
|
|
||||||
|
def test_camera_import():
|
||||||
|
"""Test that the camera driver can be imported"""
|
||||||
|
print("Testing camera driver import...")
|
||||||
|
try:
|
||||||
|
from uc480_camera import UC480Camera, CameraStreamThread
|
||||||
|
print("✓ Camera driver imported successfully")
|
||||||
|
return True
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"✗ Failed to import camera driver: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_camera_class():
|
||||||
|
"""Test that the camera class can be instantiated"""
|
||||||
|
print("\nTesting camera class instantiation...")
|
||||||
|
try:
|
||||||
|
from uc480_camera import UC480Camera
|
||||||
|
camera = UC480Camera(camera_id=0)
|
||||||
|
print("✓ Camera class instantiated successfully")
|
||||||
|
print(f" Camera handle: {camera.h_cam}")
|
||||||
|
print(f" Initialized: {camera.is_initialized}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Failed to instantiate camera class: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_scanengine_import():
|
||||||
|
"""Test that the scanengine app can be imported with camera integration"""
|
||||||
|
print("\nTesting scanengine app import...")
|
||||||
|
try:
|
||||||
|
from scanengine_app import NewScanWizard, MainLauncher
|
||||||
|
print("✓ Scanengine app imported successfully")
|
||||||
|
return True
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"✗ Failed to import scanengine app: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_wizard_with_camera():
|
||||||
|
"""Test that the wizard can be created with camera integration"""
|
||||||
|
print("\nTesting wizard with camera integration...")
|
||||||
|
try:
|
||||||
|
app = QtWidgets.QApplication(sys.argv)
|
||||||
|
from scanengine_app import NewScanWizard
|
||||||
|
|
||||||
|
wizard = NewScanWizard()
|
||||||
|
print("✓ Wizard created successfully")
|
||||||
|
print(f" Camera object: {wizard.camera}")
|
||||||
|
print(f" Camera stream thread: {wizard.camera_stream_thread}")
|
||||||
|
print(f" CCD scene: {wizard.ccd_scene}")
|
||||||
|
|
||||||
|
# Check if camera methods exist
|
||||||
|
assert hasattr(wizard, 'initialize_camera'), "Missing initialize_camera method"
|
||||||
|
assert hasattr(wizard, 'start_camera_stream'), "Missing start_camera_stream method"
|
||||||
|
assert hasattr(wizard, 'stop_camera_stream'), "Missing stop_camera_stream method"
|
||||||
|
assert hasattr(wizard, 'cleanup_camera'), "Missing cleanup_camera method"
|
||||||
|
print("✓ All camera methods present")
|
||||||
|
|
||||||
|
# Test page change triggers
|
||||||
|
print("\nTesting page change behavior...")
|
||||||
|
current_page = wizard.stackedWidget.currentIndex()
|
||||||
|
print(f" Current page: {current_page}")
|
||||||
|
|
||||||
|
# Simulate page navigation to focus page (index 1)
|
||||||
|
print(" Navigating to focus page (index 1)...")
|
||||||
|
wizard.stackedWidget.setCurrentIndex(1)
|
||||||
|
print(f" Current page after navigation: {wizard.stackedWidget.currentIndex()}")
|
||||||
|
|
||||||
|
# Navigate back to first page
|
||||||
|
print(" Navigating back to page 0...")
|
||||||
|
wizard.stackedWidget.setCurrentIndex(0)
|
||||||
|
print(f" Current page after navigation: {wizard.stackedWidget.currentIndex()}")
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
wizard.cleanup_camera()
|
||||||
|
print("✓ Camera cleanup successful")
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Failed to test wizard with camera: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run all tests"""
|
||||||
|
print("=" * 60)
|
||||||
|
print("Camera Integration Test Suite")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
results.append(("Camera Import", test_camera_import()))
|
||||||
|
results.append(("Camera Class", test_camera_class()))
|
||||||
|
results.append(("Scanengine Import", test_scanengine_import()))
|
||||||
|
results.append(("Wizard Integration", test_wizard_with_camera()))
|
||||||
|
|
||||||
|
# Print summary
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("Test Summary")
|
||||||
|
print("=" * 60)
|
||||||
|
for test_name, passed in results:
|
||||||
|
status = "PASS" if passed else "FAIL"
|
||||||
|
symbol = "✓" if passed else "✗"
|
||||||
|
print(f"{symbol} {test_name}: {status}")
|
||||||
|
|
||||||
|
all_passed = all(result[1] for result in results)
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
if all_passed:
|
||||||
|
print("All tests passed!")
|
||||||
|
else:
|
||||||
|
print("Some tests failed.")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
return 0 if all_passed else 1
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Executable
+196
@@ -0,0 +1,196 @@
|
|||||||
|
#!/opt/srasenv/bin/python3
|
||||||
|
"""
|
||||||
|
Test script for Genesis laser serial communication.
|
||||||
|
Tests basic connectivity and I2C protocol without GUI.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import serial
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
NXP_START_BYTE = 0x53
|
||||||
|
NXP_STOP_BYTE = 0x50
|
||||||
|
ADDR_PCA9555_PS_GLUE_OUT = 0x4a
|
||||||
|
ADDR_ADS7828 = 0x90
|
||||||
|
CHAN_CURRENT_ACTUAL = 0x84
|
||||||
|
|
||||||
|
def test_serial_connection(port_name="/dev/ttyUSB1", baudrate=9600):
|
||||||
|
"""Test basic serial connection."""
|
||||||
|
print(f"Testing serial connection to {port_name} @ {baudrate}...")
|
||||||
|
try:
|
||||||
|
port = serial.Serial(
|
||||||
|
port=port_name,
|
||||||
|
baudrate=baudrate,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
parity=serial.PARITY_NONE,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
timeout=1.0
|
||||||
|
)
|
||||||
|
print("✓ Serial port opened successfully")
|
||||||
|
return port
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Failed to open serial port: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def nxp_write(port, i2c_addr_write, cmd, data, data_len):
|
||||||
|
"""Build and send NXP I2C write packet."""
|
||||||
|
# Determine command length
|
||||||
|
if cmd <= 0xFF:
|
||||||
|
cmd_bytes = bytes([cmd])
|
||||||
|
else:
|
||||||
|
cmd_bytes = cmd.to_bytes(2, 'big')
|
||||||
|
|
||||||
|
# Convert data to bytes (big-endian)
|
||||||
|
data_bytes = data.to_bytes(data_len, 'big')
|
||||||
|
|
||||||
|
# Calculate total length
|
||||||
|
total_len = len(cmd_bytes) + len(data_bytes)
|
||||||
|
|
||||||
|
# Build packet: [0x53][addr][len][cmd...][data...][0x50]
|
||||||
|
packet = bytes([
|
||||||
|
NXP_START_BYTE,
|
||||||
|
i2c_addr_write,
|
||||||
|
total_len
|
||||||
|
]) + cmd_bytes + data_bytes + bytes([NXP_STOP_BYTE])
|
||||||
|
|
||||||
|
print(f" TX: {packet.hex(' ')}")
|
||||||
|
port.write(packet)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def nxp_read(port, i2c_addr_write, cmd, cmd_len, data_len):
|
||||||
|
"""Build and send NXP I2C read packet."""
|
||||||
|
# Convert command to bytes
|
||||||
|
if cmd_len == 1:
|
||||||
|
cmd_bytes = bytes([cmd])
|
||||||
|
else:
|
||||||
|
cmd_bytes = cmd.to_bytes(2, 'big')
|
||||||
|
|
||||||
|
# Build packet: [0x53][addr][cmd_len][cmd...][0x53][addr|0x01][data_len][0x50]
|
||||||
|
packet = bytes([
|
||||||
|
NXP_START_BYTE,
|
||||||
|
i2c_addr_write,
|
||||||
|
cmd_len
|
||||||
|
]) + cmd_bytes + bytes([
|
||||||
|
NXP_START_BYTE,
|
||||||
|
i2c_addr_write | 0x01, # Read address
|
||||||
|
data_len,
|
||||||
|
NXP_STOP_BYTE
|
||||||
|
])
|
||||||
|
|
||||||
|
print(f" TX: {packet.hex(' ')}")
|
||||||
|
port.write(packet)
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# Read response
|
||||||
|
response = port.read(data_len)
|
||||||
|
if response:
|
||||||
|
print(f" RX: {response.hex(' ')} ({len(response)} bytes)")
|
||||||
|
return int.from_bytes(response, 'big')
|
||||||
|
else:
|
||||||
|
print(f" RX: (no data)")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def test_read_ps_glue_out(port):
|
||||||
|
"""Test reading PS glue output port."""
|
||||||
|
print("\nTest 1: Reading PS glue output port (0x4a, reg 0x02)...")
|
||||||
|
value = nxp_read(port, ADDR_PCA9555_PS_GLUE_OUT, 0x02, 1, 1)
|
||||||
|
if value is not None:
|
||||||
|
print(f"✓ PS Glue Out status: 0x{value:02x}")
|
||||||
|
print(f" Shutter: {'OPEN' if value & 0x01 else 'CLOSED'}")
|
||||||
|
print(f" Current Mode: {'ON' if value & 0x04 else 'OFF'}")
|
||||||
|
print(f" Remote Enable: {'ON' if value & 0x08 else 'OFF'}")
|
||||||
|
print(f" Analog Enable: {'ON' if value & 0x10 else 'OFF'}")
|
||||||
|
print(f" Keyswitch: {'ON' if value & 0x20 else 'OFF'}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("✗ Failed to read PS glue out")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_read_current_adc(port):
|
||||||
|
"""Test reading current ADC."""
|
||||||
|
print("\nTest 2: Reading current ADC (ADS7828, channel 0x84)...")
|
||||||
|
value = nxp_read(port, ADDR_ADS7828, CHAN_CURRENT_ACTUAL, 1, 2)
|
||||||
|
if value is not None:
|
||||||
|
print(f"✓ Current ADC raw value: 0x{value:04x} ({value})")
|
||||||
|
scaled = value * 0.000244140625
|
||||||
|
print(f" Scaled value: {scaled:.6f}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("✗ Failed to read current ADC")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_write_current_zero(port):
|
||||||
|
"""Test setting current to zero."""
|
||||||
|
print("\nTest 3: Setting current to 0 (X9119 @ 0x52, cmd 0xa0)...")
|
||||||
|
success = nxp_write(port, 0x52, 0xa0, 0, 2)
|
||||||
|
if success:
|
||||||
|
print("✓ Current set to 0 command sent")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("✗ Failed to set current")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_read_status_bits(port):
|
||||||
|
"""Test reading all status bits from PS glue output."""
|
||||||
|
print("\nTest 4: Reading all control status bits...")
|
||||||
|
# Read current state
|
||||||
|
current_state = nxp_read(port, ADDR_PCA9555_PS_GLUE_OUT, 0x02, 1, 1)
|
||||||
|
if current_state is None:
|
||||||
|
print("✗ Failed to read current state")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f" Current state: 0x{current_state:02x}")
|
||||||
|
print(f" Note: Shutter bit status: {'SET' if current_state & 0x01 else 'CLEAR'} (manual shutter - not controlled)")
|
||||||
|
print("✓ Successfully read all status bits")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run all tests."""
|
||||||
|
print("=" * 60)
|
||||||
|
print("Genesis SLM MX 532 - Serial Communication Test")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Open serial port
|
||||||
|
port = test_serial_connection()
|
||||||
|
if not port:
|
||||||
|
print("\nTest FAILED: Cannot open serial port")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Run tests
|
||||||
|
tests_passed = 0
|
||||||
|
tests_total = 4
|
||||||
|
|
||||||
|
if test_read_ps_glue_out(port):
|
||||||
|
tests_passed += 1
|
||||||
|
|
||||||
|
if test_read_current_adc(port):
|
||||||
|
tests_passed += 1
|
||||||
|
|
||||||
|
if test_write_current_zero(port):
|
||||||
|
tests_passed += 1
|
||||||
|
|
||||||
|
if test_read_status_bits(port):
|
||||||
|
tests_passed += 1
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print(f"Test Summary: {tests_passed}/{tests_total} tests passed")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
if tests_passed == tests_total:
|
||||||
|
print("✓ All tests PASSED - Communication working!")
|
||||||
|
print("\nYou can now run the GUI application:")
|
||||||
|
print(" ./genesis_laser_gui.py")
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
print("✗ Some tests FAILED - Check connections")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
finally:
|
||||||
|
port.close()
|
||||||
|
print("\nSerial port closed.")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
Executable
+237
@@ -0,0 +1,237 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Genesis Laser Protocol Test Script
|
||||||
|
===================================
|
||||||
|
|
||||||
|
Simple command-line script to test NXP I2C-over-serial communication
|
||||||
|
with the Genesis SLM MX 532 laser.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python test_genesis_protocol.py [port] [baudrate]
|
||||||
|
|
||||||
|
Example:
|
||||||
|
python test_genesis_protocol.py /dev/ttyUSB0 9600
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import serial
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class NXPProtocolTester:
|
||||||
|
"""Simple NXP I2C protocol tester"""
|
||||||
|
|
||||||
|
NXP_START = 0x53
|
||||||
|
NXP_STOP = 0x50
|
||||||
|
|
||||||
|
def __init__(self, port: str = "/dev/ttyUSB0", baudrate: int = 9600):
|
||||||
|
self.port = serial.Serial(
|
||||||
|
port=port,
|
||||||
|
baudrate=baudrate,
|
||||||
|
bytesize=serial.EIGHTBITS,
|
||||||
|
parity=serial.PARITY_NONE,
|
||||||
|
stopbits=serial.STOPBITS_ONE,
|
||||||
|
timeout=1.0
|
||||||
|
)
|
||||||
|
time.sleep(0.1)
|
||||||
|
print(f"Connected to {port} at {baudrate} baud")
|
||||||
|
|
||||||
|
def send_packet(self, i2c_addr: int, cmd: bytes, data: bytes = b''):
|
||||||
|
"""Send NXP I2C write packet"""
|
||||||
|
if not isinstance(cmd, bytes):
|
||||||
|
cmd = bytes([cmd])
|
||||||
|
if not isinstance(data, bytes):
|
||||||
|
data = bytes(data)
|
||||||
|
|
||||||
|
length = len(cmd) + len(data)
|
||||||
|
packet = bytes([self.NXP_START, i2c_addr, length]) + cmd + data + bytes([self.NXP_STOP])
|
||||||
|
|
||||||
|
print(f"TX: {' '.join(f'{b:02x}' for b in packet)}")
|
||||||
|
self.port.write(packet)
|
||||||
|
|
||||||
|
def read_packet(self, i2c_addr_write: int, cmd: bytes, cmd_len: int, data_len: int) -> Optional[bytes]:
|
||||||
|
"""Send NXP I2C read packet and return data"""
|
||||||
|
if not isinstance(cmd, bytes):
|
||||||
|
cmd = bytes([cmd])
|
||||||
|
|
||||||
|
i2c_addr_read = i2c_addr_write | 0x01
|
||||||
|
|
||||||
|
packet_write = bytes([self.NXP_START, i2c_addr_write, cmd_len]) + cmd
|
||||||
|
packet_read = bytes([self.NXP_START, i2c_addr_read, data_len, self.NXP_STOP])
|
||||||
|
packet = packet_write + packet_read
|
||||||
|
|
||||||
|
print(f"TX: {' '.join(f'{b:02x}' for b in packet)}")
|
||||||
|
self.port.write(packet)
|
||||||
|
|
||||||
|
time.sleep(0.05)
|
||||||
|
response = self.port.read(data_len)
|
||||||
|
|
||||||
|
if response:
|
||||||
|
print(f"RX: {' '.join(f'{b:02x}' for b in response)}")
|
||||||
|
return response
|
||||||
|
else:
|
||||||
|
print("RX: (no response)")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def test_x9119_current(self, value: int = 100):
|
||||||
|
"""Test X9119 current control"""
|
||||||
|
print(f"\n=== Testing X9119 Current Control (value={value}) ===")
|
||||||
|
|
||||||
|
# X9119 current control at 0x52
|
||||||
|
# Command 0xa0, 2 bytes data (10-bit value)
|
||||||
|
value = max(0, min(1023, value))
|
||||||
|
msb = (value >> 8) & 0x03
|
||||||
|
lsb = value & 0xFF
|
||||||
|
|
||||||
|
self.send_packet(0x52, bytes([0xa0]), bytes([msb, lsb]))
|
||||||
|
print("Current command sent")
|
||||||
|
|
||||||
|
def test_pca9555_shutter(self, state: bool):
|
||||||
|
"""Test PCA9555 shutter control"""
|
||||||
|
print(f"\n=== Testing PCA9555 Shutter Control (state={'OPEN' if state else 'CLOSED'}) ===")
|
||||||
|
|
||||||
|
# Read current output port 0 value
|
||||||
|
print("Reading current port value...")
|
||||||
|
current = self.read_packet(0x4a, bytes([0x02]), 1, 1)
|
||||||
|
|
||||||
|
if current and len(current) == 1:
|
||||||
|
current_value = current[0]
|
||||||
|
print(f"Current port value: 0x{current_value:02x}")
|
||||||
|
|
||||||
|
# Modify bit 0 (shutter)
|
||||||
|
if state:
|
||||||
|
new_value = current_value | 0x01
|
||||||
|
else:
|
||||||
|
new_value = current_value & ~0x01
|
||||||
|
|
||||||
|
print(f"New port value: 0x{new_value:02x}")
|
||||||
|
|
||||||
|
# Write back
|
||||||
|
self.send_packet(0x4a, bytes([0x02]), bytes([new_value]))
|
||||||
|
print("Shutter command sent")
|
||||||
|
else:
|
||||||
|
print("Failed to read current port value")
|
||||||
|
|
||||||
|
def test_ads7828_current_reading(self):
|
||||||
|
"""Test ADS7828 current reading"""
|
||||||
|
print(f"\n=== Testing ADS7828 Current Reading ===")
|
||||||
|
|
||||||
|
# ADS7828 at 0x90/0x91
|
||||||
|
# Command byte: 0x80 (single-ended) | (channel << 4) | 0x0c (internal ref)
|
||||||
|
# Reading channel 0
|
||||||
|
cmd_byte = 0x80 | (0 << 4) | 0x0c
|
||||||
|
|
||||||
|
data = self.read_packet(0x90, bytes([cmd_byte]), 1, 2)
|
||||||
|
|
||||||
|
if data and len(data) == 2:
|
||||||
|
value = (data[0] << 8) | data[1]
|
||||||
|
value = (value >> 4) & 0x0FFF
|
||||||
|
print(f"Current reading: {value} counts (0x{value:03x})")
|
||||||
|
else:
|
||||||
|
print("Failed to read current")
|
||||||
|
|
||||||
|
def test_pca9555_read_ports(self):
|
||||||
|
"""Test reading all PCA9555 I/O expander ports"""
|
||||||
|
print(f"\n=== Testing PCA9555 Port Reads ===")
|
||||||
|
|
||||||
|
devices = [
|
||||||
|
(0x4a, "Main DIO (0x14a)"),
|
||||||
|
(0x48, "PS Glue (0x148)"),
|
||||||
|
(0x44, "Head DIO (0x144)"),
|
||||||
|
(0x40, "LDD Control (0x140)"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for addr, name in devices:
|
||||||
|
print(f"\n{name}:")
|
||||||
|
for port in range(8):
|
||||||
|
data = self.read_packet(addr, bytes([port]), 1, 1)
|
||||||
|
if data and len(data) == 1:
|
||||||
|
print(f" Register 0x{port:02x}: 0x{data[0]:02x} (0b{data[0]:08b})")
|
||||||
|
else:
|
||||||
|
print(f" Register 0x{port:02x}: read failed")
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Close serial port"""
|
||||||
|
self.port.close()
|
||||||
|
print("\nConnection closed")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main test function"""
|
||||||
|
# Parse command line arguments
|
||||||
|
port = sys.argv[1] if len(sys.argv) > 1 else "/dev/ttyUSB0"
|
||||||
|
baudrate = int(sys.argv[2]) if len(sys.argv) > 2 else 9600
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create tester
|
||||||
|
tester = NXPProtocolTester(port, baudrate)
|
||||||
|
|
||||||
|
# Menu
|
||||||
|
while True:
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("Genesis Laser Protocol Test Menu")
|
||||||
|
print("="*60)
|
||||||
|
print("1. Test X9119 current control (set to 100)")
|
||||||
|
print("2. Test PCA9555 shutter OPEN")
|
||||||
|
print("3. Test PCA9555 shutter CLOSE")
|
||||||
|
print("4. Test ADS7828 current reading")
|
||||||
|
print("5. Test read all PCA9555 ports")
|
||||||
|
print("6. Set current to 0 (safe state)")
|
||||||
|
print("7. Emergency stop (shutter close + current 0)")
|
||||||
|
print("8. Custom X9119 current value")
|
||||||
|
print("0. Exit")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
choice = input("Enter choice: ").strip()
|
||||||
|
|
||||||
|
if choice == "1":
|
||||||
|
tester.test_x9119_current(100)
|
||||||
|
|
||||||
|
elif choice == "2":
|
||||||
|
tester.test_pca9555_shutter(True)
|
||||||
|
|
||||||
|
elif choice == "3":
|
||||||
|
tester.test_pca9555_shutter(False)
|
||||||
|
|
||||||
|
elif choice == "4":
|
||||||
|
tester.test_ads7828_current_reading()
|
||||||
|
|
||||||
|
elif choice == "5":
|
||||||
|
tester.test_pca9555_read_ports()
|
||||||
|
|
||||||
|
elif choice == "6":
|
||||||
|
print("\n=== Setting current to 0 ===")
|
||||||
|
tester.test_x9119_current(0)
|
||||||
|
|
||||||
|
elif choice == "7":
|
||||||
|
print("\n=== EMERGENCY STOP ===")
|
||||||
|
tester.test_pca9555_shutter(False)
|
||||||
|
time.sleep(0.1)
|
||||||
|
tester.test_x9119_current(0)
|
||||||
|
print("Emergency stop complete")
|
||||||
|
|
||||||
|
elif choice == "8":
|
||||||
|
try:
|
||||||
|
value = int(input("Enter current value (0-1023): "))
|
||||||
|
tester.test_x9119_current(value)
|
||||||
|
except ValueError:
|
||||||
|
print("Invalid value")
|
||||||
|
|
||||||
|
elif choice == "0":
|
||||||
|
break
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("Invalid choice")
|
||||||
|
|
||||||
|
tester.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for the new rotated AoI bounding box approach.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
from scanning.sc3_scan_model import SC3ScanModel
|
||||||
|
|
||||||
|
def test_rotated_aoi():
|
||||||
|
"""Test the rotated AoI bounding box implementation."""
|
||||||
|
|
||||||
|
# Create scan model
|
||||||
|
model = SC3ScanModel()
|
||||||
|
|
||||||
|
# Configure scan parameters
|
||||||
|
model.x_origin = Decimal('50.0')
|
||||||
|
model.y_origin = Decimal('35.0')
|
||||||
|
model.x_delta = Decimal('10.0')
|
||||||
|
model.y_delta = Decimal('5.0')
|
||||||
|
model.row_spacing = Decimal('1.0')
|
||||||
|
model.laser_frequency = Decimal('2000.0')
|
||||||
|
model.scan_velocity = Decimal('100.0')
|
||||||
|
model.scan_angles = 4 # 0, 45, 90, 135 degrees
|
||||||
|
|
||||||
|
print("Scan Model Configuration:")
|
||||||
|
print(f" AoI Origin: ({model.x_origin}, {model.y_origin})")
|
||||||
|
print(f" AoI Size: {model.x_delta} x {model.y_delta}")
|
||||||
|
print(f" Row Spacing: {model.row_spacing}")
|
||||||
|
print(f" Optical Origin: ({model.optical_x_origin}, {model.optical_y_origin})")
|
||||||
|
print(f" Scan Angles: {model.get_angle_list()}")
|
||||||
|
print(f" Rows Required: {model.rows_required}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Verify zero-angle scan (should be horizontal lines)
|
||||||
|
print("Zero-Angle Scan (first 3 lines):")
|
||||||
|
for i, coords in enumerate(model.scan_coordinates[:3]):
|
||||||
|
print(f" Line {i}: ({coords[0]}, {coords[1]}) -> ({coords[2]}, {coords[3]})")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Verify rotated scans
|
||||||
|
print("Rotated Scans (first line of each angle):")
|
||||||
|
for angle_idx, angle_deg in enumerate(model.get_angle_list()):
|
||||||
|
if angle_idx < len(model.rotated_coordinates):
|
||||||
|
rotated_scan = model.rotated_coordinates[angle_idx]
|
||||||
|
if rotated_scan:
|
||||||
|
coords = rotated_scan[0]
|
||||||
|
print(f" Angle {angle_deg}°: ({coords[0]:.3f}, {coords[1]:.3f}) -> ({coords[2]:.3f}, {coords[3]:.3f})")
|
||||||
|
print(f" Number of lines: {len(rotated_scan)}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Verify that scans are horizontal (+x direction)
|
||||||
|
print("Verifying horizontal scan direction:")
|
||||||
|
for angle_idx, angle_deg in enumerate(model.get_angle_list()):
|
||||||
|
if angle_idx < len(model.rotated_coordinates):
|
||||||
|
rotated_scan = model.rotated_coordinates[angle_idx]
|
||||||
|
all_horizontal = True
|
||||||
|
for coords in rotated_scan:
|
||||||
|
# Check if y_start == y_end (horizontal line)
|
||||||
|
if abs(coords[1] - coords[3]) > 1e-10:
|
||||||
|
all_horizontal = False
|
||||||
|
break
|
||||||
|
status = "✓" if all_horizontal else "✗"
|
||||||
|
print(f" Angle {angle_deg}°: {status} All lines horizontal")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("Test completed successfully!")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_rotated_aoi()
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
"""
|
||||||
|
Test script to examine raw status update packets from the BBD202 controller.
|
||||||
|
|
||||||
|
This script:
|
||||||
|
1. Connects to the controller at a low level
|
||||||
|
2. Sends HW_START_UPDATEMSGS to enable automatic status updates
|
||||||
|
3. Captures and displays raw packets to verify the format matches our parsing
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import struct
|
||||||
|
from hardware.bbd202 import MotionController, MsgId
|
||||||
|
|
||||||
|
|
||||||
|
def hexdump(data: bytes, prefix: str = "") -> str:
|
||||||
|
"""Format bytes as hex dump."""
|
||||||
|
hex_str = " ".join(f"{b:02X}" for b in data)
|
||||||
|
ascii_str = "".join(chr(b) if 32 <= b < 127 else "." for b in data)
|
||||||
|
return f"{prefix}{hex_str} |{ascii_str}|"
|
||||||
|
|
||||||
|
|
||||||
|
def decode_status_update(data: bytes) -> dict:
|
||||||
|
"""Decode a MOT_GET_USTATUSUPDATE message data payload."""
|
||||||
|
if len(data) < 14:
|
||||||
|
return {"error": f"Data too short: {len(data)} bytes, expected 14"}
|
||||||
|
|
||||||
|
chan_ident = struct.unpack('<H', data[0:2])[0]
|
||||||
|
position_counts = struct.unpack('<i', data[2:6])[0]
|
||||||
|
velocity_counts = struct.unpack('<H', data[6:8])[0]
|
||||||
|
motor_current = struct.unpack('<H', data[8:10])[0]
|
||||||
|
status_bits = struct.unpack('<I', data[10:14])[0]
|
||||||
|
|
||||||
|
# Convert to physical units
|
||||||
|
ENCODER_COUNTS_PER_MM = 20000
|
||||||
|
position_mm = position_counts / ENCODER_COUNTS_PER_MM
|
||||||
|
|
||||||
|
return {
|
||||||
|
"chan_ident": chan_ident,
|
||||||
|
"position_counts": position_counts,
|
||||||
|
"position_mm": position_mm,
|
||||||
|
"velocity_counts": velocity_counts,
|
||||||
|
"motor_current": motor_current,
|
||||||
|
"status_bits": status_bits,
|
||||||
|
"status_hex": f"0x{status_bits:08X}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 70)
|
||||||
|
print("BBD202 Status Update Packet Test")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
mc = None
|
||||||
|
try:
|
||||||
|
# Connect to controller WITHOUT enabling updates yet
|
||||||
|
print("\nConnecting to BBD202 controller...")
|
||||||
|
mc = MotionController()
|
||||||
|
mc.connect(enable_updates=False) # Don't auto-enable updates
|
||||||
|
print("Connected!")
|
||||||
|
|
||||||
|
# Get hardware info
|
||||||
|
try:
|
||||||
|
hw_info = mc.get_hw_info()
|
||||||
|
print(f"Hardware: {hw_info['model']} (S/N: {hw_info['serial_number']})")
|
||||||
|
print(f"Firmware: {hw_info['firmware_version']}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Could not get hardware info: {e}")
|
||||||
|
|
||||||
|
# Clear any pending messages
|
||||||
|
print("\nClearing RX queue...")
|
||||||
|
msgs = mc.get_all_messages()
|
||||||
|
print(f"Cleared {len(msgs)} pending messages")
|
||||||
|
|
||||||
|
# Now enable status updates
|
||||||
|
print("\n" + "-" * 70)
|
||||||
|
print("Sending HW_START_UPDATEMSGS to enable automatic status updates...")
|
||||||
|
print("-" * 70)
|
||||||
|
mc.start_update_messages()
|
||||||
|
|
||||||
|
# Wait a moment for updates to start arriving
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# Collect messages for a few seconds
|
||||||
|
print("\nCollecting status update packets for 3 seconds...")
|
||||||
|
print("(Looking for MOT_GET_USTATUSUPDATE = 0x0491)")
|
||||||
|
print()
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
update_count = 0
|
||||||
|
other_count = 0
|
||||||
|
|
||||||
|
while time.time() - start_time < 3.0:
|
||||||
|
msg = mc.get_message(timeout=0.1)
|
||||||
|
if msg:
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
|
||||||
|
if msg.msg_id == MsgId.MOT_GET_USTATUSUPDATE:
|
||||||
|
update_count += 1
|
||||||
|
source_name = "X-axis" if msg.source == 0x21 else "Y-axis" if msg.source == 0x22 else f"0x{msg.source:02X}"
|
||||||
|
|
||||||
|
print(f"[{elapsed:5.2f}s] MOT_GET_USTATUSUPDATE from {source_name}")
|
||||||
|
print(f" Raw ({len(msg.raw)} bytes): {hexdump(msg.raw)}")
|
||||||
|
print(f" Data ({len(msg.data)} bytes): {hexdump(msg.data)}")
|
||||||
|
|
||||||
|
decoded = decode_status_update(msg.data)
|
||||||
|
if "error" in decoded:
|
||||||
|
print(f" DECODE ERROR: {decoded['error']}")
|
||||||
|
else:
|
||||||
|
print(f" Decoded: chan={decoded['chan_ident']}, "
|
||||||
|
f"pos={decoded['position_mm']:.4f}mm ({decoded['position_counts']} counts), "
|
||||||
|
f"vel={decoded['velocity_counts']}, cur={decoded['motor_current']}, "
|
||||||
|
f"status={decoded['status_hex']}")
|
||||||
|
print()
|
||||||
|
else:
|
||||||
|
other_count += 1
|
||||||
|
msg_name = MsgId(msg.msg_id).name if msg.msg_id in [m.value for m in MsgId] else f"0x{msg.msg_id:04X}"
|
||||||
|
print(f"[{elapsed:5.2f}s] Other message: {msg_name} from 0x{msg.source:02X}")
|
||||||
|
print(f" Raw: {hexdump(msg.raw)}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("-" * 70)
|
||||||
|
print(f"Summary: Received {update_count} status updates, {other_count} other messages")
|
||||||
|
print("-" * 70)
|
||||||
|
|
||||||
|
if update_count == 0:
|
||||||
|
print("\n*** WARNING: No status updates received! ***")
|
||||||
|
print("Possible causes:")
|
||||||
|
print(" 1. HW_START_UPDATEMSGS not being processed")
|
||||||
|
print(" 2. Controller firmware doesn't support automatic updates")
|
||||||
|
print(" 3. Updates are being sent but not parsed correctly")
|
||||||
|
print("\nTrying to manually request a status update...")
|
||||||
|
|
||||||
|
# Try requesting status update manually
|
||||||
|
mc.request_status_update(mc.DEST_X_AXIS)
|
||||||
|
mc.request_status_update(mc.DEST_Y_AXIS)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
msgs = mc.get_all_messages()
|
||||||
|
print(f"\nReceived {len(msgs)} messages after manual request:")
|
||||||
|
for msg in msgs:
|
||||||
|
msg_name = MsgId(msg.msg_id).name if msg.msg_id in [m.value for m in MsgId] else f"0x{msg.msg_id:04X}"
|
||||||
|
print(f" {msg_name} from 0x{msg.source:02X}: {hexdump(msg.raw)}")
|
||||||
|
|
||||||
|
# Also check cached positions
|
||||||
|
print("\n" + "-" * 70)
|
||||||
|
print("Cached positions in driver:")
|
||||||
|
print("-" * 70)
|
||||||
|
print(f" X position: {mc.get_stage_position_x()}")
|
||||||
|
print(f" Y position: {mc.get_stage_position_y()}")
|
||||||
|
print(f" X encoder: {mc.encoder_count_x}")
|
||||||
|
print(f" Y encoder: {mc.encoder_count_y}")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n\nTest interrupted by user")
|
||||||
|
return 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n*** ERROR: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return 1
|
||||||
|
finally:
|
||||||
|
if mc is not None:
|
||||||
|
print("\nDisconnecting from controller...")
|
||||||
|
mc.disconnect()
|
||||||
|
print("Disconnected")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Executable
+71
@@ -0,0 +1,71 @@
|
|||||||
|
#!/opt/srasenv/bin/python3
|
||||||
|
"""
|
||||||
|
Test temperature scaling calculations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
ADC_TO_VOLTS = 0.000244140625
|
||||||
|
STEINHART_A = 0.0011279
|
||||||
|
STEINHART_B = 0.00023429
|
||||||
|
STEINHART_C = 8.7298e-8
|
||||||
|
|
||||||
|
def test_main_temp_calculation(raw_adc):
|
||||||
|
"""Test main temperature calculation with different circuit assumptions."""
|
||||||
|
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f"Testing Main Temperature with RAW ADC = {raw_adc}")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
|
||||||
|
v_thermistor = raw_adc * ADC_TO_VOLTS
|
||||||
|
print(f"V_thermistor = {v_thermistor:.6f} V")
|
||||||
|
|
||||||
|
# Try different circuit configurations
|
||||||
|
configs = [
|
||||||
|
(10000, 5.0, "10kΩ series, 5V ref"),
|
||||||
|
(10000, 3.3, "10kΩ series, 3.3V ref"),
|
||||||
|
(100000, 5.0, "100kΩ series, 5V ref"),
|
||||||
|
(10000, 2.5, "10kΩ series, 2.5V ref"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for r_series, vref, desc in configs:
|
||||||
|
print(f"\n{desc}:")
|
||||||
|
print(f" R_series = {r_series} Ω, Vref = {vref} V")
|
||||||
|
|
||||||
|
if v_thermistor >= vref:
|
||||||
|
print(f" ERROR: V_thermistor >= Vref")
|
||||||
|
continue
|
||||||
|
|
||||||
|
r_thermistor = r_series * v_thermistor / (vref - v_thermistor)
|
||||||
|
print(f" R_thermistor = {r_thermistor:.2f} Ω")
|
||||||
|
|
||||||
|
if r_thermistor <= 0:
|
||||||
|
print(f" ERROR: Invalid resistance")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Steinhart-Hart
|
||||||
|
ln_r = math.log(r_thermistor)
|
||||||
|
inv_t = STEINHART_A + STEINHART_B * ln_r + STEINHART_C * (ln_r ** 3)
|
||||||
|
temp_k = 1.0 / inv_t
|
||||||
|
temp_c = temp_k - 273.15
|
||||||
|
|
||||||
|
print(f" ln(R) = {ln_r:.6f}")
|
||||||
|
print(f" Temperature = {temp_c:.2f} °C")
|
||||||
|
|
||||||
|
def test_current_scaling(raw_adc):
|
||||||
|
"""Test current scaling."""
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f"Testing Current Scaling with RAW ADC = {raw_adc}")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
|
||||||
|
current = raw_adc * 12.0 * ADC_TO_VOLTS
|
||||||
|
print(f"Current = {current:.6f} A")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Test with the reported raw values
|
||||||
|
test_main_temp_calculation(2)
|
||||||
|
test_main_temp_calculation(3)
|
||||||
|
test_current_scaling(0)
|
||||||
|
test_current_scaling(100)
|
||||||
|
test_current_scaling(4095)
|
||||||
Executable
+828
@@ -0,0 +1,828 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Genesis SLM MX 532 Laser Control Application
|
||||||
|
=============================================
|
||||||
|
|
||||||
|
This application provides comprehensive control of a Genesis SLM MX 532 laser
|
||||||
|
over serial port using NXP I2C-over-serial protocol.
|
||||||
|
|
||||||
|
Protocol Overview:
|
||||||
|
-----------------
|
||||||
|
The laser uses NXP I2C-over-serial protocol with packet format:
|
||||||
|
[0x53] [I2C_ADDR] [LENGTH] [COMMAND_BYTES] [DATA_BYTES] [0x50]
|
||||||
|
|
||||||
|
For reads:
|
||||||
|
[0x53] [ADDR_WRITE] [CMD_LEN] [CMD] [0x53] [ADDR_READ] [DATA_LEN] [0x50]
|
||||||
|
|
||||||
|
I2C Devices:
|
||||||
|
-----------
|
||||||
|
- X9119 digital potentiometer at 0x152 - laser current control
|
||||||
|
- PCA9555 I/O expander at 0x14a - digital I/O
|
||||||
|
- AD5254 digital potentiometer at 0x158 - limits control
|
||||||
|
- ADS7828 ADC at 0x190 - sensor readings
|
||||||
|
- M24C64 EEPROM at 0x2a4 - configuration storage
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
------
|
||||||
|
python genesis_laser_control.py
|
||||||
|
|
||||||
|
Author: Claude
|
||||||
|
Date: 2026-01-24
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import struct
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional, List, Tuple
|
||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
|
import serial
|
||||||
|
from serial.tools import list_ports
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||||
|
QTabWidget, QLabel, QSlider, QPushButton, QSpinBox, QCheckBox,
|
||||||
|
QComboBox, QTextEdit, QLineEdit, QGroupBox, QGridLayout,
|
||||||
|
QMessageBox, QStatusBar, QProgressBar
|
||||||
|
)
|
||||||
|
from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QSettings
|
||||||
|
from PyQt6.QtGui import QFont, QPalette, QColor
|
||||||
|
|
||||||
|
# Import core hardware control classes
|
||||||
|
from hardware.genesis_core import (
|
||||||
|
I2CAddress, PCA9555Register, ControlBitmask,
|
||||||
|
SerialComm, I2CProtocol, I2CDevices, LaserControl
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Main Window GUI
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
class MainWindow(QMainWindow):
|
||||||
|
"""Main application window with tabbed interface"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
# Initialize backend
|
||||||
|
self.serial_comm = SerialComm()
|
||||||
|
self.i2c_protocol = I2CProtocol(self.serial_comm)
|
||||||
|
self.i2c_devices = I2CDevices(self.i2c_protocol)
|
||||||
|
self.laser_control = LaserControl(self.i2c_devices)
|
||||||
|
|
||||||
|
# Settings for persistence
|
||||||
|
self.settings = QSettings("Genesis", "LaserControl")
|
||||||
|
|
||||||
|
# Monitoring timer
|
||||||
|
self.monitor_timer = QTimer()
|
||||||
|
self.monitor_timer.timeout.connect(self.update_monitoring)
|
||||||
|
self.monitor_enabled = False
|
||||||
|
|
||||||
|
# Initialize UI
|
||||||
|
self.init_ui()
|
||||||
|
self.apply_stylesheet()
|
||||||
|
|
||||||
|
# Restore settings
|
||||||
|
self.restore_settings()
|
||||||
|
|
||||||
|
# Initial state: all controls disabled
|
||||||
|
self.update_control_states()
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
"""Initialize user interface"""
|
||||||
|
self.setWindowTitle("Genesis SLM MX 532 Laser Control")
|
||||||
|
self.setMinimumSize(800, 600)
|
||||||
|
|
||||||
|
# Central widget with tabs
|
||||||
|
central_widget = QWidget()
|
||||||
|
self.setCentralWidget(central_widget)
|
||||||
|
|
||||||
|
layout = QVBoxLayout(central_widget)
|
||||||
|
|
||||||
|
# Tab widget
|
||||||
|
self.tabs = QTabWidget()
|
||||||
|
layout.addWidget(self.tabs)
|
||||||
|
|
||||||
|
# Create tabs
|
||||||
|
self.tab_basic = self.create_basic_controls_tab()
|
||||||
|
self.tab_monitoring = self.create_monitoring_tab()
|
||||||
|
self.tab_advanced = self.create_advanced_tab()
|
||||||
|
self.tab_config = self.create_configuration_tab()
|
||||||
|
|
||||||
|
self.tabs.addTab(self.tab_basic, "Basic Controls")
|
||||||
|
self.tabs.addTab(self.tab_monitoring, "Monitoring")
|
||||||
|
self.tabs.addTab(self.tab_advanced, "Advanced")
|
||||||
|
self.tabs.addTab(self.tab_config, "Configuration")
|
||||||
|
|
||||||
|
# Status bar
|
||||||
|
self.status_bar = QStatusBar()
|
||||||
|
self.setStatusBar(self.status_bar)
|
||||||
|
self.status_bar.showMessage("Disconnected")
|
||||||
|
|
||||||
|
def create_basic_controls_tab(self) -> QWidget:
|
||||||
|
"""Create basic controls tab"""
|
||||||
|
tab = QWidget()
|
||||||
|
layout = QVBoxLayout(tab)
|
||||||
|
|
||||||
|
# Current control group
|
||||||
|
current_group = QGroupBox("Current Control")
|
||||||
|
current_layout = QGridLayout()
|
||||||
|
|
||||||
|
current_layout.addWidget(QLabel("Current Command (0-1023):"), 0, 0)
|
||||||
|
|
||||||
|
self.current_slider = QSlider(Qt.Orientation.Horizontal)
|
||||||
|
self.current_slider.setMinimum(0)
|
||||||
|
self.current_slider.setMaximum(1023)
|
||||||
|
self.current_slider.setValue(0)
|
||||||
|
self.current_slider.setToolTip("Set laser current (0-1023)")
|
||||||
|
self.current_slider.valueChanged.connect(self.on_current_changed)
|
||||||
|
current_layout.addWidget(self.current_slider, 0, 1)
|
||||||
|
|
||||||
|
self.current_spinbox = QSpinBox()
|
||||||
|
self.current_spinbox.setMinimum(0)
|
||||||
|
self.current_spinbox.setMaximum(1023)
|
||||||
|
self.current_spinbox.setValue(0)
|
||||||
|
self.current_spinbox.valueChanged.connect(self.current_slider.setValue)
|
||||||
|
current_layout.addWidget(self.current_spinbox, 0, 2)
|
||||||
|
|
||||||
|
self.current_percent_label = QLabel("0.0%")
|
||||||
|
current_layout.addWidget(self.current_percent_label, 0, 3)
|
||||||
|
|
||||||
|
current_group.setLayout(current_layout)
|
||||||
|
layout.addWidget(current_group)
|
||||||
|
|
||||||
|
# Power control group
|
||||||
|
power_group = QGroupBox("Power Control")
|
||||||
|
power_layout = QGridLayout()
|
||||||
|
|
||||||
|
power_layout.addWidget(QLabel("Power Command (0-1023):"), 0, 0)
|
||||||
|
|
||||||
|
self.power_slider = QSlider(Qt.Orientation.Horizontal)
|
||||||
|
self.power_slider.setMinimum(0)
|
||||||
|
self.power_slider.setMaximum(1023)
|
||||||
|
self.power_slider.setValue(0)
|
||||||
|
self.power_slider.setToolTip("Set laser power command (0-1023)")
|
||||||
|
self.power_slider.valueChanged.connect(self.on_power_changed)
|
||||||
|
power_layout.addWidget(self.power_slider, 0, 1)
|
||||||
|
|
||||||
|
self.power_spinbox = QSpinBox()
|
||||||
|
self.power_spinbox.setMinimum(0)
|
||||||
|
self.power_spinbox.setMaximum(1023)
|
||||||
|
self.power_spinbox.setValue(0)
|
||||||
|
self.power_spinbox.valueChanged.connect(self.power_slider.setValue)
|
||||||
|
power_layout.addWidget(self.power_spinbox, 0, 2)
|
||||||
|
|
||||||
|
power_group.setLayout(power_layout)
|
||||||
|
layout.addWidget(power_group)
|
||||||
|
|
||||||
|
# Toggle controls group
|
||||||
|
toggle_group = QGroupBox("Digital Controls")
|
||||||
|
toggle_layout = QGridLayout()
|
||||||
|
|
||||||
|
self.shutter_btn = QPushButton("Shutter: CLOSED")
|
||||||
|
self.shutter_btn.setCheckable(True)
|
||||||
|
self.shutter_btn.setToolTip("Open/close laser shutter")
|
||||||
|
self.shutter_btn.clicked.connect(self.on_shutter_toggled)
|
||||||
|
toggle_layout.addWidget(self.shutter_btn, 0, 0)
|
||||||
|
|
||||||
|
self.keyswitch_btn = QPushButton("Keyswitch: OFF")
|
||||||
|
self.keyswitch_btn.setCheckable(True)
|
||||||
|
self.keyswitch_btn.setToolTip("Enable/disable keyswitch")
|
||||||
|
self.keyswitch_btn.clicked.connect(self.on_keyswitch_toggled)
|
||||||
|
toggle_layout.addWidget(self.keyswitch_btn, 0, 1)
|
||||||
|
|
||||||
|
self.remote_enable_btn = QPushButton("Remote: DISABLED")
|
||||||
|
self.remote_enable_btn.setCheckable(True)
|
||||||
|
self.remote_enable_btn.setToolTip("Enable/disable remote control")
|
||||||
|
self.remote_enable_btn.clicked.connect(self.on_remote_enable_toggled)
|
||||||
|
toggle_layout.addWidget(self.remote_enable_btn, 1, 0)
|
||||||
|
|
||||||
|
self.analog_enable_btn = QPushButton("Analog Input: DISABLED")
|
||||||
|
self.analog_enable_btn.setCheckable(True)
|
||||||
|
self.analog_enable_btn.setToolTip("Enable/disable analog input")
|
||||||
|
self.analog_enable_btn.clicked.connect(self.on_analog_enable_toggled)
|
||||||
|
toggle_layout.addWidget(self.analog_enable_btn, 1, 1)
|
||||||
|
|
||||||
|
self.current_mode_btn = QPushButton("Current Mode: OFF")
|
||||||
|
self.current_mode_btn.setCheckable(True)
|
||||||
|
self.current_mode_btn.setToolTip("Enable/disable current mode")
|
||||||
|
self.current_mode_btn.clicked.connect(self.on_current_mode_toggled)
|
||||||
|
toggle_layout.addWidget(self.current_mode_btn, 2, 0)
|
||||||
|
|
||||||
|
toggle_group.setLayout(toggle_layout)
|
||||||
|
layout.addWidget(toggle_group)
|
||||||
|
|
||||||
|
# Emergency stop
|
||||||
|
self.emergency_stop_btn = QPushButton("EMERGENCY STOP")
|
||||||
|
self.emergency_stop_btn.setStyleSheet("background-color: #ff0000; color: white; font-weight: bold; font-size: 14pt;")
|
||||||
|
self.emergency_stop_btn.clicked.connect(self.on_emergency_stop)
|
||||||
|
self.emergency_stop_btn.setEnabled(True) # Always enabled
|
||||||
|
layout.addWidget(self.emergency_stop_btn)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
|
||||||
|
return tab
|
||||||
|
|
||||||
|
def create_monitoring_tab(self) -> QWidget:
|
||||||
|
"""Create monitoring tab"""
|
||||||
|
tab = QWidget()
|
||||||
|
layout = QVBoxLayout(tab)
|
||||||
|
|
||||||
|
# Auto-refresh control
|
||||||
|
refresh_layout = QHBoxLayout()
|
||||||
|
self.auto_refresh_check = QCheckBox("Auto-refresh")
|
||||||
|
self.auto_refresh_check.stateChanged.connect(self.on_auto_refresh_toggled)
|
||||||
|
refresh_layout.addWidget(self.auto_refresh_check)
|
||||||
|
|
||||||
|
refresh_layout.addWidget(QLabel("Interval (ms):"))
|
||||||
|
self.refresh_interval_spin = QSpinBox()
|
||||||
|
self.refresh_interval_spin.setMinimum(100)
|
||||||
|
self.refresh_interval_spin.setMaximum(5000)
|
||||||
|
self.refresh_interval_spin.setValue(500)
|
||||||
|
self.refresh_interval_spin.setSingleStep(100)
|
||||||
|
refresh_layout.addWidget(self.refresh_interval_spin)
|
||||||
|
|
||||||
|
self.refresh_now_btn = QPushButton("Refresh Now")
|
||||||
|
self.refresh_now_btn.clicked.connect(self.update_monitoring)
|
||||||
|
refresh_layout.addWidget(self.refresh_now_btn)
|
||||||
|
|
||||||
|
refresh_layout.addStretch()
|
||||||
|
layout.addLayout(refresh_layout)
|
||||||
|
|
||||||
|
# Readings group
|
||||||
|
readings_group = QGroupBox("Sensor Readings")
|
||||||
|
readings_layout = QGridLayout()
|
||||||
|
|
||||||
|
readings_layout.addWidget(QLabel("Actual Current:"), 0, 0)
|
||||||
|
self.current_actual_label = QLabel("---")
|
||||||
|
readings_layout.addWidget(self.current_actual_label, 0, 1)
|
||||||
|
|
||||||
|
readings_layout.addWidget(QLabel("Interlock Status:"), 1, 0)
|
||||||
|
self.interlock_label = QLabel("---")
|
||||||
|
readings_layout.addWidget(self.interlock_label, 1, 1)
|
||||||
|
|
||||||
|
readings_layout.addWidget(QLabel("LDD Enable:"), 2, 0)
|
||||||
|
self.ldd_enable_label = QLabel("---")
|
||||||
|
readings_layout.addWidget(self.ldd_enable_label, 2, 1)
|
||||||
|
|
||||||
|
readings_layout.addWidget(QLabel("PS Glue In:"), 3, 0)
|
||||||
|
self.psglue_in_label = QLabel("---")
|
||||||
|
readings_layout.addWidget(self.psglue_in_label, 3, 1)
|
||||||
|
|
||||||
|
readings_layout.addWidget(QLabel("PS Glue Out:"), 4, 0)
|
||||||
|
self.psglue_out_label = QLabel("---")
|
||||||
|
readings_layout.addWidget(self.psglue_out_label, 4, 1)
|
||||||
|
|
||||||
|
readings_layout.addWidget(QLabel("Head DIO:"), 5, 0)
|
||||||
|
self.head_dio_label = QLabel("---")
|
||||||
|
readings_layout.addWidget(self.head_dio_label, 5, 1)
|
||||||
|
|
||||||
|
readings_group.setLayout(readings_layout)
|
||||||
|
layout.addWidget(readings_group)
|
||||||
|
|
||||||
|
# Laser info group
|
||||||
|
info_group = QGroupBox("Laser Information")
|
||||||
|
info_layout = QGridLayout()
|
||||||
|
|
||||||
|
info_layout.addWidget(QLabel("Model:"), 0, 0)
|
||||||
|
self.laser_model_label = QLabel("Genesis SLM MX 532")
|
||||||
|
info_layout.addWidget(self.laser_model_label, 0, 1)
|
||||||
|
|
||||||
|
info_layout.addWidget(QLabel("Wavelength:"), 1, 0)
|
||||||
|
self.wavelength_label = QLabel("532 nm")
|
||||||
|
info_layout.addWidget(self.wavelength_label, 1, 1)
|
||||||
|
|
||||||
|
info_group.setLayout(info_layout)
|
||||||
|
layout.addWidget(info_group)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
|
||||||
|
return tab
|
||||||
|
|
||||||
|
def create_advanced_tab(self) -> QWidget:
|
||||||
|
"""Create advanced tab"""
|
||||||
|
tab = QWidget()
|
||||||
|
layout = QVBoxLayout(tab)
|
||||||
|
|
||||||
|
# Raw I2C packet sender
|
||||||
|
packet_group = QGroupBox("Raw I2C Packet Sender")
|
||||||
|
packet_layout = QGridLayout()
|
||||||
|
|
||||||
|
packet_layout.addWidget(QLabel("I2C Address (hex):"), 0, 0)
|
||||||
|
self.i2c_addr_edit = QLineEdit("52")
|
||||||
|
self.i2c_addr_edit.setMaximumWidth(100)
|
||||||
|
packet_layout.addWidget(self.i2c_addr_edit, 0, 1)
|
||||||
|
|
||||||
|
packet_layout.addWidget(QLabel("Command (hex):"), 1, 0)
|
||||||
|
self.i2c_cmd_edit = QLineEdit("a0")
|
||||||
|
self.i2c_cmd_edit.setMaximumWidth(200)
|
||||||
|
packet_layout.addWidget(self.i2c_cmd_edit, 1, 1)
|
||||||
|
|
||||||
|
packet_layout.addWidget(QLabel("Data (hex):"), 2, 0)
|
||||||
|
self.i2c_data_edit = QLineEdit("00 00")
|
||||||
|
packet_layout.addWidget(self.i2c_data_edit, 2, 1)
|
||||||
|
|
||||||
|
self.send_packet_btn = QPushButton("Send Packet")
|
||||||
|
self.send_packet_btn.clicked.connect(self.on_send_raw_packet)
|
||||||
|
packet_layout.addWidget(self.send_packet_btn, 3, 0, 1, 2)
|
||||||
|
|
||||||
|
self.packet_response_label = QLabel("Response: ---")
|
||||||
|
packet_layout.addWidget(self.packet_response_label, 4, 0, 1, 2)
|
||||||
|
|
||||||
|
packet_group.setLayout(packet_layout)
|
||||||
|
layout.addWidget(packet_group)
|
||||||
|
|
||||||
|
# Packet log
|
||||||
|
log_group = QGroupBox("Packet Capture Log")
|
||||||
|
log_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
log_controls = QHBoxLayout()
|
||||||
|
self.clear_log_btn = QPushButton("Clear Log")
|
||||||
|
self.clear_log_btn.clicked.connect(self.on_clear_log)
|
||||||
|
log_controls.addWidget(self.clear_log_btn)
|
||||||
|
log_controls.addStretch()
|
||||||
|
log_layout.addLayout(log_controls)
|
||||||
|
|
||||||
|
self.packet_log_text = QTextEdit()
|
||||||
|
self.packet_log_text.setReadOnly(True)
|
||||||
|
self.packet_log_text.setFont(QFont("Courier", 9))
|
||||||
|
log_layout.addWidget(self.packet_log_text)
|
||||||
|
|
||||||
|
log_group.setLayout(log_layout)
|
||||||
|
layout.addWidget(log_group)
|
||||||
|
|
||||||
|
# Update log periodically
|
||||||
|
self.log_timer = QTimer()
|
||||||
|
self.log_timer.timeout.connect(self.update_packet_log)
|
||||||
|
self.log_timer.start(1000) # Update every second
|
||||||
|
|
||||||
|
return tab
|
||||||
|
|
||||||
|
def create_configuration_tab(self) -> QWidget:
|
||||||
|
"""Create configuration tab"""
|
||||||
|
tab = QWidget()
|
||||||
|
layout = QVBoxLayout(tab)
|
||||||
|
|
||||||
|
# Serial port configuration
|
||||||
|
serial_group = QGroupBox("Serial Port Configuration")
|
||||||
|
serial_layout = QGridLayout()
|
||||||
|
|
||||||
|
serial_layout.addWidget(QLabel("Port:"), 0, 0)
|
||||||
|
self.port_combo = QComboBox()
|
||||||
|
self.refresh_ports()
|
||||||
|
serial_layout.addWidget(self.port_combo, 0, 1)
|
||||||
|
|
||||||
|
self.refresh_ports_btn = QPushButton("Refresh Ports")
|
||||||
|
self.refresh_ports_btn.clicked.connect(self.refresh_ports)
|
||||||
|
serial_layout.addWidget(self.refresh_ports_btn, 0, 2)
|
||||||
|
|
||||||
|
serial_layout.addWidget(QLabel("Baud Rate:"), 1, 0)
|
||||||
|
self.baudrate_combo = QComboBox()
|
||||||
|
self.baudrate_combo.addItems(["9600", "19200", "38400", "57600", "115200"])
|
||||||
|
self.baudrate_combo.setCurrentText("9600")
|
||||||
|
serial_layout.addWidget(self.baudrate_combo, 1, 1)
|
||||||
|
|
||||||
|
self.connect_btn = QPushButton("Connect")
|
||||||
|
self.connect_btn.clicked.connect(self.on_connect_clicked)
|
||||||
|
serial_layout.addWidget(self.connect_btn, 2, 0, 1, 3)
|
||||||
|
|
||||||
|
serial_group.setLayout(serial_layout)
|
||||||
|
layout.addWidget(serial_group)
|
||||||
|
|
||||||
|
# DTR/RTS control
|
||||||
|
control_group = QGroupBox("Serial Control Lines")
|
||||||
|
control_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
self.dtr_check = QCheckBox("DTR (Data Terminal Ready)")
|
||||||
|
self.dtr_check.stateChanged.connect(self.on_dtr_changed)
|
||||||
|
control_layout.addWidget(self.dtr_check)
|
||||||
|
|
||||||
|
self.rts_check = QCheckBox("RTS (Request To Send)")
|
||||||
|
self.rts_check.stateChanged.connect(self.on_rts_changed)
|
||||||
|
control_layout.addWidget(self.rts_check)
|
||||||
|
|
||||||
|
control_group.setLayout(control_layout)
|
||||||
|
layout.addWidget(control_group)
|
||||||
|
|
||||||
|
# About section
|
||||||
|
about_group = QGroupBox("About")
|
||||||
|
about_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
about_text = QLabel(
|
||||||
|
"Genesis SLM MX 532 Laser Control\n\n"
|
||||||
|
"Protocol: NXP I2C-over-serial\n"
|
||||||
|
"Baud Rate: 9600 (default)\n\n"
|
||||||
|
"This application provides comprehensive control of the Genesis laser\n"
|
||||||
|
"using I2C devices over serial communication."
|
||||||
|
)
|
||||||
|
about_layout.addWidget(about_text)
|
||||||
|
|
||||||
|
about_group.setLayout(about_layout)
|
||||||
|
layout.addWidget(about_group)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
|
||||||
|
return tab
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
# Event Handlers - Basic Controls
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def on_current_changed(self, value: int):
|
||||||
|
"""Handle current slider change"""
|
||||||
|
self.current_spinbox.setValue(value)
|
||||||
|
percent = (value / 1023.0) * 100.0
|
||||||
|
self.current_percent_label.setText(f"{percent:.1f}%")
|
||||||
|
|
||||||
|
if self.serial_comm.is_connected():
|
||||||
|
if self.laser_control.set_current(value):
|
||||||
|
self.status_bar.showMessage(f"Current set to {value} ({percent:.1f}%)", 2000)
|
||||||
|
|
||||||
|
def on_power_changed(self, value: int):
|
||||||
|
"""Handle power slider change"""
|
||||||
|
self.power_spinbox.setValue(value)
|
||||||
|
|
||||||
|
if self.serial_comm.is_connected():
|
||||||
|
if self.laser_control.set_power_cmd(value):
|
||||||
|
self.status_bar.showMessage(f"Power command set to {value}", 2000)
|
||||||
|
|
||||||
|
def on_shutter_toggled(self, checked: bool):
|
||||||
|
"""Handle shutter toggle"""
|
||||||
|
if checked and self.current_slider.value() > 0:
|
||||||
|
reply = QMessageBox.question(
|
||||||
|
self,
|
||||||
|
"Confirm Shutter Open",
|
||||||
|
"Current is set above 0. Are you sure you want to open the shutter?",
|
||||||
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||||
|
)
|
||||||
|
if reply == QMessageBox.StandardButton.No:
|
||||||
|
self.shutter_btn.setChecked(False)
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.laser_control.set_shutter(checked):
|
||||||
|
self.shutter_btn.setText("Shutter: OPEN" if checked else "Shutter: CLOSED")
|
||||||
|
self.status_bar.showMessage(f"Shutter {'opened' if checked else 'closed'}", 2000)
|
||||||
|
else:
|
||||||
|
self.shutter_btn.setChecked(not checked)
|
||||||
|
|
||||||
|
def on_keyswitch_toggled(self, checked: bool):
|
||||||
|
"""Handle keyswitch toggle"""
|
||||||
|
if self.laser_control.set_keyswitch(checked):
|
||||||
|
self.keyswitch_btn.setText("Keyswitch: ON" if checked else "Keyswitch: OFF")
|
||||||
|
self.status_bar.showMessage(f"Keyswitch {'enabled' if checked else 'disabled'}", 2000)
|
||||||
|
else:
|
||||||
|
self.keyswitch_btn.setChecked(not checked)
|
||||||
|
|
||||||
|
def on_remote_enable_toggled(self, checked: bool):
|
||||||
|
"""Handle remote enable toggle"""
|
||||||
|
if self.laser_control.set_remote_enable(checked):
|
||||||
|
self.remote_enable_btn.setText("Remote: ENABLED" if checked else "Remote: DISABLED")
|
||||||
|
self.status_bar.showMessage(f"Remote control {'enabled' if checked else 'disabled'}", 2000)
|
||||||
|
else:
|
||||||
|
self.remote_enable_btn.setChecked(not checked)
|
||||||
|
|
||||||
|
def on_analog_enable_toggled(self, checked: bool):
|
||||||
|
"""Handle analog enable toggle"""
|
||||||
|
if self.laser_control.set_analog_enable(checked):
|
||||||
|
self.analog_enable_btn.setText("Analog Input: ENABLED" if checked else "Analog Input: DISABLED")
|
||||||
|
self.status_bar.showMessage(f"Analog input {'enabled' if checked else 'disabled'}", 2000)
|
||||||
|
else:
|
||||||
|
self.analog_enable_btn.setChecked(not checked)
|
||||||
|
|
||||||
|
def on_current_mode_toggled(self, checked: bool):
|
||||||
|
"""Handle current mode toggle"""
|
||||||
|
if self.laser_control.set_current_mode(checked):
|
||||||
|
self.current_mode_btn.setText("Current Mode: ON" if checked else "Current Mode: OFF")
|
||||||
|
self.status_bar.showMessage(f"Current mode {'enabled' if checked else 'disabled'}", 2000)
|
||||||
|
else:
|
||||||
|
self.current_mode_btn.setChecked(not checked)
|
||||||
|
|
||||||
|
def on_emergency_stop(self):
|
||||||
|
"""Handle emergency stop button"""
|
||||||
|
if self.laser_control.emergency_stop():
|
||||||
|
# Reset UI to safe state
|
||||||
|
self.current_slider.setValue(0)
|
||||||
|
self.power_slider.setValue(0)
|
||||||
|
self.shutter_btn.setChecked(False)
|
||||||
|
self.shutter_btn.setText("Shutter: CLOSED")
|
||||||
|
self.keyswitch_btn.setChecked(False)
|
||||||
|
self.keyswitch_btn.setText("Keyswitch: OFF")
|
||||||
|
|
||||||
|
self.status_bar.showMessage("EMERGENCY STOP ACTIVATED", 5000)
|
||||||
|
QMessageBox.warning(self, "Emergency Stop", "Emergency stop activated!\nLaser is now in safe state.")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
# Event Handlers - Monitoring
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def on_auto_refresh_toggled(self, state):
|
||||||
|
"""Handle auto-refresh toggle"""
|
||||||
|
if state:
|
||||||
|
interval = self.refresh_interval_spin.value()
|
||||||
|
self.monitor_timer.start(interval)
|
||||||
|
self.monitor_enabled = True
|
||||||
|
else:
|
||||||
|
self.monitor_timer.stop()
|
||||||
|
self.monitor_enabled = False
|
||||||
|
|
||||||
|
def update_monitoring(self):
|
||||||
|
"""Update monitoring readings"""
|
||||||
|
if not self.serial_comm.is_connected():
|
||||||
|
return
|
||||||
|
|
||||||
|
# Read actual current
|
||||||
|
current = self.laser_control.get_current_actual()
|
||||||
|
if current is not None:
|
||||||
|
self.current_actual_label.setText(f"{current} counts")
|
||||||
|
else:
|
||||||
|
self.current_actual_label.setText("Error reading")
|
||||||
|
|
||||||
|
# Read interlock status
|
||||||
|
interlock = self.laser_control.get_interlock_status()
|
||||||
|
if interlock is not None:
|
||||||
|
if interlock:
|
||||||
|
self.interlock_label.setText("OK")
|
||||||
|
self.interlock_label.setStyleSheet("color: green; font-weight: bold;")
|
||||||
|
else:
|
||||||
|
self.interlock_label.setText("FAULT")
|
||||||
|
self.interlock_label.setStyleSheet("color: red; font-weight: bold;")
|
||||||
|
else:
|
||||||
|
self.interlock_label.setText("Error reading")
|
||||||
|
self.interlock_label.setStyleSheet("")
|
||||||
|
|
||||||
|
# Read LDD enable
|
||||||
|
ldd = self.laser_control.get_ldd_enable_status()
|
||||||
|
if ldd is not None:
|
||||||
|
self.ldd_enable_label.setText("Enabled" if ldd else "Disabled")
|
||||||
|
else:
|
||||||
|
self.ldd_enable_label.setText("Error reading")
|
||||||
|
|
||||||
|
# Read PS glue status
|
||||||
|
psglue_in = self.laser_control.get_psglue_in_status()
|
||||||
|
if psglue_in is not None:
|
||||||
|
self.psglue_in_label.setText(f"0x{psglue_in:02x}")
|
||||||
|
else:
|
||||||
|
self.psglue_in_label.setText("Error reading")
|
||||||
|
|
||||||
|
psglue_out = self.laser_control.get_psglue_out_status()
|
||||||
|
if psglue_out is not None:
|
||||||
|
self.psglue_out_label.setText(f"0x{psglue_out:02x}")
|
||||||
|
else:
|
||||||
|
self.psglue_out_label.setText("Error reading")
|
||||||
|
|
||||||
|
# Read head DIO
|
||||||
|
head_dio = self.laser_control.get_head_dio_status()
|
||||||
|
if head_dio is not None:
|
||||||
|
self.head_dio_label.setText(f"0x{head_dio:02x}")
|
||||||
|
else:
|
||||||
|
self.head_dio_label.setText("Error reading")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
# Event Handlers - Advanced
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def on_send_raw_packet(self):
|
||||||
|
"""Handle raw packet send"""
|
||||||
|
if not self.serial_comm.is_connected():
|
||||||
|
QMessageBox.warning(self, "Not Connected", "Please connect to serial port first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Parse inputs
|
||||||
|
addr = int(self.i2c_addr_edit.text(), 16)
|
||||||
|
cmd_hex = self.i2c_cmd_edit.text().replace(" ", "")
|
||||||
|
cmd = bytes.fromhex(cmd_hex)
|
||||||
|
|
||||||
|
data_hex = self.i2c_data_edit.text().replace(" ", "")
|
||||||
|
data = bytes.fromhex(data_hex) if data_hex else b''
|
||||||
|
|
||||||
|
# Send packet
|
||||||
|
if self.i2c_protocol.write(addr, cmd, data):
|
||||||
|
self.packet_response_label.setText("Response: Packet sent successfully")
|
||||||
|
self.status_bar.showMessage("Raw packet sent", 2000)
|
||||||
|
else:
|
||||||
|
self.packet_response_label.setText("Response: Send failed")
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
QMessageBox.warning(self, "Invalid Input", f"Invalid hex value: {e}")
|
||||||
|
|
||||||
|
def on_clear_log(self):
|
||||||
|
"""Handle clear log button"""
|
||||||
|
self.serial_comm.clear_packet_log()
|
||||||
|
self.packet_log_text.clear()
|
||||||
|
|
||||||
|
def update_packet_log(self):
|
||||||
|
"""Update packet log display"""
|
||||||
|
if not self.serial_comm.is_connected():
|
||||||
|
return
|
||||||
|
|
||||||
|
log_entries = self.serial_comm.get_packet_log(100)
|
||||||
|
if log_entries:
|
||||||
|
self.packet_log_text.setPlainText("\n".join(log_entries))
|
||||||
|
# Scroll to bottom
|
||||||
|
scrollbar = self.packet_log_text.verticalScrollBar()
|
||||||
|
scrollbar.setValue(scrollbar.maximum())
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
# Event Handlers - Configuration
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def on_connect_clicked(self):
|
||||||
|
"""Handle connect/disconnect button"""
|
||||||
|
if self.serial_comm.is_connected():
|
||||||
|
# Disconnect
|
||||||
|
self.laser_control.enter_safe_state()
|
||||||
|
self.serial_comm.disconnect()
|
||||||
|
self.connect_btn.setText("Connect")
|
||||||
|
self.status_bar.showMessage("Disconnected")
|
||||||
|
self.update_control_states()
|
||||||
|
else:
|
||||||
|
# Connect
|
||||||
|
port = self.port_combo.currentText()
|
||||||
|
baudrate = int(self.baudrate_combo.currentText())
|
||||||
|
|
||||||
|
if self.serial_comm.connect(port, baudrate):
|
||||||
|
self.connect_btn.setText("Disconnect")
|
||||||
|
self.status_bar.showMessage(f"Connected to {port} at {baudrate} baud")
|
||||||
|
self.update_control_states()
|
||||||
|
else:
|
||||||
|
QMessageBox.critical(self, "Connection Error", f"Failed to connect to {port}")
|
||||||
|
|
||||||
|
def on_dtr_changed(self, state):
|
||||||
|
"""Handle DTR checkbox change"""
|
||||||
|
if self.serial_comm.is_connected():
|
||||||
|
self.serial_comm.port.dtr = bool(state)
|
||||||
|
|
||||||
|
def on_rts_changed(self, state):
|
||||||
|
"""Handle RTS checkbox change"""
|
||||||
|
if self.serial_comm.is_connected():
|
||||||
|
self.serial_comm.port.rts = bool(state)
|
||||||
|
|
||||||
|
def refresh_ports(self):
|
||||||
|
"""Refresh available serial ports"""
|
||||||
|
self.port_combo.clear()
|
||||||
|
ports = list_ports.comports()
|
||||||
|
for port in ports:
|
||||||
|
self.port_combo.addItem(port.device)
|
||||||
|
|
||||||
|
# Add default if no ports found
|
||||||
|
if self.port_combo.count() == 0:
|
||||||
|
self.port_combo.addItem("/dev/ttyUSB0")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
# UI Update Functions
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def update_control_states(self):
|
||||||
|
"""Enable/disable controls based on connection state"""
|
||||||
|
connected = self.serial_comm.is_connected()
|
||||||
|
|
||||||
|
# Basic controls
|
||||||
|
self.current_slider.setEnabled(connected)
|
||||||
|
self.current_spinbox.setEnabled(connected)
|
||||||
|
self.power_slider.setEnabled(connected)
|
||||||
|
self.power_spinbox.setEnabled(connected)
|
||||||
|
self.shutter_btn.setEnabled(connected)
|
||||||
|
self.keyswitch_btn.setEnabled(connected)
|
||||||
|
self.remote_enable_btn.setEnabled(connected)
|
||||||
|
self.analog_enable_btn.setEnabled(connected)
|
||||||
|
self.current_mode_btn.setEnabled(connected)
|
||||||
|
|
||||||
|
# Monitoring controls
|
||||||
|
self.auto_refresh_check.setEnabled(connected)
|
||||||
|
self.refresh_now_btn.setEnabled(connected)
|
||||||
|
|
||||||
|
# Advanced controls
|
||||||
|
self.send_packet_btn.setEnabled(connected)
|
||||||
|
|
||||||
|
# Configuration controls
|
||||||
|
self.port_combo.setEnabled(not connected)
|
||||||
|
self.baudrate_combo.setEnabled(not connected)
|
||||||
|
self.dtr_check.setEnabled(connected)
|
||||||
|
self.rts_check.setEnabled(connected)
|
||||||
|
|
||||||
|
def apply_stylesheet(self):
|
||||||
|
"""Apply custom stylesheet for professional look"""
|
||||||
|
self.setStyleSheet("""
|
||||||
|
QGroupBox {
|
||||||
|
font-weight: bold;
|
||||||
|
border: 2px solid #cccccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QGroupBox::title {
|
||||||
|
subcontrol-origin: margin;
|
||||||
|
left: 10px;
|
||||||
|
padding: 0 5px 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton {
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: #d0d0d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton:pressed {
|
||||||
|
background-color: #c0c0c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton:disabled {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
color: #a0a0a0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton:checked {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSlider::groove:horizontal {
|
||||||
|
border: 1px solid #999999;
|
||||||
|
height: 8px;
|
||||||
|
background: #e0e0e0;
|
||||||
|
margin: 2px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSlider::handle:horizontal {
|
||||||
|
background: #4CAF50;
|
||||||
|
border: 1px solid #5c5c5c;
|
||||||
|
width: 18px;
|
||||||
|
margin: -5px 0;
|
||||||
|
border-radius: 9px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
# Settings Persistence
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def save_settings(self):
|
||||||
|
"""Save window geometry and settings"""
|
||||||
|
self.settings.setValue("geometry", self.saveGeometry())
|
||||||
|
self.settings.setValue("port", self.port_combo.currentText())
|
||||||
|
self.settings.setValue("baudrate", self.baudrate_combo.currentText())
|
||||||
|
|
||||||
|
def restore_settings(self):
|
||||||
|
"""Restore window geometry and settings"""
|
||||||
|
geometry = self.settings.value("geometry")
|
||||||
|
if geometry:
|
||||||
|
self.restoreGeometry(geometry)
|
||||||
|
|
||||||
|
port = self.settings.value("port")
|
||||||
|
if port:
|
||||||
|
index = self.port_combo.findText(port)
|
||||||
|
if index >= 0:
|
||||||
|
self.port_combo.setCurrentIndex(index)
|
||||||
|
|
||||||
|
baudrate = self.settings.value("baudrate")
|
||||||
|
if baudrate:
|
||||||
|
index = self.baudrate_combo.findText(baudrate)
|
||||||
|
if index >= 0:
|
||||||
|
self.baudrate_combo.setCurrentIndex(index)
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
"""Handle window close event"""
|
||||||
|
if self.serial_comm.is_connected():
|
||||||
|
reply = QMessageBox.question(
|
||||||
|
self,
|
||||||
|
"Confirm Exit",
|
||||||
|
"Laser is connected. Enter safe state and disconnect?",
|
||||||
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||||
|
)
|
||||||
|
if reply == QMessageBox.StandardButton.Yes:
|
||||||
|
self.laser_control.enter_safe_state()
|
||||||
|
self.serial_comm.disconnect()
|
||||||
|
else:
|
||||||
|
event.ignore()
|
||||||
|
return
|
||||||
|
|
||||||
|
self.save_settings()
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Main Entry Point
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main application entry point"""
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
app.setApplicationName("Genesis Laser Control")
|
||||||
|
app.setOrganizationName("Genesis")
|
||||||
|
|
||||||
|
window = MainWindow()
|
||||||
|
window.show()
|
||||||
|
|
||||||
|
sys.exit(app.exec())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+1445
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user