From 42a88f7d25fa089618da98c3ea8131e745d07445 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Mon, 2 Jun 2025 18:44:51 +0200 Subject: [PATCH] feat: restructure bec signals --- ophyd_devices/sim/sim_camera.py | 2 +- ophyd_devices/sim/sim_test_devices.py | 2 +- ophyd_devices/utils/bec_signals.py | 449 +++++++++++++++++++++----- tests/test_utils.py | 83 ++++- 4 files changed, 434 insertions(+), 102 deletions(-) diff --git a/ophyd_devices/sim/sim_camera.py b/ophyd_devices/sim/sim_camera.py index 2aabb5c..97f5e2b 100644 --- a/ophyd_devices/sim/sim_camera.py +++ b/ophyd_devices/sim/sim_camera.py @@ -39,7 +39,7 @@ class SimCameraControl(Device): compute_readback=True, kind=Kind.omitted, ) - preview = Cpt(PreviewSignal, name="preview", ndim=2) + preview = Cpt(PreviewSignal, name="preview", ndim=2, num_rotation_90=0) write_to_disk = Cpt(SetableSignal, name="write_to_disk", value=False, kind=Kind.config) def __init__(self, name, *, parent=None, sim_init: dict = None, device_manager=None, **kwargs): diff --git a/ophyd_devices/sim/sim_test_devices.py b/ophyd_devices/sim/sim_test_devices.py index 99b1000..7d6737d 100644 --- a/ophyd_devices/sim/sim_test_devices.py +++ b/ophyd_devices/sim/sim_test_devices.py @@ -329,7 +329,7 @@ class SimCameraWithPSIComponents(SimCamera): file_event = Cpt(FileEventSignal, doc="File event signal") progress = Cpt(ProgressSignal, doc="Progress signal") dynamic_signal = Cpt( - DynamicSignal, doc="Dynamic signals", signal_names=["preview_2d", "preview_1d"] + DynamicSignal, doc="Dynamic signals", signals=["dyn_signal1", "dyn_signal2"] ) # TODO Handling of AsyncComponents is postponed, issue #104 is created diff --git a/ophyd_devices/utils/bec_signals.py b/ophyd_devices/utils/bec_signals.py index abad070..d7b115e 100644 --- a/ophyd_devices/utils/bec_signals.py +++ b/ophyd_devices/utils/bec_signals.py @@ -7,16 +7,58 @@ from typing import Any, Callable, Literal, Type import numpy as np from bec_lib import messages +from bec_lib.logger import bec_logger from ophyd import DeviceStatus, Kind, Signal -from pydantic import ValidationError -from typeguard import typechecked +from pydantic import BaseModel, Field, ValidationError +logger = bec_logger.logger # pylint: disable=arguments-differ # pylint: disable=arguments-renamed # pylint: disable=too-many-arguments # pylint: disable=signature-differs +class SignalInfo(BaseModel): + """ + Base class for signal information. + This is used to store metadata about the signal. + """ + + data_type: Literal["raw", "processed"] = Field( + default="raw", + description="The data type of the signal indicates whether the signal is raw data or processed data.", + ) + saved: bool = Field(default=True, description="Indicates whether the signal is saved to disk.") + ndim: Literal[0, 1, 2] | None = Field( + default=None, + description="The number of dimensions of the signal. If None, the signal is not expected to have a shape. " + "If set to 0, the signal is expected to be a scalar. For signals with multiple sub-signals, " + "ndim is expected to be valid for all sub-signals.", + ) + scope: Literal["scan", "continuous"] = Field( + default="scan", + description="The scope of the signal indicates whether it is relevant for a specific " + "scan or provides continuous updates, independent of a scan.", + ) + role: Literal["main", "preview", "diagnostic", "file event", "progress"] = Field( + default="main", + description="The role of the signal provides context for its usage and allows other components to filter" + " or prioritize signals based on their intended function.", + ) + enabled: bool = True + rpc_access: bool = Field( + default=False, + description="Indicates whether the signal is accessible via RPC. If False, the signal is not shown in the RPC interface.", + ) + signals: list[tuple[str, int]] | None = Field( + default=None, description="List of sub-signals with their kinds." + ) + signal_metadata: dict | None = Field( + default=None, + description="Metadata for the signal, which can include additional information about the signal's properties.", + ) + + class BECMessageSignal(Signal): """ Custom signal class that accepts BECMessage objects as values. @@ -29,7 +71,18 @@ class BECMessageSignal(Signal): *, bec_message_type: Type[messages.BECMessage], value: messages.BECMessage | dict | None = None, - kind: Kind | str = Kind.omitted, + data_type: Literal["raw", "processed"] = "raw", + saved: bool = True, + ndim: Literal[0, 1, 2] | None = None, + scope: Literal["scan", "continuous"] = "scan", + role: Literal["main", "preview", "diagnostic", "file event", "progress"] = "main", + enabled: bool = True, + signals: ( + Callable[[], list[str]] + | Callable[[], list[tuple[str, str | Kind]]] + | list[tuple[str, str | Kind] | str] + | None + ) = None, signal_metadata: dict | None = None, **kwargs, ): @@ -40,7 +93,6 @@ class BECMessageSignal(Signal): name (str) : The name of the signal. bec_message_type: type[BECMessage] : The type of BECMessage to accept as values. value (BECMessage | dict | None) : The value of the signal. Defaults to None. - kind (Kind | str) : The kind of the signal. Defaults to Kind.omitted. """ if isinstance(value, dict): value = bec_message_type(**value) @@ -48,15 +100,73 @@ class BECMessageSignal(Signal): raise ValueError( f"Value must be a {bec_message_type.__name__} or a dict for signal {name}" ) - self.signal_metadata = signal_metadata - self._bec_message_type = bec_message_type kwargs.pop("dtype", None) # Ignore dtype if specified kwargs.pop("shape", None) # Ignore shape if specified - super().__init__(name=name, value=value, shape=(), dtype=None, kind=kind, **kwargs) + kind = kwargs.pop("kind", None) # Ignore kind if specified + if kind is not None: + logger.warning("The 'kind' argument is ignored for BECMessageSignal. Please remove it.") + super().__init__(name=name, value=value, shape=(), dtype=None, kind=Kind.omitted, **kwargs) + + self.data_type = data_type + self.saved = saved + self.ndim = ndim if ndim is not None else 0 + self.scope = scope + self.role = role + self.enabled = enabled + self.signals = self._unify_signals(signals) + self.signal_metadata = signal_metadata + self._bec_message_type = bec_message_type + + def _unify_signals( + self, signals: Callable[[], list[str]] | list[tuple[str, str | Kind] | str] | str | None + ) -> list[tuple[str, int]]: + """ + Unify the signals list to a list of tuples with signal name and kind. + + Args: + signals (list[tuple[str, str | Kind] | str]): The list of signals to unify. + + Returns: + list[tuple[str, str]]: The unified list of signals. + """ + if isinstance(signals, Callable): + signals = signals() + if signals is None: + return [(self.attr_name or self.name, Kind.hinted.value)] + if isinstance(signals, str): + return [(signals, Kind.hinted.value)] + if not isinstance(signals, list): + raise ValueError( + f"Signals must be a list of tuples or strings, got {type(signals).__name__}." + ) + out = [] + for signal in signals: + if isinstance(signal, str): + out.append((signal, Kind.normal.value)) + elif isinstance(signal, tuple) and len(signal) == 2: + if isinstance(signal[1], Kind): + out.append((signal[0], signal[1].value)) + else: + out.append((signal[0], signal[1])) + else: + raise ValueError( + f"Invalid signal format: {signal}. Expected a tuple of (name, kind) or a string." + ) + return out def describe(self): out = super().describe() - out[self.name]["signal_metadata"] = self.signal_metadata or {} + + out[self.name]["signal_info"] = SignalInfo( + data_type=self.data_type, # type: ignore + saved=self.saved, + ndim=self.ndim, # type: ignore + scope=self.scope, # type: ignore + role=self.role, # type: ignore + enabled=self.enabled, + signals=self.signals, + signal_metadata=self.signal_metadata, + ).model_dump() return out @property @@ -121,9 +231,14 @@ class ProgressSignal(BECMessageSignal): kwargs.pop("kind", None) # Ignore kind if specified super().__init__( name=name, + data_type="raw", + saved=False, + ndim=0, + scope="scan", + role="progress", + signal_metadata=None, value=value, bec_message_type=messages.ProgressMessage, - kind=Kind.omitted, **kwargs, ) @@ -145,21 +260,51 @@ class ProgressSignal(BECMessageSignal): Otherwise, at least value, max_value and done must be provided. Args: - msg (ProgressMessage | dict | None) : The progress message. - value (float) : The current progress value. - max_value (float) : The maximum progress value. - done (bool) : Whether the progress is done. - metadata (dict | None) : Additional metadata + msg (ProgressMessage | dict | None): The progress message. + value (float): The current progress value. + max_value (float): The maximum progress value. + done (bool): Whether the progress is done. + metadata (dict | None): Additional metadata """ - # msg is ProgressMessage or dict - if isinstance(msg, (messages.ProgressMessage, dict)): + if msg is None and (value is None or max_value is None or done is None): + raise ValueError( + "Either msg must be provided or value, max_value and done must be set." + ) + + if isinstance(msg, messages.ProgressMessage): + if ( + value is not None + or max_value is not None + or done is not None + or metadata is not None + ): + logger.warning( + "Ignoring value, max_value, done and metadata arguments when msg is provided." + ) return super().put(msg, **kwargs) + + if isinstance(msg, dict): + if ( + value is not None + or max_value is not None + or done is not None + or metadata is not None + ): + logger.warning( + "Ignoring value, max_value, done and metadata arguments when msg is provided as dict." + ) + return super().put(msg, **kwargs) + + if value is None or max_value is None or done is None: + raise ValueError("If msg is not provided, value, max_value and done must be set.") + try: msg = messages.ProgressMessage( value=value, max_value=max_value, done=done, metadata=metadata ) except ValidationError as exc: raise ValueError(f"Error setting signal {self.name}: {exc}") from exc + return super().put(msg, **kwargs) def set( @@ -180,11 +325,11 @@ class ProgressSignal(BECMessageSignal): Otherwise, at least value, max_value and done must be provided. Args: - msg (ProgressMessage | dict | None) : The progress message. - value (float) : The current progress value. - max_value (float) : The maximum progress value. - done (bool) : Whether the progress is done. - metadata (dict | None) : Additional metadata + msg (ProgressMessage | dict | None): The progress message. + value (float): The current progress value. + max_value (float): The maximum progress value. + done (bool): Whether the progress is done. + metadata (dict | None): Additional metadata """ self.put(msg=msg, value=value, max_value=max_value, done=done, metadata=metadata, **kwargs) status = DeviceStatus(device=self) @@ -207,9 +352,14 @@ class FileEventSignal(BECMessageSignal): kwargs.pop("kind", None) # Ignore kind if specified super().__init__( name=name, + data_type="raw", + saved=False, + ndim=0, + scope="scan", + role="file event", + signal_metadata=None, value=value, bec_message_type=messages.FileMessage, - kind=Kind.omitted, **kwargs, ) @@ -220,7 +370,6 @@ class FileEventSignal(BECMessageSignal): file_path: str | None = None, done: bool | None = None, successful: bool | None = None, - device_name: str | None = None, file_type: str = "h5", hinted_h5_entries: dict[str, str] | None = None, metadata: dict | None = None, @@ -234,18 +383,53 @@ class FileEventSignal(BECMessageSignal): Otherwise, at least file_path, done and successful must be provided. Args: - msg (FileMessage | dict | None) : The file event message. - file_path (str) : The path of the file. - done (bool) : Whether the file event is done. - successful (bool) : Whether the file event was successful. - device_name (str | None) : The name of the device. - file_type (str | None) : The type of the file. + msg (FileMessage | dict | None): The file event message. + file_path (str | None): The path of the file. + done (bool | None): Whether the file event is done. + successful (bool | None): Whether the file writing finished successfully. + file_type (str): The type of the file, defaults to "h5". hinted_h5_entries (dict[str, str] | None): The hinted h5 entries. - metadata (dict | None) : Additional metadata. + metadata (dict | None): Additional metadata. + """ - if isinstance(msg, (messages.FileMessage, dict)): + device_name = self.root.name if self.root else self.name + if msg is None and (file_path is None or done is None or successful is None): + raise ValueError( + "Either msg must be provided or file_path, done and successful must be set." + ) + if isinstance(msg, messages.FileMessage): + if ( + file_path is not None + or done is not None + or successful is not None + or file_type != "h5" + or hinted_h5_entries is not None + or metadata is not None + ): + logger.warning( + "Ignoring file_path, done, successful, file_type, " + "hinted_h5_entries and metadata arguments when msg is provided." + ) + msg.device_name = device_name return super().put(msg, **kwargs) - # kwargs provided + if isinstance(msg, dict): + if ( + file_path is not None + or done is not None + or successful is not None + or file_type != "h5" + or hinted_h5_entries is not None + or metadata is not None + ): + logger.warning( + "Ignoring file_path, done, successful, device_name, file_type, " + "hinted_h5_entries and metadata arguments when msg is provided as dict." + ) + msg["device_name"] = device_name + return super().put(msg, **kwargs) + + if file_path is None or done is None or successful is None: + raise ValueError("If msg is not provided, file_path, done and successful must be set.") try: msg = messages.FileMessage( file_path=file_path, @@ -282,14 +466,14 @@ class FileEventSignal(BECMessageSignal): Otherwise, at least file_path, done and successful must be provided. Args: - msg (FileMessage | dict | None) : The file event message. - file_path (str) : The path of the file. - done (bool) : Whether the file event is done. - successful (bool) : Whether the file event was successful. - device_name (str | None) : The name of the device. - file_type (str | None) : The type of the file. + msg (FileMessage | dict | None): The file event message. + file_path (str | None): The path of the file. + done (bool | None): Whether the file event is done. + successful (bool | None): Whether the file writing finished successfully. + device_name (str | None): The name of the device. + file_type (str): The type of the file, defaults to "h5". hinted_h5_entries (dict[str, str] | None): The hinted h5 entries. - metadata (dict | None) : Additional metadata. + metadata (dict | None): Additional metadata. """ self.put( msg=msg, @@ -321,27 +505,44 @@ class PreviewSignal(BECMessageSignal): **kwargs, ): """ - Create a new FileEventSignal object. + Create a new PreviewSignal object. + For 2D data, it can be rotated by 90 degrees counter-clockwise and / or transposed for visualization. These modifications + are applied directly to the data before it is sent to BEC. Args: - name (str) : The name of the signal. - ndim (Literal[1, 2]) : The number of dimensions. - value (DeviceMonitorMessage | dict | None) : The initial value of the signal. Defaults to None. + name (str): The name of the signal. + ndim (Literal[1, 2]): The number of dimensions. + num_rotation_90 (Literal[0, 1, 2, 3]): The number of 90 degree counter-clockwise rotations to apply to the data for visualization. + transpose (bool): Whether to transpose the data for visualization. + value (DeviceMonitorMessage | dict | None): The initial value of the signal. Defaults to None. """ + self.num_rotation_90 = num_rotation_90 + self.transpose = transpose kwargs.pop("kind", None) super().__init__( name=name, + data_type="raw", + saved=False, + ndim=ndim, + scope="scan", + role="preview", value=value, bec_message_type=messages.DevicePreviewMessage, - kind=Kind.omitted, - signal_metadata={ - "ndim": ndim, - "num_rotation_90": num_rotation_90, - "transpose": transpose, - }, + signal_metadata={"num_rotation_90": num_rotation_90, "transpose": transpose}, **kwargs, ) + def _process_data(self, value: np.ndarray) -> np.ndarray: + if self.ndim == 1: + return value + + if self.num_rotation_90: + value = np.rot90(value, k=self.num_rotation_90, axes=(0, 1)) + if self.transpose: + value = np.transpose(value) + + return value + # pylint: disable=signature-differs def put( self, @@ -360,24 +561,38 @@ class PreviewSignal(BECMessageSignal): value (list | np.ndarray | dict | self._bec_message_type): The preview data. Must be 1D. metadata (dict | None): Additional metadata. If dict or self._bec_message_type is passed, it will be ignored. """ + signal_name = self.dotted_name or self.name device_name = self.parent.name - if isinstance(value, self._bec_message_type): - return super().put(value, **kwargs) - if isinstance(value, dict): - value["device"] = device_name - value["signal"] = signal_name + + if isinstance(value, messages.DevicePreviewMessage): + value.data = self._process_data(value.data) return super().put(value, **kwargs) - if isinstance(value, list): - value = np.array(value) - try: - msg = self._bec_message_type( - data=value, device=device_name, signal=signal_name, metadata=metadata - ) - except ValidationError as exc: - raise ValueError(f"Error setting signal {self.name}: {exc}") from exc - super().put(msg, **kwargs) + if isinstance(value, dict): + if "data" not in value: + raise ValueError("Dictionary value must contain 'data' key.") + + value["device"] = device_name + value["signal"] = signal_name + value["data"] = self._process_data(value["data"]) + return super().put(value, **kwargs) + + if isinstance(value, (list, np.ndarray)): + if not isinstance(value, np.ndarray): + value = np.array(value) + value = self._process_data(value) + try: + msg = messages.DevicePreviewMessage( + data=value, device=device_name, signal=signal_name, metadata=metadata + ) + except ValidationError as exc: + raise ValueError(f"Error setting signal {self.name}: {exc}") from exc + return super().put(msg, **kwargs) + + raise ValueError( + f"Value must be a {self._bec_message_type.__name__}, a dict, a list or a numpy array for signal {self.name}" + ) # pylint: disable=signature-differs def set( @@ -390,8 +605,8 @@ class PreviewSignal(BECMessageSignal): if value is a dict, it will be converted to a DevicePreviewMessage. Args: - value (list | np.ndarray | dict | self._bec_message_type) : The preview data. Must be 1D. - metadata (dict | None) : Additional metadata. If dict or self._bec_message_type is passed, it will be ignored. + value (list | np.ndarray | dict | self._bec_message_type): The preview data. Must be 1D. + metadata (dict | None): Additional metadata. If dict or self._bec_message_type is passed, it will be ignored. """ self.put(value=value, metadata=metadata, **kwargs) status = DeviceStatus(device=self) @@ -402,39 +617,39 @@ class PreviewSignal(BECMessageSignal): class DynamicSignal(BECMessageSignal): """Signal to emit dynamic device data.""" - @typechecked + strict_signal_validation = False # Disable strict signal validation + def __init__( self, *, name: str, - signal_names: list[str] | Callable[[], list[str]], + signals: list[str] | Callable[[], list[str]] | None = None, value: messages.DeviceMessage | dict | None = None, + async_update: dict | None = None, **kwargs, ): """ Create a new DynamicSignal object. Args: - name (str) : The name of the signal. - signal_names (list[str] | Callable) : The names of all signals. Can be a list or a callable. - value (DeviceMessage | dict | None) : The initial value of the signal. Defaults to None. + name (str): The name of the signal. + signal_names (list[str] | Callable): The names of all signals. Can be a list or a callable. + value (DeviceMessage | dict | None): The initial value of the signal. Defaults to None. + async_update (dict | None): Additional metadata for asynchronous updates. Defaults to None. """ - _raise = False - if callable(signal_names): - self.signal_names = signal_names() - elif isinstance(signal_names, list): - self.signal_names = signal_names - else: - _raise = True + self.async_update = async_update - if _raise is True or len(self.signal_names) == 0: - raise ValueError(f"No names provided for Dynamic signal {name} via {signal_names}") kwargs.pop("kind", None) # Ignore kind if specified super().__init__( name=name, + data_type=kwargs.pop("data_type", "raw"), + saved=kwargs.pop("saved", True), + ndim=kwargs.pop("ndim", 1), + scope=kwargs.pop("scope", "scan"), + role=kwargs.pop("role", "main"), + signals=signals, value=value, - bec_message_type=messages.DeviceMessage, - kind=Kind.omitted, + bec_message_type=kwargs.pop("bec_message_type", messages.DeviceMessage), **kwargs, ) @@ -443,6 +658,7 @@ class DynamicSignal(BECMessageSignal): value: messages.DeviceMessage | dict[str, dict[Literal["value", "timestamp"], Any]], *, metadata: dict | None = None, + async_update: dict | None = None, **kwargs, ) -> None: """ @@ -453,13 +669,24 @@ class DynamicSignal(BECMessageSignal): if value is a dict, it will be converted to a DeviceMessage. Args: - value (dict | DeviceMessage) : The dynamic device data. - metadata (dict | None) : Additional metadata. + value (dict | DeviceMessage): The dynamic device data. + metadata (dict | None): Additional metadata. """ if isinstance(value, messages.DeviceMessage): + if metadata is not None or async_update is not None: + logger.warning( + "Ignoring metadata and async_update arguments when value is a DeviceMessage." + ) self._check_signals(value) return super().put(value, **kwargs) try: + metadata = metadata or {} + if "async_update" not in metadata: + if async_update is not None: + metadata["async_update"] = async_update + elif self.async_update is not None: + metadata["async_update"] = self.async_update + msg = messages.DeviceMessage(signals=value, metadata=metadata) except ValidationError as exc: raise ValueError(f"Error setting signal {self.name}: {exc}") from exc @@ -468,16 +695,26 @@ class DynamicSignal(BECMessageSignal): def _check_signals(self, msg: messages.DeviceMessage) -> None: """Check if all signals are valid.""" - if any(name not in self.signal_names for name in msg.signals): - raise ValueError( - f"Invalid signal name in message {list(msg.signals.keys())} for signals {self.signal_names}" - ) + available_signals = [name for name, _ in self.signals] + if self.strict_signal_validation: + if set(msg.signals.keys()) != set(available_signals): + raise ValueError( + f"Signal names in message {list(msg.signals.keys())} do not match expected signals {available_signals}" + ) + # If strict validation is disabled, we only check if the names are valid + # and not if all are present + else: + if any(name not in available_signals for name in msg.signals): + raise ValueError( + f"Invalid signal name in message {list(msg.signals.keys())} for signals {available_signals}" + ) def set( self, value: messages.DeviceMessage | dict[str, dict[Literal["value"], Any]], *, metadata: dict | None = None, + async_update: dict | None = None, **kwargs, ) -> DeviceStatus: """ @@ -491,7 +728,43 @@ class DynamicSignal(BECMessageSignal): value (dict | DeviceMessage) : The dynamic device data. metadata (dict | None) : Additional metadata. """ - self.put(value, metadata=metadata, **kwargs) + self.put(value, metadata=metadata, async_update=async_update, **kwargs) status = DeviceStatus(device=self) status.set_finished() return status + + +class AsyncSignal(DynamicSignal): + """Signal to emit asynchronous data.""" + + strict_signal_validation = True + + def __init__( + self, + *, + name: str, + ndim: Literal[0, 1, 2], + value: messages.DeviceMessage | dict | None = None, + async_update: dict | None = None, + **kwargs, + ): + """ + Create a new AsyncSignal object. + + Args: + name (str): The name of the signal. + value (AsyncMessage | dict | None): The initial value of the signal. Defaults to None. + """ + kwargs.pop("kind", None) # Ignore kind if specified + super().__init__( + name=name, + data_type="raw", + saved=True, + ndim=ndim, + scope="scan", + role="main", + value=value, + bec_message_type=messages.DeviceMessage, + async_update=async_update, + **kwargs, + ) diff --git a/tests/test_utils.py b/tests/test_utils.py index 23d778e..eb909c1 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -221,7 +221,17 @@ def test_utils_bec_message_signal(): "source": "BECMessageSignal:bec_message_signal", "dtype": "GUIInstructionMessage", "shape": [], - "signal_metadata": {}, + "signal_info": { + "data_type": "raw", + "saved": True, + "ndim": 0, + "scope": "scan", + "role": "main", + "enabled": True, + "rpc_access": False, + "signals": [("bec_message_signal", 5)], + "signal_metadata": None, + }, } } # Put works with Message @@ -246,20 +256,28 @@ def test_utils_bec_message_signal(): def test_utils_dynamic_signal(): """Test DynamicSignal""" dev = Device(name="device") - signal = DynamicSignal( - name="dynamic_signal", signal_names=["sig1", "sig2"], value=None, parent=dev - ) + signal = DynamicSignal(name="dynamic_signal", signals=["sig1", "sig2"], value=None, parent=dev) assert signal.parent == dev assert signal._bec_message_type == messages.DeviceMessage assert signal._readback is None assert signal.name == "dynamic_signal" - assert signal.signal_names == ["sig1", "sig2"] + assert signal.signals == [("sig1", 1), ("sig2", 1)] assert signal.describe() == { "dynamic_signal": { "source": "BECMessageSignal:dynamic_signal", "dtype": "DeviceMessage", "shape": [], - "signal_metadata": {}, + "signal_info": { + "data_type": "raw", + "saved": True, + "ndim": 1, + "scope": "scan", + "role": "main", + "enabled": True, + "rpc_access": False, + "signals": [("sig1", 1), ("sig2", 1)], + "signal_metadata": None, + }, } } @@ -295,9 +313,20 @@ def test_utils_file_event_signal(): "source": "BECMessageSignal:file_event_signal", "dtype": "FileMessage", "shape": [], - "signal_metadata": {}, + "signal_info": { + "data_type": "raw", + "saved": False, + "ndim": 0, + "scope": "scan", + "role": "file event", + "enabled": True, + "rpc_access": False, + "signals": [("file_event_signal", 5)], + "signal_metadata": None, + }, } } + # Test put works with FileMessage msg_dict = {"file_path": "/path/to/another/file.txt", "done": False, "successful": True} msg = messages.FileMessage(**msg_dict) @@ -325,7 +354,7 @@ def test_utils_preview_1d_signal(): """Test Preview1DSignal""" dev = Device(name="device") signal = PreviewSignal(name="preview_1d_signal", ndim=1, value=None, parent=dev) - assert signal.signal_metadata.get("ndim") == 1 + assert signal.ndim == 1 assert signal.parent == dev assert signal._bec_message_type == messages.DevicePreviewMessage assert signal._readback is None @@ -335,7 +364,17 @@ def test_utils_preview_1d_signal(): "source": "BECMessageSignal:preview_1d_signal", "dtype": "DevicePreviewMessage", "shape": [], - "signal_metadata": {"ndim": 1, "num_rotation_90": 0, "transpose": False}, + "signal_info": { + "data_type": "raw", + "saved": False, + "ndim": 1, + "scope": "scan", + "role": "preview", + "enabled": True, + "rpc_access": False, + "signals": [("preview_1d_signal", 5)], + "signal_metadata": {"num_rotation_90": 0, "transpose": False}, + }, } } # Put works with Message @@ -377,7 +416,7 @@ def test_utils_preview_2d_signal(): """Test Preview2DSignal""" dev = Device(name="device") signal = PreviewSignal(name="preview_2d_signal", ndim=2, value=None, parent=dev) - assert signal.signal_metadata.get("ndim") == 2 + assert signal.ndim == 2 assert signal.parent == dev assert signal._bec_message_type == messages.DevicePreviewMessage assert signal._readback is None @@ -387,7 +426,17 @@ def test_utils_preview_2d_signal(): "source": "BECMessageSignal:preview_2d_signal", "dtype": "DevicePreviewMessage", "shape": [], - "signal_metadata": {"ndim": 2, "num_rotation_90": 0, "transpose": False}, + "signal_info": { + "data_type": "raw", + "saved": False, + "ndim": 2, + "scope": "scan", + "role": "preview", + "enabled": True, + "rpc_access": False, + "signals": [("preview_2d_signal", 5)], + "signal_metadata": {"num_rotation_90": 0, "transpose": False}, + }, } } # Put works with Message @@ -442,7 +491,17 @@ def test_utils_progress_signal(): "source": "BECMessageSignal:progress_signal", "dtype": "ProgressMessage", "shape": [], - "signal_metadata": {}, + "signal_info": { + "data_type": "raw", + "saved": False, + "ndim": 0, + "scope": "scan", + "role": "progress", + "enabled": True, + "rpc_access": False, + "signals": [("progress_signal", 5)], + "signal_metadata": None, + }, } } # Put works with Message