refactor(galil-rio): fix socket-signal cached readings
CI for csaxs_bec / test (pull_request) Failing after 32s
CI for csaxs_bec / test (push) Failing after 35s

This commit is contained in:
2026-02-16 15:14:50 +01:00
parent f925a7c1db
commit 14dcc86685
+58 -72
View File
@@ -26,7 +26,6 @@ from ophyd_devices.utils.socket import SocketIO
from csaxs_bec.devices.omny.galil.galil_ophyd import (
GalilCommunicationError,
GalilSignalBase,
GalilSignalRO,
retry_once,
)
@@ -67,9 +66,10 @@ class GalilRIOController(Controller):
)
class GalilRIOSignal(GalilSignalBase):
class GalilRIOAnalogSignalRO(GalilSignalBase):
"""
Read-only Signal for reading a single analog input channel from the Galil RIO controller.
To make the readback more efficient, we will read all analog channels
It always read all 8 analog channels at once, and updates the reabacks of all channels.
New readbacks are only fetched from the controller if the last readback is older than
_READ_TIMEOUT seconds, otherwise the last cached readback is returned to reduce network traffic.
@@ -80,74 +80,12 @@ class GalilRIOSignal(GalilSignalBase):
parent (GalilRIO): Parent GalilRIO device.
"""
_READ_TIMEOUT = 0.1 # seconds
def __init__(self, signal_name: str, channel: int, parent: GalilRIO, **kwargs):
super().__init__(signal_name, parent=parent, **kwargs)
self._readback_metadata = (
self.root._readback_metadata
if hasattr(self.root, "_readback_metadata")
else {"last_readback": 0.0}
)
self._channel = channel
self._metadata["connected"] = False
def get(self):
"""Get current analog channel values from the Galil RIO controller."""
# If the last readback has happend more than _READ_TIMEOUT seconds ago, read all channels again
if time.monotonic() - self._readback_metadata["last_readback"] > self._READ_TIMEOUT:
self._readback = self._socket_get()
return self._readback
# pylint: disable=protected-access
def _update_all_channels(self, values: list[float], signal_cls: Type[GalilRIOSignal]) -> None:
"""
Update all analog channel readbacks based on the provided list of values.
List of values must be in order from an_ch0 to an_ch7.
We first have to update the timestamp of the GalilRIO _readback_metadata device.
Then we update all readbacks of all an_ch channels, before we run any subscriptions.
This ensures that all readbacks are updated before any subscriptions are run, which
may themselves read other channels.
Args:
values (list[float]): List of 8 float values corresponding to the analog channels.
They must be in order from an_ch0 to an_ch7.
signal_cls (Type[GalilRIOSignal]): The class of the signal to update, used to identify which signals to update.
"""
timestamp = time.time()
# Update parent's last readback before running subscriptions!!
self._readback_metadata["last_readback"] = time.monotonic()
updates: dict[str, tuple[float, float]] = {} # attr_name -> (new_val, old_val)
# Update all readbacks first
for walk in self.parent.walk_signals():
if isinstance(walk.item, signal_cls):
idx = int(walk.item.attr_name[-1])
if 0 <= idx < len(values):
old_val = walk.item._readback
new_val = values[idx]
walk.item._metadata["timestamp"] = timestamp
walk.item._readback = new_val
updates[walk.item.attr_name] = (new_val, old_val)
# Run subscriptions after all readbacks have been updated
for walk in self.parent.walk_signals():
if walk.item.attr_name in updates:
new_val, old_val = updates[walk.item.attr_name]
walk.item._run_subs(
sub_type=walk.item.SUB_VALUE,
old_value=old_val,
value=new_val,
timestamp=timestamp,
)
class GalilRIOSignalRO(GalilRIOSignal):
_NUM_ANALOG_CHANNELS = 8
def __init__(self, signal_name: str, channel: int, parent: GalilRIO, **kwargs):
super().__init__(signal_name=signal_name, channel=channel, parent=parent, **kwargs)
self._channel = channel
self._metadata["connected"] = False
self._metadata["write_access"] = False
def _socket_set(self, val):
@@ -158,17 +96,65 @@ class GalilRIOSignalRO(GalilRIOSignal):
cmd = "MG@" + ", @".join([f"AN[{ii}]" for ii in range(self._NUM_ANALOG_CHANNELS)])
ret = self.controller.socket_put_and_receive(cmd)
values = [float(val) for val in ret.strip().split(" ")]
# This updates all channels' readbacks, including self._readback
self._update_all_channels(values, signal_cls=GalilRIOSignalRO)
# This updates all channels' readbacks, including self._readback. Channels must be named following
# the convention ch0, ch1, ..., ch7 for this to work correctly.
self._update_all_channels(values)
return self._readback
# pylint: disable=protected-access
def _update_all_channels(self, values: list[float]) -> None:
"""
This method updates the readback values of all analog channels based on the list of values provided.
It also runs the subscriptions for each channel after updating the readbacks. It relies on
_last_readback being updated before calling _socket_get to ensure that timestamps are properly updated.
This convention is implemented in the SocketIO class. We futher rely on the convention that the channels
are named ch0, ch1, ..., ch7 for this to work correctly. Only GalilRIOAnalogSignalRO signals will be considered
in this update logic.
class GalilRIODigitalOutSignal(GalilRIOSignal): # We reuse the logic implemented for Galil
Args:
values (list[float]): List of new readback values for all channels, where the
index corresponds to the channel number (0-7).
"""
updates: dict[str, tuple[float, float]] = {} # attr_name -> (new_val, old_val)
# Update all readbacks first
for walk in self.parent.walk_signals():
if isinstance(walk.item, GalilRIOAnalogSignalRO):
idx = int(walk.item.attr_name[-1])
if 0 <= idx < len(values):
old_val = walk.item._readback
new_val = values[idx]
walk.item._metadata["timestamp"] = self._last_readback
walk.item._last_readback = self._last_readback
walk.item._readback = new_val
updates[walk.item.attr_name] = (new_val, old_val)
else:
logger.warning(
f"Received {len(values)} values but found channel index {idx} in signal {walk.item.name}. Skipping update for this signal."
)
# Run subscriptions after all readbacks have been updated
for walk in self.parent.walk_signals():
if walk.item.attr_name in updates:
new_val, old_val = updates[walk.item.attr_name]
walk.item._run_subs(
sub_type=walk.item.SUB_VALUE,
old_value=old_val,
value=new_val,
timestamp=self._last_readback,
)
class GalilRIODigitalOutSignal(GalilSignalBase): # We reuse the logic implemented for Galil
"""
Signal for controlling digital outputs of the Galil RIO controller.
"""
_NUM_DIGITAL_OUTPUT_CHANNELS = 24
_NUM_DIGITAL_OUTPUT_CHANNELS = 16
def __init__(self, signal_name: str, channel: int, parent: GalilRIO, **kwargs):
super().__init__(signal_name, parent=parent, **kwargs)
self._channel = channel
self._metadata["connected"] = False
def _socket_get(self) -> float:
"""Get command for the readback signal"""
@@ -197,7 +183,7 @@ def _create_analog_channels(num_channels: int) -> dict[str, tuple]:
an_channels = {}
for i in range(0, num_channels):
an_channels[f"ch{i}"] = (
GalilRIOSignalRO,
GalilRIOAnalogSignalRO,
f"ch{i}",
{
"kind": Kind.normal,
@@ -248,7 +234,7 @@ class GalilRIO(PSIDeviceBase):
#############################
analog_in = DDC(
_create_analog_channels(GalilRIOSignalRO._NUM_ANALOG_CHANNELS)
_create_analog_channels(GalilRIOAnalogSignalRO._NUM_ANALOG_CHANNELS)
) # Creates ch0 to ch7
digital_out = DDC(
_create_digital_output_channels(GalilRIODigitalOutSignal._NUM_DIGITAL_OUTPUT_CHANNELS)