diff --git a/csaxs_bec/devices/omny/galil/galil_rio.py b/csaxs_bec/devices/omny/galil/galil_rio.py index 2403277..910bb95 100644 --- a/csaxs_bec/devices/omny/galil/galil_rio.py +++ b/csaxs_bec/devices/omny/galil/galil_rio.py @@ -17,21 +17,29 @@ from __future__ import annotations import time from typing import TYPE_CHECKING +from ophyd import Kind +from ophyd_devices import PSIDeviceBase from ophyd_devices.utils.controller import Controller, threadlocked -from ophyd_devices.utils.socket import SocketIO, SocketSignal +from ophyd_devices.utils.socket import SocketIO from csaxs_bec.devices.omny.galil.galil_ophyd import ( GalilCommunicationError, - ReadOnlyError, + GalilSignalRO, retry_once, ) if TYPE_CHECKING: # pragma: no cover + from bec_lib.devicemanager import ScanInfo from bec_server.device_server.devices.devicemanager import DeviceManagerDS class GalilRIOController(Controller): - """Controller Class for Galil RIO controller communication.""" + """ + Controller Class for Galil RIO controller communication. + + Multiple controllers are in use at the cSAXS beamline: + - 129.129.98.64 (port 23) + """ @threadlocked def socket_put(self, val: str) -> None: @@ -55,70 +63,26 @@ class GalilRIOController(Controller): ) -class GalilRIOSignal(SocketSignal): +class GalilRIOSignalRO(GalilSignalRO): """ - Read-only Signal for reading _NUM_ANALOG_CH analog input channels from Galil RIO controller. - The signal reads all channels at once and returns a list of float values. - - The host (129.129.98.64) and port (23) are set to the default values for the Galil RIO controller. - But can be configured during initialization. + Read-only Signal for reading a single analog input channel from the Galil RIO controller. """ - _NUM_ANALOG_CH = 8 + def __init__(self, signal_name: str, channel: int, **kwargs): + super().__init__(signal_name=signal_name, **kwargs) + self._channel = channel - def __init__( - self, - name: str, - host: str = "129.129.98.64", - port: int = 23, - socket_cls=SocketIO, - device_manager: DeviceManagerDS | None = None, - **kwargs, - ): - super().__init__(name=name, **kwargs) - self._metadata["write_access"] = False - self.controller = GalilRIOController( - socket_cls=socket_cls, socket_host=host, socket_port=port, device_manager=device_manager - ) - self._metadata["connected"] = False - self._readback = [0.0] * self._NUM_ANALOG_CH # Set proper initial value type - - def wait_for_connection(self, timeout=10, **kwargs): - """ - Wait for socket connection to be established within timeout period. - This is needed to ensure that the controller is connected before starting - to read values. - - Args: - timeout (int): Time in seconds to wait for connection - """ - self.controller.on(timeout=timeout) - self._metadata["connected"] = True - - def destroy(self): - """Make sure to turn off the controller socket on destroy.""" - self.controller.off() - return super().destroy() - - def _socket_set(self, val): - raise ReadOnlyError("Read-only signals cannot be set") - - @threadlocked - def _socket_get(self) -> list[float]: - """Get command for the readback signal - - Returns: - list[float]: List of analog channel values - """ - cmd = "MG@" + ",@".join([f"AN[{ii}]" for ii in range(self._NUM_ANALOG_CH)]) + def _socket_get(self) -> float: + """Get command for the readback signal""" + cmd = f"MG@AN[{self._channel}]" ret = self.controller.socket_put_and_receive(cmd) timestamp = time.time() self._metadata["timestamp"] = timestamp - return [float(val) for val in ret.strip().split(" ")] + return float(ret.strip().split(" ")[0]) def get(self): """Get current analog channel values from the Galil RIO controller.""" - old_val = self._readback if isinstance(self._readback, list) else [] + old_val = self._readback self._readback = self._socket_get() self._run_subs( sub_type=self.SUB_VALUE, @@ -127,3 +91,72 @@ class GalilRIOSignal(SocketSignal): timestamp=self._metadata.get("timestamp", time.time()), ) return self._readback + + +class GalilRIO(PSIDeviceBase): + """Base integration for the Galil RIO card in BEC.""" + + an_ch1 = GalilRIOSignalRO("an_ch1", channel=1, doc="Analog input channel 1") + an_ch2 = GalilRIOSignalRO("an_ch2", channel=2, doc="Analog input channel 2") + an_ch3 = GalilRIOSignalRO("an_ch3", channel=3, doc="Analog input channel 3") + an_ch4 = GalilRIOSignalRO("an_ch4", channel=4, doc="Analog input channel 4") + an_ch5 = GalilRIOSignalRO("an_ch5", channel=5, doc="Analog input channel 5") + an_ch6 = GalilRIOSignalRO("an_ch6", channel=6, doc="Analog input channel 6") + an_ch7 = GalilRIOSignalRO("an_ch7", channel=7, doc="Analog input channel 7") + an_ch8 = GalilRIOSignalRO("an_ch8", channel=8, doc="Analog input channel 8") + + def __init__( + self, + name: str, + host: str, + device_manager: DeviceManagerDS, + port: int | None = None, + socket_cls: type[SocketIO] = SocketIO, + scan_info: ScanInfo | None = None, + **kwargs, + ): + if port is None: + port = 23 # Default port for Galil RIO controller + super().__init__(name=name, device_manager=device_manager, scan_info=scan_info, **kwargs) + self.controller = GalilRIOController( + socket_cls=socket_cls, host=host, port=port, device_manager=device_manager + ) + self._metadata["connected"] = False + + def wait_for_connection(self, timeout: float = 30.0, **kwargs) -> None: + """Wait for the RIO controller to be connected within timeout period.""" + self.controller.on(timeout=timeout) + self._metadata["connected"] = True + + def destroy(self) -> None: + """Make sure to turn off the controller socket on destroy.""" + self.controller.off() + return super().destroy() + + def read(self): + """Read all 8 analog input channels from the Galil RIO controller.""" + # Get number of channels + channels: list[tuple[int, str]] = [()] + res = super().read() + + for _, signal in self._get_components_of_kind(Kind.normal): # This gets all signals + channels.append((signal._channel, signal.name)) # pylint: disable=protected-access + + cmd = "MG@" + ",@".join([f"AN[{ii}]" for ii, _ in channels]) + ret = self.controller.socket_put_and_receive(cmd) + timestamp = time.time() + values = [float(val) for val in ret.strip().split(" ")] + for val, (_, signal_name) in zip(values, channels): + res[signal_name] = {"value": val, "timestamp": timestamp} + return res + + +if __name__ == "__main__": + host = "129.129.98.64" + from bec_server.device_server.tests.utils import DMMock + + dm = DMMock() + rio = GalilRIO("rio", host=host, device_manager=dm) + rio.wait_for_connection(timeout=10) + print("Connected:", rio.an_ch1.read()) + print("All channels:", rio.read())