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

View File

@@ -448,7 +448,7 @@ samimg:
prefix: 'X06DA-SAMCAM:image1:'
deviceTags:
- detector
enabled: true
enabled: false
readoutPriority: async
readOnly: false
softwareTrigger: false
@@ -599,7 +599,7 @@ abr:
shx:
description: SmarGon X axis
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
enabled: true
readoutPriority: monitored
@@ -608,7 +608,7 @@ shx:
shy:
description: SmarGon Y axis
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
enabled: true
readoutPriority: monitored
@@ -626,7 +626,7 @@ shz:
chi:
description: SmarGon CHI axis
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
enabled: true
readoutPriority: monitored
@@ -641,17 +641,3 @@ phi:
readoutPriority: monitored
readOnly: 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

View File

@@ -140,7 +140,7 @@ class AerotechAbrMixin(CustomDeviceMixin):
scan_range_y = scanargs["range"]
scan_steps_y = scanargs["steps"]
d["scan_command"] = AbrCmd.VERTICAL_LINE_SCAN
d["var_1"] = scan_range_y / scan_steps_y
d["var_1"] = scan_range_y / scan_steps_y
d["var_2"] = scan_steps_y
d["var_3"] = scan_exp_time
d["var_4"] = 0

View File

@@ -8,10 +8,10 @@ import types
from ophyd import Component, PVPositioner, Signal, EpicsSignal, EpicsSignalRO, Kind, PositionerBase
from ophyd.status import Status, MoveStatus
from .A3200enums import AbrMode
from bec_lib import bec_logger
from .A3200enums import AbrMode
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
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
if kw in self.add_prefix:
return suffix

View File

@@ -20,9 +20,11 @@ class NDArrayPreview(Device):
This is a monolithic class to display images from AreaDetector's
ImagePlugin without the use of DynamicDeviceComponent or multiple
interitance (that doesn't work with BEC).
NOTE: As an explicit request, it doesnt record the data, unless
"""
# Subscriptions for plotting image
USER_ACCESS = ["image"]
USER_ACCESS = ["image", "savemode"]
SUB_MONITOR = "device_monitor_2d"
_default_sub = SUB_MONITOR
@@ -37,7 +39,7 @@ class NDArrayPreview(Device):
derived_from="array_data",
shape=("array_size_z", "array_size_y", "array_size_x"),
num_dimensions="ndimensions",
kind=Kind.normal,
kind=Kind.omitted,
)
def read(self):
@@ -47,6 +49,14 @@ class NDArrayPreview(Device):
self._run_subs(sub_type=self.SUB_MONITOR, value=image)
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):
""" 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())

View File

@@ -1,6 +1,8 @@
import time
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:
from bec_lib import bec_logger
@@ -36,7 +38,7 @@ class SmarGonSignal(Signal):
timestamp = time.time()
# 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
self._timestamp = timestamp
@@ -79,23 +81,32 @@ class SmarGonSignalRO(Signal):
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)
self._metadata["write_access"] = False
self.read_addr = read_addr
self.addr = self.parent.name
if auto_monitor:
self._mon = Thread(target=self.poll, daemon=True)
self._mon.start()
def get(self, *args, **kwargs):
r = self.parent._go_n_get(self.read_addr)
# print(r)
if isinstance(r, dict):
self.put(r[self.addr.upper()], force=True)
else:
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
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)
# Axis parameters
readback = Component(SmarGonSignalRO, kind=Kind.hinted)
readback = Component(SmarGonSignalRO, kind=Kind.hinted, auto_monitor=True)
setpoint = Component(SmarGonSignal, kind=Kind.normal)
done = Component(SignalRO, value=1, kind=Kind.normal)
# moving = Component(SmarGonMovingSignalRO, kind=Kind.config)
moving = 1
_tol = 0.001
def __init__(
self,
@@ -122,7 +134,6 @@ class SmarGonAxis(Device):
read_attrs=None,
configuration_attrs=None,
parent=None,
device_manager=None,
sg_url: str = "http://x06da-smargopolo.psi.ch:3000",
low_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["low_limit"] = low_limit
self.__class__.__dict__["setpoint"].kwargs["high_limit"] = high_limit
self.__class__.__dict__["sg_url"].kwargs["value"] = sg_url
super().__init__(
prefix=prefix,
name=name,
@@ -140,11 +151,8 @@ class SmarGonAxis(Device):
read_attrs=read_attrs,
configuration_attrs=configuration_attrs,
parent=parent,
# device_manager=device_manager,
**kwargs,
)
self.sg_url._metadata["write_access"] = False
self.sg_url.set(sg_url, force=True).wait()
def initialize(self):
"""Helper function for initial readings"""
@@ -153,6 +161,24 @@ class SmarGonAxis(Device):
r = self._go_n_get("corr_type")
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):
"""Helper function to connect to smargopolo"""
cmd = f"{self.sg_url.get()}/{address}"

View File

@@ -7,27 +7,20 @@ Created on Thu Jun 27 17:28:43 2024
@author: mohacsi_i
"""
import json
import enum
from time import sleep, time
from threading import Thread
import zmq
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 (
CustomDetectorMixin,
PSIDetectorBase,
)
from bec_lib import bec_logger
logger = bec_logger.logger
ZMQ_TOPIC_FILTER = b''
class StdDaqPreviewState(enum.IntEnum):
"""Standard DAQ ophyd device states"""
UNKNOWN = 0
DETACHED = 1
MONITORING = 2
ZMQ_TOPIC_FILTER = b""
class StdDaqPreviewMixin(CustomDetectorMixin):
@@ -35,6 +28,7 @@ class StdDaqPreviewMixin(CustomDetectorMixin):
Parent class: CustomDetectorMixin
"""
_mon = None
def on_stage(self):
@@ -43,9 +37,7 @@ class StdDaqPreviewMixin(CustomDetectorMixin):
self.parent.unstage()
sleep(0.5)
logger.info(
f"[{self.parent.name}] Attaching monitor to {self.parent.url.get()}"
)
logger.info(f"[{self.parent.name}] Attaching monitor to {self.parent.url.get()}")
self.parent.connect()
self._stop_polling = False
self._mon = Thread(target=self.poll, daemon=True)
@@ -66,7 +58,6 @@ class StdDaqPreviewMixin(CustomDetectorMixin):
def poll(self):
"""Collect streamed updates"""
self.parent.status.set(StdDaqPreviewState.MONITORING, force=True)
try:
t_last = time()
while True:
@@ -82,7 +73,8 @@ class StdDaqPreviewMixin(CustomDetectorMixin):
# Length and throtling checks
if len(r) != 2:
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_elapsed = t_curr - t_last
if t_elapsed < self.parent.throttle.get():
@@ -94,26 +86,26 @@ class StdDaqPreviewMixin(CustomDetectorMixin):
# Update image and update subscribers
header = json.loads(meta)
if header["type"] == "uint16":
image = np.frombuffer(data, dtype=np.uint16)
if header["type"] == "uint8":
image = np.frombuffer(data, dtype=np.uint8)
if image.size != np.prod(header['shape']):
image = np.frombuffer(data, dtype=header["type"])
if image.size != np.prod(header["shape"]):
err = f"Unexpected array size of {image.size} for header: {header}"
raise ValueError(err)
image = image.reshape(header['shape'])
image = image.reshape(header["shape"])
# Update image and update subscribers
self.parent.frameno.put(header['frame'], force=True)
self.parent.image_shape.put(header['shape'], force=True)
self.parent.image.put(image, force=True)
self.parent.array_counter.put(header["frame"], force=True)
self.parent.ndimensions.put(len(header["shape"]), 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._run_subs(sub_type=self.parent.SUB_MONITOR, value=image)
t_last = t_curr
logger.info(
f"[{self.parent.name}] Updated frame {header['frame']}\t"
f"Shape: {header['shape']}\tMean: {np.mean(image):.3f}"
)
)
except ValueError:
# Happens when ZMQ partially delivers the multipart message
pass
@@ -125,37 +117,39 @@ class StdDaqPreviewMixin(CustomDetectorMixin):
raise
finally:
self._mon = None
self.parent.status.set(StdDaqPreviewState.DETACHED, force=True)
logger.info(f"[{self.parent.name}]\tDetaching monitor")
class StdDaqPreviewDetector(PSIDetectorBase):
"""Detector wrapper class around the StdDaq preview image stream.
This was meant to provide live image stream directly from the StdDAQ
but also works with other ARRAY v1 streamers, like the AreaDetector
ZMQ plugin.
Note that the preview stream must be already throtled in order to cope
with the incoming data and the python class might throttle it further.
This was meant to provide live image stream directly from the StdDAQ but
also works with other ARRAY v1 streamers, like the AreaDetector ZMQ plugin.
Note that the preview stream must be already throtled in order to cope 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:
cam_widget = gui.add_dock('cam_dock1').add_widget('BECFigure').image('daq_stream1')
"""
# Subscriptions for plotting image
USER_ACCESS = ["get_image"]
USER_ACCESS = ["image", "savemode"]
SUB_MONITOR = "device_monitor_2d"
_default_sub = SUB_MONITOR
custom_prepare_cls = StdDaqPreviewMixin
# Status attributes
# Configuration attributes
url = Component(Signal, kind=Kind.config, metadata={"write_access": False})
throttle = Component(Signal, value=0.25, kind=Kind.config)
status = Component(Signal, value=StdDaqPreviewState.UNKNOWN, kind=Kind.omitted, metadata={"write_access": False})
frameno = Component(Signal, kind=Kind.hinted, metadata={"write_access": False})
image_shape = Component(Signal, kind=Kind.normal, metadata={"write_access": False})
# FIXME: The BEC client caches the read()s from the last 50 scans
image = Component(Signal, kind=Kind.omitted, metadata={"write_access": False})
# Streamed data status
array_counter = Component(Signal, kind=Kind.hinted, metadata={"write_access": False})
ndimensions = Component(Signal, kind=Kind.normal, metadata={"write_access": False})
array_size = Component(Signal, kind=Kind.normal, 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
def __init__(
@@ -183,8 +177,16 @@ class StdDaqPreviewDetector(PSIDetectorBase):
sleep(1)
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
due to some caching on the BEC.
"""

View File

@@ -8,4 +8,4 @@ from .A3200 import AerotechAbrStage
from .A3200utils import A3200Axis
from .SmarGon import SmarGonAxis
from .StdDaqPreview import StdDaqPreviewDetector
from .NDArrayPreview import NDArrayPreview
from .NDArrayPreview import NDArrayPreview