diff --git a/tomcat_bec/device_configs/microxas_test_bed.yaml b/tomcat_bec/device_configs/microxas_test_bed.yaml index 09c760e..2c8a479 100644 --- a/tomcat_bec/device_configs/microxas_test_bed.yaml +++ b/tomcat_bec/device_configs/microxas_test_bed.yaml @@ -64,18 +64,18 @@ es1_roty: # 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: @@ -92,18 +92,18 @@ es1_roty: # 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: @@ -152,7 +152,7 @@ gfdaq: readoutPriority: monitored softwareTrigger: false -daq_stream0: +gf_stream0: description: stdDAQ preview (2 every 555) deviceClass: tomcat_bec.devices.StdDaqPreviewDetector deviceConfig: @@ -166,21 +166,6 @@ daq_stream0: 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 - - gfcam - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: monitored - softwareTrigger: false - - pcocam: description: PCO.edge camera client deviceClass: tomcat_bec.devices.PcoEdge5M diff --git a/tomcat_bec/devices/aerotech/AerotechDriveDataCollection.py b/tomcat_bec/devices/aerotech/AerotechDriveDataCollection.py index b81ec3f..1489a02 100644 --- a/tomcat_bec/devices/aerotech/AerotechDriveDataCollection.py +++ b/tomcat_bec/devices/aerotech/AerotechDriveDataCollection.py @@ -36,19 +36,22 @@ class AerotechDriveDataCollectionMixin(CustomDeviceMixin): # NOTE: Scans don't have to fully configure the device if "ddc_trigger" in scanargs: d["ddc_trigger"] = scanargs["ddc_trigger"] - if "ddc_num_points" in scanargs: + + if "ddc_num_points" in scanargs and scanargs["ddc_num_points"] is not None: d["num_points_total"] = scanargs["ddc_num_points"] + elif "daq_num_points" in scanargs and scanargs["daq_num_points"] is not None: + 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: + 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: + if "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: + if "repeats" in scanargs and scanargs["repeats"] is not None: num_points *= scanargs["repeats"] points_valid = True if points_valid: @@ -143,6 +146,10 @@ class aa1AxisDriveDataCollection(PSIDeviceBase): """Bluesky-style stage""" self._switch.set("Start", settle_time=0.2).wait() + def blueunstage(self): + """Bluesky-style unstage""" + self._switch.set("Stop", settle_time=0.2).wait() + def reset(self): """Reset incremental readback""" self._switch.set("ResetRB", settle_time=0.1).wait() @@ -170,6 +177,8 @@ class aa1AxisDriveDataCollection(PSIDeviceBase): elif index == 1: status = SubscriptionStatus(self._readstatus1, neg_edge, settle_time=0.5) self._readback1.set(1).wait() + else: + raise ValueError(f"[{self.name}] Invalid data acquisition channel {index}") # Start asynchronous readback status.wait() diff --git a/tomcat_bec/devices/aerotech/AerotechSimpleSequenceTemplate.ascript b/tomcat_bec/devices/aerotech/AerotechSimpleSequenceTemplate.ascript index df0ff92..b7b0c21 100644 --- a/tomcat_bec/devices/aerotech/AerotechSimpleSequenceTemplate.ascript +++ b/tomcat_bec/devices/aerotech/AerotechSimpleSequenceTemplate.ascript @@ -34,7 +34,8 @@ program ////////////////////////////////////////////////////////////////////////// // Internal parameters - dont use var $axis as axis = ROTY - var $ii as integer + var $ii as integer + var $axisFaults as integer = 0 var $iDdcSafeSpace as integer = 4096 // Set acceleration @@ -126,7 +127,12 @@ program if $eScanType == ScanType.POS || $eScanType == ScanType.NEG PsoDistanceConfigureArrayDistances($axis, $iPsoArrayPosAddr, $iPsoArrayPosSize, 0) MoveAbsolute($axis, $fPosEnd, $fVelScan) - WaitForInPosition($axis) + WaitForMotionDone($axis) + + $axisFaults = StatusGetAxisItem($axis, AxisDataSignal.AxisFault) + if $axisFaults + TaskSetError(TaskGetIndex(), "AxisFault on axis ROTY") + end elseif $eScanType == ScanType.POSNEG || $eScanType == ScanType.NEGPOS for $ii = 0 to ($iNumRepeat-1) // Feedback on progress @@ -134,11 +140,15 @@ program if ($ii % 2) == 0 PsoDistanceConfigureArrayDistances($axis, $iPsoArrayPosAddr, $iPsoArrayPosSize, 0) MoveAbsolute($axis, $fPosEnd, $fVelScan) - WaitForInPosition($axis) elseif ($ii % 2) == 1 PsoDistanceConfigureArrayDistances($axis, $iPsoArrayNegAddr, $iPsoArrayNegSize, 0) MoveAbsolute($axis, $fPosStart, $fVelScan) - WaitForInPosition($axis) + end + WaitForMotionDone($axis) + + $axisFaults = StatusGetAxisItem($axis, AxisDataSignal.AxisFault) + if $axisFaults + TaskSetError(TaskGetIndex(), "AxisFault on axis ROTY") end Dwell(0.2) end diff --git a/tomcat_bec/devices/aerotech/AerotechSnapAndStepTemplate.ascript b/tomcat_bec/devices/aerotech/AerotechSnapAndStepTemplate.ascript index e8c69e5..80657d0 100644 --- a/tomcat_bec/devices/aerotech/AerotechSnapAndStepTemplate.ascript +++ b/tomcat_bec/devices/aerotech/AerotechSnapAndStepTemplate.ascript @@ -78,6 +78,7 @@ program /////////////////////////////////////////////////////////// $iglobal[2] = $iNumSteps for $ii = 0 to ($iNumSteps-1) + $rglobal[4] = $ii MoveAbsolute($axis, $fStartPosition + $ii * $fStepSize, $fVelScan) WaitForMotionDone($axis) diff --git a/tomcat_bec/devices/aerotech/AerotechTasks.py b/tomcat_bec/devices/aerotech/AerotechTasks.py index bdac316..74b0b74 100644 --- a/tomcat_bec/devices/aerotech/AerotechTasks.py +++ b/tomcat_bec/devices/aerotech/AerotechTasks.py @@ -47,7 +47,7 @@ class AerotechTasksMixin(CustomDeviceMixin): # Perform bluesky-style configuration if len(d) > 0: - logger.warning(f"[{self.parent.name}] Configuring with:\n{d}") + # logger.warning(f"[{self.parent.name}] Configuring with:\n{d}") self.parent.configure(d=d) # The actual staging @@ -55,12 +55,16 @@ class AerotechTasksMixin(CustomDeviceMixin): def on_unstage(self): """Stop the currently selected task""" - self.parent.switch.set("Stop").wait() + self.parent.blueunstage() def on_stop(self): """Stop the currently selected task""" self.parent.switch.set("Stop").wait() + def on_kickoff(self): + """Start execution of the selected task""" + self.parent.bluekickoff() + class aa1Tasks(PSIDeviceBase): """Task management API @@ -146,6 +150,10 @@ class aa1Tasks(PSIDeviceBase): raise RuntimeError("Failed to load task, please check the Aerotech IOC") return status + def blueunstage(self): + """Bluesky style unstage, stops execution""" + self.switch.set("Stop").wait() + def bluekickoff(self): """Bluesky style kickoff""" # Launch and check success @@ -155,6 +163,10 @@ class aa1Tasks(PSIDeviceBase): raise RuntimeError("Failed to load task, please check the Aerotech IOC") return status + def kickoff(self): + """Missing kickoff, for real?""" + self.custom_prepare.on_kickoff() + ########################################################################## # Bluesky flyer interface def complete(self) -> DeviceStatus: diff --git a/tomcat_bec/devices/gigafrost/gigafrostcamera.py b/tomcat_bec/devices/gigafrost/gigafrostcamera.py index 59ee53f..1b90dbe 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostcamera.py +++ b/tomcat_bec/devices/gigafrost/gigafrostcamera.py @@ -7,7 +7,7 @@ Created on Thu Jun 27 17:28:43 2024 @author: mohacsi_i """ from time import sleep -from ophyd import Signal, SignalRO, Component, EpicsSignal, EpicsSignalRO, Kind, DeviceStatus +from ophyd import Signal, Component, EpicsSignal, EpicsSignalRO, Kind, DeviceStatus from ophyd.device import DynamicDeviceComponent from ophyd_devices.interfaces.base_classes.psi_detector_base import ( CustomDetectorMixin, @@ -174,6 +174,10 @@ class GigaFrostCameraMixin(CustomDetectorMixin): d["image_height"] = scanargs["image_height"] if "exp_time" in scanargs and scanargs["exp_time"] is not None: d["exposure_time_ms"] = scanargs["exp_time"] + if "acq_time" in scanargs and scanargs["acq_time"] is not None: + d["exposure_time_ms"] = scanargs["acq_time"] + if "acq_period" in scanargs and scanargs["acq_period"] is not None: + d["exposure_period_ms"] = scanargs["acq_period"] if "exp_burst" in scanargs and scanargs["exp_burst"] is not None: d["exposure_num_burst"] = scanargs["exp_burst"] if "acq_mode" in scanargs and scanargs["acq_mode"] is not None: @@ -497,6 +501,7 @@ class GigaFrostCamera(PSIDetectorBase): # There's no status readback from the camera, so we just wait sleep_time = self.cfgExposure.value * self.cfgCntNum.value * 0.001 + 0.2 + logger.warning(f"Gigafrost sleeping for {sleep_time} sec") sleep(sleep_time) return DeviceStatus(self, done=True, success=True, settle_time=sleep_time) diff --git a/tomcat_bec/devices/gigafrost/pcoedgecamera.py b/tomcat_bec/devices/gigafrost/pcoedgecamera.py index 0f046a6..d9e0f07 100644 --- a/tomcat_bec/devices/gigafrost/pcoedgecamera.py +++ b/tomcat_bec/devices/gigafrost/pcoedgecamera.py @@ -7,7 +7,7 @@ Created on Wed Dec 6 11:33:54 2023 import time from ophyd import Component, EpicsSignal, EpicsSignalRO, Kind from ophyd.status import SubscriptionStatus, DeviceStatus -from ophyd_devices import BECDeviceBase +from ophyd_devices.interfaces.base_classes.psi_detector_base import PSIDetectorBase as BECDeviceBase from ophyd_devices.interfaces.base_classes.psi_detector_base import ( CustomDetectorMixin as CustomPrepare, ) @@ -54,6 +54,10 @@ class PcoEdgeCameraMixin(CustomPrepare): d["exposure_time_ms"] = scanargs["exp_time"] if "exp_period" in scanargs and scanargs["exp_period"] is not None: d["exposure_period_ms"] = scanargs["exp_period"] + if "acq_time" in scanargs and scanargs["acq_time"] is not None: + d["exposure_time_ms"] = scanargs["acq_time"] + if "acq_period" in scanargs and scanargs["acq_period"] is not None: + d["exposure_period_ms"] = scanargs["acq_period"] # if 'exp_burst' in scanargs and scanargs['exp_burst'] is not None: # d['exposure_num_burst'] = scanargs['exp_burst'] # if 'acq_mode' in scanargs and scanargs['acq_mode'] is not None: @@ -469,5 +473,5 @@ class PcoEdge5M(HelgeCameraBase): if __name__ == "__main__": # Drive data collection - cam = PcoEdge5M("X02DA-CCDCAM2:", name="mcpcam") + cam = PcoEdge5M(prefix="X02DA-CCDCAM2:", name="mcpcam") cam.wait_for_connection() diff --git a/tomcat_bec/scans/__init__.py b/tomcat_bec/scans/__init__.py index ab461c9..4693852 100644 --- a/tomcat_bec/scans/__init__.py +++ b/tomcat_bec/scans/__init__.py @@ -1,2 +1,2 @@ from .tutorial_fly_scan import AcquireDark, AcquireWhite, AcquireRefs, TutorialFlyScanContLine -from .tomcat_scans import TomcatStepScan, TomcatSnapNStep, TomcatSimpleSequence +from .tomcat_scans import TomcatSnapNStep, TomcatSimpleSequence diff --git a/tomcat_bec/scans/tomcat_scans.py b/tomcat_bec/scans/tomcat_scans.py index c6e980d..3c830b2 100644 --- a/tomcat_bec/scans/tomcat_scans.py +++ b/tomcat_bec/scans/tomcat_scans.py @@ -18,95 +18,11 @@ import os import time from bec_lib import bec_logger -from bec_server.scan_server.scans import AsyncFlyScanBase, ScanBase, ScanArgType +from bec_server.scan_server.scans import AsyncFlyScanBase, ScanArgType logger = bec_logger.logger -class TomcatStepScan(ScanBase): - """Simple software step scan for Tomcat - - Example class for simple BEC-based step scan using the low-level API. It's just a standard - 'line_scan' with the only difference that overrides burst behavior to use camera burst instead - of individual software triggers. - - NOTE: As decided by Tomcat, the scans should not manage the scope of devices - - All enabled devices are expected to be configured for acquisition by the end of stage - - Some devices can configure themselves from mandatory scan parameters (steps, burst) - - Other devices can be optionally configured by keyword arguments - - Devices will try to stage using whatever was set on them before - - Example: - -------- - >>> scans.tomcatstepscan(scan_start=-25, scan_end=155, steps=180, exp_time=0.005, exp_burst=5) - - Common keyword arguments: - ------------------------- - image_width : int - image_height : int - ddc_trigger : str - """ - - scan_name = "tomcatstepscan" - scan_type = "step" - required_kwargs = ["scan_start", "scan_end", "steps"] - gui_config = { - "Movement parameters": ["steps"], - "Acquisition parameters": ["exp_time", "exp_burst"], - } - - def update_scan_motors(self): - self.scan_motors = ["es1_roty"] - - def _get_scan_motors(self): - self.scan_motors = ["es1_roty"] - - def __init__( - self, - scan_start: float, - scan_end: float, - steps: int, - exp_time: float=0.005, - settling_time: float=0.2, - exp_burst: int=1, - **kwargs, - ): - # Converting generic kwargs to tomcat device configuration parameters - super().__init__( - exp_time=exp_time, - settling_time=settling_time, - burst_at_each_point=1, - optim_trajectory=None, - **kwargs, - ) - - # For position calculation - self.motor = "es1_roty" - self.scan_start = scan_start - self.scan_end = scan_end - self.scan_steps = steps - self.scan_stepsize = (scan_end - scan_start) / steps - - def _calculate_positions(self) -> None: - """Pre-calculate scan positions""" - for ii in range(self.scan_steps + 1): - self.positions.append(self.scan_start + ii * self.scan_stepsize) - - def _at_each_point(self, ind=None, pos=None): - """ Overriden at_each_point, using detector burst instaead of manual triggering""" - yield from self._move_scan_motors_and_wait(pos) - time.sleep(self.settling_time) - - trigger_time = 0.001*self.exp_time * self.burst_at_each_point - yield from self.stubs.trigger(min_wait=trigger_time) - # yield from self.stubs.trigger(group='trigger', point_id=self.point_id) - # time.sleep(trigger_time) - - yield from self.stubs.read(group="monitored", point_id=self.point_id) - # yield from self.stubs.read(group="monitored", point_id=self.point_id, wait_group=None) - self.point_id += 1 - - class TomcatSnapNStep(AsyncFlyScanBase): """Simple software step scan forTomcat @@ -115,7 +31,7 @@ class TomcatSnapNStep(AsyncFlyScanBase): Example ------- - >>> scans.tomcatsnapnstepscan(scan_start=-25, scan_end=155, steps=180, exp_time=0.005, exp_burst=5) + >>> scans.tomcatsnapnstepscan(scan_start=-25, scan_end=155, steps=180, acq_time=5, exp_burst=5) """ scan_name = "tomcatsnapnstepscan" @@ -126,10 +42,10 @@ class TomcatSnapNStep(AsyncFlyScanBase): required_kwargs = ["scan_start", "scan_end", "steps"] gui_config = { "Movement parameters": ["steps"], - "Acquisition parameters": ["exp_time", "exp_burst"], + "Acquisition parameters": ["acq_time", "exp_burst"], } - def _get_scan_motors(self): + def _update_scan_motors(self): self.scan_motors = ["es1_roty"] def __init__( @@ -137,8 +53,8 @@ class TomcatSnapNStep(AsyncFlyScanBase): scan_start: float, scan_end: float, steps: int, - exp_time:float=0.005, settling_time:float=0.2, + acq_time:float=5.0, exp_burst:int=1, sync:str="event", **kwargs, @@ -147,14 +63,12 @@ class TomcatSnapNStep(AsyncFlyScanBase): self.scan_start = scan_start self.scan_end = scan_end self.scan_steps = steps - self.scan_stepsize = (scan_end - scan_start) / steps - self.scan_ntotal = exp_burst * (steps + 1) - self.exp_time = exp_time + self.exp_time = acq_time self.exp_burst = exp_burst self.settling_time = settling_time self.scan_sync = sync - # General device configuration + # Gigafrost trigger mode kwargs["parameter"]["kwargs"]["acq_mode"] = "ext_enable" # Used for Aeroscript file substitutions for the task interface @@ -165,7 +79,8 @@ class TomcatSnapNStep(AsyncFlyScanBase): kwargs["parameter"]["kwargs"]["script_file"] = "bec.ascript" super().__init__( - exp_time=exp_time, + acq_time=acq_time, + exp_burst=exp_burst, settling_time=settling_time, burst_at_each_point=1, optim_trajectory=None, @@ -182,12 +97,12 @@ class TomcatSnapNStep(AsyncFlyScanBase): } filesubs = { "startpos": self.scan_start, - "stepsize": self.scan_stepsize, + "stepsize": (self.scan_end - self.scan_start) / self.scan_steps, "numsteps": self.scan_steps, - "exptime": 2 * self.exp_time * self.exp_burst, + "exptime": 0.002 * self.exp_time * self.exp_burst, "settling": self.settling_time, "psotrigger": p_modes[self.scan_sync], - "npoints": self.scan_ntotal, + "npoints": self.exp_burst * (self.scan_steps + 1), } return filesubs @@ -196,7 +111,7 @@ class TomcatSnapNStep(AsyncFlyScanBase): # Load the test file filename = os.path.join(os.path.dirname(__file__), "../devices/aerotech/" + filename) logger.info(f"Attempting to load file {filename}") - with open(filename) as f: + with open(filename, "r", encoding="utf-8") as f: templatetext = f.read() # Substitute jinja template @@ -208,12 +123,15 @@ class TomcatSnapNStep(AsyncFlyScanBase): """The actual scan routine""" print("TOMCAT Running scripted scan (via Jinjad AeroScript)") t_start = time.time() + # Kickoff + yield from self.stubs.send_rpc_and_wait("es1_tasks", "kickoff") # Complete # FIXME: this will swallow errors # yield from self.stubs.complete(device="es1_tasks") - st = yield from self.stubs.send_rpc_and_wait("es1_tasks", "complete") - # st.wait() + yield from self.stubs.send_rpc_and_wait("es1_tasks", "complete") + + # Check final state task_states = yield from self.stubs.send_rpc_and_wait("es1_tasks", "taskStates.get") if task_states[4] == 8: raise RuntimeError(f"Task {4} finished in ERROR state") @@ -243,7 +161,7 @@ class TomcatSimpleSequence(AsyncFlyScanBase): Example ------- - >>> scans.tomcatsimplesequencescan(33, 180, 180, exp_time=0.005, exp_burst=1800, repeats=10) + >>> scans.tomcatsimplesequencescan(33, 180, 180, acq_time=5, exp_burst=1800, repeats=10) """ scan_name = "tomcatsimplesequencescan" @@ -255,7 +173,7 @@ class TomcatSimpleSequence(AsyncFlyScanBase): "Acquisition parameters": [ "gate_high", "gate_low", - "exp_time", + "acq_time", "exp_burst", "sync", ], @@ -271,7 +189,7 @@ class TomcatSimpleSequence(AsyncFlyScanBase): gate_low: float, repeats: int = 1, repmode: str = "PosNeg", - exp_time: float = 0.005, + acq_time: float = 5, exp_burst: float = 180, sync: str = "pso", **kwargs, @@ -282,13 +200,13 @@ class TomcatSimpleSequence(AsyncFlyScanBase): self.gate_low = gate_low self.scan_repnum = repeats self.scan_repmode = repmode.upper() - self.exp_time = exp_time + self.exp_time = acq_time self.exp_burst = exp_burst self.scan_sync = sync # Synthetic values self.scan_ntotal = exp_burst * repeats - self.scan_velocity = gate_high / (exp_time * exp_burst) + self.scan_velocity = gate_high / (0.001*acq_time * exp_burst) self.scan_acceleration = 500 self.scan_safedistance = 10 self.scan_accdistance = ( @@ -314,7 +232,7 @@ class TomcatSimpleSequence(AsyncFlyScanBase): kwargs["parameter"]["kwargs"]["script_file"] = "bec.ascript" super().__init__( - exp_time=exp_time, + acq_time=acq_time, settling_time=0.5, relative=False, burst_at_each_point=1, @@ -327,7 +245,7 @@ class TomcatSimpleSequence(AsyncFlyScanBase): # Load the test file filename = os.path.join(os.path.dirname(__file__), "../devices/aerotech/" + filename) logger.info(f"Attempting to load file {filename}") - with open(filename) as f: + with open(filename, 'r', encoding="utf-8") as f: templatetext = f.read() # Substitute jinja template @@ -339,12 +257,14 @@ class TomcatSimpleSequence(AsyncFlyScanBase): """The actual scan routine""" print("TOMCAT Running scripted scan (via Jinjad AeroScript)") t_start = time.time() + # Kickoff + yield from self.stubs.send_rpc_and_wait("es1_tasks", "kickoff") # Complete # FIXME: this will swallow errors # yield from self.stubs.complete(device="es1_tasks") - st = yield from self.stubs.send_rpc_and_wait("es1_tasks", "complete") - # st.wait() + yield from self.stubs.send_rpc_and_wait("es1_tasks", "complete") + task_states = yield from self.stubs.send_rpc_and_wait("es1_tasks", "taskStates.get") if task_states[4] == 8: raise RuntimeError(f"Task {4} finished in ERROR state")