diff --git a/apstools/devices/__init__.py b/apstools/devices/__init__.py index bb62f5177..ae8598863 100644 --- a/apstools/devices/__init__.py +++ b/apstools/devices/__init__.py @@ -17,8 +17,7 @@ from .aps_machine import ApsMachineParametersDevice -from .aps_undulator import ApsUndulator -from .aps_undulator import ApsUndulatorDual +from .aps_undulator import PlanarUndulator from .area_detector_support import AD_EpicsFileNameMixin from .area_detector_support import AD_FrameType_schemes diff --git a/apstools/devices/aps_undulator.py b/apstools/devices/aps_undulator.py index 2f556740f..0a62883b4 100644 --- a/apstools/devices/aps_undulator.py +++ b/apstools/devices/aps_undulator.py @@ -8,81 +8,93 @@ ~ApsUndulatorDual """ +import logging +from enum import IntEnum + from ophyd import Component +from ophyd import DerivedSignal from ophyd import Device from ophyd import EpicsSignal from ophyd import EpicsSignalRO +from ophyd import PVPositioner from ophyd import Signal from .tracking_signal import TrackingSignal +logger = logging.getLogger(__name__) -class ApsUndulator(Device): - """ - APS Undulator - .. index:: Ophyd Device; ApsUndulator +class DoneStatus(IntEnum): + MOVING = 0 + DONE = 1 - EXAMPLE:: - undulator = ApsUndulator("ID09ds:", name="undulator") - """ +class BusyStatus(IntEnum): + DONE = 0 + BUSY = 1 + + +class MotorDriveStatus(IntEnum): + NOT_READY = 0 + READY_TO_MOVE = 1 + + +class UndulatorPositioner(PVPositioner): + """A positioner for any of the gap control parameters. + + Communicates with the parent (presumably the undulator device) to + start and stop the device. - energy = Component( - EpicsSignal, - "Energy", - write_pv="EnergySet", - put_complete=True, - kind="hinted", - ) - energy_taper = Component( - EpicsSignal, - "TaperEnergy", - write_pv="TaperEnergySet", - kind="config", - ) - gap = Component(EpicsSignal, "Gap", write_pv="GapSet") - gap_taper = Component(EpicsSignal, "TaperGap", write_pv="TaperGapSet", kind="config") - start_button = Component(EpicsSignal, "Start", put_complete=True, kind="omitted") - stop_button = Component(EpicsSignal, "Stop", kind="omitted") - harmonic_value = Component(EpicsSignal, "HarmonicValue", kind="config") - gap_deadband = Component(EpicsSignal, "DeadbandGap", kind="config") - device_limit = Component(EpicsSignal, "DeviceLimit", kind="config") - - access_mode = Component(EpicsSignalRO, "AccessSecurity", kind="omitted") - device_status = Component(EpicsSignalRO, "Busy", kind="omitted") - total_power = Component(EpicsSignalRO, "TotalPower", kind="config") - message1 = Component(EpicsSignalRO, "Message1", kind="omitted") - message2 = Component(EpicsSignalRO, "Message2", kind="omitted") - message3 = Component(EpicsSignalRO, "Message3", kind="omitted") - time_left = Component(EpicsSignalRO, "ShClosedTime", kind="omitted") - - device = Component(EpicsSignalRO, "Device", kind="config") - location = Component(EpicsSignalRO, "Location", kind="config") - version = Component(EpicsSignalRO, "Version", kind="config") - - # Useful undulator parameters that are not EPICS PVs. - energy_deadband = Component(Signal, value=0.0, kind="config") - energy_backlash = Component(Signal, value=0.0, kind="config") - energy_offset = Component(Signal, value=0, kind="config") - tracking = Component(TrackingSignal, value=False, kind="config") - - -class ApsUndulatorDual(Device): """ - APS Undulator with upstream *and* downstream controls - .. index:: Ophyd Device; ApsUndulatorDual + setpoint = Component(EpicsSignal, "SetC.VAL") + readback = Component(EpicsSignalRO, "M.VAL") + + actuate = Component(DerivedSignal, derived_from="parent.start_button", kind="omitted") + stop_signal = Component(DerivedSignal, derived_from="parent.stop_button", kind="omitted") + done = Component(DerivedSignal, derived_from="parent.done", kind="omitted") + done_value = DoneStatus.DONE + + +class PlanarUndulator(Device): + """APS Planar Undulator. + + .. index:: Ophyd Device; PlanarUndulator + + The signals *busy* and *done* convey complementary + information. *busy* comes from the IOC, while *done* comes + directly from the controller. EXAMPLE:: - undulator = ApsUndulatorDual("ID09", name="undulator") + undulator = PlanarUndulator("S25ID:USID:", name="undulator") - note:: the trailing ``:`` in the PV prefix should be omitted """ - upstream = Component(ApsUndulator, "us:") - downstream = Component(ApsUndulator, "ds:") + # X-ray spectrum parameters + energy = Component(UndulatorPositioner, "Energy") + energy_taper = Component(UndulatorPositioner, "TaperEnergy") + gap = Component(UndulatorPositioner, "Gap") + gap_taper = Component(UndulatorPositioner, "TaperGap") + harmonic_value = Component(EpicsSignal, "HarmonicValueC", kind="config") + total_power = Component(EpicsSignalRO, "TotalPowerM.VAL", kind="config") + # Signals for moving the undulator + start_button = Component(EpicsSignal, "StartC.VAL", put_complete=True, kind="omitted") + stop_button = Component(EpicsSignal, "StopC.VAL", kind="omitted") + busy = Component(EpicsSignalRO, "BusyM.VAL", kind="omitted") + done = Component(EpicsSignalRO, "BusyDeviceM.VAL", kind="omitted") + motor_drive_status = Component(EpicsSignalRO, "MotorDriveStatusM.VAL", kind="omitted") + # Miscellaneous control signals + gap_deadband = Component(EpicsSignal, "DeadbandGapC", kind="config") + device_limit = Component(EpicsSignal, "DeviceLimitM.VAL", kind="config") + access_mode = Component(EpicsSignalRO, "AccessSecurityC", kind="omitted") + message1 = Component(EpicsSignalRO, "Message1M.VAL", kind="omitted") + message2 = Component(EpicsSignalRO, "Message2M.VAL", kind="omitted") + device = Component(EpicsSignalRO, "DeviceM", kind="config") + magnet = Component(EpicsSignalRO, "DeviceMagnetM", kind="config") + location = Component(EpicsSignalRO, "LocationM", kind="config") + version_plc = Component(EpicsSignalRO, "PLCVersionM.VAL", kind="config") + version_hpmu = Component(EpicsSignalRO, "HPMUVersionM.VAL", kind="config") # ----------------------------------------------------------------------------- diff --git a/apstools/devices/tests/test_aps_undulator.py b/apstools/devices/tests/test_aps_undulator.py new file mode 100644 index 000000000..66c535cdf --- /dev/null +++ b/apstools/devices/tests/test_aps_undulator.py @@ -0,0 +1,23 @@ +import pytest +from ophyd.sim import instantiate_fake_device + +from ..aps_undulator import PlanarUndulator + + +@pytest.fixture() +def undulator(): + undulator = instantiate_fake_device(PlanarUndulator, prefix="PSS:255ID:", name="undulator") + return undulator + + +def test_set_energy(undulator): + assert undulator.start_button.get() == 0 + undulator.energy.set(5) + assert undulator.energy.setpoint.get() == 5 + assert undulator.start_button.get() == 1 + + +def test_stop_energy(undulator): + assert undulator.stop_button.get() == 0 + undulator.stop() + assert undulator.stop_button.get() == 1