From 7dfa47077ba28bdedc6065dffdbc507bc46aa4ab Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Wed, 27 Nov 2024 17:18:16 +0100 Subject: [PATCH] Works with BEC v3 --- .../device_configs/microxas_test_bed.yaml | 4 +- .../aerotech/AerotechDriveDataCollection.py | 12 ++- tomcat_bec/devices/aerotech/AerotechPso.py | 27 ++++--- .../AerotechSnapAndStepTemplate.ascript | 2 + tomcat_bec/devices/aerotech/AerotechTasks.py | 34 +++++---- tomcat_bec/devices/gigafrost/stddaq_client.py | 75 ++++++++++--------- tomcat_bec/scans/tomcat_scans.py | 32 ++++---- 7 files changed, 102 insertions(+), 84 deletions(-) diff --git a/tomcat_bec/device_configs/microxas_test_bed.yaml b/tomcat_bec/device_configs/microxas_test_bed.yaml index ca29ef6..7edb0a3 100644 --- a/tomcat_bec/device_configs/microxas_test_bed.yaml +++ b/tomcat_bec/device_configs/microxas_test_bed.yaml @@ -71,7 +71,7 @@ es1_ismc: enabled: true onFailure: buffer readOnly: false - readoutPriority: baseline + readoutPriority: monitored softwareTrigger: false es1_tasks: @@ -157,7 +157,7 @@ gfdaq: onFailure: buffer readOnly: false readoutPriority: monitored - softwareTrigger: true + softwareTrigger: false daq_stream0: description: stdDAQ preview (2 every 555) diff --git a/tomcat_bec/devices/aerotech/AerotechDriveDataCollection.py b/tomcat_bec/devices/aerotech/AerotechDriveDataCollection.py index b8c5b84..c9f015e 100644 --- a/tomcat_bec/devices/aerotech/AerotechDriveDataCollection.py +++ b/tomcat_bec/devices/aerotech/AerotechDriveDataCollection.py @@ -20,6 +20,10 @@ logger = bec_logger.logger class AerotechDriveDataCollectionMixin(CustomDeviceMixin): + """Configuration and staging + + NOTE: scripted scans start drive data collection internally + """ # parent : aa1Tasks def on_stage(self) -> None: """Configuration and staging""" @@ -48,10 +52,10 @@ class AerotechDriveDataCollectionMixin(CustomDeviceMixin): logger.warning(f"[{self.parent.name}] Configuring with:\n{d}") self.parent.configure(d=d) - # Stage the DDC distance module + # Stage the data collection if not in internally launced mode # NOTE: Scripted scans start acquiring from the scrits if self.parent.scaninfo.scan_type not in ("script", "scripted"): - self.parent._switch.set("Start", settle_time=0.2).wait() + self.parent.bluestage() def on_unstage(self): """Standard bluesky unstage""" @@ -127,6 +131,10 @@ class aa1AxisDriveDataCollection(PSIDeviceBase): new = self.read_configuration() return (old, new) + def bluestage(self) -> None: + """Bluesky-style stage""" + self._switch.set("Start", settle_time=0.2).wait() + def reset(self): """Reset incremental readback""" self._switch.set("ResetRB", settle_time=0.1).wait() diff --git a/tomcat_bec/devices/aerotech/AerotechPso.py b/tomcat_bec/devices/aerotech/AerotechPso.py index 9ba3319..678818e 100644 --- a/tomcat_bec/devices/aerotech/AerotechPso.py +++ b/tomcat_bec/devices/aerotech/AerotechPso.py @@ -21,11 +21,8 @@ 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. """ - # logger.warning(self.parent.scaninfo.scan_msg.info['kwargs'].keys()) - - # Fish out configuration from scaninfo - # NOTE: Scans don't have to fully configure the device, but it is expected that it's + # 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}") @@ -49,14 +46,7 @@ class AerotechPsoDistanceMixin(CustomDeviceMixin): self.parent.configure(d=d) # Stage the PSO distance module - if isinstance(self.parent._distance_value, (np.ndarray, list, tuple)): - self.dstArrayRearm.set(1).wait() - # Wait for polling - sleep(0.5) - # Start monitoring the counters if distance is valid - if self.parent.dstDistanceVal.get() > 0: - self.parent.dstEventsEna.set("On").wait() - self.parent.dstCounterEna.set("On").wait() + self.parent.bluestage() def on_unstage(self): """Standard bluesky unstage""" @@ -286,6 +276,19 @@ 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 + if isinstance(self._distance_value, (np.ndarray, list, tuple)): + self.dstArrayRearm.set(1).wait() + # Wait for polling + sleep(0.5) + # Start monitoring the counters if distance is valid + if self.dstDistanceVal.get() > 0: + self.dstEventsEna.set("On").wait() + self.dstCounterEna.set("On").wait() + # # ######################################################################## # # Bluesky flyer interface diff --git a/tomcat_bec/devices/aerotech/AerotechSnapAndStepTemplate.ascript b/tomcat_bec/devices/aerotech/AerotechSnapAndStepTemplate.ascript index 5ebfc2c..2d23b41 100644 --- a/tomcat_bec/devices/aerotech/AerotechSnapAndStepTemplate.ascript +++ b/tomcat_bec/devices/aerotech/AerotechSnapAndStepTemplate.ascript @@ -91,6 +91,8 @@ program Dwell($fSettlingTimeSec) PsoEventGenerateSingle($axis) Dwell($fExposureTimeSec) + PsoEventGenerateSingle($axis) + $iglobal[1] = $ii end diff --git a/tomcat_bec/devices/aerotech/AerotechTasks.py b/tomcat_bec/devices/aerotech/AerotechTasks.py index fd506f4..005cfc1 100644 --- a/tomcat_bec/devices/aerotech/AerotechTasks.py +++ b/tomcat_bec/devices/aerotech/AerotechTasks.py @@ -57,20 +57,7 @@ class AerotechTasksMixin(CustomDeviceMixin): self.parent.configure(d=d) # The actual staging - settle_time = 0.2 - if self.parent._is_configured: - if self.parent._text_to_execute is not None: - status = self.parent._execute.set(self.parent._text_to_execute, settle_time=settle_time) - else: - status = self.parent.switch.set("Run", settle_time=settle_time) - else: - status = DeviceStatus(self.parent) - status.set_finished() - if settle_time is not None and settle_time > 0: - sleep(settle_time) - if self.parent._failure.value: - raise RuntimeError("Failed to kick off task, please check the Aerotech IOC") - return status + self.parent.bluestage() def on_unstage(self): """Stop the currently selected task""" @@ -205,6 +192,25 @@ class aa1Tasks(PSIDeviceBase): new = self.read_configuration() return (old, new) + def bluestage(self) -> None: + """Bluesky style stage""" + settle_time = 0.2 + if self._is_configured: + if self.taskIndex.get() in (0, 1): + logger.warning(f"[{self.name}] Woah, launching AeroScript on a system task. Daring today are we?") + if self._text_to_execute is not None: + status = self._execute.set(self._text_to_execute, settle_time=settle_time) + else: + status = self.switch.set("Run", settle_time=settle_time) + else: + status = DeviceStatus(self) + status.set_finished() + if settle_time is not None and settle_time > 0: + sleep(settle_time) + if self._failure.value: + raise RuntimeError("Failed to kick off task, please check the Aerotech IOC") + return status + ########################################################################## # Bluesky flyer interface def complete(self) -> DeviceStatus: diff --git a/tomcat_bec/devices/gigafrost/stddaq_client.py b/tomcat_bec/devices/gigafrost/stddaq_client.py index 71a4b5d..1549b5b 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_client.py +++ b/tomcat_bec/devices/gigafrost/stddaq_client.py @@ -58,7 +58,7 @@ class StdDaqMixin(CustomDeviceMixin): if len(d) > 0: # Stop if current status is not idle if self.parent.state() != "idle": - self.parent.safestop() + self.parent.surestop() # Configure new run (will restart the stdDAQ) logger.warning(f"[{self.parent.name}] Configuring with:\n{d}") self.parent.configure(d=d) @@ -74,7 +74,7 @@ class StdDaqMixin(CustomDeviceMixin): 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['status']}") + 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 @@ -99,19 +99,12 @@ class StdDaqMixin(CustomDeviceMixin): def on_unstage(self): """ Stop a running acquisition and close connection """ - try: - self.parent.message({"command": "stop_all"}, wait_reply=False) - except (RuntimeError, TypeError): - # The poller thread locks recv raising a RuntimeError - pass - finally: - logger.debug(f"[{self.parent.name}] Deleting socket after unstage") - self.parent._wsclient = None + self.parent.surestop() def on_stop(self): """ Stop a running acquisition and close connection """ - self.parent.unstage() + self.parent.surestop() def poll(self) -> None: """ Monitor status messages while connection is open. This will block the reply monitoring @@ -151,7 +144,7 @@ class StdDaqClient(PSIDeviceBase): """ # pylint: disable=too-many-instance-attributes custom_prepare_cls = StdDaqMixin - USER_ACCESS = ["set_daq_config", "get_daq_config", "safestop", "restart", "connect", "message", "state"] + USER_ACCESS = ["set_daq_config", "get_daq_config", "surestop", "nuke", "connect", "message", "state"] _wsclient = None # Status attributes @@ -223,7 +216,7 @@ class StdDaqClient(PSIDeviceBase): "The stdDAQ websocket interface refused connection 5 times.") logger.debug(f"[{self.name}] Connected to DAQ after {num_retry} tries") - def message(self, message: dict, timeout=1, wait_reply=True) -> None | str: + def message(self, message: dict, timeout=1, wait_reply=True, client=None) -> None | str: """Send a message to the StdDAQ and receive a reply Note: finishing acquisition means StdDAQ will close connection, so @@ -301,7 +294,7 @@ class StdDaqClient(PSIDeviceBase): if cfg['image_pixel_height'] != self.cfg_pixel_height.get() or cfg['image_pixel_width'] != self.cfg_pixel_width.get(): # Stop if current status is not idle if self.state() != "idle": - self.safestop() + self.surestop() # Stop running acquisition self.unstage() @@ -348,7 +341,7 @@ class StdDaqClient(PSIDeviceBase): raise ConnectionError(f"[{self.name}] Error {r.status_code}:\t{r.text}") return r.json() - def restart(self): + def nuke(self): """ Reconfigures the stdDAQ to restart the services """ cfg = self.get_daq_config() @@ -363,35 +356,43 @@ class StdDaqClient(PSIDeviceBase): r = json.loads(r) return r['status'] - def safestop(self, timeout=5): + 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 - state = self.state() for rr in range(5): - try: - r = self.message({"command": "stop_all"}) - if r is not None: - r = json.loads(r) - logger.warning(f"[{self.name}] Stop-all command {r['status']}") - else: - # NOTE: stdDAQ just closes connection if idle - logger.warning(f"[{self.name}] Stop-all command unknown") - finally: - sleep(0.2) + 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}] Stop-all command did not finish in time, nuking the stdDAQ") + self.nuke() + sleep(timeout) + + + + - # Wait until status is back to idle - if timeout > 0.2: - wait_time = 0 - while wait_time < timeout/5: - state = self.state() - if state is not None: - if state in ['idle', 'stoped']: - return - sleep(0.2) - wait_time += 0.2 # Automatically connect to microXAS testbench if directly invoked diff --git a/tomcat_bec/scans/tomcat_scans.py b/tomcat_bec/scans/tomcat_scans.py index 39e5350..5b59516 100644 --- a/tomcat_bec/scans/tomcat_scans.py +++ b/tomcat_bec/scans/tomcat_scans.py @@ -28,8 +28,7 @@ class TomcatStepScan(ScanBase): 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 software triggers. - + 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 @@ -98,13 +97,13 @@ class TomcatStepScan(ScanBase): yield from self._move_scan_motors_and_wait(pos) time.sleep(self.settling_time) - # yield from self.stubs.trigger(min_wait=trigger_time) - yield from self.stubs.trigger(group='trigger', point_id=self.point_id) - trigger_time = self.exp_time * self.burst_at_each_point - time.sleep(trigger_time) + 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, wait_group=None) + 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 @@ -195,15 +194,14 @@ class TomcatSnapNStep(AsyncFlyScanBase): def render_file(self, filename, filesubs): """Prepare action: render AeroScript file""" # Load the test file - if filename is not None: - filename = os.path.join(os.path.dirname(__file__), "../devices/aerotech/" + filename) - logger.info(f"Attempting to load file {filename}") - with open(filename) as f: - templatetext = f.read() + filename = os.path.join(os.path.dirname(__file__), "../devices/aerotech/" + filename) + logger.info(f"Attempting to load file {filename}") + with open(filename) as f: + templatetext = f.read() - # Substitute jinja template - tm = jinja2.Template(templatetext) - filetext = tm.render(scan=filesubs) + # Substitute jinja template + tm = jinja2.Template(templatetext) + filetext = tm.render(scan=filesubs) return filetext def scan_core(self): @@ -215,7 +213,7 @@ class TomcatSnapNStep(AsyncFlyScanBase): # 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() + # st.wait() 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") @@ -346,7 +344,7 @@ class TomcatSimpleSequence(AsyncFlyScanBase): # 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() + # st.wait() 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")