From dc42e3e5d7f92c67a349862dfb0be77bb73dbd9f Mon Sep 17 00:00:00 2001 From: sligara7 Date: Tue, 21 Jul 2026 16:19:11 -0400 Subject: [PATCH 1/5] Add Eurotherm3k ophyd-async device (HXM-1305) Add nslsii.ophyd_async.devices.eurotherm3k: Eurotherm3kLoop (a Movable PID loop with a settle-aware set()) plus a two-loop Eurotherm3k container and its StrictEnums, mapping the nsls2.ioc_deploy eurotherm3k IOC role. Export the new classes from the devices package and fix the pre-existing broken absolute 'from rbd9103 import ...' there (-> relative). Add mock-backend unit tests including a legacy PV-surface equivalence check against the classic Eurotherm, and add pytest-asyncio to the dev requirements. Assisted-by: copilot:claude-opus-4-8 --- nslsii/ophyd_async/devices/__init__.py | 16 ++- nslsii/ophyd_async/devices/eurotherm3k.py | 106 +++++++++++++++++++ nslsii/tests/ophyd_async/test_eurotherm3k.py | 85 +++++++++++++++ 3 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 nslsii/ophyd_async/devices/eurotherm3k.py create mode 100644 nslsii/tests/ophyd_async/test_eurotherm3k.py diff --git a/nslsii/ophyd_async/devices/__init__.py b/nslsii/ophyd_async/devices/__init__.py index 06f47fda..0be90d2b 100644 --- a/nslsii/ophyd_async/devices/__init__.py +++ b/nslsii/ophyd_async/devices/__init__.py @@ -1,4 +1,4 @@ -from rbd9103 import ( +from .rbd9103 import ( RBD9103, RBD9103Range, RBD9103Input, @@ -6,6 +6,13 @@ RBD9103InRangeState, RBD9103Filter, ) +from .eurotherm3k import ( + Eurotherm3k, + Eurotherm3kLoop, + Eurotherm3kControlMode, + Eurotherm3kOnOff, + Eurotherm3kRampRateUnit, +) __all__ = [ "RBD9103", @@ -14,4 +21,9 @@ "RBD9103SamplingMode", "RBD9103InRangeState", "RBD9103Filter", -] + "Eurotherm3k", + "Eurotherm3kLoop", + "Eurotherm3kControlMode", + "Eurotherm3kOnOff", + "Eurotherm3kRampRateUnit", +] \ No newline at end of file diff --git a/nslsii/ophyd_async/devices/eurotherm3k.py b/nslsii/ophyd_async/devices/eurotherm3k.py new file mode 100644 index 00000000..db460d8b --- /dev/null +++ b/nslsii/ophyd_async/devices/eurotherm3k.py @@ -0,0 +1,106 @@ +import time +from typing import Annotated as A + +from ophyd_async.core import ( + AsyncStatus, + SignalR, + SignalRW, + StandardReadable, + StrictEnum, + observe_value, +) +from ophyd_async.core import StandardReadableFormat as Format +from ophyd_async.epics.core import EpicsDevice, PvSuffix + + +class Eurotherm3kControlMode(StrictEnum): + AUTOMATIC = "Automatic" + MANUAL = "Manual" + + +class Eurotherm3kOnOff(StrictEnum): + OFF = "Off" + ON = "On" + + +class Eurotherm3kRampRateUnit(StrictEnum): + PER_SECOND = "C/s" + PER_MINUTE = "C/min" + + +class Eurotherm3kLoop(StandardReadable, EpicsDevice): + """One PID control loop of a Eurotherm 3000-series controller. + + Behaves as a settable temperature axis: ``set(T)`` writes the setpoint and + completes once the readback has settled within ``tolerance`` of ``T``. + """ + + # Settle behaviour (override per instance, e.g. ``dev.loop1.tolerance = 0.5``) + tolerance = 1.0 # deg: |readback - setpoint| band counted as "there" + settle_time = 0.0 # s: must stay in band this long (0.0 = first touch) + move_timeout = 3600.0 # s: overall timeout for a move + + setpoint: A[SignalRW[float], PvSuffix.rbv("SP", ":RBV")] + readback: A[SignalR[float], PvSuffix("PV:RBV"), Format.HINTED_SIGNAL] + working_setpoint: A[SignalR[float], PvSuffix("WSP:RBV")] + ramp_rate: A[SignalRW[float], PvSuffix.rbv("RR", ":RBV"), Format.CONFIG_SIGNAL] + ramp_rate_unit: A[ + SignalR[Eurotherm3kRampRateUnit], PvSuffix("RR:UNIT"), Format.CONFIG_SIGNAL + ] + p: A[SignalRW[float], PvSuffix.rbv("P", ":RBV"), Format.CONFIG_SIGNAL] + i: A[SignalRW[float], PvSuffix.rbv("I", ":RBV"), Format.CONFIG_SIGNAL] + d: A[SignalRW[float], PvSuffix.rbv("D", ":RBV"), Format.CONFIG_SIGNAL] + control_mode: A[ + SignalRW[Eurotherm3kControlMode], + PvSuffix.rbv("MAN", ":RBV"), + Format.CONFIG_SIGNAL, + ] + autotune: A[SignalRW[Eurotherm3kOnOff], PvSuffix.rbv("AUTOTUNE", ":RBV")] + output: A[SignalRW[float], PvSuffix.rbv("O", ":RBV")] + output_high: A[ + SignalRW[float], PvSuffix.rbv("OUTPHI", ":RBV"), Format.CONFIG_SIGNAL + ] + output_low: A[SignalRW[float], PvSuffix.rbv("OUTPLO", ":RBV"), Format.CONFIG_SIGNAL] + loop_break_time: A[ + SignalRW[float], PvSuffix.rbv("LBT", ":RBV"), Format.CONFIG_SIGNAL + ] + + @AsyncStatus.wrap + async def set(self, value: float) -> None: + # 1. command the setpoint (waits for the write to be accepted) + await self.setpoint.set(value) + # 2. watch the readback until it settles, or time out overall + in_band_since: float | None = None + async for current in observe_value( + self.readback, done_timeout=self.move_timeout + ): + if abs(current - value) <= self.tolerance: + if self.settle_time <= 0: + return # "first touch" mode: done + now = time.monotonic() + if in_band_since is None: + in_band_since = now # entered the band, start the hold timer + elif now - in_band_since >= self.settle_time: + return # held in band long enough: done + else: + in_band_since = None # left the band, reset the hold timer + + +class Eurotherm3k(StandardReadable, EpicsDevice): + """Eurotherm 3000-series controller (nsls2.ioc_deploy eurotherm3k role).""" + + loop1: A[Eurotherm3kLoop, PvSuffix("LOOP1:"), Format.CHILD] + loop2: A[Eurotherm3kLoop, PvSuffix("LOOP2:"), Format.CHILD] + + # --- Global (controller-wide) --- + disable: A[SignalRW[str], PvSuffix("DISABLE"), Format.CONFIG_SIGNAL] + programmer_number: A[ + SignalRW[int], PvSuffix.rbv("PROGNUM", ":RBV"), Format.CONFIG_SIGNAL + ] + programmer_run: A[SignalRW[bool], PvSuffix.rbv("PROGRUN", ":RBV")] + programmer_reset: A[SignalRW[bool], PvSuffix("PROGRESET")] + programmer_status: A[SignalR[str], PvSuffix("PROGSTAT:RBV")] + recipe_select: A[ + SignalRW[str], PvSuffix.rbv("RECSEL", ":RBV"), Format.CONFIG_SIGNAL + ] + recipe_status: A[SignalR[str], PvSuffix("RECSTAT:RBV")] diff --git a/nslsii/tests/ophyd_async/test_eurotherm3k.py b/nslsii/tests/ophyd_async/test_eurotherm3k.py new file mode 100644 index 00000000..e28c9d16 --- /dev/null +++ b/nslsii/tests/ophyd_async/test_eurotherm3k.py @@ -0,0 +1,85 @@ +import pytest +from ophyd_async.core import get_mock_put, init_devices, set_mock_value + +from nslsii.ophyd_async.devices import Eurotherm3k, Eurotherm3kControlMode + +PREFIX = "XF:28ID1-ES{ET:05}" + + +@pytest.mark.asyncio +async def test_pvs_are_addressed(): + async with init_devices(mock=True): + dev = Eurotherm3k(PREFIX, name="et") + # child loop signals compose under LOOPn: + assert dev.loop1.setpoint.source == f"mock+ca://{PREFIX}LOOP1:SP:RBV" + assert dev.loop1.readback.source == f"mock+ca://{PREFIX}LOOP1:PV:RBV" + assert dev.loop2.setpoint.source == f"mock+ca://{PREFIX}LOOP2:SP:RBV" + # global signal sits directly under the prefix + assert dev.programmer_status.source == f"mock+ca://{PREFIX}PROGSTAT:RBV" + + +@pytest.mark.asyncio +async def test_format_tags_route_signals(): + async with init_devices(mock=True): + dev = Eurotherm3k(PREFIX, name="et") + reading = await dev.read() + config = await dev.read_configuration() + # HINTED readbacks (merged from both loop CHILDren) appear in read() + assert "et-loop1-readback" in reading + assert "et-loop2-readback" in reading + # CONFIG signals appear in read_configuration() + assert "et-loop1-ramp_rate" in config + + +@pytest.mark.asyncio +async def test_control_mode_enum_roundtrip(): + async with init_devices(mock=True): + dev = Eurotherm3k(PREFIX, name="et") + await dev.loop1.control_mode.set(Eurotherm3kControlMode.MANUAL) + assert await dev.loop1.control_mode.get_value() is Eurotherm3kControlMode.MANUAL + + +@pytest.mark.asyncio +async def test_set_commands_setpoint_and_settles(): + async with init_devices(mock=True): + dev = Eurotherm3k(PREFIX, name="et") + dev.loop1.tolerance = 1.0 + dev.loop1.settle_time = 0.0 + # pretend the controller is already at temperature + set_mock_value(dev.loop1.readback, 300.0) + await dev.loop1.set(300.0) # returns => it settled + get_mock_put(dev.loop1.setpoint).assert_called_once() # and it wrote the setpoint + + +@pytest.mark.asyncio +async def test_set_times_out_if_never_in_band(): + async with init_devices(mock=True): + dev = Eurotherm3k(PREFIX, name="et") + dev.loop1.tolerance = 0.5 + dev.loop1.move_timeout = 0.3 # keep the test fast + set_mock_value(dev.loop1.readback, 0.0) # stays far from 300 + with pytest.raises(TimeoutError): + await dev.loop1.set(300.0) + + +# Legacy PDF eurotherm3k (pdf-profile-collection/startup/16-eurotherm_HAB.py): +# each component -> the ophyd-async signal that must address the same PV. +_LEGACY_PV_MAP = { + "setpoint": ("loop1", "setpoint", "LOOP1:SP:RBV"), + "readback": ("loop1", "readback", "LOOP1:PV:RBV"), + "working": ("loop1", "working_setpoint", "LOOP1:WSP:RBV"), + "output": ("loop1", "output", "LOOP1:O:RBV"), + "ramprate": ("loop1", "ramp_rate", "LOOP1:RR:RBV"), + "manual_mode": ("loop1", "control_mode", "LOOP1:MAN:RBV"), + "autotune": ("loop1", "autotune", "LOOP1:AUTOTUNE:RBV"), +} + + +@pytest.mark.asyncio +async def test_legacy_pv_surface_equivalence(): + """Every PV of the legacy PDF eurotherm3k is addressed identically here.""" + async with init_devices(mock=True): + dev = Eurotherm3k(PREFIX, name="et") + for _legacy, (loop, attr, suffix) in _LEGACY_PV_MAP.items(): + signal = getattr(getattr(dev, loop), attr) + assert signal.source == f"mock+ca://{PREFIX}{suffix}" From 1c1b3545feb192357d54dd9c0cd7badc1f06d55a Mon Sep 17 00:00:00 2001 From: sligara7 Date: Wed, 22 Jul 2026 13:58:19 -0400 Subject: [PATCH 2/5] Eurotherm3k: fix settle-loop hang; strengthen set() tests (PR #264 review) - set(): a stable readback that stopped updating while in band could block until move_timeout because the hold timer only advanced on observe_value() yields. Restructure with asyncio.wait_for around a 'leave band' watcher: no departure within settle_time => settled; overall bounded by move_timeout (TimeoutError). (Copilot comment 2) - test: assert set() writes the commanded value, not just that a put happened. (comment 3) - test: add test_set_holds_in_band_for_settle_time covering the settle_time>0 hold path and guarding against the move_timeout hang. (comment 4) --- nslsii/ophyd_async/devices/eurotherm3k.py | 44 +++++++++++++------- nslsii/tests/ophyd_async/test_eurotherm3k.py | 25 ++++++++++- 2 files changed, 54 insertions(+), 15 deletions(-) diff --git a/nslsii/ophyd_async/devices/eurotherm3k.py b/nslsii/ophyd_async/devices/eurotherm3k.py index db460d8b..a1d76f82 100644 --- a/nslsii/ophyd_async/devices/eurotherm3k.py +++ b/nslsii/ophyd_async/devices/eurotherm3k.py @@ -1,4 +1,4 @@ -import time +import asyncio from typing import Annotated as A from ophyd_async.core import ( @@ -69,21 +69,37 @@ class Eurotherm3kLoop(StandardReadable, EpicsDevice): async def set(self, value: float) -> None: # 1. command the setpoint (waits for the write to be accepted) await self.setpoint.set(value) - # 2. watch the readback until it settles, or time out overall - in_band_since: float | None = None - async for current in observe_value( - self.readback, done_timeout=self.move_timeout - ): - if abs(current - value) <= self.tolerance: + + async def _reach_band() -> None: + """Return once the readback is within tolerance of the target.""" + async for current in observe_value(self.readback): + if abs(current - value) <= self.tolerance: + return + + async def _leave_band() -> None: + """Return if/when the readback leaves the tolerance band.""" + async for current in observe_value(self.readback): + if abs(current - value) > self.tolerance: + return + + async def _settle() -> None: + while True: + await _reach_band() if self.settle_time <= 0: return # "first touch" mode: done - now = time.monotonic() - if in_band_since is None: - in_band_since = now # entered the band, start the hold timer - elif now - in_band_since >= self.settle_time: - return # held in band long enough: done - else: - in_band_since = None # left the band, reset the hold timer + try: + # Stay settled unless the readback leaves the band within + # settle_time. A stable PV that stops updating while in band + # just times out here -> treated as "held long enough", so a + # settled controller no longer blocks until move_timeout. + await asyncio.wait_for(_leave_band(), timeout=self.settle_time) + except asyncio.TimeoutError: + return # held in band for settle_time: done + # left the band before settling -> loop and re-reach + + # Enforce the overall timeout. On Python >= 3.11 asyncio.TimeoutError is + # the builtin TimeoutError, so callers can catch TimeoutError. + await asyncio.wait_for(_settle(), timeout=self.move_timeout) class Eurotherm3k(StandardReadable, EpicsDevice): diff --git a/nslsii/tests/ophyd_async/test_eurotherm3k.py b/nslsii/tests/ophyd_async/test_eurotherm3k.py index e28c9d16..8fce2399 100644 --- a/nslsii/tests/ophyd_async/test_eurotherm3k.py +++ b/nslsii/tests/ophyd_async/test_eurotherm3k.py @@ -1,3 +1,5 @@ +import time + import pytest from ophyd_async.core import get_mock_put, init_devices, set_mock_value @@ -48,7 +50,28 @@ async def test_set_commands_setpoint_and_settles(): # pretend the controller is already at temperature set_mock_value(dev.loop1.readback, 300.0) await dev.loop1.set(300.0) # returns => it settled - get_mock_put(dev.loop1.setpoint).assert_called_once() # and it wrote the setpoint + # it wrote the commanded setpoint (not just "something") + get_mock_put(dev.loop1.setpoint).assert_called_once_with(300.0) + + +@pytest.mark.asyncio +async def test_set_holds_in_band_for_settle_time(): + """With settle_time > 0, set() waits the hold time even if the readback is + already in band and stops updating -- and must not block until move_timeout.""" + async with init_devices(mock=True): + dev = Eurotherm3k(PREFIX, name="et") + dev.loop1.tolerance = 1.0 + dev.loop1.settle_time = 0.2 + dev.loop1.move_timeout = 5.0 + # already in band and stable (no further updates arrive) + set_mock_value(dev.loop1.readback, 300.0) + start = time.monotonic() + await dev.loop1.set(300.0) + elapsed = time.monotonic() - start + # waited ~settle_time, and crucially did NOT hang until move_timeout + assert elapsed >= 0.2 + assert elapsed < dev.loop1.move_timeout + get_mock_put(dev.loop1.setpoint).assert_called_once_with(300.0) @pytest.mark.asyncio From 45f6a38f9a0862e56c83595e8f1df887e4739b8a Mon Sep 17 00:00:00 2001 From: sligara7 Date: Wed, 22 Jul 2026 14:19:45 -0400 Subject: [PATCH 3/5] rbd9103: import EpicsDevice/PvSuffix from ophyd_async.epics.core (drop deprecated .epics.signal) Follow-up from PR #264 review: clears the DeprecationWarning and aligns rbd9103 with the rest of the package (eurotherm3k) on the current ophyd-async import path. --- nslsii/ophyd_async/devices/rbd9103.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nslsii/ophyd_async/devices/rbd9103.py b/nslsii/ophyd_async/devices/rbd9103.py index f729fbfd..bc6ab4fc 100644 --- a/nslsii/ophyd_async/devices/rbd9103.py +++ b/nslsii/ophyd_async/devices/rbd9103.py @@ -6,7 +6,7 @@ AsyncStatus, ) from ophyd_async.core import StandardReadableFormat as Format -from ophyd_async.epics.signal import PvSuffix, EpicsDevice +from ophyd_async.epics.core import PvSuffix, EpicsDevice from typing import Annotated as A From 81a169445882350f7e3013b433c89b0f3c0c3fe1 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Mon, 5 Oct 2026 13:11:33 -0400 Subject: [PATCH 4/5] Update to use StandardMovable and a logic Assisted-by: oh-my-pi:gpt-5.6-terra --- nslsii/ophyd_async/devices/eurotherm3k.py | 111 ++++++++++--------- nslsii/tests/ophyd_async/test_eurotherm3k.py | 53 ++++----- 2 files changed, 84 insertions(+), 80 deletions(-) diff --git a/nslsii/ophyd_async/devices/eurotherm3k.py b/nslsii/ophyd_async/devices/eurotherm3k.py index a1d76f82..5d92134c 100644 --- a/nslsii/ophyd_async/devices/eurotherm3k.py +++ b/nslsii/ophyd_async/devices/eurotherm3k.py @@ -1,13 +1,17 @@ -import asyncio +from dataclasses import dataclass +from functools import cached_property from typing import Annotated as A from ophyd_async.core import ( - AsyncStatus, + MovableLogic, SignalR, SignalRW, + StandardMovable, StandardReadable, StrictEnum, - observe_value, + TimeoutCalculator, + set_and_wait_for_other_value, + wait_for_value, ) from ophyd_async.core import StandardReadableFormat as Format from ophyd_async.epics.core import EpicsDevice, PvSuffix @@ -28,7 +32,48 @@ class Eurotherm3kRampRateUnit(StrictEnum): PER_MINUTE = "C/min" -class Eurotherm3kLoop(StandardReadable, EpicsDevice): +@dataclass +class _Eurotherm3kLoopLogic(MovableLogic[float]): + loop: "Eurotherm3kLoop" + + async def calculate_timeout(self, old_position: float, new_position: float) -> float: + return self.loop.move_timeout + + async def move(self, new_position: float, timeout: TimeoutCalculator) -> None: + def in_band(value: float) -> bool: + return abs(value - new_position) <= self.loop.tolerance + + def out_of_band(value: float) -> bool: + return not in_band(value) + + await set_and_wait_for_other_value( + self.setpoint, + new_position, + self.readback, + in_band, + timeout=timeout(), + ) + if self.loop.settle_time <= 0: + return + + while True: + settle_time = self.loop.settle_time + if settle_time <= 0: + return + remaining_timeout = timeout() + hold_timeout = ( + settle_time if remaining_timeout is None or remaining_timeout >= settle_time else remaining_timeout + ) + try: + await wait_for_value(self.readback, out_of_band, timeout=hold_timeout) + except TimeoutError: + if remaining_timeout is None or remaining_timeout >= settle_time: + return + raise + await wait_for_value(self.readback, in_band, timeout=timeout()) + + +class Eurotherm3kLoop(EpicsDevice, StandardReadable, StandardMovable[float]): """One PID control loop of a Eurotherm 3000-series controller. Behaves as a settable temperature axis: ``set(T)`` writes the setpoint and @@ -44,9 +89,7 @@ class Eurotherm3kLoop(StandardReadable, EpicsDevice): readback: A[SignalR[float], PvSuffix("PV:RBV"), Format.HINTED_SIGNAL] working_setpoint: A[SignalR[float], PvSuffix("WSP:RBV")] ramp_rate: A[SignalRW[float], PvSuffix.rbv("RR", ":RBV"), Format.CONFIG_SIGNAL] - ramp_rate_unit: A[ - SignalR[Eurotherm3kRampRateUnit], PvSuffix("RR:UNIT"), Format.CONFIG_SIGNAL - ] + ramp_rate_unit: A[SignalR[Eurotherm3kRampRateUnit], PvSuffix("RR:UNIT"), Format.CONFIG_SIGNAL] p: A[SignalRW[float], PvSuffix.rbv("P", ":RBV"), Format.CONFIG_SIGNAL] i: A[SignalRW[float], PvSuffix.rbv("I", ":RBV"), Format.CONFIG_SIGNAL] d: A[SignalRW[float], PvSuffix.rbv("D", ":RBV"), Format.CONFIG_SIGNAL] @@ -57,49 +100,13 @@ class Eurotherm3kLoop(StandardReadable, EpicsDevice): ] autotune: A[SignalRW[Eurotherm3kOnOff], PvSuffix.rbv("AUTOTUNE", ":RBV")] output: A[SignalRW[float], PvSuffix.rbv("O", ":RBV")] - output_high: A[ - SignalRW[float], PvSuffix.rbv("OUTPHI", ":RBV"), Format.CONFIG_SIGNAL - ] + output_high: A[SignalRW[float], PvSuffix.rbv("OUTPHI", ":RBV"), Format.CONFIG_SIGNAL] output_low: A[SignalRW[float], PvSuffix.rbv("OUTPLO", ":RBV"), Format.CONFIG_SIGNAL] - loop_break_time: A[ - SignalRW[float], PvSuffix.rbv("LBT", ":RBV"), Format.CONFIG_SIGNAL - ] + loop_break_time: A[SignalRW[float], PvSuffix.rbv("LBT", ":RBV"), Format.CONFIG_SIGNAL] - @AsyncStatus.wrap - async def set(self, value: float) -> None: - # 1. command the setpoint (waits for the write to be accepted) - await self.setpoint.set(value) - - async def _reach_band() -> None: - """Return once the readback is within tolerance of the target.""" - async for current in observe_value(self.readback): - if abs(current - value) <= self.tolerance: - return - - async def _leave_band() -> None: - """Return if/when the readback leaves the tolerance band.""" - async for current in observe_value(self.readback): - if abs(current - value) > self.tolerance: - return - - async def _settle() -> None: - while True: - await _reach_band() - if self.settle_time <= 0: - return # "first touch" mode: done - try: - # Stay settled unless the readback leaves the band within - # settle_time. A stable PV that stops updating while in band - # just times out here -> treated as "held long enough", so a - # settled controller no longer blocks until move_timeout. - await asyncio.wait_for(_leave_band(), timeout=self.settle_time) - except asyncio.TimeoutError: - return # held in band for settle_time: done - # left the band before settling -> loop and re-reach - - # Enforce the overall timeout. On Python >= 3.11 asyncio.TimeoutError is - # the builtin TimeoutError, so callers can catch TimeoutError. - await asyncio.wait_for(_settle(), timeout=self.move_timeout) + @cached_property + def movable_logic(self) -> MovableLogic[float]: + return _Eurotherm3kLoopLogic(setpoint=self.setpoint, readback=self.readback, loop=self) class Eurotherm3k(StandardReadable, EpicsDevice): @@ -110,13 +117,9 @@ class Eurotherm3k(StandardReadable, EpicsDevice): # --- Global (controller-wide) --- disable: A[SignalRW[str], PvSuffix("DISABLE"), Format.CONFIG_SIGNAL] - programmer_number: A[ - SignalRW[int], PvSuffix.rbv("PROGNUM", ":RBV"), Format.CONFIG_SIGNAL - ] + programmer_number: A[SignalRW[int], PvSuffix.rbv("PROGNUM", ":RBV"), Format.CONFIG_SIGNAL] programmer_run: A[SignalRW[bool], PvSuffix.rbv("PROGRUN", ":RBV")] programmer_reset: A[SignalRW[bool], PvSuffix("PROGRESET")] programmer_status: A[SignalR[str], PvSuffix("PROGSTAT:RBV")] - recipe_select: A[ - SignalRW[str], PvSuffix.rbv("RECSEL", ":RBV"), Format.CONFIG_SIGNAL - ] + recipe_select: A[SignalRW[str], PvSuffix.rbv("RECSEL", ":RBV"), Format.CONFIG_SIGNAL] recipe_status: A[SignalR[str], PvSuffix("RECSTAT:RBV")] diff --git a/nslsii/tests/ophyd_async/test_eurotherm3k.py b/nslsii/tests/ophyd_async/test_eurotherm3k.py index 8fce2399..fce3445a 100644 --- a/nslsii/tests/ophyd_async/test_eurotherm3k.py +++ b/nslsii/tests/ophyd_async/test_eurotherm3k.py @@ -1,9 +1,13 @@ import time import pytest -from ophyd_async.core import get_mock_put, init_devices, set_mock_value +from ophyd_async.core import LazyMock, get_mock_put, init_devices, set_mock_value -from nslsii.ophyd_async.devices import Eurotherm3k, Eurotherm3kControlMode +from nslsii.ophyd_async.devices import ( + Eurotherm3k, + Eurotherm3kControlMode, + Eurotherm3kLoop, +) PREFIX = "XF:28ID1-ES{ET:05}" @@ -27,8 +31,8 @@ async def test_format_tags_route_signals(): reading = await dev.read() config = await dev.read_configuration() # HINTED readbacks (merged from both loop CHILDren) appear in read() - assert "et-loop1-readback" in reading - assert "et-loop2-readback" in reading + assert "et-loop1" in reading + assert "et-loop2" in reading # CONFIG signals appear in read_configuration() assert "et-loop1-ramp_rate" in config @@ -45,44 +49,41 @@ async def test_control_mode_enum_roundtrip(): async def test_set_commands_setpoint_and_settles(): async with init_devices(mock=True): dev = Eurotherm3k(PREFIX, name="et") - dev.loop1.tolerance = 1.0 - dev.loop1.settle_time = 0.0 - # pretend the controller is already at temperature - set_mock_value(dev.loop1.readback, 300.0) - await dev.loop1.set(300.0) # returns => it settled - # it wrote the commanded setpoint (not just "something") + await dev.loop1.set(300.0) get_mock_put(dev.loop1.setpoint).assert_called_once_with(300.0) + location = await dev.loop1.locate() + assert location["setpoint"] == 300.0 + assert location["readback"] == 300.0 @pytest.mark.asyncio async def test_set_holds_in_band_for_settle_time(): """With settle_time > 0, set() waits the hold time even if the readback is already in band and stops updating -- and must not block until move_timeout.""" - async with init_devices(mock=True): - dev = Eurotherm3k(PREFIX, name="et") - dev.loop1.tolerance = 1.0 - dev.loop1.settle_time = 0.2 - dev.loop1.move_timeout = 5.0 + loop = Eurotherm3kLoop(f"{PREFIX}LOOP1:", name="et-loop1") + await loop.connect(mock=LazyMock()) + loop.tolerance = 1.0 + loop.settle_time = 0.2 + loop.move_timeout = 5.0 # already in band and stable (no further updates arrive) - set_mock_value(dev.loop1.readback, 300.0) + set_mock_value(loop.readback, 300.0) start = time.monotonic() - await dev.loop1.set(300.0) + await loop.set(300.0) elapsed = time.monotonic() - start # waited ~settle_time, and crucially did NOT hang until move_timeout - assert elapsed >= 0.2 - assert elapsed < dev.loop1.move_timeout - get_mock_put(dev.loop1.setpoint).assert_called_once_with(300.0) + assert 0.2 <= elapsed < 5.0 + get_mock_put(loop.setpoint).assert_called_once_with(300.0) @pytest.mark.asyncio async def test_set_times_out_if_never_in_band(): - async with init_devices(mock=True): - dev = Eurotherm3k(PREFIX, name="et") - dev.loop1.tolerance = 0.5 - dev.loop1.move_timeout = 0.3 # keep the test fast - set_mock_value(dev.loop1.readback, 0.0) # stays far from 300 + loop = Eurotherm3kLoop(f"{PREFIX}LOOP1:", name="et-loop1") + await loop.connect(mock=LazyMock()) + loop.tolerance = 0.5 + loop.move_timeout = 0.3 # keep the test fast + set_mock_value(loop.readback, 0.0) # stays far from 300 with pytest.raises(TimeoutError): - await dev.loop1.set(300.0) + await loop.set(300.0) # Legacy PDF eurotherm3k (pdf-profile-collection/startup/16-eurotherm_HAB.py): From 202aa65c9da7ed19f68b9ca52c0ee7c16e6e87e0 Mon Sep 17 00:00:00 2001 From: thomashopkins32 Date: Mon, 5 Oct 2026 13:42:44 -0400 Subject: [PATCH 5/5] Add one missing test; Upgrade ophyd-async version Assisted-by: oh-my-pi:gpt-5.6-terra --- nslsii/tests/ophyd_async/test_eurotherm3k.py | 40 +++++++++- pixi.lock | 78 ++++++++++++-------- pyproject.toml | 2 +- 3 files changed, 88 insertions(+), 32 deletions(-) diff --git a/nslsii/tests/ophyd_async/test_eurotherm3k.py b/nslsii/tests/ophyd_async/test_eurotherm3k.py index fce3445a..12e47c60 100644 --- a/nslsii/tests/ophyd_async/test_eurotherm3k.py +++ b/nslsii/tests/ophyd_async/test_eurotherm3k.py @@ -1,7 +1,14 @@ +import asyncio import time import pytest -from ophyd_async.core import LazyMock, get_mock_put, init_devices, set_mock_value +from ophyd_async.core import ( + LazyMock, + callback_on_mock_put, + get_mock_put, + init_devices, + set_mock_value, +) from nslsii.ophyd_async.devices import ( Eurotherm3k, @@ -75,6 +82,37 @@ async def test_set_holds_in_band_for_settle_time(): get_mock_put(loop.setpoint).assert_called_once_with(300.0) +@pytest.mark.asyncio +async def test_set_restarts_settle_time_after_out_of_band_excursion(): + loop = Eurotherm3kLoop(f"{PREFIX}LOOP1:", name="et-loop1") + await loop.connect(mock=LazyMock()) + loop.tolerance = 1.0 + loop.settle_time = 0.2 + loop.move_timeout = 5.0 + set_mock_value(loop.readback, 300.0) + + async def make_excursion() -> None: + await asyncio.sleep(loop.settle_time / 2) + set_mock_value(loop.readback, 0.0) + await asyncio.sleep(0) + set_mock_value(loop.readback, 300.0) + + excursion_task: asyncio.Task[None] | None = None + + def start_excursion(value: float) -> None: + nonlocal excursion_task + excursion_task = asyncio.create_task(make_excursion()) + + callback_on_mock_put(loop.setpoint, start_excursion) + start = time.monotonic() + await loop.set(300.0) + elapsed = time.monotonic() - start + assert excursion_task is not None + await excursion_task + assert 0.3 <= elapsed < 5.0 + get_mock_put(loop.setpoint).assert_called_once_with(300.0) + + @pytest.mark.asyncio async def test_set_times_out_if_never_in_band(): loop = Eurotherm3kLoop(f"{PREFIX}LOOP1:", name="et-loop1") diff --git a/pixi.lock b/pixi.lock index 85af3e65..ec2f05e6 100644 --- a/pixi.lock +++ b/pixi.lock @@ -77,6 +77,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/68/b9/f44b139096467996b4d85589d9880fd55f0c80a204d298a1b73d0adc2654/scanspec-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6c/01/626bf9585228485b82f0afb4460413f34d2898c8482fab9bfc200be369e1/ophyd_async-0.21.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/72/89/0265b2b79424ed05b8d1e9c8fca71e1b150478e5b0c19aa50b0ae397326e/velocity_profile-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl @@ -86,7 +87,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/83/05/1001c7b589430a45219348c8c32622305304be2173b74d202fd596a39c3e/ophyd_async-0.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl @@ -185,6 +185,7 @@ environments: - pypi: ./ - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl @@ -241,6 +242,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/68/b9/f44b139096467996b4d85589d9880fd55f0c80a204d298a1b73d0adc2654/scanspec-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6c/01/626bf9585228485b82f0afb4460413f34d2898c8482fab9bfc200be369e1/ophyd_async-0.21.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/72/89/0265b2b79424ed05b8d1e9c8fca71e1b150478e5b0c19aa50b0ae397326e/velocity_profile-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl @@ -258,7 +260,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/83/05/1001c7b589430a45219348c8c32622305304be2173b74d202fd596a39c3e/ophyd_async-0.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/c7/b5c8015d823bfda1a346adb2c634a2101d50bb75d421eb6dcb31acd25ebc/sphinx_rtd_theme-3.1.0-py2.py3-none-any.whl @@ -381,6 +382,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/03/8d/671d81534ea0e2b0e8a121be100020da09eb78861fe3aa8f3ef7dcd3bed1/msgpack-1.2.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl @@ -443,6 +445,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/68/b9/f44b139096467996b4d85589d9880fd55f0c80a204d298a1b73d0adc2654/scanspec-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6c/01/626bf9585228485b82f0afb4460413f34d2898c8482fab9bfc200be369e1/ophyd_async-0.21.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/72/89/0265b2b79424ed05b8d1e9c8fca71e1b150478e5b0c19aa50b0ae397326e/velocity_profile-1.0.0-py3-none-any.whl @@ -459,7 +462,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/83/05/1001c7b589430a45219348c8c32622305304be2173b74d202fd596a39c3e/ophyd_async-0.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/c7/b5c8015d823bfda1a346adb2c634a2101d50bb75d421eb6dcb31acd25ebc/sphinx_rtd_theme-3.1.0-py2.py3-none-any.whl @@ -576,6 +578,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl @@ -635,6 +638,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/68/b9/f44b139096467996b4d85589d9880fd55f0c80a204d298a1b73d0adc2654/scanspec-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6a/fd/6adabd4f6d5e686f97dd02ce7fce3fe4cf672cbac36b8f67ff4040e8ad8b/msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/6c/01/626bf9585228485b82f0afb4460413f34d2898c8482fab9bfc200be369e1/ophyd_async-0.21.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/72/89/0265b2b79424ed05b8d1e9c8fca71e1b150478e5b0c19aa50b0ae397326e/velocity_profile-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl @@ -650,7 +654,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/83/05/1001c7b589430a45219348c8c32622305304be2173b74d202fd596a39c3e/ophyd_async-0.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl @@ -768,6 +771,7 @@ environments: - pypi: ./ - pypi: https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl @@ -824,6 +828,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/68/b9/f44b139096467996b4d85589d9880fd55f0c80a204d298a1b73d0adc2654/scanspec-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6c/01/626bf9585228485b82f0afb4460413f34d2898c8482fab9bfc200be369e1/ophyd_async-0.21.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/72/89/0265b2b79424ed05b8d1e9c8fca71e1b150478e5b0c19aa50b0ae397326e/velocity_profile-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl @@ -841,7 +846,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/83/05/1001c7b589430a45219348c8c32622305304be2173b74d202fd596a39c3e/ophyd_async-0.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/87/c7/b5c8015d823bfda1a346adb2c634a2101d50bb75d421eb6dcb31acd25ebc/sphinx_rtd_theme-3.1.0-py2.py3-none-any.whl @@ -1442,7 +1446,7 @@ packages: - numpy - opencv-python - ophyd - - ophyd-async[ca] + - ophyd-async[ca]>=0.21.3 - packaging - pillow - psutil @@ -1478,6 +1482,20 @@ packages: version: 1.2.1 sha256: a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4 requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl + name: pytest-asyncio + version: 1.4.0 + sha256: 933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1 + requires_dist: + - backports-asyncio-runner>=1.1,<2 ; python_full_version < '3.11' + - pytest>=8.4,<10 + - typing-extensions>=4.12 ; python_full_version < '3.13' + - sphinx>=5.3 ; extra == 'docs' + - sphinx-rtd-theme>=1 ; extra == 'docs' + - sphinx-tabs>=3.5 ; extra == 'docs' + - coverage>=6.2 ; extra == 'testing' + - hypothesis>=5.7.1 ; extra == 'testing' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl name: h11 version: 0.16.0 @@ -3034,6 +3052,30 @@ packages: version: 1.2.1 sha256: 020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/6c/01/626bf9585228485b82f0afb4460413f34d2898c8482fab9bfc200be369e1/ophyd_async-0.21.3-py3-none-any.whl + name: ophyd-async + version: 0.21.3 + sha256: 63a868c0c64cd2b349983dcce5a46ba9dcea6c922de1f7a3ea92c717f918afd4 + requires_dist: + - numpy + - bluesky>=1.13.1rc2 + - event-model>=1.23 + - pyyaml + - colorlog + - pydantic>=2.0 + - pydantic-numpy + - stamina>=23.1.0 + - scanspec>=1.0.0 + - velocity-profile + - h5py ; extra == 'sim' + - aioca>=2.0a4 ; extra == 'ca' + - p4p>=4.2.0 ; extra == 'pva' + - pytango>=10.1.3 ; extra == 'tango' + - ipython ; extra == 'demo' + - matplotlib ; extra == 'demo' + - pyqt6<6.10 ; extra == 'demo' + - pytest ; extra == 'demo' + requires_python: '>=3.11' - pypi: https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl name: pyyaml version: 6.0.3 @@ -3282,30 +3324,6 @@ packages: - markdown-it-py>=2.2.0 - pygments>=2.13.0,<3.0.0 requires_python: '>=3.9.0' -- pypi: https://files.pythonhosted.org/packages/83/05/1001c7b589430a45219348c8c32622305304be2173b74d202fd596a39c3e/ophyd_async-0.20.0-py3-none-any.whl - name: ophyd-async - version: 0.20.0 - sha256: 3459199794587131bd03ee65b75122f6656fd7500d5fbf91cc5ecfd14c889ca3 - requires_dist: - - numpy - - bluesky>=1.13.1rc2 - - event-model>=1.23 - - pyyaml - - colorlog - - pydantic>=2.0 - - pydantic-numpy - - stamina>=23.1.0 - - scanspec>=0.8 - - velocity-profile - - h5py ; extra == 'sim' - - aioca>=2.0a4 ; extra == 'ca' - - p4p>=4.2.0 ; extra == 'pva' - - pytango>=10.1.3 ; extra == 'tango' - - ipython ; extra == 'demo' - - matplotlib ; extra == 'demo' - - pyqt6 ; extra == 'demo' - - pytest ; extra == 'demo' - requires_python: '>=3.11' - pypi: https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl name: pydantic-settings version: 2.11.0 diff --git a/pyproject.toml b/pyproject.toml index 5d77fe1b..75fdb91c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ dependencies = [ "numpy", "opencv-python", "ophyd", - "ophyd-async[ca]", + "ophyd-async[ca]>=0.21.3", "packaging", "pillow", "psutil",