Compare commits

...

6 Commits

Author SHA1 Message Date
149af32ab1 fix: rate limit warning log in live mode
All checks were successful
CI for csaxs_bec / test (pull_request) Successful in 1m30s
CI for csaxs_bec / test (push) Successful in 1m32s
2026-01-06 13:22:43 +01:00
x01dc
47f0b66791 mod gui tools for pdf viewer
All checks were successful
CI for csaxs_bec / test (pull_request) Successful in 1m29s
CI for csaxs_bec / test (push) Successful in 1m28s
2026-01-06 12:49:40 +01:00
2c0fced9b7 FZP layout 60 nm
All checks were successful
CI for csaxs_bec / test (push) Successful in 1m32s
2026-01-06 12:00:56 +01:00
d99b44b619 refactor(configs):Migrate deviceClass for EpicsMotorEX to EpicsUserMotorVME
All checks were successful
CI for csaxs_bec / test (pull_request) Successful in 1m30s
CI for csaxs_bec / test (push) Successful in 1m30s
2025-12-11 07:52:52 +01:00
x12sa
dbab981ac2 add micfoc and change some motor settings 2025-12-11 07:52:52 +01:00
x12sa
bdd7f1767f new yaml file sastt for sastt setup 2025-12-11 07:52:52 +01:00
6 changed files with 185 additions and 37 deletions

View File

@@ -20,6 +20,7 @@ class flomniGuiTools:
def __init__(self):
self.text_box = None
self.progressbar = None
self.pdf_viewer = None
def set_client(self, client):
self.client = client
@@ -97,6 +98,7 @@ class flomniGuiTools:
self.gui.flomni.delete_all()
self.progressbar = None
self.text_box = None
self.pdf_viewer = None
def flomnigui_idle(self):
self.flomnigui_show_gui()
@@ -114,6 +116,27 @@ class flomniGuiTools:
)
idle_text_box.set_html_text(text)
def flomnigui_docs(self):
import csaxs_bec
from pathlib import Path
# Ensure this is a Path object, not a string
csaxs_bec_basepath = Path(csaxs_bec.__file__)
pdf_file = "FZP_flomni_with_CS.pdf"
# Build the absolute path correctly
pdf_file = (
csaxs_bec_basepath.parent / 'bec_ipython_client' / 'plugins' / 'flomni' / 'docs' / pdf_file
).resolve()
if self.pdf_viewer is None:
self.flomnigui_show_gui()
self.flomnigui_remove_all_docks()
self.pdf_viewer = self.gui.flomni.new(widget="PdfViewerWidget")
self.pdf_viewer.PdfViewerWidget.load_pdf(str(pdf_file))
def _flomnicam_check_device_exists(self, device):
try:
device
@@ -156,8 +179,8 @@ class flomniGuiTools:
)
self.progressbar.set_value([progress, subtomo_progress, 0])
if self.text_box is not None:
text = f"Progress report:\n Tomo type: ....................... {self.progress['tomo_type']}\n Projection: ...................... {self.progress['projection']:.0f}\n Total projections expected ....... {self.progress['total_projections']}\n Angle: ........................... {self.progress['angle']}\n Current subtomo: ................. {self.progress['subtomo']}\n Current projection within subtomo: {self.progress['subtomo_projection']}\n Total projections per subtomo: ... {self.progress['subtomo_total_projections']}"
self.text_box.set_plain_text(text)
text = f"Progress report:\n Tomo type: ....................... {self.progress['tomo_type']}\n Projection: ...................... {self.progress['projection']:.0f}\n Total projections expected ....... {self.progress['total_projections']}\n Angle: ........................... {self.progress['angle']}\n Current subtomo: ................. {self.progress['subtomo']}\n Current projection within subtomo: {self.progress['subtomo_projection']}\n Total projections per subtomo: ... {self.progress['subtomo_total_projections']}"
self.text_box.set_plain_text(text)
if __name__ == "__main__":

View File

@@ -115,7 +115,7 @@ samy:
softwareTrigger: false
micfoc:
description: Focusing motor of Microscope stage
deviceClass: ophyd_devices.devices.EpicsMotorEx
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
deviceConfig:
prefix: X12SA-ES2-ES06
motor_resolution: 0.00125
@@ -133,7 +133,7 @@ micfoc:
softwareTrigger: false
owis_samx:
description: Owis motor stage samx
deviceClass: ophyd_devices.devices.EpicsMotorEx
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
deviceConfig:
prefix: X12SA-ES2-ES01
motor_resolution: 0.00125
@@ -151,7 +151,7 @@ owis_samx:
softwareTrigger: false
owis_samy:
description: Owis motor stage samx
deviceClass: ophyd_devices.devices.EpicsMotorEx
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
deviceConfig:
prefix: X12SA-ES2-ES02
motor_resolution: 0.00125
@@ -169,7 +169,7 @@ owis_samy:
softwareTrigger: false
rotx:
description: Rotation stage rotx
deviceClass: ophyd_devices.devices.EpicsMotorEx
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
deviceConfig:
prefix: X12SA-ES2-ES05
motor_resolution: 0.0025
@@ -190,7 +190,7 @@ rotx:
softwareTrigger: false
roty:
description: Rotation stage rotx
deviceClass: ophyd_devices.devices.EpicsMotorEx
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
deviceConfig:
prefix: X12SA-ES2-ES04
motor_resolution: 0.0025

View File

@@ -0,0 +1,96 @@
samx:
description: Owis motor stage samx
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
deviceConfig:
prefix: X12SA-ES2-ES01
motor_resolution: 0.00125
base_velocity: 0.0625
velocity: 10
backlash_distance: 0.125
acceleration: 0.2
user_offset_dir: 1
deviceTags:
- cSAXS
- owis_samx
onFailure: buffer
enabled: true
readoutPriority: baseline
softwareTrigger: false
samy:
description: Owis motor stage samy
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
deviceConfig:
prefix: X12SA-ES2-ES02
motor_resolution: 0.00125
base_velocity: 0.0625
velocity: 10
backlash_distance: 0.125
acceleration: 0.2
user_offset_dir: 0
deviceTags:
- cSAXS
- owis_samx
onFailure: buffer
enabled: true
readoutPriority: baseline
softwareTrigger: false
rotx:
description: Rotation stage rotx
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
deviceConfig:
prefix: X12SA-ES2-ES03
motor_resolution: 0.0025
base_velocity: 0.5
velocity: 7.5
backlash_distance: 0.25
acceleration: 0.2
user_offset_dir: 1
limits:
- -0.1
- 0.1
deviceTags:
- cSAXS
- rotx
onFailure: buffer
enabled: true
readoutPriority: baseline
softwareTrigger: false
roty:
description: Rotation stage roty
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
deviceConfig:
prefix: X12SA-ES2-ES04
motor_resolution: 0.0025
base_velocity: 0.5
velocity: 7.5
backlash_distance: 0.25
acceleration: 0.2
user_offset_dir: 0
limits:
- -0.1
- 0.1
deviceTags:
- cSAXS
- roty
onFailure: buffer
enabled: true
readoutPriority: baseline
softwareTrigger: false
micfoc:
description: Focusing motor of Microscope stage
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
deviceConfig:
prefix: X12SA-ES2-ES05
motor_resolution: 0.00125
base_velocity: 0.25
velocity: 2.5
backlash_distance: 0.125
acceleration: 0.4
user_offset_dir: 0
deviceTags:
- cSAXS
- micfoc
onFailure: buffer
enabled: true
readoutPriority: baseline
softwareTrigger: false

View File

