Samcam image preview and smargon waiting

This commit is contained in:
gac-x06da
2025-01-27 17:37:53 +01:00
parent 286c7a4bff
commit 4fc31e5f5d
7 changed files with 101 additions and 77 deletions
@@ -448,7 +448,7 @@ samimg:
prefix: 'X06DA-SAMCAM:image1:' prefix: 'X06DA-SAMCAM:image1:'
deviceTags: deviceTags:
- detector - detector
enabled: true enabled: false
readoutPriority: async readoutPriority: async
readOnly: false readOnly: false
softwareTrigger: false softwareTrigger: false
@@ -599,7 +599,7 @@ abr:
shx: shx:
description: SmarGon X axis description: SmarGon X axis
deviceClass: pxiii_bec.devices.SmarGonAxis deviceClass: pxiii_bec.devices.SmarGonAxis
deviceConfig: {prefix: 'SCS', sg_url: 'http://x06da-smargopolo.psi.ch:3000'} deviceConfig: {prefix: 'SCS', low_limit: -2, high_limit: 2, sg_url: 'http://x06da-smargopolo.psi.ch:3000'}
onFailure: buffer onFailure: buffer
enabled: true enabled: true
readoutPriority: monitored readoutPriority: monitored
@@ -608,7 +608,7 @@ shx:
shy: shy:
description: SmarGon Y axis description: SmarGon Y axis
deviceClass: pxiii_bec.devices.SmarGonAxis deviceClass: pxiii_bec.devices.SmarGonAxis
deviceConfig: {prefix: 'SCS', sg_url: 'http://x06da-smargopolo.psi.ch:3000'} deviceConfig: {prefix: 'SCS', low_limit: -2, high_limit: 2, sg_url: 'http://x06da-smargopolo.psi.ch:3000'}
onFailure: buffer onFailure: buffer
enabled: true enabled: true
readoutPriority: monitored readoutPriority: monitored
@@ -626,7 +626,7 @@ shz:
chi: chi:
description: SmarGon CHI axis description: SmarGon CHI axis
deviceClass: pxiii_bec.devices.SmarGonAxis deviceClass: pxiii_bec.devices.SmarGonAxis
deviceConfig: {prefix: 'SCS', sg_url: 'http://x06da-smargopolo.psi.ch:3000'} deviceConfig: {prefix: 'SCS', low_limit: 0, high_limit: 40, sg_url: 'http://x06da-smargopolo.psi.ch:3000'}
onFailure: buffer onFailure: buffer
enabled: true enabled: true
readoutPriority: monitored readoutPriority: monitored
@@ -641,17 +641,3 @@ phi:
readoutPriority: monitored readoutPriority: monitored
readOnly: false readOnly: false
softwareTrigger: false softwareTrigger: false
# samimgs:
# description: Sample camera image
# deviceClass: ophyd_devices.devices.areadetector.plugins.ImagePlugin_V35
# deviceConfig: {prefix: 'X06DA-SAMCAM:image1:', foo: 'bar'}
# onFailure: buffer
# enabled: false
# readoutPriority: monitored
# readOnly: true
# softwareTrigger: false
+3 -3
View File
@@ -8,10 +8,10 @@ import types
from ophyd import Component, PVPositioner, Signal, EpicsSignal, EpicsSignalRO, Kind, PositionerBase from ophyd import Component, PVPositioner, Signal, EpicsSignal, EpicsSignalRO, Kind, PositionerBase
from ophyd.status import Status, MoveStatus from ophyd.status import Status, MoveStatus
from .A3200enums import AbrMode
from bec_lib import bec_logger from bec_lib import bec_logger
from .A3200enums import AbrMode
logger = bec_logger.logger logger = bec_logger.logger
@@ -115,7 +115,7 @@ class A3200Axis(PVPositioner):
# Patching the parent's PVs into the axis class to check for direct/locked mode # Patching the parent's PVs into the axis class to check for direct/locked mode
if parent is None: if parent is None:
def maybe_add_prefix(self, instance, kw, suffix): def maybe_add_prefix(self, _, kw, suffix):
# Patched not to enforce parent prefix when no parent # Patched not to enforce parent prefix when no parent
if kw in self.add_prefix: if kw in self.add_prefix:
return suffix return suffix
+12 -2
View File
@@ -20,9 +20,11 @@ class NDArrayPreview(Device):
This is a monolithic class to display images from AreaDetector's This is a monolithic class to display images from AreaDetector's
ImagePlugin without the use of DynamicDeviceComponent or multiple ImagePlugin without the use of DynamicDeviceComponent or multiple
interitance (that doesn't work with BEC). interitance (that doesn't work with BEC).
NOTE: As an explicit request, it doesnt record the data, unless
""" """
# Subscriptions for plotting image # Subscriptions for plotting image
USER_ACCESS = ["image"] USER_ACCESS = ["image", "savemode"]
SUB_MONITOR = "device_monitor_2d" SUB_MONITOR = "device_monitor_2d"
_default_sub = SUB_MONITOR _default_sub = SUB_MONITOR
@@ -37,7 +39,7 @@ class NDArrayPreview(Device):
derived_from="array_data", derived_from="array_data",
shape=("array_size_z", "array_size_y", "array_size_x"), shape=("array_size_z", "array_size_y", "array_size_x"),
num_dimensions="ndimensions", num_dimensions="ndimensions",
kind=Kind.normal, kind=Kind.omitted,
) )
def read(self): def read(self):
@@ -47,6 +49,14 @@ class NDArrayPreview(Device):
self._run_subs(sub_type=self.SUB_MONITOR, value=image) self._run_subs(sub_type=self.SUB_MONITOR, value=image)
return super().read() return super().read()
def savemode(self, save=False):
""" Toggle save mode for the shaped image"""
#pylint: disable=protected-access
if save:
self.shaped_image._kind = Kind.normal
else:
self.shaped_image._kind = Kind.omitted
def image(self): def image(self):
""" Fallback method in case image streaming fills up the BEC""" """ Fallback method in case image streaming fills up the BEC"""
array_size = (self.array_size_z.get(), self.array_size_y.get(), self.array_size_x.get()) array_size = (self.array_size_z.get(), self.array_size_y.get(), self.array_size_x.get())
+38 -12
View File
@@ -1,6 +1,8 @@
import time import time
import requests import requests
from ophyd import Component, Device, Kind, Signal, SignalRO from threading import Thread
from ophyd import Component, Device, Kind, Signal, SignalRO, PVPositioner
from ophyd.status import SubscriptionStatus
try: try:
from bec_lib import bec_logger from bec_lib import bec_logger
@@ -36,7 +38,7 @@ class SmarGonSignal(Signal):
timestamp = time.time() timestamp = time.time()
# Perform the actual write to SmargoPolo # Perform the actual write to SmargoPolo
r = self.parent._go_n_put(f"{self.write_addr}?{self.addr.upper()}={value}", **kwargs) r = self.parent._go_n_put(f"{self.write_addr}?{self.addr.upper()}={value}")
old_value = self._readback old_value = self._readback
self._timestamp = timestamp self._timestamp = timestamp
@@ -79,23 +81,32 @@ class SmarGonSignalRO(Signal):
TODO: Add monitoring TODO: Add monitoring
""" """
def __init__(self, *args, read_addr="readbackSCS", **kwargs): def __init__(self, *args, read_addr="readbackSCS", auto_monitor=False, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._metadata["write_access"] = False self._metadata["write_access"] = False
self.read_addr = read_addr self.read_addr = read_addr
self.addr = self.parent.name self.addr = self.parent.name
if auto_monitor:
self._mon = Thread(target=self.poll, daemon=True)
self._mon.start()
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
r = self.parent._go_n_get(self.read_addr) r = self.parent._go_n_get(self.read_addr)
# print(r)
if isinstance(r, dict): if isinstance(r, dict):
self.put(r[self.addr.upper()], force=True) self.put(r[self.addr.upper()], force=True)
else: else:
self.put(r, force=True) self.put(r, force=True)
return super().get(*args, **kwargs) return self._readback
def poll(self, *args, **kwargs):
""" Fooo"""
while True:
time.sleep(0.2)
self.get()
class SmarGonAxis(Device): class SmarGonAxis(PVPositioner):
"""SmarGon client deice """SmarGon client deice
This class controls the SmarGon goniometer via the REST interface. This class controls the SmarGon goniometer via the REST interface.
@@ -107,11 +118,12 @@ class SmarGonAxis(Device):
mode = Component(SmarGonSignalRO, read_addr="mode", kind=Kind.config) mode = Component(SmarGonSignalRO, read_addr="mode", kind=Kind.config)
# Axis parameters # Axis parameters
readback = Component(SmarGonSignalRO, kind=Kind.hinted) readback = Component(SmarGonSignalRO, kind=Kind.hinted, auto_monitor=True)
setpoint = Component(SmarGonSignal, kind=Kind.normal) setpoint = Component(SmarGonSignal, kind=Kind.normal)
done = Component(SignalRO, value=1, kind=Kind.normal) done = Component(SignalRO, value=1, kind=Kind.normal)
# moving = Component(SmarGonMovingSignalRO, kind=Kind.config) # moving = Component(SmarGonMovingSignalRO, kind=Kind.config)
moving = 1 moving = 1
_tol = 0.001
def __init__( def __init__(
self, self,
@@ -122,7 +134,6 @@ class SmarGonAxis(Device):
read_attrs=None, read_attrs=None,
configuration_attrs=None, configuration_attrs=None,
parent=None, parent=None,
device_manager=None,
sg_url: str = "http://x06da-smargopolo.psi.ch:3000", sg_url: str = "http://x06da-smargopolo.psi.ch:3000",
low_limit=None, low_limit=None,
high_limit=None, high_limit=None,
@@ -132,7 +143,7 @@ class SmarGonAxis(Device):
self.__class__.__dict__["setpoint"].kwargs["write_addr"] = f"target{prefix}" self.__class__.__dict__["setpoint"].kwargs["write_addr"] = f"target{prefix}"
self.__class__.__dict__["setpoint"].kwargs["low_limit"] = low_limit self.__class__.__dict__["setpoint"].kwargs["low_limit"] = low_limit
self.__class__.__dict__["setpoint"].kwargs["high_limit"] = high_limit self.__class__.__dict__["setpoint"].kwargs["high_limit"] = high_limit
self.__class__.__dict__["sg_url"].kwargs["value"] = sg_url
super().__init__( super().__init__(
prefix=prefix, prefix=prefix,
name=name, name=name,
@@ -140,11 +151,8 @@ class SmarGonAxis(Device):
read_attrs=read_attrs, read_attrs=read_attrs,
configuration_attrs=configuration_attrs, configuration_attrs=configuration_attrs,
parent=parent, parent=parent,
# device_manager=device_manager,
**kwargs, **kwargs,
) )
self.sg_url._metadata["write_access"] = False
self.sg_url.set(sg_url, force=True).wait()
def initialize(self): def initialize(self):
"""Helper function for initial readings""" """Helper function for initial readings"""
@@ -153,6 +161,24 @@ class SmarGonAxis(Device):
r = self._go_n_get("corr_type") r = self._go_n_get("corr_type")
print(r) print(r)
def move(self, position, wait=True, timeout=None, moved_cb=None):
status = self.setpoint.set(position, settle_time=0.1)
if not wait:
return status
else:
status.wait()
def on_target(*, value, **_):
distance = abs(value-position)
print(distance)
return bool(distance<self._tol)
status = SubscriptionStatus(
self.readback, on_target, timeout=timeout, settle_time=0.1
)
return status
def _go_n_get(self, address, **kwargs): def _go_n_get(self, address, **kwargs):
"""Helper function to connect to smargopolo""" """Helper function to connect to smargopolo"""
cmd = f"{self.sg_url.get()}/{address}" cmd = f"{self.sg_url.get()}/{address}"
+41 -39
View File
@@ -7,27 +7,20 @@ Created on Thu Jun 27 17:28:43 2024
@author: mohacsi_i @author: mohacsi_i
""" """
import json import json
import enum
from time import sleep, time from time import sleep, time
from threading import Thread from threading import Thread
import zmq import zmq
import numpy as np import numpy as np
from ophyd import Device, Signal, Component, Kind, DeviceStatus from ophyd import Device, Signal, Component, Kind
from ophyd_devices.interfaces.base_classes.psi_detector_base import ( from ophyd_devices.interfaces.base_classes.psi_detector_base import (
CustomDetectorMixin, CustomDetectorMixin,
PSIDetectorBase, PSIDetectorBase,
) )
from bec_lib import bec_logger from bec_lib import bec_logger
logger = bec_logger.logger logger = bec_logger.logger
ZMQ_TOPIC_FILTER = b'' ZMQ_TOPIC_FILTER = b""
class StdDaqPreviewState(enum.IntEnum):
"""Standard DAQ ophyd device states"""
UNKNOWN = 0
DETACHED = 1
MONITORING = 2
class StdDaqPreviewMixin(CustomDetectorMixin): class StdDaqPreviewMixin(CustomDetectorMixin):
@@ -35,6 +28,7 @@ class StdDaqPreviewMixin(CustomDetectorMixin):
Parent class: CustomDetectorMixin Parent class: CustomDetectorMixin
""" """
_mon = None _mon = None
def on_stage(self): def on_stage(self):
@@ -43,9 +37,7 @@ class StdDaqPreviewMixin(CustomDetectorMixin):
self.parent.unstage() self.parent.unstage()
sleep(0.5) sleep(0.5)
logger.info( logger.info(f"[{self.parent.name}] Attaching monitor to {self.parent.url.get()}")
f"[{self.parent.name}] Attaching monitor to {self.parent.url.get()}"
)
self.parent.connect() self.parent.connect()
self._stop_polling = False self._stop_polling = False
self._mon = Thread(target=self.poll, daemon=True) self._mon = Thread(target=self.poll, daemon=True)
@@ -66,7 +58,6 @@ class StdDaqPreviewMixin(CustomDetectorMixin):
def poll(self): def poll(self):
"""Collect streamed updates""" """Collect streamed updates"""
self.parent.status.set(StdDaqPreviewState.MONITORING, force=True)
try: try:
t_last = time() t_last = time()
while True: while True:
@@ -82,7 +73,8 @@ class StdDaqPreviewMixin(CustomDetectorMixin):
# Length and throtling checks # Length and throtling checks
if len(r) != 2: if len(r) != 2:
logger.warning( logger.warning(
f"[{self.parent.name}] Received malformed array of length {len(r)}") f"[{self.parent.name}] Received malformed array of length {len(r)}"
)
t_curr = time() t_curr = time()
t_elapsed = t_curr - t_last t_elapsed = t_curr - t_last
if t_elapsed < self.parent.throttle.get(): if t_elapsed < self.parent.throttle.get():
@@ -94,26 +86,26 @@ class StdDaqPreviewMixin(CustomDetectorMixin):
# Update image and update subscribers # Update image and update subscribers
header = json.loads(meta) header = json.loads(meta)
if header["type"] == "uint16": image = np.frombuffer(data, dtype=header["type"])
image = np.frombuffer(data, dtype=np.uint16) if image.size != np.prod(header["shape"]):
if header["type"] == "uint8":
image = np.frombuffer(data, dtype=np.uint8)
if image.size != np.prod(header['shape']):
err = f"Unexpected array size of {image.size} for header: {header}" err = f"Unexpected array size of {image.size} for header: {header}"
raise ValueError(err) raise ValueError(err)
image = image.reshape(header['shape']) image = image.reshape(header["shape"])
# Update image and update subscribers # Update image and update subscribers
self.parent.frameno.put(header['frame'], force=True) self.parent.array_counter.put(header["frame"], force=True)
self.parent.image_shape.put(header['shape'], force=True) self.parent.ndimensions.put(len(header["shape"]), force=True)
self.parent.image.put(image, force=True) self.parent.array_size.put(header["shape"], force=True)
# self.parent.array_data.put(data, force=True)
self.parent.shaped_image.put(image, force=True)
self.parent._last_image = image self.parent._last_image = image
self.parent._run_subs(sub_type=self.parent.SUB_MONITOR, value=image) self.parent._run_subs(sub_type=self.parent.SUB_MONITOR, value=image)
t_last = t_curr t_last = t_curr
logger.info( logger.info(
f"[{self.parent.name}] Updated frame {header['frame']}\t" f"[{self.parent.name}] Updated frame {header['frame']}\t"
f"Shape: {header['shape']}\tMean: {np.mean(image):.3f}" f"Shape: {header['shape']}\tMean: {np.mean(image):.3f}"
) )
except ValueError: except ValueError:
# Happens when ZMQ partially delivers the multipart message # Happens when ZMQ partially delivers the multipart message
pass pass
@@ -125,37 +117,39 @@ class StdDaqPreviewMixin(CustomDetectorMixin):
raise raise
finally: finally:
self._mon = None self._mon = None
self.parent.status.set(StdDaqPreviewState.DETACHED, force=True)
logger.info(f"[{self.parent.name}]\tDetaching monitor") logger.info(f"[{self.parent.name}]\tDetaching monitor")
class StdDaqPreviewDetector(PSIDetectorBase): class StdDaqPreviewDetector(PSIDetectorBase):
"""Detector wrapper class around the StdDaq preview image stream. """Detector wrapper class around the StdDaq preview image stream.
This was meant to provide live image stream directly from the StdDAQ This was meant to provide live image stream directly from the StdDAQ but
but also works with other ARRAY v1 streamers, like the AreaDetector also works with other ARRAY v1 streamers, like the AreaDetector ZMQ plugin.
ZMQ plugin. Note that the preview stream must be already throtled in order to cope with
Note that the preview stream must be already throtled in order to cope the incoming data and the python class might throttle it further.
with the incoming data and the python class might throttle it further.
NOTE: As an explicit request, it does not record the image data.
You can add a preview widget to the dock by: You can add a preview widget to the dock by:
cam_widget = gui.add_dock('cam_dock1').add_widget('BECFigure').image('daq_stream1') cam_widget = gui.add_dock('cam_dock1').add_widget('BECFigure').image('daq_stream1')
""" """
# Subscriptions for plotting image # Subscriptions for plotting image
USER_ACCESS = ["get_image"] USER_ACCESS = ["image", "savemode"]
SUB_MONITOR = "device_monitor_2d" SUB_MONITOR = "device_monitor_2d"
_default_sub = SUB_MONITOR _default_sub = SUB_MONITOR
custom_prepare_cls = StdDaqPreviewMixin custom_prepare_cls = StdDaqPreviewMixin
# Status attributes # Configuration attributes
url = Component(Signal, kind=Kind.config, metadata={"write_access": False}) url = Component(Signal, kind=Kind.config, metadata={"write_access": False})
throttle = Component(Signal, value=0.25, kind=Kind.config) throttle = Component(Signal, value=0.25, kind=Kind.config)
status = Component(Signal, value=StdDaqPreviewState.UNKNOWN, kind=Kind.omitted, metadata={"write_access": False}) # Streamed data status
frameno = Component(Signal, kind=Kind.hinted, metadata={"write_access": False}) array_counter = Component(Signal, kind=Kind.hinted, metadata={"write_access": False})
image_shape = Component(Signal, kind=Kind.normal, metadata={"write_access": False}) ndimensions = Component(Signal, kind=Kind.normal, metadata={"write_access": False})
# FIXME: The BEC client caches the read()s from the last 50 scans array_size = Component(Signal, kind=Kind.normal, metadata={"write_access": False})
image = Component(Signal, kind=Kind.omitted, metadata={"write_access": False}) # array_data = Component(Signal, kind=Kind.omitted, metadata={"write_access": False})
shaped_image = Component(Signal, kind=Kind.omitted, metadata={"write_access": False})
_last_image = None _last_image = None
def __init__( def __init__(
@@ -183,7 +177,15 @@ class StdDaqPreviewDetector(PSIDetectorBase):
sleep(1) sleep(1)
self._socket.connect(self.url.get()) self._socket.connect(self.url.get())
def get_image(self): def savemode(self, save=False):
""" Toggle save mode for the shaped image"""
#pylint: disable=protected-access
if save:
self.shaped_image._kind = Kind.normal
else:
self.shaped_image._kind = Kind.omitted
def image(self):
""" """
Gets the last image as an attribute in case image must be abandoned Gets the last image as an attribute in case image must be abandoned
due to some caching on the BEC. due to some caching on the BEC.