From 37f136f227f898e9bba02a0ac1c5732dc739b689 Mon Sep 17 00:00:00 2001 From: Peter Wegmann <85115389+Bilchreis@users.noreply.github.com> Date: Wed, 3 Apr 2024 11:02:32 +0200 Subject: [PATCH 1/6] cache signals in ophyd, tests --- src/secop_ophyd/SECoPDevices.py | 6 +++++ tests/test_Node.py | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/secop_ophyd/SECoPDevices.py b/src/secop_ophyd/SECoPDevices.py index 71a602e..3d8c894 100644 --- a/src/secop_ophyd/SECoPDevices.py +++ b/src/secop_ophyd/SECoPDevices.py @@ -142,6 +142,12 @@ def _signal_from_parameter(self, path: Path, sig_name: str, readonly: bool): else: setattr(self, sig_name, SignalRW(paramb)) + def noop(): + pass + + sig: SignalR = getattr(self, sig_name) + sig.subscribe(noop) + async def wait_for_idle(self): """asynchronously waits until module is IDLE again. this is helpful, for running commands that are not done immediately diff --git a/tests/test_Node.py b/tests/test_Node.py index ee5f422..efc3f13 100644 --- a/tests/test_Node.py +++ b/tests/test_Node.py @@ -46,6 +46,48 @@ async def test_dev_read(cryo_sim, cryo_node_internal_loop: SECoPNodeDevice): await cryo_node_internal_loop.disconnect_async() +async def test_signal_read(cryo_sim, cryo_node_internal_loop: SECoPNodeDevice): + # Node device has no read value, it has to return an empty dict + cryo_dev: SECoPMoveableDevice = cryo_node_internal_loop.cryo + + p = await cryo_dev.p.get_value(cached=False) + assert p == 40.0 + + await cryo_node_internal_loop.disconnect_async() + + +async def test_signal_read_cached(cryo_sim, cryo_node_internal_loop: SECoPNodeDevice): + # Node device has no read value, it has to return an empty dict + cryo_dev: SECoPMoveableDevice = cryo_node_internal_loop.cryo + + p = await cryo_dev.p.get_value(cached=True) + + assert p == 40.0 + + await cryo_node_internal_loop.disconnect_async() + + +async def test_signal_stage_unstage_read_cached( + cryo_sim, cryo_node_internal_loop: SECoPNodeDevice +): + # Node device has no read value, it has to return an empty dict + cryo_dev: SECoPMoveableDevice = cryo_node_internal_loop.cryo + + await cryo_dev.value.stage() + + await asyncio.sleep(1) + + await cryo_dev.value.unstage() + + await asyncio.sleep(1) + + p = await cryo_dev.p.get_value(cached=True) + + assert p == 40.0 + + await cryo_node_internal_loop.disconnect_async() + + async def test_status(cryo_sim, cryo_node_internal_loop: SECoPNodeDevice): # Node device has no read value, it has to return an empty dict cryo_dev: SECoPMoveableDevice = cryo_node_internal_loop.cryo From 3e04e2e8ab410e911c416e48ae3cb619aa042d6f Mon Sep 17 00:00:00 2001 From: Peter Wegmann <85115389+Bilchreis@users.noreply.github.com> Date: Wed, 3 Apr 2024 11:57:48 +0200 Subject: [PATCH 2/6] fixes noop signature --- src/secop_ophyd/SECoPDevices.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/secop_ophyd/SECoPDevices.py b/src/secop_ophyd/SECoPDevices.py index 3d8c894..5cef8c6 100644 --- a/src/secop_ophyd/SECoPDevices.py +++ b/src/secop_ophyd/SECoPDevices.py @@ -142,11 +142,11 @@ def _signal_from_parameter(self, path: Path, sig_name: str, readonly: bool): else: setattr(self, sig_name, SignalRW(paramb)) - def noop(): + def noop(val): pass sig: SignalR = getattr(self, sig_name) - sig.subscribe(noop) + sig.subscribe_value(noop) async def wait_for_idle(self): """asynchronously waits until module is IDLE again. this is helpful, From e1869179b8a38bf73a33de42390ed317f3a0ee2c Mon Sep 17 00:00:00 2001 From: Peter Wegmann Date: Tue, 9 Jul 2024 15:40:39 +0200 Subject: [PATCH 3/6] fixed ophyd-async v0.3.4 compatability issue --- pyproject.toml | 4 ++-- src/secop_ophyd/SECoPSignal.py | 34 ++++++++++++++++++++++++++-------- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e3ce87c..dbbddda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,8 @@ version = "0.0.1" description = "An Interface between bluesky and SECoP, using ophyd and frappy-client" dependencies = [ - 'ophyd-async == 0.2.0', + 'ophyd-async == 0.3.4', + 'frappy-core@git+https://github.com/SampleEnvironment/frappy@v0.19.2' ] @@ -21,7 +22,6 @@ requires-python = ">=3.10" dev = [ 'isort', 'pytest == 7.4.2', - 'frappy-core@git+https://github.com/SampleEnvironment/frappy', 'black', 'flake8', "flake8-isort == 6.1.1", diff --git a/src/secop_ophyd/SECoPSignal.py b/src/secop_ophyd/SECoPSignal.py index bbf4ee9..71c9775 100644 --- a/src/secop_ophyd/SECoPSignal.py +++ b/src/secop_ophyd/SECoPSignal.py @@ -71,11 +71,11 @@ def __init__( self.describe_dict: dict - self.source = self.path._module_name + ":" + self.path._accessible_name + self.source_name = self.path._module_name + ":" + self.path._accessible_name self.describe_dict = {} - self.describe_dict["source"] = self.source + self.describe_dict["source"] = self.source() self.describe_dict.update(self.SECoP_type_info.describe_dict) @@ -84,6 +84,10 @@ def __init__( property_name = "SECoP_dtype" self.describe_dict[property_name] = prop_val + + def source(self, name: str) -> str: + return self.source_name + async def connect(self): pass @@ -141,7 +145,11 @@ def __init__( self.argument: LocalBackend | None = argument self.result: LocalBackend | None = result - self.source = self.path._module_name + ":" + self.path._accessible_name + self.source_name = self.path._module_name + ":" + self.path._accessible_name + + def source(self, name: str) -> str: + return self.source_name + async def connect(self): pass @@ -175,7 +183,7 @@ async def get_descriptor(self) -> Descriptor: res = {} - res["source"] = self.source + res["source"] = self.source() # ophyd datatype (some SECoP datatypeshaveto be converted) # signalx has no datatype and is never read @@ -237,7 +245,7 @@ def __init__(self, path: Path, secclient: AsyncFrappyClient) -> None: self.describe_dict: dict = {} - self.source = ( + self.source_name = ( secclient.uri + ":" + secclient.nodename @@ -250,7 +258,7 @@ def __init__(self, path: Path, secclient: AsyncFrappyClient) -> None: # SECoP metadata is static and can only change when connection is reset self.describe_dict = {} - self.describe_dict["source"] = self.source + self.describe_dict["source"] = self.source_name # add gathered keys from SECoPdtype: self.describe_dict.update(self.SECoP_type_info.describe_dict) @@ -266,6 +274,12 @@ def __init__(self, path: Path, secclient: AsyncFrappyClient) -> None: property_name = "SECoP_dtype" self.describe_dict[property_name] = prop_val + + + def source(self, name: str) -> str: + return self.source_name + + async def connect(self): pass @@ -351,7 +365,11 @@ def __init__( self._datatype = self._get_datatype() self._secclient: AsyncFrappyClient = secclient # TODO full property path - self.source = prop_key + self.source_name = prop_key + + + def source(self, name: str) -> str: + return str(self.source_name) def _get_datatype(self) -> str: prop_val = self._property_dict[self._prop_key] @@ -382,7 +400,7 @@ async def get_descriptor(self) -> Descriptor: """Metadata like source, dtype, shape, precision, units""" description = {} - description["source"] = str(self.source) + description["source"] = self.source() description["dtype"] = self._get_datatype() description["shape"] = [] # type: ignore From 56f74b43f6ab2339eb7d77ba2d2ca16ee6cdc6f6 Mon Sep 17 00:00:00 2001 From: Peter Wegmann Date: Tue, 9 Jul 2024 15:43:39 +0200 Subject: [PATCH 4/6] linter fixes --- src/secop_ophyd/SECoPSignal.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/secop_ophyd/SECoPSignal.py b/src/secop_ophyd/SECoPSignal.py index 71c9775..f622ced 100644 --- a/src/secop_ophyd/SECoPSignal.py +++ b/src/secop_ophyd/SECoPSignal.py @@ -75,7 +75,7 @@ def __init__( self.describe_dict = {} - self.describe_dict["source"] = self.source() + self.describe_dict["source"] = self.source("") self.describe_dict.update(self.SECoP_type_info.describe_dict) @@ -84,7 +84,6 @@ def __init__( property_name = "SECoP_dtype" self.describe_dict[property_name] = prop_val - def source(self, name: str) -> str: return self.source_name @@ -150,7 +149,6 @@ def __init__( def source(self, name: str) -> str: return self.source_name - async def connect(self): pass @@ -183,7 +181,7 @@ async def get_descriptor(self) -> Descriptor: res = {} - res["source"] = self.source() + res["source"] = self.source("") # ophyd datatype (some SECoP datatypeshaveto be converted) # signalx has no datatype and is never read @@ -274,11 +272,8 @@ def __init__(self, path: Path, secclient: AsyncFrappyClient) -> None: property_name = "SECoP_dtype" self.describe_dict[property_name] = prop_val - - def source(self, name: str) -> str: return self.source_name - async def connect(self): pass @@ -367,7 +362,6 @@ def __init__( # TODO full property path self.source_name = prop_key - def source(self, name: str) -> str: return str(self.source_name) @@ -400,7 +394,7 @@ async def get_descriptor(self) -> Descriptor: """Metadata like source, dtype, shape, precision, units""" description = {} - description["source"] = self.source() + description["source"] = self.source("") description["dtype"] = self._get_datatype() description["shape"] = [] # type: ignore From c6caa8f71b70c7320c38ad6c61a690e6b659fd1f Mon Sep 17 00:00:00 2001 From: Peter Wegmann Date: Mon, 22 Jul 2024 13:26:38 +0200 Subject: [PATCH 5/6] switch to datakey --- src/secop_ophyd/SECoPDevices.py | 16 +++++++++++----- src/secop_ophyd/SECoPSignal.py | 14 ++++++++------ 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/secop_ophyd/SECoPDevices.py b/src/secop_ophyd/SECoPDevices.py index 5cef8c6..97bb2b9 100644 --- a/src/secop_ophyd/SECoPDevices.py +++ b/src/secop_ophyd/SECoPDevices.py @@ -32,7 +32,11 @@ ) from ophyd_async.core.async_status import AsyncStatus from ophyd_async.core.signal import SignalR, SignalRW, SignalX, observe_value -from ophyd_async.core.standard_readable import StandardReadable +from ophyd_async.core.standard_readable import ( + ConfigSignal, + HintedSignal, + StandardReadable, +) from ophyd_async.core.utils import T from typing_extensions import Self @@ -252,7 +256,8 @@ def __init__(self, path: Path, secclient: AsyncFrappyClient): self.commandx = SignalX(exec_backend) - self.set_readable_signals(read=read, config=config) + self.add_readables(read, wrapper=HintedSignal) + self.add_readables(config, wrapper=ConfigSignal) super().__init__(name=dev_name) @@ -378,7 +383,8 @@ def __init__(self, secclient: AsyncFrappyClient, module_name: str): self.plans.append(plan) - self.set_readable_signals(read=self._read, config=self._config) + self.add_readables(self._read, wrapper=HintedSignal) + self.add_readables(self._config, wrapper=ConfigSignal) self.set_name(module_name) @@ -605,7 +611,7 @@ def __init__(self, secclient: AsyncFrappyClient): setattr(self, module, secop_dev_class(self._secclient, module)) self.mod_devices[module] = getattr(self, module) - self.set_readable_signals(config=config) + self.add_readables(config, wrapper=ConfigSignal) # register secclient callbacks (these are useful if sec node description # changes after a reconnect) @@ -803,7 +809,7 @@ def descriptiveDataChange(self, module, description): # noqa: N802 setattr(self, property, SignalR(backend=propb)) config.append(getattr(self, property)) - self.set_readable_signals(config=config) + self.add_readables(config, wrapper=ConfigSignal) else: # Refresh changed modules module_desc = self._secclient.modules[module] diff --git a/src/secop_ophyd/SECoPSignal.py b/src/secop_ophyd/SECoPSignal.py index f622ced..37b06bb 100644 --- a/src/secop_ophyd/SECoPSignal.py +++ b/src/secop_ophyd/SECoPSignal.py @@ -3,7 +3,7 @@ from functools import wraps from typing import Any, Callable, Dict, Optional -from bluesky.protocols import Descriptor, Reading +from bluesky.protocols import DataKey, Reading from frappy.client import CacheItem from frappy.datatypes import ( ArrayOf, @@ -96,7 +96,8 @@ async def put(self, value: Any | None, wait=True, timeout=None): if self.callback is not None: self.callback(self.reading.get_reading(), self.reading.get_value()) - async def get_descriptor(self) -> Descriptor: + async def get_datakey(self, source: str) -> DataKey: + """Metadata like source, dtype, shape, precision, units""" return self.describe_dict async def get_reading(self) -> Reading: @@ -177,8 +178,8 @@ async def put(self, value: Any | None, wait=True, timeout=None): await self.result.put(val) - async def get_descriptor(self) -> Descriptor: - + async def get_datakey(self, source: str) -> DataKey: + """Metadata like source, dtype, shape, precision, units""" res = {} res["source"] = self.source("") @@ -287,7 +288,8 @@ async def put(self, value: Any | None, wait=True, timeout=None): timeout=timeout, ) - async def get_descriptor(self) -> Descriptor: + async def get_datakey(self, source: str) -> DataKey: + """Metadata like source, dtype, shape, precision, units""" return self.describe_dict async def get_reading(self) -> Reading: @@ -390,7 +392,7 @@ async def put(self, value: Optional[T], wait=True, timeout=None): # Properties are readonly pass - async def get_descriptor(self) -> Descriptor: + async def get_datakey(self, source: str) -> DataKey: """Metadata like source, dtype, shape, precision, units""" description = {} From 2ba9df9854e749e000ff42580bd7301e1583f279 Mon Sep 17 00:00:00 2001 From: Peter Wegmann Date: Mon, 22 Jul 2024 13:49:53 +0200 Subject: [PATCH 6/6] fix tests --- src/secop_ophyd/SECoPDevices.py | 6 +++--- tests/test_async_frappy_client.py | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/secop_ophyd/SECoPDevices.py b/src/secop_ophyd/SECoPDevices.py index 97bb2b9..2863d73 100644 --- a/src/secop_ophyd/SECoPDevices.py +++ b/src/secop_ophyd/SECoPDevices.py @@ -271,14 +271,14 @@ def trigger(self) -> AsyncStatus: :rtype: AsyncStatus """ coro = asyncio.wait_for(fut=self._exec_cmd(), timeout=None) - return AsyncStatus(awaitable=coro, watchers=None) + return AsyncStatus(awaitable=coro) def kickoff(self) -> AsyncStatus: # trigger execution of secop command, wait until Device is Busy self._start_time = ttime.time() coro = asyncio.wait_for(fut=asyncio.sleep(1), timeout=None) - return AsyncStatus(coro, watchers=None) + return AsyncStatus(coro) async def _exec_cmd(self): stat = self.commandx.trigger() @@ -287,7 +287,7 @@ async def _exec_cmd(self): def complete(self) -> AsyncStatus: coro = asyncio.wait_for(fut=self._exec_cmd(), timeout=None) - return AsyncStatus(awaitable=coro, watchers=None) + return AsyncStatus(awaitable=coro) def collect(self) -> Iterator[PartialEvent]: yield dict( diff --git a/tests/test_async_frappy_client.py b/tests/test_async_frappy_client.py index f226e08..4956108 100644 --- a/tests/test_async_frappy_client.py +++ b/tests/test_async_frappy_client.py @@ -56,6 +56,9 @@ async def test_async_secopclient_reconn( while async_frappy_client.state == "reconnecting": await asyncio.sleep(0.001) + while async_frappy_client.state == "activating": + await asyncio.sleep(0.001) + assert async_frappy_client.state == "connected" # ensures we are connected and getting fresh data again