-
Notifications
You must be signed in to change notification settings - Fork 38
Description
In dodal, we are wanting to implement a very simple device (DiamondLightSource/dodal#1661)
class UndulatorOrder(StandardReadable, Locatable[int]):
"""
Represents the order of an undulator device. Allows setting and locating the order.
"""
def __init__(self, name: str = "") -> None:
"""
Args:
name: Name for device. Defaults to ""
"""
with self.add_children_as_readables():
self._value = soft_signal_rw(int, initial_value=3)
super().__init__(name=name)
@AsyncStatus.wrap
async def set(self, value: int) -> None:
if (value >= 0) and isinstance(value, int):
await self._value.set(value)
else:
raise ValueError(
f"Undulator order must be a positive integer. Requested value: {value}"
)
async def locate(self) -> Location[int]:
return await self._value.locate()The device has a set method with validation, but this can be ignored by just moving the signal directly
e.g
await order._value.set(invalid_value)It would be nice when creating a soft_signal_rw, you could provide some sort of validation function. We have played around using a derived_signal_rw, however we still have to create a soft_signal which defeats the purpose as a user can still bypass this (DiamondLightSource/dodal#1661 (comment))
class UndulatorOrder(StandardReadable, Locatable[int]):
"""
Represents the order of an undulator device. Allows setting and locating the order.
"""
def __init__(self, name: str = "") -> None:
"""
Args:
name: Name for device. Defaults to ""
"""
self._internal_order = soft_signal_rw(int, initial_value=3)
with self.add_children_as_readables():
self.order = derived_signal_rw(
self._get_order,
self._set_order,
current_order = self._internal_order
)
super().__init__(name=name)
@AsyncStatus.wrap
async def set(self, value: int) -> None:
await self.order.set(value)
def _get_order(self, current_order:int) -> int:
return current_order
async def _set_order(self, new_order: int) -> None:
if (new_order < 0) or not isinstance(new_order, int):
raise ValueError("Undulator order must be a positive integer")
LOGGER.info(f"Setting undulator order to {new_order}")
self._internal_order.set(new_order)
async def locate(self) -> Location[int]:
return await self.order.locate()It would be great if we could edit SoftSignalBackend to allow us to add custom validation functions e.g something like below:
def __init__(self, name: str = "") -> None:
"""
Args:
name: Name for device. Defaults to ""
"""
with self.add_children_as_readables():
self._value = soft_signal_rw(int, initial_value=3, validation=SignalValidator(func=self._value_validation))
super().__init__(name=name)
def _value_validation(self, value: int) -> None:
if (value <= 0) and not isinstance(value, int):
raise ValueError(
f"Undulator order must be a positive integer. Requested value: {value}"
)
@AsyncStatus.wrap
async def set(self, value: int):
await self._value(value)
async def locate(self) -> Location[int]:
return await self._value.locate()Now in this example, we don't need to rely on external signals for validation, but I can see in future we may need a soft signal which can only be validated by other signals e.g
...
with self.add_children_as_readables():
self.epics_signal1 = epics_signal_rw(float, prefix + "1")
self.epics_signal2 = epics_signal_rw(float, prefix + "2")
self._value = soft_signal_rw(int, initial_value=3, validation=SignalValidator(func=self._signal_validation, epics_value1=self.epics_signal1, epics_value2 = epics_value2))
...
def _signal_validation(value: int, epics_value1: float, epics_value2: float) -> None:
# My custom validation logic here
...So it is similar to derived_signal, but you don't need an existing soft signal with no validation