From 2768f2b53416c081049ae15c6d529822423317f1 Mon Sep 17 00:00:00 2001 From: appel_c Date: Mon, 9 Feb 2026 17:12:35 +0100 Subject: [PATCH] fix(panda-box): add signal_alias to map PandaBlock names ot beamline signal names. --- device_test_reports/report_demo_config.txt | 434 ------------------- ophyd_devices/devices/panda_box/panda_box.py | 78 +++- ophyd_devices/devices/panda_box/utils.py | 50 +++ 3 files changed, 113 insertions(+), 449 deletions(-) delete mode 100644 device_test_reports/report_demo_config.txt create mode 100644 ophyd_devices/devices/panda_box/utils.py diff --git a/device_test_reports/report_demo_config.txt b/device_test_reports/report_demo_config.txt deleted file mode 100644 index e6ce2af..0000000 --- a/device_test_reports/report_demo_config.txt +++ /dev/null @@ -1,434 +0,0 @@ -Checking eiger... -OK -Checking dyn_signals... -OK -Checking pseudo_signal1... -OK -Checking hexapod... -OK -Checking eyefoc... -OK -Checking eyex... -OK -Checking eyey... -OK -Checking flyer_sim... -OK -Checking hrox... -OK -Checking hroy... -OK -Checking hroz... -OK -Checking hx... -OK -Checking hy... -OK -Checking hz... -OK -Checking mbsx... -OK -Checking mbsy... -OK -Checking pinx... -OK -Checking piny... -OK -Checking pinz... -OK -Checking samx... -OK -Checking samy... -OK -Checking samz... -OK -Checking bpm3a... -OK -Checking bpm3b... -OK -Checking bpm3c... -OK -Checking bpm3d... -OK -Checking bpm3i... -OK -Checking bpm3x... -OK -Checking bpm3y... -OK -Checking bpm3z... -OK -Checking bpm4a... -OK -Checking bpm4b... -OK -Checking bpm4c... -OK -Checking bpm4d... -OK -Checking bpm4i... -OK -Checking bpm4s... -OK -Checking bpm4x... -OK -Checking bpm4xf... -OK -Checking bpm4xm... -OK -Checking bpm4y... -OK -Checking bpm4yf... -OK -Checking bpm4ym... -OK -Checking bpm4z... -OK -Checking bpm5a... -OK -Checking bpm5b... -OK -Checking bpm5c... -OK -Checking bpm5d... -OK -Checking bpm5i... -OK -Checking bpm5x... -OK -Checking bpm5y... -OK -Checking bpm5z... -OK -Checking bpm6a... -OK -Checking bpm6b... -OK -Checking bpm6c... -OK -Checking bpm6d... -OK -Checking bpm6i... -OK -Checking bpm6x... -OK -Checking bpm6y... -OK -Checking bpm6z... -OK -Checking curr... -OK -Checking diode... -OK -Checking ebpmdx... -OK -Checking ebpmdy... -OK -Checking ebpmux... -OK -Checking ebpmuy... -OK -Checking ftp... -OK -Checking temp... -OK -Checking transd... -OK -Checking aptrx... -OK -Checking aptry... -OK -Checking bim2x... -OK -Checking bim2y... -OK -Checking bm1trx... -OK -Checking bm1try... -OK -Checking bm2trx... -OK -Checking bm2try... -OK -Checking bm3trx... -OK -Checking bm3try... -OK -Checking bm4trx... -OK -Checking bm4try... -OK -Checking bm5trx... -OK -Checking bm5try... -OK -Checking bm6trx... -OK -Checking bm6try... -OK -Checking bpm4r... -OK -Checking bpm5r... -OK -Checking bs1x... -OK -Checking bs1y... -OK -Checking bs2x... -OK -Checking bs2y... -OK -Checking burstn... -OK -Checking burstr... -OK -Checking ddg1a... -OK -Checking ddg1b... -OK -Checking ddg1c... -OK -Checking ddg1d... -OK -Checking ddg1e... -OK -Checking ddg1f... -OK -Checking ddg1g... -OK -Checking ddg1h... -OK -Checking dettrx... -OK -Checking di2trx... -OK -Checking di2try... -OK -Checking dtpush... -OK -Checking dtth... -OK -Checking dttrx... -OK -Checking dttry... -OK -Checking dttrz... -OK -Checking ebcsx... -OK -Checking ebcsy... -OK -Checking ebfi1... -OK -Checking ebfi2... -OK -Checking ebfi3... -OK -Checking ebfi4... -OK -Checking ebfzpx... -OK -Checking ebfzpy... -OK -Checking ebtrx... -OK -Checking ebtry... -OK -Checking ebtrz... -OK -Checking fi1try... -OK -Checking fi2try... -OK -Checking fi3try... -OK -Checking fsh1x... -OK -Checking fsh2x... -OK -Checking ftrans... -OK -Checking fttrx1... -OK -Checking fttrx2... -OK -Checking fttry1... -OK -Checking fttry2... -OK -Checking fttrz... -OK -Checking idgap... -OK -Checking mibd... -OK -Checking mibd1... -OK -Checking mibd2... -OK -Checking miroll... -OK -Checking mith... -OK -Checking mitrx... -OK -Checking mitry... -OK -Checking mitry1... -OK -Checking mitry2... -OK -Checking mitry3... -OK -Checking mobd... -OK -Checking mobdai... -OK -Checking mobdbo... -OK -Checking mobdco... -OK -Checking mobddi... -OK -Checking mokev... -OK -Checking mopush1... -OK -Checking mopush2... -OK -Checking moroll1... -OK -Checking moroll2... -OK -Checking moth1... -OK -Checking moth1e... -OK -Checking moth2... -OK -Checking moth2e... -OK -Checking motrx2... -OK -Checking motry... -OK -Checking motry2... -OK -Checking motrz1... -OK -Checking motrz1e... -OK -Checking moyaw2... -OK -Checking sl0ch... -OK -Checking sl0trxi... -OK -Checking sl0trxo... -OK -Checking sl0wh... -OK -Checking sl1ch... -OK -Checking sl1cv... -OK -Checking sl1trxi... -OK -Checking sl1trxo... -OK -Checking sl1tryb... -OK -Checking sl1tryt... -OK -Checking sl1wh... -OK -Checking sl1wv... -OK -Checking sl2ch... -OK -Checking sl2cv... -OK -Checking sl2trxi... -OK -Checking sl2trxo... -OK -Checking sl2tryb... -OK -Checking sl2tryt... -OK -Checking sl2wh... -OK -Checking sl2wv... -OK -Checking sl3ch... -OK -Checking sl3cv... -OK -Checking sl3trxi... -OK -Checking sl3trxo... -OK -Checking sl3tryb... -OK -Checking sl3tryt... -OK -Checking sl3wh... -OK -Checking sl3wv... -OK -Checking sl4ch... -OK -Checking sl4cv... -OK -Checking sl4trxi... -OK -Checking sl4trxo... -OK -Checking sl4tryb... -OK -Checking sl4tryt... -OK -Checking sl4wh... -OK -Checking sl4wv... -OK -Checking sl5ch... -OK -Checking sl5cv... -OK -Checking sl5trxi... -OK -Checking sl5trxo... -OK -Checking sl5tryb... -OK -Checking sl5tryt... -OK -Checking sl5wh... -OK -Checking sl5wv... -OK -Checking strox... -OK -Checking stroy... -OK -Checking stroz... -OK -Checking sttrx... -OK -Checking sttry... -OK -Checking ring_current_sim... -OK -Checking monitor_async... -OK -Checking rt_controller... -OK -Checking waveform... -OK - - - -======================================== -Summary: -All devices passed the test. diff --git a/ophyd_devices/devices/panda_box/panda_box.py b/ophyd_devices/devices/panda_box/panda_box.py index b8c9a60..0de062c 100644 --- a/ophyd_devices/devices/panda_box/panda_box.py +++ b/ophyd_devices/devices/panda_box/panda_box.py @@ -11,7 +11,7 @@ into the on_connected, stage, unstage, pre_scan, kickoff, complete methods as ne be aware that the on_connected method is wrapped in here and should therefore always call super().on_connected(). Child integrations should register data callbacks to handle incoming data from the PandaBox during acquisition, and set the respective data on their ophyd signals. -The utility method _compile_frame_data_to_dict can be used to convert FrameData objects received +The utility method convert_frame_data can be used to convert FrameData objects received to the expected signal dict format. Please be aware that naming conventions here map to the names of the blocks, and should be mapped to the beamline specific signal names in the child class. @@ -31,11 +31,14 @@ from typing import TYPE_CHECKING, Any, Callable, TypeAlias, Union import pandablocks.commands as pbc from bec_lib import bec_logger +from ophyd import Component as Cpt +from ophyd import Staged from ophyd.status import WaitTimeoutError from pandablocks.blocking import BlockingClient from pandablocks.responses import Data, EndData, FrameData, ReadyData, StartData -from ophyd_devices import PSIDeviceBase, StatusBase +from ophyd_devices import DynamicSignal, PSIDeviceBase, StatusBase +from ophyd_devices.devices.panda_box.utils import get_pcap_capture_fields if TYPE_CHECKING: # pragma: no cover from bec_lib.devicemanager import ScanInfo @@ -153,8 +156,23 @@ class PandaBox(PSIDeviceBase): to integrate pre-defined PandaBox layout directly into the BEC scan interface, stage/unstage, trigger/complete, pre_scan or kickoff methods. + A signal_alias can be provided during initialization to specify the mapping from PandaBox signal names to + beamline specific signal names. Any signal that is found in the data frames will be automatically + mapped to the provided signal names. If data is received for a signal that is not included in the signal_alias, + the original name from the PandaBox will be used as the signal name. Signal config should be provided as a + dict with keys corresponding to the signal names from the PandaBox, and values corresponding to the desired + signal names to be used in the data frames. """ + data = Cpt( + DynamicSignal, + name="data", + ndim=0, + max_size=1000, + signals=get_pcap_capture_fields(), + async_update={"type": "add", "max_shape": [None]}, + ) + USER_ACCESS = ["send_raw", "add_status_callback", "remove_status_callback", "get_panda_state"] def __init__( @@ -162,10 +180,15 @@ class PandaBox(PSIDeviceBase): *, name: str, host: str, + signal_alias: dict[str, str] | None = None, scan_info: ScanInfo | None = None, device_manager: DeviceManagerDS | None = None, **kwargs, ) -> None: + self.signal_alias = signal_alias if signal_alias is not None else {} + kwargs.pop( + "signal_alias", None + ) # Remove signal_alias from kwargs to avoid issues with super().__init__() super().__init__(name=name, scan_info=scan_info, device_manager=device_manager, **kwargs) self.host = host @@ -186,6 +209,19 @@ class PandaBox(PSIDeviceBase): self.data_thread_kill_event = threading.Event() self.data_thread_run_event = threading.Event() + # Acquisition group of the PandaBox data. + self._acquisition_group = "panda" + + def on_init(self): + """Initialize the PandaBox device. This method can be used to perform any additional initialization logic.""" + super().on_init() + new_names = [ + self.signal_alias.get(original_name, original_name) + for original_name, _ in self.data.signals + ] + # Unify names for data + self.data.signals = self.data._unify_signals(new_names) + ########################## ### Public API methods ### ########################## @@ -486,6 +522,13 @@ class PandaBox(PSIDeviceBase): raise e from e super().on_connected() self.data_thread.start() + self.add_data_callback(data_type=PandaState.FRAME.value, callback=self._receive_frame_data) + + def _receive_frame_data(self, data: FrameData) -> None: + logger.info(f"Received frame data with signals {data}") + out = self.convert_frame_data(frame_data=data) + logger.info(f"Compiled data {out}") + self.data.put(out, acquisition_group=self._acquisition_group) def stop(self, *, success=False): """ @@ -493,8 +536,8 @@ class PandaBox(PSIDeviceBase): We call this prior to the super().stop() call to ensure that the PandaBox is disarmed before any additional stopping logic from child classes is executed. """ - super().stop(success=success) self._disarm() + super().stop(success=success) def destroy(self): """ @@ -522,6 +565,9 @@ class PandaBox(PSIDeviceBase): list[object] | StatusBase: The result of the super().stage() call. """ # First make sure that the data readout loop is not running + if self.staged != Staged.no: + return super().stage() + self.stopped = False status = StatusBase(obj=self) self.add_status_callback(status=status, success=[PandaState.DISARMED], failure=[]) try: @@ -530,7 +576,7 @@ class PandaBox(PSIDeviceBase): logger.error(f"PandaBox {self.name} did not disarm before staging.") # pylint: disable=raise-from-missing raise RuntimeError( - f"PandaBox {self.name} did not disarm properly. Please connection and the device integration." + f"PandaBox {self.name} did not disarm properly. Please check the connection and the device integration." ) ret = super().stage() @@ -545,9 +591,6 @@ class PandaBox(PSIDeviceBase): """ status = super().pre_scan() status_ready_data_received = StatusBase(obj=self) - self.cancel_on_stop( - status_ready_data_received - ) # Make sure we cancel if the scan is stopped status_ready_data_received.add_callback(self._pre_scan_status_callback) self.add_status_callback( status=status_ready_data_received, @@ -559,6 +602,7 @@ class PandaBox(PSIDeviceBase): ret_status = status_ready_data_received & status else: ret_status = status_ready_data_received + self.cancel_on_stop(ret_status) # Make sure status is cancelled if externally stopped return ret_status def unstage(self) -> list[object] | StatusBase: @@ -584,33 +628,30 @@ class PandaBox(PSIDeviceBase): ret = self.send_raw("*CAPTURE?") return [key.split(" ")[0].strip("!") for key in ret if key.strip(".")] - def _compile_frame_data_to_dict( - self, frame_data: FrameData, signal_name_key_mapping: dict[str, str] | None = None - ) -> dict[str, Any]: + def convert_frame_data(self, frame_data: FrameData) -> dict[str, Any]: """ - Compile the data from a FrameData object into a dictionary with expected OPHYD + Convert the data from a FrameData object into a dictionary with expected OPHYD read format, e.g. signal {signal_name: {"value": [...]}}. Args: frame_data (FrameData): The FrameData object received from the PandaBox. Returns: - dict[str, Any]: The compiled data in OPHYD read format. + dict[str, Any]: The converted data in OPHYD read format. """ - if signal_name_key_mapping is None: - signal_name_key_mapping = {} # Create output dict out = {} data = frame_data.data keys = data.dtype.names # Map keys if mapping is provided - mapped_key = [signal_name_key_mapping.get(key, key) for key in keys] + mapped_key = [self.signal_alias.get(key, key) for key in keys] # Initialize lists for each key, consider adjusting names to match for k in mapped_key: out[k] = {"value": []} # Timestamp? for entry in data: for i, k in enumerate(mapped_key): out[k]["value"].append(entry[i]) # Fill values from data + return out def _pre_scan_status_callback(self, status: StatusBase): """ @@ -635,3 +676,10 @@ class PandaBox(PSIDeviceBase): def _disarm(self) -> None: """Disarm the PandaBox device.""" self._send_command(pbc.Disarm()) + + +if __name__ == "__main__": + # Example usage of the PandaBox class + panda_box = PandaBox( + name="PandaBox1", host="localhost", signal_alias={"long_list": "mapped_signal_name"} + ) diff --git a/ophyd_devices/devices/panda_box/utils.py b/ophyd_devices/devices/panda_box/utils.py new file mode 100644 index 0000000..89d0310 --- /dev/null +++ b/ophyd_devices/devices/panda_box/utils.py @@ -0,0 +1,50 @@ +PANDA_AVAIL_PCAP_BLOCKS = [ + "INENC1.VAL", + "INENC2.VAL", + "INENC3.VAL", + "INENC4.VAL", + "PCAP.TS_START", + "PCAP.TS_END", + "PCAP.TS_TRIG", + "PCAP.GATE_DURATION", + "PCAP.BITS0", + "PCAP.BITS1", + "PCAP.BITS2", + "PCAP.BITS3", + "CALC1.OUT", + "CALC2.OUT", + "COUNTER1.OUT", + "COUNTER2.OUT", + "COUNTER3.OUT", + "COUNTER4.OUT", + "COUNTER5.OUT", + "COUNTER6.OUT", + "COUNTER7.OUT", + "COUNTER8.OUT", + "FILTER1.OUT", + "FILTER2.OUT", + "PGEN1.OUT", + "PGEN2.OUT", + "FMC_IN.VAL1", + "FMC_IN.VAL2", + "FMC_IN.VAL3", + "FMC_IN.VAL4", + "FMC_IN.VAL5", + "FMC_IN.VAL6", + "FMC_IN.VAL7", + "FMC_IN.VAL8", + "SFP3_SYNC_IN.POS1", + "SFP3_SYNC_IN.POS2", + "SFP3_SYNC_IN.POS3", + "SFP3_SYNC_IN.POS4", +] + +PANDA_AVAIL_PCAP_CAPTURE_FIELDS = ["Value", "Diff", "Sum", "Mean", "Min", "Max"] + + +def get_pcap_capture_fields(): + out = [] + for block in PANDA_AVAIL_PCAP_BLOCKS: + for field in PANDA_AVAIL_PCAP_CAPTURE_FIELDS: + out.append(f"{block}.{field}") + return out