Files
scanengine-3/hardware/helios_laser.py
T
2026-02-09 14:40:34 -06:00

297 lines
7.8 KiB
Python

"""
Helios Laser System Driver
Basic implementation for controlling the Helios pulsed laser.
"""
import serial
import time
import logging
from typing import Optional, List
from enum import Enum
logger = logging.getLogger(__name__)
class PulseMode(Enum):
"""Helios pulse mode enumeration"""
SINGLE_PULSE = 0
CONTINUOUS_GATING = 1
CONTINUOUS_PULSING = 2
class HeliosLaser:
"""
Driver for Helios pulsed laser system.
Communication: RS-232, 9600 baud, 8N1
Commands are ASCII strings terminated with CR
"""
def __init__(self, port: str = None, timeout: float = 1.0):
"""
Initialize Helios laser driver.
Args:
port: Serial port (e.g., '/dev/ttyUSB0' or 'COM5')
timeout: Serial timeout in seconds
"""
self.port = port
self.timeout = timeout
self.serial = None
self.is_connected = False
@staticmethod
def list_available_ports() -> List[str]:
"""List available serial ports"""
import serial.tools.list_ports
ports = serial.tools.list_ports.comports()
return [port.device for port in ports]
def connect(self, port: str = None) -> bool:
"""
Connect to the Helios laser.
Args:
port: Serial port (uses stored port if None)
Returns:
True if connection successful
"""
if port:
self.port = port
if not self.port:
logger.error("No port specified")
return False
try:
self.serial = serial.Serial(
port=self.port,
baudrate=9600,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=self.timeout
)
time.sleep(0.1) # Allow time for connection to stabilize
self.is_connected = True
logger.info(f"Connected to Helios laser on {self.port}")
return True
except Exception as e:
logger.error(f"Failed to connect to Helios laser: {e}")
self.is_connected = False
return False
def disconnect(self):
"""Disconnect from the laser"""
if self.serial and self.serial.is_open:
try:
# Disable laser before disconnecting
self.set_laser_enable(False)
self.serial.close()
logger.info("Disconnected from Helios laser")
except Exception as e:
logger.error(f"Error during disconnect: {e}")
self.is_connected = False
self.serial = None
def _send_command(self, command: str) -> bool:
"""
Send a command to the laser.
Args:
command: ASCII command string (without CR)
Returns:
True if sent successfully
"""
if not self.is_connected or not self.serial:
logger.error("Not connected to laser")
return False
try:
cmd_bytes = (command + '\r').encode('ascii')
self.serial.write(cmd_bytes)
logger.debug(f"Sent command: {command}")
return True
except Exception as e:
logger.error(f"Failed to send command '{command}': {e}")
return False
def _query(self, command: str) -> Optional[str]:
"""
Send a query and read response.
Args:
command: ASCII query command (without CR)
Returns:
Response string or None if error
"""
if not self._send_command(command):
return None
try:
response = self.serial.readline().decode('ascii').strip()
logger.debug(f"Query '{command}' response: {response}")
return response
except Exception as e:
logger.error(f"Failed to read response for '{command}': {e}")
return None
def set_frequency_hz(self, frequency: int) -> bool:
"""
Set laser pulse frequency in Hz.
Args:
frequency: Frequency in Hz (16700 - 125000)
Returns:
True if successful
"""
if not (16700 <= frequency <= 125000):
logger.error(f"Frequency {frequency} Hz out of range (16700-125000)")
return False
# Convert frequency to period in nanoseconds
period_ns = int(1e9 / frequency)
command = f"FP={period_ns}"
return self._send_command(command)
def set_current_ma(self, current: int) -> bool:
"""
Set pump diode current in mA.
Args:
current: Current in mA (0 - 7000)
Returns:
True if successful
"""
if not (0 <= current <= 7000):
logger.error(f"Current {current} mA out of range (0-7000)")
return False
command = f"PC={current}"
return self._send_command(command)
def set_pulse_mode(self, mode: PulseMode) -> bool:
"""
Set pulse mode.
Args:
mode: PulseMode enumeration value
Returns:
True if successful
"""
command = f"PM={mode.value}"
return self._send_command(command)
def set_laser_enable(self, enable: bool) -> bool:
"""
Enable or disable laser emission.
Args:
enable: True to enable, False to disable
Returns:
True if successful
"""
command = f"LE={1 if enable else 0}"
success = self._send_command(command)
if success:
state = "enabled" if enable else "disabled"
logger.info(f"Laser {state}")
return success
def is_laser_enabled(self) -> bool:
"""
Check if laser is currently enabled.
Returns:
True if laser is enabled
"""
response = self._query("LE?")
if response:
try:
return int(response) == 1
except ValueError:
logger.error(f"Invalid response for LE?: {response}")
return False
def get_frequency_hz(self) -> Optional[int]:
"""
Get current laser frequency in Hz.
Returns:
Frequency in Hz or None if error
"""
response = self._query("FP?")
if response:
try:
period_ns = int(response)
return int(1e9 / period_ns)
except (ValueError, ZeroDivisionError):
logger.error(f"Invalid response for FP?: {response}")
return None
def get_current_ma(self) -> Optional[int]:
"""
Get current pump diode current in mA.
Returns:
Current in mA or None if error
"""
response = self._query("PC?")
if response:
try:
return int(response)
except ValueError:
logger.error(f"Invalid response for PC?: {response}")
return None
def get_power_mw(self) -> Optional[float]:
"""
Get laser output power in mW.
Returns:
Power in mW or None if error
"""
response = self._query("PO?")
if response:
try:
return float(response)
except ValueError:
logger.error(f"Invalid response for PO?: {response}")
return None
def get_controller_serial(self) -> Optional[str]:
"""
Get controller serial number.
Returns:
Serial number string or None if error
"""
return self._query("SN?")
def get_head_serial(self) -> Optional[str]:
"""
Get laser head serial number.
Returns:
Serial number string or None if error
"""
return self._query("HSN?")
def __del__(self):
"""Destructor - ensure cleanup"""
self.disconnect()