wip feat(psi_pos): (#118) add base with optional signals

This commit is contained in:
2025-06-20 12:07:25 +02:00
parent 3c3a163137
commit 245328adf1
2 changed files with 142 additions and 0 deletions

View File

@ -0,0 +1,115 @@
from abc import ABC
from ophyd.device import Device
from ophyd.positioner import PositionerBase
from ophyd.signal import EpicsSignalBase
_OPTIONAL_SIGNAL = object()
_REQUIRED_SIGNAL = object()
_SIGNAL_NOT_AVAILABLE = "Signal not available"
class PSIPositionerException(Exception): ...
class RequiredSignalNotSpecified(PSIPositionerException): ...
class OptionalSignalNotSpecified(PSIPositionerException): ...
_SIGNAL_NAMES = {
"user_readback",
"user_setpoint",
"user_offset",
"user_offset_dir",
"offset_freeze_switch",
"set_use_switch",
"velocity",
"acceleration",
"motor_egu",
"motor_is_moving",
"motor_done_move",
"high_limit_switch",
"low_limit_switch",
"high_limit_travel",
"low_limit_travel",
"direction_of_travel",
"motor_stop",
"home_forward",
"home_reverse",
"tolerated_alarm",
}
class PSIPositionerBase(ABC, Device, PositionerBase):
"""Base class for positioners which are similar to a motor but do not implement
all the required signals for an EpicsMotor or have different PV suffices."""
# position
user_readback = _REQUIRED_SIGNAL
user_setpoint = _REQUIRED_SIGNAL
# calibration dial <-> user
user_offset = _OPTIONAL_SIGNAL
user_offset_dir = _OPTIONAL_SIGNAL
offset_freeze_switch = _OPTIONAL_SIGNAL
set_use_switch = _OPTIONAL_SIGNAL
# configuration
velocity = _OPTIONAL_SIGNAL
acceleration = _OPTIONAL_SIGNAL
motor_egu = _OPTIONAL_SIGNAL
# motor status
motor_is_moving = _OPTIONAL_SIGNAL
motor_done_move = _OPTIONAL_SIGNAL
high_limit_switch = _OPTIONAL_SIGNAL
low_limit_switch = _OPTIONAL_SIGNAL
high_limit_travel = _OPTIONAL_SIGNAL
low_limit_travel = _OPTIONAL_SIGNAL
direction_of_travel = _OPTIONAL_SIGNAL
# commands
motor_stop = _OPTIONAL_SIGNAL
home_forward = _OPTIONAL_SIGNAL
home_reverse = _OPTIONAL_SIGNAL
# alarm information
tolerated_alarm = _OPTIONAL_SIGNAL
def __init__(
self,
prefix="",
*,
name,
kind=None,
read_attrs=None,
configuration_attrs=None,
parent=None,
child_name_separator="_",
connection_timeout=...,
**kwargs,
):
super().__init__(
prefix,
name=name,
kind=kind,
read_attrs=read_attrs,
configuration_attrs=configuration_attrs,
parent=parent,
child_name_separator=child_name_separator,
connection_timeout=connection_timeout,
**kwargs,
)
not_implemented = {
signal for signal in _SIGNAL_NAMES if getattr(self, signal) is _REQUIRED_SIGNAL
}
if not_implemented != set():
raise RequiredSignalNotSpecified(
f"Signal(s) {not_implemented} must be defined in a subclass"
)
# Make the default alias for the user_readback the name of the motor itself
# for compatibility with EpicsMotor
self.user_readback.name = self.name

View File

@ -0,0 +1,27 @@
from unittest.mock import MagicMock
import pytest
from ophyd.device import Component as Cpt
from ophyd.signal import EpicsSignal
from ophyd.sim import FakeEpicsSignal, FakeEpicsSignalRO
from ophyd_devices.interfaces.base_classes.psi_positioner_base import (
PSIPositionerBase,
RequiredSignalNotSpecified,
)
def test_cannot_isntantiate_without_required_signals():
class PSITestPositionerWOSignal(PSIPositionerBase): ...
class PSITestPositionerWithSignal(PSIPositionerBase):
user_setpoint: EpicsSignal = Cpt(FakeEpicsSignal, ".VAL", limits=True, auto_monitor=True)
user_readback = Cpt(FakeEpicsSignalRO, ".RBV", kind="hinted", auto_monitor=True)
with pytest.raises(RequiredSignalNotSpecified) as e:
PSITestPositionerWOSignal("", name="")
assert e.match("user_setpoint")
assert e.match("user_readback")
dev = PSITestPositionerWithSignal("", name="")
assert dev.user_setpoint.get() == 0