diff --git a/tomcat_bec/device_configs/microxas_test_bed.yaml b/tomcat_bec/device_configs/microxas_test_bed.yaml index 2d5f77a..90d835c 100644 --- a/tomcat_bec/device_configs/microxas_test_bed.yaml +++ b/tomcat_bec/device_configs/microxas_test_bed.yaml @@ -89,12 +89,19 @@ es1_tasks: es1_psod: - description: 'AA1 PSO output interface' + description: 'AA1 PSO output interface (trigger)' deviceClass: tomcat_bec.devices.aa1AxisPsoDistance - deviceConfig: {prefix: 'X02DA-ES1-SMP1:ROTY:PSO:'} - onFailure: buffer + deviceConfig: + prefix: 'X02DA-ES1-SMP1:ROTY:PSO:' + deviceTags: + - es1 + - trigger enabled: true + onFailure: buffer + readOnly: false readoutPriority: monitored + softwareTrigger: false + es1_ddaq: description: 'Automation1 position recording interface' @@ -153,16 +160,15 @@ daq_stream0: readoutPriority: monitored softwareTrigger: false -# Unthrottled stream -# daq_stream1: -# description: stdDAQ preview (4 at 10 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 +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 diff --git a/tomcat_bec/devices/aerotech/AerotechAutomation1.py b/tomcat_bec/devices/aerotech/AerotechAutomation1.py index ae16eb2..a295bd4 100644 --- a/tomcat_bec/devices/aerotech/AerotechAutomation1.py +++ b/tomcat_bec/devices/aerotech/AerotechAutomation1.py @@ -512,8 +512,8 @@ class aa1AxisPsoBase(Device): USER_ACCESS = ["configure"] # ######################################################################## # General module status - status = Component(EpicsSignalRO, "STATUS", auto_monitor=True, kind=Kind.hinted) - output = Component(EpicsSignalRO, "OUTPUT-RBV", auto_monitor=True, kind=Kind.hinted) + status = Component(EpicsSignalRO, "STATUS", auto_monitor=True, kind=Kind.normal) + output = Component(EpicsSignalRO, "OUTPUT-RBV", auto_monitor=True, kind=Kind.normal) address = Component(EpicsSignalRO, "ARRAY-ADDR", kind=Kind.config) _eventSingle = Component(EpicsSignal, "EVENT:SINGLE", put_complete=True, kind=Kind.omitted) @@ -561,9 +561,12 @@ class aa1AxisPsoBase(Device): outPin = Component(EpicsSignalRO, "PIN", auto_monitor=True, kind=Kind.config) outSource = Component(EpicsSignal, "SOURCE", put_complete=True, kind=Kind.config) - def trigger(self, settle_time=0.1): + def trigger(self, settle_time=0.1) -> DeviceStatus: """Fire a single PSO event (i.e. manual software trigger)""" self._eventSingle.set(1, settle_time=settle_time).wait() + status = DeviceStatus(self) + status.set_finished() + return status def toggle(self): """ Toggle waveform outup""" @@ -658,6 +661,7 @@ class aa1AxisPsoDistance(aa1AxisPsoBase): pso.kickoff().wait() """ _distance_value = None + USER_ACCESS = ["configure", "prepare", "kickoff", "complete"] # ######################################################################## # PSO high level interface @@ -698,27 +702,35 @@ class aa1AxisPsoDistance(aa1AxisPsoBase): return (old, new) # ######################################################################## - # Bluesky step scan interface + # Bluesky step scan interface gutted def stage(self): """ Stage the device: just ignore it... """ return super().stage() # ######################################################################## # Bluesky flyer interface - def prepare(self): - """ Rearms the configured array""" - # Rearm the configured array - if isinstance(self._distance_value, (np.ndarray, list, tuple)): - self.dstArrayRearm.set(1).wait() + def prepare(self) -> DeviceStatus: + """ Arm trigger for a synchronous or asynchronous acquisition""" + if self._distance_value is not None: + # Rearm the 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 def kickoff(self) -> DeviceStatus: - """ Kick off an asynchronous acquisition""" - # Rearm the 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() + """ Kick off asynchronous triggering on the controller""" + if self._distance_value is not None: + # Rearm the 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() print("PSO kicked off") @@ -883,7 +895,7 @@ class aa1AxisDriveDataCollection(Device): # ######################################################################## # General module status - state = Component(EpicsSignalRO, "STATE", auto_monitor=True, kind=Kind.hinted) + state = Component(EpicsSignalRO, "STATE", auto_monitor=True, kind=Kind.normal) nsamples_rbv = Component(EpicsSignalRO, "SAMPLES_RBV", auto_monitor=True, kind=Kind.hinted) _switch = Component(EpicsSignal, "ACQUIRE", put_complete=True, kind=Kind.omitted) _input0 = Component(EpicsSignal, "INPUT0", put_complete=True, kind=Kind.config) diff --git a/tomcat_bec/devices/gigafrost/gigafrostcamera.py b/tomcat_bec/devices/gigafrost/gigafrostcamera.py index 241df67..bc5a5a7 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostcamera.py +++ b/tomcat_bec/devices/gigafrost/gigafrostcamera.py @@ -430,12 +430,8 @@ class GigaFrostCamera(PSIDetectorBase): super().trigger() # There's no status readback from the camera, so we just wait - status = DeviceStatus(self) sleep_time = self.cfgExposure.value*self.cfgCntNum.value*0.001+0.050 - sleep(sleep_time) - logger.debug(f"[{self.name}] Slept for {sleep_time} seconds") - status.set_finished() - return status + return DeviceStatus(self, done=True, success=True, settle_time=sleep_time) def configure(self, d: dict = None): """Configure the next scan with the GigaFRoST camera diff --git a/tomcat_bec/devices/gigafrost/stddaq_preview.py b/tomcat_bec/devices/gigafrost/stddaq_preview.py index cba4f7e..3742a01 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_preview.py +++ b/tomcat_bec/devices/gigafrost/stddaq_preview.py @@ -143,9 +143,10 @@ class StdDaqPreviewDetector(PSIDetectorBase): url = Component(Signal, kind=Kind.config) throttle = Component(Signal, value=0.25, kind=Kind.config) status = Component(Signal, value=StdDaqPreviewState.UNKNOWN, kind=Kind.omitted) - image = Component(Signal, kind=Kind.normal) frame = Component(Signal, kind=Kind.hinted) image_shape = Component(Signal, kind=Kind.normal) + # FIXME: The BEC client caches the read()s from the last 50 scans + image = Component(Signal, kind=Kind.config) def __init__( self, *args, url: str = "tcp://129.129.95.38:20000", parent: Device = None, **kwargs diff --git a/tomcat_bec/scans/__init__.py b/tomcat_bec/scans/__init__.py index 0f372cd..c46698a 100644 --- a/tomcat_bec/scans/__init__.py +++ b/tomcat_bec/scans/__init__.py @@ -1,2 +1,2 @@ from .gigafrost_test import GigaFrostStepScan -from .tomcat_scanbase import SnapAndStepScanBase, SimpleStepScanBase +from .tomcat_scanbase import SnapAndStepScanBase, GigaStepScanBase diff --git a/tomcat_bec/scans/tomcat_scanbase.py b/tomcat_bec/scans/tomcat_scanbase.py index f9c0e30..e41170f 100644 --- a/tomcat_bec/scans/tomcat_scanbase.py +++ b/tomcat_bec/scans/tomcat_scanbase.py @@ -55,8 +55,6 @@ class TemplatedScanBase(AsyncFlyScanBase): scan_name = "aeroscript_scan_base" scan_report_hint = "table" required_kwargs = ["filename", "subs"] - arg_input = {} - arg_bundle_size = {"bundle": len(arg_input), "min": None, "max": None} def __init__(self, *args, parameter: dict = None, **kwargs): """Executes an AeroScript template as a flyer @@ -170,179 +168,99 @@ class TemplatedScanBase(AsyncFlyScanBase): +class GigaStepScanBase(ScanBase): + """ Gigafrost Step scan base class - - - - - - - - - - - - - - - - - - - -class SimpleStepScanBase(TemplatedScanBase): - """ Simple Step scan base class - - Example temporary base class for AeroScript-based step scans. As a temporary - setup until the final cabling is done, the scan is in a soft-trigger mode. - The Gigafrost is just software triggered, and the position readback is wired - to the internal trigger. + Example base class for AeroScript-based step scans using the low-level API. + Until the final cabling is done, the scan is in soft-trigger mode. Example ------- - >>> scans.simplestep(range=(0, 180), steps=1800, exp_time=20, exp_burst=5) - - Parameters - ---------- - range : (float, float) - Scan range of the axis. - steps : int, optional - Number of scan steps to cover the range. (default = 10) - exp_time : float, optional [0.01 ... 40] - Exposure time for each frame in [ms]. The IOC fixes the exposure - period to be 2x this long so it doesnt matter. (default = 20) - exp_burst : float, optional - Number of images to be taken for each scan point. (default=1) + >>> scans.gigastep(scan_start=-25, scan_end=155, steps=180, exp_time=0.005, exp_burst=5) """ - scan_name = "simplestep" - scan_report_hint = "table" - required_kwargs = ["range","steps"] - arg_input = {} - arg_bundle_size = {"bundle": len(arg_input), "min": None, "max": None} + scan_name = "gigastep" + required_kwargs = ["scan_start", "scan_end","steps"] + gui_config = { + "Movement parameters": ["steps"], + "Acquisition parameters" : ["exp_time", "burst_at_each_point"] + } - def __init__(self, *args, parameter: dict = None, **kwargs): - """Example step scan + def __init__( + self, + scan_start: float, + scan_end: float, + steps: int, + exp_time = 0.005, + settling_time = 0.2, + burst_at_each_point = 1, + **kwargs): + super().__init__( + exp_time=exp_time, + settling_time=settling_time, + relative=False, + burst_at_each_point=1, + optim_trajectory=None, + **kwargs + ) - Perform a simple step scan with a motor while software triggering the - gigafrost burst sequence at each point and recording it to the StdDAQ. - Actually only the configuration is gigafrost specific, everything else - is just using standard Bluesky event model. + 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 + self.scan_ntotal = burst_at_each_point * (steps + 1) + self.scan_nburst = burst_at_each_point - """ - # Auto-setup configuration parameters from input - logger.info(parameter) - self.scan_axis = "es1_roty" - self.scan_range = parameter['kwargs'].get("range") - self.scan_stepnum = int(parameter['kwargs'].get("steps")) - self.scan_exptime = float(parameter['kwargs'].get("exp_time", 20)) - self.scan_expburst = float(parameter['kwargs'].get("exp_burst", 1)) - self.scan_roix = int(parameter['kwargs'].get("roix", 2016)) - self.scan_roiy = int(parameter['kwargs'].get("roiy", 2016)) + # Override wait time with actual latency + # NOTE : The BEC does not wait for trigger() status done + self.exp_time = settling_time + exp_time * 2 * burst_at_each_point - # Synthetic values - self.scan_startpos = self.scan_range[0] - self.scan_stepsize = (self.scan_range[1]-self.scan_range[0])/self.scan_stepnum - self.scan_ntotal = self.scan_stepnum * self.scan_expburst - - # Aerotech DDC settings: - # PsoOut = 0 - # PsoEvent = 1 - # PsoHsInp0_rise = 2 - # PsoHsInp0_fall = 3 - # PsoHsInp1_rise = 4 - # PsoHsInp1_fall = 5 - parameter['kwargs']['daqname'] = "es1_ddaq" - parameter['kwargs']['daqcfg'] = {'ntotal': self.scan_ntotal, 'trigger': 1} - # Gigafrost config - parameter['kwargs']['camcfg'] = { + # Prepare configuration dicts + self.psocfg = {} + self.daqcfg = {'ntotal': self.scan_ntotal, 'trigger': 1} + self.camcfg = { "ntotal": self.scan_ntotal, - "nimages": self.scan_expburst, - "exposure": self.scan_exptime, - "period": 2*self.scan_exptime, - "pixel_width": self.scan_roix, - "pixel_height": self.scan_roiy + "nimages": burst_at_each_point, + "exposure": 1000*exp_time, + "period": 2000*exp_time, + "pixel_width": 2016, + "pixel_height": 2016 } - # Parameter validation before scan - if self.scan_stepnum <=0: - raise RuntimeError(f"Requires at least one scan step, got {self.scan_stepnum}") - - # Call super() - self.filename = "foo" - self.filetext = "bar" - super().__init__(parameter=parameter, **kwargs) - - def prepare_positions(self): + def _calculate_positions(self) -> None: """ Pre-calculate scan positions """ - self.positions = [self.scan_startpos] - for ii in range(self.scan_stepnum): - self.positions.append(self.scan_startpos + ii*self.scan_stepsize) - yield None - - def pre_scan(self): - # Move roughly to start position - logger.info(f"\n\nScan start position: {self.scan_startpos} with axis {self.scan_axis}\n\n") - yield from self.stubs.send_rpc_and_wait(self.scan_axis, "motor_enable.set", 1) - st = yield from self.stubs.send_rpc_and_wait(self.scan_axis, "move", self.scan_startpos) - st.wait() - yield None + self.positions = [self.scan_start] + for ii in range(self.scan_steps): + self.positions.append(self.scan_start + ii*self.scan_stepsize) def stage(self): - """ Configure and stage all devices""" + """ First do the actual configuration then the gutted staging""" - print(f"TOMCAT Staging step scan with motors {self.scan_axis}") + print(f"TOMCAT Staging step scan with motors {self.motor}") # Configure the camera (usually together wit the DAQ) - yield from self.stubs.send_rpc_and_wait(self.camera, "configure", self.camcfg) - - # Configure the camera (usually together wit the DAQ) - if self.daqname is not None: - yield from self.stubs.send_rpc_and_wait(self.daqname, "configure", self.daqcfg) - - # ################################################################################### - # Staging - if self.camera is not None: - yield from self.stubs.send_rpc_and_wait(self.camera, "stage") - if self.daqname: - yield from self.stubs.send_rpc_and_wait(self.daqname, "stage") - if self.preview is not None: - yield from self.stubs.send_rpc_and_wait(self.preview, "stage") - # For God, NO! - # yield from super().stage() - - - def scan_core(self): - """ Start""" - t_start = time.time() - - for ii in range(self.scan_stepnum): - print(f"Point: {ii}") - st = yield from self.stubs.send_rpc_and_wait(self.scan_axis, "move", self.positions[ii]) - st.wait() - st = yield from self.stubs.send_rpc_and_wait("gfclient", "trigger") - st = yield from self.stubs.send_rpc_and_wait("es1_psod", "trigger") - time.sleep(0.1) - - t_end = time.time() - t_elapsed = t_end - t_start - print(f"Elapsed scan time: {t_elapsed}") - time.sleep(0.5) - - # Collect - if self.daqmode=="collect": - positions = yield from self.stubs.send_rpc_and_wait(self.daqname, "collect") - logger.info(f"Finished scan with collected positions: {positions}") - - logger.info("Scan done\n\n") - - - - - - + yield from self.stubs.send_rpc_and_wait("gfclient", "configure", self.camcfg) + # Configure the position readback + yield from self.stubs.send_rpc_and_wait("es1_ddaq", "configure", self.daqcfg) + # Configure the manual triggers + yield from self.stubs.send_rpc_and_wait("es1_psod", "configure", self.psocfg) + # Explicitly arm trigger + yield from self.stubs.send_rpc_and_wait("es1_psod", "prepare") + # TODO: Explicitly arm detector + # Explicitly arm trigger + yield from self.stubs.send_rpc_and_wait("gfclient", "stage") + yield from self.stubs.send_rpc_and_wait("es1_ddaq", "stage") + yield from self.stubs.send_rpc_and_wait("es1_psod", "stage") + yield from self.stubs.send_rpc_and_wait("daq_stream1", "stage") + # FIXME: Shouldn't this stage() everything? + return super().stage() + def unstage(self): + """ Wait for DAQ before unstaging""" + time.sleep(5) + yield from super().unstage() diff --git a/tomcat_bec/scripts/demoscans.py b/tomcat_bec/scripts/demoscans.py index 30e3729..cfe1207 100644 --- a/tomcat_bec/scripts/demoscans.py +++ b/tomcat_bec/scripts/demoscans.py @@ -1,7 +1,19 @@ +""" Demo scans for Tomcat at the microXAS test bench +""" -def demostepscan(scan_start, scan_end, steps, exp_time=0.005, burst_at_each_point=1): + +def demostepscan(scan_start, scan_end, steps, exp_time=0.005, burst_at_each_point=1, settling_time=0): + """ Demo step scan with GigaFrost + + This is a small user-space demo step scan at the microXAS testbench using + the gigafrost in software triggering mode. It tries to be as standard as + possible, while still setting up the environment. + + Example: + -------- + demostepscan(scan_start=-32, scan_end=148, steps=180, exp_time=0.005, burst_at_each_point=5) + """ scan_ntotal = burst_at_each_point * (steps + 1) - print("NI") # Move to start position cfg = {'ntotal': scan_ntotal, 'trigger': 1} @@ -11,17 +23,20 @@ def demostepscan(scan_start, scan_end, steps, exp_time=0.005, burst_at_each_poin "ntotal": scan_ntotal, "nimages": burst_at_each_point, "exposure": 1000*exp_time, "period": 2000*exp_time} dev.gfclient.configure(d=cfg) - # Configure PSO trigger - cfg = {} - dev.es1_psod.configure(d=cfg) + # Configure PSO trigger (trigger is normally disabled in fly mode) + dev.es1_psod.configure(d={}) + dev.es1_psod.software_trigger = True + # Explicitly arm trigger (and detector in future) + dev.es1_psod.prepare() print("Handing over to 'scans.line_scan'") + wait_time = 0.1 + 2*exp_time * burst_at_each_point scans.line_scan( dev.es1_roty, scan_start, scan_end, - steps=steps, exp_time=2*exp_time, relative=False) + steps=steps, exp_time=wait_time, relative=False, settling_time=settling_time) - # Move to start position - umv(dev.es1_roty, scan_start) + # Cleanup: disable SW trigger + dev.es1_psod.software_trigger = False