when'd i last commit this pos?
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
"""Hardware driver modules for ScanEngine-3"""
|
||||
from .bbd202 import *
|
||||
from .uc480_camera import *
|
||||
from .tektronix_base import *
|
||||
from .coherent_hops_laser import *
|
||||
from .genesis_core import *
|
||||
+3147
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
Coherent HOPS Laser Driver - Stub Module
|
||||
This is a temporary stub to allow testing camera integration.
|
||||
"""
|
||||
|
||||
|
||||
class CoherentHOPSLaser:
|
||||
"""Stub class for Coherent HOPS Laser"""
|
||||
pass
|
||||
|
||||
|
||||
class DummyLaser:
|
||||
"""Dummy laser for testing without hardware"""
|
||||
|
||||
def connect(self):
|
||||
"""Simulate connection"""
|
||||
pass
|
||||
|
||||
def disconnect(self):
|
||||
"""Simulate disconnection"""
|
||||
pass
|
||||
|
||||
def get_hardware_id(self):
|
||||
"""Return simulated hardware ID"""
|
||||
return "SIM-12345"
|
||||
|
||||
def get_laser_model(self):
|
||||
"""Return simulated model"""
|
||||
return "Genesis Simulator"
|
||||
|
||||
def get_interlock_status(self):
|
||||
"""Return simulated interlock status"""
|
||||
return "OK"
|
||||
|
||||
def get_key_switch_status(self):
|
||||
"""Return simulated key switch status"""
|
||||
return "ON"
|
||||
|
||||
def get_temperature_main(self):
|
||||
"""Return simulated main temperature"""
|
||||
return 25.5
|
||||
|
||||
def get_temperature_eta(self):
|
||||
"""Return simulated ETA temperature"""
|
||||
return 26.3
|
||||
@@ -0,0 +1,669 @@
|
||||
"""
|
||||
Genesis SLM MX 532 Laser Core Hardware Control Module
|
||||
======================================================
|
||||
|
||||
This module provides low-level hardware control for the Genesis SLM MX 532 laser
|
||||
using NXP I2C-over-serial protocol. It contains reusable classes for serial
|
||||
communication, I2C protocol handling, device control, and laser operations.
|
||||
|
||||
This module is GUI-independent and can be used by both command-line and GUI applications.
|
||||
|
||||
Protocol Overview:
|
||||
-----------------
|
||||
The laser uses NXP I2C-over-serial protocol with packet format:
|
||||
[0x53] [I2C_ADDR] [LENGTH] [COMMAND_BYTES] [DATA_BYTES] [0x50]
|
||||
|
||||
For reads:
|
||||
[0x53] [ADDR_WRITE] [CMD_LEN] [CMD] [0x53] [ADDR_READ] [DATA_LEN] [0x50]
|
||||
|
||||
I2C Devices:
|
||||
-----------
|
||||
- X9119 digital potentiometer at 0x52 - laser current control
|
||||
- PCA9555 I/O expander at 0x4a - digital I/O
|
||||
- AD5254 digital potentiometer at 0x58 - limits control
|
||||
- ADS7828 ADC at 0x90 - sensor readings
|
||||
- M24C64 EEPROM at 0xa4 - configuration storage
|
||||
|
||||
Author: Claude
|
||||
Date: 2026-01-24
|
||||
"""
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from enum import IntEnum
|
||||
|
||||
import serial
|
||||
from serial.tools import list_ports
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Constants - I2C Addresses
|
||||
# ============================================================================
|
||||
|
||||
class I2CAddress(IntEnum):
|
||||
"""I2C device addresses (7-bit addresses shifted left by 1)"""
|
||||
# X9119 Digital Potentiometer (current control)
|
||||
X9119_CURRENT_WRITE = 0x52
|
||||
X9119_CURRENT_READ = 0x53
|
||||
|
||||
# PCA9555 I/O Expanders
|
||||
PCA9555_DIO_WRITE = 0x4a # Main control I/O at 0x14a
|
||||
PCA9555_DIO_READ = 0x4b
|
||||
PCA9555_PSGLUE_WRITE = 0x48 # Power supply glue at 0x148
|
||||
PCA9555_PSGLUE_READ = 0x49
|
||||
PCA9555_HEAD_WRITE = 0x44 # Head DIO at 0x144
|
||||
PCA9555_HEAD_READ = 0x45
|
||||
PCA9555_LDD_WRITE = 0x40 # LDD control at 0x140
|
||||
PCA9555_LDD_READ = 0x41
|
||||
|
||||
# AD5254 Digital Potentiometer (limits)
|
||||
AD5254_WRITE = 0x58
|
||||
|
||||
# ADS7828 ADC (sensor readings)
|
||||
ADS7828_WRITE = 0x90
|
||||
ADS7828_READ = 0x91
|
||||
|
||||
# M24C64 EEPROM (configuration)
|
||||
EEPROM_WRITE = 0xa4
|
||||
EEPROM_READ = 0xa5
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Constants - PCA9555 Register Addresses
|
||||
# ============================================================================
|
||||
|
||||
class PCA9555Register(IntEnum):
|
||||
"""PCA9555 I/O expander register addresses"""
|
||||
INPUT_PORT_0 = 0x00
|
||||
INPUT_PORT_1 = 0x01
|
||||
OUTPUT_PORT_0 = 0x02
|
||||
OUTPUT_PORT_1 = 0x03
|
||||
POLARITY_INV_0 = 0x04
|
||||
POLARITY_INV_1 = 0x05
|
||||
CONFIG_PORT_0 = 0x06
|
||||
CONFIG_PORT_1 = 0x07
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Constants - Control Bitmasks
|
||||
# ============================================================================
|
||||
|
||||
class ControlBitmask(IntEnum):
|
||||
"""Bitmasks for PCA9555 port 0 control bits"""
|
||||
SHUTTER = 0x01 # Bit 0
|
||||
CURRENT_MODE = 0x04 # Bit 2
|
||||
REMOTE_ENABLE = 0x08 # Bit 3
|
||||
ANALOG_ENABLE = 0x10 # Bit 4
|
||||
KEYSWITCH = 0x20 # Bit 5
|
||||
INTERLOCK = 0x01 # Bit 0 (on different PCA9555)
|
||||
LDD_ENABLE = 0x01 # Bit 0 (on LDD PCA9555)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Serial Communication Layer
|
||||
# ============================================================================
|
||||
|
||||
class SerialComm:
|
||||
"""Handles low-level serial port communication"""
|
||||
|
||||
def __init__(self):
|
||||
self.port: Optional[serial.Serial] = None
|
||||
self.port_name: str = "/dev/ttyUSB0"
|
||||
self.baudrate: int = 9600
|
||||
self.timeout: float = 1.0
|
||||
self.packet_log: List[str] = []
|
||||
|
||||
def connect(self, port_name: str, baudrate: int = 9600) -> bool:
|
||||
"""
|
||||
Connect to serial port
|
||||
|
||||
Args:
|
||||
port_name: Serial port device name
|
||||
baudrate: Baud rate (default 9600)
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
self.port_name = port_name
|
||||
self.baudrate = baudrate
|
||||
self.port = serial.Serial(
|
||||
port=port_name,
|
||||
baudrate=baudrate,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
parity=serial.PARITY_NONE,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
timeout=self.timeout
|
||||
)
|
||||
time.sleep(0.1) # Allow port to stabilize
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Serial connection error: {e}")
|
||||
return False
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect from serial port"""
|
||||
if self.port and self.port.is_open:
|
||||
self.port.close()
|
||||
self.port = None
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
"""Check if serial port is connected"""
|
||||
return self.port is not None and self.port.is_open
|
||||
|
||||
def write(self, data: bytes) -> bool:
|
||||
"""
|
||||
Write data to serial port
|
||||
|
||||
Args:
|
||||
data: Bytes to write
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
if not self.is_connected():
|
||||
return False
|
||||
|
||||
try:
|
||||
self.port.write(data)
|
||||
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
||||
hex_str = " ".join(f"{b:02x}" for b in data)
|
||||
self.packet_log.append(f"[{timestamp}] TX: {hex_str}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Serial write error: {e}")
|
||||
return False
|
||||
|
||||
def read(self, size: int) -> Optional[bytes]:
|
||||
"""
|
||||
Read data from serial port
|
||||
|
||||
Args:
|
||||
size: Number of bytes to read
|
||||
|
||||
Returns:
|
||||
Bytes read or None on error
|
||||
"""
|
||||
if not self.is_connected():
|
||||
return None
|
||||
|
||||
try:
|
||||
data = self.port.read(size)
|
||||
if data:
|
||||
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
||||
hex_str = " ".join(f"{b:02x}" for b in data)
|
||||
self.packet_log.append(f"[{timestamp}] RX: {hex_str}")
|
||||
return data
|
||||
except Exception as e:
|
||||
print(f"Serial read error: {e}")
|
||||
return None
|
||||
|
||||
def flush(self):
|
||||
"""Flush serial port buffers"""
|
||||
if self.is_connected():
|
||||
self.port.reset_input_buffer()
|
||||
self.port.reset_output_buffer()
|
||||
|
||||
def get_packet_log(self, last_n: int = 100) -> List[str]:
|
||||
"""Get last N entries from packet log"""
|
||||
return self.packet_log[-last_n:]
|
||||
|
||||
def clear_packet_log(self):
|
||||
"""Clear packet log"""
|
||||
self.packet_log.clear()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# I2C Protocol Layer
|
||||
# ============================================================================
|
||||
|
||||
class I2CProtocol:
|
||||
"""Handles NXP I2C-over-serial protocol packet construction"""
|
||||
|
||||
NXP_START = 0x53
|
||||
NXP_STOP = 0x50
|
||||
|
||||
def __init__(self, serial_comm: SerialComm):
|
||||
self.serial = serial_comm
|
||||
|
||||
def write(self, i2c_addr_write: int, cmd: bytes, data: bytes = b'') -> bool:
|
||||
"""
|
||||
Build and send NXP I2C write packet
|
||||
|
||||
Packet format: [0x53] [I2C_ADDR] [LENGTH] [COMMAND] [DATA] [0x50]
|
||||
|
||||
Args:
|
||||
i2c_addr_write: I2C write address
|
||||
cmd: Command bytes
|
||||
data: Data bytes (optional)
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
if not isinstance(cmd, bytes):
|
||||
cmd = bytes([cmd])
|
||||
if not isinstance(data, bytes):
|
||||
data = bytes(data)
|
||||
|
||||
length = len(cmd) + len(data)
|
||||
packet = bytes([self.NXP_START, i2c_addr_write, length]) + cmd + data + bytes([self.NXP_STOP])
|
||||
|
||||
return self.serial.write(packet)
|
||||
|
||||
def read(self, i2c_addr_write: int, cmd: bytes, cmd_len: int, data_len: int) -> Optional[bytes]:
|
||||
"""
|
||||
Build and send NXP I2C read packet, return data
|
||||
|
||||
Packet format for read:
|
||||
[0x53] [ADDR_WRITE] [CMD_LEN] [CMD] [0x53] [ADDR_READ] [DATA_LEN] [0x50]
|
||||
|
||||
Args:
|
||||
i2c_addr_write: I2C write address
|
||||
cmd: Command bytes
|
||||
cmd_len: Length of command
|
||||
data_len: Expected data length to read
|
||||
|
||||
Returns:
|
||||
Data bytes read or None on error
|
||||
"""
|
||||
if not isinstance(cmd, bytes):
|
||||
cmd = bytes([cmd])
|
||||
|
||||
i2c_addr_read = i2c_addr_write | 0x01 # Set read bit
|
||||
|
||||
# First part: write command
|
||||
packet_write = bytes([self.NXP_START, i2c_addr_write, cmd_len]) + cmd
|
||||
|
||||
# Second part: read data
|
||||
packet_read = bytes([self.NXP_START, i2c_addr_read, data_len, self.NXP_STOP])
|
||||
|
||||
packet = packet_write + packet_read
|
||||
|
||||
if not self.serial.write(packet):
|
||||
return None
|
||||
|
||||
# Read response
|
||||
# Expected response: [data_bytes]
|
||||
time.sleep(0.05) # Give device time to respond
|
||||
response = self.serial.read(data_len)
|
||||
|
||||
return response if response and len(response) == data_len else None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# I2C Device Functions
|
||||
# ============================================================================
|
||||
|
||||
class I2CDevices:
|
||||
"""High-level I2C device control functions"""
|
||||
|
||||
def __init__(self, protocol: I2CProtocol):
|
||||
self.protocol = protocol
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# PCA9555 I/O Expander Functions
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
def pca9555_read_port(self, addr_write: int, port: int) -> Optional[int]:
|
||||
"""
|
||||
Read PCA9555 port register
|
||||
|
||||
Args:
|
||||
addr_write: I2C write address
|
||||
port: Register address (0-7)
|
||||
|
||||
Returns:
|
||||
Port value (0-255) or None on error
|
||||
"""
|
||||
data = self.protocol.read(addr_write, bytes([port]), 1, 1)
|
||||
return data[0] if data else None
|
||||
|
||||
def pca9555_write_port(self, addr_write: int, port: int, value: int) -> bool:
|
||||
"""
|
||||
Write PCA9555 port register
|
||||
|
||||
Args:
|
||||
addr_write: I2C write address
|
||||
port: Register address (0-7)
|
||||
value: Value to write (0-255)
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
return self.protocol.write(addr_write, bytes([port]), bytes([value]))
|
||||
|
||||
def pca9555_set_bit(self, addr_write: int, port: int, bitmask: int, state: bool) -> bool:
|
||||
"""
|
||||
Set or clear specific bit on PCA9555
|
||||
|
||||
Args:
|
||||
addr_write: I2C write address
|
||||
port: Register address
|
||||
bitmask: Bit mask (e.g., 0x01 for bit 0)
|
||||
state: True to set bit, False to clear
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
# Read current value
|
||||
current = self.pca9555_read_port(addr_write, port)
|
||||
if current is None:
|
||||
return False
|
||||
|
||||
# Modify bit
|
||||
if state:
|
||||
new_value = current | bitmask
|
||||
else:
|
||||
new_value = current & ~bitmask
|
||||
|
||||
# Write back
|
||||
return self.pca9555_write_port(addr_write, port, new_value)
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# X9119 Digital Potentiometer Functions
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
def x9119_write_wiper(self, addr_write: int, value: int) -> bool:
|
||||
"""
|
||||
Set X9119 wiper position
|
||||
|
||||
Args:
|
||||
addr_write: I2C write address
|
||||
value: Wiper position (0-1023, 10-bit)
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
# X9119 write wiper command: 0xa0 followed by 2 bytes (10-bit value)
|
||||
# Value is split into two bytes: MSB contains upper 2 bits, LSB contains lower 8 bits
|
||||
value = max(0, min(1023, value)) # Clamp to valid range
|
||||
|
||||
msb = (value >> 8) & 0x03 # Upper 2 bits
|
||||
lsb = value & 0xFF # Lower 8 bits
|
||||
|
||||
cmd = bytes([0xa0])
|
||||
data = bytes([msb, lsb])
|
||||
|
||||
return self.protocol.write(addr_write, cmd, data)
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# ADS7828 ADC Functions
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
def ads7828_read(self, addr_write: int, channel: int) -> Optional[int]:
|
||||
"""
|
||||
Read ADS7828 ADC channel
|
||||
|
||||
Args:
|
||||
addr_write: I2C write address
|
||||
channel: Channel to read (0-7)
|
||||
|
||||
Returns:
|
||||
12-bit ADC value (0-4095) or None on error
|
||||
"""
|
||||
# ADS7828 command byte format:
|
||||
# Bit 7: SD (single-ended=1)
|
||||
# Bits 6-4: Channel select
|
||||
# Bit 3: PD1 (power-down mode)
|
||||
# Bit 2: PD0 (power-down mode)
|
||||
# Bits 1-0: Don't care
|
||||
|
||||
# For single-ended read with internal reference on
|
||||
cmd_byte = 0x80 | ((channel & 0x07) << 4) | 0x0c
|
||||
|
||||
data = self.protocol.read(addr_write, bytes([cmd_byte]), 1, 2)
|
||||
if not data or len(data) != 2:
|
||||
return None
|
||||
|
||||
# Combine two bytes into 12-bit value
|
||||
value = (data[0] << 8) | data[1]
|
||||
value = (value >> 4) & 0x0FFF # Extract 12-bit value
|
||||
|
||||
return value
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Laser Control Layer
|
||||
# ============================================================================
|
||||
|
||||
class LaserControl:
|
||||
"""High-level laser control functions"""
|
||||
|
||||
def __init__(self, devices: I2CDevices):
|
||||
self.devices = devices
|
||||
self._safe_state_active = False
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# SET Commands
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
def set_current(self, value: int) -> bool:
|
||||
"""
|
||||
Set laser current command (CCMD=)
|
||||
|
||||
Args:
|
||||
value: Current value (0-1023)
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
return self.devices.x9119_write_wiper(I2CAddress.X9119_CURRENT_WRITE, value)
|
||||
|
||||
def set_power_cmd(self, value: int) -> bool:
|
||||
"""
|
||||
Set power command (PCMD=)
|
||||
|
||||
Note: Uses same device as current control, different channel
|
||||
|
||||
Args:
|
||||
value: Power value (0-1023)
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
return self.devices.x9119_write_wiper(I2CAddress.X9119_CURRENT_WRITE, value)
|
||||
|
||||
def set_shutter(self, state: bool) -> bool:
|
||||
"""
|
||||
Set shutter state (SHCMD=)
|
||||
|
||||
Args:
|
||||
state: True for open, False for closed
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
return self.devices.pca9555_set_bit(
|
||||
I2CAddress.PCA9555_DIO_WRITE,
|
||||
PCA9555Register.OUTPUT_PORT_0,
|
||||
ControlBitmask.SHUTTER,
|
||||
state
|
||||
)
|
||||
|
||||
def set_keyswitch(self, state: bool) -> bool:
|
||||
"""
|
||||
Set keyswitch state (KSWCMD=)
|
||||
|
||||
Args:
|
||||
state: True for on, False for off
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
return self.devices.pca9555_set_bit(
|
||||
I2CAddress.PCA9555_DIO_WRITE,
|
||||
PCA9555Register.OUTPUT_PORT_0,
|
||||
ControlBitmask.KEYSWITCH,
|
||||
state
|
||||
)
|
||||
|
||||
def set_current_mode(self, state: bool) -> bool:
|
||||
"""
|
||||
Set current mode (CMODECMD=)
|
||||
|
||||
Args:
|
||||
state: True for on, False for off
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
return self.devices.pca9555_set_bit(
|
||||
I2CAddress.PCA9555_DIO_WRITE,
|
||||
PCA9555Register.OUTPUT_PORT_0,
|
||||
ControlBitmask.CURRENT_MODE,
|
||||
state
|
||||
)
|
||||
|
||||
def set_analog_enable(self, state: bool) -> bool:
|
||||
"""
|
||||
Set analog input enable (ANACMD=)
|
||||
|
||||
Args:
|
||||
state: True for enabled, False for disabled
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
return self.devices.pca9555_set_bit(
|
||||
I2CAddress.PCA9555_DIO_WRITE,
|
||||
PCA9555Register.OUTPUT_PORT_0,
|
||||
ControlBitmask.ANALOG_ENABLE,
|
||||
state
|
||||
)
|
||||
|
||||
def set_remote_enable(self, state: bool) -> bool:
|
||||
"""
|
||||
Set remote enable (REM=)
|
||||
|
||||
Args:
|
||||
state: True for enabled, False for disabled
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
return self.devices.pca9555_set_bit(
|
||||
I2CAddress.PCA9555_DIO_WRITE,
|
||||
PCA9555Register.OUTPUT_PORT_0,
|
||||
ControlBitmask.REMOTE_ENABLE,
|
||||
state
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# GET/Query Commands
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
def get_current_actual(self) -> Optional[float]:
|
||||
"""
|
||||
Get actual current reading
|
||||
|
||||
Returns:
|
||||
Current in arbitrary units (0-4095) or None on error
|
||||
"""
|
||||
# Read from ADS7828, channel based on command 0x84
|
||||
# Command 0x84 suggests channel 0
|
||||
value = self.devices.ads7828_read(I2CAddress.ADS7828_WRITE, 0)
|
||||
return value if value is not None else None
|
||||
|
||||
def get_interlock_status(self) -> Optional[bool]:
|
||||
"""
|
||||
Get interlock status
|
||||
|
||||
Returns:
|
||||
True if interlock OK, False if fault, None on error
|
||||
"""
|
||||
value = self.devices.pca9555_read_port(
|
||||
I2CAddress.PCA9555_PSGLUE_WRITE,
|
||||
PCA9555Register.INPUT_PORT_0
|
||||
)
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
# Bit 0 indicates interlock status
|
||||
return bool(value & ControlBitmask.INTERLOCK)
|
||||
|
||||
def get_ldd_enable_status(self) -> Optional[bool]:
|
||||
"""
|
||||
Get laser diode driver enable status
|
||||
|
||||
Returns:
|
||||
True if enabled, False if disabled, None on error
|
||||
"""
|
||||
value = self.devices.pca9555_read_port(
|
||||
I2CAddress.PCA9555_LDD_WRITE,
|
||||
PCA9555Register.INPUT_PORT_0
|
||||
)
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
return bool(value & ControlBitmask.LDD_ENABLE)
|
||||
|
||||
def get_psglue_in_status(self) -> Optional[int]:
|
||||
"""
|
||||
Get power supply glue input status
|
||||
|
||||
Returns:
|
||||
Port value or None on error
|
||||
"""
|
||||
return self.devices.pca9555_read_port(
|
||||
I2CAddress.PCA9555_PSGLUE_WRITE,
|
||||
PCA9555Register.INPUT_PORT_0
|
||||
)
|
||||
|
||||
def get_psglue_out_status(self) -> Optional[int]:
|
||||
"""
|
||||
Get power supply glue output status
|
||||
|
||||
Returns:
|
||||
Port value or None on error
|
||||
"""
|
||||
return self.devices.pca9555_read_port(
|
||||
I2CAddress.PCA9555_DIO_WRITE,
|
||||
PCA9555Register.OUTPUT_PORT_0
|
||||
)
|
||||
|
||||
def get_head_dio_status(self) -> Optional[int]:
|
||||
"""
|
||||
Get head DIO status
|
||||
|
||||
Returns:
|
||||
Port value or None on error
|
||||
"""
|
||||
return self.devices.pca9555_read_port(
|
||||
I2CAddress.PCA9555_HEAD_WRITE,
|
||||
PCA9555Register.INPUT_PORT_0
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Safety Functions
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
def emergency_stop(self) -> bool:
|
||||
"""
|
||||
Emergency stop: close shutter, set current to 0, disable keyswitch
|
||||
|
||||
Returns:
|
||||
True if all operations successful, False otherwise
|
||||
"""
|
||||
success = True
|
||||
success &= self.set_shutter(False)
|
||||
success &= self.set_current(0)
|
||||
success &= self.set_keyswitch(False)
|
||||
self._safe_state_active = True
|
||||
return success
|
||||
|
||||
def enter_safe_state(self) -> bool:
|
||||
"""
|
||||
Enter safe state (similar to emergency stop but also disables remote)
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
success = True
|
||||
success &= self.set_shutter(False)
|
||||
success &= self.set_current(0)
|
||||
success &= self.set_power_cmd(0)
|
||||
success &= self.set_keyswitch(False)
|
||||
success &= self.set_remote_enable(False)
|
||||
self._safe_state_active = True
|
||||
return success
|
||||
@@ -0,0 +1,296 @@
|
||||
"""
|
||||
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()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,475 @@
|
||||
"""
|
||||
uC480 Camera Driver
|
||||
Driver for IDS/Thorlabs uEye uC480 cameras using pyueye library.
|
||||
Provides camera control, live streaming, and image capture capabilities.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from pyueye import ueye
|
||||
from PyQt6.QtCore import QThread, pyqtSignal, QObject
|
||||
from PyQt6.QtGui import QImage
|
||||
import logging
|
||||
from typing import Optional, Tuple
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UC480Camera(QObject):
|
||||
"""
|
||||
Driver class for uC480 camera.
|
||||
Handles initialization, configuration, and image acquisition.
|
||||
"""
|
||||
|
||||
# Signals
|
||||
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):
|
||||
"""
|
||||
Initialize the uC480 camera driver.
|
||||
|
||||
Args:
|
||||
camera_id: Camera ID (0 for first available camera)
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.camera_id = camera_id
|
||||
self.h_cam = ueye.HIDS(camera_id)
|
||||
self.is_initialized = False
|
||||
self.is_capturing = False
|
||||
|
||||
# Memory and image info
|
||||
self.mem_ptr = ueye.c_mem_p()
|
||||
self.mem_id = ueye.int()
|
||||
self.pitch = ueye.INT()
|
||||
|
||||
# Camera info
|
||||
self.sensor_info = ueye.SENSORINFO()
|
||||
self.cam_info = ueye.CAMINFO()
|
||||
self.rect_aoi = ueye.IS_RECT()
|
||||
|
||||
# Image dimensions
|
||||
self.width = 0
|
||||
self.height = 0
|
||||
self.bits_per_pixel = 24 # Default to 24-bit color
|
||||
self.bytes_per_pixel = 3
|
||||
self.color_mode = ueye.IS_CM_BGR8_PACKED
|
||||
|
||||
def initialize(self) -> bool:
|
||||
"""
|
||||
Initialize the camera and allocate memory.
|
||||
|
||||
Returns:
|
||||
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
|
||||
|
||||
# Get sensor info
|
||||
ret = ueye.is_GetSensorInfo(self.h_cam, self.sensor_info)
|
||||
if ret != ueye.IS_SUCCESS:
|
||||
logger.error(f"Failed to get sensor info: {ret}")
|
||||
self.cleanup()
|
||||
return False
|
||||
|
||||
# Get camera info
|
||||
ret = ueye.is_GetCameraInfo(self.h_cam, self.cam_info)
|
||||
if ret != ueye.IS_SUCCESS:
|
||||
logger.error(f"Failed to get camera info: {ret}")
|
||||
self.cleanup()
|
||||
return False
|
||||
|
||||
# Set color mode
|
||||
ret = ueye.is_SetColorMode(self.h_cam, self.color_mode)
|
||||
if ret != ueye.IS_SUCCESS:
|
||||
logger.error(f"Failed to set color mode: {ret}")
|
||||
self.cleanup()
|
||||
return False
|
||||
|
||||
# Get maximum image size
|
||||
self.width = self.sensor_info.nMaxWidth.value
|
||||
self.height = self.sensor_info.nMaxHeight.value
|
||||
|
||||
# Set Area of Interest (AOI) to maximum size
|
||||
self.rect_aoi.s32X = ueye.int(0)
|
||||
self.rect_aoi.s32Y = ueye.int(0)
|
||||
self.rect_aoi.s32Width = ueye.int(self.width)
|
||||
self.rect_aoi.s32Height = ueye.int(self.height)
|
||||
|
||||
ret = ueye.is_AOI(self.h_cam, ueye.IS_AOI_IMAGE_SET_AOI, self.rect_aoi, ueye.sizeof(self.rect_aoi))
|
||||
if ret != ueye.IS_SUCCESS:
|
||||
logger.error(f"Failed to set AOI: {ret}")
|
||||
self.cleanup()
|
||||
return False
|
||||
|
||||
# Allocate image memory
|
||||
ret = ueye.is_AllocImageMem(
|
||||
self.h_cam,
|
||||
self.width,
|
||||
self.height,
|
||||
self.bits_per_pixel,
|
||||
self.mem_ptr,
|
||||
self.mem_id
|
||||
)
|
||||
if ret != ueye.IS_SUCCESS:
|
||||
logger.error(f"Failed to allocate image memory: {ret}")
|
||||
self.cleanup()
|
||||
return False
|
||||
|
||||
# Set active memory
|
||||
ret = ueye.is_SetImageMem(self.h_cam, self.mem_ptr, self.mem_id)
|
||||
if ret != ueye.IS_SUCCESS:
|
||||
logger.error(f"Failed to set active memory: {ret}")
|
||||
self.cleanup()
|
||||
return False
|
||||
|
||||
# Get pitch (bytes per line)
|
||||
ret = ueye.is_GetImageMemPitch(self.h_cam, self.pitch)
|
||||
if ret != ueye.IS_SUCCESS:
|
||||
logger.error(f"Failed to get pitch: {ret}")
|
||||
self.cleanup()
|
||||
return False
|
||||
|
||||
self.is_initialized = True
|
||||
logger.info(f"Camera initialized: {self.width}x{self.height}, {self.bits_per_pixel}bpp")
|
||||
|
||||
# Set default settings
|
||||
self.set_exposure(10.0) # 10ms default exposure
|
||||
self.set_pixel_clock(30) # 30MHz default pixel clock
|
||||
self.set_framerate(30.0) # 30fps default
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Exception during camera initialization: {e}")
|
||||
self.error_occurred.emit(f"Exception during initialization: {e}")
|
||||
self.cleanup()
|
||||
return False
|
||||
|
||||
def cleanup(self):
|
||||
"""Release camera resources."""
|
||||
if self.is_capturing:
|
||||
self.stop_capture()
|
||||
|
||||
if self.mem_ptr:
|
||||
ueye.is_FreeImageMem(self.h_cam, self.mem_ptr, self.mem_id)
|
||||
self.mem_ptr = None
|
||||
|
||||
if self.is_initialized:
|
||||
ueye.is_ExitCamera(self.h_cam)
|
||||
self.is_initialized = False
|
||||
logger.info("Camera resources released")
|
||||
|
||||
def start_capture(self) -> bool:
|
||||
"""
|
||||
Start continuous video capture.
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
if not self.is_initialized:
|
||||
logger.error("Camera not initialized")
|
||||
return False
|
||||
|
||||
if self.is_capturing:
|
||||
logger.warning("Camera already capturing")
|
||||
return True
|
||||
|
||||
ret = ueye.is_CaptureVideo(self.h_cam, ueye.IS_DONT_WAIT)
|
||||
if ret != ueye.IS_SUCCESS:
|
||||
logger.error(f"Failed to start capture: {ret}")
|
||||
self.error_occurred.emit(f"Failed to start capture: {ret}")
|
||||
return False
|
||||
|
||||
self.is_capturing = True
|
||||
logger.info("Video capture started")
|
||||
return True
|
||||
|
||||
def stop_capture(self) -> bool:
|
||||
"""
|
||||
Stop continuous video capture.
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
if not self.is_capturing:
|
||||
return True
|
||||
|
||||
ret = ueye.is_StopLiveVideo(self.h_cam, ueye.IS_WAIT)
|
||||
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
|
||||
|
||||
def get_frame(self) -> Optional[QImage]:
|
||||
"""
|
||||
Capture a single frame from the camera.
|
||||
|
||||
Returns:
|
||||
QImage if successful, None otherwise
|
||||
"""
|
||||
if not self.is_initialized:
|
||||
logger.error("Camera not initialized")
|
||||
return None
|
||||
|
||||
# Create numpy array from image memory
|
||||
try:
|
||||
array = ueye.get_data(
|
||||
self.mem_ptr,
|
||||
self.width,
|
||||
self.height,
|
||||
self.bits_per_pixel,
|
||||
self.pitch,
|
||||
copy=True
|
||||
)
|
||||
|
||||
# Reshape to image dimensions
|
||||
frame = np.reshape(array, (self.height, self.width, self.bytes_per_pixel))
|
||||
|
||||
# Convert to QImage (BGR to RGB)
|
||||
height, width, channel = frame.shape
|
||||
bytes_per_line = self.bytes_per_pixel * width
|
||||
|
||||
# Convert BGR to RGB
|
||||
rgb_frame = frame[:, :, ::-1].copy()
|
||||
|
||||
q_image = QImage(
|
||||
rgb_frame.data,
|
||||
width,
|
||||
height,
|
||||
bytes_per_line,
|
||||
QImage.Format.Format_RGB888
|
||||
)
|
||||
|
||||
# Make a copy since the numpy array will be deleted
|
||||
return q_image.copy()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get frame: {e}")
|
||||
self.error_occurred.emit(f"Failed to get frame: {e}")
|
||||
return None
|
||||
|
||||
def set_exposure(self, exposure_ms: float) -> bool:
|
||||
"""
|
||||
Set camera exposure time.
|
||||
|
||||
Args:
|
||||
exposure_ms: Exposure time in milliseconds
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
if not self.is_initialized:
|
||||
return False
|
||||
|
||||
exposure = ueye.c_double(exposure_ms)
|
||||
ret = ueye.is_Exposure(
|
||||
self.h_cam,
|
||||
ueye.IS_EXPOSURE_CMD_SET_EXPOSURE,
|
||||
exposure,
|
||||
ueye.sizeof(exposure)
|
||||
)
|
||||
|
||||
if ret == ueye.IS_SUCCESS:
|
||||
logger.debug(f"Exposure set to {exposure_ms}ms")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"Failed to set exposure: {ret}")
|
||||
return False
|
||||
|
||||
def get_exposure(self) -> Optional[float]:
|
||||
"""
|
||||
Get current exposure time.
|
||||
|
||||
Returns:
|
||||
Exposure time in milliseconds, or None if failed
|
||||
"""
|
||||
if not self.is_initialized:
|
||||
return None
|
||||
|
||||
exposure = ueye.c_double()
|
||||
ret = ueye.is_Exposure(
|
||||
self.h_cam,
|
||||
ueye.IS_EXPOSURE_CMD_GET_EXPOSURE,
|
||||
exposure,
|
||||
ueye.sizeof(exposure)
|
||||
)
|
||||
|
||||
if ret == ueye.IS_SUCCESS:
|
||||
return exposure.value
|
||||
else:
|
||||
return None
|
||||
|
||||
def set_pixel_clock(self, pixel_clock_mhz: int) -> bool:
|
||||
"""
|
||||
Set camera pixel clock.
|
||||
|
||||
Args:
|
||||
pixel_clock_mhz: Pixel clock in MHz
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
if not self.is_initialized:
|
||||
return False
|
||||
|
||||
ret = ueye.is_PixelClock(
|
||||
self.h_cam,
|
||||
ueye.IS_PIXELCLOCK_CMD_SET,
|
||||
ueye.c_uint(pixel_clock_mhz),
|
||||
ueye.sizeof(ueye.c_uint)
|
||||
)
|
||||
|
||||
if ret == ueye.IS_SUCCESS:
|
||||
logger.debug(f"Pixel clock set to {pixel_clock_mhz}MHz")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"Failed to set pixel clock: {ret}")
|
||||
return False
|
||||
|
||||
def set_framerate(self, fps: float) -> bool:
|
||||
"""
|
||||
Set camera framerate.
|
||||
|
||||
Args:
|
||||
fps: Frames per second
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
if not self.is_initialized:
|
||||
return False
|
||||
|
||||
new_fps = ueye.c_double(fps)
|
||||
actual_fps = ueye.c_double()
|
||||
ret = ueye.is_SetFrameRate(self.h_cam, new_fps, actual_fps)
|
||||
|
||||
if ret == ueye.IS_SUCCESS:
|
||||
logger.debug(f"Framerate set to {fps}fps")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"Failed to set framerate: {ret}")
|
||||
return False
|
||||
|
||||
def get_framerate(self) -> Optional[float]:
|
||||
"""
|
||||
Get current framerate.
|
||||
|
||||
Returns:
|
||||
Framerate in fps, or None if failed
|
||||
"""
|
||||
if not self.is_initialized:
|
||||
return None
|
||||
|
||||
fps = ueye.c_double()
|
||||
ret = ueye.is_GetFramesPerSecond(self.h_cam, fps)
|
||||
|
||||
if ret == ueye.IS_SUCCESS:
|
||||
return fps.value
|
||||
else:
|
||||
return None
|
||||
|
||||
def set_gain(self, master_gain: int) -> bool:
|
||||
"""
|
||||
Set camera master gain.
|
||||
|
||||
Args:
|
||||
master_gain: Gain value (0-100)
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
if not self.is_initialized:
|
||||
return False
|
||||
|
||||
if master_gain < 0 or master_gain > 100:
|
||||
logger.error(f"Gain value {master_gain} out of range (0-100)")
|
||||
return False
|
||||
|
||||
ret = ueye.is_SetHardwareGain(
|
||||
self.h_cam,
|
||||
master_gain,
|
||||
ueye.IS_IGNORE_PARAMETER,
|
||||
ueye.IS_IGNORE_PARAMETER,
|
||||
ueye.IS_IGNORE_PARAMETER
|
||||
)
|
||||
|
||||
if ret == ueye.IS_SUCCESS:
|
||||
logger.debug(f"Master gain set to {master_gain}")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"Failed to set gain: {ret}")
|
||||
return False
|
||||
|
||||
def get_sensor_info(self) -> dict:
|
||||
"""
|
||||
Get camera sensor information.
|
||||
|
||||
Returns:
|
||||
Dictionary with sensor information
|
||||
"""
|
||||
if not self.is_initialized:
|
||||
return {}
|
||||
|
||||
return {
|
||||
'sensor_name': self.sensor_info.strSensorName.decode('utf-8'),
|
||||
'max_width': self.sensor_info.nMaxWidth.value,
|
||||
'max_height': self.sensor_info.nMaxHeight.value,
|
||||
'color_mode': self.sensor_info.nColorMode.value,
|
||||
'pixel_size': self.sensor_info.wPixelSize.value / 100.0, # in µm
|
||||
}
|
||||
|
||||
def __del__(self):
|
||||
"""Destructor - ensure cleanup."""
|
||||
self.cleanup()
|
||||
|
||||
|
||||
class CameraStreamThread(QThread):
|
||||
"""
|
||||
Thread for continuous camera frame acquisition and streaming.
|
||||
"""
|
||||
|
||||
frame_ready = pyqtSignal(QImage)
|
||||
error_occurred = pyqtSignal(str)
|
||||
|
||||
def __init__(self, camera: UC480Camera):
|
||||
"""
|
||||
Initialize the camera stream thread.
|
||||
|
||||
Args:
|
||||
camera: UC480Camera instance
|
||||
"""
|
||||
super().__init__()
|
||||
self.camera = camera
|
||||
self.running = False
|
||||
|
||||
def run(self):
|
||||
"""Main thread loop for frame acquisition."""
|
||||
self.running = True
|
||||
|
||||
if not self.camera.start_capture():
|
||||
self.error_occurred.emit("Failed to start camera capture")
|
||||
return
|
||||
|
||||
while self.running:
|
||||
frame = self.camera.get_frame()
|
||||
if frame is not None:
|
||||
self.frame_ready.emit(frame)
|
||||
else:
|
||||
# Small delay on error to prevent CPU spinning
|
||||
self.msleep(10)
|
||||
|
||||
self.camera.stop_capture()
|
||||
|
||||
def stop(self):
|
||||
"""Stop the streaming thread."""
|
||||
self.running = False
|
||||
self.wait()
|
||||
Reference in New Issue
Block a user