Files
scanengine-3/nuescan/hardware/helios_protocol.py
T
Thomas Ales [M S E] fc43fbe4b0 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>
2026-01-16 20:11:31 -06:00

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)