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:
+177
@@ -0,0 +1,177 @@
|
|||||||
|
# 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
|
||||||
|
*.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
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# UV
|
||||||
|
#uv.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
#poetry.lock
|
||||||
|
#poetry.toml
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
#pdm.lock
|
||||||
|
#pdm.toml
|
||||||
|
.pdm-python
|
||||||
|
.pdm-build/
|
||||||
|
|
||||||
|
# pixi
|
||||||
|
#pixi.lock
|
||||||
|
.pixi
|
||||||
|
|
||||||
|
# PEP 582
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.envrc
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Project-specific virtual environments
|
||||||
|
nuenv/
|
||||||
|
msoenv/
|
||||||
|
msodev/
|
||||||
|
|
||||||
|
# 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
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
# Abstra
|
||||||
|
.abstra/
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
# .vscode/
|
||||||
|
|
||||||
|
# Ruff stuff:
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# PyPI configuration file
|
||||||
|
.pypirc
|
||||||
|
|
||||||
|
# Cursor
|
||||||
|
.cursorignore
|
||||||
|
.cursorindexingignore
|
||||||
|
|
||||||
|
# Marimo
|
||||||
|
marimo/_static/
|
||||||
|
marimo/_lsp/
|
||||||
|
__marimo__/
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# scanengine-3
|
||||||
|
|
||||||
|
A unified scanning and instrumentation control platform combining multiple hardware control modules.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
scanengine-3 merges four previously separate projects into a single, cohesive platform:
|
||||||
|
|
||||||
|
- **nuescan**: SRAS scan planning and control software with PyQt6 GUI
|
||||||
|
- **pymso**: Tektronix oscilloscope control and data acquisition
|
||||||
|
- **pybbd202**: Thorlabs BBD203/MLS203 motor controller driver
|
||||||
|
- **pypewpewhops**: Coherent HOPS laser control via I2C
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
scanengine-3/
|
||||||
|
├── nuescan/ # Main scan control application with GUI
|
||||||
|
├── pymso/ # Oscilloscope control module
|
||||||
|
├── pybbd202/ # Stage controller driver
|
||||||
|
├── pypewpewhops/ # Laser control module
|
||||||
|
├── requirements.txt # Unified dependencies
|
||||||
|
└── README.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Create a virtual environment:
|
||||||
|
```bash
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install dependencies:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **PyQt6**: GUI framework for nuescan
|
||||||
|
- **pyserial**: Serial communication for hardware interfaces
|
||||||
|
- **pyvisa/pyvisa-py**: VISA instrument control for oscilloscopes
|
||||||
|
- **pyftdi**: FTDI device support for laser and stage controllers
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Refer to the README files in each subdirectory for specific usage instructions:
|
||||||
|
|
||||||
|
- `nuescan/README.md` - Main application usage
|
||||||
|
- `pymso/` - Oscilloscope control examples
|
||||||
|
- `pybbd202/README.md` - Stage controller documentation
|
||||||
|
- `pypewpewhops/README.md` - Laser control documentation
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Each module retains its original license. See individual subdirectories for license information.
|
||||||
@@ -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()
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Thomas K Ales
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,668 @@
|
|||||||
|
# BBD202/203 Motion Controller Library
|
||||||
|
|
||||||
|
Python library for controlling Thorlabs BBD202/BBD203 motion controllers via FTDI interface using the APT protocol.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Full control of X and Y axes
|
||||||
|
- Absolute and relative positioning
|
||||||
|
- Configurable velocity and acceleration
|
||||||
|
- Automatic position tracking
|
||||||
|
- Status monitoring with convenient properties
|
||||||
|
- Thread-safe operation
|
||||||
|
- Context manager support for automatic cleanup
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
The FTDI D2XX driver is required for this code to work correctly. You will need to disable linux's
|
||||||
|
ftdi_sio module in order to use it. I am not sure why, and I have no intent of diagnosing it.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install pyftdi
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```python
|
||||||
|
from bbd203_controller import MotionController
|
||||||
|
|
||||||
|
# Connect using context manager (automatic cleanup)
|
||||||
|
with MotionController() as mc:
|
||||||
|
# Print hardware information
|
||||||
|
print(f"Model: {mc.get_model()}")
|
||||||
|
print(f"Serial: {mc.get_serial_number()}")
|
||||||
|
print(f"Firmware: {mc.get_firmware_version()}")
|
||||||
|
|
||||||
|
# Enable and home X-axis
|
||||||
|
mc.set_channel_enable_state(mc.DEST_X_AXIS, enabled=True)
|
||||||
|
mc.home_x_axis(timeout=20.0)
|
||||||
|
|
||||||
|
# Move to absolute position
|
||||||
|
mc.set_velocity_params(mc.DEST_X_AXIS,
|
||||||
|
min_velocity=0.0,
|
||||||
|
acceleration=100.0,
|
||||||
|
max_velocity=50.0)
|
||||||
|
mc.set_move_abs_params(mc.DEST_X_AXIS, absolute_position=25.0)
|
||||||
|
result = mc.move_absolute(mc.DEST_X_AXIS, timeout=30.0)
|
||||||
|
|
||||||
|
print(f"Final position: {result['position']:.3f} mm")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Connection Management
|
||||||
|
|
||||||
|
### Basic Connection
|
||||||
|
|
||||||
|
```python
|
||||||
|
from bbd203_controller import MotionController
|
||||||
|
|
||||||
|
# Manual connection
|
||||||
|
mc = MotionController()
|
||||||
|
mc.connect()
|
||||||
|
|
||||||
|
# Use the controller...
|
||||||
|
|
||||||
|
mc.disconnect()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Context Manager (Recommended)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Automatic connection and cleanup
|
||||||
|
with MotionController() as mc:
|
||||||
|
# Use the controller...
|
||||||
|
pass # Automatically disconnects when exiting context
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom FTDI URL
|
||||||
|
|
||||||
|
```python
|
||||||
|
mc = MotionController(url='ftdi://0x0403:0xfaf0/1', baudrate=115200)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Axis Control
|
||||||
|
|
||||||
|
### Enabling Axes
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Enable X-axis
|
||||||
|
mc.set_channel_enable_state(mc.DEST_X_AXIS, enabled=True)
|
||||||
|
|
||||||
|
# Enable Y-axis
|
||||||
|
mc.set_channel_enable_state(mc.DEST_Y_AXIS, enabled=True)
|
||||||
|
|
||||||
|
# Check if enabled
|
||||||
|
is_enabled = mc.get_channel_enable_state(mc.DEST_X_AXIS)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Homing
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Home X-axis (blocks until complete)
|
||||||
|
if mc.home_x_axis(timeout=20.0):
|
||||||
|
print(f"X-axis homed at position: {mc.position_x} mm")
|
||||||
|
else:
|
||||||
|
print("Homing timed out")
|
||||||
|
|
||||||
|
# Home Y-axis
|
||||||
|
mc.home_y_axis(timeout=20.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
After homing, the position automatically resets to 0 mm.
|
||||||
|
|
||||||
|
## Motion Control
|
||||||
|
|
||||||
|
### Setting Velocity Parameters
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Set velocity parameters for X-axis
|
||||||
|
mc.set_velocity_params(
|
||||||
|
mc.DEST_X_AXIS,
|
||||||
|
min_velocity=0.0, # mm/s
|
||||||
|
acceleration=100.0, # mm/s²
|
||||||
|
max_velocity=50.0 # mm/s
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get current velocity parameters
|
||||||
|
params = mc.get_velocity_params(mc.DEST_X_AXIS)
|
||||||
|
print(f"Max velocity: {params['max_velocity']:.2f} mm/s")
|
||||||
|
print(f"Acceleration: {params['acceleration']:.2f} mm/s²")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Setting Acceleration Only
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Change just the acceleration, preserving velocity settings
|
||||||
|
mc.set_acceleration(mc.DEST_X_AXIS, 75.0)
|
||||||
|
|
||||||
|
# Get just the acceleration value
|
||||||
|
accel = mc.get_acceleration(mc.DEST_X_AXIS)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Absolute Moves
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Move to absolute position
|
||||||
|
mc.set_move_abs_params(mc.DEST_X_AXIS, absolute_position=30.0)
|
||||||
|
result = mc.move_absolute(mc.DEST_X_AXIS, timeout=30.0)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
print(f"Moved to: {result['position']:.3f} mm")
|
||||||
|
print(f"Status: 0x{result['status_bits']:08X}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Relative Moves
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Move relative to current position
|
||||||
|
mc.set_move_rel_params(mc.DEST_X_AXIS, relative_distance=5.0)
|
||||||
|
result = mc.move_relative(mc.DEST_X_AXIS, timeout=30.0)
|
||||||
|
|
||||||
|
# Move backwards
|
||||||
|
mc.set_move_rel_params(mc.DEST_X_AXIS, relative_distance=-2.5)
|
||||||
|
mc.move_relative(mc.DEST_X_AXIS, timeout=30.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stopping Motion
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Controlled stop (gradual deceleration)
|
||||||
|
mc.stop_x_axis(stop_mode=mc.StopMode.CONTROLLED, wait_for_stopped=True)
|
||||||
|
|
||||||
|
# Immediate stop
|
||||||
|
mc.stop_x_axis(stop_mode=mc.StopMode.IMMEDIATE, wait_for_stopped=True)
|
||||||
|
|
||||||
|
# Stop both axes simultaneously
|
||||||
|
results = mc.stop_all_axes(stop_mode=mc.StopMode.CONTROLLED)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Position Tracking
|
||||||
|
|
||||||
|
### Reading Current Position
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Query position from controller (blocking)
|
||||||
|
position = mc.get_position(mc.DEST_X_AXIS, timeout=5.0)
|
||||||
|
print(f"X position: {position:.3f} mm")
|
||||||
|
|
||||||
|
# Access cached position (non-blocking)
|
||||||
|
x_pos = mc.position_x
|
||||||
|
y_pos = mc.position_y
|
||||||
|
|
||||||
|
# Get encoder counts (raw values)
|
||||||
|
x_counts = mc.encoder_count_x
|
||||||
|
```
|
||||||
|
|
||||||
|
## Status Monitoring
|
||||||
|
|
||||||
|
### Using Status Properties
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Check various status flags
|
||||||
|
print(f"X-axis enabled: {mc.is_enabled_x}")
|
||||||
|
print(f"X-axis homed: {mc.is_homed_x}")
|
||||||
|
print(f"X-axis in motion: {mc.is_in_motion_x}")
|
||||||
|
print(f"X-axis settled: {mc.is_settled_x}")
|
||||||
|
print(f"X-axis has errors: {mc.has_errors_x}")
|
||||||
|
print(f"Power OK: {mc.power_ok_x}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Decoding Status Bits
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = mc.move_absolute(mc.DEST_X_AXIS, timeout=30.0)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
status_bits = result['status_bits']
|
||||||
|
|
||||||
|
# Get human-readable description
|
||||||
|
description = MotionController.get_status_description(status_bits)
|
||||||
|
print(description)
|
||||||
|
|
||||||
|
# Check for errors
|
||||||
|
if MotionController.has_errors(status_bits):
|
||||||
|
print("ERROR: Motion completed with errors!")
|
||||||
|
|
||||||
|
# Check motion state
|
||||||
|
if MotionController.is_settled(status_bits):
|
||||||
|
print("Stage is settled at target position")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available Status Checks
|
||||||
|
|
||||||
|
- `is_enabled_x` / `is_enabled_y` - Motor output enabled
|
||||||
|
- `is_homed_x` / `is_homed_y` - Axis has been homed
|
||||||
|
- `is_homing_x` / `is_homing_y` - Currently homing
|
||||||
|
- `is_in_motion_x` / `is_in_motion_y` - Currently moving
|
||||||
|
- `is_settled_x` / `is_settled_y` - Settled at target
|
||||||
|
- `is_tracking_x` / `is_tracking_y` - Within tracking window
|
||||||
|
- `is_connected_x` / `is_connected_y` - Motor recognized
|
||||||
|
- `has_errors_x` / `has_errors_y` - Any error condition
|
||||||
|
- `power_ok_x` / `power_ok_y` - Power supply OK
|
||||||
|
- `is_active_x` / `is_active_y` - Executing motion command
|
||||||
|
- `at_cw_limit_x` / `at_cw_limit_y` - At clockwise limit
|
||||||
|
- `at_ccw_limit_x` / `at_ccw_limit_y` - At counter-clockwise limit
|
||||||
|
|
||||||
|
## Multi-Axis Operations
|
||||||
|
|
||||||
|
### Simultaneous Moves (Using Threading)
|
||||||
|
|
||||||
|
```python
|
||||||
|
import threading
|
||||||
|
|
||||||
|
def move_x():
|
||||||
|
mc.set_move_abs_params(mc.DEST_X_AXIS, 50.0)
|
||||||
|
mc.move_absolute(mc.DEST_X_AXIS, timeout=30.0)
|
||||||
|
|
||||||
|
def move_y():
|
||||||
|
mc.set_move_abs_params(mc.DEST_Y_AXIS, 30.0)
|
||||||
|
mc.move_absolute(mc.DEST_Y_AXIS, timeout=30.0)
|
||||||
|
|
||||||
|
# Start both moves in parallel
|
||||||
|
x_thread = threading.Thread(target=move_x)
|
||||||
|
y_thread = threading.Thread(target=move_y)
|
||||||
|
|
||||||
|
x_thread.start()
|
||||||
|
y_thread.start()
|
||||||
|
|
||||||
|
# Wait for both to complete
|
||||||
|
x_thread.join()
|
||||||
|
y_thread.join()
|
||||||
|
|
||||||
|
print(f"Final position: ({mc.position_x:.2f}, {mc.position_y:.2f}) mm")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hardware Information
|
||||||
|
|
||||||
|
```python
|
||||||
|
with MotionController() as mc:
|
||||||
|
# Individual fields
|
||||||
|
print(f"Serial Number: {mc.get_serial_number()}")
|
||||||
|
print(f"Model: {mc.get_model()}")
|
||||||
|
print(f"Firmware: {mc.get_firmware_version()}")
|
||||||
|
print(f"Hardware Version: {mc.get_hw_version()}")
|
||||||
|
print(f"Number of Channels: {mc.get_num_channels()}")
|
||||||
|
|
||||||
|
# All info at once
|
||||||
|
info = mc.get_hw_info()
|
||||||
|
for key, value in info.items():
|
||||||
|
print(f"{key}: {value}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Examples
|
||||||
|
|
||||||
|
### Example 1: Simple Linear Move
|
||||||
|
|
||||||
|
```python
|
||||||
|
from bbd203_controller import MotionController
|
||||||
|
import time
|
||||||
|
|
||||||
|
with MotionController() as mc:
|
||||||
|
# Enable and home X-axis
|
||||||
|
mc.set_channel_enable_state(mc.DEST_X_AXIS, enabled=True)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
print("Homing X-axis...")
|
||||||
|
mc.home_x_axis(timeout=20.0)
|
||||||
|
print(f"Homed at {mc.position_x} mm")
|
||||||
|
|
||||||
|
# Set velocity for smooth motion
|
||||||
|
mc.set_velocity_params(mc.DEST_X_AXIS, 0.0, 50.0, 25.0)
|
||||||
|
|
||||||
|
# Move to 40mm
|
||||||
|
print("Moving to 40mm...")
|
||||||
|
mc.set_move_abs_params(mc.DEST_X_AXIS, 40.0)
|
||||||
|
result = mc.move_absolute(mc.DEST_X_AXIS, timeout=30.0)
|
||||||
|
|
||||||
|
if result and not mc.has_errors_x:
|
||||||
|
print(f"Successfully moved to {result['position']:.3f} mm")
|
||||||
|
else:
|
||||||
|
print("Move failed or has errors")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Square Pattern with Two Axes
|
||||||
|
|
||||||
|
```python
|
||||||
|
from bbd203_controller import MotionController
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
def move_to_position(mc, x, y, label):
|
||||||
|
"""Move to (x, y) with both axes moving simultaneously."""
|
||||||
|
print(f"Moving to {label}: ({x}, {y}) mm")
|
||||||
|
|
||||||
|
# Set parameters for both axes
|
||||||
|
mc.set_move_abs_params(mc.DEST_X_AXIS, x)
|
||||||
|
mc.set_move_abs_params(mc.DEST_Y_AXIS, y)
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# Execute moves in parallel
|
||||||
|
results = [None, None]
|
||||||
|
|
||||||
|
def move_x():
|
||||||
|
results[0] = mc.move_absolute(mc.DEST_X_AXIS, timeout=30.0)
|
||||||
|
|
||||||
|
def move_y():
|
||||||
|
results[1] = mc.move_absolute(mc.DEST_Y_AXIS, timeout=30.0)
|
||||||
|
|
||||||
|
x_thread = threading.Thread(target=move_x)
|
||||||
|
y_thread = threading.Thread(target=move_y)
|
||||||
|
|
||||||
|
x_thread.start()
|
||||||
|
y_thread.start()
|
||||||
|
x_thread.join()
|
||||||
|
y_thread.join()
|
||||||
|
|
||||||
|
if results[0] and results[1]:
|
||||||
|
print(f" Reached ({results[0]['position']:.2f}, {results[1]['position']:.2f}) mm")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
with MotionController() as mc:
|
||||||
|
# Enable both axes
|
||||||
|
mc.set_channel_enable_state(mc.DEST_X_AXIS, enabled=True)
|
||||||
|
mc.set_channel_enable_state(mc.DEST_Y_AXIS, enabled=True)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# Home both axes
|
||||||
|
print("Homing axes...")
|
||||||
|
mc.home_x_axis(timeout=20.0)
|
||||||
|
mc.home_y_axis(timeout=20.0)
|
||||||
|
|
||||||
|
# Set velocity for both axes
|
||||||
|
velocity = 50.0
|
||||||
|
acceleration = 100.0
|
||||||
|
mc.set_velocity_params(mc.DEST_X_AXIS, 0.0, acceleration, velocity)
|
||||||
|
mc.set_velocity_params(mc.DEST_Y_AXIS, 0.0, acceleration, velocity)
|
||||||
|
|
||||||
|
# Define 20mm square centered at (55, 37.5)
|
||||||
|
center_x, center_y = 55.0, 37.5
|
||||||
|
half_size = 10.0
|
||||||
|
|
||||||
|
waypoints = [
|
||||||
|
(center_x - half_size, center_y - half_size, "Bottom Left"),
|
||||||
|
(center_x + half_size, center_y - half_size, "Bottom Right"),
|
||||||
|
(center_x + half_size, center_y + half_size, "Top Right"),
|
||||||
|
(center_x - half_size, center_y + half_size, "Top Left"),
|
||||||
|
(center_x, center_y, "Center"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Execute square pattern
|
||||||
|
for x, y, label in waypoints:
|
||||||
|
if not move_to_position(mc, x, y, label):
|
||||||
|
print(f"Failed at {label}")
|
||||||
|
break
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
print("Square pattern complete!")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3: Velocity Ramping Test
|
||||||
|
|
||||||
|
```python
|
||||||
|
from bbd203_controller import MotionController
|
||||||
|
import time
|
||||||
|
|
||||||
|
with MotionController() as mc:
|
||||||
|
mc.set_channel_enable_state(mc.DEST_X_AXIS, enabled=True)
|
||||||
|
time.sleep(0.5)
|
||||||
|
mc.home_x_axis(timeout=20.0)
|
||||||
|
|
||||||
|
# Test at different velocities
|
||||||
|
test_velocities = [10.0, 25.0, 50.0, 100.0]
|
||||||
|
move_distance = 20.0
|
||||||
|
|
||||||
|
for velocity in test_velocities:
|
||||||
|
print(f"\n--- Testing at {velocity} mm/s ---")
|
||||||
|
|
||||||
|
# Set velocity parameters
|
||||||
|
mc.set_velocity_params(mc.DEST_X_AXIS, 0.0, 100.0, velocity)
|
||||||
|
|
||||||
|
# Move forward
|
||||||
|
mc.set_move_abs_params(mc.DEST_X_AXIS, move_distance)
|
||||||
|
start_time = time.time()
|
||||||
|
result = mc.move_absolute(mc.DEST_X_AXIS, timeout=30.0)
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
|
||||||
|
if result:
|
||||||
|
print(f" Moved {move_distance}mm in {elapsed:.2f}s")
|
||||||
|
print(f" Average speed: {move_distance/elapsed:.2f} mm/s")
|
||||||
|
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# Move back to start
|
||||||
|
mc.set_move_abs_params(mc.DEST_X_AXIS, 0.0)
|
||||||
|
mc.move_absolute(mc.DEST_X_AXIS, timeout=30.0)
|
||||||
|
time.sleep(0.5)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 4: Position Monitoring During Move
|
||||||
|
|
||||||
|
```python
|
||||||
|
from bbd203_controller import MotionController
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
with MotionController() as mc:
|
||||||
|
mc.set_channel_enable_state(mc.DEST_X_AXIS, enabled=True)
|
||||||
|
time.sleep(0.5)
|
||||||
|
mc.home_x_axis(timeout=20.0)
|
||||||
|
|
||||||
|
# Set slow velocity for visible monitoring
|
||||||
|
mc.set_velocity_params(mc.DEST_X_AXIS, 0.0, 50.0, 10.0)
|
||||||
|
|
||||||
|
# Start move in background thread
|
||||||
|
move_complete = threading.Event()
|
||||||
|
|
||||||
|
def do_move():
|
||||||
|
mc.set_move_abs_params(mc.DEST_X_AXIS, 50.0)
|
||||||
|
mc.move_absolute(mc.DEST_X_AXIS, timeout=60.0)
|
||||||
|
move_complete.set()
|
||||||
|
|
||||||
|
move_thread = threading.Thread(target=do_move)
|
||||||
|
move_thread.start()
|
||||||
|
|
||||||
|
# Monitor position while moving
|
||||||
|
print("Position monitoring:")
|
||||||
|
while not move_complete.is_set():
|
||||||
|
# Request current position
|
||||||
|
pos = mc.get_position(mc.DEST_X_AXIS, timeout=1.0)
|
||||||
|
if pos is not None:
|
||||||
|
print(f" Current position: {pos:.3f} mm, "
|
||||||
|
f"In motion: {mc.is_in_motion_x}, "
|
||||||
|
f"Settled: {mc.is_settled_x}")
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
move_thread.join()
|
||||||
|
print(f"Move complete! Final position: {mc.position_x:.3f} mm")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Constants and Enumerations
|
||||||
|
|
||||||
|
### Axis Destinations
|
||||||
|
|
||||||
|
```python
|
||||||
|
mc.DEST_CONTROLLER # 0x11 - Controller/motherboard
|
||||||
|
mc.DEST_X_AXIS # 0x21 - X-axis
|
||||||
|
mc.DEST_Y_AXIS # 0x22 - Y-axis
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stop Modes
|
||||||
|
|
||||||
|
```python
|
||||||
|
from bbd203_controller import StopMode
|
||||||
|
|
||||||
|
StopMode.IMMEDIATE # 1 - Instant stop
|
||||||
|
StopMode.CONTROLLED # 2 - Controlled deceleration (default)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Jog Modes
|
||||||
|
|
||||||
|
```python
|
||||||
|
from bbd203_controller import JogMode
|
||||||
|
|
||||||
|
JogMode.CONTINUOUS # 1 - Continuous jogging
|
||||||
|
JogMode.SINGLE_STEP # 2 - Single step jogging
|
||||||
|
```
|
||||||
|
|
||||||
|
### Channel Enable States
|
||||||
|
|
||||||
|
```python
|
||||||
|
from bbd203_controller import ChannelEnableState
|
||||||
|
|
||||||
|
ChannelEnableState.DISABLED # 0x02
|
||||||
|
ChannelEnableState.ENABLED # 0x01
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scaling Factors
|
||||||
|
|
||||||
|
The library handles all unit conversions automatically:
|
||||||
|
|
||||||
|
- **Position**: 20,000 encoder counts per mm
|
||||||
|
- **Velocity**: 13,421.77 counts per mm/s
|
||||||
|
- **Acceleration**: 13.744 counts per mm/s²
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
```python
|
||||||
|
from bbd203_controller import MotionController
|
||||||
|
|
||||||
|
try:
|
||||||
|
with MotionController() as mc:
|
||||||
|
# Invalid axis destination
|
||||||
|
mc.set_channel_enable_state(0x99, enabled=True)
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"ValueError: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with MotionController() as mc:
|
||||||
|
mc.set_channel_enable_state(mc.DEST_X_AXIS, enabled=True)
|
||||||
|
mc.home_x_axis(timeout=5.0) # Too short timeout
|
||||||
|
|
||||||
|
if not mc.is_homed_x:
|
||||||
|
print("Homing failed - axis not homed")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
|
### Message Callbacks
|
||||||
|
|
||||||
|
```python
|
||||||
|
from bbd203_controller import MotionController, AptMessage, MsgId
|
||||||
|
|
||||||
|
def on_move_stopped(msg: AptMessage):
|
||||||
|
print(f"Axis stopped unexpectedly!")
|
||||||
|
print(f"Source: 0x{msg.source:02X}")
|
||||||
|
|
||||||
|
with MotionController() as mc:
|
||||||
|
# Register callback for stop events
|
||||||
|
mc.register_callback(MsgId.MOT_MOVE_STOPPED, on_move_stopped)
|
||||||
|
|
||||||
|
# Your motion code here...
|
||||||
|
|
||||||
|
# Unregister when done
|
||||||
|
mc.unregister_callback(MsgId.MOT_MOVE_STOPPED, on_move_stopped)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Direct Message Access
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Wait for a specific message type
|
||||||
|
msg = mc.wait_for_message(MsgId.MOT_MOVE_COMPLETED, timeout=30.0)
|
||||||
|
|
||||||
|
# Get next message from queue
|
||||||
|
msg = mc.get_message(timeout=0.1)
|
||||||
|
|
||||||
|
# Get all queued messages
|
||||||
|
messages = mc.get_all_messages()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Connection Control
|
||||||
|
|
||||||
|
```python
|
||||||
|
mc = MotionController()
|
||||||
|
|
||||||
|
# Connect with updates disabled
|
||||||
|
mc.connect(enable_updates=False)
|
||||||
|
|
||||||
|
# Manually start/stop status updates
|
||||||
|
mc.start_update_messages()
|
||||||
|
# ...
|
||||||
|
mc.stop_update_messages()
|
||||||
|
|
||||||
|
mc.disconnect()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tips for Linear Scans
|
||||||
|
|
||||||
|
For performing linear scans at controlled speeds:
|
||||||
|
|
||||||
|
1. **Set velocity parameters** before each scan to ensure consistent motion
|
||||||
|
2. **Use absolute moves** with pre-calculated waypoints for accuracy
|
||||||
|
3. **For continuous scanning**: Execute moves sequentially without waiting
|
||||||
|
4. **For synchronized 2-axis moves**: Use threading (see examples above)
|
||||||
|
5. **Monitor position** during moves if needed for data acquisition timing
|
||||||
|
|
||||||
|
### Simple 1D Linear Scan
|
||||||
|
|
||||||
|
```python
|
||||||
|
with MotionController() as mc:
|
||||||
|
mc.set_channel_enable_state(mc.DEST_X_AXIS, enabled=True)
|
||||||
|
mc.home_x_axis(timeout=20.0)
|
||||||
|
|
||||||
|
# Scan parameters
|
||||||
|
start_pos = 10.0 # mm
|
||||||
|
end_pos = 90.0 # mm
|
||||||
|
step_size = 2.0 # mm
|
||||||
|
scan_speed = 20.0 # mm/s
|
||||||
|
|
||||||
|
# Set velocity for consistent speed
|
||||||
|
mc.set_velocity_params(mc.DEST_X_AXIS, 0.0, 100.0, scan_speed)
|
||||||
|
|
||||||
|
# Execute scan
|
||||||
|
position = start_pos
|
||||||
|
while position <= end_pos:
|
||||||
|
mc.set_move_abs_params(mc.DEST_X_AXIS, position)
|
||||||
|
result = mc.move_absolute(mc.DEST_X_AXIS, timeout=30.0)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
# Acquire data at this position
|
||||||
|
print(f"Scan point at {result['position']:.3f} mm")
|
||||||
|
# Your data acquisition code here...
|
||||||
|
|
||||||
|
position += step_size
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Controller Not Responding
|
||||||
|
|
||||||
|
If the controller stops responding after many commands:
|
||||||
|
- The library automatically sends ACK messages every second
|
||||||
|
- This is handled internally and should not require user intervention
|
||||||
|
|
||||||
|
### Moves Timing Out
|
||||||
|
|
||||||
|
- Increase the `timeout` parameter on move commands
|
||||||
|
- Check that velocity and acceleration are set appropriately
|
||||||
|
- Ensure the axis is enabled and homed
|
||||||
|
|
||||||
|
### Position Inaccurate After Homing
|
||||||
|
|
||||||
|
- Position automatically resets to 0 mm after homing completes
|
||||||
|
- Always wait for homing to complete before issuing move commands
|
||||||
|
- Check `is_homed_x` / `is_homed_y` properties to verify
|
||||||
|
|
||||||
|
### Unexpected Stops
|
||||||
|
|
||||||
|
- Register a callback for `MsgId.MOT_MOVE_STOPPED` to detect stop events
|
||||||
|
- Check error flags in status bits
|
||||||
|
- Ensure no limit switches are being triggered
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
## Author
|
||||||
|
|
||||||
|
Generated for BBD202/BBD203 motion controller control via APT protocol.
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
|||||||
|
msodev/
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for acquisition mode get/set functions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from tektronix_base import TektronixOscilloscopeBase
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Test acquisition mode functions on oscilloscope at 192.168.10.105"""
|
||||||
|
|
||||||
|
scope_ip = "192.168.10.105"
|
||||||
|
|
||||||
|
print(f"Connecting to oscilloscope at {scope_ip}...")
|
||||||
|
|
||||||
|
scope = TektronixOscilloscopeBase(resource_name=scope_ip)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Connect to scope
|
||||||
|
scope.connect()
|
||||||
|
print(f"✓ Connected to {scope.identify()}\n")
|
||||||
|
|
||||||
|
# Get current acquisition mode
|
||||||
|
print("Getting current acquisition mode...")
|
||||||
|
current_mode = scope.get_acquire_mode()
|
||||||
|
print(f"✓ Current acquisition mode: {current_mode}\n")
|
||||||
|
|
||||||
|
# Test setting different acquisition modes
|
||||||
|
print("Testing acquisition mode changes:")
|
||||||
|
test_modes = ['SAMple', 'HIRes', 'AVErage', 'PEAKdetect', 'ENVelope']
|
||||||
|
|
||||||
|
for mode in test_modes:
|
||||||
|
print(f"\n Setting mode to: {mode}")
|
||||||
|
scope.set_acquire_mode(mode)
|
||||||
|
|
||||||
|
# Verify the change
|
||||||
|
actual_mode = scope.get_acquire_mode()
|
||||||
|
if actual_mode == mode:
|
||||||
|
print(f" ✓ Verified: {actual_mode}")
|
||||||
|
else:
|
||||||
|
print(f" ✗ Mismatch: expected {mode}, got {actual_mode}")
|
||||||
|
|
||||||
|
# Restore original mode
|
||||||
|
print(f"\nRestoring original mode: {current_mode}")
|
||||||
|
scope.set_acquire_mode(current_mode)
|
||||||
|
print(f"✓ Restored to: {scope.get_acquire_mode()}")
|
||||||
|
|
||||||
|
# Test invalid mode
|
||||||
|
print("\nTesting invalid mode (should raise ValueError)...")
|
||||||
|
try:
|
||||||
|
scope.set_acquire_mode("INVALID")
|
||||||
|
print("✗ ERROR: Should have raised ValueError!")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"✓ Correctly raised ValueError: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Error: {type(e).__name__}: {e}")
|
||||||
|
finally:
|
||||||
|
if scope.is_connected:
|
||||||
|
scope.disconnect()
|
||||||
|
print("\n✓ Disconnected from oscilloscope")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+257
@@ -0,0 +1,257 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for channel control functionality.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from tektronix_base import TektronixOscilloscopeBase
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Test channel control functions on oscilloscope at 192.168.10.105"""
|
||||||
|
|
||||||
|
scope_ip = "192.168.10.105"
|
||||||
|
|
||||||
|
print(f"Connecting to oscilloscope at {scope_ip}...")
|
||||||
|
|
||||||
|
scope = TektronixOscilloscopeBase(resource_name=scope_ip)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Connect to scope
|
||||||
|
scope.connect()
|
||||||
|
print(f"Connected to {scope.identify()}\n")
|
||||||
|
|
||||||
|
# Test with CH1
|
||||||
|
test_channel = 1
|
||||||
|
print(f"=== Testing Channel {test_channel} ===\n")
|
||||||
|
|
||||||
|
# Query all channel parameters
|
||||||
|
print("Querying all channel parameters...")
|
||||||
|
all_params = scope.query_channel(test_channel)
|
||||||
|
print(f"CH{test_channel} parameters: {all_params[:100]}...\n")
|
||||||
|
|
||||||
|
# Get current settings to restore later
|
||||||
|
print("Getting current channel settings...")
|
||||||
|
current_bandwidth = scope.get_channel_bandwidth(test_channel)
|
||||||
|
current_coupling = scope.get_channel_coupling(test_channel)
|
||||||
|
current_termination = scope.get_channel_termination(test_channel)
|
||||||
|
current_scale = scope.get_channel_scale(test_channel)
|
||||||
|
current_offset = scope.get_channel_offset(test_channel)
|
||||||
|
current_position = scope.get_channel_position(test_channel)
|
||||||
|
current_label_name = scope.get_channel_label_name(test_channel)
|
||||||
|
current_label_color = scope.get_channel_label_color(test_channel)
|
||||||
|
current_label_font_size = scope.get_channel_label_font_size(test_channel)
|
||||||
|
current_label_font_type = scope.get_channel_label_font_type(test_channel)
|
||||||
|
current_label_xpos = scope.get_channel_label_xpos(test_channel)
|
||||||
|
current_label_ypos = scope.get_channel_label_ypos(test_channel)
|
||||||
|
|
||||||
|
print(f" Current bandwidth: {current_bandwidth}")
|
||||||
|
print(f" Current coupling: {current_coupling}")
|
||||||
|
print(f" Current termination: {current_termination} ohms")
|
||||||
|
print(f" Current scale: {current_scale} V/div")
|
||||||
|
print(f" Current offset: {current_offset} V")
|
||||||
|
print(f" Current position: {current_position} divisions")
|
||||||
|
print(f" Current label name: {current_label_name}")
|
||||||
|
print(f" Current label color: {current_label_color}")
|
||||||
|
print(f" Current label font size: {current_label_font_size} pt")
|
||||||
|
print(f" Current label font type: {current_label_font_type}")
|
||||||
|
print(f" Current label X position: {current_label_xpos} px")
|
||||||
|
print(f" Current label Y position: {current_label_ypos} px\n")
|
||||||
|
|
||||||
|
# Test coupling modes
|
||||||
|
print("Testing coupling modes:")
|
||||||
|
for coupling in ['DC', 'AC']:
|
||||||
|
print(f" Setting coupling to {coupling}...")
|
||||||
|
scope.set_channel_coupling(test_channel, coupling)
|
||||||
|
actual = scope.get_channel_coupling(test_channel)
|
||||||
|
print(f" Actual coupling: {actual}")
|
||||||
|
|
||||||
|
# Test termination
|
||||||
|
print("\nTesting termination settings:")
|
||||||
|
for term in [50, 1000000]:
|
||||||
|
print(f" Setting termination to {term} ohms...")
|
||||||
|
scope.set_channel_termination(test_channel, term)
|
||||||
|
actual = scope.get_channel_termination(test_channel)
|
||||||
|
print(f" Actual termination: {actual} ohms")
|
||||||
|
|
||||||
|
# Test vertical scale
|
||||||
|
print("\nTesting vertical scale:")
|
||||||
|
test_scales = [0.1, 0.5, 1.0, 2.0]
|
||||||
|
for scale in test_scales:
|
||||||
|
print(f" Setting scale to {scale} V/div...")
|
||||||
|
scope.set_channel_scale(test_channel, scale)
|
||||||
|
actual = scope.get_channel_scale(test_channel)
|
||||||
|
print(f" Actual scale: {actual} V/div")
|
||||||
|
|
||||||
|
# Test vertical offset
|
||||||
|
print("\nTesting vertical offset:")
|
||||||
|
test_offsets = [0.0, 0.5, -0.5, 1.0]
|
||||||
|
for offset in test_offsets:
|
||||||
|
print(f" Setting offset to {offset} V...")
|
||||||
|
scope.set_channel_offset(test_channel, offset)
|
||||||
|
actual = scope.get_channel_offset(test_channel)
|
||||||
|
print(f" Actual offset: {actual} V")
|
||||||
|
|
||||||
|
# Test vertical position
|
||||||
|
print("\nTesting vertical position:")
|
||||||
|
test_positions = [0.0, 1.0, -1.0, 2.5]
|
||||||
|
for position in test_positions:
|
||||||
|
print(f" Setting position to {position} divisions...")
|
||||||
|
scope.set_channel_position(test_channel, position)
|
||||||
|
actual = scope.get_channel_position(test_channel)
|
||||||
|
print(f" Actual position: {actual} divisions")
|
||||||
|
|
||||||
|
# Test label name
|
||||||
|
print("\nTesting label name:")
|
||||||
|
test_names = ["Test Signal", "CH1-Custom", "Probe Input"]
|
||||||
|
for name in test_names:
|
||||||
|
print(f" Setting label to '{name}'...")
|
||||||
|
scope.set_channel_label_name(test_channel, name)
|
||||||
|
actual = scope.get_channel_label_name(test_channel)
|
||||||
|
print(f" Actual label: {actual}")
|
||||||
|
|
||||||
|
# Test label color
|
||||||
|
print("\nTesting label color:")
|
||||||
|
test_colors = ["#FF0000", "#00FF00", "#0000FF", "#FFFF00"]
|
||||||
|
for color in test_colors:
|
||||||
|
print(f" Setting color to {color}...")
|
||||||
|
scope.set_channel_label_color(test_channel, color)
|
||||||
|
actual = scope.get_channel_label_color(test_channel)
|
||||||
|
print(f" Actual color: {actual}")
|
||||||
|
|
||||||
|
# Test label font size
|
||||||
|
print("\nTesting label font size:")
|
||||||
|
test_sizes = [10, 12, 14, 16]
|
||||||
|
for size in test_sizes:
|
||||||
|
print(f" Setting font size to {size} pt...")
|
||||||
|
scope.set_channel_label_font_size(test_channel, size)
|
||||||
|
actual = scope.get_channel_label_font_size(test_channel)
|
||||||
|
print(f" Actual font size: {actual} pt")
|
||||||
|
|
||||||
|
# Test label font type
|
||||||
|
print("\nTesting label font type:")
|
||||||
|
test_fonts = ["Arial", "Helvetica", "Courier"]
|
||||||
|
for font in test_fonts:
|
||||||
|
print(f" Setting font to {font}...")
|
||||||
|
scope.set_channel_label_font_type(test_channel, font)
|
||||||
|
actual = scope.get_channel_label_font_type(test_channel)
|
||||||
|
print(f" Actual font: {actual}")
|
||||||
|
|
||||||
|
# Test label position
|
||||||
|
print("\nTesting label X position:")
|
||||||
|
test_xpos = [100, 200, 300]
|
||||||
|
for xpos in test_xpos:
|
||||||
|
print(f" Setting X position to {xpos} px...")
|
||||||
|
scope.set_channel_label_xpos(test_channel, xpos)
|
||||||
|
actual = scope.get_channel_label_xpos(test_channel)
|
||||||
|
print(f" Actual X position: {actual} px")
|
||||||
|
|
||||||
|
print("\nTesting label Y position:")
|
||||||
|
test_ypos = [50, 100, 150]
|
||||||
|
for ypos in test_ypos:
|
||||||
|
print(f" Setting Y position to {ypos} px...")
|
||||||
|
scope.set_channel_label_ypos(test_channel, ypos)
|
||||||
|
actual = scope.get_channel_label_ypos(test_channel)
|
||||||
|
print(f" Actual Y position: {actual} px")
|
||||||
|
|
||||||
|
# Test using string channel format
|
||||||
|
print("\nTesting with string channel format ('CH2'):")
|
||||||
|
print(" Setting CH2 coupling to AC...")
|
||||||
|
scope.set_channel_coupling('CH2', 'AC')
|
||||||
|
actual = scope.get_channel_coupling('CH2')
|
||||||
|
print(f" Actual CH2 coupling: {actual}")
|
||||||
|
|
||||||
|
print(" Setting CH2 scale to 0.5 V/div...")
|
||||||
|
scope.set_channel_scale('CH2', 0.5)
|
||||||
|
actual = scope.get_channel_scale('CH2')
|
||||||
|
print(f" Actual CH2 scale: {actual} V/div")
|
||||||
|
|
||||||
|
# Restore original settings
|
||||||
|
print(f"\nRestoring original settings for CH{test_channel}...")
|
||||||
|
scope.set_channel_coupling(test_channel, current_coupling)
|
||||||
|
scope.set_channel_termination(test_channel, current_termination)
|
||||||
|
scope.set_channel_scale(test_channel, current_scale)
|
||||||
|
scope.set_channel_offset(test_channel, current_offset)
|
||||||
|
scope.set_channel_position(test_channel, current_position)
|
||||||
|
scope.set_channel_label_name(test_channel, current_label_name)
|
||||||
|
scope.set_channel_label_color(test_channel, current_label_color)
|
||||||
|
scope.set_channel_label_font_size(test_channel, current_label_font_size)
|
||||||
|
scope.set_channel_label_font_type(test_channel, current_label_font_type)
|
||||||
|
scope.set_channel_label_xpos(test_channel, current_label_xpos)
|
||||||
|
scope.set_channel_label_ypos(test_channel, current_label_ypos)
|
||||||
|
print(f" Restored coupling: {scope.get_channel_coupling(test_channel)}")
|
||||||
|
print(f" Restored termination: {scope.get_channel_termination(test_channel)} ohms")
|
||||||
|
print(f" Restored scale: {scope.get_channel_scale(test_channel)} V/div")
|
||||||
|
|
||||||
|
# Test invalid inputs
|
||||||
|
print("\n=== Testing Error Handling ===\n")
|
||||||
|
|
||||||
|
print("Testing invalid channel number (should raise ValueError)...")
|
||||||
|
try:
|
||||||
|
scope.get_channel_coupling(5)
|
||||||
|
print(" ERROR: Should have raised ValueError!")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f" Correctly raised ValueError: {e}")
|
||||||
|
|
||||||
|
print("\nTesting invalid channel string (should raise ValueError)...")
|
||||||
|
try:
|
||||||
|
scope.get_channel_coupling('CH5')
|
||||||
|
print(" ERROR: Should have raised ValueError!")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f" Correctly raised ValueError: {e}")
|
||||||
|
|
||||||
|
print("\nTesting invalid coupling (should raise ValueError)...")
|
||||||
|
try:
|
||||||
|
scope.set_channel_coupling(1, 'INVALID')
|
||||||
|
print(" ERROR: Should have raised ValueError!")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f" Correctly raised ValueError: {e}")
|
||||||
|
|
||||||
|
print("\nTesting invalid termination (should raise ValueError)...")
|
||||||
|
try:
|
||||||
|
scope.set_channel_termination(1, 75)
|
||||||
|
print(" ERROR: Should have raised ValueError!")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f" Correctly raised ValueError: {e}")
|
||||||
|
|
||||||
|
print("\nTesting invalid scale (should raise ValueError)...")
|
||||||
|
try:
|
||||||
|
scope.set_channel_scale(1, -1.0)
|
||||||
|
print(" ERROR: Should have raised ValueError!")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f" Correctly raised ValueError: {e}")
|
||||||
|
|
||||||
|
print("\nTesting invalid color format (should raise ValueError)...")
|
||||||
|
try:
|
||||||
|
scope.set_channel_label_color(1, "FF0000") # Missing #
|
||||||
|
print(" ERROR: Should have raised ValueError!")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f" Correctly raised ValueError: {e}")
|
||||||
|
|
||||||
|
print("\nTesting invalid font size (should raise ValueError)...")
|
||||||
|
try:
|
||||||
|
scope.set_channel_label_font_size(1, -5)
|
||||||
|
print(" ERROR: Should have raised ValueError!")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f" Correctly raised ValueError: {e}")
|
||||||
|
|
||||||
|
print("\nTesting invalid X position (should raise ValueError)...")
|
||||||
|
try:
|
||||||
|
scope.set_channel_label_xpos(1, -10)
|
||||||
|
print(" ERROR: Should have raised ValueError!")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f" Correctly raised ValueError: {e}")
|
||||||
|
|
||||||
|
print("\n=== All tests completed successfully! ===")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {type(e).__name__}: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
finally:
|
||||||
|
if scope.is_connected:
|
||||||
|
scope.disconnect()
|
||||||
|
print("\nDisconnected from oscilloscope")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script to connect to a Tektronix oscilloscope via raw sockets.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from tektronix_base import TektronixOscilloscopeBase
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Test connection to oscilloscope at 192.168.10.105"""
|
||||||
|
|
||||||
|
scope_ip = "192.168.10.105"
|
||||||
|
scope_port = 4000
|
||||||
|
|
||||||
|
print(f"Attempting to connect to oscilloscope at {scope_ip}:{scope_port}...")
|
||||||
|
|
||||||
|
scope = TektronixOscilloscopeBase(resource_name=scope_ip, port=scope_port, timeout=5.0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Attempt connection
|
||||||
|
scope.connect()
|
||||||
|
print(f"✓ Successfully connected to {scope_ip}:{scope_port}")
|
||||||
|
print(f"✓ Connection status: {scope.is_connected}")
|
||||||
|
|
||||||
|
# Get instrument identification
|
||||||
|
print("\nQuerying instrument identification...")
|
||||||
|
idn = scope.identify()
|
||||||
|
print(f"✓ Instrument ID: {idn}")
|
||||||
|
|
||||||
|
# Test a simple query
|
||||||
|
print("\nTesting SCPI query...")
|
||||||
|
response = scope.query("*OPT?")
|
||||||
|
print(f"✓ Installed options: {response}")
|
||||||
|
|
||||||
|
# Test a write command
|
||||||
|
print("\nTesting SCPI write command...")
|
||||||
|
scope.write("*CLS")
|
||||||
|
print("✓ Cleared status registers")
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"✗ Configuration error: {e}")
|
||||||
|
except ConnectionError as e:
|
||||||
|
print(f"✗ Connection failed: {e}")
|
||||||
|
print("\nTroubleshooting tips:")
|
||||||
|
print(" - Verify the oscilloscope IP address is correct")
|
||||||
|
print(" - Check network connectivity (try: ping 192.168.10.105)")
|
||||||
|
print(" - Ensure the oscilloscope has LXI/socket server enabled")
|
||||||
|
print(" - Verify port 4000 is correct (check scope network settings)")
|
||||||
|
except RuntimeError as e:
|
||||||
|
print(f"✗ Runtime error: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Unexpected error: {type(e).__name__}: {e}")
|
||||||
|
finally:
|
||||||
|
# Always disconnect
|
||||||
|
if scope.is_connected:
|
||||||
|
scope.disconnect()
|
||||||
|
print("\n✓ Disconnected from oscilloscope")
|
||||||
|
else:
|
||||||
|
print("\n✗ Not connected")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for FastFrame functionality.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from tektronix_base import TektronixOscilloscopeBase
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Test FastFrame functions on oscilloscope at 192.168.10.105"""
|
||||||
|
|
||||||
|
scope_ip = "192.168.10.105"
|
||||||
|
|
||||||
|
print(f"Connecting to oscilloscope at {scope_ip}...")
|
||||||
|
|
||||||
|
scope = TektronixOscilloscopeBase(resource_name=scope_ip)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Connect to scope
|
||||||
|
scope.connect()
|
||||||
|
print(f"✓ Connected to {scope.identify()}\n")
|
||||||
|
|
||||||
|
# Get current FastFrame state
|
||||||
|
print("Getting current FastFrame state...")
|
||||||
|
current_state = scope.get_fastframe_state()
|
||||||
|
print(f"✓ Current FastFrame state: {current_state} ({'active' if current_state else 'off'})\n")
|
||||||
|
|
||||||
|
# Get current frame count
|
||||||
|
print("Getting current frame count...")
|
||||||
|
current_count = scope.get_fastframe_count()
|
||||||
|
print(f"✓ Current frame count: {current_count}\n")
|
||||||
|
|
||||||
|
# Test enabling FastFrame
|
||||||
|
print("Enabling FastFrame...")
|
||||||
|
scope.set_fastframe_state(1)
|
||||||
|
state = scope.get_fastframe_state()
|
||||||
|
print(f"✓ FastFrame state: {state} ({'active' if state else 'off'})\n")
|
||||||
|
|
||||||
|
# Test setting frame count
|
||||||
|
print("Setting frame count to 100...")
|
||||||
|
scope.set_fastframe_count(100)
|
||||||
|
count = scope.get_fastframe_count()
|
||||||
|
print(f"✓ Frame count: {count}\n")
|
||||||
|
|
||||||
|
# Test using boolean for state
|
||||||
|
print("Testing boolean state (True)...")
|
||||||
|
scope.set_fastframe_state(True)
|
||||||
|
state = scope.get_fastframe_state()
|
||||||
|
print(f"✓ FastFrame state: {state} ({'active' if state else 'off'})\n")
|
||||||
|
|
||||||
|
print("Testing boolean state (False)...")
|
||||||
|
scope.set_fastframe_state(False)
|
||||||
|
state = scope.get_fastframe_state()
|
||||||
|
print(f"✓ FastFrame state: {state} ({'active' if state else 'off'})\n")
|
||||||
|
|
||||||
|
# Test different frame counts
|
||||||
|
print("Testing different frame counts:")
|
||||||
|
test_counts = [10, 50, 200, 500]
|
||||||
|
for test_count in test_counts:
|
||||||
|
print(f" Setting count to {test_count}...")
|
||||||
|
scope.set_fastframe_count(test_count)
|
||||||
|
actual_count = scope.get_fastframe_count()
|
||||||
|
if actual_count == test_count:
|
||||||
|
print(f" ✓ Verified: {actual_count}")
|
||||||
|
else:
|
||||||
|
print(f" ✗ Mismatch: expected {test_count}, got {actual_count}")
|
||||||
|
|
||||||
|
# Restore original settings
|
||||||
|
print(f"\nRestoring original settings...")
|
||||||
|
scope.set_fastframe_state(current_state)
|
||||||
|
scope.set_fastframe_count(current_count)
|
||||||
|
print(f"✓ Restored FastFrame state: {scope.get_fastframe_state()}")
|
||||||
|
print(f"✓ Restored frame count: {scope.get_fastframe_count()}")
|
||||||
|
|
||||||
|
# Test invalid inputs
|
||||||
|
print("\nTesting invalid state (should raise ValueError)...")
|
||||||
|
try:
|
||||||
|
scope.set_fastframe_state(2)
|
||||||
|
print("✗ ERROR: Should have raised ValueError!")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"✓ Correctly raised ValueError: {e}")
|
||||||
|
|
||||||
|
print("\nTesting invalid frame count (should raise ValueError)...")
|
||||||
|
try:
|
||||||
|
scope.set_fastframe_count(-1)
|
||||||
|
print("✗ ERROR: Should have raised ValueError!")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"✓ Correctly raised ValueError: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Error: {type(e).__name__}: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
finally:
|
||||||
|
if scope.is_connected:
|
||||||
|
scope.disconnect()
|
||||||
|
print("\n✓ Disconnected from oscilloscope")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+292
@@ -0,0 +1,292 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for FastFrame waveform acquisition.
|
||||||
|
|
||||||
|
Tests acquiring 1000 FastFrame records from a 1MHz square wave on CH1.
|
||||||
|
Signal: 1MHz square wave, +250mV to -250mV
|
||||||
|
Records: 1000 frames, 2500 points each
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
from tektronix_base import TektronixOscilloscopeBase
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Test FastFrame acquisition with 1MHz square wave on CH1"""
|
||||||
|
|
||||||
|
scope_ip = "192.168.10.105"
|
||||||
|
|
||||||
|
print(f"Connecting to oscilloscope at {scope_ip}...")
|
||||||
|
|
||||||
|
scope = TektronixOscilloscopeBase(resource_name=scope_ip, timeout=20.0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Connect to scope
|
||||||
|
scope.connect()
|
||||||
|
print(f"Connected to {scope.identify()}\n")
|
||||||
|
|
||||||
|
print("=== Configuring Oscilloscope ===\n")
|
||||||
|
|
||||||
|
# Configure CH1 for the signal
|
||||||
|
print("Configuring CH1...")
|
||||||
|
print(" Setting vertical scale to 100mV/div...")
|
||||||
|
scope.set_channel_scale(1, 0.1) # 100mV/div for ±250mV signal
|
||||||
|
print(" Setting vertical offset to 0V...")
|
||||||
|
scope.set_channel_offset(1, 0.0)
|
||||||
|
print(" Setting coupling to DC...")
|
||||||
|
scope.set_channel_coupling(1, 'DC')
|
||||||
|
print(" Setting termination to 50 ohms...")
|
||||||
|
scope.set_channel_termination(1, 50)
|
||||||
|
|
||||||
|
# Verify settings
|
||||||
|
actual_scale = scope.get_channel_scale(1)
|
||||||
|
actual_offset = scope.get_channel_offset(1)
|
||||||
|
actual_coupling = scope.get_channel_coupling(1)
|
||||||
|
actual_term = scope.get_channel_termination(1)
|
||||||
|
print(f" Verified: {actual_scale}V/div, {actual_offset}V offset, {actual_coupling} coupling, {actual_term}Ω\n")
|
||||||
|
|
||||||
|
# Configure horizontal timebase
|
||||||
|
# For 1MHz square wave (1µs period), let's capture ~4 cycles (4µs)
|
||||||
|
# 4µs over 10 divisions = 400ns/div
|
||||||
|
print("Configuring horizontal timebase...")
|
||||||
|
print(" Setting time scale to 400ns/div (4µs total for ~4 cycles of 1MHz)...")
|
||||||
|
scope.set_time_scale(400e-9) # 400ns/div
|
||||||
|
print(" Setting record length to 2500 points...")
|
||||||
|
scope.set_record_length(2500)
|
||||||
|
|
||||||
|
# Verify settings
|
||||||
|
actual_time_scale = scope.get_time_scale()
|
||||||
|
actual_record_length = scope.get_record_length()
|
||||||
|
actual_sample_rate = scope.get_sample_rate()
|
||||||
|
print(f" Verified: {actual_time_scale*1e9:.0f}ns/div, {actual_record_length} points")
|
||||||
|
print(f" Sample rate: {actual_sample_rate/1e6:.1f} MS/s\n")
|
||||||
|
|
||||||
|
# Configure trigger
|
||||||
|
print("Configuring trigger...")
|
||||||
|
print(" Setting trigger source to CH1...")
|
||||||
|
scope.set_trigger_source('CH1')
|
||||||
|
print(" Setting trigger level to +100mV...")
|
||||||
|
scope.set_trigger_level(1, 0.1) # +100mV
|
||||||
|
print(" Setting trigger slope to rising...")
|
||||||
|
scope.set_trigger_slope('RISE')
|
||||||
|
print(" Setting trigger mode to NORMAL...")
|
||||||
|
scope.set_trigger_mode('NORMAL')
|
||||||
|
print(" Setting trigger coupling to DC...")
|
||||||
|
scope.set_trigger_coupling('DC')
|
||||||
|
|
||||||
|
# Verify trigger settings
|
||||||
|
actual_trigger_source = scope.get_trigger_source()
|
||||||
|
actual_trigger_level = scope.get_trigger_level(1)
|
||||||
|
actual_trigger_slope = scope.get_trigger_slope()
|
||||||
|
actual_trigger_mode = scope.get_trigger_mode()
|
||||||
|
print(f" Verified: {actual_trigger_source}, {actual_trigger_level}V, {actual_trigger_slope}, {actual_trigger_mode}\n")
|
||||||
|
|
||||||
|
# Configure FastFrame
|
||||||
|
print("Configuring FastFrame...")
|
||||||
|
print(" Enabling FastFrame...")
|
||||||
|
scope.set_fastframe_state(True)
|
||||||
|
print(" Setting frame count to 1000...")
|
||||||
|
scope.set_fastframe_count(1000)
|
||||||
|
|
||||||
|
# Verify FastFrame settings
|
||||||
|
ff_state = scope.get_fastframe_state()
|
||||||
|
ff_count = scope.get_fastframe_count()
|
||||||
|
print(f" Verified: FastFrame {'enabled' if ff_state else 'disabled'}, {ff_count} frames\n")
|
||||||
|
|
||||||
|
# Configure waveform transfer
|
||||||
|
print("Configuring waveform transfer...")
|
||||||
|
scope.set_data_encoding('RIBinary')
|
||||||
|
scope.set_wfmoutpre_encoding('BINary')
|
||||||
|
scope.set_wfmoutpre_byte_count(1)
|
||||||
|
scope.set_wfmoutpre_byte_order('MSB')
|
||||||
|
scope.set_data_source('CH1')
|
||||||
|
|
||||||
|
# Verify configuration
|
||||||
|
actual_encoding = scope.get_data_encoding()
|
||||||
|
actual_source = scope.get_data_source()
|
||||||
|
print(f" Verified: {actual_encoding} encoding, source {actual_source}")
|
||||||
|
print(" Data transfer configured for 8-bit signed binary\n")
|
||||||
|
|
||||||
|
print("=== Acquiring FastFrame Records ===\n")
|
||||||
|
|
||||||
|
# Wait for FastFrame settings to take effect
|
||||||
|
print("Waiting for FastFrame configuration to settle...")
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# Start acquisition - this arms the scope and begins acquiring triggered frames
|
||||||
|
print("Starting acquisition (arming scope)...")
|
||||||
|
scope.write("ACQuire:STATE RUN")
|
||||||
|
print(" Scope is now armed and acquiring triggered events")
|
||||||
|
|
||||||
|
# Wait and poll for acquisition to complete
|
||||||
|
print(f" Waiting for {ff_count} triggers to be acquired...")
|
||||||
|
print(" (Polling acquisition state...)")
|
||||||
|
|
||||||
|
# Poll for up to 10 seconds
|
||||||
|
for i in range(100):
|
||||||
|
time.sleep(0.1)
|
||||||
|
state = scope.query("ACQuire:STATE?")
|
||||||
|
if i % 10 == 0: # Print every second
|
||||||
|
print(f" Polling... state: {state.strip()}")
|
||||||
|
# Check if we've acquired enough frames
|
||||||
|
if i > 20: # After 2 seconds minimum
|
||||||
|
break
|
||||||
|
|
||||||
|
# Stop acquisition
|
||||||
|
print(" Stopping acquisition...")
|
||||||
|
scope.write("ACQuire:STATE STOP")
|
||||||
|
|
||||||
|
# Verify we have frames
|
||||||
|
time.sleep(0.2)
|
||||||
|
print("✓ Acquisition stopped, scope should now have all 1000 frames\n")
|
||||||
|
|
||||||
|
# Clear any leftover data in the receive buffer
|
||||||
|
print("Clearing receive buffer...")
|
||||||
|
scope.socket.setblocking(False)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
junk = scope.socket.recv(4096)
|
||||||
|
if not junk:
|
||||||
|
break
|
||||||
|
print(f" Cleared {len(junk)} bytes of junk data")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
scope.socket.setblocking(True)
|
||||||
|
print(" Buffer cleared\n")
|
||||||
|
|
||||||
|
# Test transferring a single frame first
|
||||||
|
print("Testing single frame transfer first...")
|
||||||
|
try:
|
||||||
|
scope.set_fastframe_selected(1)
|
||||||
|
print(" Selected frame 1")
|
||||||
|
test_curve = scope.transfer_curve()
|
||||||
|
print(f" ✓ Successfully transferred {len(test_curve)} bytes")
|
||||||
|
test_waveform = scope.parse_curve_data(test_curve, byte_count=1, signed=True, byte_order='MSB')
|
||||||
|
print(f" ✓ Parsed {len(test_waveform)} samples")
|
||||||
|
print(f" First 10 values: {test_waveform[:10]}\n")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ✗ Single frame test failed: {e}")
|
||||||
|
print(" Cannot proceed with bulk transfer\n")
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Now transfer all 1000 frames
|
||||||
|
print(f"Transferring {ff_count} frames from scope...")
|
||||||
|
print("This may take a while...\n")
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
all_waveforms = []
|
||||||
|
|
||||||
|
for frame_num in range(1, ff_count + 1):
|
||||||
|
# Show progress every 100 frames
|
||||||
|
if frame_num % 100 == 0 or frame_num == 1:
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
if frame_num > 1:
|
||||||
|
rate = frame_num / elapsed
|
||||||
|
eta = (ff_count - frame_num) / rate
|
||||||
|
print(f" Frame {frame_num}/{ff_count} - Elapsed: {elapsed:.1f}s - Rate: {rate:.1f} frames/s - ETA: {eta:.1f}s")
|
||||||
|
else:
|
||||||
|
print(f" Frame {frame_num}/{ff_count}...")
|
||||||
|
|
||||||
|
# Select this frame
|
||||||
|
scope.set_fastframe_selected(frame_num)
|
||||||
|
|
||||||
|
# Verify frame selection for first frame
|
||||||
|
if frame_num == 1:
|
||||||
|
actual_frame = scope.get_fastframe_selected()
|
||||||
|
if actual_frame != frame_num:
|
||||||
|
print(f" Warning: Frame mismatch - requested {frame_num}, got {actual_frame}")
|
||||||
|
|
||||||
|
# Transfer curve data directly with longer timeout
|
||||||
|
old_timeout = scope.socket.gettimeout()
|
||||||
|
scope.socket.settimeout(30.0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
curve_bytes = scope.transfer_curve()
|
||||||
|
waveform = scope.parse_curve_data(curve_bytes, byte_count=1, signed=True, byte_order='MSB')
|
||||||
|
all_waveforms.append(waveform)
|
||||||
|
finally:
|
||||||
|
scope.socket.settimeout(old_timeout)
|
||||||
|
|
||||||
|
end_time = time.time()
|
||||||
|
total_time = end_time - start_time
|
||||||
|
|
||||||
|
print(f"\n✓ Acquired all {ff_count} frames in {total_time:.2f} seconds")
|
||||||
|
print(f" Average rate: {ff_count / total_time:.2f} frames/second")
|
||||||
|
print(f" Average time per frame: {total_time / ff_count * 1000:.2f} ms\n")
|
||||||
|
|
||||||
|
# Analyze the acquired data
|
||||||
|
print("=== Waveform Statistics ===\n")
|
||||||
|
|
||||||
|
# Check that all frames have the expected length
|
||||||
|
frame_lengths = [len(wf) for wf in all_waveforms]
|
||||||
|
print(f"Frame lengths: {min(frame_lengths)} to {max(frame_lengths)} points")
|
||||||
|
|
||||||
|
# Calculate statistics for first frame
|
||||||
|
first_frame = all_waveforms[0]
|
||||||
|
print(f"\nFirst frame (frame 1):")
|
||||||
|
print(f" Samples: {len(first_frame)}")
|
||||||
|
print(f" Min ADC value: {min(first_frame)}")
|
||||||
|
print(f" Max ADC value: {max(first_frame)}")
|
||||||
|
print(f" Average ADC value: {sum(first_frame) / len(first_frame):.2f}")
|
||||||
|
print(f" First 20 values: {first_frame[:20]}")
|
||||||
|
|
||||||
|
# Calculate statistics for middle frame
|
||||||
|
mid_frame_idx = ff_count // 2
|
||||||
|
mid_frame = all_waveforms[mid_frame_idx]
|
||||||
|
print(f"\nMiddle frame (frame {mid_frame_idx + 1}):")
|
||||||
|
print(f" Samples: {len(mid_frame)}")
|
||||||
|
print(f" Min ADC value: {min(mid_frame)}")
|
||||||
|
print(f" Max ADC value: {max(mid_frame)}")
|
||||||
|
print(f" Average ADC value: {sum(mid_frame) / len(mid_frame):.2f}")
|
||||||
|
|
||||||
|
# Calculate statistics for last frame
|
||||||
|
last_frame = all_waveforms[-1]
|
||||||
|
print(f"\nLast frame (frame {ff_count}):")
|
||||||
|
print(f" Samples: {len(last_frame)}")
|
||||||
|
print(f" Min ADC value: {min(last_frame)}")
|
||||||
|
print(f" Max ADC value: {max(last_frame)}")
|
||||||
|
print(f" Average ADC value: {sum(last_frame) / len(last_frame):.2f}")
|
||||||
|
|
||||||
|
# Calculate overall statistics
|
||||||
|
all_values = [val for wf in all_waveforms for val in wf]
|
||||||
|
print(f"\nOverall statistics (all {ff_count} frames, {len(all_values)} total samples):")
|
||||||
|
print(f" Min ADC value: {min(all_values)}")
|
||||||
|
print(f" Max ADC value: {max(all_values)}")
|
||||||
|
print(f" Average ADC value: {sum(all_values) / len(all_values):.2f}")
|
||||||
|
print(f" Total data transferred: {len(all_values)} bytes")
|
||||||
|
print(f" Transfer rate: {len(all_values) / total_time / 1024 / 1024:.2f} MB/s")
|
||||||
|
|
||||||
|
# Check for square wave characteristics
|
||||||
|
print(f"\nSquare wave detection:")
|
||||||
|
# A square wave should have values clustered around two levels
|
||||||
|
positive_samples = sum(1 for v in first_frame if v > 0)
|
||||||
|
negative_samples = sum(1 for v in first_frame if v < 0)
|
||||||
|
zero_samples = sum(1 for v in first_frame if v == 0)
|
||||||
|
print(f" Frame 1: {positive_samples} positive, {negative_samples} negative, {zero_samples} zero samples")
|
||||||
|
|
||||||
|
# Estimate duty cycle from first frame
|
||||||
|
if len(first_frame) > 0:
|
||||||
|
duty_cycle = (positive_samples / len(first_frame)) * 100
|
||||||
|
print(f" Estimated duty cycle: {duty_cycle:.1f}%")
|
||||||
|
|
||||||
|
print("\n=== FastFrame Acquisition Test Completed Successfully! ===")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nError: {type(e).__name__}: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
finally:
|
||||||
|
if scope.is_connected:
|
||||||
|
# Try to disable FastFrame before disconnecting
|
||||||
|
try:
|
||||||
|
print("\nDisabling FastFrame...")
|
||||||
|
scope.set_fastframe_state(False)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
scope.disconnect()
|
||||||
|
print("Disconnected from oscilloscope")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script to verify FastFrame packed transfer hypothesis.
|
||||||
|
|
||||||
|
Tests if all FastFrame records come in a single CURVe? transfer,
|
||||||
|
similar to WFMv3 file format.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
from tektronix_base import TektronixOscilloscopeBase
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Test FastFrame packed transfer"""
|
||||||
|
|
||||||
|
scope_ip = "192.168.10.105"
|
||||||
|
num_frames = 1000
|
||||||
|
target_record_length = 5000
|
||||||
|
|
||||||
|
print(f"Connecting to oscilloscope at {scope_ip}...")
|
||||||
|
|
||||||
|
scope = TektronixOscilloscopeBase(resource_name=scope_ip, timeout=20.0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
scope.connect()
|
||||||
|
print(f"Connected to {scope.identify()}\n")
|
||||||
|
|
||||||
|
# Configure CH1 for 1MHz square wave ±250mV
|
||||||
|
print("Configuring CH1...")
|
||||||
|
scope.set_channel_scale(1, 0.1) # 100mV/div
|
||||||
|
scope.set_channel_offset(1, 0.0)
|
||||||
|
scope.set_channel_coupling(1, 'DC')
|
||||||
|
scope.set_channel_termination(1, 50)
|
||||||
|
|
||||||
|
# Configure horizontal for 5000 point record length
|
||||||
|
# At 6.25 GS/s: 5000 points / 6.25e9 = 800ns window = 80ns/div
|
||||||
|
print("Configuring horizontal for 5000 point record length...")
|
||||||
|
scope.set_record_length(target_record_length)
|
||||||
|
scope.set_time_scale(80e-9) # 80ns/div = 800ns window at 6.25 GS/s
|
||||||
|
|
||||||
|
# Check actual settings
|
||||||
|
actual_record_length = scope.get_record_length()
|
||||||
|
actual_time_scale = scope.get_time_scale()
|
||||||
|
actual_sample_rate = scope.get_sample_rate()
|
||||||
|
print(f" Record length: {actual_record_length}")
|
||||||
|
print(f" Time scale: {actual_time_scale*1e9:.0f} ns/div")
|
||||||
|
print(f" Sample rate: {actual_sample_rate/1e6:.1f} MS/s")
|
||||||
|
|
||||||
|
# Configure trigger
|
||||||
|
print("Configuring trigger...")
|
||||||
|
scope.set_trigger_source('CH1')
|
||||||
|
scope.set_trigger_level(1, 0.1)
|
||||||
|
scope.set_trigger_slope('RISE')
|
||||||
|
scope.set_trigger_mode('NORMAL')
|
||||||
|
|
||||||
|
# Configure FastFrame
|
||||||
|
print(f"Configuring FastFrame for {num_frames} frames...")
|
||||||
|
scope.set_fastframe_state(True)
|
||||||
|
scope.set_fastframe_count(num_frames)
|
||||||
|
|
||||||
|
# Check record length AFTER enabling FastFrame
|
||||||
|
actual_record_length_ff = scope.get_record_length()
|
||||||
|
actual_sample_rate_ff = scope.get_sample_rate()
|
||||||
|
print(f" Record length after FastFrame: {actual_record_length_ff}")
|
||||||
|
print(f" Sample rate after FastFrame: {actual_sample_rate_ff/1e6:.1f} MS/s")
|
||||||
|
|
||||||
|
ff_state = scope.get_fastframe_state()
|
||||||
|
ff_count = scope.get_fastframe_count()
|
||||||
|
print(f" FastFrame: {'enabled' if ff_state else 'disabled'}, {ff_count} frames\n")
|
||||||
|
|
||||||
|
# Configure waveform transfer
|
||||||
|
print("Configuring waveform transfer...")
|
||||||
|
scope.set_data_encoding('RIBinary')
|
||||||
|
scope.set_wfmoutpre_encoding('BINary')
|
||||||
|
scope.set_wfmoutpre_byte_count(1)
|
||||||
|
scope.set_wfmoutpre_byte_order('MSB')
|
||||||
|
scope.set_data_source('CH1')
|
||||||
|
|
||||||
|
# Acquire data
|
||||||
|
print("\nAcquiring FastFrame data...")
|
||||||
|
time.sleep(0.5)
|
||||||
|
scope.write("ACQuire:STATE RUN")
|
||||||
|
|
||||||
|
# Wait for triggers (no polling - just wait)
|
||||||
|
print(" Waiting for triggers...")
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
scope.write("ACQuire:STATE STOP")
|
||||||
|
time.sleep(0.5)
|
||||||
|
print(" Acquisition stopped\n")
|
||||||
|
|
||||||
|
# Transfer all frames individually
|
||||||
|
print(f"\n=== Transferring {num_frames} Frames Individually ===\n")
|
||||||
|
|
||||||
|
all_waveforms = []
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
for frame_num in range(1, num_frames + 1):
|
||||||
|
# Progress every 100 frames
|
||||||
|
if frame_num == 1 or frame_num % 100 == 0:
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
if frame_num > 1:
|
||||||
|
rate = frame_num / elapsed
|
||||||
|
eta = (num_frames - frame_num) / rate
|
||||||
|
print(f" Frame {frame_num}/{num_frames} - {elapsed:.1f}s elapsed - {rate:.1f} fps - ETA {eta:.1f}s")
|
||||||
|
else:
|
||||||
|
print(f" Frame {frame_num}/{num_frames}...")
|
||||||
|
|
||||||
|
# Select frame and wait for it to take effect
|
||||||
|
scope.set_fastframe_selected(frame_num)
|
||||||
|
time.sleep(0.01) # Small delay for command processing
|
||||||
|
|
||||||
|
# Transfer curve data
|
||||||
|
curve_bytes = scope.transfer_curve()
|
||||||
|
waveform = scope.parse_curve_data(curve_bytes, byte_count=1, signed=True, byte_order='MSB')
|
||||||
|
all_waveforms.append(waveform)
|
||||||
|
|
||||||
|
end_time = time.time()
|
||||||
|
total_time = end_time - start_time
|
||||||
|
|
||||||
|
print(f"\n=== Transfer Complete ===")
|
||||||
|
print(f"Frames transferred: {len(all_waveforms)}")
|
||||||
|
print(f"Points per frame: {len(all_waveforms[0])}")
|
||||||
|
print(f"Total time: {total_time:.2f}s")
|
||||||
|
print(f"Rate: {num_frames / total_time:.1f} frames/s")
|
||||||
|
print(f"Total data: {sum(len(wf) for wf in all_waveforms)} samples")
|
||||||
|
print(f"Throughput: {sum(len(wf) for wf in all_waveforms) / total_time / 1e6:.2f} MS/s")
|
||||||
|
|
||||||
|
# Show statistics for selected frames
|
||||||
|
print(f"\n=== Frame Statistics ===")
|
||||||
|
for frame_idx in [0, num_frames//2, num_frames-1]:
|
||||||
|
wf = all_waveforms[frame_idx]
|
||||||
|
print(f"\nFrame {frame_idx + 1}:")
|
||||||
|
print(f" Samples: {len(wf)}")
|
||||||
|
print(f" Min: {min(wf)}, Max: {max(wf)}, Avg: {sum(wf)/len(wf):.1f}")
|
||||||
|
print(f" First 10: {wf[:10]}")
|
||||||
|
|
||||||
|
print("\n=== Test Complete ===")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nError: {type(e).__name__}: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
finally:
|
||||||
|
if scope.is_connected:
|
||||||
|
try:
|
||||||
|
scope.set_fastframe_state(False)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
scope.disconnect()
|
||||||
|
print("\nDisconnected")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Simple FastFrame test - minimal setup.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
from tektronix_base import TektronixOscilloscopeBase
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
scope_ip = "192.168.10.105"
|
||||||
|
|
||||||
|
print(f"Connecting to {scope_ip}...")
|
||||||
|
scope = TektronixOscilloscopeBase(resource_name=scope_ip, timeout=20.0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
scope.connect()
|
||||||
|
print(f"Connected: {scope.identify()}\n")
|
||||||
|
|
||||||
|
# Configure CH1 (same as failing test)
|
||||||
|
print("Configuring CH1...")
|
||||||
|
scope.set_channel_scale(1, 0.1)
|
||||||
|
scope.set_channel_offset(1, 0.0)
|
||||||
|
scope.set_channel_coupling(1, 'DC')
|
||||||
|
scope.set_channel_termination(1, 50)
|
||||||
|
|
||||||
|
# Configure horizontal (same as failing test)
|
||||||
|
print("Configuring horizontal...")
|
||||||
|
scope.set_record_length(5000)
|
||||||
|
scope.set_time_scale(80e-9)
|
||||||
|
actual_record_length = scope.get_record_length()
|
||||||
|
actual_time_scale = scope.get_time_scale()
|
||||||
|
actual_sample_rate = scope.get_sample_rate()
|
||||||
|
print(f" Record: {actual_record_length}, Time: {actual_time_scale*1e9:.0f}ns/div, Rate: {actual_sample_rate/1e6:.1f}MS/s")
|
||||||
|
|
||||||
|
# Configure trigger (same as failing test)
|
||||||
|
print("Configuring trigger...")
|
||||||
|
scope.set_trigger_source('CH1')
|
||||||
|
scope.set_trigger_level(1, 0.1)
|
||||||
|
scope.set_trigger_slope('RISE')
|
||||||
|
scope.set_trigger_mode('NORMAL')
|
||||||
|
scope.set_trigger_coupling('DC')
|
||||||
|
|
||||||
|
# Enable FastFrame with 1000 frames
|
||||||
|
print("Enabling FastFrame (1000 frames)...")
|
||||||
|
scope.set_fastframe_state(True)
|
||||||
|
scope.set_fastframe_count(1000)
|
||||||
|
|
||||||
|
ff_state = scope.get_fastframe_state()
|
||||||
|
ff_count = scope.get_fastframe_count()
|
||||||
|
print(f" State: {ff_state}, Count: {ff_count}")
|
||||||
|
|
||||||
|
# Configure data transfer (same as failing test)
|
||||||
|
print("Configuring data transfer...")
|
||||||
|
scope.set_data_encoding('RIBinary')
|
||||||
|
scope.set_wfmoutpre_encoding('BINary')
|
||||||
|
scope.set_wfmoutpre_byte_count(1)
|
||||||
|
scope.set_wfmoutpre_byte_order('MSB')
|
||||||
|
scope.set_data_source('CH1')
|
||||||
|
|
||||||
|
# Acquire
|
||||||
|
print("Acquiring...")
|
||||||
|
scope.write("ACQuire:STATE RUN")
|
||||||
|
time.sleep(2) # Wait for triggers
|
||||||
|
scope.write("ACQuire:STATE STOP")
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# Check record length
|
||||||
|
record_len = scope.get_record_length()
|
||||||
|
print(f"Record length: {record_len}")
|
||||||
|
|
||||||
|
# Test: Send single CURVe? and read ALL frames
|
||||||
|
print(f"\nTesting: Send one CURVe? and read all {ff_count} frames...")
|
||||||
|
scope.set_fastframe_selected(1) # Start from frame 1
|
||||||
|
scope.write("CURVe?")
|
||||||
|
|
||||||
|
all_waveforms = []
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
for frame_num in range(1, ff_count + 1):
|
||||||
|
if frame_num == 1 or frame_num % 100 == 0:
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
if frame_num > 1:
|
||||||
|
rate = frame_num / elapsed
|
||||||
|
print(f" Frame {frame_num}/{ff_count} - {rate:.1f} fps")
|
||||||
|
else:
|
||||||
|
print(f" Frame {frame_num}/{ff_count}...")
|
||||||
|
|
||||||
|
# Read one frame using read_raw (no new CURVe? command)
|
||||||
|
curve_bytes = scope.read_raw()
|
||||||
|
waveform = scope.parse_curve_data(curve_bytes, byte_count=1, signed=True)
|
||||||
|
all_waveforms.append(waveform)
|
||||||
|
|
||||||
|
end_time = time.time()
|
||||||
|
total_time = end_time - start_time
|
||||||
|
|
||||||
|
print(f"\nTransferred {len(all_waveforms)} frames in {total_time:.2f}s")
|
||||||
|
print(f"Rate: {ff_count / total_time:.1f} frames/s")
|
||||||
|
print(f"Points per frame: {len(all_waveforms[0])}")
|
||||||
|
print(f"First frame: min={min(all_waveforms[0])}, max={max(all_waveforms[0])}")
|
||||||
|
print(f"Last frame: min={min(all_waveforms[-1])}, max={max(all_waveforms[-1])}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nError: {type(e).__name__}: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
finally:
|
||||||
|
if scope.is_connected:
|
||||||
|
try:
|
||||||
|
scope.set_fastframe_state(False)
|
||||||
|
print("FastFrame disabled")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
scope.disconnect()
|
||||||
|
print("Disconnected")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for horizontal mode control functionality.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from tektronix_base import TektronixOscilloscopeBase
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Test horizontal mode functions on oscilloscope at 192.168.10.105"""
|
||||||
|
|
||||||
|
scope_ip = "192.168.10.105"
|
||||||
|
|
||||||
|
print(f"Connecting to oscilloscope at {scope_ip}...")
|
||||||
|
|
||||||
|
scope = TektronixOscilloscopeBase(resource_name=scope_ip)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Connect to scope
|
||||||
|
scope.connect()
|
||||||
|
print(f"✓ Connected to {scope.identify()}\n")
|
||||||
|
|
||||||
|
# Get current horizontal settings
|
||||||
|
print("Getting current horizontal settings...")
|
||||||
|
current_record_length = scope.get_record_length()
|
||||||
|
current_sample_rate = scope.get_sample_rate()
|
||||||
|
current_time_scale = scope.get_time_scale()
|
||||||
|
print(f"✓ Current record length: {current_record_length} samples")
|
||||||
|
print(f"✓ Current sample rate: {current_sample_rate} S/s")
|
||||||
|
print(f"✓ Current time scale: {current_time_scale} s/div\n")
|
||||||
|
|
||||||
|
# Test setting record length
|
||||||
|
print("Testing record length changes:")
|
||||||
|
test_lengths = [1000, 10000, 100000]
|
||||||
|
for length in test_lengths:
|
||||||
|
print(f" Setting record length to {length}...")
|
||||||
|
scope.set_record_length(length)
|
||||||
|
actual_length = scope.get_record_length()
|
||||||
|
print(f" ✓ Actual record length: {actual_length}")
|
||||||
|
|
||||||
|
# Test setting sample rate
|
||||||
|
print("\nTesting sample rate changes:")
|
||||||
|
test_rates = [1e6, 10e6, 100e6] # 1 MS/s, 10 MS/s, 100 MS/s
|
||||||
|
for rate in test_rates:
|
||||||
|
print(f" Setting sample rate to {rate:.0f} S/s...")
|
||||||
|
scope.set_sample_rate(rate)
|
||||||
|
actual_rate = scope.get_sample_rate()
|
||||||
|
print(f" ✓ Actual sample rate: {actual_rate:.0f} S/s")
|
||||||
|
|
||||||
|
# Test setting time scale
|
||||||
|
print("\nTesting time scale changes:")
|
||||||
|
test_scales = [1e-6, 10e-6, 100e-6, 1e-3] # 1 µs/div, 10 µs/div, 100 µs/div, 1 ms/div
|
||||||
|
for scale in test_scales:
|
||||||
|
print(f" Setting time scale to {scale:.6f} s/div...")
|
||||||
|
scope.set_time_scale(scale)
|
||||||
|
actual_scale = scope.get_time_scale()
|
||||||
|
print(f" ✓ Actual time scale: {actual_scale:.6f} s/div")
|
||||||
|
|
||||||
|
# Restore original settings
|
||||||
|
print(f"\nRestoring original settings...")
|
||||||
|
scope.set_record_length(current_record_length)
|
||||||
|
scope.set_sample_rate(current_sample_rate)
|
||||||
|
scope.set_time_scale(current_time_scale)
|
||||||
|
print(f"✓ Restored record length: {scope.get_record_length()}")
|
||||||
|
print(f"✓ Restored sample rate: {scope.get_sample_rate()}")
|
||||||
|
print(f"✓ Restored time scale: {scope.get_time_scale()}")
|
||||||
|
|
||||||
|
# Test invalid inputs
|
||||||
|
print("\nTesting invalid record length (should raise ValueError)...")
|
||||||
|
try:
|
||||||
|
scope.set_record_length(-1)
|
||||||
|
print("✗ ERROR: Should have raised ValueError!")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"✓ Correctly raised ValueError: {e}")
|
||||||
|
|
||||||
|
print("\nTesting invalid sample rate (should raise ValueError)...")
|
||||||
|
try:
|
||||||
|
scope.set_sample_rate(0)
|
||||||
|
print("✗ ERROR: Should have raised ValueError!")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"✓ Correctly raised ValueError: {e}")
|
||||||
|
|
||||||
|
print("\nTesting invalid time scale (should raise ValueError)...")
|
||||||
|
try:
|
||||||
|
scope.set_time_scale(-1.0)
|
||||||
|
print("✗ ERROR: Should have raised ValueError!")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"✓ Correctly raised ValueError: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Error: {type(e).__name__}: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
finally:
|
||||||
|
if scope.is_connected:
|
||||||
|
scope.disconnect()
|
||||||
|
print("\n✓ Disconnected from oscilloscope")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Simple curve transfer test - no FastFrame.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
from tektronix_base import TektronixOscilloscopeBase
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
scope_ip = "192.168.10.105"
|
||||||
|
|
||||||
|
print(f"Connecting to {scope_ip}...")
|
||||||
|
scope = TektronixOscilloscopeBase(resource_name=scope_ip, timeout=20.0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
scope.connect()
|
||||||
|
print(f"Connected: {scope.identify()}\n")
|
||||||
|
|
||||||
|
# Make sure FastFrame is OFF
|
||||||
|
print("Disabling FastFrame...")
|
||||||
|
scope.set_fastframe_state(False)
|
||||||
|
|
||||||
|
# Configure simple acquisition
|
||||||
|
print("Configuring acquisition...")
|
||||||
|
scope.set_data_encoding('RIBinary')
|
||||||
|
scope.set_data_source('CH1')
|
||||||
|
|
||||||
|
# Single acquisition
|
||||||
|
print("Running single acquisition...")
|
||||||
|
scope.write("ACQuire:STATE RUN")
|
||||||
|
time.sleep(0.5)
|
||||||
|
scope.write("ACQuire:STATE STOP")
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
# Transfer curve
|
||||||
|
print("Transferring curve data...")
|
||||||
|
curve_bytes = scope.transfer_curve()
|
||||||
|
print(f"Received {len(curve_bytes)} bytes")
|
||||||
|
|
||||||
|
# Parse
|
||||||
|
waveform = scope.parse_curve_data(curve_bytes, byte_count=1, signed=True)
|
||||||
|
print(f"Parsed {len(waveform)} samples")
|
||||||
|
print(f"Min: {min(waveform)}, Max: {max(waveform)}")
|
||||||
|
print(f"First 10: {waveform[:10]}")
|
||||||
|
|
||||||
|
print("\nSuccess!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nError: {type(e).__name__}: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
finally:
|
||||||
|
if scope.is_connected:
|
||||||
|
scope.disconnect()
|
||||||
|
print("Disconnected")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Simple test to debug query timeout issues.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from tektronix_base import TektronixOscilloscopeBase
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Test simple queries with the oscilloscope"""
|
||||||
|
|
||||||
|
scope_ip = "192.168.10.105"
|
||||||
|
|
||||||
|
print(f"Connecting to oscilloscope at {scope_ip}...")
|
||||||
|
|
||||||
|
scope = TektronixOscilloscopeBase(resource_name=scope_ip, timeout=10.0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Connect
|
||||||
|
scope.connect()
|
||||||
|
print("✓ Connected")
|
||||||
|
|
||||||
|
# Wait a moment to ensure connection is stable
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# Try a simple query
|
||||||
|
print("\nSending *IDN? query...")
|
||||||
|
idn = scope.query("*IDN?")
|
||||||
|
print(f"✓ Response: {idn}")
|
||||||
|
|
||||||
|
# Try acquisition mode query
|
||||||
|
print("\nSending ACQuire:MODe? query...")
|
||||||
|
mode = scope.query("ACQuire:MODe?")
|
||||||
|
print(f"✓ Current mode: {mode}")
|
||||||
|
|
||||||
|
# Set mode to sample using short form
|
||||||
|
print("\nSetting mode to SAM (short form)...")
|
||||||
|
scope.set_acquire_mode("SAM")
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
mode = scope.get_acquire_mode()
|
||||||
|
print(f"✓ Mode is now: {mode}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Error: {type(e).__name__}: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
finally:
|
||||||
|
if scope.is_connected:
|
||||||
|
scope.disconnect()
|
||||||
|
print("\n✓ Disconnected")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+59
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Simple test to verify basic waveform acquisition works.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from tektronix_base import TektronixOscilloscopeBase
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Test single waveform acquisition from CH1"""
|
||||||
|
|
||||||
|
scope_ip = "192.168.10.105"
|
||||||
|
print(f"Connecting to {scope_ip}...")
|
||||||
|
|
||||||
|
scope = TektronixOscilloscopeBase(resource_name=scope_ip, timeout=15.0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
scope.connect()
|
||||||
|
print(f"✓ Connected to {scope.identify()}\n")
|
||||||
|
|
||||||
|
# Check current settings
|
||||||
|
print("Current settings:")
|
||||||
|
print(f" Data encoding: {scope.get_data_encoding()}")
|
||||||
|
print(f" Data source: {scope.get_data_source()}")
|
||||||
|
print(f" Record length: {scope.get_record_length()}")
|
||||||
|
print(f" FastFrame state: {scope.get_fastframe_state()}\n")
|
||||||
|
|
||||||
|
# Ensure FastFrame is off
|
||||||
|
if scope.get_fastframe_state():
|
||||||
|
print("Disabling FastFrame...")
|
||||||
|
scope.set_fastframe_state(False)
|
||||||
|
|
||||||
|
# Set data source to CH1
|
||||||
|
print("Setting data source to CH1...")
|
||||||
|
scope.set_data_source('CH1')
|
||||||
|
|
||||||
|
# Try to acquire a single waveform
|
||||||
|
print("\nAttempting to acquire waveform from CH1...")
|
||||||
|
print("(This will timeout if the scope isn't responding properly)\n")
|
||||||
|
|
||||||
|
waveform = scope.acquire_waveform('CH1')
|
||||||
|
|
||||||
|
print(f"✓ SUCCESS! Acquired {len(waveform)} samples")
|
||||||
|
print(f" First 10 values: {waveform[:10]}")
|
||||||
|
print(f" Min: {min(waveform)}, Max: {max(waveform)}")
|
||||||
|
print(f" Average: {sum(waveform)/len(waveform):.2f}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n✗ Error: {type(e).__name__}: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
finally:
|
||||||
|
if scope.is_connected:
|
||||||
|
scope.disconnect()
|
||||||
|
print("\n✓ Disconnected")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for transfer_fastframe() method.
|
||||||
|
|
||||||
|
Tests bulk FastFrame transfer with 1MHz square wave on CH1.
|
||||||
|
Signal: 1MHz square wave, +125mV to -125mV
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
from tektronix_base import TektronixOscilloscopeBase
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
scope_ip = "192.168.10.105"
|
||||||
|
num_frames = 1000
|
||||||
|
|
||||||
|
print(f"Connecting to {scope_ip}...")
|
||||||
|
scope = TektronixOscilloscopeBase(resource_name=scope_ip, timeout=30.0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
scope.connect()
|
||||||
|
print(f"Connected: {scope.identify()}\n")
|
||||||
|
|
||||||
|
# Configure CH1
|
||||||
|
print("Configuring CH1...")
|
||||||
|
scope.set_channel_scale(1, 0.1) # 100mV/div
|
||||||
|
scope.set_channel_offset(1, 0.0)
|
||||||
|
scope.set_channel_coupling(1, 'DC')
|
||||||
|
scope.set_channel_termination(1, 50)
|
||||||
|
|
||||||
|
# Configure horizontal
|
||||||
|
print("Configuring horizontal...")
|
||||||
|
scope.set_record_length(5000)
|
||||||
|
scope.set_time_scale(80e-9) # 80ns/div
|
||||||
|
|
||||||
|
# Configure trigger
|
||||||
|
print("Configuring trigger...")
|
||||||
|
scope.set_trigger_source('CH1')
|
||||||
|
scope.set_trigger_level(1, 0.05) # 50mV
|
||||||
|
scope.set_trigger_slope('RISE')
|
||||||
|
scope.set_trigger_mode('NORMAL')
|
||||||
|
|
||||||
|
# Configure FastFrame
|
||||||
|
print(f"Configuring FastFrame ({num_frames} frames)...")
|
||||||
|
scope.set_fastframe_state(True)
|
||||||
|
scope.set_fastframe_count(num_frames)
|
||||||
|
|
||||||
|
# Verify settings
|
||||||
|
ff_count = scope.get_fastframe_count()
|
||||||
|
record_len = scope.get_record_length()
|
||||||
|
print(f" Frames: {ff_count}, Record length: {record_len}")
|
||||||
|
|
||||||
|
# Configure data transfer
|
||||||
|
print("Configuring data transfer...")
|
||||||
|
scope.set_data_encoding('RIBinary')
|
||||||
|
scope.set_data_source('CH1')
|
||||||
|
|
||||||
|
# Acquire
|
||||||
|
print("\nAcquiring FastFrame data...")
|
||||||
|
scope.write("ACQuire:STATE RUN")
|
||||||
|
time.sleep(2) # Wait for triggers
|
||||||
|
scope.write("ACQuire:STATE STOP")
|
||||||
|
time.sleep(0.5)
|
||||||
|
print(" Acquisition complete")
|
||||||
|
|
||||||
|
# Transfer all frames using the new method
|
||||||
|
print(f"\nTransferring {ff_count} frames using transfer_fastframe()...")
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
waveforms = scope.transfer_fastframe(parse=True, byte_count=1, signed=True)
|
||||||
|
|
||||||
|
end_time = time.time()
|
||||||
|
total_time = end_time - start_time
|
||||||
|
|
||||||
|
# Results
|
||||||
|
print(f"\n=== Transfer Complete ===")
|
||||||
|
print(f"Frames received: {len(waveforms)}")
|
||||||
|
print(f"Points per frame: {len(waveforms[0])}")
|
||||||
|
print(f"Total samples: {sum(len(wf) for wf in waveforms)}")
|
||||||
|
print(f"Transfer time: {total_time:.3f}s")
|
||||||
|
print(f"Frame rate: {len(waveforms) / total_time:.1f} frames/s")
|
||||||
|
print(f"Sample rate: {sum(len(wf) for wf in waveforms) / total_time / 1e6:.2f} MS/s")
|
||||||
|
|
||||||
|
# Waveform statistics
|
||||||
|
print(f"\n=== Waveform Statistics ===")
|
||||||
|
print(f"Frame 1: min={min(waveforms[0])}, max={max(waveforms[0])}, avg={sum(waveforms[0])/len(waveforms[0]):.1f}")
|
||||||
|
print(f"Frame {len(waveforms)//2}: min={min(waveforms[len(waveforms)//2])}, max={max(waveforms[len(waveforms)//2])}")
|
||||||
|
print(f"Frame {len(waveforms)}: min={min(waveforms[-1])}, max={max(waveforms[-1])}")
|
||||||
|
|
||||||
|
# Check for square wave
|
||||||
|
first_frame = waveforms[0]
|
||||||
|
positive = sum(1 for v in first_frame if v > 0)
|
||||||
|
negative = sum(1 for v in first_frame if v < 0)
|
||||||
|
print(f"\nSquare wave check (frame 1): {positive} positive, {negative} negative samples")
|
||||||
|
print(f"Duty cycle estimate: {positive / len(first_frame) * 100:.1f}%")
|
||||||
|
|
||||||
|
print("\n=== Success! ===")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nError: {type(e).__name__}: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
finally:
|
||||||
|
if scope.is_connected:
|
||||||
|
try:
|
||||||
|
scope.set_fastframe_state(False)
|
||||||
|
print("\nFastFrame disabled")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
scope.disconnect()
|
||||||
|
print("Disconnected")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for trigger control functionality.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from tektronix_base import TektronixOscilloscopeBase
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Test trigger functions on oscilloscope at 192.168.10.105"""
|
||||||
|
|
||||||
|
scope_ip = "192.168.10.105"
|
||||||
|
|
||||||
|
print(f"Connecting to oscilloscope at {scope_ip}...")
|
||||||
|
|
||||||
|
scope = TektronixOscilloscopeBase(resource_name=scope_ip)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Connect to scope
|
||||||
|
scope.connect()
|
||||||
|
print(f"✓ Connected to {scope.identify()}\n")
|
||||||
|
|
||||||
|
# Get current trigger settings
|
||||||
|
print("Getting current trigger settings...")
|
||||||
|
current_coupling = scope.get_trigger_coupling()
|
||||||
|
current_slope = scope.get_trigger_slope()
|
||||||
|
current_source = scope.get_trigger_source()
|
||||||
|
current_mode = scope.get_trigger_mode()
|
||||||
|
print(f"✓ Current trigger coupling: {current_coupling}")
|
||||||
|
print(f"✓ Current trigger slope: {current_slope}")
|
||||||
|
print(f"✓ Current trigger source: {current_source}")
|
||||||
|
print(f"✓ Current trigger mode: {current_mode}")
|
||||||
|
|
||||||
|
# Get trigger level for current source
|
||||||
|
# Extract channel number from source
|
||||||
|
if current_source.startswith('CH'):
|
||||||
|
channel = int(current_source[2])
|
||||||
|
current_level = scope.get_trigger_level(channel)
|
||||||
|
print(f"✓ Current trigger level for {current_source}: {current_level} V\n")
|
||||||
|
else:
|
||||||
|
current_level = 0.0
|
||||||
|
channel = 1
|
||||||
|
print(f" (Source is {current_source}, not a channel)\n")
|
||||||
|
|
||||||
|
# Test trigger coupling
|
||||||
|
print("Testing trigger coupling modes:")
|
||||||
|
test_couplings = ['DC', 'HFRej', 'LFRej', 'NOISErej']
|
||||||
|
for coupling in test_couplings:
|
||||||
|
print(f" Setting coupling to {coupling}...")
|
||||||
|
scope.set_trigger_coupling(coupling)
|
||||||
|
actual_coupling = scope.get_trigger_coupling()
|
||||||
|
print(f" ✓ Actual coupling: {actual_coupling}")
|
||||||
|
|
||||||
|
# Test trigger slope
|
||||||
|
print("\nTesting trigger slope modes:")
|
||||||
|
test_slopes = ['RISe', 'FALL', 'EITher']
|
||||||
|
for slope in test_slopes:
|
||||||
|
print(f" Setting slope to {slope}...")
|
||||||
|
scope.set_trigger_slope(slope)
|
||||||
|
actual_slope = scope.get_trigger_slope()
|
||||||
|
print(f" ✓ Actual slope: {actual_slope}")
|
||||||
|
|
||||||
|
# Test trigger source
|
||||||
|
print("\nTesting trigger source selection:")
|
||||||
|
test_sources = ['CH1', 'CH2', 'CH3', 'CH4']
|
||||||
|
for source in test_sources:
|
||||||
|
print(f" Setting source to {source}...")
|
||||||
|
scope.set_trigger_source(source)
|
||||||
|
actual_source = scope.get_trigger_source()
|
||||||
|
print(f" ✓ Actual source: {actual_source}")
|
||||||
|
|
||||||
|
# Test trigger source with integer
|
||||||
|
print("\nTesting trigger source with integer (2)...")
|
||||||
|
scope.set_trigger_source(2)
|
||||||
|
actual_source = scope.get_trigger_source()
|
||||||
|
print(f"✓ Actual source: {actual_source}")
|
||||||
|
|
||||||
|
# Test trigger level
|
||||||
|
print("\nTesting trigger level settings:")
|
||||||
|
test_levels = [0.0, 0.5, 1.0, -0.5, 2.5]
|
||||||
|
for level in test_levels:
|
||||||
|
print(f" Setting CH1 trigger level to {level} V...")
|
||||||
|
scope.set_trigger_level(1, level)
|
||||||
|
actual_level = scope.get_trigger_level(1)
|
||||||
|
print(f" ✓ Actual level: {actual_level} V")
|
||||||
|
|
||||||
|
# Test trigger level with channel string
|
||||||
|
print("\nTesting trigger level with channel string ('CH2')...")
|
||||||
|
scope.set_trigger_level('CH2', 1.5)
|
||||||
|
actual_level = scope.get_trigger_level('CH2')
|
||||||
|
print(f"✓ Actual level: {actual_level} V")
|
||||||
|
|
||||||
|
# Test trigger mode
|
||||||
|
print("\nTesting trigger modes:")
|
||||||
|
test_modes = ['AUTO', 'NORMal']
|
||||||
|
for mode in test_modes:
|
||||||
|
print(f" Setting trigger mode to {mode}...")
|
||||||
|
scope.set_trigger_mode(mode)
|
||||||
|
actual_mode = scope.get_trigger_mode()
|
||||||
|
print(f" ✓ Actual mode: {actual_mode}")
|
||||||
|
|
||||||
|
# Restore original settings
|
||||||
|
print(f"\nRestoring original trigger settings...")
|
||||||
|
scope.set_trigger_coupling(current_coupling)
|
||||||
|
scope.set_trigger_slope(current_slope)
|
||||||
|
scope.set_trigger_source(current_source)
|
||||||
|
scope.set_trigger_mode(current_mode)
|
||||||
|
if current_source.startswith('CH'):
|
||||||
|
scope.set_trigger_level(channel, current_level)
|
||||||
|
print(f"✓ Restored trigger coupling: {scope.get_trigger_coupling()}")
|
||||||
|
print(f"✓ Restored trigger slope: {scope.get_trigger_slope()}")
|
||||||
|
print(f"✓ Restored trigger source: {scope.get_trigger_source()}")
|
||||||
|
print(f"✓ Restored trigger mode: {scope.get_trigger_mode()}")
|
||||||
|
|
||||||
|
# Test invalid inputs
|
||||||
|
print("\nTesting invalid coupling (should raise ValueError)...")
|
||||||
|
try:
|
||||||
|
scope.set_trigger_coupling("INVALID")
|
||||||
|
print("✗ ERROR: Should have raised ValueError!")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"✓ Correctly raised ValueError: {e}")
|
||||||
|
|
||||||
|
print("\nTesting invalid slope (should raise ValueError)...")
|
||||||
|
try:
|
||||||
|
scope.set_trigger_slope("INVALID")
|
||||||
|
print("✗ ERROR: Should have raised ValueError!")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"✓ Correctly raised ValueError: {e}")
|
||||||
|
|
||||||
|
print("\nTesting invalid source (should raise ValueError)...")
|
||||||
|
try:
|
||||||
|
scope.set_trigger_source("CH5")
|
||||||
|
print("✗ ERROR: Should have raised ValueError!")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"✓ Correctly raised ValueError: {e}")
|
||||||
|
|
||||||
|
print("\nTesting invalid channel for trigger level (should raise ValueError)...")
|
||||||
|
try:
|
||||||
|
scope.set_trigger_level(5, 0.0)
|
||||||
|
print("✗ ERROR: Should have raised ValueError!")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"✓ Correctly raised ValueError: {e}")
|
||||||
|
|
||||||
|
print("\nTesting invalid trigger mode (should raise ValueError)...")
|
||||||
|
try:
|
||||||
|
scope.set_trigger_mode("INVALID")
|
||||||
|
print("✗ ERROR: Should have raised ValueError!")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"✓ Correctly raised ValueError: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Error: {type(e).__name__}: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
finally:
|
||||||
|
if scope.is_connected:
|
||||||
|
scope.disconnect()
|
||||||
|
print("\n✓ Disconnected from oscilloscope")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+273
@@ -0,0 +1,273 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for waveform transfer functionality.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from tektronix_base import TektronixOscilloscopeBase
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Test waveform transfer functions on oscilloscope at 192.168.10.105"""
|
||||||
|
|
||||||
|
scope_ip = "192.168.10.105"
|
||||||
|
|
||||||
|
print(f"Connecting to oscilloscope at {scope_ip}...")
|
||||||
|
|
||||||
|
scope = TektronixOscilloscopeBase(resource_name=scope_ip)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Connect to scope
|
||||||
|
scope.connect()
|
||||||
|
print(f"Connected to {scope.identify()}\n")
|
||||||
|
|
||||||
|
# Save current settings
|
||||||
|
print("Saving current waveform transfer settings...")
|
||||||
|
current_data_encoding = scope.get_data_encoding()
|
||||||
|
current_data_source = scope.get_data_source()
|
||||||
|
current_wfmoutpre_encoding = scope.get_wfmoutpre_encoding()
|
||||||
|
current_byte_count = scope.get_wfmoutpre_byte_count()
|
||||||
|
current_byte_order = scope.get_wfmoutpre_byte_order()
|
||||||
|
print(f" Current data encoding: {current_data_encoding}")
|
||||||
|
print(f" Current data source: {current_data_source}")
|
||||||
|
print(f" Current WFMOutpre encoding: {current_wfmoutpre_encoding}")
|
||||||
|
print(f" Current byte count: {current_byte_count}")
|
||||||
|
print(f" Current byte order: {current_byte_order}\n")
|
||||||
|
|
||||||
|
# Test setting data encoding
|
||||||
|
print("Testing data encoding settings:")
|
||||||
|
test_encodings = ['ASCIi', 'RIBinary', 'RPBinary']
|
||||||
|
for encoding in test_encodings:
|
||||||
|
print(f" Setting data encoding to {encoding}...")
|
||||||
|
scope.set_data_encoding(encoding)
|
||||||
|
actual = scope.get_data_encoding()
|
||||||
|
print(f" Actual encoding: {actual}")
|
||||||
|
|
||||||
|
# Test setting data source
|
||||||
|
print("\nTesting data source selection:")
|
||||||
|
for source in ['CH1', 'CH2', 'CH3', 'CH4']:
|
||||||
|
print(f" Setting data source to {source}...")
|
||||||
|
scope.set_data_source(source)
|
||||||
|
actual = scope.get_data_source()
|
||||||
|
print(f" Actual source: {actual}")
|
||||||
|
|
||||||
|
# Test with integer source
|
||||||
|
print("\nTesting data source with integer (2)...")
|
||||||
|
scope.set_data_source(2)
|
||||||
|
actual = scope.get_data_source()
|
||||||
|
print(f" Actual source: {actual}")
|
||||||
|
|
||||||
|
# Test waveform preamble encoding
|
||||||
|
print("\nTesting waveform preamble encoding:")
|
||||||
|
for encoding in ['BINary', 'ASCii']:
|
||||||
|
print(f" Setting WFMOutpre encoding to {encoding}...")
|
||||||
|
scope.set_wfmoutpre_encoding(encoding)
|
||||||
|
actual = scope.get_wfmoutpre_encoding()
|
||||||
|
print(f" Actual encoding: {actual}")
|
||||||
|
|
||||||
|
# Test byte count
|
||||||
|
print("\nTesting byte count settings:")
|
||||||
|
for byte_count in [1, 2]:
|
||||||
|
print(f" Setting byte count to {byte_count}...")
|
||||||
|
scope.set_wfmoutpre_byte_count(byte_count)
|
||||||
|
actual = scope.get_wfmoutpre_byte_count()
|
||||||
|
print(f" Actual byte count: {actual}")
|
||||||
|
|
||||||
|
# Test byte order
|
||||||
|
print("\nTesting byte order settings:")
|
||||||
|
for byte_order in ['MSB', 'LSB']:
|
||||||
|
print(f" Setting byte order to {byte_order}...")
|
||||||
|
scope.set_wfmoutpre_byte_order(byte_order)
|
||||||
|
actual = scope.get_wfmoutpre_byte_order()
|
||||||
|
print(f" Actual byte order: {actual}")
|
||||||
|
|
||||||
|
# Query complete waveform preamble
|
||||||
|
print("\nQuerying complete waveform preamble...")
|
||||||
|
preamble = scope.query_wfmoutpre()
|
||||||
|
print(f" Preamble (first 100 chars): {preamble[:100]}...")
|
||||||
|
|
||||||
|
# Set up for binary waveform acquisition
|
||||||
|
print("\nConfiguring for binary waveform transfer:")
|
||||||
|
print(" Setting data encoding to RIBinary...")
|
||||||
|
scope.set_data_encoding('RIBinary')
|
||||||
|
print(" Setting WFMOutpre encoding to BINary...")
|
||||||
|
scope.set_wfmoutpre_encoding('BINary')
|
||||||
|
print(" Setting byte count to 1...")
|
||||||
|
scope.set_wfmoutpre_byte_count(1)
|
||||||
|
print(" Setting byte order to MSB...")
|
||||||
|
scope.set_wfmoutpre_byte_order('MSB')
|
||||||
|
print(" Setting data source to CH1...")
|
||||||
|
scope.set_data_source('CH1')
|
||||||
|
print(" Configuration complete")
|
||||||
|
|
||||||
|
# Transfer curve data
|
||||||
|
print("\nTransferring curve data from CH1...")
|
||||||
|
curve_data = scope.transfer_curve()
|
||||||
|
print(f" Received {len(curve_data)} bytes of curve data")
|
||||||
|
print(f" First 10 bytes (raw): {list(curve_data[:10])}")
|
||||||
|
|
||||||
|
# Parse the curve data
|
||||||
|
print("\nParsing curve data...")
|
||||||
|
values = scope.parse_curve_data(curve_data, byte_count=1, signed=True, byte_order='MSB')
|
||||||
|
print(f" Parsed {len(values)} samples")
|
||||||
|
print(f" First 10 values: {values[:10]}")
|
||||||
|
print(f" Min value: {min(values)}")
|
||||||
|
print(f" Max value: {max(values)}")
|
||||||
|
print(f" Average value: {sum(values) / len(values):.2f}")
|
||||||
|
|
||||||
|
# Transfer complete waveform (preamble + curve)
|
||||||
|
print("\nTransferring complete waveform (WAVFrm?)...")
|
||||||
|
preamble_str, curve_bytes = scope.transfer_waveform()
|
||||||
|
print(f" Received preamble: {preamble_str[:100]}...")
|
||||||
|
print(f" Received {len(curve_bytes)} bytes of curve data")
|
||||||
|
|
||||||
|
# Parse this curve data too
|
||||||
|
waveform_values = scope.parse_curve_data(curve_bytes, byte_count=1, signed=True, byte_order='MSB')
|
||||||
|
print(f" Parsed {len(waveform_values)} samples from complete waveform")
|
||||||
|
|
||||||
|
# Test high-level acquire_waveform method
|
||||||
|
print("\nTesting high-level acquire_waveform method:")
|
||||||
|
print(" Acquiring waveform from CH1...")
|
||||||
|
waveform = scope.acquire_waveform('CH1')
|
||||||
|
print(f" Acquired {len(waveform)} samples")
|
||||||
|
print(f" First 10 values: {waveform[:10]}")
|
||||||
|
print(f" Min: {min(waveform)}, Max: {max(waveform)}, Avg: {sum(waveform)/len(waveform):.2f}")
|
||||||
|
|
||||||
|
print("\n Acquiring waveform from CH2 (using integer)...")
|
||||||
|
try:
|
||||||
|
waveform2 = scope.acquire_waveform(2)
|
||||||
|
print(f" Acquired {len(waveform2)} samples from CH2")
|
||||||
|
print(f" First 10 values: {waveform2[:10]}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Could not acquire from CH2 (channel may not be active): {e}")
|
||||||
|
|
||||||
|
# Test FastFrame support
|
||||||
|
print("\nTesting FastFrame frame selection:")
|
||||||
|
|
||||||
|
# Check if FastFrame is currently enabled
|
||||||
|
try:
|
||||||
|
ff_state = scope.get_fastframe_state()
|
||||||
|
ff_count = scope.get_fastframe_count()
|
||||||
|
print(f" Current FastFrame state: {ff_state} ({'active' if ff_state else 'off'})")
|
||||||
|
print(f" Current frame count: {ff_count}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Could not query FastFrame state (skipping FastFrame tests): {e}")
|
||||||
|
ff_state = None
|
||||||
|
|
||||||
|
if ff_state is not None and ff_state:
|
||||||
|
# FastFrame is active, test frame selection
|
||||||
|
print(" FastFrame is active, testing frame selection...")
|
||||||
|
current_frame = scope.get_fastframe_selected()
|
||||||
|
print(f" Current selected frame: {current_frame}")
|
||||||
|
|
||||||
|
# Try selecting different frames
|
||||||
|
for frame in [1, min(5, ff_count), ff_count]:
|
||||||
|
print(f" Selecting frame {frame}...")
|
||||||
|
scope.set_fastframe_selected(frame)
|
||||||
|
actual = scope.get_fastframe_selected()
|
||||||
|
print(f" Selected frame: {actual}")
|
||||||
|
|
||||||
|
# Acquire waveform from this frame
|
||||||
|
print(f" Acquiring waveform from frame {frame}...")
|
||||||
|
frame_waveform = scope.acquire_waveform('CH1', frame_number=frame)
|
||||||
|
print(f" Acquired {len(frame_waveform)} samples")
|
||||||
|
print(f" First 5 values: {frame_waveform[:5]}")
|
||||||
|
|
||||||
|
# Restore original frame
|
||||||
|
scope.set_fastframe_selected(current_frame)
|
||||||
|
elif ff_state is not None:
|
||||||
|
print(" FastFrame is not active, enabling it temporarily...")
|
||||||
|
scope.set_fastframe_state(True)
|
||||||
|
scope.set_fastframe_count(10)
|
||||||
|
print(" FastFrame enabled with 10 frames")
|
||||||
|
|
||||||
|
# Test frame selection
|
||||||
|
for frame in [1, 5, 10]:
|
||||||
|
print(f" Selecting frame {frame}...")
|
||||||
|
scope.set_fastframe_selected(frame)
|
||||||
|
actual = scope.get_fastframe_selected()
|
||||||
|
print(f" Selected frame: {actual}")
|
||||||
|
|
||||||
|
# Restore FastFrame state
|
||||||
|
scope.set_fastframe_state(False)
|
||||||
|
print(" FastFrame disabled (restored)")
|
||||||
|
|
||||||
|
# Restore original settings
|
||||||
|
print("\nRestoring original waveform transfer settings...")
|
||||||
|
try:
|
||||||
|
scope.set_data_encoding(current_data_encoding)
|
||||||
|
scope.set_data_source(current_data_source)
|
||||||
|
scope.set_wfmoutpre_encoding(current_wfmoutpre_encoding)
|
||||||
|
scope.set_wfmoutpre_byte_count(current_byte_count)
|
||||||
|
scope.set_wfmoutpre_byte_order(current_byte_order)
|
||||||
|
print(" Settings restored")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Could not restore settings (connection may be in bad state): {e}")
|
||||||
|
|
||||||
|
# Test error handling
|
||||||
|
print("\n=== Testing Error Handling ===\n")
|
||||||
|
|
||||||
|
# Skip error handling tests if connection is already bad
|
||||||
|
try:
|
||||||
|
# Quick connectivity check
|
||||||
|
scope.query("*OPC?")
|
||||||
|
except Exception:
|
||||||
|
print("Connection appears to be in bad state, skipping error handling tests\n")
|
||||||
|
print("=== Waveform transfer tests completed (with some skipped due to connection issues) ===")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("Testing invalid data encoding (should raise ValueError)...")
|
||||||
|
try:
|
||||||
|
scope.set_data_encoding('INVALID')
|
||||||
|
print(" ERROR: Should have raised ValueError!")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f" Correctly raised ValueError: {e}")
|
||||||
|
|
||||||
|
print("\nTesting invalid data source (should raise ValueError)...")
|
||||||
|
try:
|
||||||
|
scope.set_data_source('CH5')
|
||||||
|
print(" ERROR: Should have raised ValueError!")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f" Correctly raised ValueError: {e}")
|
||||||
|
|
||||||
|
print("\nTesting invalid byte count (should raise ValueError)...")
|
||||||
|
try:
|
||||||
|
scope.set_wfmoutpre_byte_count(3)
|
||||||
|
print(" ERROR: Should have raised ValueError!")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f" Correctly raised ValueError: {e}")
|
||||||
|
|
||||||
|
print("\nTesting invalid byte order (should raise ValueError)...")
|
||||||
|
try:
|
||||||
|
scope.set_wfmoutpre_byte_order('INVALID')
|
||||||
|
print(" ERROR: Should have raised ValueError!")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f" Correctly raised ValueError: {e}")
|
||||||
|
|
||||||
|
print("\nTesting invalid FastFrame frame number (should raise ValueError)...")
|
||||||
|
try:
|
||||||
|
scope.set_fastframe_selected(-1)
|
||||||
|
print(" ERROR: Should have raised ValueError!")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f" Correctly raised ValueError: {e}")
|
||||||
|
|
||||||
|
print("\nTesting invalid channel in acquire_waveform (should raise ValueError)...")
|
||||||
|
try:
|
||||||
|
scope.acquire_waveform(5)
|
||||||
|
print(" ERROR: Should have raised ValueError!")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f" Correctly raised ValueError: {e}")
|
||||||
|
|
||||||
|
print("\n=== All waveform transfer tests completed successfully! ===")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nError: {type(e).__name__}: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
finally:
|
||||||
|
if scope.is_connected:
|
||||||
|
scope.disconnect()
|
||||||
|
print("\nDisconnected from oscilloscope")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+77
@@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Simple focused test for waveform transfer functionality.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from tektronix_base import TektronixOscilloscopeBase
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Simple test of core waveform transfer on CH1"""
|
||||||
|
|
||||||
|
scope_ip = "192.168.10.105"
|
||||||
|
|
||||||
|
print(f"Connecting to oscilloscope at {scope_ip}...")
|
||||||
|
|
||||||
|
scope = TektronixOscilloscopeBase(resource_name=scope_ip, timeout=10.0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Connect to scope
|
||||||
|
scope.connect()
|
||||||
|
print(f"Connected to {scope.identify()}\n")
|
||||||
|
|
||||||
|
# Test 1: Configure and transfer using low-level methods
|
||||||
|
print("=== Test 1: Low-level waveform transfer ===")
|
||||||
|
print("Configuring data transfer settings...")
|
||||||
|
scope.set_data_encoding('RIBinary')
|
||||||
|
scope.set_wfmoutpre_encoding('BINary')
|
||||||
|
scope.set_wfmoutpre_byte_count(1)
|
||||||
|
scope.set_wfmoutpre_byte_order('MSB')
|
||||||
|
scope.set_data_source('CH1')
|
||||||
|
|
||||||
|
print("Transferring curve data from CH1...")
|
||||||
|
curve_data = scope.transfer_curve()
|
||||||
|
print(f" Received {len(curve_data)} bytes")
|
||||||
|
|
||||||
|
print("Parsing curve data...")
|
||||||
|
values = scope.parse_curve_data(curve_data, byte_count=1, signed=True, byte_order='MSB')
|
||||||
|
print(f" Parsed {len(values)} samples")
|
||||||
|
print(f" First 10 values: {values[:10]}")
|
||||||
|
print(f" Min: {min(values)}, Max: {max(values)}, Avg: {sum(values)/len(values):.2f}")
|
||||||
|
|
||||||
|
# Test 2: Use high-level acquire_waveform method
|
||||||
|
print("\n=== Test 2: High-level waveform acquisition ===")
|
||||||
|
print("Acquiring waveform from CH1...")
|
||||||
|
waveform = scope.acquire_waveform('CH1')
|
||||||
|
print(f" Acquired {len(waveform)} samples")
|
||||||
|
print(f" First 10 values: {waveform[:10]}")
|
||||||
|
print(f" Min: {min(waveform)}, Max: {max(waveform)}, Avg: {sum(waveform)/len(waveform):.2f}")
|
||||||
|
|
||||||
|
# Test 3: Query waveform preamble
|
||||||
|
print("\n=== Test 3: Waveform preamble ===")
|
||||||
|
preamble = scope.query_wfmoutpre()
|
||||||
|
print(f" Preamble: {preamble[:150]}...")
|
||||||
|
|
||||||
|
# Test 4: Transfer complete waveform (preamble + curve)
|
||||||
|
print("\n=== Test 4: Complete waveform transfer ===")
|
||||||
|
print("Transferring complete waveform...")
|
||||||
|
preamble_str, curve_bytes = scope.transfer_waveform()
|
||||||
|
print(f" Preamble length: {len(preamble_str)} chars")
|
||||||
|
print(f" Curve data: {len(curve_bytes)} bytes")
|
||||||
|
wf_values = scope.parse_curve_data(curve_bytes, byte_count=1, signed=True, byte_order='MSB')
|
||||||
|
print(f" Parsed {len(wf_values)} samples")
|
||||||
|
|
||||||
|
print("\n=== All tests completed successfully! ===")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nError: {type(e).__name__}: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
finally:
|
||||||
|
if scope.is_connected:
|
||||||
|
scope.disconnect()
|
||||||
|
print("\nDisconnected from oscilloscope")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
# Coherent HOPS Laser I2C Protocol Documentation
|
||||||
|
|
||||||
|
This document describes the I2C communication protocol used to control Coherent HOPS laser systems, extracted from the CohrHopsDemo v2.0.7 codebase.
|
||||||
|
|
||||||
|
## Hardware Overview
|
||||||
|
|
||||||
|
### FTDI Interface
|
||||||
|
- **Chip**: FT2232C (dual-channel USB)
|
||||||
|
- **Protocol**: I2C via MPSSE (Multi-Protocol Synchronous Serial Engine)
|
||||||
|
- **Library**: CohrFTCI2C.dll (Windows), use libftdi/libmpsse on Linux
|
||||||
|
|
||||||
|
### I2C Configuration
|
||||||
|
| Parameter | Value/Range |
|
||||||
|
|-----------|-------------|
|
||||||
|
| Clock Divisor | 0 - 65535 |
|
||||||
|
| Modes | STANDARD, FAST |
|
||||||
|
| Control Bytes | 1 - 255 |
|
||||||
|
| Data Bytes | 1 - 65535 |
|
||||||
|
|
||||||
|
### I2C Slave
|
||||||
|
- **Device**: NXP microcontroller
|
||||||
|
- **Role**: Intermediary between FTDI and laser hardware
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## I2C Library Functions
|
||||||
|
|
||||||
|
These are the low-level FTDI I2C functions (from CohrFTCI2C.dll):
|
||||||
|
|
||||||
|
| Function | Purpose |
|
||||||
|
|----------|---------|
|
||||||
|
| `I2C_GetNumDevices` | Enumerate connected I2C devices |
|
||||||
|
| `I2C_GetDeviceNameLocID` | Get device location identifier |
|
||||||
|
| `I2C_GetDeviceNameSerialNumber` | Get device serial number |
|
||||||
|
| `I2C_Open` | Open I2C device |
|
||||||
|
| `I2C_OpenEx` | Extended open with options |
|
||||||
|
| `I2C_OpenSerialNumber` | Open device by serial number |
|
||||||
|
| `I2C_InitDevice` | Initialize MPSSE interface |
|
||||||
|
| `I2C_SetMode` | Set STANDARD or FAST mode |
|
||||||
|
| `I2C_GetClock` | Get current clock divisor |
|
||||||
|
| `I2C_SetClock` | Set clock divisor |
|
||||||
|
| `I2C_SetLoopback` | Enable/disable loopback testing |
|
||||||
|
| `I2C_Write` | Write control + data bytes |
|
||||||
|
| `I2C_Read` | Read data bytes |
|
||||||
|
| `I2C_ReadAlt` | Alternative read function |
|
||||||
|
| `I2C_Close` | Close I2C device |
|
||||||
|
| `I2C_GetErrorCodeString` | Get error descriptions |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## NXP Slave Operations
|
||||||
|
|
||||||
|
The NXP microcontroller provides these I2C operations:
|
||||||
|
|
||||||
|
| Method | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `NXP::Write` | Write data to I2C slave |
|
||||||
|
| `NXP::Read` | Read data from I2C slave |
|
||||||
|
| `NXP::WriteRegister` | Write to internal registers |
|
||||||
|
| `NXP::ReadRegister` | Read from internal registers |
|
||||||
|
| `NXP::WriteGPIO` | Control GPIO outputs |
|
||||||
|
| `NXP::ReadGPIO` | Read GPIO inputs |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## I2C Transaction Format
|
||||||
|
|
||||||
|
### Write Operation
|
||||||
|
```
|
||||||
|
1. WriteControlBuffer: I2C slave address + W bit (0)
|
||||||
|
2. WriteDataBuffer: Register address + data
|
||||||
|
- BYTE mode: Single byte writes
|
||||||
|
- PAGE mode: Multi-byte writes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Read Operation
|
||||||
|
```
|
||||||
|
1. WriteControlBuffer: I2C slave address + R bit (1)
|
||||||
|
2. ReadDataBuffer: Receive response
|
||||||
|
- BYTE mode: Single byte reads
|
||||||
|
- BLOCK mode: Multi-byte reads
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## High-Level Command Interface
|
||||||
|
|
||||||
|
Commands are sent via `CohrHOPS_SendCommand()` using the format `?COMMAND` for queries.
|
||||||
|
|
||||||
|
### System Information Commands
|
||||||
|
|
||||||
|
| Command | Purpose | Example Response |
|
||||||
|
|---------|---------|------------------|
|
||||||
|
| `?HID` | Query Hardware ID | Device identifier |
|
||||||
|
| `?HTYPE` | Query Head Type | Head variant |
|
||||||
|
| `?HBDREV` | Query Head Board Revision | PCB revision |
|
||||||
|
| `?HEADDIO` | Query Head Digital I/O | DIO configuration |
|
||||||
|
| `?LASERMODEL` | Query Laser Model | G532, Tina, Mini00, MiniX |
|
||||||
|
| `?POWERUNITS` | Query Power Units | mW, W, etc. |
|
||||||
|
| `?WAVELENGTH` | Query Wavelength | 532nm, etc. |
|
||||||
|
|
||||||
|
### Temperature Monitoring
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|---------|---------|
|
||||||
|
| `?TMAIN` | Main Heatsink Temperature |
|
||||||
|
| `?TBRF` | BRF (Birefringent Filter) Temperature |
|
||||||
|
| `?TSHG` | SHG (Second Harmonic Generator) Temperature |
|
||||||
|
| `?TTHG` | THG (Third Harmonic Generator) Temperature |
|
||||||
|
| `?TETA` | ETA Temperature |
|
||||||
|
|
||||||
|
### Temperature Control (Setpoints)
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|---------|---------|
|
||||||
|
| `?TMAINCMD` | Get/Set Main Temperature Setpoint |
|
||||||
|
| `?TBRFCMD` | Get/Set BRF Temperature Setpoint |
|
||||||
|
| `?TSHGCMD` | Get/Set SHG Temperature Setpoint |
|
||||||
|
| `?TTHGCMD` | Get/Set THG Temperature Setpoint |
|
||||||
|
| `?TETACMD` | Get/Set ETA Temperature Setpoint |
|
||||||
|
|
||||||
|
### Temperature Data
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|---------|---------|
|
||||||
|
| `?MAIND` | Main Temperature Data |
|
||||||
|
| `?BRFD` | BRF Temperature Data |
|
||||||
|
| `?SHGD` | SHG Temperature Data |
|
||||||
|
| `?THGD` | THG Temperature Data |
|
||||||
|
| `?ETAD` | ETA Temperature Data |
|
||||||
|
|
||||||
|
### Power Control
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|---------|---------|
|
||||||
|
| `?PCMD` | Get/Set Power Command |
|
||||||
|
| `?PMEM` | Query Power Memory (stored settings) |
|
||||||
|
| `?PLIM` | Query Power Limits |
|
||||||
|
|
||||||
|
### Current Control
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|---------|---------|
|
||||||
|
| `?CCMD` | Get/Set Current Command |
|
||||||
|
| `?CLIM` | Query Current Limits |
|
||||||
|
| `?CMODE` | Get/Set Control Mode |
|
||||||
|
| `?CMODECMD` | Get/Set Control Mode Command |
|
||||||
|
|
||||||
|
### Digital I/O
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|---------|---------|
|
||||||
|
| `?PSDIO` | Power Supply Digital I/O |
|
||||||
|
| `?PSGLUEIN` | Power Supply Glue Logic Input |
|
||||||
|
| `?PSGLUEOUT` | Power Supply Glue Logic Output |
|
||||||
|
|
||||||
|
### Monitoring & Status
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|---------|---------|
|
||||||
|
| `?ANA` | Query Analog Values |
|
||||||
|
| `?ANACMD` | Get/Set Analog Command |
|
||||||
|
| `?KSW` | Key Switch Status |
|
||||||
|
| `?KSWCMD` | Get/Set Key Switch Command |
|
||||||
|
| `?FAN` | Fan Status/Control |
|
||||||
|
| `?INT` | Interlock Status |
|
||||||
|
| `?REM` | Remote Control Status |
|
||||||
|
| `?EEH` | EEPROM Header |
|
||||||
|
|
||||||
|
### Configuration Registers
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|---------|---------|
|
||||||
|
| `?CFG0` | Configuration Register 0 |
|
||||||
|
| `?CFG1` | Configuration Register 1 |
|
||||||
|
| `?CFG2` | Configuration Register 2 |
|
||||||
|
| `?CFG3` | Configuration Register 3 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Supported Laser Models
|
||||||
|
|
||||||
|
| Model | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| G532 | 532nm Green Laser |
|
||||||
|
| Tina | Proprietary Model |
|
||||||
|
| Mini00 | Compact Variant |
|
||||||
|
| MiniX | Extended Mini Variant |
|
||||||
|
| CommonLaser | Base Implementation |
|
||||||
|
| DummyLaser | Test/Simulation |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Linux Implementation Guide
|
||||||
|
|
||||||
|
### Required Libraries
|
||||||
|
|
||||||
|
For Linux implementation, use one of:
|
||||||
|
- **libftdi** + **libmpsse** - Direct FTDI MPSSE control
|
||||||
|
- **pylibftdi** - Python bindings for libftdi
|
||||||
|
- Standard Linux I2C (`/dev/i2c-*`) if FTDI exposes as I2C adapter
|
||||||
|
|
||||||
|
### Installation (Debian/Ubuntu)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install libftdi-dev libmpsse-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Basic Implementation Steps
|
||||||
|
|
||||||
|
1. **Initialize FTDI Device**
|
||||||
|
```c
|
||||||
|
// Find and open FT2232C device
|
||||||
|
ftdi_init(&ftdi);
|
||||||
|
ftdi_usb_open(&ftdi, 0x0403, 0x6010); // FTDI VID/PID
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Configure MPSSE for I2C**
|
||||||
|
```c
|
||||||
|
// Enable MPSSE mode
|
||||||
|
ftdi_set_bitmode(&ftdi, 0, BITMODE_MPSSE);
|
||||||
|
|
||||||
|
// Set I2C clock speed
|
||||||
|
// Clock = 60MHz / ((1 + divisor) * 2)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Send I2C Commands**
|
||||||
|
```c
|
||||||
|
// Write command to laser
|
||||||
|
i2c_write(slave_addr, "?HID", 4);
|
||||||
|
|
||||||
|
// Read response
|
||||||
|
i2c_read(slave_addr, buffer, sizeof(buffer));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: Query Laser Model
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <ftdi.h>
|
||||||
|
#include <mpsse.h>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
struct mpsse_context *i2c;
|
||||||
|
char response[256];
|
||||||
|
|
||||||
|
// Open I2C at 100kHz
|
||||||
|
i2c = MPSSE(I2C, ONE_HUNDRED_KHZ, MSB);
|
||||||
|
|
||||||
|
if (i2c && i2c->open) {
|
||||||
|
// Send query command
|
||||||
|
Start(i2c);
|
||||||
|
Write(i2c, "?LASERMODEL", 11);
|
||||||
|
Stop(i2c);
|
||||||
|
|
||||||
|
// Read response
|
||||||
|
Start(i2c);
|
||||||
|
char *data = Read(i2c, 256);
|
||||||
|
Stop(i2c);
|
||||||
|
|
||||||
|
printf("Laser Model: %s\n", data);
|
||||||
|
free(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
Close(i2c);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Common Errors
|
||||||
|
|
||||||
|
| Error | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| Timeout after control byte | No ACK received after sending slave address |
|
||||||
|
| Timeout after data byte | No ACK received after sending data |
|
||||||
|
| MPSSE sync failure | Failed to synchronize FTDI MPSSE interface |
|
||||||
|
|
||||||
|
### Recovery
|
||||||
|
|
||||||
|
1. Reset MPSSE interface
|
||||||
|
2. Re-initialize I2C
|
||||||
|
3. Check physical connections
|
||||||
|
4. Verify I2C slave address
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Protocol Notes
|
||||||
|
|
||||||
|
- Commands use ASCII text format
|
||||||
|
- Query commands start with `?`
|
||||||
|
- Set commands likely use `=` followed by value
|
||||||
|
- Responses are ASCII strings
|
||||||
|
- Temperature values likely in degrees Celsius
|
||||||
|
- Power values use units from `?POWERUNITS` response
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Source Files Reference
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `CohrHOPS.dll` | Main laser control library |
|
||||||
|
| `CohrFTCI2C.dll` | FTDI I2C bridge library |
|
||||||
|
| `main.c` | Demo application |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
- FTDI MPSSE Documentation: https://ftdichip.com/software-examples/mpsse-projects/
|
||||||
|
- libmpsse: https://github.com/devttys0/libmpsse
|
||||||
|
- Linux I2C: https://www.kernel.org/doc/html/latest/i2c/
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
# PyPewPewHOPS - Coherent HOPS Laser Control Library
|
||||||
|
|
||||||
|
Python library for controlling Coherent HOPS laser systems via I2C protocol through an FTDI FT2232C USB interface.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Complete implementation of all documented I2C commands
|
||||||
|
- Support for system information queries
|
||||||
|
- Temperature monitoring and control for all sensors
|
||||||
|
- Power and current control
|
||||||
|
- Digital I/O operations
|
||||||
|
- Configuration register access
|
||||||
|
- Context manager support for safe resource handling
|
||||||
|
- Dummy laser simulator for development without hardware
|
||||||
|
- Comprehensive error handling
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- Python 3.7+
|
||||||
|
- FTDI FT2232C USB device
|
||||||
|
- libftdi library (for Linux)
|
||||||
|
|
||||||
|
### Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linux Setup
|
||||||
|
|
||||||
|
On Linux, you may need to install libftdi:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Debian/Ubuntu
|
||||||
|
sudo apt install libftdi-dev
|
||||||
|
|
||||||
|
# Fedora
|
||||||
|
sudo dnf install libftdi-devel
|
||||||
|
```
|
||||||
|
|
||||||
|
You may also need to add your user to the appropriate group:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo usermod -a -G dialout $USER
|
||||||
|
sudo usermod -a -G plugdev $USER
|
||||||
|
```
|
||||||
|
|
||||||
|
Then log out and log back in for the changes to take effect.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Using the Simulator (No Hardware)
|
||||||
|
|
||||||
|
```python
|
||||||
|
from coherent_hops_laser import DummyLaser
|
||||||
|
|
||||||
|
with DummyLaser() as laser:
|
||||||
|
# Query system information
|
||||||
|
info = laser.get_system_info()
|
||||||
|
print(f"Model: {info.laser_model}")
|
||||||
|
print(f"Wavelength: {info.wavelength}")
|
||||||
|
|
||||||
|
# Monitor temperatures
|
||||||
|
temps = laser.get_all_temperatures()
|
||||||
|
print(f"Main temperature: {temps.main}°C")
|
||||||
|
|
||||||
|
# Control power
|
||||||
|
laser.set_power_command(100.0)
|
||||||
|
power = laser.get_power_command()
|
||||||
|
print(f"Power set to: {power} mW")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Real Hardware
|
||||||
|
|
||||||
|
```python
|
||||||
|
from coherent_hops_laser import CoherentHOPSLaser, I2CMode
|
||||||
|
|
||||||
|
# Initialize laser controller
|
||||||
|
laser = CoherentHOPSLaser(
|
||||||
|
slave_address=0x50, # I2C slave address
|
||||||
|
i2c_mode=I2CMode.STANDARD # 100 kHz
|
||||||
|
)
|
||||||
|
|
||||||
|
# Connect to FTDI device
|
||||||
|
laser.connect('ftdi://ftdi:2232/1')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Query laser model
|
||||||
|
model = laser.get_laser_model()
|
||||||
|
print(f"Laser Model: {model}")
|
||||||
|
|
||||||
|
# Get all temperatures
|
||||||
|
temps = laser.get_all_temperatures()
|
||||||
|
print(f"Temperatures: {temps}")
|
||||||
|
|
||||||
|
# Set power
|
||||||
|
laser.set_power_command(50.0)
|
||||||
|
|
||||||
|
# Check control mode
|
||||||
|
mode = laser.get_control_mode()
|
||||||
|
print(f"Control Mode: {mode}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
laser.disconnect()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Context Manager
|
||||||
|
|
||||||
|
```python
|
||||||
|
from coherent_hops_laser import CoherentHOPSLaser
|
||||||
|
|
||||||
|
with CoherentHOPSLaser() as laser:
|
||||||
|
laser.connect()
|
||||||
|
|
||||||
|
# Your laser control code here
|
||||||
|
info = laser.get_system_info()
|
||||||
|
print(info)
|
||||||
|
|
||||||
|
# Automatically disconnects
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Commands
|
||||||
|
|
||||||
|
### System Information
|
||||||
|
|
||||||
|
- `get_hardware_id()` - Hardware ID
|
||||||
|
- `get_head_type()` - Head type
|
||||||
|
- `get_head_board_revision()` - PCB revision
|
||||||
|
- `get_laser_model()` - Laser model (G532, Tina, Mini00, MiniX)
|
||||||
|
- `get_power_units()` - Power units (mW, W)
|
||||||
|
- `get_wavelength()` - Wavelength (e.g., 532nm)
|
||||||
|
- `get_system_info()` - All system info at once
|
||||||
|
|
||||||
|
### Temperature Monitoring
|
||||||
|
|
||||||
|
- `get_temperature_main()` - Main heatsink temperature
|
||||||
|
- `get_temperature_brf()` - BRF temperature
|
||||||
|
- `get_temperature_shg()` - SHG temperature
|
||||||
|
- `get_temperature_thg()` - THG temperature
|
||||||
|
- `get_temperature_eta()` - ETA temperature
|
||||||
|
- `get_all_temperatures()` - All temperatures at once
|
||||||
|
|
||||||
|
### Temperature Control
|
||||||
|
|
||||||
|
- `get_temperature_setpoint_main()` / `set_temperature_setpoint_main(temp)`
|
||||||
|
- `get_temperature_setpoint_brf()` / `set_temperature_setpoint_brf(temp)`
|
||||||
|
- `get_temperature_setpoint_shg()` / `set_temperature_setpoint_shg(temp)`
|
||||||
|
- `get_temperature_setpoint_thg()` / `set_temperature_setpoint_thg(temp)`
|
||||||
|
- `get_temperature_setpoint_eta()` / `set_temperature_setpoint_eta(temp)`
|
||||||
|
|
||||||
|
### Power Control
|
||||||
|
|
||||||
|
- `get_power_command()` / `set_power_command(power)` - Get/set power
|
||||||
|
- `get_power_memory()` - Stored power settings
|
||||||
|
- `get_power_limits()` - Power limits
|
||||||
|
|
||||||
|
### Current Control
|
||||||
|
|
||||||
|
- `get_current_command()` / `set_current_command(current)` - Get/set current
|
||||||
|
- `get_current_limits()` - Current limits
|
||||||
|
- `get_control_mode()` / `set_control_mode(mode)` - Control mode (POWER/CURRENT)
|
||||||
|
|
||||||
|
### Status Monitoring
|
||||||
|
|
||||||
|
- `get_key_switch_status()` - Key switch status
|
||||||
|
- `get_fan_status()` / `set_fan_control(value)` - Fan control
|
||||||
|
- `get_interlock_status()` - Interlock status
|
||||||
|
- `get_remote_control_status()` - Remote control status
|
||||||
|
- `get_analog_values()` - Analog sensor values
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
- `get_config_register_0()` / `set_config_register_0(value)`
|
||||||
|
- `get_config_register_1()` / `set_config_register_1(value)`
|
||||||
|
- `get_config_register_2()` / `set_config_register_2(value)`
|
||||||
|
- `get_config_register_3()` / `set_config_register_3(value)`
|
||||||
|
|
||||||
|
See the [API documentation](LASER_I2C_PROTOCOL.md) for complete command reference.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Run the example script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Simulation mode (no hardware)
|
||||||
|
python3 example_usage.py 1
|
||||||
|
|
||||||
|
# Real hardware mode
|
||||||
|
python3 example_usage.py 2
|
||||||
|
|
||||||
|
# Continuous monitoring
|
||||||
|
python3 example_usage.py 3
|
||||||
|
```
|
||||||
|
|
||||||
|
Or run the built-in test:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 coherent_hops_laser.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Continuous Monitoring Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
from coherent_hops_laser import CoherentHOPSLaser
|
||||||
|
import time
|
||||||
|
|
||||||
|
with CoherentHOPSLaser() as laser:
|
||||||
|
laser.connect()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
temps = laser.get_all_temperatures()
|
||||||
|
power = laser.get_power_command()
|
||||||
|
|
||||||
|
print(f"Main: {temps.main:.1f}°C Power: {power:.1f}mW")
|
||||||
|
time.sleep(1)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Cannot find FTDI device
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if device is connected
|
||||||
|
lsusb | grep FTDI
|
||||||
|
|
||||||
|
# Should show something like:
|
||||||
|
# Bus 001 Device 005: ID 0403:6010 Future Technology Devices International, Ltd FT2232C
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permission denied
|
||||||
|
|
||||||
|
Add your user to the dialout/plugdev group:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo usermod -a -G dialout $USER
|
||||||
|
sudo usermod -a -G plugdev $USER
|
||||||
|
```
|
||||||
|
|
||||||
|
Then log out and back in.
|
||||||
|
|
||||||
|
### I2C communication errors
|
||||||
|
|
||||||
|
- Verify correct slave address (default: 0x50)
|
||||||
|
- Check I2C speed (try I2CMode.STANDARD instead of FAST)
|
||||||
|
- Verify physical connections
|
||||||
|
- Check for other devices on the bus
|
||||||
|
|
||||||
|
### Import errors
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install pyftdi
|
||||||
|
pip install pyftdi
|
||||||
|
|
||||||
|
# If that fails, try:
|
||||||
|
pip install --user pyftdi
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- **CoherentHOPSLaser**: Main class for real hardware control
|
||||||
|
- **DummyLaser**: Simulator for development without hardware
|
||||||
|
- **I2CController**: Low-level FTDI I2C communication (from pyftdi)
|
||||||
|
- **LaserInfo / TemperatureStatus**: Data classes for structured responses
|
||||||
|
|
||||||
|
## Safety Notes
|
||||||
|
|
||||||
|
- Always verify power levels before enabling laser output
|
||||||
|
- Monitor temperatures during operation
|
||||||
|
- Check interlock status before operation
|
||||||
|
- Use appropriate laser safety equipment
|
||||||
|
- Follow all manufacturer safety guidelines
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This implementation is based on the Coherent HOPS Demo v2.0.7 protocol documentation.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- FTDI MPSSE Documentation: https://ftdichip.com/software-examples/mpsse-projects/
|
||||||
|
- PyFTDI: https://github.com/eblot/pyftdi
|
||||||
|
- Original Protocol Documentation: [LASER_I2C_PROTOCOL.md](LASER_I2C_PROTOCOL.md)
|
||||||
@@ -0,0 +1,773 @@
|
|||||||
|
"""
|
||||||
|
Coherent HOPS Laser I2C Control Library
|
||||||
|
|
||||||
|
This module provides a Python interface to control Coherent HOPS laser systems
|
||||||
|
via I2C protocol through an FTDI FT2232C USB interface.
|
||||||
|
|
||||||
|
Dependencies:
|
||||||
|
pip install pyftdi
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from coherent_hops_laser import CoherentHOPSLaser
|
||||||
|
|
||||||
|
laser = CoherentHOPSLaser()
|
||||||
|
laser.connect()
|
||||||
|
|
||||||
|
# Query system information
|
||||||
|
model = laser.get_laser_model()
|
||||||
|
wavelength = laser.get_wavelength()
|
||||||
|
|
||||||
|
# Monitor temperatures
|
||||||
|
main_temp = laser.get_temperature_main()
|
||||||
|
|
||||||
|
# Control power
|
||||||
|
laser.set_power_command(100.0) # Set power in mW or W
|
||||||
|
|
||||||
|
laser.disconnect()
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional, Union, List
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pyftdi.i2c import I2cController, I2cNackError
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError(
|
||||||
|
"pyftdi library is required. Install with: pip install pyftdi"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class I2CMode(Enum):
|
||||||
|
"""I2C communication modes"""
|
||||||
|
STANDARD = 100000 # 100 kHz
|
||||||
|
FAST = 400000 # 400 kHz
|
||||||
|
|
||||||
|
|
||||||
|
class ControlMode(Enum):
|
||||||
|
"""Laser control modes"""
|
||||||
|
POWER = "POWER"
|
||||||
|
CURRENT = "CURRENT"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LaserInfo:
|
||||||
|
"""Laser system information"""
|
||||||
|
hardware_id: Optional[str] = None
|
||||||
|
head_type: Optional[str] = None
|
||||||
|
head_board_revision: Optional[str] = None
|
||||||
|
laser_model: Optional[str] = None
|
||||||
|
power_units: Optional[str] = None
|
||||||
|
wavelength: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TemperatureStatus:
|
||||||
|
"""Temperature monitoring data"""
|
||||||
|
main: Optional[float] = None
|
||||||
|
brf: Optional[float] = None
|
||||||
|
shg: Optional[float] = None
|
||||||
|
thg: Optional[float] = None
|
||||||
|
eta: Optional[float] = None
|
||||||
|
|
||||||
|
|
||||||
|
class CoherentHOPSLaser:
|
||||||
|
"""
|
||||||
|
Main interface class for Coherent HOPS laser control via I2C.
|
||||||
|
|
||||||
|
This class provides high-level methods for all documented laser commands
|
||||||
|
including system queries, temperature control, power/current management,
|
||||||
|
and digital I/O operations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Default I2C slave address for NXP microcontroller
|
||||||
|
DEFAULT_SLAVE_ADDRESS = 0x50
|
||||||
|
|
||||||
|
# FTDI USB VID/PID for FT2232C
|
||||||
|
FTDI_VID = 0x0403
|
||||||
|
FTDI_PID = 0x6010
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
slave_address: int = DEFAULT_SLAVE_ADDRESS,
|
||||||
|
i2c_mode: I2CMode = I2CMode.STANDARD,
|
||||||
|
timeout: float = 1.0
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Initialize the laser controller.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
slave_address: I2C slave address of the NXP microcontroller
|
||||||
|
i2c_mode: I2C communication speed mode
|
||||||
|
timeout: Command timeout in seconds
|
||||||
|
"""
|
||||||
|
self.slave_address = slave_address
|
||||||
|
self.i2c_mode = i2c_mode
|
||||||
|
self.timeout = timeout
|
||||||
|
|
||||||
|
self._i2c_controller = I2cController()
|
||||||
|
self._i2c_slave = None
|
||||||
|
self._connected = False
|
||||||
|
|
||||||
|
def connect(self, url: str = 'ftdi://ftdi:2232/1') -> None:
|
||||||
|
"""
|
||||||
|
Connect to the FTDI I2C device.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: FTDI device URL (default: first FT2232C device, channel 1)
|
||||||
|
Examples:
|
||||||
|
- 'ftdi://ftdi:2232/1' - First FT2232 device, channel 1
|
||||||
|
- 'ftdi://ftdi:2232:SERIAL/1' - Device with specific serial number
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
IOError: If connection fails
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Configure I2C controller
|
||||||
|
self._i2c_controller.configure(url, frequency=self.i2c_mode.value)
|
||||||
|
|
||||||
|
# Get I2C slave interface
|
||||||
|
self._i2c_slave = self._i2c_controller.get_port(self.slave_address)
|
||||||
|
|
||||||
|
self._connected = True
|
||||||
|
logger.info(f"Connected to laser at I2C address 0x{self.slave_address:02X}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to connect to I2C device: {e}")
|
||||||
|
raise IOError(f"I2C connection failed: {e}")
|
||||||
|
|
||||||
|
def disconnect(self) -> None:
|
||||||
|
"""Disconnect from the I2C device."""
|
||||||
|
if self._connected:
|
||||||
|
self._i2c_controller.terminate()
|
||||||
|
self._connected = False
|
||||||
|
logger.info("Disconnected from laser")
|
||||||
|
|
||||||
|
def _ensure_connected(self) -> None:
|
||||||
|
"""Verify device is connected before operations."""
|
||||||
|
if not self._connected:
|
||||||
|
raise RuntimeError("Not connected. Call connect() first.")
|
||||||
|
|
||||||
|
def _send_command(self, command: str, value: Optional[str] = None) -> str:
|
||||||
|
"""
|
||||||
|
Send a command to the laser and read response.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command: Command string (e.g., 'HID', 'LASERMODEL')
|
||||||
|
value: Optional value for set commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response string from the laser
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
I2cNackError: If I2C communication fails
|
||||||
|
TimeoutError: If response timeout occurs
|
||||||
|
"""
|
||||||
|
self._ensure_connected()
|
||||||
|
|
||||||
|
# Format command: query = ?COMMAND, set = COMMAND=VALUE
|
||||||
|
if value is not None:
|
||||||
|
cmd_str = f"{command}={value}"
|
||||||
|
else:
|
||||||
|
cmd_str = f"?{command}"
|
||||||
|
|
||||||
|
cmd_bytes = cmd_str.encode('ascii')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Write command
|
||||||
|
self._i2c_slave.write(cmd_bytes)
|
||||||
|
|
||||||
|
# Small delay for laser to process
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
# Read response (max 256 bytes)
|
||||||
|
response = self._i2c_slave.read(256)
|
||||||
|
|
||||||
|
# Decode and strip null bytes and whitespace
|
||||||
|
result = response.decode('ascii', errors='ignore').rstrip('\x00').strip()
|
||||||
|
|
||||||
|
logger.debug(f"Command: {cmd_str} -> Response: {result}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
except I2cNackError as e:
|
||||||
|
logger.error(f"I2C NACK error for command {cmd_str}: {e}")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Communication error for command {cmd_str}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# System Information Commands
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
def get_hardware_id(self) -> str:
|
||||||
|
"""Query Hardware ID."""
|
||||||
|
return self._send_command('HID')
|
||||||
|
|
||||||
|
def get_head_type(self) -> str:
|
||||||
|
"""Query Head Type."""
|
||||||
|
return self._send_command('HTYPE')
|
||||||
|
|
||||||
|
def get_head_board_revision(self) -> str:
|
||||||
|
"""Query Head Board Revision."""
|
||||||
|
return self._send_command('HBDREV')
|
||||||
|
|
||||||
|
def get_head_digital_io(self) -> str:
|
||||||
|
"""Query Head Digital I/O configuration."""
|
||||||
|
return self._send_command('HEADDIO')
|
||||||
|
|
||||||
|
def get_laser_model(self) -> str:
|
||||||
|
"""
|
||||||
|
Query Laser Model.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Model name (e.g., 'G532', 'Tina', 'Mini00', 'MiniX')
|
||||||
|
"""
|
||||||
|
return self._send_command('LASERMODEL')
|
||||||
|
|
||||||
|
def get_power_units(self) -> str:
|
||||||
|
"""
|
||||||
|
Query Power Units.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Power units (e.g., 'mW', 'W')
|
||||||
|
"""
|
||||||
|
return self._send_command('POWERUNITS')
|
||||||
|
|
||||||
|
def get_wavelength(self) -> str:
|
||||||
|
"""
|
||||||
|
Query Wavelength.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Wavelength (e.g., '532nm')
|
||||||
|
"""
|
||||||
|
return self._send_command('WAVELENGTH')
|
||||||
|
|
||||||
|
def get_system_info(self) -> LaserInfo:
|
||||||
|
"""
|
||||||
|
Query all system information.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
LaserInfo dataclass with all system parameters
|
||||||
|
"""
|
||||||
|
return LaserInfo(
|
||||||
|
hardware_id=self.get_hardware_id(),
|
||||||
|
head_type=self.get_head_type(),
|
||||||
|
head_board_revision=self.get_head_board_revision(),
|
||||||
|
laser_model=self.get_laser_model(),
|
||||||
|
power_units=self.get_power_units(),
|
||||||
|
wavelength=self.get_wavelength()
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# Temperature Monitoring
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
def get_temperature_main(self) -> float:
|
||||||
|
"""
|
||||||
|
Get Main Heatsink Temperature.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Temperature in degrees Celsius
|
||||||
|
"""
|
||||||
|
response = self._send_command('TMAIN')
|
||||||
|
return float(response)
|
||||||
|
|
||||||
|
def get_temperature_brf(self) -> float:
|
||||||
|
"""
|
||||||
|
Get BRF (Birefringent Filter) Temperature.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Temperature in degrees Celsius
|
||||||
|
"""
|
||||||
|
response = self._send_command('TBRF')
|
||||||
|
return float(response)
|
||||||
|
|
||||||
|
def get_temperature_shg(self) -> float:
|
||||||
|
"""
|
||||||
|
Get SHG (Second Harmonic Generator) Temperature.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Temperature in degrees Celsius
|
||||||
|
"""
|
||||||
|
response = self._send_command('TSHG')
|
||||||
|
return float(response)
|
||||||
|
|
||||||
|
def get_temperature_thg(self) -> float:
|
||||||
|
"""
|
||||||
|
Get THG (Third Harmonic Generator) Temperature.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Temperature in degrees Celsius
|
||||||
|
"""
|
||||||
|
response = self._send_command('TTHG')
|
||||||
|
return float(response)
|
||||||
|
|
||||||
|
def get_temperature_eta(self) -> float:
|
||||||
|
"""
|
||||||
|
Get ETA Temperature.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Temperature in degrees Celsius
|
||||||
|
"""
|
||||||
|
response = self._send_command('TETA')
|
||||||
|
return float(response)
|
||||||
|
|
||||||
|
def get_all_temperatures(self) -> TemperatureStatus:
|
||||||
|
"""
|
||||||
|
Query all temperature sensors.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TemperatureStatus dataclass with all temperature readings
|
||||||
|
"""
|
||||||
|
return TemperatureStatus(
|
||||||
|
main=self.get_temperature_main(),
|
||||||
|
brf=self.get_temperature_brf(),
|
||||||
|
shg=self.get_temperature_shg(),
|
||||||
|
thg=self.get_temperature_thg(),
|
||||||
|
eta=self.get_temperature_eta()
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# Temperature Control (Setpoints)
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
def get_temperature_setpoint_main(self) -> float:
|
||||||
|
"""Get Main Temperature Setpoint."""
|
||||||
|
response = self._send_command('TMAINCMD')
|
||||||
|
return float(response)
|
||||||
|
|
||||||
|
def set_temperature_setpoint_main(self, temperature: float) -> None:
|
||||||
|
"""Set Main Temperature Setpoint."""
|
||||||
|
self._send_command('TMAINCMD', str(temperature))
|
||||||
|
|
||||||
|
def get_temperature_setpoint_brf(self) -> float:
|
||||||
|
"""Get BRF Temperature Setpoint."""
|
||||||
|
response = self._send_command('TBRFCMD')
|
||||||
|
return float(response)
|
||||||
|
|
||||||
|
def set_temperature_setpoint_brf(self, temperature: float) -> None:
|
||||||
|
"""Set BRF Temperature Setpoint."""
|
||||||
|
self._send_command('TBRFCMD', str(temperature))
|
||||||
|
|
||||||
|
def get_temperature_setpoint_shg(self) -> float:
|
||||||
|
"""Get SHG Temperature Setpoint."""
|
||||||
|
response = self._send_command('TSHGCMD')
|
||||||
|
return float(response)
|
||||||
|
|
||||||
|
def set_temperature_setpoint_shg(self, temperature: float) -> None:
|
||||||
|
"""Set SHG Temperature Setpoint."""
|
||||||
|
self._send_command('TSHGCMD', str(temperature))
|
||||||
|
|
||||||
|
def get_temperature_setpoint_thg(self) -> float:
|
||||||
|
"""Get THG Temperature Setpoint."""
|
||||||
|
response = self._send_command('TTHGCMD')
|
||||||
|
return float(response)
|
||||||
|
|
||||||
|
def set_temperature_setpoint_thg(self, temperature: float) -> None:
|
||||||
|
"""Set THG Temperature Setpoint."""
|
||||||
|
self._send_command('TTHGCMD', str(temperature))
|
||||||
|
|
||||||
|
def get_temperature_setpoint_eta(self) -> float:
|
||||||
|
"""Get ETA Temperature Setpoint."""
|
||||||
|
response = self._send_command('TETACMD')
|
||||||
|
return float(response)
|
||||||
|
|
||||||
|
def set_temperature_setpoint_eta(self, temperature: float) -> None:
|
||||||
|
"""Set ETA Temperature Setpoint."""
|
||||||
|
self._send_command('TETACMD', str(temperature))
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# Temperature Data
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
def get_temperature_data_main(self) -> str:
|
||||||
|
"""Get Main Temperature Data."""
|
||||||
|
return self._send_command('MAIND')
|
||||||
|
|
||||||
|
def get_temperature_data_brf(self) -> str:
|
||||||
|
"""Get BRF Temperature Data."""
|
||||||
|
return self._send_command('BRFD')
|
||||||
|
|
||||||
|
def get_temperature_data_shg(self) -> str:
|
||||||
|
"""Get SHG Temperature Data."""
|
||||||
|
return self._send_command('SHGD')
|
||||||
|
|
||||||
|
def get_temperature_data_thg(self) -> str:
|
||||||
|
"""Get THG Temperature Data."""
|
||||||
|
return self._send_command('THGD')
|
||||||
|
|
||||||
|
def get_temperature_data_eta(self) -> str:
|
||||||
|
"""Get ETA Temperature Data."""
|
||||||
|
return self._send_command('ETAD')
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# Power Control
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
def get_power_command(self) -> float:
|
||||||
|
"""
|
||||||
|
Get Power Command value.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Power value in units from get_power_units()
|
||||||
|
"""
|
||||||
|
response = self._send_command('PCMD')
|
||||||
|
return float(response)
|
||||||
|
|
||||||
|
def set_power_command(self, power: float) -> None:
|
||||||
|
"""
|
||||||
|
Set Power Command value.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
power: Power value in units from get_power_units()
|
||||||
|
"""
|
||||||
|
self._send_command('PCMD', str(power))
|
||||||
|
|
||||||
|
def get_power_memory(self) -> str:
|
||||||
|
"""Query Power Memory (stored settings)."""
|
||||||
|
return self._send_command('PMEM')
|
||||||
|
|
||||||
|
def get_power_limits(self) -> str:
|
||||||
|
"""Query Power Limits."""
|
||||||
|
return self._send_command('PLIM')
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# Current Control
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
def get_current_command(self) -> float:
|
||||||
|
"""
|
||||||
|
Get Current Command value.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Current value in Amperes
|
||||||
|
"""
|
||||||
|
response = self._send_command('CCMD')
|
||||||
|
return float(response)
|
||||||
|
|
||||||
|
def set_current_command(self, current: float) -> None:
|
||||||
|
"""
|
||||||
|
Set Current Command value.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current: Current value in Amperes
|
||||||
|
"""
|
||||||
|
self._send_command('CCMD', str(current))
|
||||||
|
|
||||||
|
def get_current_limits(self) -> str:
|
||||||
|
"""Query Current Limits."""
|
||||||
|
return self._send_command('CLIM')
|
||||||
|
|
||||||
|
def get_control_mode(self) -> str:
|
||||||
|
"""
|
||||||
|
Get Control Mode.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Control mode (e.g., 'POWER' or 'CURRENT')
|
||||||
|
"""
|
||||||
|
return self._send_command('CMODE')
|
||||||
|
|
||||||
|
def set_control_mode(self, mode: Union[str, ControlMode]) -> None:
|
||||||
|
"""
|
||||||
|
Set Control Mode.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mode: Control mode ('POWER' or 'CURRENT', or ControlMode enum)
|
||||||
|
"""
|
||||||
|
if isinstance(mode, ControlMode):
|
||||||
|
mode = mode.value
|
||||||
|
self._send_command('CMODE', mode)
|
||||||
|
|
||||||
|
def get_control_mode_command(self) -> str:
|
||||||
|
"""Get Control Mode Command."""
|
||||||
|
return self._send_command('CMODECMD')
|
||||||
|
|
||||||
|
def set_control_mode_command(self, mode: str) -> None:
|
||||||
|
"""Set Control Mode Command."""
|
||||||
|
self._send_command('CMODECMD', mode)
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# Digital I/O
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
def get_ps_digital_io(self) -> str:
|
||||||
|
"""Get Power Supply Digital I/O status."""
|
||||||
|
return self._send_command('PSDIO')
|
||||||
|
|
||||||
|
def get_ps_glue_input(self) -> str:
|
||||||
|
"""Get Power Supply Glue Logic Input status."""
|
||||||
|
return self._send_command('PSGLUEIN')
|
||||||
|
|
||||||
|
def get_ps_glue_output(self) -> str:
|
||||||
|
"""Get Power Supply Glue Logic Output status."""
|
||||||
|
return self._send_command('PSGLUEOUT')
|
||||||
|
|
||||||
|
def set_ps_glue_output(self, value: str) -> None:
|
||||||
|
"""Set Power Supply Glue Logic Output."""
|
||||||
|
self._send_command('PSGLUEOUT', value)
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# Monitoring & Status
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
def get_analog_values(self) -> str:
|
||||||
|
"""Query Analog Values."""
|
||||||
|
return self._send_command('ANA')
|
||||||
|
|
||||||
|
def get_analog_command(self) -> str:
|
||||||
|
"""Get Analog Command."""
|
||||||
|
return self._send_command('ANACMD')
|
||||||
|
|
||||||
|
def set_analog_command(self, value: str) -> None:
|
||||||
|
"""Set Analog Command."""
|
||||||
|
self._send_command('ANACMD', value)
|
||||||
|
|
||||||
|
def get_key_switch_status(self) -> str:
|
||||||
|
"""Get Key Switch Status."""
|
||||||
|
return self._send_command('KSW')
|
||||||
|
|
||||||
|
def get_key_switch_command(self) -> str:
|
||||||
|
"""Get Key Switch Command."""
|
||||||
|
return self._send_command('KSWCMD')
|
||||||
|
|
||||||
|
def set_key_switch_command(self, value: str) -> None:
|
||||||
|
"""Set Key Switch Command."""
|
||||||
|
self._send_command('KSWCMD', value)
|
||||||
|
|
||||||
|
def get_fan_status(self) -> str:
|
||||||
|
"""Get Fan Status/Control."""
|
||||||
|
return self._send_command('FAN')
|
||||||
|
|
||||||
|
def set_fan_control(self, value: str) -> None:
|
||||||
|
"""Set Fan Control."""
|
||||||
|
self._send_command('FAN', value)
|
||||||
|
|
||||||
|
def get_interlock_status(self) -> str:
|
||||||
|
"""Get Interlock Status."""
|
||||||
|
return self._send_command('INT')
|
||||||
|
|
||||||
|
def get_remote_control_status(self) -> str:
|
||||||
|
"""Get Remote Control Status."""
|
||||||
|
return self._send_command('REM')
|
||||||
|
|
||||||
|
def get_eeprom_header(self) -> str:
|
||||||
|
"""Get EEPROM Header."""
|
||||||
|
return self._send_command('EEH')
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# Configuration Registers
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
def get_config_register_0(self) -> str:
|
||||||
|
"""Get Configuration Register 0."""
|
||||||
|
return self._send_command('CFG0')
|
||||||
|
|
||||||
|
def set_config_register_0(self, value: str) -> None:
|
||||||
|
"""Set Configuration Register 0."""
|
||||||
|
self._send_command('CFG0', value)
|
||||||
|
|
||||||
|
def get_config_register_1(self) -> str:
|
||||||
|
"""Get Configuration Register 1."""
|
||||||
|
return self._send_command('CFG1')
|
||||||
|
|
||||||
|
def set_config_register_1(self, value: str) -> None:
|
||||||
|
"""Set Configuration Register 1."""
|
||||||
|
self._send_command('CFG1', value)
|
||||||
|
|
||||||
|
def get_config_register_2(self) -> str:
|
||||||
|
"""Get Configuration Register 2."""
|
||||||
|
return self._send_command('CFG2')
|
||||||
|
|
||||||
|
def set_config_register_2(self, value: str) -> None:
|
||||||
|
"""Set Configuration Register 2."""
|
||||||
|
self._send_command('CFG2', value)
|
||||||
|
|
||||||
|
def get_config_register_3(self) -> str:
|
||||||
|
"""Get Configuration Register 3."""
|
||||||
|
return self._send_command('CFG3')
|
||||||
|
|
||||||
|
def set_config_register_3(self, value: str) -> None:
|
||||||
|
"""Set Configuration Register 3."""
|
||||||
|
self._send_command('CFG3', value)
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# Context Manager Support
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
"""Context manager entry."""
|
||||||
|
if not self._connected:
|
||||||
|
self.connect()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
"""Context manager exit."""
|
||||||
|
self.disconnect()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class DummyLaser(CoherentHOPSLaser):
|
||||||
|
"""
|
||||||
|
Simulated laser for testing without hardware.
|
||||||
|
|
||||||
|
This class provides dummy responses for all commands to enable
|
||||||
|
software development and testing without physical hardware.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize dummy laser (no I2C connection needed)."""
|
||||||
|
super().__init__()
|
||||||
|
self._connected = True # Simulate connection
|
||||||
|
|
||||||
|
# Simulated state
|
||||||
|
self._power_cmd = 50.0
|
||||||
|
self._current_cmd = 1.5
|
||||||
|
self._control_mode = 'POWER'
|
||||||
|
self._temps = {
|
||||||
|
'main': 25.0,
|
||||||
|
'brf': 30.0,
|
||||||
|
'shg': 35.0,
|
||||||
|
'thg': 32.0,
|
||||||
|
'eta': 28.0
|
||||||
|
}
|
||||||
|
self._temp_setpoints = {
|
||||||
|
'main': 25.0,
|
||||||
|
'brf': 30.0,
|
||||||
|
'shg': 35.0,
|
||||||
|
'thg': 32.0,
|
||||||
|
'eta': 28.0
|
||||||
|
}
|
||||||
|
|
||||||
|
def connect(self, url: str = 'dummy') -> None:
|
||||||
|
"""Dummy connection (always succeeds)."""
|
||||||
|
self._connected = True
|
||||||
|
logger.info("Connected to DummyLaser (simulation mode)")
|
||||||
|
|
||||||
|
def _send_command(self, command: str, value: Optional[str] = None) -> str:
|
||||||
|
"""Simulate command responses."""
|
||||||
|
logger.debug(f"DummyLaser command: {command}, value: {value}")
|
||||||
|
|
||||||
|
# Handle set commands
|
||||||
|
if value is not None:
|
||||||
|
if command == 'PCMD':
|
||||||
|
self._power_cmd = float(value)
|
||||||
|
return value
|
||||||
|
elif command == 'CCMD':
|
||||||
|
self._current_cmd = float(value)
|
||||||
|
return value
|
||||||
|
elif command == 'CMODE':
|
||||||
|
self._control_mode = value
|
||||||
|
return value
|
||||||
|
elif 'CMD' in command and 'T' in command:
|
||||||
|
# Temperature setpoint
|
||||||
|
key = command.replace('CMD', '').replace('T', '').lower()
|
||||||
|
if key in self._temp_setpoints:
|
||||||
|
self._temp_setpoints[key] = float(value)
|
||||||
|
return value
|
||||||
|
return 'OK'
|
||||||
|
|
||||||
|
# Handle query commands
|
||||||
|
responses = {
|
||||||
|
'HID': 'HOPS-12345',
|
||||||
|
'HTYPE': 'Standard',
|
||||||
|
'HBDREV': 'Rev 2.1',
|
||||||
|
'HEADDIO': '0xFF',
|
||||||
|
'LASERMODEL': 'G532',
|
||||||
|
'POWERUNITS': 'mW',
|
||||||
|
'WAVELENGTH': '532nm',
|
||||||
|
'TMAIN': str(self._temps['main']),
|
||||||
|
'TBRF': str(self._temps['brf']),
|
||||||
|
'TSHG': str(self._temps['shg']),
|
||||||
|
'TTHG': str(self._temps['thg']),
|
||||||
|
'TETA': str(self._temps['eta']),
|
||||||
|
'TMAINCMD': str(self._temp_setpoints['main']),
|
||||||
|
'TBRFCMD': str(self._temp_setpoints['brf']),
|
||||||
|
'TSHGCMD': str(self._temp_setpoints['shg']),
|
||||||
|
'TTHGCMD': str(self._temp_setpoints['thg']),
|
||||||
|
'TETACMD': str(self._temp_setpoints['eta']),
|
||||||
|
'MAIND': 'MainTempData',
|
||||||
|
'BRFD': 'BRFTempData',
|
||||||
|
'SHGD': 'SHGTempData',
|
||||||
|
'THGD': 'THGTempData',
|
||||||
|
'ETAD': 'ETATempData',
|
||||||
|
'PCMD': str(self._power_cmd),
|
||||||
|
'PMEM': '100',
|
||||||
|
'PLIM': '0-200',
|
||||||
|
'CCMD': str(self._current_cmd),
|
||||||
|
'CLIM': '0-5',
|
||||||
|
'CMODE': self._control_mode,
|
||||||
|
'CMODECMD': self._control_mode,
|
||||||
|
'PSDIO': '0x00',
|
||||||
|
'PSGLUEIN': '0x00',
|
||||||
|
'PSGLUEOUT': '0x00',
|
||||||
|
'ANA': '0,0,0,0',
|
||||||
|
'ANACMD': '0',
|
||||||
|
'KSW': 'ON',
|
||||||
|
'KSWCMD': 'ON',
|
||||||
|
'FAN': 'AUTO',
|
||||||
|
'INT': 'OK',
|
||||||
|
'REM': 'ENABLED',
|
||||||
|
'EEH': 'EEPROM_V1',
|
||||||
|
'CFG0': '0x00',
|
||||||
|
'CFG1': '0x00',
|
||||||
|
'CFG2': '0x00',
|
||||||
|
'CFG3': '0x00',
|
||||||
|
}
|
||||||
|
|
||||||
|
return responses.get(command, 'UNKNOWN')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
"""Example usage and testing."""
|
||||||
|
|
||||||
|
# Test with dummy laser
|
||||||
|
print("=== Testing with DummyLaser ===\n")
|
||||||
|
|
||||||
|
with DummyLaser() as laser:
|
||||||
|
# System info
|
||||||
|
print("System Information:")
|
||||||
|
info = laser.get_system_info()
|
||||||
|
print(f" Model: {info.laser_model}")
|
||||||
|
print(f" Wavelength: {info.wavelength}")
|
||||||
|
print(f" Power Units: {info.power_units}")
|
||||||
|
print(f" Hardware ID: {info.hardware_id}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Temperature monitoring
|
||||||
|
print("Temperature Status:")
|
||||||
|
temps = laser.get_all_temperatures()
|
||||||
|
print(f" Main: {temps.main}°C")
|
||||||
|
print(f" BRF: {temps.brf}°C")
|
||||||
|
print(f" SHG: {temps.shg}°C")
|
||||||
|
print(f" THG: {temps.thg}°C")
|
||||||
|
print(f" ETA: {temps.eta}°C")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Power control
|
||||||
|
print("Power Control:")
|
||||||
|
current_power = laser.get_power_command()
|
||||||
|
print(f" Current Power: {current_power} {info.power_units}")
|
||||||
|
laser.set_power_command(75.0)
|
||||||
|
new_power = laser.get_power_command()
|
||||||
|
print(f" New Power: {new_power} {info.power_units}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Control mode
|
||||||
|
print("Control Mode:")
|
||||||
|
mode = laser.get_control_mode()
|
||||||
|
print(f" Current Mode: {mode}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("\n=== For real hardware, use: ===")
|
||||||
|
print("laser = CoherentHOPSLaser()")
|
||||||
|
print("laser.connect('ftdi://ftdi:2232/1')")
|
||||||
|
print("# ... perform operations ...")
|
||||||
|
print("laser.disconnect()")
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Example usage of the Coherent HOPS Laser control library.
|
||||||
|
|
||||||
|
This script demonstrates how to use the library to control a real laser system.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from coherent_hops_laser import CoherentHOPSLaser, DummyLaser, I2CMode
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def demo_system_info(laser):
|
||||||
|
"""Demonstrate system information queries."""
|
||||||
|
print("=" * 60)
|
||||||
|
print("SYSTEM INFORMATION")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
info = laser.get_system_info()
|
||||||
|
print(f"Hardware ID: {info.hardware_id}")
|
||||||
|
print(f"Laser Model: {info.laser_model}")
|
||||||
|
print(f"Wavelength: {info.wavelength}")
|
||||||
|
print(f"Power Units: {info.power_units}")
|
||||||
|
print(f"Head Type: {info.head_type}")
|
||||||
|
print(f"Board Revision: {info.head_board_revision}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def demo_temperature_monitoring(laser):
|
||||||
|
"""Demonstrate temperature monitoring."""
|
||||||
|
print("=" * 60)
|
||||||
|
print("TEMPERATURE MONITORING")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
temps = laser.get_all_temperatures()
|
||||||
|
print(f"Main Heatsink: {temps.main:.2f}°C")
|
||||||
|
print(f"BRF (Birefringent): {temps.brf:.2f}°C")
|
||||||
|
print(f"SHG (2nd Harmonic): {temps.shg:.2f}°C")
|
||||||
|
print(f"THG (3rd Harmonic): {temps.thg:.2f}°C")
|
||||||
|
print(f"ETA: {temps.eta:.2f}°C")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def demo_temperature_control(laser):
|
||||||
|
"""Demonstrate temperature setpoint control."""
|
||||||
|
print("=" * 60)
|
||||||
|
print("TEMPERATURE CONTROL")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Read current setpoints
|
||||||
|
print("Current Setpoints:")
|
||||||
|
print(f" Main: {laser.get_temperature_setpoint_main():.2f}°C")
|
||||||
|
print(f" BRF: {laser.get_temperature_setpoint_brf():.2f}°C")
|
||||||
|
print(f" SHG: {laser.get_temperature_setpoint_shg():.2f}°C")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Example: Set a new setpoint (commented out for safety)
|
||||||
|
# print("Setting Main temperature setpoint to 26.0°C...")
|
||||||
|
# laser.set_temperature_setpoint_main(26.0)
|
||||||
|
# print(f" New setpoint: {laser.get_temperature_setpoint_main():.2f}°C")
|
||||||
|
print("(Temperature setpoint modification disabled in demo)")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def demo_power_control(laser):
|
||||||
|
"""Demonstrate power control."""
|
||||||
|
print("=" * 60)
|
||||||
|
print("POWER CONTROL")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
units = laser.get_power_units()
|
||||||
|
current_power = laser.get_power_command()
|
||||||
|
print(f"Current Power: {current_power} {units}")
|
||||||
|
|
||||||
|
power_limits = laser.get_power_limits()
|
||||||
|
print(f"Power Limits: {power_limits}")
|
||||||
|
|
||||||
|
power_memory = laser.get_power_memory()
|
||||||
|
print(f"Power Memory: {power_memory}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Example: Set power (commented out for safety)
|
||||||
|
# print("Setting power to 100.0 mW...")
|
||||||
|
# laser.set_power_command(100.0)
|
||||||
|
# print(f" New power: {laser.get_power_command()} {units}")
|
||||||
|
print("(Power modification disabled in demo)")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def demo_current_control(laser):
|
||||||
|
"""Demonstrate current control."""
|
||||||
|
print("=" * 60)
|
||||||
|
print("CURRENT CONTROL")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
current = laser.get_current_command()
|
||||||
|
print(f"Current Command: {current} A")
|
||||||
|
|
||||||
|
limits = laser.get_current_limits()
|
||||||
|
print(f"Current Limits: {limits}")
|
||||||
|
|
||||||
|
mode = laser.get_control_mode()
|
||||||
|
print(f"Control Mode: {mode}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def demo_status_monitoring(laser):
|
||||||
|
"""Demonstrate status monitoring."""
|
||||||
|
print("=" * 60)
|
||||||
|
print("STATUS MONITORING")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
print(f"Key Switch: {laser.get_key_switch_status()}")
|
||||||
|
print(f"Fan Status: {laser.get_fan_status()}")
|
||||||
|
print(f"Interlock: {laser.get_interlock_status()}")
|
||||||
|
print(f"Remote Control: {laser.get_remote_control_status()}")
|
||||||
|
print(f"Analog Values: {laser.get_analog_values()}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def demo_configuration(laser):
|
||||||
|
"""Demonstrate configuration register access."""
|
||||||
|
print("=" * 60)
|
||||||
|
print("CONFIGURATION REGISTERS")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
print(f"Config Register 0: {laser.get_config_register_0()}")
|
||||||
|
print(f"Config Register 1: {laser.get_config_register_1()}")
|
||||||
|
print(f"Config Register 2: {laser.get_config_register_2()}")
|
||||||
|
print(f"Config Register 3: {laser.get_config_register_3()}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def main_dummy_demo():
|
||||||
|
"""Run demo with simulated hardware."""
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("COHERENT HOPS LASER CONTROL - SIMULATION MODE")
|
||||||
|
print("=" * 60 + "\n")
|
||||||
|
|
||||||
|
with DummyLaser() as laser:
|
||||||
|
demo_system_info(laser)
|
||||||
|
demo_temperature_monitoring(laser)
|
||||||
|
demo_temperature_control(laser)
|
||||||
|
demo_power_control(laser)
|
||||||
|
demo_current_control(laser)
|
||||||
|
demo_status_monitoring(laser)
|
||||||
|
demo_configuration(laser)
|
||||||
|
|
||||||
|
# Demonstrate power control
|
||||||
|
print("=" * 60)
|
||||||
|
print("POWER CONTROL DEMONSTRATION (Simulation)")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"Initial power: {laser.get_power_command()} mW")
|
||||||
|
laser.set_power_command(125.0)
|
||||||
|
print(f"After setting to 125.0 mW: {laser.get_power_command()} mW")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def main_real_hardware():
|
||||||
|
"""Run demo with real hardware."""
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("COHERENT HOPS LASER CONTROL - REAL HARDWARE")
|
||||||
|
print("=" * 60 + "\n")
|
||||||
|
|
||||||
|
# Configure for your specific setup
|
||||||
|
FTDI_URL = 'ftdi://ftdi:2232/1' # Adjust if needed
|
||||||
|
SLAVE_ADDRESS = 0x50 # Default NXP slave address
|
||||||
|
I2C_FREQUENCY = I2CMode.STANDARD # or I2CMode.FAST
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Connect to laser
|
||||||
|
laser = CoherentHOPSLaser(
|
||||||
|
slave_address=SLAVE_ADDRESS,
|
||||||
|
i2c_mode=I2C_FREQUENCY
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Connecting to FTDI device at {FTDI_URL}...")
|
||||||
|
laser.connect(FTDI_URL)
|
||||||
|
print("Connected successfully!\n")
|
||||||
|
|
||||||
|
# Run demos
|
||||||
|
demo_system_info(laser)
|
||||||
|
demo_temperature_monitoring(laser)
|
||||||
|
demo_status_monitoring(laser)
|
||||||
|
demo_power_control(laser)
|
||||||
|
demo_current_control(laser)
|
||||||
|
|
||||||
|
# Clean disconnect
|
||||||
|
laser.disconnect()
|
||||||
|
print("Disconnected successfully.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
print("\nTroubleshooting:")
|
||||||
|
print("1. Check FTDI device is connected (lsusb | grep FTDI)")
|
||||||
|
print("2. Verify user permissions (add user to 'dialout' or 'plugdev' group)")
|
||||||
|
print("3. Check FTDI URL matches your device")
|
||||||
|
print("4. Try: sudo python3 example_usage.py (not recommended long-term)")
|
||||||
|
|
||||||
|
|
||||||
|
def continuous_monitoring_example():
|
||||||
|
"""Example of continuous temperature and power monitoring."""
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("CONTINUOUS MONITORING EXAMPLE")
|
||||||
|
print("=" * 60 + "\n")
|
||||||
|
|
||||||
|
with DummyLaser() as laser:
|
||||||
|
print("Monitoring laser parameters (5 iterations)...")
|
||||||
|
print("Press Ctrl+C to stop\n")
|
||||||
|
|
||||||
|
try:
|
||||||
|
for i in range(5):
|
||||||
|
temps = laser.get_all_temperatures()
|
||||||
|
power = laser.get_power_command()
|
||||||
|
mode = laser.get_control_mode()
|
||||||
|
|
||||||
|
print(f"[{i+1}] T_main={temps.main:.1f}°C "
|
||||||
|
f"T_shg={temps.shg:.1f}°C "
|
||||||
|
f"Power={power:.1f}mW "
|
||||||
|
f"Mode={mode}")
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nMonitoring stopped.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
|
||||||
|
print("Coherent HOPS Laser Control - Example Usage\n")
|
||||||
|
print("Available demos:")
|
||||||
|
print(" 1. Simulation mode (no hardware required)")
|
||||||
|
print(" 2. Real hardware mode")
|
||||||
|
print(" 3. Continuous monitoring example")
|
||||||
|
print()
|
||||||
|
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
choice = sys.argv[1]
|
||||||
|
else:
|
||||||
|
choice = input("Select demo (1/2/3) [default: 1]: ").strip() or "1"
|
||||||
|
|
||||||
|
if choice == "1":
|
||||||
|
main_dummy_demo()
|
||||||
|
print("\nContinuous monitoring demo:")
|
||||||
|
continuous_monitoring_example()
|
||||||
|
elif choice == "2":
|
||||||
|
main_real_hardware()
|
||||||
|
elif choice == "3":
|
||||||
|
continuous_monitoring_example()
|
||||||
|
else:
|
||||||
|
print("Invalid choice. Use 1, 2, or 3.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("Demo complete!")
|
||||||
|
print("=" * 60)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pyftdi>=0.54.0
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# scanengine-3 - Unified Requirements
|
||||||
|
# Combined dependencies from nuescan, pymso, pybbd202, and pypewpewhops
|
||||||
|
|
||||||
|
# GUI Framework (from nuescan)
|
||||||
|
PyQt6>=6.4.0
|
||||||
|
|
||||||
|
# Serial Communication (from nuescan)
|
||||||
|
pyserial>=3.5
|
||||||
|
|
||||||
|
# VISA instrument control (from nuescan - for oscilloscope/pymso)
|
||||||
|
pyvisa>=1.13.0
|
||||||
|
pyvisa-py>=0.7.0
|
||||||
|
|
||||||
|
# FTDI device support (from pypewpewhops and pybbd202)
|
||||||
|
pyftdi>=0.54.0
|
||||||
|
|
||||||
|
# Development dependencies (optional)
|
||||||
|
# pytest>=7.0.0
|
||||||
|
# pytest-qt>=4.0.0
|
||||||
Reference in New Issue
Block a user