pre uc480 integration
This commit is contained in:
+142
-23
@@ -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"""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user