diff --git a/environment.yml b/environment.yml index be570477..d19d2842 100644 --- a/environment.yml +++ b/environment.yml @@ -58,7 +58,8 @@ dependencies: - bluesky-adaptive - bluesky >=1.8.1 - ophyd >=1.6.3 - - ophyd-async >= 0.6.0 + # - ophyd-async > 0.8.0a4 + - git+https://github.com/bluesky/ophyd-async.git # switch back to pip once a new release (0.8.0a5) is available - apstools == 1.6.20 # Leave at 1.6.20 until this is fixed: https://github.com/BCDA-APS/apstools/issues/1022 - pcdsdevices # For extra signal types - pydm >=1.18.0 diff --git a/pyproject.toml b/pyproject.toml index 145d1492..25469391 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ classifiers = [ "Topic :: System :: Hardware", ] keywords = ["synchrotron", "xray", "bluesky"] -dependencies = ["aioca", "aiokafka", "bluesky", "ophyd", "ophyd-async>=0.7.0", "databroker", "apsbss", "xraydb", +dependencies = ["aioca", "aiokafka", "bluesky", "ophyd", "ophyd-async>=0.8.0a3", "databroker", "apsbss", "xraydb", "mergedeep", "xrayutilities", "bluesky-queueserver-api", "tomlkit", "apstools", "databroker", "ophyd-registry", "caproto", "pcdsdevices", "strenum", "bluesky-adaptive", "tiled[client]"] diff --git a/src/haven/devices/delay.py b/src/haven/devices/delay.py index 85b7a52f..af454a0f 100644 --- a/src/haven/devices/delay.py +++ b/src/haven/devices/delay.py @@ -2,14 +2,14 @@ from typing import Type from ophyd_async.core import ( - ConfigSignal, - DeviceVector, SignalRW, StandardReadable, + StandardReadableFormat, + StrictEnum, SubsetEnum, T, ) -from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw, epics_signal_x +from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x class StrEnum(str, enum.Enum): @@ -36,22 +36,31 @@ def epics_signal_io(datatype: Type[T], prefix: str, name: str = "") -> SignalRW[ class DG645Channel(StandardReadable): - Reference = SubsetEnum["T0", "A", "B", "C", "D", "E", "F", "G", "H"] + class Reference(StrictEnum): + T0 = "T0" + A = "A" + B = "B" + C = "C" + D = "D" + E = "E" + F = "F" + G = "G" + H = "H" def __init__(self, prefix: str, name: str = ""): - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.reference = epics_signal_io(self.Reference, f"{prefix}ReferenceM") self.delay = epics_signal_io(float, f"{prefix}DelayA") super().__init__(name=name) class DG645Output(StandardReadable): - class Polarity(StrEnum): + class Polarity(StrictEnum): NEG = "NEG" POS = "POS" def __init__(self, prefix: str, name: str = ""): - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.polarity = epics_signal_io(self.Polarity, f"{prefix}OutputPolarityB") self.amplitude = epics_signal_io(float, f"{prefix}OutputAmpA") self.offset = epics_signal_io(float, f"{prefix}OutputOffsetA") @@ -62,16 +71,46 @@ def __init__(self, prefix: str, name: str = ""): class DG645DelayOutput(DG645Output): def __init__(self, prefix: str, name: str = ""): - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.trigger_prescale = epics_signal_io(int, f"{prefix}TriggerPrescaleL") self.trigger_phase = epics_signal_io(int, f"{prefix}TriggerPhaseL") super().__init__(prefix=prefix, name=name) class DG645Delay(StandardReadable): + + class BaudRate(SubsetEnum): + B4800 = "4800" + B9600 = "9600" + B19200 = "19200" + B38400 = "38400" + B57600 = "57600" + B115200 = "115200" + + class TriggerSource(SubsetEnum): + INTERNAL = "Internal" + EXT_RISING_EDGE = "Ext rising edge" + EXT_FALLING_EDGE = "Ext falling edge" + SS_EXT_RISE_EDGE = "SS ext rise edge" + SS_EXT_FALL_EDGE = "SS ext fall edge" + SINGLE_SHOT = "Single shot" + LINE = "Line" + + class TriggerInhibit(SubsetEnum): + OFF = "Off" + TRIGGERS = "Triggers" + AB = "AB" + AB_CD = "AB,CD" + AB_CD_EF = "AB,CD,EF" + AB_CD_EF_GH = "AB,CD,EF,GH" + + class BurstConfig(SubsetEnum): + ALL_CYCLES = "All Cycles" + FIRST_CYCLE = "1st Cycle" + def __init__(self, prefix: str, name: str = ""): # Conventional signals - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.label = epics_signal_rw(str, f"{prefix}Label") self.device_id = epics_signal_r(str, f"{prefix}IdentSI") self.status = epics_signal_r(str, f"{prefix}StatusSI") @@ -82,10 +121,7 @@ def __init__(self, prefix: str, name: str = ""): self.status_checking = epics_signal_rw(bool, f"{prefix}StatusCheckingBO") self.reset_serial = epics_signal_x(f"{prefix}IfaceSerialResetBO") self.serial_state = epics_signal_io(bool, f"{prefix}IfaceSerialStateB") - self.serial_baud = epics_signal_io( - SubsetEnum["4800", "9600", "19200", "38400", "57600", "115200"], - f"{prefix}IfaceSerialBaudM", - ) + self.serial_baud = epics_signal_io(self.BaudRate, f"{prefix}IfaceSerialBaudM") self.reset_gpib = epics_signal_x(f"{prefix}IfaceGpibResetBO") self.gpib_state = epics_signal_io(bool, f"{prefix}IfaceGpibStateB") self.gpib_address = epics_signal_io(int, f"{prefix}IfaceGpibAddrL") @@ -103,46 +139,29 @@ def __init__(self, prefix: str, name: str = ""): self.gateway = epics_signal_io(str, f"{prefix}IfaceGatewayS") # Individual delay channels with self.add_children_as_readables(): - self.channels = DeviceVector( - { - "A": DG645Channel(f"{prefix}A"), - "B": DG645Channel(f"{prefix}B"), - "C": DG645Channel(f"{prefix}C"), - "D": DG645Channel(f"{prefix}D"), - "E": DG645Channel(f"{prefix}E"), - "F": DG645Channel(f"{prefix}F"), - "G": DG645Channel(f"{prefix}G"), - "H": DG645Channel(f"{prefix}H"), - } - ) + self.channel_A = DG645Channel(f"{prefix}A") + self.channel_B = DG645Channel(f"{prefix}B") + self.channel_C = DG645Channel(f"{prefix}C") + self.channel_D = DG645Channel(f"{prefix}D") + self.channel_E = DG645Channel(f"{prefix}E") + self.channel_F = DG645Channel(f"{prefix}F") + self.channel_G = DG645Channel(f"{prefix}G") + self.channel_H = DG645Channel(f"{prefix}H") # 2-channel delay outputs with self.add_children_as_readables(): - self.outputs = DeviceVector( - { - "T0": DG645Output(f"{prefix}T0"), - "AB": DG645DelayOutput(f"{prefix}AB"), - "CD": DG645DelayOutput(f"{prefix}CD"), - "EF": DG645DelayOutput(f"{prefix}EF"), - "GH": DG645DelayOutput(f"{prefix}GH"), - } - ) + self.output_T0 = DG645Output(f"{prefix}T0") + self.output_AB = DG645DelayOutput(f"{prefix}AB") + self.output_CD = DG645DelayOutput(f"{prefix}CD") + self.output_EF = DG645DelayOutput(f"{prefix}EF") + self.output_GH = DG645DelayOutput(f"{prefix}GH") # Trigger control - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.trigger_source = epics_signal_io( - SubsetEnum[ - "Internal", - "Ext rising edge", - "Ext falling edge", - "SS ext rise edge", - "SS ext fall edge", - "Single shot", - "Line", - ], + self.TriggerSource, f"{prefix}TriggerSourceM", ) self.trigger_inhibit = epics_signal_io( - SubsetEnum["Off", "Triggers", "AB", "AB,CD", "AB,CD,EF", "AB,CD,EF,GH"], - f"{prefix}TriggerInhibitM", + self.TriggerInhibit, f"{prefix}TriggerInhibitM" ) self.trigger_level = epics_signal_io(float, f"{prefix}TriggerLevelA") self.trigger_rate = epics_signal_io(float, f"{prefix}TriggerRateA") @@ -152,11 +171,11 @@ def __init__(self, prefix: str, name: str = ""): self.trigger_holdoff = epics_signal_io(float, f"{prefix}TriggerHoldoffA") self.trigger_prescale = epics_signal_io(int, f"{prefix}TriggerPrescaleL") # Burst settings - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.burst_mode = epics_signal_io(bool, f"{prefix}BurstModeB") self.burst_count = epics_signal_io(int, f"{prefix}BurstCountL") self.burst_config = epics_signal_io( - SubsetEnum["All Cycles", "1st Cycle"], f"{prefix}BurstConfigB" + self.BurstConfig, f"{prefix}BurstConfigB" ) self.burst_delay = epics_signal_io(float, f"{prefix}BurstDelayA") self.burst_period = epics_signal_io(float, f"{prefix}BurstPeriodA") diff --git a/src/haven/devices/detectors/aravis.py b/src/haven/devices/detectors/aravis.py index 3710c6df..e865e444 100644 --- a/src/haven/devices/detectors/aravis.py +++ b/src/haven/devices/detectors/aravis.py @@ -1,10 +1,13 @@ from ophyd_async.core import SubsetEnum from ophyd_async.epics.adaravis import AravisDetector as DetectorBase -from ophyd_async.epics.signal import epics_signal_rw_rbv +from ophyd_async.epics.core import epics_signal_rw_rbv from .area_detectors import HavenDetector -AravisTriggerSource = SubsetEnum["Software", "Line1"] + +class AravisTriggerSource(SubsetEnum): + SOFTWARE = "Software" + LINE1 = "Line1" class AravisDetector(HavenDetector, DetectorBase): diff --git a/src/haven/devices/energy_positioner.py b/src/haven/devices/energy_positioner.py index 7ec13ab2..6e1dd529 100644 --- a/src/haven/devices/energy_positioner.py +++ b/src/haven/devices/energy_positioner.py @@ -4,8 +4,8 @@ CALCULATE_TIMEOUT, AsyncStatus, CalculatableTimeout, - HintedSignal, Signal, + StandardReadableFormat, ) from ..positioner import Positioner @@ -78,7 +78,7 @@ def __init__( forward=self.set_energy, inverse=self.get_energy, ) - with self.add_children_as_readables(HintedSignal): + with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL): self.readback = derived_signal_r( float, derived_from={"mono": self.monochromator.energy.user_readback}, diff --git a/src/haven/devices/ion_chamber.py b/src/haven/devices/ion_chamber.py index 86b381bc..7b28c8ba 100644 --- a/src/haven/devices/ion_chamber.py +++ b/src/haven/devices/ion_chamber.py @@ -9,8 +9,8 @@ from ophyd_async.core import ( DEFAULT_TIMEOUT, AsyncStatus, - ConfigSignal, StandardReadable, + StandardReadableFormat, TriggerInfo, soft_signal_rw, wait_for_value, @@ -65,24 +65,64 @@ def __init__( self._scaler_channel = scaler_channel self._voltmeter_channel = voltmeter_channel self.auto_name = auto_name - with self.add_children_as_readables(): - self.preamp = SRS570PreAmplifier(preamp_prefix) - self.voltmeter = LabJackT7( - prefix=voltmeter_prefix, - analog_inputs=[voltmeter_channel], - digital_ios=[], - analog_outputs=[], - digital_words=[], - ) - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.counts_per_volt_second = soft_signal_rw( float, initial_value=counts_per_volt_second ) - # Add subordinate devices + # Add the SRS570 pre-amplifier signals + with self.add_children_as_readables(): + self.preamp = SRS570PreAmplifier(preamp_prefix) + # Add the labjack voltmeter device + self.voltmeter = LabJackT7( + prefix=voltmeter_prefix, + analog_inputs=[voltmeter_channel], + digital_ios=[], + analog_outputs=[], + digital_words=[], + ) + self.add_readables([self.voltmeter_channel.final_value]) + self.add_readables( + [ + self.voltmeter.analog_in_resolution_all, + self.voltmeter.analog_in_sampling_rate, + self.voltmeter.analog_in_settling_time_all, + self.voltmeter_channel.description, + self.voltmeter_channel.device_type, + self.voltmeter_channel.differential, + self.voltmeter_channel.enable, + self.voltmeter_channel.high, + self.voltmeter_channel.input_link, + self.voltmeter_channel.low, + self.voltmeter_channel.mode, + self.voltmeter_channel.range, + self.voltmeter_channel.resolution, + self.voltmeter_channel.scanning_rate, + self.voltmeter_channel.temperature_units, + self.voltmeter.device_temperature, + self.voltmeter.driver_version, + self.voltmeter.firmware_version, + self.voltmeter.last_error_message, + self.voltmeter.ljm_version, + self.voltmeter.model_name, + self.voltmeter.poll_sleep_ms, + self.voltmeter.serial_number, + ], + StandardReadableFormat.CONFIG_SIGNAL, + ) + # Add scaler channel self.mcs = MultiChannelScaler( prefix=scaler_prefix, channels=[0, scaler_channel] ) - self.add_readables([self.mcs.scaler]) + self.add_readables( + [ + self.mcs.scaler.channels[0].net_count, + self.mcs.scaler.channels[0].raw_count, + self.scaler_channel.net_count, + self.scaler_channel.raw_count, + self.mcs.scaler.elapsed_time, + ], + StandardReadableFormat.UNCACHED_SIGNAL, + ) self.add_readables( [ self.mcs.acquire_mode, @@ -106,24 +146,38 @@ def __init__( self.mcs.prescale, self.mcs.preset_time, self.mcs.snl_connected, + self.mcs.scaler.clock_frequency, + self.mcs.scaler.count_mode, + self.mcs.scaler.delay, + self.mcs.scaler.preset_time, + self.mcs.scaler.channels[0].description, + self.mcs.scaler.channels[0].is_gate, + self.mcs.scaler.channels[0].offset_rate, + self.mcs.scaler.channels[0].preset_count, + self.scaler_channel.description, + self.scaler_channel.is_gate, + self.scaler_channel.offset_rate, + self.scaler_channel.preset_count, ], - ConfigSignal, + StandardReadableFormat.CONFIG_SIGNAL, ) # Add calculated signals - with self.add_children_as_readables(): - self.net_current = derived_signal_r( - float, - name="current", - units="A", - derived_from={ - "gain": self.preamp.gain, - "count": self.scaler_channel.net_count, - "clock_count": self.mcs.scaler.channels[0].raw_count, - "clock_frequency": self.mcs.scaler.clock_frequency, - "counts_per_volt_second": self.counts_per_volt_second, - }, - inverse=self._counts_to_amps, - ) + self.net_current = derived_signal_r( + float, + name="current", + units="A", + derived_from={ + "gain": self.preamp.gain, + "count": self.scaler_channel.net_count, + "clock_count": self.mcs.scaler.channels[0].raw_count, + "clock_frequency": self.mcs.scaler.clock_frequency, + "counts_per_volt_second": self.counts_per_volt_second, + }, + inverse=self._counts_to_amps, + ) + self.add_readables( + [self.net_current], StandardReadableFormat.HINTED_UNCACHED_SIGNAL + ) # Measured current without dark current correction self.raw_current = derived_signal_r( float, @@ -138,6 +192,7 @@ def __init__( }, inverse=self._counts_to_amps, ) + self.add_readables([self.raw_current], StandardReadableFormat.UNCACHED_SIGNAL) super().__init__(name=name) def _counts_to_amps( @@ -161,7 +216,10 @@ def _counts_to_amps( return float("nan") def __repr__(self): - return f"<{type(self).__name__}: '{self.name}' ({self.scaler_channel.raw_count.source})>" + return ( + f"<{type(self).__name__}: '{self.name}' " + f"({self.scaler_channel.raw_count.source})>" + ) @property def scaler_channel(self): @@ -237,7 +295,7 @@ async def trigger(self, record_dark_current=False): await last_status return # Nothing to wait on yet, so trigger the scaler and stash the result - st = signal.set(self.mcs.scaler.CountState.COUNT) + st = signal.set(True) self._trigger_statuses[signal.source] = st await st @@ -248,9 +306,7 @@ async def record_dark_current(self): integration_time = await self.mcs.scaler.dark_current_time.get_value() timeout = integration_time + DEFAULT_TIMEOUT count_signal = self.mcs.scaler.count - await wait_for_value( - count_signal, self.mcs.scaler.CountState.DONE, timeout=timeout - ) + await wait_for_value(count_signal, False, timeout=timeout) def record_fly_reading(self, reading, **kwargs): if self._is_flying: diff --git a/src/haven/devices/labjack.py b/src/haven/devices/labjack.py index c247d330..9fbfd6a0 100644 --- a/src/haven/devices/labjack.py +++ b/src/haven/devices/labjack.py @@ -38,20 +38,19 @@ def __init__(self, prefix: str, name: str = ""): import numpy as np from bluesky.protocols import Triggerable -from numpy.typing import NDArray from ophyd_async.core import ( DEFAULT_TIMEOUT, + Array1D, AsyncStatus, - ConfigSignal, DeviceVector, - HintedSignal, StandardReadable, + StandardReadableFormat, + StrictEnum, SubsetEnum, observe_value, ) -from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw, epics_signal_x +from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x -from ..typing import StrEnum from .synApps import EpicsRecordInputFields, EpicsRecordOutputFields __all__ = [ @@ -86,25 +85,6 @@ def __init__(self, prefix: str, name: str = ""): class BinaryInput(Input): - DeviceType = SubsetEnum[ - "Soft Channel", - "Raw Soft Channel", - "Async Soft Channel", - "Db State", - "asynInt32", - "asynUInt32Digital", - "asyn bi stringParm", - "asyn MPC", - "Vg307 GPIB Instrument", - "asyn Televac", - "asyn TPG261", - "lua", - "stream", - "EtherIP", - "dg535", - "Zed", - ] - def __init__(self, prefix: str, name: str = ""): with self.add_children_as_readables(): self.final_value = epics_signal_r(bool, f"{prefix}.VAL") @@ -123,59 +103,22 @@ class Output(EpicsRecordOutputFields): class BinaryOutput(Output): """A binary input on the labjack.""" - DeviceType = SubsetEnum[ - "Soft Channel", - "Raw Soft Channel", - "Async Soft Channel", - "General Time", - "Db State", - "asynInt32", - "asynUInt32Digital", - "asyn bo stringParm", - "asyn MPC", - "Vg307 GPIB Instrument", - "PZT Bug", - "asyn TPG261", - "lua", - "stream", - "EtherIP", - "dg535", - ] - def __init__(self, prefix: str, name: str = ""): with self.add_children_as_readables(): self.desired_value = epics_signal_rw(bool, f"{prefix}.VAL") self.raw_value = epics_signal_rw(float, f"{prefix}.RVAL") self.readback_value = epics_signal_r(float, f"{prefix}.RBV") + super().__init__(prefix=prefix, name=name) class AnalogOutput(Output): """An analog output on a labjack device.""" - DeviceType = SubsetEnum[ - "Soft Channel", - "Raw Soft Channel", - "Async Soft Channel", - "asynInt32", - "asynFloat64", - "asynInt64", - "IOC stats", - "asyn ao stringParm", - "asyn ao Eurotherm", - "asyn MPC", - "PZT Bug", - "asyn TPG261", - "lua", - "stream", - "EtherIP", - "dg535", - ] - def __init__(self, prefix: str, name: str = ""): with self.add_children_as_readables(): self.desired_value = epics_signal_rw(float, f"{prefix}.VAL") self.raw_value = epics_signal_rw(int, f"{prefix}.RVAL") - with self.add_children_as_readables(HintedSignal): + with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL): self.readback_value = epics_signal_r(int, f"{prefix}.RBV") super().__init__(prefix=prefix, name=name) @@ -188,16 +131,16 @@ class AnalogInput(Input, Triggerable): """ - class DifferentialMode(StrEnum): + class DifferentialMode(StrictEnum): SINGLE_ENDED = "Single-Ended" DIFFERENTIAL = "Differential" - class TemperatureUnits(StrEnum): + class TemperatureUnits(SubsetEnum): KELVIN = "K" CELSIUS = "C" FAHRENHEIT = "F" - class Mode(StrEnum): + class Mode(SubsetEnum): VOLTS = "Volts" TYPE_B_TC = "Type B TC" TYPE_C_TC = "Type C TC" @@ -209,13 +152,13 @@ class Mode(StrEnum): TYPE_S_TC = "Type S TC" TYPE_T_TC = "Type T TC" - class Range(StrEnum): + class Range(SubsetEnum): TEN_VOLTS = "+= 10V" ONE_VOLT = "+= 1V" HUNDRED_MILLIVOLTS = "+= 0.1V" TEN_MILLIVOLTS = "+= 0.01V" - class Resolution(StrEnum): + class Resolution(SubsetEnum): DEFAULT = "Default" ONE = "1" TWO = "2" @@ -226,27 +169,8 @@ class Resolution(StrEnum): SEVEN = "7" EIGHT = "8" - DeviceType = SubsetEnum[ - "Soft Channel", - "Raw Soft Channel", - "Async Soft Channel", - "Soft Timestamp", - "General Time", - "asynInt32", - "asynInt32Average", - "asynFloat64", - "asynFloat64Average", - "asynInt64", - "IOC stats", - "IOC stats clusts", - "GPIB init/report", - "Sec Past Epoch", - "asyn ai stringParm", - "asyn ai HeidND261", - ] - def __init__(self, prefix: str, ch_num: int, name: str = ""): - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.differential = epics_signal_rw( self.DifferentialMode, f"{prefix}AiDiff{ch_num}" ) @@ -262,7 +186,7 @@ def __init__(self, prefix: str, ch_num: int, name: str = ""): self.range = epics_signal_rw(self.Range, f"{prefix}AiRange{ch_num}") self.mode = epics_signal_rw(self.Mode, f"{prefix}AiMode{ch_num}") self.enable = epics_signal_rw(bool, f"{prefix}AiEnable{ch_num}") - with self.add_children_as_readables(HintedSignal): + with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL): self.final_value = epics_signal_r(float, f"{prefix}Ai{ch_num}.VAL") self.raw_value = epics_signal_rw(int, f"{prefix}Ai{ch_num}.RVAL") super().__init__(prefix=f"{prefix}Ai{ch_num}", name=name) @@ -291,7 +215,7 @@ class DigitalIO(StandardReadable): """ - class Direction(StrEnum): + class Direction(SubsetEnum): INPUT = "In" OUTPUT = "Out" @@ -299,7 +223,7 @@ def __init__(self, prefix: str, ch_num: int, name: str = ""): with self.add_children_as_readables(): self.input = BinaryInput(f"{prefix}Bi{ch_num}") self.output = BinaryOutput(f"{prefix}Bo{ch_num}") - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.direction = epics_signal_rw(self.Direction, f"{prefix}Bd{ch_num}") super().__init__(name=name) @@ -307,61 +231,74 @@ def __init__(self, prefix: str, ch_num: int, name: str = ""): class WaveformDigitizer(StandardReadable, Triggerable): """A feature of the Labjack devices that allows waveform capture.""" - class TriggerSource(StrEnum): + class TriggerSource(StrictEnum): INTERNAL = "Internal" EXTERNAL = "External" + class ReadWaveform(StrictEnum): + DONE = "Done" + READ = "Read" + + class FirstChannel(SubsetEnum): + ONE = "1" + TWO = "2" + THREE = "3" + FOUR = "4" + FIVE = "5" + SIX = "6" + SEVEN = "7" + EIGHT = "8" + NINE = "9" + TEN = "10" + ELEVEN = "11" + TWELVE = "12" + THIRTEEN = "13" + + class NumberOfChannels(SubsetEnum): + ONE = "1" + TWO = "2" + THREE = "3" + FOUR = "4" + FIVE = "5" + SIX = "6" + SEVEN = "7" + EIGHT = "8" + NINE = "9" + TEN = "10" + ELEVEN = "11" + TWELVE = "12" + THIRTEEN = "13" + FOURTEEN = "14" + + class Resolution(SubsetEnum): + DEFAULT = "Default" + ONE = "1" + TWO = "2" + THREE = "3" + FOUR = "4" + FIVE = "5" + SIX = "6" + SEVEN = "7" + EIGHT = "8" + def __init__(self, prefix: str, name: str = "", waveforms=[]): with self.add_children_as_readables(): self.timebase_waveform = epics_signal_rw( - NDArray[np.float64], f"{prefix}WaveDigTimeWF" + Array1D[np.float64], f"{prefix}WaveDigTimeWF" ) self.dwell_actual = epics_signal_rw(float, f"{prefix}WaveDigDwellActual") self.total_time = epics_signal_rw(float, f"{prefix}WaveDigTotalTime") - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.num_points = epics_signal_rw(int, f"{prefix}WaveDigNumPoints") self.dwell_time = epics_signal_rw(float, f"{prefix}WaveDigDwell") self.first_chan = epics_signal_rw( - SubsetEnum[ - "0", - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - "10", - "11", - "12", - "13", - ], - f"{prefix}WaveDigFirstChan", + self.FirstChannel, f"{prefix}WaveDigFirstChan" ) self.num_chans = epics_signal_rw( - SubsetEnum[ - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - "10", - "11", - "12", - "13", - "14", - ], - f"{prefix}WaveDigNumChans", + self.NumberOfChannels, f"{prefix}WaveDigNumChans" ) self.resolution = epics_signal_rw( - SubsetEnum["Default", "1", "2", "3", "4", "5", "6", "7", "8"], - f"{prefix}WaveDigResolution", + self.Resolution, f"{prefix}WaveDigResolution" ) self.settling_time = epics_signal_rw(float, f"{prefix}WaveDigSettlingTime") self.current_point = epics_signal_rw(int, f"{prefix}WaveDigCurrentPoint") @@ -372,14 +309,14 @@ def __init__(self, prefix: str, name: str = "", waveforms=[]): self.auto_restart = epics_signal_x(f"{prefix}WaveDigAutoRestart") self.run = epics_signal_rw(bool, f"{prefix}WaveDigRun") self.read_waveform = epics_signal_rw( - SubsetEnum["Done", "Read"], f"{prefix}WaveDigReadWF" + self.ReadWaveform, f"{prefix}WaveDigReadWF" ) # Add waveforms with self.add_children_as_readables(): self.waveforms = DeviceVector( { idx: epics_signal_r( - NDArray[np.float64], f"{prefix}WaveDigVoltWF{idx}" + Array1D[np.float64], f"{prefix}WaveDigVoltWF{idx}" ) for idx in waveforms } @@ -405,7 +342,7 @@ async def trigger(self): class WaveformGenerator(StandardReadable): """A feature of the Labjack devices that generates output waveforms.""" - class WaveType(StrEnum): + class WaveType(StrictEnum): USER_DEFINED = "User-defined" SINE_WAVE = "Sin wave" SQUARE_WAVE = "Square wave" @@ -415,12 +352,12 @@ class WaveType(StrEnum): TriggerSource = WaveformDigitizer.TriggerSource - class TriggerMode(StrEnum): + class TriggerMode(StrictEnum): ONE_SHOT = "One-shot" CONTINUOS = "Continuous" def __init__(self, prefix: str, name: str = ""): - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.external_trigger = epics_signal_rw( self.TriggerSource, f"{prefix}WaveGenExtTrigger" ) @@ -439,14 +376,14 @@ def __init__(self, prefix: str, name: str = ""): self.dwell = epics_signal_r(float, f"{prefix}WaveGenDwell") self.dwell_actual = epics_signal_r(float, f"{prefix}WaveGenDwellActual") self.total_time = epics_signal_r(float, f"{prefix}WaveGenTotalTime") - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.num_points = epics_signal_r(int, f"{prefix}WaveGenNumPoints") self.current_point = epics_signal_r(int, f"{prefix}WaveGenCurrentPoint") # Settings for user-defined waveforms with self.add_children_as_readables(): self.user_time_waveform = epics_signal_rw( - NDArray[np.float64], f"{prefix}WaveGenUserTimeWF" + Array1D[np.float64], f"{prefix}WaveGenUserTimeWF" ) self.user_num_points = epics_signal_rw(int, f"{prefix}WaveGenUserNumPoints") self.user_dwell = epics_signal_rw(float, f"{prefix}WaveGenUserDwell") @@ -455,16 +392,16 @@ def __init__(self, prefix: str, name: str = ""): # Settings for internal waveforms with self.add_children_as_readables(): self.internal_time_waveform = epics_signal_rw( - NDArray[np.float64], f"{prefix}WaveGenIntTimeWF" + Array1D[np.float64], f"{prefix}WaveGenIntTimeWF" ) self.internal_num_points = epics_signal_rw(int, f"{prefix}WaveGenIntNumPoints") self.internal_dwell = epics_signal_rw(float, f"{prefix}WaveGenIntDwell") self.internal_frequency = epics_signal_rw(float, f"{prefix}WaveGenIntFrequency") # Waveform specific settings - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.user_waveform_0 = epics_signal_rw( - NDArray[np.float64], f"{prefix}WaveGenUserWF0" + Array1D[np.float64], f"{prefix}WaveGenUserWF0" ) self.enable_0 = epics_signal_rw(bool, f"{prefix}WaveGenEnable0") self.type_0 = epics_signal_rw(self.WaveType, f"{prefix}WaveGenType0") @@ -472,7 +409,7 @@ def __init__(self, prefix: str, name: str = ""): self.amplitude_0 = epics_signal_rw(float, f"{prefix}WaveGenAmplitude0") self.offset_0 = epics_signal_rw(float, f"{prefix}WaveGenOffset0") self.user_waveform_1 = epics_signal_rw( - NDArray[np.float64], f"{prefix}WaveGenUserWF1" + Array1D[np.float64], f"{prefix}WaveGenUserWF1" ) self.enable_1 = epics_signal_rw(bool, f"{prefix}WaveGenEnable1") self.type_1 = epics_signal_rw(self.WaveType, f"{prefix}WaveGenType1") @@ -480,10 +417,10 @@ def __init__(self, prefix: str, name: str = ""): self.amplitude_1 = epics_signal_rw(float, f"{prefix}WaveGenAmplitude1") self.offset_1 = epics_signal_rw(float, f"{prefix}WaveGenOffset1") self.internal_waveform_0 = epics_signal_rw( - NDArray[np.float64], f"{prefix}WaveGenInternalWF0" + Array1D[np.float64], f"{prefix}WaveGenInternalWF0" ) self.internal_waveform_1 = epics_signal_r( - NDArray[np.float64], f"{prefix}WaveGenInternalWF1" + Array1D[np.float64], f"{prefix}WaveGenInternalWF1" ) super().__init__(name=name) @@ -532,26 +469,8 @@ class LabJackBase(StandardReadable): """ Resolution = AnalogInput.Resolution - DeviceType = SubsetEnum[ - "Soft Channel", - "Raw Soft Channel", - "Async Soft Channel", - "Soft Timestamp", - "General Time", - "asynInt32", - "asynInt32Average", - "asynFloat64", - "asynFloat64Average", - "asynInt64", - "IOC stats", - "IOC stats clusts", - "GPIB init/report", - "Sec Past Epoch", - "asyn ai stringParm", - "asyn ai HeidND261", - ] - - class Model(StrEnum): + + class Model(StrictEnum): T4 = "T4" T7 = "T7" T7_PRO = "T7-Pro" @@ -566,7 +485,7 @@ def __init__( analog_outputs=range(2), digital_words=["dio", "eio", "fio", "mio", "cio"], ): - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.model_name = epics_signal_r(self.Model, f"{prefix}ModelName") self.firmware_version = epics_signal_r(str, f"{prefix}FirmwareVersion") self.serial_number = epics_signal_r(str, f"{prefix}SerialNumber") @@ -576,7 +495,7 @@ def __init__( self.ljm_version = epics_signal_r(str, f"{prefix}LJMVersion") self.driver_version = epics_signal_r(str, f"{prefix}DriverVersion") self.last_error_message = epics_signal_r( - NDArray[np.uint8], f"{prefix}LastErrorMessage" + Array1D[np.uint8], f"{prefix}LastErrorMessage" ) self.poll_sleep_ms = epics_signal_rw(float, f"{prefix}PollSleepMS") self.analog_in_settling_time_all = epics_signal_rw( @@ -606,12 +525,8 @@ def __init__( self.digital_ios = DeviceVector( {idx: DigitalIO(prefix, ch_num=idx) for idx in digital_ios} ) - self.digital_words = DeviceVector( - { - word: epics_signal_r(int, f"{prefix}{word.upper()}In") - for word in digital_words - } - ) + for word in digital_words: + setattr(self, word, epics_signal_r(int, f"{prefix}{word.upper()}In")) # Waveform devices (not read by default, should be made readable as needed) self.waveform_digitizer = WaveformDigitizer( f"{prefix}", waveforms=analog_inputs diff --git a/src/haven/devices/mirrors.py b/src/haven/devices/mirrors.py index 0df96e4e..fc4d2e5f 100644 --- a/src/haven/devices/mirrors.py +++ b/src/haven/devices/mirrors.py @@ -59,6 +59,7 @@ def __init__( transform_prefix = "".join(prefix.rsplit(":", 2)) self.drive_transform = TransformRecord(f"{transform_prefix}:Drive") self.readback_transform = TransformRecord(f"{transform_prefix}:Readback") + super().__init__(name=name) class KBMirrors(Device): diff --git a/src/haven/devices/monochromator.py b/src/haven/devices/monochromator.py index eb30e634..e59d575c 100644 --- a/src/haven/devices/monochromator.py +++ b/src/haven/devices/monochromator.py @@ -1,8 +1,7 @@ import logging -from enum import Enum -from ophyd_async.core import ConfigSignal, StandardReadable -from ophyd_async.epics.signal import epics_signal_rw +from ophyd_async.core import StandardReadable, StandardReadableFormat, StrictEnum +from ophyd_async.epics.core import epics_signal_rw from .motor import Motor @@ -12,7 +11,7 @@ class Monochromator(StandardReadable): _ophyd_labels_ = {"monochromators"} - class Mode(str, Enum): + class Mode(StrictEnum): FIXED_OFFSET = "Si(111) Fixed Offset" CHANNEL_CUT = "Si(111) Channel-cut" ML48 = "Multi-layer 4.8nm" @@ -30,7 +29,7 @@ def __init__(self, prefix: str, name: str = ""): self.vert = Motor(f"{prefix}ACS:m2") self.roll2 = Motor(f"{prefix}ACS:m5") self.pitch2 = Motor(f"{prefix}ACS:m6") - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): # Transform constants, etc. self.id_tracking = epics_signal_rw(bool, f"{prefix}ID_tracking") self.id_offset = epics_signal_rw(float, f"{prefix}ID_offset") diff --git a/src/haven/devices/motor.py b/src/haven/devices/motor.py index 7b83ca19..b2813a64 100644 --- a/src/haven/devices/motor.py +++ b/src/haven/devices/motor.py @@ -3,9 +3,14 @@ from ophyd import Component as Cpt from ophyd import EpicsMotor, EpicsSignal, EpicsSignalRO, Kind -from ophyd_async.core import DEFAULT_TIMEOUT, ConfigSignal, SubsetEnum +from ophyd_async.core import ( + DEFAULT_TIMEOUT, + StandardReadableFormat, + StrictEnum, + SubsetEnum, +) +from ophyd_async.epics.core import epics_signal_r, epics_signal_rw from ophyd_async.epics.motor import Motor as MotorBase -from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw from ophydregistry import Registry from .motor_flyer import MotorFlyer @@ -17,6 +22,14 @@ class Motor(MotorBase): """The default motor for asynchrnous movement.""" + class Direction(StrictEnum): + POSITIVE = "Pos" + NEGATIVE = "Neg" + + class FreezeSwitch(SubsetEnum): + VARIABLE = "Variable" + FROZEN = "Frozen" + def __init__( self, prefix: str, name="", labels={"motors"}, auto_name: bool = None ) -> None: @@ -32,14 +45,12 @@ def __init__( self._old_flyer_velocity = None self.auto_name = auto_name # Configuration signals - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.description = epics_signal_rw(str, f"{prefix}.DESC") self.user_offset = epics_signal_rw(float, f"{prefix}.OFF") - self.user_offset_dir = epics_signal_rw( - SubsetEnum["Pos", "Neg"], f"{prefix}.DIR" - ) + self.user_offset_dir = epics_signal_rw(self.Direction, f"{prefix}.DIR") self.offset_freeze_switch = epics_signal_rw( - SubsetEnum["Variable", "Frozen"], f"{prefix}.FOFF" + self.FreezeSwitch, f"{prefix}.FOFF" ) # Motor status signals self.motor_is_moving = epics_signal_r(int, f"{prefix}.MOVN") diff --git a/src/haven/devices/scaler.py b/src/haven/devices/scaler.py index d67aa749..cd758828 100644 --- a/src/haven/devices/scaler.py +++ b/src/haven/devices/scaler.py @@ -1,11 +1,13 @@ -from enum import Enum - import numpy as np -from numpy.typing import NDArray -from ophyd_async.core import ConfigSignal, DeviceVector, HintedSignal, StandardReadable -from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw, epics_signal_x - -from ..typing import StrEnum +from ophyd_async.core import ( + Array1D, + DeviceVector, + StandardReadable, + StandardReadableFormat, + StrictEnum, + SubsetEnum, +) +from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x def num_to_char(num): @@ -17,7 +19,7 @@ class ScalerChannel(StandardReadable): def __init__(self, prefix, channel_num, name=""): epics_ch_num = channel_num + 1 # EPICS is 1-indexed # Hinted signals - with self.add_children_as_readables(HintedSignal): + with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL): net_suffix = ( f"_net{num_to_char((channel_num // 12))}" f".{num_to_char(channel_num % 12)}" @@ -27,7 +29,7 @@ def __init__(self, prefix, channel_num, name=""): with self.add_children_as_readables(): self.raw_count = epics_signal_r(float, f"{prefix}.S{epics_ch_num}") # Configuration signals - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.description = epics_signal_rw(str, f"{prefix}.NM{epics_ch_num}") self.is_gate = epics_signal_rw(bool, f"{prefix}.G{epics_ch_num}") self.preset_count = epics_signal_rw(float, f"{prefix}.PR{epics_ch_num}") @@ -38,17 +40,17 @@ def __init__(self, prefix, channel_num, name=""): class MCA(StandardReadable): - class MCAMode(str, Enum): + class MCAMode(SubsetEnum): PHA = "PHA" MCS = "MCS" LIST = "List" def __init__(self, prefix, name=""): # Signals - with self.add_children_as_readables(HintedSignal): - self.spectrum = epics_signal_r(NDArray[np.int32], f"{prefix}.VAL") - self.background = epics_signal_r(NDArray[np.int32], f"{prefix}.BG") - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL): + self.spectrum = epics_signal_r(Array1D[np.int32], f"{prefix}.VAL") + self.background = epics_signal_r(Array1D[np.int32], f"{prefix}.BG") + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.mode = epics_signal_rw(self.MCAMode, f"{prefix}.MODE") super().__init__(name=name) @@ -68,37 +70,37 @@ class MultiChannelScaler(StandardReadable): _ophyd_labels_ = {"scalers"} - class ChannelAdvanceSource(str, Enum): + class ChannelAdvanceSource(SubsetEnum): INTERNAL = "Internal" EXTERNAL = "External" - class Acquiring(str, Enum): + class Acquiring(StrictEnum): DONE = "Done" ACQUIRING = "Acquiring" - class ScalerModel(str, Enum): + class ScalerModel(SubsetEnum): SIS_3801 = "SIS3801" SIS_3820 = "SIS3820" - class Channel1Source(str, Enum): + class Channel1Source(SubsetEnum): INTERNAL_CLOCK = "Int. clock" EXTERNAL = "External" - class AcquireMode(str, Enum): + class AcquireMode(SubsetEnum): MCS = "MCS" SCALER = "Scaler" - class Polarity(str, Enum): + class Polarity(StrictEnum): NORMAL = "Normal" INVERTED = "Inverted" - class OutputMode(str, Enum): + class OutputMode(SubsetEnum): MODE_0 = "Mode 0" MODE_1 = "Mode 1" MODE_2 = "Mode 2" MODE_3 = "Mode 3" - class InputMode(str, Enum): + class InputMode(SubsetEnum): MODE_0 = "Mode 0" MODE_1 = "Mode 1" MODE_2 = "Mode 2" @@ -120,7 +122,7 @@ def __init__(self, prefix, channels: list[int], name=""): self.acquiring = epics_signal_r(self.Acquiring, f"{prefix}Acquiring") self.user_led = epics_signal_rw(bool, f"{prefix}UserLED") # Config signals - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.preset_time = epics_signal_rw(float, f"{prefix}PresetReal") self.dwell_time = epics_signal_rw(float, f"{prefix}Dwell") self.prescale = epics_signal_rw(int, f"{prefix}Prescale") @@ -168,14 +170,10 @@ def __init__(self, prefix, channels: list[int], name=""): class Scaler(StandardReadable): """A scaler device that has one or more channels.""" - class CountMode(StrEnum): + class CountMode(SubsetEnum): ONE_SHOT = "OneShot" AUTO_COUNT = "AutoCount" - class CountState(StrEnum): - DONE = "Done" - COUNT = "Count" - def __init__(self, prefix, channels: list[int], name=""): # Add invidiaul scaler channels with self.add_children_as_readables(): @@ -189,13 +187,13 @@ def __init__(self, prefix, channels: list[int], name=""): # Scaler-specific signals with self.add_children_as_readables(): self.elapsed_time = epics_signal_r(float, f"{prefix}.T") - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.delay = epics_signal_rw(float, f"{prefix}.DLY") self.clock_frequency = epics_signal_rw(float, f"{prefix}.FREQ") self.count_mode = epics_signal_rw(self.CountMode, f"{prefix}.CONT") self.preset_time = epics_signal_rw(float, f"{prefix}.TP") self.auto_count = epics_signal_rw(bool, f"{prefix}.CONT") - self.count = epics_signal_rw(self.CountState, f"{prefix}.CNT") + self.count = epics_signal_rw(bool, f"{prefix}.CNT") self.record_dark_current = epics_signal_x(f"{prefix}_offset_start.PROC") self.auto_count_delay = epics_signal_rw(float, f"{prefix}.DLY1") self.auto_count_time = epics_signal_rw(float, f"{prefix}.TP1") diff --git a/src/haven/devices/shutter.py b/src/haven/devices/shutter.py index 82dcfd13..8a2cc29c 100644 --- a/src/haven/devices/shutter.py +++ b/src/haven/devices/shutter.py @@ -5,7 +5,7 @@ from ophyd.utils.errors import ReadOnlyError from ophyd_async.core import soft_signal_rw -from ophyd_async.epics.signal import epics_signal_r +from ophyd_async.epics.core import epics_signal_r from ..positioner import Positioner from .signal import derived_signal_rw, epics_signal_xval diff --git a/src/haven/devices/signal.py b/src/haven/devices/signal.py index 9589f08b..0e9ac231 100644 --- a/src/haven/devices/signal.py +++ b/src/haven/devices/signal.py @@ -11,16 +11,17 @@ DEFAULT_TIMEOUT, AsyncStatus, CalculatableTimeout, - ReadingValueCallback, + Callback, SignalBackend, - SignalMetadata, + SignalDatatypeT, SignalR, SignalRW, SignalX, SoftSignalBackend, T, ) -from ophyd_async.epics.signal._signal import _epics_signal_backend +from ophyd_async.core._signal import _wait_for +from ophyd_async.epics.core._signal import _epics_signal_backend class DerivedSignalBackend(SoftSignalBackend): @@ -116,30 +117,48 @@ def inverse(self, values, **kw): msg += "Provide an explicit inverse transform." raise ValueError(msg) - def source(self, name: str = ""): - src = super().source(name) + def source(self, name: str, read: bool): + src = super().source(name, read) args = ",".join(self._derived_from.keys()) return f"{src}({args})" + async def _subscribe_child(self, child_signal): + """Subscribe to a child signal for updating value changes. + + If *child_signal* is not yet connected, keep retrying until + sucessful or the timeout value is reached. + + """ + handler = partial(self.update_readings, signal=child_signal) + while True: + try: + child_signal.subscribe(handler) + except NotImplementedError: + await asyncio.sleep(0.01) + else: + break + async def connect(self, timeout=DEFAULT_TIMEOUT) -> None: - await super().connect(timeout=timeout) - # Ensure dependent signals are connected - connectors = ( - sig.connect(timeout=timeout) for sig in self._derived_from.values() - ) - await asyncio.gather(*connectors) # Listen for changes in the derived_from signals - for sig in self._derived_from.values(): - # Subscribe with a partial in case the signal's name changes - if isinstance(sig, Subscribable): - sig.subscribe(partial(self.update_readings, signal=sig)) + sub_signals = self._derived_from.values() + sub_signals = (sig for sig in sub_signals if isinstance(sig, Subscribable)) + subs = ( + asyncio.wait_for(self._subscribe_child(sig), timeout=timeout) + for sig in sub_signals + ) + await asyncio.gather(super().connect(timeout=timeout), *subs) def combine_readings(self, readings): timestamp = max([rd["timestamp"] for rd in readings.values()]) severity = max([rd.get("severity", 0) for rd in readings.values()]) values = {sig: rdg["value"] for sig, rdg in readings.items()} new_value = self.inverse(values, **self._derived_from) - return self.converter.reading(new_value, timestamp, severity) + self.reading = Reading( + value=self.converter.write_value(new_value), + timestamp=timestamp, + alarm_severity=severity, + ) + return self.reading def update_readings(self, reading, signal): """Callback receives readings from derived_from signals. @@ -163,9 +182,9 @@ def send_latest_reading(self): # We have all the readings, so update the cached values new_reading = self.combine_readings(readings) if self.callback is not None: - self.callback(new_reading, new_reading["value"]) + self.callback(new_reading) - def set_callback(self, callback: Optional[ReadingValueCallback[T]]) -> None: + def set_callback(self, callback: Callback[Reading[SignalDatatypeT]] | None) -> None: super().set_callback(callback) self.send_latest_reading() @@ -173,7 +192,7 @@ async def put(self, value: Optional[T], wait=True, timeout=None): write_value = ( self.converter.write_value(value) if value is not None - else self._initial_value + else self.initial_value ) # Calculate the derived set points new_values = await self.forward(write_value, **self._derived_from) @@ -199,21 +218,6 @@ async def get_reading(self) -> Reading: # Return a proper reading for this derived value return self.combine_readings(readings) - async def get_value(self) -> T: - # Sort out which types of signals we have - gettable_signals = [ - sig for sig in self._derived_from.values() if hasattr(sig, "get_value") - ] - # Retrieve current values from signals - values = await asyncio.gather(*(sig.get_value() for sig in gettable_signals)) - values = {sig: val for sig, val in zip(gettable_signals, values)} - # Set default value of None for missing signals - for sig in self._derived_from.values(): - values.setdefault(sig, None) - # Compute the new value - new_value = self.inverse(values, **self._derived_from) - return self.converter.value(new_value) - def derived_signal_rw( datatype: Optional[Type[T]], @@ -287,14 +291,14 @@ def __init__(self, prefix, name="", **kwargs): values. """ - metadata = SignalMetadata(units=units, precision=precision) backend = DerivedSignalBackend( datatype, derived_from=derived_from, forward=forward, inverse=inverse, initial_value=initial_value, - metadata=metadata, + units=units, + precision=precision, ) signal = SignalRW(backend, name=name) return signal @@ -357,13 +361,13 @@ def __init__(self, prefix, name="", **kwargs): values. """ - metadata = SignalMetadata(units=units, precision=precision) backend = DerivedSignalBackend( datatype, derived_from=derived_from, inverse=inverse, initial_value=initial_value, - metadata=metadata, + units=units, + precision=precision, ) signal = SignalR(backend, name=name) return signal @@ -422,12 +426,10 @@ def __init__(self, prefix, name="", **kwargs): values. """ - metadata = SignalMetadata() backend = DerivedSignalBackend( int, derived_from=derived_from, forward=forward, - metadata=metadata, ) signal = SignalX(backend, name=name) return signal @@ -440,14 +442,19 @@ def __init__(self, *args, trigger_value=1, **kwargs): self.trigger_value = trigger_value super().__init__(*args, **kwargs) - def trigger( - self, wait=False, timeout: CalculatableTimeout = CALCULATE_TIMEOUT - ) -> AsyncStatus: + @AsyncStatus.wrap + async def trigger( + self, wait=True, timeout: CalculatableTimeout = CALCULATE_TIMEOUT + ) -> None: """Trigger the action and return a status saying when it's done""" - if timeout is CALCULATE_TIMEOUT: + if timeout == CALCULATE_TIMEOUT: timeout = self._timeout - coro = self._backend.put(self.trigger_value, wait=wait, timeout=timeout) - return AsyncStatus(coro) + source = self._connector.backend.source(self.name, read=False) + self.log.debug(f"Putting default value to backend at source {source}") + await _wait_for( + self._connector.backend.put(self.trigger_value, wait=wait), timeout, source + ) + self.log.debug(f"Successfully put default value to backend at source {source}") def epics_signal_xval(write_pv: str, name: str = "", trigger_value=1) -> SignalXVal: diff --git a/src/haven/devices/srs570.py b/src/haven/devices/srs570.py index a502da04..c189dd5f 100644 --- a/src/haven/devices/srs570.py +++ b/src/haven/devices/srs570.py @@ -16,7 +16,6 @@ import logging import math from collections import OrderedDict -from enum import Enum from typing import Optional, Type from ophyd_async.core import ( @@ -25,11 +24,11 @@ CalculatableTimeout, Device, SignalRW, - SubsetEnum, + StrictEnum, T, ) -from ophyd_async.epics.signal import epics_signal_rw, epics_signal_x -from ophyd_async.epics.signal._signal import _epics_signal_backend +from ophyd_async.epics.core import epics_signal_rw, epics_signal_x +from ophyd_async.epics.core._signal import _epics_signal_backend from .. import exceptions from .signal import derived_signal_r, derived_signal_rw @@ -42,12 +41,12 @@ gain_modes = ["LOW NOISE", "HIGH BW"] -class Sign(str, Enum): +class Sign(StrictEnum): PLUS = "+" MINUS = "-" -class Cal(str, Enum): +class Cal(StrictEnum): CAL = "CAL" UNCAL = "UNCAL" @@ -257,20 +256,52 @@ class SRS570PreAmplifier(Device): offset_difference = -3 # How many levels higher should the offset be - class FilterType(str, Enum): - NO_FILTER = "No filter" - _6DB_HIGHPASS = "6 dB highpass" + class FilterType(StrictEnum): + NO_FILTER = " No filter" + _6DB_HIGHPASS = " 6 dB highpass" _12DB_HIGHPASS = "12 dB highpass" - _6DB_BANDPASS = "6 dB bandpass" - _6DB_LOWPASS = "6 dB lowpass" - _12DB_LOWPASS = "6 dB lowpass" - - class GainMode(str, Enum): + _6DB_BANDPASS = " 6 dB bandpass" + _6DB_LOWPASS = " 6 dB lowpass" + _12DB_LOWPASS = "12 dB lowpass" + + class FilterLowPass(StrictEnum): + _0_03_HZ = " 0.03 Hz" + _0_1_HZ = " 0.1 Hz" + _0_3_HZ = " 0.3 Hz" + _1_HZ = " 1 Hz" + _3_HZ = " 3 Hz" + _10_HZ = " 10 Hz" + _30_HZ = " 30 Hz" + _100_HZ = "100 Hz" + _300_HZ = "300 Hz" + _1_KHZ = " 1 kHz" + _3_KHZ = " 3 kHz" + _10_KHZ = " 10 kHz" + _30_KHZ = " 30 kHz" + _100_KHZ = "100 kHz" + _300_KHZ = "300 kHz" + _1_MHZ = " 1 MHz" + + class FilterHighPass(StrictEnum): + _0_03_HZ = " 0.03 Hz" + _0_1_HZ = " 0.1 Hz" + _0_3_HZ = " 0.3 Hz" + _1_HZ = " 1 Hz" + _3_HZ = " 3 Hz" + _10_HZ = " 10 Hz" + _30_HZ = " 30 Hz" + _100_HZ = "100 Hz" + _300_HZ = "300 Hz" + _1_KHZ = " 1 kHz" + _3_KHZ = " 3 kHz" + _10_KHZ = " 10 kHz" + + class GainMode(StrictEnum): LOW_NOISE = "LOW NOISE" HIGH_BW = "HIGH BW" LOW_DRIFT = "LOW DRIFT" - class SensValue(str, Enum): + class SensValue(StrictEnum): _1 = "1" _2 = "2" _5 = "5" @@ -281,13 +312,13 @@ class SensValue(str, Enum): _200 = "200" _500 = "500" - class SensUnit(str, Enum): + class SensUnit(StrictEnum): pA_V = "pA/V" nA_V = "nA/V" uA_V = "uA/V" mA_V = "mA/V" - class OffsetUnit(str, Enum): + class OffsetUnit(StrictEnum): pA = "pA" nA = "nA" uA = "uA" @@ -313,56 +344,14 @@ def __init__(self, prefix: str, name: str = ""): self.bias_on = epics_signal_rw(bool, f"{prefix}bias_on") self.filter_type = epics_signal_rw( - SubsetEnum[ - " No filter", - " 6 dB highpass", - "12 dB highpass", - " 6 dB bandpass", - " 6 dB lowpass", - "12 dB lowpass", - ], + self.FilterType, f"{prefix}filter_type", ) self.filter_reset = epics_signal_x(f"{prefix}filter_reset.PROC") - self.filter_lowpass = epics_signal_rw( - SubsetEnum[ - " 0.03 Hz", - " 0.1 Hz", - " 0.3 Hz", - " 1 Hz", - " 3 Hz", - " 10 Hz", - " 30 Hz", - "100 Hz", - "300 Hz", - " 1 kHz", - " 3 kHz", - " 10 kHz", - " 30 kHz", - "100 kHz", - "300 kHz", - " 1 MHz", - ], - f"{prefix}low_freq", - ) + self.filter_lowpass = epics_signal_rw(self.FilterLowPass, f"{prefix}low_freq") self.filter_highpass = epics_signal_rw( - SubsetEnum[ - " 0.03 Hz", - " 0.1 Hz", - " 0.3 Hz", - " 1 Hz", - " 3 Hz", - " 10 Hz", - " 30 Hz", - "100 Hz", - "300 Hz", - " 1 kHz", - " 3 kHz", - " 10 kHz", - ], - f"{prefix}high_freq", + self.FilterHighPass, f"{prefix}high_freq" ) - self.gain_mode = gain_signal(self.GainMode, f"{prefix}gain_mode") self.invert = epics_signal_rw(bool, f"{prefix}invert_on") self.blank = epics_signal_rw(bool, f"{prefix}blank_on") diff --git a/src/haven/devices/synApps.py b/src/haven/devices/synApps.py index 508a1bc4..62071cdc 100644 --- a/src/haven/devices/synApps.py +++ b/src/haven/devices/synApps.py @@ -1,12 +1,16 @@ import asyncio -from ophyd_async.core import ConfigSignal, Device, StandardReadable, SubsetEnum -from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw, epics_signal_x +from ophyd_async.core import ( + Device, + StandardReadable, + StandardReadableFormat, + StrictEnum, + SubsetEnum, +) +from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x -from ..typing import StrEnum - -class AlarmStatus(StrEnum): +class AlarmStatus(SubsetEnum): NO_ALARM = "NO_ALARM" READ = "READ" WRITE = "WRITE" @@ -31,7 +35,7 @@ class AlarmStatus(StrEnum): # WRITE_ACCESS = "WRITE_ACCESS" -class AlarmSeverity(StrEnum): +class AlarmSeverity(StrictEnum): NO_ALARM = "NO_ALARM" MINOR = "MINOR" MAJOR = "MAJOR" @@ -46,11 +50,9 @@ class EpicsRecordDeviceCommonAll(StandardReadable): an EPICS client or are already provided in other support. """ - # The valid options are specific to the record type - # Subclasses should set this properly - DeviceType = SubsetEnum["None"] - - class ScanInterval(StrEnum): + # More valid options are specific to the record type + # Subclasses may override this attribute + class ScanInterval(SubsetEnum): PASSIVE = "Passive" EVENT = "Event" IO_INTR = "I/O Intr" @@ -64,10 +66,9 @@ class ScanInterval(StrEnum): # Config signals def __init__(self, prefix: str, name: str = ""): - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.description = epics_signal_rw(str, f"{prefix}.DESC") self.scanning_rate = epics_signal_rw(self.ScanInterval, f"{prefix}.SCAN") - self.device_type = epics_signal_r(self.DeviceType, f"{prefix}.DTYP") # Other signals, not included in read self.disable_value = epics_signal_rw(int, f"{prefix}.DISV") self.scan_disable_input_link_value = epics_signal_rw(int, f"{prefix}.DISA") @@ -90,7 +91,7 @@ class EpicsSynAppsRecordEnableMixin(Device): """Supports ``{PV}Enable`` feature from user databases.""" def __init__(self, prefix, name=""): - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.enable = epics_signal_rw(int, "Enable") super().__init__(name=name) @@ -106,9 +107,15 @@ class EpicsRecordInputFields(EpicsRecordDeviceCommonAll): Some fields common to EPICS input records. """ + class DeviceType(SubsetEnum): + SOFT_CHANNEL = "Soft Channel" + RAW_SOFT_CHANNEL = "Raw Soft Channel" + ASYNC_SOFT_CHANNEL = "Async Soft Channel" + def __init__(self, prefix: str, name: str = ""): - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.input_link = epics_signal_rw(str, f"{prefix}.INP") + self.device_type = epics_signal_r(self.DeviceType, f"{prefix}.DTYP") super().__init__(prefix=prefix, name=name) @@ -117,13 +124,19 @@ class EpicsRecordOutputFields(EpicsRecordDeviceCommonAll): Some fields common to EPICS output records. """ - class ModeSelect(StrEnum): + class DeviceType(SubsetEnum): + SOFT_CHANNEL = "Soft Channel" + RAW_SOFT_CHANNEL = "Raw Soft Channel" + ASYNC_SOFT_CHANNEL = "Async Soft Channel" + + class ModeSelect(SubsetEnum): SUPERVISORY = "supervisory" CLOSED_LOOP = "closed_loop" def __init__(self, prefix: str, name: str = ""): - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.output_link = epics_signal_rw(str, f"{prefix}.OUT") self.desired_output_location = epics_signal_rw(str, f"{prefix}.DOL") self.output_mode_select = epics_signal_rw(self.ModeSelect, f"{prefix}.OMSL") + self.device_type = epics_signal_r(self.DeviceType, f"{prefix}.DTYP") super().__init__(prefix=prefix, name=name) diff --git a/src/haven/devices/transform.py b/src/haven/devices/transform.py index f1a75805..be0908b5 100644 --- a/src/haven/devices/transform.py +++ b/src/haven/devices/transform.py @@ -4,15 +4,14 @@ # from ophyd import Device from ophyd_async.core import ( - ConfigSignal, Device, - DeviceVector, - HintedSignal, StandardReadable, + StandardReadableFormat, + StrictEnum, + SubsetEnum, ) -from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw +from ophyd_async.epics.core import epics_signal_r, epics_signal_rw -from ..typing import StrEnum from .synApps import EpicsRecordDeviceCommonAll, EpicsSynAppsRecordEnableMixin CHANNEL_LETTERS_LIST = "A B C D E F G H I J K L M N O P".split() @@ -34,7 +33,7 @@ class TransformRecordChannel(StandardReadable): ~reset """ - class PVValidity(StrEnum): + class PVValidity(SubsetEnum): EXT_PV_NC = "Ext PV NC" EXT_PV_OK = "Ext PV OK" LOCAL_PV = "Local PV" @@ -44,7 +43,7 @@ def __init__(self, prefix, letter, name=""): self._ch_letter = letter with self.add_children_as_readables(): self.current_value = epics_signal_rw(float, f"{prefix}.{letter}") - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.input_pv = epics_signal_rw(str, f"{prefix}.INP{letter}") self.comment = epics_signal_rw(str, f"{prefix}.CMT{letter}") self.expression = epics_signal_rw( @@ -86,16 +85,16 @@ class TransformRecord(EpicsRecordDeviceCommonAll): :see: https://htmlpreview.github.io/?https://raw.githubusercontent.com/epics-modules/calc/R3-6-1/documentation/TransformRecord.html#Fields """ - class CalcOption(StrEnum): + class CalcOption(StrictEnum): CONDITIONAL = "Conditional" ALWAYS = "Always" - class InvalidLinkAction(StrEnum): + class InvalidLinkAction(SubsetEnum): IGNORE_ERROR = "Ignore error" DO_NOTHING = "Do Nothing" def __init__(self, prefix, name=""): - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.units = epics_signal_rw( str, f"{prefix}.EGU", @@ -118,20 +117,18 @@ def __init__(self, prefix, name=""): int, f"{prefix}.MAP", name="input_bitmap" ) with self.add_children_as_readables(): - self.channels = DeviceVector( - { - char: TransformRecordChannel(prefix=prefix, letter=char) - for char in CHANNEL_LETTERS_LIST - } - ) + for letter in CHANNEL_LETTERS_LIST: + setattr( + self, + f"channel_{letter}", + TransformRecordChannel(prefix=prefix, letter=letter), + ) super().__init__(prefix=prefix, name=name) - # Remove dtype, it's broken for some reason - del self.device_type async def reset(self): """set all fields to default values""" - channels = self.channels.values() + channels = [getattr(self, letter) for letter in CHANNEL_LETTERS_LIST] await asyncio.gather( self.scanning_rate.set(self.ScanInterval.PASSIVE), self.description.set(self.name), @@ -142,7 +139,7 @@ async def reset(self): *[ch.reset() for ch in channels], ) # Restore the hinted channels - self.add_readables(channels, HintedSignal) + self.add_readables(channels, StandardReadableFormat.HINTED_SIGNAL) class UserTransformN(EpicsSynAppsRecordEnableMixin, TransformRecord): @@ -158,7 +155,7 @@ class UserTransformsDevice(Device): def __init__(self, prefix, name=""): # Config attrs - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.enable = epics_signal_rw(int, f"{prefix}userTranEnable", name="enable") # Read attrs with self.add_children_as_readables(): diff --git a/src/haven/devices/xray_source.py b/src/haven/devices/xray_source.py index 06919502..5f292dd9 100644 --- a/src/haven/devices/xray_source.py +++ b/src/haven/devices/xray_source.py @@ -1,14 +1,14 @@ import logging -from enum import Enum, IntEnum +from enum import IntEnum from ophyd_async.core import ( - ConfigSignal, - HintedSignal, Signal, StandardReadable, + StandardReadableFormat, + SubsetEnum, soft_signal_rw, ) -from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw, epics_signal_x +from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x from ..positioner import Positioner from .signal import derived_signal_r, derived_signal_x @@ -44,11 +44,11 @@ def __init__( name: str = "", min_move: float = 0.0, ): - with self.add_children_as_readables(HintedSignal): + with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL): self.readback = epics_signal_rw(float, f"{prefix}M.VAL") with self.add_children_as_readables(): self.setpoint = epics_signal_rw(float, f"{prefix}SetC.VAL") - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.units = epics_signal_r(str, f"{prefix}SetC.EGU") self.precision = epics_signal_r(int, f"{prefix}SetC.PREC") self.velocity = soft_signal_rw( @@ -84,7 +84,7 @@ class PlanarUndulator(StandardReadable): _ophyd_labels_ = {"xray_sources", "undulators"} - class AccessMode(str, Enum): + class AccessMode(SubsetEnum): USER = "User" OPERATOR = "Operator" MACHINE_PHYSICS = "Machine Physics" @@ -98,7 +98,7 @@ def __init__(self, prefix: str, name: str = ""): self.done = epics_signal_r(bool, f"{prefix}BusyDeviceM.VAL") self.motor_drive_status = epics_signal_r(int, f"{prefix}MotorDriveStatusM.VAL") # Configuration state for the undulator - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.harmonic_value = epics_signal_rw(int, f"{prefix}HarmonicValueC") self.total_power = epics_signal_r(float, f"{prefix}TotalPowerM.VAL") self.gap_deadband = epics_signal_rw(int, f"{prefix}DeadbandGapC") diff --git a/src/haven/ipython_startup.ipy b/src/haven/ipython_startup.ipy index 7b0beff6..95b15271 100644 --- a/src/haven/ipython_startup.ipy +++ b/src/haven/ipython_startup.ipy @@ -13,6 +13,7 @@ from bluesky.plan_stubs import mv, mvr, rd # noqa: F401 from bluesky.run_engine import RunEngine, call_in_bluesky_event_loop # noqa: F401 from bluesky.simulators import summarize_plan # noqa: F401 from ophyd_async.core import NotConnected +from ophydregistry import ComponentNotFound import matplotlib.pyplot as plt from rich import print from rich.align import Align @@ -61,8 +62,14 @@ print(f"Connected to {num_devices} devices in {time.monotonic() - t0:.2f} second # Save references to some commonly used things in the global namespace registry = haven.beamline.registry -ion_chambers = registry.findall("ion_chambers", allow_none=True) -energy = registry['energy'] +try: + ion_chambers = registry.findall("ion_chambers", allow_none=True) +except ComponentNotFound as exc: + log.exception(exc) +try: + energy = registry['energy'] +except ComponentNotFound as exc: + log.exception(exc) # Print helpful information to the console custom_theme = Theme( diff --git a/src/haven/positioner.py b/src/haven/positioner.py index 85abe5cd..e5e9e5e2 100644 --- a/src/haven/positioner.py +++ b/src/haven/positioner.py @@ -80,7 +80,6 @@ async def set( wait: bool = True, timeout: CalculatableTimeout = CALCULATE_TIMEOUT, ): - assert wait, "``wait==False`` not supported." new_position = value self._set_success = True old_position, current_position, units, precision, velocity = ( @@ -113,8 +112,10 @@ async def set( else: # Wait for the value to set, but don't wait for put completion callback set_status = self.setpoint.set( - new_position, wait=self.put_complete, timeout=timeout + new_position, wait=(wait and self.put_complete), timeout=timeout ) + if not wait: + return # Decide on how we will wait for completion if self.put_complete: # await the set call directly @@ -133,6 +134,9 @@ async def set( # Monitor based on readback position aws = asyncio.gather(reached_setpoint.wait(), set_status) done_status = AsyncStatus(asyncio.wait_for(aws, timeout)) + # If we don't care to wait for the return value, we can end + if not wait: + return # Monitor the position of the readback value async for current_position in observe_value( self.readback, done_status=done_status diff --git a/src/haven/tests/test_delay.py b/src/haven/tests/test_delay.py index 36665c6e..baa21e9d 100644 --- a/src/haven/tests/test_delay.py +++ b/src/haven/tests/test_delay.py @@ -20,47 +20,47 @@ async def test_dg645_device(): "delay-burst_delay", "delay-burst_mode", "delay-burst_period", - "delay-channels-A-reference", - "delay-channels-A-delay", - "delay-channels-B-reference", - "delay-channels-B-delay", - "delay-channels-C-reference", - "delay-channels-C-delay", - "delay-channels-D-reference", - "delay-channels-D-delay", - "delay-channels-E-reference", - "delay-channels-E-delay", - "delay-channels-F-reference", - "delay-channels-F-delay", - "delay-channels-G-reference", - "delay-channels-G-delay", - "delay-channels-H-reference", - "delay-channels-H-delay", + "delay-channel_A-reference", + "delay-channel_A-delay", + "delay-channel_B-reference", + "delay-channel_B-delay", + "delay-channel_C-reference", + "delay-channel_C-delay", + "delay-channel_D-reference", + "delay-channel_D-delay", + "delay-channel_E-reference", + "delay-channel_E-delay", + "delay-channel_F-reference", + "delay-channel_F-delay", + "delay-channel_G-reference", + "delay-channel_G-delay", + "delay-channel_H-reference", + "delay-channel_H-delay", "delay-device_id", "delay-label", - "delay-outputs-AB-amplitude", - "delay-outputs-AB-offset", - "delay-outputs-AB-polarity", - "delay-outputs-AB-trigger_phase", - "delay-outputs-AB-trigger_prescale", - "delay-outputs-CD-amplitude", - "delay-outputs-CD-offset", - "delay-outputs-CD-polarity", - "delay-outputs-CD-trigger_phase", - "delay-outputs-CD-trigger_prescale", - "delay-outputs-EF-amplitude", - "delay-outputs-EF-offset", - "delay-outputs-EF-polarity", - "delay-outputs-EF-trigger_phase", - "delay-outputs-EF-trigger_prescale", - "delay-outputs-GH-amplitude", - "delay-outputs-GH-offset", - "delay-outputs-GH-polarity", - "delay-outputs-GH-trigger_phase", - "delay-outputs-GH-trigger_prescale", - "delay-outputs-T0-amplitude", - "delay-outputs-T0-offset", - "delay-outputs-T0-polarity", + "delay-output_AB-amplitude", + "delay-output_AB-offset", + "delay-output_AB-polarity", + "delay-output_AB-trigger_phase", + "delay-output_AB-trigger_prescale", + "delay-output_CD-amplitude", + "delay-output_CD-offset", + "delay-output_CD-polarity", + "delay-output_CD-trigger_phase", + "delay-output_CD-trigger_prescale", + "delay-output_EF-amplitude", + "delay-output_EF-offset", + "delay-output_EF-polarity", + "delay-output_EF-trigger_phase", + "delay-output_EF-trigger_prescale", + "delay-output_GH-amplitude", + "delay-output_GH-offset", + "delay-output_GH-polarity", + "delay-output_GH-trigger_phase", + "delay-output_GH-trigger_prescale", + "delay-output_T0-amplitude", + "delay-output_T0-offset", + "delay-output_T0-polarity", "delay-trigger_advanced_mode", "delay-trigger_holdoff", "delay-trigger_inhibit", @@ -81,7 +81,14 @@ async def test_dg645_device(): "delay-burst_delay", "delay-burst_mode", "delay-burst_period", - "delay-channels", + "delay-channel_A", + "delay-channel_B", + "delay-channel_C", + "delay-channel_D", + "delay-channel_E", + "delay-channel_F", + "delay-channel_G", + "delay-channel_H", "delay-clear_error", "delay-device_id", "delay-dhcp_state", @@ -95,7 +102,11 @@ async def test_dg645_device(): "delay-lan_state", "delay-mac_address", "delay-network_mask", - "delay-outputs", + "delay-output_AB", + "delay-output_CD", + "delay-output_EF", + "delay-output_GH", + "delay-output_T0", "delay-reset", "delay-reset_gpib", "delay-reset_lan", diff --git a/src/haven/tests/test_energy_positioner.py b/src/haven/tests/test_energy_positioner.py index 8c0268ca..161f03d3 100644 --- a/src/haven/tests/test_energy_positioner.py +++ b/src/haven/tests/test_energy_positioner.py @@ -23,12 +23,9 @@ async def test_set_energy(positioner): # Set up dependent values set_mock_value(positioner.monochromator.id_offset, 150) # Change the energy - status = positioner.set(10000, timeout=3) + await positioner.set(10000, timeout=3, wait=False) # Trick the Undulator into being done - set_mock_value(positioner.undulator.energy.done, BusyStatus.BUSY) - await asyncio.sleep(0.01) # Let the event loop run - set_mock_value(positioner.undulator.energy.done, BusyStatus.DONE) - await status + await asyncio.sleep(0.05) # Let the event loop run # Check that all the sub-components were set properly assert await positioner.monochromator.energy.user_setpoint.get_value() == 10000 assert await positioner.undulator.energy.setpoint.get_value() == 10.150 @@ -45,11 +42,12 @@ async def test_disable_id_tracking(positioner): energy = positioner # Turn on tracking to start with set_mock_value(energy.monochromator.id_tracking, 1) + set_mock_value(energy.velocity, 100) # Set the energy - status = energy.set(5000) + status = energy.set(5000, wait=False) # Trick the Undulator into being done set_mock_value(positioner.undulator.energy.done, BusyStatus.BUSY) - await asyncio.sleep(0.01) # Let the event loop run + await asyncio.sleep(0.05) # Let the event loop run set_mock_value(positioner.undulator.energy.done, BusyStatus.DONE) await status # Check that ID tracking was disabled diff --git a/src/haven/tests/test_instrument.py b/src/haven/tests/test_instrument.py index 93036bfc..cce8bb23 100644 --- a/src/haven/tests/test_instrument.py +++ b/src/haven/tests/test_instrument.py @@ -84,11 +84,13 @@ async def test_connect(instrument): # Are devices disconnected to start with? assert all([d._connect_task is None for d in async_devices]) assert all([not d.connected is None for d in sync_devices]) + for device in async_devices: + device._connector.connect_mock = AsyncMock() # Connect the device await instrument.connect(mock=True) # Are devices connected afterwards? # NB: This doesn't actually test the code for threaded devices - assert all([d._connect_task.done for d in async_devices]) + assert all([d._connector.connect_mock.called for d in async_devices]) async def test_load(monkeypatch): diff --git a/src/haven/tests/test_ion_chamber.py b/src/haven/tests/test_ion_chamber.py index 4ffb823a..6fcd9d8f 100644 --- a/src/haven/tests/test_ion_chamber.py +++ b/src/haven/tests/test_ion_chamber.py @@ -1,3 +1,4 @@ +import asyncio from unittest.mock import AsyncMock import numpy as np @@ -39,6 +40,7 @@ async def test_readables(ion_chamber): await ion_chamber.connect(mock=True) expected_readables = [ "I0-net_current", + "I0-raw_current", "I0-voltmeter-analog_inputs-1-final_value", "I0-mcs-scaler-channels-0-net_count", "I0-mcs-scaler-channels-0-raw_count", @@ -48,7 +50,13 @@ async def test_readables(ion_chamber): ] actual_readables = (await ion_chamber.describe()).keys() assert sorted(actual_readables) == sorted(expected_readables) - # Check confirables + # Check signal hints + expected_hints = [ + "I0-net_current", + ] + actual_hints = ion_chamber.hints["fields"] + assert sorted(actual_hints) == sorted(expected_hints) + # Check configurables expected_configables = [ "I0-counts_per_volt_second", "I0-voltmeter-model_name", @@ -142,6 +150,10 @@ async def test_trigger_dark_current(ion_chamber, monkeypatch): async def test_net_current_signal(ion_chamber): """Test that scaler tick counts get properly converted to ion chamber current.""" await ion_chamber.connect(mock=True) + await asyncio.gather( + ion_chamber.net_current.connect(mock=False), + ion_chamber.preamp.gain.connect(mock=False), + ) # Set the necessary dependent signals set_mock_value(ion_chamber.counts_per_volt_second, 10e6) # 100 Mhz / 10 V set_mock_value(ion_chamber.scaler_channel.net_count, int(13e6)) # 1.3V @@ -161,6 +173,10 @@ async def test_net_current_signal(ion_chamber): async def test_raw_current_signal(ion_chamber): """Test that scaler tick counts get properly converted to ion chamber current.""" await ion_chamber.connect(mock=True) + await asyncio.gather( + ion_chamber.raw_current.connect(mock=False), + ion_chamber.preamp.gain.connect(mock=False), + ) # Set the necessary dependent signals set_mock_value(ion_chamber.counts_per_volt_second, 10e6) # 100 Mhz / 10 V set_mock_value(ion_chamber.scaler_channel.raw_count, int(13e6)) # 1.3V @@ -181,8 +197,11 @@ async def test_voltmeter_name(ion_chamber): await ion_chamber.connect(mock=True) assert (await ion_chamber.voltmeter_channel.description.get_value()) != "Icake" # Change the ion chamber name, and see if the voltmeter name updates - set_mock_value(ion_chamber.scaler_channel.description, "Icake") + # set_mock_value(ion_chamber.scaler_channel.description, "Icake") + ion_chamber.scaler_channel.description.get_value = AsyncMock(return_value="Icake") + assert (await ion_chamber.scaler_channel.description.get_value()) == "Icake" await ion_chamber.connect(mock=True) + assert (await ion_chamber.scaler_channel.description.get_value()) == "Icake" assert (await ion_chamber.voltmeter_channel.description.get_value()) == "Icake" @@ -302,14 +321,11 @@ async def test_flyscan_collect(ion_chamber, trigger_info): } for (datum, timestamp) in zip(channel_numbers, expected_timestamps) ] - # Ignore the first collected data point because it's during taxiing - expected_data = sim_data[1:] # The real timestamps should be midway between PSO pulses collected = [c async for c in ion_chamber.collect_pages()] assert len(collected) == 1 collected = collected[0] # Confirm data have the right structure - raw_name = ion_chamber.scaler_channel.net_count.name assert collected["time"] == 1024 assert_allclose( collected["data"][ion_chamber.scaler_channel.raw_count.name], sim_raw_data[:6] diff --git a/src/haven/tests/test_labjack.py b/src/haven/tests/test_labjack.py index c4a1847a..4202bf2d 100644 --- a/src/haven/tests/test_labjack.py +++ b/src/haven/tests/test_labjack.py @@ -202,18 +202,17 @@ async def test_digital_words(LabJackDevice, num_dios): """Test analog inputs for different device types.""" device = LabJackDevice(PV_PREFIX, name="labjack_T") await device.connect(mock=True) - assert hasattr(device, "digital_words") # Check that the individual digital word outputs were created - assert "dio" in device.digital_words.keys() - assert "fio" in device.digital_words.keys() - assert "eio" in device.digital_words.keys() - assert "cio" in device.digital_words.keys() - assert "mio" in device.digital_words.keys() + assert hasattr(device, "dio") + assert hasattr(device, "fio") + assert hasattr(device, "eio") + assert hasattr(device, "cio") + assert hasattr(device, "mio") # Check read attrs read_attrs = ["dio", "eio", "fio", "mio", "cio"] description = await device.describe() for attr in read_attrs: - assert f"{device.name}-digital_words-{attr}" in description.keys() + assert f"{device.name}-{attr}" in description.keys() async def test_waveform_digitizer(): diff --git a/src/haven/tests/test_mirrors.py b/src/haven/tests/test_mirrors.py index 1ad38436..c984d31b 100644 --- a/src/haven/tests/test_mirrors.py +++ b/src/haven/tests/test_mirrors.py @@ -14,11 +14,11 @@ async def test_high_heat_load_mirror_PVs(): assert mirror.bender.user_setpoint.source == "mock+ca://255ida:ORM2:m5.VAL" # Check the transform PVs assert ( - mirror.drive_transform.channels["B"].input_pv.source + mirror.drive_transform.channel_B.input_pv.source == "mock+ca://255ida:ORM2:lats:Drive.INPB" ) assert ( - mirror.readback_transform.channels["B"].input_pv.source + mirror.readback_transform.channel_B.input_pv.source == "mock+ca://255ida:ORM2:lats:Readback.INPB" ) @@ -51,10 +51,10 @@ async def test_kb_mirrors_PVs(): assert kb.vert.downstream.user_setpoint.source == "mock+ca://255idcVME:m36.VAL" # Check the transforms assert ( - kb.horiz.drive_transform.channels["B"].input_pv.source + kb.horiz.drive_transform.channel_B.input_pv.source == "mock+ca://255idcVME:LongKB_CdnH:Drive.INPB" ) assert ( - kb.horiz.readback_transform.channels["B"].input_pv.source + kb.horiz.readback_transform.channel_B.input_pv.source == "mock+ca://255idcVME:LongKB_CdnH:Readback.INPB" ) diff --git a/src/haven/tests/test_motor.py b/src/haven/tests/test_motor.py index f9bb8cbe..25b3171c 100644 --- a/src/haven/tests/test_motor.py +++ b/src/haven/tests/test_motor.py @@ -66,7 +66,7 @@ async def test_stop_button(motor): assert motor.motor_stop.parent is motor await motor.motor_stop.trigger() mock = get_mock_put(motor.motor_stop) - mock.assert_called_once_with(1, wait=False, timeout=10.0) + mock.assert_called_once_with(1, wait=True) @pytest.mark.asyncio diff --git a/src/haven/tests/test_positioner.py b/src/haven/tests/test_positioner.py index 478f1f30..40e031cc 100644 --- a/src/haven/tests/test_positioner.py +++ b/src/haven/tests/test_positioner.py @@ -2,7 +2,7 @@ import pytest from ophyd_async.core import get_mock_put, set_mock_value -from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw, epics_signal_x +from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x from haven.positioner import Positioner diff --git a/src/haven/tests/test_shutter.py b/src/haven/tests/test_shutter.py index 456a2ea4..8b37ec6f 100644 --- a/src/haven/tests/test_shutter.py +++ b/src/haven/tests/test_shutter.py @@ -36,7 +36,7 @@ async def test_shutter_setpoint(shutter): set_mock_value(shutter.readback, ShutterState.CLOSED) await status assert not open_put.called - close_put.assert_called_once_with(1, timeout=16, wait=False) + close_put.assert_called_once_with(1, wait=False) # Open the shutter open_put.reset_mock() close_put.reset_mock() @@ -46,7 +46,7 @@ async def test_shutter_setpoint(shutter): set_mock_value(shutter.readback, ShutterState.OPEN) await status assert not close_put.called - open_put.assert_called_once_with(1, timeout=18, wait=False) + open_put.assert_called_once_with(1, wait=False) async def test_shutter_check_value(shutter): diff --git a/src/haven/tests/test_signal.py b/src/haven/tests/test_signal.py index f33d2bd3..b62f1bdf 100644 --- a/src/haven/tests/test_signal.py +++ b/src/haven/tests/test_signal.py @@ -1,10 +1,11 @@ import asyncio import math +from unittest.mock import MagicMock import pytest -from ophyd_async.core import Device, get_mock_put +from ophyd_async.core import Device, DeviceVector, get_mock_put from ophyd_async.core._signal import soft_signal_rw -from ophyd_async.epics.signal import epics_signal_x +from ophyd_async.epics.core import epics_signal_rw, epics_signal_x from haven.devices.signal import derived_signal_rw, derived_signal_x @@ -82,6 +83,21 @@ async def test_derived_forward(device): assert await device.y.get_value() == pytest.approx(2 / math.sqrt(2)) +async def test_mock_subscribe(): + """This is to test the behavior of ophyd-async, not Haven.""" + base_signal = epics_signal_rw(float, "sldfkj") + derived_signal = derived_signal_rw(float, derived_from={"base": base_signal}) + callback = MagicMock() + await base_signal.connect(mock=True) + await derived_signal.connect(mock=False) + derived_signal.subscribe(callback) + callback.reset_mock() + assert not callback.called + # Now change the value and check whether the mocked signal was changed + await base_signal.set(5) + assert callback.called + + @pytest.mark.asyncio async def test_derived_defaults(device): """Does the derived signal report the derived value by default.""" @@ -155,4 +171,23 @@ async def test_signal_x_trigger(device): # Now trigger the parent mocked_put = get_mock_put(signal) await derived.trigger() - mocked_put.assert_called_once_with(None, wait=True, timeout=10.0) + mocked_put.assert_called_once_with(None, wait=True) + + +async def test_device_vector_parent(): + class MyDevice(Device): + def __init__(self, name): + self.my_signal = soft_signal_rw(float) + self.channels = DeviceVector({0: soft_signal_rw(float)}) + super().__init__(name=name) + + my_device = MyDevice(name="my_device") + await my_device.connect(mock=True) + # Good tests + assert my_device.my_signal.name == "my_device-my_signal" + assert my_device.my_signal.parent is my_device + assert my_device.channels.name == "my_device-channels" + assert my_device.channels.parent is my_device + assert my_device.channels[0].name == "my_device-channels-0" + # Bad tests + assert my_device.channels[0].parent is my_device.channels diff --git a/src/haven/tests/test_srs570.py b/src/haven/tests/test_srs570.py index 6f361491..8feef2dd 100644 --- a/src/haven/tests/test_srs570.py +++ b/src/haven/tests/test_srs570.py @@ -1,7 +1,8 @@ +import asyncio from unittest import mock import pytest -from ophyd_async.core import DEFAULT_TIMEOUT +from ophyd_async.core import get_mock_put from haven.devices.srs570 import GainSignal, SRS570PreAmplifier @@ -9,7 +10,13 @@ @pytest.fixture() async def preamp(): preamp = SRS570PreAmplifier("255idcVEM:SR02:", name="") + # Derived signals should not be mocked await preamp.connect(mock=True) + await asyncio.gather( + preamp.gain_level.connect(mock=False), + preamp.gain.connect(mock=False), + preamp.gain_db.connect(mock=False), + ) return preamp @@ -138,8 +145,9 @@ async def test_preamp_gain_settling(gain_value, gain_unit, gain_mode, mocker, pr sleep_mock.reset_mock() await preamp.sensitivity_value.set(gain_value) # Check that the signal's ``set`` was called with correct arguments - preamp.sensitivity_value._backend.put_mock.assert_called_once_with( - gain_value, wait=True, timeout=DEFAULT_TIMEOUT + get_mock_put(preamp.sensitivity_value).assert_called_once_with( + gain_value, + wait=True, ) # Check that the settle time was included sleep_mock.assert_called_once_with(settle_time) @@ -216,7 +224,7 @@ async def test_get_gain_level(preamp): await preamp.sensitivity_value.set("20") await preamp.sensitivity_unit.set("uA/V"), await preamp.offset_value.set("2"), # 2 uA/V - await preamp.offset_unit.set("uA/V"), + await preamp.offset_unit.set("uA"), # Check that the gain level moved gain_level = await preamp.gain_level.get_value() assert gain_level == 5 diff --git a/src/haven/typing.py b/src/haven/typing.py index 6ff8166d..a5a37698 100644 --- a/src/haven/typing.py +++ b/src/haven/typing.py @@ -1,4 +1,3 @@ -from enum import Enum from typing import Sequence, Union from ophyd import Component, Device, Signal @@ -12,9 +11,6 @@ Motor = Union[Device, Component, Signal, str] -class StrEnum(str, Enum): ... - - # ----------------------------------------------------------------------------- # :author: Mark Wolfman # :email: wolfman@anl.gov