From 0633a43957c0b63346817aa7e860b1598cae75be Mon Sep 17 00:00:00 2001 From: ci_update_bot Date: Sun, 23 Jun 2024 09:16:36 +0000 Subject: [PATCH 01/47] docs: Update device list --- tomcat_bec/devices/device_list.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tomcat_bec/devices/device_list.md b/tomcat_bec/devices/device_list.md index 8491d1a..7dc6ab2 100644 --- a/tomcat_bec/devices/device_list.md +++ b/tomcat_bec/devices/device_list.md @@ -3,11 +3,6 @@ ### tomcat_bec | Device | Documentation | Module | | :----- | :------------- | :------ | -| GrashopperTOMCAT |
Grashopper detector for TOMCAT

Parent class: PSIDetectorBase

class attributes:
custom_prepare_cls (GrashopperTOMCATSetup) : Custom detector setup class for TOMCAT,
inherits from CustomDetectorMixin
cam (SLSDetectorCam) : Detector camera
image (SLSImagePlugin) : Image plugin for detector
| [tomcat_bec.devices.grashopper_tomcat](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/grashopper_tomcat.py) | -| SLSDetectorCam |
SLS Detector Camera - Grashoppter

Base class to map EPICS PVs to ophyd signals.
| [tomcat_bec.devices.grashopper_tomcat](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/grashopper_tomcat.py) | -| SLSImagePlugin | SLS Image Plugin

Image plugin for SLS detector imitating the behaviour of ImagePlugin from
ophyd's areadetector plugins.
| [tomcat_bec.devices.grashopper_tomcat](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/grashopper_tomcat.py) | -| TomcatAerotechRotation | Special motor class that provides flyer interface and progress bar. | [tomcat_bec.devices.tomcat_rotation_motors](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/tomcat_rotation_motors.py) | -| EpicsMotorX | Special motor class that provides flyer interface and progress bar. | [tomcat_bec.devices.aerotech.AerotechAutomation1](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/aerotech/AerotechAutomation1.py) | | aa1AxisDriveDataCollection | Axis data collection

This class provides convenience wrappers around the Aerotech API's axis
specific data collection functionality. This module allows to record
hardware synchronized signals with up to 200 kHz.

The default configuration is using a fixed memory mapping allowing up to
1 million recorded data points on an XC4e (this depends on controller).

Usage:
# Configure the DDC with default internal triggers
ddc = aa1AxisPsoDistance(AA1_IOC_NAME+":ROTY:DDC:", name="ddc")
ddc.wait_for_connection()
ddc.configure(d={'npoints': 5000})
ddc.kickoff().wait()
...
ret = yield from ddc.collect()
| [tomcat_bec.devices.aerotech.AerotechAutomation1](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/aerotech/AerotechAutomation1.py) | | aa1AxisIo | Analog / digital Input-Output

This class provides convenience wrappers around the Aerotech API's axis
specific IO functionality. Note that this is a low-speed API, actual work
should be done in AeroScript. Only one pin can be writen directly but
several can be polled!
| [tomcat_bec.devices.aerotech.AerotechAutomation1](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/aerotech/AerotechAutomation1.py) | | aa1AxisPsoBase | Position Sensitive Output - Base class

This class provides convenience wrappers around the Aerotech IOC's PSO
functionality. As a base class, it's just a collection of PVs without
significant logic (that should be implemented in the child classes).
It uses event-waveform concept to produce signals on the configured
output pin: a specified position based event will trigger the generation
of a waveform on the oputput that can be either used as exposure enable,
as individual trigger or as a series of triggers per each event.
As a first approach, the module follows a simple pipeline structure:
Genrator --> Event --> Waveform --> Output

Specific operation modes should be implemented in child classes.
| [tomcat_bec.devices.aerotech.AerotechAutomation1](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/aerotech/AerotechAutomation1.py) | @@ -17,5 +12,10 @@ | aa1DataAcquisition | Controller Data Acquisition - DONT USE at Tomcat

This class implements the controller data collection feature of the
Automation1 controller. This feature logs various inputs at a
**fixed frequency** from 1 kHz up to 200 kHz.
Usage:
1. Start a new configuration with "startConfig"
2. Add your signals with "addXxxSignal"
3. Start your data collection
4. Read back the recorded data with "readback"
| [tomcat_bec.devices.aerotech.AerotechAutomation1](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/aerotech/AerotechAutomation1.py) | | aa1GlobalVariableBindings | Polled global variables

This class provides an interface to read/write the first few global variables
on the Automation1 controller. These variables are continuously polled
and are thus a convenient way to interface scripts with the outside word.
| [tomcat_bec.devices.aerotech.AerotechAutomation1](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/aerotech/AerotechAutomation1.py) | | aa1GlobalVariables | Global variables

This class provides an interface to directly read/write global variables
on the Automation1 controller. These variables are accesible from script
files and are thus a convenient way to interface with the outside word.

Read operations take as input the memory address and the size
Write operations work with the memory address and value

Usage:
var = aa1Tasks(AA1_IOC_NAME+":VAR:", name="var")
var.wait_for_connection()
ret = var.readInt(42)
var.writeFloat(1000, np.random.random(1024))
ret_arr = var.readFloat(1000, 1024)

| [tomcat_bec.devices.aerotech.AerotechAutomation1](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/aerotech/AerotechAutomation1.py) | -| aa1TaskState | Task state monitoring API

This is the task state monitoring interface for Automation1 tasks. It
does not launch execution, but can wait for the execution to complete.
| [tomcat_bec.devices.aerotech.AerotechAutomation1](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/aerotech/AerotechAutomation1.py) | | aa1Tasks | Task management API

The place to manage tasks and AeroScript user files on the controller.
You can read/write/compile/execute AeroScript files and also retrieve
saved data files from the controller. It will also work around an ophyd
bug that swallows failures.

Execution does not require to store the script in a file, it will compile
it and run it directly on a certain thread. But there's no way to
retrieve the source code.

Write a text into a file on the aerotech controller and execute it with kickoff.
'''
script="var $axis as axis = ROTY\nMoveAbsolute($axis, 42, 90)"
tsk = aa1Tasks(AA1_IOC_NAME+":TASK:", name="tsk")
tsk.wait_for_connection()
tsk.configure({'text': script, 'filename': "foobar.ascript", 'taskIndex': 4})
tsk.kickoff().wait()
'''

Just execute an ascript file already on the aerotech controller.
'''
tsk = aa1Tasks(AA1_IOC_NAME+":TASK:", name="tsk")
tsk.wait_for_connection()
tsk.configure({'filename': "foobar.ascript", 'taskIndex': 4})
tsk.kickoff().wait()
'''

| [tomcat_bec.devices.aerotech.AerotechAutomation1](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/aerotech/AerotechAutomation1.py) | +| aa1TaskState | Task state monitoring API

This is the task state monitoring interface for Automation1 tasks. It
does not launch execution, but can wait for the execution to complete.
| [tomcat_bec.devices.aerotech.AerotechAutomation1](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/aerotech/AerotechAutomation1.py) | +| EpicsMotorX | Special motor class that provides flyer interface and progress bar. | [tomcat_bec.devices.aerotech.AerotechAutomation1](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/aerotech/AerotechAutomation1.py) | +| GrashopperTOMCAT |
Grashopper detector for TOMCAT

Parent class: PSIDetectorBase

class attributes:
custom_prepare_cls (GrashopperTOMCATSetup) : Custom detector setup class for TOMCAT,
inherits from CustomDetectorMixin
cam (SLSDetectorCam) : Detector camera
image (SLSImagePlugin) : Image plugin for detector
| [tomcat_bec.devices.grashopper_tomcat](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/grashopper_tomcat.py) | +| SLSDetectorCam |
SLS Detector Camera - Grashoppter

Base class to map EPICS PVs to ophyd signals.
| [tomcat_bec.devices.grashopper_tomcat](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/grashopper_tomcat.py) | +| SLSImagePlugin | SLS Image Plugin

Image plugin for SLS detector imitating the behaviour of ImagePlugin from
ophyd's areadetector plugins.
| [tomcat_bec.devices.grashopper_tomcat](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/grashopper_tomcat.py) | +| TomcatAerotechRotation | Special motor class that provides flyer interface and progress bar. | [tomcat_bec.devices.tomcat_rotation_motors](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/tomcat_rotation_motors.py) | From b3ea9f63de5a05628823529aa1b0e631853fe725 Mon Sep 17 00:00:00 2001 From: Mohacsi Istvan Date: Fri, 21 Jun 2024 17:06:25 +0200 Subject: [PATCH 02/47] Minimal working implementation, needs a lot of work --- tomcat_bec/devices/stddaqclient.py | 115 +++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 tomcat_bec/devices/stddaqclient.py diff --git a/tomcat_bec/devices/stddaqclient.py b/tomcat_bec/devices/stddaqclient.py new file mode 100644 index 0000000..e07cebf --- /dev/null +++ b/tomcat_bec/devices/stddaqclient.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +""" +Created on Mon Jun 3 14:16:29 2024 + +@author: mohacsi_i +""" +from time import sleep +from ophyd import Device, Signal, Component +from websockets.sync.client import connect +from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError + +import json + +class StdDaqClientDevice(Device): + """ Lightweight wrapper around the undocumented StdDaq websocket interface. + This was meant to replace the documented python client. + """ + # Status attributes + n_image = Component(Signal) + file_path = Component(Signal) + + def __init__(self, *args, parent: Device = None, **kwargs) -> None: + super().__init__(*args, parent=parent, **kwargs) + self.ws_server_url = ( + kwargs["daq_url"] if "daq_url" in kwargs else "ws://xbl-daq-29:8080") + self._client = connect(self.ws_server_url) + + self.n_image.set(100) + self.file_path.set("/gpfs/test/test-beamline") + + def configure(self, d: dict) -> tuple: + """ + Example: + std.configure(d={'n_images': 234, 'file_path': "/data/test/raw"}) + """ + if "num_images" in d: + self.n_images.set(d['n_images']) + del d['num_images'] + if "file_path" in d: + self.output_file.set(d['file_path']) + del d['file_path'] + return (old_config, new_config) + + def stage(self): + file_path = self.file_path.get() + n_image = self.n_image.get() + + message = {"command":"start", "path": file_path, "n_image": n_image} + self.message(message) + return super().stage() + + def unstage(self): + """ Stop a running acquisition + + WARN: This will also close the connection!!! + """ + message = {"command":"stop"} + self.message(message) + return super().unstage() + + def stop(self, *, success=False): + """ Stop a running acquisition + + WARN: This will also close the connection!!! + """ + message = {"command":"stop"} + self.message(message) + + def status(self): + return self.message({"command": "status"}) + + def abort(self): + return self.message({"command": "abort"}) + + def message(self, d: dict, timeout=1): + """ + + Note: finishing acquisition meang StdDAQ will close connections + """ + print(d) + reply = None + if isinstance(d, dict): + msg = json.dumps(d) + else: + msg = str(d) + # Send message (reopen connection if needed) + try: + self._client.send(msg) + except ConnectionClosedError: + # StdDAQ may reject connection for a few seconds + try: + self._client = connect(self.ws_server_url) + except ConnectionRefusedError: + sleep(5) + self._client = connect(self.ws_server_url) + self._client.send(msg) + except ConnectionClosedOK: + # StdDAQ may reject connection for a few seconds + try: + self._client = connect(self.ws_server_url) + except ConnectionRefusedError: + sleep(5) + self._client = connect(self.ws_server_url) + self._client.send(msg) + # Wait for reply + try: + reply = self._client.recv(timeout) + print(reply) + except ConnectionClosedError: + pass + except TimeoutError: + pass + return reply + + From 4053616ec16e5987ffbd23010b128c2649a95155 Mon Sep 17 00:00:00 2001 From: Mohacsi Istvan Date: Mon, 24 Jun 2024 09:44:07 +0200 Subject: [PATCH 03/47] The websockets interface is useless --- tomcat_bec/devices/stddaqclient.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tomcat_bec/devices/stddaqclient.py b/tomcat_bec/devices/stddaqclient.py index e07cebf..d1d13fc 100644 --- a/tomcat_bec/devices/stddaqclient.py +++ b/tomcat_bec/devices/stddaqclient.py @@ -28,6 +28,9 @@ class StdDaqClientDevice(Device): self.n_image.set(100) self.file_path.set("/gpfs/test/test-beamline") + def connect(self): + self._client = connect(self.ws_server_url) + def configure(self, d: dict) -> tuple: """ Example: @@ -77,12 +80,12 @@ class StdDaqClientDevice(Device): Note: finishing acquisition meang StdDAQ will close connections """ - print(d) reply = None if isinstance(d, dict): msg = json.dumps(d) else: msg = str(d) + print("Q: ", msg) # Send message (reopen connection if needed) try: self._client.send(msg) @@ -105,9 +108,10 @@ class StdDaqClientDevice(Device): # Wait for reply try: reply = self._client.recv(timeout) - print(reply) - except ConnectionClosedError: - pass + print("A: ", reply) + except ConnectionClosedError as ex: + print(ex) + pass except TimeoutError: pass return reply From edd11849869ff588b45270704810e2351119536f Mon Sep 17 00:00:00 2001 From: Mohacsi Istvan Date: Tue, 25 Jun 2024 14:27:55 +0200 Subject: [PATCH 04/47] Started adapting the gfclient --- .../devices/gigafrost/gigafrostclient.py | 498 ++++++++++++++++++ .../devices/gigafrost/gigafrostconstants.py | 39 ++ tomcat_bec/devices/stddaqclient.py | 12 + 3 files changed, 549 insertions(+) create mode 100644 tomcat_bec/devices/gigafrost/gigafrostclient.py create mode 100644 tomcat_bec/devices/gigafrost/gigafrostconstants.py diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py new file mode 100644 index 0000000..cd7b5ed --- /dev/null +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -0,0 +1,498 @@ +from ophyd import Device, Component, EpicsMotor, EpicsSignal, EpicsSignalRO, Kind, DerivedSignal +from ophyd.status import Status, SubscriptionStatus, StatusBase, DeviceStatus +from ophyd.flyers import FlyerInterface +from time import sleep +import warnings +import numpy as np +import time + +from typing import Union +from collections import OrderedDict + + +class GigaFrostClient(Device): + """ + Ophyd device class to the Gigafrost cameras at Tomcat + + The actual hardware is implemented on an old fork of Helge's camera IOC. + This means that the camera behaves differently than the SF cameras and + it has a lot of Tomcat specific additions. + """ + cfgConnectionParam = Component(EpicsSignalRO, "CONN_PARM", string=True, auto_monitor=True) + cmdStartCamera = Component(EpicsSignal, "START_CAM", put_complete=True) + cmdSetParam = Component(EpicsSignal, "SET_PARAM.PROC", put_complete=True) + + + cfgUdpNumPorts = Component(EpicsSignal, "PORTS", put_complete=True) + cfgUdpNumFrames = Component(EpicsSignal, "FRAMENUM", put_complete=True) + cfgUdpHtOffset = Component(EpicsSignal, "HT_OFFSET", put_complete=True) + cmdWriteService = Component(EpicsSignal, "WRITE_SRV.PROC", put_complete=True) + + # Standard camera configs + cfgExposure = Component(EpicsSignal, "EXPOSURE", put_complete=True) + cfgFramerate = Component(EpicsSignal, "FRAMERATE", put_complete=True) + cfgRoiX = Component(EpicsSignal, "ROIX", put_complete=True) + cfgRoiY = Component(EpicsSignal, "ROIY", put_complete=True) + cfgScanId = Component(EpicsSignal, "SCAN_ID", put_complete=True) + cfgCntNum = Component(EpicsSignal, "CNT_NUM", put_complete=True) + cfgCorrMode = Component(EpicsSignal, "CORR_MODE", put_complete=True) + + # Software signals + cmdSoftEnable = Component(EpicsSignal, "SOFT_ENABLE", put_complete=True) + cmdSoftTrigger = Component(EpicsSignal, "SOFT_TRIG.PROC", put_complete=True) + cmdSoftExposure = Component(EpicsSignal, "SOFT_EXP", put_complete=True) + + # Trigger configuration PVs + cfgCntStartBit = Component(EpicsSignal, "CNT_STARTBIT", read_pv="CNT_STARTBIT_RBV", put_complete=True) + cfgCntEndBit = Component(EpicsSignal, "CNT_ENDBIT", read_pv="CNT_ENDBIT_RBV", put_complete=True) + # Enable modes + cfgTrigEnableExt = Component(EpicsSignal, "MODE_ENBL_EXT", read_pv="MODE_ENBL_EXT_RBV", put_complete=True) + cfgTrigEnableSoft = Component(EpicsSignal, "MODE_ENBL_SOFT", read_pv="MODE_ENBL_SOFT_RBV", put_complete=True) + cfgTrigEnableAuto = Component(EpicsSignal, "MODE_ENBL_AUTO", read_pv="MODE_ENBL_AUTO_RBV", put_complete=True) + cfgTrigVirtEnable = Component(EpicsSignal, "MODE_ENBL_EXP", read_pv="MODE_ENBL_EXP_RBV", put_complete=True) + # Trigger modes + cfgTrigExt = Component(EpicsSignal, "MODE_TRIG_EXT", read_pv="MODE_TRIG_EXT_RBV", put_complete=True) + cfgTrigSoft = Component(EpicsSignal, "MODE_TRIG_SOFT", read_pv="MODE_TRIG_SOFT_RBV", put_complete=True) + cfgTrigTimer = Component(EpicsSignal, "MODE_TRIG_TIMER", read_pv="MODE_TRIG_TIMER_RBV", put_complete=True) + cfgTrigAuto = Component(EpicsSignal, "MODE_TRIG_AUTO", read_pv="MODE_TRIG_AUTO_RBV", put_complete=True) + # Exposure modes + cfgTrigExpExt = Component(EpicsSignal, "MODE_EXP_EXT", read_pv="MODE_EXP_EXT_RBV", put_complete=True) + cfgTrigExpSoft = Component(EpicsSignal, "MODE_EXP_SOFT", read_pv="MODE_EXP_SOFT_RBV", put_complete=True) + cfgTrigExpTimer = Component(EpicsSignal, "MODE_EXP_TIMER", read_pv="MODE_EXP_TIMER_RBV", put_complete=True) + + # Line swap selection + cfgLineSwapSW = Component(EpicsSignal, "LS_SW", put_complete=True) + cfgLineSwapNW = Component(EpicsSignal, "LS_NW", put_complete=True) + cfgLineSwapSE = Component(EpicsSignal, "LS_SE", put_complete=True) + cfgLineSwapNE = Component(EpicsSignal, "LS_NE", put_complete=True) + + + def __init__(self, prefix="", *, name, kind=None, read_attrs=None, configuration_attrs=None, parent=None, **kwargs): + super().__init__(prefix=prefix, name=name, kind=kind, read_attrs=read_attrs, configuration_attrs=configuration_attrs, parent=parent, **kwargs) + self.oldInitializer() + + + def oldInitializer(self, use_soft_enable=False, + timeout=10.0, backend_url=const.BE1_DAFL_CLIENT): + """ + Class to control the Gigafrost camera and readout system. + + Parameters + ---------- + use_soft_enable : bool + Flag to use the camera's soft enable (default: False) + timeout : int + Maximum time to wait (in seconds) for value before returning None + backend_url : str + Backend url address necessary to set up the camera's udp header. + (default: http://xbl-daq-23:8080) + + """ + + self.name = name + self._auto_soft_enable = auto_soft_enable + self._timeout = timeout + self._backend_url = backend_url + + self.state = const.GfStatus.NEW + self._original_soft_enable = self.cmdSoftEnable.get() + self._settings = None + self._north_mac, self._south_mac = self._define_backend_mac() + self._north_ip, self._south_ip = self._define_backend_ip() + + self._valid_enable_modes = ('soft', 'external', 'soft+ext', 'always') + self._valid_exposure_modes = ('external', 'timer', 'soft') + self._valid_trigger_modes = ('auto', 'external', 'timer', 'soft') + self._valid_fix_nframe_modes = ('off', 'start', 'end', 'start+end') + + # Continue initialization + self.initialize() + + def initialize(self): + """ + Initialize the camera, set channel values + """ + ## Stop acquisition + self.cmdStartCamera.set(0).wait() + + + + ### set entry to UDP table + # number of UDP ports to use + self.cfgUdpNumPorts.set(2).wait() + # number of images to send to each UDP port before switching to next + self.cfgUdpNumFrames.set(5).wait() + # offset in UDP table - where to find the first entry + self.cfgUdpHtOffset.set(0).wait() + + # activate changes + self.cmdWriteService.set(1).wait() + + if self._use_soft_enable: + # trigger modes + self.cfgCntStartBit.set(1).wait() + self.cfgCntEndBit.set(0).wait() + + # set modes + self.put_enable_mode('soft') + self.put_trigger_mode('auto') + self.put_exposure_mode('timer') + + # line swap - on for west, off for east + self.cfgLineSwapSW.set(1).wait() + self.cfgLineSwapNW.set(1).wait() + self.cfgLineSwapSE.set(0).wait() + self.cfgLineSwapNE.set(0).wait() + + # Commit parameters + self.cmdSetParam.set(1).wait() + + self._set_udp_header_table() + self.state = const.GfStatus.INIT + + # sets the basic settings + self._settings = {'backend_url' : self._backend_url, + 'use_soft_enable' : self._use_soft_enable, + 'ioc_name' : self.prefix} + + + def configure(self, nimages=10, exposure=0.2, period=1.0, + roix=2016, roiy=2016, scanid=0, correction_mode=5): + """ + Configure the next scan with the GigaFRoST camera + + Parameters + ---------- + nimages : int, optional + Number of images to be taken during each scan. Set to -1 for an + unlimited number of images (limited by the ringbuffer size and + backend speed). (default = 10) + exposure : float, optional + Exposure time [ms]. (default = 0.2) + period : float, optional + Exposure period [ms]. (default = 1.0) + roix : int, optional + ROI size in the x-direction [pixels] (default = 2016) + roiy : int, optional + ROI size in the y-direction [pixels] (default = 2016) + scanid : int, optional + Scan identification number to be associated with the scan data + (default = 0) + correction_mode : int, optional + The correction to be applied to the imaging data. The following + modes are available (default = 5): + + * 0: Bypass. No corrections are applied to the data. + * 1: Send correction factor A instead of pixel values + * 2: Send correction factor B instead of pixel values + * 3: Send correction factor C instead of pixel values + * 4: Invert pixel values, but do not apply any linearity correction + * 5: Apply the full linearity correction + """ + + # switch to idle + ## Stop acquisition + self.cmdStartCamera.set(0).wait() + if self._use_soft_enable: + self.set_soft_enable(0) + + # change settings + self.cfgExposure.set(exposure).wait() + self.cfgFramerate.set(period).wait() + self.cfgRoiX.set(roix).wait() + self.cfgRoiY.set(roiy).wait() + self.cfgScanId.set(scanid).wait() + self.cfgCntNum.set(nimages).wait() + self.cfgCorrMode.set(correction_mode).wait() + + # Commit parameter + self.cmdSetParam.set(1).wait() + self.state = const.GfStatus.CONFIGURED + + self._settings = {'nimages' : nimages, + 'exposure' : exposure, + 'frame_rate': period, + 'roix' : roix, + 'roiy' : roiy, + 'scanid' : scanid, + 'correction_mode' : correction_mode, + 'backend_url' : self._backend_url, + 'use_soft_enable' : self._use_soft_enable, + 'ioc_name' : self.name + } + + def stage(self): + """ + Standard ophyd method to start an acquisition + """ + # change to running + self.cmdStartCamera.set(1).wait() + # soft trigger on + if self._use_soft_enable: + self.cmdSoftEnable.set(1).wait() + self.state = const.GfStatus.OPEN + return super().stage() + + def unstage(self): + """ + Standard ophyd method to stop an acquisition + """ + # switch to idle + self.cmdStartCamera.set(0).wait() + if self._use_soft_enable: + self.cmdSoftEnable.set(0).wait() + self.state = const.GfStatus.CLOSED + return super().unstage() + + def stop(self): + """ + Standard ophyd method to stop an acquisition + """ + self.unstage() + + def reset(self): + try: + self.unstage() + except: + pass + self.state = const.GfStatus.INIT + + def get_exposure_mode(self): + """ + Returns the current exposure mode of the GigaFRost camera. + + Returns + ------- + exp_mode : {'external', 'timer', 'soft'} + The camera's active exposure mode. + If more than one mode is active at the same time, it returns None. + + """ + + mode_soft = self.cfgTrigExpSoft.get() + mode_timer = self.cfgTrigExpTimer.get() + mode_external = self.cfgTrigExpExt.get() + if mode_soft and not mode_timer and not mode_external: + return 'soft' + elif not mode_soft and mode_timer and not mode_external: + return 'timer' + elif not mode_soft and not mode_timer and mode_external: + return 'external' + else: + return None + + + def get_fix_nframes_mode(self): + """ + Return the current fixed number of frames mode of the GigaFRoST camera. + + Returns + ------- + fix_nframes_mode : {'off', 'start', 'end', 'start+end'} + The camera's active fixed number of frames mode. + + """ + + start_bit = self.cfgCntStartBit.get() + end_bit = self.cfgCntStartBit.get() + + if not start_bit and not end_bit: + return 'off' + elif start_bit and not end_bit: + return 'start' + elif not start_bit and end_bit: + return 'end' + elif start_bit and end_bit: + return 'start+end' + else: + return None + + def get_trigger_mode(self): + """ + Method to detect the current trigger mode set in the GigaFRost camera. + + Returns + ------- + mode : {'auto', 'external', 'timer', 'soft'} + The camera's active trigger mode. If more than one mode is active + at the moment, None is returned. + + """ + mode_auto = self.cfgTrigAuto.get() + mode_external = self.cfgTrigExt.get() + mode_timer = self.cfgTrigTimer.get() + mode_soft = self.cfgTrigSoft.get() + if mode_auto: + return 'auto' + elif mode_soft: + return 'soft' + elif mode_timer: + return 'timer' + elif mode_external: + return 'external' + else: + return None + + def put_enable_mode(self, mode): + """ + Apply the enable mode for the GigaFRoST camera. + + Parameters + ---------- + mode : {'soft', 'external', 'soft+ext', 'always'} + The enable mode to be applied. + + """ + + if mode not in self._valid_enable_modes: + raise ValueError("Invalid enable mode! Valid modes are:\n" + "{}".format(self._valid_enable_modes)) + + if mode == 'soft': + self.cfgTrigEnableExt.set(0).wait() + self.cfgTrigEnableSoft.set(1).wait() + self.cfgTrigEnableAuto.set(0).wait() + elif mode == 'external': + self.cfgTrigEnableExt.set(1).wait() + self.cfgTrigEnableSoft.set(0).wait() + self.cfgTrigEnableAuto.set(0).wait() + elif mode == 'soft+ext': + self.cfgTrigEnableExt.set(1).wait() + self.cfgTrigEnableSoft.set(1).wait() + self.cfgTrigEnableAuto.set(0).wait() + elif mode == 'always': + self.cfgTrigEnableExt.set(0).wait() + self.cfgTrigEnableSoft.set(0).wait() + self.cfgTrigEnableAuto.set(1).wait() + # Commit parameters + self.cmdSetParam.set(1).wait() + + def put_exposure_mode(self, exp_mode): + """ + Apply the exposure mode for the GigaFRoST camera. + + Parameters + ---------- + exp_mode : {'external', 'timer', 'soft'} + The exposure mode to be set. + + """ + + if exp_mode not in self._valid_exposure_modes: + raise ValueError("Invalid exposure mode! Valid modes are:\n" + "{}".format(self._valid_exposure_modes)) + + if exp_mode == 'external': + self.cfgTrigExpExt.set(1).wait() + self.cfgTrigExpSoft.set(0).wait() + self.cfgTrigExpTimer.set(0).wait() + elif exp_mode == 'timer': + self.cfgTrigExpExt.set(0).wait() + self.cfgTrigExpSoft.set(0).wait() + self.cfgTrigExpTimer.set(1).wait() + elif exp_mode == 'soft': + self.cfgTrigExpExt.set(0).wait() + self.cfgTrigExpSoft.set(1).wait() + self.cfgTrigExpTimer.set(0).wait() + # Commit parameters + self.cmdSetParam.set(1).wait() + + def put_fix_nframes_mode(self, mode): + """ + Apply the fixed number of frames settings to the GigaFRoST camera. + + Parameters + ---------- + mode : {'off', 'start', 'end', 'start+end'} + The fixed number of frames mode to be applied. + + """ + + if mode not in self._valid_fix_nframe_modes: + raise ValueError("Invalid fixed number of frames mode! " + "Valid modes are:\n{}".format(self._valid_fix_nframe_modes)) + + self._fix_nframes_mode = mode + if self._fix_nframes_mode == 'off': + self.cfgCntStartBit.set(0).wait() + self.cfgCntEndBit.set(0).wait() + elif self._fix_nframes_mode == 'start': + self.cfgCntStartBit.set(1).wait() + self.cfgCntEndBit.set(0).wait() + elif self._fix_nframes_mode == 'end': + self.cfgCntStartBit.set(0).wait() + self.cfgCntEndBit.set(1).wait() + elif self._fix_nframes_mode == 'start+end': + self.cfgCntStartBit.set(1).wait() + self.cfgCntEndBit.set(1).wait() + # Commit parameters + self.cmdSetParam.set(1).wait() + + def get_state(self): + return self.state + + def get_south_mac(self): + return self._south_mac + + def get_north_mac(self): + return self._north_mac + + def get_north_ip(self): + return self._north_ip + + def get_south_ip(self): + return self._south_ip + + def get_backend_url(self): + return self._backend_url + + def _build_udp_header_table(self): + """ + Build the header table for the communication + """ + udp_header_table = [] + + for i in range(0,64,1): + for j in range(0,8,1): + dest_port = 2000 + 8 * i + j + source_port = 3000+j + if j < 4: + extend_header_table( + udp_header_table, self._south_mac, self._south_ip, + dest_port, source_port) + else: + extend_header_table( + udp_header_table, self._north_mac, self._north_ip, + dest_port, source_port) + + return udp_header_table + + def _define_backend_ip(self): + if self._backend_url == const.BE3_DAFL_CLIENT: # xbl-daq-33 + return const.BE3_NORTH_IP, const.BE3_SOUTH_IP + else: + raise RuntimeError(const.ERROR_BACKEND_NAME % ( + const.GF1, const.GF2, const.GF3)) + + def _define_backend_mac(self): + if self._backend_url == const.BE3_DAFL_CLIENT: # xbl-daq-33 + return const.BE3_NORTH_MAC, const.BE3_SOUTH_MAC + else: + raise RuntimeError(const.ERROR_BACKEND_NAME % ( + const.GF1, const.GF2, const.GF3)) + + def _set_udp_header_table(self): + """ + Set the communication parameters for the camera module + """ + self.cfgConnectionParam.set(self._build_udp_header_table()).wait() + + + +# Automatically start simulation if directly invoked +if __name__ == "__main__": + gf = GigaFrostClient("X02DA-CAM-GF1") + gf.wait_for_connection() + + + diff --git a/tomcat_bec/devices/gigafrost/gigafrostconstants.py b/tomcat_bec/devices/gigafrost/gigafrostconstants.py new file mode 100644 index 0000000..64be4ee --- /dev/null +++ b/tomcat_bec/devices/gigafrost/gigafrostconstants.py @@ -0,0 +1,39 @@ +##################################################################### + +from enum import Enum + + +# STATUS +class GfStatus(Enum): + NEW = 1 + INITIALIZED = 2 + ACQUIRING = 3 + CONFIGURED = 4 + STOPPED = 5 + +# BACKEND ADDRESSES +BE1_DAFL_CLIENT = 'http://xbl-daq-33:8080' +BE1_NORTH_MAC = [0x94, 0x40, 0xc9, 0xb4, 0xb8, 0x00] +BE1_SOUTH_MAC = [0x94, 0x40, 0xc9, 0xb4, 0xa8, 0xd8] +BE1_NORTH_IP = [10, 4, 0, 102] +BE1_SOUTH_IP = [10, 0, 0, 102] +BE2_DAFL_CLIENT = 'http://xbl-daq-23:8080' +BE2_NORTH_MAC = [0x24, 0xBE, 0x05, 0xAC, 0x03, 0x62] +BE2_SOUTH_MAC = [0x24, 0xBE, 0x05, 0xAC, 0x03, 0x72] +BE2_NORTH_IP = [10, 4, 0, 100] +BE2_SOUTH_IP = [10, 0, 0, 100] +BE3_DAFL_CLIENT = 'http://xbl-daq-26:8080' +BE3_NORTH_MAC = [0x50, 0x65, 0xf3, 0x81, 0x66, 0x51] +BE3_SOUTH_MAC = [0x50, 0x65, 0xf3, 0x81, 0xd5, 0x31] # ens4 +BE3_NORTH_IP = [10, 4, 0, 101] +BE3_SOUTH_IP = [10, 0, 0, 101] + +# GF Names +GF1 = 'gf1' +GF2 = 'gf2' +GF3 = 'gf3' + +# CAMERA +ERROR_BACKEND_NAME = 'Backend not recognized.' + + diff --git a/tomcat_bec/devices/stddaqclient.py b/tomcat_bec/devices/stddaqclient.py index d1d13fc..b606b05 100644 --- a/tomcat_bec/devices/stddaqclient.py +++ b/tomcat_bec/devices/stddaqclient.py @@ -14,6 +14,18 @@ import json class StdDaqClientDevice(Device): """ Lightweight wrapper around the undocumented StdDaq websocket interface. This was meant to replace the documented python client. + + + + + + + A bit more about the Standard DAQ configuration: + + The standard DAQ configuration is a single JSON file locally autodeployed + to the DAQ servers (as root!!!). Previously there was a service to offer + a REST API to write this file, but since there's no frontend group, this + is no longer available. """ # Status attributes n_image = Component(Signal) From ba819d6df83978fa442fdea95ba84249875560da Mon Sep 17 00:00:00 2001 From: Mohacsi Istvan Date: Tue, 25 Jun 2024 14:41:44 +0200 Subject: [PATCH 05/47] Readme with panle command --- tomcat_bec/devices/gigafrost/Readme.md | 9 +++++++++ tomcat_bec/devices/gigafrost/gigafrostclient.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 tomcat_bec/devices/gigafrost/Readme.md diff --git a/tomcat_bec/devices/gigafrost/Readme.md b/tomcat_bec/devices/gigafrost/Readme.md new file mode 100644 index 0000000..0a5e996 --- /dev/null +++ b/tomcat_bec/devices/gigafrost/Readme.md @@ -0,0 +1,9 @@ + + + +# Opening GigaFrost panel + + +''' + caqtdm -macro "CAM=X02DA-CAM-GF2" X_X02DA_GIGAFROST_camControl_user.ui & +''' diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index cd7b5ed..4b78258 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -491,7 +491,7 @@ class GigaFrostClient(Device): # Automatically start simulation if directly invoked if __name__ == "__main__": - gf = GigaFrostClient("X02DA-CAM-GF1") + gf = GigaFrostClient("X02DA-CAM-GF2") gf.wait_for_connection() From dc6a8eea0abe337d7b2b65f10feb7edae48ee691 Mon Sep 17 00:00:00 2001 From: Mohacsi Istvan Date: Tue, 25 Jun 2024 16:00:36 +0200 Subject: [PATCH 06/47] GFclient direct adaptation instantiates --- .../{gigafrostconstants.py => gfconstants.py} | 20 +- tomcat_bec/devices/gigafrost/gfutils.py | 244 ++++++++++++++++++ .../devices/gigafrost/gigafrostclient.py | 91 ++++--- 3 files changed, 319 insertions(+), 36 deletions(-) rename tomcat_bec/devices/gigafrost/{gigafrostconstants.py => gfconstants.py} (76%) create mode 100644 tomcat_bec/devices/gigafrost/gfutils.py diff --git a/tomcat_bec/devices/gigafrost/gigafrostconstants.py b/tomcat_bec/devices/gigafrost/gfconstants.py similarity index 76% rename from tomcat_bec/devices/gigafrost/gigafrostconstants.py rename to tomcat_bec/devices/gigafrost/gfconstants.py index 64be4ee..fbf9173 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostconstants.py +++ b/tomcat_bec/devices/gigafrost/gfconstants.py @@ -10,6 +10,7 @@ class GfStatus(Enum): ACQUIRING = 3 CONFIGURED = 4 STOPPED = 5 + INIT = 6 # BACKEND ADDRESSES BE1_DAFL_CLIENT = 'http://xbl-daq-33:8080' @@ -28,12 +29,23 @@ BE3_SOUTH_MAC = [0x50, 0x65, 0xf3, 0x81, 0xd5, 0x31] # ens4 BE3_NORTH_IP = [10, 4, 0, 101] BE3_SOUTH_IP = [10, 0, 0, 101] + + +BE999_DAFL_CLIENT = "http://xbl-daq-28:8080" +BE999_SOUTH_MAC = [0x9C, 0xDC, 0x71, 0x47, 0xE5, 0xD1] # 9c:dc:71:47:e5:d1 +BE999_NORTH_MAC = [0x9C, 0xDC, 0x71, 0x47, 0xE5, 0xDD] # 9c:dc:71:47:e5:dd +BE999_NORTH_IP = [10, 4, 0, 101] +BE999_SOUTH_IP = [10, 0, 0, 101] + + + + + + + + # GF Names GF1 = 'gf1' GF2 = 'gf2' GF3 = 'gf3' -# CAMERA -ERROR_BACKEND_NAME = 'Backend not recognized.' - - diff --git a/tomcat_bec/devices/gigafrost/gfutils.py b/tomcat_bec/devices/gigafrost/gfutils.py new file mode 100644 index 0000000..ab45263 --- /dev/null +++ b/tomcat_bec/devices/gigafrost/gfutils.py @@ -0,0 +1,244 @@ +import inspect +import os +import sys +import jsonschema + +_min_exposure_ms = 0.002 +_max_exposure_ms = 40 + +_min_roix = 48 +_max_roix = 2016 +_step_roix = 48 + +_min_roiy = 4 +_max_roiy = 2016 +_step_roiy = 4 + +valid_roix = range(_min_roix, _max_roix + 1, _step_roix) +valid_roiy = range(_min_roiy, _max_roiy + 1, _step_roiy) + + +class NoTraceBackWithLineNumber(Exception): + def __init__(self, msg): + if type(msg).__name__ in ["ConnectionError", "ReadTimeout"]: + print("\n ConnectionError/ReadTimeout: it seems that the server " + "is not running/responding.\n") + try: + ln = sys.exc_info()[-1].tb_lineno + except AttributeError: + ln = inspect.currentframe().f_back.f_lineno + self.args = "{0.__name__} (line {1}): {2}".format(type(self), ln, msg), + sys.tracebacklimit = None + return None + + +class GfError(NoTraceBackWithLineNumber): + pass + + +class GfWarning(NoTraceBackWithLineNumber): + pass + + +class GfNotAValidConfig(NoTraceBackWithLineNumber): + pass + + +class GfCamNotFound(NoTraceBackWithLineNumber): + pass + + +def is_valid_url(url): + return(url.startswith('http://')) # FIXME: do more checks? + + +def is_valid_exposure_ms(e): + """check if an exposure time e is valid for gigafrost + + e: exposure time in milliseconds + """ + return e >= _min_exposure_ms and e <= _max_exposure_ms + + +def port2byte(port): + return [(port >> 8) & 0xff, port & 0xff] + +def extend_header_table(table, mac, destination_ip, destination_port, + source_port): + """ + Extend the header table by a further entry. + + Parameters + ---------- + table : + The table to be extended + mac : + The mac address for the new entry + destination_ip : + The destination IP address for the new entry + destination_port : + The destination port for the new entry + source_port : + The source port for the new entry + + """ + table.extend(mac) + table.extend(destination_ip) + table.extend(port2byte(destination_port)) + table.extend(port2byte(source_port)) + return table + + +def is_valid_roi(roiy, roix): + return roiy in valid_roiy and roix in valid_roix + + +def _print_max_framerate(exposure, roix, roiy): + print("roiy=%4i roix=%4i exposure=%6.3fms: %8.1fHz" % + (roiy, roix, exposure, + max_framerate_Hz(exposure, roix=roix, roiy=roiy))) + + +def print_max_framerate(exposure_ms=_min_exposure_ms, shape='square'): + + valid_shapes = ['square', 'landscape', 'portrait'] + + if shape not in valid_shapes: + raise ValueError("shape must be one of %s" % valid_shapes) + if shape == 'square': + for r in valid_roix: + _print_max_framerate(exposure_ms, r, r) + + if shape == 'portrait': + for x in valid_roix: + _print_max_framerate(exposure_ms, roix=x, roiy=_max_roiy) + + if shape == 'landscape': + for y in valid_roix: # valid_roix is a subset of valid_roiy. Use the smaller set to get a more manageable amount of output lines + _print_max_framerate(exposure_ms, roix=_max_roix, roiy=y) + + +def max_framerate_Hz(exposure_ms=_min_exposure_ms, + roix=_max_roix, roiy=_max_roiy, clk_mhz=62.5): + """ + returns maximum achievable frame rate in auto mode in Hz + + Gerd Theidel wrote: + Hallo zusammen, + + hier wie besprochen die Info zu den Zeiten. + Im Anhang findet ihr ein Python Beispiel zur + Berechnung der maximalen Framerate im auto trigger mode. + Bei den anderen modes sollte man etwas darunter bleiben, + wie auch im PCO Manual beschrieben. + + Zusammengefasst mit den aktuellen Konstanten: + + exposure_ms : 0.002 ... 40 (ms) + clk_mhz : 62.5 | 55.0 | 52.5 | 50.0 (MHz) + + t_readout = ( ((roi_x / 24) + 14) * (roi_y / 4) + 405) / (1e6 * clk_mhz) + + t_exp_sys = (exposure_ms / 1000.0) + (261 / (1e6 * clk_mhz)) + + framerate = 1.0 / max(t_readout, t_exp_sys) + + Gruss, + Gerd + + """ + if exposure_ms < 0.002 or exposure_ms > 40: + raise ValueError('exposure_ms not in interval [0.002, 40.]') + + valid_clock_values = [62.5, 55.0, 52.5, 50.0] + if clk_mhz not in valid_clock_values: + raise ValueError('clock rate not in %s' % valid_clock_values) + + # Constants + PLB_IMG_SENS_COUNT_X_OFFS = 2 + PLB_IMG_SENS_COUNT_Y_OFFS = 1 + + # Constant Clock Cycles + CC_T2 = 88 # 99 + CC_T3 = 125 + CC_T4 = 1 + CC_T10 = 2 + CC_T11 = 4 + CC_T5_MINUS_T11 = 20 + CC_T15_MINUS_T10 = 3 + CC_TR3 = 1 + CC_T13_MINUS_TR3 = 2 + CC_150NS = 7 # additional delay through states + CC_DELAY_BEFORE_RESET = 4 # at least 50 ns + CC_ADDITIONAL_TSYS = 10 + CC_PIX_RESET_LENGTH = 40 # at least 40 CLK Cycles at 62.5 MHz + CC_COUNT_X_MAX = 84 + + CC_ROW_OVERHEAD_TIME = (11 + CC_TR3 + CC_T13_MINUS_TR3) + CC_FRAME_OVERHEAD_TIME = ( + 8 + CC_T15_MINUS_T10 + CC_T10 + CC_T2 + CC_T3 + CC_T4 + CC_T5_MINUS_T11 + CC_T11) + CC_INTEGRATION_START_DELAY = CC_COUNT_X_MAX + CC_ROW_OVERHEAD_TIME + \ + CC_DELAY_BEFORE_RESET + CC_PIX_RESET_LENGTH + CC_150NS + 5 + CC_TIME_TSYS = CC_FRAME_OVERHEAD_TIME + CC_ADDITIONAL_TSYS + + # 12 pixel blocks per quarter + roix = max(((roix + 47) / 48) * 48, PLB_IMG_SENS_COUNT_X_OFFS * 24) + # 2 line blocks per quarter + roiy = max(((roiy + 3) / 4) * 4, PLB_IMG_SENS_COUNT_Y_OFFS * 4) + + # print("CC_INTEGRATION_START_DELAY + CC_FRAME_OVERHEAD_TIME",CC_INTEGRATION_START_DELAY + CC_FRAME_OVERHEAD_TIME) + # print("CC_ROW_OVERHEAD_TIME",CC_ROW_OVERHEAD_TIME) + # print("CC_TIME_TSYS",CC_TIME_TSYS) + + t_readout = (CC_INTEGRATION_START_DELAY + CC_FRAME_OVERHEAD_TIME + + ((roix / 24) + CC_ROW_OVERHEAD_TIME) * (roiy / 4)) / (1e6 * clk_mhz) + t_exp_sys = (exposure_ms / 1000.0) + (CC_TIME_TSYS / (1e6 * clk_mhz)) + + # with constants as of time of writing: + # t_readout = ( ((roix / 24) + 14) * (roiy / 4) + 405) / (1e6 * clk_mhz) + # t_exp_sys = (exposure_ms / 1000.0) + (261 / (1e6 * clk_mhz)) + + framerate = 1.0 / max(t_readout, t_exp_sys) + + return (framerate) + +layoutSchema = { + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "patternProperties": + { + "^[a-zA-Z0-9_.-]*$": + { + "type": "object", + "required":["writer","DaflClient", "zmq_stream", "live_preview", "ioc_name", "description"], + "properties":{ + "writer": { + "type": "string" + }, + "DaflClient": { + "type": "string" + }, + "zmq_stream": { + "type": "string" + }, + "live_preview": { + "type": "string" + }, + "ioc_name": { + "type": "string" + }, + "description": { + "type": "string" + } + } + } + }, + "additionalProperties": False +} + +def validateJson(jsonData): + try: + jsonschema.validate(instance=jsonData, schema=layoutSchema) + except jsonschema.exceptions.ValidationError: + return False + return True diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index 4b78258..b807873 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -8,7 +8,8 @@ import time from typing import Union from collections import OrderedDict - +import gfconstants as const +from gfutils import extend_header_table, port2byte class GigaFrostClient(Device): """ @@ -18,7 +19,10 @@ class GigaFrostClient(Device): This means that the camera behaves differently than the SF cameras and it has a lot of Tomcat specific additions. """ - cfgConnectionParam = Component(EpicsSignalRO, "CONN_PARM", string=True, auto_monitor=True) + infoBusyFlag = Component(EpicsSignalRO, "BUSY_STAT", auto_monitor=True) + infoSyncFlag = Component(EpicsSignalRO, "SYNC_FLAG", auto_monitor=True) + cmdSyncHw = Component(EpicsSignal, "SYNC_SWHW.PROC", put_complete=True) + cfgConnectionParam = Component(EpicsSignal, "CONN_PARM", string=True, put_complete=True) cmdStartCamera = Component(EpicsSignal, "START_CAM", put_complete=True) cmdSetParam = Component(EpicsSignal, "SET_PARAM.PROC", put_complete=True) @@ -43,22 +47,22 @@ class GigaFrostClient(Device): cmdSoftExposure = Component(EpicsSignal, "SOFT_EXP", put_complete=True) # Trigger configuration PVs - cfgCntStartBit = Component(EpicsSignal, "CNT_STARTBIT", read_pv="CNT_STARTBIT_RBV", put_complete=True) - cfgCntEndBit = Component(EpicsSignal, "CNT_ENDBIT", read_pv="CNT_ENDBIT_RBV", put_complete=True) + cfgCntStartBit = Component(EpicsSignal, "CNT_STARTBIT_RBV", write_pv="CNT_STARTBIT", put_complete=True) + cfgCntEndBit = Component(EpicsSignal, "CNT_ENDBIT_RBV", write_pv="CNT_ENDBIT", put_complete=True) # Enable modes - cfgTrigEnableExt = Component(EpicsSignal, "MODE_ENBL_EXT", read_pv="MODE_ENBL_EXT_RBV", put_complete=True) - cfgTrigEnableSoft = Component(EpicsSignal, "MODE_ENBL_SOFT", read_pv="MODE_ENBL_SOFT_RBV", put_complete=True) - cfgTrigEnableAuto = Component(EpicsSignal, "MODE_ENBL_AUTO", read_pv="MODE_ENBL_AUTO_RBV", put_complete=True) - cfgTrigVirtEnable = Component(EpicsSignal, "MODE_ENBL_EXP", read_pv="MODE_ENBL_EXP_RBV", put_complete=True) + cfgTrigEnableExt = Component(EpicsSignal, "MODE_ENBL_EXT_RBV", write_pv="MODE_ENBL_EXT", put_complete=True) + cfgTrigEnableSoft = Component(EpicsSignal, "MODE_ENBL_SOFT_RBV", write_pv="MODE_ENBL_SOFT", put_complete=True) + cfgTrigEnableAuto = Component(EpicsSignal, "MODE_ENBL_AUTO_RBV", write_pv="MODE_ENBL_AUTO", put_complete=True) + cfgTrigVirtEnable = Component(EpicsSignal, "MODE_ENBL_EXP_RBV", write_pv="MODE_ENBL_EXP", put_complete=True) # Trigger modes - cfgTrigExt = Component(EpicsSignal, "MODE_TRIG_EXT", read_pv="MODE_TRIG_EXT_RBV", put_complete=True) - cfgTrigSoft = Component(EpicsSignal, "MODE_TRIG_SOFT", read_pv="MODE_TRIG_SOFT_RBV", put_complete=True) - cfgTrigTimer = Component(EpicsSignal, "MODE_TRIG_TIMER", read_pv="MODE_TRIG_TIMER_RBV", put_complete=True) - cfgTrigAuto = Component(EpicsSignal, "MODE_TRIG_AUTO", read_pv="MODE_TRIG_AUTO_RBV", put_complete=True) + cfgTrigExt = Component(EpicsSignal, "MODE_TRIG_EXT_RBV", write_pv="MODE_TRIG_EXT", put_complete=True) + cfgTrigSoft = Component(EpicsSignal, "MODE_TRIG_SOFT_RBV", write_pv="MODE_TRIG_SOFT", put_complete=True) + cfgTrigTimer = Component(EpicsSignal, "MODE_TRIG_TIMER_RBV", write_pv="MODE_TRIG_TIMER", put_complete=True) + cfgTrigAuto = Component(EpicsSignal, "MODE_TRIG_AUTO_RBV", write_pv="MODE_TRIG_AUTO", put_complete=True) # Exposure modes - cfgTrigExpExt = Component(EpicsSignal, "MODE_EXP_EXT", read_pv="MODE_EXP_EXT_RBV", put_complete=True) - cfgTrigExpSoft = Component(EpicsSignal, "MODE_EXP_SOFT", read_pv="MODE_EXP_SOFT_RBV", put_complete=True) - cfgTrigExpTimer = Component(EpicsSignal, "MODE_EXP_TIMER", read_pv="MODE_EXP_TIMER_RBV", put_complete=True) + cfgTrigExpExt = Component(EpicsSignal, "MODE_EXP_EXT_RBV", write_pv="MODE_EXP_EXT", put_complete=True) + cfgTrigExpSoft = Component(EpicsSignal, "MODE_EXP_SOFT_RBV", write_pv="MODE_EXP_SOFT", put_complete=True) + cfgTrigExpTimer = Component(EpicsSignal, "MODE_EXP_TIMER_RBV", write_pv="MODE_EXP_TIMER", put_complete=True) # Line swap selection cfgLineSwapSW = Component(EpicsSignal, "LS_SW", put_complete=True) @@ -66,13 +70,25 @@ class GigaFrostClient(Device): cfgLineSwapSE = Component(EpicsSignal, "LS_SE", put_complete=True) cfgLineSwapNE = Component(EpicsSignal, "LS_NE", put_complete=True) + # HW settings as read only + cfgSyncFlag = Component(EpicsSignalRO, "PIXRATE", auto_monitor=True) + cfgTrigDelay = Component(EpicsSignalRO, "TRIG_DELAY", auto_monitor=True) + cfgSyncoutDelay = Component(EpicsSignalRO, "SYNCOUT_DLY", auto_monitor=True) + cfgOutputPolarity0 = Component(EpicsSignalRO, "BNC0_RBV", auto_monitor=True) + cfgOutputPolarity1 = Component(EpicsSignalRO, "BNC1_RBV", auto_monitor=True) + cfgOutputPolarity2 = Component(EpicsSignalRO, "BNC2_RBV", auto_monitor=True) + cfgOutputPolarity3 = Component(EpicsSignalRO, "BNC3_RBV", auto_monitor=True) + cfgInputPolarity1 = Component(EpicsSignalRO, "BNC4_RBV", auto_monitor=True) + cfgInputPolarity2 = Component(EpicsSignalRO, "BNC5_RBV", auto_monitor=True) + infoBoardTemp = Component(EpicsSignalRO, "T_BOARD", auto_monitor=True) - def __init__(self, prefix="", *, name, kind=None, read_attrs=None, configuration_attrs=None, parent=None, **kwargs): + def __init__(self, prefix="", *, name, backend_url=const.BE999_DAFL_CLIENT, + kind=None, read_attrs=None, configuration_attrs=None, parent=None, **kwargs): super().__init__(prefix=prefix, name=name, kind=kind, read_attrs=read_attrs, configuration_attrs=configuration_attrs, parent=parent, **kwargs) - self.oldInitializer() + self.oldInitializer(backend_url=backend_url) - def oldInitializer(self, use_soft_enable=False, + def oldInitializer(self, auto_soft_enable=False, timeout=10.0, backend_url=const.BE1_DAFL_CLIENT): """ Class to control the Gigafrost camera and readout system. @@ -88,14 +104,11 @@ class GigaFrostClient(Device): (default: http://xbl-daq-23:8080) """ - - self.name = name self._auto_soft_enable = auto_soft_enable self._timeout = timeout self._backend_url = backend_url self.state = const.GfStatus.NEW - self._original_soft_enable = self.cmdSoftEnable.get() self._settings = None self._north_mac, self._south_mac = self._define_backend_mac() self._north_ip, self._south_ip = self._define_backend_ip() @@ -128,7 +141,7 @@ class GigaFrostClient(Device): # activate changes self.cmdWriteService.set(1).wait() - if self._use_soft_enable: + if self._auto_soft_enable: # trigger modes self.cfgCntStartBit.set(1).wait() self.cfgCntEndBit.set(0).wait() @@ -152,7 +165,7 @@ class GigaFrostClient(Device): # sets the basic settings self._settings = {'backend_url' : self._backend_url, - 'use_soft_enable' : self._use_soft_enable, + 'auto_soft_enable' : self._auto_soft_enable, 'ioc_name' : self.prefix} @@ -193,7 +206,7 @@ class GigaFrostClient(Device): # switch to idle ## Stop acquisition self.cmdStartCamera.set(0).wait() - if self._use_soft_enable: + if self._auto_soft_enable: self.set_soft_enable(0) # change settings @@ -217,7 +230,7 @@ class GigaFrostClient(Device): 'scanid' : scanid, 'correction_mode' : correction_mode, 'backend_url' : self._backend_url, - 'use_soft_enable' : self._use_soft_enable, + 'auto_soft_enable' : self._auto_soft_enable, 'ioc_name' : self.name } @@ -228,7 +241,7 @@ class GigaFrostClient(Device): # change to running self.cmdStartCamera.set(1).wait() # soft trigger on - if self._use_soft_enable: + if self._auto_soft_enable: self.cmdSoftEnable.set(1).wait() self.state = const.GfStatus.OPEN return super().stage() @@ -239,7 +252,7 @@ class GigaFrostClient(Device): """ # switch to idle self.cmdStartCamera.set(0).wait() - if self._use_soft_enable: + if self._auto_soft_enable: self.cmdSoftEnable.set(0).wait() self.state = const.GfStatus.CLOSED return super().unstage() @@ -446,6 +459,18 @@ class GigaFrostClient(Device): def get_backend_url(self): return self._backend_url + def set_backend_ip(self, north, south): + """ + Method to manually set the backend ip + """ + self._north_ip, self._south_ip = north, south + + def set_backend_mac(self, north, south): + """ + Method to manually set the backend mac + """ + self._north_mac, self._south_mac = north, south + def _build_udp_header_table(self): """ Build the header table for the communication @@ -470,16 +495,18 @@ class GigaFrostClient(Device): def _define_backend_ip(self): if self._backend_url == const.BE3_DAFL_CLIENT: # xbl-daq-33 return const.BE3_NORTH_IP, const.BE3_SOUTH_IP + elif self._backend_url == const.BE999_DAFL_CLIENT: + return const.BE999_NORTH_IP, const.BE999_SOUTH_IP else: - raise RuntimeError(const.ERROR_BACKEND_NAME % ( - const.GF1, const.GF2, const.GF3)) + raise RuntimeError(f"Backend not recognized. {(const.GF1, const.GF2, const.GF3)}") def _define_backend_mac(self): if self._backend_url == const.BE3_DAFL_CLIENT: # xbl-daq-33 return const.BE3_NORTH_MAC, const.BE3_SOUTH_MAC + elif self._backend_url == const.BE999_DAFL_CLIENT: + return const.BE999_NORTH_MAC, const.BE999_SOUTH_MAC else: - raise RuntimeError(const.ERROR_BACKEND_NAME % ( - const.GF1, const.GF2, const.GF3)) + raise RuntimeError(f"Backend not recognized. {(const.GF1, const.GF2, const.GF3)}") def _set_udp_header_table(self): """ @@ -491,7 +518,7 @@ class GigaFrostClient(Device): # Automatically start simulation if directly invoked if __name__ == "__main__": - gf = GigaFrostClient("X02DA-CAM-GF2") + gf = GigaFrostClient("X02DA-CAM-GF2:", name="gf2") gf.wait_for_connection() From 1aa478db92fc043da22c2cd8b5e579d4f5b418e1 Mon Sep 17 00:00:00 2001 From: Mohacsi Istvan Date: Tue, 25 Jun 2024 16:11:45 +0200 Subject: [PATCH 07/47] Can run a simple acquisition --- tomcat_bec/devices/gigafrost/gigafrostclient.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index b807873..67717d9 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -243,7 +243,7 @@ class GigaFrostClient(Device): # soft trigger on if self._auto_soft_enable: self.cmdSoftEnable.set(1).wait() - self.state = const.GfStatus.OPEN + self.state = const.GfStatus.ACQUIRING return super().stage() def unstage(self): @@ -254,7 +254,7 @@ class GigaFrostClient(Device): self.cmdStartCamera.set(0).wait() if self._auto_soft_enable: self.cmdSoftEnable.set(0).wait() - self.state = const.GfStatus.CLOSED + self.state = const.GfStatus.STOPPED return super().unstage() def stop(self): From 43fe8e37bd7824f01f22909ee49434a866ad92f2 Mon Sep 17 00:00:00 2001 From: Mohacsi Istvan Date: Wed, 26 Jun 2024 13:53:45 +0200 Subject: [PATCH 08/47] GigaFrost works in soft trigger mode --- .../devices/gigafrost/gigafrostclient.py | 57 +++++- tomcat_bec/devices/gigafrost/stddaq_rest.py | 155 +++++++++++++++ tomcat_bec/devices/gigafrost/stddaq_ws.py | 179 ++++++++++++++++++ tomcat_bec/devices/stddaqclient.py | 131 ------------- 4 files changed, 387 insertions(+), 135 deletions(-) create mode 100644 tomcat_bec/devices/gigafrost/stddaq_rest.py create mode 100644 tomcat_bec/devices/gigafrost/stddaq_ws.py delete mode 100644 tomcat_bec/devices/stddaqclient.py diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index 67717d9..6bb3eea 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -1,6 +1,8 @@ from ophyd import Device, Component, EpicsMotor, EpicsSignal, EpicsSignalRO, Kind, DerivedSignal from ophyd.status import Status, SubscriptionStatus, StatusBase, DeviceStatus from ophyd.flyers import FlyerInterface +from ophyd.utils import RedundantStaging +from ophyd.device import Staged from time import sleep import warnings import numpy as np @@ -82,10 +84,10 @@ class GigaFrostClient(Device): cfgInputPolarity2 = Component(EpicsSignalRO, "BNC5_RBV", auto_monitor=True) infoBoardTemp = Component(EpicsSignalRO, "T_BOARD", auto_monitor=True) - def __init__(self, prefix="", *, name, backend_url=const.BE999_DAFL_CLIENT, + def __init__(self, prefix="", *, name, auto_soft_enable=False, backend_url=const.BE999_DAFL_CLIENT, kind=None, read_attrs=None, configuration_attrs=None, parent=None, **kwargs): super().__init__(prefix=prefix, name=name, kind=kind, read_attrs=read_attrs, configuration_attrs=configuration_attrs, parent=parent, **kwargs) - self.oldInitializer(backend_url=backend_url) + self.oldInitializer(backend_url=backend_url, auto_soft_enable=auto_soft_enable) def oldInitializer(self, auto_soft_enable=False, @@ -207,7 +209,7 @@ class GigaFrostClient(Device): ## Stop acquisition self.cmdStartCamera.set(0).wait() if self._auto_soft_enable: - self.set_soft_enable(0) + self.cmdSoftEnable.set(0).wait() # change settings self.cfgExposure.set(exposure).wait() @@ -244,6 +246,9 @@ class GigaFrostClient(Device): if self._auto_soft_enable: self.cmdSoftEnable.set(1).wait() self.state = const.GfStatus.ACQUIRING + + # Gigafrost can finish a run without explicit unstaging + self._staged = Staged.no return super().stage() def unstage(self): @@ -263,6 +268,12 @@ class GigaFrostClient(Device): """ self.unstage() + def trigger(self): + """ + Sends a software trigger + """ + self.cmdSoftTrigger.set(1).wait() + def reset(self): try: self.unstage() @@ -346,6 +357,44 @@ class GigaFrostClient(Device): else: return None + def put_trigger_mode(self, mode): + """ + Set the trigger mode for the GigaFRoST camera. + + Parameters + ---------- + mode : {'auto', 'external', 'timer', 'soft'} + The GigaFRoST trigger mode. + + """ + + if mode not in self._valid_trigger_modes: + raise ValueError("Invalid trigger mode! Valid modes are:\n" + "{}".format(self._valid_trigger_modes)) + + if mode == 'auto': + self.cfgTrigAuto.set(1).wait() + self.cfgTrigSoft.set(0).wait() + self.cfgTrigTimer.set(0).wait() + self.cfgTrigExt.set(0).wait() + elif mode == 'external': + self.cfgTrigAuto.set(0).wait() + self.cfgTrigSoft.set(0).wait() + self.cfgTrigTimer.set(0).wait() + self.cfgTrigExt.set(1).wait() + elif mode == 'timer': + self.cfgTrigAuto.set(0).wait() + self.cfgTrigSoft.set(0).wait() + self.cfgTrigTimer.set(1).wait() + self.cfgTrigExt.set(0).wait() + elif mode == 'soft': + self.cfgTrigAuto.set(0).wait() + self.cfgTrigSoft.set(1).wait() + self.cfgTrigTimer.set(0).wait() + self.cfgTrigExt.set(0).wait() + # Commit parameters + self.cmdSetParam.set(1).wait() + def put_enable_mode(self, mode): """ Apply the enable mode for the GigaFRoST camera. @@ -516,7 +565,7 @@ class GigaFrostClient(Device): -# Automatically start simulation if directly invoked +# Automatically connect to MicroSAXS testbench if directly invoked if __name__ == "__main__": gf = GigaFrostClient("X02DA-CAM-GF2:", name="gf2") gf.wait_for_connection() diff --git a/tomcat_bec/devices/gigafrost/stddaq_rest.py b/tomcat_bec/devices/gigafrost/stddaq_rest.py new file mode 100644 index 0000000..a2b6216 --- /dev/null +++ b/tomcat_bec/devices/gigafrost/stddaq_rest.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +""" +Created on Mon Jun 3 14:16:29 2024 + +@author: mohacsi_i +""" + +from time import sleep +from ophyd import Device, SignalRO, Component +from std_daq_client import StdDaqClient + + + +class StdDaqRestClient(Device): + """ Lightweight wrapper around the official StdDaqClient ophyd package. + Coincidentally also the StdDaqClient is using a Redis broker, that can + potentially be directly fed to the BEC. + + """ + # Status attributes + num_images = Component(SignalRO) + num_images_counter = Component(SignalRO) + output_file = Component(SignalRO) + run_id = Component(SignalRO) + state = Component(SignalRO) + + # Configuration attributes + bit_depth = Component(SignalRO) + detector_name = Component(SignalRO) + detector_type = Component(SignalRO) + image_pixel_width = Component(SignalRO) + image_pixel_height = Component(SignalRO) + start_udp_port = Component(SignalRO) + + def __init__(self, *args, url: str="http://localhost:5000", parent: Device = None, **kwargs) -> None: + + + super().__init__(*args, parent=parent, **kwargs) + self.url = url + + self._n_images = None + self._output_file = None + # Fill signals from current DAQ config + #self.poll_device_config() + #self.poll() + + def connect(self): + self.client = StdDaqClient(url_base=self.url) + + def configure(self, d: dict) -> tuple: + """ + Example: + std.configure(d={'bit_depth': 16, 'writer_user_id': 0}) + + """ + if "n_images" in d: + self._n_images = d['n_images'] + del d['n_images'] + if "output_file" in d: + self._output_file = d['output_file'] + del d['output_file'] + + old_config = self.client.get_config() + + self.client.set_config(daq_config=d) + + new_config = self.client.get_config() + return (old_config, new_config) + + + def stage(self): + self.client.start_writer_async( + {'output_file': self._output_file, 'n_images': self._n_images} + ) + sleep(0.1) + return super().stage() + #while True: + # sleep(0.1) + # daq_status = self.client.get_status() + # if daq_status['acquisition']['state'] in ["ACQUIRING"]: + # break + + def unstage(self): + """ Stop a running acquisition """ + self.client.stop_writer() + return super().unstage() + + def stop(self, *, success=False): + """ Stop a running acquisition """ + self.client.stop_writer() + + if success: + while True: + sleep(0.1) + daq_status = self.client.get_status() + if daq_status['acquisition']['state'] in ["STOPPED", "FINISHED"]: + break + + def poll(self): + """ Querry the currrent status from Std DAQ""" + daq_status = self.client.get_status() + + # Put if new value (put runs subscriptions) + if self.n_images.value != daq_status['acquisition']['info']['n_images']: + self.n_images.put(daq_status['acquisition']['info']['n_images'])get_ + + if self.n_written.value != daq_status['acquisition']['stats']['n_write_completed']: + self.n_written.put(daq_status['acquisition']['stats']['n_write_completed']) + + if self.output_file.value != daq_status['acquisition']['info']['output_file']: + self.output_file.put(daq_status['acquisition']['info']['output_file']) + + if self.run_id.value != daq_status['acquisition']['info']['run_id']: + self.run_id.put(daq_status['acquisition']['info']['run_id']) + + if self.state.value != daq_status['acquisition']['state']: + self.state.put(daq_status['acquisition']['state']) + + + def poll_device_config(self): + """ Querry the currrent configuration from Std DAQ""" + daq_config = self.client.get_config() + + # Put if new value (put runs subscriptions) + if self.bit_depth.value != daq_config['bit_depth']: + self.bit_depth.put(daq_config['bit_depth']) + + if self.detector_name.value != daq_config['detector_name']: + self.detector_name.put(daq_config['detector_name']) + + if self.detector_type.value != daq_config['detector_type']: + self.detector_type.put(daq_config['detector_type']) + + if self.image_pixel_width.value != daq_config['image_pixel_width']: + self.image_pixel_width.put(daq_config['image_pixel_width']) + + if self.image_pixel_height.value != daq_config['image_pixel_height']: + self.image_pixel_height.put(daq_config['image_pixel_height']) + + if self.start_udp_port.value != daq_config['start_udp_port']: + self.start_udp_port.put(daq_config['start_udp_port']) + + + + + +# Automatically connect to MicroSAXS testbench if directly invoked +if __name__ == "__main__": + daq = StdDaqRestClient(name='daq', url="http://xbl-daq-29:5001") + daq.wait_for_connection() + + + + + diff --git a/tomcat_bec/devices/gigafrost/stddaq_ws.py b/tomcat_bec/devices/gigafrost/stddaq_ws.py new file mode 100644 index 0000000..79f7712 --- /dev/null +++ b/tomcat_bec/devices/gigafrost/stddaq_ws.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +""" +Created on Mon Jun 3 14:16:29 2024 + +@author: mohacsi_i +""" +from time import sleep +from threading import Thread +from ophyd import Device, Signal, Component +from ophyd.utils import ReadOnlyError +from websockets.sync.client import connect +from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError + +import json + + + +class SignalRO(Signal): + """ Reimplementation of SignalRO that allows forced writes""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._metadata.update( + connected=True, + write_access=False, + ) + + def put(self, value, *, timestamp=None, force=False): + if not force: + raise ReadOnlyError("The signal {} is readonly.".format(self.name)) + super().put(value, timestamp=timestamp, force=force) + + def set(self, value, *, timestamp=None, force=False): + raise ReadOnlyError("The signal {} is readonly.".format(self.name)) + + + +class StdDaqWsClient(Device): + """ Lightweight wrapper around the undocumented StdDaq websocket interface. + This was meant to replace the documented python client. We cannot read + or change the current configuration through this interface. + + A bit more about the Standard DAQ configuration: + + The standard DAQ configuration is a single JSON file locally autodeployed + to the DAQ servers (as root!!!). Previously there was a service to offer + a REST API to write this file, but since there's no frontend group, this + is no longer available. + """ + # Status attributes + status = Component(SignalRO, value='unknown') + n_image = Component(Signal, value=100) + file_path = Component(Signal, value="/gpfs/test/test-beamline") + + def __init__(self, *args, url: str="ws://localhost:8080", parent: Device = None, **kwargs) -> None: + super().__init__(*args, parent=parent, **kwargs) + self._ws_url = url + + # Connect ro the DAQ + self.connect() + + def connect(self): + # StdDAQ may reject connection for a few seconds + try: + self._client = connect(self._ws_url) + except ConnectionRefusedError: + sleep(5) + self._client = connect(self._ws_url) + + def configure(self, d: dict) -> tuple: + """ + Example: + std.configure(d={'n_images': 234, 'file_path': "/data/test/raw"}) + """ + if "num_images" in d: + self.n_images.set(d['n_images']) + del d['num_images'] + if "file_path" in d: + self.output_file.set(d['file_path']) + del d['file_path'] + return (old_config, new_config) + + def stage(self) -> list: + """Behavior: the StdDAQ can stop the previous run either by itself or + by calling unstage. So it might start from an already running state or + not, we can't query if not running. + """ + + file_path = self.file_path.get() + n_image = self.n_image.get() + + message = {"command":"start", "path": file_path, "n_image": n_image} + reply = self.message(message) + + + reply = json.loads(reply) + if reply['status'] in ('creating_file'): + self.status.put(reply['status'], force=True) + elif reply['status'] in ('rejected'): + raise RuntimeError(f"Start command rejected (might be already running): {reply['reason']}") + + self.t = Thread(target = self.poll) + self.t.start() + return super().stage() + + def unstage(self): + """ Stop a running acquisition + + WARN: This will also close the connection!!! + """ + message = {"command":"stop"} + self.message(message, wait_reply=False) + return super().unstage() + + def stop(self, *, success=False): + """ Stop a running acquisition + + WARN: This will also close the connection!!! + """ + message = {"command":"stop"} + # The poller thread locks recv raising a RuntimeError + self.message(message, wait_reply=False) + + def abort(self): + return self.message({"command": "abort"}) + + def message(self, d: dict, timeout=1, wait_reply=True): + """Send a message to the StdDAQ and receive a reply + + Note: finishing acquisition meang StdDAQ will close connections so + there's no idle state polling. + """ + reply = None + if isinstance(d, dict): + msg = json.dumps(d) + else: + msg = str(d) + + # Send message (reopen connection if needed) + try: + self._client.send(msg) + except ConnectionClosedError: + self.connect() + self._client.send(msg) + except ConnectionClosedOK: + self.connect() + self._client.send(msg) + # Wait for reply + if wait_reply: + try: + reply = self._client.recv(timeout) + print("A: ", reply) + return reply + except ConnectionClosedError as ex: + print(ex) + return None + except TimeoutError: + return None + return None + + def poll(self): + """Monitor status messages until connection is open""" + for msg in self._client: + try: + message = json.loads(msg) + self.status.put(message['status'], force=True) + except Exception as ex: + print(ex) + return + + +# Automatically connect to MicroSAXS testbench if directly invoked +if __name__ == "__main__": + daq = StdDaqWsClient(name='daq', url="ws://xbl-daq-29:8080") + daq.wait_for_connection() + + + + + diff --git a/tomcat_bec/devices/stddaqclient.py b/tomcat_bec/devices/stddaqclient.py deleted file mode 100644 index b606b05..0000000 --- a/tomcat_bec/devices/stddaqclient.py +++ /dev/null @@ -1,131 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Mon Jun 3 14:16:29 2024 - -@author: mohacsi_i -""" -from time import sleep -from ophyd import Device, Signal, Component -from websockets.sync.client import connect -from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError - -import json - -class StdDaqClientDevice(Device): - """ Lightweight wrapper around the undocumented StdDaq websocket interface. - This was meant to replace the documented python client. - - - - - - - A bit more about the Standard DAQ configuration: - - The standard DAQ configuration is a single JSON file locally autodeployed - to the DAQ servers (as root!!!). Previously there was a service to offer - a REST API to write this file, but since there's no frontend group, this - is no longer available. - """ - # Status attributes - n_image = Component(Signal) - file_path = Component(Signal) - - def __init__(self, *args, parent: Device = None, **kwargs) -> None: - super().__init__(*args, parent=parent, **kwargs) - self.ws_server_url = ( - kwargs["daq_url"] if "daq_url" in kwargs else "ws://xbl-daq-29:8080") - self._client = connect(self.ws_server_url) - - self.n_image.set(100) - self.file_path.set("/gpfs/test/test-beamline") - - def connect(self): - self._client = connect(self.ws_server_url) - - def configure(self, d: dict) -> tuple: - """ - Example: - std.configure(d={'n_images': 234, 'file_path': "/data/test/raw"}) - """ - if "num_images" in d: - self.n_images.set(d['n_images']) - del d['num_images'] - if "file_path" in d: - self.output_file.set(d['file_path']) - del d['file_path'] - return (old_config, new_config) - - def stage(self): - file_path = self.file_path.get() - n_image = self.n_image.get() - - message = {"command":"start", "path": file_path, "n_image": n_image} - self.message(message) - return super().stage() - - def unstage(self): - """ Stop a running acquisition - - WARN: This will also close the connection!!! - """ - message = {"command":"stop"} - self.message(message) - return super().unstage() - - def stop(self, *, success=False): - """ Stop a running acquisition - - WARN: This will also close the connection!!! - """ - message = {"command":"stop"} - self.message(message) - - def status(self): - return self.message({"command": "status"}) - - def abort(self): - return self.message({"command": "abort"}) - - def message(self, d: dict, timeout=1): - """ - - Note: finishing acquisition meang StdDAQ will close connections - """ - reply = None - if isinstance(d, dict): - msg = json.dumps(d) - else: - msg = str(d) - print("Q: ", msg) - # Send message (reopen connection if needed) - try: - self._client.send(msg) - except ConnectionClosedError: - # StdDAQ may reject connection for a few seconds - try: - self._client = connect(self.ws_server_url) - except ConnectionRefusedError: - sleep(5) - self._client = connect(self.ws_server_url) - self._client.send(msg) - except ConnectionClosedOK: - # StdDAQ may reject connection for a few seconds - try: - self._client = connect(self.ws_server_url) - except ConnectionRefusedError: - sleep(5) - self._client = connect(self.ws_server_url) - self._client.send(msg) - # Wait for reply - try: - reply = self._client.recv(timeout) - print("A: ", reply) - except ConnectionClosedError as ex: - print(ex) - pass - except TimeoutError: - pass - return reply - - From 4e64899b99682fc5c985361ff5d24fe03aa4b016 Mon Sep 17 00:00:00 2001 From: Mohacsi Istvan Date: Wed, 26 Jun 2024 16:53:47 +0200 Subject: [PATCH 09/47] Kind of working before GF shutdown --- tomcat_bec/devices/gigafrost/gigafrostclient.py | 4 ++-- tomcat_bec/devices/gigafrost/stddaq_ws.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index 6bb3eea..3912730 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -28,7 +28,7 @@ class GigaFrostClient(Device): cmdStartCamera = Component(EpicsSignal, "START_CAM", put_complete=True) cmdSetParam = Component(EpicsSignal, "SET_PARAM.PROC", put_complete=True) - + # UDP header cfgUdpNumPorts = Component(EpicsSignal, "PORTS", put_complete=True) cfgUdpNumFrames = Component(EpicsSignal, "FRAMENUM", put_complete=True) cfgUdpHtOffset = Component(EpicsSignal, "HT_OFFSET", put_complete=True) @@ -567,7 +567,7 @@ class GigaFrostClient(Device): # Automatically connect to MicroSAXS testbench if directly invoked if __name__ == "__main__": - gf = GigaFrostClient("X02DA-CAM-GF2:", name="gf2") + gf = GigaFrostClient("X02DA-CAM-GF2:", name="gf2", backend_url="http://xbl-daq-28:8080", auto_soft_enable=True) gf.wait_for_connection() diff --git a/tomcat_bec/devices/gigafrost/stddaq_ws.py b/tomcat_bec/devices/gigafrost/stddaq_ws.py index 79f7712..1ae39d6 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_ws.py +++ b/tomcat_bec/devices/gigafrost/stddaq_ws.py @@ -48,7 +48,7 @@ class StdDaqWsClient(Device): """ # Status attributes status = Component(SignalRO, value='unknown') - n_image = Component(Signal, value=100) + n_image = Component(Signal, value=10000) file_path = Component(Signal, value="/gpfs/test/test-beamline") def __init__(self, *args, url: str="ws://localhost:8080", parent: Device = None, **kwargs) -> None: From 5d08823768b447aabd636a1064e391924361c51e Mon Sep 17 00:00:00 2001 From: Mohacsi Istvan Date: Thu, 27 Jun 2024 16:59:36 +0200 Subject: [PATCH 10/47] Repository cleanup --- tomcat_bec/devices/gigafrost/gfconstants.py | 13 +- .../devices/gigafrost/gigafrostclient.py | 286 ++++++++---------- tomcat_bec/devices/gigafrost/stddaq_rest.py | 155 ---------- tomcat_bec/devices/gigafrost/stddaq_ws.py | 122 ++++---- 4 files changed, 182 insertions(+), 394 deletions(-) delete mode 100644 tomcat_bec/devices/gigafrost/stddaq_rest.py diff --git a/tomcat_bec/devices/gigafrost/gfconstants.py b/tomcat_bec/devices/gigafrost/gfconstants.py index fbf9173..13a841d 100644 --- a/tomcat_bec/devices/gigafrost/gfconstants.py +++ b/tomcat_bec/devices/gigafrost/gfconstants.py @@ -1,5 +1,3 @@ -##################################################################### - from enum import Enum @@ -29,23 +27,14 @@ BE3_SOUTH_MAC = [0x50, 0x65, 0xf3, 0x81, 0xd5, 0x31] # ens4 BE3_NORTH_IP = [10, 4, 0, 101] BE3_SOUTH_IP = [10, 0, 0, 101] - - +# Backend for MicroXAS test-stand BE999_DAFL_CLIENT = "http://xbl-daq-28:8080" BE999_SOUTH_MAC = [0x9C, 0xDC, 0x71, 0x47, 0xE5, 0xD1] # 9c:dc:71:47:e5:d1 BE999_NORTH_MAC = [0x9C, 0xDC, 0x71, 0x47, 0xE5, 0xDD] # 9c:dc:71:47:e5:dd BE999_NORTH_IP = [10, 4, 0, 101] BE999_SOUTH_IP = [10, 0, 0, 101] - - - - - - - # GF Names GF1 = 'gf1' GF2 = 'gf2' GF3 = 'gf3' - diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index 3912730..b2d86b8 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -1,25 +1,29 @@ -from ophyd import Device, Component, EpicsMotor, EpicsSignal, EpicsSignalRO, Kind, DerivedSignal -from ophyd.status import Status, SubscriptionStatus, StatusBase, DeviceStatus -from ophyd.flyers import FlyerInterface +from ophyd import Device, Component, EpicsSignal, EpicsSignalRO, Kind, DerivedSignal from ophyd.utils import RedundantStaging from ophyd.device import Staged -from time import sleep -import warnings -import numpy as np -import time -from typing import Union -from collections import OrderedDict import gfconstants as const from gfutils import extend_header_table, port2byte -class GigaFrostClient(Device): - """ - Ophyd device class to the Gigafrost cameras at Tomcat - The actual hardware is implemented on an old fork of Helge's camera IOC. - This means that the camera behaves differently than the SF cameras and - it has a lot of Tomcat specific additions. +class GigaFrostClient(Device): + """Ophyd device class to control Gigafrost cameras at Tomcat + + The actual hardware is implemented by an IOC based on an old fork of Helge's + cameras. This means that the camera behaves differently than the SF cameras + in particular it provides even less feedback about it's internal progress. + Helge will update the GigaFrost IOC after working beamline. + The ophyd class is based on the 'gfclient' package and has a lot of Tomcat + specific additions. It does behave differently though, as ophyd swallows the + errors from failed PV writes. + + Parameters + ---------- + use_soft_enable : bool + Flag to use the camera's soft enable (default: False) + backend_url : str + Backend url address necessary to set up the camera's udp header. + (default: http://xbl-daq-23:8080) """ infoBusyFlag = Component(EpicsSignalRO, "BUSY_STAT", auto_monitor=True) infoSyncFlag = Component(EpicsSignalRO, "SYNC_FLAG", auto_monitor=True) @@ -84,28 +88,11 @@ class GigaFrostClient(Device): cfgInputPolarity2 = Component(EpicsSignalRO, "BNC5_RBV", auto_monitor=True) infoBoardTemp = Component(EpicsSignalRO, "T_BOARD", auto_monitor=True) - def __init__(self, prefix="", *, name, auto_soft_enable=False, backend_url=const.BE999_DAFL_CLIENT, + USER_ACCESS = ["exposure_mode", "fix_nframes_mode", "trigger_mode", "enable_mode"] + + def __init__(self, prefix="", *, name, auto_soft_enable=False, timeout=10, backend_url=const.BE999_DAFL_CLIENT, kind=None, read_attrs=None, configuration_attrs=None, parent=None, **kwargs): super().__init__(prefix=prefix, name=name, kind=kind, read_attrs=read_attrs, configuration_attrs=configuration_attrs, parent=parent, **kwargs) - self.oldInitializer(backend_url=backend_url, auto_soft_enable=auto_soft_enable) - - - def oldInitializer(self, auto_soft_enable=False, - timeout=10.0, backend_url=const.BE1_DAFL_CLIENT): - """ - Class to control the Gigafrost camera and readout system. - - Parameters - ---------- - use_soft_enable : bool - Flag to use the camera's soft enable (default: False) - timeout : int - Maximum time to wait (in seconds) for value before returning None - backend_url : str - Backend url address necessary to set up the camera's udp header. - (default: http://xbl-daq-23:8080) - - """ self._auto_soft_enable = auto_soft_enable self._timeout = timeout self._backend_url = backend_url @@ -124,14 +111,10 @@ class GigaFrostClient(Device): self.initialize() def initialize(self): - """ - Initialize the camera, set channel values - """ + """Initialize the camera, set channel values""" ## Stop acquisition self.cmdStartCamera.set(0).wait() - - ### set entry to UDP table # number of UDP ports to use self.cfgUdpNumPorts.set(2).wait() @@ -139,19 +122,19 @@ class GigaFrostClient(Device): self.cfgUdpNumFrames.set(5).wait() # offset in UDP table - where to find the first entry self.cfgUdpHtOffset.set(0).wait() - # activate changes self.cmdWriteService.set(1).wait() + # Configure software triggering if needed if self._auto_soft_enable: # trigger modes self.cfgCntStartBit.set(1).wait() self.cfgCntEndBit.set(0).wait() # set modes - self.put_enable_mode('soft') - self.put_trigger_mode('auto') - self.put_exposure_mode('timer') + self.enable_mode = 'soft' + self.trigger_mode = 'auto' + self.exposure_mode = 'timer' # line swap - on for west, off for east self.cfgLineSwapSW.set(1).wait() @@ -173,8 +156,7 @@ class GigaFrostClient(Device): def configure(self, nimages=10, exposure=0.2, period=1.0, roix=2016, roiy=2016, scanid=0, correction_mode=5): - """ - Configure the next scan with the GigaFRoST camera + """Configure the next scan with the GigaFRoST camera Parameters ---------- @@ -204,9 +186,7 @@ class GigaFrostClient(Device): * 4: Invert pixel values, but do not apply any linearity correction * 5: Apply the full linearity correction """ - - # switch to idle - ## Stop acquisition + # Stop acquisition self.cmdStartCamera.set(0).wait() if self._auto_soft_enable: self.cmdSoftEnable.set(0).wait() @@ -237,9 +217,7 @@ class GigaFrostClient(Device): } def stage(self): - """ - Standard ophyd method to start an acquisition - """ + """Standard ophyd method to start an acquisition""" # change to running self.cmdStartCamera.set(1).wait() # soft trigger on @@ -252,9 +230,7 @@ class GigaFrostClient(Device): return super().stage() def unstage(self): - """ - Standard ophyd method to stop an acquisition - """ + """Standard ophyd method to finish an acquisition""" # switch to idle self.cmdStartCamera.set(0).wait() if self._auto_soft_enable: @@ -263,15 +239,11 @@ class GigaFrostClient(Device): return super().unstage() def stop(self): - """ - Standard ophyd method to stop an acquisition - """ + """Standard ophyd method to stop an acquisition""" self.unstage() def trigger(self): - """ - Sends a software trigger - """ + """Sends a software trigger""" self.cmdSoftTrigger.set(1).wait() def reset(self): @@ -281,18 +253,16 @@ class GigaFrostClient(Device): pass self.state = const.GfStatus.INIT - def get_exposure_mode(self): - """ - Returns the current exposure mode of the GigaFRost camera. + @property + def exposure_mode(self): + """Returns the current exposure mode of the GigaFRost camera. Returns ------- exp_mode : {'external', 'timer', 'soft'} The camera's active exposure mode. If more than one mode is active at the same time, it returns None. - """ - mode_soft = self.cfgTrigExpSoft.get() mode_timer = self.cfgTrigExpTimer.get() mode_external = self.cfgTrigExpExt.get() @@ -305,18 +275,43 @@ class GigaFrostClient(Device): else: return None + @exposure_mode.setter + def exposure_mode(self, exp_mode): + """Apply the exposure mode for the GigaFRoST camera. - def get_fix_nframes_mode(self): + Parameters + ---------- + exp_mode : {'external', 'timer', 'soft'} + The exposure mode to be set. """ - Return the current fixed number of frames mode of the GigaFRoST camera. + if exp_mode not in self._valid_exposure_modes: + raise ValueError("Invalid exposure mode! Valid modes are:\n" + "{}".format(self._valid_exposure_modes)) + + if exp_mode == 'external': + self.cfgTrigExpExt.set(1).wait() + self.cfgTrigExpSoft.set(0).wait() + self.cfgTrigExpTimer.set(0).wait() + elif exp_mode == 'timer': + self.cfgTrigExpExt.set(0).wait() + self.cfgTrigExpSoft.set(0).wait() + self.cfgTrigExpTimer.set(1).wait() + elif exp_mode == 'soft': + self.cfgTrigExpExt.set(0).wait() + self.cfgTrigExpSoft.set(1).wait() + self.cfgTrigExpTimer.set(0).wait() + # Commit parameters + self.cmdSetParam.set(1).wait() + + @property + def fix_nframes_mode(self): + """Return the current fixed number of frames mode of the GigaFRoST camera. Returns ------- fix_nframes_mode : {'off', 'start', 'end', 'start+end'} The camera's active fixed number of frames mode. - """ - start_bit = self.cfgCntStartBit.get() end_bit = self.cfgCntStartBit.get() @@ -331,16 +326,45 @@ class GigaFrostClient(Device): else: return None - def get_trigger_mode(self): + @fix_nframes_mode.setter + def fix_nframes_mode(self, mode): + """Apply the fixed number of frames settings to the GigaFRoST camera. + + Parameters + ---------- + mode : {'off', 'start', 'end', 'start+end'} + The fixed number of frames mode to be applied. """ - Method to detect the current trigger mode set in the GigaFRost camera. + if mode not in self._valid_fix_nframe_modes: + raise ValueError("Invalid fixed number of frames mode! " + "Valid modes are:\n{}".format(self._valid_fix_nframe_modes)) + + self._fix_nframes_mode = mode + if self._fix_nframes_mode == 'off': + self.cfgCntStartBit.set(0).wait() + self.cfgCntEndBit.set(0).wait() + elif self._fix_nframes_mode == 'start': + self.cfgCntStartBit.set(1).wait() + self.cfgCntEndBit.set(0).wait() + elif self._fix_nframes_mode == 'end': + self.cfgCntStartBit.set(0).wait() + self.cfgCntEndBit.set(1).wait() + elif self._fix_nframes_mode == 'start+end': + self.cfgCntStartBit.set(1).wait() + self.cfgCntEndBit.set(1).wait() + # Commit parameters + self.cmdSetParam.set(1).wait() + + + @property + def trigger_mode(self): + """Method to detect the current trigger mode set in the GigaFRost camera. Returns ------- mode : {'auto', 'external', 'timer', 'soft'} The camera's active trigger mode. If more than one mode is active at the moment, None is returned. - """ mode_auto = self.cfgTrigAuto.get() mode_external = self.cfgTrigExt.get() @@ -356,18 +380,15 @@ class GigaFrostClient(Device): return 'external' else: return None - - def put_trigger_mode(self, mode): - """ - Set the trigger mode for the GigaFRoST camera. + @trigger_mode.setter + def trigger_mode(self, mode): + """Set the trigger mode for the GigaFRoST camera. Parameters ---------- mode : {'auto', 'external', 'timer', 'soft'} The GigaFRoST trigger mode. - """ - if mode not in self._valid_trigger_modes: raise ValueError("Invalid trigger mode! Valid modes are:\n" "{}".format(self._valid_trigger_modes)) @@ -395,17 +416,39 @@ class GigaFrostClient(Device): # Commit parameters self.cmdSetParam.set(1).wait() - def put_enable_mode(self, mode): + @property + def enable_mode(self): + """Return the enable mode set in the GigaFRost camera. + + Returns + ------- + enable_mode: {'soft', 'external', 'soft+ext', 'always'} + The camera's active enable mode. """ - Apply the enable mode for the GigaFRoST camera. + mode_soft = self.cfgTrigEnableSoft.get() + mode_external = self.cfgTrigEnableExt.get() + mode_auto = self.cfgTrigEnableAuto.get() + if mode_soft and not mode_auto: + if mode_external: + return 'soft+ext' + else: + return 'soft' + elif mode_auto and not mode_soft and not mode_external: + return 'always' + elif mode_external and not mode_soft and not mode_auto: + return 'external' + else: + return None + + @enable_mode.setter + def enable_mode(self, mode): + """Apply the enable mode for the GigaFRoST camera. Parameters ---------- mode : {'soft', 'external', 'soft+ext', 'always'} The enable mode to be applied. - """ - if mode not in self._valid_enable_modes: raise ValueError("Invalid enable mode! Valid modes are:\n" "{}".format(self._valid_enable_modes)) @@ -429,67 +472,6 @@ class GigaFrostClient(Device): # Commit parameters self.cmdSetParam.set(1).wait() - def put_exposure_mode(self, exp_mode): - """ - Apply the exposure mode for the GigaFRoST camera. - - Parameters - ---------- - exp_mode : {'external', 'timer', 'soft'} - The exposure mode to be set. - - """ - - if exp_mode not in self._valid_exposure_modes: - raise ValueError("Invalid exposure mode! Valid modes are:\n" - "{}".format(self._valid_exposure_modes)) - - if exp_mode == 'external': - self.cfgTrigExpExt.set(1).wait() - self.cfgTrigExpSoft.set(0).wait() - self.cfgTrigExpTimer.set(0).wait() - elif exp_mode == 'timer': - self.cfgTrigExpExt.set(0).wait() - self.cfgTrigExpSoft.set(0).wait() - self.cfgTrigExpTimer.set(1).wait() - elif exp_mode == 'soft': - self.cfgTrigExpExt.set(0).wait() - self.cfgTrigExpSoft.set(1).wait() - self.cfgTrigExpTimer.set(0).wait() - # Commit parameters - self.cmdSetParam.set(1).wait() - - def put_fix_nframes_mode(self, mode): - """ - Apply the fixed number of frames settings to the GigaFRoST camera. - - Parameters - ---------- - mode : {'off', 'start', 'end', 'start+end'} - The fixed number of frames mode to be applied. - - """ - - if mode not in self._valid_fix_nframe_modes: - raise ValueError("Invalid fixed number of frames mode! " - "Valid modes are:\n{}".format(self._valid_fix_nframe_modes)) - - self._fix_nframes_mode = mode - if self._fix_nframes_mode == 'off': - self.cfgCntStartBit.set(0).wait() - self.cfgCntEndBit.set(0).wait() - elif self._fix_nframes_mode == 'start': - self.cfgCntStartBit.set(1).wait() - self.cfgCntEndBit.set(0).wait() - elif self._fix_nframes_mode == 'end': - self.cfgCntStartBit.set(0).wait() - self.cfgCntEndBit.set(1).wait() - elif self._fix_nframes_mode == 'start+end': - self.cfgCntStartBit.set(1).wait() - self.cfgCntEndBit.set(1).wait() - # Commit parameters - self.cmdSetParam.set(1).wait() - def get_state(self): return self.state @@ -509,21 +491,15 @@ class GigaFrostClient(Device): return self._backend_url def set_backend_ip(self, north, south): - """ - Method to manually set the backend ip - """ + """Method to manually set the backend ip""" self._north_ip, self._south_ip = north, south def set_backend_mac(self, north, south): - """ - Method to manually set the backend mac - """ + """Method to manually set the backend mac""" self._north_mac, self._south_mac = north, south def _build_udp_header_table(self): - """ - Build the header table for the communication - """ + """Build the header table for the communication""" udp_header_table = [] for i in range(0,64,1): @@ -558,9 +534,7 @@ class GigaFrostClient(Device): raise RuntimeError(f"Backend not recognized. {(const.GF1, const.GF2, const.GF3)}") def _set_udp_header_table(self): - """ - Set the communication parameters for the camera module - """ + """Set the communication parameters for the camera module""" self.cfgConnectionParam.set(self._build_udp_header_table()).wait() diff --git a/tomcat_bec/devices/gigafrost/stddaq_rest.py b/tomcat_bec/devices/gigafrost/stddaq_rest.py deleted file mode 100644 index a2b6216..0000000 --- a/tomcat_bec/devices/gigafrost/stddaq_rest.py +++ /dev/null @@ -1,155 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Mon Jun 3 14:16:29 2024 - -@author: mohacsi_i -""" - -from time import sleep -from ophyd import Device, SignalRO, Component -from std_daq_client import StdDaqClient - - - -class StdDaqRestClient(Device): - """ Lightweight wrapper around the official StdDaqClient ophyd package. - Coincidentally also the StdDaqClient is using a Redis broker, that can - potentially be directly fed to the BEC. - - """ - # Status attributes - num_images = Component(SignalRO) - num_images_counter = Component(SignalRO) - output_file = Component(SignalRO) - run_id = Component(SignalRO) - state = Component(SignalRO) - - # Configuration attributes - bit_depth = Component(SignalRO) - detector_name = Component(SignalRO) - detector_type = Component(SignalRO) - image_pixel_width = Component(SignalRO) - image_pixel_height = Component(SignalRO) - start_udp_port = Component(SignalRO) - - def __init__(self, *args, url: str="http://localhost:5000", parent: Device = None, **kwargs) -> None: - - - super().__init__(*args, parent=parent, **kwargs) - self.url = url - - self._n_images = None - self._output_file = None - # Fill signals from current DAQ config - #self.poll_device_config() - #self.poll() - - def connect(self): - self.client = StdDaqClient(url_base=self.url) - - def configure(self, d: dict) -> tuple: - """ - Example: - std.configure(d={'bit_depth': 16, 'writer_user_id': 0}) - - """ - if "n_images" in d: - self._n_images = d['n_images'] - del d['n_images'] - if "output_file" in d: - self._output_file = d['output_file'] - del d['output_file'] - - old_config = self.client.get_config() - - self.client.set_config(daq_config=d) - - new_config = self.client.get_config() - return (old_config, new_config) - - - def stage(self): - self.client.start_writer_async( - {'output_file': self._output_file, 'n_images': self._n_images} - ) - sleep(0.1) - return super().stage() - #while True: - # sleep(0.1) - # daq_status = self.client.get_status() - # if daq_status['acquisition']['state'] in ["ACQUIRING"]: - # break - - def unstage(self): - """ Stop a running acquisition """ - self.client.stop_writer() - return super().unstage() - - def stop(self, *, success=False): - """ Stop a running acquisition """ - self.client.stop_writer() - - if success: - while True: - sleep(0.1) - daq_status = self.client.get_status() - if daq_status['acquisition']['state'] in ["STOPPED", "FINISHED"]: - break - - def poll(self): - """ Querry the currrent status from Std DAQ""" - daq_status = self.client.get_status() - - # Put if new value (put runs subscriptions) - if self.n_images.value != daq_status['acquisition']['info']['n_images']: - self.n_images.put(daq_status['acquisition']['info']['n_images'])get_ - - if self.n_written.value != daq_status['acquisition']['stats']['n_write_completed']: - self.n_written.put(daq_status['acquisition']['stats']['n_write_completed']) - - if self.output_file.value != daq_status['acquisition']['info']['output_file']: - self.output_file.put(daq_status['acquisition']['info']['output_file']) - - if self.run_id.value != daq_status['acquisition']['info']['run_id']: - self.run_id.put(daq_status['acquisition']['info']['run_id']) - - if self.state.value != daq_status['acquisition']['state']: - self.state.put(daq_status['acquisition']['state']) - - - def poll_device_config(self): - """ Querry the currrent configuration from Std DAQ""" - daq_config = self.client.get_config() - - # Put if new value (put runs subscriptions) - if self.bit_depth.value != daq_config['bit_depth']: - self.bit_depth.put(daq_config['bit_depth']) - - if self.detector_name.value != daq_config['detector_name']: - self.detector_name.put(daq_config['detector_name']) - - if self.detector_type.value != daq_config['detector_type']: - self.detector_type.put(daq_config['detector_type']) - - if self.image_pixel_width.value != daq_config['image_pixel_width']: - self.image_pixel_width.put(daq_config['image_pixel_width']) - - if self.image_pixel_height.value != daq_config['image_pixel_height']: - self.image_pixel_height.put(daq_config['image_pixel_height']) - - if self.start_udp_port.value != daq_config['start_udp_port']: - self.start_udp_port.put(daq_config['start_udp_port']) - - - - - -# Automatically connect to MicroSAXS testbench if directly invoked -if __name__ == "__main__": - daq = StdDaqRestClient(name='daq', url="http://xbl-daq-29:5001") - daq.wait_for_connection() - - - - - diff --git a/tomcat_bec/devices/gigafrost/stddaq_ws.py b/tomcat_bec/devices/gigafrost/stddaq_ws.py index 1ae39d6..d3368d9 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_ws.py +++ b/tomcat_bec/devices/gigafrost/stddaq_ws.py @@ -1,58 +1,32 @@ -# -*- coding: utf-8 -*- -""" -Created on Mon Jun 3 14:16:29 2024 - -@author: mohacsi_i -""" +from ophyd import Device, Signal, Component, Kind +import json from time import sleep from threading import Thread -from ophyd import Device, Signal, Component -from ophyd.utils import ReadOnlyError from websockets.sync.client import connect from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError -import json - - - -class SignalRO(Signal): - """ Reimplementation of SignalRO that allows forced writes""" - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._metadata.update( - connected=True, - write_access=False, - ) - - def put(self, value, *, timestamp=None, force=False): - if not force: - raise ReadOnlyError("The signal {} is readonly.".format(self.name)) - super().put(value, timestamp=timestamp, force=force) - - def set(self, value, *, timestamp=None, force=False): - raise ReadOnlyError("The signal {} is readonly.".format(self.name)) - - class StdDaqWsClient(Device): - """ Lightweight wrapper around the undocumented StdDaq websocket interface. - This was meant to replace the documented python client. We cannot read - or change the current configuration through this interface. + """Wrapper class around the StdDaq websocket interface. - A bit more about the Standard DAQ configuration: - - The standard DAQ configuration is a single JSON file locally autodeployed - to the DAQ servers (as root!!!). Previously there was a service to offer - a REST API to write this file, but since there's no frontend group, this - is no longer available. + This was meant to replace the documented python client. We cannot read + or change the current configuration through this interface. + + A bit more about the Standard DAQ configuration: + + The standard DAQ configuration is a single JSON file locally autodeployed + to the DAQ servers (as root!!!). Previously there was a service to offer + a REST API to write this file, but since there's no frontend group, this + is no longer available. """ # Status attributes - status = Component(SignalRO, value='unknown') - n_image = Component(Signal, value=10000) - file_path = Component(Signal, value="/gpfs/test/test-beamline") + status = Component(Signal, value='unknown') + n_images = Component(Signal, value=10000, kind=Kind.config) + file_path = Component(Signal, value="/gpfs/test/test-beamline", kind=Kind.config) def __init__(self, *args, url: str="ws://localhost:8080", parent: Device = None, **kwargs) -> None: super().__init__(*args, parent=parent, **kwargs) + self.status._metadata['write_access'] = False self._ws_url = url # Connect ro the DAQ @@ -66,31 +40,49 @@ class StdDaqWsClient(Device): sleep(5) self._client = connect(self._ws_url) - def configure(self, d: dict) -> tuple: - """ + def configure(self, n_images: int=None, file_path: str=None) -> tuple: + """Set the standard DAQ parameters for the next run + + Note that full reconfiguration is not possible with the websocket + interface, only changing acquisition parameters. These changes are only + activated upon staging! + Example: - std.configure(d={'n_images': 234, 'file_path': "/data/test/raw"}) + ---------- + std.configure(n_images=10000, file_path="/data/test/raw") + + Parameters + ---------- + n_images : int, optional + Number of images to be taken during each scan. Set to -1 for an + unlimited number of images (limited by the ringbuffer size and + backend speed). (default = 10000) + file_path : string, optional + Save file path. (default = '/gpfs/test/test-beamline') + """ - if "num_images" in d: - self.n_images.set(d['n_images']) - del d['num_images'] - if "file_path" in d: - self.output_file.set(d['file_path']) - del d['file_path'] + old_config = self.read_configuration() + + if n_images is not None: + self.n_images.set(int(n_images)) + if file_path is not None: + self.output_file.set(str(file_path)) + + new_config = self.read_configuration() return (old_config, new_config) def stage(self) -> list: - """Behavior: the StdDAQ can stop the previous run either by itself or + """Start a new run with the standard DAQ + + Behavior: the StdDAQ can stop the previous run either by itself or by calling unstage. So it might start from an already running state or not, we can't query if not running. """ - file_path = self.file_path.get() - n_image = self.n_image.get() + n_image = self.n_images.get() message = {"command":"start", "path": file_path, "n_image": n_image} reply = self.message(message) - reply = json.loads(reply) if reply['status'] in ('creating_file'): @@ -129,7 +121,6 @@ class StdDaqWsClient(Device): Note: finishing acquisition meang StdDAQ will close connections so there's no idle state polling. """ - reply = None if isinstance(d, dict): msg = json.dumps(d) else: @@ -138,24 +129,18 @@ class StdDaqWsClient(Device): # Send message (reopen connection if needed) try: self._client.send(msg) - except ConnectionClosedError: - self.connect() - self._client.send(msg) - except ConnectionClosedOK: + except (ConnectionClosedError, ConnectionClosedOK): self.connect() self._client.send(msg) # Wait for reply + reply = None if wait_reply: try: reply = self._client.recv(timeout) - print("A: ", reply) return reply - except ConnectionClosedError as ex: + except (ConnectionClosedError, ConnectionClosedOK, TimeoutError) as ex: print(ex) - return None - except TimeoutError: - return None - return None + return reply def poll(self): """Monitor status messages until connection is open""" @@ -172,8 +157,3 @@ class StdDaqWsClient(Device): if __name__ == "__main__": daq = StdDaqWsClient(name='daq', url="ws://xbl-daq-29:8080") daq.wait_for_connection() - - - - - From 8e5ae633d72ff7f1255684c9435373f3a00f1fb0 Mon Sep 17 00:00:00 2001 From: Mohacsi Istvan Date: Thu, 27 Jun 2024 17:14:04 +0200 Subject: [PATCH 11/47] Repository cleanup --- tomcat_bec/devices/gigafrost/gfutils.py | 13 +++-- .../devices/gigafrost/gigafrostclient.py | 50 ++++++++++++------- tomcat_bec/devices/gigafrost/stddaq_ws.py | 18 +++---- 3 files changed, 51 insertions(+), 30 deletions(-) diff --git a/tomcat_bec/devices/gigafrost/gfutils.py b/tomcat_bec/devices/gigafrost/gfutils.py index ab45263..3e6c0b1 100644 --- a/tomcat_bec/devices/gigafrost/gfutils.py +++ b/tomcat_bec/devices/gigafrost/gfutils.py @@ -49,7 +49,8 @@ class GfCamNotFound(NoTraceBackWithLineNumber): def is_valid_url(url): - return(url.startswith('http://')) # FIXME: do more checks? + # FIXME: do more checks? + return(url.startswith('http://')) def is_valid_exposure_ms(e): @@ -114,7 +115,9 @@ def print_max_framerate(exposure_ms=_min_exposure_ms, shape='square'): _print_max_framerate(exposure_ms, roix=x, roiy=_max_roiy) if shape == 'landscape': - for y in valid_roix: # valid_roix is a subset of valid_roiy. Use the smaller set to get a more manageable amount of output lines + # valid_roix is a subset of valid_roiy. + # Use the smaller set to get a more manageable amount of output lines + for y in valid_roix: _print_max_framerate(exposure_ms, roix=_max_roix, roiy=y) @@ -186,7 +189,8 @@ def max_framerate_Hz(exposure_ms=_min_exposure_ms, # 2 line blocks per quarter roiy = max(((roiy + 3) / 4) * 4, PLB_IMG_SENS_COUNT_Y_OFFS * 4) - # print("CC_INTEGRATION_START_DELAY + CC_FRAME_OVERHEAD_TIME",CC_INTEGRATION_START_DELAY + CC_FRAME_OVERHEAD_TIME) + # print("CC_INTEGRATION_START_DELAY + CC_FRAME_OVERHEAD_TIME", + # CC_INTEGRATION_START_DELAY + CC_FRAME_OVERHEAD_TIME) # print("CC_ROW_OVERHEAD_TIME",CC_ROW_OVERHEAD_TIME) # print("CC_TIME_TSYS",CC_TIME_TSYS) @@ -210,7 +214,8 @@ layoutSchema = { "^[a-zA-Z0-9_.-]*$": { "type": "object", - "required":["writer","DaflClient", "zmq_stream", "live_preview", "ioc_name", "description"], + "required":["writer","DaflClient", "zmq_stream", "live_preview", "ioc_name", + "description"], "properties":{ "writer": { "type": "string" diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index b2d86b8..b81754c 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -53,22 +53,35 @@ class GigaFrostClient(Device): cmdSoftExposure = Component(EpicsSignal, "SOFT_EXP", put_complete=True) # Trigger configuration PVs - cfgCntStartBit = Component(EpicsSignal, "CNT_STARTBIT_RBV", write_pv="CNT_STARTBIT", put_complete=True) - cfgCntEndBit = Component(EpicsSignal, "CNT_ENDBIT_RBV", write_pv="CNT_ENDBIT", put_complete=True) + cfgCntStartBit = Component(EpicsSignal, "CNT_STARTBIT_RBV", write_pv="CNT_STARTBIT", + put_complete=True) + cfgCntEndBit = Component(EpicsSignal, "CNT_ENDBIT_RBV", write_pv="CNT_ENDBIT", + put_complete=True) # Enable modes - cfgTrigEnableExt = Component(EpicsSignal, "MODE_ENBL_EXT_RBV", write_pv="MODE_ENBL_EXT", put_complete=True) - cfgTrigEnableSoft = Component(EpicsSignal, "MODE_ENBL_SOFT_RBV", write_pv="MODE_ENBL_SOFT", put_complete=True) - cfgTrigEnableAuto = Component(EpicsSignal, "MODE_ENBL_AUTO_RBV", write_pv="MODE_ENBL_AUTO", put_complete=True) - cfgTrigVirtEnable = Component(EpicsSignal, "MODE_ENBL_EXP_RBV", write_pv="MODE_ENBL_EXP", put_complete=True) + cfgTrigEnableExt = Component(EpicsSignal, "MODE_ENBL_EXT_RBV", write_pv="MODE_ENBL_EXT", + put_complete=True) + cfgTrigEnableSoft = Component(EpicsSignal, "MODE_ENBL_SOFT_RBV", write_pv="MODE_ENBL_SOFT", + put_complete=True) + cfgTrigEnableAuto = Component(EpicsSignal, "MODE_ENBL_AUTO_RBV", write_pv="MODE_ENBL_AUTO", + put_complete=True) + cfgTrigVirtEnable = Component(EpicsSignal, "MODE_ENBL_EXP_RBV", write_pv="MODE_ENBL_EXP", + put_complete=True) # Trigger modes - cfgTrigExt = Component(EpicsSignal, "MODE_TRIG_EXT_RBV", write_pv="MODE_TRIG_EXT", put_complete=True) - cfgTrigSoft = Component(EpicsSignal, "MODE_TRIG_SOFT_RBV", write_pv="MODE_TRIG_SOFT", put_complete=True) - cfgTrigTimer = Component(EpicsSignal, "MODE_TRIG_TIMER_RBV", write_pv="MODE_TRIG_TIMER", put_complete=True) - cfgTrigAuto = Component(EpicsSignal, "MODE_TRIG_AUTO_RBV", write_pv="MODE_TRIG_AUTO", put_complete=True) + cfgTrigExt = Component(EpicsSignal, "MODE_TRIG_EXT_RBV", write_pv="MODE_TRIG_EXT", + put_complete=True) + cfgTrigSoft = Component(EpicsSignal, "MODE_TRIG_SOFT_RBV", write_pv="MODE_TRIG_SOFT", + put_complete=True) + cfgTrigTimer = Component(EpicsSignal, "MODE_TRIG_TIMER_RBV", write_pv="MODE_TRIG_TIMER", + put_complete=True) + cfgTrigAuto = Component(EpicsSignal, "MODE_TRIG_AUTO_RBV", write_pv="MODE_TRIG_AUTO", + put_complete=True) # Exposure modes - cfgTrigExpExt = Component(EpicsSignal, "MODE_EXP_EXT_RBV", write_pv="MODE_EXP_EXT", put_complete=True) - cfgTrigExpSoft = Component(EpicsSignal, "MODE_EXP_SOFT_RBV", write_pv="MODE_EXP_SOFT", put_complete=True) - cfgTrigExpTimer = Component(EpicsSignal, "MODE_EXP_TIMER_RBV", write_pv="MODE_EXP_TIMER", put_complete=True) + cfgTrigExpExt = Component(EpicsSignal, "MODE_EXP_EXT_RBV", write_pv="MODE_EXP_EXT", + put_complete=True) + cfgTrigExpSoft = Component(EpicsSignal, "MODE_EXP_SOFT_RBV", write_pv="MODE_EXP_SOFT", + put_complete=True) + cfgTrigExpTimer = Component(EpicsSignal, "MODE_EXP_TIMER_RBV", write_pv="MODE_EXP_TIMER", + put_complete=True) # Line swap selection cfgLineSwapSW = Component(EpicsSignal, "LS_SW", put_complete=True) @@ -90,9 +103,11 @@ class GigaFrostClient(Device): USER_ACCESS = ["exposure_mode", "fix_nframes_mode", "trigger_mode", "enable_mode"] - def __init__(self, prefix="", *, name, auto_soft_enable=False, timeout=10, backend_url=const.BE999_DAFL_CLIENT, - kind=None, read_attrs=None, configuration_attrs=None, parent=None, **kwargs): - super().__init__(prefix=prefix, name=name, kind=kind, read_attrs=read_attrs, configuration_attrs=configuration_attrs, parent=parent, **kwargs) + def __init__(self, prefix="", *, name, auto_soft_enable=False, timeout=10, + backend_url=const.BE999_DAFL_CLIENT, kind=None, read_attrs=None, + configuration_attrs=None, parent=None, **kwargs): + super().__init__(prefix=prefix, name=name, kind=kind, read_attrs=read_attrs, + configuration_attrs=configuration_attrs, parent=parent, **kwargs) self._auto_soft_enable = auto_soft_enable self._timeout = timeout self._backend_url = backend_url @@ -541,7 +556,8 @@ class GigaFrostClient(Device): # Automatically connect to MicroSAXS testbench if directly invoked if __name__ == "__main__": - gf = GigaFrostClient("X02DA-CAM-GF2:", name="gf2", backend_url="http://xbl-daq-28:8080", auto_soft_enable=True) + gf = GigaFrostClient("X02DA-CAM-GF2:", name="gf2", backend_url="http://xbl-daq-28:8080", + auto_soft_enable=True) gf.wait_for_connection() diff --git a/tomcat_bec/devices/gigafrost/stddaq_ws.py b/tomcat_bec/devices/gigafrost/stddaq_ws.py index d3368d9..8bdff69 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_ws.py +++ b/tomcat_bec/devices/gigafrost/stddaq_ws.py @@ -1,6 +1,6 @@ -from ophyd import Device, Signal, Component, Kind +from ophyd import Device, Signal, Component, Kind import json -from time import sleep +from time import sleep from threading import Thread from websockets.sync.client import connect from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError @@ -24,7 +24,8 @@ class StdDaqWsClient(Device): n_images = Component(Signal, value=10000, kind=Kind.config) file_path = Component(Signal, value="/gpfs/test/test-beamline", kind=Kind.config) - def __init__(self, *args, url: str="ws://localhost:8080", parent: Device = None, **kwargs) -> None: + def __init__(self, *args, url: str="ws://localhost:8080", parent: Device = None, + **kwargs) -> None: super().__init__(*args, parent=parent, **kwargs) self.status._metadata['write_access'] = False self._ws_url = url @@ -83,12 +84,14 @@ class StdDaqWsClient(Device): message = {"command":"start", "path": file_path, "n_image": n_image} reply = self.message(message) - + reply = json.loads(reply) if reply['status'] in ('creating_file'): self.status.put(reply['status'], force=True) - elif reply['status'] in ('rejected'): - raise RuntimeError(f"Start command rejected (might be already running): {reply['reason']}") + elif reply['status'] in ('rejected'): + raise RuntimeError( + f"Start command rejected (might be already running): {reply['reason']}" + ) self.t = Thread(target = self.poll) self.t.start() @@ -112,9 +115,6 @@ class StdDaqWsClient(Device): # The poller thread locks recv raising a RuntimeError self.message(message, wait_reply=False) - def abort(self): - return self.message({"command": "abort"}) - def message(self, d: dict, timeout=1, wait_reply=True): """Send a message to the StdDAQ and receive a reply From 585cad1019633e081ebf7bc395b5047df68e6275 Mon Sep 17 00:00:00 2001 From: Mohacsi Istvan Date: Thu, 27 Jun 2024 17:24:17 +0200 Subject: [PATCH 12/47] Trying blacking --- tomcat_bec/devices/gigafrost/gfconstants.py | 21 +- tomcat_bec/devices/gigafrost/gfutils.py | 120 +++--- .../devices/gigafrost/gigafrostclient.py | 382 +++++++++++------- tomcat_bec/devices/gigafrost/stddaq_ws.py | 34 +- 4 files changed, 330 insertions(+), 227 deletions(-) diff --git a/tomcat_bec/devices/gigafrost/gfconstants.py b/tomcat_bec/devices/gigafrost/gfconstants.py index 13a841d..2d9b0fe 100644 --- a/tomcat_bec/devices/gigafrost/gfconstants.py +++ b/tomcat_bec/devices/gigafrost/gfconstants.py @@ -10,20 +10,21 @@ class GfStatus(Enum): STOPPED = 5 INIT = 6 + # BACKEND ADDRESSES -BE1_DAFL_CLIENT = 'http://xbl-daq-33:8080' -BE1_NORTH_MAC = [0x94, 0x40, 0xc9, 0xb4, 0xb8, 0x00] -BE1_SOUTH_MAC = [0x94, 0x40, 0xc9, 0xb4, 0xa8, 0xd8] +BE1_DAFL_CLIENT = "http://xbl-daq-33:8080" +BE1_NORTH_MAC = [0x94, 0x40, 0xC9, 0xB4, 0xB8, 0x00] +BE1_SOUTH_MAC = [0x94, 0x40, 0xC9, 0xB4, 0xA8, 0xD8] BE1_NORTH_IP = [10, 4, 0, 102] BE1_SOUTH_IP = [10, 0, 0, 102] -BE2_DAFL_CLIENT = 'http://xbl-daq-23:8080' +BE2_DAFL_CLIENT = "http://xbl-daq-23:8080" BE2_NORTH_MAC = [0x24, 0xBE, 0x05, 0xAC, 0x03, 0x62] BE2_SOUTH_MAC = [0x24, 0xBE, 0x05, 0xAC, 0x03, 0x72] BE2_NORTH_IP = [10, 4, 0, 100] BE2_SOUTH_IP = [10, 0, 0, 100] -BE3_DAFL_CLIENT = 'http://xbl-daq-26:8080' -BE3_NORTH_MAC = [0x50, 0x65, 0xf3, 0x81, 0x66, 0x51] -BE3_SOUTH_MAC = [0x50, 0x65, 0xf3, 0x81, 0xd5, 0x31] # ens4 +BE3_DAFL_CLIENT = "http://xbl-daq-26:8080" +BE3_NORTH_MAC = [0x50, 0x65, 0xF3, 0x81, 0x66, 0x51] +BE3_SOUTH_MAC = [0x50, 0x65, 0xF3, 0x81, 0xD5, 0x31] # ens4 BE3_NORTH_IP = [10, 4, 0, 101] BE3_SOUTH_IP = [10, 0, 0, 101] @@ -35,6 +36,6 @@ BE999_NORTH_IP = [10, 4, 0, 101] BE999_SOUTH_IP = [10, 0, 0, 101] # GF Names -GF1 = 'gf1' -GF2 = 'gf2' -GF3 = 'gf3' +GF1 = "gf1" +GF2 = "gf2" +GF3 = "gf3" diff --git a/tomcat_bec/devices/gigafrost/gfutils.py b/tomcat_bec/devices/gigafrost/gfutils.py index 3e6c0b1..889fd40 100644 --- a/tomcat_bec/devices/gigafrost/gfutils.py +++ b/tomcat_bec/devices/gigafrost/gfutils.py @@ -21,13 +21,15 @@ valid_roiy = range(_min_roiy, _max_roiy + 1, _step_roiy) class NoTraceBackWithLineNumber(Exception): def __init__(self, msg): if type(msg).__name__ in ["ConnectionError", "ReadTimeout"]: - print("\n ConnectionError/ReadTimeout: it seems that the server " - "is not running/responding.\n") + print( + "\n ConnectionError/ReadTimeout: it seems that the server " + "is not running/responding.\n" + ) try: ln = sys.exc_info()[-1].tb_lineno except AttributeError: ln = inspect.currentframe().f_back.f_lineno - self.args = "{0.__name__} (line {1}): {2}".format(type(self), ln, msg), + self.args = ("{0.__name__} (line {1}): {2}".format(type(self), ln, msg),) sys.tracebacklimit = None return None @@ -50,7 +52,7 @@ class GfCamNotFound(NoTraceBackWithLineNumber): def is_valid_url(url): # FIXME: do more checks? - return(url.startswith('http://')) + return url.startswith("http://") def is_valid_exposure_ms(e): @@ -62,10 +64,10 @@ def is_valid_exposure_ms(e): def port2byte(port): - return [(port >> 8) & 0xff, port & 0xff] + return [(port >> 8) & 0xFF, port & 0xFF] -def extend_header_table(table, mac, destination_ip, destination_port, - source_port): + +def extend_header_table(table, mac, destination_ip, destination_port, source_port): """ Extend the header table by a further entry. @@ -95,34 +97,34 @@ def is_valid_roi(roiy, roix): def _print_max_framerate(exposure, roix, roiy): - print("roiy=%4i roix=%4i exposure=%6.3fms: %8.1fHz" % - (roiy, roix, exposure, - max_framerate_Hz(exposure, roix=roix, roiy=roiy))) + print( + "roiy=%4i roix=%4i exposure=%6.3fms: %8.1fHz" + % (roiy, roix, exposure, max_framerate_Hz(exposure, roix=roix, roiy=roiy)) + ) -def print_max_framerate(exposure_ms=_min_exposure_ms, shape='square'): +def print_max_framerate(exposure_ms=_min_exposure_ms, shape="square"): - valid_shapes = ['square', 'landscape', 'portrait'] + valid_shapes = ["square", "landscape", "portrait"] if shape not in valid_shapes: raise ValueError("shape must be one of %s" % valid_shapes) - if shape == 'square': + if shape == "square": for r in valid_roix: _print_max_framerate(exposure_ms, r, r) - if shape == 'portrait': + if shape == "portrait": for x in valid_roix: _print_max_framerate(exposure_ms, roix=x, roiy=_max_roiy) - if shape == 'landscape': - # valid_roix is a subset of valid_roiy. + if shape == "landscape": + # valid_roix is a subset of valid_roiy. # Use the smaller set to get a more manageable amount of output lines - for y in valid_roix: + for y in valid_roix: _print_max_framerate(exposure_ms, roix=_max_roix, roiy=y) -def max_framerate_Hz(exposure_ms=_min_exposure_ms, - roix=_max_roix, roiy=_max_roiy, clk_mhz=62.5): +def max_framerate_Hz(exposure_ms=_min_exposure_ms, roix=_max_roix, roiy=_max_roiy, clk_mhz=62.5): """ returns maximum achievable frame rate in auto mode in Hz @@ -151,18 +153,18 @@ def max_framerate_Hz(exposure_ms=_min_exposure_ms, """ if exposure_ms < 0.002 or exposure_ms > 40: - raise ValueError('exposure_ms not in interval [0.002, 40.]') + raise ValueError("exposure_ms not in interval [0.002, 40.]") valid_clock_values = [62.5, 55.0, 52.5, 50.0] if clk_mhz not in valid_clock_values: - raise ValueError('clock rate not in %s' % valid_clock_values) + raise ValueError("clock rate not in %s" % valid_clock_values) # Constants PLB_IMG_SENS_COUNT_X_OFFS = 2 PLB_IMG_SENS_COUNT_Y_OFFS = 1 # Constant Clock Cycles - CC_T2 = 88 # 99 + CC_T2 = 88 # 99 CC_T3 = 125 CC_T4 = 1 CC_T10 = 2 @@ -171,17 +173,24 @@ def max_framerate_Hz(exposure_ms=_min_exposure_ms, CC_T15_MINUS_T10 = 3 CC_TR3 = 1 CC_T13_MINUS_TR3 = 2 - CC_150NS = 7 # additional delay through states + CC_150NS = 7 # additional delay through states CC_DELAY_BEFORE_RESET = 4 # at least 50 ns CC_ADDITIONAL_TSYS = 10 CC_PIX_RESET_LENGTH = 40 # at least 40 CLK Cycles at 62.5 MHz CC_COUNT_X_MAX = 84 - CC_ROW_OVERHEAD_TIME = (11 + CC_TR3 + CC_T13_MINUS_TR3) + CC_ROW_OVERHEAD_TIME = 11 + CC_TR3 + CC_T13_MINUS_TR3 CC_FRAME_OVERHEAD_TIME = ( - 8 + CC_T15_MINUS_T10 + CC_T10 + CC_T2 + CC_T3 + CC_T4 + CC_T5_MINUS_T11 + CC_T11) - CC_INTEGRATION_START_DELAY = CC_COUNT_X_MAX + CC_ROW_OVERHEAD_TIME + \ - CC_DELAY_BEFORE_RESET + CC_PIX_RESET_LENGTH + CC_150NS + 5 + 8 + CC_T15_MINUS_T10 + CC_T10 + CC_T2 + CC_T3 + CC_T4 + CC_T5_MINUS_T11 + CC_T11 + ) + CC_INTEGRATION_START_DELAY = ( + CC_COUNT_X_MAX + + CC_ROW_OVERHEAD_TIME + + CC_DELAY_BEFORE_RESET + + CC_PIX_RESET_LENGTH + + CC_150NS + + 5 + ) CC_TIME_TSYS = CC_FRAME_OVERHEAD_TIME + CC_ADDITIONAL_TSYS # 12 pixel blocks per quarter @@ -194,8 +203,11 @@ def max_framerate_Hz(exposure_ms=_min_exposure_ms, # print("CC_ROW_OVERHEAD_TIME",CC_ROW_OVERHEAD_TIME) # print("CC_TIME_TSYS",CC_TIME_TSYS) - t_readout = (CC_INTEGRATION_START_DELAY + CC_FRAME_OVERHEAD_TIME + - ((roix / 24) + CC_ROW_OVERHEAD_TIME) * (roiy / 4)) / (1e6 * clk_mhz) + t_readout = ( + CC_INTEGRATION_START_DELAY + + CC_FRAME_OVERHEAD_TIME + + ((roix / 24) + CC_ROW_OVERHEAD_TIME) * (roiy / 4) + ) / (1e6 * clk_mhz) t_exp_sys = (exposure_ms / 1000.0) + (CC_TIME_TSYS / (1e6 * clk_mhz)) # with constants as of time of writing: @@ -204,43 +216,37 @@ def max_framerate_Hz(exposure_ms=_min_exposure_ms, framerate = 1.0 / max(t_readout, t_exp_sys) - return (framerate) + return framerate + layoutSchema = { "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", - "patternProperties": - { - "^[a-zA-Z0-9_.-]*$": - { + "patternProperties": { + "^[a-zA-Z0-9_.-]*$": { "type": "object", - "required":["writer","DaflClient", "zmq_stream", "live_preview", "ioc_name", - "description"], - "properties":{ - "writer": { - "type": "string" - }, - "DaflClient": { - "type": "string" - }, - "zmq_stream": { - "type": "string" - }, - "live_preview": { - "type": "string" - }, - "ioc_name": { - "type": "string" - }, - "description": { - "type": "string" - } - } + "required": [ + "writer", + "DaflClient", + "zmq_stream", + "live_preview", + "ioc_name", + "description", + ], + "properties": { + "writer": {"type": "string"}, + "DaflClient": {"type": "string"}, + "zmq_stream": {"type": "string"}, + "live_preview": {"type": "string"}, + "ioc_name": {"type": "string"}, + "description": {"type": "string"}, + }, } }, - "additionalProperties": False + "additionalProperties": False, } + def validateJson(jsonData): try: jsonschema.validate(instance=jsonData, schema=layoutSchema) diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index b81754c..59e8173 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -25,69 +25,133 @@ class GigaFrostClient(Device): Backend url address necessary to set up the camera's udp header. (default: http://xbl-daq-23:8080) """ + infoBusyFlag = Component(EpicsSignalRO, "BUSY_STAT", auto_monitor=True) infoSyncFlag = Component(EpicsSignalRO, "SYNC_FLAG", auto_monitor=True) cmdSyncHw = Component(EpicsSignal, "SYNC_SWHW.PROC", put_complete=True) - cfgConnectionParam = Component(EpicsSignal, "CONN_PARM", string=True, put_complete=True) cmdStartCamera = Component(EpicsSignal, "START_CAM", put_complete=True) cmdSetParam = Component(EpicsSignal, "SET_PARAM.PROC", put_complete=True) - # UDP header - cfgUdpNumPorts = Component(EpicsSignal, "PORTS", put_complete=True) - cfgUdpNumFrames = Component(EpicsSignal, "FRAMENUM", put_complete=True) - cfgUdpHtOffset = Component(EpicsSignal, "HT_OFFSET", put_complete=True) + # UDP header + cfgUdpNumPorts = Component(EpicsSignal, "PORTS", put_complete=True, kind=Kind.config) + cfgUdpNumFrames = Component(EpicsSignal, "FRAMENUM", put_complete=True, kind=Kind.config) + cfgUdpHtOffset = Component(EpicsSignal, "HT_OFFSET", put_complete=True, kind=Kind.config) cmdWriteService = Component(EpicsSignal, "WRITE_SRV.PROC", put_complete=True) # Standard camera configs - cfgExposure = Component(EpicsSignal, "EXPOSURE", put_complete=True) - cfgFramerate = Component(EpicsSignal, "FRAMERATE", put_complete=True) - cfgRoiX = Component(EpicsSignal, "ROIX", put_complete=True) - cfgRoiY = Component(EpicsSignal, "ROIY", put_complete=True) - cfgScanId = Component(EpicsSignal, "SCAN_ID", put_complete=True) - cfgCntNum = Component(EpicsSignal, "CNT_NUM", put_complete=True) - cfgCorrMode = Component(EpicsSignal, "CORR_MODE", put_complete=True) + cfgExposure = Component(EpicsSignal, "EXPOSURE", put_complete=True, kind=Kind.config) + cfgFramerate = Component(EpicsSignal, "FRAMERATE", put_complete=True, kind=Kind.config) + cfgRoiX = Component(EpicsSignal, "ROIX", put_complete=True, kind=Kind.config) + cfgRoiY = Component(EpicsSignal, "ROIY", put_complete=True, kind=Kind.config) + cfgScanId = Component(EpicsSignal, "SCAN_ID", put_complete=True, kind=Kind.config) + cfgCntNum = Component(EpicsSignal, "CNT_NUM", put_complete=True, kind=Kind.config) + cfgCorrMode = Component(EpicsSignal, "CORR_MODE", put_complete=True, kind=Kind.config) # Software signals - cmdSoftEnable = Component(EpicsSignal, "SOFT_ENABLE", put_complete=True) - cmdSoftTrigger = Component(EpicsSignal, "SOFT_TRIG.PROC", put_complete=True) - cmdSoftExposure = Component(EpicsSignal, "SOFT_EXP", put_complete=True) + cmdSoftEnable = Component(EpicsSignal, "SOFT_ENABLE", put_complete=True) + cmdSoftTrigger = Component(EpicsSignal, "SOFT_TRIG.PROC", put_complete=True) + cmdSoftExposure = Component(EpicsSignal, "SOFT_EXP", put_complete=True) # Trigger configuration PVs - cfgCntStartBit = Component(EpicsSignal, "CNT_STARTBIT_RBV", write_pv="CNT_STARTBIT", - put_complete=True) - cfgCntEndBit = Component(EpicsSignal, "CNT_ENDBIT_RBV", write_pv="CNT_ENDBIT", - put_complete=True) + cfgCntStartBit = Component( + EpicsSignal, + "CNT_STARTBIT_RBV", + write_pv="CNT_STARTBIT", + put_complete=True, + kind=Kind.config, + ) + cfgCntEndBit = Component( + EpicsSignal, "CNT_ENDBIT_RBV", write_pv="CNT_ENDBIT", put_complete=True, kind=Kind.config + ) # Enable modes - cfgTrigEnableExt = Component(EpicsSignal, "MODE_ENBL_EXT_RBV", write_pv="MODE_ENBL_EXT", - put_complete=True) - cfgTrigEnableSoft = Component(EpicsSignal, "MODE_ENBL_SOFT_RBV", write_pv="MODE_ENBL_SOFT", - put_complete=True) - cfgTrigEnableAuto = Component(EpicsSignal, "MODE_ENBL_AUTO_RBV", write_pv="MODE_ENBL_AUTO", - put_complete=True) - cfgTrigVirtEnable = Component(EpicsSignal, "MODE_ENBL_EXP_RBV", write_pv="MODE_ENBL_EXP", - put_complete=True) + cfgTrigEnableExt = Component( + EpicsSignal, + "MODE_ENBL_EXT_RBV", + write_pv="MODE_ENBL_EXT", + put_complete=True, + kind=Kind.config, + ) + cfgTrigEnableSoft = Component( + EpicsSignal, + "MODE_ENBL_SOFT_RBV", + write_pv="MODE_ENBL_SOFT", + put_complete=True, + kind=Kind.config, + ) + cfgTrigEnableAuto = Component( + EpicsSignal, + "MODE_ENBL_AUTO_RBV", + write_pv="MODE_ENBL_AUTO", + put_complete=True, + kind=Kind.config, + ) + cfgTrigVirtEnable = Component( + EpicsSignal, + "MODE_ENBL_EXP_RBV", + write_pv="MODE_ENBL_EXP", + put_complete=True, + kind=Kind.config, + ) # Trigger modes - cfgTrigExt = Component(EpicsSignal, "MODE_TRIG_EXT_RBV", write_pv="MODE_TRIG_EXT", - put_complete=True) - cfgTrigSoft = Component(EpicsSignal, "MODE_TRIG_SOFT_RBV", write_pv="MODE_TRIG_SOFT", - put_complete=True) - cfgTrigTimer = Component(EpicsSignal, "MODE_TRIG_TIMER_RBV", write_pv="MODE_TRIG_TIMER", - put_complete=True) - cfgTrigAuto = Component(EpicsSignal, "MODE_TRIG_AUTO_RBV", write_pv="MODE_TRIG_AUTO", - put_complete=True) + cfgTrigExt = Component( + EpicsSignal, + "MODE_TRIG_EXT_RBV", + write_pv="MODE_TRIG_EXT", + put_complete=True, + kind=Kind.config, + ) + cfgTrigSoft = Component( + EpicsSignal, + "MODE_TRIG_SOFT_RBV", + write_pv="MODE_TRIG_SOFT", + put_complete=True, + kind=Kind.config, + ) + cfgTrigTimer = Component( + EpicsSignal, + "MODE_TRIG_TIMER_RBV", + write_pv="MODE_TRIG_TIMER", + put_complete=True, + kind=Kind.config, + ) + cfgTrigAuto = Component( + EpicsSignal, + "MODE_TRIG_AUTO_RBV", + write_pv="MODE_TRIG_AUTO", + put_complete=True, + kind=Kind.config, + ) # Exposure modes - cfgTrigExpExt = Component(EpicsSignal, "MODE_EXP_EXT_RBV", write_pv="MODE_EXP_EXT", - put_complete=True) - cfgTrigExpSoft = Component(EpicsSignal, "MODE_EXP_SOFT_RBV", write_pv="MODE_EXP_SOFT", - put_complete=True) - cfgTrigExpTimer = Component(EpicsSignal, "MODE_EXP_TIMER_RBV", write_pv="MODE_EXP_TIMER", - put_complete=True) + cfgTrigExpExt = Component( + EpicsSignal, + "MODE_EXP_EXT_RBV", + write_pv="MODE_EXP_EXT", + put_complete=True, + kind=Kind.config, + ) + cfgTrigExpSoft = Component( + EpicsSignal, + "MODE_EXP_SOFT_RBV", + write_pv="MODE_EXP_SOFT", + put_complete=True, + kind=Kind.config, + ) + cfgTrigExpTimer = Component( + EpicsSignal, + "MODE_EXP_TIMER_RBV", + write_pv="MODE_EXP_TIMER", + put_complete=True, + kind=Kind.config, + ) # Line swap selection - cfgLineSwapSW = Component(EpicsSignal, "LS_SW", put_complete=True) - cfgLineSwapNW = Component(EpicsSignal, "LS_NW", put_complete=True) - cfgLineSwapSE = Component(EpicsSignal, "LS_SE", put_complete=True) - cfgLineSwapNE = Component(EpicsSignal, "LS_NE", put_complete=True) + cfgLineSwapSW = Component(EpicsSignal, "LS_SW", put_complete=True, kind=Kind.config) + cfgLineSwapNW = Component(EpicsSignal, "LS_NW", put_complete=True, kind=Kind.config) + cfgLineSwapSE = Component(EpicsSignal, "LS_SE", put_complete=True, kind=Kind.config) + cfgLineSwapNE = Component(EpicsSignal, "LS_NE", put_complete=True, kind=Kind.config) + cfgConnectionParam = Component( + EpicsSignal, "CONN_PARM", string=True, put_complete=True, kind=Kind.config + ) # HW settings as read only cfgSyncFlag = Component(EpicsSignalRO, "PIXRATE", auto_monitor=True) @@ -103,11 +167,29 @@ class GigaFrostClient(Device): USER_ACCESS = ["exposure_mode", "fix_nframes_mode", "trigger_mode", "enable_mode"] - def __init__(self, prefix="", *, name, auto_soft_enable=False, timeout=10, - backend_url=const.BE999_DAFL_CLIENT, kind=None, read_attrs=None, - configuration_attrs=None, parent=None, **kwargs): - super().__init__(prefix=prefix, name=name, kind=kind, read_attrs=read_attrs, - configuration_attrs=configuration_attrs, parent=parent, **kwargs) + def __init__( + self, + prefix="", + *, + name, + auto_soft_enable=False, + timeout=10, + backend_url=const.BE999_DAFL_CLIENT, + kind=None, + read_attrs=None, + configuration_attrs=None, + parent=None, + **kwargs, + ): + super().__init__( + prefix=prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + **kwargs, + ) self._auto_soft_enable = auto_soft_enable self._timeout = timeout self._backend_url = backend_url @@ -117,10 +199,10 @@ class GigaFrostClient(Device): self._north_mac, self._south_mac = self._define_backend_mac() self._north_ip, self._south_ip = self._define_backend_ip() - self._valid_enable_modes = ('soft', 'external', 'soft+ext', 'always') - self._valid_exposure_modes = ('external', 'timer', 'soft') - self._valid_trigger_modes = ('auto', 'external', 'timer', 'soft') - self._valid_fix_nframe_modes = ('off', 'start', 'end', 'start+end') + self._valid_enable_modes = ("soft", "external", "soft+ext", "always") + self._valid_exposure_modes = ("external", "timer", "soft") + self._valid_trigger_modes = ("auto", "external", "timer", "soft") + self._valid_fix_nframe_modes = ("off", "start", "end", "start+end") # Continue initialization self.initialize() @@ -143,13 +225,13 @@ class GigaFrostClient(Device): # Configure software triggering if needed if self._auto_soft_enable: # trigger modes - self.cfgCntStartBit.set(1).wait() - self.cfgCntEndBit.set(0).wait() + self.cfgCntStartBit.set(1).wait() + self.cfgCntEndBit.set(0).wait() # set modes - self.enable_mode = 'soft' - self.trigger_mode = 'auto' - self.exposure_mode = 'timer' + self.enable_mode = "soft" + self.trigger_mode = "auto" + self.exposure_mode = "timer" # line swap - on for west, off for east self.cfgLineSwapSW.set(1).wait() @@ -163,14 +245,23 @@ class GigaFrostClient(Device): self._set_udp_header_table() self.state = const.GfStatus.INIT - # sets the basic settings - self._settings = {'backend_url' : self._backend_url, - 'auto_soft_enable' : self._auto_soft_enable, - 'ioc_name' : self.prefix} + # sets the basic settings + self._settings = { + "backend_url": self._backend_url, + "auto_soft_enable": self._auto_soft_enable, + "ioc_name": self.prefix, + } - - def configure(self, nimages=10, exposure=0.2, period=1.0, - roix=2016, roiy=2016, scanid=0, correction_mode=5): + def configure( + self, + nimages=10, + exposure=0.2, + period=1.0, + roix=2016, + roiy=2016, + scanid=0, + correction_mode=5, + ): """Configure the next scan with the GigaFRoST camera Parameters @@ -219,17 +310,18 @@ class GigaFrostClient(Device): self.cmdSetParam.set(1).wait() self.state = const.GfStatus.CONFIGURED - self._settings = {'nimages' : nimages, - 'exposure' : exposure, - 'frame_rate': period, - 'roix' : roix, - 'roiy' : roiy, - 'scanid' : scanid, - 'correction_mode' : correction_mode, - 'backend_url' : self._backend_url, - 'auto_soft_enable' : self._auto_soft_enable, - 'ioc_name' : self.name - } + self._settings = { + "nimages": nimages, + "exposure": exposure, + "frame_rate": period, + "roix": roix, + "roiy": roiy, + "scanid": scanid, + "correction_mode": correction_mode, + "backend_url": self._backend_url, + "auto_soft_enable": self._auto_soft_enable, + "ioc_name": self.name, + } def stage(self): """Standard ophyd method to start an acquisition""" @@ -239,7 +331,7 @@ class GigaFrostClient(Device): if self._auto_soft_enable: self.cmdSoftEnable.set(1).wait() self.state = const.GfStatus.ACQUIRING - + # Gigafrost can finish a run without explicit unstaging self._staged = Staged.no return super().stage() @@ -282,11 +374,11 @@ class GigaFrostClient(Device): mode_timer = self.cfgTrigExpTimer.get() mode_external = self.cfgTrigExpExt.get() if mode_soft and not mode_timer and not mode_external: - return 'soft' + return "soft" elif not mode_soft and mode_timer and not mode_external: - return 'timer' + return "timer" elif not mode_soft and not mode_timer and mode_external: - return 'external' + return "external" else: return None @@ -300,18 +392,19 @@ class GigaFrostClient(Device): The exposure mode to be set. """ if exp_mode not in self._valid_exposure_modes: - raise ValueError("Invalid exposure mode! Valid modes are:\n" - "{}".format(self._valid_exposure_modes)) + raise ValueError( + "Invalid exposure mode! Valid modes are:\n" "{}".format(self._valid_exposure_modes) + ) - if exp_mode == 'external': + if exp_mode == "external": self.cfgTrigExpExt.set(1).wait() self.cfgTrigExpSoft.set(0).wait() self.cfgTrigExpTimer.set(0).wait() - elif exp_mode == 'timer': + elif exp_mode == "timer": self.cfgTrigExpExt.set(0).wait() self.cfgTrigExpSoft.set(0).wait() self.cfgTrigExpTimer.set(1).wait() - elif exp_mode == 'soft': + elif exp_mode == "soft": self.cfgTrigExpExt.set(0).wait() self.cfgTrigExpSoft.set(1).wait() self.cfgTrigExpTimer.set(0).wait() @@ -331,13 +424,13 @@ class GigaFrostClient(Device): end_bit = self.cfgCntStartBit.get() if not start_bit and not end_bit: - return 'off' + return "off" elif start_bit and not end_bit: - return 'start' + return "start" elif not start_bit and end_bit: - return 'end' + return "end" elif start_bit and end_bit: - return 'start+end' + return "start+end" else: return None @@ -351,26 +444,27 @@ class GigaFrostClient(Device): The fixed number of frames mode to be applied. """ if mode not in self._valid_fix_nframe_modes: - raise ValueError("Invalid fixed number of frames mode! " - "Valid modes are:\n{}".format(self._valid_fix_nframe_modes)) + raise ValueError( + "Invalid fixed number of frames mode! " + "Valid modes are:\n{}".format(self._valid_fix_nframe_modes) + ) self._fix_nframes_mode = mode - if self._fix_nframes_mode == 'off': - self.cfgCntStartBit.set(0).wait() - self.cfgCntEndBit.set(0).wait() - elif self._fix_nframes_mode == 'start': - self.cfgCntStartBit.set(1).wait() - self.cfgCntEndBit.set(0).wait() - elif self._fix_nframes_mode == 'end': - self.cfgCntStartBit.set(0).wait() - self.cfgCntEndBit.set(1).wait() - elif self._fix_nframes_mode == 'start+end': - self.cfgCntStartBit.set(1).wait() - self.cfgCntEndBit.set(1).wait() + if self._fix_nframes_mode == "off": + self.cfgCntStartBit.set(0).wait() + self.cfgCntEndBit.set(0).wait() + elif self._fix_nframes_mode == "start": + self.cfgCntStartBit.set(1).wait() + self.cfgCntEndBit.set(0).wait() + elif self._fix_nframes_mode == "end": + self.cfgCntStartBit.set(0).wait() + self.cfgCntEndBit.set(1).wait() + elif self._fix_nframes_mode == "start+end": + self.cfgCntStartBit.set(1).wait() + self.cfgCntEndBit.set(1).wait() # Commit parameters self.cmdSetParam.set(1).wait() - @property def trigger_mode(self): """Method to detect the current trigger mode set in the GigaFRost camera. @@ -386,15 +480,16 @@ class GigaFrostClient(Device): mode_timer = self.cfgTrigTimer.get() mode_soft = self.cfgTrigSoft.get() if mode_auto: - return 'auto' + return "auto" elif mode_soft: - return 'soft' + return "soft" elif mode_timer: - return 'timer' + return "timer" elif mode_external: - return 'external' + return "external" else: return None + @trigger_mode.setter def trigger_mode(self, mode): """Set the trigger mode for the GigaFRoST camera. @@ -405,25 +500,26 @@ class GigaFrostClient(Device): The GigaFRoST trigger mode. """ if mode not in self._valid_trigger_modes: - raise ValueError("Invalid trigger mode! Valid modes are:\n" - "{}".format(self._valid_trigger_modes)) + raise ValueError( + "Invalid trigger mode! Valid modes are:\n" "{}".format(self._valid_trigger_modes) + ) - if mode == 'auto': + if mode == "auto": self.cfgTrigAuto.set(1).wait() self.cfgTrigSoft.set(0).wait() self.cfgTrigTimer.set(0).wait() self.cfgTrigExt.set(0).wait() - elif mode == 'external': + elif mode == "external": self.cfgTrigAuto.set(0).wait() self.cfgTrigSoft.set(0).wait() self.cfgTrigTimer.set(0).wait() self.cfgTrigExt.set(1).wait() - elif mode == 'timer': + elif mode == "timer": self.cfgTrigAuto.set(0).wait() self.cfgTrigSoft.set(0).wait() self.cfgTrigTimer.set(1).wait() self.cfgTrigExt.set(0).wait() - elif mode == 'soft': + elif mode == "soft": self.cfgTrigAuto.set(0).wait() self.cfgTrigSoft.set(1).wait() self.cfgTrigTimer.set(0).wait() @@ -445,13 +541,13 @@ class GigaFrostClient(Device): mode_auto = self.cfgTrigEnableAuto.get() if mode_soft and not mode_auto: if mode_external: - return 'soft+ext' + return "soft+ext" else: - return 'soft' + return "soft" elif mode_auto and not mode_soft and not mode_external: - return 'always' + return "always" elif mode_external and not mode_soft and not mode_auto: - return 'external' + return "external" else: return None @@ -465,23 +561,24 @@ class GigaFrostClient(Device): The enable mode to be applied. """ if mode not in self._valid_enable_modes: - raise ValueError("Invalid enable mode! Valid modes are:\n" - "{}".format(self._valid_enable_modes)) + raise ValueError( + "Invalid enable mode! Valid modes are:\n" "{}".format(self._valid_enable_modes) + ) - if mode == 'soft': - self.cfgTrigEnableExt.set(0).wait() + if mode == "soft": + self.cfgTrigEnableExt.set(0).wait() self.cfgTrigEnableSoft.set(1).wait() self.cfgTrigEnableAuto.set(0).wait() - elif mode == 'external': - self.cfgTrigEnableExt.set(1).wait() + elif mode == "external": + self.cfgTrigEnableExt.set(1).wait() self.cfgTrigEnableSoft.set(0).wait() self.cfgTrigEnableAuto.set(0).wait() - elif mode == 'soft+ext': - self.cfgTrigEnableExt.set(1).wait() + elif mode == "soft+ext": + self.cfgTrigEnableExt.set(1).wait() self.cfgTrigEnableSoft.set(1).wait() self.cfgTrigEnableAuto.set(0).wait() - elif mode == 'always': - self.cfgTrigEnableExt.set(0).wait() + elif mode == "always": + self.cfgTrigEnableExt.set(0).wait() self.cfgTrigEnableSoft.set(0).wait() self.cfgTrigEnableAuto.set(1).wait() # Commit parameters @@ -517,23 +614,23 @@ class GigaFrostClient(Device): """Build the header table for the communication""" udp_header_table = [] - for i in range(0,64,1): - for j in range(0,8,1): + for i in range(0, 64, 1): + for j in range(0, 8, 1): dest_port = 2000 + 8 * i + j - source_port = 3000+j + source_port = 3000 + j if j < 4: extend_header_table( - udp_header_table, self._south_mac, self._south_ip, - dest_port, source_port) + udp_header_table, self._south_mac, self._south_ip, dest_port, source_port + ) else: extend_header_table( - udp_header_table, self._north_mac, self._north_ip, - dest_port, source_port) + udp_header_table, self._north_mac, self._north_ip, dest_port, source_port + ) return udp_header_table def _define_backend_ip(self): - if self._backend_url == const.BE3_DAFL_CLIENT: # xbl-daq-33 + if self._backend_url == const.BE3_DAFL_CLIENT: # xbl-daq-33 return const.BE3_NORTH_IP, const.BE3_SOUTH_IP elif self._backend_url == const.BE999_DAFL_CLIENT: return const.BE999_NORTH_IP, const.BE999_SOUTH_IP @@ -541,7 +638,7 @@ class GigaFrostClient(Device): raise RuntimeError(f"Backend not recognized. {(const.GF1, const.GF2, const.GF3)}") def _define_backend_mac(self): - if self._backend_url == const.BE3_DAFL_CLIENT: # xbl-daq-33 + if self._backend_url == const.BE3_DAFL_CLIENT: # xbl-daq-33 return const.BE3_NORTH_MAC, const.BE3_SOUTH_MAC elif self._backend_url == const.BE999_DAFL_CLIENT: return const.BE999_NORTH_MAC, const.BE999_SOUTH_MAC @@ -553,12 +650,9 @@ class GigaFrostClient(Device): self.cfgConnectionParam.set(self._build_udp_header_table()).wait() - # Automatically connect to MicroSAXS testbench if directly invoked if __name__ == "__main__": - gf = GigaFrostClient("X02DA-CAM-GF2:", name="gf2", backend_url="http://xbl-daq-28:8080", - auto_soft_enable=True) + gf = GigaFrostClient( + "X02DA-CAM-GF2:", name="gf2", backend_url="http://xbl-daq-28:8080", auto_soft_enable=True + ) gf.wait_for_connection() - - - diff --git a/tomcat_bec/devices/gigafrost/stddaq_ws.py b/tomcat_bec/devices/gigafrost/stddaq_ws.py index 8bdff69..db6a4fa 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_ws.py +++ b/tomcat_bec/devices/gigafrost/stddaq_ws.py @@ -19,15 +19,17 @@ class StdDaqWsClient(Device): a REST API to write this file, but since there's no frontend group, this is no longer available. """ + # Status attributes - status = Component(Signal, value='unknown') + status = Component(Signal, value="unknown") n_images = Component(Signal, value=10000, kind=Kind.config) file_path = Component(Signal, value="/gpfs/test/test-beamline", kind=Kind.config) - def __init__(self, *args, url: str="ws://localhost:8080", parent: Device = None, - **kwargs) -> None: + def __init__( + self, *args, url: str = "ws://localhost:8080", parent: Device = None, **kwargs + ) -> None: super().__init__(*args, parent=parent, **kwargs) - self.status._metadata['write_access'] = False + self.status._metadata["write_access"] = False self._ws_url = url # Connect ro the DAQ @@ -41,7 +43,7 @@ class StdDaqWsClient(Device): sleep(5) self._client = connect(self._ws_url) - def configure(self, n_images: int=None, file_path: str=None) -> tuple: + def configure(self, n_images: int = None, file_path: str = None) -> tuple: """Set the standard DAQ parameters for the next run Note that full reconfiguration is not possible with the websocket @@ -82,18 +84,18 @@ class StdDaqWsClient(Device): file_path = self.file_path.get() n_image = self.n_images.get() - message = {"command":"start", "path": file_path, "n_image": n_image} + message = {"command": "start", "path": file_path, "n_image": n_image} reply = self.message(message) reply = json.loads(reply) - if reply['status'] in ('creating_file'): - self.status.put(reply['status'], force=True) - elif reply['status'] in ('rejected'): + if reply["status"] in ("creating_file"): + self.status.put(reply["status"], force=True) + elif reply["status"] in ("rejected"): raise RuntimeError( - f"Start command rejected (might be already running): {reply['reason']}" - ) + f"Start command rejected (might be already running): {reply['reason']}" + ) - self.t = Thread(target = self.poll) + self.t = Thread(target=self.poll) self.t.start() return super().stage() @@ -102,7 +104,7 @@ class StdDaqWsClient(Device): WARN: This will also close the connection!!! """ - message = {"command":"stop"} + message = {"command": "stop"} self.message(message, wait_reply=False) return super().unstage() @@ -111,7 +113,7 @@ class StdDaqWsClient(Device): WARN: This will also close the connection!!! """ - message = {"command":"stop"} + message = {"command": "stop"} # The poller thread locks recv raising a RuntimeError self.message(message, wait_reply=False) @@ -147,7 +149,7 @@ class StdDaqWsClient(Device): for msg in self._client: try: message = json.loads(msg) - self.status.put(message['status'], force=True) + self.status.put(message["status"], force=True) except Exception as ex: print(ex) return @@ -155,5 +157,5 @@ class StdDaqWsClient(Device): # Automatically connect to MicroSAXS testbench if directly invoked if __name__ == "__main__": - daq = StdDaqWsClient(name='daq', url="ws://xbl-daq-29:8080") + daq = StdDaqWsClient(name="daq", url="ws://xbl-daq-29:8080") daq.wait_for_connection() From e978270bd90881c081cde15d680ffaa70143c9f0 Mon Sep 17 00:00:00 2001 From: Mohacsi Istvan Date: Thu, 27 Jun 2024 17:58:31 +0200 Subject: [PATCH 13/47] Blacking is not enough.. --- tomcat_bec/devices/gigafrost/gfconstants.py | 9 +++ tomcat_bec/devices/gigafrost/gfutils.py | 62 ++++++------------- .../devices/gigafrost/gigafrostclient.py | 29 ++++----- .../gigafrost/{Readme.md => readme.md} | 0 tomcat_bec/devices/gigafrost/stddaq_ws.py | 31 ++++++---- 5 files changed, 64 insertions(+), 67 deletions(-) rename tomcat_bec/devices/gigafrost/{Readme.md => readme.md} (100%) diff --git a/tomcat_bec/devices/gigafrost/gfconstants.py b/tomcat_bec/devices/gigafrost/gfconstants.py index 2d9b0fe..536c9df 100644 --- a/tomcat_bec/devices/gigafrost/gfconstants.py +++ b/tomcat_bec/devices/gigafrost/gfconstants.py @@ -1,8 +1,17 @@ +# -*- coding: utf-8 -*- +""" +Copied utility class from gfclient + +Created on Thu Jun 27 17:28:43 2024 + +@author: mohacsi_i +""" from enum import Enum # STATUS class GfStatus(Enum): + """Operation states for GigaFrost Ophyd device""" NEW = 1 INITIALIZED = 2 ACQUIRING = 3 diff --git a/tomcat_bec/devices/gigafrost/gfutils.py b/tomcat_bec/devices/gigafrost/gfutils.py index 889fd40..1ecb4e8 100644 --- a/tomcat_bec/devices/gigafrost/gfutils.py +++ b/tomcat_bec/devices/gigafrost/gfutils.py @@ -1,6 +1,11 @@ -import inspect -import os -import sys +# -*- coding: utf-8 -*- +""" +Copied utility class from gfclient + +Created on Thu Jun 27 17:28:43 2024 + +@author: mohacsi_i +""" import jsonschema _min_exposure_ms = 0.002 @@ -18,52 +23,22 @@ valid_roix = range(_min_roix, _max_roix + 1, _step_roix) valid_roiy = range(_min_roiy, _max_roiy + 1, _step_roiy) -class NoTraceBackWithLineNumber(Exception): - def __init__(self, msg): - if type(msg).__name__ in ["ConnectionError", "ReadTimeout"]: - print( - "\n ConnectionError/ReadTimeout: it seems that the server " - "is not running/responding.\n" - ) - try: - ln = sys.exc_info()[-1].tb_lineno - except AttributeError: - ln = inspect.currentframe().f_back.f_lineno - self.args = ("{0.__name__} (line {1}): {2}".format(type(self), ln, msg),) - sys.tracebacklimit = None - return None - - -class GfError(NoTraceBackWithLineNumber): - pass - - -class GfWarning(NoTraceBackWithLineNumber): - pass - - -class GfNotAValidConfig(NoTraceBackWithLineNumber): - pass - - -class GfCamNotFound(NoTraceBackWithLineNumber): - pass - - def is_valid_url(url): + """Basic URL validation""" # FIXME: do more checks? return url.startswith("http://") -def is_valid_exposure_ms(e): +def is_valid_exposure_ms(exp_t): """check if an exposure time e is valid for gigafrost - e: exposure time in milliseconds + exp_t: exposure time in milliseconds """ - return e >= _min_exposure_ms and e <= _max_exposure_ms + return _min_exposure_ms <= exp_t <= _max_exposure_ms def port2byte(port): + """Post number and endianness conversion""" return [(port >> 8) & 0xFF, port & 0xFF] @@ -75,7 +50,7 @@ def extend_header_table(table, mac, destination_ip, destination_port, source_por ---------- table : The table to be extended - mac : + mac : The mac address for the new entry destination_ip : The destination IP address for the new entry @@ -93,18 +68,20 @@ def extend_header_table(table, mac, destination_ip, destination_port, source_por def is_valid_roi(roiy, roix): + """ Validates ROI on GigaFrost""" return roiy in valid_roiy and roix in valid_roix def _print_max_framerate(exposure, roix, roiy): + """Prints the maximum GigaFrost framerate for a given ROI""" print( "roiy=%4i roix=%4i exposure=%6.3fms: %8.1fHz" - % (roiy, roix, exposure, max_framerate_Hz(exposure, roix=roix, roiy=roiy)) + % (roiy, roix, exposure, max_framerate_hz(exposure, roix=roix, roiy=roiy)) ) def print_max_framerate(exposure_ms=_min_exposure_ms, shape="square"): - + """Prints the maximum GigaFrost framerate for a given exposure time""" valid_shapes = ["square", "landscape", "portrait"] if shape not in valid_shapes: @@ -124,7 +101,7 @@ def print_max_framerate(exposure_ms=_min_exposure_ms, shape="square"): _print_max_framerate(exposure_ms, roix=_max_roix, roiy=y) -def max_framerate_Hz(exposure_ms=_min_exposure_ms, roix=_max_roix, roiy=_max_roiy, clk_mhz=62.5): +def max_framerate_hz(exposure_ms=_min_exposure_ms, roix=_max_roix, roiy=_max_roiy, clk_mhz=62.5): """ returns maximum achievable frame rate in auto mode in Hz @@ -248,6 +225,7 @@ layoutSchema = { def validateJson(jsonData): + """Silently validate a JSON data according to the predefined schema""" try: jsonschema.validate(instance=jsonData, schema=layoutSchema) except jsonschema.exceptions.ValidationError: diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index 59e8173..d33a5dc 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -1,20 +1,27 @@ -from ophyd import Device, Component, EpicsSignal, EpicsSignalRO, Kind, DerivedSignal -from ophyd.utils import RedundantStaging +# -*- coding: utf-8 -*- +""" +GigaFrost camera class module + +Created on Thu Jun 27 17:28:43 2024 + +@author: mohacsi_i +""" +from ophyd import Device, Component, EpicsSignal, EpicsSignalRO, Kind from ophyd.device import Staged import gfconstants as const -from gfutils import extend_header_table, port2byte +from gfutils import extend_header_table class GigaFrostClient(Device): """Ophyd device class to control Gigafrost cameras at Tomcat The actual hardware is implemented by an IOC based on an old fork of Helge's - cameras. This means that the camera behaves differently than the SF cameras - in particular it provides even less feedback about it's internal progress. + cameras. This means that the camera behaves differently than the SF cameras + in particular it provides even less feedback about it's internal progress. Helge will update the GigaFrost IOC after working beamline. - The ophyd class is based on the 'gfclient' package and has a lot of Tomcat - specific additions. It does behave differently though, as ophyd swallows the + The ophyd class is based on the 'gfclient' package and has a lot of Tomcat + specific additions. It does behave differently though, as ophyd swallows the errors from failed PV writes. Parameters @@ -25,6 +32,7 @@ class GigaFrostClient(Device): Backend url address necessary to set up the camera's udp header. (default: http://xbl-daq-23:8080) """ + # pylint: disable=too-many-instance-attributes infoBusyFlag = Component(EpicsSignalRO, "BUSY_STAT", auto_monitor=True) infoSyncFlag = Component(EpicsSignalRO, "SYNC_FLAG", auto_monitor=True) @@ -353,13 +361,6 @@ class GigaFrostClient(Device): """Sends a software trigger""" self.cmdSoftTrigger.set(1).wait() - def reset(self): - try: - self.unstage() - except: - pass - self.state = const.GfStatus.INIT - @property def exposure_mode(self): """Returns the current exposure mode of the GigaFRost camera. diff --git a/tomcat_bec/devices/gigafrost/Readme.md b/tomcat_bec/devices/gigafrost/readme.md similarity index 100% rename from tomcat_bec/devices/gigafrost/Readme.md rename to tomcat_bec/devices/gigafrost/readme.md diff --git a/tomcat_bec/devices/gigafrost/stddaq_ws.py b/tomcat_bec/devices/gigafrost/stddaq_ws.py index db6a4fa..19474d2 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_ws.py +++ b/tomcat_bec/devices/gigafrost/stddaq_ws.py @@ -1,7 +1,15 @@ -from ophyd import Device, Signal, Component, Kind +# -*- coding: utf-8 -*- +""" +Standard DAQ class module + +Created on Thu Jun 27 17:28:43 2024 + +@author: mohacsi_i +""" import json from time import sleep from threading import Thread +from ophyd import Device, Signal, Component, Kind from websockets.sync.client import connect from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError @@ -13,12 +21,13 @@ class StdDaqWsClient(Device): or change the current configuration through this interface. A bit more about the Standard DAQ configuration: - - The standard DAQ configuration is a single JSON file locally autodeployed + + The standard DAQ configuration is a single JSON file locally autodeployed to the DAQ servers (as root!!!). Previously there was a service to offer - a REST API to write this file, but since there's no frontend group, this + a REST API to write this file, but since there's no frontend group, this is no longer available. """ + # pylint: disable=too-many-instance-attributes # Status attributes status = Component(Signal, value="unknown") @@ -45,8 +54,8 @@ class StdDaqWsClient(Device): def configure(self, n_images: int = None, file_path: str = None) -> tuple: """Set the standard DAQ parameters for the next run - - Note that full reconfiguration is not possible with the websocket + + Note that full reconfiguration is not possible with the websocket interface, only changing acquisition parameters. These changes are only activated upon staging! @@ -77,8 +86,8 @@ class StdDaqWsClient(Device): def stage(self) -> list: """Start a new run with the standard DAQ - Behavior: the StdDAQ can stop the previous run either by itself or - by calling unstage. So it might start from an already running state or + Behavior: the StdDAQ can stop the previous run either by itself or + by calling unstage. So it might start from an already running state or not, we can't query if not running. """ file_path = self.file_path.get() @@ -110,7 +119,7 @@ class StdDaqWsClient(Device): def stop(self, *, success=False): """ Stop a running acquisition - + WARN: This will also close the connection!!! """ message = {"command": "stop"} @@ -119,8 +128,8 @@ class StdDaqWsClient(Device): def message(self, d: dict, timeout=1, wait_reply=True): """Send a message to the StdDAQ and receive a reply - - Note: finishing acquisition meang StdDAQ will close connections so + + Note: finishing acquisition meang StdDAQ will close connections so there's no idle state polling. """ if isinstance(d, dict): From 7b865f0ee19fc21464d574b7f273c1c08b8b2d7a Mon Sep 17 00:00:00 2001 From: Mohacsi Istvan Date: Fri, 28 Jun 2024 13:34:25 +0200 Subject: [PATCH 14/47] Lot of code quality improvements --- tomcat_bec/devices/gigafrost/gfutils.py | 16 ++++++------- .../devices/gigafrost/gigafrostclient.py | 13 +++++++---- tomcat_bec/devices/gigafrost/readme.md | 5 +++- tomcat_bec/devices/gigafrost/stddaq_ws.py | 23 +++++++++++-------- 4 files changed, 35 insertions(+), 22 deletions(-) diff --git a/tomcat_bec/devices/gigafrost/gfutils.py b/tomcat_bec/devices/gigafrost/gfutils.py index 1ecb4e8..b905aa9 100644 --- a/tomcat_bec/devices/gigafrost/gfutils.py +++ b/tomcat_bec/devices/gigafrost/gfutils.py @@ -87,18 +87,18 @@ def print_max_framerate(exposure_ms=_min_exposure_ms, shape="square"): if shape not in valid_shapes: raise ValueError("shape must be one of %s" % valid_shapes) if shape == "square": - for r in valid_roix: - _print_max_framerate(exposure_ms, r, r) + for px_r in valid_roix: + _print_max_framerate(exposure_ms, px_r, px_r) if shape == "portrait": - for x in valid_roix: - _print_max_framerate(exposure_ms, roix=x, roiy=_max_roiy) + for px_x in valid_roix: + _print_max_framerate(exposure_ms, roix=px_x, roiy=_max_roiy) if shape == "landscape": # valid_roix is a subset of valid_roiy. # Use the smaller set to get a more manageable amount of output lines - for y in valid_roix: - _print_max_framerate(exposure_ms, roix=_max_roix, roiy=y) + for px_y in valid_roix: + _print_max_framerate(exposure_ms, roix=_max_roix, roiy=px_y) def max_framerate_hz(exposure_ms=_min_exposure_ms, roix=_max_roix, roiy=_max_roiy, clk_mhz=62.5): @@ -224,10 +224,10 @@ layoutSchema = { } -def validateJson(jsonData): +def validate_json(json_data): """Silently validate a JSON data according to the predefined schema""" try: - jsonschema.validate(instance=jsonData, schema=layoutSchema) + jsonschema.validate(instance=json_data, schema=layoutSchema) except jsonschema.exceptions.ValidationError: return False return True diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index d33a5dc..122d715 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -31,6 +31,10 @@ class GigaFrostClient(Device): backend_url : str Backend url address necessary to set up the camera's udp header. (default: http://xbl-daq-23:8080) + + Bugs: + ---------- + FRAMERATE : Ignored in soft trigger mode, period becomes 2xexposure time """ # pylint: disable=too-many-instance-attributes @@ -281,7 +285,7 @@ class GigaFrostClient(Device): exposure : float, optional Exposure time [ms]. (default = 0.2) period : float, optional - Exposure period [ms]. (default = 1.0) + Exposure period [ms], ignored in soft trigger mode. (default = 1.0) roix : int, optional ROI size in the x-direction [pixels] (default = 2016) roiy : int, optional @@ -394,7 +398,7 @@ class GigaFrostClient(Device): """ if exp_mode not in self._valid_exposure_modes: raise ValueError( - "Invalid exposure mode! Valid modes are:\n" "{}".format(self._valid_exposure_modes) + f"Invalid exposure mode! Valid modes are:\n" "{self._valid_exposure_modes}" ) if exp_mode == "external": @@ -502,7 +506,7 @@ class GigaFrostClient(Device): """ if mode not in self._valid_trigger_modes: raise ValueError( - "Invalid trigger mode! Valid modes are:\n" "{}".format(self._valid_trigger_modes) + "Invalid trigger mode! Valid modes are:\n" "{self._valid_trigger_modes}" ) if mode == "auto": @@ -563,7 +567,7 @@ class GigaFrostClient(Device): """ if mode not in self._valid_enable_modes: raise ValueError( - "Invalid enable mode! Valid modes are:\n" "{}".format(self._valid_enable_modes) + "Invalid enable mode! Valid modes are:\n" "{self._valid_enable_modes}" ) if mode == "soft": @@ -601,6 +605,7 @@ class GigaFrostClient(Device): return self._south_ip def get_backend_url(self): + """Method to read the configured backend URL""" return self._backend_url def set_backend_ip(self, north, south): diff --git a/tomcat_bec/devices/gigafrost/readme.md b/tomcat_bec/devices/gigafrost/readme.md index 0a5e996..6dc4982 100644 --- a/tomcat_bec/devices/gigafrost/readme.md +++ b/tomcat_bec/devices/gigafrost/readme.md @@ -1,9 +1,12 @@ +# Gifgafrost camera at Tomcat +The GigaFrost camera IOC is a form from an ancient version of Helge's cameras. +As we're commissioning, the current folder also contains the standard DAQ client. # Opening GigaFrost panel - +The CaQtDM panel can be opened by: ''' caqtdm -macro "CAM=X02DA-CAM-GF2" X_X02DA_GIGAFROST_camControl_user.ui & ''' diff --git a/tomcat_bec/devices/gigafrost/stddaq_ws.py b/tomcat_bec/devices/gigafrost/stddaq_ws.py index 19474d2..a2f12a0 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_ws.py +++ b/tomcat_bec/devices/gigafrost/stddaq_ws.py @@ -40,12 +40,17 @@ class StdDaqWsClient(Device): super().__init__(*args, parent=parent, **kwargs) self.status._metadata["write_access"] = False self._ws_url = url + self._mon = None # Connect ro the DAQ self.connect() def connect(self): - # StdDAQ may reject connection for a few seconds + """Connect to te StDAQs websockets interface + + StdDAQ may reject connection for a few seconds, so if it fails, wait + a bit and try to connect again. + """ try: self._client = connect(self._ws_url) except ConnectionRefusedError: @@ -104,8 +109,8 @@ class StdDaqWsClient(Device): f"Start command rejected (might be already running): {reply['reason']}" ) - self.t = Thread(target=self.poll) - self.t.start() + self._mon = Thread(target=self.poll) + self._mon.start() return super().stage() def unstage(self): @@ -114,10 +119,10 @@ class StdDaqWsClient(Device): WARN: This will also close the connection!!! """ message = {"command": "stop"} - self.message(message, wait_reply=False) + _ = self.message(message, wait_reply=False) return super().unstage() - def stop(self, *, success=False): + def stop(self): """ Stop a running acquisition WARN: This will also close the connection!!! @@ -126,16 +131,16 @@ class StdDaqWsClient(Device): # The poller thread locks recv raising a RuntimeError self.message(message, wait_reply=False) - def message(self, d: dict, timeout=1, wait_reply=True): + def message(self, message: dict, timeout=1, wait_reply=True): """Send a message to the StdDAQ and receive a reply Note: finishing acquisition meang StdDAQ will close connections so there's no idle state polling. """ - if isinstance(d, dict): - msg = json.dumps(d) + if isinstance(message, dict): + msg = json.dumps(message) else: - msg = str(d) + msg = str(message) # Send message (reopen connection if needed) try: From 8c9e0654052dd93431fb6472a63edc999f9baf32 Mon Sep 17 00:00:00 2001 From: Mohacsi Istvan Date: Mon, 1 Jul 2024 16:08:24 +0200 Subject: [PATCH 15/47] Added DAQ preview class --- .../devices/gigafrost/gigafrostclient.py | 3 + .../devices/gigafrost/stddaq_preview.py | 155 ++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 tomcat_bec/devices/gigafrost/stddaq_preview.py diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index 122d715..fb97f9c 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -337,6 +337,9 @@ class GigaFrostClient(Device): def stage(self): """Standard ophyd method to start an acquisition""" + if self.infoBusyFlag.value: + raise RuntimeError("Camera is already busy, unstage it first!") + # change to running self.cmdStartCamera.set(1).wait() # soft trigger on diff --git a/tomcat_bec/devices/gigafrost/stddaq_preview.py b/tomcat_bec/devices/gigafrost/stddaq_preview.py new file mode 100644 index 0000000..f879cb9 --- /dev/null +++ b/tomcat_bec/devices/gigafrost/stddaq_preview.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +""" +Standard DAQ class module + +Created on Thu Jun 27 17:28:43 2024 + +@author: mohacsi_i +""" +import json +from time import sleep, time +from threading import Thread +import numpy as np +import zmq +import matplotlib.pyplot as plt +from ophyd import Device, Signal, Component, Kind + +TOPIC_FILTER = '' + + +class StdDaqPreview(Device): + """Wrapper class around the StdDaq preview image stream. + + This was meant to provide live image preview directly from the StdDAQ. + Note that the preview stream must be heavily throtled in order to cope + with the incoming data. + """ + # pylint: disable=too-many-instance-attributes + + # Status attributes + status = Component(Signal, value="detached") + image = Component(Signal, kind=Kind.hinted) + frame = Component(Signal, kind=Kind.hinted) + shape = Component(Signal, kind=Kind.hinted) + _throttle = 0.5 + + def __init__( + self, *args, url: str = "tcp://129.129.95.38:20000", parent: Device = None, **kwargs + ) -> None: + super().__init__(*args, parent=parent, **kwargs) + self.status._metadata["write_access"] = False + self.image._metadata["write_access"] = False + self.frame._metadata["write_access"] = False + self.shape._metadata["write_access"] = False + self._stream_url = url + self._stop_polling = False + self._mon = None + + # Connect ro the DAQ + self.connect() + + def connect(self): + """Connect to te StDAQs PUB-SUB streaming interface + + StdDAQ may reject connection for a few seconds when it restarts, + so if it fails, wait a bit and try to connect again. + """ + # pylint: disable=no-member + # Socket to talk to server + context = zmq.Context() + self._socket = context.socket(zmq.SUB) + self._socket.setsockopt(zmq.SUBSCRIBE, b'') + try: + self._socket.connect(self._stream_url) + except ConnectionRefusedError: + sleep(5) + self._socket.connect(self._stream_url) + + def configure(self, throttle: float = 0.5) -> tuple: + """Set the DAQ preview parameters + + Note that there's not much to do except for additional throtling if the + preview data stream is too fast. + + Example: + ---------- + std.configure(throttle=0.5) + + Parameters + ---------- + throttle : float, optional + Additional throtling for the ophyd device. (default = 0.5 sec) + """ + self._throttle = throttle + + def stage(self) -> list: + """Start listening for preview data stream""" + self._stop_polling = False + self._mon = Thread(target=self.poll) + self._mon.start() + return super().stage() + + def unstage(self): + """Stop a running preview""" + self._stop_polling = True + return super().unstage() + + def stop(self): + """Stop a running preview""" + self.unstage() + + def plot(self): + """Plot the current image""" + image = self.image.get() + plt.imshow(np.log10(image+1), vmin=0, vmax=5) + plt.pause(self._throttle) + + def plot_loop(self): + """Blocking loop to keep plotting""" + while True: + self.plot() + + def poll(self): + """Collect streamed updates""" + self.status.set("attached", force=True) + t_last = time() + while True: + try: + # pylint: disable=no-member + meta, data = self._socket.recv_multipart(flags=zmq.NOBLOCK) + header = json.loads(meta) + if header["type"]=="uint16": + image = np.frombuffer(data, dtype=np.uint16) + if image.size != np.prod(header['shape']): + self.status.set("detached", force=True) + raise ValueError(f"Unexpected array size of {image.size} for header: {header}") + image = image.reshape(header['shape']) + + t_curr = time() + t_elapsed = t_curr - t_last + if t_elapsed > self._throttle: + self.frame.put(header['frame'], force=True) + self.shape.put(header['shape'], force=True) + self.image.put(image, force=True) + t_last=t_curr + #print(header) + print(f"Frame: {header['frame']}\tMin: {np.min(image)}\tMax: {np.max(image)}") + + if self._stop_polling: + self.status.set("detached", force=True) + print("Detaching monitor") + break + except zmq.error.Again: + sleep(0.1) + except Exception as ex: + print(ex) + self.status.set("detached", force=True) + raise + finally: + pass + + +# Automatically connect to MicroSAXS testbench if directly invoked +if __name__ == "__main__": + daq = StdDaqPreview(url="tcp://129.129.95.38:20000", name="preview") + daq.wait_for_connection() From 12401c0d127f3691dceeef9cf86f762fd76cb089 Mon Sep 17 00:00:00 2001 From: Mohacsi Istvan Date: Mon, 1 Jul 2024 16:16:27 +0200 Subject: [PATCH 16/47] Added DAQ preview class --- tomcat_bec/devices/gigafrost/gfutils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tomcat_bec/devices/gigafrost/gfutils.py b/tomcat_bec/devices/gigafrost/gfutils.py index b905aa9..432eb13 100644 --- a/tomcat_bec/devices/gigafrost/gfutils.py +++ b/tomcat_bec/devices/gigafrost/gfutils.py @@ -129,6 +129,8 @@ def max_framerate_hz(exposure_ms=_min_exposure_ms, roix=_max_roix, roiy=_max_roi Gerd """ + # pylint: disable=invalid-name + # pylint: disable=too-many-locals if exposure_ms < 0.002 or exposure_ms > 40: raise ValueError("exposure_ms not in interval [0.002, 40.]") From f099e6a1814c368b0aacecf1516a3e02d8e74108 Mon Sep 17 00:00:00 2001 From: Mohacsi Istvan Date: Mon, 1 Jul 2024 16:18:45 +0200 Subject: [PATCH 17/47] Linting up to 9.5 cq --- tomcat_bec/devices/gigafrost/gfutils.py | 30 ++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tomcat_bec/devices/gigafrost/gfutils.py b/tomcat_bec/devices/gigafrost/gfutils.py index 432eb13..2975a59 100644 --- a/tomcat_bec/devices/gigafrost/gfutils.py +++ b/tomcat_bec/devices/gigafrost/gfutils.py @@ -8,19 +8,19 @@ Created on Thu Jun 27 17:28:43 2024 """ import jsonschema -_min_exposure_ms = 0.002 -_max_exposure_ms = 40 +MIN_EXPOSURE_MS = 0.002 +MAX_EXPOSURE_MS = 40 -_min_roix = 48 -_max_roix = 2016 -_step_roix = 48 +MIN_ROIX = 48 +MAX_ROIX = 2016 +STEP_ROIX = 48 -_min_roiy = 4 -_max_roiy = 2016 -_step_roiy = 4 +MIN_ROIY = 4 +MAX_ROIY = 2016 +STEP_ROIY = 4 -valid_roix = range(_min_roix, _max_roix + 1, _step_roix) -valid_roiy = range(_min_roiy, _max_roiy + 1, _step_roiy) +valid_roix = range(MIN_ROIX, MAX_ROIX + 1, STEP_ROIX) +valid_roiy = range(MIN_ROIY, MAX_ROIY + 1, STEP_ROIY) def is_valid_url(url): @@ -34,7 +34,7 @@ def is_valid_exposure_ms(exp_t): exp_t: exposure time in milliseconds """ - return _min_exposure_ms <= exp_t <= _max_exposure_ms + return MIN_EXPOSURE_MS <= exp_t <= MAX_EXPOSURE_MS def port2byte(port): @@ -80,7 +80,7 @@ def _print_max_framerate(exposure, roix, roiy): ) -def print_max_framerate(exposure_ms=_min_exposure_ms, shape="square"): +def print_max_framerate(exposure_ms=MIN_EXPOSURE_MS, shape="square"): """Prints the maximum GigaFrost framerate for a given exposure time""" valid_shapes = ["square", "landscape", "portrait"] @@ -92,16 +92,16 @@ def print_max_framerate(exposure_ms=_min_exposure_ms, shape="square"): if shape == "portrait": for px_x in valid_roix: - _print_max_framerate(exposure_ms, roix=px_x, roiy=_max_roiy) + _print_max_framerate(exposure_ms, roix=px_x, roiy=MAX_ROIY) if shape == "landscape": # valid_roix is a subset of valid_roiy. # Use the smaller set to get a more manageable amount of output lines for px_y in valid_roix: - _print_max_framerate(exposure_ms, roix=_max_roix, roiy=px_y) + _print_max_framerate(exposure_ms, roix=MAX_ROIX, roiy=px_y) -def max_framerate_hz(exposure_ms=_min_exposure_ms, roix=_max_roix, roiy=_max_roiy, clk_mhz=62.5): +def max_framerate_hz(exposure_ms=MIN_EXPOSURE_MS, roix=MAX_ROIX, roiy=MAX_ROIY, clk_mhz=62.5): """ returns maximum achievable frame rate in auto mode in Hz From 0668e3b7a276d92adbfb88074f2656338c99d513 Mon Sep 17 00:00:00 2001 From: Mohacsi Istvan Date: Wed, 3 Jul 2024 13:56:14 +0200 Subject: [PATCH 18/47] Added DAQ to YAML file --- .../device_configs/microxas_test_bed.yaml | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/tomcat_bec/device_configs/microxas_test_bed.yaml b/tomcat_bec/device_configs/microxas_test_bed.yaml index 932391f..7470aa7 100644 --- a/tomcat_bec/device_configs/microxas_test_bed.yaml +++ b/tomcat_bec/device_configs/microxas_test_bed.yaml @@ -52,7 +52,7 @@ es1_roty: deviceClass: tomcat_bec.devices.EpicsMotorX deviceConfig: {prefix: 'X02DA-ES1-SMP1:ROTY'} onFailure: buffer - enabled: true + enabled: false readoutPriority: monitored # es1_tasks: @@ -92,3 +92,56 @@ camera: readoutPriority: monitored softwareTrigger: true +gf2: + description: GigaFrost camera controls + deviceClass: tomcat_bec.devices.gigafrost.gigafrostclient.GigaFrostClient + deviceConfig: + prefix: 'X02DA-CAM-GF2:' + backend_url: 'http://xbl-daq-28:8080' + auto_soft_enable: true + deviceTags: + - camera + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: monitored + softwareTrigger: true +daq: + description: Standard DAQ controls + deviceClass: tomcat_bec.devices.gigafrost.stddaq_ws.StdDaqWsClient + deviceConfig: + url: 'http://xbl-daq-29:8080' + deviceTags: + - std-daq + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: monitored + softwareTrigger: false + +daq_stream0: + description: Standard DAQ controls + deviceClass: tomcat_bec.devices.gigafrost.stddaq_preview.StdDaqPreview + deviceConfig: + url: 'tcp://129.129.95.38:20000' + deviceTags: + - std-daq + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: monitored + softwareTrigger: false + +daq_stream1: + description: Standard DAQ controls + deviceClass: tomcat_bec.devices.gigafrost.stddaq_preview.StdDaqPreview + deviceConfig: + url: 'tcp://129.129.95.38:20001' + deviceTags: + - std-daq + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: monitored + softwareTrigger: false + From 00d1d12b4b1ae8762adfd1520be333db6ff8de5f Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Wed, 3 Jul 2024 15:40:52 +0200 Subject: [PATCH 19/47] Small changes to get the config running --- .../device_configs/microxas_test_bed.yaml | 40 +++++++++---------- tomcat_bec/devices/gigafrost/gfutils.py | 11 ++--- .../devices/gigafrost/gigafrostclient.py | 11 ++++- .../devices/gigafrost/stddaq_preview.py | 6 +-- 4 files changed, 38 insertions(+), 30 deletions(-) diff --git a/tomcat_bec/device_configs/microxas_test_bed.yaml b/tomcat_bec/device_configs/microxas_test_bed.yaml index 7470aa7..8b32ecc 100644 --- a/tomcat_bec/device_configs/microxas_test_bed.yaml +++ b/tomcat_bec/device_configs/microxas_test_bed.yaml @@ -47,13 +47,13 @@ femto_mean_curr: enabled: true readOnly: true softwareTrigger: false -es1_roty: - description: 'Test rotation stage' - deviceClass: tomcat_bec.devices.EpicsMotorX - deviceConfig: {prefix: 'X02DA-ES1-SMP1:ROTY'} - onFailure: buffer - enabled: false - readoutPriority: monitored +#es1_roty: +# description: 'Test rotation stage' +# deviceClass: tomcat_bec.devices.EpicsMotorX +# deviceConfig: {prefix: 'X02DA-ES1-SMP1:ROTY'} +# onFailure: buffer +# enabled: false +# readoutPriority: monitored # es1_tasks: # description: 'AA1 task management interface' @@ -79,18 +79,18 @@ es1_roty: # enabled: true # readoutPriority: monitored -camera: - description: Grashopper Camera - deviceClass: tomcat_bec.devices.GrashopperTOMCAT - deviceConfig: - prefix: 'X02DA-PG-USB:' - deviceTags: - - camera - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: monitored - softwareTrigger: true +#camera: +# description: Grashopper Camera +# deviceClass: tomcat_bec.devices.GrashopperTOMCAT +# deviceConfig: +# prefix: 'X02DA-PG-USB:' +# deviceTags: +# - camera +# enabled: true +# onFailure: buffer +# readOnly: false +# readoutPriority: monitored +# softwareTrigger: true gf2: description: GigaFrost camera controls @@ -110,7 +110,7 @@ daq: description: Standard DAQ controls deviceClass: tomcat_bec.devices.gigafrost.stddaq_ws.StdDaqWsClient deviceConfig: - url: 'http://xbl-daq-29:8080' + url: 'ws://xbl-daq-29:8080' deviceTags: - std-daq enabled: true diff --git a/tomcat_bec/devices/gigafrost/gfutils.py b/tomcat_bec/devices/gigafrost/gfutils.py index 2975a59..bffd8a9 100644 --- a/tomcat_bec/devices/gigafrost/gfutils.py +++ b/tomcat_bec/devices/gigafrost/gfutils.py @@ -6,7 +6,8 @@ Created on Thu Jun 27 17:28:43 2024 @author: mohacsi_i """ -import jsonschema +# Not installed on BEC server +#import jsonschema MIN_EXPOSURE_MS = 0.002 MAX_EXPOSURE_MS = 40 @@ -228,8 +229,8 @@ layoutSchema = { def validate_json(json_data): """Silently validate a JSON data according to the predefined schema""" - try: - jsonschema.validate(instance=json_data, schema=layoutSchema) - except jsonschema.exceptions.ValidationError: - return False + #try: + # jsonschema.validate(instance=json_data, schema=layoutSchema) + #except jsonschema.exceptions.ValidationError: + # return False return True diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index fb97f9c..075e049 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -9,8 +9,15 @@ Created on Thu Jun 27 17:28:43 2024 from ophyd import Device, Component, EpicsSignal, EpicsSignalRO, Kind from ophyd.device import Staged -import gfconstants as const -from gfutils import extend_header_table +try: + import gfconstants as const +except ModuleNotFoundError: + import tomcat_bec.devices.gigafrost.gfconstants as const + +try: + from gfutils import extend_header_table +except ModuleNotFoundError: + from tomcat_bec.devices.gigafrost.gfutils import extend_header_table class GigaFrostClient(Device): diff --git a/tomcat_bec/devices/gigafrost/stddaq_preview.py b/tomcat_bec/devices/gigafrost/stddaq_preview.py index f879cb9..5cdf7c5 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_preview.py +++ b/tomcat_bec/devices/gigafrost/stddaq_preview.py @@ -11,7 +11,7 @@ from time import sleep, time from threading import Thread import numpy as np import zmq -import matplotlib.pyplot as plt +#import matplotlib.pyplot as plt from ophyd import Device, Signal, Component, Kind TOPIC_FILTER = '' @@ -101,8 +101,8 @@ class StdDaqPreview(Device): def plot(self): """Plot the current image""" image = self.image.get() - plt.imshow(np.log10(image+1), vmin=0, vmax=5) - plt.pause(self._throttle) + #plt.imshow(np.log10(image+1), vmin=0, vmax=5) + #plt.pause(self._throttle) def plot_loop(self): """Blocking loop to keep plotting""" From 9babe27680ce68805394bb3ad8d9a109ca73e682 Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Wed, 3 Jul 2024 16:55:41 +0200 Subject: [PATCH 20/47] Software stepscan with GF --- .../devices/gigafrost/gigafrostclient.py | 17 +++++++++++++--- .../devices/gigafrost/stddaq_preview.py | 20 +++++++++++-------- tomcat_bec/devices/gigafrost/stddaq_ws.py | 2 +- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index 075e049..b2c0a0a 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -6,7 +6,8 @@ Created on Thu Jun 27 17:28:43 2024 @author: mohacsi_i """ -from ophyd import Device, Component, EpicsSignal, EpicsSignalRO, Kind +from time import sleep +from ophyd import Device, Component, EpicsSignal, EpicsSignalRO, Kind, DeviceStatus from ophyd.device import Staged try: @@ -371,9 +372,19 @@ class GigaFrostClient(Device): """Standard ophyd method to stop an acquisition""" self.unstage() - def trigger(self): + def trigger(self) -> DeviceStatus: """Sends a software trigger""" - self.cmdSoftTrigger.set(1).wait() + # Soft triggering based on operation mode + if self._auto_soft_enable: + # BEC teststand operation mode: poedge of SoftEnable if Started + self.cmdSoftEnable.set(0).wait() + self.cmdSoftEnable.set(1).wait() + else: + self.cmdSoftTrigger.set(1).wait() + status = DeviceStatus(self, settle_time=2.0) + status.set_finished() + sleep(2.0) + return status @property def exposure_mode(self): diff --git a/tomcat_bec/devices/gigafrost/stddaq_preview.py b/tomcat_bec/devices/gigafrost/stddaq_preview.py index 5cdf7c5..7552f56 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_preview.py +++ b/tomcat_bec/devices/gigafrost/stddaq_preview.py @@ -27,11 +27,11 @@ class StdDaqPreview(Device): # pylint: disable=too-many-instance-attributes # Status attributes - status = Component(Signal, value="detached") + status = Component(Signal, value="detached", kind=Kind.omitted) image = Component(Signal, kind=Kind.hinted) frame = Component(Signal, kind=Kind.hinted) - shape = Component(Signal, kind=Kind.hinted) - _throttle = 0.5 + shape = Component(Signal, kind=Kind.omitted) + _throttle = 0.05 def __init__( self, *args, url: str = "tcp://129.129.95.38:20000", parent: Device = None, **kwargs @@ -73,12 +73,12 @@ class StdDaqPreview(Device): Example: ---------- - std.configure(throttle=0.5) + std.configure(throttle=0.05) Parameters ---------- throttle : float, optional - Additional throtling for the ophyd device. (default = 0.5 sec) + Additional throtling for the ophyd device. (default = 0.05 sec) """ self._throttle = throttle @@ -127,18 +127,22 @@ class StdDaqPreview(Device): t_curr = time() t_elapsed = t_curr - t_last - if t_elapsed > self._throttle: + if t_elapsed > 999999*self._throttle: self.frame.put(header['frame'], force=True) self.shape.put(header['shape'], force=True) self.image.put(image, force=True) t_last=t_curr - #print(header) - print(f"Frame: {header['frame']}\tMin: {np.min(image)}\tMax: {np.max(image)}") + print(f"Frame: {header['frame']}\t<<<<>>>>") + else: + print(f"Frame: {header['frame']}\tMin: {np.min(image)}\tMax: {np.max(image)}") if self._stop_polling: self.status.set("detached", force=True) print("Detaching monitor") break + except ValueError: + # Happens when ZMQ partially delivers the multipart message + pass except zmq.error.Again: sleep(0.1) except Exception as ex: diff --git a/tomcat_bec/devices/gigafrost/stddaq_ws.py b/tomcat_bec/devices/gigafrost/stddaq_ws.py index a2f12a0..27323bb 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_ws.py +++ b/tomcat_bec/devices/gigafrost/stddaq_ws.py @@ -30,7 +30,7 @@ class StdDaqWsClient(Device): # pylint: disable=too-many-instance-attributes # Status attributes - status = Component(Signal, value="unknown") + status = Component(Signal, value="unknown", kind=Kind.hinted) n_images = Component(Signal, value=10000, kind=Kind.config) file_path = Component(Signal, value="/gpfs/test/test-beamline", kind=Kind.config) From 26022c46f0f7e6683dd9261b7e4f448d7c232f8a Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Fri, 5 Jul 2024 14:10:56 +0200 Subject: [PATCH 21/47] Added a test step scan with GigaFrost --- .../devices/gigafrost/gigafrostclient.py | 21 +- tomcat_bec/scans/aerotech_test.py | 545 ++++++++++++++++++ tomcat_bec/scans/gigafrost_test.py | 93 +++ 3 files changed, 651 insertions(+), 8 deletions(-) create mode 100644 tomcat_bec/scans/aerotech_test.py create mode 100644 tomcat_bec/scans/gigafrost_test.py diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index b2c0a0a..219cc65 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -344,15 +344,18 @@ class GigaFrostClient(Device): } def stage(self): - """Standard ophyd method to start an acquisition""" + """Standard ophyd method to start an acquisition + + In ophyd it still needs to be triggered to perfor an actual capture. + """ if self.infoBusyFlag.value: raise RuntimeError("Camera is already busy, unstage it first!") # change to running self.cmdStartCamera.set(1).wait() - # soft trigger on - if self._auto_soft_enable: - self.cmdSoftEnable.set(1).wait() + # soft trigger on (DISABLED: use trigger() in ophyd) + #if self._auto_soft_enable: + # self.cmdSoftEnable.set(1).wait() self.state = const.GfStatus.ACQUIRING # Gigafrost can finish a run without explicit unstaging @@ -373,17 +376,19 @@ class GigaFrostClient(Device): self.unstage() def trigger(self) -> DeviceStatus: - """Sends a software trigger""" + """Sends a software trigger and approximately waits to finnish""" + status = DeviceStatus(self, settle_time=2.0) + # Soft triggering based on operation mode - if self._auto_soft_enable: + if self._auto_soft_enable and self.trigger_mode=='auto' and self.enable_mode=='soft': # BEC teststand operation mode: poedge of SoftEnable if Started self.cmdSoftEnable.set(0).wait() self.cmdSoftEnable.set(1).wait() + sleep(self.cfgFramerate.value*self.cfgCntNum*0.001) else: self.cmdSoftTrigger.set(1).wait() - status = DeviceStatus(self, settle_time=2.0) status.set_finished() - sleep(2.0) + return status @property diff --git a/tomcat_bec/scans/aerotech_test.py b/tomcat_bec/scans/aerotech_test.py new file mode 100644 index 0000000..a1d6072 --- /dev/null +++ b/tomcat_bec/scans/aerotech_test.py @@ -0,0 +1,545 @@ +import time + +import numpy as np + +from bec_lib import bec_logger +from scan_server.scans import AsyncFlyScanBase, ScanArgType, ScanBase + +logger = bec_logger.logger + + +class AeroSingleScan(AsyncFlyScanBase): + scan_name = "aero_single_scan" + scan_report_hint = "scan_progress" + required_kwargs = ["startpos", "scanrange", "psodist"] + arg_input = {} + arg_bundle_size = {"bundle": len(arg_input), "min": None, "max": None} + + def __init__(self, *args, parameter: dict = None, **kwargs): + """Performs a single line scan with PSO output and data collection. + + Examples: + >>> scans.aero_single_scan(startpos=42, scanrange=2*10+3*180, psodist=[10, 180, 0.01, 180, 0.01, 180]) + + """ + super().__init__(parameter=parameter, **kwargs) + self.axis = [] + self.scan_motors = [] + self.num_pos = 0 + + self.scanStart = self.caller_kwargs.get("startpos") + self.scanEnd = self.scanStart + self.caller_kwargs.get("scanrange") + self.psoBounds = self.caller_kwargs.get("psodist") + self.scanVel = self.caller_kwargs.get("velocity", 30) + self.scanTra = self.caller_kwargs.get("travel", 80) + self.scanAcc = self.caller_kwargs.get("acceleration", 500) + self.scanExpNum = self.caller_kwargs.get("daqpoints", 5000) + + def pre_scan(self): + # Move to start position + st = yield from self.stubs.send_rpc_and_wait("es1_roty", "dmove", self.scanStart) + st.wait() + yield from self.stubs.pre_scan() + + def scan_report_instructions(self): + """Scan report instructions for the progress bar, yields from mcs card""" + if not self.scan_report_hint: + yield None + return + yield from self.stubs.scan_report_instruction({"scan_progress": ["es1_roty"]}) + + def scan_core(self): + # Configure PSO, DDC and motor + yield from self.stubs.send_rpc_and_wait( + "es1_roty", + "configure", + {"velocity": self.scanVel, "acceleration": self.scanVel / self.scanAcc}, + ) + yield from self.stubs.send_rpc_and_wait( + "es1_psod", "configure", {"distance": self.psoBounds, "wmode": "toggle"} + ) + yield from self.stubs.send_rpc_and_wait( + "es1_ddaq", + "configure", + {"npoints": self.scanExpNum}, + ) + # DAQ with real trigger + # yield from self.stubs.send_rpc_and_wait( + # "es1_ddaq", "configure", {"npoints": self.scanExpNum, "trigger": "HSINP0_RISE"}, + # ) + + # Kick off PSO and DDC + st = yield from self.stubs.send_rpc_and_wait("es1_psod", "kickoff") + st.wait() + st = yield from self.stubs.send_rpc_and_wait("es1_ddaq", "kickoff") + st.wait() + + print("Start moving") + # Start the actual movement + yield from self.stubs.send_rpc_and_wait( + "es1_roty", + "configure", + {"target": self.scanEnd}, + ) + yield from self.stubs.kickoff( + device="es1_roty", + parameter={"target": self.scanEnd}, + ) + # yield from self.stubs.set(device='es1_roty', value=self.scanEnd, wait_group="flyer") + target_diid = self.DIID - 1 + + # Wait for motion to finish + while True: + yield from self.stubs.read_and_wait(group="primary", wait_group="readout_primary", pointID=self.pointID) + self.pointID += 1 + status = self.stubs.get_req_status( + device="es1_roty", RID=self.metadata["RID"], DIID=target_diid + ) + progress = self.stubs.get_device_progress(device="es1_roty", RID=self.metadata["RID"]) + print(f"status: {status}\tprogress: {progress}") + if progress: + self.num_pos = progress + if status: + break + time.sleep(1) + print("Scan done\n\n") + self.num_pos = self.pointID + + +class AeroSequenceScan(AsyncFlyScanBase): + scan_name = "aero_sequence_scan" + scan_report_hint = "table" + required_kwargs = ["startpos", "ranges"] + arg_input = {} + arg_bundle_size = {"bundle": len(arg_input), "min": None, "max": None} + + def __init__(self, *args, parameter: dict = None, **kwargs): + """Performs a sequence scan with PSO output and data collection + + Examples: + >>> scans.aero_sequence_scan(startpos=42, ranges=([179.9, 0.1, 5]), expnum=3600, repnum=3, repmode="PosNeg") + + """ + super().__init__(parameter=parameter, **kwargs) + self.axis = [] + self.scan_motors = ["es1_roty"] + self.num_pos = 0 + + self.scanStart = self.caller_kwargs.get("startpos") + self.scanRanges = self.caller_kwargs.get("ranges") + self.scanExpNum = self.caller_kwargs.get("expnum", 25000) + self.scanRepNum = self.caller_kwargs.get("repnum", 1) + self.scanRepMode = self.caller_kwargs.get("repmode", "Pos") + self.scanVel = self.caller_kwargs.get("velocity", 30) + self.scanTra = self.caller_kwargs.get("travel", 80) + self.scanAcc = self.caller_kwargs.get("acceleration", 500) + self.scanSafeDist = self.caller_kwargs.get("safedist", 10) + + if isinstance(self.scanRanges[0], (int, float)): + self.scanRanges = self.scanRanges + + if self.scanRepMode not in ["PosNeg", "Pos", "NegPos", "Neg"]: + raise RuntimeError(f"Unexpected sequence repetition mode: {self.scanRepMode}") + + def pre_scan(self): + # Calculate PSO positions from tables + AccDist = 0.5 * self.scanVel * self.scanVel / self.scanAcc + self.scanSafeDist + + # Relative PSO bounds + self.psoBoundsPos = [AccDist] + try: + for line in self.scanRanges: + print(f"Line is: {line} of type {type(line)}") + for rr in range(int(line[2])): + self.psoBoundsPos.append(line[0]) + self.psoBoundsPos.append(line[1]) + except TypeError: + line = self.scanRanges + print(f"Line is: {line} of type {type(line)}") + for rr in range(int(line[2])): + self.psoBoundsPos.append(line[0]) + self.psoBoundsPos.append(line[1]) + del self.psoBoundsPos[-1] + + self.psoBoundsNeg = [AccDist] + self.psoBoundsNeg.extend(self.psoBoundsPos[::-1]) + + scanrange = 2 * AccDist + np.sum(self.psoBoundsPos) + if self.scanRepMode in ["PosNeg", "Pos"]: + self.PosStart = self.scanStart - AccDist + self.PosEnd = self.scanStart + scanrange + elif self.scanRepMode in ["NegPos", "Neg"]: + self.PosStart = self.scanStart + AccDist + self.PosEnd = self.scanStart - scanrange + else: + raise RuntimeError(f"Unexpected sequence repetition mode: {self.scanRepMode}") + print(f"\tCalculated scan range: {self.PosStart} to {self.PosEnd} range {scanrange}") + + # ToDo: We could append all distances and write a much longer 'distance array'. this would elliminate the need of rearming... + + # Move roughly to start position + yield from self.stubs.send_rpc_and_wait( + "es1_roty", + "configure", + {"velocity": self.scanTra, "acceleration": self.scanTra / self.scanAcc}, + ) + st = yield from self.stubs.send_rpc_and_wait("es1_roty", "move", self.PosStart) + st.wait() + + yield from self.stubs.pre_scan() + + def scan_core(self): + # Move to start position (with travel velocity) + yield from self.stubs.send_rpc_and_wait( + "es1_roty", + "configure", + {"velocity": self.scanTra, "acceleration": self.scanTra / self.scanAcc}, + ) + yield from self.stubs.send_rpc_and_wait("es1_roty", "move", self.PosStart) + + # Condigure PSO, DDC and motorHSINP0_RISE + yield from self.stubs.send_rpc_and_wait( + "es1_psod", "configure", {"distance": self.psoBoundsPos, "wmode": "toggle"} + ) + yield from self.stubs.send_rpc_and_wait( + "es1_ddaq", + "configure", + {"npoints": self.scanExpNum}, + ) + # With real trigger + # yield from self.stubs.send_rpc_and_wait( + # "es1_ddaq", "configure", {"npoints": self.scanExpNum, "trigger": "HSINP0_RISE"} + # ) + yield from self.stubs.send_rpc_and_wait( + "es1_roty", + "configure", + {"velocity": self.scanVel, "acceleration": self.scanVel / self.scanAcc}, + ) + + # Kickoff pso and daq + st = yield from self.stubs.send_rpc_and_wait("es1_psod", "kickoff") + st.wait() + st = yield from self.stubs.send_rpc_and_wait("es1_ddaq", "kickoff") + st.wait() + + # Run the actual scan (haven't figured out the proggress bar) + print("Starting actual scan loop") + for ii in range(self.scanRepNum): + print(f"Scan segment {ii}") + # No option to reset the index counter... + yield from self.stubs.send_rpc_and_wait("es1_psod", "dstArrayRearm.set", 1) + + if self.scanRepMode in ["Pos", "Neg"]: + yield from self.stubs.send_rpc_and_wait( + "es1_roty", + "configure", + {"velocity": self.scanVel, "acceleration": self.scanVel / self.scanAcc}, + ) + st = yield from self.stubs.send_rpc_and_wait("es1_roty", "move", self.PosEnd) + st.wait() + yield from self.stubs.send_rpc_and_wait( + "es1_roty", + "configure", + {"velocity": self.scanTra, "acceleration": self.scanTra / self.scanAcc}, + ) + st = yield from self.stubs.send_rpc_and_wait("es1_roty", "move", self.PosStart) + st.wait() + elif self.scanRepMode in ["PosNeg", "NegPos"]: + if ii % 2 == 0: + st = yield from self.stubs.send_rpc_and_wait("es1_roty", "move", self.PosEnd) + st.wait() + else: + st = yield from self.stubs.send_rpc_and_wait("es1_roty", "move", self.PosStart) + st.wait() + self.pointID += 1 + self.num_pos += 1 + time.sleep(0.2) + + # Complete (should complete instantly) + yield from self.stubs.complete(device="es1_psod") + yield from self.stubs.complete(device="es1_ddaq") + st = yield from self.stubs.send_rpc_and_wait("es1_psod", "complete") + st.wait() + st = yield from self.stubs.send_rpc_and_wait("es1_ddaq", "complete") + st.wait() + + # Collect - Throws a warning due to returning a generator + # st = yield from self.stubs.send_rpc_and_wait("es1_psod", "collect") + # st = yield from self.stubs.send_rpc_and_wait("es1_ddaq", "collect") + + yield from self.stubs.read_and_wait(group="primary", wait_group="readout_primary") + target_diid = self.DIID - 1 + + yield from self.stubs.kickoff( + device="es1_roty", + parameter={"target": self.PosStart}, + ) + yield from self.stubs.wait(device=["es1_roty"], wait_group="kickoff", wait_type="move") + yield from self.stubs.complete(device="es1_roty") + + # Wait for motion to finish + while True: + pso_status = self.stubs.get_req_status( + device="es1_psod", RID=self.metadata["RID"], DIID=target_diid + ) + daq_status = self.stubs.get_req_status( + device="es1_ddaq", RID=self.metadata["RID"], DIID=target_diid + ) + mot_status = self.stubs.get_req_status( + device="es1_roty", RID=self.metadata["RID"], DIID=target_diid + ) + progress = self.stubs.get_device_progress(device="es1_psod", RID=self.metadata["RID"]) + progress = self.stubs.get_device_progress(device="es1_ddaq", RID=self.metadata["RID"]) + progress = self.stubs.get_device_progress(device="es1_roty", RID=self.metadata["RID"]) + print(f"pso: {pso_status}\tdaq: {daq_status}\tmot: {mot_status}\tprogress: {progress}") + if progress: + self.num_pos = int(progress) + if mot_status: + break + time.sleep(1) + print("Scan done\n\n") + + def cleanup(self): + """Set scan progress to 1 to finish the scan""" + self.num_pos = 1 + return super().cleanup() + + +class AeroScriptedScan(AsyncFlyScanBase): + scan_name = "aero_scripted_scan" + scan_report_hint = "table" + required_kwargs = ["filename", "subs"] + arg_input = {} + arg_bundle_size = {"bundle": len(arg_input), "min": None, "max": None} + + def __init__(self, *args, parameter: dict = None, **kwargs): + """Executes an AeroScript template as a flyer + + The script is generated from a template file using jinja2. + Examples: + >>> scans.aero_scripted_scan(filename="AerotechSnapAndStepTemplate.ascript", subs={'startpos': 42, 'stepsize': 0.1, 'numsteps': 1800, 'exptime': 0.1}) + + """ + super().__init__(parameter=parameter, **kwargs) + self.axis = [] + self.scan_motors = ["es1_roty"] + self.num_pos = 0 + + self.filename = self.caller_kwargs.get("filename") + self.subs = self.caller_kwargs.get("subs") + self.taskIndex = self.caller_kwargs.get("taskindex", 4) + + def pre_scan(self): + print("TOMCAT Loading Aeroscript template") + # Load the test file + with open(self.filename) as f: + templatetext = f.read() + + # Substitute jinja template + import jinja2 + + tm = jinja2.Template(templatetext) + self.scripttext = tm.render(scan=self.subs) + + yield from self.stubs.pre_scan() + + def scan_core(self): + print("TOMCAT Sequeence scan (via Jinjad AeroScript)") + t_start = time.time() + + # Configure by copying text to controller file and compiling it + yield from self.stubs.send_rpc_and_wait( + "es1_tasks", + "configure", + {"text": self.scripttext, "filename": "becExec.ascript", "taskIndex": self.taskIndex}, + ) + + # Kickoff + st = yield from self.stubs.send_rpc_and_wait("es1_tasks", "kickoff") + st.wait() + time.sleep(0.5) + + # Complete + yield from self.stubs.complete(device="es1_tasks") + + # Collect - up to implementation + + t_end = time.time() + t_elapsed = t_end - t_start + print(f"Elapsed scan time: {t_elapsed}") + + def cleanup(self): + """Set scan progress to 1 to finish the scan""" + self.num_pos = self.pointID + return super().cleanup() + + +class AeroSnapNStep(AeroScriptedScan): + scan_name = "aero_snapNstep" + scan_report_hint = "table" + required_kwargs = ["startpos", "expnum"] + arg_input = {} + arg_bundle_size = {"bundle": len(arg_input), "min": None, "max": None} + + def __init__(self, *args, parameter: dict = None, **kwargs): + """Executes a scripted SnapNStep scan + + This scan generates and executes an AeroScript file to run + a hardware step scan on the Aerotech controller. + The script is generated from a template file using jinja2. + + Examples: + >>> scans.scans.aero_snapNstep(startpos=42, range=180, expnum=1800, exptime=0.1) + """ + super().__init__(parameter=parameter, **kwargs) + self.axis = [] + self.scan_motors = ["es1_roty"] + self.num_pos = 0 + + # self.filename = "/afs/psi.ch/user/m/mohacsi_i/ophyd_devices/ophyd_devices/epics/devices/aerotech/AerotechSnapAndStepTemplate.ascript" + self.filename = "/sls/X05LA/data/x05laop/bec/ophyd_devices/ophyd_devices/epics/devices/aerotech/AerotechSnapAndStepTemplate.ascript" + self.scanTaskIndex = self.caller_kwargs.get("taskindex", 4) + + self.scanStart = self.caller_kwargs.get("startpos") + self.scanExpNum = self.caller_kwargs.get("expnum") + self.scanRange = self.caller_kwargs.get("range", 180) + self.scanExpTime = self.caller_kwargs.get("exptime", 0.1) + self.scanStepSize = self.scanRange / self.scanExpNum + # self.scanVel = self.caller_kwargs.get("velocity", 30) + # self.scanTra = self.caller_kwargs.get("travel", 80) + # self.scanAcc = self.caller_kwargs.get("acceleration", 500) + + self.subs = { + "startpos": self.scanStart, + "stepsize": self.scanStepSize, + "numsteps": self.scanExpNum, + "exptime": self.scanExpTime, + } + + def scan_core(self): + print("TOMCAT Snap N Step scan (via Jinjad AeroScript)") + # Run template execution frm parent + yield from self.stubs.read_and_wait(group="primary", wait_group="readout_primary", pointID=self.pointID) + self.pointID += 1 + yield from super().scan_core() + + # Collect - Throws a warning due to returning a generator + yield from self.stubs.send_rpc_and_wait("es1_ddaq", "npoints.put", self.scanExpNum) + # st = yield from self.stubs.send_rpc_and_wait("es1_ddaq", "collect") + # st.wait() + + +class AeroScriptedSequence(AeroScriptedScan): + scan_name = "aero_scripted_sequence" + scan_report_hint = "table" + required_kwargs = ["startpos", "ranges"] + arg_input = {} + arg_bundle_size = {"bundle": len(arg_input), "min": None, "max": None} + + def __init__(self, *args, parameter: dict = None, **kwargs): + """Executes a scripted sequence scan + + This scan generates and executes an AeroScript file to run a hardware sequence scan on the + Aerotech controller. You might win a few seconds this way, but it has some limtations... + The script is generated from a template file using jinja2. + + Examples: + >>> scans.aero_scripted_sequence(startpos=42, ranges=([179.9, 0.1, 5]), expnum=3600, repnum=3, repmode="PosNeg") + """ + super().__init__(parameter=parameter, **kwargs) + self.axis = [] + self.scan_motors = ["es1_roty"] + self.num_pos = 0 + + self.filename = "/afs/psi.ch/user/m/mohacsi_i/ophyd_devices/ophyd_devices/epics/devices/aerotech/AerotechSimpleSequenceTemplate.ascript" + self.scanTaskIndex = self.caller_kwargs.get("taskindex", 4) + + self.scanStart = self.caller_kwargs.get("startpos") + self.scanRanges = self.caller_kwargs.get("ranges") + self.scanExpNum = self.caller_kwargs.get("expnum", 25000) + self.scanRepNum = self.caller_kwargs.get("repnum", 1) + self.scanRepMode = self.caller_kwargs.get("repmode", "Pos") + + self.scanVel = self.caller_kwargs.get("velocity", 30) + self.scanTra = self.caller_kwargs.get("travel", 80) + self.scanAcc = self.caller_kwargs.get("acceleration", 500) + self.scanSafeDist = self.caller_kwargs.get("safedist", 10) + + self.subs = { + "startpos": self.scanStart, + "scandir": self.scanRepMode, + "nrepeat": self.scanRepNum, + "npoints": self.scanExpNum, + "scanvel": self.scanVel, + "jogvel": self.scanTra, + "scanacc": self.scanAcc, + } + + if self.scanRepMode not in ["PosNeg", "Pos", "NegPos", "Neg"]: + raise RuntimeError(f"Unexpected sequence repetition mode: {self.scanRepMode}") + + if isinstance(self.scanRanges[0], (int, float)): + self.scanRanges = self.scanRanges + + def pre_scan(self): + # Calculate PSO positions from tables + AccDist = 0.5 * self.scanVel * self.scanVel / self.scanAcc + self.scanSafeDist + + # Relative PSO bounds + self.psoBoundsPos = [AccDist] + try: + for line in self.scanRanges: + print(f"Line is: {line} of type {type(line)}") + for rr in range(int(line[2])): + self.psoBoundsPos.append(line[0]) + self.psoBoundsPos.append(line[1]) + except TypeError: + line = self.scanRanges + print(f"Line is: {line} of type {type(line)}") + for rr in range(int(line[2])): + self.psoBoundsPos.append(line[0]) + self.psoBoundsPos.append(line[1]) + del self.psoBoundsPos[-1] + + self.psoBoundsNeg = [AccDist] + self.psoBoundsNeg.extend(self.psoBoundsPos[::-1]) + + self.scanrange = 2 * AccDist + np.sum(self.psoBoundsPos) + if self.scanRepMode in ["PosNeg", "Pos"]: + self.PosStart = self.scanStart - AccDist + elif self.scanRepMode in ["NegPos", "Neg"]: + self.PosStart = self.scanStart + AccDist + else: + raise RuntimeError(f"Unexpected sequence repetition mode: {self.scanRepMode}") + print(f"\tCalculated scan range: {self.PosStart} range {self.scanrange}") + + # ToDo: We could append all distances and write a much longer 'distance array'. this would elliminate the need of rearming... + self.subs.update( + { + "psoBoundsPos": self.psoBoundsPos, + "psoBoundsNeg": self.psoBoundsNeg, + "scanrange": self.scanrange, + } + ) + + # Move roughly to start position + yield from self.stubs.send_rpc_and_wait( + "es1_roty", + "configure", + {"velocity": self.scanTra, "acceleration": self.scanTra / self.scanAcc}, + ) + st = yield from self.stubs.send_rpc_and_wait("es1_roty", "move", self.PosStart) + st.wait() + + yield from super().pre_scan() + + def scan_core(self): + print("TOMCAT Sequence scan (via Jinjad AeroScript)") + # Run template execution frm parent + yield from super().scan_core() + + # Collect - Throws a warning due to returning a generator + yield from self.stubs.send_rpc_and_wait("es1_ddaq", "npoints.put", self.scanExpNum) + # st = yield from self.stubs.send_rpc_and_wait("es1_ddaq", "collect") + # st.wait() + diff --git a/tomcat_bec/scans/gigafrost_test.py b/tomcat_bec/scans/gigafrost_test.py new file mode 100644 index 0000000..d0400e0 --- /dev/null +++ b/tomcat_bec/scans/gigafrost_test.py @@ -0,0 +1,93 @@ +import time + +import numpy as np + +from bec_lib import bec_logger +from bec_server.bec_server.scan_server.scans import AsyncFlyScanBase, ScanArgType, ScanBase + +logger = bec_logger.logger + + + +class GigaFrostStepScan(AsyncFlyScanBase): + scan_name = "gigafrost_line_scan" + scan_report_hint = "table" + required_kwargs = ["startpos", "ranges"] + arg_input = {} + arg_bundle_size = {"bundle": len(arg_input), "min": None, "max": None} + + def __init__(self, *args, parameter: dict = None, **kwargs): + """Performs a step scan with StdDAQ + + Examples: + >>> scans.gigafrost_line_scan(motor='eyex', range=(-5, 5), steps=10, exp_time=20, exp_per=20, exp_burst=10, relative=True) + + """ + super().__init__(parameter=parameter, **kwargs) + self.axis = [] + self.num_pos = 0 + + self.scan_motors = [self.caller_kwargs.get("motor")] + self.scan_range = self.caller_kwargs.get("range") + self.scan_relat = self.caller_kwargs.get("relative", False) + self.scan_steps = int(self.caller_kwargs.get("steps", 10)) + self.scan_exp_t = self.caller_kwargs.get("exp_time", 5) + self.scan_exp_p = self.caller_kwargs.get("exp_per", 10) + self.scan_exp_b = self.caller_kwargs.get("exp_burst", 10) + + if self.scan_steps <=0: + raise RuntimeError(f"Requires at least one scan step") + + def prepare_positions(self): + # Calculate scan start position + if self.scan_relat: + pos = yield from self.stubs.send_rpc_and_wait(self.scan_motors[0], "read") + self.start_pos = pos[self.scan_motors[0]].get("value") + self.scan_range[0] + else: + self.start_pos = self.scan_range[0] + + # Calculate step size + self.step_size = (self.scan_range[1]-self.scan_range[0])/self.scan_steps + + self.positions = self.start_pos + for ii in range(self.scan_steps): + self.positions.append(self.start_pos + ii*self.step_size) + + self.num_pos = len(self.positions) + + def pre_scan(self): + # Move roughly to start position + st = yield from self.stubs.send_rpc_and_wait(self.scan_motors[0], "move", self.start_pos) + st.wait() + + yield from self.stubs.pre_scan() + + def stage(self): + yield from self.stubs.send_rpc_and_wait( + "gf2", "configure", {"nimages": self.scan_exp_b, "exposure": self.scan_exp_t, "period": self.scan_exp_p, "roix": 480, "roiy": 128} + ) + yield from self.stubs.send_rpc_and_wait( + "daq", "configure", {"n_images": self.scan_steps * self.scan_exp_b} + ) + yield from super().stage() + + + + def scan_core(self): + for ii in range(self.num_points): + st = yield from self.stubs.send_rpc_and_wait(self.scan_motors[0], "move", self.positions[ii]) + st.wait() + st = yield from self.stubs.send_rpc_and_wait("gf2", "trigger") + st.wait() + self.pointID += 1 + time.sleep(0.2) + + time.sleep(1) + print("Scan done\n\n") + + def cleanup(self): + """Set scan progress to 1 to finish the scan""" + self.num_pos = 1 + return super().cleanup() + + From 8936902a86de808158984a334636a203fc9f49b8 Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Tue, 9 Jul 2024 15:07:17 +0200 Subject: [PATCH 22/47] First running scan --- .../devices/gigafrost/gigafrostclient.py | 27 +++++++++++++------ tomcat_bec/devices/gigafrost/stddaq_ws.py | 5 ++++ tomcat_bec/scans/__init__.py | 2 ++ tomcat_bec/scans/gigafrost_test.py | 21 ++++++++++----- 4 files changed, 40 insertions(+), 15 deletions(-) diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index 219cc65..a40fcec 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -59,13 +59,13 @@ class GigaFrostClient(Device): cmdWriteService = Component(EpicsSignal, "WRITE_SRV.PROC", put_complete=True) # Standard camera configs - cfgExposure = Component(EpicsSignal, "EXPOSURE", put_complete=True, kind=Kind.config) - cfgFramerate = Component(EpicsSignal, "FRAMERATE", put_complete=True, kind=Kind.config) - cfgRoiX = Component(EpicsSignal, "ROIX", put_complete=True, kind=Kind.config) - cfgRoiY = Component(EpicsSignal, "ROIY", put_complete=True, kind=Kind.config) - cfgScanId = Component(EpicsSignal, "SCAN_ID", put_complete=True, kind=Kind.config) - cfgCntNum = Component(EpicsSignal, "CNT_NUM", put_complete=True, kind=Kind.config) - cfgCorrMode = Component(EpicsSignal, "CORR_MODE", put_complete=True, kind=Kind.config) + cfgExposure = Component(EpicsSignal, "EXPOSURE", put_complete=True, auto_monitor=True, kind=Kind.config) + cfgFramerate = Component(EpicsSignal, "FRAMERATE", put_complete=True, auto_monitor=True, kind=Kind.config) + cfgRoiX = Component(EpicsSignal, "ROIX", put_complete=True, auto_monitor=True, kind=Kind.config) + cfgRoiY = Component(EpicsSignal, "ROIY", put_complete=True, auto_monitor=True, kind=Kind.config) + cfgScanId = Component(EpicsSignal, "SCAN_ID", put_complete=True, auto_monitor=True, kind=Kind.config) + cfgCntNum = Component(EpicsSignal, "CNT_NUM", put_complete=True, auto_monitor=True, kind=Kind.config) + cfgCorrMode = Component(EpicsSignal, "CORR_MODE", put_complete=True, auto_monitor=True, kind=Kind.config) # Software signals cmdSoftEnable = Component(EpicsSignal, "SOFT_ENABLE", put_complete=True) @@ -312,6 +312,17 @@ class GigaFrostClient(Device): * 4: Invert pixel values, but do not apply any linearity correction * 5: Apply the full linearity correction """ + # If Bluesky style configure + if isinstance(nimages, dict): + d = nimages.copy() + nimages = d.get('nimages', 10) + exposure = d.get('exposure', exposure) + period = d.get('period', period) + roix = d.get('roix', roix) + roiy = d.get('roiy', roiy) + scanid = d.get('scanid', scanid) + correction_mode = d.get('correction_mode', correction_mode) + # Stop acquisition self.cmdStartCamera.set(0).wait() if self._auto_soft_enable: @@ -384,7 +395,7 @@ class GigaFrostClient(Device): # BEC teststand operation mode: poedge of SoftEnable if Started self.cmdSoftEnable.set(0).wait() self.cmdSoftEnable.set(1).wait() - sleep(self.cfgFramerate.value*self.cfgCntNum*0.001) + sleep(self.cfgFramerate.value*self.cfgCntNum.value*0.001+0.050) else: self.cmdSoftTrigger.set(1).wait() status.set_finished() diff --git a/tomcat_bec/devices/gigafrost/stddaq_ws.py b/tomcat_bec/devices/gigafrost/stddaq_ws.py index 27323bb..745ac1c 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_ws.py +++ b/tomcat_bec/devices/gigafrost/stddaq_ws.py @@ -79,6 +79,11 @@ class StdDaqWsClient(Device): """ old_config = self.read_configuration() + # If Bluesky style configure + if isinstance(n_images, dict): + d = n_images.copy() + n_images = d.get('n_images', None) + file_path = d.get('file_path', None) if n_images is not None: self.n_images.set(int(n_images)) diff --git a/tomcat_bec/scans/__init__.py b/tomcat_bec/scans/__init__.py index e69de29..a3ae7dc 100644 --- a/tomcat_bec/scans/__init__.py +++ b/tomcat_bec/scans/__init__.py @@ -0,0 +1,2 @@ +from .gigafrost_test import GigaFrostStepScan + diff --git a/tomcat_bec/scans/gigafrost_test.py b/tomcat_bec/scans/gigafrost_test.py index d0400e0..50d5ff1 100644 --- a/tomcat_bec/scans/gigafrost_test.py +++ b/tomcat_bec/scans/gigafrost_test.py @@ -3,13 +3,15 @@ import time import numpy as np from bec_lib import bec_logger -from bec_server.bec_server.scan_server.scans import AsyncFlyScanBase, ScanArgType, ScanBase +from bec_server.scan_server.scans import AsyncFlyScanBase, ScanArgType, ScanBase logger = bec_logger.logger class GigaFrostStepScan(AsyncFlyScanBase): + """Test scan for running the GigaFrost with standard DAQ + """ scan_name = "gigafrost_line_scan" scan_report_hint = "table" required_kwargs = ["startpos", "ranges"] @@ -31,9 +33,9 @@ class GigaFrostStepScan(AsyncFlyScanBase): self.scan_range = self.caller_kwargs.get("range") self.scan_relat = self.caller_kwargs.get("relative", False) self.scan_steps = int(self.caller_kwargs.get("steps", 10)) - self.scan_exp_t = self.caller_kwargs.get("exp_time", 5) - self.scan_exp_p = self.caller_kwargs.get("exp_per", 10) - self.scan_exp_b = self.caller_kwargs.get("exp_burst", 10) + self.scan_exp_t = float(self.caller_kwargs.get("exp_time", 5)) + self.scan_exp_p = float(self.caller_kwargs.get("exp_per", 10)) + self.scan_exp_b = int(self.caller_kwargs.get("exp_burst", 10)) if self.scan_steps <=0: raise RuntimeError(f"Requires at least one scan step") @@ -49,14 +51,15 @@ class GigaFrostStepScan(AsyncFlyScanBase): # Calculate step size self.step_size = (self.scan_range[1]-self.scan_range[0])/self.scan_steps - self.positions = self.start_pos + self.positions = [self.start_pos] for ii in range(self.scan_steps): self.positions.append(self.start_pos + ii*self.step_size) - self.num_pos = len(self.positions) + self.num_positions = len(self.positions) def pre_scan(self): # Move roughly to start position + print(f"Scan start position: {self.start_pos}") st = yield from self.stubs.send_rpc_and_wait(self.scan_motors[0], "move", self.start_pos) st.wait() @@ -74,7 +77,9 @@ class GigaFrostStepScan(AsyncFlyScanBase): def scan_core(self): - for ii in range(self.num_points): + self.pointID = 0 + for ii in range(self.num_positions): + print(f"Point: {ii}") st = yield from self.stubs.send_rpc_and_wait(self.scan_motors[0], "move", self.positions[ii]) st.wait() st = yield from self.stubs.send_rpc_and_wait("gf2", "trigger") @@ -88,6 +93,8 @@ class GigaFrostStepScan(AsyncFlyScanBase): def cleanup(self): """Set scan progress to 1 to finish the scan""" self.num_pos = 1 + print("Scan cleanup\n\n") return super().cleanup() + From 35927e46d263080cf8035e8fab97732e18b2cfc2 Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Tue, 9 Jul 2024 15:30:20 +0200 Subject: [PATCH 23/47] First running scan --- .../devices/gigafrost/gigafrostclient.py | 9 ++++--- tomcat_bec/devices/gigafrost/stddaq_ws.py | 26 ++++++++++++++----- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index a40fcec..525855f 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -9,6 +9,7 @@ Created on Thu Jun 27 17:28:43 2024 from time import sleep from ophyd import Device, Component, EpicsSignal, EpicsSignalRO, Kind, DeviceStatus from ophyd.device import Staged +import warnings try: import gfconstants as const @@ -388,14 +389,16 @@ class GigaFrostClient(Device): def trigger(self) -> DeviceStatus: """Sends a software trigger and approximately waits to finnish""" - status = DeviceStatus(self, settle_time=2.0) + status = DeviceStatus(self) # Soft triggering based on operation mode if self._auto_soft_enable and self.trigger_mode=='auto' and self.enable_mode=='soft': - # BEC teststand operation mode: poedge of SoftEnable if Started + # BEC teststand operation mode: posedge of SoftEnable if Started self.cmdSoftEnable.set(0).wait() self.cmdSoftEnable.set(1).wait() - sleep(self.cfgFramerate.value*self.cfgCntNum.value*0.001+0.050) + sleep_time = self.cfgFramerate.value*self.cfgCntNum.value*0.001+0.050 + sleep(sleep_time) + warnings.warn(f"[GF2] Slept for: {sleep_time} seconds") else: self.cmdSoftTrigger.set(1).wait() status.set_finished() diff --git a/tomcat_bec/devices/gigafrost/stddaq_ws.py b/tomcat_bec/devices/gigafrost/stddaq_ws.py index 745ac1c..fee3757 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_ws.py +++ b/tomcat_bec/devices/gigafrost/stddaq_ws.py @@ -57,6 +57,13 @@ class StdDaqWsClient(Device): sleep(5) self._client = connect(self._ws_url) + def monitor(self): + self._client = connect(self._ws_url) + self._mon = Thread(target=self.poll) + self._mon.start() + + + def configure(self, n_images: int = None, file_path: str = None) -> tuple: """Set the standard DAQ parameters for the next run @@ -165,13 +172,18 @@ class StdDaqWsClient(Device): def poll(self): """Monitor status messages until connection is open""" - for msg in self._client: - try: - message = json.loads(msg) - self.status.put(message["status"], force=True) - except Exception as ex: - print(ex) - return + try: + for msg in self._client: + try: + message = json.loads(msg) + self.status.put(message["status"], force=True) + except (ConnectionClosedError, ConnectionClosedOK) as ex: + return + except Exception as ex: + print(ex) + return + finally: + self._mon = None # Automatically connect to MicroSAXS testbench if directly invoked From 15f08b8bf3f515866802baf9020a210cc0ec8890 Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Wed, 10 Jul 2024 15:56:22 +0200 Subject: [PATCH 24/47] Daemon monitoring threads --- .../devices/gigafrost/gigafrostclient.py | 4 +-- .../devices/gigafrost/stddaq_preview.py | 31 +++++++++++----- tomcat_bec/devices/gigafrost/stddaq_ws.py | 8 ++--- tomcat_bec/scans/gigafrost_test.py | 35 ++++++++++++++----- 4 files changed, 55 insertions(+), 23 deletions(-) diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index 525855f..9c610c8 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -6,10 +6,10 @@ Created on Thu Jun 27 17:28:43 2024 @author: mohacsi_i """ +import sys from time import sleep from ophyd import Device, Component, EpicsSignal, EpicsSignalRO, Kind, DeviceStatus from ophyd.device import Staged -import warnings try: import gfconstants as const @@ -398,7 +398,7 @@ class GigaFrostClient(Device): self.cmdSoftEnable.set(1).wait() sleep_time = self.cfgFramerate.value*self.cfgCntNum.value*0.001+0.050 sleep(sleep_time) - warnings.warn(f"[GF2] Slept for: {sleep_time} seconds") + print(f"[GF2] Slept for: {sleep_time} seconds", file=sys.stderr) else: self.cmdSoftTrigger.set(1).wait() status.set_finished() diff --git a/tomcat_bec/devices/gigafrost/stddaq_preview.py b/tomcat_bec/devices/gigafrost/stddaq_preview.py index 7552f56..85dd745 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_preview.py +++ b/tomcat_bec/devices/gigafrost/stddaq_preview.py @@ -6,6 +6,7 @@ Created on Thu Jun 27 17:28:43 2024 @author: mohacsi_i """ +import sys import json from time import sleep, time from threading import Thread @@ -27,20 +28,26 @@ class StdDaqPreview(Device): # pylint: disable=too-many-instance-attributes # Status attributes + url = Component(Signal, kind=Kind.config) status = Component(Signal, value="detached", kind=Kind.omitted) - image = Component(Signal, kind=Kind.hinted) - frame = Component(Signal, kind=Kind.hinted) + process = Component(Signal, value=True, kind=Kind.omitted) + image = Component(Signal, kind=Kind.normal) + frame = Component(Signal, kind=Kind.normal) shape = Component(Signal, kind=Kind.omitted) + value = Component(Signal, kind=Kind.hinted) _throttle = 0.05 def __init__( self, *args, url: str = "tcp://129.129.95.38:20000", parent: Device = None, **kwargs ) -> None: super().__init__(*args, parent=parent, **kwargs) + self.url._metadata["write_access"] = False self.status._metadata["write_access"] = False self.image._metadata["write_access"] = False self.frame._metadata["write_access"] = False self.shape._metadata["write_access"] = False + self.value._metadata["write_access"] = False + self.url.set(url, force=True).wait() self._stream_url = url self._stop_polling = False self._mon = None @@ -85,7 +92,7 @@ class StdDaqPreview(Device): def stage(self) -> list: """Start listening for preview data stream""" self._stop_polling = False - self._mon = Thread(target=self.poll) + self._mon = Thread(target=self.poll, daemon=True) self._mon.start() return super().stage() @@ -109,6 +116,10 @@ class StdDaqPreview(Device): while True: self.plot() + def proc(self, image): + """Basic image processing""" + return np.mean(image) + def poll(self): """Collect streamed updates""" self.status.set("attached", force=True) @@ -124,17 +135,21 @@ class StdDaqPreview(Device): self.status.set("detached", force=True) raise ValueError(f"Unexpected array size of {image.size} for header: {header}") image = image.reshape(header['shape']) + #print(f"Received frame {header['frame']}", file=sys.stderr) t_curr = time() t_elapsed = t_curr - t_last - if t_elapsed > 999999*self._throttle: + if t_elapsed > self._throttle: self.frame.put(header['frame'], force=True) self.shape.put(header['shape'], force=True) self.image.put(image, force=True) t_last=t_curr - print(f"Frame: {header['frame']}\t<<<<>>>>") - else: - print(f"Frame: {header['frame']}\tMin: {np.min(image)}\tMax: {np.max(image)}") + print(f"[DPREV] Updated frame {header['frame']}\tMean: {np.mean(image)}", file=sys.stderr) + + # Perform some basic analysis on the image + if self.process.get(): + self.value.put(self.proc(image), force=True) + print(f"Frame: {header['frame']}\tMin: {np.min(image)}\tMax: {np.max(image)}") if self._stop_polling: self.status.set("detached", force=True) @@ -149,8 +164,6 @@ class StdDaqPreview(Device): print(ex) self.status.set("detached", force=True) raise - finally: - pass # Automatically connect to MicroSAXS testbench if directly invoked diff --git a/tomcat_bec/devices/gigafrost/stddaq_ws.py b/tomcat_bec/devices/gigafrost/stddaq_ws.py index fee3757..15db947 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_ws.py +++ b/tomcat_bec/devices/gigafrost/stddaq_ws.py @@ -6,6 +6,7 @@ Created on Thu Jun 27 17:28:43 2024 @author: mohacsi_i """ +import sys import json from time import sleep from threading import Thread @@ -58,12 +59,11 @@ class StdDaqWsClient(Device): self._client = connect(self._ws_url) def monitor(self): + """Attach monitoring to the DAQ""" self._client = connect(self._ws_url) - self._mon = Thread(target=self.poll) + self._mon = Thread(target=self.poll, daemon=True) self._mon.start() - - def configure(self, n_images: int = None, file_path: str = None) -> tuple: """Set the standard DAQ parameters for the next run @@ -121,7 +121,7 @@ class StdDaqWsClient(Device): f"Start command rejected (might be already running): {reply['reason']}" ) - self._mon = Thread(target=self.poll) + self._mon = Thread(target=self.poll, daemon=True) self._mon.start() return super().stage() diff --git a/tomcat_bec/scans/gigafrost_test.py b/tomcat_bec/scans/gigafrost_test.py index 50d5ff1..e817d14 100644 --- a/tomcat_bec/scans/gigafrost_test.py +++ b/tomcat_bec/scans/gigafrost_test.py @@ -8,22 +8,43 @@ from bec_server.scan_server.scans import AsyncFlyScanBase, ScanArgType, ScanBase logger = bec_logger.logger - class GigaFrostStepScan(AsyncFlyScanBase): """Test scan for running the GigaFrost with standard DAQ """ scan_name = "gigafrost_line_scan" scan_report_hint = "table" - required_kwargs = ["startpos", "ranges"] + required_kwargs = ["motor", "range"] arg_input = {} arg_bundle_size = {"bundle": len(arg_input), "min": None, "max": None} def __init__(self, *args, parameter: dict = None, **kwargs): - """Performs a step scan with StdDAQ + """Example step scan - Examples: - >>> scans.gigafrost_line_scan(motor='eyex', range=(-5, 5), steps=10, exp_time=20, exp_per=20, exp_burst=10, relative=True) + Perform a simple step scan with a motor while software triggering the + gigafrost burst sequence at each point and recording it to the StdDAQ. + Actually only the configuration is gigafrost specific, everything else + is just using standard Bluesky event model. + Example + ------- + >>> scans.gigafrost_line_scan(motor='eyex', range=(-5, 5), steps=10, exp_time=20, exp_burst=10, relative=True) + + Parameters + ---------- + motor: str + Scan motor name, moveable, mandatory. + range : (float, float) + Scan range of the axis. + steps : int, optional + Number of scan steps to cover the range. (default = 10) + exp_time : float, optional [0.01 ... 40] + Exposure time for each frame in [ms]. The IOC fixes the exposure + period to be 2x this long so it doesnt matter. (default = 20) + exp_burst : float, optional + Number of images to be taken for each scan point. (default=10) + relative : boolean, optional + Toggle between relative and absolute input coordinates. + (default = False) """ super().__init__(parameter=parameter, **kwargs) self.axis = [] @@ -34,7 +55,7 @@ class GigaFrostStepScan(AsyncFlyScanBase): self.scan_relat = self.caller_kwargs.get("relative", False) self.scan_steps = int(self.caller_kwargs.get("steps", 10)) self.scan_exp_t = float(self.caller_kwargs.get("exp_time", 5)) - self.scan_exp_p = float(self.caller_kwargs.get("exp_per", 10)) + self.scan_exp_p = 2 * self.scan_exp_t self.scan_exp_b = int(self.caller_kwargs.get("exp_burst", 10)) if self.scan_steps <=0: @@ -74,8 +95,6 @@ class GigaFrostStepScan(AsyncFlyScanBase): ) yield from super().stage() - - def scan_core(self): self.pointID = 0 for ii in range(self.num_positions): From 0e020f326d6fbb2987f2e5a8777f0332bdce3c3c Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Thu, 18 Jul 2024 10:40:28 +0200 Subject: [PATCH 25/47] Preview data stream --- .../device_configs/microxas_test_bed.yaml | 4 +- .../devices/gigafrost/stddaq_preview.py | 13 +- tomcat_bec/devices/gigafrost/stddaq_urllib.py | 249 ++++++++++++++++++ 3 files changed, 263 insertions(+), 3 deletions(-) create mode 100644 tomcat_bec/devices/gigafrost/stddaq_urllib.py diff --git a/tomcat_bec/device_configs/microxas_test_bed.yaml b/tomcat_bec/device_configs/microxas_test_bed.yaml index 8b32ecc..0093dfa 100644 --- a/tomcat_bec/device_configs/microxas_test_bed.yaml +++ b/tomcat_bec/device_configs/microxas_test_bed.yaml @@ -120,7 +120,7 @@ daq: softwareTrigger: false daq_stream0: - description: Standard DAQ controls + description: Standard DAQ preview stream 2 frames every 1000 deviceClass: tomcat_bec.devices.gigafrost.stddaq_preview.StdDaqPreview deviceConfig: url: 'tcp://129.129.95.38:20000' @@ -133,7 +133,7 @@ daq_stream0: softwareTrigger: false daq_stream1: - description: Standard DAQ controls + description: Standard DAQ preview stream 4 frames at 10 Hz deviceClass: tomcat_bec.devices.gigafrost.stddaq_preview.StdDaqPreview deviceConfig: url: 'tcp://129.129.95.38:20001' diff --git a/tomcat_bec/devices/gigafrost/stddaq_preview.py b/tomcat_bec/devices/gigafrost/stddaq_preview.py index 85dd745..776229d 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_preview.py +++ b/tomcat_bec/devices/gigafrost/stddaq_preview.py @@ -24,9 +24,18 @@ class StdDaqPreview(Device): This was meant to provide live image preview directly from the StdDAQ. Note that the preview stream must be heavily throtled in order to cope with the incoming data. + + You can add a preview widget to the dock by: + cam_widget = gui.add_dock('cam_dock1').add_widget('BECFigure').image('daq_stream1') + """ # pylint: disable=too-many-instance-attributes + + SUB_MONITOR = "monitor" + _default_sub = SUB_MONITOR + + # Status attributes url = Component(Signal, kind=Kind.config) status = Component(Signal, value="detached", kind=Kind.omitted) @@ -143,8 +152,10 @@ class StdDaqPreview(Device): self.frame.put(header['frame'], force=True) self.shape.put(header['shape'], force=True) self.image.put(image, force=True) + self._run_subs(sub_type=self.SUB_MONITOR, value=image) + t_last=t_curr - print(f"[DPREV] Updated frame {header['frame']}\tMean: {np.mean(image)}", file=sys.stderr) + print(f"[{self.name}]\tUpdated frame {header['frame']}\tMean: {np.mean(image)}", file=sys.stderr) # Perform some basic analysis on the image if self.process.get(): diff --git a/tomcat_bec/devices/gigafrost/stddaq_urllib.py b/tomcat_bec/devices/gigafrost/stddaq_urllib.py new file mode 100644 index 0000000..df93639 --- /dev/null +++ b/tomcat_bec/devices/gigafrost/stddaq_urllib.py @@ -0,0 +1,249 @@ +# -*- coding: utf-8 -*- +""" +Standard DAQ class module + +Created on Thu Jun 27 17:28:43 2024 + +@author: mohacsi_i +""" +import sys +import json +from time import sleep +from threading import Thread +from ophyd import Device, Signal, Component, Kind +from websockets.sync.client import connect +from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError + + +class StdDaqRestClient(Device): + """Wrapper class around the new StdDaq REST interface. + + This was meant to replace the websocket inteface that replaced the + documented python client. We can finally read configuration with urllib3: + ''' + import urllib3 as url3 + r = url3.request(method='GET', url='http://xbl-daq-29:5000/api/config/get', fields={'config_file': "/etc/std_daq/configs/gf1.json", 'user':"ioc"}) + ''' + """ + # pylint: disable=too-many-instance-attributes + + # Status attributes + status = Component(Signal, value="unknown", kind=Kind.hinted) + n_images = Component(Signal, value=10000, kind=Kind.config) + file_path = Component(Signal, value="/gpfs/test/test-beamline", kind=Kind.config) + + + cfg_detector_name = Component(Signal, kind=Kind.config) + cfg_detector_type = Component(Signal, kind=Kind.config) + cfg_num_modules = Component(Signal, kind=Kind.config) + cfg_bit_depth = Component(Signal, kind=Kind.config) + cfg_image_pixel_height = Component(Signal, kind=Kind.config) + cfg_image_pixel_width = Component(Signal, kind=Kind.config) + cfg_start_udp_port = Component(Signal, kind=Kind.config) + cfg_writer_user_id = Component(Signal, kind=Kind.config) + cfg_submodule_info = Component(Signal, kind=Kind.config) + cfg_max_number_of_forwarders_spawned = Component(Signal, kind=Kind.config) + cfg_use_all_forwarders = Component(Signal, kind=Kind.config) + cfg_module_sync_queue_size = Component(Signal, kind=Kind.config) + cfg_module_positions = Component(Signal, kind=Kind.config) + + + + def __init__( + self, *args, url: str = "http://xbl-daq-29:5000", parent: Device = None, **kwargs + ) -> None: + super().__init__(*args, parent=parent, **kwargs) + self.status._metadata["write_access"] = False + self._url = url + self._mon = None + + # Connect ro the DAQ + self.connect() + + def read_daq_config(self): + """Read the current configuration from the JSON file + """ + r = url3.request(method='GET', url='http://xbl-daq-29:5000/api/config/get', fields={'config_file': "/etc/std_daq/configs/gf1.json", 'user':"ioc"}) + r = r.json() + + if 'detail' in r: + # Failed to read config, probbaly wrong file + return + + for key, val in r: + getattr(self, "cfg_"+key).set(val).wait() + + def write_daq_config(self): + + + config = { + 'detector_name': str(self.cfg_detector_name.get()), + 'detector_type': str(self.cfg_detector_type.get()), + 'n_modules': int(self.cfg_n_modules.get()), + 'bit_depth': int(self.cfg_bit_depth.get()), + 'image_pixel_height': int(self.cfg_image_pixel_height.get()), + 'image_pixel_width': int(self.cfg_image_pixel_width.get()), + 'start_udp_port': int(self.cfg_start_udp_port.get()), + 'writer_user_id' int(self.cfg_writer_user_id.get()), + 'submodule_info': self.cfg_submodule_info.get(), + 'max_number_of_forwarders_spawned': int(self.cfg_max_number_of_forwarders_spawned.get()), + 'use_all_forwarders': bool(self.cfg_use_all_forwarders.get()), + 'module_sync_queue_size': int(self.cfg_module_sync_queue_size.get()), + 'module_positions': self.cfg_module_positions.get() + } + + + + + + def connect(self): + """Connect to te StDAQs websockets interface + + StdDAQ may reject connection for a few seconds, so if it fails, wait + a bit and try to connect again. + """ + try: + self._client = connect(self._ws_url) + except ConnectionRefusedError: + sleep(5) + self._client = connect(self._ws_url) + + def monitor(self): + """Attach monitoring to the DAQ""" + self._client = connect(self._ws_url) + self._mon = Thread(target=self.poll, daemon=True) + self._mon.start() + + + + + + + + + def configure(self, n_images: int = None, file_path: str = None) -> tuple: + """Set the standard DAQ parameters for the next run + + Note that full reconfiguration is not possible with the websocket + interface, only changing acquisition parameters. These changes are only + activated upon staging! + + Example: + ---------- + std.configure(n_images=10000, file_path="/data/test/raw") + + Parameters + ---------- + n_images : int, optional + Number of images to be taken during each scan. Set to -1 for an + unlimited number of images (limited by the ringbuffer size and + backend speed). (default = 10000) + file_path : string, optional + Save file path. (default = '/gpfs/test/test-beamline') + + """ + old_config = self.read_configuration() + # If Bluesky style configure + if isinstance(n_images, dict): + d = n_images.copy() + n_images = d.get('n_images', None) + file_path = d.get('file_path', None) + + if n_images is not None: + self.n_images.set(int(n_images)) + if file_path is not None: + self.output_file.set(str(file_path)) + + new_config = self.read_configuration() + return (old_config, new_config) + + def stage(self) -> list: + """Start a new run with the standard DAQ + + Behavior: the StdDAQ can stop the previous run either by itself or + by calling unstage. So it might start from an already running state or + not, we can't query if not running. + """ + file_path = self.file_path.get() + n_image = self.n_images.get() + + message = {"command": "start", "path": file_path, "n_image": n_image} + reply = self.message(message) + + reply = json.loads(reply) + if reply["status"] in ("creating_file"): + self.status.put(reply["status"], force=True) + elif reply["status"] in ("rejected"): + raise RuntimeError( + f"Start command rejected (might be already running): {reply['reason']}" + ) + + self._mon = Thread(target=self.poll, daemon=True) + self._mon.start() + return super().stage() + + def unstage(self): + """ Stop a running acquisition + + WARN: This will also close the connection!!! + """ + message = {"command": "stop"} + _ = self.message(message, wait_reply=False) + return super().unstage() + + def stop(self): + """ Stop a running acquisition + + WARN: This will also close the connection!!! + """ + message = {"command": "stop"} + # The poller thread locks recv raising a RuntimeError + self.message(message, wait_reply=False) + + def message(self, message: dict, timeout=1, wait_reply=True): + """Send a message to the StdDAQ and receive a reply + + Note: finishing acquisition meang StdDAQ will close connections so + there's no idle state polling. + """ + if isinstance(message, dict): + msg = json.dumps(message) + else: + msg = str(message) + + # Send message (reopen connection if needed) + try: + self._client.send(msg) + except (ConnectionClosedError, ConnectionClosedOK): + self.connect() + self._client.send(msg) + # Wait for reply + reply = None + if wait_reply: + try: + reply = self._client.recv(timeout) + return reply + except (ConnectionClosedError, ConnectionClosedOK, TimeoutError) as ex: + print(ex) + return reply + + def poll(self): + """Monitor status messages until connection is open""" + try: + for msg in self._client: + try: + message = json.loads(msg) + self.status.put(message["status"], force=True) + except (ConnectionClosedError, ConnectionClosedOK) as ex: + return + except Exception as ex: + print(ex) + return + finally: + self._mon = None + + +# Automatically connect to MicroSAXS testbench if directly invoked +if __name__ == "__main__": + daq = StdDaqWsClient(name="daq", url="ws://xbl-daq-29:8080") + daq.wait_for_connection() From 7c5df6d0b46e438ea10ce6c9a09ec4a9a3f8ca93 Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Thu, 18 Jul 2024 12:08:16 +0200 Subject: [PATCH 26/47] DAQ config reader --- .../device_configs/microxas_test_bed.yaml | 13 + tomcat_bec/devices/gigafrost/stddaq_rest.py | 112 ++++++++ tomcat_bec/devices/gigafrost/stddaq_urllib.py | 249 ------------------ 3 files changed, 125 insertions(+), 249 deletions(-) create mode 100644 tomcat_bec/devices/gigafrost/stddaq_rest.py delete mode 100644 tomcat_bec/devices/gigafrost/stddaq_urllib.py diff --git a/tomcat_bec/device_configs/microxas_test_bed.yaml b/tomcat_bec/device_configs/microxas_test_bed.yaml index 0093dfa..5c282e7 100644 --- a/tomcat_bec/device_configs/microxas_test_bed.yaml +++ b/tomcat_bec/device_configs/microxas_test_bed.yaml @@ -119,6 +119,19 @@ daq: readoutPriority: monitored softwareTrigger: false +daqcfg: + description: Standard DAQ config + deviceClass: tomcat_bec.devices.gigafrost.stddaq_rest.StdDaqRestConfig + deviceConfig: + url: 'http://xbl-daq-29:5000' + deviceTags: + - std-daq + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: monitored + softwareTrigger: false + daq_stream0: description: Standard DAQ preview stream 2 frames every 1000 deviceClass: tomcat_bec.devices.gigafrost.stddaq_preview.StdDaqPreview diff --git a/tomcat_bec/devices/gigafrost/stddaq_rest.py b/tomcat_bec/devices/gigafrost/stddaq_rest.py new file mode 100644 index 0000000..8560056 --- /dev/null +++ b/tomcat_bec/devices/gigafrost/stddaq_rest.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +""" +Standard DAQ class module + +Created on Thu Jun 27 17:28:43 2024 + +@author: mohacsi_i +""" +import sys +import json +from time import sleep +from threading import Thread +from ophyd import Device, Signal, Component, Kind +import requests + + +class StdDaqRestConfig(Device): + """Wrapper class around the new StdDaq REST interface. + + This was meant to replace the websocket inteface that replaced the + documented python client. We can finally read configuration through + standard HTTP requests, although the secondary server is ot reachable + at the time. + """ + # pylint: disable=too-many-instance-attributes + + # Status attributes + cfg_detector_name = Component(Signal, kind=Kind.config) + cfg_detector_type = Component(Signal, kind=Kind.config) + cfg_n_modules = Component(Signal, kind=Kind.config) + cfg_bit_depth = Component(Signal, kind=Kind.config) + cfg_image_pixel_height = Component(Signal, kind=Kind.config) + cfg_image_pixel_width = Component(Signal, kind=Kind.config) + cfg_start_udp_port = Component(Signal, kind=Kind.config) + cfg_writer_user_id = Component(Signal, kind=Kind.config) + cfg_submodule_info = Component(Signal, kind=Kind.config) + cfg_max_number_of_forwarders_spawned = Component(Signal, kind=Kind.config) + cfg_use_all_forwarders = Component(Signal, kind=Kind.config) + cfg_module_sync_queue_size = Component(Signal, kind=Kind.config) + cfg_module_positions = Component(Signal, kind=Kind.config) + + + def __init__( + self, *args, url: str = "http://xbl-daq-29:5000", parent: Device = None, **kwargs + ) -> None: + super().__init__(*args, parent=parent, **kwargs) + self._url_base = url + + # Connect ro the DAQ and initialize values + self.read_daq_config() + + def read_daq_config(self): + """Read the current configuration from the JSON file + """ + r = requests.get(self._url_base + '/api/config/get', params={'config_file': "/etc/std_daq/configs/gf1.json", 'user':"ioc"}) + if r.status_code != 200: + raise ConnectionError(f"[{self.name}] Error {r.status_code}:\t{r.text}") + + cfg = r.json() + for key, val in cfg.items(): + if isinstance(val, (int, float, str)): + getattr(self, "cfg_"+key).set(val).wait() + return cfg + + def write_daq_config(self): + config = { + 'detector_name': str(self.cfg_detector_name.get()), + 'detector_type': str(self.cfg_detector_type.get()), + 'n_modules': int(self.cfg_n_modules.get()), + 'bit_depth': int(self.cfg_bit_depth.get()), + 'image_pixel_height': int(self.cfg_image_pixel_height.get()), + 'image_pixel_width': int(self.cfg_image_pixel_width.get()), + 'start_udp_port': int(self.cfg_start_udp_port.get()), + 'writer_user_id': int(self.cfg_writer_user_id.get()), + 'submodule_info': self.cfg_submodule_info.get(), + 'max_number_of_forwarders_spawned': int(self.cfg_max_number_of_forwarders_spawned.get()), + 'use_all_forwarders': bool(self.cfg_use_all_forwarders.get()), + 'module_sync_queue_size': int(self.cfg_module_sync_queue_size.get()), + 'module_positions': self.cfg_module_positions.get() + } + + params = {"user": "ioc", "config_file": "/etc/std_daq/configs/gf1.json"} + + r = requests.post(self._url_base +'/api/config/set', params=params, json=config, headers={"Content-Type": "application/json"}) + if r.status_code != 200: + raise ConnectionError(f"[{self.name}] Error {r.status_code}:\t{r.text}") + + + def stage(self) -> list: + """Read the current configuration from the DAQ + """ + self.read_daq_config() + return super().stage() + + + def unstage(self): + """Read the current configuration from the DAQ + """ + self.read_daq_config() + return super().unstage() + + + def stop(self): + """Read the current configuration from the DAQ + """ + self.unstage() + + +# Automatically connect to MicroSAXS testbench if directly invoked +if __name__ == "__main__": + daqcfg = StdDaqRestConfig(name="daqcfg", url="http://xbl-daq-29:5000") + daqcfg.wait_for_connection() diff --git a/tomcat_bec/devices/gigafrost/stddaq_urllib.py b/tomcat_bec/devices/gigafrost/stddaq_urllib.py deleted file mode 100644 index df93639..0000000 --- a/tomcat_bec/devices/gigafrost/stddaq_urllib.py +++ /dev/null @@ -1,249 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Standard DAQ class module - -Created on Thu Jun 27 17:28:43 2024 - -@author: mohacsi_i -""" -import sys -import json -from time import sleep -from threading import Thread -from ophyd import Device, Signal, Component, Kind -from websockets.sync.client import connect -from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError - - -class StdDaqRestClient(Device): - """Wrapper class around the new StdDaq REST interface. - - This was meant to replace the websocket inteface that replaced the - documented python client. We can finally read configuration with urllib3: - ''' - import urllib3 as url3 - r = url3.request(method='GET', url='http://xbl-daq-29:5000/api/config/get', fields={'config_file': "/etc/std_daq/configs/gf1.json", 'user':"ioc"}) - ''' - """ - # pylint: disable=too-many-instance-attributes - - # Status attributes - status = Component(Signal, value="unknown", kind=Kind.hinted) - n_images = Component(Signal, value=10000, kind=Kind.config) - file_path = Component(Signal, value="/gpfs/test/test-beamline", kind=Kind.config) - - - cfg_detector_name = Component(Signal, kind=Kind.config) - cfg_detector_type = Component(Signal, kind=Kind.config) - cfg_num_modules = Component(Signal, kind=Kind.config) - cfg_bit_depth = Component(Signal, kind=Kind.config) - cfg_image_pixel_height = Component(Signal, kind=Kind.config) - cfg_image_pixel_width = Component(Signal, kind=Kind.config) - cfg_start_udp_port = Component(Signal, kind=Kind.config) - cfg_writer_user_id = Component(Signal, kind=Kind.config) - cfg_submodule_info = Component(Signal, kind=Kind.config) - cfg_max_number_of_forwarders_spawned = Component(Signal, kind=Kind.config) - cfg_use_all_forwarders = Component(Signal, kind=Kind.config) - cfg_module_sync_queue_size = Component(Signal, kind=Kind.config) - cfg_module_positions = Component(Signal, kind=Kind.config) - - - - def __init__( - self, *args, url: str = "http://xbl-daq-29:5000", parent: Device = None, **kwargs - ) -> None: - super().__init__(*args, parent=parent, **kwargs) - self.status._metadata["write_access"] = False - self._url = url - self._mon = None - - # Connect ro the DAQ - self.connect() - - def read_daq_config(self): - """Read the current configuration from the JSON file - """ - r = url3.request(method='GET', url='http://xbl-daq-29:5000/api/config/get', fields={'config_file': "/etc/std_daq/configs/gf1.json", 'user':"ioc"}) - r = r.json() - - if 'detail' in r: - # Failed to read config, probbaly wrong file - return - - for key, val in r: - getattr(self, "cfg_"+key).set(val).wait() - - def write_daq_config(self): - - - config = { - 'detector_name': str(self.cfg_detector_name.get()), - 'detector_type': str(self.cfg_detector_type.get()), - 'n_modules': int(self.cfg_n_modules.get()), - 'bit_depth': int(self.cfg_bit_depth.get()), - 'image_pixel_height': int(self.cfg_image_pixel_height.get()), - 'image_pixel_width': int(self.cfg_image_pixel_width.get()), - 'start_udp_port': int(self.cfg_start_udp_port.get()), - 'writer_user_id' int(self.cfg_writer_user_id.get()), - 'submodule_info': self.cfg_submodule_info.get(), - 'max_number_of_forwarders_spawned': int(self.cfg_max_number_of_forwarders_spawned.get()), - 'use_all_forwarders': bool(self.cfg_use_all_forwarders.get()), - 'module_sync_queue_size': int(self.cfg_module_sync_queue_size.get()), - 'module_positions': self.cfg_module_positions.get() - } - - - - - - def connect(self): - """Connect to te StDAQs websockets interface - - StdDAQ may reject connection for a few seconds, so if it fails, wait - a bit and try to connect again. - """ - try: - self._client = connect(self._ws_url) - except ConnectionRefusedError: - sleep(5) - self._client = connect(self._ws_url) - - def monitor(self): - """Attach monitoring to the DAQ""" - self._client = connect(self._ws_url) - self._mon = Thread(target=self.poll, daemon=True) - self._mon.start() - - - - - - - - - def configure(self, n_images: int = None, file_path: str = None) -> tuple: - """Set the standard DAQ parameters for the next run - - Note that full reconfiguration is not possible with the websocket - interface, only changing acquisition parameters. These changes are only - activated upon staging! - - Example: - ---------- - std.configure(n_images=10000, file_path="/data/test/raw") - - Parameters - ---------- - n_images : int, optional - Number of images to be taken during each scan. Set to -1 for an - unlimited number of images (limited by the ringbuffer size and - backend speed). (default = 10000) - file_path : string, optional - Save file path. (default = '/gpfs/test/test-beamline') - - """ - old_config = self.read_configuration() - # If Bluesky style configure - if isinstance(n_images, dict): - d = n_images.copy() - n_images = d.get('n_images', None) - file_path = d.get('file_path', None) - - if n_images is not None: - self.n_images.set(int(n_images)) - if file_path is not None: - self.output_file.set(str(file_path)) - - new_config = self.read_configuration() - return (old_config, new_config) - - def stage(self) -> list: - """Start a new run with the standard DAQ - - Behavior: the StdDAQ can stop the previous run either by itself or - by calling unstage. So it might start from an already running state or - not, we can't query if not running. - """ - file_path = self.file_path.get() - n_image = self.n_images.get() - - message = {"command": "start", "path": file_path, "n_image": n_image} - reply = self.message(message) - - reply = json.loads(reply) - if reply["status"] in ("creating_file"): - self.status.put(reply["status"], force=True) - elif reply["status"] in ("rejected"): - raise RuntimeError( - f"Start command rejected (might be already running): {reply['reason']}" - ) - - self._mon = Thread(target=self.poll, daemon=True) - self._mon.start() - return super().stage() - - def unstage(self): - """ Stop a running acquisition - - WARN: This will also close the connection!!! - """ - message = {"command": "stop"} - _ = self.message(message, wait_reply=False) - return super().unstage() - - def stop(self): - """ Stop a running acquisition - - WARN: This will also close the connection!!! - """ - message = {"command": "stop"} - # The poller thread locks recv raising a RuntimeError - self.message(message, wait_reply=False) - - def message(self, message: dict, timeout=1, wait_reply=True): - """Send a message to the StdDAQ and receive a reply - - Note: finishing acquisition meang StdDAQ will close connections so - there's no idle state polling. - """ - if isinstance(message, dict): - msg = json.dumps(message) - else: - msg = str(message) - - # Send message (reopen connection if needed) - try: - self._client.send(msg) - except (ConnectionClosedError, ConnectionClosedOK): - self.connect() - self._client.send(msg) - # Wait for reply - reply = None - if wait_reply: - try: - reply = self._client.recv(timeout) - return reply - except (ConnectionClosedError, ConnectionClosedOK, TimeoutError) as ex: - print(ex) - return reply - - def poll(self): - """Monitor status messages until connection is open""" - try: - for msg in self._client: - try: - message = json.loads(msg) - self.status.put(message["status"], force=True) - except (ConnectionClosedError, ConnectionClosedOK) as ex: - return - except Exception as ex: - print(ex) - return - finally: - self._mon = None - - -# Automatically connect to MicroSAXS testbench if directly invoked -if __name__ == "__main__": - daq = StdDaqWsClient(name="daq", url="ws://xbl-daq-29:8080") - daq.wait_for_connection() From 7f8dd3a4bb031d2696f5694d919b927a35de84ff Mon Sep 17 00:00:00 2001 From: Istvan Mohacsi Date: Mon, 22 Jul 2024 17:59:11 +0200 Subject: [PATCH 27/47] Daili commit coming to shape --- .gitignore | 1 + tomcat_bec/devices/gigafrost/gfconstants.py | 6 + .../devices/gigafrost/gigafrostcamera.py | 696 +++++++++++++++ .../devices/gigafrost/gigafrostclient.py | 840 ++++-------------- 4 files changed, 893 insertions(+), 650 deletions(-) create mode 100644 tomcat_bec/devices/gigafrost/gigafrostcamera.py diff --git a/.gitignore b/.gitignore index f4c73aa..4c2ec57 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ share/python-wheels/ *.egg-info/ .installed.cfg *.egg +*.bak MANIFEST # PyInstaller diff --git a/tomcat_bec/devices/gigafrost/gfconstants.py b/tomcat_bec/devices/gigafrost/gfconstants.py index 536c9df..f7786bf 100644 --- a/tomcat_bec/devices/gigafrost/gfconstants.py +++ b/tomcat_bec/devices/gigafrost/gfconstants.py @@ -9,6 +9,12 @@ Created on Thu Jun 27 17:28:43 2024 from enum import Enum +gf_valid_enable_modes = ("soft", "external", "soft+ext", "always") +gf_valid_exposure_modes = ("external", "timer", "soft") +gf_valid_trigger_modes = ("auto", "external", "timer", "soft") +gf_valid_fix_nframe_modes = ("off", "start", "end", "start+end") + + # STATUS class GfStatus(Enum): """Operation states for GigaFrost Ophyd device""" diff --git a/tomcat_bec/devices/gigafrost/gigafrostcamera.py b/tomcat_bec/devices/gigafrost/gigafrostcamera.py new file mode 100644 index 0000000..a63b217 --- /dev/null +++ b/tomcat_bec/devices/gigafrost/gigafrostcamera.py @@ -0,0 +1,696 @@ +# -*- coding: utf-8 -*- +""" +GigaFrost camera class module + +Created on Thu Jun 27 17:28:43 2024 + +@author: mohacsi_i +""" +import sys +from time import sleep +from ophyd import Device, Component, EpicsSignal, EpicsSignalRO, Kind, DeviceStatus +from ophyd.device import Staged + +from ophyd_devices.interfaces.base_classes.psi_detector_base import ( + CustomDetectorMixin, + PSIDetectorBase, +) + +try: + import gfconstants as const +except ModuleNotFoundError: + import tomcat_bec.devices.gigafrost.gfconstants as const + +try: + from gfutils import extend_header_table +except ModuleNotFoundError: + from tomcat_bec.devices.gigafrost.gfutils import extend_header_table + + +class GigaFrostCameraMixin(CustomDetectorMixin): + """Mixin class to setup TOMCAT specific implementations of the detector. + + This class will be called by the custom_prepare_cls attribute of the detector class. + """ + def _define_backend_ip(self): + if self.parent.backendUrl.get() == const.BE3_DAFL_CLIENT: # xbl-daq-33 + return const.BE3_NORTH_IP, const.BE3_SOUTH_IP + elif self.parent.backendUrl.get() == const.BE999_DAFL_CLIENT: + return const.BE999_NORTH_IP, const.BE999_SOUTH_IP + else: + raise RuntimeError(f"Backend not recognized. {(const.GF1, const.GF2, const.GF3)}") + + def _define_backend_mac(self): + if self.parent.backendUrl.get() == const.BE3_DAFL_CLIENT: # xbl-daq-33 + return const.BE3_NORTH_MAC, const.BE3_SOUTH_MAC + elif self.parent.backendUrl.get() == const.BE999_DAFL_CLIENT: + return const.BE999_NORTH_MAC, const.BE999_SOUTH_MAC + else: + raise RuntimeError(f"Backend not recognized. {(const.GF1, const.GF2, const.GF3)}") + + def _set_udp_header_table(self): + """Set the communication parameters for the camera module""" + self.parent.cfgConnectionParam.set(self._build_udp_header_table()).wait() + + def _build_udp_header_table(self): + """Build the header table for the communication""" + udp_header_table = [] + + for i in range(0, 64, 1): + for j in range(0, 8, 1): + dest_port = 2000 + 8 * i + j + source_port = 3000 + j + if j < 4: + extend_header_table( + udp_header_table, self.parent.macSouth, self.parent.ipSouth, dest_port, source_port + ) + else: + extend_header_table( + udp_header_table, self.parent.macNorth, self.parent.ipNorth, dest_port, source_port + ) + + return udp_header_table + + + def on_init(self) -> None: + """Initialize the camera, set channel values""" + ## Stop acquisition + self.parent.cmdStartCamera.set(0).wait() + + ### set entry to UDP table + # number of UDP ports to use + self.parent.cfgUdpNumPorts.set(2).wait() + # number of images to send to each UDP port before switching to next + self.parent.cfgUdpNumFrames.set(5).wait() + # offset in UDP table - where to find the first entry + self.parent.cfgUdpHtOffset.set(0).wait() + # activate changes + self.parent.cmdWriteService.set(1).wait() + + # Configure software triggering if needed + if self.parent._auto_soft_enable: + # trigger modes + self.parent.cfgCntStartBit.set(1).wait() + self.parent.cfgCntEndBit.set(0).wait() + + # set modes + self.parent.enable_mode = "soft" + self.parent.trigger_mode = "auto" + self.parent.exposure_mode = "timer" + + # line swap - on for west, off for east + self.parent.cfgLineSwapSW.set(1).wait() + self.parent.cfgLineSwapNW.set(1).wait() + self.parent.cfgLineSwapSE.set(0).wait() + self.parent.cfgLineSwapNE.set(0).wait() + + # Commit parameters + self.parent.cmdSetParam.set(1).wait() + + # Initialize data backend + n, s = self._define_backend_ip() + self.parent.ipNorth.put(n, force=True) + self.parent.ipSouth.put(s, force=True) + n, s = self._define_backend_mac() + self.parent.macNorth.put(n, force=True) + self.parent.macSouth.put(s, force=True) + # Set udp header table + self._set_udp_header_table() + + self.parent.state.put(const.GfStatus.INIT, force=True) + return super().on_init() + + + + def on_stage(self) -> None: + """ + Specify actions to be executed during stage in preparation for a scan. + self.parent.scaninfo already has all current parameters for the upcoming scan. + + In case the backend service is writing data on disk, this step should include publishing + a file_event and file_message to BEC to inform the system where the data is written to. + + IMPORTANT: + It must be safe to assume that the device is ready for the scan + to start immediately once this function is finished. + """ + if self.parent.infoBusyFlag.value: + raise RuntimeError("Camera is already busy, unstage it first!") + # Switch to acquiring + self.parent.cmdStartCamera.set(1).wait() + self.parent.state.put(const.GfStatus.ACQUIRING, force=True) + # Gigafrost can finish a run without explicit unstaging + self.parent._staged = Staged.no + + def on_unstage(self) -> None: + """ + Specify actions to be executed during unstage. + + This step should include checking if the acqusition was successful, + and publishing the file location and file event message, + with flagged done to BEC. + """ + # Switch to idle + self.parent.cmdStartCamera.set(0).wait() + if self.parent.autoSoftEnable.get(): + self.cmdSoftEnable.set(0).wait() + self.parent.state.put(const.GfStatus.STOPPED, force=True) + + def on_stop(self) -> None: + """ + Specify actions to be executed during stop. + This must also set self.parent.stopped to True. + + This step should include stopping the detector and backend service. + """ + return self.on_unstage() + + def on_trigger(self) -> None | DeviceStatus: + """ + Specify actions to be executed upon receiving trigger signal. + Return a DeviceStatus object or None + """ + status = DeviceStatus(self.parent) + + # Soft triggering based on operation mode + if self.parent.autoSoftEnable.get() and self.parent.trigger_mode=='auto' and self.parent.enable_mode=='soft': + # BEC teststand operation mode: posedge of SoftEnable if Started + self.parent.cmdSoftEnable.set(0).wait() + self.parent.cmdSoftEnable.set(1).wait() + sleep_time = self.parent.cfgFramerate.value*self.parent.cfgCntNum.value*0.001+0.050 + # There's no status readback from the camera, so we just wait + sleep(sleep_time) + print(f"[GF2] Slept for: {sleep_time} seconds", file=sys.stderr) + else: + self.parent.cmdSoftTrigger.set(1).wait() + status.set_finished() + + return status + + +class GigaFrostCamera(PSIDetectorBase): + """Ophyd device class to control Gigafrost cameras at Tomcat + + The actual hardware is implemented by an IOC based on an old fork of Helge's + cameras. This means that the camera behaves differently than the SF cameras + in particular it provides even less feedback about it's internal progress. + Helge will update the GigaFrost IOC after working beamline. + The ophyd class is based on the 'gfclient' package and has a lot of Tomcat + specific additions. It does behave differently though, as ophyd swallows the + errors from failed PV writes. + + Parameters + ---------- + use_soft_enable : bool + Flag to use the camera's soft enable (default: False) + backend_url : str + Backend url address necessary to set up the camera's udp header. + (default: http://xbl-daq-23:8080) + + Bugs: + ---------- + FRAMERATE : Ignored in soft trigger mode, period becomes 2xexposure time + """ + # pylint: disable=too-many-instance-attributes + + custom_prepare_cls = GigaFrostCameraMixin + + + + infoBusyFlag = Component(EpicsSignalRO, "BUSY_STAT", auto_monitor=True) + infoSyncFlag = Component(EpicsSignalRO, "SYNC_FLAG", auto_monitor=True) + cmdSyncHw = Component(EpicsSignal, "SYNC_SWHW.PROC", put_complete=True) + cmdStartCamera = Component(EpicsSignal, "START_CAM", put_complete=True) + cmdSetParam = Component(EpicsSignal, "SET_PARAM.PROC", put_complete=True) + + # UDP header + cfgUdpNumPorts = Component(EpicsSignal, "PORTS", put_complete=True, kind=Kind.config) + cfgUdpNumFrames = Component(EpicsSignal, "FRAMENUM", put_complete=True, kind=Kind.config) + cfgUdpHtOffset = Component(EpicsSignal, "HT_OFFSET", put_complete=True, kind=Kind.config) + cmdWriteService = Component(EpicsSignal, "WRITE_SRV.PROC", put_complete=True) + + # Standard camera configs + cfgExposure = Component(EpicsSignal, "EXPOSURE", put_complete=True, auto_monitor=True, kind=Kind.config) + cfgFramerate = Component(EpicsSignal, "FRAMERATE", put_complete=True, auto_monitor=True, kind=Kind.config) + cfgRoiX = Component(EpicsSignal, "ROIX", put_complete=True, auto_monitor=True, kind=Kind.config) + cfgRoiY = Component(EpicsSignal, "ROIY", put_complete=True, auto_monitor=True, kind=Kind.config) + cfgScanId = Component(EpicsSignal, "SCAN_ID", put_complete=True, auto_monitor=True, kind=Kind.config) + cfgCntNum = Component(EpicsSignal, "CNT_NUM", put_complete=True, auto_monitor=True, kind=Kind.config) + cfgCorrMode = Component(EpicsSignal, "CORR_MODE", put_complete=True, auto_monitor=True, kind=Kind.config) + + # Software signals + cmdSoftEnable = Component(EpicsSignal, "SOFT_ENABLE", put_complete=True) + cmdSoftTrigger = Component(EpicsSignal, "SOFT_TRIG.PROC", put_complete=True) + cmdSoftExposure = Component(EpicsSignal, "SOFT_EXP", put_complete=True) + + # Trigger configuration PVs + cfgCntStartBit = Component( + EpicsSignal, + "CNT_STARTBIT_RBV", + write_pv="CNT_STARTBIT", + put_complete=True, + kind=Kind.config, + ) + cfgCntEndBit = Component( + EpicsSignal, "CNT_ENDBIT_RBV", write_pv="CNT_ENDBIT", put_complete=True, kind=Kind.config + ) + # Enable modes + cfgTrigEnableExt = Component( + EpicsSignal, + "MODE_ENBL_EXT_RBV", + write_pv="MODE_ENBL_EXT", + put_complete=True, + kind=Kind.config, + ) + cfgTrigEnableSoft = Component( + EpicsSignal, + "MODE_ENBL_SOFT_RBV", + write_pv="MODE_ENBL_SOFT", + put_complete=True, + kind=Kind.config, + ) + cfgTrigEnableAuto = Component( + EpicsSignal, + "MODE_ENBL_AUTO_RBV", + write_pv="MODE_ENBL_AUTO", + put_complete=True, + kind=Kind.config, + ) + cfgTrigVirtEnable = Component( + EpicsSignal, + "MODE_ENBL_EXP_RBV", + write_pv="MODE_ENBL_EXP", + put_complete=True, + kind=Kind.config, + ) + # Trigger modes + cfgTrigExt = Component( + EpicsSignal, + "MODE_TRIG_EXT_RBV", + write_pv="MODE_TRIG_EXT", + put_complete=True, + kind=Kind.config, + ) + cfgTrigSoft = Component( + EpicsSignal, + "MODE_TRIG_SOFT_RBV", + write_pv="MODE_TRIG_SOFT", + put_complete=True, + kind=Kind.config, + ) + cfgTrigTimer = Component( + EpicsSignal, + "MODE_TRIG_TIMER_RBV", + write_pv="MODE_TRIG_TIMER", + put_complete=True, + kind=Kind.config, + ) + cfgTrigAuto = Component( + EpicsSignal, + "MODE_TRIG_AUTO_RBV", + write_pv="MODE_TRIG_AUTO", + put_complete=True, + kind=Kind.config, + ) + # Exposure modes + cfgTrigExpExt = Component( + EpicsSignal, + "MODE_EXP_EXT_RBV", + write_pv="MODE_EXP_EXT", + put_complete=True, + kind=Kind.config, + ) + cfgTrigExpSoft = Component( + EpicsSignal, + "MODE_EXP_SOFT_RBV", + write_pv="MODE_EXP_SOFT", + put_complete=True, + kind=Kind.config, + ) + cfgTrigExpTimer = Component( + EpicsSignal, + "MODE_EXP_TIMER_RBV", + write_pv="MODE_EXP_TIMER", + put_complete=True, + kind=Kind.config, + ) + + # Line swap selection + cfgLineSwapSW = Component(EpicsSignal, "LS_SW", put_complete=True, kind=Kind.config) + cfgLineSwapNW = Component(EpicsSignal, "LS_NW", put_complete=True, kind=Kind.config) + cfgLineSwapSE = Component(EpicsSignal, "LS_SE", put_complete=True, kind=Kind.config) + cfgLineSwapNE = Component(EpicsSignal, "LS_NE", put_complete=True, kind=Kind.config) + cfgConnectionParam = Component( + EpicsSignal, "CONN_PARM", string=True, put_complete=True, kind=Kind.config + ) + + # HW settings as read only + cfgSyncFlag = Component(EpicsSignalRO, "PIXRATE", auto_monitor=True) + cfgTrigDelay = Component(EpicsSignalRO, "TRIG_DELAY", auto_monitor=True) + cfgSyncoutDelay = Component(EpicsSignalRO, "SYNCOUT_DLY", auto_monitor=True) + cfgOutputPolarity0 = Component(EpicsSignalRO, "BNC0_RBV", auto_monitor=True) + cfgOutputPolarity1 = Component(EpicsSignalRO, "BNC1_RBV", auto_monitor=True) + cfgOutputPolarity2 = Component(EpicsSignalRO, "BNC2_RBV", auto_monitor=True) + cfgOutputPolarity3 = Component(EpicsSignalRO, "BNC3_RBV", auto_monitor=True) + cfgInputPolarity1 = Component(EpicsSignalRO, "BNC4_RBV", auto_monitor=True) + cfgInputPolarity2 = Component(EpicsSignalRO, "BNC5_RBV", auto_monitor=True) + infoBoardTemp = Component(EpicsSignalRO, "T_BOARD", auto_monitor=True) + + USER_ACCESS = ["exposure_mode", "fix_nframes_mode", "trigger_mode", "enable_mode"] + + autoSoftEnable = Component(Signal, auto_monitor=True, kind=Kind.config) + backendUrl = Component(Signal, auto_monitor=True, kind=Kind.config) + state = Component(Signal, auto_monitor=True, kind=Kind.config) + + def __init__( + self, + prefix="", + *, + name, + auto_soft_enable=False, + timeout=10, + backend_url=const.BE999_DAFL_CLIENT, + kind=None, + read_attrs=None, + configuration_attrs=None, + parent=None, + **kwargs, + ): + # Additional parameters + self.autoSoftEnable._metadata["write_access"] = False + self.backendUrl._metadata["write_access"] = False + self.state._metadata["write_access"] = False + self.autoSoftEnable.put(auto_soft_enable, force=True) + self.backendUrl.put(backend_url, force=True) + self.state.put(const.GfStatus.NEW, force=True) + + # super() will call the mixin class + super().__init__( + prefix=prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + **kwargs, + ) + + def configure( + self, + nimages=10, + exposure=0.2, + period=1.0, + roix=2016, + roiy=2016, + scanid=0, + correction_mode=5, + ): + """Configure the next scan with the GigaFRoST camera + + Parameters + ---------- + nimages : int, optional + Number of images to be taken during each scan. Set to -1 for an + unlimited number of images (limited by the ringbuffer size and + backend speed). (default = 10) + exposure : float, optional + Exposure time [ms]. (default = 0.2) + period : float, optional + Exposure period [ms], ignored in soft trigger mode. (default = 1.0) + roix : int, optional + ROI size in the x-direction [pixels] (default = 2016) + roiy : int, optional + ROI size in the y-direction [pixels] (default = 2016) + scanid : int, optional + Scan identification number to be associated with the scan data + (default = 0) + correction_mode : int, optional + The correction to be applied to the imaging data. The following + modes are available (default = 5): + + * 0: Bypass. No corrections are applied to the data. + * 1: Send correction factor A instead of pixel values + * 2: Send correction factor B instead of pixel values + * 3: Send correction factor C instead of pixel values + * 4: Invert pixel values, but do not apply any linearity correction + * 5: Apply the full linearity correction + """ + # If Bluesky style configure + if isinstance(nimages, dict): + d = nimages.copy() + nimages = d.get('nimages', 10) + exposure = d.get('exposure', exposure) + period = d.get('period', period) + roix = d.get('roix', roix) + roiy = d.get('roiy', roiy) + scanid = d.get('scanid', scanid) + correction_mode = d.get('correction_mode', correction_mode) + + # Stop acquisition + self.cmdStartCamera.set(0).wait() + if self._auto_soft_enable: + self.cmdSoftEnable.set(0).wait() + + # change settings + self.cfgExposure.set(exposure).wait() + self.cfgFramerate.set(period).wait() + self.cfgRoiX.set(roix).wait() + self.cfgRoiY.set(roiy).wait() + self.cfgScanId.set(scanid).wait() + self.cfgCntNum.set(nimages).wait() + self.cfgCorrMode.set(correction_mode).wait() + + # Commit parameter + self.cmdSetParam.set(1).wait() + self.state.set(const.GfStatus.CONFIGURED, force=True) + + @property + def exposure_mode(self): + """Returns the current exposure mode of the GigaFRost camera. + + Returns + ------- + exp_mode : {'external', 'timer', 'soft'} + The camera's active exposure mode. + If more than one mode is active at the same time, it returns None. + """ + mode_soft = self.cfgTrigExpSoft.get() + mode_timer = self.cfgTrigExpTimer.get() + mode_external = self.cfgTrigExpExt.get() + if mode_soft and not mode_timer and not mode_external: + return "soft" + elif not mode_soft and mode_timer and not mode_external: + return "timer" + elif not mode_soft and not mode_timer and mode_external: + return "external" + else: + return None + + @exposure_mode.setter + def exposure_mode(self, exp_mode): + """Apply the exposure mode for the GigaFRoST camera. + + Parameters + ---------- + exp_mode : {'external', 'timer', 'soft'} + The exposure mode to be set. + """ + if exp_mode not in const.gf_valid_exposure_modes: + raise ValueError( + f"Invalid exposure mode! Valid modes are:\n" "{const.gf_valid_exposure_modes}" + ) + + if exp_mode == "external": + self.cfgTrigExpExt.set(1).wait() + self.cfgTrigExpSoft.set(0).wait() + self.cfgTrigExpTimer.set(0).wait() + elif exp_mode == "timer": + self.cfgTrigExpExt.set(0).wait() + self.cfgTrigExpSoft.set(0).wait() + self.cfgTrigExpTimer.set(1).wait() + elif exp_mode == "soft": + self.cfgTrigExpExt.set(0).wait() + self.cfgTrigExpSoft.set(1).wait() + self.cfgTrigExpTimer.set(0).wait() + # Commit parameters + self.cmdSetParam.set(1).wait() + + @property + def fix_nframes_mode(self): + """Return the current fixed number of frames mode of the GigaFRoST camera. + + Returns + ------- + fix_nframes_mode : {'off', 'start', 'end', 'start+end'} + The camera's active fixed number of frames mode. + """ + start_bit = self.cfgCntStartBit.get() + end_bit = self.cfgCntStartBit.get() + + if not start_bit and not end_bit: + return "off" + elif start_bit and not end_bit: + return "start" + elif not start_bit and end_bit: + return "end" + elif start_bit and end_bit: + return "start+end" + else: + return None + + @fix_nframes_mode.setter + def fix_nframes_mode(self, mode): + """Apply the fixed number of frames settings to the GigaFRoST camera. + + Parameters + ---------- + mode : {'off', 'start', 'end', 'start+end'} + The fixed number of frames mode to be applied. + """ + if mode not in const.gf_valid_fix_nframe_modes: + raise ValueError( + f"Invalid fixed number of frames mode! Valid modes are:\n{const.gf_valid_fix_nframe_modes}" + ) + + self._fix_nframes_mode = mode + if self._fix_nframes_mode == "off": + self.cfgCntStartBit.set(0).wait() + self.cfgCntEndBit.set(0).wait() + elif self._fix_nframes_mode == "start": + self.cfgCntStartBit.set(1).wait() + self.cfgCntEndBit.set(0).wait() + elif self._fix_nframes_mode == "end": + self.cfgCntStartBit.set(0).wait() + self.cfgCntEndBit.set(1).wait() + elif self._fix_nframes_mode == "start+end": + self.cfgCntStartBit.set(1).wait() + self.cfgCntEndBit.set(1).wait() + # Commit parameters + self.cmdSetParam.set(1).wait() + + @property + def trigger_mode(self): + """Method to detect the current trigger mode set in the GigaFRost camera. + + Returns + ------- + mode : {'auto', 'external', 'timer', 'soft'} + The camera's active trigger mode. If more than one mode is active + at the moment, None is returned. + """ + mode_auto = self.cfgTrigAuto.get() + mode_external = self.cfgTrigExt.get() + mode_timer = self.cfgTrigTimer.get() + mode_soft = self.cfgTrigSoft.get() + if mode_auto: + return "auto" + elif mode_soft: + return "soft" + elif mode_timer: + return "timer" + elif mode_external: + return "external" + else: + return None + + @trigger_mode.setter + def trigger_mode(self, mode): + """Set the trigger mode for the GigaFRoST camera. + + Parameters + ---------- + mode : {'auto', 'external', 'timer', 'soft'} + The GigaFRoST trigger mode. + """ + if mode not in const.gf_valid_trigger_modes: + raise ValueError( + "Invalid trigger mode! Valid modes are:\n" "{const.gf_valid_trigger_modes}" + ) + + if mode == "auto": + self.cfgTrigAuto.set(1).wait() + self.cfgTrigSoft.set(0).wait() + self.cfgTrigTimer.set(0).wait() + self.cfgTrigExt.set(0).wait() + elif mode == "external": + self.cfgTrigAuto.set(0).wait() + self.cfgTrigSoft.set(0).wait() + self.cfgTrigTimer.set(0).wait() + self.cfgTrigExt.set(1).wait() + elif mode == "timer": + self.cfgTrigAuto.set(0).wait() + self.cfgTrigSoft.set(0).wait() + self.cfgTrigTimer.set(1).wait() + self.cfgTrigExt.set(0).wait() + elif mode == "soft": + self.cfgTrigAuto.set(0).wait() + self.cfgTrigSoft.set(1).wait() + self.cfgTrigTimer.set(0).wait() + self.cfgTrigExt.set(0).wait() + # Commit parameters + self.cmdSetParam.set(1).wait() + + @property + def enable_mode(self): + """Return the enable mode set in the GigaFRost camera. + + Returns + ------- + enable_mode: {'soft', 'external', 'soft+ext', 'always'} + The camera's active enable mode. + """ + mode_soft = self.cfgTrigEnableSoft.get() + mode_external = self.cfgTrigEnableExt.get() + mode_auto = self.cfgTrigEnableAuto.get() + if mode_soft and not mode_auto: + if mode_external: + return "soft+ext" + else: + return "soft" + elif mode_auto and not mode_soft and not mode_external: + return "always" + elif mode_external and not mode_soft and not mode_auto: + return "external" + else: + return None + + @enable_mode.setter + def enable_mode(self, mode): + """Apply the enable mode for the GigaFRoST camera. + + Parameters + ---------- + mode : {'soft', 'external', 'soft+ext', 'always'} + The enable mode to be applied. + """ + if mode not in const.gf_valid_enable_modes: + raise ValueError( + "Invalid enable mode! Valid modes are:\n" "{const.gf_valid_enable_modes}" + ) + + if mode == "soft": + self.cfgTrigEnableExt.set(0).wait() + self.cfgTrigEnableSoft.set(1).wait() + self.cfgTrigEnableAuto.set(0).wait() + elif mode == "external": + self.cfgTrigEnableExt.set(1).wait() + self.cfgTrigEnableSoft.set(0).wait() + self.cfgTrigEnableAuto.set(0).wait() + elif mode == "soft+ext": + self.cfgTrigEnableExt.set(1).wait() + self.cfgTrigEnableSoft.set(1).wait() + self.cfgTrigEnableAuto.set(0).wait() + elif mode == "always": + self.cfgTrigEnableExt.set(0).wait() + self.cfgTrigEnableSoft.set(0).wait() + self.cfgTrigEnableAuto.set(1).wait() + # Commit parameters + self.cmdSetParam.set(1).wait() + + +# Automatically connect to MicroSAXS testbench if directly invoked +if __name__ == "__main__": + gf = GigaFrostCamera( + "X02DA-CAM-GF2:", name="gf2", backend_url="http://xbl-daq-28:8080", auto_soft_enable=True + ) + gf.wait_for_connection() diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index 9c610c8..27f6df0 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -11,18 +11,190 @@ from time import sleep from ophyd import Device, Component, EpicsSignal, EpicsSignalRO, Kind, DeviceStatus from ophyd.device import Staged -try: - import gfconstants as const -except ModuleNotFoundError: - import tomcat_bec.devices.gigafrost.gfconstants as const +from ophyd_devices.interfaces.base_classes.psi_detector_base import ( + CustomDetectorMixin, + PSIDetectorBase, +) + try: - from gfutils import extend_header_table + from StdDaqClient import StdDaqClient except ModuleNotFoundError: - from tomcat_bec.devices.gigafrost.gfutils import extend_header_table + from tomcat_bec.devices.gigafrost.StdDaqClient import StdDaqClient + +try: + from gfcamera import GigaFrostCamera +except ModuleNotFoundError: + from tomcat_bec.devices.gigafrost.gfcamera import GigaFrostCamera -class GigaFrostClient(Device): + + + + +class GigafrostClientMixin(CustomDetectorMixin): + """Mixin class to setup TOMCAT specific implementations of the detector. + + This class will be called by the custom_prepare_cls attribute of the detector class. + """ + + def _define_backend_ip(self): + if self.parent.backendUrl.get() == const.BE3_DAFL_CLIENT: # xbl-daq-33 + return const.BE3_NORTH_IP, const.BE3_SOUTH_IP + elif self.parent.backendUrl.get() == const.BE999_DAFL_CLIENT: + return const.BE999_NORTH_IP, const.BE999_SOUTH_IP + else: + raise RuntimeError(f"Backend not recognized. {(const.GF1, const.GF2, const.GF3)}") + + def _define_backend_mac(self): + if self.parent.backendUrl.get() == const.BE3_DAFL_CLIENT: # xbl-daq-33 + return const.BE3_NORTH_MAC, const.BE3_SOUTH_MAC + elif self.parent.backendUrl.get() == const.BE999_DAFL_CLIENT: + return const.BE999_NORTH_MAC, const.BE999_SOUTH_MAC + else: + raise RuntimeError(f"Backend not recognized. {(const.GF1, const.GF2, const.GF3)}") + + def _set_udp_header_table(self): + """Set the communication parameters for the camera module""" + self.parent.cfgConnectionParam.set(self._build_udp_header_table()).wait() + + def _build_udp_header_table(self): + """Build the header table for the communication""" + udp_header_table = [] + + for i in range(0, 64, 1): + for j in range(0, 8, 1): + dest_port = 2000 + 8 * i + j + source_port = 3000 + j + if j < 4: + extend_header_table( + udp_header_table, self.parent.macSouth, self.parent.ipSouth, dest_port, source_port + ) + else: + extend_header_table( + udp_header_table, self.parent.macNorth, self.parent.ipNorth, dest_port, source_port + ) + + return udp_header_table + + + def on_init(self) -> None: + """Initialize the camera, set channel values""" + ## Stop acquisition + self.parent.cmdStartCamera.set(0).wait() + + ### set entry to UDP table + # number of UDP ports to use + self.parent.cfgUdpNumPorts.set(2).wait() + # number of images to send to each UDP port before switching to next + self.parent.cfgUdpNumFrames.set(5).wait() + # offset in UDP table - where to find the first entry + self.parent.cfgUdpHtOffset.set(0).wait() + # activate changes + self.parent.cmdWriteService.set(1).wait() + + # Configure software triggering if needed + if self.parent._auto_soft_enable: + # trigger modes + self.parent.cfgCntStartBit.set(1).wait() + self.parent.cfgCntEndBit.set(0).wait() + + # set modes + self.parent.enable_mode = "soft" + self.parent.trigger_mode = "auto" + self.parent.exposure_mode = "timer" + + # line swap - on for west, off for east + self.parent.cfgLineSwapSW.set(1).wait() + self.parent.cfgLineSwapNW.set(1).wait() + self.parent.cfgLineSwapSE.set(0).wait() + self.parent.cfgLineSwapNE.set(0).wait() + + # Commit parameters + self.parent.cmdSetParam.set(1).wait() + + # Initialize data backend + n, s = self._define_backend_ip() + self.parent.ipNorth.put(n, force=True) + self.parent.ipSouth.put(s, force=True) + n, s = self._define_backend_mac() + self.parent.macNorth.put(n, force=True) + self.parent.macSouth.put(s, force=True) + # Set udp header table + self._set_udp_header_table() + + self.parent.state.put(const.GfStatus.INIT, force=True) + return super().on_init() + + + + def on_stage(self) -> None: + """ + Specify actions to be executed during stage in preparation for a scan. + self.parent.scaninfo already has all current parameters for the upcoming scan. + + In case the backend service is writing data on disk, this step should include publishing + a file_event and file_message to BEC to inform the system where the data is written to. + + IMPORTANT: + It must be safe to assume that the device is ready for the scan + to start immediately once this function is finished. + """ + if self.parent.infoBusyFlag.value: + raise RuntimeError("Camera is already busy, unstage it first!") + # Switch to acquiring + self.parent.cmdStartCamera.set(1).wait() + self.parent.state.put(const.GfStatus.ACQUIRING, force=True) + # Gigafrost can finish a run without explicit unstaging + self.parent._staged = Staged.no + + def on_unstage(self) -> None: + """ + Specify actions to be executed during unstage. + + This step should include checking if the acqusition was successful, + and publishing the file location and file event message, + with flagged done to BEC. + """ + # Switch to idle + self.parent.cmdStartCamera.set(0).wait() + if self.parent.autoSoftEnable.get(): + self.cmdSoftEnable.set(0).wait() + self.parent.state.put(const.GfStatus.STOPPED, force=True) + + def on_stop(self) -> None: + """ + Specify actions to be executed during stop. + This must also set self.parent.stopped to True. + + This step should include stopping the detector and backend service. + """ + return self.on_unstage() + + def on_trigger(self) -> None | DeviceStatus: + """ + Specify actions to be executed upon receiving trigger signal. + Return a DeviceStatus object or None + """ + status = DeviceStatus(self.parent) + + # Soft triggering based on operation mode + if self.parent.autoSoftEnable.get() and self.parent.trigger_mode=='auto' and self.parent.enable_mode=='soft': + # BEC teststand operation mode: posedge of SoftEnable if Started + self.parent.cmdSoftEnable.set(0).wait() + self.parent.cmdSoftEnable.set(1).wait() + sleep_time = self.parent.cfgFramerate.value*self.parent.cfgCntNum.value*0.001+0.050 + # There's no status readback from the camera, so we just wait + sleep(sleep_time) + print(f"[GF2] Slept for: {sleep_time} seconds", file=sys.stderr) + else: + self.parent.cmdSoftTrigger.set(1).wait() + status.set_finished() + + return status + + +class GigaFrostClient(PSIDetectorBase): """Ophyd device class to control Gigafrost cameras at Tomcat The actual hardware is implemented by an IOC based on an old fork of Helge's @@ -47,654 +219,22 @@ class GigaFrostClient(Device): """ # pylint: disable=too-many-instance-attributes - infoBusyFlag = Component(EpicsSignalRO, "BUSY_STAT", auto_monitor=True) - infoSyncFlag = Component(EpicsSignalRO, "SYNC_FLAG", auto_monitor=True) - cmdSyncHw = Component(EpicsSignal, "SYNC_SWHW.PROC", put_complete=True) - cmdStartCamera = Component(EpicsSignal, "START_CAM", put_complete=True) - cmdSetParam = Component(EpicsSignal, "SET_PARAM.PROC", put_complete=True) - - # UDP header - cfgUdpNumPorts = Component(EpicsSignal, "PORTS", put_complete=True, kind=Kind.config) - cfgUdpNumFrames = Component(EpicsSignal, "FRAMENUM", put_complete=True, kind=Kind.config) - cfgUdpHtOffset = Component(EpicsSignal, "HT_OFFSET", put_complete=True, kind=Kind.config) - cmdWriteService = Component(EpicsSignal, "WRITE_SRV.PROC", put_complete=True) - - # Standard camera configs - cfgExposure = Component(EpicsSignal, "EXPOSURE", put_complete=True, auto_monitor=True, kind=Kind.config) - cfgFramerate = Component(EpicsSignal, "FRAMERATE", put_complete=True, auto_monitor=True, kind=Kind.config) - cfgRoiX = Component(EpicsSignal, "ROIX", put_complete=True, auto_monitor=True, kind=Kind.config) - cfgRoiY = Component(EpicsSignal, "ROIY", put_complete=True, auto_monitor=True, kind=Kind.config) - cfgScanId = Component(EpicsSignal, "SCAN_ID", put_complete=True, auto_monitor=True, kind=Kind.config) - cfgCntNum = Component(EpicsSignal, "CNT_NUM", put_complete=True, auto_monitor=True, kind=Kind.config) - cfgCorrMode = Component(EpicsSignal, "CORR_MODE", put_complete=True, auto_monitor=True, kind=Kind.config) - - # Software signals - cmdSoftEnable = Component(EpicsSignal, "SOFT_ENABLE", put_complete=True) - cmdSoftTrigger = Component(EpicsSignal, "SOFT_TRIG.PROC", put_complete=True) - cmdSoftExposure = Component(EpicsSignal, "SOFT_EXP", put_complete=True) - - # Trigger configuration PVs - cfgCntStartBit = Component( - EpicsSignal, - "CNT_STARTBIT_RBV", - write_pv="CNT_STARTBIT", - put_complete=True, - kind=Kind.config, - ) - cfgCntEndBit = Component( - EpicsSignal, "CNT_ENDBIT_RBV", write_pv="CNT_ENDBIT", put_complete=True, kind=Kind.config - ) - # Enable modes - cfgTrigEnableExt = Component( - EpicsSignal, - "MODE_ENBL_EXT_RBV", - write_pv="MODE_ENBL_EXT", - put_complete=True, - kind=Kind.config, - ) - cfgTrigEnableSoft = Component( - EpicsSignal, - "MODE_ENBL_SOFT_RBV", - write_pv="MODE_ENBL_SOFT", - put_complete=True, - kind=Kind.config, - ) - cfgTrigEnableAuto = Component( - EpicsSignal, - "MODE_ENBL_AUTO_RBV", - write_pv="MODE_ENBL_AUTO", - put_complete=True, - kind=Kind.config, - ) - cfgTrigVirtEnable = Component( - EpicsSignal, - "MODE_ENBL_EXP_RBV", - write_pv="MODE_ENBL_EXP", - put_complete=True, - kind=Kind.config, - ) - # Trigger modes - cfgTrigExt = Component( - EpicsSignal, - "MODE_TRIG_EXT_RBV", - write_pv="MODE_TRIG_EXT", - put_complete=True, - kind=Kind.config, - ) - cfgTrigSoft = Component( - EpicsSignal, - "MODE_TRIG_SOFT_RBV", - write_pv="MODE_TRIG_SOFT", - put_complete=True, - kind=Kind.config, - ) - cfgTrigTimer = Component( - EpicsSignal, - "MODE_TRIG_TIMER_RBV", - write_pv="MODE_TRIG_TIMER", - put_complete=True, - kind=Kind.config, - ) - cfgTrigAuto = Component( - EpicsSignal, - "MODE_TRIG_AUTO_RBV", - write_pv="MODE_TRIG_AUTO", - put_complete=True, - kind=Kind.config, - ) - # Exposure modes - cfgTrigExpExt = Component( - EpicsSignal, - "MODE_EXP_EXT_RBV", - write_pv="MODE_EXP_EXT", - put_complete=True, - kind=Kind.config, - ) - cfgTrigExpSoft = Component( - EpicsSignal, - "MODE_EXP_SOFT_RBV", - write_pv="MODE_EXP_SOFT", - put_complete=True, - kind=Kind.config, - ) - cfgTrigExpTimer = Component( - EpicsSignal, - "MODE_EXP_TIMER_RBV", - write_pv="MODE_EXP_TIMER", - put_complete=True, - kind=Kind.config, - ) - - # Line swap selection - cfgLineSwapSW = Component(EpicsSignal, "LS_SW", put_complete=True, kind=Kind.config) - cfgLineSwapNW = Component(EpicsSignal, "LS_NW", put_complete=True, kind=Kind.config) - cfgLineSwapSE = Component(EpicsSignal, "LS_SE", put_complete=True, kind=Kind.config) - cfgLineSwapNE = Component(EpicsSignal, "LS_NE", put_complete=True, kind=Kind.config) - cfgConnectionParam = Component( - EpicsSignal, "CONN_PARM", string=True, put_complete=True, kind=Kind.config - ) - - # HW settings as read only - cfgSyncFlag = Component(EpicsSignalRO, "PIXRATE", auto_monitor=True) - cfgTrigDelay = Component(EpicsSignalRO, "TRIG_DELAY", auto_monitor=True) - cfgSyncoutDelay = Component(EpicsSignalRO, "SYNCOUT_DLY", auto_monitor=True) - cfgOutputPolarity0 = Component(EpicsSignalRO, "BNC0_RBV", auto_monitor=True) - cfgOutputPolarity1 = Component(EpicsSignalRO, "BNC1_RBV", auto_monitor=True) - cfgOutputPolarity2 = Component(EpicsSignalRO, "BNC2_RBV", auto_monitor=True) - cfgOutputPolarity3 = Component(EpicsSignalRO, "BNC3_RBV", auto_monitor=True) - cfgInputPolarity1 = Component(EpicsSignalRO, "BNC4_RBV", auto_monitor=True) - cfgInputPolarity2 = Component(EpicsSignalRO, "BNC5_RBV", auto_monitor=True) - infoBoardTemp = Component(EpicsSignalRO, "T_BOARD", auto_monitor=True) - - USER_ACCESS = ["exposure_mode", "fix_nframes_mode", "trigger_mode", "enable_mode"] - - def __init__( - self, - prefix="", - *, - name, - auto_soft_enable=False, - timeout=10, - backend_url=const.BE999_DAFL_CLIENT, - kind=None, - read_attrs=None, - configuration_attrs=None, - parent=None, - **kwargs, - ): - super().__init__( - prefix=prefix, - name=name, - kind=kind, - read_attrs=read_attrs, - configuration_attrs=configuration_attrs, - parent=parent, - **kwargs, - ) - self._auto_soft_enable = auto_soft_enable - self._timeout = timeout - self._backend_url = backend_url - - self.state = const.GfStatus.NEW - self._settings = None - self._north_mac, self._south_mac = self._define_backend_mac() - self._north_ip, self._south_ip = self._define_backend_ip() - - self._valid_enable_modes = ("soft", "external", "soft+ext", "always") - self._valid_exposure_modes = ("external", "timer", "soft") - self._valid_trigger_modes = ("auto", "external", "timer", "soft") - self._valid_fix_nframe_modes = ("off", "start", "end", "start+end") - - # Continue initialization - self.initialize() - - def initialize(self): - """Initialize the camera, set channel values""" - ## Stop acquisition - self.cmdStartCamera.set(0).wait() - - ### set entry to UDP table - # number of UDP ports to use - self.cfgUdpNumPorts.set(2).wait() - # number of images to send to each UDP port before switching to next - self.cfgUdpNumFrames.set(5).wait() - # offset in UDP table - where to find the first entry - self.cfgUdpHtOffset.set(0).wait() - # activate changes - self.cmdWriteService.set(1).wait() - - # Configure software triggering if needed - if self._auto_soft_enable: - # trigger modes - self.cfgCntStartBit.set(1).wait() - self.cfgCntEndBit.set(0).wait() - - # set modes - self.enable_mode = "soft" - self.trigger_mode = "auto" - self.exposure_mode = "timer" - - # line swap - on for west, off for east - self.cfgLineSwapSW.set(1).wait() - self.cfgLineSwapNW.set(1).wait() - self.cfgLineSwapSE.set(0).wait() - self.cfgLineSwapNE.set(0).wait() - - # Commit parameters - self.cmdSetParam.set(1).wait() - - self._set_udp_header_table() - self.state = const.GfStatus.INIT - - # sets the basic settings - self._settings = { - "backend_url": self._backend_url, - "auto_soft_enable": self._auto_soft_enable, - "ioc_name": self.prefix, - } - - def configure( - self, - nimages=10, - exposure=0.2, - period=1.0, - roix=2016, - roiy=2016, - scanid=0, - correction_mode=5, - ): - """Configure the next scan with the GigaFRoST camera - - Parameters - ---------- - nimages : int, optional - Number of images to be taken during each scan. Set to -1 for an - unlimited number of images (limited by the ringbuffer size and - backend speed). (default = 10) - exposure : float, optional - Exposure time [ms]. (default = 0.2) - period : float, optional - Exposure period [ms], ignored in soft trigger mode. (default = 1.0) - roix : int, optional - ROI size in the x-direction [pixels] (default = 2016) - roiy : int, optional - ROI size in the y-direction [pixels] (default = 2016) - scanid : int, optional - Scan identification number to be associated with the scan data - (default = 0) - correction_mode : int, optional - The correction to be applied to the imaging data. The following - modes are available (default = 5): - - * 0: Bypass. No corrections are applied to the data. - * 1: Send correction factor A instead of pixel values - * 2: Send correction factor B instead of pixel values - * 3: Send correction factor C instead of pixel values - * 4: Invert pixel values, but do not apply any linearity correction - * 5: Apply the full linearity correction - """ - # If Bluesky style configure - if isinstance(nimages, dict): - d = nimages.copy() - nimages = d.get('nimages', 10) - exposure = d.get('exposure', exposure) - period = d.get('period', period) - roix = d.get('roix', roix) - roiy = d.get('roiy', roiy) - scanid = d.get('scanid', scanid) - correction_mode = d.get('correction_mode', correction_mode) - - # Stop acquisition - self.cmdStartCamera.set(0).wait() - if self._auto_soft_enable: - self.cmdSoftEnable.set(0).wait() - - # change settings - self.cfgExposure.set(exposure).wait() - self.cfgFramerate.set(period).wait() - self.cfgRoiX.set(roix).wait() - self.cfgRoiY.set(roiy).wait() - self.cfgScanId.set(scanid).wait() - self.cfgCntNum.set(nimages).wait() - self.cfgCorrMode.set(correction_mode).wait() - - # Commit parameter - self.cmdSetParam.set(1).wait() - self.state = const.GfStatus.CONFIGURED - - self._settings = { - "nimages": nimages, - "exposure": exposure, - "frame_rate": period, - "roix": roix, - "roiy": roiy, - "scanid": scanid, - "correction_mode": correction_mode, - "backend_url": self._backend_url, - "auto_soft_enable": self._auto_soft_enable, - "ioc_name": self.name, - } + cam = Component(GigaFrostCamera, "", auto_monitor=True) + daq = Component(StdDaqClient, "", auto_monitor=True) def stage(self): - """Standard ophyd method to start an acquisition + px_daq_h = self.daq.config.cfg_image_pixel_height.get() + px_daq_w = self.daq.config.cfg_image_pixel_width.get() - In ophyd it still needs to be triggered to perfor an actual capture. - """ - if self.infoBusyFlag.value: - raise RuntimeError("Camera is already busy, unstage it first!") + px_gf_h = self.cam.cfgRoiX.get() + px_gf_y = self.cam.cfgRoiY.get() - # change to running - self.cmdStartCamera.set(1).wait() - # soft trigger on (DISABLED: use trigger() in ophyd) - #if self._auto_soft_enable: - # self.cmdSoftEnable.set(1).wait() - self.state = const.GfStatus.ACQUIRING - - # Gigafrost can finish a run without explicit unstaging - self._staged = Staged.no + if px_daq_h != px_gf_h or px_daq_w != px_gf_w: + raise RuntimeError(f"Different image size configured on GF and the DAQ") + return super().stage() - - def unstage(self): - """Standard ophyd method to finish an acquisition""" - # switch to idle - self.cmdStartCamera.set(0).wait() - if self._auto_soft_enable: - self.cmdSoftEnable.set(0).wait() - self.state = const.GfStatus.STOPPED - return super().unstage() - - def stop(self): - """Standard ophyd method to stop an acquisition""" - self.unstage() - - def trigger(self) -> DeviceStatus: - """Sends a software trigger and approximately waits to finnish""" - status = DeviceStatus(self) - - # Soft triggering based on operation mode - if self._auto_soft_enable and self.trigger_mode=='auto' and self.enable_mode=='soft': - # BEC teststand operation mode: posedge of SoftEnable if Started - self.cmdSoftEnable.set(0).wait() - self.cmdSoftEnable.set(1).wait() - sleep_time = self.cfgFramerate.value*self.cfgCntNum.value*0.001+0.050 - sleep(sleep_time) - print(f"[GF2] Slept for: {sleep_time} seconds", file=sys.stderr) - else: - self.cmdSoftTrigger.set(1).wait() - status.set_finished() - - return status - - @property - def exposure_mode(self): - """Returns the current exposure mode of the GigaFRost camera. - - Returns - ------- - exp_mode : {'external', 'timer', 'soft'} - The camera's active exposure mode. - If more than one mode is active at the same time, it returns None. - """ - mode_soft = self.cfgTrigExpSoft.get() - mode_timer = self.cfgTrigExpTimer.get() - mode_external = self.cfgTrigExpExt.get() - if mode_soft and not mode_timer and not mode_external: - return "soft" - elif not mode_soft and mode_timer and not mode_external: - return "timer" - elif not mode_soft and not mode_timer and mode_external: - return "external" - else: - return None - - @exposure_mode.setter - def exposure_mode(self, exp_mode): - """Apply the exposure mode for the GigaFRoST camera. - - Parameters - ---------- - exp_mode : {'external', 'timer', 'soft'} - The exposure mode to be set. - """ - if exp_mode not in self._valid_exposure_modes: - raise ValueError( - f"Invalid exposure mode! Valid modes are:\n" "{self._valid_exposure_modes}" - ) - - if exp_mode == "external": - self.cfgTrigExpExt.set(1).wait() - self.cfgTrigExpSoft.set(0).wait() - self.cfgTrigExpTimer.set(0).wait() - elif exp_mode == "timer": - self.cfgTrigExpExt.set(0).wait() - self.cfgTrigExpSoft.set(0).wait() - self.cfgTrigExpTimer.set(1).wait() - elif exp_mode == "soft": - self.cfgTrigExpExt.set(0).wait() - self.cfgTrigExpSoft.set(1).wait() - self.cfgTrigExpTimer.set(0).wait() - # Commit parameters - self.cmdSetParam.set(1).wait() - - @property - def fix_nframes_mode(self): - """Return the current fixed number of frames mode of the GigaFRoST camera. - - Returns - ------- - fix_nframes_mode : {'off', 'start', 'end', 'start+end'} - The camera's active fixed number of frames mode. - """ - start_bit = self.cfgCntStartBit.get() - end_bit = self.cfgCntStartBit.get() - - if not start_bit and not end_bit: - return "off" - elif start_bit and not end_bit: - return "start" - elif not start_bit and end_bit: - return "end" - elif start_bit and end_bit: - return "start+end" - else: - return None - - @fix_nframes_mode.setter - def fix_nframes_mode(self, mode): - """Apply the fixed number of frames settings to the GigaFRoST camera. - - Parameters - ---------- - mode : {'off', 'start', 'end', 'start+end'} - The fixed number of frames mode to be applied. - """ - if mode not in self._valid_fix_nframe_modes: - raise ValueError( - "Invalid fixed number of frames mode! " - "Valid modes are:\n{}".format(self._valid_fix_nframe_modes) - ) - - self._fix_nframes_mode = mode - if self._fix_nframes_mode == "off": - self.cfgCntStartBit.set(0).wait() - self.cfgCntEndBit.set(0).wait() - elif self._fix_nframes_mode == "start": - self.cfgCntStartBit.set(1).wait() - self.cfgCntEndBit.set(0).wait() - elif self._fix_nframes_mode == "end": - self.cfgCntStartBit.set(0).wait() - self.cfgCntEndBit.set(1).wait() - elif self._fix_nframes_mode == "start+end": - self.cfgCntStartBit.set(1).wait() - self.cfgCntEndBit.set(1).wait() - # Commit parameters - self.cmdSetParam.set(1).wait() - - @property - def trigger_mode(self): - """Method to detect the current trigger mode set in the GigaFRost camera. - - Returns - ------- - mode : {'auto', 'external', 'timer', 'soft'} - The camera's active trigger mode. If more than one mode is active - at the moment, None is returned. - """ - mode_auto = self.cfgTrigAuto.get() - mode_external = self.cfgTrigExt.get() - mode_timer = self.cfgTrigTimer.get() - mode_soft = self.cfgTrigSoft.get() - if mode_auto: - return "auto" - elif mode_soft: - return "soft" - elif mode_timer: - return "timer" - elif mode_external: - return "external" - else: - return None - - @trigger_mode.setter - def trigger_mode(self, mode): - """Set the trigger mode for the GigaFRoST camera. - - Parameters - ---------- - mode : {'auto', 'external', 'timer', 'soft'} - The GigaFRoST trigger mode. - """ - if mode not in self._valid_trigger_modes: - raise ValueError( - "Invalid trigger mode! Valid modes are:\n" "{self._valid_trigger_modes}" - ) - - if mode == "auto": - self.cfgTrigAuto.set(1).wait() - self.cfgTrigSoft.set(0).wait() - self.cfgTrigTimer.set(0).wait() - self.cfgTrigExt.set(0).wait() - elif mode == "external": - self.cfgTrigAuto.set(0).wait() - self.cfgTrigSoft.set(0).wait() - self.cfgTrigTimer.set(0).wait() - self.cfgTrigExt.set(1).wait() - elif mode == "timer": - self.cfgTrigAuto.set(0).wait() - self.cfgTrigSoft.set(0).wait() - self.cfgTrigTimer.set(1).wait() - self.cfgTrigExt.set(0).wait() - elif mode == "soft": - self.cfgTrigAuto.set(0).wait() - self.cfgTrigSoft.set(1).wait() - self.cfgTrigTimer.set(0).wait() - self.cfgTrigExt.set(0).wait() - # Commit parameters - self.cmdSetParam.set(1).wait() - - @property - def enable_mode(self): - """Return the enable mode set in the GigaFRost camera. - - Returns - ------- - enable_mode: {'soft', 'external', 'soft+ext', 'always'} - The camera's active enable mode. - """ - mode_soft = self.cfgTrigEnableSoft.get() - mode_external = self.cfgTrigEnableExt.get() - mode_auto = self.cfgTrigEnableAuto.get() - if mode_soft and not mode_auto: - if mode_external: - return "soft+ext" - else: - return "soft" - elif mode_auto and not mode_soft and not mode_external: - return "always" - elif mode_external and not mode_soft and not mode_auto: - return "external" - else: - return None - - @enable_mode.setter - def enable_mode(self, mode): - """Apply the enable mode for the GigaFRoST camera. - - Parameters - ---------- - mode : {'soft', 'external', 'soft+ext', 'always'} - The enable mode to be applied. - """ - if mode not in self._valid_enable_modes: - raise ValueError( - "Invalid enable mode! Valid modes are:\n" "{self._valid_enable_modes}" - ) - - if mode == "soft": - self.cfgTrigEnableExt.set(0).wait() - self.cfgTrigEnableSoft.set(1).wait() - self.cfgTrigEnableAuto.set(0).wait() - elif mode == "external": - self.cfgTrigEnableExt.set(1).wait() - self.cfgTrigEnableSoft.set(0).wait() - self.cfgTrigEnableAuto.set(0).wait() - elif mode == "soft+ext": - self.cfgTrigEnableExt.set(1).wait() - self.cfgTrigEnableSoft.set(1).wait() - self.cfgTrigEnableAuto.set(0).wait() - elif mode == "always": - self.cfgTrigEnableExt.set(0).wait() - self.cfgTrigEnableSoft.set(0).wait() - self.cfgTrigEnableAuto.set(1).wait() - # Commit parameters - self.cmdSetParam.set(1).wait() - - def get_state(self): - return self.state - - def get_south_mac(self): - return self._south_mac - - def get_north_mac(self): - return self._north_mac - - def get_north_ip(self): - return self._north_ip - - def get_south_ip(self): - return self._south_ip - - def get_backend_url(self): - """Method to read the configured backend URL""" - return self._backend_url - - def set_backend_ip(self, north, south): - """Method to manually set the backend ip""" - self._north_ip, self._south_ip = north, south - - def set_backend_mac(self, north, south): - """Method to manually set the backend mac""" - self._north_mac, self._south_mac = north, south - - def _build_udp_header_table(self): - """Build the header table for the communication""" - udp_header_table = [] - - for i in range(0, 64, 1): - for j in range(0, 8, 1): - dest_port = 2000 + 8 * i + j - source_port = 3000 + j - if j < 4: - extend_header_table( - udp_header_table, self._south_mac, self._south_ip, dest_port, source_port - ) - else: - extend_header_table( - udp_header_table, self._north_mac, self._north_ip, dest_port, source_port - ) - - return udp_header_table - - def _define_backend_ip(self): - if self._backend_url == const.BE3_DAFL_CLIENT: # xbl-daq-33 - return const.BE3_NORTH_IP, const.BE3_SOUTH_IP - elif self._backend_url == const.BE999_DAFL_CLIENT: - return const.BE999_NORTH_IP, const.BE999_SOUTH_IP - else: - raise RuntimeError(f"Backend not recognized. {(const.GF1, const.GF2, const.GF3)}") - - def _define_backend_mac(self): - if self._backend_url == const.BE3_DAFL_CLIENT: # xbl-daq-33 - return const.BE3_NORTH_MAC, const.BE3_SOUTH_MAC - elif self._backend_url == const.BE999_DAFL_CLIENT: - return const.BE999_NORTH_MAC, const.BE999_SOUTH_MAC - else: - raise RuntimeError(f"Backend not recognized. {(const.GF1, const.GF2, const.GF3)}") - - def _set_udp_header_table(self): - """Set the communication parameters for the camera module""" - self.cfgConnectionParam.set(self._build_udp_header_table()).wait() - + + # Automatically connect to MicroSAXS testbench if directly invoked if __name__ == "__main__": From e3145dfd8814b2f7cde6e33a5063b1ed2de61154 Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Thu, 25 Jul 2024 17:41:38 +0200 Subject: [PATCH 28/47] Refactored the camera class --- .../device_configs/microxas_test_bed.yaml | 19 ++++- tomcat_bec/devices/gigafrost/gfconstants.py | 4 +- .../devices/gigafrost/gigafrostcamera.py | 82 +++++++++++-------- 3 files changed, 69 insertions(+), 36 deletions(-) diff --git a/tomcat_bec/device_configs/microxas_test_bed.yaml b/tomcat_bec/device_configs/microxas_test_bed.yaml index 5c282e7..1fef45e 100644 --- a/tomcat_bec/device_configs/microxas_test_bed.yaml +++ b/tomcat_bec/device_configs/microxas_test_bed.yaml @@ -94,7 +94,7 @@ femto_mean_curr: gf2: description: GigaFrost camera controls - deviceClass: tomcat_bec.devices.gigafrost.gigafrostclient.GigaFrostClient + deviceClass: tomcat_bec.devices.gigafrost.gigafrostcamera.GigaFrostCamera deviceConfig: prefix: 'X02DA-CAM-GF2:' backend_url: 'http://xbl-daq-28:8080' @@ -106,6 +106,23 @@ gf2: readOnly: false readoutPriority: monitored softwareTrigger: true + +#gf2: +# description: GigaFrost camera controls +# deviceClass: tomcat_bec.devices.gigafrost.gigafrostclient.GigaFrostClient +# deviceConfig: +# prefix: 'X02DA-CAM-GF2:' +# backend_url: 'http://xbl-daq-28:8080' +# auto_soft_enable: true +# deviceTags: +# - camera +# enabled: true +# onFailure: buffer +# readOnly: false +# readoutPriority: monitored +# softwareTrigger: true + + daq: description: Standard DAQ controls deviceClass: tomcat_bec.devices.gigafrost.stddaq_ws.StdDaqWsClient diff --git a/tomcat_bec/devices/gigafrost/gfconstants.py b/tomcat_bec/devices/gigafrost/gfconstants.py index f7786bf..e0eab37 100644 --- a/tomcat_bec/devices/gigafrost/gfconstants.py +++ b/tomcat_bec/devices/gigafrost/gfconstants.py @@ -6,7 +6,7 @@ Created on Thu Jun 27 17:28:43 2024 @author: mohacsi_i """ -from enum import Enum +from enum import IntEnum gf_valid_enable_modes = ("soft", "external", "soft+ext", "always") @@ -16,7 +16,7 @@ gf_valid_fix_nframe_modes = ("off", "start", "end", "start+end") # STATUS -class GfStatus(Enum): +class GfStatus(IntEnum): """Operation states for GigaFrost Ophyd device""" NEW = 1 INITIALIZED = 2 diff --git a/tomcat_bec/devices/gigafrost/gigafrostcamera.py b/tomcat_bec/devices/gigafrost/gigafrostcamera.py index a63b217..9f5e5ee 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostcamera.py +++ b/tomcat_bec/devices/gigafrost/gigafrostcamera.py @@ -6,9 +6,8 @@ Created on Thu Jun 27 17:28:43 2024 @author: mohacsi_i """ -import sys from time import sleep -from ophyd import Device, Component, EpicsSignal, EpicsSignalRO, Kind, DeviceStatus +from ophyd import Signal, SignalRO, Device, Component, EpicsSignal, EpicsSignalRO, Kind, DeviceStatus from ophyd.device import Staged from ophyd_devices.interfaces.base_classes.psi_detector_base import ( @@ -26,6 +25,13 @@ try: except ModuleNotFoundError: from tomcat_bec.devices.gigafrost.gfutils import extend_header_table +try: + from bec_lib import bec_logger + logger = bec_logger.logger +except ModuleNotFoundError: + import logging + logger = logging.getLogger("GfCam") + class GigaFrostCameraMixin(CustomDetectorMixin): """Mixin class to setup TOMCAT specific implementations of the detector. @@ -38,7 +44,7 @@ class GigaFrostCameraMixin(CustomDetectorMixin): elif self.parent.backendUrl.get() == const.BE999_DAFL_CLIENT: return const.BE999_NORTH_IP, const.BE999_SOUTH_IP else: - raise RuntimeError(f"Backend not recognized. {(const.GF1, const.GF2, const.GF3)}") + raise RuntimeError(f"Backend {self.parent.backendUrl.get()} not recognized. {(const.GF1, const.GF2, const.GF3)}") def _define_backend_mac(self): if self.parent.backendUrl.get() == const.BE3_DAFL_CLIENT: # xbl-daq-33 @@ -46,7 +52,7 @@ class GigaFrostCameraMixin(CustomDetectorMixin): elif self.parent.backendUrl.get() == const.BE999_DAFL_CLIENT: return const.BE999_NORTH_MAC, const.BE999_SOUTH_MAC else: - raise RuntimeError(f"Backend not recognized. {(const.GF1, const.GF2, const.GF3)}") + raise RuntimeError(f"Backend {self.parent.backendUrl.get()} not recognized. {(const.GF1, const.GF2, const.GF3)}") def _set_udp_header_table(self): """Set the communication parameters for the camera module""" @@ -62,11 +68,11 @@ class GigaFrostCameraMixin(CustomDetectorMixin): source_port = 3000 + j if j < 4: extend_header_table( - udp_header_table, self.parent.macSouth, self.parent.ipSouth, dest_port, source_port + udp_header_table, self.parent.macSouth.get(), self.parent.ipSouth.get(), dest_port, source_port ) else: extend_header_table( - udp_header_table, self.parent.macNorth, self.parent.ipNorth, dest_port, source_port + udp_header_table, self.parent.macNorth.get(), self.parent.ipNorth.get(), dest_port, source_port ) return udp_header_table @@ -88,7 +94,7 @@ class GigaFrostCameraMixin(CustomDetectorMixin): self.parent.cmdWriteService.set(1).wait() # Configure software triggering if needed - if self.parent._auto_soft_enable: + if self.parent.autoSoftEnable.get(): # trigger modes self.parent.cfgCntStartBit.set(1).wait() self.parent.cfgCntEndBit.set(0).wait() @@ -120,15 +126,15 @@ class GigaFrostCameraMixin(CustomDetectorMixin): self.parent.state.put(const.GfStatus.INIT, force=True) return super().on_init() - - def on_stage(self) -> None: - """ - Specify actions to be executed during stage in preparation for a scan. - self.parent.scaninfo already has all current parameters for the upcoming scan. + """Specify actions to be executed during stage - In case the backend service is writing data on disk, this step should include publishing - a file_event and file_message to BEC to inform the system where the data is written to. + Specifies actions to be executed during the stage step in preparation + for a scan. self.parent.scaninfo already has all current parameters for + the upcoming scan. + + The gigafrost camera IOC does not write data to disk, that's done by + the DAQ ophyd device. IMPORTANT: It must be safe to assume that the device is ready for the scan @@ -143,8 +149,7 @@ class GigaFrostCameraMixin(CustomDetectorMixin): self.parent._staged = Staged.no def on_unstage(self) -> None: - """ - Specify actions to be executed during unstage. + """Specify actions to be executed during unstage. This step should include checking if the acqusition was successful, and publishing the file location and file event message, @@ -153,7 +158,7 @@ class GigaFrostCameraMixin(CustomDetectorMixin): # Switch to idle self.parent.cmdStartCamera.set(0).wait() if self.parent.autoSoftEnable.get(): - self.cmdSoftEnable.set(0).wait() + self.parent.cmdSoftEnable.set(0).wait() self.parent.state.put(const.GfStatus.STOPPED, force=True) def on_stop(self) -> None: @@ -180,7 +185,7 @@ class GigaFrostCameraMixin(CustomDetectorMixin): sleep_time = self.parent.cfgFramerate.value*self.parent.cfgCntNum.value*0.001+0.050 # There's no status readback from the camera, so we just wait sleep(sleep_time) - print(f"[GF2] Slept for: {sleep_time} seconds", file=sys.stderr) + logger.info(f"[GF2] Slept for: {sleep_time} seconds") else: self.parent.cmdSoftTrigger.set(1).wait() status.set_finished() @@ -209,13 +214,12 @@ class GigaFrostCamera(PSIDetectorBase): Bugs: ---------- - FRAMERATE : Ignored in soft trigger mode, period becomes 2xexposure time + FRAMERATE : Ignored in soft trigger mode, period becomes 2xExposure time """ # pylint: disable=too-many-instance-attributes custom_prepare_cls = GigaFrostCameraMixin - - + USER_ACCESS = [""] infoBusyFlag = Component(EpicsSignalRO, "BUSY_STAT", auto_monitor=True) infoSyncFlag = Component(EpicsSignalRO, "SYNC_FLAG", auto_monitor=True) @@ -358,9 +362,13 @@ class GigaFrostCamera(PSIDetectorBase): USER_ACCESS = ["exposure_mode", "fix_nframes_mode", "trigger_mode", "enable_mode"] - autoSoftEnable = Component(Signal, auto_monitor=True, kind=Kind.config) - backendUrl = Component(Signal, auto_monitor=True, kind=Kind.config) - state = Component(Signal, auto_monitor=True, kind=Kind.config) + autoSoftEnable = Component(Signal, kind=Kind.config) + backendUrl = Component(Signal, kind=Kind.config) + macNorth = Component(Signal, kind=Kind.config) + macSouth = Component(Signal, kind=Kind.config) + ipNorth = Component(Signal, kind=Kind.config) + ipSouth = Component(Signal, kind=Kind.config) + state = Component(Signal, value=int(const.GfStatus.NEW), kind=Kind.config) def __init__( self, @@ -376,14 +384,11 @@ class GigaFrostCamera(PSIDetectorBase): parent=None, **kwargs, ): - # Additional parameters - self.autoSoftEnable._metadata["write_access"] = False - self.backendUrl._metadata["write_access"] = False - self.state._metadata["write_access"] = False - self.autoSoftEnable.put(auto_soft_enable, force=True) - self.backendUrl.put(backend_url, force=True) - self.state.put(const.GfStatus.NEW, force=True) - + # Ugly hack to pass values before on_init() + self._signals_to_be_set = {} + self._signals_to_be_set['auto_soft_enable'] = auto_soft_enable + self._signals_to_be_set['backend_url'] = backend_url + # super() will call the mixin class super().__init__( prefix=prefix, @@ -395,6 +400,17 @@ class GigaFrostCamera(PSIDetectorBase): **kwargs, ) + def _init(self): + """Ugly hack: values must be set before on_init() is called""" + # Additional parameters + self.autoSoftEnable._metadata["write_access"] = False + self.backendUrl._metadata["write_access"] = False + self.state._metadata["write_access"] = False + self.autoSoftEnable.put(self._signals_to_be_set['auto_soft_enable'], force=True) + self.backendUrl.put(self._signals_to_be_set['backend_url'], force=True) + self.state.put(const.GfStatus.NEW, force=True) + return super()._init() + def configure( self, nimages=10, @@ -448,7 +464,7 @@ class GigaFrostCamera(PSIDetectorBase): # Stop acquisition self.cmdStartCamera.set(0).wait() - if self._auto_soft_enable: + if self.autoSoftEnable.get(): self.cmdSoftEnable.set(0).wait() # change settings From 780e64d81d0d6814486251c62457bfbeeb994faa Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Thu, 25 Jul 2024 17:58:17 +0200 Subject: [PATCH 29/47] Half of GFClient instantiates --- .../device_configs/microxas_test_bed.yaml | 29 +++--- .../devices/gigafrost/gigafrostclient.py | 88 +++++++++++++++++-- 2 files changed, 96 insertions(+), 21 deletions(-) diff --git a/tomcat_bec/device_configs/microxas_test_bed.yaml b/tomcat_bec/device_configs/microxas_test_bed.yaml index 1fef45e..8edf221 100644 --- a/tomcat_bec/device_configs/microxas_test_bed.yaml +++ b/tomcat_bec/device_configs/microxas_test_bed.yaml @@ -107,21 +107,20 @@ gf2: readoutPriority: monitored softwareTrigger: true -#gf2: -# description: GigaFrost camera controls -# deviceClass: tomcat_bec.devices.gigafrost.gigafrostclient.GigaFrostClient -# deviceConfig: -# prefix: 'X02DA-CAM-GF2:' -# backend_url: 'http://xbl-daq-28:8080' -# auto_soft_enable: true -# deviceTags: -# - camera -# enabled: true -# onFailure: buffer -# readOnly: false -# readoutPriority: monitored -# softwareTrigger: true - +gfclient: + description: GigaFrost camera controls + deviceClass: tomcat_bec.devices.gigafrost.gigafrostclient.GigaFrostClient + deviceConfig: + prefix: 'X02DA-CAM-GF2:' + backend_url: 'http://xbl-daq-28:8080' + auto_soft_enable: true + deviceTags: + - camera + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: monitored + softwareTrigger: true daq: description: Standard DAQ controls diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index 27f6df0..cced95a 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -6,7 +6,6 @@ Created on Thu Jun 27 17:28:43 2024 @author: mohacsi_i """ -import sys from time import sleep from ophyd import Device, Component, EpicsSignal, EpicsSignalRO, Kind, DeviceStatus from ophyd.device import Staged @@ -16,16 +15,21 @@ from ophyd_devices.interfaces.base_classes.psi_detector_base import ( PSIDetectorBase, ) +try: + import gfconstants as const +except ModuleNotFoundError: + import tomcat_bec.devices.gigafrost.gfconstants as const + try: from StdDaqClient import StdDaqClient except ModuleNotFoundError: - from tomcat_bec.devices.gigafrost.StdDaqClient import StdDaqClient + from tomcat_bec.devices.gigafrost.stddaq_ws import StdDaqWsClient try: - from gfcamera import GigaFrostCamera + from gigafrostcamera import GigaFrostCamera except ModuleNotFoundError: - from tomcat_bec.devices.gigafrost.gfcamera import GigaFrostCamera + from tomcat_bec.devices.gigafrost.gigafrostcamera import GigaFrostCamera @@ -219,8 +223,80 @@ class GigaFrostClient(PSIDetectorBase): """ # pylint: disable=too-many-instance-attributes - cam = Component(GigaFrostCamera, "", auto_monitor=True) - daq = Component(StdDaqClient, "", auto_monitor=True) + cam = Component(GigaFrostCamera, prefix="X02DA-CAM-GF2:", name="cam") + #daq = Component(StdDaqWsClient, "") + + def __init__( + self, + prefix="", + *, + name, + auto_soft_enable=False, + backend_url=const.BE999_DAFL_CLIENT, + kind=None, + read_attrs=None, + configuration_attrs=None, + parent=None, + **kwargs, + ): + + self.__class__.__dict__["cam"].kwargs.update({'backend_url':"http://xbl-daq-28:8080",}) + self.__class__.__dict__["cam"].kwargs.update({'auto_soft_enable': True}) + + + + + + + super().__init__( + prefix=prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + **kwargs, + ) + + + + def configure(self, d: dict=None, **kwargs): + """Configure the next scan with the GigaFRoST camera + + Parameters + ---------- + nimages : int, optional + Number of images to be taken during each scan. Set to -1 for an + unlimited number of images (limited by the ringbuffer size and + backend speed). (default = 10) + exposure : float, optional + Exposure time [ms]. (default = 0.2) + period : float, optional + Exposure period [ms], ignored in soft trigger mode. (default = 1.0) + roix : int, optional + ROI size in the x-direction [pixels] (default = 2016) + roiy : int, optional + ROI size in the y-direction [pixels] (default = 2016) + scanid : int, optional + Scan identification number to be associated with the scan data + (default = 0) + correction_mode : int, optional + The correction to be applied to the imaging data. The following + modes are available (default = 5): + + * 0: Bypass. No corrections are applied to the data. + * 1: Send correction factor A instead of pixel values + * 2: Send correction factor B instead of pixel values + * 3: Send correction factor C instead of pixel values + * 4: Invert pixel values, but do not apply any linearity correction + * 5: Apply the full linearity correction + """ + # If Bluesky style configure + old = self.read_configuration() + self.cam.configure(d) + self.daq.configure(d) + new = self.read_configuration() + return old, new def stage(self): px_daq_h = self.daq.config.cfg_image_pixel_height.get() From 51cfae82c4e0a0cca509e7bb0fd6353d4093b4bb Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Fri, 26 Jul 2024 12:17:50 +0200 Subject: [PATCH 30/47] GFclient instantiates but camera is not yet running --- .../devices/gigafrost/gigafrostcamera.py | 32 ++++---- .../devices/gigafrost/gigafrostclient.py | 33 +++++---- tomcat_bec/devices/gigafrost/stddaq_rest.py | 48 ++++++++---- tomcat_bec/devices/gigafrost/stddaq_ws.py | 74 ++++++++++++------- 4 files changed, 120 insertions(+), 67 deletions(-) diff --git a/tomcat_bec/devices/gigafrost/gigafrostcamera.py b/tomcat_bec/devices/gigafrost/gigafrostcamera.py index 9f5e5ee..78adc5f 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostcamera.py +++ b/tomcat_bec/devices/gigafrost/gigafrostcamera.py @@ -223,15 +223,15 @@ class GigaFrostCamera(PSIDetectorBase): infoBusyFlag = Component(EpicsSignalRO, "BUSY_STAT", auto_monitor=True) infoSyncFlag = Component(EpicsSignalRO, "SYNC_FLAG", auto_monitor=True) - cmdSyncHw = Component(EpicsSignal, "SYNC_SWHW.PROC", put_complete=True) - cmdStartCamera = Component(EpicsSignal, "START_CAM", put_complete=True) - cmdSetParam = Component(EpicsSignal, "SET_PARAM.PROC", put_complete=True) + cmdSyncHw = Component(EpicsSignal, "SYNC_SWHW.PROC", put_complete=True, kind=Kind.omitted) + cmdStartCamera = Component(EpicsSignal, "START_CAM", put_complete=True, kind=Kind.omitted) + cmdSetParam = Component(EpicsSignal, "SET_PARAM.PROC", put_complete=True, kind=Kind.omitted) # UDP header cfgUdpNumPorts = Component(EpicsSignal, "PORTS", put_complete=True, kind=Kind.config) cfgUdpNumFrames = Component(EpicsSignal, "FRAMENUM", put_complete=True, kind=Kind.config) cfgUdpHtOffset = Component(EpicsSignal, "HT_OFFSET", put_complete=True, kind=Kind.config) - cmdWriteService = Component(EpicsSignal, "WRITE_SRV.PROC", put_complete=True) + cmdWriteService = Component(EpicsSignal, "WRITE_SRV.PROC", put_complete=True, kind=Kind.omitted) # Standard camera configs cfgExposure = Component(EpicsSignal, "EXPOSURE", put_complete=True, auto_monitor=True, kind=Kind.config) @@ -244,7 +244,7 @@ class GigaFrostCamera(PSIDetectorBase): # Software signals cmdSoftEnable = Component(EpicsSignal, "SOFT_ENABLE", put_complete=True) - cmdSoftTrigger = Component(EpicsSignal, "SOFT_TRIG.PROC", put_complete=True) + cmdSoftTrigger = Component(EpicsSignal, "SOFT_TRIG.PROC", put_complete=True, kind=Kind.omitted) cmdSoftExposure = Component(EpicsSignal, "SOFT_EXP", put_complete=True) # Trigger configuration PVs @@ -349,15 +349,15 @@ class GigaFrostCamera(PSIDetectorBase): ) # HW settings as read only - cfgSyncFlag = Component(EpicsSignalRO, "PIXRATE", auto_monitor=True) - cfgTrigDelay = Component(EpicsSignalRO, "TRIG_DELAY", auto_monitor=True) - cfgSyncoutDelay = Component(EpicsSignalRO, "SYNCOUT_DLY", auto_monitor=True) - cfgOutputPolarity0 = Component(EpicsSignalRO, "BNC0_RBV", auto_monitor=True) - cfgOutputPolarity1 = Component(EpicsSignalRO, "BNC1_RBV", auto_monitor=True) - cfgOutputPolarity2 = Component(EpicsSignalRO, "BNC2_RBV", auto_monitor=True) - cfgOutputPolarity3 = Component(EpicsSignalRO, "BNC3_RBV", auto_monitor=True) - cfgInputPolarity1 = Component(EpicsSignalRO, "BNC4_RBV", auto_monitor=True) - cfgInputPolarity2 = Component(EpicsSignalRO, "BNC5_RBV", auto_monitor=True) + cfgSyncFlag = Component(EpicsSignalRO, "PIXRATE", auto_monitor=True, kind=Kind.config) + cfgTrigDelay = Component(EpicsSignalRO, "TRIG_DELAY", auto_monitor=True, kind=Kind.config) + cfgSyncoutDelay = Component(EpicsSignalRO, "SYNCOUT_DLY", auto_monitor=True, kind=Kind.config) + cfgOutputPolarity0 = Component(EpicsSignalRO, "BNC0_RBV", auto_monitor=True, kind=Kind.config) + cfgOutputPolarity1 = Component(EpicsSignalRO, "BNC1_RBV", auto_monitor=True, kind=Kind.config) + cfgOutputPolarity2 = Component(EpicsSignalRO, "BNC2_RBV", auto_monitor=True, kind=Kind.config) + cfgOutputPolarity3 = Component(EpicsSignalRO, "BNC3_RBV", auto_monitor=True, kind=Kind.config) + cfgInputPolarity1 = Component(EpicsSignalRO, "BNC4_RBV", auto_monitor=True, kind=Kind.config) + cfgInputPolarity2 = Component(EpicsSignalRO, "BNC5_RBV", auto_monitor=True, kind=Kind.config) infoBoardTemp = Component(EpicsSignalRO, "T_BOARD", auto_monitor=True) USER_ACCESS = ["exposure_mode", "fix_nframes_mode", "trigger_mode", "enable_mode"] @@ -413,6 +413,7 @@ class GigaFrostCamera(PSIDetectorBase): def configure( self, + d: dict=None, nimages=10, exposure=0.2, period=1.0, @@ -452,8 +453,7 @@ class GigaFrostCamera(PSIDetectorBase): * 5: Apply the full linearity correction """ # If Bluesky style configure - if isinstance(nimages, dict): - d = nimages.copy() + if d is not None: nimages = d.get('nimages', 10) exposure = d.get('exposure', exposure) period = d.get('period', period) diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index cced95a..c835af9 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -217,6 +217,13 @@ class GigaFrostClient(PSIDetectorBase): Backend url address necessary to set up the camera's udp header. (default: http://xbl-daq-23:8080) + Usage: + ---------- + gf = GigaFrostClient( + "X02DA-CAM-GF2:", name="gf2", backend_url="http://xbl-daq-28:8080", auto_soft_enable=True, + daq_ws_url="ws://xbl-daq-29:8080", daq_rest_url="http://xbl-daq-29:5000" + ) + Bugs: ---------- FRAMERATE : Ignored in soft trigger mode, period becomes 2xexposure time @@ -224,7 +231,7 @@ class GigaFrostClient(PSIDetectorBase): # pylint: disable=too-many-instance-attributes cam = Component(GigaFrostCamera, prefix="X02DA-CAM-GF2:", name="cam") - #daq = Component(StdDaqWsClient, "") + daq = Component(StdDaqWsClient, name="daq") def __init__( self, @@ -233,20 +240,19 @@ class GigaFrostClient(PSIDetectorBase): name, auto_soft_enable=False, backend_url=const.BE999_DAFL_CLIENT, + daq_ws_url = "ws://localhost:8080", + daq_rest_url = "http://localhost:5000", kind=None, read_attrs=None, configuration_attrs=None, parent=None, **kwargs, ): - - self.__class__.__dict__["cam"].kwargs.update({'backend_url':"http://xbl-daq-28:8080",}) - self.__class__.__dict__["cam"].kwargs.update({'auto_soft_enable': True}) - - - - - + self.__class__.__dict__["cam"].kwargs['backend_url'] = backend_url + self.__class__.__dict__["cam"].kwargs['auto_soft_enable'] = auto_soft_enable + self.__class__.__dict__["daq"].kwargs['ws_url'] = daq_ws_url + self.__class__.__dict__["daq"].kwargs['rest_url'] = daq_rest_url + #self.__class__.__dict__["daq"].__class__.__dict__["cfg"].kwargs['rest_url'] = daq_rest_url super().__init__( prefix=prefix, @@ -299,11 +305,11 @@ class GigaFrostClient(PSIDetectorBase): return old, new def stage(self): - px_daq_h = self.daq.config.cfg_image_pixel_height.get() - px_daq_w = self.daq.config.cfg_image_pixel_width.get() + px_daq_h = self.daq.cfg.cfg_image_pixel_height.get() + px_daq_w = self.daq.cfg.cfg_image_pixel_width.get() px_gf_h = self.cam.cfgRoiX.get() - px_gf_y = self.cam.cfgRoiY.get() + px_gf_w = self.cam.cfgRoiY.get() if px_daq_h != px_gf_h or px_daq_w != px_gf_w: raise RuntimeError(f"Different image size configured on GF and the DAQ") @@ -315,6 +321,7 @@ class GigaFrostClient(PSIDetectorBase): # Automatically connect to MicroSAXS testbench if directly invoked if __name__ == "__main__": gf = GigaFrostClient( - "X02DA-CAM-GF2:", name="gf2", backend_url="http://xbl-daq-28:8080", auto_soft_enable=True + "X02DA-CAM-GF2:", name="gf2", backend_url="http://xbl-daq-28:8080", auto_soft_enable=True, + daq_ws_url="ws://xbl-daq-29:8080", daq_rest_url="http://xbl-daq-29:5000" ) gf.wait_for_connection() diff --git a/tomcat_bec/devices/gigafrost/stddaq_rest.py b/tomcat_bec/devices/gigafrost/stddaq_rest.py index 8560056..374b860 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_rest.py +++ b/tomcat_bec/devices/gigafrost/stddaq_rest.py @@ -14,12 +14,12 @@ from ophyd import Device, Signal, Component, Kind import requests -class StdDaqRestConfig(Device): +class StdDaqRestClient(Device): """Wrapper class around the new StdDaq REST interface. - This was meant to replace the websocket inteface that replaced the - documented python client. We can finally read configuration through - standard HTTP requests, although the secondary server is ot reachable + This was meant to replace or extend the websocket inteface that replaced + the documented python client. We can finally read configuration through + standard HTTP requests, although the secondary server is not reachable at the time. """ # pylint: disable=too-many-instance-attributes @@ -39,12 +39,13 @@ class StdDaqRestConfig(Device): cfg_module_sync_queue_size = Component(Signal, kind=Kind.config) cfg_module_positions = Component(Signal, kind=Kind.config) + _config_read = False def __init__( - self, *args, url: str = "http://xbl-daq-29:5000", parent: Device = None, **kwargs + self, *args, rest_url: str = "http://localhost:5000", parent: Device = None, **kwargs ) -> None: super().__init__(*args, parent=parent, **kwargs) - self._url_base = url + self._url_base = rest_url # Connect ro the DAQ and initialize values self.read_daq_config() @@ -57,12 +58,31 @@ class StdDaqRestConfig(Device): raise ConnectionError(f"[{self.name}] Error {r.status_code}:\t{r.text}") cfg = r.json() - for key, val in cfg.items(): - if isinstance(val, (int, float, str)): - getattr(self, "cfg_"+key).set(val).wait() + self.cfg_detector_name.set(cfg['detector_name']).wait() + self.cfg_detector_type.set(cfg['detector_type']).wait() + + self.cfg_n_modules.set(cfg['n_modules']).wait() + self.cfg_bit_depth.set(cfg['bit_depth']).wait() + self.cfg_image_pixel_height.set(cfg['image_pixel_height']).wait() + self.cfg_image_pixel_width.set(cfg['image_pixel_width']).wait() + self.cfg_start_udp_port.set(cfg['start_udp_port']).wait() + self.cfg_writer_user_id.set(cfg['writer_user_id']).wait() + self.cfg_submodule_info.set(cfg['submodule_info']).wait() + self.cfg_max_number_of_forwarders_spawned.set(cfg['max_number_of_forwarders_spawned']).wait() + self.cfg_use_all_forwarders.set(cfg['use_all_forwarders']).wait() + self.cfg_module_sync_queue_size.set(cfg['module_sync_queue_size']).wait() + self.cfg_module_positions.set(cfg['module_positions']).wait() + + self._config_read = True return cfg def write_daq_config(self): + """Write configuration ased on current PV values. Some fields might be + unchangeable. + """ + if not self._config_read: + raise RuntimeError("Pleae read config before editing") + config = { 'detector_name': str(self.cfg_detector_name.get()), 'detector_type': str(self.cfg_detector_type.get()), @@ -85,28 +105,30 @@ class StdDaqRestConfig(Device): if r.status_code != 200: raise ConnectionError(f"[{self.name}] Error {r.status_code}:\t{r.text}") + def read(self): + self.read_daq_config() def stage(self) -> list: - """Read the current configuration from the DAQ + """Stage op: Read the current configuration from the DAQ """ self.read_daq_config() return super().stage() def unstage(self): - """Read the current configuration from the DAQ + """Unstage op: Read the current configuration from the DAQ """ self.read_daq_config() return super().unstage() def stop(self): - """Read the current configuration from the DAQ + """Stop op: Read the current configuration from the DAQ """ self.unstage() # Automatically connect to MicroSAXS testbench if directly invoked if __name__ == "__main__": - daqcfg = StdDaqRestConfig(name="daqcfg", url="http://xbl-daq-29:5000") + daqcfg = StdDaqRestClient(name="daqcfg", rest_url="http://xbl-daq-29:5000") daqcfg.wait_for_connection() diff --git a/tomcat_bec/devices/gigafrost/stddaq_ws.py b/tomcat_bec/devices/gigafrost/stddaq_ws.py index 15db947..cd04be8 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_ws.py +++ b/tomcat_bec/devices/gigafrost/stddaq_ws.py @@ -14,33 +14,49 @@ from ophyd import Device, Signal, Component, Kind from websockets.sync.client import connect from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError +try: + from stddaq_rest import StdDaqRestClient +except ModuleNotFoundError: + from tomcat_bec.devices.gigafrost.stddaq_rest import StdDaqRestClient + + + class StdDaqWsClient(Device): - """Wrapper class around the StdDaq websocket interface. + """StdDaq API - This was meant to replace the documented python client. We cannot read - or change the current configuration through this interface. - - A bit more about the Standard DAQ configuration: + This class combines the new websocket and REST interfaces that were meant + to replace the documented python client. The websocket interface starts + and stops the acquisition and provides status, while the REST interface + can read and write the configuration. The standard DAQ configuration is a single JSON file locally autodeployed - to the DAQ servers (as root!!!). Previously there was a service to offer - a REST API to write this file, but since there's no frontend group, this - is no longer available. + to the DAQ servers (as root!!!). It can only be written through a primary + REST API that is semi-supported, as there's no frontend group. The DAQ + might be distributed across several servers, meaning that the primary REST + interface will try to synchronize with secondary REST servers, but this + might fail, yielding a flawed configuration. + + daq = StdDaqWsClient(name="daq", ws_url="ws://xbl-daq-29:8080", rest_url="http://xbl-daq-29:5000") + """ # pylint: disable=too-many-instance-attributes # Status attributes - status = Component(Signal, value="unknown", kind=Kind.hinted) - n_images = Component(Signal, value=10000, kind=Kind.config) + status = Component(Signal, value="unknown", kind=Kind.normal) + n_total = Component(Signal, value=10000, kind=Kind.config) file_path = Component(Signal, value="/gpfs/test/test-beamline", kind=Kind.config) + cfg = Component(StdDaqRestClient, kind=Kind.config) + def __init__( - self, *args, url: str = "ws://localhost:8080", parent: Device = None, **kwargs + self, *args, ws_url: str = "ws://localhost:8080", rest_url="http://localhost:5000", parent: Device = None, **kwargs ) -> None: + self.__class__.__dict__['cfg'].kwargs['rest_url'] = rest_url + super().__init__(*args, parent=parent, **kwargs) self.status._metadata["write_access"] = False - self._ws_url = url + self._ws_url = ws_url self._mon = None # Connect ro the DAQ @@ -58,13 +74,17 @@ class StdDaqWsClient(Device): sleep(5) self._client = connect(self._ws_url) + def __del__(self): + """Try to close the socket""" + self._client.close_socket() + def monitor(self): """Attach monitoring to the DAQ""" self._client = connect(self._ws_url) self._mon = Thread(target=self.poll, daemon=True) self._mon.start() - def configure(self, n_images: int = None, file_path: str = None) -> tuple: + def configure(self, d: dict=None, n_total: int = None, file_path: str = None) -> tuple: """Set the standard DAQ parameters for the next run Note that full reconfiguration is not possible with the websocket @@ -73,13 +93,13 @@ class StdDaqWsClient(Device): Example: ---------- - std.configure(n_images=10000, file_path="/data/test/raw") + std.configure(n_total=10000, file_path="/data/test/raw") Parameters ---------- - n_images : int, optional - Number of images to be taken during each scan. Set to -1 for an - unlimited number of images (limited by the ringbuffer size and + n_total : int, optional + Total number of images to be taken during each scan. Set to -1 for + an unlimited number of images (limited by the ringbuffer size and backend speed). (default = 10000) file_path : string, optional Save file path. (default = '/gpfs/test/test-beamline') @@ -87,13 +107,12 @@ class StdDaqWsClient(Device): """ old_config = self.read_configuration() # If Bluesky style configure - if isinstance(n_images, dict): - d = n_images.copy() - n_images = d.get('n_images', None) + if d is not None: + n_total = d.get('n_total', None) file_path = d.get('file_path', None) - if n_images is not None: - self.n_images.set(int(n_images)) + if n_total is not None: + self.n_total.set(int(n_total)) if file_path is not None: self.output_file.set(str(file_path)) @@ -108,9 +127,9 @@ class StdDaqWsClient(Device): not, we can't query if not running. """ file_path = self.file_path.get() - n_image = self.n_images.get() + n_total = self.n_total.get() - message = {"command": "start", "path": file_path, "n_image": n_image} + message = {"command": "start", "path": file_path, "n_image": n_total} reply = self.message(message) reply = json.loads(reply) @@ -186,7 +205,12 @@ class StdDaqWsClient(Device): self._mon = None +class StdDaqClient(StdDaqWsClient): + """Just an alias""" + pass + + # Automatically connect to MicroSAXS testbench if directly invoked if __name__ == "__main__": - daq = StdDaqWsClient(name="daq", url="ws://xbl-daq-29:8080") + daq = StdDaqWsClient(name="daq", ws_url="ws://xbl-daq-29:8080", rest_url="http://xbl-daq-29:5000") daq.wait_for_connection() From da823f0a4078fff6a976230a55562a63ed517ef5 Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Wed, 24 Jul 2024 10:53:47 +0200 Subject: [PATCH 31/47] DAQ preview using standard detector class --- .../devices/gigafrost/stddaq_preview.py | 251 +++++++++++++----- 1 file changed, 182 insertions(+), 69 deletions(-) diff --git a/tomcat_bec/devices/gigafrost/stddaq_preview.py b/tomcat_bec/devices/gigafrost/stddaq_preview.py index 776229d..4c42fd3 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_preview.py +++ b/tomcat_bec/devices/gigafrost/stddaq_preview.py @@ -6,16 +6,28 @@ Created on Thu Jun 27 17:28:43 2024 @author: mohacsi_i """ -import sys import json +import enum +import zmq +import numpy as np from time import sleep, time from threading import Thread -import numpy as np -import zmq -#import matplotlib.pyplot as plt from ophyd import Device, Signal, Component, Kind +from ophyd_devices.interfaces.base_classes.psi_detector_base import ( + CustomDetectorMixin, + PSIDetectorBase, +) -TOPIC_FILTER = '' +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 class StdDaqPreview(Device): @@ -27,22 +39,19 @@ class StdDaqPreview(Device): You can add a preview widget to the dock by: cam_widget = gui.add_dock('cam_dock1').add_widget('BECFigure').image('daq_stream1') - """ # pylint: disable=too-many-instance-attributes - + # Subscriptions for plotting image SUB_MONITOR = "monitor" _default_sub = SUB_MONITOR - # Status attributes url = Component(Signal, kind=Kind.config) - status = Component(Signal, value="detached", kind=Kind.omitted) - process = Component(Signal, value=True, kind=Kind.omitted) + status = Component(Signal, value=StdDaqPreviewState.UNKNOWN, kind=Kind.omitted) image = Component(Signal, kind=Kind.normal) frame = Component(Signal, kind=Kind.normal) - shape = Component(Signal, kind=Kind.omitted) + image_shape = Component(Signal, kind=Kind.omitted) value = Component(Signal, kind=Kind.hinted) _throttle = 0.05 @@ -54,10 +63,9 @@ class StdDaqPreview(Device): self.status._metadata["write_access"] = False self.image._metadata["write_access"] = False self.frame._metadata["write_access"] = False - self.shape._metadata["write_access"] = False + self.image_shape._metadata["write_access"] = False self.value._metadata["write_access"] = False self.url.set(url, force=True).wait() - self._stream_url = url self._stop_polling = False self._mon = None @@ -74,18 +82,19 @@ class StdDaqPreview(Device): # Socket to talk to server context = zmq.Context() self._socket = context.socket(zmq.SUB) - self._socket.setsockopt(zmq.SUBSCRIBE, b'') + self._socket.setsockopt(zmq.SUBSCRIBE, ZMQ_TOPIC_FILTER) try: - self._socket.connect(self._stream_url) + self._socket.connect(self.url.get()) except ConnectionRefusedError: - sleep(5) - self._socket.connect(self._stream_url) + sleep(1) + self._socket.connect(self.url.get()) def configure(self, throttle: float = 0.5) -> tuple: """Set the DAQ preview parameters Note that there's not much to do except for additional throtling if the - preview data stream is too fast. + preview data stream is too fast. Perhaps later we can add some online + processing to ophyd. Example: ---------- @@ -100,6 +109,7 @@ class StdDaqPreview(Device): def stage(self) -> list: """Start listening for preview data stream""" + self.connect() self._stop_polling = False self._mon = Thread(target=self.poll, daemon=True) self._mon.start() @@ -114,67 +124,170 @@ class StdDaqPreview(Device): """Stop a running preview""" self.unstage() - def plot(self): - """Plot the current image""" - image = self.image.get() - #plt.imshow(np.log10(image+1), vmin=0, vmax=5) - #plt.pause(self._throttle) + def poll(self): + """Collect streamed updates""" + self.status.set(StdDaqPreviewState.MONITORING, force=True) + t_last = time() + try: + while True: + try: + # pylint: disable=no-member + meta, data = self._socket.recv_multipart(flags=zmq.NOBLOCK) + header = json.loads(meta) + if header["type"]=="uint16": + image = np.frombuffer(data, dtype=np.uint16) + if image.size != np.prod(header['shape']): + raise ValueError(f"Unexpected array size of {image.size} for header: {header}") + image = image.reshape(header['shape']) - def plot_loop(self): - """Blocking loop to keep plotting""" - while True: - self.plot() + # Update image and update subscribers + t_curr = time() + t_elapsed = t_curr - t_last + if t_elapsed > self._throttle: + self.frame.put(header['frame'], force=True) + self.image_shape.put(header['shape'], force=True) + self.image.put(image, force=True) + self._run_subs(sub_type=self.SUB_MONITOR, value=image) + t_last=t_curr + logger.info(f"[{self.name}]\tUpdated frame {header['frame']}\tMean: {np.mean(image)}") - def proc(self, image): - """Basic image processing""" - return np.mean(image) + # Exit loop and finish monitoring + if self._stop_polling: + logger.info(f"[{self.name}]\tDetaching monitor") + break + except ValueError: + # Happens when ZMQ partially delivers the multipart message + pass + except zmq.error.Again: + # Happens when receive queue is empty + sleep(0.1) + except Exception as ex: + logger.info(f"[{self.name}]\t{str(ex)}") + raise + finally: + self._mon = None + self.status.set(StdDaqPreviewState.DETACHED, force=True) + + + +class StdDaqPreviewMixin(CustomDetectorMixin): + """Setup class for the standard DAQ preview stream + + Parent class: CustomDetectorMixin + """ + def on_stage(self): + """Start listening for preview data stream""" + self.parent.connect() + self._stop_polling = False + self._mon = Thread(target=self.poll, daemon=True) + self._mon.start() + + def on_unstage(self): + """Stop a running preview""" + self._stop_polling = True + + def on_stop(self): + """Stop a running preview""" + self.on_unstage() def poll(self): """Collect streamed updates""" - self.status.set("attached", force=True) + self.parent.status.set(StdDaqPreviewState.MONITORING, force=True) t_last = time() - while True: - try: - # pylint: disable=no-member - meta, data = self._socket.recv_multipart(flags=zmq.NOBLOCK) - header = json.loads(meta) - if header["type"]=="uint16": - image = np.frombuffer(data, dtype=np.uint16) - if image.size != np.prod(header['shape']): - self.status.set("detached", force=True) - raise ValueError(f"Unexpected array size of {image.size} for header: {header}") - image = image.reshape(header['shape']) - #print(f"Received frame {header['frame']}", file=sys.stderr) + try: + while True: + try: + # Exit loop and finish monitoring + if self._stop_polling: + break - t_curr = time() - t_elapsed = t_curr - t_last - if t_elapsed > self._throttle: - self.frame.put(header['frame'], force=True) - self.shape.put(header['shape'], force=True) - self.image.put(image, force=True) - self._run_subs(sub_type=self.SUB_MONITOR, value=image) + # pylint: disable=no-member + meta, data = self.parent._socket.recv_multipart(flags=zmq.NOBLOCK) + header = json.loads(meta) + if header["type"]=="uint16": + image = np.frombuffer(data, dtype=np.uint16) + image = image.reshape(header['shape']) - t_last=t_curr - print(f"[{self.name}]\tUpdated frame {header['frame']}\tMean: {np.mean(image)}", file=sys.stderr) + # Update image and update subscribers + t_curr = time() + t_elapsed = t_curr - t_last + if t_elapsed > self.parent.throttle.get(): + self.parent.frame.put(header['frame'], force=True) + self.parent.image_shape.put(header['shape'], force=True) + self.parent.image.put(image, force=True) + self.parent._run_subs(sub_type=self.parent.SUB_MONITOR, value=image) + t_last=t_curr + logger.info(f"[{self.parent.name}]\tUpdated frame {header['frame']}\tMean: {np.mean(image)}") + except ValueError: + # Happens when ZMQ partially delivers the multipart message + pass + except zmq.error.Again: + # Happens when receive queue is empty + sleep(0.1) + except Exception as ex: + logger.info(f"[{self.parent.name}]\t{str(ex)}") + raise + finally: + self._mon = None + self.parent.status.set(StdDaqPreviewState.DETACHED, force=True) + logger.info(f"[{self.parent.name}]\tDetaching monitor") - # Perform some basic analysis on the image - if self.process.get(): - self.value.put(self.proc(image), force=True) - print(f"Frame: {header['frame']}\tMin: {np.min(image)}\tMax: {np.max(image)}") - if self._stop_polling: - self.status.set("detached", force=True) - print("Detaching monitor") - break - except ValueError: - # Happens when ZMQ partially delivers the multipart message - pass - except zmq.error.Again: - sleep(0.1) - except Exception as ex: - print(ex) - self.status.set("detached", force=True) - raise +class StdDaqPreviewDetector(PSIDetectorBase): + """Detector wrapper class around the StdDaq preview image stream. + + This was meant to provide live image stream directly from the StdDAQ. + 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. + + 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 + SUB_MONITOR = "monitor" + _default_sub = SUB_MONITOR + + custom_prepare_cls = StdDaqPreviewMixin + + # Status attributes + url = Component(Signal, kind=Kind.config) + throttle = Component(Signal, value=0.1, kind=Kind.config) + status = Component(Signal, value=StdDaqPreviewState.UNKNOWN, kind=Kind.omitted) + image = Component(Signal, kind=Kind.normal) + frame = Component(Signal, kind=Kind.hinted) + image_shape = Component(Signal, kind=Kind.omitted) + + def __init__( + self, *args, url: str = "tcp://129.129.95.38:20000", parent: Device = None, **kwargs + ) -> None: + super().__init__(*args, parent=parent, **kwargs) + self.url._metadata["write_access"] = False + self.status._metadata["write_access"] = False + self.image._metadata["write_access"] = False + self.frame._metadata["write_access"] = False + self.image_shape._metadata["write_access"] = False + self.url.set(url, force=True).wait() + + # Connect ro the DAQ + self.connect() + + def connect(self): + """Connect to te StDAQs PUB-SUB streaming interface + + StdDAQ may reject connection for a few seconds when it restarts, + so if it fails, wait a bit and try to connect again. + """ + # pylint: disable=no-member + # Socket to talk to server + context = zmq.Context() + self._socket = context.socket(zmq.SUB) + self._socket.setsockopt(zmq.SUBSCRIBE, ZMQ_TOPIC_FILTER) + try: + self._socket.connect(self.url.get()) + except ConnectionRefusedError: + sleep(1) + self._socket.connect(self.url.get()) + # Automatically connect to MicroSAXS testbench if directly invoked From 6e98d5789cf4ba8b67603dec3d82675f2d08361a Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Fri, 26 Jul 2024 15:25:50 +0200 Subject: [PATCH 32/47] Bluesky-like parameter passing --- .../device_configs/microxas_test_bed.yaml | 9 ++- .../devices/gigafrost/gigafrostcamera.py | 41 ++++++------- .../devices/gigafrost/gigafrostclient.py | 4 +- tomcat_bec/devices/gigafrost/stddaq_rest.py | 61 +++++++++++++++---- tomcat_bec/devices/gigafrost/stddaq_ws.py | 19 +++--- 5 files changed, 85 insertions(+), 49 deletions(-) diff --git a/tomcat_bec/device_configs/microxas_test_bed.yaml b/tomcat_bec/device_configs/microxas_test_bed.yaml index 8edf221..9989460 100644 --- a/tomcat_bec/device_configs/microxas_test_bed.yaml +++ b/tomcat_bec/device_configs/microxas_test_bed.yaml @@ -114,6 +114,8 @@ gfclient: prefix: 'X02DA-CAM-GF2:' backend_url: 'http://xbl-daq-28:8080' auto_soft_enable: true + daq_ws_url: 'ws://xbl-daq-29:8080' + daq_rest_url: 'http://xbl-daq-29:5000' deviceTags: - camera enabled: true @@ -126,7 +128,8 @@ daq: description: Standard DAQ controls deviceClass: tomcat_bec.devices.gigafrost.stddaq_ws.StdDaqWsClient deviceConfig: - url: 'ws://xbl-daq-29:8080' + ws_url: 'ws://xbl-daq-29:8080' + rest_url: 'http://xbl-daq-29:5000' deviceTags: - std-daq enabled: true @@ -137,9 +140,9 @@ daq: daqcfg: description: Standard DAQ config - deviceClass: tomcat_bec.devices.gigafrost.stddaq_rest.StdDaqRestConfig + deviceClass: tomcat_bec.devices.gigafrost.stddaq_rest.StdDaqRestClient deviceConfig: - url: 'http://xbl-daq-29:5000' + rest_url: 'http://xbl-daq-29:5000' deviceTags: - std-daq enabled: true diff --git a/tomcat_bec/devices/gigafrost/gigafrostcamera.py b/tomcat_bec/devices/gigafrost/gigafrostcamera.py index 78adc5f..066159e 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostcamera.py +++ b/tomcat_bec/devices/gigafrost/gigafrostcamera.py @@ -411,21 +411,11 @@ class GigaFrostCamera(PSIDetectorBase): self.state.put(const.GfStatus.NEW, force=True) return super()._init() - def configure( - self, - d: dict=None, - nimages=10, - exposure=0.2, - period=1.0, - roix=2016, - roiy=2016, - scanid=0, - correction_mode=5, - ): + def configure(self, d: dict=None): """Configure the next scan with the GigaFRoST camera - Parameters - ---------- + Parameters as 'd' dictionary + ---------------------------- nimages : int, optional Number of images to be taken during each scan. Set to -1 for an unlimited number of images (limited by the ringbuffer size and @@ -434,9 +424,9 @@ class GigaFrostCamera(PSIDetectorBase): Exposure time [ms]. (default = 0.2) period : float, optional Exposure period [ms], ignored in soft trigger mode. (default = 1.0) - roix : int, optional + pixel_width : int, optional ROI size in the x-direction [pixels] (default = 2016) - roiy : int, optional + pixel_height : int, optional ROI size in the y-direction [pixels] (default = 2016) scanid : int, optional Scan identification number to be associated with the scan data @@ -455,14 +445,19 @@ class GigaFrostCamera(PSIDetectorBase): # If Bluesky style configure if d is not None: nimages = d.get('nimages', 10) - exposure = d.get('exposure', exposure) - period = d.get('period', period) - roix = d.get('roix', roix) - roiy = d.get('roiy', roiy) - scanid = d.get('scanid', scanid) - correction_mode = d.get('correction_mode', correction_mode) + exposure = d.get('exposure', 0.2) + period = d.get('period', 1.0) + pixel_width = d.get('pixel_width', 2016) + pixel_height = d.get('pixel_height', 2016) + pixel_width = d.get('image_width', pixel_width) + pixel_height = d.get('image_height', pixel_height) + pixel_width = d.get('roix', pixel_width) + pixel_height = d.get('roiy', pixel_height) + scanid = d.get('scanid', 0) + correction_mode = d.get('correction_mode', 5) # Stop acquisition + self.unstage() self.cmdStartCamera.set(0).wait() if self.autoSoftEnable.get(): self.cmdSoftEnable.set(0).wait() @@ -470,8 +465,8 @@ class GigaFrostCamera(PSIDetectorBase): # change settings self.cfgExposure.set(exposure).wait() self.cfgFramerate.set(period).wait() - self.cfgRoiX.set(roix).wait() - self.cfgRoiY.set(roiy).wait() + self.cfgRoiX.set(pixel_width).wait() + self.cfgRoiY.set(pixel_height).wait() self.cfgScanId.set(scanid).wait() self.cfgCntNum.set(nimages).wait() self.cfgCorrMode.set(correction_mode).wait() diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index c835af9..b34ea89 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -308,8 +308,8 @@ class GigaFrostClient(PSIDetectorBase): px_daq_h = self.daq.cfg.cfg_image_pixel_height.get() px_daq_w = self.daq.cfg.cfg_image_pixel_width.get() - px_gf_h = self.cam.cfgRoiX.get() - px_gf_w = self.cam.cfgRoiY.get() + px_gf_w = self.cam.cfgRoiX.get() + px_gf_h = self.cam.cfgRoiY.get() if px_daq_h != px_gf_h or px_daq_w != px_gf_w: raise RuntimeError(f"Different image size configured on GF and the DAQ") diff --git a/tomcat_bec/devices/gigafrost/stddaq_rest.py b/tomcat_bec/devices/gigafrost/stddaq_rest.py index 374b860..97ed957 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_rest.py +++ b/tomcat_bec/devices/gigafrost/stddaq_rest.py @@ -67,22 +67,16 @@ class StdDaqRestClient(Device): self.cfg_image_pixel_width.set(cfg['image_pixel_width']).wait() self.cfg_start_udp_port.set(cfg['start_udp_port']).wait() self.cfg_writer_user_id.set(cfg['writer_user_id']).wait() - self.cfg_submodule_info.set(cfg['submodule_info']).wait() + #self.cfg_submodule_info.set(cfg['submodule_info']).wait() self.cfg_max_number_of_forwarders_spawned.set(cfg['max_number_of_forwarders_spawned']).wait() self.cfg_use_all_forwarders.set(cfg['use_all_forwarders']).wait() self.cfg_module_sync_queue_size.set(cfg['module_sync_queue_size']).wait() - self.cfg_module_positions.set(cfg['module_positions']).wait() + #self.cfg_module_positions.set(cfg['module_positions']).wait() self._config_read = True - return cfg - - def write_daq_config(self): - """Write configuration ased on current PV values. Some fields might be - unchangeable. - """ - if not self._config_read: - raise RuntimeError("Pleae read config before editing") + return r + def _build_config(self): config = { 'detector_name': str(self.cfg_detector_name.get()), 'detector_type': str(self.cfg_detector_type.get()), @@ -92,21 +86,64 @@ class StdDaqRestClient(Device): 'image_pixel_width': int(self.cfg_image_pixel_width.get()), 'start_udp_port': int(self.cfg_start_udp_port.get()), 'writer_user_id': int(self.cfg_writer_user_id.get()), - 'submodule_info': self.cfg_submodule_info.get(), + 'submodule_info': {}, 'max_number_of_forwarders_spawned': int(self.cfg_max_number_of_forwarders_spawned.get()), 'use_all_forwarders': bool(self.cfg_use_all_forwarders.get()), 'module_sync_queue_size': int(self.cfg_module_sync_queue_size.get()), - 'module_positions': self.cfg_module_positions.get() + 'module_positions': {} } + return config + + def write_daq_config(self): + """Write configuration ased on current PV values. Some fields might be + unchangeable. + """ + if not self._config_read: + raise RuntimeError("Pleae read config before editing") + + config = self._build_config() params = {"user": "ioc", "config_file": "/etc/std_daq/configs/gf1.json"} r = requests.post(self._url_base +'/api/config/set', params=params, json=config, headers={"Content-Type": "application/json"}) if r.status_code != 200: raise ConnectionError(f"[{self.name}] Error {r.status_code}:\t{r.text}") + return r + + + def configure(self, d: dict=None): + """Configure the next scan with the GigaFRoST camera + + Parameters + ---------- + pixel_width : int, optional + Image size in the x-direction [pixels] (default = 2016) + pixel_height : int, optional + Image size in the y-direction [pixels] (default = 2016) + """ + old = self.read_configuration() + + # If Bluesky style configure + if d is not None: + # Only reconfigure if we're instructed + if ('pixel_width' in d) or ('pixel_height' in d) or ('image_width' in d) or ('image_height' in d): + pixel_width = d.get('pixel_width', 2016) + pixel_height = d.get('pixel_height', 2016) + pixel_width = d.get('image_width', pixel_width) + pixel_height = d.get('image_height', pixel_height) + + + self.cfg_image_pixel_height.set(pixel_height).wait() + self.cfg_image_pixel_width.set(pixel_width).wait() + + self.write_daq_config() + + new = self.read_configuration() + return old, new def read(self): self.read_daq_config() + return super().read() def stage(self) -> list: """Stage op: Read the current configuration from the DAQ diff --git a/tomcat_bec/devices/gigafrost/stddaq_ws.py b/tomcat_bec/devices/gigafrost/stddaq_ws.py index cd04be8..4181927 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_ws.py +++ b/tomcat_bec/devices/gigafrost/stddaq_ws.py @@ -84,7 +84,7 @@ class StdDaqWsClient(Device): self._mon = Thread(target=self.poll, daemon=True) self._mon.start() - def configure(self, d: dict=None, n_total: int = None, file_path: str = None) -> tuple: + def configure(self, d: dict={}) -> tuple: """Set the standard DAQ parameters for the next run Note that full reconfiguration is not possible with the websocket @@ -106,15 +106,16 @@ class StdDaqWsClient(Device): """ old_config = self.read_configuration() - # If Bluesky style configure - if d is not None: - n_total = d.get('n_total', None) - file_path = d.get('file_path', None) - if n_total is not None: - self.n_total.set(int(n_total)) - if file_path is not None: - self.output_file.set(str(file_path)) + if d is not None: + # Set acquisition parameters + if 'n_total' in d: + self.n_total.set(int(d['n_total'])) + if 'file_path' in d: + self.output_file.set(str(d['file_path'])) + # Configure DAQ + if 'pixel_width' in d or 'pixel_height' in d: + self.cfg.configure(d) new_config = self.read_configuration() return (old_config, new_config) From 242598d252e808f946bfcc3ac76c0b5bb88a349b Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Tue, 30 Jul 2024 16:01:18 +0200 Subject: [PATCH 33/47] Working GFclient class --- .../devices/gigafrost/gigafrostcamera.py | 24 ++-- .../devices/gigafrost/gigafrostclient.py | 105 +++++------------- .../devices/gigafrost/stddaq_preview.py | 34 +++--- tomcat_bec/devices/gigafrost/stddaq_rest.py | 13 ++- tomcat_bec/devices/gigafrost/stddaq_ws.py | 4 +- 5 files changed, 75 insertions(+), 105 deletions(-) diff --git a/tomcat_bec/devices/gigafrost/gigafrostcamera.py b/tomcat_bec/devices/gigafrost/gigafrostcamera.py index 066159e..40c51e5 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostcamera.py +++ b/tomcat_bec/devices/gigafrost/gigafrostcamera.py @@ -6,6 +6,7 @@ Created on Thu Jun 27 17:28:43 2024 @author: mohacsi_i """ +import sys from time import sleep from ophyd import Signal, SignalRO, Device, Component, EpicsSignal, EpicsSignalRO, Kind, DeviceStatus from ophyd.device import Staged @@ -175,22 +176,17 @@ class GigaFrostCameraMixin(CustomDetectorMixin): Specify actions to be executed upon receiving trigger signal. Return a DeviceStatus object or None """ - status = DeviceStatus(self.parent) + if self.parent.infoBusyFlag.get() in (0, 'IDLE'): + raise RuntimeError('GigaFrost must be running before triggering') # Soft triggering based on operation mode if self.parent.autoSoftEnable.get() and self.parent.trigger_mode=='auto' and self.parent.enable_mode=='soft': # BEC teststand operation mode: posedge of SoftEnable if Started self.parent.cmdSoftEnable.set(0).wait() self.parent.cmdSoftEnable.set(1).wait() - sleep_time = self.parent.cfgFramerate.value*self.parent.cfgCntNum.value*0.001+0.050 - # There's no status readback from the camera, so we just wait - sleep(sleep_time) - logger.info(f"[GF2] Slept for: {sleep_time} seconds") + else: self.parent.cmdSoftTrigger.set(1).wait() - status.set_finished() - - return status class GigaFrostCamera(PSIDetectorBase): @@ -411,6 +407,18 @@ class GigaFrostCamera(PSIDetectorBase): self.state.put(const.GfStatus.NEW, force=True) return super()._init() + def trigger(self) -> DeviceStatus: + + super().trigger() + + # There's no status readback from the camera, so we just wait + status = DeviceStatus(self) + sleep_time = self.cfgExposure.value*self.cfgCntNum.value*0.001+0.050 + sleep(sleep_time) + logger.info(f"[GF2] Slept for: {sleep_time} seconds") + status.set_finished() + return status + def configure(self, d: dict=None): """Configure the next scan with the GigaFRoST camera diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index b34ea89..aff02f2 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -32,11 +32,18 @@ except ModuleNotFoundError: from tomcat_bec.devices.gigafrost.gigafrostcamera import GigaFrostCamera +try: + from bec_lib import bec_logger + logger = bec_logger.logger +except ModuleNotFoundError: + import logging + logger = logging.getLogger("GfCam") -class GigafrostClientMixin(CustomDetectorMixin): + +class GigaFrostClientMixin(CustomDetectorMixin): """Mixin class to setup TOMCAT specific implementations of the detector. This class will be called by the custom_prepare_cls attribute of the detector class. @@ -83,51 +90,10 @@ class GigafrostClientMixin(CustomDetectorMixin): def on_init(self) -> None: - """Initialize the camera, set channel values""" - ## Stop acquisition - self.parent.cmdStartCamera.set(0).wait() + """Initialize the camera, set channel values - ### set entry to UDP table - # number of UDP ports to use - self.parent.cfgUdpNumPorts.set(2).wait() - # number of images to send to each UDP port before switching to next - self.parent.cfgUdpNumFrames.set(5).wait() - # offset in UDP table - where to find the first entry - self.parent.cfgUdpHtOffset.set(0).wait() - # activate changes - self.parent.cmdWriteService.set(1).wait() - - # Configure software triggering if needed - if self.parent._auto_soft_enable: - # trigger modes - self.parent.cfgCntStartBit.set(1).wait() - self.parent.cfgCntEndBit.set(0).wait() - - # set modes - self.parent.enable_mode = "soft" - self.parent.trigger_mode = "auto" - self.parent.exposure_mode = "timer" - - # line swap - on for west, off for east - self.parent.cfgLineSwapSW.set(1).wait() - self.parent.cfgLineSwapNW.set(1).wait() - self.parent.cfgLineSwapSE.set(0).wait() - self.parent.cfgLineSwapNE.set(0).wait() - - # Commit parameters - self.parent.cmdSetParam.set(1).wait() - - # Initialize data backend - n, s = self._define_backend_ip() - self.parent.ipNorth.put(n, force=True) - self.parent.ipSouth.put(s, force=True) - n, s = self._define_backend_mac() - self.parent.macNorth.put(n, force=True) - self.parent.macSouth.put(s, force=True) - # Set udp header table - self._set_udp_header_table() - - self.parent.state.put(const.GfStatus.INIT, force=True) + on_init is automatically called during __init__ of the sub devices. + """ return super().on_init() @@ -144,14 +110,13 @@ class GigafrostClientMixin(CustomDetectorMixin): It must be safe to assume that the device is ready for the scan to start immediately once this function is finished. """ - if self.parent.infoBusyFlag.value: - raise RuntimeError("Camera is already busy, unstage it first!") - # Switch to acquiring - self.parent.cmdStartCamera.set(1).wait() - self.parent.state.put(const.GfStatus.ACQUIRING, force=True) # Gigafrost can finish a run without explicit unstaging self.parent._staged = Staged.no + #self.parent.daq.stage() + #self.parent.cam.stage() + + def on_unstage(self) -> None: """ Specify actions to be executed during unstage. @@ -160,11 +125,8 @@ class GigafrostClientMixin(CustomDetectorMixin): and publishing the file location and file event message, with flagged done to BEC. """ - # Switch to idle - self.parent.cmdStartCamera.set(0).wait() - if self.parent.autoSoftEnable.get(): - self.cmdSoftEnable.set(0).wait() - self.parent.state.put(const.GfStatus.STOPPED, force=True) + self.parent.cam.unstage() + self.parent.daq.unstage() def on_stop(self) -> None: """ @@ -180,22 +142,7 @@ class GigafrostClientMixin(CustomDetectorMixin): Specify actions to be executed upon receiving trigger signal. Return a DeviceStatus object or None """ - status = DeviceStatus(self.parent) - - # Soft triggering based on operation mode - if self.parent.autoSoftEnable.get() and self.parent.trigger_mode=='auto' and self.parent.enable_mode=='soft': - # BEC teststand operation mode: posedge of SoftEnable if Started - self.parent.cmdSoftEnable.set(0).wait() - self.parent.cmdSoftEnable.set(1).wait() - sleep_time = self.parent.cfgFramerate.value*self.parent.cfgCntNum.value*0.001+0.050 - # There's no status readback from the camera, so we just wait - sleep(sleep_time) - print(f"[GF2] Slept for: {sleep_time} seconds", file=sys.stderr) - else: - self.parent.cmdSoftTrigger.set(1).wait() - status.set_finished() - - return status + return self.parent.cam.trigger() class GigaFrostClient(PSIDetectorBase): @@ -229,6 +176,8 @@ class GigaFrostClient(PSIDetectorBase): FRAMERATE : Ignored in soft trigger mode, period becomes 2xexposure time """ # pylint: disable=too-many-instance-attributes + custom_prepare_cls = GigaFrostClientMixin + USER_ACCESS = [""] cam = Component(GigaFrostCamera, prefix="X02DA-CAM-GF2:", name="cam") daq = Component(StdDaqWsClient, name="daq") @@ -279,10 +228,10 @@ class GigaFrostClient(PSIDetectorBase): Exposure time [ms]. (default = 0.2) period : float, optional Exposure period [ms], ignored in soft trigger mode. (default = 1.0) - roix : int, optional - ROI size in the x-direction [pixels] (default = 2016) - roiy : int, optional - ROI size in the y-direction [pixels] (default = 2016) + pixel_width : int, optional + Image size in the x-direction [pixels] (default = 2016) + pixel_height : int, optional + Image size in the y-direction [pixels] (default = 2016) scanid : int, optional Scan identification number to be associated with the scan data (default = 0) @@ -297,6 +246,8 @@ class GigaFrostClient(PSIDetectorBase): * 4: Invert pixel values, but do not apply any linearity correction * 5: Apply the full linearity correction """ + # Unstage camera (reconfiguration will anyway stop camera) + super().unstage() # If Bluesky style configure old = self.read_configuration() self.cam.configure(d) @@ -316,7 +267,9 @@ class GigaFrostClient(PSIDetectorBase): return super().stage() - + def trigger(self) -> DeviceStatus: + status = super().trigger() + return status # Automatically connect to MicroSAXS testbench if directly invoked if __name__ == "__main__": diff --git a/tomcat_bec/devices/gigafrost/stddaq_preview.py b/tomcat_bec/devices/gigafrost/stddaq_preview.py index 4c42fd3..d7fb4f0 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_preview.py +++ b/tomcat_bec/devices/gigafrost/stddaq_preview.py @@ -53,7 +53,7 @@ class StdDaqPreview(Device): frame = Component(Signal, kind=Kind.normal) image_shape = Component(Signal, kind=Kind.omitted) value = Component(Signal, kind=Kind.hinted) - _throttle = 0.05 + _throttle = 0.2 def __init__( self, *args, url: str = "tcp://129.129.95.38:20000", parent: Device = None, **kwargs @@ -89,7 +89,7 @@ class StdDaqPreview(Device): sleep(1) self._socket.connect(self.url.get()) - def configure(self, throttle: float = 0.5) -> tuple: + def configure(self, throttle: float = 0.2) -> tuple: """Set the DAQ preview parameters Note that there's not much to do except for additional throtling if the @@ -98,7 +98,7 @@ class StdDaqPreview(Device): Example: ---------- - std.configure(throttle=0.05) + std.configure(throttle=0.2) Parameters ---------- @@ -130,31 +130,31 @@ class StdDaqPreview(Device): t_last = time() try: while True: + # Exit loop and finish monitoring + if self._stop_polling: + logger.info(f"[{self.name}]\tDetaching monitor") + break + try: # pylint: disable=no-member meta, data = self._socket.recv_multipart(flags=zmq.NOBLOCK) - header = json.loads(meta) - if header["type"]=="uint16": - image = np.frombuffer(data, dtype=np.uint16) - if image.size != np.prod(header['shape']): - raise ValueError(f"Unexpected array size of {image.size} for header: {header}") - image = image.reshape(header['shape']) - - # Update image and update subscribers t_curr = time() t_elapsed = t_curr - t_last if t_elapsed > self._throttle: + header = json.loads(meta) + if header["type"]=="uint16": + image = np.frombuffer(data, dtype=np.uint16) + if image.size != np.prod(header['shape']): + raise ValueError(f"Unexpected array size of {image.size} for header: {header}") + image = image.reshape(header['shape']) + + # Update image and update subscribers self.frame.put(header['frame'], force=True) self.image_shape.put(header['shape'], force=True) self.image.put(image, force=True) self._run_subs(sub_type=self.SUB_MONITOR, value=image) t_last=t_curr - logger.info(f"[{self.name}]\tUpdated frame {header['frame']}\tMean: {np.mean(image)}") - - # Exit loop and finish monitoring - if self._stop_polling: - logger.info(f"[{self.name}]\tDetaching monitor") - break + logger.info(f"[{self.name}] Updated frame {header['frame']}\tShape: {header['shape']}\tMean: {np.mean(image):.3f}") except ValueError: # Happens when ZMQ partially delivers the multipart message pass diff --git a/tomcat_bec/devices/gigafrost/stddaq_rest.py b/tomcat_bec/devices/gigafrost/stddaq_rest.py index 97ed957..db1e4fc 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_rest.py +++ b/tomcat_bec/devices/gigafrost/stddaq_rest.py @@ -14,6 +14,15 @@ from ophyd import Device, Signal, Component, Kind import requests +try: + from bec_lib import bec_logger + logger = bec_logger.logger +except ModuleNotFoundError: + import logging + logger = logging.getLogger("GfCam") + + + class StdDaqRestClient(Device): """Wrapper class around the new StdDaq REST interface. @@ -137,7 +146,7 @@ class StdDaqRestClient(Device): self.cfg_image_pixel_width.set(pixel_width).wait() self.write_daq_config() - + logger.info(f"[{self.name}] Reconfigured the StdDAQ") new = self.read_configuration() return old, new @@ -159,7 +168,7 @@ class StdDaqRestClient(Device): return super().unstage() - def stop(self): + def stop(self, success=False): """Stop op: Read the current configuration from the DAQ """ self.unstage() diff --git a/tomcat_bec/devices/gigafrost/stddaq_ws.py b/tomcat_bec/devices/gigafrost/stddaq_ws.py index 4181927..97901b6 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_ws.py +++ b/tomcat_bec/devices/gigafrost/stddaq_ws.py @@ -138,7 +138,7 @@ class StdDaqWsClient(Device): self.status.put(reply["status"], force=True) elif reply["status"] in ("rejected"): raise RuntimeError( - f"Start command rejected (might be already running): {reply['reason']}" + f"Start StdDAQ command rejected (might be already running): {reply['reason']}" ) self._mon = Thread(target=self.poll, daemon=True) @@ -154,7 +154,7 @@ class StdDaqWsClient(Device): _ = self.message(message, wait_reply=False) return super().unstage() - def stop(self): + def stop(self, success=False): """ Stop a running acquisition WARN: This will also close the connection!!! From 938e411b587b0ff43eb469faf3bfab0bf8f99e19 Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Tue, 30 Jul 2024 17:39:53 +0200 Subject: [PATCH 34/47] Code quality fixes --- .../device_configs/microxas_test_bed.yaml | 29 +---- tomcat_bec/devices/gigafrost/gfconstants.py | 2 +- .../devices/gigafrost/gigafrostcamera.py | 114 +++++++++--------- .../devices/gigafrost/gigafrostclient.py | 88 +++----------- .../devices/gigafrost/stddaq_preview.py | 31 ++--- tomcat_bec/devices/gigafrost/stddaq_rest.py | 25 ++-- tomcat_bec/devices/gigafrost/stddaq_ws.py | 16 ++- 7 files changed, 105 insertions(+), 200 deletions(-) diff --git a/tomcat_bec/device_configs/microxas_test_bed.yaml b/tomcat_bec/device_configs/microxas_test_bed.yaml index 9989460..3a4bf14 100644 --- a/tomcat_bec/device_configs/microxas_test_bed.yaml +++ b/tomcat_bec/device_configs/microxas_test_bed.yaml @@ -124,35 +124,8 @@ gfclient: readoutPriority: monitored softwareTrigger: true -daq: - description: Standard DAQ controls - deviceClass: tomcat_bec.devices.gigafrost.stddaq_ws.StdDaqWsClient - deviceConfig: - ws_url: 'ws://xbl-daq-29:8080' - rest_url: 'http://xbl-daq-29:5000' - deviceTags: - - std-daq - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: monitored - softwareTrigger: false - -daqcfg: - description: Standard DAQ config - deviceClass: tomcat_bec.devices.gigafrost.stddaq_rest.StdDaqRestClient - deviceConfig: - rest_url: 'http://xbl-daq-29:5000' - deviceTags: - - std-daq - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: monitored - softwareTrigger: false - daq_stream0: - description: Standard DAQ preview stream 2 frames every 1000 + description: Standard DAQ preview stream 2 frames every 1000 image deviceClass: tomcat_bec.devices.gigafrost.stddaq_preview.StdDaqPreview deviceConfig: url: 'tcp://129.129.95.38:20000' diff --git a/tomcat_bec/devices/gigafrost/gfconstants.py b/tomcat_bec/devices/gigafrost/gfconstants.py index e0eab37..0577422 100644 --- a/tomcat_bec/devices/gigafrost/gfconstants.py +++ b/tomcat_bec/devices/gigafrost/gfconstants.py @@ -12,7 +12,7 @@ from enum import IntEnum gf_valid_enable_modes = ("soft", "external", "soft+ext", "always") gf_valid_exposure_modes = ("external", "timer", "soft") gf_valid_trigger_modes = ("auto", "external", "timer", "soft") -gf_valid_fix_nframe_modes = ("off", "start", "end", "start+end") +gf_valid_fix_nframe_modes = ("off", "start", "end", "start+end") # STATUS diff --git a/tomcat_bec/devices/gigafrost/gigafrostcamera.py b/tomcat_bec/devices/gigafrost/gigafrostcamera.py index 40c51e5..637804a 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostcamera.py +++ b/tomcat_bec/devices/gigafrost/gigafrostcamera.py @@ -6,9 +6,8 @@ Created on Thu Jun 27 17:28:43 2024 @author: mohacsi_i """ -import sys from time import sleep -from ophyd import Signal, SignalRO, Device, Component, EpicsSignal, EpicsSignalRO, Kind, DeviceStatus +from ophyd import Signal, Component, EpicsSignal, EpicsSignalRO, Kind, DeviceStatus from ophyd.device import Staged from ophyd_devices.interfaces.base_classes.psi_detector_base import ( @@ -37,23 +36,24 @@ except ModuleNotFoundError: class GigaFrostCameraMixin(CustomDetectorMixin): """Mixin class to setup TOMCAT specific implementations of the detector. - This class will be called by the custom_prepare_cls attribute of the detector class. - """ + This class will be called by the custom_prepare_cls attribute of the + detector class. + """ def _define_backend_ip(self): if self.parent.backendUrl.get() == const.BE3_DAFL_CLIENT: # xbl-daq-33 return const.BE3_NORTH_IP, const.BE3_SOUTH_IP - elif self.parent.backendUrl.get() == const.BE999_DAFL_CLIENT: + if self.parent.backendUrl.get() == const.BE999_DAFL_CLIENT: return const.BE999_NORTH_IP, const.BE999_SOUTH_IP - else: - raise RuntimeError(f"Backend {self.parent.backendUrl.get()} not recognized. {(const.GF1, const.GF2, const.GF3)}") + + raise RuntimeError(f"Backend {self.parent.backendUrl.get()} not recognized.") def _define_backend_mac(self): if self.parent.backendUrl.get() == const.BE3_DAFL_CLIENT: # xbl-daq-33 return const.BE3_NORTH_MAC, const.BE3_SOUTH_MAC - elif self.parent.backendUrl.get() == const.BE999_DAFL_CLIENT: + if self.parent.backendUrl.get() == const.BE999_DAFL_CLIENT: return const.BE999_NORTH_MAC, const.BE999_SOUTH_MAC - else: - raise RuntimeError(f"Backend {self.parent.backendUrl.get()} not recognized. {(const.GF1, const.GF2, const.GF3)}") + + raise RuntimeError(f"Backend {self.parent.backendUrl.get()} not recognized.") def _set_udp_header_table(self): """Set the communication parameters for the camera module""" @@ -123,10 +123,10 @@ class GigaFrostCameraMixin(CustomDetectorMixin): self.parent.macSouth.put(s, force=True) # Set udp header table self._set_udp_header_table() - + self.parent.state.put(const.GfStatus.INIT, force=True) return super().on_init() - + def on_stage(self) -> None: """Specify actions to be executed during stage @@ -372,7 +372,6 @@ class GigaFrostCamera(PSIDetectorBase): *, name, auto_soft_enable=False, - timeout=10, backend_url=const.BE999_DAFL_CLIENT, kind=None, read_attrs=None, @@ -450,6 +449,9 @@ class GigaFrostCamera(PSIDetectorBase): * 4: Invert pixel values, but do not apply any linearity correction * 5: Apply the full linearity correction """ + # Stop acquisition + self.unstage() + # If Bluesky style configure if d is not None: nimages = d.get('nimages', 10) @@ -464,20 +466,14 @@ class GigaFrostCamera(PSIDetectorBase): scanid = d.get('scanid', 0) correction_mode = d.get('correction_mode', 5) - # Stop acquisition - self.unstage() - self.cmdStartCamera.set(0).wait() - if self.autoSoftEnable.get(): - self.cmdSoftEnable.set(0).wait() - - # change settings - self.cfgExposure.set(exposure).wait() - self.cfgFramerate.set(period).wait() - self.cfgRoiX.set(pixel_width).wait() - self.cfgRoiY.set(pixel_height).wait() - self.cfgScanId.set(scanid).wait() - self.cfgCntNum.set(nimages).wait() - self.cfgCorrMode.set(correction_mode).wait() + # change settings + self.cfgExposure.set(exposure).wait() + self.cfgFramerate.set(period).wait() + self.cfgRoiX.set(pixel_width).wait() + self.cfgRoiY.set(pixel_height).wait() + self.cfgScanId.set(scanid).wait() + self.cfgCntNum.set(nimages).wait() + self.cfgCorrMode.set(correction_mode).wait() # Commit parameter self.cmdSetParam.set(1).wait() @@ -498,12 +494,12 @@ class GigaFrostCamera(PSIDetectorBase): mode_external = self.cfgTrigExpExt.get() if mode_soft and not mode_timer and not mode_external: return "soft" - elif not mode_soft and mode_timer and not mode_external: + if not mode_soft and mode_timer and not mode_external: return "timer" - elif not mode_soft and not mode_timer and mode_external: + if not mode_soft and not mode_timer and mode_external: return "external" - else: - return None + + return None @exposure_mode.setter def exposure_mode(self, exp_mode): @@ -514,11 +510,6 @@ class GigaFrostCamera(PSIDetectorBase): exp_mode : {'external', 'timer', 'soft'} The exposure mode to be set. """ - if exp_mode not in const.gf_valid_exposure_modes: - raise ValueError( - f"Invalid exposure mode! Valid modes are:\n" "{const.gf_valid_exposure_modes}" - ) - if exp_mode == "external": self.cfgTrigExpExt.set(1).wait() self.cfgTrigExpSoft.set(0).wait() @@ -531,6 +522,11 @@ class GigaFrostCamera(PSIDetectorBase): self.cfgTrigExpExt.set(0).wait() self.cfgTrigExpSoft.set(1).wait() self.cfgTrigExpTimer.set(0).wait() + else: + raise ValueError( + f"Invalid exposure mode! Valid modes are:\n{const.gf_valid_exposure_modes}" + ) + # Commit parameters self.cmdSetParam.set(1).wait() @@ -548,14 +544,14 @@ class GigaFrostCamera(PSIDetectorBase): if not start_bit and not end_bit: return "off" - elif start_bit and not end_bit: + if start_bit and not end_bit: return "start" - elif not start_bit and end_bit: + if not start_bit and end_bit: return "end" - elif start_bit and end_bit: + if start_bit and end_bit: return "start+end" - else: - return None + + return None @fix_nframes_mode.setter def fix_nframes_mode(self, mode): @@ -566,11 +562,6 @@ class GigaFrostCamera(PSIDetectorBase): mode : {'off', 'start', 'end', 'start+end'} The fixed number of frames mode to be applied. """ - if mode not in const.gf_valid_fix_nframe_modes: - raise ValueError( - f"Invalid fixed number of frames mode! Valid modes are:\n{const.gf_valid_fix_nframe_modes}" - ) - self._fix_nframes_mode = mode if self._fix_nframes_mode == "off": self.cfgCntStartBit.set(0).wait() @@ -584,6 +575,11 @@ class GigaFrostCamera(PSIDetectorBase): elif self._fix_nframes_mode == "start+end": self.cfgCntStartBit.set(1).wait() self.cfgCntEndBit.set(1).wait() + else: + raise ValueError( + f"Invalid fixed number of frames mode! Valid modes are:\n{const.gf_valid_fix_nframe_modes}" + ) + # Commit parameters self.cmdSetParam.set(1).wait() @@ -603,14 +599,14 @@ class GigaFrostCamera(PSIDetectorBase): mode_soft = self.cfgTrigSoft.get() if mode_auto: return "auto" - elif mode_soft: + if mode_soft: return "soft" - elif mode_timer: + if mode_timer: return "timer" - elif mode_external: + if mode_external: return "external" - else: - return None + + return None @trigger_mode.setter def trigger_mode(self, mode): @@ -621,11 +617,6 @@ class GigaFrostCamera(PSIDetectorBase): mode : {'auto', 'external', 'timer', 'soft'} The GigaFRoST trigger mode. """ - if mode not in const.gf_valid_trigger_modes: - raise ValueError( - "Invalid trigger mode! Valid modes are:\n" "{const.gf_valid_trigger_modes}" - ) - if mode == "auto": self.cfgTrigAuto.set(1).wait() self.cfgTrigSoft.set(0).wait() @@ -646,6 +637,11 @@ class GigaFrostCamera(PSIDetectorBase): self.cfgTrigSoft.set(1).wait() self.cfgTrigTimer.set(0).wait() self.cfgTrigExt.set(0).wait() + else: + raise ValueError( + "Invalid trigger mode! Valid modes are:\n{const.gf_valid_trigger_modes}" + ) + # Commit parameters self.cmdSetParam.set(1).wait() @@ -670,8 +666,8 @@ class GigaFrostCamera(PSIDetectorBase): return "always" elif mode_external and not mode_soft and not mode_auto: return "external" - else: - return None + + return None @enable_mode.setter def enable_mode(self, mode): @@ -684,7 +680,7 @@ class GigaFrostCamera(PSIDetectorBase): """ if mode not in const.gf_valid_enable_modes: raise ValueError( - "Invalid enable mode! Valid modes are:\n" "{const.gf_valid_enable_modes}" + "Invalid enable mode! Valid modes are:\n{const.gf_valid_enable_modes}" ) if mode == "soft": diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index aff02f2..4d5d93a 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -1,13 +1,12 @@ # -*- coding: utf-8 -*- """ -GigaFrost camera class module +GigaFrost client module that combines camera and DAQ Created on Thu Jun 27 17:28:43 2024 @author: mohacsi_i """ -from time import sleep -from ophyd import Device, Component, EpicsSignal, EpicsSignalRO, Kind, DeviceStatus +from ophyd import Component, DeviceStatus from ophyd.device import Staged from ophyd_devices.interfaces.base_classes.psi_detector_base import ( @@ -17,87 +16,26 @@ from ophyd_devices.interfaces.base_classes.psi_detector_base import ( try: import gfconstants as const -except ModuleNotFoundError: - import tomcat_bec.devices.gigafrost.gfconstants as const - - -try: from StdDaqClient import StdDaqClient -except ModuleNotFoundError: - from tomcat_bec.devices.gigafrost.stddaq_ws import StdDaqWsClient - -try: from gigafrostcamera import GigaFrostCamera except ModuleNotFoundError: + import tomcat_bec.devices.gigafrost.gfconstants as const + from tomcat_bec.devices.gigafrost.stddaq_ws import StdDaqClient from tomcat_bec.devices.gigafrost.gigafrostcamera import GigaFrostCamera -try: - from bec_lib import bec_logger - logger = bec_logger.logger -except ModuleNotFoundError: - import logging - logger = logging.getLogger("GfCam") - - - - - class GigaFrostClientMixin(CustomDetectorMixin): """Mixin class to setup TOMCAT specific implementations of the detector. This class will be called by the custom_prepare_cls attribute of the detector class. """ - - def _define_backend_ip(self): - if self.parent.backendUrl.get() == const.BE3_DAFL_CLIENT: # xbl-daq-33 - return const.BE3_NORTH_IP, const.BE3_SOUTH_IP - elif self.parent.backendUrl.get() == const.BE999_DAFL_CLIENT: - return const.BE999_NORTH_IP, const.BE999_SOUTH_IP - else: - raise RuntimeError(f"Backend not recognized. {(const.GF1, const.GF2, const.GF3)}") - - def _define_backend_mac(self): - if self.parent.backendUrl.get() == const.BE3_DAFL_CLIENT: # xbl-daq-33 - return const.BE3_NORTH_MAC, const.BE3_SOUTH_MAC - elif self.parent.backendUrl.get() == const.BE999_DAFL_CLIENT: - return const.BE999_NORTH_MAC, const.BE999_SOUTH_MAC - else: - raise RuntimeError(f"Backend not recognized. {(const.GF1, const.GF2, const.GF3)}") - - def _set_udp_header_table(self): - """Set the communication parameters for the camera module""" - self.parent.cfgConnectionParam.set(self._build_udp_header_table()).wait() - - def _build_udp_header_table(self): - """Build the header table for the communication""" - udp_header_table = [] - - for i in range(0, 64, 1): - for j in range(0, 8, 1): - dest_port = 2000 + 8 * i + j - source_port = 3000 + j - if j < 4: - extend_header_table( - udp_header_table, self.parent.macSouth, self.parent.ipSouth, dest_port, source_port - ) - else: - extend_header_table( - udp_header_table, self.parent.macNorth, self.parent.ipNorth, dest_port, source_port - ) - - return udp_header_table - - def on_init(self) -> None: """Initialize the camera, set channel values on_init is automatically called during __init__ of the sub devices. """ return super().on_init() - - - + def on_stage(self) -> None: """ Specify actions to be executed during stage in preparation for a scan. @@ -180,7 +118,7 @@ class GigaFrostClient(PSIDetectorBase): USER_ACCESS = [""] cam = Component(GigaFrostCamera, prefix="X02DA-CAM-GF2:", name="cam") - daq = Component(StdDaqWsClient, name="daq") + daq = Component(StdDaqClient, name="daq") def __init__( self, @@ -215,11 +153,15 @@ class GigaFrostClient(PSIDetectorBase): - def configure(self, d: dict=None, **kwargs): - """Configure the next scan with the GigaFRoST camera + def configure(self, d: dict=None): + """Configure the next scan with the GigaFRoST camera and standard DAQ backend Parameters ---------- + ntotal : int, optional + Total mumber of images to be taken during the whole scan. Set to -1 + for an unlimited number of images (limited by the ringbuffer size and + backend speed). (default = 10000) nimages : int, optional Number of images to be taken during each scan. Set to -1 for an unlimited number of images (limited by the ringbuffer size and @@ -263,10 +205,10 @@ class GigaFrostClient(PSIDetectorBase): px_gf_h = self.cam.cfgRoiY.get() if px_daq_h != px_gf_h or px_daq_w != px_gf_w: - raise RuntimeError(f"Different image size configured on GF and the DAQ") - + raise RuntimeError("Different image size configured on GF and the DAQ") + return super().stage() - + def trigger(self) -> DeviceStatus: status = super().trigger() return status diff --git a/tomcat_bec/devices/gigafrost/stddaq_preview.py b/tomcat_bec/devices/gigafrost/stddaq_preview.py index d7fb4f0..0f27705 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_preview.py +++ b/tomcat_bec/devices/gigafrost/stddaq_preview.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -Standard DAQ class module +Standard DAQ preview image stream module Created on Thu Jun 27 17:28:43 2024 @@ -8,10 +8,10 @@ Created on Thu Jun 27 17:28:43 2024 """ import json import enum -import zmq -import numpy as np from time import sleep, time from threading import Thread +import zmq +import numpy as np from ophyd import Device, Signal, Component, Kind from ophyd_devices.interfaces.base_classes.psi_detector_base import ( CustomDetectorMixin, @@ -89,24 +89,6 @@ class StdDaqPreview(Device): sleep(1) self._socket.connect(self.url.get()) - def configure(self, throttle: float = 0.2) -> tuple: - """Set the DAQ preview parameters - - Note that there's not much to do except for additional throtling if the - preview data stream is too fast. Perhaps later we can add some online - processing to ophyd. - - Example: - ---------- - std.configure(throttle=0.2) - - Parameters - ---------- - throttle : float, optional - Additional throtling for the ophyd device. (default = 0.05 sec) - """ - self._throttle = throttle - def stage(self) -> list: """Start listening for preview data stream""" self.connect() @@ -120,7 +102,7 @@ class StdDaqPreview(Device): self._stop_polling = True return super().unstage() - def stop(self): + def stop(self, success=False): """Stop a running preview""" self.unstage() @@ -154,7 +136,10 @@ class StdDaqPreview(Device): self.image.put(image, force=True) self._run_subs(sub_type=self.SUB_MONITOR, value=image) t_last=t_curr - logger.info(f"[{self.name}] Updated frame {header['frame']}\tShape: {header['shape']}\tMean: {np.mean(image):.3f}") + logger.info( + f"[{self.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 diff --git a/tomcat_bec/devices/gigafrost/stddaq_rest.py b/tomcat_bec/devices/gigafrost/stddaq_rest.py index db1e4fc..e4ff21d 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_rest.py +++ b/tomcat_bec/devices/gigafrost/stddaq_rest.py @@ -1,15 +1,11 @@ # -*- coding: utf-8 -*- """ -Standard DAQ class module +Standard DAQ configuration interface module through the latrest REST API Created on Thu Jun 27 17:28:43 2024 @author: mohacsi_i """ -import sys -import json -from time import sleep -from threading import Thread from ophyd import Device, Signal, Component, Kind import requests @@ -62,7 +58,11 @@ class StdDaqRestClient(Device): def read_daq_config(self): """Read the current configuration from the JSON file """ - r = requests.get(self._url_base + '/api/config/get', params={'config_file': "/etc/std_daq/configs/gf1.json", 'user':"ioc"}) + r = requests.get( + self._url_base + '/api/config/get', + params={'config_file': "/etc/std_daq/configs/gf1.json", 'user':"ioc"}, + timeout=2 + ) if r.status_code != 200: raise ConnectionError(f"[{self.name}] Error {r.status_code}:\t{r.text}") @@ -111,15 +111,20 @@ class StdDaqRestClient(Device): raise RuntimeError("Pleae read config before editing") config = self._build_config() - + params = {"user": "ioc", "config_file": "/etc/std_daq/configs/gf1.json"} - r = requests.post(self._url_base +'/api/config/set', params=params, json=config, headers={"Content-Type": "application/json"}) + r = requests.post( + self._url_base +'/api/config/set', + params=params, + json=config, + timeout=2, + headers={"Content-Type": "application/json"} + ) if r.status_code != 200: raise ConnectionError(f"[{self.name}] Error {r.status_code}:\t{r.text}") return r - def configure(self, d: dict=None): """Configure the next scan with the GigaFRoST camera @@ -134,7 +139,7 @@ class StdDaqRestClient(Device): # If Bluesky style configure if d is not None: - # Only reconfigure if we're instructed + # Only reconfigure if we're instructed if ('pixel_width' in d) or ('pixel_height' in d) or ('image_width' in d) or ('image_height' in d): pixel_width = d.get('pixel_width', 2016) pixel_height = d.get('pixel_height', 2016) diff --git a/tomcat_bec/devices/gigafrost/stddaq_ws.py b/tomcat_bec/devices/gigafrost/stddaq_ws.py index 97901b6..d5806dd 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_ws.py +++ b/tomcat_bec/devices/gigafrost/stddaq_ws.py @@ -1,12 +1,11 @@ # -*- coding: utf-8 -*- """ -Standard DAQ class module +Standard DAQ control interface module through the websocket API Created on Thu Jun 27 17:28:43 2024 @author: mohacsi_i """ -import sys import json from time import sleep from threading import Thread @@ -50,7 +49,12 @@ class StdDaqWsClient(Device): cfg = Component(StdDaqRestClient, kind=Kind.config) def __init__( - self, *args, ws_url: str = "ws://localhost:8080", rest_url="http://localhost:5000", parent: Device = None, **kwargs + self, + *args, + ws_url: str = "ws://localhost:8080", + rest_url="http://localhost:5000", + parent: Device = None, + **kwargs ) -> None: self.__class__.__dict__['cfg'].kwargs['rest_url'] = rest_url @@ -60,6 +64,7 @@ class StdDaqWsClient(Device): self._mon = None # Connect ro the DAQ + self._client = None self.connect() def connect(self): @@ -84,7 +89,7 @@ class StdDaqWsClient(Device): self._mon = Thread(target=self.poll, daemon=True) self._mon.start() - def configure(self, d: dict={}) -> tuple: + def configure(self, d: dict=None) -> tuple: """Set the standard DAQ parameters for the next run Note that full reconfiguration is not possible with the websocket @@ -197,7 +202,7 @@ class StdDaqWsClient(Device): try: message = json.loads(msg) self.status.put(message["status"], force=True) - except (ConnectionClosedError, ConnectionClosedOK) as ex: + except (ConnectionClosedError, ConnectionClosedOK): return except Exception as ex: print(ex) @@ -208,7 +213,6 @@ class StdDaqWsClient(Device): class StdDaqClient(StdDaqWsClient): """Just an alias""" - pass # Automatically connect to MicroSAXS testbench if directly invoked From fa2e5c1fd0f8e675a24cb01ecdc14fcd846c9293 Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Wed, 21 Aug 2024 17:54:54 +0200 Subject: [PATCH 35/47] Changes before redeployment --- .../devices/gigafrost/gigafrostclient.py | 6 +-- .../devices/gigafrost/stddaq_preview.py | 4 +- tomcat_bec/devices/gigafrost/stddaq_rest.py | 22 +++++--- tomcat_bec/devices/gigafrost/stddaq_ws.py | 51 +++++++++++-------- 4 files changed, 51 insertions(+), 32 deletions(-) diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index 4d5d93a..a53f6d2 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -139,7 +139,7 @@ class GigaFrostClient(PSIDetectorBase): self.__class__.__dict__["cam"].kwargs['auto_soft_enable'] = auto_soft_enable self.__class__.__dict__["daq"].kwargs['ws_url'] = daq_ws_url self.__class__.__dict__["daq"].kwargs['rest_url'] = daq_rest_url - #self.__class__.__dict__["daq"].__class__.__dict__["cfg"].kwargs['rest_url'] = daq_rest_url + #self.__class__.__dict__["daq"].__class__.__dict__["config"].kwargs['rest_url'] = daq_rest_url super().__init__( prefix=prefix, @@ -198,8 +198,8 @@ class GigaFrostClient(PSIDetectorBase): return old, new def stage(self): - px_daq_h = self.daq.cfg.cfg_image_pixel_height.get() - px_daq_w = self.daq.cfg.cfg_image_pixel_width.get() + px_daq_h = self.daq.config.cfg_image_pixel_height.get() + px_daq_w = self.daq.config.cfg_image_pixel_width.get() px_gf_w = self.cam.cfgRoiX.get() px_gf_h = self.cam.cfgRoiY.get() diff --git a/tomcat_bec/devices/gigafrost/stddaq_preview.py b/tomcat_bec/devices/gigafrost/stddaq_preview.py index 0f27705..832a6a3 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_preview.py +++ b/tomcat_bec/devices/gigafrost/stddaq_preview.py @@ -53,7 +53,7 @@ class StdDaqPreview(Device): frame = Component(Signal, kind=Kind.normal) image_shape = Component(Signal, kind=Kind.omitted) value = Component(Signal, kind=Kind.hinted) - _throttle = 0.2 + throttle = Component(Signal, value=0.2, kind=Kind.omitted) def __init__( self, *args, url: str = "tcp://129.129.95.38:20000", parent: Device = None, **kwargs @@ -122,7 +122,7 @@ class StdDaqPreview(Device): meta, data = self._socket.recv_multipart(flags=zmq.NOBLOCK) t_curr = time() t_elapsed = t_curr - t_last - if t_elapsed > self._throttle: + if t_elapsed > self.throttle.get(): header = json.loads(meta) if header["type"]=="uint16": image = np.frombuffer(data, dtype=np.uint16) diff --git a/tomcat_bec/devices/gigafrost/stddaq_rest.py b/tomcat_bec/devices/gigafrost/stddaq_rest.py index e4ff21d..b00b9ae 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_rest.py +++ b/tomcat_bec/devices/gigafrost/stddaq_rest.py @@ -6,6 +6,7 @@ Created on Thu Jun 27 17:28:43 2024 @author: mohacsi_i """ +from time import sleep from ophyd import Device, Signal, Component, Kind import requests @@ -30,6 +31,7 @@ class StdDaqRestClient(Device): # pylint: disable=too-many-instance-attributes # Status attributes + rest_url = Component(Signal, kind=Kind.config) cfg_detector_name = Component(Signal, kind=Kind.config) cfg_detector_type = Component(Signal, kind=Kind.config) cfg_n_modules = Component(Signal, kind=Kind.config) @@ -50,7 +52,8 @@ class StdDaqRestClient(Device): self, *args, rest_url: str = "http://localhost:5000", parent: Device = None, **kwargs ) -> None: super().__init__(*args, parent=parent, **kwargs) - self._url_base = rest_url + self.rest_url._metadata["write_access"] = False + self.rest_url.put(rest_url, force=True) # Connect ro the DAQ and initialize values self.read_daq_config() @@ -59,7 +62,7 @@ class StdDaqRestClient(Device): """Read the current configuration from the JSON file """ r = requests.get( - self._url_base + '/api/config/get', + self.rest_url.get() + '/api/config/get', params={'config_file': "/etc/std_daq/configs/gf1.json", 'user':"ioc"}, timeout=2 ) @@ -95,6 +98,7 @@ class StdDaqRestClient(Device): 'image_pixel_width': int(self.cfg_image_pixel_width.get()), 'start_udp_port': int(self.cfg_start_udp_port.get()), 'writer_user_id': int(self.cfg_writer_user_id.get()), + 'log_level': "debug", 'submodule_info': {}, 'max_number_of_forwarders_spawned': int(self.cfg_max_number_of_forwarders_spawned.get()), 'use_all_forwarders': bool(self.cfg_use_all_forwarders.get()), @@ -112,10 +116,10 @@ class StdDaqRestClient(Device): config = self._build_config() - params = {"user": "ioc", "config_file": "/etc/std_daq/configs/gf1.json"} - + #params = {"user": "ioc", "config_file": "/etc/std_daq/configs/gf1.json"} + params = {"user": "ioc"} r = requests.post( - self._url_base +'/api/config/set', + self.rest_url.get() +'/api/config/set', params=params, json=config, timeout=2, @@ -146,12 +150,14 @@ class StdDaqRestClient(Device): pixel_width = d.get('image_width', pixel_width) pixel_height = d.get('image_height', pixel_height) - self.cfg_image_pixel_height.set(pixel_height).wait() self.cfg_image_pixel_width.set(pixel_width).wait() self.write_daq_config() logger.info(f"[{self.name}] Reconfigured the StdDAQ") + # No feedback on restart, we just sleep + sleep(3) + new = self.read_configuration() return old, new @@ -159,6 +165,10 @@ class StdDaqRestClient(Device): self.read_daq_config() return super().read() + def read_configuration(self): + self.read_daq_config() + return super().read_configuration() + def stage(self) -> list: """Stage op: Read the current configuration from the DAQ """ diff --git a/tomcat_bec/devices/gigafrost/stddaq_ws.py b/tomcat_bec/devices/gigafrost/stddaq_ws.py index d5806dd..78ea0e7 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_ws.py +++ b/tomcat_bec/devices/gigafrost/stddaq_ws.py @@ -21,7 +21,7 @@ except ModuleNotFoundError: -class StdDaqWsClient(Device): +class StdDaqClient(Device): """StdDaq API This class combines the new websocket and REST interfaces that were meant @@ -46,17 +46,17 @@ class StdDaqWsClient(Device): n_total = Component(Signal, value=10000, kind=Kind.config) file_path = Component(Signal, value="/gpfs/test/test-beamline", kind=Kind.config) - cfg = Component(StdDaqRestClient, kind=Kind.config) + config = Component(StdDaqRestClient, kind=Kind.config) def __init__( self, *args, ws_url: str = "ws://localhost:8080", - rest_url="http://localhost:5000", + rest_url: str="http://localhost:5000", parent: Device = None, **kwargs ) -> None: - self.__class__.__dict__['cfg'].kwargs['rest_url'] = rest_url + self.__class__.__dict__['config'].kwargs['rest_url'] = rest_url super().__init__(*args, parent=parent, **kwargs) self.status._metadata["write_access"] = False @@ -73,11 +73,16 @@ class StdDaqWsClient(Device): StdDAQ may reject connection for a few seconds, so if it fails, wait a bit and try to connect again. """ - try: - self._client = connect(self._ws_url) - except ConnectionRefusedError: - sleep(5) - self._client = connect(self._ws_url) + num_retry = 0 + while num_retry < 5: + try: + self._client = connect(self._ws_url) + break + except ConnectionRefusedError: + num_retry += 1 + sleep(3) + if num_retry==5: + raise ConnectionRefusedError("The standard DAQ websocket interface refused connection 5 times.") def __del__(self): """Try to close the socket""" @@ -116,11 +121,15 @@ class StdDaqWsClient(Device): # Set acquisition parameters if 'n_total' in d: self.n_total.set(int(d['n_total'])) + if 'ntotal' in d: + self.n_total.set(int(d['ntotal'])) if 'file_path' in d: self.output_file.set(str(d['file_path'])) # Configure DAQ if 'pixel_width' in d or 'pixel_height' in d: - self.cfg.configure(d) + self.stop() + sleep(1) + self.config.configure(d) new_config = self.read_configuration() return (old_config, new_config) @@ -148,6 +157,7 @@ class StdDaqWsClient(Device): self._mon = Thread(target=self.poll, daemon=True) self._mon.start() + sleep(3) return super().stage() def unstage(self): @@ -155,8 +165,12 @@ class StdDaqWsClient(Device): WARN: This will also close the connection!!! """ - message = {"command": "stop"} - _ = self.message(message, wait_reply=False) + # The poller thread locks recv raising a RuntimeError + try: + message = {"command": "stop"} + self.message(message, wait_reply=False) + except RuntimeError: + pass return super().unstage() def stop(self, success=False): @@ -164,9 +178,7 @@ class StdDaqWsClient(Device): WARN: This will also close the connection!!! """ - message = {"command": "stop"} - # The poller thread locks recv raising a RuntimeError - self.message(message, wait_reply=False) + self.unstage() def message(self, message: dict, timeout=1, wait_reply=True): """Send a message to the StdDAQ and receive a reply @@ -198,6 +210,7 @@ class StdDaqWsClient(Device): def poll(self): """Monitor status messages until connection is open""" try: + sleep(0.1) for msg in self._client: try: message = json.loads(msg) @@ -211,11 +224,7 @@ class StdDaqWsClient(Device): self._mon = None -class StdDaqClient(StdDaqWsClient): - """Just an alias""" - - -# Automatically connect to MicroSAXS testbench if directly invoked +# Automatically connect to microXAS testbench if directly invoked if __name__ == "__main__": - daq = StdDaqWsClient(name="daq", ws_url="ws://xbl-daq-29:8080", rest_url="http://xbl-daq-29:5000") + daq = StdDaqClient(name="daq", ws_url="ws://xbl-daq-29:8080", rest_url="http://xbl-daq-29:5000") daq.wait_for_connection() From 069c902be2176aec925cd0b1843cbf0519f5f8b8 Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Fri, 23 Aug 2024 15:57:56 +0200 Subject: [PATCH 36/47] Getting good, now debugging daq --- .../device_configs/microxas_test_bed.yaml | 16 +++++- .../devices/gigafrost/gigafrostclient.py | 4 +- .../devices/gigafrost/stddaq_preview.py | 4 +- tomcat_bec/devices/gigafrost/stddaq_rest.py | 57 +++++++++++-------- tomcat_bec/devices/gigafrost/stddaq_ws.py | 47 ++++++++------- 5 files changed, 79 insertions(+), 49 deletions(-) diff --git a/tomcat_bec/device_configs/microxas_test_bed.yaml b/tomcat_bec/device_configs/microxas_test_bed.yaml index 3a4bf14..f29c7c0 100644 --- a/tomcat_bec/device_configs/microxas_test_bed.yaml +++ b/tomcat_bec/device_configs/microxas_test_bed.yaml @@ -128,7 +128,7 @@ daq_stream0: description: Standard DAQ preview stream 2 frames every 1000 image deviceClass: tomcat_bec.devices.gigafrost.stddaq_preview.StdDaqPreview deviceConfig: - url: 'tcp://129.129.95.38:20000' + url: 'tcp://129.129.95.38:20002' deviceTags: - std-daq enabled: true @@ -150,3 +150,17 @@ daq_stream1: readoutPriority: monitored softwareTrigger: false +daq_stream2: + description: Standard DAQ preview stream from first server + deviceClass: tomcat_bec.devices.gigafrost.stddaq_preview.StdDaqPreview + deviceConfig: + url: 'tcp://129.129.95.40:20001' + deviceTags: + - std-daq + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: monitored + softwareTrigger: false + + diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index a53f6d2..5898f97 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -198,8 +198,8 @@ class GigaFrostClient(PSIDetectorBase): return old, new def stage(self): - px_daq_h = self.daq.config.cfg_image_pixel_height.get() - px_daq_w = self.daq.config.cfg_image_pixel_width.get() + px_daq_h = self.daq.config.cfg_pixel_height.get() + px_daq_w = self.daq.config.cfg_pixel_width.get() px_gf_w = self.cam.cfgRoiX.get() px_gf_h = self.cam.cfgRoiY.get() diff --git a/tomcat_bec/devices/gigafrost/stddaq_preview.py b/tomcat_bec/devices/gigafrost/stddaq_preview.py index 832a6a3..4b11978 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_preview.py +++ b/tomcat_bec/devices/gigafrost/stddaq_preview.py @@ -43,7 +43,7 @@ class StdDaqPreview(Device): # pylint: disable=too-many-instance-attributes # Subscriptions for plotting image - SUB_MONITOR = "monitor" + SUB_MONITOR = "device_monitor_2d" _default_sub = SUB_MONITOR # Status attributes @@ -229,7 +229,7 @@ class StdDaqPreviewDetector(PSIDetectorBase): cam_widget = gui.add_dock('cam_dock1').add_widget('BECFigure').image('daq_stream1') """ # Subscriptions for plotting image - SUB_MONITOR = "monitor" + SUB_MONITOR = "device_monitor_2d" _default_sub = SUB_MONITOR custom_prepare_cls = StdDaqPreviewMixin diff --git a/tomcat_bec/devices/gigafrost/stddaq_rest.py b/tomcat_bec/devices/gigafrost/stddaq_rest.py index b00b9ae..7f21e94 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_rest.py +++ b/tomcat_bec/devices/gigafrost/stddaq_rest.py @@ -10,7 +10,6 @@ from time import sleep from ophyd import Device, Signal, Component, Kind import requests - try: from bec_lib import bec_logger logger = bec_logger.logger @@ -19,25 +18,37 @@ except ModuleNotFoundError: logger = logging.getLogger("GfCam") - class StdDaqRestClient(Device): """Wrapper class around the new StdDaq REST interface. - This was meant to replace or extend the websocket inteface that replaced - the documented python client. We can finally read configuration through - standard HTTP requests, although the secondary server is not reachable - at the time. + This was meant to extend the websocket inteface that replaced the documented + python client. It is used as a part of the StdDaqClient aggregator class. + Good to know that the stdDAQ restarts all services after reconfiguration. + + The standard DAQ configuration is a single JSON file locally autodeployed + to the DAQ servers (as root!!!). It can only be written through the REST API + via standard HTTP requests. The DAQ might be distributed across several servers, + we'll only interface with the primary REST interface will synchronize with + all secondary REST servers. In the past this was a source of problems. + + Example: + ''' + daqcfg = StdDaqRestClient(name="daqcfg", rest_url="http://xbl-daq-29:5000") + ''' """ # pylint: disable=too-many-instance-attributes + USER_ACCESS = ["write_daq_config"] + _config_read = False + # Status attributes rest_url = Component(Signal, kind=Kind.config) cfg_detector_name = Component(Signal, kind=Kind.config) cfg_detector_type = Component(Signal, kind=Kind.config) cfg_n_modules = Component(Signal, kind=Kind.config) cfg_bit_depth = Component(Signal, kind=Kind.config) - cfg_image_pixel_height = Component(Signal, kind=Kind.config) - cfg_image_pixel_width = Component(Signal, kind=Kind.config) + cfg_pixel_height = Component(Signal, kind=Kind.config) + cfg_pixel_width = Component(Signal, kind=Kind.config) cfg_start_udp_port = Component(Signal, kind=Kind.config) cfg_writer_user_id = Component(Signal, kind=Kind.config) cfg_submodule_info = Component(Signal, kind=Kind.config) @@ -46,8 +57,6 @@ class StdDaqRestClient(Device): cfg_module_sync_queue_size = Component(Signal, kind=Kind.config) cfg_module_positions = Component(Signal, kind=Kind.config) - _config_read = False - def __init__( self, *args, rest_url: str = "http://localhost:5000", parent: Device = None, **kwargs ) -> None: @@ -58,7 +67,7 @@ class StdDaqRestClient(Device): # Connect ro the DAQ and initialize values self.read_daq_config() - def read_daq_config(self): + def read_daq_config(self) -> dict: """Read the current configuration from the JSON file """ r = requests.get( @@ -75,8 +84,8 @@ class StdDaqRestClient(Device): self.cfg_n_modules.set(cfg['n_modules']).wait() self.cfg_bit_depth.set(cfg['bit_depth']).wait() - self.cfg_image_pixel_height.set(cfg['image_pixel_height']).wait() - self.cfg_image_pixel_width.set(cfg['image_pixel_width']).wait() + self.cfg_pixel_height.set(cfg['image_pixel_height']).wait() + self.cfg_pixel_width.set(cfg['image_pixel_width']).wait() self.cfg_start_udp_port.set(cfg['start_udp_port']).wait() self.cfg_writer_user_id.set(cfg['writer_user_id']).wait() #self.cfg_submodule_info.set(cfg['submodule_info']).wait() @@ -88,14 +97,14 @@ class StdDaqRestClient(Device): self._config_read = True return r - def _build_config(self): + def _build_config(self) -> dict: config = { 'detector_name': str(self.cfg_detector_name.get()), 'detector_type': str(self.cfg_detector_type.get()), 'n_modules': int(self.cfg_n_modules.get()), 'bit_depth': int(self.cfg_bit_depth.get()), - 'image_pixel_height': int(self.cfg_image_pixel_height.get()), - 'image_pixel_width': int(self.cfg_image_pixel_width.get()), + 'image_pixel_height': int(self.cfg_pixel_height.get()), + 'image_pixel_width': int(self.cfg_pixel_width.get()), 'start_udp_port': int(self.cfg_start_udp_port.get()), 'writer_user_id': int(self.cfg_writer_user_id.get()), 'log_level': "debug", @@ -139,6 +148,7 @@ class StdDaqRestClient(Device): pixel_height : int, optional Image size in the y-direction [pixels] (default = 2016) """ + # Reads the current config old = self.read_configuration() # If Bluesky style configure @@ -150,13 +160,14 @@ class StdDaqRestClient(Device): pixel_width = d.get('image_width', pixel_width) pixel_height = d.get('image_height', pixel_height) - self.cfg_image_pixel_height.set(pixel_height).wait() - self.cfg_image_pixel_width.set(pixel_width).wait() + if self.cfg_pixel_height.get()!=pixel_height or self.cfg_pixel_width.get() != pixel_width: + self.cfg_pixel_height.set(pixel_height).wait() + self.cfg_pixel_width.set(pixel_width).wait() - self.write_daq_config() - logger.info(f"[{self.name}] Reconfigured the StdDAQ") - # No feedback on restart, we just sleep - sleep(3) + self.write_daq_config() + logger.info(f"[{self.name}] Reconfigured the StdDAQ") + # No feedback on restart, we just sleep + sleep(3) new = self.read_configuration() return old, new @@ -175,14 +186,12 @@ class StdDaqRestClient(Device): self.read_daq_config() return super().stage() - def unstage(self): """Unstage op: Read the current configuration from the DAQ """ self.read_daq_config() return super().unstage() - def stop(self, success=False): """Stop op: Read the current configuration from the DAQ """ diff --git a/tomcat_bec/devices/gigafrost/stddaq_ws.py b/tomcat_bec/devices/gigafrost/stddaq_ws.py index 78ea0e7..0d8fcb4 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_ws.py +++ b/tomcat_bec/devices/gigafrost/stddaq_ws.py @@ -19,25 +19,28 @@ except ModuleNotFoundError: from tomcat_bec.devices.gigafrost.stddaq_rest import StdDaqRestClient - - class StdDaqClient(Device): """StdDaq API - This class combines the new websocket and REST interfaces that were meant - to replace the documented python client. The websocket interface starts - and stops the acquisition and provides status, while the REST interface - can read and write the configuration. + This class combines the new websocket and REST interfaces of the stdDAQ that + were meant to replace the documented python client. The websocket interface + starts and stops the acquisition and provides status, while the REST + interface can read and write the configuration. The DAQ needs to restart + all services to reconfigure with a new config. + + The websocket provides status updates about a running acquisition but the + interface breaks connection at the end of the run. The standard DAQ configuration is a single JSON file locally autodeployed - to the DAQ servers (as root!!!). It can only be written through a primary - REST API that is semi-supported, as there's no frontend group. The DAQ - might be distributed across several servers, meaning that the primary REST - interface will try to synchronize with secondary REST servers, but this - might fail, yielding a flawed configuration. - - daq = StdDaqWsClient(name="daq", ws_url="ws://xbl-daq-29:8080", rest_url="http://xbl-daq-29:5000") + to the DAQ servers (as root!!!). It can only be written through a REST API + that is semi-supported. The DAQ might be distributed across several servers, + we'll only interface with the primary REST interface will synchronize with + all secondary REST servers. In the past this was a source of problems. + Example: + ''' + daq = StdDaqClient(name="daq", ws_url="ws://xbl-daq-29:8080", rest_url="http://xbl-daq-29:5000") + ''' """ # pylint: disable=too-many-instance-attributes @@ -45,7 +48,7 @@ class StdDaqClient(Device): status = Component(Signal, value="unknown", kind=Kind.normal) n_total = Component(Signal, value=10000, kind=Kind.config) file_path = Component(Signal, value="/gpfs/test/test-beamline", kind=Kind.config) - + # Configuration config = Component(StdDaqRestClient, kind=Kind.config) def __init__( @@ -70,8 +73,8 @@ class StdDaqClient(Device): def connect(self): """Connect to te StDAQs websockets interface - StdDAQ may reject connection for a few seconds, so if it fails, wait - a bit and try to connect again. + StdDAQ may reject connection for a few seconds after restart, + so if it fails, wait a bit and try to connect again. """ num_retry = 0 while num_retry < 5: @@ -113,7 +116,6 @@ class StdDaqClient(Device): backend speed). (default = 10000) file_path : string, optional Save file path. (default = '/gpfs/test/test-beamline') - """ old_config = self.read_configuration() @@ -157,7 +159,6 @@ class StdDaqClient(Device): self._mon = Thread(target=self.poll, daemon=True) self._mon.start() - sleep(3) return super().stage() def unstage(self): @@ -208,9 +209,13 @@ class StdDaqClient(Device): return reply def poll(self): - """Monitor status messages until connection is open""" + """Monitor status messages until connection is open + + This will block the reply monitoring to calling unstage() might throw. + Status updates are sent every 1 seconds + """ try: - sleep(0.1) + sleep(1.2) for msg in self._client: try: message = json.loads(msg) @@ -220,6 +225,8 @@ class StdDaqClient(Device): except Exception as ex: print(ex) return + except (ConnectionClosedError, ConnectionClosedOK): + return finally: self._mon = None From a0ae47aba6dcd8116fa5accf1b2c8e95c2b83528 Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Mon, 26 Aug 2024 16:56:58 +0200 Subject: [PATCH 37/47] Adding motor class and PEPing up --- .../devices/gigafrost/gigafrostcamera.py | 61 ++++--- .../devices/gigafrost/gigafrostclient.py | 42 ++--- .../devices/gigafrost/stddaq_preview.py | 17 +- tomcat_bec/devices/gigafrost/stddaq_rest.py | 10 +- tomcat_bec/devices/gigafrost/stddaq_ws.py | 5 +- tomcat_bec/devices/psimotor.py | 149 ++++++++++++++++++ 6 files changed, 216 insertions(+), 68 deletions(-) create mode 100644 tomcat_bec/devices/psimotor.py diff --git a/tomcat_bec/devices/gigafrost/gigafrostcamera.py b/tomcat_bec/devices/gigafrost/gigafrostcamera.py index 637804a..aec94a0 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostcamera.py +++ b/tomcat_bec/devices/gigafrost/gigafrostcamera.py @@ -69,11 +69,19 @@ class GigaFrostCameraMixin(CustomDetectorMixin): source_port = 3000 + j if j < 4: extend_header_table( - udp_header_table, self.parent.macSouth.get(), self.parent.ipSouth.get(), dest_port, source_port + udp_header_table, + self.parent.macSouth.get(), + self.parent.ipSouth.get(), + dest_port, + source_port ) else: extend_header_table( - udp_header_table, self.parent.macNorth.get(), self.parent.ipNorth.get(), dest_port, source_port + udp_header_table, + self.parent.macNorth.get(), + self.parent.ipNorth.get(), + dest_port, + source_port ) return udp_header_table @@ -230,17 +238,25 @@ class GigaFrostCamera(PSIDetectorBase): cmdWriteService = Component(EpicsSignal, "WRITE_SRV.PROC", put_complete=True, kind=Kind.omitted) # Standard camera configs - cfgExposure = Component(EpicsSignal, "EXPOSURE", put_complete=True, auto_monitor=True, kind=Kind.config) - cfgFramerate = Component(EpicsSignal, "FRAMERATE", put_complete=True, auto_monitor=True, kind=Kind.config) - cfgRoiX = Component(EpicsSignal, "ROIX", put_complete=True, auto_monitor=True, kind=Kind.config) - cfgRoiY = Component(EpicsSignal, "ROIY", put_complete=True, auto_monitor=True, kind=Kind.config) - cfgScanId = Component(EpicsSignal, "SCAN_ID", put_complete=True, auto_monitor=True, kind=Kind.config) - cfgCntNum = Component(EpicsSignal, "CNT_NUM", put_complete=True, auto_monitor=True, kind=Kind.config) - cfgCorrMode = Component(EpicsSignal, "CORR_MODE", put_complete=True, auto_monitor=True, kind=Kind.config) + cfgExposure = Component( + EpicsSignal, "EXPOSURE", put_complete=True, auto_monitor=True, kind=Kind.config) + cfgFramerate = Component( + EpicsSignal, "FRAMERATE", put_complete=True, auto_monitor=True, kind=Kind.config) + cfgRoiX = Component( + EpicsSignal, "ROIX", put_complete=True, auto_monitor=True, kind=Kind.config) + cfgRoiY = Component( + EpicsSignal, "ROIY", put_complete=True, auto_monitor=True, kind=Kind.config) + cfgScanId = Component( + EpicsSignal, "SCAN_ID", put_complete=True, auto_monitor=True, kind=Kind.config) + cfgCntNum = Component( + EpicsSignal, "CNT_NUM", put_complete=True, auto_monitor=True, kind=Kind.config) + cfgCorrMode = Component( + EpicsSignal, "CORR_MODE", put_complete=True, auto_monitor=True, kind=Kind.config) # Software signals cmdSoftEnable = Component(EpicsSignal, "SOFT_ENABLE", put_complete=True) - cmdSoftTrigger = Component(EpicsSignal, "SOFT_TRIG.PROC", put_complete=True, kind=Kind.omitted) + cmdSoftTrigger = Component( + EpicsSignal, "SOFT_TRIG.PROC", put_complete=True, kind=Kind.omitted) cmdSoftExposure = Component(EpicsSignal, "SOFT_EXP", put_complete=True) # Trigger configuration PVs @@ -252,7 +268,11 @@ class GigaFrostCamera(PSIDetectorBase): kind=Kind.config, ) cfgCntEndBit = Component( - EpicsSignal, "CNT_ENDBIT_RBV", write_pv="CNT_ENDBIT", put_complete=True, kind=Kind.config + EpicsSignal, + "CNT_ENDBIT_RBV", + write_pv="CNT_ENDBIT", + put_complete=True, + kind=Kind.config ) # Enable modes cfgTrigEnableExt = Component( @@ -373,10 +393,6 @@ class GigaFrostCamera(PSIDetectorBase): name, auto_soft_enable=False, backend_url=const.BE999_DAFL_CLIENT, - kind=None, - read_attrs=None, - configuration_attrs=None, - parent=None, **kwargs, ): # Ugly hack to pass values before on_init() @@ -388,10 +404,6 @@ class GigaFrostCamera(PSIDetectorBase): super().__init__( prefix=prefix, name=name, - kind=kind, - read_attrs=read_attrs, - configuration_attrs=configuration_attrs, - parent=parent, **kwargs, ) @@ -414,7 +426,7 @@ class GigaFrostCamera(PSIDetectorBase): status = DeviceStatus(self) sleep_time = self.cfgExposure.value*self.cfgCntNum.value*0.001+0.050 sleep(sleep_time) - logger.info(f"[GF2] Slept for: {sleep_time} seconds") + logger.info("[%s] Slept for %f seconds", self.name, sleep_time) status.set_finished() return status @@ -658,13 +670,10 @@ class GigaFrostCamera(PSIDetectorBase): mode_external = self.cfgTrigEnableExt.get() mode_auto = self.cfgTrigEnableAuto.get() if mode_soft and not mode_auto: - if mode_external: - return "soft+ext" - else: - return "soft" - elif mode_auto and not mode_soft and not mode_external: + return "soft+ext" if mode_external else "soft" + if mode_auto and not mode_soft and not mode_external: return "always" - elif mode_external and not mode_soft and not mode_auto: + if mode_external and not mode_soft and not mode_auto: return "external" return None diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index 5898f97..e8133e7 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -29,13 +29,6 @@ class GigaFrostClientMixin(CustomDetectorMixin): This class will be called by the custom_prepare_cls attribute of the detector class. """ - def on_init(self) -> None: - """Initialize the camera, set channel values - - on_init is automatically called during __init__ of the sub devices. - """ - return super().on_init() - def on_stage(self) -> None: """ Specify actions to be executed during stage in preparation for a scan. @@ -120,6 +113,7 @@ class GigaFrostClient(PSIDetectorBase): cam = Component(GigaFrostCamera, prefix="X02DA-CAM-GF2:", name="cam") daq = Component(StdDaqClient, name="daq") + # pylint: disable=too-many-arguments def __init__( self, prefix="", @@ -130,31 +124,25 @@ class GigaFrostClient(PSIDetectorBase): daq_ws_url = "ws://localhost:8080", daq_rest_url = "http://localhost:5000", kind=None, - read_attrs=None, - configuration_attrs=None, - parent=None, **kwargs, ): self.__class__.__dict__["cam"].kwargs['backend_url'] = backend_url self.__class__.__dict__["cam"].kwargs['auto_soft_enable'] = auto_soft_enable self.__class__.__dict__["daq"].kwargs['ws_url'] = daq_ws_url self.__class__.__dict__["daq"].kwargs['rest_url'] = daq_rest_url - #self.__class__.__dict__["daq"].__class__.__dict__["config"].kwargs['rest_url'] = daq_rest_url super().__init__( prefix=prefix, name=name, kind=kind, - read_attrs=read_attrs, - configuration_attrs=configuration_attrs, - parent=parent, **kwargs, ) - def configure(self, d: dict=None): - """Configure the next scan with the GigaFRoST camera and standard DAQ backend + """Configure the next scan with the GigaFRoST camera and standard DAQ backend. + It also makes some simple checks for consistent configuration, but otherwise + status feedback is missing on both sides. Parameters ---------- @@ -163,30 +151,22 @@ class GigaFrostClient(PSIDetectorBase): for an unlimited number of images (limited by the ringbuffer size and backend speed). (default = 10000) nimages : int, optional - Number of images to be taken during each scan. Set to -1 for an - unlimited number of images (limited by the ringbuffer size and - backend speed). (default = 10) + Number of images to be taken during each trigger (i.e. burst). + Maximum is 16777215 images. (default = 10) exposure : float, optional - Exposure time [ms]. (default = 0.2) + Exposure time, max 40 ms. [ms]. (default = 0.2) period : float, optional Exposure period [ms], ignored in soft trigger mode. (default = 1.0) pixel_width : int, optional - Image size in the x-direction [pixels] (default = 2016) + Image size in the x-direction, must be multiple of 48 [pixels] (default = 2016) pixel_height : int, optional - Image size in the y-direction [pixels] (default = 2016) + Image size in the y-direction, must be multiple of 16 [pixels] (default = 2016) scanid : int, optional - Scan identification number to be associated with the scan data - (default = 0) + Scan identification number to be associated with the scan data. + ToDo: This should be retrieved from the BEC. (default = 0) correction_mode : int, optional The correction to be applied to the imaging data. The following modes are available (default = 5): - - * 0: Bypass. No corrections are applied to the data. - * 1: Send correction factor A instead of pixel values - * 2: Send correction factor B instead of pixel values - * 3: Send correction factor C instead of pixel values - * 4: Invert pixel values, but do not apply any linearity correction - * 5: Apply the full linearity correction """ # Unstage camera (reconfiguration will anyway stop camera) super().unstage() diff --git a/tomcat_bec/devices/gigafrost/stddaq_preview.py b/tomcat_bec/devices/gigafrost/stddaq_preview.py index 4b11978..a87d381 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_preview.py +++ b/tomcat_bec/devices/gigafrost/stddaq_preview.py @@ -102,7 +102,7 @@ class StdDaqPreview(Device): self._stop_polling = True return super().unstage() - def stop(self, success=False): + def stop(self, *, success=False): """Stop a running preview""" self.unstage() @@ -119,7 +119,13 @@ class StdDaqPreview(Device): try: # pylint: disable=no-member - meta, data = self._socket.recv_multipart(flags=zmq.NOBLOCK) + r = self._socket.recv_multipart(flags=zmq.NOBLOCK) + if len(r)==2: + meta, data = r + else: + sleep(0.1) + continue + t_curr = time() t_elapsed = t_curr - t_last if t_elapsed > self.throttle.get(): @@ -127,7 +133,8 @@ class StdDaqPreview(Device): if header["type"]=="uint16": image = np.frombuffer(data, dtype=np.uint16) if image.size != np.prod(header['shape']): - raise ValueError(f"Unexpected array size of {image.size} for header: {header}") + err = f"Unexpected array size of {image.size} for header: {header}" + raise ValueError(err) image = image.reshape(header['shape']) # Update image and update subscribers @@ -202,7 +209,9 @@ class StdDaqPreviewMixin(CustomDetectorMixin): self.parent.image.put(image, force=True) self.parent._run_subs(sub_type=self.parent.SUB_MONITOR, value=image) t_last=t_curr - logger.info(f"[{self.parent.name}]\tUpdated frame {header['frame']}\tMean: {np.mean(image)}") + name = self.parent.name + nfo = f"[{name}]\tFrameNo: {header['frame']}\tMean: {np.mean(image)}" + logger.info(nfo) except ValueError: # Happens when ZMQ partially delivers the multipart message pass diff --git a/tomcat_bec/devices/gigafrost/stddaq_rest.py b/tomcat_bec/devices/gigafrost/stddaq_rest.py index 7f21e94..b3b5ddd 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_rest.py +++ b/tomcat_bec/devices/gigafrost/stddaq_rest.py @@ -52,7 +52,7 @@ class StdDaqRestClient(Device): cfg_start_udp_port = Component(Signal, kind=Kind.config) cfg_writer_user_id = Component(Signal, kind=Kind.config) cfg_submodule_info = Component(Signal, kind=Kind.config) - cfg_max_number_of_forwarders_spawned = Component(Signal, kind=Kind.config) + cfg_max_number_of_forwarders = Component(Signal, kind=Kind.config) cfg_use_all_forwarders = Component(Signal, kind=Kind.config) cfg_module_sync_queue_size = Component(Signal, kind=Kind.config) cfg_module_positions = Component(Signal, kind=Kind.config) @@ -89,7 +89,7 @@ class StdDaqRestClient(Device): self.cfg_start_udp_port.set(cfg['start_udp_port']).wait() self.cfg_writer_user_id.set(cfg['writer_user_id']).wait() #self.cfg_submodule_info.set(cfg['submodule_info']).wait() - self.cfg_max_number_of_forwarders_spawned.set(cfg['max_number_of_forwarders_spawned']).wait() + self.cfg_max_number_of_forwarders.set(cfg['max_number_of_forwarders']).wait() self.cfg_use_all_forwarders.set(cfg['use_all_forwarders']).wait() self.cfg_module_sync_queue_size.set(cfg['module_sync_queue_size']).wait() #self.cfg_module_positions.set(cfg['module_positions']).wait() @@ -109,7 +109,7 @@ class StdDaqRestClient(Device): 'writer_user_id': int(self.cfg_writer_user_id.get()), 'log_level': "debug", 'submodule_info': {}, - 'max_number_of_forwarders_spawned': int(self.cfg_max_number_of_forwarders_spawned.get()), + 'max_number_of_forwarders': int(self.cfg_max_number_of_forwarders.get()), 'use_all_forwarders': bool(self.cfg_use_all_forwarders.get()), 'module_sync_queue_size': int(self.cfg_module_sync_queue_size.get()), 'module_positions': {} @@ -165,7 +165,7 @@ class StdDaqRestClient(Device): self.cfg_pixel_width.set(pixel_width).wait() self.write_daq_config() - logger.info(f"[{self.name}] Reconfigured the StdDAQ") + logger.info("[%s] Reconfigured the StdDAQ", self.name) # No feedback on restart, we just sleep sleep(3) @@ -192,7 +192,7 @@ class StdDaqRestClient(Device): self.read_daq_config() return super().unstage() - def stop(self, success=False): + def stop(self, *, success=False): """Stop op: Read the current configuration from the DAQ """ self.unstage() diff --git a/tomcat_bec/devices/gigafrost/stddaq_ws.py b/tomcat_bec/devices/gigafrost/stddaq_ws.py index 0d8fcb4..4423d82 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_ws.py +++ b/tomcat_bec/devices/gigafrost/stddaq_ws.py @@ -85,7 +85,8 @@ class StdDaqClient(Device): num_retry += 1 sleep(3) if num_retry==5: - raise ConnectionRefusedError("The standard DAQ websocket interface refused connection 5 times.") + raise ConnectionRefusedError( + "The stdDAQ websocket interface refused connection 5 times.") def __del__(self): """Try to close the socket""" @@ -174,7 +175,7 @@ class StdDaqClient(Device): pass return super().unstage() - def stop(self, success=False): + def stop(self, *, success=False): """ Stop a running acquisition WARN: This will also close the connection!!! diff --git a/tomcat_bec/devices/psimotor.py b/tomcat_bec/devices/psimotor.py new file mode 100644 index 0000000..f06a7be --- /dev/null +++ b/tomcat_bec/devices/psimotor.py @@ -0,0 +1,149 @@ +""" Extension class for EpicsMotor + + +This module extends the basic EpicsMotor with additional functionality. It +exposes additional parameters of the EPICS MotorRecord and provides a more +detailed interface for motors using the new ECMC-based motion systems at PSI. +""" + +import warnings + +from ophyd import Component, EpicsMotor, EpicsSignal, EpicsSignalRO, Kind +from ophyd.status import MoveStatus + + +class SpmgStates: + """ Enum for the EPICS MotorRecord's SPMG state""" + # pylint: disable=too-few-public-methods + STOP = 0 + PAUSE = 1 + MOVE= 2 + GO = 3 + + +class EpicsMotorMR(EpicsMotor): + """ Extended EPICS Motor class + + Special motor class that exposes additional motor record functionality. + It extends EpicsMotor base class to provide some simple status checks + before movement. + """ + + SUB_PROGRESS = "progress" + motor_deadband = Component( + EpicsSignalRO, ".RDBD", auto_monitor=True, kind=Kind.config) + motor_mode = Component( + EpicsSignal, ".SPMG", auto_monitor=True, put_complete=True, kind=Kind.omitted) + + _start_position = None + _target_position = None + + # pylint: disable=too-many-arguments + def __init__( + self, + prefix="", + *, + name, + kind=None, + read_attrs=None, + configuration_attrs=None, + parent=None, + **kwargs, + ): + super().__init__( + prefix=prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + **kwargs, + ) + + self.subscribe(self._progress_update, run=False) + + def move(self, position, wait=True, **kwargs) -> MoveStatus: + """ Extended move function with a few sanity checks + + Note that the default EpicsMotor only supports the 'GO' movement mode. + """ + # Reset SPMG before move + spmg = self.motor_mode.get() + if spmg != SpmgStates.GO: + self.motor_mode.put(SpmgStates.GO).wait() + #Warni if trying to move beyond an active limit + if self.high_limit_switch and position > self.position: + warnings.warn("Attempting to move above active HLS", RuntimeWarning) + if self.low_limit_switch and position < self.position: + warnings.warn("Attempting to move below active LLS", RuntimeWarning) + + self._start_position = self.position + self._target_position = position + + return super().move(position, wait, **kwargs) + + def _progress_update(self, value, **kwargs) -> None: + """Progress update on the current movement""" + if (self._start_position is None) or (self._target_position is None) or (not self.moving): + self._run_subs(sub_type=self.SUB_PROGRESS, value=1, max_value=1, done=1) + return + + progress = abs( + (value - self._start_position) / (self._target_position - self._start_position) + ) + self._run_subs( + sub_type=self.SUB_PROGRESS, value=progress, max_value=1, done=self.moving) + + +class EpicsMotorEC(EpicsMotorMR): + """ Detailed ECMC EPICS motor class + + Special motor class to provide additional functionality for ECMC based motors. + It exposes additional diagnostic fields and includes basic error management. + """ + + USER_ACCESS = ['reset'] + enable_readback = Component(EpicsSignalRO, "-EnaAct", auto_monitor=True, kind=Kind.normal) + enable = Component( + EpicsSignal, "-EnaCmd-RB", write_pv="-EnaCmd", auto_monitor=True, kind=Kind.normal) + homed = Component(EpicsSignalRO, "-Homed", auto_monitor=True, kind=Kind.normal) + velocity_readback = Component(EpicsSignalRO, "-VelAct", auto_monitor=True, kind=Kind.normal) + position_readback = Component(EpicsSignalRO, "-PosAct", auto_monitor=True, kind=Kind.normal) + position_error = Component(EpicsSignalRO, "-PosErr", auto_monitor=True, kind=Kind.normal) + #high_interlock = Component(EpicsSignalRO, "-SumIlockFwd", auto_monitor=True, kind=Kind.normal) + #low_interlock = Component(EpicsSignalRO, "-SumIlockBwd", auto_monitor=True, kind=Kind.normal) + + ecmc_status = Component(EpicsSignalRO, "-Status", auto_monitor=True, kind=Kind.normal) + error = Component(EpicsSignalRO, "-ErrId", auto_monitor=True, kind=Kind.normal) + error_msg = Component(EpicsSignalRO, "-MsgTxt", auto_monitor=True, kind=Kind.normal) + error_reset = Component(EpicsSignal, "-ErrRst", put_complete=True, kind=Kind.omitted) + + def move(self, position, wait=True, **kwargs) -> MoveStatus: + """ Extended move function with a few sanity checks + + Note that the default EpicsMotor only supports the 'GO' movement mode. + """ + # Reset SPMG before move + error = self.error.get() + if error: + raise RuntimeError(f"Motor is in error state with message: '{self.error_msg.get()}'") + + return super().move(position, wait, **kwargs) + + def reset(self): + """ Resets an ECMC axis + + Recovers an ECMC axis from a previous error. Note that this does not + solve the cause of the error, that you'll have to do yourself. + + Common error causes: + ------------------------- + - MAX_POSITION_LAG_EXCEEDED : The PID tuning is wrong. + - MAX_VELOCITY_EXCEEDED : Either the PID is wrong or the motor is sticking-slipping + - BOTH_LIMITS_ACTIVE : The motors are probably not connected + """ + # Reset the error + self.error_reset.set(1, settle_time=0.1).wait() + # Check if it disappeared + if self.error.get(): + raise RuntimeError(f"Failed to reset axis error: '{self.error_msg.get()}'") From 810c62387e8e392bb5d047571e9212455ba6917a Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Mon, 26 Aug 2024 17:10:20 +0200 Subject: [PATCH 38/47] Updated configuration with cleaner paths --- .../device_configs/microxas_test_bed.yaml | 23 ++++--------------- tomcat_bec/devices/__init__.py | 5 ++++ tomcat_bec/devices/gigafrost/stddaq_rest.py | 4 ++-- 3 files changed, 11 insertions(+), 21 deletions(-) diff --git a/tomcat_bec/device_configs/microxas_test_bed.yaml b/tomcat_bec/device_configs/microxas_test_bed.yaml index f29c7c0..7f305b0 100644 --- a/tomcat_bec/device_configs/microxas_test_bed.yaml +++ b/tomcat_bec/device_configs/microxas_test_bed.yaml @@ -92,24 +92,9 @@ femto_mean_curr: # readoutPriority: monitored # softwareTrigger: true -gf2: - description: GigaFrost camera controls - deviceClass: tomcat_bec.devices.gigafrost.gigafrostcamera.GigaFrostCamera - deviceConfig: - prefix: 'X02DA-CAM-GF2:' - backend_url: 'http://xbl-daq-28:8080' - auto_soft_enable: true - deviceTags: - - camera - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: monitored - softwareTrigger: true - gfclient: description: GigaFrost camera controls - deviceClass: tomcat_bec.devices.gigafrost.gigafrostclient.GigaFrostClient + deviceClass: tomcat_bec.devices.GigaFrostClient deviceConfig: prefix: 'X02DA-CAM-GF2:' backend_url: 'http://xbl-daq-28:8080' @@ -126,7 +111,7 @@ gfclient: daq_stream0: description: Standard DAQ preview stream 2 frames every 1000 image - deviceClass: tomcat_bec.devices.gigafrost.stddaq_preview.StdDaqPreview + deviceClass: tomcat_bec.devices.StdDaqPreview deviceConfig: url: 'tcp://129.129.95.38:20002' deviceTags: @@ -139,7 +124,7 @@ daq_stream0: daq_stream1: description: Standard DAQ preview stream 4 frames at 10 Hz - deviceClass: tomcat_bec.devices.gigafrost.stddaq_preview.StdDaqPreview + deviceClass: tomcat_bec.devices.StdDaqPreview deviceConfig: url: 'tcp://129.129.95.38:20001' deviceTags: @@ -152,7 +137,7 @@ daq_stream1: daq_stream2: description: Standard DAQ preview stream from first server - deviceClass: tomcat_bec.devices.gigafrost.stddaq_preview.StdDaqPreview + deviceClass: tomcat_bec.devices.StdDaqPreview deviceConfig: url: 'tcp://129.129.95.40:20001' deviceTags: diff --git a/tomcat_bec/devices/__init__.py b/tomcat_bec/devices/__init__.py index 341c4e8..03ee446 100644 --- a/tomcat_bec/devices/__init__.py +++ b/tomcat_bec/devices/__init__.py @@ -8,3 +8,8 @@ from .aerotech.AerotechAutomation1 import ( aa1Tasks, ) from .grashopper_tomcat import GrashopperTOMCAT + +from .psimotor import EpicsMotorMR, EpicsMotorEC + +from .gigafrost.gigafrostclient import GigaFrostClient +from .gigafrost.stddaq_preview import StdDaqPreview diff --git a/tomcat_bec/devices/gigafrost/stddaq_rest.py b/tomcat_bec/devices/gigafrost/stddaq_rest.py index b3b5ddd..15f048c 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_rest.py +++ b/tomcat_bec/devices/gigafrost/stddaq_rest.py @@ -89,7 +89,7 @@ class StdDaqRestClient(Device): self.cfg_start_udp_port.set(cfg['start_udp_port']).wait() self.cfg_writer_user_id.set(cfg['writer_user_id']).wait() #self.cfg_submodule_info.set(cfg['submodule_info']).wait() - self.cfg_max_number_of_forwarders.set(cfg['max_number_of_forwarders']).wait() + self.cfg_max_number_of_forwarders.set(cfg['max_number_of_forwarders_spawned']).wait() self.cfg_use_all_forwarders.set(cfg['use_all_forwarders']).wait() self.cfg_module_sync_queue_size.set(cfg['module_sync_queue_size']).wait() #self.cfg_module_positions.set(cfg['module_positions']).wait() @@ -109,7 +109,7 @@ class StdDaqRestClient(Device): 'writer_user_id': int(self.cfg_writer_user_id.get()), 'log_level': "debug", 'submodule_info': {}, - 'max_number_of_forwarders': int(self.cfg_max_number_of_forwarders.get()), + 'max_number_of_forwarders_spawned': int(self.cfg_max_number_of_forwarders.get()), 'use_all_forwarders': bool(self.cfg_use_all_forwarders.get()), 'module_sync_queue_size': int(self.cfg_module_sync_queue_size.get()), 'module_positions': {} From ae958c010e12d500d7f8223d47762492d6ad59a4 Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Wed, 28 Aug 2024 15:05:38 +0200 Subject: [PATCH 39/47] Allow gfclient startup if daq REST API is down, it'll fail when it tries to write --- tomcat_bec/devices/gigafrost/gigafrostclient.py | 4 ++++ tomcat_bec/devices/gigafrost/stddaq_rest.py | 10 ++++++++-- tomcat_bec/devices/gigafrost/stddaq_ws.py | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index e8133e7..3f2d3ce 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -178,6 +178,8 @@ class GigaFrostClient(PSIDetectorBase): return old, new def stage(self): + """ Stages the current device and all sub-devices + """ px_daq_h = self.daq.config.cfg_pixel_height.get() px_daq_w = self.daq.config.cfg_pixel_width.get() @@ -190,6 +192,8 @@ class GigaFrostClient(PSIDetectorBase): return super().stage() def trigger(self) -> DeviceStatus: + """ Triggers the current device and all sub-devices, i.e. the camera. + """ status = super().trigger() return status diff --git a/tomcat_bec/devices/gigafrost/stddaq_rest.py b/tomcat_bec/devices/gigafrost/stddaq_rest.py index 15f048c..f4f0827 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_rest.py +++ b/tomcat_bec/devices/gigafrost/stddaq_rest.py @@ -65,7 +65,10 @@ class StdDaqRestClient(Device): self.rest_url.put(rest_url, force=True) # Connect ro the DAQ and initialize values - self.read_daq_config() + try: + self.read_daq_config() + except Exception as ex: + logger.error(f"Failed to connect to the StdDAQ REST API\n{ex}") def read_daq_config(self) -> dict: """Read the current configuration from the JSON file @@ -177,7 +180,10 @@ class StdDaqRestClient(Device): return super().read() def read_configuration(self): - self.read_daq_config() + try: + self.read_daq_config() + except Exception as ex: + logger.error(f"Failed to connect to the StdDAQ REST API\n{ex}") return super().read_configuration() def stage(self) -> list: diff --git a/tomcat_bec/devices/gigafrost/stddaq_ws.py b/tomcat_bec/devices/gigafrost/stddaq_ws.py index 4423d82..62ad95c 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_ws.py +++ b/tomcat_bec/devices/gigafrost/stddaq_ws.py @@ -71,7 +71,7 @@ class StdDaqClient(Device): self.connect() def connect(self): - """Connect to te StDAQs websockets interface + """Connect to te StdDAQ's websockets interface StdDAQ may reject connection for a few seconds after restart, so if it fails, wait a bit and try to connect again. From 0c8c60cfe3a33fac7bf1fd34a0e417518983535f Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Thu, 29 Aug 2024 16:34:23 +0200 Subject: [PATCH 40/47] Seems to be working configuration --- .../device_configs/microxas_test_bed.yaml | 37 +++------ tomcat_bec/devices/__init__.py | 2 +- .../devices/gigafrost/gigafrostcamera.py | 8 +- .../devices/gigafrost/gigafrostclient.py | 3 +- .../devices/gigafrost/stddaq_preview.py | 42 +++++++--- tomcat_bec/devices/gigafrost/stddaq_rest.py | 24 ++++-- tomcat_bec/devices/gigafrost/stddaq_ws.py | 19 +++-- tomcat_bec/devices/psimotor.py | 83 +++++++------------ tomcat_bec/scans/gigafrost_test.py | 19 +++-- 9 files changed, 120 insertions(+), 117 deletions(-) diff --git a/tomcat_bec/device_configs/microxas_test_bed.yaml b/tomcat_bec/device_configs/microxas_test_bed.yaml index 7f305b0..40dcc9a 100644 --- a/tomcat_bec/device_configs/microxas_test_bed.yaml +++ b/tomcat_bec/device_configs/microxas_test_bed.yaml @@ -1,7 +1,7 @@ eyex: readoutPriority: baseline - description: X-ray eye translation x - deviceClass: ophyd.EpicsMotor + description: X-ray eye axis X + deviceClass: tomcat_bec.devices.psimotor.EpicsMotorMR deviceConfig: prefix: MTEST-X05LA-ES2-XRAYEYE:M1 deviceTags: @@ -12,8 +12,8 @@ eyex: softwareTrigger: false eyey: readoutPriority: baseline - description: X-ray eye translation y - deviceClass: ophyd.EpicsMotor + description: X-ray eye axis Y + deviceClass: tomcat_bec.devices.psimotor.EpicsMotorMR deviceConfig: prefix: MTEST-X05LA-ES2-XRAYEYE:M2 deviceTags: @@ -24,8 +24,8 @@ eyey: softwareTrigger: false eyez: readoutPriority: baseline - description: X-ray eye translation z - deviceClass: ophyd.EpicsMotor + description: X-ray eye axis Z + deviceClass: tomcat_bec.devices.psimotor.EpicsMotorEC deviceConfig: prefix: MTEST-X05LA-ES2-XRAYEYE:M3 deviceTags: @@ -93,7 +93,7 @@ femto_mean_curr: # softwareTrigger: true gfclient: - description: GigaFrost camera controls + description: GigaFrost camera client deviceClass: tomcat_bec.devices.GigaFrostClient deviceConfig: prefix: 'X02DA-CAM-GF2:' @@ -110,8 +110,8 @@ gfclient: softwareTrigger: true daq_stream0: - description: Standard DAQ preview stream 2 frames every 1000 image - deviceClass: tomcat_bec.devices.StdDaqPreview + description: stdDAQ preview (2 every 555) + deviceClass: tomcat_bec.devices.StdDaqPreviewDetector deviceConfig: url: 'tcp://129.129.95.38:20002' deviceTags: @@ -123,8 +123,8 @@ daq_stream0: softwareTrigger: false daq_stream1: - description: Standard DAQ preview stream 4 frames at 10 Hz - deviceClass: tomcat_bec.devices.StdDaqPreview + description: stdDAQ preview (4 at 10 Hz) + deviceClass: tomcat_bec.devices.StdDaqPreviewDetector deviceConfig: url: 'tcp://129.129.95.38:20001' deviceTags: @@ -134,18 +134,3 @@ daq_stream1: readOnly: false readoutPriority: monitored softwareTrigger: false - -daq_stream2: - description: Standard DAQ preview stream from first server - deviceClass: tomcat_bec.devices.StdDaqPreview - deviceConfig: - url: 'tcp://129.129.95.40:20001' - deviceTags: - - std-daq - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: monitored - softwareTrigger: false - - diff --git a/tomcat_bec/devices/__init__.py b/tomcat_bec/devices/__init__.py index 03ee446..ba25c64 100644 --- a/tomcat_bec/devices/__init__.py +++ b/tomcat_bec/devices/__init__.py @@ -12,4 +12,4 @@ from .grashopper_tomcat import GrashopperTOMCAT from .psimotor import EpicsMotorMR, EpicsMotorEC from .gigafrost.gigafrostclient import GigaFrostClient -from .gigafrost.stddaq_preview import StdDaqPreview +from .gigafrost.stddaq_preview import StdDaqPreview, StdDaqPreviewDetector diff --git a/tomcat_bec/devices/gigafrost/gigafrostcamera.py b/tomcat_bec/devices/gigafrost/gigafrostcamera.py index aec94a0..cb6fc88 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostcamera.py +++ b/tomcat_bec/devices/gigafrost/gigafrostcamera.py @@ -149,8 +149,14 @@ class GigaFrostCameraMixin(CustomDetectorMixin): It must be safe to assume that the device is ready for the scan to start immediately once this function is finished. """ + # Either an acquisition is running or it's already done if self.parent.infoBusyFlag.value: - raise RuntimeError("Camera is already busy, unstage it first!") + logger.warn("Camera is already busy, unstage it first!") + self.parent.unstage() + sleep(0.5) + # Sync if out of sync + if self.parent.infoSyncFlag.value == 0: + self.parent.cmdSyncHw.set(1).wait() # Switch to acquiring self.parent.cmdStartCamera.set(1).wait() self.parent.state.put(const.GfStatus.ACQUIRING, force=True) diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index 3f2d3ce..08ad971 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -42,7 +42,8 @@ class GigaFrostClientMixin(CustomDetectorMixin): to start immediately once this function is finished. """ # Gigafrost can finish a run without explicit unstaging - self.parent._staged = Staged.no + if self.parent._staged: + self.parent.unstage() #self.parent.daq.stage() #self.parent.cam.stage() diff --git a/tomcat_bec/devices/gigafrost/stddaq_preview.py b/tomcat_bec/devices/gigafrost/stddaq_preview.py index a87d381..720ea20 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_preview.py +++ b/tomcat_bec/devices/gigafrost/stddaq_preview.py @@ -167,8 +167,13 @@ class StdDaqPreviewMixin(CustomDetectorMixin): Parent class: CustomDetectorMixin """ + _mon = None def on_stage(self): """Start listening for preview data stream""" + if self._mon is not None: + self.parent.unstage() + sleep(0.5) + self.parent.connect() self._stop_polling = False self._mon = Thread(target=self.poll, daemon=True) @@ -176,8 +181,13 @@ class StdDaqPreviewMixin(CustomDetectorMixin): def on_unstage(self): """Stop a running preview""" - self._stop_polling = True - + if self._mon is not None: + self._stop_polling = True + # Might hang on recv_multipart + self._mon.join(timeout=1) + # So also disconnect the socket + self.parent._socket.disconnect() + def on_stop(self): """Stop a running preview""" self.on_unstage() @@ -191,27 +201,39 @@ class StdDaqPreviewMixin(CustomDetectorMixin): try: # Exit loop and finish monitoring if self._stop_polling: + logger.info(f"[{self.parent.name}]\tDetaching monitor") break # pylint: disable=no-member - meta, data = self.parent._socket.recv_multipart(flags=zmq.NOBLOCK) - header = json.loads(meta) - if header["type"]=="uint16": - image = np.frombuffer(data, dtype=np.uint16) - image = image.reshape(header['shape']) + r = self.parent._socket.recv_multipart(flags=zmq.NOBLOCK) + if len(r)==2: + meta, data = r + else: + sleep(0.1) + continue # Update image and update subscribers t_curr = time() t_elapsed = t_curr - t_last if t_elapsed > self.parent.throttle.get(): + header = json.loads(meta) + if header["type"]=="uint16": + image = np.frombuffer(data, dtype=np.uint16) + 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']) + + # Update image and update subscribers self.parent.frame.put(header['frame'], force=True) self.parent.image_shape.put(header['shape'], force=True) self.parent.image.put(image, force=True) self.parent._run_subs(sub_type=self.parent.SUB_MONITOR, value=image) t_last=t_curr - name = self.parent.name - nfo = f"[{name}]\tFrameNo: {header['frame']}\tMean: {np.mean(image)}" - logger.info(nfo) + 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 diff --git a/tomcat_bec/devices/gigafrost/stddaq_rest.py b/tomcat_bec/devices/gigafrost/stddaq_rest.py index f4f0827..822df51 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_rest.py +++ b/tomcat_bec/devices/gigafrost/stddaq_rest.py @@ -70,8 +70,8 @@ class StdDaqRestClient(Device): except Exception as ex: logger.error(f"Failed to connect to the StdDAQ REST API\n{ex}") - def read_daq_config(self) -> dict: - """Read the current configuration from the JSON file + def get_daq_config(self) -> dict: + """Read the current configuration from the DAQ """ r = requests.get( self.rest_url.get() + '/api/config/get', @@ -80,8 +80,12 @@ class StdDaqRestClient(Device): ) if r.status_code != 200: raise ConnectionError(f"[{self.name}] Error {r.status_code}:\t{r.text}") + return r.json() - cfg = r.json() + def read_daq_config(self) -> None: + """Extract the current configuration from the JSON file + """ + cfg = self.get_daq_config() self.cfg_detector_name.set(cfg['detector_name']).wait() self.cfg_detector_type.set(cfg['detector_type']).wait() @@ -98,9 +102,8 @@ class StdDaqRestClient(Device): #self.cfg_module_positions.set(cfg['module_positions']).wait() self._config_read = True - return r - def _build_config(self) -> dict: + def _build_config(self, orig) -> dict: config = { 'detector_name': str(self.cfg_detector_name.get()), 'detector_type': str(self.cfg_detector_type.get()), @@ -110,13 +113,15 @@ class StdDaqRestClient(Device): 'image_pixel_width': int(self.cfg_pixel_width.get()), 'start_udp_port': int(self.cfg_start_udp_port.get()), 'writer_user_id': int(self.cfg_writer_user_id.get()), - 'log_level': "debug", + 'log_level': "info", 'submodule_info': {}, 'max_number_of_forwarders_spawned': int(self.cfg_max_number_of_forwarders.get()), 'use_all_forwarders': bool(self.cfg_use_all_forwarders.get()), 'module_sync_queue_size': int(self.cfg_module_sync_queue_size.get()), - 'module_positions': {} + 'module_positions': {}, + 'number_of_writers': 14 } + config = orig.update(config) return config def write_daq_config(self): @@ -126,7 +131,8 @@ class StdDaqRestClient(Device): if not self._config_read: raise RuntimeError("Pleae read config before editing") - config = self._build_config() + orig = self.get_daq_config() + config = self._build_config(orig) #params = {"user": "ioc", "config_file": "/etc/std_daq/configs/gf1.json"} params = {"user": "ioc"} @@ -153,7 +159,7 @@ class StdDaqRestClient(Device): """ # Reads the current config old = self.read_configuration() - + self.read_daq_config() # If Bluesky style configure if d is not None: # Only reconfigure if we're instructed diff --git a/tomcat_bec/devices/gigafrost/stddaq_ws.py b/tomcat_bec/devices/gigafrost/stddaq_ws.py index 62ad95c..415c3a4 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_ws.py +++ b/tomcat_bec/devices/gigafrost/stddaq_ws.py @@ -144,19 +144,24 @@ class StdDaqClient(Device): by calling unstage. So it might start from an already running state or not, we can't query if not running. """ + if self._staged: + self.unstage() + self._client.close() + file_path = self.file_path.get() n_total = self.n_total.get() message = {"command": "start", "path": file_path, "n_image": n_total} reply = self.message(message) - reply = json.loads(reply) - if reply["status"] in ("creating_file"): - self.status.put(reply["status"], force=True) - elif reply["status"] in ("rejected"): - raise RuntimeError( - f"Start StdDAQ command rejected (might be already running): {reply['reason']}" - ) + if reply is not None: + reply = json.loads(reply) + if reply["status"] in ("creating_file"): + self.status.put(reply["status"], force=True) + elif reply["status"] in ("rejected"): + raise RuntimeError( + f"Start StdDAQ command rejected (might be already running): {reply['reason']}" + ) self._mon = Thread(target=self.poll, daemon=True) self._mon.start() diff --git a/tomcat_bec/devices/psimotor.py b/tomcat_bec/devices/psimotor.py index f06a7be..da02287 100644 --- a/tomcat_bec/devices/psimotor.py +++ b/tomcat_bec/devices/psimotor.py @@ -9,7 +9,9 @@ detailed interface for motors using the new ECMC-based motion systems at PSI. import warnings from ophyd import Component, EpicsMotor, EpicsSignal, EpicsSignalRO, Kind -from ophyd.status import MoveStatus +from ophyd.status import DeviceStatus, MoveStatus +from ophyd.utils.errors import UnknownStatusFailure +from ophyd.utils.epics_pvs import AlarmSeverity class SpmgStates: @@ -21,6 +23,7 @@ class SpmgStates: GO = 3 + class EpicsMotorMR(EpicsMotor): """ Extended EPICS Motor class @@ -28,39 +31,16 @@ class EpicsMotorMR(EpicsMotor): It extends EpicsMotor base class to provide some simple status checks before movement. """ + tolerated_alarm = AlarmSeverity.INVALID - SUB_PROGRESS = "progress" motor_deadband = Component( EpicsSignalRO, ".RDBD", auto_monitor=True, kind=Kind.config) motor_mode = Component( EpicsSignal, ".SPMG", auto_monitor=True, put_complete=True, kind=Kind.omitted) - - _start_position = None - _target_position = None - - # pylint: disable=too-many-arguments - def __init__( - self, - prefix="", - *, - name, - kind=None, - read_attrs=None, - configuration_attrs=None, - parent=None, - **kwargs, - ): - super().__init__( - prefix=prefix, - name=name, - kind=kind, - read_attrs=read_attrs, - configuration_attrs=configuration_attrs, - parent=parent, - **kwargs, - ) - - self.subscribe(self._progress_update, run=False) + motor_status = Component( + EpicsSignal, ".STAT", auto_monitor=True, kind=Kind.omitted) + motor_enable = Component( + EpicsSignal, ".CNEN", auto_monitor=True, kind=Kind.omitted) def move(self, position, wait=True, **kwargs) -> MoveStatus: """ Extended move function with a few sanity checks @@ -70,29 +50,25 @@ class EpicsMotorMR(EpicsMotor): # Reset SPMG before move spmg = self.motor_mode.get() if spmg != SpmgStates.GO: - self.motor_mode.put(SpmgStates.GO).wait() - #Warni if trying to move beyond an active limit - if self.high_limit_switch and position > self.position: - warnings.warn("Attempting to move above active HLS", RuntimeWarning) - if self.low_limit_switch and position < self.position: - warnings.warn("Attempting to move below active LLS", RuntimeWarning) + self.motor_mode.set(SpmgStates.GO).wait() + # Warn if EPIC motorRecord claims an error + status = self.motor_status.get() + if status: + warnings.warn(f"EPICS MotorRecord is in alarm state {status}, ophyd will raise", RuntimeWarning) + # Warni if trying to move beyond an active limit + # if self.high_limit_switch and position > self.position: + # warnings.warn("Attempting to move above active HLS", RuntimeWarning) + # if self.low_limit_switch and position < self.position: + # warnings.warn("Attempting to move below active LLS", RuntimeWarning) - self._start_position = self.position - self._target_position = position + try: + status = super().move(position, wait, **kwargs) + return status + except UnknownStatusFailure: + status = DeviceStatus(self) + status.set_finished() + return status - return super().move(position, wait, **kwargs) - - def _progress_update(self, value, **kwargs) -> None: - """Progress update on the current movement""" - if (self._start_position is None) or (self._target_position is None) or (not self.moving): - self._run_subs(sub_type=self.SUB_PROGRESS, value=1, max_value=1, done=1) - return - - progress = abs( - (value - self._start_position) / (self._target_position - self._start_position) - ) - self._run_subs( - sub_type=self.SUB_PROGRESS, value=progress, max_value=1, done=self.moving) class EpicsMotorEC(EpicsMotorMR): @@ -101,7 +77,6 @@ class EpicsMotorEC(EpicsMotorMR): Special motor class to provide additional functionality for ECMC based motors. It exposes additional diagnostic fields and includes basic error management. """ - USER_ACCESS = ['reset'] enable_readback = Component(EpicsSignalRO, "-EnaAct", auto_monitor=True, kind=Kind.normal) enable = Component( @@ -113,7 +88,7 @@ class EpicsMotorEC(EpicsMotorMR): #high_interlock = Component(EpicsSignalRO, "-SumIlockFwd", auto_monitor=True, kind=Kind.normal) #low_interlock = Component(EpicsSignalRO, "-SumIlockBwd", auto_monitor=True, kind=Kind.normal) - ecmc_status = Component(EpicsSignalRO, "-Status", auto_monitor=True, kind=Kind.normal) + #ecmc_status = Component(EpicsSignalRO, "-Status", auto_monitor=True, kind=Kind.normal) error = Component(EpicsSignalRO, "-ErrId", auto_monitor=True, kind=Kind.normal) error_msg = Component(EpicsSignalRO, "-MsgTxt", auto_monitor=True, kind=Kind.normal) error_reset = Component(EpicsSignal, "-ErrRst", put_complete=True, kind=Kind.omitted) @@ -123,7 +98,7 @@ class EpicsMotorEC(EpicsMotorMR): Note that the default EpicsMotor only supports the 'GO' movement mode. """ - # Reset SPMG before move + # Check ECMC error status before move error = self.error.get() if error: raise RuntimeError(f"Motor is in error state with message: '{self.error_msg.get()}'") @@ -146,4 +121,4 @@ class EpicsMotorEC(EpicsMotorMR): self.error_reset.set(1, settle_time=0.1).wait() # Check if it disappeared if self.error.get(): - raise RuntimeError(f"Failed to reset axis error: '{self.error_msg.get()}'") + raise RuntimeError(f"Failed to reset axis, error still present: '{self.error_msg.get()}'") diff --git a/tomcat_bec/scans/gigafrost_test.py b/tomcat_bec/scans/gigafrost_test.py index e817d14..e9a7b6c 100644 --- a/tomcat_bec/scans/gigafrost_test.py +++ b/tomcat_bec/scans/gigafrost_test.py @@ -1,5 +1,4 @@ import time - import numpy as np from bec_lib import bec_logger @@ -87,12 +86,16 @@ class GigaFrostStepScan(AsyncFlyScanBase): yield from self.stubs.pre_scan() def stage(self): - yield from self.stubs.send_rpc_and_wait( - "gf2", "configure", {"nimages": self.scan_exp_b, "exposure": self.scan_exp_t, "period": self.scan_exp_p, "roix": 480, "roiy": 128} - ) - yield from self.stubs.send_rpc_and_wait( - "daq", "configure", {"n_images": self.scan_steps * self.scan_exp_b} - ) + d= { + "ntotal": self.scan_steps * self.scan_exp_b, + "nimages": self.scan_exp_b, + "exposure": self.scan_exp_t, + "period": self.scan_exp_p, + "pixel_width": 480, + "pixel_height": 128 + } + yield from self.stubs.send_rpc_and_wait("gfclient", "configure", d) + # For god, NO! yield from super().stage() def scan_core(self): @@ -101,7 +104,7 @@ class GigaFrostStepScan(AsyncFlyScanBase): print(f"Point: {ii}") st = yield from self.stubs.send_rpc_and_wait(self.scan_motors[0], "move", self.positions[ii]) st.wait() - st = yield from self.stubs.send_rpc_and_wait("gf2", "trigger") + st = yield from self.stubs.send_rpc_and_wait("gfclient", "trigger") st.wait() self.pointID += 1 time.sleep(0.2) From fc0e4268500dcb207791ae226f65d0067e8472a4 Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Fri, 30 Aug 2024 17:09:48 +0200 Subject: [PATCH 41/47] Code cleanup, needs checking --- .../device_configs/microxas_test_bed.yaml | 4 +- tomcat_bec/devices/__init__.py | 3 +- .../devices/gigafrost/gigafrostcamera.py | 48 +++-- .../devices/gigafrost/gigafrostclient.py | 47 ++--- tomcat_bec/devices/gigafrost/readme.md | 14 ++ .../devices/gigafrost/stddaq_preview.py | 186 +++--------------- tomcat_bec/devices/gigafrost/stddaq_rest.py | 37 ++-- tomcat_bec/devices/gigafrost/stddaq_ws.py | 16 +- 8 files changed, 116 insertions(+), 239 deletions(-) diff --git a/tomcat_bec/device_configs/microxas_test_bed.yaml b/tomcat_bec/device_configs/microxas_test_bed.yaml index 40dcc9a..a8ba010 100644 --- a/tomcat_bec/device_configs/microxas_test_bed.yaml +++ b/tomcat_bec/device_configs/microxas_test_bed.yaml @@ -1,7 +1,7 @@ eyex: readoutPriority: baseline description: X-ray eye axis X - deviceClass: tomcat_bec.devices.psimotor.EpicsMotorMR + deviceClass: tomcat_bec.devices.psimotor.EpicsMotorEC deviceConfig: prefix: MTEST-X05LA-ES2-XRAYEYE:M1 deviceTags: @@ -13,7 +13,7 @@ eyex: eyey: readoutPriority: baseline description: X-ray eye axis Y - deviceClass: tomcat_bec.devices.psimotor.EpicsMotorMR + deviceClass: tomcat_bec.devices.psimotor.EpicsMotorEC deviceConfig: prefix: MTEST-X05LA-ES2-XRAYEYE:M2 deviceTags: diff --git a/tomcat_bec/devices/__init__.py b/tomcat_bec/devices/__init__.py index ba25c64..a63b62c 100644 --- a/tomcat_bec/devices/__init__.py +++ b/tomcat_bec/devices/__init__.py @@ -8,8 +8,7 @@ from .aerotech.AerotechAutomation1 import ( aa1Tasks, ) from .grashopper_tomcat import GrashopperTOMCAT - from .psimotor import EpicsMotorMR, EpicsMotorEC from .gigafrost.gigafrostclient import GigaFrostClient -from .gigafrost.stddaq_preview import StdDaqPreview, StdDaqPreviewDetector +from .gigafrost.stddaq_preview import StdDaqPreviewDetector diff --git a/tomcat_bec/devices/gigafrost/gigafrostcamera.py b/tomcat_bec/devices/gigafrost/gigafrostcamera.py index cb6fc88..3b4e651 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostcamera.py +++ b/tomcat_bec/devices/gigafrost/gigafrostcamera.py @@ -40,6 +40,7 @@ class GigaFrostCameraMixin(CustomDetectorMixin): detector class. """ def _define_backend_ip(self): + """ Select backend IP address for UDP stream""" if self.parent.backendUrl.get() == const.BE3_DAFL_CLIENT: # xbl-daq-33 return const.BE3_NORTH_IP, const.BE3_SOUTH_IP if self.parent.backendUrl.get() == const.BE999_DAFL_CLIENT: @@ -48,6 +49,7 @@ class GigaFrostCameraMixin(CustomDetectorMixin): raise RuntimeError(f"Backend {self.parent.backendUrl.get()} not recognized.") def _define_backend_mac(self): + """ Select backend MAC address for UDP stream""" if self.parent.backendUrl.get() == const.BE3_DAFL_CLIENT: # xbl-daq-33 return const.BE3_NORTH_MAC, const.BE3_SOUTH_MAC if self.parent.backendUrl.get() == const.BE999_DAFL_CLIENT: @@ -60,7 +62,7 @@ class GigaFrostCameraMixin(CustomDetectorMixin): self.parent.cfgConnectionParam.set(self._build_udp_header_table()).wait() def _build_udp_header_table(self): - """Build the header table for the communication""" + """Build the header table for the UDP communication""" udp_header_table = [] for i in range(0, 64, 1): @@ -86,9 +88,15 @@ class GigaFrostCameraMixin(CustomDetectorMixin): return udp_header_table - def on_init(self) -> None: - """Initialize the camera, set channel values""" + """ Initialize the camera, set channel values""" + # ToDo: Not sure if it's a good idea to change camera settings upon + # ophyd device startup, i.e. each deviceserver restart. + self._init_gigafrost() + self.parent._initialized = True + + def _init_gigafrost(self) -> None: + """ Initialize the camera, set channel values""" ## Stop acquisition self.parent.cmdStartCamera.set(0).wait() @@ -132,7 +140,6 @@ class GigaFrostCameraMixin(CustomDetectorMixin): # Set udp header table self._set_udp_header_table() - self.parent.state.put(const.GfStatus.INIT, force=True) return super().on_init() def on_stage(self) -> None: @@ -149,9 +156,9 @@ class GigaFrostCameraMixin(CustomDetectorMixin): It must be safe to assume that the device is ready for the scan to start immediately once this function is finished. """ - # Either an acquisition is running or it's already done + # Gigafrost can finish a run without explicit unstaging if self.parent.infoBusyFlag.value: - logger.warn("Camera is already busy, unstage it first!") + logger.warn("Camera is already busy, unstaging it first!") self.parent.unstage() sleep(0.5) # Sync if out of sync @@ -159,9 +166,6 @@ class GigaFrostCameraMixin(CustomDetectorMixin): self.parent.cmdSyncHw.set(1).wait() # Switch to acquiring self.parent.cmdStartCamera.set(1).wait() - self.parent.state.put(const.GfStatus.ACQUIRING, force=True) - # Gigafrost can finish a run without explicit unstaging - self.parent._staged = Staged.no def on_unstage(self) -> None: """Specify actions to be executed during unstage. @@ -174,7 +178,6 @@ class GigaFrostCameraMixin(CustomDetectorMixin): self.parent.cmdStartCamera.set(0).wait() if self.parent.autoSoftEnable.get(): self.parent.cmdSoftEnable.set(0).wait() - self.parent.state.put(const.GfStatus.STOPPED, force=True) def on_stop(self) -> None: """ @@ -198,7 +201,6 @@ class GigaFrostCameraMixin(CustomDetectorMixin): # BEC teststand operation mode: posedge of SoftEnable if Started self.parent.cmdSoftEnable.set(0).wait() self.parent.cmdSoftEnable.set(1).wait() - else: self.parent.cmdSoftTrigger.set(1).wait() @@ -230,6 +232,7 @@ class GigaFrostCamera(PSIDetectorBase): custom_prepare_cls = GigaFrostCameraMixin USER_ACCESS = [""] + _initialized = False infoBusyFlag = Component(EpicsSignalRO, "BUSY_STAT", auto_monitor=True) infoSyncFlag = Component(EpicsSignalRO, "SYNC_FLAG", auto_monitor=True) @@ -390,7 +393,6 @@ class GigaFrostCamera(PSIDetectorBase): macSouth = Component(Signal, kind=Kind.config) ipNorth = Component(Signal, kind=Kind.config) ipSouth = Component(Signal, kind=Kind.config) - state = Component(Signal, value=int(const.GfStatus.NEW), kind=Kind.config) def __init__( self, @@ -407,25 +409,19 @@ class GigaFrostCamera(PSIDetectorBase): self._signals_to_be_set['backend_url'] = backend_url # super() will call the mixin class - super().__init__( - prefix=prefix, - name=name, - **kwargs, - ) + super().__init__(prefix=prefix, name=name, **kwargs) def _init(self): """Ugly hack: values must be set before on_init() is called""" # Additional parameters self.autoSoftEnable._metadata["write_access"] = False self.backendUrl._metadata["write_access"] = False - self.state._metadata["write_access"] = False self.autoSoftEnable.put(self._signals_to_be_set['auto_soft_enable'], force=True) self.backendUrl.put(self._signals_to_be_set['backend_url'], force=True) - self.state.put(const.GfStatus.NEW, force=True) return super()._init() def trigger(self) -> DeviceStatus: - + """ Sends a software trigger to GigaFrost""" super().trigger() # There's no status readback from the camera, so we just wait @@ -469,6 +465,8 @@ class GigaFrostCamera(PSIDetectorBase): """ # Stop acquisition self.unstage() + if not self._initialized: + pass # If Bluesky style configure if d is not None: @@ -495,7 +493,15 @@ class GigaFrostCamera(PSIDetectorBase): # Commit parameter self.cmdSetParam.set(1).wait() - self.state.set(const.GfStatus.CONFIGURED, force=True) + + def stage(self): + """ Standard stage command""" + if not self._initialized: + logger.warn( + "Ophyd device havent ran the initialization sequence," + "IOC might be in unknown configuration." + ) + return super().stage() @property def exposure_mode(self): diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index 08ad971..a827d21 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -7,8 +7,6 @@ Created on Thu Jun 27 17:28:43 2024 @author: mohacsi_i """ from ophyd import Component, DeviceStatus -from ophyd.device import Staged - from ophyd_devices.interfaces.base_classes.psi_detector_base import ( CustomDetectorMixin, PSIDetectorBase, @@ -49,16 +47,16 @@ class GigaFrostClientMixin(CustomDetectorMixin): #self.parent.cam.stage() - def on_unstage(self) -> None: - """ - Specify actions to be executed during unstage. + # def on_unstage(self) -> None: + # """ + # Specify actions to be executed during unstage. - This step should include checking if the acqusition was successful, - and publishing the file location and file event message, - with flagged done to BEC. - """ - self.parent.cam.unstage() - self.parent.daq.unstage() + # This step should include checking if the acqusition was successful, + # and publishing the file location and file event message, + # with flagged done to BEC. + # """ + # self.parent.cam.unstage() + # self.parent.daq.unstage() def on_stop(self) -> None: """ @@ -132,13 +130,7 @@ class GigaFrostClient(PSIDetectorBase): self.__class__.__dict__["daq"].kwargs['ws_url'] = daq_ws_url self.__class__.__dict__["daq"].kwargs['rest_url'] = daq_rest_url - super().__init__( - prefix=prefix, - name=name, - kind=kind, - **kwargs, - ) - + super().__init__(prefix=prefix, name=name, kind=kind, **kwargs) def configure(self, d: dict=None): """Configure the next scan with the GigaFRoST camera and standard DAQ backend. @@ -148,11 +140,11 @@ class GigaFrostClient(PSIDetectorBase): Parameters ---------- ntotal : int, optional - Total mumber of images to be taken during the whole scan. Set to -1 - for an unlimited number of images (limited by the ringbuffer size and - backend speed). (default = 10000) + Total mumber of images to be taken by the DAQ during the whole scan. + Set to -1 for an unlimited number of images (limited by the + ringbuffer size and backend speed). (default = 10000) nimages : int, optional - Number of images to be taken during each trigger (i.e. burst). + Number of images to be taken during each trigger (i.e. burst). Maximum is 16777215 images. (default = 10) exposure : float, optional Exposure time, max 40 ms. [ms]. (default = 0.2) @@ -183,7 +175,6 @@ class GigaFrostClient(PSIDetectorBase): """ px_daq_h = self.daq.config.cfg_pixel_height.get() px_daq_w = self.daq.config.cfg_pixel_width.get() - px_gf_w = self.cam.cfgRoiX.get() px_gf_h = self.cam.cfgRoiY.get() @@ -192,11 +183,11 @@ class GigaFrostClient(PSIDetectorBase): return super().stage() - def trigger(self) -> DeviceStatus: - """ Triggers the current device and all sub-devices, i.e. the camera. - """ - status = super().trigger() - return status + # def trigger(self) -> DeviceStatus: + # """ Triggers the current device and all sub-devices, i.e. the camera. + # """ + # status = super().trigger() + # return status # Automatically connect to MicroSAXS testbench if directly invoked if __name__ == "__main__": diff --git a/tomcat_bec/devices/gigafrost/readme.md b/tomcat_bec/devices/gigafrost/readme.md index 6dc4982..f838e7a 100644 --- a/tomcat_bec/devices/gigafrost/readme.md +++ b/tomcat_bec/devices/gigafrost/readme.md @@ -2,6 +2,20 @@ The GigaFrost camera IOC is a form from an ancient version of Helge's cameras. As we're commissioning, the current folder also contains the standard DAQ client. +The ophyd implementation tries to balance between familiarity with the old +**gfclient** pyepics library and the BEC/bluesky event model. + +# Examples + +A simple code example with soft triggering: +''' +d = {'ntotal':100000, 'nimages':3009, 'exposure':10.0, 'period':20.0, 'pixel_width':2016, 'pixel_height':2016} +gfc.configure(d) +gfc.stage() +for ii in range(10): + gfc.trigger() +gfc.unstage() +''' # Opening GigaFrost panel diff --git a/tomcat_bec/devices/gigafrost/stddaq_preview.py b/tomcat_bec/devices/gigafrost/stddaq_preview.py index 720ea20..f8785c8 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_preview.py +++ b/tomcat_bec/devices/gigafrost/stddaq_preview.py @@ -30,137 +30,6 @@ class StdDaqPreviewState(enum.IntEnum): MONITORING = 2 -class StdDaqPreview(Device): - """Wrapper class around the StdDaq preview image stream. - - This was meant to provide live image preview directly from the StdDAQ. - Note that the preview stream must be heavily throtled in order to cope - with the incoming data. - - You can add a preview widget to the dock by: - cam_widget = gui.add_dock('cam_dock1').add_widget('BECFigure').image('daq_stream1') - """ - # pylint: disable=too-many-instance-attributes - - # Subscriptions for plotting image - SUB_MONITOR = "device_monitor_2d" - _default_sub = SUB_MONITOR - - # Status attributes - url = Component(Signal, kind=Kind.config) - status = Component(Signal, value=StdDaqPreviewState.UNKNOWN, kind=Kind.omitted) - image = Component(Signal, kind=Kind.normal) - frame = Component(Signal, kind=Kind.normal) - image_shape = Component(Signal, kind=Kind.omitted) - value = Component(Signal, kind=Kind.hinted) - throttle = Component(Signal, value=0.2, kind=Kind.omitted) - - def __init__( - self, *args, url: str = "tcp://129.129.95.38:20000", parent: Device = None, **kwargs - ) -> None: - super().__init__(*args, parent=parent, **kwargs) - self.url._metadata["write_access"] = False - self.status._metadata["write_access"] = False - self.image._metadata["write_access"] = False - self.frame._metadata["write_access"] = False - self.image_shape._metadata["write_access"] = False - self.value._metadata["write_access"] = False - self.url.set(url, force=True).wait() - self._stop_polling = False - self._mon = None - - # Connect ro the DAQ - self.connect() - - def connect(self): - """Connect to te StDAQs PUB-SUB streaming interface - - StdDAQ may reject connection for a few seconds when it restarts, - so if it fails, wait a bit and try to connect again. - """ - # pylint: disable=no-member - # Socket to talk to server - context = zmq.Context() - self._socket = context.socket(zmq.SUB) - self._socket.setsockopt(zmq.SUBSCRIBE, ZMQ_TOPIC_FILTER) - try: - self._socket.connect(self.url.get()) - except ConnectionRefusedError: - sleep(1) - self._socket.connect(self.url.get()) - - def stage(self) -> list: - """Start listening for preview data stream""" - self.connect() - self._stop_polling = False - self._mon = Thread(target=self.poll, daemon=True) - self._mon.start() - return super().stage() - - def unstage(self): - """Stop a running preview""" - self._stop_polling = True - return super().unstage() - - def stop(self, *, success=False): - """Stop a running preview""" - self.unstage() - - def poll(self): - """Collect streamed updates""" - self.status.set(StdDaqPreviewState.MONITORING, force=True) - t_last = time() - try: - while True: - # Exit loop and finish monitoring - if self._stop_polling: - logger.info(f"[{self.name}]\tDetaching monitor") - break - - try: - # pylint: disable=no-member - r = self._socket.recv_multipart(flags=zmq.NOBLOCK) - if len(r)==2: - meta, data = r - else: - sleep(0.1) - continue - - t_curr = time() - t_elapsed = t_curr - t_last - if t_elapsed > self.throttle.get(): - header = json.loads(meta) - if header["type"]=="uint16": - image = np.frombuffer(data, dtype=np.uint16) - 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']) - - # Update image and update subscribers - self.frame.put(header['frame'], force=True) - self.image_shape.put(header['shape'], force=True) - self.image.put(image, force=True) - self._run_subs(sub_type=self.SUB_MONITOR, value=image) - t_last=t_curr - logger.info( - f"[{self.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 - except zmq.error.Again: - # Happens when receive queue is empty - sleep(0.1) - except Exception as ex: - logger.info(f"[{self.name}]\t{str(ex)}") - raise - finally: - self._mon = None - self.status.set(StdDaqPreviewState.DETACHED, force=True) - - class StdDaqPreviewMixin(CustomDetectorMixin): """Setup class for the standard DAQ preview stream @@ -195,8 +64,8 @@ class StdDaqPreviewMixin(CustomDetectorMixin): def poll(self): """Collect streamed updates""" self.parent.status.set(StdDaqPreviewState.MONITORING, force=True) - t_last = time() try: + t_last = time() while True: try: # Exit loop and finish monitoring @@ -206,34 +75,37 @@ class StdDaqPreviewMixin(CustomDetectorMixin): # pylint: disable=no-member r = self.parent._socket.recv_multipart(flags=zmq.NOBLOCK) - if len(r)==2: - meta, data = r - else: - sleep(0.1) + # Length and throtling checks + if len(r)!=2: continue - - # Update image and update subscribers t_curr = time() t_elapsed = t_curr - t_last if t_elapsed > self.parent.throttle.get(): - header = json.loads(meta) - if header["type"]=="uint16": - image = np.frombuffer(data, dtype=np.uint16) - 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']) + sleep(0.1) + continue - # Update image and update subscribers - self.parent.frame.put(header['frame'], force=True) - self.parent.image_shape.put(header['shape'], force=True) - self.parent.image.put(image, force=True) - 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}" - ) + # Unpack the Array V1 reply to metadata and array data + meta, data = r + + # Update image and update subscribers + header = json.loads(meta) + if header["type"]=="uint16": + image = np.frombuffer(data, dtype=np.uint16) + 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']) + + # Update image and update subscribers + self.parent.frame.put(header['frame'], force=True) + self.parent.image_shape.put(header['shape'], force=True) + self.parent.image.put(image, force=True) + 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 @@ -271,7 +143,7 @@ class StdDaqPreviewDetector(PSIDetectorBase): status = Component(Signal, value=StdDaqPreviewState.UNKNOWN, kind=Kind.omitted) image = Component(Signal, kind=Kind.normal) frame = Component(Signal, kind=Kind.hinted) - image_shape = Component(Signal, kind=Kind.omitted) + image_shape = Component(Signal, kind=Kind.normal) def __init__( self, *args, url: str = "tcp://129.129.95.38:20000", parent: Device = None, **kwargs @@ -308,5 +180,5 @@ class StdDaqPreviewDetector(PSIDetectorBase): # Automatically connect to MicroSAXS testbench if directly invoked if __name__ == "__main__": - daq = StdDaqPreview(url="tcp://129.129.95.38:20000", name="preview") + daq = StdDaqPreviewDetector(url="tcp://129.129.95.38:20000", name="preview") daq.wait_for_connection() diff --git a/tomcat_bec/devices/gigafrost/stddaq_rest.py b/tomcat_bec/devices/gigafrost/stddaq_rest.py index 822df51..ce1979d 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_rest.py +++ b/tomcat_bec/devices/gigafrost/stddaq_rest.py @@ -39,7 +39,6 @@ class StdDaqRestClient(Device): # pylint: disable=too-many-instance-attributes USER_ACCESS = ["write_daq_config"] - _config_read = False # Status attributes rest_url = Component(Signal, kind=Kind.config) @@ -101,36 +100,32 @@ class StdDaqRestClient(Device): self.cfg_module_sync_queue_size.set(cfg['module_sync_queue_size']).wait() #self.cfg_module_positions.set(cfg['module_positions']).wait() - self._config_read = True - - def _build_config(self, orig) -> dict: + def _build_config(self, orig=None) -> dict: config = { - 'detector_name': str(self.cfg_detector_name.get()), - 'detector_type': str(self.cfg_detector_type.get()), - 'n_modules': int(self.cfg_n_modules.get()), - 'bit_depth': int(self.cfg_bit_depth.get()), + # 'detector_name': str(self.cfg_detector_name.get()), + # 'detector_type': str(self.cfg_detector_type.get()), + # 'n_modules': int(self.cfg_n_modules.get()), + # 'bit_depth': int(self.cfg_bit_depth.get()), 'image_pixel_height': int(self.cfg_pixel_height.get()), 'image_pixel_width': int(self.cfg_pixel_width.get()), - 'start_udp_port': int(self.cfg_start_udp_port.get()), - 'writer_user_id': int(self.cfg_writer_user_id.get()), - 'log_level': "info", - 'submodule_info': {}, - 'max_number_of_forwarders_spawned': int(self.cfg_max_number_of_forwarders.get()), - 'use_all_forwarders': bool(self.cfg_use_all_forwarders.get()), - 'module_sync_queue_size': int(self.cfg_module_sync_queue_size.get()), - 'module_positions': {}, - 'number_of_writers': 14 + # 'start_udp_port': int(self.cfg_start_udp_port.get()), + # 'writer_user_id': int(self.cfg_writer_user_id.get()), + # 'log_level': "info", + # 'submodule_info': {}, + # 'max_number_of_forwarders_spawned': int(self.cfg_max_number_of_forwarders.get()), + # 'use_all_forwarders': bool(self.cfg_use_all_forwarders.get()), + # 'module_sync_queue_size': int(self.cfg_module_sync_queue_size.get()), + # 'module_positions': {}, + # 'number_of_writers': 14 } - config = orig.update(config) + if orig is not None: + config = orig.update(config) return config def write_daq_config(self): """Write configuration ased on current PV values. Some fields might be unchangeable. """ - if not self._config_read: - raise RuntimeError("Pleae read config before editing") - orig = self.get_daq_config() config = self._build_config(orig) diff --git a/tomcat_bec/devices/gigafrost/stddaq_ws.py b/tomcat_bec/devices/gigafrost/stddaq_ws.py index 415c3a4..e71e754 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_ws.py +++ b/tomcat_bec/devices/gigafrost/stddaq_ws.py @@ -45,6 +45,7 @@ class StdDaqClient(Device): # pylint: disable=too-many-instance-attributes # Status attributes + url = Component(Signal, kind=Kind.config) status = Component(Signal, value="unknown", kind=Kind.normal) n_total = Component(Signal, value=10000, kind=Kind.config) file_path = Component(Signal, value="/gpfs/test/test-beamline", kind=Kind.config) @@ -63,6 +64,8 @@ class StdDaqClient(Device): super().__init__(*args, parent=parent, **kwargs) self.status._metadata["write_access"] = False + self.url._metadata["write_access"] = False + self.url.set(ws_url).wait() self._ws_url = ws_url self._mon = None @@ -71,10 +74,10 @@ class StdDaqClient(Device): self.connect() def connect(self): - """Connect to te StdDAQ's websockets interface + """Connect to the StdDAQ's websockets interface - StdDAQ may reject connection for a few seconds after restart, - so if it fails, wait a bit and try to connect again. + StdDAQ may reject connection for a few seconds after restart, or when + it wants so if it fails, wait a bit and try to connect again. """ num_retry = 0 while num_retry < 5: @@ -88,10 +91,6 @@ class StdDaqClient(Device): raise ConnectionRefusedError( "The stdDAQ websocket interface refused connection 5 times.") - def __del__(self): - """Try to close the socket""" - self._client.close_socket() - def monitor(self): """Attach monitoring to the DAQ""" self._client = connect(self._ws_url) @@ -190,7 +189,7 @@ class StdDaqClient(Device): def message(self, message: dict, timeout=1, wait_reply=True): """Send a message to the StdDAQ and receive a reply - Note: finishing acquisition meang StdDAQ will close connections so + Note: finishing acquisition means StdDAQ will close connection, so there's no idle state polling. """ if isinstance(message, dict): @@ -204,6 +203,7 @@ class StdDaqClient(Device): except (ConnectionClosedError, ConnectionClosedOK): self.connect() self._client.send(msg) + # Wait for reply reply = None if wait_reply: From 138a87245e231a6512f4c0d82f93eb404d6e6664 Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Tue, 3 Sep 2024 11:05:41 +0200 Subject: [PATCH 42/47] Fixing import style --- tomcat_bec/devices/gigafrost/gigafrostclient.py | 4 ++-- .../devices/gigafrost/{stddaq_ws.py => stddaq_client.py} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename tomcat_bec/devices/gigafrost/{stddaq_ws.py => stddaq_client.py} (100%) diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index a827d21..0df81b4 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -13,8 +13,8 @@ from ophyd_devices.interfaces.base_classes.psi_detector_base import ( ) try: - import gfconstants as const - from StdDaqClient import StdDaqClient + from . import gfconstants as const + from stddaq_client import StdDaqClient from gigafrostcamera import GigaFrostCamera except ModuleNotFoundError: import tomcat_bec.devices.gigafrost.gfconstants as const diff --git a/tomcat_bec/devices/gigafrost/stddaq_ws.py b/tomcat_bec/devices/gigafrost/stddaq_client.py similarity index 100% rename from tomcat_bec/devices/gigafrost/stddaq_ws.py rename to tomcat_bec/devices/gigafrost/stddaq_client.py From d86527771d45c4d173fef67a3110b00ce16f8104 Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Tue, 3 Sep 2024 11:25:47 +0200 Subject: [PATCH 43/47] Fixing startup --- tomcat_bec/devices/gigafrost/gigafrostcamera.py | 17 +++++++++++------ tomcat_bec/devices/gigafrost/gigafrostclient.py | 10 +++++----- tomcat_bec/devices/gigafrost/stddaq_client.py | 2 +- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/tomcat_bec/devices/gigafrost/gigafrostcamera.py b/tomcat_bec/devices/gigafrost/gigafrostcamera.py index 3b4e651..7d39783 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostcamera.py +++ b/tomcat_bec/devices/gigafrost/gigafrostcamera.py @@ -88,12 +88,12 @@ class GigaFrostCameraMixin(CustomDetectorMixin): return udp_header_table - def on_init(self) -> None: - """ Initialize the camera, set channel values""" - # ToDo: Not sure if it's a good idea to change camera settings upon - # ophyd device startup, i.e. each deviceserver restart. - self._init_gigafrost() - self.parent._initialized = True + # def on_init(self) -> None: + # """ Initialize the camera, set channel values""" + # # ToDo: Not sure if it's a good idea to change camera settings upon + # # ophyd device startup, i.e. each deviceserver restart. + # self._init_gigafrost() + # self.parent._initialized = True def _init_gigafrost(self) -> None: """ Initialize the camera, set channel values""" @@ -420,6 +420,11 @@ class GigaFrostCamera(PSIDetectorBase): self.backendUrl.put(self._signals_to_be_set['backend_url'], force=True) return super()._init() + def initialize(self): + """ Initialization in separate command""" + self.custom_prepare_cls._init_gigafrost() + self._initialized = True + def trigger(self) -> DeviceStatus: """ Sends a software trigger to GigaFrost""" super().trigger() diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index 0df81b4..8f244ef 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -14,11 +14,11 @@ from ophyd_devices.interfaces.base_classes.psi_detector_base import ( try: from . import gfconstants as const - from stddaq_client import StdDaqClient - from gigafrostcamera import GigaFrostCamera + from . import stddaq_client as stddaq + from . import gigafrostcamera as gfcam except ModuleNotFoundError: import tomcat_bec.devices.gigafrost.gfconstants as const - from tomcat_bec.devices.gigafrost.stddaq_ws import StdDaqClient + from tomcat_bec.devices.gigafrost.stddaq_client import StdDaqClient from tomcat_bec.devices.gigafrost.gigafrostcamera import GigaFrostCamera @@ -109,8 +109,8 @@ class GigaFrostClient(PSIDetectorBase): custom_prepare_cls = GigaFrostClientMixin USER_ACCESS = [""] - cam = Component(GigaFrostCamera, prefix="X02DA-CAM-GF2:", name="cam") - daq = Component(StdDaqClient, name="daq") + cam = Component(gfcam.GigaFrostCamera, prefix="X02DA-CAM-GF2:", name="cam") + daq = Component(stddaq.StdDaqClient, name="daq") # pylint: disable=too-many-arguments def __init__( diff --git a/tomcat_bec/devices/gigafrost/stddaq_client.py b/tomcat_bec/devices/gigafrost/stddaq_client.py index e71e754..730957a 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_client.py +++ b/tomcat_bec/devices/gigafrost/stddaq_client.py @@ -65,7 +65,7 @@ class StdDaqClient(Device): super().__init__(*args, parent=parent, **kwargs) self.status._metadata["write_access"] = False self.url._metadata["write_access"] = False - self.url.set(ws_url).wait() + self.url.set(ws_url, force=True).wait() self._ws_url = ws_url self._mon = None From eed2e280d22b191b9c8bd117cfa4359b97b108a1 Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Tue, 3 Sep 2024 13:21:14 +0200 Subject: [PATCH 44/47] Added websockets depenndency --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8bd6709..b2c9f4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ classifiers = [ "Programming Language :: Python :: 3", "Topic :: Scientific/Engineering", ] -dependencies = ["ophyd_devices", "bec_lib"] +dependencies = ["ophyd_devices", "bec_lib", "websockets"] [project.optional-dependencies] dev = ["black", "isort", "coverage", "pylint", "pytest", "pytest-random-order", "ophyd_devices", "bec_server"] From 015be3f281628f303fc6d9f05a0057c2797fe2d9 Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Tue, 3 Sep 2024 13:25:51 +0200 Subject: [PATCH 45/47] Added zmq dependency --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b2c9f4d..cc9254e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ classifiers = [ "Programming Language :: Python :: 3", "Topic :: Scientific/Engineering", ] -dependencies = ["ophyd_devices", "bec_lib", "websockets"] +dependencies = ["ophyd_devices", "bec_lib", "websockets", "pyzmq"] [project.optional-dependencies] dev = ["black", "isort", "coverage", "pylint", "pytest", "pytest-random-order", "ophyd_devices", "bec_server"] From 0c8155f4faa06da9e7cf541c2944733df815447c Mon Sep 17 00:00:00 2001 From: ci_update_bot Date: Tue, 3 Sep 2024 11:30:16 +0000 Subject: [PATCH 46/47] docs: Update device list --- tomcat_bec/devices/device_list.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tomcat_bec/devices/device_list.md b/tomcat_bec/devices/device_list.md index 7dc6ab2..0e165e5 100644 --- a/tomcat_bec/devices/device_list.md +++ b/tomcat_bec/devices/device_list.md @@ -14,8 +14,15 @@ | aa1GlobalVariables | Global variables

This class provides an interface to directly read/write global variables
on the Automation1 controller. These variables are accesible from script
files and are thus a convenient way to interface with the outside word.

Read operations take as input the memory address and the size
Write operations work with the memory address and value

Usage:
var = aa1Tasks(AA1_IOC_NAME+":VAR:", name="var")
var.wait_for_connection()
ret = var.readInt(42)
var.writeFloat(1000, np.random.random(1024))
ret_arr = var.readFloat(1000, 1024)

| [tomcat_bec.devices.aerotech.AerotechAutomation1](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/aerotech/AerotechAutomation1.py) | | aa1Tasks | Task management API

The place to manage tasks and AeroScript user files on the controller.
You can read/write/compile/execute AeroScript files and also retrieve
saved data files from the controller. It will also work around an ophyd
bug that swallows failures.

Execution does not require to store the script in a file, it will compile
it and run it directly on a certain thread. But there's no way to
retrieve the source code.

Write a text into a file on the aerotech controller and execute it with kickoff.
'''
script="var $axis as axis = ROTY\nMoveAbsolute($axis, 42, 90)"
tsk = aa1Tasks(AA1_IOC_NAME+":TASK:", name="tsk")
tsk.wait_for_connection()
tsk.configure({'text': script, 'filename': "foobar.ascript", 'taskIndex': 4})
tsk.kickoff().wait()
'''

Just execute an ascript file already on the aerotech controller.
'''
tsk = aa1Tasks(AA1_IOC_NAME+":TASK:", name="tsk")
tsk.wait_for_connection()
tsk.configure({'filename': "foobar.ascript", 'taskIndex': 4})
tsk.kickoff().wait()
'''

| [tomcat_bec.devices.aerotech.AerotechAutomation1](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/aerotech/AerotechAutomation1.py) | | aa1TaskState | Task state monitoring API

This is the task state monitoring interface for Automation1 tasks. It
does not launch execution, but can wait for the execution to complete.
| [tomcat_bec.devices.aerotech.AerotechAutomation1](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/aerotech/AerotechAutomation1.py) | +| EpicsMotorEC | Detailed ECMC EPICS motor class

Special motor class to provide additional functionality for ECMC based motors.
It exposes additional diagnostic fields and includes basic error management.
| [tomcat_bec.devices.psimotor](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/psimotor.py) | +| EpicsMotorMR | Extended EPICS Motor class

Special motor class that exposes additional motor record functionality.
It extends EpicsMotor base class to provide some simple status checks
before movement.
| [tomcat_bec.devices.psimotor](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/psimotor.py) | | EpicsMotorX | Special motor class that provides flyer interface and progress bar. | [tomcat_bec.devices.aerotech.AerotechAutomation1](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/aerotech/AerotechAutomation1.py) | +| GigaFrostCamera | Ophyd device class to control Gigafrost cameras at Tomcat

The actual hardware is implemented by an IOC based on an old fork of Helge's
cameras. This means that the camera behaves differently than the SF cameras
in particular it provides even less feedback about it's internal progress.
Helge will update the GigaFrost IOC after working beamline.
The ophyd class is based on the 'gfclient' package and has a lot of Tomcat
specific additions. It does behave differently though, as ophyd swallows the
errors from failed PV writes.

Parameters
----------
use_soft_enable : bool
Flag to use the camera's soft enable (default: False)
backend_url : str
Backend url address necessary to set up the camera's udp header.
(default: http://xbl-daq-23:8080)

Bugs:
----------
FRAMERATE : Ignored in soft trigger mode, period becomes 2xExposure time
| [tomcat_bec.devices.gigafrost.gigafrostcamera](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/gigafrost/gigafrostcamera.py) | +| GigaFrostClient | Ophyd device class to control Gigafrost cameras at Tomcat

The actual hardware is implemented by an IOC based on an old fork of Helge's
cameras. This means that the camera behaves differently than the SF cameras
in particular it provides even less feedback about it's internal progress.
Helge will update the GigaFrost IOC after working beamline.
The ophyd class is based on the 'gfclient' package and has a lot of Tomcat
specific additions. It does behave differently though, as ophyd swallows the
errors from failed PV writes.

Parameters
----------
use_soft_enable : bool
Flag to use the camera's soft enable (default: False)
backend_url : str
Backend url address necessary to set up the camera's udp header.
(default: http://xbl-daq-23:8080)

Usage:
----------
gf = GigaFrostClient(
"X02DA-CAM-GF2:", name="gf2", backend_url="http://xbl-daq-28:8080", auto_soft_enable=True,
daq_ws_url="ws://xbl-daq-29:8080", daq_rest_url="http://xbl-daq-29:5000"
)

Bugs:
----------
FRAMERATE : Ignored in soft trigger mode, period becomes 2xexposure time
| [tomcat_bec.devices.gigafrost.gigafrostclient](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/gigafrost/gigafrostclient.py) | | GrashopperTOMCAT |
Grashopper detector for TOMCAT

Parent class: PSIDetectorBase

class attributes:
custom_prepare_cls (GrashopperTOMCATSetup) : Custom detector setup class for TOMCAT,
inherits from CustomDetectorMixin
cam (SLSDetectorCam) : Detector camera
image (SLSImagePlugin) : Image plugin for detector
| [tomcat_bec.devices.grashopper_tomcat](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/grashopper_tomcat.py) | | SLSDetectorCam |
SLS Detector Camera - Grashoppter

Base class to map EPICS PVs to ophyd signals.
| [tomcat_bec.devices.grashopper_tomcat](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/grashopper_tomcat.py) | | SLSImagePlugin | SLS Image Plugin

Image plugin for SLS detector imitating the behaviour of ImagePlugin from
ophyd's areadetector plugins.
| [tomcat_bec.devices.grashopper_tomcat](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/grashopper_tomcat.py) | +| StdDaqClient | StdDaq API

This class combines the new websocket and REST interfaces of the stdDAQ that
were meant to replace the documented python client. The websocket interface
starts and stops the acquisition and provides status, while the REST
interface can read and write the configuration. The DAQ needs to restart
all services to reconfigure with a new config.

The websocket provides status updates about a running acquisition but the
interface breaks connection at the end of the run.

The standard DAQ configuration is a single JSON file locally autodeployed
to the DAQ servers (as root!!!). It can only be written through a REST API
that is semi-supported. The DAQ might be distributed across several servers,
we'll only interface with the primary REST interface will synchronize with
all secondary REST servers. In the past this was a source of problems.

Example:
'''
daq = StdDaqClient(name="daq", ws_url="ws://xbl-daq-29:8080", rest_url="http://xbl-daq-29:5000")
'''
| [tomcat_bec.devices.gigafrost.stddaq_client](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/gigafrost/stddaq_client.py) | +| StdDaqPreviewDetector | Detector wrapper class around the StdDaq preview image stream.

This was meant to provide live image stream directly from the StdDAQ.
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.

You can add a preview widget to the dock by:
cam_widget = gui.add_dock('cam_dock1').add_widget('BECFigure').image('daq_stream1')
| [tomcat_bec.devices.gigafrost.stddaq_preview](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/gigafrost/stddaq_preview.py) | +| StdDaqRestClient | Wrapper class around the new StdDaq REST interface.

This was meant to extend the websocket inteface that replaced the documented
python client. It is used as a part of the StdDaqClient aggregator class.
Good to know that the stdDAQ restarts all services after reconfiguration.

The standard DAQ configuration is a single JSON file locally autodeployed
to the DAQ servers (as root!!!). It can only be written through the REST API
via standard HTTP requests. The DAQ might be distributed across several servers,
we'll only interface with the primary REST interface will synchronize with
all secondary REST servers. In the past this was a source of problems.

Example:
'''
daqcfg = StdDaqRestClient(name="daqcfg", rest_url="http://xbl-daq-29:5000")
'''
| [tomcat_bec.devices.gigafrost.stddaq_rest](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/gigafrost/stddaq_rest.py) | | TomcatAerotechRotation | Special motor class that provides flyer interface and progress bar. | [tomcat_bec.devices.tomcat_rotation_motors](https://gitlab.psi.ch/bec/tomcat_bec/-/blob/main/tomcat_bec/devices/tomcat_rotation_motors.py) | From ae8a28b09b69f3cb9b860534de268de4d21ac3af Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Tue, 1 Oct 2024 10:17:49 +0200 Subject: [PATCH 47/47] chore: added license --- LICENSE | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b593d95 --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2024, Paul Scherrer Institute + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.