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:
Thomas Ales [M S E]
2026-01-16 20:11:31 -06:00
commit fc43fbe4b0
65 changed files with 19127 additions and 0 deletions
+315
View File
@@ -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/
+283
View File
@@ -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)
+773
View File
@@ -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()")
+256
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
pyftdi>=0.54.0