""" 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()")