pre uc480 integration

This commit is contained in:
Thomas Ales [M S E]
2026-05-22 09:38:39 -05:00
parent 43e7b99512
commit a0e0151b5d
30 changed files with 12557 additions and 41 deletions
+142 -23
View File
@@ -126,17 +126,34 @@ class HeliosLaser:
Send a query and read response.
Args:
command: ASCII query command (without CR)
command: ASCII query command (without CR or ?)
Returns:
Response string or None if error
Response value string or None if error
"""
if not self._send_command(command):
return None
try:
response = self.serial.readline().decode('ascii').strip()
# Clear any pending data in the buffer
self.serial.reset_input_buffer()
time.sleep(0.05)
if not self._send_command(command):
return None
time.sleep(0.2) # Give device time to respond
response = self.serial.read_until(b'\r').decode('ascii', errors='replace').strip()
logger.debug(f"Query '{command}' response: {response}")
# Helios format: "COMMAND = VALUE UNIT"
# Extract just the value part
if '=' in response:
parts = response.split('=')
if len(parts) >= 2:
value_part = parts[1].strip()
# Remove unit suffix if present (e.g., "ns", "mA", "mW")
value = value_part.split()[0]
return value
return response
except Exception as e:
@@ -160,7 +177,12 @@ class HeliosLaser:
# Convert frequency to period in nanoseconds
period_ns = int(1e9 / frequency)
command = f"FP={period_ns}"
# Clamp to valid range (8000-60000 ns)
if not (8000 <= period_ns <= 60000):
logger.error(f"Period {period_ns} ns out of range (8000-60000)")
return False
command = f"LDF {period_ns}"
return self._send_command(command)
def set_current_ma(self, current: int) -> bool:
@@ -168,16 +190,16 @@ class HeliosLaser:
Set pump diode current in mA.
Args:
current: Current in mA (0 - 7000)
current: Current in mA (0 - 2000 for this model)
Returns:
True if successful
"""
if not (0 <= current <= 7000):
logger.error(f"Current {current} mA out of range (0-7000)")
if not (0 <= current <= 2000):
logger.error(f"Current {current} mA out of range (0-2000)")
return False
command = f"PC={current}"
command = f"LDS {current}"
return self._send_command(command)
def set_pulse_mode(self, mode: PulseMode) -> bool:
@@ -190,7 +212,7 @@ class HeliosLaser:
Returns:
True if successful
"""
command = f"PM={mode.value}"
command = f"LDG {mode.value}"
return self._send_command(command)
def set_laser_enable(self, enable: bool) -> bool:
@@ -203,7 +225,7 @@ class HeliosLaser:
Returns:
True if successful
"""
command = f"LE={1 if enable else 0}"
command = f"LDO {1 if enable else 0}"
success = self._send_command(command)
if success:
@@ -219,12 +241,12 @@ class HeliosLaser:
Returns:
True if laser is enabled
"""
response = self._query("LE?")
response = self._query("LDO")
if response:
try:
return int(response) == 1
except ValueError:
logger.error(f"Invalid response for LE?: {response}")
logger.error(f"Invalid response for LDO: {response}")
return False
def get_frequency_hz(self) -> Optional[int]:
@@ -234,13 +256,13 @@ class HeliosLaser:
Returns:
Frequency in Hz or None if error
"""
response = self._query("FP?")
response = self._query("LDF")
if response:
try:
period_ns = int(response)
return int(1e9 / period_ns)
except (ValueError, ZeroDivisionError):
logger.error(f"Invalid response for FP?: {response}")
logger.error(f"Invalid response for LDF: {response}")
return None
def get_current_ma(self) -> Optional[int]:
@@ -250,12 +272,12 @@ class HeliosLaser:
Returns:
Current in mA or None if error
"""
response = self._query("PC?")
response = self._query("LDS")
if response:
try:
return int(response)
except ValueError:
logger.error(f"Invalid response for PC?: {response}")
logger.error(f"Invalid response for LDS: {response}")
return None
def get_power_mw(self) -> Optional[float]:
@@ -265,12 +287,12 @@ class HeliosLaser:
Returns:
Power in mW or None if error
"""
response = self._query("PO?")
response = self._query("HMP")
if response:
try:
return float(response)
except ValueError:
logger.error(f"Invalid response for PO?: {response}")
logger.error(f"Invalid response for HMP: {response}")
return None
def get_controller_serial(self) -> Optional[str]:
@@ -280,7 +302,7 @@ class HeliosLaser:
Returns:
Serial number string or None if error
"""
return self._query("SN?")
return self._query("CSR")
def get_head_serial(self) -> Optional[str]:
"""
@@ -289,7 +311,104 @@ class HeliosLaser:
Returns:
Serial number string or None if error
"""
return self._query("HSN?")
return self._query("HSR")
def get_status_registers(self) -> tuple:
"""
Query LER, LCE, and CCE status registers.
Each register is a bitmask (sum of flags). Non-zero values indicate
active faults. Reset with reset_faults().
Returns:
Tuple of (ler, lce, cce) as ints, or None for each on error.
"""
def _read_reg(cmd):
resp = self._query(cmd)
if resp is not None:
try:
return int(resp)
except ValueError:
logger.error(f"Invalid response for {cmd}: {resp}")
return None
ler = _read_reg("LER")
lce = _read_reg("LCE")
cce = _read_reg("CCE")
return (ler, lce, cce)
def reset_faults(self) -> bool:
"""
Execute the controller reset sequence to clear status registers.
Protocol-specified sequence: CCE 0 -> LCE 0 -> LER 0
Returns:
True if all three commands sent successfully
"""
ok = True
ok = self._send_command("CCE 0") and ok
time.sleep(0.1)
ok = self._send_command("LCE 0") and ok
time.sleep(0.1)
ok = self._send_command("LER 0") and ok
if ok:
logger.info("Fault reset sequence sent")
return ok
def get_remote_enable(self) -> Optional[bool]:
"""
Query the remote enable state (LRE - activates utility connector pin 8).
Returns:
True if remote enable is active, False if not, None on error
"""
response = self._query("LRE")
if response is not None:
try:
return int(response) == 1
except ValueError:
logger.error(f"Invalid response for LRE: {response}")
return None
def send_raw_command(self, command: str) -> Optional[str]:
"""
Send a raw command string and return the raw response.
Useful for diagnostics. Sends *command* + CR, waits briefly,
then reads whatever the device returns (up to the first CR or timeout).
Returns:
Raw response string (decoded, stripped) or None on error.
"""
if not self.is_connected or not self.serial:
logger.error("Not connected to laser")
return None
try:
self.serial.reset_input_buffer()
time.sleep(0.05)
self.serial.write((command + '\r').encode('ascii'))
time.sleep(0.3)
raw = self.serial.read_until(b'\r')
if not raw:
raw = self.serial.read(self.serial.in_waiting)
return raw.decode('ascii', errors='replace').strip()
except Exception as e:
logger.error(f"send_raw_command error: {e}")
return None
def set_remote_enable(self, enable: bool) -> bool:
"""
Set the remote enable state (LRE - utility connector pin 8).
Args:
enable: True to activate remote enable, False to deactivate
Returns:
True if successful
"""
command = f"LRE {1 if enable else 0}"
return self._send_command(command)
def __del__(self):
"""Destructor - ensure cleanup"""
+6 -1
View File
@@ -265,6 +265,11 @@ class ThorlabsServoDriver():
if(msg['status_bits'] & StatusBits.MOT_SB_HOMED):
self.am_homed[ch] = True
if(msg['status_bits'] & StatusBits.MOT_SB_ENABLED):
self.am_enabled[ch] = True
else:
self.am_enabled[ch] = False
def _update0x0464(self, msg):
'''
_update0x0464 - internal function for MOVE_COMPLETED messages.
@@ -344,7 +349,7 @@ class ThorlabsServoDriver():
else:
raise ValueError("I don't know that axis!")
self.send_and_wait(0x0443, timeout=timeout, chan_ident=1,
self.send_and_wait(0x0443, timeout=timeout, retries=0, chan_ident=1,
destination=axis, source=0x01)
return
+7 -4
View File
@@ -1411,10 +1411,13 @@ class TektronixOscilloscopeBase:
if not self.get_fastframe_state():
raise RuntimeError("FastFrame is not enabled. Enable it with set_fastframe_state(True)")
# Get the frame count
frame_count = self.get_fastframe_count()
if frame_count <= 0:
raise RuntimeError(f"Invalid FastFrame count: {frame_count}")
# Use the number of frames actually acquired, not the configured maximum.
# If the stage stops early, fewer triggers arrive and the scope captures
# fewer frames than configured — reading the configured count would block.
acquired = int(self.query("ACQuire:NUMFRAMESACQuired?"))
if acquired <= 0:
raise RuntimeError(f"Scope acquired 0 FastFrame frames — no data to read")
frame_count = acquired
# Send a single CURVe? query - scope will return all frames
self.write("CURVe?")
+66 -12
View File
@@ -4,11 +4,14 @@ Driver for IDS/Thorlabs uEye uC480 cameras using pyueye library.
Provides camera control, live streaming, and image capture capabilities.
"""
import time
import numpy as np
from pyueye import ueye
from PyQt6.QtCore import QThread, pyqtSignal, QObject
from PyQt6.QtGui import QImage
import logging
import threading
from contextlib import contextmanager
from typing import Optional, Tuple
logger = logging.getLogger(__name__)
@@ -24,17 +27,21 @@ class UC480Camera(QObject):
frame_ready = pyqtSignal(QImage) # Emitted when a new frame is captured
error_occurred = pyqtSignal(str) # Emitted when an error occurs
def __init__(self, camera_id: int = 0):
def __init__(self, camera_id: int = 1):
"""
Initialize the uC480 camera driver.
Args:
camera_id: Camera ID (0 for first available camera)
camera_id: Camera ID (1-based; use is_GetCameraList to find IDs)
"""
super().__init__()
self.camera_id = camera_id
self.h_cam = ueye.HIDS(camera_id)
# IS_ALLOW_STARTER_FW_UPLOAD (0x10000): instructs the SDK to block
# inside is_InitCamera until firmware upload and USB re-enumeration
# finish. Without this flag, the handle becomes invalid the moment
# the device reconnects and the SDK segfaults on the very next call.
self.h_cam = ueye.HIDS(camera_id | 0x10000)
self.is_initialized = False
self.is_capturing = False
@@ -55,6 +62,9 @@ class UC480Camera(QObject):
self.bytes_per_pixel = 3
self.color_mode = ueye.IS_CM_BGR8_PACKED
# Lock to serialize parameter changes that require stopping live video
self._settings_lock = threading.Lock()
def initialize(self) -> bool:
"""
Initialize the camera and allocate memory.
@@ -63,12 +73,23 @@ class UC480Camera(QObject):
True if successful, False otherwise
"""
try:
# Initialize camera
ret = ueye.is_InitCamera(self.h_cam, None)
if ret != ueye.IS_SUCCESS:
logger.error(f"Failed to initialize camera: {ret}")
self.error_occurred.emit(f"Failed to initialize camera: {ret}")
return False
# Initialize camera. After is_ExitCamera the UI124x series
# resets and re-enumerates on USB (firmware reload), so retry
# for up to ~10 s if IS_CANT_OPEN_DEVICE is returned.
for attempt in range(20):
ret = ueye.is_InitCamera(self.h_cam, None)
if ret == ueye.IS_SUCCESS:
break
if ret == ueye.IS_CANT_OPEN_DEVICE and attempt < 19:
logger.debug(f"Camera not ready (IS_CANT_OPEN_DEVICE), retrying ({attempt+1}/20)…")
time.sleep(0.5)
# h_cam value is consumed by a failed init on some SDK
# versions; recreate it to avoid IS_INVALID_CAMERA_HANDLE
self.h_cam = ueye.HIDS(self.camera_id | 0x10000)
else:
logger.error(f"Failed to initialize camera: {ret}")
self.error_occurred.emit(f"Failed to initialize camera: {ret}")
return False
# Get sensor info
ret = ueye.is_GetSensorInfo(self.h_cam, self.sensor_info)
@@ -161,9 +182,12 @@ class UC480Camera(QObject):
self.mem_ptr = None
if self.is_initialized:
ueye.is_ExitCamera(self.h_cam)
ret = ueye.is_ExitCamera(self.h_cam)
self.is_initialized = False
logger.info("Camera resources released")
if ret == ueye.IS_SUCCESS:
logger.info("Camera resources released")
else:
logger.error(f"is_ExitCamera failed: {ret} — camera handle may still be held by daemon")
def start_capture(self) -> bool:
"""
@@ -201,14 +225,37 @@ class UC480Camera(QObject):
return True
ret = ueye.is_StopLiveVideo(self.h_cam, ueye.IS_WAIT)
self.is_capturing = False # Always reset, even if the call fails
if ret != ueye.IS_SUCCESS:
logger.error(f"Failed to stop capture: {ret}")
return False
self.is_capturing = False
logger.info("Video capture stopped")
return True
@contextmanager
def _capture_paused(self):
"""
Context manager that temporarily stops live video while a camera
parameter is being changed, then restarts it. Many IDS cameras
return IS_CANT_COMMUNICATE_WITH_DRIVER (17) or IS_NO_SUCCESS (-1)
when gain/exposure commands are issued during active capture.
"""
with self._settings_lock:
was_capturing = self.is_capturing
if was_capturing:
ueye.is_StopLiveVideo(self.h_cam, ueye.IS_WAIT)
self.is_capturing = False
try:
yield
finally:
if was_capturing:
ret = ueye.is_CaptureVideo(self.h_cam, ueye.IS_DONT_WAIT)
if ret == ueye.IS_SUCCESS:
self.is_capturing = True
else:
logger.error(f"Failed to restart capture after settings change: {ret}")
def get_frame(self) -> Optional[QImage]:
"""
Capture a single frame from the camera.
@@ -405,6 +452,13 @@ class UC480Camera(QObject):
if ret == ueye.IS_SUCCESS:
logger.debug(f"Master gain set to {master_gain}")
return True
elif ret == ueye.IS_CANT_COMMUNICATE_WITH_DRIVER:
logger.error(
f"Hardware gain not supported by this camera model "
f"(IS_CANT_COMMUNICATE_WITH_DRIVER). "
f"Consider using gain boost instead."
)
return False
else:
logger.error(f"Failed to set gain: {ret}")
return False