@@ -15,10 +15,10 @@ CI/CD pipelines can run without the pyueye library or the related DLLs installed
from __future__ import annotations
import atexit
import time
from typing import Literal
import numpy as np
import time
from bec_lib.logger import bec_logger
from csaxs_bec.devices.ids_cameras.base_integration.utils import check_error
@@ -67,8 +67,8 @@ class 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
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
@@ -77,16 +77,16 @@ class IDSCameraObject:
) # 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
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
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
@@ -160,12 +160,12 @@ class Camera:
"""
def __init__(
self,
camera_id: int,
m_n_colormode: Literal[0, 1, 2, 3] = 1,
bits_per_pixel: int = 24,
connect: bool = True,
force_monochrome: bool = False,
self,
camera_id: int,
m_n_colormode: Literal[0, 1, 2, 3] = 1,
bits_per_pixel: int = 24,
connect: bool = True,
force_monochrome: bool = False,
):
self.ueye = ueye
self.camera_id = camera_id
@@ -173,8 +173,13 @@ class Camera:
self.force_monochrome = force_monochrome
self._connected = False
self.cam = None
atexit.register(self.on_disconnect)
self._enable_warning_rate_limit: bool = False
self._last_rate_limited_log: float = 0
self._warning_log_rate_limit_s: float = 10
if connect:
self.on_connect()
@@ -255,7 +260,7 @@ class Camera:
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.")
self._rate_limited_warning_log("Camera is not connected.")
return None
array = self.ueye.get_data(
self.cam.pc_image_mem,
@@ -282,6 +287,22 @@ class Camera:
return img
def set_camera_rate_limiting(self, enabled: bool, rate_limit_s: float | None = None):
if rate_limit_s is not None:
if rate_limit_s <= 0:
raise ValueError(f"Invalid rate limit: {rate_limit_s}, must be positive nonzero.")
self._warning_log_rate_limit_s = rate_limit_s
self._enable_warning_rate_limit = enabled
def _rate_limited_warning_log(self, msg: "str"):
if (
self._enable_warning_rate_limit
and time.monotonic() < self._last_rate_limited_log + self._warning_log_rate_limit_s
):
return
self._last_rate_limited_log = time.monotonic()
logger.warning(msg)
if __name__ == "__main__":
# Example usage

View File

@@ -29,8 +29,14 @@ class IDSCamera(PSIDeviceBase):
to interact with the IDS camera using the pyueye library.
"""
image = Cpt(PreviewSignal, name="image", ndim=2, doc="Preview signal for the camera.", num_rotation_90=0,
transpose=False)
image = Cpt(
PreviewSignal,
name="image",
ndim=2,
doc="Preview signal for the camera.",
num_rotation_90=0,
transpose=False,
)
roi_signal = Cpt(
AsyncSignal,
name="roi_signal",
@@ -43,19 +49,19 @@ class IDSCamera(PSIDeviceBase):
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,
num_rotation_90: int = 0,
transpose: bool = False,
force_monochrome: bool = False,
**kwargs,
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,
num_rotation_90: int = 0,
transpose: bool = False,
force_monochrome: bool = False,
**kwargs,
):
"""Initialize the IDS Camera.
@@ -133,7 +139,7 @@ class IDSCamera(PSIDeviceBase):
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
mask[y : y + height, x : x + width] = 1
self.mask = mask
def _start_live(self):
@@ -162,6 +168,7 @@ class IDSCamera(PSIDeviceBase):
def _live_mode_loop(self, stop_event: threading.Event):
"""Loop to capture images in live mode."""
self.cam.set_camera_rate_limiting(True)
while not stop_event.is_set():
try:
self.process_data(self.cam.get_image_data())
@@ -169,6 +176,7 @@ class IDSCamera(PSIDeviceBase):
logger.error(f"Error in live mode loop: {e}")
break
stop_event.wait(0.2) # 5 Hz
self.cam.set_camera_rate_limiting(False)
def process_data(self, image: np.ndarray | None):
"""Process the image data before sending it to the preview signal."""