fc43fbe4b0
- 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>
303 lines
10 KiB
Python
303 lines
10 KiB
Python
"""
|
|
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)
|