First round of bamline feedback #18
@@ -49,9 +49,9 @@ femto_mean_curr:
|
||||
softwareTrigger: false
|
||||
|
||||
es1_roty:
|
||||
readoutPriority: baseline
|
||||
readoutPriority: monitored
|
||||
description: 'Test rotation stage'
|
||||
deviceClass: tomcat_bec.devices.psimotor.EpicsMotorMR
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: X02DA-ES1-SMP1:ROTY
|
||||
deviceTags:
|
||||
@@ -61,59 +61,59 @@ es1_roty:
|
||||
readOnly: false
|
||||
softwareTrigger: false
|
||||
|
||||
es1_ismc:
|
||||
description: 'Automation1 iSMC interface'
|
||||
deviceClass: tomcat_bec.devices.aa1Controller
|
||||
deviceConfig:
|
||||
prefix: 'X02DA-ES1-SMP1:CTRL:'
|
||||
deviceTags:
|
||||
- es1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: monitored
|
||||
softwareTrigger: false
|
||||
# es1_ismc:
|
||||
# description: 'Automation1 iSMC interface'
|
||||
# deviceClass: tomcat_bec.devices.aa1Controller
|
||||
# deviceConfig:
|
||||
# prefix: 'X02DA-ES1-SMP1:CTRL:'
|
||||
# deviceTags:
|
||||
# - es1
|
||||
# enabled: true
|
||||
# onFailure: buffer
|
||||
# readOnly: false
|
||||
# readoutPriority: monitored
|
||||
# softwareTrigger: false
|
||||
|
||||
es1_tasks:
|
||||
description: 'Automation1 task management interface'
|
||||
deviceClass: tomcat_bec.devices.aa1Tasks
|
||||
deviceConfig:
|
||||
prefix: 'X02DA-ES1-SMP1:TASK:'
|
||||
deviceTags:
|
||||
- es1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: monitored
|
||||
softwareTrigger: false
|
||||
# es1_tasks:
|
||||
# description: 'Automation1 task management interface'
|
||||
# deviceClass: tomcat_bec.devices.aa1Tasks
|
||||
# deviceConfig:
|
||||
# prefix: 'X02DA-ES1-SMP1:TASK:'
|
||||
# deviceTags:
|
||||
# - es1
|
||||
# enabled: true
|
||||
# onFailure: buffer
|
||||
# readOnly: false
|
||||
# readoutPriority: monitored
|
||||
# softwareTrigger: false
|
||||
|
||||
|
||||
es1_psod:
|
||||
description: 'AA1 PSO output interface (trigger)'
|
||||
deviceClass: tomcat_bec.devices.aa1AxisPsoDistance
|
||||
deviceConfig:
|
||||
prefix: 'X02DA-ES1-SMP1:ROTY:PSO:'
|
||||
deviceTags:
|
||||
- es1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: monitored
|
||||
softwareTrigger: true
|
||||
# es1_psod:
|
||||
# description: 'AA1 PSO output interface (trigger)'
|
||||
# deviceClass: tomcat_bec.devices.aa1AxisPsoDistance
|
||||
# deviceConfig:
|
||||
# prefix: 'X02DA-ES1-SMP1:ROTY:PSO:'
|
||||
# deviceTags:
|
||||
# - es1
|
||||
# enabled: true
|
||||
# onFailure: buffer
|
||||
# readOnly: false
|
||||
# readoutPriority: monitored
|
||||
# softwareTrigger: true
|
||||
|
||||
|
||||
es1_ddaq:
|
||||
description: 'Automation1 position recording interface'
|
||||
deviceClass: tomcat_bec.devices.aa1AxisDriveDataCollection
|
||||
deviceConfig:
|
||||
prefix: 'X02DA-ES1-SMP1:ROTY:DDC:'
|
||||
deviceTags:
|
||||
- es1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: monitored
|
||||
softwareTrigger: false
|
||||
# es1_ddaq:
|
||||
# description: 'Automation1 position recording interface'
|
||||
# deviceClass: tomcat_bec.devices.aa1AxisDriveDataCollection
|
||||
# deviceConfig:
|
||||
# prefix: 'X02DA-ES1-SMP1:ROTY:DDC:'
|
||||
# deviceTags:
|
||||
# - es1
|
||||
# enabled: true
|
||||
# onFailure: buffer
|
||||
# readOnly: false
|
||||
# readoutPriority: monitored
|
||||
# softwareTrigger: false
|
||||
|
||||
|
||||
#camera:
|
||||
@@ -151,6 +151,7 @@ gfdaq:
|
||||
deviceConfig:
|
||||
ws_url: 'ws://129.129.95.111:8080'
|
||||
rest_url: 'http://129.129.95.111:5000'
|
||||
data_source_name: 'gfcam'
|
||||
deviceTags:
|
||||
- std-daq
|
||||
enabled: true
|
||||
|
||||
186
tomcat_bec/device_configs/microxas_test_bed_tmp.yaml
Normal file
186
tomcat_bec/device_configs/microxas_test_bed_tmp.yaml
Normal file
@@ -0,0 +1,186 @@
|
||||
eyex:
|
||||
readoutPriority: baseline
|
||||
description: X-ray eye axis X
|
||||
deviceClass: tomcat_bec.devices.psimotor.EpicsMotorEC
|
||||
deviceConfig:
|
||||
prefix: MTEST-X05LA-ES2-XRAYEYE:M1
|
||||
deviceTags:
|
||||
- xray-eye
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readOnly: false
|
||||
softwareTrigger: false
|
||||
# eyey:
|
||||
# readoutPriority: baseline
|
||||
# description: X-ray eye axis Y
|
||||
# deviceClass: tomcat_bec.devices.psimotor.EpicsMotorEC
|
||||
# deviceConfig:
|
||||
# prefix: MTEST-X05LA-ES2-XRAYEYE:M2
|
||||
# deviceTags:
|
||||
# - xray-eye
|
||||
# onFailure: buffer
|
||||
# enabled: true
|
||||
# readOnly: false
|
||||
# softwareTrigger: false
|
||||
# eyez:
|
||||
# readoutPriority: baseline
|
||||
# description: X-ray eye axis Z
|
||||
# deviceClass: tomcat_bec.devices.psimotor.EpicsMotorEC
|
||||
# deviceConfig:
|
||||
# prefix: MTEST-X05LA-ES2-XRAYEYE:M3
|
||||
# deviceTags:
|
||||
# - xray-eye
|
||||
# onFailure: buffer
|
||||
# enabled: true
|
||||
# readOnly: false
|
||||
# softwareTrigger: false
|
||||
femto_mean_curr:
|
||||
readoutPriority: monitored
|
||||
description: Femto mean current
|
||||
deviceClass: ophyd.EpicsSignal
|
||||
deviceConfig:
|
||||
auto_monitor: true
|
||||
read_pv: MTEST-X05LA-ES2-XRAYEYE:FEMTO-MEAN-CURR
|
||||
deviceTags:
|
||||
- xray-eye
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readOnly: true
|
||||
softwareTrigger: false
|
||||
|
||||
es1_roty:
|
||||
readoutPriority: baseline
|
||||
description: 'Test rotation stage'
|
||||
deviceClass: tomcat_bec.devices.psimotor.EpicsMotorMR
|
||||
deviceConfig:
|
||||
prefix: X02DA-ES1-SMP1:ROTY
|
||||
deviceTags:
|
||||
- es1-sam
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readOnly: false
|
||||
softwareTrigger: false
|
||||
|
||||
es1_ismc:
|
||||
description: 'Automation1 iSMC interface'
|
||||
deviceClass: tomcat_bec.devices.aa1Controller
|
||||
deviceConfig:
|
||||
prefix: 'X02DA-ES1-SMP1:CTRL:'
|
||||
deviceTags:
|
||||
- es1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: monitored
|
||||
softwareTrigger: false
|
||||
|
||||
es1_tasks:
|
||||
description: 'Automation1 task management interface'
|
||||
deviceClass: tomcat_bec.devices.aa1Tasks
|
||||
deviceConfig:
|
||||
prefix: 'X02DA-ES1-SMP1:TASK:'
|
||||
deviceTags:
|
||||
- es1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: monitored
|
||||
softwareTrigger: false
|
||||
|
||||
|
||||
es1_psod:
|
||||
description: 'AA1 PSO output interface (trigger)'
|
||||
deviceClass: tomcat_bec.devices.aa1AxisPsoDistance
|
||||
deviceConfig:
|
||||
prefix: 'X02DA-ES1-SMP1:ROTY:PSO:'
|
||||
deviceTags:
|
||||
- es1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: monitored
|
||||
softwareTrigger: true
|
||||
|
||||
|
||||
es1_ddaq:
|
||||
description: 'Automation1 position recording interface'
|
||||
deviceClass: tomcat_bec.devices.aa1AxisDriveDataCollection
|
||||
deviceConfig:
|
||||
prefix: 'X02DA-ES1-SMP1:ROTY:DDC:'
|
||||
deviceTags:
|
||||
- es1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: monitored
|
||||
softwareTrigger: false
|
||||
|
||||
|
||||
#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
|
||||
|
||||
gfcam:
|
||||
description: GigaFrost camera client
|
||||
deviceClass: tomcat_bec.devices.GigaFrostCamera
|
||||
deviceConfig:
|
||||
prefix: 'X02DA-CAM-GF2:'
|
||||
backend_url: 'http://sls-daq-001:8080'
|
||||
auto_soft_enable: true
|
||||
deviceTags:
|
||||
- camera
|
||||
- trigger
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: monitored
|
||||
softwareTrigger: true
|
||||
|
||||
gfdaq:
|
||||
description: GigaFrost stdDAQ client
|
||||
deviceClass: tomcat_bec.devices.StdDaqClient
|
||||
deviceConfig:
|
||||
ws_url: 'ws://129.129.95.111:8080'
|
||||
rest_url: 'http://129.129.95.111:5000'
|
||||
deviceTags:
|
||||
- std-daq
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: monitored
|
||||
softwareTrigger: false
|
||||
|
||||
daq_stream0:
|
||||
description: stdDAQ preview (2 every 555)
|
||||
deviceClass: tomcat_bec.devices.StdDaqPreviewDetector
|
||||
deviceConfig:
|
||||
url: 'tcp://129.129.95.111:20000'
|
||||
deviceTags:
|
||||
- std-daq
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: monitored
|
||||
softwareTrigger: false
|
||||
|
||||
daq_stream1:
|
||||
description: stdDAQ preview (1 at 5 Hz)
|
||||
deviceClass: tomcat_bec.devices.StdDaqPreviewDetector
|
||||
deviceConfig:
|
||||
url: 'tcp://129.129.95.111:20001'
|
||||
deviceTags:
|
||||
- std-daq
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: monitored
|
||||
softwareTrigger: false
|
||||
@@ -1,7 +1,11 @@
|
||||
from time import sleep
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Ophyd device for the Aerotech Automation1 IOC's generic interfaces
|
||||
|
||||
@author: mohacsi_i
|
||||
"""
|
||||
import numpy as np
|
||||
from ophyd import Component, Device, EpicsMotor, EpicsSignal, EpicsSignalRO, Kind
|
||||
from ophyd.status import DeviceStatus, SubscriptionStatus
|
||||
|
||||
from bec_lib import bec_logger
|
||||
logger = bec_logger.logger
|
||||
@@ -38,8 +42,8 @@ class aa1Controller(Device):
|
||||
taskcount = Component(EpicsSignalRO, "TASKCOUNT", kind=Kind.config)
|
||||
fastpoll = Component(EpicsSignalRO, "POLLTIME", auto_monitor=True, kind=Kind.normal)
|
||||
slowpoll = Component(EpicsSignalRO, "DRVPOLLTIME", auto_monitor=True, kind=Kind.normal)
|
||||
errno = Component(EpicsSignalRO, "ERRNO", auto_monitor=True, kind=Kind.hinted)
|
||||
errnmsg = Component(EpicsSignalRO, "ERRMSG", auto_monitor=True, kind=Kind.hinted)
|
||||
errno = Component(EpicsSignalRO, "ERRNO", auto_monitor=True, kind=Kind.normal)
|
||||
errnmsg = Component(EpicsSignalRO, "ERRMSG", auto_monitor=True, kind=Kind.normal)
|
||||
_set_ismc = Component(EpicsSignal, "SET", put_complete=True, kind=Kind.omitted)
|
||||
|
||||
USER_ACCESS = ["reset"]
|
||||
@@ -111,10 +115,10 @@ class aa1GlobalVariables(Device):
|
||||
if size is None or size == 0:
|
||||
self.integer_addr.set(address).wait()
|
||||
return self.integer_rb.get()
|
||||
else:
|
||||
self.integer_addr.set(address).wait()
|
||||
self.integer_size.set(size).wait()
|
||||
return self.integerarr_rb.get()
|
||||
|
||||
self.integer_addr.set(address).wait()
|
||||
self.integer_size.set(size).wait()
|
||||
return self.integerarr_rb.get()
|
||||
|
||||
def write_int(self, address: int, value, settle_time=0.1) -> None:
|
||||
"""Write a 64-bit integer global variable
|
||||
@@ -150,10 +154,10 @@ class aa1GlobalVariables(Device):
|
||||
if size is None:
|
||||
self.real_addr.set(address).wait()
|
||||
return self.real_rb.get()
|
||||
else:
|
||||
self.real_addr.set(address).wait()
|
||||
self.real_size.set(size).wait()
|
||||
return self.realarr_rb.get()
|
||||
|
||||
self.real_addr.set(address).wait()
|
||||
self.real_size.set(size).wait()
|
||||
return self.realarr_rb.get()
|
||||
|
||||
def write_float(self, address: int, value, settle_time=0.1) -> None:
|
||||
"""Write a 64-bit float global variable"""
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Enumerations for the Aerotech Automation1 controller as adopted from the Aerotech library.
|
||||
|
||||
@author: mohacsi_i
|
||||
"""
|
||||
from enum import Enum
|
||||
|
||||
# pylint: disable=missing-class-docstring
|
||||
# pylint: disable=too-few-public-methods
|
||||
|
||||
|
||||
class TomcatSequencerState:
|
||||
IDLE = 0
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Ophyd device for the Aerotech Automation1 IOC's axis-specific synchronized
|
||||
drive data collection (DDC) interface.
|
||||
|
||||
@author: mohacsi_i
|
||||
"""
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
|
||||
from ophyd import Component, EpicsSignal, EpicsSignalRO, Kind
|
||||
from ophyd.status import SubscriptionStatus
|
||||
|
||||
try:
|
||||
from AerotechAutomation1Enums import DriveDataCaptureInput, DriveDataCaptureTrigger
|
||||
except ModuleNotFoundError:
|
||||
from tomcat_bec.devices.aerotech.AerotechAutomation1Enums import DriveDataCaptureInput
|
||||
from tomcat_bec.devices.aerotech.AerotechAutomation1Enums import DriveDataCaptureTrigger
|
||||
|
||||
from ophyd_devices.interfaces.base_classes.psi_detector_base import PSIDetectorBase as PSIDeviceBase
|
||||
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
|
||||
CustomDetectorMixin as CustomDeviceMixin,
|
||||
@@ -20,7 +21,7 @@ logger = bec_logger.logger
|
||||
|
||||
|
||||
class AerotechDriveDataCollectionMixin(CustomDeviceMixin):
|
||||
"""Configuration and staging
|
||||
"""Mixin class for self-configuration and staging
|
||||
|
||||
NOTE: scripted scans start drive data collection internally
|
||||
"""
|
||||
@@ -29,23 +30,29 @@ class AerotechDriveDataCollectionMixin(CustomDeviceMixin):
|
||||
"""Configuration and staging"""
|
||||
|
||||
# Fish out configuration from scaninfo (does not need to be full configuration)
|
||||
scanparam = self.parent.scaninfo.scan_msg.info
|
||||
alias = self.parent.parent.name if self.parent.parent is not None else self.parent.name
|
||||
logger.warning(f"[{alias}] Scan parameters:\n{scanparam}")
|
||||
d = {}
|
||||
if "kwargs" in scanparam:
|
||||
scanargs = scanparam["kwargs"]
|
||||
if "kwargs" in self.parent.scaninfo.scan_msg.info:
|
||||
scanargs = self.parent.scaninfo.scan_msg.info["kwargs"]
|
||||
# NOTE: Scans don't have to fully configure the device
|
||||
if "ddc_trigger" in scanargs:
|
||||
d["ddc_trigger"] = scanargs["ddc_trigger"]
|
||||
if "steps" in scanargs and "exp_burst" in scanargs:
|
||||
scan_steps = scanargs["steps"]
|
||||
scan_burst = scanargs["exp_burst"]
|
||||
d["num_points_total"] = (scan_steps+1) * scan_burst
|
||||
elif "exp_burst" in scanargs:
|
||||
d["num_points_total"] = scanargs["exp_burst"]
|
||||
elif "steps" in scanargs:
|
||||
d["num_points_total"] = scanargs["steps"]
|
||||
if "ddc_num_points" in scanargs:
|
||||
d["num_points_total"] = scanargs["ddc_num_points"]
|
||||
else:
|
||||
# Try to figure out number of points
|
||||
num_points = 1
|
||||
points_valid = False
|
||||
if "steps" in scanargs and scanargs['steps'] is not None:
|
||||
num_points *= scanargs["steps"]
|
||||
points_valid = True
|
||||
elif "exp_burst" in scanargs and scanargs['exp_burst'] is not None:
|
||||
num_points *= scanargs["exp_burst"]
|
||||
points_valid = True
|
||||
elif "repeats" in scanargs and scanargs['repeats'] is not None:
|
||||
num_points *= scanargs["repeats"]
|
||||
points_valid = True
|
||||
if points_valid:
|
||||
d["num_points_total"] = num_points
|
||||
|
||||
# Perform bluesky-style configuration
|
||||
if len(d) > 0:
|
||||
@@ -107,7 +114,7 @@ class aa1AxisDriveDataCollection(PSIDeviceBase):
|
||||
custom_prepare_cls = AerotechDriveDataCollectionMixin
|
||||
USER_ACCESS = ["configure", "reset"]
|
||||
|
||||
def configure(self, d: dict = {}) -> tuple:
|
||||
def configure(self, d: dict = None) -> tuple:
|
||||
"""Configure data capture
|
||||
|
||||
Configures the hardware synchronized drive data capture (DDC) on an
|
||||
@@ -117,14 +124,15 @@ class aa1AxisDriveDataCollection(PSIDeviceBase):
|
||||
"""
|
||||
old = self.read_configuration()
|
||||
|
||||
if "num_points_total" in d:
|
||||
self.npoints.set(d["num_points_total"]).wait()
|
||||
if "ddc_trigger" in d:
|
||||
self._trigger.set(d['ddc_trigger']).wait()
|
||||
if "ddc_source0" in d:
|
||||
self._input0.set(d['ddc_source0']).wait()
|
||||
if "ddc_source1" in d:
|
||||
self._input1.set(d['ddc_source1']).wait()
|
||||
if d is not None:
|
||||
if "num_points_total" in d:
|
||||
self.npoints.set(d["num_points_total"]).wait()
|
||||
if "ddc_trigger" in d:
|
||||
self._trigger.set(d['ddc_trigger']).wait()
|
||||
if "ddc_source0" in d:
|
||||
self._input0.set(d['ddc_source0']).wait()
|
||||
if "ddc_source1" in d:
|
||||
self._input1.set(d['ddc_source1']).wait()
|
||||
|
||||
# Reset incremental readback
|
||||
self._switch.set("ResetRB", settle_time=0.1).wait()
|
||||
@@ -150,17 +158,17 @@ class aa1AxisDriveDataCollection(PSIDeviceBase):
|
||||
# Define wait until the busy flag goes down (excluding initial update)
|
||||
timestamp_ = 0
|
||||
|
||||
def negEdge(*args, old_value, value, timestamp, **kwargs):
|
||||
def neg_edge(*args, old_value, value, timestamp, **kwargs):
|
||||
nonlocal timestamp_
|
||||
result = False if (timestamp_ == 0) else (old_value == 1 and value == 0)
|
||||
timestamp_ = timestamp
|
||||
return result
|
||||
|
||||
if index == 0:
|
||||
status = SubscriptionStatus(self._readstatus0, negEdge, settle_time=0.5)
|
||||
status = SubscriptionStatus(self._readstatus0, neg_edge, settle_time=0.5)
|
||||
self._readback0.set(1).wait()
|
||||
elif index == 1:
|
||||
status = SubscriptionStatus(self._readstatus1, negEdge, settle_time=0.5)
|
||||
status = SubscriptionStatus(self._readstatus1, neg_edge, settle_time=0.5)
|
||||
self._readback1.set(1).wait()
|
||||
|
||||
# Start asynchronous readback
|
||||
@@ -168,6 +176,8 @@ class aa1AxisDriveDataCollection(PSIDeviceBase):
|
||||
return status
|
||||
|
||||
def describe_collect(self) -> OrderedDict:
|
||||
"""Describes collected array format according to JSONschema
|
||||
"""
|
||||
ret = OrderedDict()
|
||||
ret["buffer0"] = {
|
||||
"source": "internal",
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Ophyd device for the Aerotech Automation1 IOC's axis-specific position
|
||||
synchronized output (PSO) interface.
|
||||
|
||||
@author: mohacsi_i
|
||||
"""
|
||||
from time import sleep
|
||||
import numpy as np
|
||||
from ophyd import Component, EpicsSignal, EpicsSignalRO, Kind
|
||||
@@ -12,6 +19,8 @@ logger = bec_logger.logger
|
||||
|
||||
|
||||
class AerotechPsoDistanceMixin(CustomDeviceMixin):
|
||||
"""Mixin class for self-configuration and staging
|
||||
"""
|
||||
# parent : aa1Tasks
|
||||
def on_stage(self) -> None:
|
||||
"""Configuration and staging
|
||||
@@ -21,14 +30,10 @@ class AerotechPsoDistanceMixin(CustomDeviceMixin):
|
||||
when not in use. I.e. this method is not expected to be called when
|
||||
PSO is not needed or when it'd conflict with other devices.
|
||||
"""
|
||||
|
||||
# Fish out configuration from scaninfo (does not need to be full configuration)
|
||||
scanparam = self.parent.scaninfo.scan_msg.info
|
||||
alias = self.parent.parent.name if self.parent.parent is not None else self.parent.name
|
||||
logger.warning(f"[{alias}] Scan parameters:\n{scanparam}")
|
||||
d = {}
|
||||
if "kwargs" in scanparam:
|
||||
scanargs = scanparam["kwargs"]
|
||||
if "kwargs" in self.parent.scaninfo.scan_msg.info:
|
||||
scanargs = self.parent.scaninfo.scan_msg.info["kwargs"]
|
||||
if "pso_distance" in scanargs:
|
||||
d["pso_distance"] = scanargs["pso_distance"]
|
||||
if "pso_wavemode" in scanargs:
|
||||
@@ -237,7 +242,7 @@ class aa1AxisPsoDistance(aa1AxisPsoBase):
|
||||
"""Simplified configuration interface to access the most common
|
||||
functionality for distance mode PSO.
|
||||
|
||||
:param pso_distance: The trigger distance or the array of distances between subsequent points.
|
||||
:param pso_distance: Distance or array of distances between subsequent trigger points.
|
||||
:param pso_wavemode: Waveform mode configuration, usually pulsed/toggled (default: pulsed).
|
||||
:param pso_t_pulse : trigger high duration in pulsed mode (default: 100 us)
|
||||
:param pso_w_pulse : trigger hold-off time in pulsed mode (default: 200 us)
|
||||
@@ -276,7 +281,7 @@ class aa1AxisPsoDistance(aa1AxisPsoBase):
|
||||
new = self.read_configuration()
|
||||
logger.info(f"[{self.name}] PSO configured to {pso_wavemode} mode")
|
||||
return (old, new)
|
||||
|
||||
|
||||
def bluestage(self) -> None:
|
||||
"""Bluesky style stage"""
|
||||
# Stage the PSO distance module and zero counter
|
||||
@@ -288,25 +293,3 @@ class aa1AxisPsoDistance(aa1AxisPsoBase):
|
||||
if self.dstDistanceVal.get() > 0:
|
||||
self.dstEventsEna.set("On").wait()
|
||||
self.dstCounterEna.set("On").wait()
|
||||
|
||||
|
||||
# # ########################################################################
|
||||
# # Bluesky flyer interface
|
||||
# def prepare(self, distance=None) -> DeviceStatus:
|
||||
# """ Arm trigger for a synchronous or asynchronous acquisition"""
|
||||
# if distance is not None:
|
||||
# # Write a new array
|
||||
# if isinstance(distance, (float, int)):
|
||||
# self.dstDistance.set(distance).wait()
|
||||
# elif isinstance(distance, (np.ndarray, list, tuple)):
|
||||
# self.dstDistanceArr.set(distance).wait()
|
||||
# else:
|
||||
# # Rearm the already configured array
|
||||
# if isinstance(self._distance_value, (np.ndarray, list, tuple)):
|
||||
# self.dstArrayRearm.set(1).wait()
|
||||
# # Start monitoring the counters
|
||||
# self.dstEventsEna.set("On").wait()
|
||||
# self.dstCounterEna.set("On").wait()
|
||||
# status = DeviceStatus(self)
|
||||
# status.set_finished()
|
||||
# return status
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Ophyd device for the Aerotech Automation1 IOC's controller's task management
|
||||
interface.
|
||||
|
||||
@author: mohacsi_i
|
||||
"""
|
||||
from time import sleep
|
||||
from ophyd import Component, EpicsSignal, EpicsSignalRO, Kind
|
||||
from ophyd.status import DeviceStatus, SubscriptionStatus
|
||||
@@ -12,6 +19,8 @@ logger = bec_logger.logger
|
||||
|
||||
|
||||
class AerotechTasksMixin(CustomDeviceMixin):
|
||||
"""Mixin class for self-configuration and staging
|
||||
"""
|
||||
# parent : aa1Tasks
|
||||
def on_stage(self) -> None:
|
||||
"""Configuration and staging
|
||||
@@ -27,20 +36,16 @@ class AerotechTasksMixin(CustomDeviceMixin):
|
||||
# logger.warning(self.parent.scaninfo.scan_msg.info['kwargs'].keys())
|
||||
|
||||
# Fish out our configuration from scaninfo (via explicit or generic addressing)
|
||||
scanparam = self.parent.scaninfo.scan_msg.info
|
||||
alias = self.parent.parent.name if self.parent.parent is not None else self.parent.name
|
||||
# logger.warning(f"[{alias}] Scan parameters:\n{scanparam}")
|
||||
d = {}
|
||||
|
||||
if "kwargs" in scanparam:
|
||||
scanargs = scanparam["kwargs"]
|
||||
if "kwargs" in self.parent.scaninfo.scan_msg.info:
|
||||
scanargs = self.parent.scaninfo.scan_msg.info["kwargs"]
|
||||
if self.parent.scaninfo.scan_type in ("script", "scripted"):
|
||||
# NOTE: Scans don't have to fully configure the device
|
||||
if "script_text" in scanargs:
|
||||
if "script_text" in scanargs and scanargs["script_text"] is not None:
|
||||
d["script_text"] = scanargs["script_text"]
|
||||
if "script_file" in scanargs:
|
||||
if "script_file" in scanargs and scanargs["script_file"] is not None:
|
||||
d["script_file"] = scanargs["script_file"]
|
||||
if "script_task" in scanargs:
|
||||
if "script_task" in scanargs and scanargs["script_task"] is not None:
|
||||
d["script_task"] = scanargs["script_task"]
|
||||
|
||||
# Perform bluesky-style configuration
|
||||
@@ -114,7 +119,7 @@ class aa1Tasks(PSIDeviceBase):
|
||||
# Common operations
|
||||
old = self.read_configuration()
|
||||
self.switch.set("Reset").wait()
|
||||
# Check if
|
||||
# Check what we got
|
||||
if "script_task" in d:
|
||||
if d['script_task'] < 3 or d['script_task'] > 21:
|
||||
raise RuntimeError(f"Invalid task index: {d['script_task']}")
|
||||
@@ -136,7 +141,7 @@ class aa1Tasks(PSIDeviceBase):
|
||||
def bluestage(self) -> None:
|
||||
"""Bluesky style stage"""
|
||||
if self.taskIndex.get() in (0, 1, 2):
|
||||
logger.error(f"[{self.name}] Woah, launching AeroScript on a system task. Daring today are we?")
|
||||
logger.error(f"[{self.name}] Launching AeroScript on system task. Daring today are we?")
|
||||
# Launch and check success
|
||||
status = self.switch.set("Run", settle_time=0.2)
|
||||
status.wait()
|
||||
@@ -151,13 +156,10 @@ class aa1Tasks(PSIDeviceBase):
|
||||
timestamp_ = 0
|
||||
task_idx = int(self.taskIndex.get())
|
||||
|
||||
def not_running(*args, old_value, value, timestamp, **kwargs):
|
||||
def not_running(*args, value, timestamp, **kwargs):
|
||||
nonlocal timestamp_
|
||||
result = False if value[task_idx] in ["Running", 4] else True
|
||||
# FIXME: BEC will swallow this exception
|
||||
# error = bool(value[task_idx] in ["Error", 8])
|
||||
result = value[task_idx] not in ["Running", 4]
|
||||
timestamp_ = timestamp
|
||||
# print(result)
|
||||
return result
|
||||
|
||||
# Subscribe and wait for update
|
||||
|
||||
@@ -2,5 +2,3 @@ from .AerotechTasks import aa1Tasks
|
||||
from .AerotechPso import aa1AxisPsoDistance
|
||||
from .AerotechDriveDataCollection import aa1AxisDriveDataCollection
|
||||
from .AerotechAutomation1 import aa1Controller, aa1GlobalVariables, aa1GlobalVariableBindings, aa1AxisIo
|
||||
|
||||
|
||||
|
||||
@@ -256,7 +256,7 @@ class GigaFrostCamera(PSIDetectorBase):
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
|
||||
custom_prepare_cls = GigaFrostCameraMixin
|
||||
USER_ACCESS = [""]
|
||||
USER_ACCESS = ["initialize"]
|
||||
_initialized = False
|
||||
|
||||
infoBusyFlag = Component(EpicsSignalRO, "BUSY_STAT", auto_monitor=True)
|
||||
@@ -466,7 +466,7 @@ class GigaFrostCamera(PSIDetectorBase):
|
||||
|
||||
def initialize(self):
|
||||
""" Initialization in separate command"""
|
||||
self.custom_prepare_cls._init_gigafrost()
|
||||
self.custom_prepare._init_gigafrost()
|
||||
self._initialized = True
|
||||
|
||||
def trigger(self) -> DeviceStatus:
|
||||
|
||||
388
tomcat_bec/devices/gigafrost/helgecamera.py
Normal file
388
tomcat_bec/devices/gigafrost/helgecamera.py
Normal file
@@ -0,0 +1,388 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created on Wed Dec 6 11:33:54 2023
|
||||
|
||||
@author: mohacsi_i
|
||||
"""
|
||||
|
||||
from ophyd import Device, Component, EpicsMotor, EpicsSignal, EpicsSignalRO, Kind
|
||||
from ophyd.status import Status, SubscriptionStatus, StatusBase, DeviceStatus
|
||||
from time import sleep
|
||||
import warnings
|
||||
import numpy as np
|
||||
import time
|
||||
from ophyd_devices.epics.devices.psi_detector_base import CustomDetectorMixin, PSIDetectorBase
|
||||
|
||||
|
||||
class HelgeCameraMixin(CustomDetectorMixin):
|
||||
"""Mixin class to setup the Helge camera bae class.
|
||||
|
||||
This class will be called by the custom_prepare_cls attribute of the detector class.
|
||||
"""
|
||||
|
||||
def __init__(self, *_args, parent: Device = None, **_kwargs) -> None:
|
||||
super().__init__(*_args, parent=parent, **_kwargs)
|
||||
self.monitor_thread = None
|
||||
self.stop_monitor = False
|
||||
self.update_frequency = 1
|
||||
|
||||
def set_exposure_time(self, exposure_time: float) -> None:
|
||||
"""Set the detector framerate.
|
||||
|
||||
Args:
|
||||
exposure_time (float): Desired exposure time in [sec]
|
||||
"""
|
||||
if exposure_time is not None:
|
||||
self.parent.acqExpTime.set(exposure_time).wait()
|
||||
|
||||
def prepare_detector_backend(self) -> None:
|
||||
pass
|
||||
|
||||
def prepare_detector(self) -> None:
|
||||
"""Prepare detector for acquisition.
|
||||
|
||||
State machine:
|
||||
BUSY and SET both low -> BUSY high, SET low -> BUSY low, SET high -> BUSY low, SET low
|
||||
"""
|
||||
|
||||
self.parent.camSetParam.set(1).wait()
|
||||
def risingEdge(*args, old_value, value, timestamp, **kwargs):
|
||||
return bool(not old_value and value)
|
||||
def fallingEdge(*args, old_value, value, timestamp, **kwargs):
|
||||
return bool(old_value and not value)
|
||||
# Subscribe and wait for update
|
||||
status = SubscriptionStatus(self.parent.camSetParam, fallingEdge, settle_time=0.5)
|
||||
status.wait()
|
||||
|
||||
|
||||
def arm_acquisition(self) -> None:
|
||||
"""Arm camera for acquisition"""
|
||||
|
||||
# Acquisition is only allowed when the IOC is not busy
|
||||
if self.parent.state in ("OFFLINE", "BUSY", "REMOVED", "RUNNING"):
|
||||
raise RuntimeError(f"Camera in in state: {self.parent.state}")
|
||||
|
||||
# Start the acquisition (this sets parameers and starts acquisition)
|
||||
self.parent.camStatusCmd.set("Running").wait()
|
||||
|
||||
# Subscribe and wait for update
|
||||
def isRunning(*args, old_value, value, timestamp, **kwargs):
|
||||
return bool(self.parent.state=="RUNNING")
|
||||
status = SubscriptionStatus(self.parent.camStatusCode, isRunning, settle_time=0.2)
|
||||
status.wait()
|
||||
|
||||
def stop_detector(self) -> None:
|
||||
self.camStatusCmd.set("Idle").wait()
|
||||
|
||||
|
||||
# Subscribe and wait for update
|
||||
def isIdle(*args, old_value, value, timestamp, **kwargs):
|
||||
return bool(value==2)
|
||||
status = SubscriptionStatus(self.parent.camStatusCode, isIdle, settle_time=0.5)
|
||||
status.wait()
|
||||
|
||||
def send_data(self) -> None:
|
||||
"""Send data to monitor endpoint in redis."""
|
||||
try:
|
||||
img = self.parent.image.get()
|
||||
# pylint: disable=protected-access
|
||||
self.parent._run_subs(sub_type=self.parent.SUB_VALUE, value=img)
|
||||
except Exception as e:
|
||||
logger.debug(f"{e} for image with shape {self.parent.image.get().shape}")
|
||||
|
||||
def monitor_loop(self) -> None:
|
||||
"""
|
||||
Monitor the detector status and send data.
|
||||
"""
|
||||
while True:
|
||||
self.send_data()
|
||||
time.sleep(1 / self.update_frequency)
|
||||
if self.parent.state != "RUNNING":
|
||||
break
|
||||
if self.stop_monitor:
|
||||
break
|
||||
|
||||
def run_monitor(self) -> None:
|
||||
"""
|
||||
Run the monitor loop in a separate thread.
|
||||
"""
|
||||
self.monitor_thread = threading.Thread(target=self.monitor_loop, daemon=True)
|
||||
self.monitor_thread.start()
|
||||
|
||||
|
||||
|
||||
class HelgeCameraCore(PSIDetectorBase):
|
||||
"""Ophyd baseclass for Helge camera IOCs
|
||||
|
||||
This class provides wrappers for Helge's camera IOCs around SwissFEL and
|
||||
for high performance SLS 2.0 cameras. The IOC's operation is a bit arcane
|
||||
and there are different versions and cameras all around. So this device
|
||||
only covers the absolute basics.
|
||||
|
||||
Probably the most important part is the configuration state machine. As
|
||||
the SET_PARAMS takes care of buffer allocations it might take some time,
|
||||
as well as afull re-configuration is required every time we change the
|
||||
binning, roi, etc... This is automatically performed upon starting an
|
||||
exposure (if it heven't been done before).
|
||||
|
||||
The status flag state machine during re-configuration is:
|
||||
BUSY low, SET low -> BUSY high, SET low -> BUSY low, SET high -> BUSY low, SET low
|
||||
|
||||
"""
|
||||
# Specify Mixin class
|
||||
custom_prepare_cls = HelgeCameraMixin
|
||||
|
||||
|
||||
USER_ACCESS = ["kickoff"]
|
||||
# ########################################################################
|
||||
# General hardware info
|
||||
camType = Component(EpicsSignalRO, "QUERY", kind=Kind.omitted)
|
||||
camBoard = Component(EpicsSignalRO, "BOARD", kind=Kind.config)
|
||||
camError = Component(EpicsSignalRO, "ERRCODE", auto_monitor=True, kind=Kind.config)
|
||||
camWarning = Component(EpicsSignalRO, "WARNCODE", auto_monitor=True, kind=Kind.config)
|
||||
|
||||
# ########################################################################
|
||||
# Acquisition commands
|
||||
camStatusCmd = Component(EpicsSignal, "CAMERASTATUS", put_complete=True, kind=Kind.config)
|
||||
|
||||
# ########################################################################
|
||||
# Acquisition configuration
|
||||
acqExpTime = Component(EpicsSignalRO, "EXPOSURE", auto_monitor=True, kind=Kind.config)
|
||||
|
||||
# ########################################################################
|
||||
# Configuration state maschine with separate transition states
|
||||
camStatusCode = Component(EpicsSignalRO, "STATUSCODE", auto_monitor=True, kind=Kind.config)
|
||||
camSetParam = Component(EpicsSignal, "SET_PARAM", auto_monitor=True, kind=Kind.config)
|
||||
camSetParamBusy = Component(EpicsSignalRO, "BUSY_SET_PARAM", auto_monitor=True, kind=Kind.config)
|
||||
camCamera = Component(EpicsSignalRO, "CAMERA", auto_monitor=True, kind=Kind.config)
|
||||
camCameraBusy = Component(EpicsSignalRO, "BUSY_CAMERA", auto_monitor=True, kind=Kind.config)
|
||||
camInit= Component(EpicsSignalRO, "INIT", auto_monitor=True, kind=Kind.config)
|
||||
camInitBusy = Component(EpicsSignalRO, "BUSY_INIT", auto_monitor=True, kind=Kind.config)
|
||||
|
||||
# ########################################################################
|
||||
# Throtled image preview
|
||||
image = Component(EpicsSignalRO, "FPICTURE", kind=Kind.omitted)
|
||||
|
||||
# ########################################################################
|
||||
# Misc PVs
|
||||
#camRemoval = Component(EpicsSignalRO, "REMOVAL", auto_monitor=True, kind=Kind.config)
|
||||
camStateString = Component(EpicsSignalRO, "SS_CAMERA", string=True, auto_monitor=True, kind=Kind.config)
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
""" Single word camera state"""
|
||||
if self.camSetParamBusy.value:
|
||||
return "BUSY"
|
||||
if self.camStatusCode.value==2 and self.camInit.value==1:
|
||||
return "IDLE"
|
||||
if self.camStatusCode.value==6 and self.camInit.value==1:
|
||||
return "RUNNING"
|
||||
#if self.camRemoval.value==0 and self.camInit.value==0:
|
||||
if self.camInit.value==0:
|
||||
return "OFFLINE"
|
||||
#if self.camRemoval.value:
|
||||
# return "REMOVED"
|
||||
return "UNKNOWN"
|
||||
|
||||
@state.setter
|
||||
def state(self):
|
||||
raise ReadOnlyError("State is a ReadOnly property")
|
||||
|
||||
def configure(self, d: dict = {}) -> tuple:
|
||||
if self.state in ["OFFLINE", "REMOVED", "RUNNING"]:
|
||||
raise RuntimeError(f"Can't change configuration from state {self.state}")
|
||||
|
||||
def stage(self) -> list[object]:
|
||||
""" Start acquisition"""
|
||||
self.custom_prepare.arm_acquisition()
|
||||
return super().stage()
|
||||
|
||||
|
||||
def kickoff(self) -> DeviceStatus:
|
||||
""" Start acquisition"""
|
||||
return self.stage()
|
||||
|
||||
def stop(self):
|
||||
""" Stop the running acquisition """
|
||||
self.camStatusCmd.set("Idle").wait()
|
||||
self.custom_prepare.stop_monitor = True
|
||||
return super().unstage()
|
||||
|
||||
def unstage(self):
|
||||
""" Stop the running acquisition and unstage the device"""
|
||||
self.camStatusCmd.set("Idle").wait()
|
||||
self.custom_prepare.stop_monitor = True
|
||||
return super().unstage()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class HelgeCameraBase(HelgeCameraCore):
|
||||
"""Ophyd baseclass for Helge camera IOCs
|
||||
|
||||
This class provides wrappers for Helge's camera IOCs around SwissFEL and
|
||||
for high performance SLS 2.0 cameras. Theese are mostly PCO cameras running
|
||||
on a special Windows IOC host with lots of RAM and CPU power.
|
||||
|
||||
The IOC's operation is a bit arcane, and is documented on the "read the code"
|
||||
level. However the most important part is the state machine of 7+1 PV signals:
|
||||
INIT
|
||||
BUSY_INIT
|
||||
SET_PARAM
|
||||
BUSY_SET_PARAM
|
||||
CAMERA
|
||||
BUSY_CAMERA
|
||||
CAMERASTATUSCODE
|
||||
CAMERASTATUS
|
||||
"""
|
||||
|
||||
|
||||
|
||||
USER_ACCESS = ["describe", "shape", "bin", "roi"]
|
||||
# ########################################################################
|
||||
# Additional status info
|
||||
busy = Component(EpicsSignalRO, "BUSY", auto_monitor=True, kind=Kind.config)
|
||||
camState = Component(EpicsSignalRO, "SS_CAMERA", auto_monitor=True, kind=Kind.config)
|
||||
camProgress = Component(EpicsSignalRO, "CAMPROGRESS", auto_monitor=True, kind=Kind.config)
|
||||
camRate = Component(EpicsSignalRO, "CAMRATE", auto_monitor=True, kind=Kind.config)
|
||||
|
||||
# ########################################################################
|
||||
# Acquisition configuration
|
||||
acqMode = Component(EpicsSignalRO, "ACQMODE", auto_monitor=True, kind=Kind.config)
|
||||
acqDelay = Component(EpicsSignalRO, "DELAY", auto_monitor=True, kind=Kind.config)
|
||||
acqTriggerEna = Component(EpicsSignalRO, "TRIGGER", auto_monitor=True, kind=Kind.config)
|
||||
#acqTriggerSource = Component(EpicsSignalRO, "TRIGGERSOURCE", auto_monitor=True, kind=Kind.config)
|
||||
#acqTriggerEdge = Component(EpicsSignalRO, "TRIGGEREDGE", auto_monitor=True, kind=Kind.config)
|
||||
|
||||
# ########################################################################
|
||||
# Image size settings
|
||||
# Priority is: binning -> roi -> final size
|
||||
pxBinX = Component(EpicsSignal, "BINX", put_complete=True, auto_monitor=True, kind=Kind.config)
|
||||
pxBinY = Component(EpicsSignal, "BINY", put_complete=True, auto_monitor=True, kind=Kind.config)
|
||||
pxRoiX_lo = Component(EpicsSignal, "REGIONX_START", put_complete=True, auto_monitor=True, kind=Kind.config)
|
||||
pxRoiX_hi = Component(EpicsSignal, "REGIONX_END", put_complete=True, auto_monitor=True, kind=Kind.config)
|
||||
pxRoiY_lo = Component(EpicsSignal, "REGIONY_START", put_complete=True, auto_monitor=True, kind=Kind.config)
|
||||
pxRoiY_hi = Component(EpicsSignal, "REGIONY_END", put_complete=True, auto_monitor=True, kind=Kind.config)
|
||||
pxNumX = Component(EpicsSignalRO, "WIDTH", auto_monitor=True, kind=Kind.config)
|
||||
pxNumY = Component(EpicsSignalRO, "HEIGHT", auto_monitor=True, kind=Kind.config)
|
||||
|
||||
|
||||
# ########################################################################
|
||||
# Buffer configuration
|
||||
bufferRecMode = Component(EpicsSignalRO, "RECMODE", auto_monitor=True, kind=Kind.config)
|
||||
bufferStoreMode = Component(EpicsSignalRO, "STOREMODE", auto_monitor=True, kind=Kind.config)
|
||||
fileRecMode = Component(EpicsSignalRO, "RECMODE", auto_monitor=True, kind=Kind.config)
|
||||
|
||||
# ########################################################################
|
||||
# File interface
|
||||
camFileFormat = Component(EpicsSignal, "FILEFORMAT", put_complete=True, kind=Kind.config)
|
||||
camFilePath = Component(EpicsSignal, "FILEPATH", put_complete=True, kind=Kind.config)
|
||||
camFileName = Component(EpicsSignal, "FILENAME", put_complete=True, kind=Kind.config)
|
||||
camFileNr = Component(EpicsSignal, "FILENR", put_complete=True, kind=Kind.config)
|
||||
camFilePath = Component(EpicsSignal, "FILEPATH", put_complete=True, kind=Kind.config)
|
||||
camFileTransferStart = Component(EpicsSignal, "FTRANSFER", put_complete=True, kind=Kind.config)
|
||||
camFileTransferStop = Component(EpicsSignal, "SAVESTOP", put_complete=True, kind=Kind.config)
|
||||
|
||||
|
||||
|
||||
def configure(self, d: dict = {}) -> tuple:
|
||||
"""
|
||||
Camera configuration instructions:
|
||||
After setting the corresponding PVs, one needs to process SET_PARAM and wait until
|
||||
BUSY_SET_PARAM goes high and low, followed by SET_PARAM goes high and low. This will
|
||||
both send the settings to the camera and allocate the necessary buffers in the correct
|
||||
size and shape (that takes time). Starting the exposure with CAMERASTATUS will also
|
||||
call SET_PARAM, but it might take long.
|
||||
"""
|
||||
old = self.read_configuration()
|
||||
super().configure(d)
|
||||
|
||||
if "exptime" in d:
|
||||
exposure_time = d['exptime']
|
||||
if exposure_time is not None:
|
||||
self.acqExpTime.set(exposure_time).wait()
|
||||
|
||||
if "roi" in d:
|
||||
roi = d["roi"]
|
||||
if not isinstance(roi, (list, tuple)):
|
||||
raise ValueError(f"Unknown ROI data type {type(roi)}")
|
||||
if not len(roi[0])==2 and len(roi[1])==2:
|
||||
raise ValueError(f"Unknown ROI shape: {roi}")
|
||||
# Values are rounded to multiples of 16
|
||||
self.pxRoiX_lo.set(roi[0][0]).wait()
|
||||
self.pxRoiX_hi.set(roi[0][1]).wait()
|
||||
self.pxRoiY_lo.set(roi[1][0]).wait()
|
||||
self.pxRoiY_hi.set(roi[1][1]).wait()
|
||||
|
||||
if "bin" in d:
|
||||
binning = d["bin"]
|
||||
if not isinstance(binning, (list, tuple)):
|
||||
raise ValueError(f"Unknown BINNING data type {type(binning)}")
|
||||
if not len(binning)==2:
|
||||
raise ValueError(f"Unknown ROI shape: {binning}")
|
||||
self.pxBinX.set(binning[0]).wait()
|
||||
self.pxBinY.set(binning[1]).wait()
|
||||
|
||||
# State machine
|
||||
# Initial: BUSY and SET both low
|
||||
# 1. BUSY set to high
|
||||
# 2. BUSY goes low, SET goes high
|
||||
# 3. SET goes low
|
||||
self.camSetParam.set(1).wait()
|
||||
def risingEdge(*args, old_value, value, timestamp, **kwargs):
|
||||
return bool(not old_value and value)
|
||||
def fallingEdge(*args, old_value, value, timestamp, **kwargs):
|
||||
return bool(old_value and not value)
|
||||
# Subscribe and wait for update
|
||||
status = SubscriptionStatus(self.camSetParam, fallingEdge, settle_time=0.5)
|
||||
status.wait()
|
||||
new = self.read_configuration()
|
||||
return (old, new)
|
||||
|
||||
@property
|
||||
def shape(self):
|
||||
return (int(self.pxNumX.value), int(self.pxNumY.value))
|
||||
|
||||
@shape.setter
|
||||
def shape(self):
|
||||
raise ReadOnlyError("Shape is a ReadOnly property")
|
||||
|
||||
@property
|
||||
def bin(self):
|
||||
return (int(self.pxBinX.value), int(self.pxBinY.value))
|
||||
|
||||
@bin.setter
|
||||
def bin(self):
|
||||
raise ReadOnlyError("Bin is a ReadOnly property")
|
||||
|
||||
@property
|
||||
def roi(self):
|
||||
return ((int(self.pxRoiX_lo.value), int(self.pxRoiX_hi.value)), (int(self.pxRoiY_lo.value), int(self.pxRoiY_hi.value)))
|
||||
|
||||
@roi.setter
|
||||
def roi(self):
|
||||
raise ReadOnlyError("Roi is a ReadOnly property")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Automatically connect to test camera if directly invoked
|
||||
if __name__ == "__main__":
|
||||
|
||||
# Drive data collection
|
||||
cam = HelgeCameraBase("SINBC02-DSRM310:", name="mcpcam")
|
||||
cam.wait_for_connection()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@ from threading import Thread
|
||||
import requests
|
||||
|
||||
from ophyd import Device, Signal, Component, Kind, Staged
|
||||
from websockets.sync.client import connect
|
||||
from ophyd.status import SubscriptionStatus
|
||||
from ophyd.flyers import FlyerInterface
|
||||
from websockets.sync.client import connect, ClientConnection
|
||||
from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError
|
||||
|
||||
from ophyd_devices.interfaces.base_classes.psi_detector_base import PSIDetectorBase as PSIDeviceBase
|
||||
@@ -34,81 +36,64 @@ class StdDaqMixin(CustomDeviceMixin):
|
||||
NOTE: Tomcat might use multiple cameras with their own separate DAQ instances.
|
||||
"""
|
||||
# Fish out our configuration from scaninfo (via explicit or generic addressing)
|
||||
scanparam = self.parent.scaninfo.scan_msg.info
|
||||
alias = self.parent.parent.name if self.parent.parent is not None else self.parent.name
|
||||
# logger.warning(f"[{alias}] Scan parameters:\n{scanparam}")
|
||||
# NOTE: Scans don't have to fully configure the device
|
||||
d = {}
|
||||
if 'kwargs' in scanparam:
|
||||
scanargs = scanparam['kwargs']
|
||||
if 'kwargs' in self.parent.scaninfo.scan_msg.info:
|
||||
scanargs = self.parent.scaninfo.scan_msg.info['kwargs']
|
||||
if 'image_width' in scanargs and scanargs['image_width'] != None:
|
||||
d['image_width'] = scanargs['image_width']
|
||||
if 'image_height' in scanargs and scanargs['image_height'] != None:
|
||||
d['image_height'] = scanargs['image_height']
|
||||
# NOTE: Scans don't have to fully configure the device
|
||||
points_total = 1
|
||||
if 'steps' in scanargs and scanargs['steps'] != None:
|
||||
points_total *= scanargs['steps']
|
||||
if 'exp_burst' in scanargs and scanargs['exp_burst'] != None:
|
||||
points_total *= scanargs['exp_burst']
|
||||
if 'repeats' in scanargs and scanargs['repeats']!= None:
|
||||
points_total *= scanargs['repeats']
|
||||
if points_total != 1:
|
||||
d['num_points_total'] = points_total
|
||||
if 'nr_writers' in scanargs and scanargs['nr_writers'] != None:
|
||||
d['nr_writers'] = scanargs['nr_writers']
|
||||
if 'file_path' in scanargs and scanargs['file_path']!=None:
|
||||
self.parent.file_path.set(scanargs['file_path']).wait()
|
||||
|
||||
if "daq_num_points" in scanargs:
|
||||
d["num_points_total"] = scanargs["daq_num_points"]
|
||||
else:
|
||||
# Try to figure out number of points
|
||||
num_points = 1
|
||||
points_valid = False
|
||||
if "steps" in scanargs and scanargs['steps'] is not None:
|
||||
num_points *= scanargs["steps"]
|
||||
points_valid = True
|
||||
if "exp_burst" in scanargs and scanargs['exp_burst'] is not None:
|
||||
num_points *= scanargs["exp_burst"]
|
||||
points_valid = True
|
||||
if "repeats" in scanargs and scanargs['repeats'] is not None:
|
||||
num_points *= scanargs["repeats"]
|
||||
points_valid = True
|
||||
if points_valid:
|
||||
d["num_points_total"] = num_points
|
||||
|
||||
# Perform bluesky-style configuration
|
||||
if len(d) > 0:
|
||||
# Stop if current status is not idle
|
||||
if self.parent.state() != "idle":
|
||||
self.parent.surestop()
|
||||
# Configure new run (will restart the stdDAQ)
|
||||
logger.warning(f"[{self.parent.name}] Configuring with:\n{d}")
|
||||
logger.warning(f"[{self.parent.name}] stdDAQ needs reconfiguring with:\n{d}")
|
||||
self.parent.configure(d=d)
|
||||
# Wait for REST API to kill the DAQ
|
||||
sleep(0.5)
|
||||
|
||||
# Try to start a new run
|
||||
file_path = self.parent.file_path.get()
|
||||
num_images = self.parent.num_images.get()
|
||||
message = {"command": "start", "path": file_path, "n_image": num_images}
|
||||
ii = 0
|
||||
while True:
|
||||
self.parent.connect()
|
||||
reply = self.parent.message(message)
|
||||
|
||||
if reply is not None:
|
||||
reply = json.loads(reply)
|
||||
self.parent.status.set(reply["status"], force=True).wait()
|
||||
logger.info(f"[{self.parent.name}] Start DAQ reply: {reply}")
|
||||
# Give it more time to reconfigure
|
||||
if reply["status"] in ("rejected"):
|
||||
# FIXME: running exposure is a nogo
|
||||
if reply['reason'] == "gerhtrhjfjf":
|
||||
raise RuntimeError(f"[{self.parent.name}] Start StdDAQ command rejected: already running")
|
||||
else:
|
||||
# Give it more time to restart
|
||||
sleep(2)
|
||||
else:
|
||||
break
|
||||
ii += 1
|
||||
if ii == 5:
|
||||
break
|
||||
if reply is not None and reply["status"] in ("rejected"):
|
||||
raise RuntimeError(
|
||||
f"Start StdDAQ command rejected (might be already running): {reply['reason']}"
|
||||
)
|
||||
# Try to start a new run (reconnects)
|
||||
self.parent.bluestage()
|
||||
# And start status monitoring
|
||||
self._mon = Thread(target=self.poll, daemon=True)
|
||||
self._mon = Thread(target=self.monitor, daemon=True)
|
||||
self._mon.start()
|
||||
|
||||
def on_unstage(self):
|
||||
""" Stop a running acquisition and close connection
|
||||
"""
|
||||
self.parent.surestop()
|
||||
print("Creating virtual dataset")
|
||||
self.parent.create_virtual_dataset()
|
||||
self.parent.blueunstage()
|
||||
|
||||
def on_stop(self):
|
||||
""" Stop a running acquisition and close connection
|
||||
"""
|
||||
self.parent.surestop()
|
||||
self.parent.blueunstage()
|
||||
|
||||
def poll(self) -> None:
|
||||
def monitor(self) -> None:
|
||||
""" Monitor status messages while connection is open. This will block the reply monitoring
|
||||
to calling unstage() might throw. Status updates are sent every 1 seconds, but finishing
|
||||
acquisition means StdDAQ will close connection, so there's no idle state polling.
|
||||
@@ -117,8 +102,8 @@ class StdDaqMixin(CustomDeviceMixin):
|
||||
sleep(0.2)
|
||||
for msg in self.parent._wsclient:
|
||||
message = json.loads(msg)
|
||||
self.parent.status.put(message["status"], force=True)
|
||||
# logger.info(f"[{self.parent.name}] Pushed status: {message['status']}")
|
||||
self.parent.runstatus.put(message["status"], force=True)
|
||||
logger.info(f"[{self.parent.name}] Pushed status: {message['status']}")
|
||||
except (ConnectionClosedError, ConnectionClosedOK, AssertionError):
|
||||
# Libraty throws theese after connection is closed
|
||||
return
|
||||
@@ -132,11 +117,10 @@ class StdDaqMixin(CustomDeviceMixin):
|
||||
class StdDaqClient(PSIDeviceBase):
|
||||
"""StdDaq API
|
||||
|
||||
This class combines the new websocket and REST interfaces of the stdDAQ replaced the documented
|
||||
python client. The websocket interface starts and stops the acquisition and provides status,
|
||||
while the REST interface can read and write the JSON configuration file.
|
||||
|
||||
The DAQ needs to restart all services to reconfigure with a new config, which might corrupt
|
||||
This class combines the new websocket and REST interfaces of the stdDAQ. The websocket
|
||||
interface starts and stops the acquisition and provides status, while the REST interface
|
||||
can read and write the JSON configuration file. The stdDAQ needs to restart all services
|
||||
to reconfigure with a new config, which might corrupt
|
||||
the currently written files (fix is underway).
|
||||
|
||||
Example:
|
||||
@@ -146,22 +130,22 @@ class StdDaqClient(PSIDeviceBase):
|
||||
"""
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
custom_prepare_cls = StdDaqMixin
|
||||
USER_ACCESS = ["set_daq_config", "get_daq_config", "surestop", "nuke", "connect", "message", "state"]
|
||||
USER_ACCESS = ["set_daq_config", "get_daq_config", "nuke", "connect", "message", "state", "bluestage", "blueunstage"]
|
||||
_wsclient = None
|
||||
|
||||
# Status attributes
|
||||
ws_url = Component(Signal, kind=Kind.config)
|
||||
status = Component(Signal, value="unknown", kind=Kind.normal)
|
||||
ws_url = Component(Signal, kind=Kind.config, metadata={'write_access': False})
|
||||
runstatus = Component(Signal, value="unknown", kind=Kind.normal, metadata={'write_access': False})
|
||||
num_images = Component(Signal, value=10000, kind=Kind.config)
|
||||
file_path = Component(Signal, value="/gpfs/test/test-beamline", kind=Kind.config)
|
||||
# Configuration attributes
|
||||
rest_url = Component(Signal, kind=Kind.config)
|
||||
rest_url = Component(Signal, kind=Kind.config, metadata={'write_access': False})
|
||||
cfg_detector_name = Component(Signal, kind=Kind.config)
|
||||
cfg_detector_type = Component(Signal, kind=Kind.config)
|
||||
cfg_bit_depth = Component(Signal, kind=Kind.config)
|
||||
cfg_pixel_height = Component(Signal, kind=Kind.config)
|
||||
cfg_pixel_width = Component(Signal, kind=Kind.config)
|
||||
|
||||
cfg_nr_writers = Component(Signal, kind=Kind.config)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -173,32 +157,23 @@ class StdDaqClient(PSIDeviceBase):
|
||||
configuration_attrs=None,
|
||||
parent=None,
|
||||
device_manager=None,
|
||||
sim_mode=False,
|
||||
ws_url: str = "ws://localhost:8080",
|
||||
rest_url: str = "http://localhost:5000",
|
||||
data_source_name = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
super().__init__(prefix=prefix, name=name, kind=kind, read_attrs=read_attrs, configuration_attrs=configuration_attrs, parent=parent, device_manager=device_manager, **kwargs)
|
||||
self.status._metadata["write_access"] = False
|
||||
self.ws_url._metadata["write_access"] = False
|
||||
self.ws_url.set(ws_url, force=True).wait()
|
||||
self.rest_url._metadata["write_access"] = False
|
||||
self.rest_url.set(rest_url, force=True).wait()
|
||||
self.data_source_name = data_source_name
|
||||
|
||||
# Connect ro the DAQ and initialize values
|
||||
# Connect to the DAQ and initialize values
|
||||
try:
|
||||
self.read_daq_config()
|
||||
self.get_daq_config(update=True)
|
||||
except Exception as ex:
|
||||
logger.error(f"Failed to connect to the stdDAQ REST API\n{ex}")
|
||||
|
||||
def __del__(self) -> None:
|
||||
try:
|
||||
self._wsclient.close()
|
||||
except TypeError:
|
||||
pass
|
||||
return super().__del__()
|
||||
|
||||
def connect(self):
|
||||
def connect(self) -> ClientConnection:
|
||||
"""Connect to the StdDAQ's websockets interface
|
||||
|
||||
StdDAQ may reject connection for a few seconds after restart, or when
|
||||
@@ -207,16 +182,14 @@ class StdDaqClient(PSIDeviceBase):
|
||||
num_retry = 0
|
||||
while num_retry < 5:
|
||||
try:
|
||||
logger.debug(f"[{self.name}] Connecting to {self.ws_url.get()}")
|
||||
self._wsclient = connect(self.ws_url.get())
|
||||
break
|
||||
logger.debug(f"[{self.name}] Connecting to stdDAQ at {self.ws_url.get()}")
|
||||
connection = connect(self.ws_url.get())
|
||||
logger.debug(f"[{self.name}] Connected to stdDAQ after {num_retry} tries")
|
||||
return connection
|
||||
except ConnectionRefusedError:
|
||||
num_retry += 1
|
||||
sleep(3)
|
||||
if num_retry == 5:
|
||||
raise ConnectionRefusedError(
|
||||
"The stdDAQ websocket interface refused connection 5 times.")
|
||||
logger.debug(f"[{self.name}] Connected to DAQ after {num_retry} tries")
|
||||
sleep(2)
|
||||
raise ConnectionRefusedError("The stdDAQ websocket interface refused connection 5 times.")
|
||||
|
||||
def message(self, message: dict, timeout=1, wait_reply=True, client=None) -> None | str:
|
||||
"""Send a message to the StdDAQ and receive a reply
|
||||
@@ -224,9 +197,12 @@ class StdDaqClient(PSIDeviceBase):
|
||||
Note: finishing acquisition means StdDAQ will close connection, so
|
||||
there's no idle state polling.
|
||||
"""
|
||||
# Prepare message
|
||||
msg = json.dumps(message) if isinstance(message, dict) else str(message)
|
||||
|
||||
# Connect if client was destroyed
|
||||
if self._wsclient is None:
|
||||
self.connect()
|
||||
self._wsclient = self.connect()
|
||||
|
||||
# Send message (reopen connection if needed)
|
||||
msg = json.dumps(message) if isinstance(message, dict) else str(message)
|
||||
@@ -234,7 +210,7 @@ class StdDaqClient(PSIDeviceBase):
|
||||
self._wsclient.send(msg)
|
||||
except (ConnectionClosedError, ConnectionClosedOK, AttributeError) as ex:
|
||||
# Re-connect if the connection was closed
|
||||
self.connect()
|
||||
self._wsclient = self.connect()
|
||||
self._wsclient.send(msg)
|
||||
|
||||
# Wait for reply
|
||||
@@ -243,70 +219,167 @@ class StdDaqClient(PSIDeviceBase):
|
||||
try:
|
||||
reply = self._wsclient.recv(timeout)
|
||||
return reply
|
||||
except (ConnectionClosedError, ConnectionClosedOK, TimeoutError, RuntimeError) as ex:
|
||||
except (ConnectionClosedError, ConnectionClosedOK) as ex:
|
||||
self._wsclient = None
|
||||
logger.error(f"[{self.name}] WS connection was closed before reply: {ex}")
|
||||
except (TimeoutError, RuntimeError) as ex:
|
||||
logger.error(f"[{self.name}] Error in receiving ws reply: {ex}")
|
||||
return reply
|
||||
|
||||
def configure(self, d: dict = None):
|
||||
"""Configure the next scan with the stdDAQ
|
||||
|
||||
Parameters as 'd' dictionary
|
||||
Parameters as 'd' dictionary, the default is unchanged.
|
||||
----------------------------
|
||||
num_points_total : 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)
|
||||
pixel_width : int, optional
|
||||
ROI size in the x-direction [pixels] (default = 2016)
|
||||
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
|
||||
(default = 0)
|
||||
trigger_mode : str, optional
|
||||
Trigger mode of the gifafrost
|
||||
(default = unchanged)
|
||||
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
|
||||
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).
|
||||
file_path: str, optional
|
||||
File path to save the data, usually GPFS.
|
||||
image_width : int, optional
|
||||
ROI size in the x-direction [pixels].
|
||||
image_height : int, optional
|
||||
ROI size in the y-direction [pixels].
|
||||
bit_depth: int, optional
|
||||
Image bit depth for cameras that can change it [int].
|
||||
nr_writers: int, optional
|
||||
Number of writers [int].
|
||||
"""
|
||||
|
||||
if 'image_width' in d:
|
||||
# Configuration parameters
|
||||
if 'image_width' in d and d['image_width']!=None:
|
||||
self.cfg_pixel_width.set(d['image_width']).wait()
|
||||
if 'image_height' in d:
|
||||
if 'image_height' in d and d['image_height']!=None:
|
||||
self.cfg_pixel_height.set(d['image_height']).wait()
|
||||
if 'bit_depth' in d:
|
||||
self.cfg_bit_depth.set(d['bit_depth']).wait()
|
||||
if 'nr_writers' in d and d['nr_writers']!=None:
|
||||
self.cfg_nr_writers.set(d['nr_writers']).wait()
|
||||
# Run parameters
|
||||
if 'num_points_total' in d:
|
||||
self.num_images.set(d['num_points_total']).wait()
|
||||
if 'file_path' in d:
|
||||
if 'file_path' in d and d['file_path']!=None:
|
||||
self.file_path.set(d['file_path']).wait()
|
||||
|
||||
# Restart the DAQ if resolution changed
|
||||
cfg = self.get_daq_config()
|
||||
if cfg['image_pixel_height'] != self.cfg_pixel_height.get() or cfg['image_pixel_width'] != self.cfg_pixel_width.get():
|
||||
if cfg['image_pixel_height'] != self.cfg_pixel_height.get() or \
|
||||
cfg['image_pixel_width'] != self.cfg_pixel_width.get() or \
|
||||
cfg['bit_depth'] != self.cfg_bit_depth.get() or \
|
||||
cfg['number_of_writers'] != self.cfg_nr_writers.get():
|
||||
|
||||
# Stop if current status is not idle
|
||||
if self.state() != "idle":
|
||||
self.surestop()
|
||||
raise RuntimeWarning(f"[{self.name}] stdDAQ reconfiguration might corrupt files")
|
||||
|
||||
# Stop running acquisition
|
||||
self.unstage()
|
||||
# Update retrieved config
|
||||
cfg['image_pixel_height'] = int(self.cfg_pixel_height.get())
|
||||
cfg['image_pixel_width'] = int(self.cfg_pixel_width.get())
|
||||
cfg['bit_depth'] = int(self.cfg_bit_depth.get())
|
||||
cfg['number_of_writers'] = int(self.cfg_nr_writers.get())
|
||||
self.set_daq_config(cfg)
|
||||
self.read_daq_config()
|
||||
sleep(1)
|
||||
self.get_daq_config(update=True)
|
||||
|
||||
def get_daq_config(self) -> dict:
|
||||
def bluestage(self):
|
||||
""" Stages the stdDAQ
|
||||
|
||||
Opens a new connection to the stdDAQ, sends the start command with
|
||||
the current configuration. It waits for the first reply and checks
|
||||
it for obvious failures.
|
||||
"""
|
||||
# Can't stage into a running exposure
|
||||
print('Before')
|
||||
if self.state() != 'idle':
|
||||
raise RuntimeError(f"[{self.name}] stdDAQ can't stage from state: {self.state()}")
|
||||
print('After')
|
||||
|
||||
# Must make sure that image size matches the data source
|
||||
if self.data_source_name is not None:
|
||||
cam_img_w = self.device_manager.devices[self.data_source_name].cfgRoiX.get()
|
||||
cam_img_h = self.device_manager.devices[self.data_source_name].cfgRoiY.get()
|
||||
daq_img_w = self.cfg_pixel_width.get()
|
||||
daq_img_h = self.cfg_pixel_height.get()
|
||||
|
||||
if not (daq_img_w == cam_img_w and daq_img_h == cam_img_h):
|
||||
raise RuntimeError(f"[{self.name}] stdDAQ image resolution ({daq_img_w} , {daq_img_h}) does not match camera with ({cam_img_w} , {cam_img_h})")
|
||||
else:
|
||||
logger.warning(f"[{self.name}] stdDAQ image resolution ({daq_img_w} , {daq_img_h}) matches camera with ({cam_img_w} , {cam_img_h})")
|
||||
|
||||
|
||||
file_path = self.file_path.get()
|
||||
num_images = self.num_images.get()
|
||||
|
||||
# New connection
|
||||
self._wsclient = self.connect()
|
||||
message = {"command": "start", "path": file_path, "n_image": num_images, }
|
||||
reply = self.message(message)
|
||||
|
||||
if reply is not None:
|
||||
reply = json.loads(reply)
|
||||
self.runstatus.set(reply["status"], force=True).wait()
|
||||
logger.info(f"[{self.name}] Start DAQ reply: {reply}")
|
||||
|
||||
# Give it more time to reconfigure
|
||||
if reply["status"] in ("rejected"):
|
||||
# FIXME: running exposure is a nogo
|
||||
if reply['reason'] == "driver is busy!":
|
||||
raise RuntimeError(f"[{self.name}] Start stdDAQ command rejected: already running")
|
||||
else:
|
||||
# Give it more time to consolidate
|
||||
sleep(1)
|
||||
else:
|
||||
# Success!!!
|
||||
print(f"[{self.name}] Started stdDAQ in: {reply['status']}")
|
||||
return
|
||||
|
||||
raise RuntimeError(f"[{self.name}] Failed to start the stdDAQ in 1 tries, reason: {reply['reason']}")
|
||||
|
||||
def blueunstage(self):
|
||||
""" Unstages the stdDAQ
|
||||
|
||||
Opens a new connection to the stdDAQ, sends the stop command and
|
||||
waits for the idle state.
|
||||
"""
|
||||
ii = 0
|
||||
while ii<10:
|
||||
# Stop the DAQ (will close connection) - reply is always "success"
|
||||
self._wsclient = self.connect()
|
||||
self.message({"command": "stop_all"}, wait_reply=False)
|
||||
|
||||
# Let it consolidate
|
||||
sleep(0.2)
|
||||
|
||||
# Check final status (from new connection)
|
||||
self._wsclient = self.connect()
|
||||
reply = self.message({"command": "status"})
|
||||
if reply is not None:
|
||||
logger.info(f"[{self.name}] DAQ status reply: {reply}")
|
||||
reply = json.loads(reply)
|
||||
|
||||
if reply["status"] in ("idle", "error"):
|
||||
# Only 'idle' state accepted
|
||||
print(f"DAQ stopped on try {ii}")
|
||||
return
|
||||
elif reply["status"] in ("stop"):
|
||||
# Give it more time to stop
|
||||
sleep(0.5)
|
||||
elif ii >= 6:
|
||||
raise RuntimeError(f"Failed to stop StdDAQ: {reply}")
|
||||
ii += 1
|
||||
raise RuntimeError(f"Failed to stop StdDAQ in time")
|
||||
|
||||
##########################################################################
|
||||
# Bluesky flyer interface
|
||||
def complete(self) -> SubscriptionStatus:
|
||||
"""Wait for current run. Must end in status 'file_saved'."""
|
||||
def is_running(*args, value, timestamp, **kwargs):
|
||||
result = value in ["idle", "file_saved", "error"]
|
||||
return result
|
||||
|
||||
status = SubscriptionStatus(self.runstatus, is_running, settle_time=0.5)
|
||||
return status
|
||||
|
||||
def get_daq_config(self, update=False) -> dict:
|
||||
"""Read the current configuration from the DAQ
|
||||
"""
|
||||
r = requests.get(
|
||||
@@ -315,20 +388,18 @@ class StdDaqClient(PSIDeviceBase):
|
||||
timeout=2)
|
||||
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) -> dict:
|
||||
"""Read the current configuration from the DAQ and update the ophyd device
|
||||
"""
|
||||
cfg = self.get_daq_config()
|
||||
self.cfg_detector_name.set(cfg['detector_name']).wait()
|
||||
self.cfg_detector_type.set(cfg['detector_type']).wait()
|
||||
self.cfg_bit_depth.set(cfg['bit_depth']).wait()
|
||||
self.cfg_pixel_height.set(cfg['image_pixel_height']).wait()
|
||||
self.cfg_pixel_width.set(cfg['image_pixel_width']).wait()
|
||||
if update:
|
||||
self.cfg_detector_name.set(cfg['detector_name']).wait()
|
||||
self.cfg_detector_type.set(cfg['detector_type']).wait()
|
||||
self.cfg_bit_depth.set(cfg['bit_depth']).wait()
|
||||
self.cfg_pixel_height.set(cfg['image_pixel_height']).wait()
|
||||
self.cfg_pixel_width.set(cfg['image_pixel_width']).wait()
|
||||
self.cfg_nr_writers.set(cfg['number_of_writers']).wait()
|
||||
return cfg
|
||||
|
||||
def set_daq_config(self, config):
|
||||
def set_daq_config(self, config, settle_time=1):
|
||||
"""Write a full configuration to the DAQ
|
||||
"""
|
||||
url = self.rest_url.get() + '/api/config/set'
|
||||
@@ -341,63 +412,50 @@ class StdDaqClient(PSIDeviceBase):
|
||||
)
|
||||
if r.status_code != 200:
|
||||
raise ConnectionError(f"[{self.name}] Error {r.status_code}:\t{r.text}")
|
||||
# Wait for service to restart (and connect to make sure)
|
||||
sleep(settle_time)
|
||||
self.connect()
|
||||
return r.json()
|
||||
|
||||
def nuke(self):
|
||||
""" Reconfigures the stdDAQ to restart the services
|
||||
def create_virtual_dataset(self):
|
||||
"""Combine the stddaq written files in a given folder in an interleaved
|
||||
h5 virtual dataset
|
||||
"""
|
||||
url = self.rest_url.get() + '/api/h5/create_interleaved_vds'
|
||||
file_path = self.file_path.get()
|
||||
|
||||
r = requests.post(
|
||||
url,
|
||||
params = {'user': 'ioc'},
|
||||
data = {'base_path': file_path, 'output_file': 'fede_virtual_test'},
|
||||
timeout = 2,
|
||||
headers = {'Content-type': 'application/json'}
|
||||
)
|
||||
print(r)
|
||||
print(file_path)
|
||||
|
||||
def nuke(self, restarttime=5):
|
||||
""" Reconfigures the stdDAQ to restart the services. This causes
|
||||
systemd to kill the current DAQ service and restart it with the same
|
||||
configuration. Which might corrupt the currently written file...
|
||||
"""
|
||||
cfg = self.get_daq_config()
|
||||
self.set_daq_config(cfg)
|
||||
sleep(restarttime)
|
||||
|
||||
def state(self) -> str | None:
|
||||
""" Querry the current system state"""
|
||||
r = self.message({'command': 'status'}, wait_reply=True)
|
||||
if r is None:
|
||||
return None
|
||||
else:
|
||||
""" Querry the current system status"""
|
||||
try:
|
||||
wsclient = self.connect()
|
||||
wsclient.send(json.dumps({'command': 'status'}))
|
||||
r = wsclient.recv(timeout=1)
|
||||
r = json.loads(r)
|
||||
return r['status']
|
||||
|
||||
def surestop(self, timeout=5):
|
||||
""" Stops a running acquisition
|
||||
|
||||
REST reconfiguration restarts with systemd and can corrupt currently written files.
|
||||
"""
|
||||
# Retries to steal connection from poller
|
||||
for rr in range(5):
|
||||
client = connect(self.ws_url.get())
|
||||
msg = json.dumps({"command": "stop_all"})
|
||||
client.send(msg)
|
||||
reply = client.recv(timeout=1)
|
||||
reply = json.loads(reply)
|
||||
# logger.warning(reply)
|
||||
# if r['status'] == 'success':
|
||||
# break
|
||||
|
||||
sleep(0.5)
|
||||
|
||||
client = connect(self.ws_url.get())
|
||||
msg = json.dumps({"command": "status"})
|
||||
client.send(msg)
|
||||
reply = client.recv(timeout=1)
|
||||
reply = json.loads(reply)
|
||||
|
||||
if reply['status'] in ['idle', 'stoped']:
|
||||
logger.warning(f"[{self.name}] Stop-all command finished in {reply['status']}")
|
||||
return
|
||||
|
||||
# If stop_all didn't stop, nuke the whole thing
|
||||
logger.error(f"[{self.name}] Don't stop, make it rock!!!")
|
||||
self.nuke()
|
||||
sleep(timeout)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
except ConnectionRefusedError:
|
||||
raise
|
||||
|
||||
|
||||
# Automatically connect to microXAS testbench if directly invoked
|
||||
if __name__ == "__main__":
|
||||
daq = StdDaqClient(name="daq", ws_url="ws://xbl-daq-29:8080", rest_url="http://xbl-daq-29:5000")
|
||||
daq = StdDaqClient(name="daq", ws_url="ws://sls-daq-001:8080", rest_url="http://sls-daq-001:5000")
|
||||
daq.wait_for_connection()
|
||||
|
||||
@@ -102,6 +102,7 @@ class StdDaqPreviewMixin(CustomDetectorMixin):
|
||||
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._last_image = image
|
||||
self.parent._run_subs(sub_type=self.parent.SUB_MONITOR, value=image)
|
||||
t_last = t_curr
|
||||
logger.info(
|
||||
@@ -134,7 +135,7 @@ class StdDaqPreviewDetector(PSIDetectorBase):
|
||||
cam_widget = gui.add_dock('cam_dock1').add_widget('BECFigure').image('daq_stream1')
|
||||
"""
|
||||
# Subscriptions for plotting image
|
||||
USER_ACCESS = ["kickoff"]
|
||||
USER_ACCESS = ["kickoff", "get_last_image"]
|
||||
SUB_MONITOR = "device_monitor_2d"
|
||||
_default_sub = SUB_MONITOR
|
||||
|
||||
@@ -147,7 +148,8 @@ class StdDaqPreviewDetector(PSIDetectorBase):
|
||||
frame = Component(Signal, kind=Kind.hinted)
|
||||
image_shape = Component(Signal, kind=Kind.normal)
|
||||
# FIXME: The BEC client caches the read()s from the last 50 scans
|
||||
image = Component(Signal, kind=Kind.config)
|
||||
image = Component(Signal, kind=Kind.omitted)
|
||||
_last_image = None
|
||||
|
||||
def __init__(
|
||||
self, *args, url: str = "tcp://129.129.95.38:20000", parent: Device = None, **kwargs
|
||||
@@ -180,6 +182,9 @@ class StdDaqPreviewDetector(PSIDetectorBase):
|
||||
sleep(1)
|
||||
self._socket.connect(self.url.get())
|
||||
|
||||
def get_image(self):
|
||||
return self._last_image
|
||||
|
||||
def kickoff(self) -> DeviceStatus:
|
||||
""" The DAQ was not meant to be toggled"""
|
||||
return DeviceStatus(self, done=True, success=True, settle_time=0.1)
|
||||
|
||||
@@ -7,16 +7,30 @@ from bec_server.scan_server.scans import Acquire, AsyncFlyScanBase
|
||||
|
||||
class AcquireDark(Acquire):
|
||||
scan_name = "acquire_dark"
|
||||
required_kwargs = ["num"]
|
||||
gui_config = {"Acquisition parameters": ["num"]}
|
||||
required_kwargs = ["exp_burst"]
|
||||
gui_config = {"Acquisition parameters": ["exp_burst"]}
|
||||
|
||||
def __init__(self, num: int, **kwargs):
|
||||
def __init__(self, exp_burst: int, **kwargs):
|
||||
"""
|
||||
Acquire a dark image. This scan is used to acquire a dark image. The dark image is an image taken with the shutter
|
||||
closed and no beam on the sample. The dark image is used to correct the data images for dark current.
|
||||
Acquire dark images. This scan is used to acquire dark images. Dark images are images taken with the shutter
|
||||
closed and no beam on the sample. Dark images are used to correct the data images for dark current.
|
||||
|
||||
Args:
|
||||
num (int): number of dark images to acquire
|
||||
|
||||
exp_burst : int
|
||||
Number of dark images to acquire (no default)
|
||||
exp_time : float, optional
|
||||
Exposure time [ms]. If not specified, the currently configured value on the camera will be used
|
||||
exp_period : float, optional
|
||||
Exposure period [ms]
|
||||
image_width : int, optional
|
||||
ROI size in the x-direction [pixels]
|
||||
image_height : int, optional
|
||||
ROI size in the y-direction [pixels]
|
||||
acq_mode : str, optional
|
||||
Predefined acquisition mode (default=)
|
||||
file_path : str, optional
|
||||
File path for standard daq
|
||||
|
||||
Returns:
|
||||
ScanReport
|
||||
@@ -26,7 +40,7 @@ class AcquireDark(Acquire):
|
||||
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
self.burst_at_each_point = num
|
||||
self.burst_at_each_point = 1 # At each point, how many times I want to individually trigger
|
||||
self.scan_motors = ["eyex"] # change to the correct shutter device
|
||||
#self.shutter = "eyex" # change to the correct shutter device
|
||||
self.dark_shutter_pos = 0 ### change with a variable
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
|
||||
def dev_disable_all():
|
||||
"""Disable all devices """
|
||||
for d in dev:
|
||||
d.enabled = False
|
||||
for k in dev:
|
||||
dev[k].enabled = False
|
||||
|
||||
|
||||
|
||||
@@ -41,12 +41,12 @@ def anotherstepscan(
|
||||
--------
|
||||
demostepscan(scan_start=-32, scan_end=148, steps=180, exp_time=0.005, exp_burst=5)
|
||||
"""
|
||||
if not bl_check_beam():
|
||||
raise RuntimeError("Beamline is not in ready state")
|
||||
|
||||
dev.es1_tasks.enabled = False
|
||||
dev.es1_psod.enabled = False
|
||||
dev.es1_ddaq.enabled = True
|
||||
# if not bl_check_beam():
|
||||
# raise RuntimeError("Beamline is not in ready state")
|
||||
|
||||
dev_disable_all()
|
||||
dev.es1_roty.enabled = True
|
||||
#dev.es1_ddaq.enabled = True
|
||||
dev.gfcam.enabled = True
|
||||
dev.gfdaq.enabled = True
|
||||
dev.daq_stream0.enabled = True
|
||||
|
||||
@@ -1,61 +1,163 @@
|
||||
def fede_darks(nimages_dark, exposure_time=None, exposure_period=None, roix=None, roiy=None, acq_mode=None):
|
||||
import os.path
|
||||
|
||||
class Measurement:
|
||||
"""
|
||||
Acquire a set of dark images with shutters closed.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
nimages_dark : int
|
||||
Number of dark images to acquire (no default)
|
||||
exposure_time : float, optional
|
||||
Exposure time [ms]. If not specified, the currently configured value on the camera will be used
|
||||
exposure_period : float, optional
|
||||
Exposure period [ms]
|
||||
roix : int, optional
|
||||
ROI size in the x-direction [pixels]
|
||||
roiy : int, optional
|
||||
ROI size in the y-direction [pixels]
|
||||
acq_mode : str, optional
|
||||
Predefined acquisition mode (default=)
|
||||
|
||||
|
||||
|
||||
Example:
|
||||
--------
|
||||
tutorialdarks(num=100, exp_time=5)
|
||||
This class provides a standard set of tomographic measurement functions
|
||||
that can be used to acquire data at the TOMCAT beamline
|
||||
"""
|
||||
dev.es1_tasks.enabled = False
|
||||
dev.es1_psod.enabled = False
|
||||
dev.es1_ddaq.enabled = False
|
||||
dev.es1_ismc.enabled = False
|
||||
dev.es1_roty.enabled = False
|
||||
dev.gfcam.enabled = True
|
||||
dev.gfdaq.enabled = True
|
||||
dev.daq_stream0.enabled = True
|
||||
dev.daq_stream1.enabled = False
|
||||
def __init__(self):
|
||||
self.sample_name = 'tmp'
|
||||
self.data_path = 'disk_test'
|
||||
self.nimages = 1000
|
||||
self.nimages_dark = 50
|
||||
self.nimages_white = 100
|
||||
|
||||
# To be able to keep what is already set on the camera
|
||||
self.exposure_time = None
|
||||
self.exposure_period = None
|
||||
self.roix = None
|
||||
self.roiy = None
|
||||
|
||||
bec.system_config.file_suffix = self.sample_name
|
||||
bec.system_config.file_directory = os.path.join(self.data_path,self.sample_name)
|
||||
self.build_filename()
|
||||
|
||||
def build_filename(self):
|
||||
"""
|
||||
Build and set filename for bec and stddaq
|
||||
"""
|
||||
bec.system_config.file_suffix = self.sample_name
|
||||
bec.system_config.file_directory = os.path.join(self.data_path,self.sample_name)
|
||||
self.file_path = '/data/test/test-beamline/test_fede'
|
||||
if os.path.isdir(self.file_path):
|
||||
pass
|
||||
else:
|
||||
os.mkdir(self.file_path)
|
||||
|
||||
def configure(self,sample_name=None, data_path=None, exposure_time=None,
|
||||
exposure_period=None, roix=None, roiy=None,nimages=None,
|
||||
nimages_dark=None, nimages_white=None):
|
||||
"""
|
||||
Reconfigure the measurement with any number of new parameter
|
||||
|
||||
Parameters
|
||||
----------
|
||||
sample_name : string, optional
|
||||
Name of the sample or measurement. This name will be used to construct
|
||||
the name of the measurement directory (default=None)
|
||||
data_path : string, optional
|
||||
Information used to build the data directory for the measurement
|
||||
(default=None)
|
||||
exposure_time : float, optional
|
||||
Exposure time [ms] (default=None)
|
||||
exposure_period : float, optional
|
||||
Exposure period [ms] (default=None)
|
||||
roix : int, optional
|
||||
ROI size in the x-direction [pixels] (default=None)
|
||||
roiy : int, optional
|
||||
ROI size in the y-direction [pixels] (default=None)
|
||||
nimages : int, optional
|
||||
Number of images to acquire (default=None)
|
||||
nimages_dark : int, optional
|
||||
Number of dark images to acquire (default=None)
|
||||
nimages_white : int, optional
|
||||
Number of white images to acquire (default=None)
|
||||
"""
|
||||
|
||||
if sample_name != None:
|
||||
self.sample_name = sample_name
|
||||
if data_path != None:
|
||||
self.data_path = data_path
|
||||
if nimages != None:
|
||||
self.nimages = nimages
|
||||
if nimages_dark != None:
|
||||
self.nimages_dark = nimages_dark
|
||||
if nimages_white != None:
|
||||
self.nimages_white = nimages_white
|
||||
if exposure_time != None:
|
||||
self.exposure_time = exposure_time
|
||||
if exposure_period != None:
|
||||
self.exposure_period = exposure_period
|
||||
if roix != None:
|
||||
self.roix = roix
|
||||
if roiy != None:
|
||||
self.roiy = roiy
|
||||
|
||||
self.build_filename()
|
||||
|
||||
def acquire_darks(self,nimages_dark, exposure_time=None, exposure_period=None,
|
||||
roix=None, roiy=None, acq_mode=None, file_path=None):
|
||||
"""
|
||||
Acquire a set of dark images with shutters closed.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
nimages_dark : int
|
||||
Number of dark images to acquire (no default)
|
||||
exposure_time : float, optional
|
||||
Exposure time [ms]. If not specified, the currently configured value on the camera will be used
|
||||
exposure_period : float, optional
|
||||
Exposure period [ms]
|
||||
roix : int, optional
|
||||
ROI size in the x-direction [pixels]
|
||||
roiy : int, optional
|
||||
ROI size in the y-direction [pixels]
|
||||
acq_mode : str, optional
|
||||
Predefined acquisition mode (default=None)
|
||||
file_path : str, optional
|
||||
File path for standard daq (default=None)
|
||||
|
||||
Example:
|
||||
--------
|
||||
fede_darks(100, exposure_time=5)
|
||||
"""
|
||||
# dev.es1_tasks.enabled = False
|
||||
# dev.es1_psod.enabled = False
|
||||
# dev.es1_ddaq.enabled = False
|
||||
# dev.es1_ismc.enabled = False
|
||||
# dev.es1_roty.enabled = False
|
||||
dev.gfcam.enabled = True
|
||||
dev.gfdaq.enabled = True
|
||||
dev.daq_stream0.enabled = True
|
||||
dev.daq_stream1.enabled = False
|
||||
|
||||
|
||||
dev.gfcam.cfgAcqMode.set(1).wait()
|
||||
dev.gfcam.cmdSetParam.set(1).wait()
|
||||
dev.gfcam.cfgEnableExt.set(0).wait()
|
||||
dev.gfcam.cfgEnableSoft.set(0).wait()
|
||||
dev.gfcam.cfgEnableAlways.set(1).wait()
|
||||
dev.gfcam.cfgAcqMode.set(1).wait()
|
||||
dev.gfcam.cmdSetParam.set(1).wait()
|
||||
dev.gfcam.cfgEnableExt.set(0).wait()
|
||||
dev.gfcam.cfgEnableSoft.set(0).wait()
|
||||
dev.gfcam.cfgEnableAlways.set(1).wait()
|
||||
|
||||
dev.gfcam.cfgTrigExt.set(0).wait()
|
||||
dev.gfcam.cfgTrigSoft.set(0).wait()
|
||||
dev.gfcam.cfgTrigTimer.set(0).wait()
|
||||
dev.gfcam.cfgTrigAuto.set(1).wait()
|
||||
dev.gfcam.cfgTrigExt.set(0).wait()
|
||||
dev.gfcam.cfgTrigSoft.set(0).wait()
|
||||
dev.gfcam.cfgTrigTimer.set(0).wait()
|
||||
dev.gfcam.cfgTrigAuto.set(1).wait()
|
||||
|
||||
dev.gfcam.cfgExpExt.set(0).wait()
|
||||
dev.gfcam.cfgExpSoft.set(0).wait()
|
||||
dev.gfcam.cfgExpTimer.set(1).wait()
|
||||
dev.gfcam.cfgExpExt.set(0).wait()
|
||||
dev.gfcam.cfgExpSoft.set(0).wait()
|
||||
dev.gfcam.cfgExpTimer.set(1).wait()
|
||||
|
||||
dev.gfcam.cfgCntStartBit.set(0).wait()
|
||||
dev.gfcam.cfgCntStartBit.set(0).wait()
|
||||
|
||||
# Commit changes to GF
|
||||
dev.gfcam.cmdSetParam.set(1).wait()
|
||||
# Commit changes to GF
|
||||
dev.gfcam.cmdSetParam.set(1).wait()
|
||||
|
||||
### TODO: camera reset
|
||||
print("Handing over to 'scans.acquire_dark")
|
||||
scans.acquire_dark(num=1, exp_burst=nimages_dark, exp_time=exposure_time, exp_period=exposure_period, image_width=roix, image_height=roiy)
|
||||
if exposure_time != None:
|
||||
self.exposure_time = exposure_time
|
||||
if exposure_period != None:
|
||||
self.exposure_period = exposure_period
|
||||
if roix != None:
|
||||
self.roix = roix
|
||||
if roiy != None:
|
||||
self.roiy = roiy
|
||||
|
||||
if file_path!=None:
|
||||
if os.path.isdir(file_path):
|
||||
pass
|
||||
else:
|
||||
os.mkdir(file_path)
|
||||
|
||||
### TODO: camera reset
|
||||
print("Handing over to 'scans.acquire_dark")
|
||||
scans.acquire_dark(exp_burst=nimages_dark, exp_time=self.exposure_time, exp_period=self.exposure_period, image_width=self.roix,
|
||||
image_height=self.roiy, acq_mode=acq_mode, file_path=file_path, nr_writers=2)
|
||||
Reference in New Issue
Block a user