From 2d23c5e0717b377dac11b87155720bef97f942fc Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Thu, 17 Apr 2025 13:01:22 +0200 Subject: [PATCH 1/3] Verifying scan compatibility --- .../device_configs/microxas_test_bed.yaml | 36 ++++++++++++ .../aerotech/AerotechDriveDataCollection.py | 56 ++++++++++--------- tomcat_bec/devices/aerotech/AerotechPso.py | 28 +++++----- tomcat_bec/devices/aerotech/AerotechTasks.py | 23 ++++---- 4 files changed, 95 insertions(+), 48 deletions(-) diff --git a/tomcat_bec/device_configs/microxas_test_bed.yaml b/tomcat_bec/device_configs/microxas_test_bed.yaml index f0a0d51..9bb6db8 100644 --- a/tomcat_bec/device_configs/microxas_test_bed.yaml +++ b/tomcat_bec/device_configs/microxas_test_bed.yaml @@ -132,6 +132,25 @@ es1_ddaq: # 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 + - gfcam + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: monitored + softwareTrigger: true + + # gfcam: # description: GigaFrost camera client # deviceClass: tomcat_bec.devices.GigaFrostCamera @@ -182,6 +201,23 @@ es1_ddaq: # readoutPriority: monitored # softwareTrigger: false + +pcocam: + description: PCO.edge camera client + deviceClass: tomcat_bec.devices.PcoEdge5M + deviceConfig: + prefix: 'X02DA-CCDCAM2:' + deviceTags: + - camera + - trigger + - pcocam + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: monitored + softwareTrigger: true + + # 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 5bcc858..3bfcc5b 100644 --- a/tomcat_bec/devices/aerotech/AerotechDriveDataCollection.py +++ b/tomcat_bec/devices/aerotech/AerotechDriveDataCollection.py @@ -116,38 +116,44 @@ class aa1AxisDriveDataCollection(PSIDeviceBase, Device): def on_stage(self) -> None: """Configuration and staging""" # Fish out configuration from scaninfo (does not need to be full configuration) + d = {} - if "kwargs" in self.scaninfo.scan_msg.info: - scanargs = self.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 "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 + scan_args = { + **self.scan_info.msg.request_inputs["inputs"], + **self.scan_info.msg.request_inputs["kwargs"], + **self.scan_info.msg.scan_parameters, + } + # NOTE: Scans don't have to fully configure the device + if "ddc_trigger" in scan_args: + d["ddc_trigger"] = scan_args["ddc_trigger"] + if "ddc_num_points" in scan_args: + d["num_points_total"] = scan_args["ddc_num_points"] + else: + # Try to figure out number of points + num_points = 1 + points_valid = False + if "steps" in scan_args and scan_args["steps"] is not None: + num_points *= scan_args["steps"] + points_valid = True + if "exp_burst" in scan_args and scan_args["exp_burst"] is not None: + num_points *= scan_args["exp_burst"] + points_valid = True + if "repeats" in scan_args and scan_args["repeats"] is not None: + num_points *= scan_args["repeats"] + points_valid = True + if "burst_at_each_point" in scan_args and scan_args["burst_at_each_point"] is not None: + num_points *= scan_args["burst_at_each_point"] + points_valid = True + if points_valid: + d["num_points_total"] = num_points # Perform bluesky-style configuration - if len(d) > 0: - logger.warning(f"[{self.name}] Configuring with:\n{d}") + if d: self.configure(d=d) # Stage the data collection if not in internally launced mode # NOTE: Scripted scans start acquiring from the scrits - if self.scaninfo.scan_type not in ("script", "scripted"): + if "scan_type" in scan_args and scan_args["scan_type"] in ("scripted", "script"): self.arm() # Reset readback self.reset() diff --git a/tomcat_bec/devices/aerotech/AerotechPso.py b/tomcat_bec/devices/aerotech/AerotechPso.py index cdd29d6..0349890 100644 --- a/tomcat_bec/devices/aerotech/AerotechPso.py +++ b/tomcat_bec/devices/aerotech/AerotechPso.py @@ -257,22 +257,24 @@ class aa1AxisPsoDistance(AerotechPsoBase): """ # Fish out configuration from scaninfo (does not need to be full configuration) d = {} - if "kwargs" in self.scaninfo.scan_msg.info: - scanargs = self.scaninfo.scan_msg.info["kwargs"] - if "pso_distance" in scanargs: - d["pso_distance"] = scanargs["pso_distance"] - if "pso_wavemode" in scanargs: - d["pso_wavemode"] = scanargs["pso_wavemode"] - if "pso_w_pulse" in scanargs: - d["pso_w_pulse"] = scanargs["pso_w_pulse"] - if "pso_t_pulse" in scanargs: - d["pso_t_pulse"] = scanargs["pso_t_pulse"] - if "pso_n_pulse" in scanargs: - d["pso_n_pulse"] = scanargs["pso_n_pulse"] + scan_args = { + **self.scan_info.msg.request_inputs["inputs"], + **self.scan_info.msg.request_inputs["kwargs"], + **self.scan_info.msg.scan_parameters, + } + if "pso_distance" in scan_args: + d["pso_distance"] = scan_args["pso_distance"] + if "pso_wavemode" in scan_args: + d["pso_wavemode"] = scan_args["pso_wavemode"] + if "pso_w_pulse" in scan_args: + d["pso_w_pulse"] = scan_args["pso_w_pulse"] + if "pso_t_pulse" in scan_args: + d["pso_t_pulse"] = scan_args["pso_t_pulse"] + if "pso_n_pulse" in scan_args: + d["pso_n_pulse"] = scan_args["pso_n_pulse"] # Perform bluesky-style configuration if d: - # logger.info(f"[{self.name}] Configuring with:\n{d}") self.configure(d=d) # Stage the PSO distance module diff --git a/tomcat_bec/devices/aerotech/AerotechTasks.py b/tomcat_bec/devices/aerotech/AerotechTasks.py index a094c0d..a29c4e0 100644 --- a/tomcat_bec/devices/aerotech/AerotechTasks.py +++ b/tomcat_bec/devices/aerotech/AerotechTasks.py @@ -129,16 +129,19 @@ class aa1Tasks(PSIDeviceBase, Device): """ # Fish out our configuration from scaninfo (via explicit or generic addressing) d = {} - if "kwargs" in self.scaninfo.scan_msg.info: - scanargs = self.scaninfo.scan_msg.info["kwargs"] - if self.scaninfo.scan_type in ("script", "scripted"): - # NOTE: Scans don't have to fully configure the device - if "script_text" in scanargs and scanargs["script_text"] is not None: - d["script_text"] = scanargs["script_text"] - if "script_file" in scanargs and scanargs["script_file"] is not None: - d["script_file"] = scanargs["script_file"] - if "script_task" in scanargs and scanargs["script_task"] is not None: - d["script_task"] = scanargs["script_task"] + scan_args = { + **self.scan_info.msg.request_inputs["inputs"], + **self.scan_info.msg.request_inputs["kwargs"], + **self.scan_info.msg.scan_parameters, + } + # if self.scan_info.scan_type in ("script", "scripted"): + # NOTE: Scans don't have to fully configure the device + if "script_text" in scan_args and scan_args["script_text"] is not None: + d["script_text"] = scan_args["script_text"] + if "script_file" in scan_args and scan_args["script_file"] is not None: + d["script_file"] = scan_args["script_file"] + if "script_task" in scan_args and scan_args["script_task"] is not None: + d["script_task"] = scan_args["script_task"] # FIXME: The above should be exchanged with: # d = self.scan_info.scan_msg.scan_parameters.get("aerotech_config") -- 2.49.1 From 672cb60e7243462f9fab816130b0d15bafa25b28 Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Thu, 17 Apr 2025 14:21:27 +0200 Subject: [PATCH 2/3] Works without stdDAQ --- tomcat_bec/devices/gigafrost/gigafrostcamera.py | 2 ++ tomcat_bec/devices/gigafrost/pco_datasink.py | 1 + 2 files changed, 3 insertions(+) diff --git a/tomcat_bec/devices/gigafrost/gigafrostcamera.py b/tomcat_bec/devices/gigafrost/gigafrostcamera.py index 8527627..7be4e01 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostcamera.py +++ b/tomcat_bec/devices/gigafrost/gigafrostcamera.py @@ -510,6 +510,8 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase): success=[StdDaqStatus.IDLE, StdDaqStatus.FILE_SAVED], error=[StdDaqStatus.REJECTED, StdDaqStatus.ERROR], ) + else: + status.set_finished() return status ######################################## diff --git a/tomcat_bec/devices/gigafrost/pco_datasink.py b/tomcat_bec/devices/gigafrost/pco_datasink.py index 214dc8c..705e239 100644 --- a/tomcat_bec/devices/gigafrost/pco_datasink.py +++ b/tomcat_bec/devices/gigafrost/pco_datasink.py @@ -96,6 +96,7 @@ class PcoTestConsumerMixin(CustomDetectorMixin): # f"[{self.parent.name}] Updated frame {header['frame']}\t" # f"Shape: {header['shape']}\tMean: {np.mean(image):.3f}" # ) + print(f"[{self.parent.name}] Updated frame {header['frame']}\t") except ValueError: # Happens when ZMQ partially delivers the multipart message pass -- 2.49.1 From b3e0a64bf1e7c74c669dfca85a91359f1096bbf4 Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Mon, 28 Apr 2025 17:37:17 +0200 Subject: [PATCH 3/3] Found out stdDAQ filename problem --- .../device_configs/microxas_test_bed.yaml | 72 +++++++++---------- .../devices/gigafrost/gigafrostcamera.py | 7 +- tomcat_bec/devices/gigafrost/pcoedgecamera.py | 41 ++++++++--- .../devices/gigafrost/std_daq_client.py | 19 +++-- tomcat_bec/scans/simple_scans.py | 0 5 files changed, 85 insertions(+), 54 deletions(-) create mode 100644 tomcat_bec/scans/simple_scans.py diff --git a/tomcat_bec/device_configs/microxas_test_bed.yaml b/tomcat_bec/device_configs/microxas_test_bed.yaml index 9bb6db8..96905cb 100644 --- a/tomcat_bec/device_configs/microxas_test_bed.yaml +++ b/tomcat_bec/device_configs/microxas_test_bed.yaml @@ -102,7 +102,7 @@ es1_psod: onFailure: buffer readOnly: false readoutPriority: monitored - softwareTrigger: true + softwareTrigger: false es1_ddaq: @@ -133,22 +133,22 @@ es1_ddaq: # 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 - - gfcam - 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 +# - gfcam +# enabled: true +# onFailure: buffer +# readOnly: false +# readoutPriority: monitored +# softwareTrigger: true # gfcam: @@ -202,30 +202,11 @@ gfcam: # softwareTrigger: false -pcocam: - description: PCO.edge camera client - deviceClass: tomcat_bec.devices.PcoEdge5M - deviceConfig: - prefix: 'X02DA-CCDCAM2:' - deviceTags: - - camera - - trigger - - pcocam - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: monitored - softwareTrigger: true - - # pcocam: # description: PCO.edge camera client # deviceClass: tomcat_bec.devices.PcoEdge5M # deviceConfig: # prefix: 'X02DA-CCDCAM2:' -# std_daq_live: 'tcp://129.129.95.111:20010' -# std_daq_ws: 'ws://129.129.95.111:8081' -# std_daq_rest: 'http://129.129.95.111:5010' # deviceTags: # - camera # - trigger @@ -236,6 +217,25 @@ pcocam: # readoutPriority: monitored # softwareTrigger: true + +pcocam: + description: PCO.edge camera client + deviceClass: tomcat_bec.devices.PcoEdge5M + deviceConfig: + prefix: 'X02DA-CCDCAM2:' + std_daq_live: 'tcp://129.129.95.111:20010' + std_daq_ws: 'ws://129.129.95.111:8081' + std_daq_rest: 'http://129.129.95.111:5010' + deviceTags: + - camera + - trigger + - pcocam + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: monitored + softwareTrigger: true + # pcodaq: # description: GigaFrost stdDAQ client # deviceClass: tomcat_bec.devices.StdDaqClient diff --git a/tomcat_bec/devices/gigafrost/gigafrostcamera.py b/tomcat_bec/devices/gigafrost/gigafrostcamera.py index 7be4e01..1f7b99c 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostcamera.py +++ b/tomcat_bec/devices/gigafrost/gigafrostcamera.py @@ -199,7 +199,7 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase): supplied signal. Use external enable instead, that works! """ - if acq_mode == "default": + if acq_mode in ["default", "step"]: # NOTE: Software trigger via softEnable (actually works) # Trigger parameters self.fix_nframes_mode = "start" @@ -630,14 +630,11 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase): and self.trigger_mode == "auto" and self.enable_mode == "soft" ): - t_start = time() # BEC teststand operation mode: posedge of SoftEnable if Started self.soft_enable.set(0).wait() self.soft_enable.set(1).wait() - logger.info(f"Elapsed: {time()-t_start}") - - if self.acquire_block.get(): + if self.acquire_block.get() or self.backend is None: wait_time = 0.2 + 0.001 * self.num_exposures.value * max( self.acquire_time.value, self.acquire_period.value ) diff --git a/tomcat_bec/devices/gigafrost/pcoedgecamera.py b/tomcat_bec/devices/gigafrost/pcoedgecamera.py index 7653bdd..f8461c2 100644 --- a/tomcat_bec/devices/gigafrost/pcoedgecamera.py +++ b/tomcat_bec/devices/gigafrost/pcoedgecamera.py @@ -59,7 +59,6 @@ class PcoEdge5M(PSIDeviceBase, PcoEdgeBase): USER_ACCESS = [ "complete", "backend", - # "acq_done", "live_preview", "arm", "disarm", @@ -125,6 +124,8 @@ class PcoEdge5M(PSIDeviceBase, PcoEdgeBase): *'FIFO buffer' for continous streaming data_format : str Usually set to 'ZEROMQ' + acq_mode : str + Store mode and data format according to preconfigured settings """ if self.state not in ("IDLE"): raise RuntimeError(f"Can't change configuration from state {self.state}") @@ -149,6 +150,10 @@ class PcoEdge5M(PSIDeviceBase, PcoEdgeBase): if "data_format" in d: self.file_format.set(d["data_format"]).wait() + # If a pre-configured acquisition mode is specified, set it + if "acq_mode" in d: + self.set_acquisition_mode(d["acq_mode"]) + # State machine # Initial: BUSY and SET both low # 0. Write 1 to SET_PARAM @@ -165,6 +170,19 @@ class PcoEdge5M(PSIDeviceBase, PcoEdgeBase): self.set_param.set(1).wait() status.wait() + def set_acquisition_mode(self, acq_mode): + """Set acquisition mode + + Utility function to quickly select between pre-configured and tested + acquisition modes. + """ + if acq_mode in ["default", "step"]: + # NOTE: Trigger duration requires a consumer + self.bufferStoreMode.set("Recorder").wait() + # self.file_format.set("ZEROMQ").wait() + else: + raise RuntimeError(f"Unsupported acquisition mode: {acq_mode}") + def arm(self): """Bluesky style stage: arm the detector""" logger.warning("Staging PCO") @@ -206,6 +224,7 @@ class PcoEdge5M(PSIDeviceBase, PcoEdgeBase): def _on_preview_update(self, img: np.ndarray, header: dict): """Send preview stream and update frame index counter""" + # FIXME: There's also a recorded images counter provided by the stdDAQ writer self.num_images_counter.put(header["frame"], force=True) self._run_subs(sub_type=self.SUB_DEVICE_MONITOR_2D, obj=self, value=img) @@ -251,8 +270,6 @@ class PcoEdge5M(PSIDeviceBase, PcoEdgeBase): } d = {} - if "exp_burst" in scan_args and scan_args["exp_burst"] is not None: - d["exposure_num_burst"] = scan_args["exp_burst"] if "image_width" in scan_args and scan_args["image_width"] is not None: d["image_width"] = scan_args["image_width"] if "image_height" in scan_args and scan_args["image_height"] is not None: @@ -261,10 +278,16 @@ class PcoEdge5M(PSIDeviceBase, PcoEdgeBase): d["exposure_time_ms"] = scan_args["exp_time"] if "exp_period" in scan_args and scan_args["exp_period"] is not None: d["exposure_period_ms"] = scan_args["exp_period"] - # if 'exp_burst' in scan_args and scan_args['exp_burst'] is not None: - # d['exposure_num_burst'] = scan_args['exp_burst'] - # if 'acq_mode' in scan_args and scan_args['acq_mode'] is not None: - # d['acq_mode'] = scan_args['acq_mode'] + if 'exp_burst' in scan_args and scan_args['exp_burst'] is not None: + d['exposure_num_burst'] = scan_args['exp_burst'] + if "acq_time" in scan_args and scan_args["acq_time"] is not None: + d["exposure_time_ms"] = scan_args["acq_time"] + if "acq_period" in scan_args and scan_args["acq_period"] is not None: + d["exposure_period_ms"] = scan_args["acq_period"] + if 'acq_burst' in scan_args and scan_args['acq_burst'] is not None: + d['exposure_num_burst'] = scan_args['acq_burst'] + if 'acq_mode' in scan_args and scan_args['acq_mode'] is not None: + d['acq_mode'] = scan_args['acq_mode'] # elif self.scaninfo.scan_type == "step": # d['acq_mode'] = "default" if "pco_store_mode" in scan_args and scan_args["pco_store_mode"] is not None: @@ -305,6 +328,7 @@ class PcoEdge5M(PSIDeviceBase, PcoEdgeBase): def on_pre_scan(self) -> DeviceStatus | None: """Called right before the scan starts on all devices automatically.""" + logger.warning("Called op_prescan on PCO camera") # First start the stdDAQ if self.backend is not None: self.backend.start( @@ -371,8 +395,7 @@ class PcoEdge5M(PSIDeviceBase, PcoEdgeBase): def on_complete(self) -> DeviceStatus | None: """Called to inquire if a device has completed a scans.""" - # return self.acq_done() - return None + return self.acq_done() def on_kickoff(self) -> DeviceStatus | None: """Start data transfer diff --git a/tomcat_bec/devices/gigafrost/std_daq_client.py b/tomcat_bec/devices/gigafrost/std_daq_client.py index f5b8caf..56cca38 100644 --- a/tomcat_bec/devices/gigafrost/std_daq_client.py +++ b/tomcat_bec/devices/gigafrost/std_daq_client.py @@ -48,9 +48,10 @@ class StdDaqStatus(str, enum.Enum): class StdDaqClient: - USER_ACCESS = ["status", "start", "stop", "get_config", "set_config", "reset", "_status"] + USER_ACCESS = ["status", "count", "start", "stop", "get_config", "set_config", "reset", "_status"] _ws_client: ws.ClientConnection | None = None + _count: int = 0 _status: StdDaqStatus = StdDaqStatus.UNDEFINED _status_timestamp: float | None = None _ws_recv_mutex = threading.Lock() @@ -81,6 +82,11 @@ class StdDaqClient: """ return self._status + @property + def count(self) -> int: + """ Get the recorded frame count""" + return self._count + def add_status_callback( self, status: DeviceStatus, success: list[StdDaqStatus], error: list[StdDaqStatus] ): @@ -113,15 +119,17 @@ class StdDaqClient: # Ensure connection self.wait_for_connection() - logger.info(f"Starting StdDaq backend. Current status: {self.status}") status = StatusBase() - self.add_status_callback(status, success=["waiting_for_first_image"], error=["rejected"]) + # NOTE: CREATING_FILE --> IDLE is a known error, the exact cause is unknown, nut might be botched overwrite protection + # Changing file_prefix often solves the problem, but still allows overwrites + self.add_status_callback(status, success=["waiting_for_first_image"], error=["rejected", "idle"]) message = { "command": "start", "path": file_path, "file_prefix": file_prefix, "n_image": num_images, } + logger.info(f"Starting StdDaq backend. Current status: {self.status}. Message: {message}") self._ws_client.send(json.dumps(message)) if wait: status.wait(timeout=timeout) @@ -327,7 +335,10 @@ class StdDaqClient: continue msg = json.loads(msg) if self._status != msg["status"]: - logger.info(f"stdDAQ state transition by: {msg}") + logger.warning(f"stdDAQ state transition: {self._status} --> {msg}") + if msg["status"] == "recording": + self._count = msg.get("count", 0) + # Update status and run callbacks self._status = msg["status"] self._status_timestamp = msg_timestamp self._run_status_callbacks() diff --git a/tomcat_bec/scans/simple_scans.py b/tomcat_bec/scans/simple_scans.py new file mode 100644 index 0000000..e69de29 -- 2.49.1