diff --git a/CHANGES.rst b/CHANGES.rst index 8590603d1..025b60772 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -21,6 +21,10 @@ describe the future plans. Add device support for SRS570 preamplifier from synApps. Hoisted from 4-ID-C Polar and 8-ID-I XPCS. + * `#491 `_ + Add device support for PTC10 temperature controller. + Hoisted from 9-ID-C USAXS. + :1.5.2: released 2021-09-29 * Drop Codacy (https://app.codacy.com/gh/BCDA-APS/apstools) as no longer needed. diff --git a/apstools/_devices/__init__.py b/apstools/_devices/__init__.py index f587a3eaa..26bf33afc 100644 --- a/apstools/_devices/__init__.py +++ b/apstools/_devices/__init__.py @@ -22,6 +22,10 @@ from .motor_mixins import EpicsMotorResolutionMixin from .motor_mixins import EpicsMotorServoMixin from .positioner_soft_done import PVPositionerSoftDone +from .ptc10_controller import PTC10AioChannel +from .ptc10_controller import PTC10RtdChannel +from .ptc10_controller import PTC10TcChannel +from .ptc10_controller import PTC10PositionerMixin from .scaler_support import SCALER_AUTOCOUNT_MODE from .scaler_support import use_EPICS_scaler_channels from .shutters import ApsPssShutter diff --git a/apstools/_devices/ptc10_controller.py b/apstools/_devices/ptc10_controller.py new file mode 100644 index 000000000..be8883f63 --- /dev/null +++ b/apstools/_devices/ptc10_controller.py @@ -0,0 +1,166 @@ +""" +PTC10 Programmable Temperature Controller ++++++++++++++++++++++++++++++++++++++++++ + +The PTC10 is a programmable temperature controller from SRS (Stanford Research +Systems). The PTC10 is a modular system consisting of a base unit and provision +for addition of add-on boards. + +A single, complete ``ophyd.Device`` subclass will not describe all variations +that could be installed. But the add-on boards each allow standardization. Each +installation must build a custom class that matches their hardware +configuration. The APS USAXS instrument has created a `custom class +`_ +based on the `ophyd.PVPositioner` to use their `PTC10` as a temperature +*positioner*. + +.. autosummary:: + + ~PTC10AioChannel + ~PTC10RtdChannel + ~PTC10TcChannel + ~PTC10PositionerMixin + +:see: https://www.thinksrs.com/products/ptc10.html + +EXAMPLE:: + + from ophyd import PVPositioner + + class MyPTC10(PTC10PositionerMixin, PVPositioner): + readback = Component(EpicsSignalRO, "2A:temperature", kind="hinted") + setpoint = Component(EpicsSignalWithRBV, "5A:setPoint", kind="hinted") + + rtd = Component(PTC10RtdChannel, "3A:") + pid = Component(PTC10AioChannel, "5A:") + + ptc10 = USAXS_PTC10("IOC_PREFIX:ptc10:", name="ptc10") + ptc10.report_dmov_changes.put(True) # a diagnostic + ptc10.tolerance.put(1.0) # done when |readback-setpoint|<=tolerance + +New in apstools 1.5.3. +""" + +import logging + +from ophyd import Component +from ophyd import Device +from ophyd import EpicsSignal +from ophyd import EpicsSignalRO +from ophyd import EpicsSignalWithRBV +from ophyd import Signal + +logger = logging.getLogger(__name__) + + +class PTC10AioChannel(Device): + """ + SRS PTC10 AIO module + """ + + voltage = Component(EpicsSignalRO, "voltage_RBV", kind="config") + highlimit = Component(EpicsSignalWithRBV, "highLimit", kind="config") + lowlimit = Component(EpicsSignalWithRBV, "lowLimit", kind="config") + iotype = Component(EpicsSignalWithRBV, "ioType", kind="config", string=True) + setpoint = Component(EpicsSignalWithRBV, "setPoint", kind="config") + ramprate = Component(EpicsSignalWithRBV, "rampRate", kind="config") + offswitch = Component(EpicsSignal, "off", kind="config") + + pidmode = Component(EpicsSignalWithRBV, "pid:mode", kind="config", string=True) + P = Component(EpicsSignalWithRBV, "pid:P", kind="config") + I = Component(EpicsSignalWithRBV, "pid:I", kind="config") + D = Component(EpicsSignalWithRBV, "pid:D", kind="config") + + inputchoice = Component(EpicsSignalWithRBV, "pid:input", kind="config", string=True) + tunelag = Component(EpicsSignalWithRBV, "tune:lag", kind="config") + tunestep = Component(EpicsSignalWithRBV, "tune:step", kind="config") + tunemode = Component(EpicsSignalWithRBV, "tune:mode", kind="config", string=True) + tunetype = Component(EpicsSignalWithRBV, "tune:type", kind="config", string=True) + + +class PTC10RtdChannel(Device): + """ + SRS PTC10 RTD module channel + """ + + temperature = Component(EpicsSignalRO, "temperature", kind="normal") + units = Component(EpicsSignalRO, "units_RBV", kind="config", string=True) + sensor = Component(EpicsSignalWithRBV, "sensor", kind="config", string=True) + channelrange = Component(EpicsSignalWithRBV, "range", kind="config", string=True) + current = Component(EpicsSignalWithRBV, "current", kind="config", string=True) + power = Component(EpicsSignalWithRBV, "power", kind="config", string=True) + + +class PTC10TcChannel(Device): + """ + SRS PTC10 Tc (thermocouple) module channel + """ + + temperature = Component(EpicsSignalRO, "temperature", kind="normal") + + +class PTC10PositionerMixin(Device): + """ + Mixin so SRS PTC10 can be used as a (temperature) positioner. + + .. autosummary:: + + ~cb_readback + ~cb_setpoint + ~inposition + ~stop + """ + + done = Component(Signal, value=True, kind="omitted") + done_value = True + + # for computation of soft `done` signal + # default +/- 1 degree for "at temperature" + tolerance = Component(Signal, value=1, kind="config") + + # For logging when temperature is reached after a move. + report_dmov_changes = Component(Signal, value=True, kind="omitted") + + def cb_readback(self, *args, **kwargs): + """ + Called when readback changes (EPICS CA monitor event). + """ + diff = self.readback.get() - self.setpoint.get() + dmov = abs(diff) <= self.tolerance.get() + if self.report_dmov_changes.get() and dmov != self.done.get(): + logger.debug(f"{self.name} reached: {dmov}") + self.done.put(dmov) + + def cb_setpoint(self, *args, **kwargs): + """ + Called when setpoint changes (EPICS CA monitor event). + + When the setpoint is changed, force ``done=False``. For any move, + ``done`` MUST change to ``!= done_value``, then change back to + ``done_value (True)``. Without this response, a small move + (within tolerance) will not return. Next update of readback + will compute ``self.done``. + """ + self.done.put(not self.done_value) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.readback.name = self.name + + # to compute the soft `done` signal + self.readback.subscribe(self.cb_readback) + self.setpoint.subscribe(self.cb_setpoint) + + @property + def inposition(self): + """ + Report (boolean) if positioner is done. + """ + return self.done.get() == self.done_value + + def stop(self, *, success=False): + """ + Hold the current readback when the stop() method is called and not done. + """ + if not self.done.get(): + self.setpoint.put(self.position) diff --git a/apstools/devices.py b/apstools/devices.py index d2a280e44..d978636fb 100644 --- a/apstools/devices.py +++ b/apstools/devices.py @@ -70,6 +70,10 @@ ~apstools._devices.description_mixin.EpicsDescriptionMixin ~apstools._devices.kohzu_monochromator.KohzuSeqCtl_Monochromator ~ProcessController + ~apstools._devices.ptc10_controller.PTC10AioChannel + ~apstools._devices.ptc10_controller.PTC10RtdChannel + ~apstools._devices.ptc10_controller.PTC10TcChannel + ~apstools._devices.ptc10_controller.PTC10PositionerMixin ~apstools._devices.srs570_preamplifier.SRS570_PreAmplifier ~apstools._devices.struck3820.Struck3820 @@ -125,6 +129,10 @@ from ._devices.motor_mixins import EpicsMotorRawMixin from ._devices.motor_mixins import EpicsMotorResolutionMixin from ._devices.motor_mixins import EpicsMotorServoMixin +from ._devices.ptc10_controller import PTC10AioChannel +from ._devices.ptc10_controller import PTC10RtdChannel +from ._devices.ptc10_controller import PTC10TcChannel +from ._devices.ptc10_controller import PTC10PositionerMixin from ._devices.scaler_support import SCALER_AUTOCOUNT_MODE from ._devices.scaler_support import use_EPICS_scaler_channels from ._devices.shutters import ApsPssShutter diff --git a/docs/source/source/_devices.rst b/docs/source/source/_devices.rst index be932bd32..d84b4b749 100644 --- a/docs/source/source/_devices.rst +++ b/docs/source/source/_devices.rst @@ -48,6 +48,9 @@ Submodules .. automodule:: apstools._devices.preamp_base :members: +.. automodule:: apstools._devices.ptc10_controller + :members: + .. automodule:: apstools._devices.scaler_support :members: