Refactor/mcs card refactoring first light #87
@@ -20,13 +20,13 @@ def extend_command_line_args(parser):
|
||||
return parser
|
||||
|
||||
|
||||
def get_config() -> ServiceConfig:
|
||||
"""
|
||||
Create and return the ServiceConfig for the plugin repository
|
||||
"""
|
||||
deployment_path = os.path.dirname(os.path.dirname(os.path.dirname(csaxs_bec.__file__)))
|
||||
files = os.listdir(deployment_path)
|
||||
if "bec_config.yaml" in files:
|
||||
return ServiceConfig(config_path=os.path.join(deployment_path, "bec_config.yaml"))
|
||||
else:
|
||||
return ServiceConfig(redis={"host": "localhost", "port": 6379})
|
||||
# def get_config() -> ServiceConfig:
|
||||
# """
|
||||
# Create and return the ServiceConfig for the plugin repository
|
||||
# """
|
||||
# deployment_path = os.path.dirname(os.path.dirname(os.path.dirname(csaxs_bec.__file__)))
|
||||
# files = os.listdir(deployment_path)
|
||||
# if "bec_config.yaml" in files:
|
||||
# return ServiceConfig(config_path=os.path.join(deployment_path, "bec_config.yaml"))
|
||||
# else:
|
||||
# return ServiceConfig(redis={"host": "localhost", "port": 6379})
|
||||
|
||||
@@ -27,20 +27,20 @@ mokev:
|
||||
onFailure: buffer
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
mcs:
|
||||
description: Mcs scalar card for transmission readout
|
||||
deviceClass: csaxs_bec.devices.epics.mcs_csaxs.MCScSAXS
|
||||
deviceConfig:
|
||||
prefix: 'X12SA-MCS:'
|
||||
mcs_config:
|
||||
num_lines: 1
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- mcs
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readoutPriority: monitored
|
||||
softwareTrigger: false
|
||||
# mcs:
|
||||
# description: Mcs scalar card for transmission readout
|
||||
# deviceClass: csaxs_bec.devices.epics.mcs_csaxs.MCScSAXS
|
||||
# deviceConfig:
|
||||
# prefix: 'X12SA-MCS:'
|
||||
# mcs_config:
|
||||
# num_lines: 1
|
||||
# deviceTags:
|
||||
# - cSAXS
|
||||
# - mcs
|
||||
# onFailure: buffer
|
||||
# enabled: true
|
||||
# readoutPriority: monitored
|
||||
# softwareTrigger: false
|
||||
eiger9m:
|
||||
description: Eiger9m HPC area detector 9M
|
||||
deviceClass: csaxs_bec.devices.epics.eiger9m_csaxs.Eiger9McSAXS
|
||||
|
||||
@@ -20,26 +20,25 @@ ddg2:
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
|
||||
samx:
|
||||
readoutPriority: baseline
|
||||
deviceClass: ophyd_devices.SimPositioner
|
||||
mcs:
|
||||
description: Mcs scalar card for transmission readout
|
||||
deviceClass: csaxs_bec.devices.epics.mcs_card.mcs_card_csaxs.MCSCardCSAXS
|
||||
deviceConfig:
|
||||
delay: 1
|
||||
limits:
|
||||
- -50
|
||||
- 50
|
||||
tolerance: 0.01
|
||||
update_frequency: 400
|
||||
deviceTags:
|
||||
- user motors
|
||||
prefix: 'X12SA-MCS:'
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readOnly: false
|
||||
|
||||
bpm4i:
|
||||
readoutPriority: monitored
|
||||
deviceClass: ophyd_devices.SimMonitor
|
||||
softwareTrigger: false
|
||||
|
||||
ids_cam:
|
||||
description: IDS camera for live image acquisition
|
||||
deviceClass: csaxs_bec.devices.ids_cameras.IDSCamera
|
||||
deviceConfig:
|
||||
deviceTags:
|
||||
- beamline
|
||||
camera_id: 201
|
||||
bits_per_pixel: 24
|
||||
m_n_colormode: 1
|
||||
live_mode: True
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readOnly: false
|
||||
readoutPriority: async
|
||||
softwareTrigger: True
|
||||
@@ -2,4 +2,7 @@ optics:
|
||||
- !include ./optics_hutch.yaml
|
||||
|
||||
frontend:
|
||||
- !include ./frontend.yaml
|
||||
- !include ./frontend.yaml
|
||||
|
||||
endstation:
|
||||
- !include ./endstation.yaml
|
||||
@@ -6,7 +6,7 @@ idgap:
|
||||
onFailure: raise # Consider changing to buffer
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
readOnly: true # put to false if you like to move it
|
||||
readOnly: false # put to false if you like to move it
|
||||
softwareTrigger: false
|
||||
|
||||
xbpm1x:
|
||||
@@ -41,7 +41,7 @@ sl1xr:
|
||||
description: 'slit 1 (frontend) x ring'
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: 'X12SA-FE-SLDI1:TRXR'
|
||||
prefix: 'X12SA-FE-SL1:TRXR'
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
@@ -55,7 +55,7 @@ sl1xw:
|
||||
description: 'slit 1 (frontend) x wall'
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: 'X12SA-FE-SLDI1:TRXW'
|
||||
prefix: 'X12SA-FE-SL1:TRXW'
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
@@ -69,7 +69,7 @@ sl1yb:
|
||||
description: 'slit 1 (frontend) y bottom'
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: 'X12SA-FE-SLDI1:TRYB'
|
||||
prefix: 'X12SA-FE-SL1:TRYB'
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
@@ -83,7 +83,7 @@ sl1yt:
|
||||
description: 'slit 1 (frontend) y top'
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: 'X12SA-FE-SLDI1:TRYT'
|
||||
prefix: 'X12SA-FE-SL1:TRYT'
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
|
||||
@@ -79,6 +79,8 @@ xbpm2x:
|
||||
- 200
|
||||
port: 5000
|
||||
sign: 1
|
||||
# precision: 3
|
||||
# tolerance: 0.005
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
@@ -95,6 +97,8 @@ xbpm2y:
|
||||
- 200
|
||||
port: 5000
|
||||
sign: 1
|
||||
# precision: 3
|
||||
# tolerance: 0.005
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
@@ -111,6 +115,8 @@ cu_foilx:
|
||||
- 200
|
||||
port: 5000
|
||||
sign: 1
|
||||
# precision: 3
|
||||
# tolerance: 0.005
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
@@ -127,6 +133,8 @@ scinx:
|
||||
- 200
|
||||
port: 5000
|
||||
sign: 1
|
||||
# precision: 3
|
||||
# tolerance: 0.005
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
|
||||
@@ -29,21 +29,35 @@ DELAY CHANNELS:
|
||||
- f = e + 1us (short pulse to OR gate for MCS triggering)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from ophyd import DeviceStatus, StatusBase
|
||||
from ophyd import DeviceStatus
|
||||
from ophyd_devices import CompareStatus, TransitionStatus
|
||||
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||
|
||||
from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import (
|
||||
CHANNELREFERENCE,
|
||||
OUTPUTPOLARITY,
|
||||
PROC_EVENT_MODE,
|
||||
STATUSBITS,
|
||||
TRIGGERSOURCE,
|
||||
AllChannelNames,
|
||||
ChannelConfig,
|
||||
DelayGeneratorCSAXS,
|
||||
LiteralChannels,
|
||||
StatusBitsCompareStatus,
|
||||
)
|
||||
from csaxs_bec.devices.epics.mcs_card.mcs_card_csaxs import ACQUIRING, READYTOREAD
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from bec_lib.devicemanager import DeviceManagerBase, ScanInfo
|
||||
|
||||
from csaxs_bec.devices.epics.mcs_card.mcs_card_csaxs import MCSCardCSAXS
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
@@ -64,7 +78,7 @@ DEFAULT_IO_CONFIG: dict[AllChannelNames, ChannelConfig] = {
|
||||
DEFAULT_TRIGGER_SOURCE: TRIGGERSOURCE = TRIGGERSOURCE.SINGLE_SHOT
|
||||
DEFAULT_READOUT_TIMES = {"ab": 2e-4, "cd": 2e-4, "ef": 2e-4, "gh": 2e-4} # 0.2 ms 5kHz
|
||||
|
||||
DEFAULT_REFERENCES: list[tuple[AllChannelNames, CHANNELREFERENCE]] = [
|
||||
DEFAULT_REFERENCES: list[tuple[LiteralChannels, CHANNELREFERENCE]] = [
|
||||
("A", CHANNELREFERENCE.T0), # T0 + 2ms delay
|
||||
("B", CHANNELREFERENCE.A),
|
||||
("C", CHANNELREFERENCE.T0), # T0
|
||||
@@ -78,12 +92,34 @@ DEFAULT_REFERENCES: list[tuple[AllChannelNames, CHANNELREFERENCE]] = [
|
||||
|
||||
class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
|
||||
"""
|
||||
Implementation of DelayGeneratorCSAXS for the CSAXS master trigger delay generator at X12SA-CPCL-DDG1.
|
||||
It will be triggered by a soft trigger from BEC or a hardware trigger from a beamline device (e.g. the Galil stages).
|
||||
It is operated in standard mode, not burst mode and will trigger the EXT/EN of DDG2 (channel ab).
|
||||
It is responsible for opening the shutter (channel cd) and sending an extra trigger to an or gate for the MCS card (channel ef).
|
||||
Implementation of DelayGeneratorCSAXS for master trigger delay generator at X12SA-CPCL-DDG1.
|
||||
It will be triggered by a soft trigger from BEC or a hardware trigger from a beamline device
|
||||
(e.g. the Galil stages). It is operated in standard mode, not burst mode and will trigger the
|
||||
EXT/EN of DDG2 (channel ab). It is responsible for opening the shutter (channel cd) and sending
|
||||
an extra trigger to an or gate for the MCS card (channel ef).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
prefix: str = "",
|
||||
scan_info: ScanInfo | None = None,
|
||||
device_manager: DeviceManagerBase | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Initialize the MCSCardCSAXS with the given arguments and keyword arguments.
|
||||
"""
|
||||
super().__init__(
|
||||
name=name, prefix=prefix, scan_info=scan_info, device_manager=device_manager, **kwargs
|
||||
)
|
||||
self.device_manager = device_manager
|
||||
self._poll_thread = threading.Thread(target=self._poll_event_status, daemon=True)
|
||||
self._poll_thread_run_event = threading.Event()
|
||||
self._poll_thread_poll_loop_done = threading.Event()
|
||||
self._poll_thread_kill_event = threading.Event()
|
||||
self._poll_thread.start()
|
||||
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
def on_connected(self) -> None:
|
||||
"""
|
||||
@@ -96,8 +132,10 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
|
||||
self.set_io_values(channel, **config)
|
||||
self.set_trigger(DEFAULT_TRIGGER_SOURCE)
|
||||
self.set_references_for_channels(DEFAULT_REFERENCES)
|
||||
# Set proc status to passively update with 5Hz (0.2s)
|
||||
self.state.proc_status_mode.put(PROC_EVENT_MODE.EVENT)
|
||||
|
||||
def on_stage(self) -> DeviceStatus | StatusBase | None:
|
||||
def on_stage(self) -> None:
|
||||
"""
|
||||
Stage logic for the DDG1 device, being th main trigger delay generator for CSAXS.
|
||||
For standard scans, it will be triggered by a soft trigger from BEC.
|
||||
@@ -105,7 +143,8 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
|
||||
|
||||
This DDG is always not in burst mode.
|
||||
"""
|
||||
self.burst_disable()
|
||||
exp_time = self.scan_info.msg.scan_parameters["exp_time"]
|
||||
self.burst_enable(1, 0, exp_time)
|
||||
exp_time = self.scan_info.msg.scan_parameters["exp_time"]
|
||||
frames_per_trigger = self.scan_info.msg.scan_parameters["frames_per_trigger"]
|
||||
# Trigger DDG2
|
||||
@@ -122,45 +161,124 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
|
||||
# e has refernce to d, f has reference to e
|
||||
self.set_delay_pairs(channel="ef", delay=0, width=1e-6)
|
||||
|
||||
def on_trigger(self) -> DeviceStatus | StatusBase | None:
|
||||
def _prepare_mcs_on_trigger(self, mcs: MCSCardCSAXS) -> None:
|
||||
"""Prepare the MCS card for the next trigger.
|
||||
This method holds the logic to ensure that the MCS card is ready to read.
|
||||
It's logic is coupled to the MCS card implementation and the DDG1 trigger logic.
|
||||
"""
|
||||
status_ready_read = CompareStatus(mcs.ready_to_read, READYTOREAD.DONE)
|
||||
mcs.stop_all.put(1)
|
||||
status_acquiring = TransitionStatus(mcs.acquiring, [ACQUIRING.DONE, ACQUIRING.ACQUIRING])
|
||||
self.cancel_on_stop(status_ready_read)
|
||||
self.cancel_on_stop(status_acquiring)
|
||||
status_ready_read.wait(10)
|
||||
|
||||
mcs.ready_to_read.put(READYTOREAD.PROCESSING)
|
||||
mcs.erase_start.put(1)
|
||||
status_acquiring.wait(timeout=10) # Allow 10 seconds in case communication is slow
|
||||
|
||||
def _poll_event_status(self) -> None:
|
||||
"""
|
||||
Poll the event status register in a background thread. Control
|
||||
the polling with the _poll_thread_run_event and _poll_thread_kill_event.
|
||||
"""
|
||||
while not self._poll_thread_kill_event.is_set():
|
||||
self._poll_thread_run_event.wait()
|
||||
self._poll_thread_poll_loop_done.clear()
|
||||
while (
|
||||
self._poll_thread_run_event.is_set() and not self._poll_thread_kill_event.is_set()
|
||||
):
|
||||
self._poll_loop()
|
||||
|
||||
self._poll_thread_poll_loop_done.set()
|
||||
|
||||
def _poll_loop(self) -> None:
|
||||
"""
|
||||
Poll loop to update event status.
|
||||
The checks ensure that the loop exist after each operation and be stuck in sleep.
|
||||
The 20ms sleep was added to ensure that the event status is not polled too frequently,
|
||||
and to give the device time to process the previous command. This was found empirically
|
||||
to be necessary to avoid missing events.
|
||||
"""
|
||||
self.state.proc_status.put(1, use_complete=True)
|
||||
time.sleep(0.02) # 20ms delay for processing, important for not missing events
|
||||
if self._poll_thread_run_event.is_set() and not self._poll_thread_kill_event.is_set():
|
||||
return
|
||||
self.state.event_status.get(use_monitor=False)
|
||||
if self._poll_thread_run_event.is_set() and not self._poll_thread_kill_event.is_set():
|
||||
return
|
||||
time.sleep(0.02) # 20ms delay for processing, important for not missing events
|
||||
|
||||
def _start_polling(self) -> None:
|
||||
"""Start the polling loop in the background thread."""
|
||||
self._poll_thread_run_event.set()
|
||||
|
||||
def _stop_polling(self) -> None:
|
||||
"""Stop the polling loop in the background thread."""
|
||||
self._poll_thread_run_event.clear()
|
||||
|
||||
def _kill_poll_thread(self) -> None:
|
||||
"""Kill the polling thread."""
|
||||
self._poll_thread_kill_event.set()
|
||||
self._stop_polling()
|
||||
self._poll_thread.join(timeout=1)
|
||||
if self._poll_thread.is_alive():
|
||||
logger.warning("Polling thread did not stop gracefully.")
|
||||
else:
|
||||
logger.info("Polling thread stopped.")
|
||||
|
||||
def _prepare_trigger_status_event(self, timeout: float | None = None) -> DeviceStatus:
|
||||
"""Prepare the trigger status event for the DDG1, and trigger the de"""
|
||||
if timeout is None:
|
||||
# Default timeout of 5 seconds + exposure time * frames_per_trigger
|
||||
timeout = 5 + self.scan_info.msg.scan_parameters.get(
|
||||
"exp_time", 0.1
|
||||
) * self.scan_info.msg.scan_parameters.get("frames_per_trigger", 1)
|
||||
|
||||
# Callback to cancel the status if the device is stopped
|
||||
def cancel_cb(status: CompareStatus) -> None:
|
||||
"""Callback to cancel the status if the device is stopped."""
|
||||
self._stop_polling()
|
||||
|
||||
# Run false is important to ensure that the status is only checked on the next event status update
|
||||
status = StatusBitsCompareStatus(
|
||||
self.state.event_status, STATUSBITS.END_OF_BURST, timeout=timeout, run=False
|
||||
)
|
||||
status.add_callback(cancel_cb)
|
||||
self.cancel_on_stop(status)
|
||||
return status
|
||||
|
||||
def on_trigger(self) -> DeviceStatus:
|
||||
"""Note, we need to add a delay to the StatusBits callback on the event_status.
|
||||
If we don't then subsequent triggers may reach the DDG too early, and will be ignored. To
|
||||
avoid this, we've added the option to specify a delay via add_delay, default here is 50ms.
|
||||
"""
|
||||
st = StatusBase()
|
||||
self.cancel_on_stop(st)
|
||||
# Stop polling, poll once manually to ensure that the register is clean
|
||||
self._stop_polling()
|
||||
self._poll_thread_poll_loop_done.wait(timeout=1)
|
||||
|
||||
# Prepare the MCS card for the next software trigger
|
||||
mcs = self.device_manager.devices.get("mcs", None)
|
||||
if mcs is None:
|
||||
logger.info("Did not find mcs card with name 'mcs' in current session")
|
||||
else:
|
||||
self._prepare_mcs_on_trigger(mcs)
|
||||
# Prepare status with callback to cancel the polling once finished
|
||||
status = self._prepare_trigger_status_event()
|
||||
# Start polling
|
||||
self._start_polling()
|
||||
# Trigger the DDG1
|
||||
self.trigger_shot.put(1, use_complete=True)
|
||||
time.sleep(self.scan_info.msg.scan_parameters["exp_time"])
|
||||
self.cancel_on_stop(st)
|
||||
status = self.wait_for_status(status=st, bit_event=STATUSBITS.END_OF_DELAY, timeout=2)
|
||||
return status
|
||||
|
||||
def wait_for_status(
|
||||
self, status: StatusBase, bit_event: STATUSBITS, timeout: float = 2
|
||||
) -> None:
|
||||
"""Wait for a event status bit to be set.
|
||||
|
||||
Args:
|
||||
status (StatusBase): The status object to update.
|
||||
bit_event (STATUSBITS): The event status bit to wait for.
|
||||
timeout (float): Maximum time to wait for the event status bit to be set.
|
||||
"""
|
||||
current_time = time.time()
|
||||
while not status.done:
|
||||
self.state.proc_status.put(1, use_complete=True)
|
||||
event_status = self.state.event_status.get()
|
||||
if (STATUSBITS(event_status) & bit_event) == bit_event:
|
||||
status.set_finished()
|
||||
if time.time() - current_time > timeout:
|
||||
status.set_exception(TimeoutError(f"Timeout waiting for status {status}"))
|
||||
break
|
||||
time.sleep(0.1)
|
||||
time.sleep(0.05) # Give time for the IOC to be ready again
|
||||
return status
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""Stop the delay generator by setting the burst mode to 0"""
|
||||
self.stop_ddg()
|
||||
self._stop_polling()
|
||||
|
||||
def on_destroy(self) -> None:
|
||||
"""Clean up resources when the device is destroyed."""
|
||||
self._kill_poll_thread()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -36,6 +36,7 @@ from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import
|
||||
AllChannelNames,
|
||||
ChannelConfig,
|
||||
DelayGeneratorCSAXS,
|
||||
LiteralChannels,
|
||||
)
|
||||
|
||||
logger = bec_logger.logger
|
||||
@@ -57,7 +58,7 @@ DEFAULT_IO_CONFIG: dict[AllChannelNames, ChannelConfig] = {
|
||||
DEFAULT_TRIGGER_SOURCE: TRIGGERSOURCE = TRIGGERSOURCE.EXT_RISING_EDGE
|
||||
DEFAULT_READOUT_TIMES = {"ab": 2e-4, "cd": 2e-4, "ef": 2e-4, "gh": 2e-4} # 0.2 ms 5kHz
|
||||
|
||||
DEFAULT_REFERENCES: list[tuple[AllChannelNames, CHANNELREFERENCE]] = [
|
||||
DEFAULT_REFERENCES: list[tuple[LiteralChannels, CHANNELREFERENCE]] = [
|
||||
("A", CHANNELREFERENCE.T0),
|
||||
("B", CHANNELREFERENCE.A),
|
||||
("C", CHANNELREFERENCE.T0),
|
||||
@@ -100,16 +101,20 @@ class DDG2(PSIDeviceBase, DelayGeneratorCSAXS):
|
||||
frames_per_trigger = self.scan_info.msg.scan_parameters["frames_per_trigger"]
|
||||
# a = t0
|
||||
# a has reference to t0, b has reference to a
|
||||
if any(exp_time <= rt for rt in DEFAULT_READOUT_TIMES.values()):
|
||||
raise ValueError(
|
||||
f"Exposure time {exp_time} is too short for the readout times {DEFAULT_READOUT_TIMES}"
|
||||
)
|
||||
burst_pulse_width = exp_time - DEFAULT_READOUT_TIMES["ab"]
|
||||
self.set_delay_pairs(channel="ab", delay=0, width=burst_pulse_width)
|
||||
self.burst_enable(count=frames_per_trigger, delay=0, period=exp_time)
|
||||
|
||||
|
||||
def on_pre_scan(self):
|
||||
"""
|
||||
The delay generator occasionally needs a bit extra time to process all
|
||||
commands from stage. Therefore, we introduce here a short sleep
|
||||
"""
|
||||
# Delay Generator occasionaly needs a bit extra time to process all commands, sleep 50ms
|
||||
# Delay Generator occasionaly needs a bit extra time to process all commands, sleep 50ms
|
||||
time.sleep(0.05)
|
||||
|
||||
def on_trigger(self) -> DeviceStatus | StatusBase | None:
|
||||
@@ -118,7 +123,7 @@ class DDG2(PSIDeviceBase, DelayGeneratorCSAXS):
|
||||
"""
|
||||
|
||||
def wait_for_status(
|
||||
self, status: StatusBase, bit_event: STATUSBITS, timeout: float = 2
|
||||
self, status: DeviceStatus, bit_event: STATUSBITS, timeout: float = 5
|
||||
) -> None:
|
||||
"""Wait for a event status bit to be set.
|
||||
|
||||
@@ -134,7 +139,11 @@ class DDG2(PSIDeviceBase, DelayGeneratorCSAXS):
|
||||
if (STATUSBITS(event_status) & bit_event) == bit_event:
|
||||
status.set_finished()
|
||||
if time.time() - current_time > timeout:
|
||||
status.set_exception(TimeoutError(f"Timeout waiting for status {status}"))
|
||||
status.set_exception(
|
||||
TimeoutError(
|
||||
f"Timeout waiting for status of device {self.name} for event_status {bit_event}"
|
||||
)
|
||||
)
|
||||
break
|
||||
time.sleep(0.1)
|
||||
time.sleep(0.05) # Give time for the IOC to be ready again
|
||||
|
||||
@@ -6,8 +6,8 @@ https://www.thinksrs.com/downloads/pdfs/manuals/DG645m.pdf
|
||||
"""
|
||||
|
||||
import enum
|
||||
from typing import Literal, TypedDict
|
||||
import time
|
||||
from typing import Literal, TypedDict
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from ophyd import Component as Cpt
|
||||
@@ -76,9 +76,26 @@ class OUTPUTPOLARITY(enum.Enum):
|
||||
POSITIVE = 1
|
||||
|
||||
|
||||
class PROC_EVENT_MODE(int, enum.Enum):
|
||||
"""Read mode for MCS channels."""
|
||||
|
||||
PASSIVE = 0
|
||||
EVENT = 1
|
||||
IO_INTR = 2
|
||||
FREQ_0_1HZ = 3
|
||||
FREQ_0_2HZ = 4
|
||||
FREQ_0_5HZ = 5
|
||||
FREQ_1HZ = 6
|
||||
FREQ_2HZ = 7
|
||||
FREQ_5HZ = 8
|
||||
FREQ_10HZ = 9
|
||||
FREQ_100HZ = 10
|
||||
|
||||
|
||||
class STATUSBITS(enum.IntFlag):
|
||||
"""Bit flags for the status signal of the delay generator."""
|
||||
|
||||
NONE = 0 << 0 # No status bits set.
|
||||
TRIG = 1 << 0 # Got a trigger.
|
||||
RATE = 1 << 1 # Got a trigger while a delay or burst was in progress.
|
||||
END_OF_DELAY = 1 << 2 # A delay cycle has completed.
|
||||
@@ -91,6 +108,7 @@ class STATUSBITS(enum.IntFlag):
|
||||
def describe(self) -> dict:
|
||||
"""Return a description of the status bits."""
|
||||
descriptions = {
|
||||
STATUSBITS.NONE: "No status bits set.",
|
||||
STATUSBITS.TRIG: "Got a trigger.",
|
||||
STATUSBITS.RATE: "Got a trigger while a delay or burst was in progress.",
|
||||
STATUSBITS.END_OF_DELAY: "A delay cycle has completed.",
|
||||
@@ -114,7 +132,7 @@ class StatusBitsCompareStatus(SubscriptionStatus):
|
||||
*args,
|
||||
event_type=None,
|
||||
timeout: float | None = None,
|
||||
add_delay:float|None = None,
|
||||
add_delay: float | None = None,
|
||||
settle_time: float = 0,
|
||||
run: bool = True,
|
||||
**kwargs,
|
||||
@@ -137,9 +155,9 @@ class StatusBitsCompareStatus(SubscriptionStatus):
|
||||
"""Callback for subscription status"""
|
||||
obj = kwargs.get("obj", None)
|
||||
if obj is None:
|
||||
name = 'no object received'
|
||||
name = "no object received"
|
||||
else:
|
||||
name=obj.name
|
||||
name = obj.name
|
||||
if any((STATUSBITS(value) & state) == state for state in self._raise_states):
|
||||
self.set_exception(
|
||||
ValueError(
|
||||
@@ -147,7 +165,7 @@ class StatusBitsCompareStatus(SubscriptionStatus):
|
||||
)
|
||||
)
|
||||
return False
|
||||
if self._add_delay !=0:
|
||||
if self._add_delay != 0:
|
||||
time.sleep(self._add_delay)
|
||||
|
||||
return (STATUSBITS(value) & self._value) == self._value
|
||||
@@ -378,7 +396,6 @@ class DelayGeneratorEventStatus(Device):
|
||||
"EventStatusLI",
|
||||
name="event_status",
|
||||
kind=Kind.omitted,
|
||||
auto_monitor=True,
|
||||
doc="Event status register for the delay generator",
|
||||
)
|
||||
proc_status = Cpt(
|
||||
@@ -389,6 +406,13 @@ class DelayGeneratorEventStatus(Device):
|
||||
doc="Poll and flush the latest event status register entry from the HW to the event_status signal",
|
||||
)
|
||||
|
||||
proc_status_mode = Cpt(
|
||||
EpicsSignal,
|
||||
"EventStatusLI.SCAN",
|
||||
kind=Kind.omitted,
|
||||
doc="Readout mode for transferring data from status buffer to the event_status signal.",
|
||||
)
|
||||
|
||||
|
||||
class DelayGeneratorCSAXS(Device):
|
||||
"""
|
||||
@@ -403,7 +427,14 @@ class DelayGeneratorCSAXS(Device):
|
||||
In addition, the io layer allows setting amplitude, offset and polarity for each pair.
|
||||
"""
|
||||
|
||||
_pv_timeout: float = 1.5 # Default timeout for PV operations in seconds
|
||||
# USER_ACCESS = [
|
||||
# "set_channel_reference",
|
||||
# "set_references_for_channels",
|
||||
# "set_io_values",
|
||||
# "set_trigger",
|
||||
# ]
|
||||
|
||||
_pv_timeout: float = 5 # Default timeout for PV operations in seconds
|
||||
|
||||
# Front Panel
|
||||
t0 = Cpt(StaticPair, "T0", name="t0", doc="T0 static pair")
|
||||
@@ -686,11 +717,23 @@ class DelayGeneratorCSAXS(Device):
|
||||
}[channel]
|
||||
|
||||
def set_channel_reference(self, channel: LiteralChannels, reference_channel: CHANNELREFERENCE):
|
||||
"""Set the reference channel for a specific channel.
|
||||
|
||||
Args:
|
||||
channel (LiteralChannels): The channel to set the reference for.
|
||||
reference_channel (CHANNELREFERENCE): The reference channel to set.
|
||||
"""
|
||||
self._get_literal_channel(channel).reference.put(reference_channel.value)
|
||||
|
||||
def set_references_for_channels(
|
||||
self, channels_and_refs: list[tuple[LiteralChannels, CHANNELREFERENCE]]
|
||||
):
|
||||
"""Set the reference channels for multiple channels.
|
||||
|
||||
Args:
|
||||
channels_and_refs (list[tuple[LiteralChannels, CHANNELREFERENCE]]): A list of
|
||||
tuples where each tuple contains a channel and its corresponding reference channel.
|
||||
"""
|
||||
for ch, ref in channels_and_refs:
|
||||
self.set_channel_reference(ch, ref)
|
||||
|
||||
|
||||
1
csaxs_bec/devices/epics/mcs_card/__init__.py
Normal file
1
csaxs_bec/devices/epics/mcs_card/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .mcs_card import MCSCard
|
||||
341
csaxs_bec/devices/epics/mcs_card/mcs_card.py
Normal file
341
csaxs_bec/devices/epics/mcs_card/mcs_card.py
Normal file
@@ -0,0 +1,341 @@
|
||||
"""
|
||||
EPICS SIS38XX Multichannel Scaler (MCS) Interface
|
||||
|
||||
This module provides an interface to the SIS3801/SIS3820 multichannel scaler (MCS) cards via EPICS.
|
||||
It focuses on the implementation for the SIS3820 model, as input/output modes differ between SIS3801
|
||||
and SIS3820. It supports both MCS and scaler record operations, enabling configuration and control of
|
||||
acquisition parameters such as dwell time, channel advance mode, and input/output settings.
|
||||
The module facilitates data acquisition by managing FIFO buffers and simulating conventional
|
||||
MCS behavior through memory buffers.
|
||||
|
||||
At cSAXS, the SIS3820 model is used, which supports 32 channels.
|
||||
|
||||
References:
|
||||
- EPICS SIS3801 and SIS3820 Drivers: https://millenia.cars.aps.anl.gov/software/epics/mcaStruck.html
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, DynamicDeviceComponent, EpicsSignal, EpicsSignalRO, Kind
|
||||
|
||||
|
||||
class CHANNELADVANCE(int, enum.Enum):
|
||||
"""Channel advance pixel mode for MCS card."""
|
||||
|
||||
INTERNAL = 0
|
||||
EXTERNAL = 1
|
||||
|
||||
|
||||
class ACQUIRING(int, enum.Enum):
|
||||
"""Acquisition status for MCS card."""
|
||||
|
||||
DONE = 0
|
||||
ACQUIRING = 1
|
||||
|
||||
|
||||
class READMODE(int, enum.Enum):
|
||||
"""Read mode for MCS channels."""
|
||||
|
||||
PASSIVE = 0
|
||||
EVENT = 1
|
||||
IO_INTR = 2
|
||||
FREQ_0_1HZ = 3
|
||||
FREQ_0_2HZ = 4
|
||||
FREQ_0_5HZ = 5
|
||||
FREQ_1HZ = 6
|
||||
FREQ_2HZ = 7
|
||||
FREQ_5HZ = 8
|
||||
FREQ_10HZ = 9
|
||||
FREQ_100HZ = 10
|
||||
|
||||
|
||||
class CHANNEL1SOURCE(int, enum.Enum):
|
||||
"""Source for first counter pulses."""
|
||||
|
||||
INTERNAL_CLOCK = 0
|
||||
EXTERNAL = 1
|
||||
|
||||
|
||||
class POLARITY(int, enum.Enum):
|
||||
"""Polarity of input_polarity/output_polarity for MCS card."""
|
||||
|
||||
NORMAL = 0
|
||||
INVERTED = 1
|
||||
|
||||
|
||||
class ACQUIREMODE(int, enum.Enum):
|
||||
"""Acquire mode for the card. Allowed modes are Scaler and MCS."""
|
||||
|
||||
MCS = 0
|
||||
SCALER = 1
|
||||
|
||||
|
||||
class MODELS(int, enum.Enum):
|
||||
|
||||
SIS3801 = 0
|
||||
SIS3820 = 1
|
||||
|
||||
|
||||
class INPUTMODE(int, enum.Enum):
|
||||
"""SIS3820 input mode definitions, in total there are 8 modes (0-7).
|
||||
|
||||
Each mode defines the function of external inputs 1-4.
|
||||
Note: SIS3820 has extended input modes compared to SIS3801.
|
||||
Please check the EPICS documentation for details on the specific input modes supported by SIS3801.
|
||||
"""
|
||||
|
||||
MODE_0 = 0
|
||||
MODE_1 = 1
|
||||
MODE_2 = 2
|
||||
MODE_3 = 3
|
||||
MODE_4 = 4
|
||||
MODE_5 = 5
|
||||
MODE_6 = 6
|
||||
MODE_7 = 7
|
||||
|
||||
def describe(self) -> str:
|
||||
"""Return a description of the input mode."""
|
||||
descriptions = {
|
||||
self.MODE_0: "Inputs 1-4: No function (default idle mode)",
|
||||
self.MODE_1: "Inputs 1-4: Next pulse, User bit 1, User bit 2, Inhibit next pulse",
|
||||
self.MODE_2: "Inputs 1-4: Next pulse, User bit 1, Inhibit counting, Inhibit next pulse",
|
||||
self.MODE_3: "Inputs 1-4: Next pulse, User bit 1, User bit 2, Inhibit counting",
|
||||
self.MODE_4: "Inputs 1-4: Inhibit counting channels 1-8, 9-16, 17-24, 25-32",
|
||||
self.MODE_5: "Inputs 1-4: Next pulse, HISCAL_START, No function, No function",
|
||||
self.MODE_6: "Inputs 1-4: Next pulse, Inhibit counting, Clear counters, User bit 1",
|
||||
self.MODE_7: "Inputs 1-4: Encoder A, Encoder B, Encoder I, Inhibit counting",
|
||||
}
|
||||
return descriptions.get(self, "Unknown input mode")
|
||||
|
||||
|
||||
class OUTPUTMODE(int, enum.Enum):
|
||||
"""SIS3820 output mode definitions, in total there are 4 modes (0-3).
|
||||
|
||||
Each mode configures output signals 5-8.
|
||||
Note: SIS3820 supports 4 output modes (0-3), SIS3801 supports only Mode 0 with differen functionality.
|
||||
Please check the EPICS documentation for details on the specific output modes supported by SIS3801.
|
||||
"""
|
||||
|
||||
MODE_0 = 0
|
||||
MODE_1 = 1
|
||||
MODE_2 = 2
|
||||
MODE_3 = 3
|
||||
|
||||
def describe(self) -> str:
|
||||
"""Return a description of the output mode."""
|
||||
descriptions = {
|
||||
self.MODE_0: "Outputs 5-8: LNE/CIP, SDRAM empty, SDRAM threshold, User LED",
|
||||
self.MODE_1: "Outputs 5-8: LNE/CIP, Enabled, 50 MHz, User LED",
|
||||
self.MODE_2: "Outputs 5-8: LNE/CIP, 10 MHz (20ns), 10 MHz (20ns), User LED",
|
||||
self.MODE_3: "Outputs 5-8: LNE/CIP, 10 MHz (20ns), MUX OUT channel, User LED (requires firmware ≥ 0x10A)",
|
||||
}
|
||||
return descriptions.get(self, "Unknown output mode")
|
||||
|
||||
|
||||
def _create_mca_channels(num_channels: int) -> dict[str, tuple]:
|
||||
"""
|
||||
Create a dictionary of MCA channel definitions for the DynamicDeviceComponent.
|
||||
Starts from channel 1 to num_channels.
|
||||
|
||||
Args:
|
||||
num_channels (int): The number of MCA channels to create.
|
||||
"""
|
||||
mcs_channels = {}
|
||||
for i in range(1, num_channels + 1):
|
||||
mcs_channels[f"mca{i}"] = (
|
||||
EpicsSignalRO,
|
||||
f"mca{i}.VAL",
|
||||
{"kind": Kind.omitted, "auto_monitor": True, "doc": f"MCA channel {i}."},
|
||||
)
|
||||
return mcs_channels
|
||||
|
||||
|
||||
class MCSCard(Device):
|
||||
"""
|
||||
Ophyd implementation for the interface to the SIS3801/SIS3820 multichannel scaler (MCS) cards via EPICS.
|
||||
|
||||
This class provides signals to expose EPICS PVs of the MCS card. More details can be found in the
|
||||
documentation of the EPICS drivers for SIS3801 and SIS3820.
|
||||
|
||||
References:
|
||||
- EPICS SIS3801 and SIS3820 Drivers: https://millenia.cars.aps.anl.gov/software/epics/mcaStruck.html
|
||||
"""
|
||||
|
||||
snl_connected = Cpt(
|
||||
EpicsSignalRO,
|
||||
"SNL_Connected",
|
||||
kind=Kind.omitted,
|
||||
doc="Indicates whether the SNL program has connected to all PVs.",
|
||||
)
|
||||
erase_all = Cpt(
|
||||
EpicsSignal,
|
||||
"EraseAll",
|
||||
kind=Kind.omitted,
|
||||
doc="Erases all mca or waveform records, setting elapsed times and counts in all channels to 0.",
|
||||
)
|
||||
erase_start = Cpt(
|
||||
EpicsSignal,
|
||||
"EraseStart",
|
||||
kind=Kind.omitted,
|
||||
doc="Erases all mca or waveform records and starts acquisition.",
|
||||
)
|
||||
start_all = Cpt(
|
||||
EpicsSignal,
|
||||
"StartAll",
|
||||
kind=Kind.omitted,
|
||||
doc="Starts or resumes acquisition without erasing first.",
|
||||
)
|
||||
acquiring = Cpt(
|
||||
EpicsSignalRO,
|
||||
"Acquiring",
|
||||
kind=Kind.omitted,
|
||||
doc="Acquiring (=1) when acquisition is in progress and Done (=0) when acquisition is complete.",
|
||||
)
|
||||
stop_all = Cpt(EpicsSignal, "StopAll", kind=Kind.omitted, doc="Stops acquisition.")
|
||||
preset_real = Cpt(
|
||||
EpicsSignal,
|
||||
"PresetReal",
|
||||
kind=Kind.omitted,
|
||||
doc="Preset real time. If non-zero then acquisition will stop when this time is reached.",
|
||||
)
|
||||
elapsed_real = Cpt(
|
||||
EpicsSignalRO,
|
||||
"ElapsedReal",
|
||||
kind=Kind.omitted,
|
||||
doc="Elapsed time since acquisition started.",
|
||||
)
|
||||
read_all = Cpt(
|
||||
EpicsSignal,
|
||||
"DoReadAll.VAL",
|
||||
kind=Kind.omitted,
|
||||
doc="Forces a read of all mca or waveform records from the hardware. This record can be set to periodically process to update the records during acquisition. Note that even if this record has SCAN=Passive the mca or waveform records will always process once when acquisition completes.",
|
||||
)
|
||||
read_mode = Cpt(
|
||||
EpicsSignal,
|
||||
"ReadAll.SCAN",
|
||||
kind=Kind.omitted,
|
||||
doc="Readout mode for transferring data from FIFO buffer to mca EPICS scalars.",
|
||||
)
|
||||
num_use_all = Cpt(
|
||||
EpicsSignal,
|
||||
"NuseAll",
|
||||
kind=Kind.omitted,
|
||||
doc="The number of channels to use for the mca or waveform records. Acquisition will automatically stop when the number of channel advances reaches this value.",
|
||||
)
|
||||
dwell = Cpt(
|
||||
EpicsSignal,
|
||||
"Dwell",
|
||||
kind=Kind.omitted,
|
||||
doc="The dwell time per channel when using internal channel advance mode.",
|
||||
)
|
||||
channel_advance = Cpt(
|
||||
EpicsSignal,
|
||||
"ChannelAdvance",
|
||||
kind=Kind.omitted,
|
||||
doc="The channel advance mode. Choices are 'Internal' (count for a preset time per channel) or 'External' (advance on external hardware channel advance signal).",
|
||||
)
|
||||
count_on_start = Cpt(
|
||||
EpicsSignal,
|
||||
"CountOnStart",
|
||||
kind=Kind.omitted,
|
||||
doc="Flag controlling whether the module begins counting immediately when acquisition starts. This record only applies in External channel advance mode. If No (=0) then counting does not start in channel 0 until receipt of the first external channel advance pulse. If Yes (=1) then counting in channel 0 starts immediately when acquisition starts, without waiting for the first external channel advance pulse.",
|
||||
)
|
||||
software_channel_advance = Cpt(
|
||||
EpicsSignal,
|
||||
"SoftwareChannelAdvance",
|
||||
kind=Kind.omitted,
|
||||
doc="Processing this record causes a channel advance to occur immediately, without waiting for the current dwell time to be reached or the next external channel advance pulse to arrive.",
|
||||
)
|
||||
channel1_source = Cpt(
|
||||
EpicsSignal,
|
||||
"Channel1Source",
|
||||
kind=Kind.omitted,
|
||||
doc="Controls the source of pulses into the first counter. The choices are 'Int. clock' which selects the internal clock, and 'External' which selects the external pulse input to counter 1.",
|
||||
)
|
||||
prescale = Cpt(
|
||||
EpicsSignal,
|
||||
"Prescale",
|
||||
kind=Kind.omitted,
|
||||
doc="The prescale factor for external channel advance pulses. If the prescale factor is N then N external channel advance pulses must be received before a channel advance will occur.",
|
||||
)
|
||||
enable_client_wait = Cpt(
|
||||
EpicsSignal,
|
||||
"EnableClientWait",
|
||||
kind=Kind.omitted,
|
||||
doc="Flag to force acquisition to wait until a client clears the ClientWait busy record before proceeding to the next acquisition. This can be useful with the scan record.",
|
||||
)
|
||||
client_wait = Cpt(
|
||||
EpicsSignal,
|
||||
"ClientWait",
|
||||
kind=Kind.omitted,
|
||||
doc="Flag that will be set to 1 when acquisition completes, and which a client must set back to 0 to allow acquisition to proceed. This only has an effect if EnableClientWait is 1.",
|
||||
)
|
||||
acquire_mode = Cpt(
|
||||
EpicsSignal,
|
||||
"AcquireMode",
|
||||
kind=Kind.omitted,
|
||||
doc="The current acquisition mode (MCS=0 or Scaler=1). This record is used to turn off the scaler record Autocount in MCS mode.",
|
||||
)
|
||||
mux_output = Cpt(
|
||||
EpicsSignal,
|
||||
"MUXOutput",
|
||||
kind=Kind.omitted,
|
||||
doc="Value of 0-32 used to select which input signal is routed to output signal 7 on the SIS3820 in output mode 3.",
|
||||
)
|
||||
user_led = Cpt(
|
||||
EpicsSignal,
|
||||
"UserLED",
|
||||
kind=Kind.omitted,
|
||||
doc="Toggles the user LED and also output signal 8 on the SIS3820.",
|
||||
)
|
||||
input_mode = Cpt(
|
||||
EpicsSignal,
|
||||
"InputMode",
|
||||
kind=Kind.omitted,
|
||||
doc="The input mode. Supported input modes vary for SIS3801 and SIS3820.",
|
||||
)
|
||||
input_polarity = Cpt(
|
||||
EpicsSignal,
|
||||
"InputPolarity",
|
||||
kind=Kind.omitted,
|
||||
doc="The polarity of the input control signals on the SIS3820. Choices are Normal and Inverted.",
|
||||
)
|
||||
output_mode = Cpt(
|
||||
EpicsSignal,
|
||||
"OutputMode",
|
||||
kind=Kind.omitted,
|
||||
doc="The output mode. Supported output modes vary for SIS3801 and SIS3820.",
|
||||
)
|
||||
output_polarity = Cpt(
|
||||
EpicsSignal,
|
||||
"OutputPolarity",
|
||||
kind=Kind.omitted,
|
||||
doc="The polarity of the output control signals on the SIS3820. Choices are Normal and Inverted.",
|
||||
)
|
||||
model = Cpt(
|
||||
EpicsSignalRO,
|
||||
"Model",
|
||||
kind=Kind.omitted,
|
||||
doc="The scaler model. Values are 'SIS3801' and 'SIS3820'.",
|
||||
)
|
||||
firmware = Cpt(EpicsSignalRO, "Firmware", kind=Kind.omitted, doc="The firmware version.")
|
||||
max_channels = Cpt(
|
||||
EpicsSignalRO, "MaxChannels", kind=Kind.omitted, doc="The maximum number of channels."
|
||||
)
|
||||
|
||||
# Relevant counters
|
||||
current_channel = Cpt(
|
||||
EpicsSignalRO,
|
||||
"CurrentChannel",
|
||||
kind=Kind.omitted,
|
||||
auto_monitor=True,
|
||||
doc="The current channel number, i.e. the number of channel advances that have occurred minus 1.",
|
||||
)
|
||||
counters = DynamicDeviceComponent(
|
||||
_create_mca_channels(32),
|
||||
kind=Kind.omitted,
|
||||
doc="Sub-device with the mca counters 1-32 for SIS3820.",
|
||||
)
|
||||
284
csaxs_bec/devices/epics/mcs_card/mcs_card_csaxs.py
Normal file
284
csaxs_bec/devices/epics/mcs_card/mcs_card_csaxs.py
Normal file
@@ -0,0 +1,284 @@
|
||||
"""Module for the MCSCard CSAXS implementation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
from threading import RLock
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
from bec_lib.logger import bec_logger
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, EpicsSignalRO, Kind, Signal
|
||||
from ophyd_devices import CompareStatus, ProgressSignal, TransitionStatus
|
||||
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||
|
||||
from csaxs_bec.devices.epics.mcs_card.mcs_card import (
|
||||
ACQUIREMODE,
|
||||
ACQUIRING,
|
||||
CHANNEL1SOURCE,
|
||||
CHANNELADVANCE,
|
||||
INPUTMODE,
|
||||
OUTPUTMODE,
|
||||
POLARITY,
|
||||
READMODE,
|
||||
MCSCard,
|
||||
)
|
||||
from csaxs_bec.devices.epics.xbpms import DiffXYSignal, SumSignal
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from bec_lib.devicemanager import DeviceManagerBase, ScanInfo
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class READYTOREAD(int, enum.Enum):
|
||||
|
||||
PROCESSING = 0
|
||||
DONE = 1
|
||||
|
||||
|
||||
class BPMDevice(Device):
|
||||
"""Class for BPM device of the MCSCard."""
|
||||
|
||||
current1 = Cpt(Signal, kind=Kind.normal, doc="Normalized current 1")
|
||||
current2 = Cpt(Signal, kind=Kind.normal, doc="Normalized current 2")
|
||||
current3 = Cpt(Signal, kind=Kind.normal, doc="Normalized current 3")
|
||||
current4 = Cpt(Signal, kind=Kind.normal, doc="Normalized current 4")
|
||||
count_time = Cpt(Signal, kind=Kind.normal, doc="Count time for bpm signal counts")
|
||||
sum = Cpt(SumSignal, kind="hinted", doc="Sum of all currents")
|
||||
x = Cpt(
|
||||
DiffXYSignal,
|
||||
sum1=["current1", "current2"],
|
||||
sum2=["current3", "current4"],
|
||||
doc="X difference signal",
|
||||
)
|
||||
y = Cpt(
|
||||
DiffXYSignal,
|
||||
sum1=["current1", "current3"],
|
||||
sum2=["current2", "current4"],
|
||||
doc="Y difference signal",
|
||||
)
|
||||
diag = Cpt(
|
||||
DiffXYSignal,
|
||||
sum1=["current1", "current4"],
|
||||
sum2=["current2", "current3"],
|
||||
doc="Diagonal difference signal",
|
||||
)
|
||||
|
||||
|
||||
class MCSRaw(Device):
|
||||
"""Class for BPM device of the MCSCard with normalized currents."""
|
||||
|
||||
mca1 = Cpt(Signal, kind=Kind.normal, doc="Raw counts on mca1 channel")
|
||||
mca2 = Cpt(Signal, kind=Kind.normal, doc="Raw counts on mca2 channel")
|
||||
mca3 = Cpt(Signal, kind=Kind.normal, doc="Raw counts on mca3 channel")
|
||||
mca4 = Cpt(Signal, kind=Kind.normal, doc="Raw counts on mca4 channel")
|
||||
mca5 = Cpt(Signal, kind=Kind.normal, doc="Raw counts on mca5 channel")
|
||||
|
||||
|
||||
class MCSCardCSAXS(PSIDeviceBase, MCSCard):
|
||||
"""
|
||||
Implementation of the MCSCard SIS3820 for CSAXS, prefix 'X12SA-MCS:'.
|
||||
The basic functionality is inherited from the MCSCard class.
|
||||
"""
|
||||
|
||||
ready_to_read = Cpt(
|
||||
Signal,
|
||||
kind=Kind.omitted,
|
||||
doc="Signal that indicates if mcs card is ready to be read from after triggers. 0 not ready, 1 ready",
|
||||
)
|
||||
progress: ProgressSignal = Cpt(ProgressSignal, name="progress")
|
||||
# Make this an async signal..
|
||||
mcs = Cpt(
|
||||
MCSRaw,
|
||||
name="mcs",
|
||||
kind=Kind.normal,
|
||||
doc="MCS device with raw current and count time readings",
|
||||
)
|
||||
bpm = Cpt(
|
||||
BPMDevice,
|
||||
name="bpm",
|
||||
kind=Kind.normal,
|
||||
doc="BPM device for MCSCard with count times and normalized currents",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
prefix: str = "",
|
||||
scan_info: ScanInfo | None = None,
|
||||
device_manager: DeviceManagerBase | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Initialize the MCSCardCSAXS with the given arguments and keyword arguments.
|
||||
"""
|
||||
super().__init__(
|
||||
name=name, prefix=prefix, scan_info=scan_info, device_manager=device_manager, **kwargs
|
||||
)
|
||||
self._mcs_clock = 1e7 # 10MHz clock -> 1e7 Hz
|
||||
self._pv_timeout = 3 # TODO remove timeout once #129 in ophyd_devices is solved
|
||||
self._rlock = RLock() # Needed to ensure thread safety for counter updates
|
||||
self.counter_mapping = { # Any mca counter that should be updated has to be added here
|
||||
f"{self.counters.name}_mca1": "current1",
|
||||
f"{self.counters.name}_mca2": "current2",
|
||||
f"{self.counters.name}_mca3": "current3",
|
||||
f"{self.counters.name}_mca4": "current4",
|
||||
f"{self.counters.name}_mca5": "count_time",
|
||||
}
|
||||
self.counter_updated = []
|
||||
|
||||
def on_connected(self):
|
||||
"""
|
||||
Called when the device is connected.
|
||||
"""
|
||||
# Make sure card is not running
|
||||
self.stop_all.put(1)
|
||||
|
||||
# TODO Check channel1_source !!
|
||||
self.channel_advance.set(CHANNELADVANCE.EXTERNAL).wait(timeout=self._pv_timeout)
|
||||
self.channel1_source.set(CHANNEL1SOURCE.EXTERNAL).wait(timeout=self._pv_timeout)
|
||||
self.prescale.set(1).wait(timeout=self._pv_timeout)
|
||||
# Set the user LED to off
|
||||
self.user_led.set(0).wait(timeout=self._pv_timeout)
|
||||
# Only channel 1-5 are connected so far, adjust if more are needed
|
||||
self.mux_output.set(5).wait(timeout=self._pv_timeout)
|
||||
# Set the input and output modes & polarities
|
||||
self.input_mode.set(INPUTMODE.MODE_3).wait(timeout=self._pv_timeout)
|
||||
self.input_polarity.set(POLARITY.NORMAL).wait(timeout=self._pv_timeout)
|
||||
self.output_mode.set(OUTPUTMODE.MODE_2).wait(timeout=self._pv_timeout)
|
||||
self.output_polarity.set(POLARITY.NORMAL).wait(timeout=self._pv_timeout)
|
||||
self.count_on_start.set(0).wait(timeout=self._pv_timeout)
|
||||
|
||||
# Set appropriate read mode
|
||||
self.read_mode.set(READMODE.PASSIVE).wait(timeout=self._pv_timeout)
|
||||
|
||||
# Set the acquire mode
|
||||
self.acquire_mode.set(ACQUIREMODE.MCS).wait(timeout=self._pv_timeout)
|
||||
|
||||
# Subscribe the progress signal
|
||||
self.current_channel.subscribe(self._progress_update, run=False)
|
||||
|
||||
# Subscribe to the mca updates
|
||||
for name in self.counter_mapping.keys():
|
||||
sig: EpicsSignalRO = getattr(self.counters, name.split("_")[-1])
|
||||
sig.subscribe(self._on_counter_update, run=False)
|
||||
|
||||
def _on_counter_update(self, value, **kwargs) -> None:
|
||||
"""
|
||||
Callback for counter updates of the mca channels (1-32).
|
||||
|
||||
The raw data is pushed to the mcs sub-device (MCSRaw). We need to ensure that
|
||||
the MCSRaw device has all signals defined for which we want to push the values.
|
||||
|
||||
As we may receive multiple readings per point, e.g. if frames_per_trigger > 1,
|
||||
we also create a mean value for the counter signals. These are then pushed to the bpm device
|
||||
for plotting and further processing. The signal names are defined and mapped in the
|
||||
self.counter_mapping dictionary & the bpm sub-device.
|
||||
|
||||
There are multiple mca channels, each giving individual updates. We want to ensure that
|
||||
each is updated before we signal that we are ready to read. In future, these signals may
|
||||
become asynchronous, but we first need to ensure that we can properly combine monitored
|
||||
signals with async signals for plotting. Until then, we will keep this logic.
|
||||
"""
|
||||
with self._rlock:
|
||||
# Retrieve the signal object which executes this callback
|
||||
signal = kwargs.get("obj", None)
|
||||
if signal is None: # This should never happen, but just in case
|
||||
logger.info(f"Called without 'obj' in kwargs: {kwargs}")
|
||||
return
|
||||
# Get the maped signal name from the mapping dictionary
|
||||
mapped_signal_name = self.counter_mapping.get(signal.name, None)
|
||||
# If we did not map the signal name in counter_mapping, but receive an update
|
||||
# we will skip it.
|
||||
if mapped_signal_name is None:
|
||||
return
|
||||
# Push the raw values of the mca channels. The signal name has to be defined
|
||||
# in the self.mcs sub-device (MCSRaw) to be able to push the values. Otherwise
|
||||
# we will skip the update.
|
||||
mca_raw = getattr(self.mcs, signal.name.split("_")[-1], None)
|
||||
if mca_raw is None:
|
||||
return
|
||||
# In case there was more than one value received, i.e. frames_per_trigger > 1,
|
||||
# we will receive a np.array of values.
|
||||
if isinstance(value, np.ndarray):
|
||||
# We push the raw values as a list to the mca_raw signal
|
||||
# And otherwise compute the mean value for plotting of counter signals
|
||||
mca_raw.put(value.tolist())
|
||||
# compute the count_time in seconds
|
||||
if mapped_signal_name == "count_time":
|
||||
value = value / self._mcs_clock
|
||||
value = float(value.mean())
|
||||
else:
|
||||
# We received a single value, so we can directly push it
|
||||
mca_raw.put(value)
|
||||
# compute the count_time in seconds
|
||||
if mapped_signal_name == "count_time":
|
||||
value = value / self._mcs_clock
|
||||
|
||||
# Get the mapped signal from the bpm device and update it
|
||||
sig = getattr(self.bpm, mapped_signal_name)
|
||||
sig.put(value)
|
||||
self.counter_updated.append(signal.name)
|
||||
# Once all mca channels have been updated, we can signal that we are ready to read
|
||||
received_all_updates = set(self.counter_updated) == set(self.counter_mapping.keys())
|
||||
if received_all_updates:
|
||||
self.ready_to_read.put(READYTOREAD.DONE)
|
||||
# The reset of the signal is done in the on_trigger method of ddg1 for the next trigger
|
||||
self.counter_updated.clear() # Clear the list for the next update cycle
|
||||
|
||||
def _progress_update(self, value, **kwargs) -> None:
|
||||
"""Callback for progress updates from ophyd subscription on current_channel."""
|
||||
# This logic needs to be further refined as this is currently reporting the progress
|
||||
# of a single trigger from BEC within a burst scan.
|
||||
frames_per_trigger = self.scan_info.msg.scan_parameters.get("frames_per_trigger", 1)
|
||||
self.progress.put(
|
||||
value=value, max_value=frames_per_trigger, done=bool(value == frames_per_trigger)
|
||||
)
|
||||
|
||||
def on_stage(self) -> None:
|
||||
"""
|
||||
Called when the device is staged.
|
||||
"""
|
||||
self.erase_all.set(1).wait(timeout=self._pv_timeout)
|
||||
triggers = self.scan_info.msg.scan_parameters.get("frames_per_trigger", 1)
|
||||
self.preset_real.set(0).wait(timeout=self._pv_timeout)
|
||||
self.num_use_all.set(triggers).wait(timeout=self._pv_timeout)
|
||||
|
||||
def on_unstage(self) -> None:
|
||||
"""
|
||||
Called when the device is unstaged.
|
||||
"""
|
||||
self.stop_all.put(1)
|
||||
self.ready_to_read.put(READYTOREAD.DONE)
|
||||
# TODO why 0?
|
||||
self.erase_all.set(0).wait(timeout=self._pv_timeout)
|
||||
|
||||
def on_trigger(self) -> None:
|
||||
status = TransitionStatus(
|
||||
self.ready_to_read, strict=True, transitions=[READYTOREAD.PROCESSING, READYTOREAD.DONE]
|
||||
)
|
||||
self.cancel_on_stop(status)
|
||||
return status
|
||||
|
||||
def on_pre_scan(self) -> None:
|
||||
"""
|
||||
Called before the scan starts.
|
||||
"""
|
||||
|
||||
def on_complete(self) -> CompareStatus:
|
||||
"""On scan completion."""
|
||||
# Check if we should get a signal based on updates from the MCA channels
|
||||
status = CompareStatus(self.acquiring, ACQUIRING.DONE)
|
||||
self.cancel_on_stop(status)
|
||||
return status
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""
|
||||
Called when the scan is stopped.
|
||||
"""
|
||||
self.stop_all.put(1)
|
||||
self.ready_to_read.put(READYTOREAD.DONE)
|
||||
# Reset the progress signal
|
||||
# self.progress.put(0, done=True)
|
||||
@@ -1,319 +0,0 @@
|
||||
import enum
|
||||
import threading
|
||||
from collections import defaultdict
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import bec_logger, messages
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, EpicsSignal, EpicsSignalRO
|
||||
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
|
||||
CustomDetectorMixin,
|
||||
PSIDetectorBase,
|
||||
)
|
||||
from ophyd_devices.utils import bec_utils
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class MCSError(Exception):
|
||||
"""Base class for exceptions in this module."""
|
||||
|
||||
|
||||
class MCSTimeoutError(MCSError):
|
||||
"""Raise when MCS card runs into a timeout"""
|
||||
|
||||
|
||||
class TriggerSource(int, enum.Enum):
|
||||
"""Trigger source for mcs card - see manual for more information"""
|
||||
|
||||
MODE0 = 0
|
||||
MODE1 = 1
|
||||
MODE2 = 2
|
||||
MODE3 = 3
|
||||
MODE4 = 4
|
||||
MODE5 = 5
|
||||
MODE6 = 6
|
||||
|
||||
|
||||
class ChannelAdvance(int, enum.Enum):
|
||||
"""Channel advance pixel mode for mcs card - see manual for more information"""
|
||||
|
||||
INTERNAL = 0
|
||||
EXTERNAL = 1
|
||||
|
||||
|
||||
class ReadoutMode(int, enum.Enum):
|
||||
"""Readout mode for mcs card - see manual for more information"""
|
||||
|
||||
PASSIVE = 0
|
||||
EVENT = 1
|
||||
IO_INTR = 2
|
||||
FREQ_0_1HZ = 3
|
||||
FREQ_0_2HZ = 4
|
||||
FREQ_0_5HZ = 5
|
||||
FREQ_1HZ = 6
|
||||
FREQ_2HZ = 7
|
||||
FREQ_5HZ = 8
|
||||
FREQ_10HZ = 9
|
||||
FREQ_100HZ = 10
|
||||
|
||||
|
||||
class MCSSetup(CustomDetectorMixin):
|
||||
"""Setup mixin class for the MCS card"""
|
||||
|
||||
def __init__(self, *args, parent: Device = None, **kwargs) -> None:
|
||||
super().__init__(*args, parent=parent, **kwargs)
|
||||
self._lock = threading.RLock()
|
||||
self._stream_ttl = 1800
|
||||
self.acquisition_done = False
|
||||
self.counter = 0
|
||||
self.n_points = 0
|
||||
self.mca_names = [
|
||||
signal for signal in self.parent.component_names if signal.startswith("mca")
|
||||
]
|
||||
self.mca_data = defaultdict(lambda: [])
|
||||
|
||||
def on_init(self) -> None:
|
||||
"""Init sequence for the detector"""
|
||||
self.initialize_detector()
|
||||
self.initialize_detector_backend()
|
||||
|
||||
def initialize_detector(self) -> None:
|
||||
"""Initialize detector"""
|
||||
# External trigger for pixel advance
|
||||
self.parent.channel_advance.set(ChannelAdvance.EXTERNAL)
|
||||
# Use internal clock for channel 1
|
||||
self.parent.channel1_source.set(ChannelAdvance.INTERNAL)
|
||||
self.parent.user_led.set(0)
|
||||
# Set number of channels to 5
|
||||
self.parent.mux_output.set(5)
|
||||
# Trigger Mode used for cSAXS
|
||||
self.parent.input_mode.set(TriggerSource.MODE3)
|
||||
# specify polarity of trigger signals
|
||||
self.parent.input_polarity.set(0)
|
||||
self.parent.output_polarity.set(1)
|
||||
# do not start counting on start
|
||||
self.parent.count_on_start.set(0)
|
||||
self.stop_detector()
|
||||
|
||||
def initialize_detector_backend(self) -> None:
|
||||
"""Initialize detector backend"""
|
||||
for mca in self.mca_names:
|
||||
signal = getattr(self.parent, mca)
|
||||
signal.subscribe(self._on_mca_data, run=False)
|
||||
self.parent.current_channel.subscribe(self._progress_update, run=False)
|
||||
|
||||
def _progress_update(self, value, **kwargs) -> None:
|
||||
"""Progress update on the scan"""
|
||||
num_lines = self.parent.num_lines.get()
|
||||
max_value = self.parent.scaninfo.num_points
|
||||
# self.counter seems to be a deprecated variable from a former implementation of the mcs card
|
||||
# pylint: disable=protected-access
|
||||
self.parent._run_subs(
|
||||
sub_type=self.parent.SUB_PROGRESS,
|
||||
value=self.counter * int(self.parent.scaninfo.num_points / num_lines) + value,
|
||||
max_value=max_value,
|
||||
# TODO check if that is correct with
|
||||
done=bool(max_value == value), # == self.counter),
|
||||
)
|
||||
|
||||
def _on_mca_data(self, *args, obj=None, value=None, **kwargs) -> None:
|
||||
"""Callback function for scan progress"""
|
||||
with self._lock:
|
||||
if not isinstance(value, (list, np.ndarray)):
|
||||
return
|
||||
self.mca_data[obj.attr_name] = value
|
||||
if len(self.mca_names) != len(self.mca_data):
|
||||
return
|
||||
self.acquisition_done = True
|
||||
self._send_data_to_bec()
|
||||
self.mca_data = defaultdict(lambda: [])
|
||||
|
||||
def _send_data_to_bec(self) -> None:
|
||||
"""Sends bundled data to BEC"""
|
||||
if self.parent.scaninfo.scan_msg is None:
|
||||
return
|
||||
metadata = self.parent.scaninfo.scan_msg.metadata
|
||||
metadata.update({"async_update": "append", "num_lines": self.parent.num_lines.get()})
|
||||
msg = messages.DeviceMessage(
|
||||
signals=dict(self.mca_data), metadata=self.parent.scaninfo.scan_msg.metadata
|
||||
)
|
||||
self.parent.connector.xadd(
|
||||
topic=MessageEndpoints.device_async_readback(
|
||||
scan_id=self.parent.scaninfo.scan_id, device=self.parent.name
|
||||
),
|
||||
msg={"data": msg},
|
||||
expire=self._stream_ttl,
|
||||
)
|
||||
|
||||
def on_stage(self) -> None:
|
||||
"""Stage detector"""
|
||||
self.prepare_detector()
|
||||
self.prepare_detector_backend()
|
||||
|
||||
def prepare_detector(self) -> None:
|
||||
"""Prepare detector for scan"""
|
||||
self.set_acquisition_params()
|
||||
self.parent.input_mode.set(TriggerSource.MODE3)
|
||||
|
||||
def set_acquisition_params(self) -> None:
|
||||
"""Set acquisition parameters for scan"""
|
||||
if self.parent.scaninfo.scan_type == "step":
|
||||
self.n_points = int(self.parent.scaninfo.frames_per_trigger) * int(
|
||||
self.parent.scaninfo.num_points
|
||||
)
|
||||
elif self.parent.scaninfo.scan_type == "fly":
|
||||
self.n_points = int(self.parent.scaninfo.num_points) # / int(self.num_lines.get()))
|
||||
else:
|
||||
raise MCSError(f"Scantype {self.parent.scaninfo} not implemented for MCS card")
|
||||
if self.n_points > 10000:
|
||||
raise MCSError(
|
||||
f"Requested number of points N={self.n_points} exceeds hardware limit of mcs card"
|
||||
" 10000 (N-1)"
|
||||
)
|
||||
self.parent.num_use_all.set(self.n_points)
|
||||
self.parent.preset_real.set(0)
|
||||
|
||||
def prepare_detector_backend(self) -> None:
|
||||
"""Prepare detector backend for scan"""
|
||||
self.parent.erase_all.set(1)
|
||||
self.parent.read_mode.set(ReadoutMode.EVENT)
|
||||
|
||||
def arm_acquisition(self) -> None:
|
||||
"""Arm detector for acquisition"""
|
||||
self.counter = 0
|
||||
self.parent.erase_start.set(1)
|
||||
|
||||
def on_unstage(self) -> None:
|
||||
"""Unstage detector"""
|
||||
pass
|
||||
|
||||
def on_complete(self) -> None:
|
||||
"""Complete detector"""
|
||||
self.finished(timeout=self.parent.TIMEOUT_FOR_SIGNALS)
|
||||
|
||||
def finished(self, timeout: int = 5) -> None:
|
||||
"""Check if acquisition is finished, if not successful, rais MCSTimeoutError"""
|
||||
signal_conditions = [
|
||||
(lambda: self.acquisition_done, True),
|
||||
(self.parent.acquiring.get, 0), # Considering making a enum.Int class for this state
|
||||
]
|
||||
if not self.wait_for_signals(
|
||||
signal_conditions=signal_conditions,
|
||||
timeout=timeout,
|
||||
check_stopped=True,
|
||||
all_signals=True,
|
||||
):
|
||||
total_frames = self.counter * int(
|
||||
self.parent.scaninfo.num_points / self.parent.num_lines.get()
|
||||
) + max(self.parent.current_channel.get(), 0)
|
||||
raise MCSTimeoutError(
|
||||
f"Reached timeout with mcs in state {self.parent.acquiring.get()} and"
|
||||
f" {total_frames} frames arriving at the mcs card"
|
||||
)
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""Stop detector"""
|
||||
self.stop_detector()
|
||||
self.stop_detector_backend()
|
||||
|
||||
def stop_detector(self) -> None:
|
||||
"""Stop detector"""
|
||||
self.parent.stop_all.set(1)
|
||||
|
||||
def stop_detector_backend(self) -> None:
|
||||
"""Stop acquisition of data"""
|
||||
self.acquisition_done = True
|
||||
|
||||
|
||||
class SIS38XX(Device):
|
||||
"""SIS38XX card for access to EPICs PVs at cSAXS beamline"""
|
||||
|
||||
|
||||
class MCScSAXS(PSIDetectorBase):
|
||||
"""MCS card for cSAXS for implementation at cSAXS beamline"""
|
||||
|
||||
USER_ACCESS = []
|
||||
SUB_PROGRESS = "progress"
|
||||
SUB_VALUE = "value"
|
||||
_default_sub = SUB_VALUE
|
||||
|
||||
# specify Setup class
|
||||
custom_prepare_cls = MCSSetup
|
||||
# specify minimum readout time for detector
|
||||
MIN_READOUT = 0
|
||||
TIMEOUT_FOR_SIGNALS = 5
|
||||
|
||||
# PV access to SISS38XX card
|
||||
# Acquisition
|
||||
erase_all = Cpt(EpicsSignal, "EraseAll")
|
||||
erase_start = Cpt(EpicsSignal, "EraseStart") # ,trigger_value=1
|
||||
start_all = Cpt(EpicsSignal, "StartAll")
|
||||
stop_all = Cpt(EpicsSignal, "StopAll")
|
||||
acquiring = Cpt(EpicsSignal, "Acquiring")
|
||||
preset_real = Cpt(EpicsSignal, "PresetReal")
|
||||
elapsed_real = Cpt(EpicsSignal, "ElapsedReal")
|
||||
read_mode = Cpt(EpicsSignal, "ReadAll.SCAN")
|
||||
read_all = Cpt(EpicsSignal, "DoReadAll.VAL") # ,trigger_value=1
|
||||
num_use_all = Cpt(EpicsSignal, "NuseAll")
|
||||
current_channel = Cpt(EpicsSignal, "CurrentChannel")
|
||||
dwell = Cpt(EpicsSignal, "Dwell")
|
||||
channel_advance = Cpt(EpicsSignal, "ChannelAdvance")
|
||||
count_on_start = Cpt(EpicsSignal, "CountOnStart")
|
||||
software_channel_advance = Cpt(EpicsSignal, "SoftwareChannelAdvance")
|
||||
channel1_source = Cpt(EpicsSignal, "Channel1Source")
|
||||
prescale = Cpt(EpicsSignal, "Prescale")
|
||||
enable_client_wait = Cpt(EpicsSignal, "EnableClientWait")
|
||||
client_wait = Cpt(EpicsSignal, "ClientWait")
|
||||
acquire_mode = Cpt(EpicsSignal, "AcquireMode")
|
||||
mux_output = Cpt(EpicsSignal, "MUXOutput")
|
||||
user_led = Cpt(EpicsSignal, "UserLED")
|
||||
input_mode = Cpt(EpicsSignal, "InputMode")
|
||||
input_polarity = Cpt(EpicsSignal, "InputPolarity")
|
||||
output_mode = Cpt(EpicsSignal, "OutputMode")
|
||||
output_polarity = Cpt(EpicsSignal, "OutputPolarity")
|
||||
model = Cpt(EpicsSignalRO, "Model", string=True)
|
||||
firmware = Cpt(EpicsSignalRO, "Firmware")
|
||||
max_channels = Cpt(EpicsSignalRO, "MaxChannels")
|
||||
|
||||
# PV access to MCA signals
|
||||
mca1 = Cpt(EpicsSignalRO, "mca1.VAL", auto_monitor=True)
|
||||
mca3 = Cpt(EpicsSignalRO, "mca3.VAL", auto_monitor=True)
|
||||
mca4 = Cpt(EpicsSignalRO, "mca4.VAL", auto_monitor=True)
|
||||
current_channel = Cpt(EpicsSignalRO, "CurrentChannel", auto_monitor=True)
|
||||
|
||||
# Custom signal readout from device config
|
||||
num_lines = Cpt(
|
||||
bec_utils.ConfigSignal, name="num_lines", kind="config", config_storage_name="mcs_config"
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
prefix="",
|
||||
*,
|
||||
name,
|
||||
kind=None,
|
||||
parent=None,
|
||||
device_manager=None,
|
||||
mcs_config=None,
|
||||
**kwargs,
|
||||
):
|
||||
self.mcs_config = {f"{name}_num_lines": 1}
|
||||
if mcs_config is not None:
|
||||
# pylint: disable=expression-not-assigned
|
||||
[self.mcs_config.update({f"{name}_{key}": value}) for key, value in mcs_config.items()]
|
||||
|
||||
super().__init__(
|
||||
prefix=prefix,
|
||||
name=name,
|
||||
kind=kind,
|
||||
parent=parent,
|
||||
device_manager=device_manager,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
# Automatically connect to test environmenr if directly invoked
|
||||
if __name__ == "__main__":
|
||||
mcs = MCScSAXS(name="mcs", prefix="X12SA-MCS:", sim_mode=True)
|
||||
@@ -1,4 +1,5 @@
|
||||
import time
|
||||
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, EpicsSignalRO, Signal
|
||||
|
||||
@@ -22,10 +23,10 @@ class SumSignal(Signal):
|
||||
|
||||
def describe(self):
|
||||
source = [
|
||||
self.parent.current1.pvname,
|
||||
self.parent.current2.pvname,
|
||||
self.parent.current3.pvname,
|
||||
self.parent.current4.pvname,
|
||||
self.parent.current1.describe()[self.parent.current1.name]["source"],
|
||||
self.parent.current2.describe()[self.parent.current2.name]["source"],
|
||||
self.parent.current3.describe()[self.parent.current3.name]["source"],
|
||||
self.parent.current4.describe()[self.parent.current4.name]["source"],
|
||||
]
|
||||
source = " / ".join(source)
|
||||
desc = {
|
||||
@@ -33,7 +34,9 @@ class SumSignal(Signal):
|
||||
"dtype": "number",
|
||||
"source": f"PV: {source}",
|
||||
"units": "",
|
||||
"precision": self.parent.current1.precision,
|
||||
"precision": (
|
||||
self.parent.current1.precision if hasattr(self.parent.current1, "precision") else 0
|
||||
),
|
||||
}
|
||||
return desc
|
||||
|
||||
@@ -64,23 +67,36 @@ class DiffXYSignal(Signal):
|
||||
return (summed_1 - summed_2) / _sum
|
||||
|
||||
def describe(self):
|
||||
source = [getattr(self.parent, signal).pvname for signal in self.sum1 + self.sum2]
|
||||
source = [
|
||||
getattr(self.parent, signal).describe()[getattr(self.parent, signal).name]["source"]
|
||||
for signal in self.sum1 + self.sum2
|
||||
]
|
||||
source = " / ".join(source)
|
||||
desc = {
|
||||
"shape": [],
|
||||
"dtype": "number",
|
||||
"source": f"PV: {source}",
|
||||
"units": "",
|
||||
"precision": self.parent.current1.precision,
|
||||
"precision": (
|
||||
self.parent.current1.precision if hasattr(self.parent.current1, "precision") else 0
|
||||
),
|
||||
}
|
||||
return desc
|
||||
|
||||
|
||||
class BPMDevice(Device):
|
||||
current1 = Cpt(EpicsSignalRO, ":Current1:MeanValue_RBV", kind="normal", doc="Current 1", auto_monitor=True)
|
||||
current2 = Cpt(EpicsSignalRO, ":Current2:MeanValue_RBV", kind="normal", doc="Current 2", auto_monitor=True)
|
||||
current3 = Cpt(EpicsSignalRO, ":Current3:MeanValue_RBV", kind="normal", doc="Current 3", auto_monitor=True)
|
||||
current4 = Cpt(EpicsSignalRO, ":Current4:MeanValue_RBV", kind="normal", doc="Current 4", auto_monitor=True)
|
||||
current1 = Cpt(
|
||||
EpicsSignalRO, ":Current1:MeanValue_RBV", kind="normal", doc="Current 1", auto_monitor=True
|
||||
)
|
||||
current2 = Cpt(
|
||||
EpicsSignalRO, ":Current2:MeanValue_RBV", kind="normal", doc="Current 2", auto_monitor=True
|
||||
)
|
||||
current3 = Cpt(
|
||||
EpicsSignalRO, ":Current3:MeanValue_RBV", kind="normal", doc="Current 3", auto_monitor=True
|
||||
)
|
||||
current4 = Cpt(
|
||||
EpicsSignalRO, ":Current4:MeanValue_RBV", kind="normal", doc="Current 4", auto_monitor=True
|
||||
)
|
||||
sum = Cpt(SumSignal, kind="hinted", doc="Sum of all currents")
|
||||
x = Cpt(
|
||||
DiffXYSignal,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from .ids_camera_new import IDSCamera
|
||||
|
||||
274
csaxs_bec/devices/ids_cameras/base_integration/camera.py
Normal file
274
csaxs_bec/devices/ids_cameras/base_integration/camera.py
Normal file
@@ -0,0 +1,274 @@
|
||||
"""
|
||||
This module provides a Camera class for handling IDS cameras using the pyueye library,
|
||||
that links to the vendors C++ SDK. Details about the camera's C++ SDK API can be found
|
||||
in the IDS Software Suite 4.96.1 documentation:
|
||||
(https://www.1stvision.com/cameras/IDS/IDS-manuals/uEye_Manual/sdk_einleitung_schnellstart.html)
|
||||
|
||||
Here, we follow a procedure to set up the camera, configure its basic parameters and
|
||||
allow automated capturing of images. The IDSCameraObject class is the low-level interface,
|
||||
and requires the pyueye library and appropriate DLL files on the system. The Camera class
|
||||
provides a high level interface which only creates the IDSCameraObject instance when the
|
||||
on_connect method is called. This allows for lazy initialization of the camera, and
|
||||
CI/CD pipelines can run without the pyueye library or the related DLLs installed on the system.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import atexit
|
||||
from typing import Literal
|
||||
|
||||
import numpy as np
|
||||
from bec_lib.logger import bec_logger
|
||||
|
||||
from csaxs_bec.devices.ids_cameras.base_integration.utils import check_error
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
try:
|
||||
from pyueye import ueye
|
||||
except ImportError as exc:
|
||||
logger.warning(f"The pyueye library is not properly installed : {exc}")
|
||||
ueye = None # type: ignore[assignment]
|
||||
|
||||
|
||||
class IDSCameraObject:
|
||||
"""Low-level base class for IDS Camera object.
|
||||
|
||||
Args:
|
||||
device_id (int): The ID of the camera device. # e.g. 201; check idscamera tool
|
||||
m_n_colormode (int): Color mode for the camera. # 1 for cSAXS color cameras
|
||||
bits_per_pixel (int): Number of bits per pixel for the camera. # 24 for color cameras, 8 for monochrome cameras
|
||||
"""
|
||||
|
||||
def __init__(self, device_id: int, m_n_colormode, bits_per_pixel):
|
||||
if ueye is None:
|
||||
raise ImportError(
|
||||
"The pyueye library is not installed or library files are missing. Please check your Python environment or library paths."
|
||||
)
|
||||
self.ueye = ueye
|
||||
self._device_id = device_id
|
||||
self.h_cam = ueye.HIDS(device_id)
|
||||
self.s_info = ueye.SENSORINFO()
|
||||
self.c_info = ueye.CAMINFO()
|
||||
self.rect_roi = ueye.IS_RECT()
|
||||
self.pc_image_mem = ueye.c_mem_p()
|
||||
self.mem_id = ueye.int()
|
||||
self.pitch = ueye.INT()
|
||||
self.m_n_colormode = ueye.INT(m_n_colormode)
|
||||
self.n_bits_per_pixel = ueye.INT(bits_per_pixel)
|
||||
self.bytes_per_pixel = int(self.n_bits_per_pixel / 8)
|
||||
|
||||
# Sequence to initialize the camera
|
||||
check_error(ueye.is_InitCamera(self.h_cam, None), "IDSCameraObject")
|
||||
check_error(ueye.is_GetSensorInfo(self.h_cam, self.s_info), "IDSCameraObject")
|
||||
check_error(ueye.is_GetCameraInfo(self.h_cam, self.c_info), "IDSCameraObject")
|
||||
check_error(ueye.is_ResetToDefault(self.h_cam), "IDSCameraObject")
|
||||
check_error(ueye.is_SetDisplayMode(self.h_cam, ueye.IS_SET_DM_DIB), "IDSCameraObject")
|
||||
|
||||
if (
|
||||
int.from_bytes(self.s_info.nColorMode.value, byteorder="big")
|
||||
== self.ueye.IS_COLORMODE_BAYER
|
||||
):
|
||||
logger.info("Bayer color mode detected.")
|
||||
# setup the color depth to the current windows setting
|
||||
self.ueye.is_GetColorDepth(
|
||||
self.h_cam, self.n_bits_per_pixel, self.m_n_colormode
|
||||
) # TODO This raises an error - maybe check the m_n_colormode value
|
||||
self.bytes_per_pixel = int(self.n_bits_per_pixel / 8)
|
||||
elif (
|
||||
int.from_bytes(self.s_info.nColorMode.value, byteorder="big")
|
||||
== self.ueye.IS_COLORMODE_CBYCRY
|
||||
):
|
||||
# for color camera models use RGB32 mode
|
||||
self.m_n_colormode = self.ueye.IS_CM_BGRA8_PACKED
|
||||
self.n_bits_per_pixel = self.ueye.INT(32)
|
||||
self.bytes_per_pixel = int(self.n_bits_per_pixel / 8)
|
||||
elif (
|
||||
int.from_bytes(self.s_info.nColorMode.value, byteorder="big")
|
||||
== self.ueye.IS_COLORMODE_MONOCHROME
|
||||
):
|
||||
# for color camera models use RGB32 mode
|
||||
self.m_n_colormode = self.ueye.IS_CM_MONO8
|
||||
self.n_bits_per_pixel = self.ueye.INT(8)
|
||||
self.bytes_per_pixel = int(self.n_bits_per_pixel / 8)
|
||||
else:
|
||||
# for monochrome camera models use Y8 mode
|
||||
self.m_n_colormode = self.ueye.IS_CM_MONO8
|
||||
self.n_bits_per_pixel = self.ueye.INT(8)
|
||||
self.bytes_per_pixel = int(self.n_bits_per_pixel / 8)
|
||||
logger.info("Monochrome camera mode detected.")
|
||||
|
||||
# Can be used to set the size and position of an "area of interest"(AOI) within an image
|
||||
check_error(
|
||||
self.ueye.is_AOI(
|
||||
self.h_cam,
|
||||
self.ueye.IS_AOI_IMAGE_GET_AOI,
|
||||
self.rect_roi,
|
||||
self.ueye.sizeof(self.rect_roi),
|
||||
),
|
||||
"IDSCameraObject",
|
||||
)
|
||||
self.width = self.rect_roi.s32Width
|
||||
self.height = self.rect_roi.s32Height
|
||||
|
||||
check_error(
|
||||
self.ueye.is_AllocImageMem(
|
||||
self.h_cam,
|
||||
self.width,
|
||||
self.height,
|
||||
self.n_bits_per_pixel,
|
||||
self.pc_image_mem,
|
||||
self.mem_id,
|
||||
),
|
||||
"IDSCameraObject",
|
||||
)
|
||||
|
||||
check_error(
|
||||
self.ueye.is_SetImageMem(self.h_cam, self.pc_image_mem, self.mem_id), "IDSCameraObject"
|
||||
)
|
||||
check_error(self.ueye.is_SetColorMode(self.h_cam, self.m_n_colormode), "IDSCameraObject")
|
||||
|
||||
check_error(
|
||||
self.ueye.is_CaptureVideo(self.h_cam, self.ueye.IS_DONT_WAIT), "IDSCameraObject"
|
||||
)
|
||||
check_error(
|
||||
self.ueye.is_InquireImageMem(
|
||||
self.h_cam,
|
||||
self.pc_image_mem,
|
||||
self.mem_id,
|
||||
self.width,
|
||||
self.height,
|
||||
self.n_bits_per_pixel,
|
||||
self.pitch,
|
||||
),
|
||||
"IDSCameraObject",
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"IDSCameraObject\n\ndevice_id={self._device_id},\ns_info={self.s_info},\nc_info={self.c_info},\nrect_roi={self.rect_roi},\npc_image_mem={self.pc_image_mem},\nmem_id={self.mem_id},\npitch={self.pitch},\nm_n_colormode={self.m_n_colormode},\nn_bits_per_pixel={self.n_bits_per_pixel},\nbytes_per_pixel={self.bytes_per_pixel}"
|
||||
|
||||
|
||||
class Camera:
|
||||
"""High level camera base class for IDS cameras.
|
||||
|
||||
Args:
|
||||
camera_id (int): The ID of the camera device.
|
||||
m_n_colormode (Literal[0, 1, 2, 3]): Color mode for the camera.
|
||||
bits_per_pixel (Literal[8, 24]): Number of bits per pixel for the camera.
|
||||
live_mode (bool): Whether to enable live mode for the camera.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
camera_id: int,
|
||||
m_n_colormode: Literal[0, 1, 2, 3] = 1,
|
||||
bits_per_pixel: int = 24,
|
||||
connect: bool = True,
|
||||
):
|
||||
self.ueye = ueye
|
||||
self.camera_id = camera_id
|
||||
self._inputs = {"m_n_colormode": m_n_colormode, "bits_per_pixel": bits_per_pixel}
|
||||
self._connected = False
|
||||
self.cam = None
|
||||
atexit.register(self.on_disconnect)
|
||||
|
||||
if connect:
|
||||
self.on_connect()
|
||||
|
||||
def set_roi(self, x: int, y: int, width: int, height: int):
|
||||
"""Set the region of interest (ROI) for the camera."""
|
||||
rect_roi = ueye.IS_RECT()
|
||||
rect_roi.s32X = x
|
||||
rect_roi.s32Y = y
|
||||
rect_roi.s32Width = width
|
||||
rect_roi.s32Height = height
|
||||
|
||||
ret = self.ueye.is_AOI(
|
||||
self.cam.h_cam, self.ueye.IS_AOI_IMAGE_SET_AOI, rect_roi, self.ueye.sizeof(rect_roi)
|
||||
)
|
||||
check_error(ret, "IDSCameraObject")
|
||||
logger.info(f"ROI set to: {rect_roi}")
|
||||
|
||||
def on_connect(self):
|
||||
"""Connect to the camera and initialize it."""
|
||||
if self._connected:
|
||||
logger.warning("Camera is already connected.")
|
||||
return
|
||||
self.cam = IDSCameraObject(self.camera_id, **self._inputs)
|
||||
self._connected = True
|
||||
|
||||
def on_disconnect(self):
|
||||
"""Disconnect from the camera."""
|
||||
try:
|
||||
if self.cam and self.cam.h_cam:
|
||||
check_error(self.ueye.is_ExitCamera(self.cam.h_cam), "IDSCameraObject")
|
||||
self._connected = False
|
||||
self.cam = None
|
||||
logger.info("Camera disconnected.")
|
||||
except Exception as e:
|
||||
logger.info(f"Error during camera disconnection: {e}")
|
||||
|
||||
@property
|
||||
def exposure_time(self) -> float:
|
||||
"""Get the exposure time of the camera."""
|
||||
exposure = ueye.c_double()
|
||||
ret = self.ueye.is_Exposure(self.cam.h_cam, ueye.IS_EXPOSURE_CMD_GET_EXPOSURE, exposure, 8)
|
||||
check_error(ret, "IDSCameraObject")
|
||||
return exposure.value
|
||||
|
||||
@exposure_time.setter
|
||||
def exposure_time(self, value: float):
|
||||
"""Set the exposure time of the camera."""
|
||||
exposure = ueye.c_double(value)
|
||||
check_error(
|
||||
self.ueye.is_Exposure(self.cam.h_cam, ueye.IS_EXPOSURE_CMD_SET_EXPOSURE, exposure, 8),
|
||||
"IDSCameraObject",
|
||||
)
|
||||
|
||||
def set_auto_gain(self, enable: bool):
|
||||
"""Enable or disable auto gain."""
|
||||
enable = ueye.c_int(1) if enable else ueye.c_int(0)
|
||||
value_to_return = ueye.c_double()
|
||||
check_error(
|
||||
self.ueye.is_SetAutoParameter(
|
||||
self.cam.h_cam, ueye.IS_SET_ENABLE_AUTO_GAIN, enable, value_to_return
|
||||
),
|
||||
"IDSCameraObject",
|
||||
)
|
||||
|
||||
def set_auto_shutter(self, enable: bool):
|
||||
"""Enable or disable auto exposure."""
|
||||
enable = ueye.c_int(1) if enable else ueye.c_int(0)
|
||||
value_to_return = ueye.c_double()
|
||||
check_error(
|
||||
self.ueye.is_SetAutoParameter(
|
||||
self.cam.h_cam, ueye.IS_SET_ENABLE_AUTO_SHUTTER, enable, value_to_return
|
||||
),
|
||||
"IDSCameraObject",
|
||||
)
|
||||
|
||||
def get_image_data(self) -> np.ndarray | None:
|
||||
"""Get the image data from the camera."""
|
||||
if not self._connected:
|
||||
logger.warning("Camera is not connected.")
|
||||
return None
|
||||
array = self.ueye.get_data(
|
||||
self.cam.pc_image_mem,
|
||||
self.cam.width,
|
||||
self.cam.height,
|
||||
self.cam.n_bits_per_pixel,
|
||||
self.cam.pitch,
|
||||
copy=False,
|
||||
)
|
||||
if array is None:
|
||||
logger.error("Failed to get image data from the camera.")
|
||||
return None
|
||||
return np.reshape(
|
||||
array, (self.cam.height.value, self.cam.width.value, self.cam.bytes_per_pixel)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example usage
|
||||
camera = Camera(camera_id=201)
|
||||
camera.on_connect()
|
||||
282
csaxs_bec/devices/ids_cameras/base_integration/utils.py
Normal file
282
csaxs_bec/devices/ids_cameras/base_integration/utils.py
Normal file
@@ -0,0 +1,282 @@
|
||||
"""Utility functions and classes for IDS cameras using the pyueye library."""
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
try:
|
||||
from pyueye import ueye
|
||||
except ImportError as exc:
|
||||
logger.warning(f"The pyueye library is not properly installed : {exc}")
|
||||
ueye = None
|
||||
|
||||
if ueye is not None:
|
||||
error_codes = {
|
||||
ueye.IS_NO_SUCCESS: "No success",
|
||||
ueye.IS_SUCCESS: "Success",
|
||||
ueye.IS_INVALID_CAMERA_HANDLE: "Invalid camera handle",
|
||||
ueye.IS_INVALID_HANDLE: "Invalid handle",
|
||||
ueye.IS_IO_REQUEST_FAILED: "IO request failed",
|
||||
ueye.IS_CANT_OPEN_DEVICE: "Cannot open device",
|
||||
ueye.IS_CANT_CLOSE_DEVICE: "Cannot close device",
|
||||
ueye.IS_CANT_SETUP_MEMORY: "Cannot setup memory",
|
||||
ueye.IS_NO_HWND_FOR_ERROR_REPORT: "No HWND for error report",
|
||||
ueye.IS_ERROR_MESSAGE_NOT_CREATED: "Error message not created",
|
||||
ueye.IS_ERROR_STRING_NOT_FOUND: "Error string not found",
|
||||
ueye.IS_HOOK_NOT_CREATED: "Hook not created",
|
||||
ueye.IS_TIMER_NOT_CREATED: "Timer not created",
|
||||
ueye.IS_CANT_OPEN_REGISTRY: "Cannot open registry",
|
||||
ueye.IS_CANT_READ_REGISTRY: "Cannot read registry",
|
||||
ueye.IS_CANT_VALIDATE_BOARD: "Cannot validate board",
|
||||
ueye.IS_CANT_GIVE_BOARD_ACCESS: "Cannot give board access",
|
||||
ueye.IS_NO_IMAGE_MEM_ALLOCATED: "No image memory allocated",
|
||||
ueye.IS_CANT_CLEANUP_MEMORY: "Cannot clean up memory",
|
||||
ueye.IS_CANT_COMMUNICATE_WITH_DRIVER: "Cannot communicate with driver",
|
||||
ueye.IS_FUNCTION_NOT_SUPPORTED_YET: "Function not supported yet",
|
||||
ueye.IS_OPERATING_SYSTEM_NOT_SUPPORTED: "Operating system not supported",
|
||||
ueye.IS_INVALID_VIDEO_IN: "Invalid video input",
|
||||
ueye.IS_INVALID_IMG_SIZE: "Invalid image size",
|
||||
ueye.IS_INVALID_ADDRESS: "Invalid address",
|
||||
ueye.IS_INVALID_VIDEO_MODE: "Invalid video mode",
|
||||
ueye.IS_INVALID_AGC_MODE: "Invalid AGC mode",
|
||||
ueye.IS_INVALID_GAMMA_MODE: "Invalid gamma mode",
|
||||
ueye.IS_INVALID_SYNC_LEVEL: "Invalid sync level",
|
||||
ueye.IS_INVALID_CBARS_MODE: "Invalid color bars mode",
|
||||
ueye.IS_INVALID_COLOR_MODE: "Invalid color mode",
|
||||
ueye.IS_INVALID_SCALE_FACTOR: "Invalid scale factor",
|
||||
ueye.IS_INVALID_IMAGE_SIZE: "Invalid image size",
|
||||
ueye.IS_INVALID_IMAGE_POS: "Invalid image position",
|
||||
ueye.IS_INVALID_CAPTURE_MODE: "Invalid capture mode",
|
||||
ueye.IS_INVALID_RISC_PROGRAM: "Invalid RISC program",
|
||||
ueye.IS_INVALID_BRIGHTNESS: "Invalid brightness",
|
||||
ueye.IS_INVALID_CONTRAST: "Invalid contrast",
|
||||
ueye.IS_INVALID_SATURATION_U: "Invalid saturation U",
|
||||
ueye.IS_INVALID_SATURATION_V: "Invalid saturation V",
|
||||
ueye.IS_INVALID_HUE: "Invalid hue",
|
||||
ueye.IS_INVALID_HOR_FILTER_STEP: "Invalid horizontal filter step",
|
||||
ueye.IS_INVALID_VERT_FILTER_STEP: "Invalid vertical filter step",
|
||||
ueye.IS_INVALID_EEPROM_READ_ADDRESS: "Invalid EEPROM read address",
|
||||
ueye.IS_INVALID_EEPROM_WRITE_ADDRESS: "Invalid EEPROM write address",
|
||||
ueye.IS_INVALID_EEPROM_READ_LENGTH: "Invalid EEPROM read length",
|
||||
ueye.IS_INVALID_EEPROM_WRITE_LENGTH: "Invalid EEPROM write length",
|
||||
ueye.IS_INVALID_BOARD_INFO_POINTER: "Invalid board info pointer",
|
||||
ueye.IS_INVALID_DISPLAY_MODE: "Invalid display mode",
|
||||
ueye.IS_INVALID_ERR_REP_MODE: "Invalid error report mode",
|
||||
ueye.IS_INVALID_BITS_PIXEL: "Invalid bits per pixel",
|
||||
ueye.IS_INVALID_MEMORY_POINTER: "Invalid memory pointer",
|
||||
ueye.IS_FILE_WRITE_OPEN_ERROR: "File write open error",
|
||||
ueye.IS_FILE_READ_OPEN_ERROR: "File read open error",
|
||||
ueye.IS_FILE_READ_INVALID_BMP_ID: "File read invalid BMP ID",
|
||||
ueye.IS_FILE_READ_INVALID_BMP_SIZE: "File read invalid BMP size",
|
||||
ueye.IS_FILE_READ_INVALID_BIT_COUNT: "File read invalid bit count",
|
||||
ueye.IS_WRONG_KERNEL_VERSION: "Wrong kernel version",
|
||||
ueye.IS_RISC_INVALID_XLENGTH: "RISC invalid X length",
|
||||
ueye.IS_RISC_INVALID_YLENGTH: "RISC invalid Y length",
|
||||
ueye.IS_RISC_EXCEED_IMG_SIZE: "RISC exceed image size",
|
||||
ueye.IS_DD_MAIN_FAILED: "DirectDraw main surface failed",
|
||||
ueye.IS_DD_PRIMSURFACE_FAILED: "DirectDraw primary surface failed",
|
||||
ueye.IS_DD_SCRN_SIZE_NOT_SUPPORTED: "Screen size not supported",
|
||||
ueye.IS_DD_CLIPPER_FAILED: "Clipper failed",
|
||||
ueye.IS_DD_CLIPPER_HWND_FAILED: "Clipper HWND failed",
|
||||
ueye.IS_DD_CLIPPER_CONNECT_FAILED: "Clipper connect failed",
|
||||
ueye.IS_DD_BACKSURFACE_FAILED: "Backsurface failed",
|
||||
ueye.IS_DD_BACKSURFACE_IN_SYSMEM: "Backsurface in system memory",
|
||||
ueye.IS_DD_MDL_MALLOC_ERR: "Memory malloc error",
|
||||
ueye.IS_DD_MDL_SIZE_ERR: "Memory size error",
|
||||
ueye.IS_DD_CLIP_NO_CHANGE: "Clip no change",
|
||||
ueye.IS_DD_PRIMMEM_NULL: "Primary memory null",
|
||||
ueye.IS_DD_BACKMEM_NULL: "Back memory null",
|
||||
ueye.IS_DD_BACKOVLMEM_NULL: "Back overlay memory null",
|
||||
ueye.IS_DD_OVERLAYSURFACE_FAILED: "Overlay surface failed",
|
||||
ueye.IS_DD_OVERLAYSURFACE_IN_SYSMEM: "Overlay surface in system memory",
|
||||
ueye.IS_DD_OVERLAY_NOT_ALLOWED: "Overlay not allowed",
|
||||
ueye.IS_DD_OVERLAY_COLKEY_ERR: "Overlay color key error",
|
||||
ueye.IS_DD_OVERLAY_NOT_ENABLED: "Overlay not enabled",
|
||||
ueye.IS_DD_GET_DC_ERROR: "Get DC error",
|
||||
ueye.IS_DD_DDRAW_DLL_NOT_LOADED: "DirectDraw DLL not loaded",
|
||||
ueye.IS_DD_THREAD_NOT_CREATED: "DirectDraw thread not created",
|
||||
ueye.IS_DD_CANT_GET_CAPS: "Cannot get capabilities",
|
||||
ueye.IS_DD_NO_OVERLAYSURFACE: "No overlay surface",
|
||||
ueye.IS_DD_NO_OVERLAYSTRETCH: "No overlay stretch",
|
||||
ueye.IS_DD_CANT_CREATE_OVERLAYSURFACE: "Cannot create overlay surface",
|
||||
ueye.IS_DD_CANT_UPDATE_OVERLAYSURFACE: "Cannot update overlay surface",
|
||||
ueye.IS_DD_INVALID_STRETCH: "Invalid stretch",
|
||||
ueye.IS_EV_INVALID_EVENT_NUMBER: "Invalid event number",
|
||||
ueye.IS_INVALID_MODE: "Invalid mode",
|
||||
ueye.IS_CANT_FIND_HOOK: "Cannot find hook",
|
||||
ueye.IS_CANT_GET_HOOK_PROC_ADDR: "Cannot get hook procedure address",
|
||||
ueye.IS_CANT_CHAIN_HOOK_PROC: "Cannot chain hook procedure",
|
||||
ueye.IS_CANT_SETUP_WND_PROC: "Cannot setup window procedure",
|
||||
ueye.IS_HWND_NULL: "HWND is null",
|
||||
ueye.IS_INVALID_UPDATE_MODE: "Invalid update mode",
|
||||
ueye.IS_NO_ACTIVE_IMG_MEM: "No active image memory",
|
||||
ueye.IS_CANT_INIT_EVENT: "Cannot initialize event",
|
||||
ueye.IS_FUNC_NOT_AVAIL_IN_OS: "Function not available in OS",
|
||||
ueye.IS_CAMERA_NOT_CONNECTED: "Camera not connected",
|
||||
ueye.IS_SEQUENCE_LIST_EMPTY: "Sequence list empty",
|
||||
ueye.IS_CANT_ADD_TO_SEQUENCE: "Cannot add to sequence",
|
||||
ueye.IS_LOW_OF_SEQUENCE_RISC_MEM: "Low sequence RISC memory",
|
||||
ueye.IS_IMGMEM2FREE_USED_IN_SEQ: "Image memory to free used in sequence",
|
||||
ueye.IS_IMGMEM_NOT_IN_SEQUENCE_LIST: "Image memory not in sequence list",
|
||||
ueye.IS_SEQUENCE_BUF_ALREADY_LOCKED: "Sequence buffer already locked",
|
||||
ueye.IS_INVALID_DEVICE_ID: "Invalid device ID",
|
||||
ueye.IS_INVALID_BOARD_ID: "Invalid board ID",
|
||||
ueye.IS_ALL_DEVICES_BUSY: "All devices busy",
|
||||
ueye.IS_HOOK_BUSY: "Hook busy",
|
||||
ueye.IS_TIMED_OUT: "Timed out",
|
||||
ueye.IS_NULL_POINTER: "Null pointer",
|
||||
ueye.IS_WRONG_HOOK_VERSION: "Wrong hook version",
|
||||
ueye.IS_INVALID_PARAMETER: "Invalid parameter",
|
||||
ueye.IS_NOT_ALLOWED: "Not allowed",
|
||||
ueye.IS_OUT_OF_MEMORY: "Out of memory",
|
||||
ueye.IS_INVALID_WHILE_LIVE: "Invalid while live",
|
||||
ueye.IS_ACCESS_VIOLATION: "Access violation",
|
||||
ueye.IS_UNKNOWN_ROP_EFFECT: "Unknown ROP effect",
|
||||
ueye.IS_INVALID_RENDER_MODE: "Invalid render mode",
|
||||
ueye.IS_INVALID_THREAD_CONTEXT: "Invalid thread context",
|
||||
ueye.IS_NO_HARDWARE_INSTALLED: "No hardware installed",
|
||||
ueye.IS_INVALID_WATCHDOG_TIME: "Invalid watchdog time",
|
||||
ueye.IS_INVALID_WATCHDOG_MODE: "Invalid watchdog mode",
|
||||
ueye.IS_INVALID_PASSTHROUGH_IN: "Invalid passthrough input",
|
||||
ueye.IS_ERROR_SETTING_PASSTHROUGH_IN: "Error setting passthrough input",
|
||||
ueye.IS_FAILURE_ON_SETTING_WATCHDOG: "Failure setting watchdog",
|
||||
ueye.IS_NO_USB20: "No USB 2.0",
|
||||
ueye.IS_CAPTURE_RUNNING: "Capture running",
|
||||
ueye.IS_MEMORY_BOARD_ACTIVATED: "Memory board activated",
|
||||
ueye.IS_MEMORY_BOARD_DEACTIVATED: "Memory board deactivated",
|
||||
ueye.IS_NO_MEMORY_BOARD_CONNECTED: "No memory board connected",
|
||||
ueye.IS_TOO_LESS_MEMORY: "Too little memory",
|
||||
ueye.IS_IMAGE_NOT_PRESENT: "Image not present",
|
||||
ueye.IS_MEMORY_MODE_RUNNING: "Memory mode running",
|
||||
ueye.IS_MEMORYBOARD_DISABLED: "Memoryboard disabled",
|
||||
ueye.IS_TRIGGER_ACTIVATED: "Trigger activated",
|
||||
ueye.IS_WRONG_KEY: "Wrong key",
|
||||
ueye.IS_CRC_ERROR: "CRC error",
|
||||
ueye.IS_NOT_YET_RELEASED: "Not yet released",
|
||||
ueye.IS_NOT_CALIBRATED: "Not calibrated", # already present
|
||||
ueye.IS_WAITING_FOR_KERNEL: "Waiting for kernel",
|
||||
ueye.IS_NOT_SUPPORTED: "Not supported", # already present
|
||||
ueye.IS_TRIGGER_NOT_ACTIVATED: "Trigger not activated",
|
||||
ueye.IS_OPERATION_ABORTED: "Operation aborted",
|
||||
ueye.IS_BAD_STRUCTURE_SIZE: "Bad structure size",
|
||||
ueye.IS_INVALID_BUFFER_SIZE: "Invalid buffer size",
|
||||
ueye.IS_INVALID_PIXEL_CLOCK: "Invalid pixel clock",
|
||||
ueye.IS_INVALID_EXPOSURE_TIME: "Invalid exposure time",
|
||||
ueye.IS_AUTO_EXPOSURE_RUNNING: "Auto exposure running",
|
||||
ueye.IS_CANNOT_CREATE_BB_SURF: "Cannot create BB surface",
|
||||
ueye.IS_CANNOT_CREATE_BB_MIX: "Cannot create BB mix",
|
||||
ueye.IS_BB_OVLMEM_NULL: "BB overlay memory null",
|
||||
ueye.IS_CANNOT_CREATE_BB_OVL: "Cannot create BB overlay",
|
||||
ueye.IS_NOT_SUPP_IN_OVL_SURF_MODE: "Not supported in overlay surface mode",
|
||||
ueye.IS_INVALID_SURFACE: "Invalid surface",
|
||||
ueye.IS_SURFACE_LOST: "Surface lost",
|
||||
ueye.IS_RELEASE_BB_OVL_DC: "Release BB overlay DC",
|
||||
ueye.IS_BB_TIMER_NOT_CREATED: "BB timer not created",
|
||||
ueye.IS_BB_OVL_NOT_EN: "BB overlay not enabled",
|
||||
ueye.IS_ONLY_IN_BB_MODE: "Only in BB mode",
|
||||
ueye.IS_INVALID_COLOR_FORMAT: "Invalid color format",
|
||||
ueye.IS_INVALID_WB_BINNING_MODE: "Invalid WB binning mode",
|
||||
ueye.IS_INVALID_I2C_DEVICE_ADDRESS: "Invalid I²C device address",
|
||||
ueye.IS_COULD_NOT_CONVERT: "Could not convert",
|
||||
ueye.IS_TRANSFER_ERROR: "Transfer error", # already present
|
||||
ueye.IS_PARAMETER_SET_NOT_PRESENT: "Parameter set not present",
|
||||
ueye.IS_INVALID_CAMERA_TYPE: "Invalid camera type",
|
||||
ueye.IS_INVALID_HOST_IP_HIBYTE: "Invalid host IP high byte",
|
||||
ueye.IS_CM_NOT_SUPP_IN_CURR_DISPLAYMODE: "Color matrix not supported in current display mode",
|
||||
ueye.IS_NO_IR_FILTER: "No IR filter",
|
||||
ueye.IS_STARTER_FW_UPLOAD_NEEDED: "Starter firmware upload needed",
|
||||
ueye.IS_DR_LIBRARY_NOT_FOUND: "Driver library not found",
|
||||
ueye.IS_DR_DEVICE_OUT_OF_MEMORY: "Driver device out of memory",
|
||||
ueye.IS_DR_CANNOT_CREATE_SURFACE: "Driver cannot create surface",
|
||||
ueye.IS_DR_CANNOT_CREATE_VERTEX_BUFFER: "Driver cannot create vertex buffer",
|
||||
ueye.IS_DR_CANNOT_CREATE_TEXTURE: "Driver cannot create texture",
|
||||
ueye.IS_DR_CANNOT_LOCK_OVERLAY_SURFACE: "Driver cannot lock overlay surface",
|
||||
ueye.IS_DR_CANNOT_UNLOCK_OVERLAY_SURFACE: "Driver cannot unlock overlay surface",
|
||||
ueye.IS_DR_CANNOT_GET_OVERLAY_DC: "Driver cannot get overlay DC",
|
||||
ueye.IS_DR_CANNOT_RELEASE_OVERLAY_DC: "Driver cannot release overlay DC",
|
||||
ueye.IS_DR_DEVICE_CAPS_INSUFFICIENT: "Driver device capabilities insufficient",
|
||||
ueye.IS_INCOMPATIBLE_SETTING: "Incompatible setting",
|
||||
ueye.IS_DR_NOT_ALLOWED_WHILE_DC_IS_ACTIVE: "Driver not allowed while DC is active",
|
||||
ueye.IS_DEVICE_ALREADY_PAIRED: "Device already paired",
|
||||
ueye.IS_SUBNETMASK_MISMATCH: "Subnet mask mismatch",
|
||||
ueye.IS_SUBNET_MISMATCH: "Subnet mismatch",
|
||||
ueye.IS_INVALID_IP_CONFIGURATION: "Invalid IP configuration",
|
||||
ueye.IS_DEVICE_NOT_COMPATIBLE: "Device not compatible",
|
||||
ueye.IS_NETWORK_FRAME_SIZE_INCOMPATIBLE: "Network frame size incompatible",
|
||||
ueye.IS_NETWORK_CONFIGURATION_INVALID: "Network configuration invalid",
|
||||
ueye.IS_ERROR_CPU_IDLE_STATES_CONFIGURATION: "CPU idle states configuration error",
|
||||
ueye.IS_DEVICE_BUSY: "Device busy",
|
||||
ueye.IS_SENSOR_INITIALIZATION_FAILED: "Sensor initialization failed",
|
||||
ueye.IS_IMAGE_BUFFER_NOT_DWORD_ALIGNED: "Image buffer not DWORD aligned",
|
||||
ueye.IS_SEQ_BUFFER_IS_LOCKED: "Sequence buffer is locked",
|
||||
ueye.IS_FILE_PATH_DOES_NOT_EXIST: "File path does not exist",
|
||||
ueye.IS_INVALID_WINDOW_HANDLE: "Invalid window handle",
|
||||
ueye.IS_INVALID_IMAGE_PARAMETER: "Invalid image parameter",
|
||||
ueye.IS_NO_SUCH_DEVICE: "No such device",
|
||||
ueye.IS_DEVICE_IN_USE: "Device in use",
|
||||
}
|
||||
|
||||
bits_per_pixel = {
|
||||
ueye.IS_CM_SENSOR_RAW8: 8,
|
||||
ueye.IS_CM_SENSOR_RAW10: 16,
|
||||
ueye.IS_CM_SENSOR_RAW12: 16,
|
||||
ueye.IS_CM_SENSOR_RAW16: 16,
|
||||
ueye.IS_CM_MONO8: 8,
|
||||
ueye.IS_CM_RGB8_PACKED: 24,
|
||||
ueye.IS_CM_BGR8_PACKED: 24,
|
||||
ueye.IS_CM_RGBA8_PACKED: 32,
|
||||
ueye.IS_CM_BGRA8_PACKED: 32,
|
||||
ueye.IS_CM_BGR10_PACKED: 32,
|
||||
ueye.IS_CM_RGB10_PACKED: 32,
|
||||
ueye.IS_CM_BGRA12_UNPACKED: 64,
|
||||
ueye.IS_CM_BGR12_UNPACKED: 48,
|
||||
ueye.IS_CM_BGRY8_PACKED: 32,
|
||||
ueye.IS_CM_BGR565_PACKED: 16,
|
||||
ueye.IS_CM_BGR5_PACKED: 16,
|
||||
ueye.IS_CM_UYVY_PACKED: 16,
|
||||
ueye.IS_CM_UYVY_MONO_PACKED: 16,
|
||||
ueye.IS_CM_UYVY_BAYER_PACKED: 16,
|
||||
ueye.IS_CM_CBYCRY_PACKED: 16,
|
||||
}
|
||||
else:
|
||||
error_codes = {}
|
||||
bits_per_pixel = {}
|
||||
|
||||
|
||||
def get_bits_per_pixel(color_mode):
|
||||
"""
|
||||
Returns the number of bits per pixel for the given color mode.
|
||||
"""
|
||||
if color_mode not in bits_per_pixel:
|
||||
raise UEyeException(f"Unknown color mode: {color_mode}")
|
||||
return bits_per_pixel[color_mode]
|
||||
|
||||
|
||||
class UEyeException(Exception):
|
||||
"""Custom exception for uEye errors."""
|
||||
|
||||
def __init__(self, error_code, called_from: str | None = None):
|
||||
self.error_code = error_code
|
||||
self.called_from = called_from if called_from is not None else ""
|
||||
|
||||
def __str__(self):
|
||||
if self.error_code in error_codes:
|
||||
return f"Exception: {error_codes[self.error_code]} raised in {self.called_from}."
|
||||
else:
|
||||
for att, val in ueye.__dict__.items():
|
||||
if (
|
||||
att[0:2] == "IS"
|
||||
and val == self.error_code
|
||||
and ("FAILED" in att or "INVALID" in att or "ERROR" in att or "NOT" in att)
|
||||
):
|
||||
return f"Exception: {str(self.error_code)} ({att} ? <value> {val}) raised in {self.called_from}."
|
||||
return f"Exception: {str(self.error_code)} raised in {self.called_from}."
|
||||
|
||||
|
||||
def check_error(error_code, called_from: str | None = None):
|
||||
"""
|
||||
Check an error code, and raise an error if adequate.
|
||||
"""
|
||||
if error_code != ueye.IS_SUCCESS:
|
||||
called_from = called_from if called_from is not None else ""
|
||||
raise UEyeException(error_code, called_from)
|
||||
@@ -2,227 +2,99 @@ import threading
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from bec_lib.logger import bec_logger
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, Kind
|
||||
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
|
||||
CustomDetectorMixin,
|
||||
PSIDetectorBase,
|
||||
)
|
||||
from ophyd_devices.sim.sim_signals import SetableSignal
|
||||
from ophyd import DeviceStatus, Kind, Signal, StatusBase
|
||||
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||
from ophyd_devices.utils.bec_signals import PreviewSignal
|
||||
|
||||
try:
|
||||
from pyueye import ueye
|
||||
except ImportError:
|
||||
# The pyueye library is not installed or doesn't provide the necessary c libs
|
||||
ueye = None
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class IDSCustomPrepare(CustomDetectorMixin):
|
||||
class ROISignal(Signal):
|
||||
"""
|
||||
Signal to handle the Region of Interest (ROI) for the IDS camera.
|
||||
It is a tuple of (x, y, width, height).
|
||||
"""
|
||||
|
||||
USER_ACCESS = ["pyueye"]
|
||||
pyueye = ueye
|
||||
|
||||
def __init__(self, *_args, parent: Device = None, **_kwargs) -> None:
|
||||
super().__init__(*_args, parent=parent, **_kwargs)
|
||||
self.ueye = ueye
|
||||
self.h_cam = None
|
||||
self.s_info = None
|
||||
self.data_thread = None
|
||||
self.thread_event = None
|
||||
|
||||
def on_connection_established(self):
|
||||
self.hCam = self.ueye.HIDS(
|
||||
self.parent.camera_ID
|
||||
) # 0: first available camera; 1-254: The camera with the specified camera ID
|
||||
self.sInfo = self.ueye.SENSORINFO()
|
||||
self.cInfo = self.ueye.CAMINFO()
|
||||
self.pcImageMemory = self.ueye.c_mem_p()
|
||||
self.MemID = self.ueye.int()
|
||||
self.rectAOI = self.ueye.IS_RECT()
|
||||
self.pitch = self.ueye.INT()
|
||||
self.nBitsPerPixel = self.ueye.INT(
|
||||
self.parent.bits_per_pixel
|
||||
) # 24: bits per pixel for color mode; take 8 bits per pixel for monochrome
|
||||
self.channels = (
|
||||
self.parent.channels
|
||||
) # 3: channels for color mode(RGB); take 1 channel for monochrome
|
||||
self.m_nColorMode = self.ueye.INT(
|
||||
self.parent.m_n_colormode
|
||||
) # Y8/RGB16/RGB24/REG32 (1 for our color cameras)
|
||||
self.bytes_per_pixel = int(self.nBitsPerPixel / 8)
|
||||
|
||||
# Starts the driver and establishes the connection to the camera
|
||||
nRet = self.ueye.is_InitCamera(self.hCam, None)
|
||||
if nRet != self.ueye.IS_SUCCESS:
|
||||
print("is_InitCamera ERROR")
|
||||
|
||||
# Reads out the data hard-coded in the non-volatile camera memory and writes it to the data structure that cInfo points to
|
||||
nRet = self.ueye.is_GetCameraInfo(self.hCam, self.cInfo)
|
||||
if nRet != self.ueye.IS_SUCCESS:
|
||||
print("is_GetCameraInfo ERROR")
|
||||
|
||||
# You can query additional information about the sensor type used in the camera
|
||||
nRet = self.ueye.is_GetSensorInfo(self.hCam, self.sInfo)
|
||||
if nRet != self.ueye.IS_SUCCESS:
|
||||
print("is_GetSensorInfo ERROR")
|
||||
|
||||
nRet = self.ueye.is_ResetToDefault(self.hCam)
|
||||
if nRet != self.ueye.IS_SUCCESS:
|
||||
print("is_ResetToDefault ERROR")
|
||||
|
||||
# Set display mode to DIB
|
||||
nRet = self.ueye.is_SetDisplayMode(self.hCam, self.ueye.IS_SET_DM_DIB)
|
||||
|
||||
# Set the right color mode
|
||||
if (
|
||||
int.from_bytes(self.sInfo.nColorMode.value, byteorder="big")
|
||||
== self.ueye.IS_COLORMODE_BAYER
|
||||
):
|
||||
# setup the color depth to the current windows setting
|
||||
self.ueye.is_GetColorDepth(self.hCam, self.nBitsPerPixel, self.m_nColorMode)
|
||||
bytes_per_pixel = int(self.nBitsPerPixel / 8)
|
||||
print("IS_COLORMODE_BAYER: ")
|
||||
print("\tm_nColorMode: \t\t", self.m_nColorMode)
|
||||
print("\tnBitsPerPixel: \t\t", self.nBitsPerPixel)
|
||||
print("\tbytes_per_pixel: \t\t", bytes_per_pixel)
|
||||
print()
|
||||
|
||||
elif (
|
||||
int.from_bytes(self.sInfo.nColorMode.value, byteorder="big")
|
||||
== self.ueye.IS_COLORMODE_CBYCRY
|
||||
):
|
||||
# for color camera models use RGB32 mode
|
||||
m_nColorMode = ueye.IS_CM_BGRA8_PACKED
|
||||
nBitsPerPixel = ueye.INT(32)
|
||||
bytes_per_pixel = int(self.nBitsPerPixel / 8)
|
||||
print("IS_COLORMODE_CBYCRY: ")
|
||||
print("\tm_nColorMode: \t\t", m_nColorMode)
|
||||
print("\tnBitsPerPixel: \t\t", nBitsPerPixel)
|
||||
print("\tbytes_per_pixel: \t\t", bytes_per_pixel)
|
||||
print()
|
||||
|
||||
elif (
|
||||
int.from_bytes(self.sInfo.nColorMode.value, byteorder="big")
|
||||
== self.ueye.IS_COLORMODE_MONOCHROME
|
||||
):
|
||||
# for color camera models use RGB32 mode
|
||||
m_nColorMode = self.ueye.IS_CM_MONO8
|
||||
nBitsPerPixel = self.ueye.INT(8)
|
||||
bytes_per_pixel = int(nBitsPerPixel / 8)
|
||||
print("IS_COLORMODE_MONOCHROME: ")
|
||||
print("\tm_nColorMode: \t\t", m_nColorMode)
|
||||
print("\tnBitsPerPixel: \t\t", nBitsPerPixel)
|
||||
print("\tbytes_per_pixel: \t\t", bytes_per_pixel)
|
||||
print()
|
||||
|
||||
else:
|
||||
# for monochrome camera models use Y8 mode
|
||||
m_nColorMode = self.ueye.IS_CM_MONO8
|
||||
nBitsPerPixel = self.ueye.INT(8)
|
||||
bytes_per_pixel = int(nBitsPerPixel / 8)
|
||||
print("else")
|
||||
|
||||
# Can be used to set the size and position of an "area of interest"(AOI) within an image
|
||||
nRet = self.ueye.is_AOI(
|
||||
self.hCam, ueye.IS_AOI_IMAGE_GET_AOI, self.rectAOI, self.ueye.sizeof(self.rectAOI)
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name,
|
||||
roi: tuple | None = None,
|
||||
value=0,
|
||||
dtype=None,
|
||||
shape=None,
|
||||
timestamp=None,
|
||||
parent=None,
|
||||
labels=None,
|
||||
kind=Kind.hinted,
|
||||
tolerance=None,
|
||||
rtolerance=None,
|
||||
metadata=None,
|
||||
cl=None,
|
||||
attr_name="",
|
||||
):
|
||||
super().__init__(
|
||||
name=name,
|
||||
value=value,
|
||||
dtype=dtype,
|
||||
shape=shape,
|
||||
timestamp=timestamp,
|
||||
parent=parent,
|
||||
labels=labels,
|
||||
kind=kind,
|
||||
tolerance=tolerance,
|
||||
rtolerance=rtolerance,
|
||||
metadata=metadata,
|
||||
cl=cl,
|
||||
attr_name=attr_name,
|
||||
)
|
||||
if nRet != self.ueye.IS_SUCCESS:
|
||||
print("is_AOI ERROR")
|
||||
self.roi = roi
|
||||
|
||||
self.width = self.rectAOI.s32Width
|
||||
self.height = self.rectAOI.s32Height
|
||||
def get(self, **kwargs):
|
||||
image = self.parent.image_data.get().data
|
||||
if not isinstance(image, np.ndarray):
|
||||
return -1 # -1 if no valid image is available
|
||||
|
||||
# Prints out some information about the camera and the sensor
|
||||
print("Camera model:\t\t", self.sInfo.strSensorName.decode("utf-8"))
|
||||
print("Camera serial no.:\t", self.cInfo.SerNo.decode("utf-8"))
|
||||
print("Maximum image width:\t", self.width)
|
||||
print("Maximum image height:\t", self.height)
|
||||
print()
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
# Allocates an image memory for an image having its dimensions defined by width and height and its color depth defined by nBitsPerPixel
|
||||
nRet = self.ueye.is_AllocImageMem(
|
||||
self.hCam, self.width, self.height, self.nBitsPerPixel, self.pcImageMemory, self.MemID
|
||||
)
|
||||
if nRet != self.ueye.IS_SUCCESS:
|
||||
print("is_AllocImageMem ERROR")
|
||||
if self.roi is None:
|
||||
roi = (0, 0, image.shape[1], image.shape[0])
|
||||
else:
|
||||
# Makes the specified image memory the active memory
|
||||
nRet = self.ueye.is_SetImageMem(self.hCam, self.pcImageMemory, self.MemID)
|
||||
if nRet != self.ueye.IS_SUCCESS:
|
||||
print("is_SetImageMem ERROR")
|
||||
else:
|
||||
# Set the desired color mode
|
||||
nRet = self.ueye.is_SetColorMode(self.hCam, self.m_nColorMode)
|
||||
|
||||
# Activates the camera's live video mode (free run mode)
|
||||
nRet = self.ueye.is_CaptureVideo(self.hCam, self.ueye.IS_DONT_WAIT)
|
||||
if nRet != self.ueye.IS_SUCCESS:
|
||||
print("is_CaptureVideo ERROR")
|
||||
|
||||
# Enables the queue mode for existing image memory sequences
|
||||
nRet = self.ueye.is_InquireImageMem(
|
||||
self.hCam,
|
||||
self.pcImageMemory,
|
||||
self.MemID,
|
||||
self.width,
|
||||
self.height,
|
||||
self.nBitsPerPixel,
|
||||
self.pitch,
|
||||
)
|
||||
if nRet != self.ueye.IS_SUCCESS:
|
||||
print("is_InquireImageMem ERROR")
|
||||
else:
|
||||
print("Press q to leave the programm")
|
||||
startmeasureframerate = True
|
||||
Gain = False
|
||||
|
||||
# Start live mode of camera immediately
|
||||
self.parent.start_live_mode()
|
||||
|
||||
def _start_data_thread(self):
|
||||
self.thread_event = threading.Event()
|
||||
self.data_thread = threading.Thread(target=self._receive_data_from_camera, daemon=True)
|
||||
self.data_thread.start()
|
||||
|
||||
def _receive_data_from_camera(self):
|
||||
while not self.thread_event.is_set():
|
||||
|
||||
# In order to display the image in an OpenCV window we need to...
|
||||
# ...extract the data of our image memory
|
||||
array = ueye.get_data(
|
||||
self.pcImageMemory,
|
||||
self.width,
|
||||
self.height,
|
||||
self.nBitsPerPixel,
|
||||
self.pitch,
|
||||
copy=False,
|
||||
)
|
||||
|
||||
# bytes_per_pixel = int(nBitsPerPixel / 8)
|
||||
|
||||
# ...reshape it in an numpy array...
|
||||
frame = np.reshape(array, (self.height.value, self.width.value, self.bytes_per_pixel))
|
||||
self.parent.image_data.put(frame)
|
||||
self.parent._run_subs(sub_type=self.parent.SUB_MONITOR, value=frame)
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
def on_trigger(self):
|
||||
pass
|
||||
# self.parent._run_subs(sub_type=self.parent.SUB_MONITOR, value=self.parent.image_data.get())
|
||||
roi = self.roi
|
||||
if len(image.shape) > 2:
|
||||
image = np.sum(image, axis=2) # Convert to grayscale if it's a color image
|
||||
return np.sum(image[roi[1] : roi[1] + roi[3], roi[0] : roi[0] + roi[2]], (0, 1))
|
||||
|
||||
|
||||
class IDSCamera(PSIDetectorBase):
|
||||
USER_ACCESS = ["start_live_mode", "stop_live_mode"]
|
||||
class IDSCamera(PSIDeviceBase):
|
||||
""" "
|
||||
#---------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
custom_prepare_cls = IDSCustomPrepare
|
||||
#Variables
|
||||
hCam = ueye.HIDS(202) #0: first available camera; 1-254: The camera with the specified camera ID
|
||||
sInfo = ueye.SENSORINFO()
|
||||
cInfo = ueye.CAMINFO()
|
||||
pcImageMemory = ueye.c_mem_p()
|
||||
MemID = ueye.int()
|
||||
rectAOI = ueye.IS_RECT()
|
||||
pitch = ueye.INT()
|
||||
nBitsPerPixel = ueye.INT(24) #24: bits per pixel for color mode; take 8 bits per pixel for monochrome
|
||||
channels = 3 #3: channels for color mode(RGB); take 1 channel for monochrome
|
||||
m_nColorMode = ueye.INT(1) # Y8/RGB16/RGB24/REG32 (1 for our color cameras)
|
||||
bytes_per_pixel = int(nBitsPerPixel / 8)
|
||||
|
||||
image_data = Cpt(SetableSignal, value=np.empty((100, 100)), kind=Kind.omitted)
|
||||
ids_cam
|
||||
...
|
||||
"""
|
||||
|
||||
SUB_MONITOR = "device_monitor_2d"
|
||||
_default_sub = SUB_MONITOR
|
||||
USER_ACCESS = ["start_live_mode", "stop_live_mode", "set_roi", "width", "height"]
|
||||
|
||||
image_data = Cpt(PreviewSignal, ndim=2, kind=Kind.omitted)
|
||||
# roi_bot_left = Cpt(ROISignal, roi=(400, 525, 118, 105), kind=Kind.normal)
|
||||
# roi_bot_right = Cpt(ROISignal, roi=(518, 525, 118, 105), kind=Kind.normal)
|
||||
# roi_top_left = Cpt(ROISignal, roi=(400, 630, 118, 105), kind=Kind.normal)
|
||||
# roi_top_right = Cpt(ROISignal, roi=(518, 630, 118, 105), kind=Kind.normal)
|
||||
# roi_signal = Cpt(ROISignal, kind=Kind.normal, doc="Region of Interest signal")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -234,19 +106,224 @@ class IDSCamera(PSIDetectorBase):
|
||||
channels: int,
|
||||
m_n_colormode: int,
|
||||
kind=None,
|
||||
parent=None,
|
||||
device_manager=None,
|
||||
**kwargs,
|
||||
):
|
||||
|
||||
super().__init__(
|
||||
prefix=prefix, name=name, kind=kind, parent=parent, device_manager=device_manager, **kwargs
|
||||
prefix=prefix, name=name, kind=kind, device_manager=device_manager, **kwargs
|
||||
)
|
||||
self.camera_ID = camera_ID
|
||||
self.bits_per_pixel = bits_per_pixel
|
||||
self.bytes_per_pixel = None
|
||||
self.channels = channels
|
||||
self.m_n_colormode = m_n_colormode
|
||||
#TODO fix connected and wait_for_connection
|
||||
self.custom_prepare.on_connection_established()
|
||||
self._m_n_colormode_input = m_n_colormode
|
||||
self.m_n_colormode = None
|
||||
self.ueye = ueye
|
||||
self.h_cam = None
|
||||
self.s_info = None
|
||||
self.data_thread = None
|
||||
self.c_info = None
|
||||
self.pc_image_memory = None
|
||||
self.mem_id = None
|
||||
self.rect_aoi = None
|
||||
self.pitch = None
|
||||
self.n_bits_per_pixel = None
|
||||
self.width = None
|
||||
self.height = None
|
||||
self.thread_event = threading.Event()
|
||||
self.data_thread = None
|
||||
self._roi: tuple | None = None # x, y, width, height
|
||||
logger.info(
|
||||
f"Deprecation warning: The IDSCamera class is deprecated. Use the new IDSCameraNew class instead."
|
||||
)
|
||||
|
||||
def set_roi(self, x: int, y: int, width: int, height: int):
|
||||
self._roi = (x, y, width, height)
|
||||
|
||||
def start_backend(self):
|
||||
if self.ueye is None:
|
||||
raise ImportError("The pyueye library is not installed.")
|
||||
self.h_cam = self.ueye.HIDS(
|
||||
self.camera_ID
|
||||
) # 0: first available camera; 1-254: The camera with the specified camera ID
|
||||
self.s_info = self.ueye.SENSORINFO()
|
||||
self.c_info = self.ueye.CAMINFO()
|
||||
self.pc_image_memory = self.ueye.c_mem_p()
|
||||
self.mem_id = self.ueye.int()
|
||||
self.rect_aoi = self.ueye.IS_RECT()
|
||||
self.pitch = self.ueye.INT()
|
||||
self.n_bits_per_pixel = self.ueye.INT(
|
||||
self.bits_per_pixel
|
||||
) # 24: bits per pixel for color mode; take 8 bits per pixel for monochrome
|
||||
self.m_n_colormode = self.ueye.INT(
|
||||
self._m_n_colormode_input
|
||||
) # Y8/RGB16/RGB24/REG32 (1 for our color cameras)
|
||||
self.bytes_per_pixel = int(self.n_bits_per_pixel / 8)
|
||||
|
||||
# Starts the driver and establishes the connection to the camera
|
||||
ret = self.ueye.is_InitCamera(self.h_cam, None)
|
||||
if ret != self.ueye.IS_SUCCESS:
|
||||
print("is_InitCamera ERROR")
|
||||
|
||||
# Reads out the data hard-coded in the non-volatile camera memory and writes it to the data structure that c_info points to
|
||||
ret = self.ueye.is_GetCameraInfo(self.h_cam, self.c_info)
|
||||
if ret != self.ueye.IS_SUCCESS:
|
||||
print("is_GetCameraInfo ERROR")
|
||||
|
||||
# You can query additional information about the sensor type used in the camera
|
||||
ret = self.ueye.is_GetSensorInfo(self.h_cam, self.s_info)
|
||||
if ret != self.ueye.IS_SUCCESS:
|
||||
print("is_GetSensorInfo ERROR")
|
||||
|
||||
ret = self.ueye.is_ResetToDefault(self.h_cam)
|
||||
if ret != self.ueye.IS_SUCCESS:
|
||||
print("is_ResetToDefault ERROR")
|
||||
|
||||
# Set display mode to DIB
|
||||
ret = self.ueye.is_SetDisplayMode(self.h_cam, self.ueye.IS_SET_DM_DIB)
|
||||
|
||||
# Set the right color mode
|
||||
if (
|
||||
int.from_bytes(self.s_info.nColorMode.value, byteorder="big")
|
||||
== self.ueye.IS_COLORMODE_BAYER
|
||||
):
|
||||
# setup the color depth to the current windows setting
|
||||
self.ueye.is_GetColorDepth(self.h_cam, self.n_bits_per_pixel, self.m_n_colormode)
|
||||
bytes_per_pixel = int(self.n_bits_per_pixel / 8)
|
||||
print("IS_COLORMODE_BAYER: ")
|
||||
print("\tm_n_colormode: \t\t", self.m_n_colormode)
|
||||
print("\tn_bits_per_pixel: \t\t", self.n_bits_per_pixel)
|
||||
print("\tbytes_per_pixel: \t\t", bytes_per_pixel)
|
||||
print()
|
||||
|
||||
elif (
|
||||
int.from_bytes(self.s_info.nColorMode.value, byteorder="big")
|
||||
== self.ueye.IS_COLORMODE_CBYCRY
|
||||
):
|
||||
# for color camera models use RGB32 mode
|
||||
m_n_colormode = self.ueye.IS_CM_BGRA8_PACKED
|
||||
n_bits_per_pixel = self.ueye.INT(32)
|
||||
bytes_per_pixel = int(self.n_bits_per_pixel / 8)
|
||||
print("IS_COLORMODE_CBYCRY: ")
|
||||
print("\tm_n_colormode: \t\t", m_n_colormode)
|
||||
print("\tn_bits_per_pixel: \t\t", n_bits_per_pixel)
|
||||
print("\tbytes_per_pixel: \t\t", bytes_per_pixel)
|
||||
print()
|
||||
|
||||
elif (
|
||||
int.from_bytes(self.s_info.nColorMode.value, byteorder="big")
|
||||
== self.ueye.IS_COLORMODE_MONOCHROME
|
||||
):
|
||||
# for color camera models use RGB32 mode
|
||||
m_n_colormode = self.ueye.IS_CM_MONO8
|
||||
n_bits_per_pixel = self.ueye.INT(8)
|
||||
bytes_per_pixel = int(n_bits_per_pixel / 8)
|
||||
print("IS_COLORMODE_MONOCHROME: ")
|
||||
print("\tm_n_colormode: \t\t", m_n_colormode)
|
||||
print("\tn_bits_per_pixel: \t\t", n_bits_per_pixel)
|
||||
print("\tbytes_per_pixel: \t\t", bytes_per_pixel)
|
||||
print()
|
||||
|
||||
else:
|
||||
# for monochrome camera models use Y8 mode
|
||||
m_n_colormode = self.ueye.IS_CM_MONO8
|
||||
n_bits_per_pixel = self.ueye.INT(8)
|
||||
bytes_per_pixel = int(n_bits_per_pixel / 8)
|
||||
print("else")
|
||||
|
||||
# Can be used to set the size and position of an "area of interest"(AOI) within an image
|
||||
ret = self.ueye.is_AOI(
|
||||
self.h_cam,
|
||||
self.ueye.IS_AOI_IMAGE_GET_AOI,
|
||||
self.rect_aoi,
|
||||
self.ueye.sizeof(self.rect_aoi),
|
||||
)
|
||||
if ret != self.ueye.IS_SUCCESS:
|
||||
print("is_AOI ERROR")
|
||||
|
||||
self.width = self.rect_aoi.s32Width
|
||||
self.height = self.rect_aoi.s32Height
|
||||
|
||||
# Prints out some information about the camera and the sensor
|
||||
print("Camera model:\t\t", self.s_info.strSensorName.decode("utf-8"))
|
||||
print("Camera serial no.:\t", self.c_info.SerNo.decode("utf-8"))
|
||||
print("Maximum image width:\t", self.width)
|
||||
print("Maximum image height:\t", self.height)
|
||||
print()
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
# Allocates an image memory for an image having its dimensions defined by width and height and its color depth defined by n_bits_per_pixel
|
||||
ret = self.ueye.is_AllocImageMem(
|
||||
self.h_cam,
|
||||
self.width,
|
||||
self.height,
|
||||
self.n_bits_per_pixel,
|
||||
self.pc_image_memory,
|
||||
self.mem_id,
|
||||
)
|
||||
if ret != self.ueye.IS_SUCCESS:
|
||||
print("is_AllocImageMem ERROR")
|
||||
else:
|
||||
# Makes the specified image memory the active memory
|
||||
ret = self.ueye.is_SetImageMem(self.h_cam, self.pc_image_memory, self.mem_id)
|
||||
if ret != self.ueye.IS_SUCCESS:
|
||||
print("is_SetImageMem ERROR")
|
||||
else:
|
||||
# Set the desired color mode
|
||||
ret = self.ueye.is_SetColorMode(self.h_cam, self.m_n_colormode)
|
||||
|
||||
# Activates the camera's live video mode (free run mode)
|
||||
ret = self.ueye.is_CaptureVideo(self.h_cam, self.ueye.IS_DONT_WAIT)
|
||||
if ret != self.ueye.IS_SUCCESS:
|
||||
print("is_CaptureVideo ERROR")
|
||||
|
||||
# Enables the queue mode for existing image memory sequences
|
||||
ret = self.ueye.is_InquireImageMem(
|
||||
self.h_cam,
|
||||
self.pc_image_memory,
|
||||
self.mem_id,
|
||||
self.width,
|
||||
self.height,
|
||||
self.n_bits_per_pixel,
|
||||
self.pitch,
|
||||
)
|
||||
if ret != self.ueye.IS_SUCCESS:
|
||||
print("is_InquireImageMem ERROR")
|
||||
else:
|
||||
print("Press q to leave the programm")
|
||||
# startmeasureframerate = True
|
||||
# Gain = False
|
||||
|
||||
# Start live mode of camera immediately
|
||||
self.start_live_mode()
|
||||
|
||||
def _start_data_thread(self):
|
||||
self.data_thread = threading.Thread(target=self._receive_data_from_camera, daemon=True)
|
||||
self.data_thread.start()
|
||||
|
||||
def _receive_data_from_camera(self):
|
||||
while not self.thread_event.is_set():
|
||||
if self.ueye is None:
|
||||
print("pyueye library not available.")
|
||||
return
|
||||
# In order to display the image in an OpenCV window we need to...
|
||||
# ...extract the data of our image memory
|
||||
array = self.ueye.get_data(
|
||||
self.pc_image_memory,
|
||||
self.width,
|
||||
self.height,
|
||||
self.n_bits_per_pixel,
|
||||
self.pitch,
|
||||
copy=False,
|
||||
)
|
||||
|
||||
# ...reshape it in an numpy array...
|
||||
frame = np.reshape(array, (self.height.value, self.width.value, self.bytes_per_pixel))
|
||||
self.image_data.put(frame)
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
def wait_for_connection(self, all_signals=False, timeout=10):
|
||||
if ueye is None:
|
||||
@@ -254,226 +331,73 @@ class IDSCamera(PSIDetectorBase):
|
||||
"The pyueye library is not installed or doesn't provide the necessary c libs"
|
||||
)
|
||||
super().wait_for_connection(all_signals, timeout)
|
||||
#self.custom_prepare.on_connection_established()
|
||||
|
||||
def destroy(self):
|
||||
"""Extend Ophyds destroy function to kill the data thread"""
|
||||
self.stop_live_mode()
|
||||
super().destroy()
|
||||
|
||||
def start_live_mode(self):
|
||||
if self.custom_prepare.data_thread is not None:
|
||||
if self.data_thread is not None:
|
||||
self.stop_live_mode()
|
||||
self.custom_prepare._start_data_thread()
|
||||
self._start_data_thread()
|
||||
|
||||
def stop_live_mode(self):
|
||||
"""Stopping the camera live mode."""
|
||||
if self.custom_prepare.thread_event is not None:
|
||||
self.custom_prepare.thread_event.set()
|
||||
if self.custom_prepare.data_thread is not None:
|
||||
self.custom_prepare.data_thread.join()
|
||||
self.custom_prepare.thread_event = None
|
||||
self.custom_prepare.data_thread = None
|
||||
self.thread_event.set()
|
||||
if self.data_thread is not None:
|
||||
self.data_thread.join()
|
||||
self.thread_event.clear()
|
||||
self.data_thread = None
|
||||
|
||||
########################################
|
||||
# Beamline Specific Implementations #
|
||||
########################################
|
||||
|
||||
def on_init(self) -> None:
|
||||
"""
|
||||
Called when the device is initialized.
|
||||
|
||||
No signals are connected at this point. If you like to
|
||||
set default values on signals, please use on_connected instead.
|
||||
"""
|
||||
|
||||
def on_connected(self) -> None:
|
||||
"""
|
||||
Called after the device is connected and its signals are connected.
|
||||
Default values for signals should be set here.
|
||||
"""
|
||||
self.start_backend()
|
||||
self.start_live_mode()
|
||||
|
||||
def on_stage(self) -> DeviceStatus | StatusBase | None:
|
||||
"""
|
||||
Called while staging the device.
|
||||
|
||||
Information about the upcoming scan can be accessed from the scan_info (self.scan_info.msg) object.
|
||||
"""
|
||||
|
||||
def on_unstage(self) -> DeviceStatus | StatusBase | None:
|
||||
"""Called while unstaging the device."""
|
||||
|
||||
def on_pre_scan(self) -> DeviceStatus | StatusBase | None:
|
||||
"""Called right before the scan starts on all devices automatically."""
|
||||
|
||||
def on_trigger(self) -> DeviceStatus | StatusBase | None:
|
||||
"""Called when the device is triggered."""
|
||||
|
||||
def on_complete(self) -> DeviceStatus | StatusBase | None:
|
||||
"""Called to inquire if a device has completed a scans."""
|
||||
|
||||
def on_kickoff(self) -> DeviceStatus | StatusBase | None:
|
||||
"""Called to kickoff a device for a fly scan. Has to be called explicitly."""
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""Called when the device is stopped."""
|
||||
|
||||
def on_destroy(self) -> None:
|
||||
"""Called when the device is destroyed. Cleanup resources here."""
|
||||
self.stop_live_mode()
|
||||
|
||||
|
||||
"""from pyueye import ueye
|
||||
import numpy as np
|
||||
import cv2
|
||||
import sys
|
||||
import time
|
||||
if __name__ == "__main__":
|
||||
# Example usage
|
||||
camera = IDSCamera(name="camera", camera_ID=201, bits_per_pixel=24, channels=3, m_n_colormode=1)
|
||||
camera.wait_for_connection()
|
||||
|
||||
#---------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
#Variables
|
||||
hCam = ueye.HIDS(202) #0: first available camera; 1-254: The camera with the specified camera ID
|
||||
sInfo = ueye.SENSORINFO()
|
||||
cInfo = ueye.CAMINFO()
|
||||
pcImageMemory = ueye.c_mem_p()
|
||||
MemID = ueye.int()
|
||||
rectAOI = ueye.IS_RECT()
|
||||
pitch = ueye.INT()
|
||||
nBitsPerPixel = ueye.INT(24) #24: bits per pixel for color mode; take 8 bits per pixel for monochrome
|
||||
channels = 3 #3: channels for color mode(RGB); take 1 channel for monochrome
|
||||
m_nColorMode = ueye.INT(1) # Y8/RGB16/RGB24/REG32 (1 for our color cameras)
|
||||
bytes_per_pixel = int(nBitsPerPixel / 8)
|
||||
|
||||
ids_cam
|
||||
...
|
||||
deviceConfig:
|
||||
camera_ID: 202
|
||||
bits_per_pixel: 24
|
||||
channels: 3
|
||||
m_n_colormode: 1
|
||||
|
||||
#---------------------------------------------------------------------------------------------------------------------------------------
|
||||
print("START")
|
||||
print()
|
||||
|
||||
# Starts the driver and establishes the connection to the camera
|
||||
nRet = ueye.is_InitCamera(hCam, None)
|
||||
if nRet != ueye.IS_SUCCESS:
|
||||
print("is_InitCamera ERROR")
|
||||
|
||||
# Reads out the data hard-coded in the non-volatile camera memory and writes it to the data structure that cInfo points to
|
||||
nRet = ueye.is_GetCameraInfo(hCam, cInfo)
|
||||
if nRet != ueye.IS_SUCCESS:
|
||||
print("is_GetCameraInfo ERROR")
|
||||
|
||||
# You can query additional information about the sensor type used in the camera
|
||||
nRet = ueye.is_GetSensorInfo(hCam, sInfo)
|
||||
if nRet != ueye.IS_SUCCESS:
|
||||
print("is_GetSensorInfo ERROR")
|
||||
|
||||
nRet = ueye.is_ResetToDefault( hCam)
|
||||
if nRet != ueye.IS_SUCCESS:
|
||||
print("is_ResetToDefault ERROR")
|
||||
|
||||
# Set display mode to DIB
|
||||
nRet = ueye.is_SetDisplayMode(hCam, ueye.IS_SET_DM_DIB)
|
||||
|
||||
|
||||
|
||||
# Set the right color mode
|
||||
if int.from_bytes(sInfo.nColorMode.value, byteorder='big') == ueye.IS_COLORMODE_BAYER:
|
||||
# setup the color depth to the current windows setting
|
||||
ueye.is_GetColorDepth(hCam, nBitsPerPixel, m_nColorMode)
|
||||
bytes_per_pixel = int(nBitsPerPixel / 8)
|
||||
print("IS_COLORMODE_BAYER: ", )
|
||||
print("\tm_nColorMode: \t\t", m_nColorMode)
|
||||
print("\tnBitsPerPixel: \t\t", nBitsPerPixel)
|
||||
print("\tbytes_per_pixel: \t\t", bytes_per_pixel)
|
||||
print()
|
||||
|
||||
elif int.from_bytes(sInfo.nColorMode.value, byteorder='big') == ueye.IS_COLORMODE_CBYCRY:
|
||||
# for color camera models use RGB32 mode
|
||||
m_nColorMode = ueye.IS_CM_BGRA8_PACKED
|
||||
nBitsPerPixel = ueye.INT(32)
|
||||
bytes_per_pixel = int(nBitsPerPixel / 8)
|
||||
print("IS_COLORMODE_CBYCRY: ", )
|
||||
print("\tm_nColorMode: \t\t", m_nColorMode)
|
||||
print("\tnBitsPerPixel: \t\t", nBitsPerPixel)
|
||||
print("\tbytes_per_pixel: \t\t", bytes_per_pixel)
|
||||
print()
|
||||
|
||||
elif int.from_bytes(sInfo.nColorMode.value, byteorder='big') == ueye.IS_COLORMODE_MONOCHROME:
|
||||
# for color camera models use RGB32 mode
|
||||
m_nColorMode = ueye.IS_CM_MONO8
|
||||
nBitsPerPixel = ueye.INT(8)
|
||||
bytes_per_pixel = int(nBitsPerPixel / 8)
|
||||
print("IS_COLORMODE_MONOCHROME: ", )
|
||||
print("\tm_nColorMode: \t\t", m_nColorMode)
|
||||
print("\tnBitsPerPixel: \t\t", nBitsPerPixel)
|
||||
print("\tbytes_per_pixel: \t\t", bytes_per_pixel)
|
||||
print()
|
||||
|
||||
else:
|
||||
# for monochrome camera models use Y8 mode
|
||||
m_nColorMode = ueye.IS_CM_MONO8
|
||||
nBitsPerPixel = ueye.INT(8)
|
||||
bytes_per_pixel = int(nBitsPerPixel / 8)
|
||||
print("else")
|
||||
|
||||
# Can be used to set the size and position of an "area of interest"(AOI) within an image
|
||||
nRet = ueye.is_AOI(hCam, ueye.IS_AOI_IMAGE_GET_AOI, rectAOI, ueye.sizeof(rectAOI))
|
||||
if nRet != ueye.IS_SUCCESS:
|
||||
print("is_AOI ERROR")
|
||||
|
||||
width = rectAOI.s32Width
|
||||
height = rectAOI.s32Height
|
||||
|
||||
# Prints out some information about the camera and the sensor
|
||||
print("Camera model:\t\t", sInfo.strSensorName.decode('utf-8'))
|
||||
print("Camera serial no.:\t", cInfo.SerNo.decode('utf-8'))
|
||||
print("Maximum image width:\t", width)
|
||||
print("Maximum image height:\t", height)
|
||||
print()
|
||||
|
||||
#---------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
# Allocates an image memory for an image having its dimensions defined by width and height and its color depth defined by nBitsPerPixel
|
||||
nRet = ueye.is_AllocImageMem(hCam, width, height, nBitsPerPixel, pcImageMemory, MemID)
|
||||
if nRet != ueye.IS_SUCCESS:
|
||||
print("is_AllocImageMem ERROR")
|
||||
else:
|
||||
# Makes the specified image memory the active memory
|
||||
nRet = ueye.is_SetImageMem(hCam, pcImageMemory, MemID)
|
||||
if nRet != ueye.IS_SUCCESS:
|
||||
print("is_SetImageMem ERROR")
|
||||
else:
|
||||
# Set the desired color mode
|
||||
nRet = ueye.is_SetColorMode(hCam, m_nColorMode)
|
||||
|
||||
|
||||
|
||||
# Activates the camera's live video mode (free run mode)
|
||||
nRet = ueye.is_CaptureVideo(hCam, ueye.IS_DONT_WAIT)
|
||||
if nRet != ueye.IS_SUCCESS:
|
||||
print("is_CaptureVideo ERROR")
|
||||
|
||||
# Enables the queue mode for existing image memory sequences
|
||||
nRet = ueye.is_InquireImageMem(hCam, pcImageMemory, MemID, width, height, nBitsPerPixel, pitch)
|
||||
if nRet != ueye.IS_SUCCESS:
|
||||
print("is_InquireImageMem ERROR")
|
||||
else:
|
||||
print("Press q to leave the programm")
|
||||
startmeasureframerate=True
|
||||
Gain = False
|
||||
#---------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
# Continuous image display
|
||||
while(nRet == ueye.IS_SUCCESS):
|
||||
|
||||
# In order to display the image in an OpenCV window we need to...
|
||||
# ...extract the data of our image memory
|
||||
array = ueye.get_data(pcImageMemory, width, height, nBitsPerPixel, pitch, copy=False)
|
||||
|
||||
# bytes_per_pixel = int(nBitsPerPixel / 8)
|
||||
|
||||
# ...reshape it in an numpy array...
|
||||
frame = np.reshape(array,(height.value, width.value, bytes_per_pixel))
|
||||
|
||||
# ...resize the image by a half
|
||||
frame = cv2.resize(frame,(0,0),fx=0.5, fy=0.5)
|
||||
|
||||
#---------------------------------------------------------------------------------------------------------------------------------------
|
||||
#Include image data processing here
|
||||
|
||||
#---------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
#...and finally display it
|
||||
cv2.imshow("SimpleLive_Python_uEye_OpenCV", frame)
|
||||
if startmeasureframerate:
|
||||
starttime = time.time()
|
||||
startmeasureframerate=False
|
||||
framenumber=0
|
||||
if time.time() > starttime+5:
|
||||
print(f"Caught {framenumber/5} frames per second")
|
||||
startmeasureframerate=True
|
||||
Gain = ~Gain
|
||||
if Gain:
|
||||
nRet = ueye.is_SetGainBoost(hCam, 1)
|
||||
else:
|
||||
nRet = ueye.is_SetGainBoost(hCam, 0)
|
||||
print(f"Gain setting status {nRet}")
|
||||
#...and finally display it
|
||||
cv2.imshow("SimpleLive_Python_uEye_OpenCV", frame)
|
||||
framenumber+=1
|
||||
time.sleep(0.1)
|
||||
|
||||
# Press q if you want to end the loop
|
||||
if (cv2.waitKey(1) & 0xFF) == ord('q'):
|
||||
break
|
||||
#---------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
# Releases an image memory that was allocated using is_AllocImageMem() and removes it from the driver management
|
||||
ueye.is_FreeImageMem(hCam, pcImageMemory, MemID)
|
||||
|
||||
# Disables the hCam camera handle and releases the data structures and memory areas taken up by the uEye camera
|
||||
ueye.is_ExitCamera(hCam)
|
||||
|
||||
# Destroys the OpenCv windows
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
print()
|
||||
print("END")
|
||||
"""
|
||||
camera.on_destroy()
|
||||
|
||||
226
csaxs_bec/devices/ids_cameras/ids_camera_new.py
Normal file
226
csaxs_bec/devices/ids_cameras/ids_camera_new.py
Normal file
@@ -0,0 +1,226 @@
|
||||
"""IDS Camera class for cSAXS IDS cameras."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Literal, Tuple, TypedDict
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import messages
|
||||
from bec_lib.logger import bec_logger
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||
from ophyd_devices.utils.bec_signals import AsyncSignal, PreviewSignal
|
||||
|
||||
from csaxs_bec.devices.ids_cameras.base_integration.camera import Camera
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_lib.devicemanager import ScanInfo
|
||||
from pydantic import ValidationInfo
|
||||
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class IDSCamera(PSIDeviceBase):
|
||||
"""IDS Camera class for cSAXS.
|
||||
|
||||
This class inherits from PSIDeviceBase and implements the necessary methods
|
||||
to interact with the IDS camera using the pyueye library.
|
||||
"""
|
||||
|
||||
image = Cpt(PreviewSignal, name="image", ndim=2, doc="Preview signal for the camera.")
|
||||
roi_signal = Cpt(
|
||||
AsyncSignal,
|
||||
name="roi_signal",
|
||||
ndim=0,
|
||||
max_size=1000,
|
||||
doc="Signal for the region of interest (ROI).",
|
||||
async_update={"type": "add", "max_shape": [None]},
|
||||
)
|
||||
|
||||
USER_ACCESS = ["live_mode", "mask", "set_rect_roi", "get_last_image"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name: str,
|
||||
camera_id: int,
|
||||
prefix: str = "",
|
||||
scan_info: ScanInfo | None = None,
|
||||
m_n_colormode: Literal[0, 1, 2, 3] = 1,
|
||||
bits_per_pixel: Literal[8, 24] = 24,
|
||||
live_mode: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize the IDS Camera.
|
||||
|
||||
Args:
|
||||
name (str): Name of the device.
|
||||
camera_id (int): The ID of the camera device.
|
||||
prefix (str): Prefix for the device.
|
||||
scan_info (ScanInfo | None): Scan information for the device.
|
||||
m_n_colormode (Literal[0, 1, 2, 3]): Color mode for the camera.
|
||||
bits_per_pixel (Literal[8, 24]): Number of bits per pixel for the camera.
|
||||
live_mode (bool): Whether to enable live mode for the camera.
|
||||
"""
|
||||
super().__init__(name=name, prefix=prefix, scan_info=scan_info, **kwargs)
|
||||
self._live_mode_thread: threading.Thread | None = None
|
||||
self._stop_live_mode_event: threading.Event = threading.Event()
|
||||
self.cam = Camera(
|
||||
camera_id=camera_id,
|
||||
m_n_colormode=m_n_colormode,
|
||||
bits_per_pixel=bits_per_pixel,
|
||||
connect=False,
|
||||
)
|
||||
self._live_mode = False
|
||||
self._inputs = {"live_mode": live_mode}
|
||||
self._mask = np.zeros((1, 1), dtype=np.uint8)
|
||||
|
||||
############## Live Mode Methods ##############
|
||||
|
||||
@property
|
||||
def mask(self) -> np.ndarray:
|
||||
"""Return the current region of interest (ROI) for the camera."""
|
||||
return self._mask
|
||||
|
||||
@mask.setter
|
||||
def mask(self, value: np.ndarray):
|
||||
"""
|
||||
Set the region of interest (ROI) for the camera.
|
||||
|
||||
Args:
|
||||
value (np.ndarray): The mask to set as the ROI.
|
||||
"""
|
||||
if value.ndim != 2:
|
||||
raise ValueError("ROI mask must be a 2D array.")
|
||||
img_shape = (self.cam.cam.height.value, self.cam.cam.width.value)
|
||||
if value.shape[0] != img_shape[0] or value.shape[1] != img_shape[1]:
|
||||
raise ValueError(
|
||||
f"ROI mask shape {value.shape} does not match image shape {img_shape}."
|
||||
)
|
||||
self._mask = value
|
||||
|
||||
@property
|
||||
def live_mode(self) -> bool:
|
||||
"""Return whether the camera is in live mode."""
|
||||
return self._live_mode
|
||||
|
||||
@live_mode.setter
|
||||
def live_mode(self, value: bool):
|
||||
"""Set the live mode for the camera."""
|
||||
if value != self._live_mode:
|
||||
if self.cam._connected is False: # $ pylint: disable=protected-access
|
||||
self.cam.on_connect()
|
||||
self._live_mode = value
|
||||
if value:
|
||||
self._start_live()
|
||||
else:
|
||||
self._stop_live()
|
||||
|
||||
def set_rect_roi(self, x: int, y: int, width: int, height: int):
|
||||
"""Set the rectangular region of interest (ROI) for the camera."""
|
||||
if x < 0 or y < 0 or width <= 0 or height <= 0:
|
||||
raise ValueError("ROI coordinates and dimensions must be positive integers.")
|
||||
img_shape = (self.cam.cam.height.value, self.cam.cam.width.value)
|
||||
if x + width > img_shape[1] or y + height > img_shape[0]:
|
||||
raise ValueError("ROI exceeds camera dimensions.")
|
||||
mask = np.zeros(img_shape, dtype=np.uint8)
|
||||
mask[y : y + height, x : x + width] = 1
|
||||
self.mask = mask
|
||||
|
||||
def _start_live(self):
|
||||
"""Start the live mode for the camera."""
|
||||
if self._live_mode_thread is not None:
|
||||
logger.info("Live mode thread is already running.")
|
||||
return
|
||||
self._stop_live_mode_event.clear()
|
||||
self._live_mode_thread = threading.Thread(
|
||||
target=self._live_mode_loop, args=(self._stop_live_mode_event,)
|
||||
)
|
||||
self._live_mode_thread.start()
|
||||
|
||||
def _stop_live(self):
|
||||
"""Stop the live mode for the camera."""
|
||||
if self._live_mode_thread is None:
|
||||
logger.info("Live mode thread is not running.")
|
||||
return
|
||||
self._stop_live_mode_event.set()
|
||||
self._live_mode_thread.join(timeout=5)
|
||||
if self._live_mode_thread.is_alive():
|
||||
logger.warning("Live mode thread did not stop gracefully.")
|
||||
else:
|
||||
self._live_mode_thread = None
|
||||
logger.info("Live mode stopped.")
|
||||
|
||||
def _live_mode_loop(self, stop_event: threading.Event):
|
||||
"""Loop to capture images in live mode."""
|
||||
while not stop_event.is_set():
|
||||
try:
|
||||
self.process_data(self.cam.get_image_data())
|
||||
except Exception as e:
|
||||
logger.error(f"Error in live mode loop: {e}")
|
||||
break
|
||||
stop_event.wait(0.2) # 5 Hz
|
||||
|
||||
def process_data(self, image: np.ndarray | None):
|
||||
"""Process the image data before sending it to the preview signal."""
|
||||
if image is None:
|
||||
return
|
||||
self.image.put(image)
|
||||
|
||||
def get_last_image(self) -> np.ndarray:
|
||||
"""Get the last captured image from the camera."""
|
||||
image = self.image.get()
|
||||
if image:
|
||||
return image.data
|
||||
|
||||
############## User Interface Methods ##############
|
||||
|
||||
def on_connected(self):
|
||||
"""Connect to the camera."""
|
||||
self.cam.on_connect()
|
||||
self.live_mode = self._inputs.get("live_mode", False)
|
||||
self.set_rect_roi(0, 0, self.cam.cam.width.value, self.cam.cam.height.value)
|
||||
|
||||
def on_destroy(self):
|
||||
"""Clean up resources when the device is destroyed."""
|
||||
self.cam.on_disconnect()
|
||||
super().on_destroy()
|
||||
|
||||
def on_trigger(self):
|
||||
"""Handle the trigger event."""
|
||||
if not self.live_mode:
|
||||
return
|
||||
image = self.image.get()
|
||||
if image is not None:
|
||||
image: messages.DevicePreviewMessage
|
||||
if self.mask.shape[0:2] != image.data.shape[0:2]:
|
||||
logger.info(
|
||||
f"ROI shape does not match image shape, skipping ROI application for device {self.name}."
|
||||
)
|
||||
return
|
||||
|
||||
if len(image.data.shape) == 3:
|
||||
# If the image has multiple channels, apply the mask to each channel
|
||||
data = image.data * self.mask[:, :, np.newaxis] # Apply mask to the image data
|
||||
n_channels = 3
|
||||
else:
|
||||
data = image.data * self.mask
|
||||
n_channels = 1
|
||||
self.roi_signal.put(
|
||||
{
|
||||
self.roi_signal.name: {
|
||||
"value": np.sum(data)
|
||||
/ (np.sum(self.mask) * n_channels), # TODO could be optimized
|
||||
"timestamp": time.time(),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example usage of the IDSCamera class
|
||||
camera = IDSCamera(name="TestCamera", camera_id=201, live_mode=False)
|
||||
print(f"Camera {camera.name} initialized with ID {camera.cam.camera_id}.")
|
||||
@@ -1,83 +0,0 @@
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import bec_logger
|
||||
from ophyd import Kind, Signal
|
||||
from ophyd.utils import ReadOnlyError
|
||||
|
||||
from ophyd_devices.utils.bec_device_base import BECDeviceBase
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
# Readout precision for Setable/ReadOnlySignal signals
|
||||
PRECISION = 3
|
||||
|
||||
|
||||
class ReadOnlySignal(Signal):
|
||||
"""Setable signal for simulated devices.
|
||||
|
||||
The signal will store the value in sim_state of the SimulatedData class of the parent device.
|
||||
It will also return the value from sim_state when get is called. Compared to the ReadOnlySignal,
|
||||
this signal can be written to.
|
||||
The setable signal inherits from the Signal class of ophyd, thus the class attribute needs to be
|
||||
initiated as a Component (class from ophyd).
|
||||
|
||||
>>> signal = SetableSignal(name="signal", parent=parent, value=0)
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
name (string) : Name of the signal
|
||||
parent (object) : Parent object of the signal, default none.
|
||||
value (any) : Initial value of the signal, default 0.
|
||||
kind (int) : Kind of the signal, default Kind.normal.
|
||||
precision (float) : Precision of the signal, default PRECISION.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
*args,
|
||||
fcn: callable,
|
||||
kind: int = Kind.normal,
|
||||
precision: float = PRECISION,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(*args, name=name, value=value, kind=kind, **kwargs)
|
||||
self._metadata.update(connected=True, write_access=False)
|
||||
self._value = None
|
||||
self.precision = precision
|
||||
self.fcn = fcn
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
def get(self):
|
||||
"""Get the current position of the simulated device.
|
||||
|
||||
Core function for signal.
|
||||
"""
|
||||
self._value = self.fcn()
|
||||
return self._value
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
def put(self, value):
|
||||
"""Put the value to the simulated device.
|
||||
|
||||
Core function for signal.
|
||||
"""
|
||||
self._update_sim_state(value)
|
||||
self._value = value
|
||||
|
||||
def describe(self):
|
||||
"""Describe the readback signal.
|
||||
|
||||
Core function for signal.
|
||||
"""
|
||||
res = super().describe()
|
||||
if self.precision is not None:
|
||||
res[self.name]["precision"] = self.precision
|
||||
return res
|
||||
|
||||
@property
|
||||
def timestamp(self):
|
||||
"""Timestamp of the readback value"""
|
||||
return self._get_timestamp()
|
||||
@@ -191,7 +191,10 @@ def test_ddg1_stage(mock_ddg1):
|
||||
|
||||
mock_ddg1.stage()
|
||||
|
||||
assert np.isclose(mock_ddg1.burst_mode.get(), 0) # Burst mode is disabled
|
||||
assert np.isclose(mock_ddg1.burst_mode.get(), 1) # burst mode is enabled
|
||||
assert np.isclose(mock_ddg1.burst_delay.get(), 0)
|
||||
assert np.isclose(mock_ddg1.burst_period.get(), exp_time)
|
||||
|
||||
# Trigger DDG2 through EXT/EN
|
||||
|
||||
assert np.isclose(mock_ddg1.ab.delay.get(), 2e-3)
|
||||
@@ -208,13 +211,20 @@ def test_ddg1_stage(mock_ddg1):
|
||||
|
||||
def test_ddg1_trigger(mock_ddg1):
|
||||
"""Test the on_trigger method of DDG1."""
|
||||
mock_ddg1.state.event_status._read_pv.mock_data = (
|
||||
5 # STATUSBITS.END_OF_DELAY.value + STATUSBITS.TRIG.value
|
||||
)
|
||||
status = mock_ddg1.trigger()
|
||||
assert status.done is True
|
||||
assert status.success is True
|
||||
assert mock_ddg1.trigger_shot.get() == 1
|
||||
mock_ddg1.state.event_status._read_pv.mock_data = STATUSBITS.NONE.value
|
||||
|
||||
with mock.patch.object(mock_ddg1, "device_manager") as mock_device_manager:
|
||||
# TODO add device manager DMMock, and properly test logic for mcs triggering.
|
||||
mock_get = mock_device_manager.devices.get = mock.Mock(return_value=None)
|
||||
status = mock_ddg1.trigger()
|
||||
assert mock_get.call_args == mock.call("mcs", None)
|
||||
assert status.done is False
|
||||
assert status.success is False
|
||||
assert mock_ddg1.trigger_shot.get() == 1
|
||||
mock_ddg1.state.event_status._read_pv.mock_data = STATUSBITS.END_OF_BURST.value
|
||||
status.wait(timeout=1) # Wait for the status to be done
|
||||
assert status.done is True
|
||||
assert status.success is True
|
||||
|
||||
|
||||
def test_ddg1_stop(mock_ddg1):
|
||||
@@ -270,6 +280,11 @@ def test_ddg2_stage(mock_ddg2):
|
||||
assert mock_ddg2.trigger_source.get() == TRIGGERSOURCE.EXT_RISING_EDGE.value
|
||||
|
||||
assert mock_ddg2.staged == ophyd.Staged.yes
|
||||
mock_ddg2.unstage() # Reset staged state for next test
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
mock_ddg2.scan_info.msg.scan_parameters["exp_time"] = 2e-4 # too short exposure time
|
||||
mock_ddg2.stage()
|
||||
|
||||
|
||||
def test_ddg2_trigger(mock_ddg2):
|
||||
|
||||
88
tests/tests_devices/test_ids_camera.py
Normal file
88
tests/tests_devices/test_ids_camera.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""Unit tests for the IDS Camera device."""
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from csaxs_bec.devices.ids_cameras.ids_camera_new import IDSCamera
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def ids_camera():
|
||||
"""Fixture for creating an instance of the IDSCamera."""
|
||||
camera = IDSCamera(
|
||||
name="test_camera",
|
||||
camera_id=1,
|
||||
prefix="test:",
|
||||
scan_info=None,
|
||||
m_n_colormode=1,
|
||||
bits_per_pixel=24,
|
||||
live_mode=False,
|
||||
)
|
||||
# Mock camera connection and attributes
|
||||
camera.cam = mock.Mock()
|
||||
camera.cam._connected = True
|
||||
camera.cam.cam = mock.Mock()
|
||||
camera.cam.cam.width.value = 2
|
||||
camera.cam.cam.height.value = 2
|
||||
yield camera
|
||||
|
||||
|
||||
def test_mask_setter_getter(ids_camera):
|
||||
"""Test the mask setter and getter methods."""
|
||||
mask = np.zeros((2, 2), dtype=np.uint8)
|
||||
mask[0, 0] = 1
|
||||
ids_camera.mask = mask
|
||||
assert np.array_equal(ids_camera.mask, mask)
|
||||
|
||||
|
||||
def test_mask_setter_invalid_shape(ids_camera):
|
||||
"""Test the mask setter with an invalid shape."""
|
||||
with pytest.raises(ValueError):
|
||||
ids_camera.mask = np.zeros((3, 3), dtype=np.uint8) # Exceeds mocked camera dimensions
|
||||
|
||||
|
||||
def test_on_connected_sets_mask_and_live_mode(ids_camera):
|
||||
"""Test the on_connected method to ensure it sets the mask and live mode."""
|
||||
ids_camera.cam.on_connect = mock.Mock()
|
||||
ids_camera.on_connected()
|
||||
ids_camera.cam.on_connect.assert_called_once()
|
||||
expected_mask = np.ones((2, 2), dtype=np.uint8)
|
||||
assert np.array_equal(ids_camera.mask, expected_mask)
|
||||
|
||||
|
||||
def test_on_trigger_roi_signal(ids_camera):
|
||||
"""Test the on_trigger method to ensure it processes the ROI signal correctly."""
|
||||
ids_camera.live_mode = True
|
||||
test_image = np.array([[2, 4], [6, 8]])
|
||||
test_mask = np.array([[1, 0], [0, 1]], dtype=np.uint8)
|
||||
ids_camera.mask = test_mask
|
||||
mock_image = mock.Mock()
|
||||
mock_image.data = test_image
|
||||
ids_camera.image.get = mock.Mock(return_value=mock_image)
|
||||
ids_camera.roi_signal.put = mock.Mock(side_effect=ids_camera.roi_signal.put)
|
||||
ids_camera.on_trigger()
|
||||
expected_value = (2 * 1 + 4 * 0 + 6 * 0 + 8 * 1) / (np.sum(test_mask) * 1)
|
||||
result = ids_camera.roi_signal.get()
|
||||
assert np.isclose(
|
||||
result.content["signals"][ids_camera.roi_signal.name]["value"], expected_value, atol=1e-6
|
||||
)
|
||||
|
||||
|
||||
def test_get_last_image(ids_camera):
|
||||
"""Test the get_last_image method to ensure it returns the last captured image."""
|
||||
test_image = np.array([[1, 2], [3, 4]], dtype=np.uint8)
|
||||
mock_image = mock.Mock()
|
||||
mock_image.data = test_image
|
||||
ids_camera.image.get = mock.Mock(return_value=mock_image)
|
||||
|
||||
result = ids_camera.get_last_image()
|
||||
assert np.array_equal(result, test_image)
|
||||
|
||||
|
||||
def test_on_destroy(ids_camera):
|
||||
"""Test the on_destroy method to ensure it cleans up resources."""
|
||||
ids_camera.cam.on_disconnect = mock.Mock()
|
||||
ids_camera.on_destroy()
|
||||
ids_camera.cam.on_disconnect.assert_called_once()
|
||||
@@ -2,311 +2,480 @@
|
||||
import threading
|
||||
from unittest import mock
|
||||
|
||||
import numpy as np
|
||||
import ophyd
|
||||
import pytest
|
||||
from bec_lib import messages
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_server.device_server.tests.utils import DMMock
|
||||
from ophyd_devices.tests.utils import MockPV
|
||||
from ophyd_devices.tests.utils import MockPV, patch_dual_pvs
|
||||
|
||||
from csaxs_bec.devices.epics.mcs_csaxs import (
|
||||
MCScSAXS,
|
||||
MCSError,
|
||||
MCSTimeoutError,
|
||||
ReadoutMode,
|
||||
TriggerSource,
|
||||
from csaxs_bec.devices.epics.mcs_card.mcs_card import (
|
||||
ACQUIREMODE,
|
||||
ACQUIRING,
|
||||
CHANNEL1SOURCE,
|
||||
CHANNELADVANCE,
|
||||
INPUTMODE,
|
||||
OUTPUTMODE,
|
||||
POLARITY,
|
||||
READMODE,
|
||||
MCSCard,
|
||||
)
|
||||
from csaxs_bec.devices.tests_utils.utils import patch_dual_pvs
|
||||
from csaxs_bec.devices.epics.mcs_card.mcs_card_csaxs import READYTOREAD, MCSCardCSAXS
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def mock_det():
|
||||
name = "mcs"
|
||||
def mock_mcs_card():
|
||||
"""Fixture to mock the MCSCard device."""
|
||||
name = "mcs_card"
|
||||
prefix = "X12SA-MCS:"
|
||||
with mock.patch.object(ophyd, "cl") as mock_cl:
|
||||
mock_cl.get_pv = MockPV
|
||||
mock_cl.thread_class = threading
|
||||
mcs_card = MCSCard(name=name, prefix=prefix)
|
||||
patch_dual_pvs(mcs_card)
|
||||
yield mcs_card
|
||||
|
||||
|
||||
def test_mcs_card(mock_mcs_card):
|
||||
"""Test the MCSCard initialization."""
|
||||
assert mock_mcs_card.name == "mcs_card"
|
||||
assert mock_mcs_card.prefix == "X12SA-MCS:"
|
||||
assert len(mock_mcs_card.counters.component_names) == 32
|
||||
assert mock_mcs_card.counters.mca1.name == "mcs_card_counters_mca1"
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def mock_mcs_csaxs():
|
||||
"""Fixture to mock the MCSCardCSAXS device."""
|
||||
name = "mcs_csaxs"
|
||||
prefix = "X12SA-MCS-CSAXS:"
|
||||
dm = DMMock()
|
||||
with mock.patch.object(dm, "connector"):
|
||||
with (
|
||||
mock.patch(
|
||||
"ophyd_devices.interfaces.base_classes.bec_device_base.FileWriter"
|
||||
) as filemixin,
|
||||
mock.patch(
|
||||
"ophyd_devices.interfaces.base_classes.psi_detector_base.PSIDetectorBase._update_service_config"
|
||||
) as mock_service_config,
|
||||
):
|
||||
with mock.patch.object(ophyd, "cl") as mock_cl:
|
||||
mock_cl.get_pv = MockPV
|
||||
mock_cl.thread_class = threading.Thread
|
||||
with mock.patch.object(MCScSAXS, "_init"):
|
||||
det = MCScSAXS(name=name, prefix=prefix, device_manager=dm)
|
||||
patch_dual_pvs(det)
|
||||
det.TIMEOUT_FOR_SIGNALS = 0.1
|
||||
yield det
|
||||
with mock.patch.object(ophyd, "cl") as mock_cl:
|
||||
mock_cl.get_pv = MockPV
|
||||
mock_cl.thread_class = threading.Thread
|
||||
mcs_card_csaxs = MCSCardCSAXS(name=name, prefix=prefix, device_manager=dm)
|
||||
patch_dual_pvs(mcs_card_csaxs)
|
||||
yield mcs_card_csaxs
|
||||
|
||||
|
||||
def test_init():
|
||||
"""Test the _init function:"""
|
||||
name = "eiger"
|
||||
prefix = "X12SA-ES-EIGER9M:"
|
||||
dm = DMMock()
|
||||
with mock.patch.object(dm, "connector"):
|
||||
with (
|
||||
mock.patch("ophyd_devices.interfaces.base_classes.bec_device_base.FileWriter"),
|
||||
mock.patch(
|
||||
"ophyd_devices.interfaces.base_classes.psi_detector_base.PSIDetectorBase._update_service_config"
|
||||
),
|
||||
):
|
||||
with mock.patch.object(ophyd, "cl") as mock_cl:
|
||||
mock_cl.get_pv = MockPV
|
||||
with (
|
||||
mock.patch(
|
||||
"csaxs_bec.devices.epics.mcs_csaxs.MCSSetup.initialize_detector"
|
||||
) as mock_init_det,
|
||||
mock.patch(
|
||||
"csaxs_bec.devices.epics.mcs_csaxs.MCSSetup.initialize_detector_backend"
|
||||
) as mock_init_backend,
|
||||
):
|
||||
MCScSAXS(name=name, prefix=prefix, device_manager=dm)
|
||||
mock_init_det.assert_called_once()
|
||||
mock_init_backend.assert_called_once()
|
||||
def test_mcs_card_csaxs(mock_mcs_csaxs):
|
||||
"""Test the MCSCardCSAXS initialization."""
|
||||
assert mock_mcs_csaxs.name == "mcs_csaxs"
|
||||
assert mock_mcs_csaxs.prefix == "X12SA-MCS-CSAXS:"
|
||||
assert mock_mcs_csaxs.counter_mapping == {
|
||||
"mcs_csaxs_counters_mca1": "current1",
|
||||
"mcs_csaxs_counters_mca2": "current2",
|
||||
"mcs_csaxs_counters_mca3": "current3",
|
||||
"mcs_csaxs_counters_mca4": "current4",
|
||||
"mcs_csaxs_counters_mca5": "count_time",
|
||||
}
|
||||
assert mock_mcs_csaxs._mcs_clock == 1e7 # 10 MHz
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trigger_source, channel_advance, channel_source1, pv_channels",
|
||||
[
|
||||
(
|
||||
3,
|
||||
1,
|
||||
0,
|
||||
{
|
||||
"user_led": 0,
|
||||
"mux_output": 5,
|
||||
"input_pol": 0,
|
||||
"output_pol": 1,
|
||||
"count_on_start": 0,
|
||||
"stop_all": 1,
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
def test_initialize_detector(
|
||||
mock_det, trigger_source, channel_advance, channel_source1, pv_channels
|
||||
):
|
||||
"""Test the _init function:
|
||||
def test_mcs_card_csaxs_on_connected(mock_mcs_csaxs):
|
||||
"""Test the on_connected method of MCSCardCSAXS."""
|
||||
mcs = mock_mcs_csaxs
|
||||
mcs.on_connected()
|
||||
# Stop called
|
||||
assert mcs.stop_all.get() == 1
|
||||
# Channel advance settings
|
||||
assert mcs.channel_advance.get() == CHANNELADVANCE.EXTERNAL
|
||||
assert mcs.channel1_source.get() == CHANNEL1SOURCE.EXTERNAL
|
||||
assert mcs.prescale.get() == 1
|
||||
#
|
||||
assert mcs.user_led.get() == 0
|
||||
# Only 5 channels are connected
|
||||
assert mcs.mux_output.get() == 5
|
||||
# input output settings
|
||||
assert mcs.input_mode.get() == INPUTMODE.MODE_3
|
||||
assert mcs.input_polarity.get() == POLARITY.NORMAL
|
||||
assert mcs.output_mode.get() == OUTPUTMODE.MODE_2
|
||||
assert mcs.output_polarity.get() == POLARITY.NORMAL
|
||||
assert mcs.count_on_start.get() == 0
|
||||
assert mcs.read_mode.get() == READMODE.PASSIVE
|
||||
assert mcs.acquire_mode.get() == ACQUIREMODE.MCS
|
||||
|
||||
This includes testing the functions:
|
||||
- initialize_detector
|
||||
- stop_det
|
||||
- parent.set_trigger
|
||||
--> Testing the filewriter is done in test_init_filewriter
|
||||
|
||||
Validation upon setting the correct PVs
|
||||
|
||||
"""
|
||||
mock_det.custom_prepare.initialize_detector() # call the method you want to test
|
||||
assert mock_det.channel_advance.get() == channel_advance
|
||||
assert mock_det.channel1_source.get() == channel_source1
|
||||
assert mock_det.user_led.get() == pv_channels["user_led"]
|
||||
assert mock_det.mux_output.get() == pv_channels["mux_output"]
|
||||
assert mock_det.input_polarity.get() == pv_channels["input_pol"]
|
||||
assert mock_det.output_polarity.get() == pv_channels["output_pol"]
|
||||
assert mock_det.count_on_start.get() == pv_channels["count_on_start"]
|
||||
assert mock_det.input_mode.get() == trigger_source
|
||||
with mock.patch.object(mcs.current_channel, "subscribe") as mock_cur_ch_subscribe:
|
||||
with mock.patch.object(mcs.counters.mca1, "subscribe") as mock_mca_subscribe:
|
||||
mcs.on_connected()
|
||||
assert mock_cur_ch_subscribe.call_args == mock.call(mcs._progress_update, run=False)
|
||||
assert mock_mca_subscribe.call_args == mock.call(mcs._on_counter_update, run=False)
|
||||
|
||||
|
||||
def test_trigger(mock_det):
|
||||
"""Test the trigger function:
|
||||
Validate that trigger calls the custom_prepare.on_trigger() function
|
||||
"""
|
||||
with mock.patch.object(mock_det.custom_prepare, "on_trigger") as mock_on_trigger:
|
||||
mock_det.trigger()
|
||||
mock_on_trigger.assert_called_once()
|
||||
def test_mcs_card_csaxs_stage(mock_mcs_csaxs):
|
||||
"""Test on stage method of MCSCardCSAXS"""
|
||||
mcs = mock_mcs_csaxs
|
||||
triggers = 5
|
||||
mcs.scan_info.msg.scan_parameters["frames_per_trigger"] = triggers
|
||||
mcs.erase_all.put(0)
|
||||
mcs.stage()
|
||||
assert mcs._staged == ophyd.Staged.yes
|
||||
assert mcs.erase_all.get() == 1
|
||||
assert mcs.preset_real.get() == 0
|
||||
assert mcs.num_use_all.get() == triggers
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value, num_lines, num_points, done", [(100, 5, 500, False), (500, 5, 500, True)]
|
||||
)
|
||||
def test_progress_update(mock_det, value, num_lines, num_points, done):
|
||||
mock_det.num_lines.set(num_lines)
|
||||
mock_det.scaninfo.num_points = num_points
|
||||
calls = mock.call(sub_type="progress", value=value, max_value=num_points, done=done)
|
||||
with mock.patch.object(mock_det, "_run_subs") as mock_run_subs:
|
||||
mock_det.custom_prepare._progress_update(value=value)
|
||||
mock_run_subs.assert_called_once()
|
||||
assert mock_run_subs.call_args == calls
|
||||
def test_mcs_card_csaxs_unstage(mock_mcs_csaxs):
|
||||
"""Test unstage method of MCSCardCSAXS"""
|
||||
mcs = mock_mcs_csaxs
|
||||
mcs.stop_all.put(0)
|
||||
mcs.ready_to_read.put(0)
|
||||
mcs.erase_all.put(1)
|
||||
mcs.unstage()
|
||||
assert mcs.stop_all.get() == 1
|
||||
assert mcs.ready_to_read.get() == READYTOREAD.DONE
|
||||
assert mcs.erase_all.get() == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"values, expected_nothing",
|
||||
[([[100, 120, 140], [200, 220, 240], [300, 320, 340]], False), ([100, 200, 300], True)],
|
||||
)
|
||||
def test_on_mca_data(mock_det, values, expected_nothing):
|
||||
"""Test the on_mca_data function:
|
||||
Validate that on_mca_data calls the custom_prepare.on_mca_data() function
|
||||
"""
|
||||
with mock.patch.object(mock_det.custom_prepare, "_send_data_to_bec") as mock_send_data:
|
||||
mock_object = mock.MagicMock()
|
||||
for ii, name in enumerate(mock_det.custom_prepare.mca_names):
|
||||
mock_object.attr_name = name
|
||||
mock_det.custom_prepare._on_mca_data(obj=mock_object, value=values[ii])
|
||||
if not expected_nothing and ii < (len(values) - 1):
|
||||
assert mock_det.custom_prepare.mca_data[name] == values[ii]
|
||||
|
||||
if not expected_nothing:
|
||||
mock_send_data.assert_called_once()
|
||||
assert mock_det.custom_prepare.acquisition_done is True
|
||||
def test_mcs_card_csaxs_complete_and_stop(mock_mcs_csaxs):
|
||||
"""Test complete method of MCSCarcCSAXS"""
|
||||
mcs = mock_mcs_csaxs
|
||||
mcs.acquiring._read_pv.mock_data = ACQUIRING.ACQUIRING
|
||||
st = mcs.complete()
|
||||
assert st.done is False
|
||||
mcs.stop_all.put(0)
|
||||
mcs.ready_to_read.put(READYTOREAD.PROCESSING)
|
||||
mcs.stop()
|
||||
with pytest.raises(Exception):
|
||||
st.wait(timeout=3)
|
||||
assert st.done is True
|
||||
assert st.success is False
|
||||
assert mcs.stop_all.get() == 1
|
||||
assert mcs.ready_to_read.get() == READYTOREAD.DONE
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"metadata, mca_data",
|
||||
[
|
||||
(
|
||||
{"scan_id": 123},
|
||||
{
|
||||
"mca1": {"value": [100, 120, 140]},
|
||||
"mca3": {"value": [200, 220, 240]},
|
||||
"mca4": {"value": [300, 320, 340]},
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
def test_send_data_to_bec(mock_det, metadata, mca_data):
|
||||
mock_det.scaninfo.scan_msg = mock.MagicMock()
|
||||
mock_det.scaninfo.scan_msg.metadata = metadata
|
||||
mock_det.scaninfo.scan_id = metadata["scan_id"]
|
||||
mock_det.custom_prepare.mca_data = mca_data
|
||||
mock_det.custom_prepare._send_data_to_bec()
|
||||
device_metadata = mock_det.scaninfo.scan_msg.metadata
|
||||
metadata.update({"async_update": "append", "num_lines": mock_det.num_lines.get()})
|
||||
data = messages.DeviceMessage(signals=dict(mca_data), metadata=device_metadata)
|
||||
calls = mock.call(
|
||||
topic=MessageEndpoints.device_async_readback(
|
||||
scan_id=metadata["scan_id"], device=mock_det.name
|
||||
),
|
||||
msg={"data": data},
|
||||
expire=1800,
|
||||
)
|
||||
|
||||
assert mock_det.connector.xadd.call_args == calls
|
||||
def test_mcs_card_csaxs_on_counter_updated(mock_mcs_csaxs):
|
||||
mcs = mock_mcs_csaxs
|
||||
# Called for mca1
|
||||
kwargs = {"obj": mcs.counters.mca1}
|
||||
mcs._on_counter_update(1, **kwargs)
|
||||
assert mcs.mcs.mca1.get() == 1
|
||||
assert mcs.bpm.current1.get() == 1
|
||||
assert mcs.counter_updated == [mcs.counters.mca1.name]
|
||||
# Called for mca2
|
||||
kwargs = {"obj": mcs.counters.mca2}
|
||||
mcs._on_counter_update(np.array([2, 4]), **kwargs)
|
||||
assert mcs.mcs.mca2.get() == [2, 4]
|
||||
assert np.isclose(mcs.bpm.current2.get(), 3)
|
||||
assert mcs.counter_updated == [mcs.counters.mca1.name, mcs.counters.mca2.name]
|
||||
# Called for mca3
|
||||
kwargs = {"obj": mcs.counters.mca3}
|
||||
mcs._on_counter_update(1000, **kwargs)
|
||||
assert mcs.mcs.mca3.get() == 1000
|
||||
assert mcs.bpm.current3.get() == 1000
|
||||
assert mcs.counter_updated == [
|
||||
mcs.counters.mca1.name,
|
||||
mcs.counters.mca2.name,
|
||||
mcs.counters.mca3.name,
|
||||
]
|
||||
# Called for mca4
|
||||
kwargs = {"obj": mcs.counters.mca4}
|
||||
mcs._on_counter_update(np.array([20, 40]), **kwargs)
|
||||
assert mcs.mcs.mca4.get() == [20, 40]
|
||||
assert np.isclose(mcs.bpm.current4.get(), 30)
|
||||
assert mcs.counter_updated == [
|
||||
mcs.counters.mca1.name,
|
||||
mcs.counters.mca2.name,
|
||||
mcs.counters.mca3.name,
|
||||
mcs.counters.mca4.name,
|
||||
]
|
||||
# Called for mca5
|
||||
assert mcs.ready_to_read.get() == 0
|
||||
kwargs = {"obj": mcs.counters.mca5}
|
||||
mcs._on_counter_update(np.array([10000, 10000]), **kwargs)
|
||||
assert np.isclose(mcs.bpm.count_time.get(), 10000 / 1e7)
|
||||
assert mcs.mcs.mca5.get() == [10000, 10000]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"scaninfo, triggersource, stopped, expected_exception",
|
||||
[
|
||||
(
|
||||
{"num_points": 500, "frames_per_trigger": 1, "scan_type": "step"},
|
||||
TriggerSource.MODE3,
|
||||
False,
|
||||
False,
|
||||
),
|
||||
(
|
||||
{"num_points": 500, "frames_per_trigger": 1, "scan_type": "fly"},
|
||||
TriggerSource.MODE3,
|
||||
False,
|
||||
False,
|
||||
),
|
||||
(
|
||||
{"num_points": 5001, "frames_per_trigger": 2, "scan_type": "step"},
|
||||
TriggerSource.MODE3,
|
||||
False,
|
||||
True,
|
||||
),
|
||||
(
|
||||
{"num_points": 500, "frames_per_trigger": 2, "scan_type": "random"},
|
||||
TriggerSource.MODE3,
|
||||
False,
|
||||
True,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_stage(mock_det, scaninfo, triggersource, stopped, expected_exception):
|
||||
mock_det.scaninfo.num_points = scaninfo["num_points"]
|
||||
mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"]
|
||||
mock_det.scaninfo.scan_type = scaninfo["scan_type"]
|
||||
mock_det.stopped = stopped
|
||||
with mock.patch.object(mock_det.custom_prepare, "prepare_detector_backend") as mock_prep_fw:
|
||||
if expected_exception:
|
||||
with pytest.raises(MCSError):
|
||||
mock_det.stage()
|
||||
mock_prep_fw.assert_called_once()
|
||||
else:
|
||||
mock_det.stage()
|
||||
mock_prep_fw.assert_called_once()
|
||||
# Check set_trigger
|
||||
mock_det.input_mode.get() == triggersource
|
||||
if scaninfo["scan_type"] == "step":
|
||||
assert mock_det.num_use_all.get() == int(scaninfo["frames_per_trigger"]) * int(
|
||||
scaninfo["num_points"]
|
||||
)
|
||||
elif scaninfo["scan_type"] == "fly":
|
||||
assert mock_det.num_use_all.get() == int(scaninfo["num_points"])
|
||||
mock_det.preset_real.get() == 0
|
||||
|
||||
# # CHeck custom_prepare.arm_acquisition
|
||||
# assert mock_det.custom_prepare.counter == 0
|
||||
# assert mock_det.erase_start.get() == 1
|
||||
# mock_prep_fw.assert_called_once()
|
||||
# # Check _prep_det
|
||||
# assert mock_det.cam.num_images.get() == int(
|
||||
# scaninfo["num_points"] * scaninfo["frames_per_trigger"]
|
||||
# )
|
||||
# assert mock_det.cam.num_frames.get() == 1
|
||||
|
||||
# mock_publish_file_location.assert_called_with(done=False)
|
||||
# assert mock_det.cam.acquire.get() == 1
|
||||
# @pytest.fixture(scope="function")
|
||||
# def mock_det():
|
||||
# name = "mcs"
|
||||
# prefix = "X12SA-MCS:"
|
||||
# dm = DMMock()
|
||||
# with mock.patch.object(dm, "connector"):
|
||||
# with (
|
||||
# mock.patch(
|
||||
# "ophyd_devices.interfaces.base_classes.bec_device_base.FileWriter"
|
||||
# ) as filemixin,
|
||||
# mock.patch(
|
||||
# "ophyd_devices.interfaces.base_classes.psi_detector_base.PSIDetectorBase._update_service_config"
|
||||
# ) as mock_service_config,
|
||||
# ):
|
||||
# with mock.patch.object(ophyd, "cl") as mock_cl:
|
||||
# mock_cl.get_pv = MockPV
|
||||
# mock_cl.thread_class = threading.Thread
|
||||
# with mock.patch.object(MCScSAXS, "_init"):
|
||||
# det = MCScSAXS(name=name, prefix=prefix, device_manager=dm)
|
||||
# patch_dual_pvs(det)
|
||||
# det.TIMEOUT_FOR_SIGNALS = 0.1
|
||||
# yield det
|
||||
|
||||
|
||||
def test_prepare_detector_backend(mock_det):
|
||||
mock_det.custom_prepare.prepare_detector_backend()
|
||||
assert mock_det.erase_all.get() == 1
|
||||
assert mock_det.read_mode.get() == ReadoutMode.EVENT
|
||||
# def test_init():
|
||||
# """Test the _init function:"""
|
||||
# name = "eiger"
|
||||
# prefix = "X12SA-ES-EIGER9M:"
|
||||
# dm = DMMock()
|
||||
# with mock.patch.object(dm, "connector"):
|
||||
# with (
|
||||
# mock.patch("ophyd_devices.interfaces.base_classes.bec_device_base.FileWriter"),
|
||||
# mock.patch(
|
||||
# "ophyd_devices.interfaces.base_classes.psi_detector_base.PSIDetectorBase._update_service_config"
|
||||
# ),
|
||||
# ):
|
||||
# with mock.patch.object(ophyd, "cl") as mock_cl:
|
||||
# mock_cl.get_pv = MockPV
|
||||
# with (
|
||||
# mock.patch(
|
||||
# "csaxs_bec.devices.epics.mcs_csaxs.MCSSetup.initialize_detector"
|
||||
# ) as mock_init_det,
|
||||
# mock.patch(
|
||||
# "csaxs_bec.devices.epics.mcs_csaxs.MCSSetup.initialize_detector_backend"
|
||||
# ) as mock_init_backend,
|
||||
# ):
|
||||
# MCScSAXS(name=name, prefix=prefix, device_manager=dm)
|
||||
# mock_init_det.assert_called_once()
|
||||
# mock_init_backend.assert_called_once()
|
||||
|
||||
|
||||
def test_complete(mock_det):
|
||||
with (mock.patch.object(mock_det.custom_prepare, "finished") as mock_finished,):
|
||||
mock_det.complete()
|
||||
assert mock_finished.call_count == 1
|
||||
# @pytest.mark.parametrize(
|
||||
# "trigger_source, channel_advance, channel_source1, pv_channels",
|
||||
# [
|
||||
# (
|
||||
# 3,
|
||||
# 1,
|
||||
# 0,
|
||||
# {
|
||||
# "user_led": 0,
|
||||
# "mux_output": 5,
|
||||
# "input_pol": 0,
|
||||
# "output_pol": 1,
|
||||
# "count_on_start": 0,
|
||||
# "stop_all": 1,
|
||||
# },
|
||||
# )
|
||||
# ],
|
||||
# )
|
||||
# def test_initialize_detector(
|
||||
# mock_det, trigger_source, channel_advance, channel_source1, pv_channels
|
||||
# ):
|
||||
# """Test the _init function:
|
||||
|
||||
# This includes testing the functions:
|
||||
# - initialize_detector
|
||||
# - stop_det
|
||||
# - parent.set_trigger
|
||||
# --> Testing the filewriter is done in test_init_filewriter
|
||||
|
||||
# Validation upon setting the correct PVs
|
||||
|
||||
# """
|
||||
# mock_det.custom_prepare.initialize_detector() # call the method you want to test
|
||||
# assert mock_det.channel_advance.get() == channel_advance
|
||||
# assert mock_det.channel1_source.get() == channel_source1
|
||||
# assert mock_det.user_led.get() == pv_channels["user_led"]
|
||||
# assert mock_det.mux_output.get() == pv_channels["mux_output"]
|
||||
# assert mock_det.input_polarity.get() == pv_channels["input_pol"]
|
||||
# assert mock_det.output_polarity.get() == pv_channels["output_pol"]
|
||||
# assert mock_det.count_on_start.get() == pv_channels["count_on_start"]
|
||||
# assert mock_det.input_mode.get() == trigger_source
|
||||
|
||||
|
||||
def test_stop_detector_backend(mock_det):
|
||||
mock_det.custom_prepare.stop_detector_backend()
|
||||
assert mock_det.custom_prepare.acquisition_done is True
|
||||
# def test_trigger(mock_det):
|
||||
# """Test the trigger function:
|
||||
# Validate that trigger calls the custom_prepare.on_trigger() function
|
||||
# """
|
||||
# with mock.patch.object(mock_det.custom_prepare, "on_trigger") as mock_on_trigger:
|
||||
# mock_det.trigger()
|
||||
# mock_on_trigger.assert_called_once()
|
||||
|
||||
|
||||
def test_stop(mock_det):
|
||||
with (
|
||||
mock.patch.object(mock_det.custom_prepare, "stop_detector") as mock_stop_det,
|
||||
mock.patch.object(
|
||||
mock_det.custom_prepare, "stop_detector_backend"
|
||||
) as mock_stop_detector_backend,
|
||||
):
|
||||
mock_det.stop()
|
||||
mock_stop_det.assert_called_once()
|
||||
mock_stop_detector_backend.assert_called_once()
|
||||
assert mock_det.stopped is True
|
||||
# @pytest.mark.parametrize(
|
||||
# "value, num_lines, num_points, done", [(100, 5, 500, False), (500, 5, 500, True)]
|
||||
# )
|
||||
# def test_progress_update(mock_det, value, num_lines, num_points, done):
|
||||
# mock_det.num_lines.set(num_lines)
|
||||
# mock_det.scaninfo.num_points = num_points
|
||||
# calls = mock.call(sub_type="progress", value=value, max_value=num_points, done=done)
|
||||
# with mock.patch.object(mock_det, "_run_subs") as mock_run_subs:
|
||||
# mock_det.custom_prepare._progress_update(value=value)
|
||||
# mock_run_subs.assert_called_once()
|
||||
# assert mock_run_subs.call_args == calls
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"stopped, acquisition_done, acquiring_state, expected_exception",
|
||||
[
|
||||
(False, True, 0, False),
|
||||
(False, False, 0, True),
|
||||
(False, True, 1, True),
|
||||
(True, True, 0, True),
|
||||
],
|
||||
)
|
||||
def test_finished(mock_det, stopped, acquisition_done, acquiring_state, expected_exception):
|
||||
mock_det.custom_prepare.acquisition_done = acquisition_done
|
||||
mock_det.acquiring._read_pv.mock_data = acquiring_state
|
||||
mock_det.scaninfo.num_points = 500
|
||||
mock_det.num_lines.put(500)
|
||||
mock_det.current_channel._read_pv.mock_data = 1
|
||||
mock_det.stopped = stopped
|
||||
# @pytest.mark.parametrize(
|
||||
# "values, expected_nothing",
|
||||
# [([[100, 120, 140], [200, 220, 240], [300, 320, 340]], False), ([100, 200, 300], True)],
|
||||
# )
|
||||
# def test_on_mca_data(mock_det, values, expected_nothing):
|
||||
# """Test the on_mca_data function:
|
||||
# Validate that on_mca_data calls the custom_prepare.on_mca_data() function
|
||||
# """
|
||||
# with mock.patch.object(mock_det.custom_prepare, "_send_data_to_bec") as mock_send_data:
|
||||
# mock_object = mock.MagicMock()
|
||||
# for ii, name in enumerate(mock_det.custom_prepare.mca_names):
|
||||
# mock_object.attr_name = name
|
||||
# mock_det.custom_prepare._on_mca_data(obj=mock_object, value=values[ii])
|
||||
# if not expected_nothing and ii < (len(values) - 1):
|
||||
# assert mock_det.custom_prepare.mca_data[name] == values[ii]
|
||||
|
||||
if expected_exception:
|
||||
with pytest.raises(MCSTimeoutError):
|
||||
mock_det.timeout = 0.1
|
||||
mock_det.custom_prepare.finished()
|
||||
else:
|
||||
mock_det.custom_prepare.finished()
|
||||
if stopped:
|
||||
assert mock_det.stopped is stopped
|
||||
# if not expected_nothing:
|
||||
# mock_send_data.assert_called_once()
|
||||
# assert mock_det.custom_prepare.acquisition_done is True
|
||||
|
||||
|
||||
# @pytest.mark.parametrize(
|
||||
# "metadata, mca_data",
|
||||
# [
|
||||
# (
|
||||
# {"scan_id": 123},
|
||||
# {
|
||||
# "mca1": {"value": [100, 120, 140]},
|
||||
# "mca3": {"value": [200, 220, 240]},
|
||||
# "mca4": {"value": [300, 320, 340]},
|
||||
# },
|
||||
# )
|
||||
# ],
|
||||
# )
|
||||
# def test_send_data_to_bec(mock_det, metadata, mca_data):
|
||||
# mock_det.scaninfo.scan_msg = mock.MagicMock()
|
||||
# mock_det.scaninfo.scan_msg.metadata = metadata
|
||||
# mock_det.scaninfo.scan_id = metadata["scan_id"]
|
||||
# mock_det.custom_prepare.mca_data = mca_data
|
||||
# mock_det.custom_prepare._send_data_to_bec()
|
||||
# device_metadata = mock_det.scaninfo.scan_msg.metadata
|
||||
# metadata.update({"async_update": "append", "num_lines": mock_det.num_lines.get()})
|
||||
# data = messages.DeviceMessage(signals=dict(mca_data), metadata=device_metadata)
|
||||
# calls = mock.call(
|
||||
# topic=MessageEndpoints.device_async_readback(
|
||||
# scan_id=metadata["scan_id"], device=mock_det.name
|
||||
# ),
|
||||
# msg={"data": data},
|
||||
# expire=1800,
|
||||
# )
|
||||
|
||||
# assert mock_det.connector.xadd.call_args == calls
|
||||
|
||||
|
||||
# @pytest.mark.parametrize(
|
||||
# "scaninfo, triggersource, stopped, expected_exception",
|
||||
# [
|
||||
# (
|
||||
# {"num_points": 500, "frames_per_trigger": 1, "scan_type": "step"},
|
||||
# TriggerSource.MODE3,
|
||||
# False,
|
||||
# False,
|
||||
# ),
|
||||
# (
|
||||
# {"num_points": 500, "frames_per_trigger": 1, "scan_type": "fly"},
|
||||
# TriggerSource.MODE3,
|
||||
# False,
|
||||
# False,
|
||||
# ),
|
||||
# (
|
||||
# {"num_points": 5001, "frames_per_trigger": 2, "scan_type": "step"},
|
||||
# TriggerSource.MODE3,
|
||||
# False,
|
||||
# True,
|
||||
# ),
|
||||
# (
|
||||
# {"num_points": 500, "frames_per_trigger": 2, "scan_type": "random"},
|
||||
# TriggerSource.MODE3,
|
||||
# False,
|
||||
# True,
|
||||
# ),
|
||||
# ],
|
||||
# )
|
||||
# def test_stage(mock_det, scaninfo, triggersource, stopped, expected_exception):
|
||||
# mock_det.scaninfo.num_points = scaninfo["num_points"]
|
||||
# mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"]
|
||||
# mock_det.scaninfo.scan_type = scaninfo["scan_type"]
|
||||
# mock_det.stopped = stopped
|
||||
# with mock.patch.object(mock_det.custom_prepare, "prepare_detector_backend") as mock_prep_fw:
|
||||
# if expected_exception:
|
||||
# with pytest.raises(MCSError):
|
||||
# mock_det.stage()
|
||||
# mock_prep_fw.assert_called_once()
|
||||
# else:
|
||||
# mock_det.stage()
|
||||
# mock_prep_fw.assert_called_once()
|
||||
# # Check set_trigger
|
||||
# mock_det.input_mode.get() == triggersource
|
||||
# if scaninfo["scan_type"] == "step":
|
||||
# assert mock_det.num_use_all.get() == int(scaninfo["frames_per_trigger"]) * int(
|
||||
# scaninfo["num_points"]
|
||||
# )
|
||||
# elif scaninfo["scan_type"] == "fly":
|
||||
# assert mock_det.num_use_all.get() == int(scaninfo["num_points"])
|
||||
# mock_det.preset_real.get() == 0
|
||||
|
||||
# # # CHeck custom_prepare.arm_acquisition
|
||||
# # assert mock_det.custom_prepare.counter == 0
|
||||
# # assert mock_det.erase_start.get() == 1
|
||||
# # mock_prep_fw.assert_called_once()
|
||||
# # # Check _prep_det
|
||||
# # assert mock_det.cam.num_images.get() == int(
|
||||
# # scaninfo["num_points"] * scaninfo["frames_per_trigger"]
|
||||
# # )
|
||||
# # assert mock_det.cam.num_frames.get() == 1
|
||||
|
||||
# # mock_publish_file_location.assert_called_with(done=False)
|
||||
# # assert mock_det.cam.acquire.get() == 1
|
||||
|
||||
|
||||
# def test_prepare_detector_backend(mock_det):
|
||||
# mock_det.custom_prepare.prepare_detector_backend()
|
||||
# assert mock_det.erase_all.get() == 1
|
||||
# assert mock_det.read_mode.get() == ReadoutMode.EVENT
|
||||
|
||||
|
||||
# def test_complete(mock_det):
|
||||
# with (mock.patch.object(mock_det.custom_prepare, "finished") as mock_finished,):
|
||||
# mock_det.complete()
|
||||
# assert mock_finished.call_count == 1
|
||||
|
||||
|
||||
# def test_stop_detector_backend(mock_det):
|
||||
# mock_det.custom_prepare.stop_detector_backend()
|
||||
# assert mock_det.custom_prepare.acquisition_done is True
|
||||
|
||||
|
||||
# def test_stop(mock_det):
|
||||
# with (
|
||||
# mock.patch.object(mock_det.custom_prepare, "stop_detector") as mock_stop_det,
|
||||
# mock.patch.object(
|
||||
# mock_det.custom_prepare, "stop_detector_backend"
|
||||
# ) as mock_stop_detector_backend,
|
||||
# ):
|
||||
# mock_det.stop()
|
||||
# mock_stop_det.assert_called_once()
|
||||
# mock_stop_detector_backend.assert_called_once()
|
||||
# assert mock_det.stopped is True
|
||||
|
||||
|
||||
# @pytest.mark.parametrize(
|
||||
# "stopped, acquisition_done, acquiring_state, expected_exception",
|
||||
# [
|
||||
# (False, True, 0, False),
|
||||
# (False, False, 0, True),
|
||||
# (False, True, 1, True),
|
||||
# (True, True, 0, True),
|
||||
# ],
|
||||
# )
|
||||
# def test_finished(mock_det, stopped, acquisition_done, acquiring_state, expected_exception):
|
||||
# mock_det.custom_prepare.acquisition_done = acquisition_done
|
||||
# mock_det.acquiring._read_pv.mock_data = acquiring_state
|
||||
# mock_det.scaninfo.num_points = 500
|
||||
# mock_det.num_lines.put(500)
|
||||
# mock_det.current_channel._read_pv.mock_data = 1
|
||||
# mock_det.stopped = stopped
|
||||
|
||||
# if expected_exception:
|
||||
# with pytest.raises(MCSTimeoutError):
|
||||
# mock_det.timeout = 0.1
|
||||
# mock_det.custom_prepare.finished()
|
||||
# else:
|
||||
# mock_det.custom_prepare.finished()
|
||||
# if stopped:
|
||||
# assert mock_det.stopped is stopped
|
||||
|
||||
Reference in New Issue
Block a user