fix(panda-box): add signal_alias to map PandaBlock names ot beamline signal names.

This commit is contained in:
2026-02-09 17:12:35 +01:00
parent 713816560a
commit d77c9d925a
3 changed files with 113 additions and 449 deletions

View File

@@ -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.

View File

@@ -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"}
)

View File

@@ -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