Initial commit: merge nuescan, pymso, pybbd202, and pypewpewhops into scanengine-3
- Merged four separate hardware control projects into unified platform - Created unified requirements.txt with all dependencies - Added comprehensive .gitignore - Added project overview README Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
# 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__/
|
||||
@@ -0,0 +1,300 @@
|
||||
# BBD203 Connection Guide
|
||||
|
||||
## Quick Start - Connecting Your BBD203 Controller
|
||||
|
||||
This guide explains how to connect your ThorLabs BBD203 motor controller to nueScan using the simplified serial number method.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Physical Connection
|
||||
|
||||
1. **Connect USB Cable**
|
||||
- Connect the USB cable from your BBD203 controller to your computer
|
||||
- Wait for Windows/Linux to recognize the device
|
||||
- No special drivers needed (uses standard FTDI USB-Serial)
|
||||
|
||||
2. **Power On Controller**
|
||||
- Ensure BBD203 is powered on
|
||||
- Front panel should be lit
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Find Serial Number
|
||||
|
||||
The serial number is printed on a label on your BBD203 controller.
|
||||
|
||||
**Common Locations:**
|
||||
- Back panel of the controller
|
||||
- Side panel
|
||||
- Original packaging
|
||||
|
||||
**Format:**
|
||||
- Usually 8 digits (e.g., `83123456`)
|
||||
- May include letters (e.g., `83A12345`)
|
||||
|
||||
**Example Label:**
|
||||
```
|
||||
ThorLabs BBD203
|
||||
S/N: 83123456
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Connect in nueScan
|
||||
|
||||
### Using the GUI
|
||||
|
||||
1. **Launch nueScan**
|
||||
```bash
|
||||
python -m nuescan
|
||||
```
|
||||
|
||||
2. **Enter Serial Number**
|
||||
- Locate the "ThorLABS MLS Stage Serial:" field at the top of the window
|
||||
- Type your serial number (e.g., `83123456`)
|
||||
|
||||
3. **Click Connect**
|
||||
- Click the "Connect" button next to the serial field
|
||||
- Wait 1-2 seconds for connection
|
||||
|
||||
4. **Success!**
|
||||
- If successful, you'll see a confirmation dialog
|
||||
- Button changes to "Disconnect"
|
||||
- All 3 motor channels are now enabled
|
||||
|
||||
### Programmatic Connection
|
||||
|
||||
```python
|
||||
from hardware.thorlabs_stage import ThorLabsStage
|
||||
|
||||
# Create stage instance
|
||||
stage = ThorLabsStage(encoder_counts_per_mm=20000)
|
||||
|
||||
# Connect by serial number
|
||||
success = stage.connect('83123456')
|
||||
|
||||
if success:
|
||||
print("Connected! Ready to home axes.")
|
||||
else:
|
||||
print("Connection failed.")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Device not found" Error
|
||||
|
||||
**Problem:** Connection fails with "Could not find BBD203 with serial number..."
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Check USB Connection**
|
||||
- Ensure USB cable is fully inserted
|
||||
- Try a different USB port
|
||||
- Try a different USB cable
|
||||
|
||||
2. **Verify Serial Number**
|
||||
- Double-check the serial number on the controller label
|
||||
- Ensure no typos (0 vs O, 1 vs I, etc.)
|
||||
|
||||
3. **List Available Devices**
|
||||
- The error dialog will show all detected ThorLabs devices
|
||||
- Check if your device appears with a different serial number
|
||||
- If no devices shown, check USB connection and drivers
|
||||
|
||||
4. **Windows: Check Device Manager**
|
||||
- Open Device Manager
|
||||
- Look under "Ports (COM & LPT)"
|
||||
- Should see "USB Serial Port (COMx)" with FTDI in description
|
||||
- If device shows with "!" icon, driver issue
|
||||
|
||||
5. **Linux: Check Permissions**
|
||||
```bash
|
||||
# Check if device is detected
|
||||
lsusb | grep -i ftdi
|
||||
|
||||
# Check serial ports
|
||||
ls -l /dev/ttyUSB*
|
||||
|
||||
# Add user to dialout group (may require logout)
|
||||
sudo usermod -a -G dialout $USER
|
||||
```
|
||||
|
||||
### Connection Succeeds but No Response
|
||||
|
||||
**Problem:** Connection successful but motors don't respond
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Check Power**
|
||||
- Verify motors are connected and powered
|
||||
- Check motor power LEDs on BBD203 front panel
|
||||
|
||||
2. **Enable Channels**
|
||||
- Channels should auto-enable on connection
|
||||
- Check status indicators in UI
|
||||
|
||||
3. **Home Axes**
|
||||
- Axes may need homing before movement
|
||||
- Try homing each axis
|
||||
|
||||
### Multiple Controllers
|
||||
|
||||
**Problem:** You have multiple BBD203 controllers connected
|
||||
|
||||
**Solution:**
|
||||
- Each controller has a unique serial number
|
||||
- Connect to specific controller by entering its serial number
|
||||
- Error dialog will show all connected devices
|
||||
|
||||
---
|
||||
|
||||
## What Happens During Connection
|
||||
|
||||
### Automatic Process
|
||||
|
||||
When you click "Connect", the following happens automatically:
|
||||
|
||||
1. **USB Enumeration**
|
||||
- Scans all USB ports
|
||||
- Finds ThorLabs devices (FTDI vendor ID: 0x0403)
|
||||
- Matches your serial number
|
||||
|
||||
2. **Port Assignment**
|
||||
- Determines the COM port (e.g., COM3, /dev/ttyUSB0)
|
||||
- Opens serial connection at 115200 baud
|
||||
|
||||
3. **Controller Initialization**
|
||||
- Requests hardware information
|
||||
- Enables automatic status updates
|
||||
- Enables all 3 motor channels
|
||||
|
||||
4. **Default Configuration**
|
||||
- Sets velocity: 1.0 mm/s
|
||||
- Sets acceleration: 5.0 mm/s²
|
||||
- Starts position monitoring
|
||||
|
||||
### Status Updates
|
||||
|
||||
After connection:
|
||||
- Position updates received every ~100ms
|
||||
- Status bits monitored (homed, moving, errors)
|
||||
- Move completion notifications enabled
|
||||
|
||||
---
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Custom Encoder Resolution
|
||||
|
||||
If you're using a stage with different encoder resolution:
|
||||
|
||||
```python
|
||||
# Example: Stage with 2,000 counts/mm instead of default 20,000
|
||||
stage = ThorLabsStage(encoder_counts_per_mm=2000)
|
||||
stage.connect('83123456')
|
||||
```
|
||||
|
||||
Common resolutions:
|
||||
- **MLS203**: 20,000 counts/mm (default)
|
||||
- **DDS220**: 2,000 counts/mm
|
||||
- **Custom**: Check your stage specifications
|
||||
|
||||
### Direct Port Connection (Not Recommended)
|
||||
|
||||
If you need to connect to a specific port instead of using serial number:
|
||||
|
||||
```python
|
||||
from hardware.bbd203_driver import BBD203Driver
|
||||
|
||||
driver = BBD203Driver()
|
||||
driver.connect('COM3') # or '/dev/ttyUSB0' on Linux
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## System Requirements
|
||||
|
||||
### Operating Systems
|
||||
- ✅ Windows 7/8/10/11
|
||||
- ✅ Linux (Ubuntu, Fedora, etc.)
|
||||
- ✅ macOS (with FTDI driver)
|
||||
|
||||
### Dependencies
|
||||
- Python 3.8+
|
||||
- PySerial 3.5+
|
||||
- PyQt6 6.4+
|
||||
|
||||
### USB Requirements
|
||||
- USB 2.0 or higher
|
||||
- FTDI USB-Serial drivers (usually automatic)
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
After successful connection:
|
||||
|
||||
1. **Home the Axes**
|
||||
- Required before first movement
|
||||
- Establishes zero position reference
|
||||
|
||||
2. **Test Movement**
|
||||
- Try small movements to verify operation
|
||||
- Check position feedback in UI
|
||||
|
||||
3. **Configure Scan Parameters**
|
||||
- Set scan area (X/Y start, delta)
|
||||
- Set row spacing
|
||||
- Configure velocity if needed
|
||||
|
||||
4. **Begin Scanning**
|
||||
- All systems should show "Ready"
|
||||
- Click "Begin Scan" to start
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
For issues with the BBD203 driver or connection:
|
||||
|
||||
1. **Check Debug Output**
|
||||
- Console window shows connection details
|
||||
- Look for "INFO:", "DEBUG:", and "ERROR:" messages
|
||||
|
||||
2. **Review Driver Documentation**
|
||||
- `BBD203_DRIVER_README.md` - Complete driver reference
|
||||
- `BBD203_Communications_Protocol.md` - Protocol details
|
||||
|
||||
3. **ThorLabs Support**
|
||||
- For hardware issues: techsupport@thorlabs.com
|
||||
- For driver/protocol questions: Review APT documentation
|
||||
|
||||
---
|
||||
|
||||
## Example Session
|
||||
|
||||
```
|
||||
$ python -m nuescan
|
||||
|
||||
# UI appears
|
||||
# Enter serial: 83123456
|
||||
# Click Connect
|
||||
|
||||
# Console output:
|
||||
INFO: Connecting to BBD203 with serial number 83123456
|
||||
DEBUG: Found ThorLabs device - Serial: 83123456, Port: COM3
|
||||
INFO: Found device 83123456 on port COM3
|
||||
INFO: Connecting to BBD203 on COM3
|
||||
INFO: Successfully connected to BBD203 on COM3
|
||||
INFO: Stage connected and channels enabled
|
||||
|
||||
# Success dialog appears
|
||||
# Button changes to "Disconnect"
|
||||
# Ready to home and move!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*For detailed technical information, see `BBD203_DRIVER_README.md`*
|
||||
Executable
+702
@@ -0,0 +1,702 @@
|
||||
# BBD203 Motor Controller - APT Communications Protocol
|
||||
|
||||
## Version 42.1 - Extracted Documentation
|
||||
|
||||
This document contains only the information relevant to the **BBD203 3-Channel Benchtop Brushless DC Motor Controller**, extracted from the complete Thorlabs APT Communications Protocol documentation.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Introduction](#1-introduction)
|
||||
2. [General Protocol Information](#2-general-protocol-information)
|
||||
3. [BBD203 Specifications](#3-bbd203-specifications)
|
||||
4. [Message Format](#4-message-format)
|
||||
5. [BBD203 Applicable Messages](#5-bbd203-applicable-messages)
|
||||
6. [Command Examples](#6-command-examples)
|
||||
7. [Important Notes for BBD203](#7-important-notes-for-bbd203)
|
||||
|
||||
---
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
The BBD203 is a 3-channel benchtop brushless DC motor controller that is part of Thorlabs' APT motion control system. This document describes the communication protocol used to control the BBD203 via USB or RS232 interfaces.
|
||||
|
||||
### 1.1 Device Overview
|
||||
|
||||
The BBD203 provides independent control of up to three brushless DC motors with the following key features:
|
||||
|
||||
- 3 independent motor channels
|
||||
- USB and RS232 communication interfaces
|
||||
- Closed-loop position and velocity control
|
||||
- Encoder feedback support
|
||||
- Digital I/O for triggering and synchronization
|
||||
- Compatible with Thorlabs' APT software suite
|
||||
|
||||
### 1.2 Related Controllers
|
||||
|
||||
The BBD203 shares its protocol with other controllers in the BBD series:
|
||||
- **BBD201** - 1 Channel Benchtop Brushless DC Motor Driver
|
||||
- **BBD202** - 2 Channel Benchtop Brushless DC Motor Driver
|
||||
- **BBD203** - 3 Channel Benchtop Brushless DC Motor Driver
|
||||
|
||||
---
|
||||
|
||||
## 2. General Protocol Information
|
||||
|
||||
### 2.1 Communication Format
|
||||
|
||||
All communications with the BBD203 use a binary message protocol. Messages consist of a 6-byte header followed by an optional data packet.
|
||||
|
||||
#### Header Structure (6 bytes):
|
||||
|
||||
| Byte | Description |
|
||||
|------|------------|
|
||||
| 0-1 | Message ID (16-bit, little-endian) |
|
||||
| 2 | Data length (bytes) or parameter 1 |
|
||||
| 3 | Data length MSB or parameter 2 |
|
||||
| 4 | Destination |
|
||||
| 5 | Source |
|
||||
|
||||
#### Destination Byte Values:
|
||||
|
||||
| Value | Description |
|
||||
|-------|------------|
|
||||
| 0x50 | Generic USB device |
|
||||
| 0x11 | Rack controller (card slot unit) |
|
||||
| 0x21 | Bay 1 / Channel 1 |
|
||||
| 0x22 | Bay 2 / Channel 2 |
|
||||
| 0x23 | Bay 3 / Channel 3 |
|
||||
|
||||
#### Source Byte Values:
|
||||
|
||||
| Value | Description |
|
||||
|-------|------------|
|
||||
| 0x01 | Host PC |
|
||||
|
||||
### 2.2 Channel Addressing
|
||||
|
||||
The BBD203 has three motor channels. When addressing specific channels:
|
||||
|
||||
- **Channel 1**: Use destination byte `0x21`
|
||||
- **Channel 2**: Use destination byte `0x22`
|
||||
- **Channel 3**: Use destination byte `0x23`
|
||||
- **All channels**: Use destination byte `0x11`
|
||||
|
||||
**Important**: Although the BBD203 has 3 channels, each channel operates as an independent single-channel controller. In the data packet's channel identifier field, always use Channel 1 (`0x01 0x00`), and use the destination byte in the header to specify the physical channel.
|
||||
|
||||
### 2.3 Data Types
|
||||
|
||||
| Type | Size | Description |
|
||||
|------|------|------------|
|
||||
| char | 1 byte | 8-bit signed integer |
|
||||
| short | 2 bytes | 16-bit signed integer (little-endian) |
|
||||
| long | 4 bytes | 32-bit signed integer (little-endian) |
|
||||
| word | 2 bytes | 16-bit unsigned integer (little-endian) |
|
||||
| dword | 4 bytes | 32-bit unsigned integer (little-endian) |
|
||||
|
||||
---
|
||||
|
||||
## 3. BBD203 Specifications
|
||||
|
||||
### 3.1 Encoder and Position Scaling
|
||||
|
||||
For BBD203 controllers, position and velocity values are scaled based on encoder counts. The scaling formulas are:
|
||||
|
||||
#### Position Scaling:
|
||||
```
|
||||
POS_APT = EncCnt × Pos
|
||||
```
|
||||
Where:
|
||||
- `POS_APT` = Position value to send/receive via APT protocol
|
||||
- `EncCnt` = Encoder counts per unit (stage-specific)
|
||||
- `Pos` = Position in real units (mm, degrees, etc.)
|
||||
|
||||
#### Velocity Scaling:
|
||||
```
|
||||
VEL_APT = EncCnt × T × 65536 × Vel
|
||||
```
|
||||
Where:
|
||||
- `VEL_APT` = Velocity value to send/receive via APT protocol
|
||||
- `T` = 102.4 × 10⁻⁶ seconds (controller sample time)
|
||||
- `Vel` = Velocity in real units per second
|
||||
|
||||
#### Acceleration Scaling:
|
||||
```
|
||||
ACC_APT = EncCnt × T² × 65536 × Acc
|
||||
```
|
||||
Where:
|
||||
- `ACC_APT` = Acceleration value to send/receive via APT protocol
|
||||
- `T²` = (102.4 × 10⁻⁶)²
|
||||
- `Acc` = Acceleration in real units per second²
|
||||
|
||||
### 3.2 Example Scaling Values
|
||||
|
||||
For a stage with 20,000 encoder counts per mm:
|
||||
- Position of 10 mm = 200,000 counts
|
||||
- Velocity of 1 mm/s = 134,218 APT units
|
||||
- Acceleration of 1 mm/s² = 13.7 APT units
|
||||
|
||||
---
|
||||
|
||||
## 4. Message Format
|
||||
|
||||
### 4.1 Message Types
|
||||
|
||||
Messages are categorized into several types:
|
||||
|
||||
- **MOD** - Module control messages (identify, enable/disable)
|
||||
- **HW** - Hardware information and control
|
||||
- **MOT** - Motor control messages (move, velocity, position)
|
||||
- **RACK** - Rack and bay status messages
|
||||
|
||||
### 4.2 Message Flow
|
||||
|
||||
1. **Command Messages**: Sent from host to controller
|
||||
2. **Request Messages**: Request data from controller
|
||||
3. **Get Messages**: Controller response with requested data
|
||||
4. **Status Messages**: Unsolicited updates from controller
|
||||
|
||||
---
|
||||
|
||||
## 5. BBD203 Applicable Messages
|
||||
|
||||
### 5.1 Module Control Messages
|
||||
|
||||
| Message | Hex Code | Direction | Description |
|
||||
|---------|----------|-----------|-------------|
|
||||
| MGMSG_MOD_IDENTIFY | 0x0223 | Host→Device | Flash front panel LEDs to identify unit |
|
||||
| MGMSG_MOD_SET_CHANENABLESTATE | 0x0210 | Host→Device | Enable/disable a motor channel |
|
||||
| MGMSG_MOD_REQ_CHANENABLESTATE | 0x0211 | Host→Device | Request channel enable state |
|
||||
| MGMSG_MOD_GET_CHANENABLESTATE | 0x0212 | Device→Host | Get channel enable state response |
|
||||
|
||||
### 5.2 Hardware Control Messages
|
||||
|
||||
| Message | Hex Code | Direction | Description |
|
||||
|---------|----------|-----------|-------------|
|
||||
| MGMSG_HW_DISCONNECT | 0x0002 | Host→Device | Disconnect from USB bus |
|
||||
| MGMSG_HW_RESPONSE | 0x0080 | Device→Host | Response/error message |
|
||||
| MGMSG_HW_RICHRESPONSE | 0x0081 | Device→Host | Detailed response with error info |
|
||||
| MGMSG_HW_START_UPDATEMSGS | 0x0011 | Host→Device | Start automatic status updates |
|
||||
| MGMSG_HW_STOP_UPDATEMSGS | 0x0012 | Host→Device | Stop automatic status updates |
|
||||
| MGMSG_HW_REQ_INFO | 0x0005 | Host→Device | Request hardware information |
|
||||
| MGMSG_HW_GET_INFO | 0x0006 | Device→Host | Hardware information response |
|
||||
|
||||
### 5.3 Rack Status Messages
|
||||
|
||||
| Message | Hex Code | Direction | Description |
|
||||
|---------|----------|-----------|-------------|
|
||||
| MGMSG_RACK_REQ_BAYUSED | 0x0060 | Host→Device | Request which bays are occupied |
|
||||
| MGMSG_RACK_GET_BAYUSED | 0x0061 | Device→Host | Bay occupation status |
|
||||
| MGMSG_RACK_REQ_STATUSBITS | 0x0226 | Host→Device | Request rack status bits |
|
||||
| MGMSG_RACK_GET_STATUSBITS | 0x0227 | Device→Host | Rack status bits response |
|
||||
| MGMSG_RACK_SET_DIGOUTPUTS | 0x0228 | Host→Device | Set digital outputs |
|
||||
| MGMSG_RACK_REQ_DIGOUTPUTS | 0x0229 | Host→Device | Request digital output states |
|
||||
| MGMSG_RACK_GET_DIGOUTPUTS | 0x0230 | Device→Host | Digital output states |
|
||||
|
||||
### 5.4 Motor Control Messages - Basic
|
||||
|
||||
| Message | Hex Code | Direction | Description |
|
||||
|---------|----------|-----------|-------------|
|
||||
| MGMSG_MOT_SET_POSCOUNTER | 0x0410 | Host→Device | Set position counter value |
|
||||
| MGMSG_MOT_REQ_POSCOUNTER | 0x0411 | Host→Device | Request position counter |
|
||||
| MGMSG_MOT_GET_POSCOUNTER | 0x0412 | Device→Host | Position counter value |
|
||||
| MGMSG_MOT_SET_ENCCOUNTER | 0x0409 | Host→Device | Set encoder counter value |
|
||||
| MGMSG_MOT_REQ_ENCCOUNTER | 0x040A | Host→Device | Request encoder counter |
|
||||
| MGMSG_MOT_GET_ENCCOUNTER | 0x040B | Device→Host | Encoder counter value |
|
||||
|
||||
### 5.5 Motor Control Messages - Homing
|
||||
|
||||
| Message | Hex Code | Direction | Description |
|
||||
|---------|----------|-----------|-------------|
|
||||
| MGMSG_MOT_SET_HOMEPARAMS | 0x0440 | Host→Device | Set homing parameters |
|
||||
| MGMSG_MOT_REQ_HOMEPARAMS | 0x0441 | Host→Device | Request homing parameters |
|
||||
| MGMSG_MOT_GET_HOMEPARAMS | 0x0442 | Device→Host | Homing parameters |
|
||||
| MGMSG_MOT_MOVE_HOME | 0x0443 | Host→Device | Start homing sequence |
|
||||
| MGMSG_MOT_MOVE_HOMED | 0x0444 | Device→Host | Homing completed |
|
||||
|
||||
### 5.6 Motor Control Messages - Movement
|
||||
|
||||
| Message | Hex Code | Direction | Description |
|
||||
|---------|----------|-----------|-------------|
|
||||
| MGMSG_MOT_SET_MOVERELPARAMS | 0x0445 | Host→Device | Set relative move parameters |
|
||||
| MGMSG_MOT_REQ_MOVERELPARAMS | 0x0446 | Host→Device | Request relative move parameters |
|
||||
| MGMSG_MOT_GET_MOVERELPARAMS | 0x0447 | Device→Host | Relative move parameters |
|
||||
| MGMSG_MOT_MOVE_RELATIVE | 0x0448 | Host→Device | Start relative move |
|
||||
| MGMSG_MOT_SET_MOVEABSPARAMS | 0x0450 | Host→Device | Set absolute move parameters |
|
||||
| MGMSG_MOT_REQ_MOVEABSPARAMS | 0x0451 | Host→Device | Request absolute move parameters |
|
||||
| MGMSG_MOT_GET_MOVEABSPARAMS | 0x0452 | Device→Host | Absolute move parameters |
|
||||
| MGMSG_MOT_MOVE_ABSOLUTE | 0x0453 | Host→Device | Start absolute move |
|
||||
| MGMSG_MOT_MOVE_COMPLETED | 0x0464 | Device→Host | Move completed notification |
|
||||
| MGMSG_MOT_MOVE_VELOCITY | 0x0457 | Host→Device | Start velocity move |
|
||||
| MGMSG_MOT_MOVE_STOP | 0x0465 | Host→Device | Stop any motion |
|
||||
| MGMSG_MOT_MOVE_STOPPED | 0x0466 | Device→Host | Motion stopped notification |
|
||||
|
||||
### 5.7 Motor Control Messages - Velocity Parameters
|
||||
|
||||
| Message | Hex Code | Direction | Description |
|
||||
|---------|----------|-----------|-------------|
|
||||
| MGMSG_MOT_SET_VELPARAMS | 0x0413 | Host→Device | Set velocity parameters |
|
||||
| MGMSG_MOT_REQ_VELPARAMS | 0x0414 | Host→Device | Request velocity parameters |
|
||||
| MGMSG_MOT_GET_VELPARAMS | 0x0415 | Device→Host | Velocity parameters |
|
||||
|
||||
### 5.8 Motor Control Messages - Status
|
||||
|
||||
| Message | Hex Code | Direction | Description |
|
||||
|---------|----------|-----------|-------------|
|
||||
| MGMSG_MOT_REQ_STATUSUPDATE | 0x0480 | Host→Device | Request status update |
|
||||
| MGMSG_MOT_GET_STATUSUPDATE | 0x0481 | Device→Host | Status update |
|
||||
| MGMSG_MOT_REQ_STATUSBITS | 0x0429 | Host→Device | Request status bits |
|
||||
| MGMSG_MOT_GET_STATUSBITS | 0x042A | Device→Host | Status bits |
|
||||
|
||||
### 5.9 BBD-Specific Control Messages
|
||||
|
||||
| Message | Hex Code | Direction | Description |
|
||||
|---------|----------|-----------|-------------|
|
||||
| MGMSG_MOT_SET_DCPIDPARAMS | 0x04A0 | Host→Device | Set DC motor PID parameters |
|
||||
| MGMSG_MOT_REQ_DCPIDPARAMS | 0x04A1 | Host→Device | Request DC motor PID parameters |
|
||||
| MGMSG_MOT_GET_DCPIDPARAMS | 0x04A2 | Device→Host | DC motor PID parameters |
|
||||
| MGMSG_MOT_SET_POSITIONLOOPPARAMS | 0x04D7 | Host→Device | Set position loop parameters |
|
||||
| MGMSG_MOT_REQ_POSITIONLOOPPARAMS | 0x04D8 | Host→Device | Request position loop parameters |
|
||||
| MGMSG_MOT_GET_POSITIONLOOPPARAMS | 0x04D9 | Device→Host | Position loop parameters |
|
||||
| MGMSG_MOT_SET_MOTOROUTPUTPARAMS | 0x04DA | Host→Device | Set motor output parameters |
|
||||
| MGMSG_MOT_REQ_MOTOROUTPUTPARAMS | 0x04DB | Host→Device | Request motor output parameters |
|
||||
| MGMSG_MOT_GET_MOTOROUTPUTPARAMS | 0x04DC | Device→Host | Motor output parameters |
|
||||
| MGMSG_MOT_SET_TRACKSETTLEDPARAMS | 0x04E0 | Host→Device | Set tracking/settled parameters |
|
||||
| MGMSG_MOT_REQ_TRACKSETTLEDPARAMS | 0x04E1 | Host→Device | Request tracking/settled parameters |
|
||||
| MGMSG_MOT_GET_TRACKSETTLEDPARAMS | 0x04E2 | Device→Host | Tracking/settled parameters |
|
||||
| MGMSG_MOT_SET_PROFILEMODEPARAMS | 0x04E3 | Host→Device | Set profile mode parameters |
|
||||
| MGMSG_MOT_REQ_PROFILEMODEPARAMS | 0x04E4 | Host→Device | Request profile mode parameters |
|
||||
| MGMSG_MOT_GET_PROFILEMODEPARAMS | 0x04E5 | Device→Host | Profile mode parameters |
|
||||
| MGMSG_MOT_SET_JOYSTICKPARAMS | 0x04E6 | Host→Device | Set joystick parameters |
|
||||
| MGMSG_MOT_REQ_JOYSTICKPARAMS | 0x04E7 | Host→Device | Request joystick parameters |
|
||||
| MGMSG_MOT_GET_JOYSTICKPARAMS | 0x04E8 | Device→Host | Joystick parameters |
|
||||
| MGMSG_MOT_SET_CURRENTLOOPPARAMS | 0x04D4 | Host→Device | Set current loop parameters |
|
||||
| MGMSG_MOT_REQ_CURRENTLOOPPARAMS | 0x04D5 | Host→Device | Request current loop parameters |
|
||||
| MGMSG_MOT_GET_CURRENTLOOPPARAMS | 0x04D6 | Device→Host | Current loop parameters |
|
||||
| MGMSG_MOT_SET_SETTLEDCURRENTLOOPPARAMS | 0x04E9 | Host→Device | Set settled current loop parameters |
|
||||
| MGMSG_MOT_REQ_SETTLEDCURRENTLOOPPARAMS | 0x04EA | Host→Device | Request settled current loop parameters |
|
||||
| MGMSG_MOT_GET_SETTLEDCURRENTLOOPPARAMS | 0x04EB | Device→Host | Settled current loop parameters |
|
||||
| MGMSG_MOT_SET_STAGEAXISPARAMS | 0x04F0 | Host→Device | Set stage axis parameters |
|
||||
| MGMSG_MOT_REQ_STAGEAXISPARAMS | 0x04F1 | Host→Device | Request stage axis parameters |
|
||||
| MGMSG_MOT_GET_STAGEAXISPARAMS | 0x04F2 | Device→Host | Stage axis parameters |
|
||||
| MGMSG_MOT_SET_TRIGGER | 0x0500 | Host→Device | Set trigger configuration |
|
||||
| MGMSG_MOT_REQ_TRIGGER | 0x0501 | Host→Device | Request trigger configuration |
|
||||
| MGMSG_MOT_GET_TRIGGER | 0x0502 | Device→Host | Trigger configuration |
|
||||
|
||||
---
|
||||
|
||||
## 6. Command Examples
|
||||
|
||||
### 6.1 Enable Channel 1
|
||||
|
||||
To enable channel 1 on the BBD203:
|
||||
|
||||
**Command bytes:**
|
||||
```
|
||||
TX: 10 02 01 01 21 01
|
||||
```
|
||||
|
||||
**Breakdown:**
|
||||
- `10 02` - MGMSG_MOD_SET_CHANENABLESTATE
|
||||
- `01` - Enable channel (0x02 to disable)
|
||||
- `01` - Channel 1
|
||||
- `21` - Destination (Channel 1)
|
||||
- `01` - Source (Host PC)
|
||||
|
||||
### 6.2 Home Channel 2
|
||||
|
||||
To initiate homing on channel 2:
|
||||
|
||||
**Command bytes:**
|
||||
```
|
||||
TX: 43 04 01 00 22 01
|
||||
```
|
||||
|
||||
**Breakdown:**
|
||||
- `43 04` - MGMSG_MOT_MOVE_HOME
|
||||
- `01` - Channel identifier (always 0x01 for BBD203)
|
||||
- `00` - Not used
|
||||
- `22` - Destination (Channel 2)
|
||||
- `01` - Source (Host PC)
|
||||
|
||||
### 6.3 Set Position Counter
|
||||
|
||||
To set the position counter for channel 1 to 10.0 mm (assuming 20,000 counts/mm):
|
||||
|
||||
**Command bytes:**
|
||||
```
|
||||
TX: 10 04 06 00 21 01 01 00 40 0D 03 00
|
||||
```
|
||||
|
||||
**Breakdown:**
|
||||
- `10 04` - MGMSG_MOT_SET_POSCOUNTER
|
||||
- `06 00` - 6 byte data packet
|
||||
- `21` - Destination (Channel 1)
|
||||
- `01` - Source (Host PC)
|
||||
- `01 00` - Channel 1 (in data packet)
|
||||
- `40 0D 03 00` - Position = 200,000 counts (10 mm × 20,000)
|
||||
|
||||
### 6.4 Move Absolute
|
||||
|
||||
To move channel 3 to absolute position 50 mm:
|
||||
|
||||
**Command bytes:**
|
||||
```
|
||||
TX: 53 04 06 00 23 01 01 00 A0 86 01 00
|
||||
```
|
||||
|
||||
**Breakdown:**
|
||||
- `53 04` - MGMSG_MOT_MOVE_ABSOLUTE
|
||||
- `06 00` - 6 byte data packet
|
||||
- `23` - Destination (Channel 3)
|
||||
- `01` - Source (Host PC)
|
||||
- `01 00` - Channel 1 (in data packet, always 0x01 0x00)
|
||||
- `A0 86 01 00` - Position = 1,000,000 counts (50 mm × 20,000)
|
||||
|
||||
### 6.5 Set Velocity Parameters
|
||||
|
||||
To set velocity parameters for channel 2 (max velocity = 5 mm/s, acceleration = 10 mm/s²):
|
||||
|
||||
**Command bytes:**
|
||||
```
|
||||
TX: 13 04 0E 00 22 01 01 00 00 00 8A 44 0A 00 89 00 00 00
|
||||
```
|
||||
|
||||
**Breakdown:**
|
||||
- `13 04` - MGMSG_MOT_SET_VELPARAMS
|
||||
- `0E 00` - 14 byte data packet
|
||||
- `22` - Destination (Channel 2)
|
||||
- `01` - Source (Host PC)
|
||||
- `01 00` - Channel 1 (in data packet)
|
||||
- `00 00` - Min velocity (usually 0)
|
||||
- `8A 44 0A 00` - Max velocity = 671,090 APT units (5 mm/s)
|
||||
- `89 00 00 00` - Acceleration = 137 APT units (10 mm/s²)
|
||||
|
||||
### 6.6 Stop Motion
|
||||
|
||||
To immediately stop motion on all channels:
|
||||
|
||||
**Command bytes:**
|
||||
```
|
||||
TX: 65 04 01 01 11 01
|
||||
```
|
||||
|
||||
**Breakdown:**
|
||||
- `65 04` - MGMSG_MOT_MOVE_STOP
|
||||
- `01` - Channel identifier
|
||||
- `01` - Stop mode (0x01 = immediate, 0x02 = profiled)
|
||||
- `11` - Destination (All channels)
|
||||
- `01` - Source (Host PC)
|
||||
|
||||
### 6.7 Request Status Update
|
||||
|
||||
To request a status update from channel 1:
|
||||
|
||||
**Command bytes:**
|
||||
```
|
||||
TX: 80 04 01 00 21 01
|
||||
```
|
||||
|
||||
**Breakdown:**
|
||||
- `80 04` - MGMSG_MOT_REQ_STATUSUPDATE
|
||||
- `01` - Channel identifier
|
||||
- `00` - Not used
|
||||
- `21` - Destination (Channel 1)
|
||||
- `01` - Source (Host PC)
|
||||
|
||||
**Response format (20 bytes):**
|
||||
```
|
||||
RX: 81 04 14 00 01 00 [Channel] [Position-4bytes] [EncCount-4bytes] [StatusBits-4bytes]
|
||||
```
|
||||
|
||||
### 6.8 Set Position Loop Parameters
|
||||
|
||||
To set position loop PID parameters for channel 1:
|
||||
|
||||
**Command bytes:**
|
||||
```
|
||||
TX: D7 04 1C 00 21 01 01 00 41 00 AF 00 80 38 01 00 [12 more bytes...]
|
||||
```
|
||||
|
||||
**Data packet structure:**
|
||||
- Bytes 0-1: Channel (0x01 0x00)
|
||||
- Bytes 2-3: Proportional gain
|
||||
- Bytes 4-5: Integral gain
|
||||
- Bytes 6-9: Integral limit
|
||||
- Bytes 10-13: Derivative gain
|
||||
- Bytes 14-15: Derivative time
|
||||
- Bytes 16-17: Loop rate
|
||||
- Bytes 18-19: Output gain
|
||||
- Bytes 20-23: Velocity feedforward gain
|
||||
- Bytes 24-25: Acceleration feedforward gain
|
||||
- Bytes 26-27: Position error limit
|
||||
|
||||
---
|
||||
|
||||
## 7. Important Notes for BBD203
|
||||
|
||||
### 7.1 Digital Output Configuration
|
||||
|
||||
On the BBD203, the digital output and trigger output share a common pin. Before using the digital output functionality, the trigger functionality must be disabled by calling the `MGMSG_MOT_SET_TRIGGER` message with appropriate parameters.
|
||||
|
||||
**To disable trigger and enable digital output:**
|
||||
```
|
||||
TX: 00 05 06 00 21 01 01 00 00 00 00 00
|
||||
```
|
||||
|
||||
### 7.2 Multi-Channel Operation
|
||||
|
||||
Although the BBD203 has three channels, each channel operates as an independent single-channel controller. Important points:
|
||||
|
||||
- Always use Channel 1 (`0x01 0x00`) in the channel identifier field of data packets
|
||||
- Use the destination byte (`0x21`, `0x22`, or `0x23`) in the header to specify the physical channel
|
||||
- Each channel maintains its own parameters and status independently
|
||||
|
||||
### 7.3 Encoder Scaling
|
||||
|
||||
All position values must be scaled according to the encoder counts per unit of your specific motor and stage combination. Common encoder resolutions:
|
||||
|
||||
| Stage Type | Encoder Counts/mm | Notes |
|
||||
|------------|-------------------|-------|
|
||||
| MLS203 | 20,000 | Standard linear stage |
|
||||
| DDS220 | 2,000 | Direct drive stage |
|
||||
| Custom | Varies | Check motor specification |
|
||||
|
||||
### 7.4 Status Updates
|
||||
|
||||
After connecting to the BBD203, it is important to:
|
||||
|
||||
1. Call `MGMSG_HW_START_UPDATEMSGS` to enable automatic status updates
|
||||
2. This ensures move completed and other status messages are received properly
|
||||
3. Status updates can be disabled with `MGMSG_HW_STOP_UPDATEMSGS` when not needed
|
||||
|
||||
**Enable status updates:**
|
||||
```
|
||||
TX: 11 00 00 00 11 01
|
||||
```
|
||||
|
||||
### 7.5 Error Handling
|
||||
|
||||
The BBD203 returns error messages via `MGMSG_HW_RESPONSE` (0x0080) or `MGMSG_HW_RICHRESPONSE` (0x0081). Common error conditions:
|
||||
|
||||
| Error | Description |
|
||||
|-------|------------|
|
||||
| Over current | Motor drawing excessive current |
|
||||
| Following error | Position error exceeds limit |
|
||||
| Limit switch | Hardware limit reached |
|
||||
| Not homed | Attempting move before homing |
|
||||
|
||||
### 7.6 Trigger Configuration
|
||||
|
||||
The BBD203 supports hardware triggering for synchronized motion. Trigger modes:
|
||||
|
||||
| Mode | Value | Description |
|
||||
|------|-------|------------|
|
||||
| Disabled | 0x00 | No triggering |
|
||||
| In/Out Relative Move | 0x01 | Trigger initiates relative move |
|
||||
| In/Out Absolute Move | 0x02 | Trigger initiates absolute move |
|
||||
| In/Out Home | 0x03 | Trigger initiates homing |
|
||||
| In/Out Stop | 0x04 | Trigger stops motion |
|
||||
| Out Only | 0x10 | Generate trigger output on move |
|
||||
| Out Position | 0x11 | Trigger at specific position |
|
||||
|
||||
### 7.7 Profile Modes
|
||||
|
||||
The BBD203 supports different motion profile modes:
|
||||
|
||||
| Mode | Value | Description |
|
||||
|------|-------|------------|
|
||||
| Trapezoidal | 0x00 | Linear acceleration/deceleration |
|
||||
| S-Curve | 0x02 | Smooth acceleration with jerk limiting |
|
||||
|
||||
### 7.8 Communication Best Practices
|
||||
|
||||
1. **Initialization Sequence:**
|
||||
- Send `MGMSG_HW_REQ_INFO` to verify connection
|
||||
- Enable required channels with `MGMSG_MOD_SET_CHANENABLESTATE`
|
||||
- Start update messages with `MGMSG_HW_START_UPDATEMSGS`
|
||||
- Home axes if required
|
||||
|
||||
2. **Movement Sequence:**
|
||||
- Set velocity/acceleration parameters
|
||||
- Clear any errors
|
||||
- Send move command
|
||||
- Wait for move completed message
|
||||
|
||||
3. **Shutdown Sequence:**
|
||||
- Stop any motion with `MGMSG_MOT_MOVE_STOP`
|
||||
- Disable channels if needed
|
||||
- Send `MGMSG_HW_DISCONNECT` before closing port
|
||||
|
||||
---
|
||||
|
||||
## Additional Information
|
||||
|
||||
This document contains only the essential information for controlling the BBD203 motor controller. For complete protocol details, advanced features, and other Thorlabs motion control products, please refer to the full APT Communications Protocol documentation.
|
||||
|
||||
### Contact Information
|
||||
|
||||
**Thorlabs, Inc.**
|
||||
- Website: www.thorlabs.com
|
||||
- Technical Support: techsupport@thorlabs.com
|
||||
|
||||
---
|
||||
|
||||
*Document generated from Thorlabs APT Communications Protocol v42.1*
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Introduction](#1-introduction)
|
||||
2. [General Protocol Information](#2-general-protocol-information)
|
||||
3. [BBD203 Specifications](#3-bbd203-specifications)
|
||||
4. [Message Format](#4-message-format)
|
||||
5. [BBD203 Applicable Messages](#5-bbd203-applicable-messages)
|
||||
6. [Command Examples](#6-command-examples)
|
||||
7. [Important Notes](#7-important-notes)
|
||||
|
||||
---
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
The BBD203 is a 3-channel benchtop brushless DC motor controller that is part of Thorlabs' APT motion control system. This document describes the communication protocol used to control the BBD203 via USB or RS232 interfaces.
|
||||
|
||||
### 1.1 Device Overview
|
||||
|
||||
The BBD203 provides independent control of up to three brushless DC motors with the following key features:
|
||||
|
||||
- 3 independent motor channels
|
||||
- USB and RS232 communication interfaces
|
||||
- Closed-loop position and velocity control
|
||||
- Encoder feedback support
|
||||
- Digital I/O for triggering and synchronization
|
||||
- Compatible with Thorlabs' APT software suite
|
||||
|
||||
### 1.2 Device Information
|
||||
|
||||
- **Product Name**: BBD203 - 3 Channel Benchtop Brushless DC Motor Driver
|
||||
- **Protocol Version**: 42.1
|
||||
- **Communication**: Binary message protocol over USB/RS232
|
||||
|
||||
---
|
||||
|
||||
## 2. General Protocol Information
|
||||
|
||||
### 2.1 Communication Format
|
||||
|
||||
All communications with the BBD203 use a binary message protocol. Messages consist of a 6-byte header followed by an optional data packet.
|
||||
|
||||
#### Header Structure
|
||||
|
||||
| Byte | Description |
|
||||
|------|-------------|
|
||||
| 0-1 | Message ID (16-bit, little-endian) |
|
||||
| 2 | Data length (bytes) or parameter 1 |
|
||||
| 3 | Data length MSB or parameter 2 |
|
||||
| 4 | Destination |
|
||||
| 5 | Source |
|
||||
|
||||
#### Destination Byte Values
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| 0x50 | USB interface |
|
||||
| 0x11 | All channels (unit) |
|
||||
| 0x21 | Channel 1 (Bay 1) |
|
||||
| 0x22 | Channel 2 (Bay 2) |
|
||||
| 0x23 | Channel 3 (Bay 3) |
|
||||
|
||||
#### Source Byte Values
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| 0x01 | Host PC |
|
||||
|
||||
### 2.2 Channel Addressing
|
||||
|
||||
The BBD203 has three motor channels. When addressing specific channels:
|
||||
|
||||
- **Channel 1**: Use destination byte `0x21`
|
||||
- **Channel 2**: Use destination byte `0x22`
|
||||
- **Channel 3**: Use destination byte `0x23`
|
||||
- **All channels**: Use destination byte `0x11`
|
||||
|
||||
**Important**: Although the BBD203 has three channels, each channel operates as an independent single-channel controller. In the data packet's channel identifier field, always use Channel 1 (0x01), and specify the physical channel using the destination byte in the header.
|
||||
|
||||
### 2.3 Data Types
|
||||
|
||||
| Type | Size | Description |
|
||||
|------|------|-------------|
|
||||
| char | 1 byte | 8-bit signed integer |
|
||||
| short | 2 bytes | 16-bit signed integer |
|
||||
| long | 4 bytes | 32-bit signed integer |
|
||||
| word | 2 bytes | 16-bit unsigned integer |
|
||||
| dword | 4 bytes | 32-bit unsigned integer |
|
||||
|
||||
All multi-byte values are transmitted in little-endian format.
|
||||
|
||||
---
|
||||
|
||||
## 3. BBD203 Specifications
|
||||
|
||||
### 3.1 Encoder and Position Scaling
|
||||
|
||||
For BBD203 controllers, position and velocity values are scaled based on encoder counts. The scaling depends on your specific motor and stage combination.
|
||||
|
||||
#### Position Scaling
|
||||
```
|
||||
POSAPT = EncCnt × Pos
|
||||
```
|
||||
Where:
|
||||
- `POSAPT` = Position value to send/receive via APT protocol
|
||||
- `EncCnt` = Encoder counts per unit (e.g., counts per mm)
|
||||
- `Pos` = Actual position in real units
|
||||
|
||||
#### Velocity Scaling
|
||||
```
|
||||
VELAPT = EncCnt × T × 65536 × Vel
|
||||
```
|
||||
Where:
|
||||
- `VELAPT` = Velocity value to send/receive via APT protocol
|
||||
- `EncCnt` = Encoder counts per unit
|
||||
- `T` = 102.4 × 10⁻⁶
|
||||
- `Vel` = Actual velocity in real units per second
|
||||
|
||||
#### Acceleration Scaling
|
||||
```
|
||||
ACCAPT = EncCnt × T² × 65536 × Acc
|
||||
```
|
||||
Where:
|
||||
- `ACCAPT` = Acceleration value to send/receive via APT protocol
|
||||
- `EncCnt` = Encoder counts per unit
|
||||
- `T` = 102.4 × 10⁻⁶
|
||||
- `Acc` = Actual acceleration in real units per second²
|
||||
|
||||
### 3.2 Example Scaling Values
|
||||
|
||||
For a stage with 20,000 encoder counts per mm:
|
||||
|
||||
| Parameter | Real Value | APT Value |
|
||||
|-----------|------------|-----------|
|
||||
| Position | 10 mm | 200,000 |
|
||||
| Position | 50 mm | 1,000,000 |
|
||||
| Velocity | 1 mm/s | 134.218 |
|
||||
| Acceleration | 1 mm/s² | 0.0137 |
|
||||
|
||||
---
|
||||
|
||||
## 4. Message Format
|
||||
|
||||
### 4.1 Message Categories
|
||||
|
||||
Messages are organized into the following categories:
|
||||
|
||||
- **MOD** - Module control messages (identify, enable/disable)
|
||||
- **HW** - Hardware information and control
|
||||
- **MOT** - Motor control messages (move, velocity, position)
|
||||
- **RACK** - Rack and bay status messages
|
||||
|
||||
### 4.2 Message Direction
|
||||
|
||||
- **SET** - Host sends command with parameters to controller
|
||||
- **REQ
|
||||
@@ -0,0 +1,390 @@
|
||||
# ThorLabs BBD203 Motor Controller Driver
|
||||
|
||||
## Overview
|
||||
|
||||
This directory contains a complete implementation of the ThorLabs BBD203 3-channel benchtop brushless DC motor controller driver. The driver implements the full APT (Advanced Positioning Technology) binary communications protocol as specified in the BBD203_Communications_Protocol.md document.
|
||||
|
||||
## Architecture
|
||||
|
||||
The BBD203 driver is split into three layers:
|
||||
|
||||
### 1. Protocol Layer (`bbd203_protocol.py`)
|
||||
|
||||
Low-level protocol implementation that handles:
|
||||
- Binary message construction and parsing
|
||||
- APT protocol message IDs and structures
|
||||
- Unit conversions (mm ↔ encoder counts, velocity/acceleration scaling)
|
||||
- Status bit definitions
|
||||
|
||||
**Key Classes:**
|
||||
- `MessageID`: Enumeration of all APT message IDs
|
||||
- `APTMessage`: Message builder and parser for binary protocol
|
||||
- `APTProtocol`: High-level protocol interface with unit conversions
|
||||
|
||||
### 2. Driver Layer (`bbd203_driver.py`)
|
||||
|
||||
Complete driver implementation providing:
|
||||
- Serial communication with automatic message reception thread
|
||||
- 3-channel management (independent motor control)
|
||||
- Blocking and non-blocking move operations
|
||||
- Status monitoring with automatic updates
|
||||
- Event callbacks for move/home completion
|
||||
- Thread-safe operation
|
||||
|
||||
**Key Classes:**
|
||||
- `BBD203Channel`: Represents state of a single motor channel
|
||||
- `BBD203Driver`: Main driver class for controller communication
|
||||
|
||||
### 3. Stage Interface Layer (`thorlabs_stage.py`)
|
||||
|
||||
Application-specific wrapper that:
|
||||
- Maps 3 motor channels to X/Y/Z axes
|
||||
- Provides simplified API for stage control
|
||||
- Integrates with the nueScan application
|
||||
- Maintains compatibility with existing UI
|
||||
|
||||
**Channel Mapping:**
|
||||
- Channel 1 → X-axis
|
||||
- Channel 2 → Y-axis
|
||||
- Channel 3 → Z-axis (optional)
|
||||
|
||||
## Features
|
||||
|
||||
### Communication
|
||||
- Binary APT protocol over USB/RS232
|
||||
- Baud rate: 115200 (configurable)
|
||||
- Automatic message reception in background thread
|
||||
- Command/response handling with proper timeout
|
||||
|
||||
### Motion Control
|
||||
- Absolute positioning
|
||||
- Relative moves
|
||||
- Velocity control
|
||||
- Immediate and profiled stops
|
||||
- Configurable acceleration
|
||||
|
||||
### Position Feedback
|
||||
- Real-time position updates (encoder counts)
|
||||
- Position in mm (with configurable scaling)
|
||||
- Status bit monitoring (homing, moving, errors, etc.)
|
||||
|
||||
### Homing
|
||||
- Individual axis homing
|
||||
- All-axes homing
|
||||
- Blocking or non-blocking operation
|
||||
- Completion callbacks
|
||||
|
||||
### Safety
|
||||
- Interlock checking before moves
|
||||
- Error detection and reporting
|
||||
- Motion error monitoring
|
||||
- Limit switch status
|
||||
|
||||
## Usage
|
||||
|
||||
### Connection Methods
|
||||
|
||||
The driver supports two connection methods:
|
||||
|
||||
#### Method 1: Connect by Serial Number (Recommended)
|
||||
|
||||
Similar to ThorLabs Kinesis library - automatically finds the USB device:
|
||||
|
||||
```python
|
||||
from hardware.bbd203_driver import BBD203Driver
|
||||
|
||||
# Create driver instance
|
||||
driver = BBD203Driver(encoder_counts_per_mm=20000)
|
||||
|
||||
# List available ThorLabs devices
|
||||
devices = driver.list_thorlabs_devices()
|
||||
for device in devices:
|
||||
print(f"Serial: {device['serial']}, Port: {device['port']}")
|
||||
|
||||
# Connect by serial number (auto-finds the port)
|
||||
driver.connect_by_serial('83123456') # Serial printed on controller
|
||||
```
|
||||
|
||||
#### Method 2: Connect by Port Name
|
||||
|
||||
Direct connection to a specific port:
|
||||
|
||||
```python
|
||||
# Connect to specific port
|
||||
driver.connect('/dev/ttyUSB0') # or 'COM3' on Windows
|
||||
```
|
||||
|
||||
### Basic Movement
|
||||
|
||||
```python
|
||||
|
||||
# Enable all channels
|
||||
driver.enable_channel(1, True) # X-axis
|
||||
driver.enable_channel(2, True) # Y-axis
|
||||
driver.enable_channel(3, True) # Z-axis
|
||||
|
||||
# Home all channels (blocking)
|
||||
driver.home_all_channels(wait=True, timeout=60)
|
||||
|
||||
# Set velocity parameters
|
||||
driver.set_velocity_params(channel=1, max_vel_mm_s=5.0, accel_mm_s2=10.0)
|
||||
|
||||
# Move to absolute position (non-blocking)
|
||||
driver.move_absolute(channel=1, position_mm=10.0, wait=False)
|
||||
|
||||
# Move to absolute position (blocking)
|
||||
driver.move_absolute(channel=2, position_mm=25.0, wait=True, timeout=30)
|
||||
|
||||
# Move relative
|
||||
driver.move_relative(channel=1, distance_mm=-5.0, wait=True)
|
||||
|
||||
# Stop motion
|
||||
driver.stop(channel=1, immediate=True)
|
||||
|
||||
# Get position
|
||||
pos = driver.get_position(channel=1)
|
||||
print(f"Position: {pos} mm")
|
||||
|
||||
# Get detailed status
|
||||
status = driver.get_channel_status(channel=1)
|
||||
print(f"Enabled: {status['enabled']}")
|
||||
print(f"Homed: {status['homed']}")
|
||||
print(f"Moving: {status['moving']}")
|
||||
|
||||
# Disconnect
|
||||
driver.disconnect()
|
||||
```
|
||||
|
||||
### Using the Stage Interface
|
||||
|
||||
```python
|
||||
from hardware.thorlabs_stage import ThorLabsStage
|
||||
|
||||
# Create stage controller
|
||||
stage = ThorLabsStage(encoder_counts_per_mm=20000)
|
||||
|
||||
# List available devices
|
||||
devices = stage.list_devices()
|
||||
for device in devices:
|
||||
print(f"BBD203 Serial: {device['serial']}")
|
||||
|
||||
# Connect by serial number (automatically enables all channels)
|
||||
stage.connect('83123456') # Serial number from controller label
|
||||
|
||||
# Home all axes
|
||||
stage.home_all_axes(wait=True)
|
||||
|
||||
# Move to position
|
||||
stage.move_absolute(x=10.0, y=20.0, wait=True)
|
||||
|
||||
# Move relative
|
||||
stage.move_relative(dx=5.0, dy=-2.5, wait=True)
|
||||
|
||||
# Get position
|
||||
pos = stage.get_position()
|
||||
print(f"X: {pos['x']} mm, Y: {pos['y']} mm, Z: {pos['z']} mm")
|
||||
|
||||
# Check status
|
||||
status = stage.get_status()
|
||||
print(f"Ready: {status['ready']}")
|
||||
print(f"X Homed: {status['x_homed']}")
|
||||
|
||||
# Disconnect
|
||||
stage.disconnect()
|
||||
```
|
||||
|
||||
### Event Callbacks
|
||||
|
||||
```python
|
||||
# Define callback function
|
||||
def on_move_complete(channel):
|
||||
print(f"Channel {channel} move completed!")
|
||||
|
||||
# Register callback
|
||||
driver.register_move_complete_callback(1, on_move_complete)
|
||||
|
||||
# Start non-blocking move - callback will be called when complete
|
||||
driver.move_absolute(channel=1, position_mm=50.0, wait=False)
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Encoder Scaling
|
||||
|
||||
The encoder resolution must be configured to match your specific motor/stage combination:
|
||||
|
||||
```python
|
||||
# Example: MLS203 stage with 20,000 counts/mm
|
||||
driver = BBD203Driver(encoder_counts_per_mm=20000)
|
||||
|
||||
# Example: Custom stage with 2,000 counts/mm
|
||||
driver = BBD203Driver(encoder_counts_per_mm=2000)
|
||||
```
|
||||
|
||||
Common encoder resolutions:
|
||||
- **MLS203**: 20,000 counts/mm
|
||||
- **DDS220**: 2,000 counts/mm
|
||||
- **Custom**: Varies (check motor specifications)
|
||||
|
||||
### Velocity and Acceleration
|
||||
|
||||
Velocity and acceleration use the APT scaling formulas:
|
||||
|
||||
```
|
||||
VEL_APT = EncCnt × 102.4e-6 × 65536 × Vel
|
||||
ACC_APT = EncCnt × (102.4e-6)² × 65536 × Acc
|
||||
```
|
||||
|
||||
The driver handles these conversions automatically:
|
||||
|
||||
```python
|
||||
# Set velocity to 5 mm/s, acceleration to 10 mm/s²
|
||||
driver.set_velocity_params(
|
||||
channel=1,
|
||||
max_vel_mm_s=5.0,
|
||||
accel_mm_s2=10.0
|
||||
)
|
||||
```
|
||||
|
||||
## Integration with nueScan
|
||||
|
||||
The driver is integrated into nueScan through the `ThorLabsStage` wrapper class. The connection is simplified using serial number auto-detection:
|
||||
|
||||
### Connecting in the UI
|
||||
|
||||
1. **Find Serial Number**: Look at the label on your BBD203 controller (e.g., `83123456`)
|
||||
2. **Enter Serial**: Type the serial number in the "ThorLABS MLS Stage Serial" field
|
||||
3. **Connect**: Click "Connect" button
|
||||
- Driver automatically finds the USB device
|
||||
- All 3 channels are enabled
|
||||
- Status updates begin
|
||||
4. **Ready**: The controller is now ready to home and move axes
|
||||
|
||||
### Connection Process
|
||||
|
||||
When you click "Connect":
|
||||
- The driver scans all USB ports for ThorLabs devices (FTDI VID: 0x0403)
|
||||
- Finds the device matching your serial number
|
||||
- Automatically uses the correct COM port
|
||||
- Enables all channels (X/Y/Z axes)
|
||||
- Sets default velocity parameters
|
||||
|
||||
### Troubleshooting Connection
|
||||
|
||||
If connection fails, a dialog will show:
|
||||
- The serial number you entered
|
||||
- List of all detected ThorLabs devices with their serial numbers
|
||||
- Helps you identify the correct serial to use
|
||||
|
||||
## Protocol Details
|
||||
|
||||
### Message Structure
|
||||
|
||||
All APT messages consist of:
|
||||
- 6-byte header (message ID, length, destination, source)
|
||||
- Optional data packet (variable length)
|
||||
|
||||
### Destination Addressing
|
||||
|
||||
- `0x21`: Channel 1 (X-axis)
|
||||
- `0x22`: Channel 2 (Y-axis)
|
||||
- `0x23`: Channel 3 (Z-axis)
|
||||
- `0x11`: All channels
|
||||
- `0x50`: USB interface
|
||||
|
||||
### Status Bits
|
||||
|
||||
Key status bits monitored by the driver:
|
||||
|
||||
| Bit | Mask | Meaning |
|
||||
|-----|------|---------|
|
||||
| HOMING | 0x00000200 | Homing in progress |
|
||||
| HOMED | 0x00000400 | Axis has been homed |
|
||||
| TRACKING | 0x00001000 | Following target position |
|
||||
| SETTLED | 0x00002000 | Position settled |
|
||||
| MOTION_ERROR | 0x00004000 | Following error exceeded |
|
||||
| MOTOR_ENABLED | 0x80000000 | Motor drive enabled |
|
||||
| IN_MOTION_FORWARD | 0x00000010 | Moving forward |
|
||||
| IN_MOTION_REVERSE | 0x00000020 | Moving reverse |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Issues
|
||||
|
||||
**Problem:** Cannot connect to controller
|
||||
|
||||
**Solutions:**
|
||||
- Verify COM port name is correct (`ThorLabsStage.list_available_ports()`)
|
||||
- Check USB cable connection
|
||||
- Verify no other software has the port open
|
||||
- Try different baud rate (default: 115200)
|
||||
- Check device permissions on Linux
|
||||
|
||||
### Homing Fails
|
||||
|
||||
**Problem:** Homing timeout or never completes
|
||||
|
||||
**Solutions:**
|
||||
- Increase homing timeout parameter
|
||||
- Check limit switches are functioning
|
||||
- Verify motor is enabled
|
||||
- Check for mechanical obstructions
|
||||
- Review homing parameters (direction, velocity)
|
||||
|
||||
### Position Errors
|
||||
|
||||
**Problem:** Reported position doesn't match reality
|
||||
|
||||
**Solutions:**
|
||||
- Verify `encoder_counts_per_mm` setting matches your stage
|
||||
- Check encoder connections
|
||||
- Reset position counter if needed: `driver.cmd_set_position_counter()`
|
||||
- Verify stage is homed before moves
|
||||
|
||||
### Communication Errors
|
||||
|
||||
**Problem:** Commands not acknowledged or responses missing
|
||||
|
||||
**Solutions:**
|
||||
- Increase serial timeout
|
||||
- Check for message buffer overflow
|
||||
- Verify automatic status updates are enabled
|
||||
- Add delays between rapid commands
|
||||
|
||||
## Debug Output
|
||||
|
||||
The driver provides extensive debug output:
|
||||
|
||||
```
|
||||
INFO: Messages about successful operations
|
||||
DEBUG: Detailed command/response information
|
||||
ERROR: Error conditions and failures
|
||||
WARNING: Potential issues
|
||||
```
|
||||
|
||||
Enable Python logging to capture all output:
|
||||
|
||||
```python
|
||||
import logging
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
```
|
||||
|
||||
## Performance Notes
|
||||
|
||||
- Message processing runs in separate thread (no blocking)
|
||||
- Typical command response time: 10-50ms
|
||||
- Position updates: ~10Hz when status messages enabled
|
||||
- Move completion detected via asynchronous message
|
||||
- Thread-safe for concurrent channel operations
|
||||
|
||||
## References
|
||||
|
||||
- **Protocol Documentation**: `BBD203_Communications_Protocol.md`
|
||||
- **APT Protocol Version**: 42.1
|
||||
- **Product Manual**: Available from Thorlabs.com
|
||||
- **Technical Support**: techsupport@thorlabs.com
|
||||
|
||||
## License
|
||||
|
||||
Copyright (C) 2025 Thomas Ales
|
||||
Licensed under GNU General Public License v2.0
|
||||
@@ -0,0 +1,596 @@
|
||||
# Helios Laser System Driver
|
||||
|
||||
## Overview
|
||||
|
||||
This directory contains a complete implementation of the Helios laser system driver. The driver implements the full RS-232 ASCII communications protocol as specified in the helios_comms_protocol.pdf document.
|
||||
|
||||
The Helios laser system is a pulsed solid-state laser with:
|
||||
- Diode-pumped Nd:YAG/Nd:YLF laser head
|
||||
- Q-switched operation
|
||||
- Frequency control (16.7 kHz - 125 kHz)
|
||||
- Current control (0-7000 mA)
|
||||
- Power monitoring
|
||||
- Temperature monitoring (4 sensors)
|
||||
- External trigger capability
|
||||
|
||||
## Architecture
|
||||
|
||||
The Helios driver is split into three layers:
|
||||
|
||||
### 1. Protocol Layer (`helios_protocol.py`)
|
||||
|
||||
Low-level protocol implementation that handles:
|
||||
- ASCII command construction
|
||||
- Response parsing and validation
|
||||
- Unit conversions (Hz ↔ ns, °C ↔ m°C)
|
||||
- Parameter range validation
|
||||
- Status register decoding
|
||||
|
||||
**Key Classes:**
|
||||
- `HeliosCommand`: Command constants and builders
|
||||
- `HeliosProtocol`: High-level protocol interface with validation
|
||||
|
||||
### 2. Driver Layer (`helios_driver.py`)
|
||||
|
||||
Complete driver implementation providing:
|
||||
- RS-232 serial communication (9600 baud, 8N1)
|
||||
- Thread-safe command/query operations
|
||||
- Comprehensive status monitoring
|
||||
- Temperature monitoring (pump, resonator, q-switch, power stage)
|
||||
- Power monitoring
|
||||
- Laser enable/disable control
|
||||
- Pulse mode control (single, gating, continuous)
|
||||
- Frequency and current control
|
||||
|
||||
**Key Classes:**
|
||||
- `PulseMode`: Enumeration of pulse modes
|
||||
- `HeliosStatus`: Status data structure
|
||||
- `HeliosDriver`: Main driver class for laser communication
|
||||
|
||||
### 3. Integration Layer (`hardware/microscope.py`)
|
||||
|
||||
Application-specific integration that:
|
||||
- Combines Helios with Genesis microscope systems
|
||||
- Provides unified status monitoring
|
||||
- Integrates with nueScan application
|
||||
- Implements safety interlocks
|
||||
- Provides emergency stop functionality
|
||||
|
||||
## Features
|
||||
|
||||
### Communication
|
||||
- ASCII-based RS-232 protocol
|
||||
- Baud rate: 9600, 8 data bits, no parity, 1 stop bit
|
||||
- Commands terminated with carriage return (CR)
|
||||
- Thread-safe operation with mutex locking
|
||||
- Configurable timeout (default: 1 second)
|
||||
|
||||
### Laser Control
|
||||
- Laser enable/disable
|
||||
- Three pulse modes:
|
||||
- Single pulse (one pulse per trigger)
|
||||
- Continuous gating (pulse train while triggered)
|
||||
- Continuous pulsing (free-running)
|
||||
- Frequency control (16.7 kHz to 125 kHz)
|
||||
- Diode current control (0-7000 mA)
|
||||
|
||||
### Monitoring
|
||||
- Real-time power measurement (mW)
|
||||
- Four temperature sensors:
|
||||
- Pump diode temperature
|
||||
- Resonator temperature
|
||||
- Q-switch temperature
|
||||
- Power stage temperature
|
||||
- Operation hours counter
|
||||
- Comprehensive status register
|
||||
- Error detection
|
||||
|
||||
### Safety
|
||||
- Temperature monitoring with warnings
|
||||
- Error status detection
|
||||
- Laser enable/disable control
|
||||
- Emergency stop capability
|
||||
- Integration with system interlocks
|
||||
|
||||
## Usage
|
||||
|
||||
### Connection Methods
|
||||
|
||||
The driver supports connection to a specific COM port:
|
||||
|
||||
```python
|
||||
from hardware.helios_driver import HeliosDriver
|
||||
|
||||
# Create driver instance
|
||||
driver = HeliosDriver(timeout=1.0)
|
||||
|
||||
# List available COM ports
|
||||
ports = HeliosDriver.list_available_ports()
|
||||
for port in ports:
|
||||
print(f"Available port: {port}")
|
||||
|
||||
# Connect to specific port
|
||||
driver.connect('COM5') # or '/dev/ttyUSB0' on Linux
|
||||
|
||||
# Get device information
|
||||
print(f"Controller S/N: {driver.get_controller_serial()}")
|
||||
print(f"Head S/N: {driver.get_head_serial()}")
|
||||
```
|
||||
|
||||
### Basic Laser Control
|
||||
|
||||
```python
|
||||
# Set frequency (in Hz)
|
||||
driver.set_frequency_hz(10000) # 10 kHz
|
||||
|
||||
# Set diode current (in mA)
|
||||
driver.set_current_ma(500) # 500 mA
|
||||
|
||||
# Set pulse mode
|
||||
from hardware.helios_driver import PulseMode
|
||||
driver.set_pulse_mode(PulseMode.CONTINUOUS_PULSING)
|
||||
|
||||
# Enable laser
|
||||
driver.set_laser_enable(True)
|
||||
|
||||
# Check if laser is enabled
|
||||
if driver.is_laser_enabled():
|
||||
print("Laser is ON")
|
||||
|
||||
# Disable laser
|
||||
driver.set_laser_enable(False)
|
||||
|
||||
# Disconnect
|
||||
driver.disconnect()
|
||||
```
|
||||
|
||||
### Monitoring
|
||||
|
||||
```python
|
||||
# Get current power
|
||||
power_mw = driver.get_power_mw()
|
||||
print(f"Output power: {power_mw} mW")
|
||||
|
||||
# Get temperatures (in Celsius)
|
||||
temps = driver.get_all_temperatures()
|
||||
print(f"Pump: {temps['pump_temp_c']:.1f}°C")
|
||||
print(f"Resonator: {temps['resonator_temp_c']:.1f}°C")
|
||||
print(f"Q-switch: {temps['qswitch_temp_c']:.1f}°C")
|
||||
print(f"Power stage: {temps['power_stage_temp_c']:.1f}°C")
|
||||
|
||||
# Get operation hours
|
||||
hours = driver.get_operation_hours()
|
||||
print(f"Operation time: {hours} hours")
|
||||
|
||||
# Get comprehensive status
|
||||
status = driver.get_status()
|
||||
print(f"Connected: {status['connected']}")
|
||||
print(f"Laser enabled: {status['laser_enabled']}")
|
||||
print(f"Frequency: {status['frequency_hz']} Hz")
|
||||
print(f"Current: {status['current_ma']} mA")
|
||||
print(f"Power: {status['power_mw']} mW")
|
||||
print(f"Has errors: {status['has_errors']}")
|
||||
```
|
||||
|
||||
### Status Updates
|
||||
|
||||
```python
|
||||
# Manually update status from hardware
|
||||
driver.update_status()
|
||||
|
||||
# Status is automatically updated on each get_status() call
|
||||
status = driver.get_status()
|
||||
|
||||
# Access cached values without querying hardware
|
||||
freq = driver.get_frequency_hz() # Returns last read value
|
||||
current = driver.get_current_ma() # Returns last read value
|
||||
```
|
||||
|
||||
### Using Through Microscope Controller
|
||||
|
||||
The Helios driver is integrated into the application through the `MicroscopeController`:
|
||||
|
||||
```python
|
||||
from hardware.microscope import MicroscopeController
|
||||
|
||||
# Create controller
|
||||
microscope = MicroscopeController()
|
||||
|
||||
# Connect Helios
|
||||
microscope.connect_helios('COM5')
|
||||
|
||||
# Apply settings from dialog
|
||||
settings = {
|
||||
'com_port': 'COM5',
|
||||
'frequency_hz': 10000,
|
||||
'current_ma': 500
|
||||
}
|
||||
microscope.apply_helios_settings(settings)
|
||||
|
||||
# Enable laser
|
||||
microscope.helios_enable_laser(True)
|
||||
|
||||
# Get status
|
||||
status = microscope.get_helios_status()
|
||||
print(f"Helios ready: {status['ready']}")
|
||||
print(f"Power: {status['power_mw']} mW")
|
||||
|
||||
# Disable laser
|
||||
microscope.helios_enable_laser(False)
|
||||
|
||||
# Disconnect
|
||||
microscope.disconnect_helios()
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Frequency Control
|
||||
|
||||
The Helios laser operates by setting the pulse period in nanoseconds. The driver automatically converts between frequency (Hz) and period (ns):
|
||||
|
||||
```python
|
||||
# Set frequency in Hz (driver converts to period in ns)
|
||||
driver.set_frequency_hz(10000) # 10 kHz → 100,000 ns period
|
||||
|
||||
# Valid frequency range: 16.7 kHz to 125 kHz
|
||||
# Valid period range: 8000 ns to 60000 ns
|
||||
```
|
||||
|
||||
Conversion formulas:
|
||||
```
|
||||
Period (ns) = 1,000,000,000 / Frequency (Hz)
|
||||
Frequency (Hz) = 1,000,000,000 / Period (ns)
|
||||
```
|
||||
|
||||
### Current Control
|
||||
|
||||
The diode current controls the laser output power:
|
||||
|
||||
```python
|
||||
# Set current in milliamps
|
||||
driver.set_current_ma(500) # 500 mA
|
||||
|
||||
# Valid range: 0 to 7000 mA
|
||||
```
|
||||
|
||||
**Important:** Higher currents produce more power but also more heat. Monitor temperatures when operating at high current.
|
||||
|
||||
### Pulse Modes
|
||||
|
||||
Three pulse modes are available:
|
||||
|
||||
```python
|
||||
from hardware.helios_driver import PulseMode
|
||||
|
||||
# Single pulse mode (one pulse per trigger)
|
||||
driver.set_pulse_mode(PulseMode.SINGLE_PULSE)
|
||||
|
||||
# Continuous gating mode (pulse train while triggered)
|
||||
driver.set_pulse_mode(PulseMode.CONTINUOUS_GATING)
|
||||
|
||||
# Continuous pulsing mode (free-running)
|
||||
driver.set_pulse_mode(PulseMode.CONTINUOUS_PULSING)
|
||||
```
|
||||
|
||||
**Mode Descriptions:**
|
||||
- **Single Pulse (LDG=0)**: One pulse generated per external trigger
|
||||
- **Continuous Gating (LDG=1)**: Pulse train while external trigger is high
|
||||
- **Continuous Pulsing (LDG=2)**: Free-running at set frequency (default)
|
||||
|
||||
### Temperature Monitoring
|
||||
|
||||
The driver monitors four temperature sensors:
|
||||
|
||||
```python
|
||||
# Individual temperatures
|
||||
pump_temp = driver.query_pump_temp_c()
|
||||
resonator_temp = driver.query_resonator_temp_c()
|
||||
qswitch_temp = driver.query_qswitch_temp_c()
|
||||
power_stage_temp = driver.query_power_stage_temp_c()
|
||||
|
||||
# All temperatures at once
|
||||
temps = driver.get_all_temperatures()
|
||||
```
|
||||
|
||||
**Temperature Ranges:**
|
||||
- Normal operation: < 50°C
|
||||
- Warning threshold: > 60°C
|
||||
- Critical threshold: > 70°C
|
||||
|
||||
## Integration with nueScan
|
||||
|
||||
The Helios driver is integrated into nueScan through the settings dialog and microscope controller.
|
||||
|
||||
### Configuration in UI
|
||||
|
||||
1. **Open Helios Settings**
|
||||
- Click "Helios Device Settings" button in main window
|
||||
|
||||
2. **Configure Parameters**
|
||||
- **COM Port**: Select from dropdown (automatically populated)
|
||||
- **Frequency**: Enter in Hz (16,666 - 125,000 Hz)
|
||||
- **Current**: Enter in mA (0 - 7000 mA)
|
||||
|
||||
3. **Apply Settings**
|
||||
- Click OK to apply and connect
|
||||
- Settings are validated before sending to hardware
|
||||
|
||||
### Settings Dialog Integration
|
||||
|
||||
The `HeliosDialog` class provides:
|
||||
- Automatic COM port enumeration
|
||||
- Input validation with range checking
|
||||
- User-friendly error messages
|
||||
- Settings persistence
|
||||
|
||||
```python
|
||||
# Dialog usage (called from main window)
|
||||
from dialogs.helios_dialog import HeliosDialog
|
||||
|
||||
dialog = HeliosDialog(parent=self)
|
||||
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||
settings = dialog.get_settings() # Returns None if validation fails
|
||||
if settings:
|
||||
self.microscope.apply_helios_settings(settings)
|
||||
```
|
||||
|
||||
### Validation Rules
|
||||
|
||||
The dialog validates all inputs before accepting:
|
||||
|
||||
**Frequency Validation:**
|
||||
- Range: 16,666 Hz to 125,000 Hz
|
||||
- Reason: Hardware period limit of 8000-60000 ns
|
||||
- Error message shows entered value and valid range
|
||||
|
||||
**Current Validation:**
|
||||
- Range: 0 to 7000 mA
|
||||
- Reason: Maximum diode current rating
|
||||
- Error message shows entered value and valid range
|
||||
|
||||
**COM Port Validation:**
|
||||
- Must select valid port from list
|
||||
- Cannot accept "No ports found" placeholder
|
||||
- Error message prompts to check connections
|
||||
|
||||
## Protocol Details
|
||||
|
||||
### Command Format
|
||||
|
||||
All commands follow the format:
|
||||
```
|
||||
COMMAND [value]<CR>
|
||||
```
|
||||
|
||||
Where:
|
||||
- `COMMAND` is a 3-letter mnemonic (e.g., LDO, LDF, LDS)
|
||||
- `[value]` is optional numeric parameter
|
||||
- `<CR>` is carriage return (0x0D)
|
||||
|
||||
### Command Set
|
||||
|
||||
| Command | Parameter | Description |
|
||||
|---------|-----------|-------------|
|
||||
| LDO | 0/1 | Laser enable (0=off, 1=on) |
|
||||
| LDG | 0/1/2 | Pulse mode (0=single, 1=gating, 2=continuous) |
|
||||
| LDF | 8000-60000 | Pulse period in nanoseconds |
|
||||
| LDS | 0-7000 | Diode current in milliamps |
|
||||
| LDP | - | Query output power (mW) |
|
||||
| LDPT | - | Query pump temperature (m°C) |
|
||||
| LDRT | - | Query resonator temperature (m°C) |
|
||||
| LDQT | - | Query q-switch temperature (m°C) |
|
||||
| LDPST | - | Query power stage temperature (m°C) |
|
||||
| LDSR | - | Query status register |
|
||||
| LDOH | - | Query operation hours |
|
||||
| LDCSN | - | Query controller serial number |
|
||||
| LDHSN | - | Query head serial number |
|
||||
|
||||
### Response Format
|
||||
|
||||
Responses are numeric values terminated with `<CR>`:
|
||||
```
|
||||
12345<CR>
|
||||
```
|
||||
|
||||
**Exception:** Serial numbers are returned as strings:
|
||||
```
|
||||
SN12345678<CR>
|
||||
```
|
||||
|
||||
### Status Register
|
||||
|
||||
The status register (LDSR) returns a 16-bit value with error flags:
|
||||
|
||||
| Bit | Mask | Meaning |
|
||||
|-----|------|---------|
|
||||
| 0 | 0x0001 | Pump temperature error |
|
||||
| 1 | 0x0002 | Resonator temperature error |
|
||||
| 2 | 0x0004 | Q-switch temperature error |
|
||||
| 3 | 0x0008 | Power stage temperature error |
|
||||
| 4 | 0x0010 | Diode current error |
|
||||
| 5 | 0x0020 | Interlock open |
|
||||
| 6 | 0x0040 | Over-power condition |
|
||||
| 7 | 0x0080 | Under-voltage condition |
|
||||
|
||||
A status of 0 indicates no errors.
|
||||
|
||||
### Set and Verify Pattern
|
||||
|
||||
For critical parameters, the driver uses a set-and-verify pattern:
|
||||
|
||||
```python
|
||||
def _set_and_verify(self, set_cmd: bytes, query_cmd: bytes, expected: str) -> bool:
|
||||
# Send set command
|
||||
self._serial.write(set_cmd)
|
||||
time.sleep(0.05) # Allow hardware to process
|
||||
|
||||
# Query back the value
|
||||
self._serial.write(query_cmd)
|
||||
response = self._read_response()
|
||||
|
||||
# Verify it matches
|
||||
return response.strip() == expected.strip()
|
||||
```
|
||||
|
||||
This ensures commands are executed correctly and hardware state matches software state.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Issues
|
||||
|
||||
**Problem:** Cannot connect to laser
|
||||
|
||||
**Solutions:**
|
||||
- Verify COM port name is correct (`HeliosDriver.list_available_ports()`)
|
||||
- Check RS-232 cable connection
|
||||
- Verify laser controller is powered on
|
||||
- Try different COM port
|
||||
- Check device permissions on Linux (`sudo usermod -a -G dialout $USER`)
|
||||
|
||||
### Communication Errors
|
||||
|
||||
**Problem:** Commands fail or no response
|
||||
|
||||
**Solutions:**
|
||||
- Verify baud rate is 9600 (default)
|
||||
- Check cable for proper null-modem configuration if needed
|
||||
- Increase timeout: `driver = HeliosDriver(timeout=2.0)`
|
||||
- Check for CR line termination (0x0D)
|
||||
- Verify no other software has port open
|
||||
|
||||
### Frequency/Current Not Updating
|
||||
|
||||
**Problem:** Settings don't change on hardware
|
||||
|
||||
**Solutions:**
|
||||
- Check return value of `set_frequency_hz()` and `set_current_ma()`
|
||||
- Verify parameters are in valid range
|
||||
- Check status register for errors: `driver.query_status_register()`
|
||||
- Ensure laser is not in error state
|
||||
- Try power cycling the controller
|
||||
|
||||
### Temperature Warnings
|
||||
|
||||
**Problem:** High temperature readings
|
||||
|
||||
**Solutions:**
|
||||
- Check ventilation around laser head and controller
|
||||
- Reduce diode current if at maximum
|
||||
- Allow longer cool-down between operations
|
||||
- Clean air filters if present
|
||||
- Check for blocked cooling fans
|
||||
|
||||
### Laser Won't Enable
|
||||
|
||||
**Problem:** `set_laser_enable(True)` fails or laser stays off
|
||||
|
||||
**Solutions:**
|
||||
- Check interlock connections (bit 5 of status register)
|
||||
- Verify all interlocks are closed
|
||||
- Check for error flags in status register
|
||||
- Ensure parameters (frequency, current) are set
|
||||
- Check external enable switch if present
|
||||
- Review safety interlock documentation
|
||||
|
||||
### Status Register Errors
|
||||
|
||||
**Problem:** Status register shows error bits set
|
||||
|
||||
**Solutions:**
|
||||
- Decode status register: `HeliosProtocol.decode_status_register(value)`
|
||||
- Address specific error conditions:
|
||||
- Temperature errors: Improve cooling
|
||||
- Current error: Reduce current setting
|
||||
- Interlock open: Check safety connections
|
||||
- Over-power: Reduce current
|
||||
- Under-voltage: Check power supply
|
||||
|
||||
## Debug Output
|
||||
|
||||
The driver provides extensive debug output:
|
||||
|
||||
```
|
||||
INFO: Informational messages about operations
|
||||
DEBUG: Detailed command/response information
|
||||
WARNING: Potential issues (high temp, errors)
|
||||
ERROR: Operation failures
|
||||
```
|
||||
|
||||
Enable Python logging to capture all output:
|
||||
|
||||
```python
|
||||
import logging
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
```
|
||||
|
||||
Example debug output:
|
||||
```
|
||||
INFO: Connecting to Helios laser on COM5
|
||||
DEBUG: Sending command: b'LDCSN\r'
|
||||
DEBUG: Received response: SN12345678
|
||||
INFO: Successfully connected to Helios on COM5
|
||||
DEBUG: Sending command: b'LDF 100000\r'
|
||||
DEBUG: Verifying frequency setting...
|
||||
INFO: Frequency set to 10000.0 Hz (period: 100000 ns)
|
||||
```
|
||||
|
||||
## Performance Notes
|
||||
|
||||
- Command response time: 50-100ms typical
|
||||
- Temperature queries: ~100ms per sensor
|
||||
- Status register query: ~50ms
|
||||
- All queries are synchronous (blocking)
|
||||
- Thread-safe for concurrent access (mutex protected)
|
||||
- Set-and-verify adds ~50ms overhead for reliability
|
||||
|
||||
## Safety Considerations
|
||||
|
||||
### Laser Safety
|
||||
- **Class 4 Laser**: Hazardous to eyes and skin
|
||||
- Always verify laser is disabled before opening beam paths
|
||||
- Use appropriate laser safety eyewear
|
||||
- Follow all facility laser safety procedures
|
||||
- Ensure proper interlock connections
|
||||
|
||||
### Thermal Management
|
||||
- Monitor temperatures during operation
|
||||
- Allow adequate cool-down between high-power operations
|
||||
- Ensure proper ventilation
|
||||
- Do not block cooling vents
|
||||
|
||||
### Electrical Safety
|
||||
- Verify proper grounding
|
||||
- Use shielded cables for trigger/status connections
|
||||
- Follow proper ESD procedures when servicing
|
||||
|
||||
## Hardware Connections
|
||||
|
||||
### Utility Connector (9-pin D-Sub)
|
||||
|
||||
The utility connector provides external control:
|
||||
|
||||
| Pin | Signal | Description |
|
||||
|-----|--------|-------------|
|
||||
| 1 | GND | Ground |
|
||||
| 2 | Laser Disable | Input: Pull low to disable laser |
|
||||
| 3 | External Trigger | Input: Rising edge triggers pulse |
|
||||
| 4 | Status Out | Output: High when ready |
|
||||
| 5 | GND | Ground |
|
||||
| 6-9 | NC | Not connected |
|
||||
|
||||
Trigger specifications:
|
||||
- Input: TTL/CMOS compatible
|
||||
- Minimum pulse width: 100ns
|
||||
- Maximum frequency: Limited by pulse mode setting
|
||||
|
||||
## References
|
||||
|
||||
- **Protocol Documentation**: `helios_comms_protocol.pdf`
|
||||
- **RS-232 Standard**: EIA/TIA-232
|
||||
- **Integration Guide**: `SETUP.md`
|
||||
- **Connection Guide**: See main window Helios settings dialog
|
||||
|
||||
## License
|
||||
|
||||
Copyright (C) 2025 Thomas Ales
|
||||
Licensed under GNU General Public License v2.0
|
||||
+339
@@ -0,0 +1,339 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
@@ -0,0 +1,2 @@
|
||||
# nuescan
|
||||
SRAS Scan Planning and Control Software
|
||||
@@ -0,0 +1,161 @@
|
||||
# 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
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/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()
|
||||
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Dialog controllers for nueScan application
|
||||
"""
|
||||
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
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']))
|
||||
@@ -0,0 +1,169 @@
|
||||
"""
|
||||
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']))
|
||||
@@ -0,0 +1,126 @@
|
||||
"""
|
||||
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()
|
||||
@@ -0,0 +1,163 @@
|
||||
"""
|
||||
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
|
||||
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
Hardware communication modules for nueScan
|
||||
Handles communication with ThorLabs stage, T3R device, and microscopes
|
||||
"""
|
||||
@@ -0,0 +1,928 @@
|
||||
"""
|
||||
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
|
||||
@@ -0,0 +1,532 @@
|
||||
"""
|
||||
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
|
||||
)
|
||||
@@ -0,0 +1,627 @@
|
||||
"""
|
||||
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()
|
||||
@@ -0,0 +1,302 @@
|
||||
"""
|
||||
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)
|
||||
@@ -0,0 +1,387 @@
|
||||
"""
|
||||
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
|
||||
@@ -0,0 +1,244 @@
|
||||
"""
|
||||
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)
|
||||
@@ -0,0 +1,259 @@
|
||||
"""
|
||||
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()
|
||||
@@ -0,0 +1,651 @@
|
||||
"""
|
||||
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()
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,411 @@
|
||||
"""
|
||||
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
|
||||
)
|
||||
@@ -0,0 +1,78 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,107 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,484 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,170 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,275 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,473 @@
|
||||
<?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>
|
||||
Binary file not shown.
@@ -0,0 +1,19 @@
|
||||
# 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
|
||||
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
This module contains a StageScanPlanGenerator class that generates scanning plans
|
||||
for microscope stages. The scans are generated in a single direction based on provided
|
||||
start and end coordinates, as well as spacing between scan lines.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from typing import List, Tuple, Optional
|
||||
|
||||
|
||||
class StageScanPlanGenerator:
|
||||
"""
|
||||
A class to generate scanning plans for microscope stages.
|
||||
|
||||
Attributes:
|
||||
start_coords (Tuple[float, float]): Starting X and Y coordinates.
|
||||
end_coords (Tuple[float, float]): Ending X and Y coordinates.
|
||||
spacing (float): Spacing between scan lines in the perpendicular direction.
|
||||
"""
|
||||
|
||||
def __init__(self, start_x: float, start_y: float,
|
||||
end_x: float, end_y: float, spacing: float):
|
||||
"""
|
||||
Initialize the StageScanPlanGenerator with scan parameters.
|
||||
|
||||
Args:
|
||||
start_x (float): Starting X coordinate.
|
||||
start_y (float): Starting Y coordinate.
|
||||
end_x (float): Ending X coordinate.
|
||||
end_y (float): Ending Y coordinate.
|
||||
spacing (float): Spacing between scan lines in the perpendicular direction.
|
||||
"""
|
||||
self.start_coords = (start_x, start_y)
|
||||
self.end_coords = (end_x, end_y)
|
||||
self.spacing = spacing
|
||||
|
||||
def _calculate_scan_direction(self) -> Tuple[float, float]:
|
||||
"""
|
||||
Calculate the direction vector of the scan.
|
||||
|
||||
Returns:
|
||||
Tuple[float, float]: Normalized direction vector (dx, dy).
|
||||
"""
|
||||
dx = self.end_coords[0] - self.start_coords[0]
|
||||
dy = self.end_coords[1] - self.start_coords[1]
|
||||
length = np.sqrt(dx**2 + dy**2)
|
||||
|
||||
if length == 0:
|
||||
raise ValueError("Start and end coordinates cannot be the same")
|
||||
|
||||
return dx / length, dy / length
|
||||
|
||||
def _calculate_perpendicular_direction(self) -> Tuple[float, float]:
|
||||
"""
|
||||
Calculate a perpendicular direction vector to the scan direction.
|
||||
|
||||
Returns:
|
||||
Tuple[float, float]: Perpendicular vector (px, py).
|
||||
"""
|
||||
dx, dy = self._calculate_scan_direction()
|
||||
# Rotate (dx, dy) by 90 degrees to get perpendicular vector
|
||||
px = -dy
|
||||
py = dx
|
||||
return px, py
|
||||
|
||||
def generate_scan_plan(self) -> List[Tuple[Tuple[float, float], Tuple[float, float]]]:
|
||||
"""
|
||||
Generate a scan plan with waypoints for the microscope stage.
|
||||
|
||||
Returns:
|
||||
List[Tuple[Tuple[float, float], Tuple[float, float]]]:
|
||||
A list of (start_point, end_point) tuples for each scan line.
|
||||
"""
|
||||
dx, dy = self._calculate_scan_direction()
|
||||
px, py = self._calculate_perpendicular_direction()
|
||||
|
||||
# Calculate the total length in the perpendicular direction
|
||||
start_x, start_y = self.start_coords
|
||||
end_x, end_y = self.end_coords
|
||||
min_coord_perp = min(start_x * px + start_y * py, end_x * px + end_y * py)
|
||||
max_coord_perp = max(start_x * px + start_y * py, end_x * px + end_y * py)
|
||||
|
||||
# Generate scan lines
|
||||
waypoints = []
|
||||
current_pos_perp = min_coord_perp
|
||||
|
||||
while current_pos_perp <= max_coord_perp:
|
||||
# Calculate start and end points for this scan line
|
||||
perp_offset = current_pos_perp - (start_x * px + start_y * py)
|
||||
line_start_x = start_x + perp_offset * dx
|
||||
line_start_y = start_y + perp_offset * dy
|
||||
|
||||
line_end_x = line_start_x + dx * abs(self.end_coords[0] - self.start_coords[0])
|
||||
line_end_y = line_start_y + dy * abs(self.end_coords[1] - self.start_coords[1])
|
||||
|
||||
waypoints.append(((line_start_x, line_start_y), (line_end_x, line_end_y)))
|
||||
current_pos_perp += self.spacing
|
||||
|
||||
return waypoints
|
||||
|
||||
|
||||
# Example usage:
|
||||
if __name__ == "__main__":
|
||||
# Create a scan plan generator
|
||||
generator = StageScanPlanGenerator(
|
||||
start_x=0.0, start_y=0.0,
|
||||
end_x=10.0, end_y=10.0,
|
||||
spacing=2.5
|
||||
)
|
||||
|
||||
# Generate the scan plan
|
||||
scan_plan = generator.generate_scan_plan()
|
||||
|
||||
# Print the scan plan
|
||||
print("Scan Plan:")
|
||||
for i, (start, end) in enumerate(scan_plan):
|
||||
print(f"Line {i+1}: Start at {start}, End at {end}")
|
||||
@@ -0,0 +1,797 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user