Skip to content

Commit e1910a2

Browse files
author
Peter Braun
committed
migrate to new connect paradigm, omit all comlex runengine handling
1 parent b324464 commit e1910a2

File tree

9 files changed

+198
-296
lines changed

9 files changed

+198
-296
lines changed

src/secop_ophyd/SECoPDevices.py

Lines changed: 82 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import inspect
33
import logging
44
import re
5-
import threading
65
import time as ttime
76
from logging import Logger
87
from types import MethodType
@@ -32,7 +31,9 @@
3231
TupleOf,
3332
)
3433
from ophyd_async.core import (
34+
DEFAULT_TIMEOUT,
3535
AsyncStatus,
36+
LazyMock,
3637
SignalR,
3738
SignalRW,
3839
SignalX,
@@ -839,8 +840,17 @@ class SECoPNodeDevice(StandardReadable):
839840
to the Sec-node properties
840841
"""
841842

843+
name: str = ""
844+
842845
def __init__(
843-
self, secclient: AsyncFrappyClient, logger: Logger, logdir: str | None = None
846+
self,
847+
sec_node_uri: str,
848+
# `prefix` not used, it's just that the device connecter requires it for
849+
# some reason.
850+
prefix: str = "",
851+
name: str = "",
852+
loglevel: str = "INFO",
853+
logdir: str | None = None,
844854
):
845855
"""Initializes the node device and generates all node signals and subdevices
846856
corresponding to the SECoP-modules of the secnode
@@ -849,170 +859,98 @@ def __init__(
849859
:type secclient: AsyncFrappyClient
850860
"""
851861

852-
self.equipment_id: SignalR
853-
self.description: SignalR
854-
self.version: SignalR
855-
856-
self._secclient: AsyncFrappyClient = secclient
857-
858-
self._module_name: str = ""
859-
self._node_cls_name: str = ""
860-
self.mod_devices: Dict[str, SECoPReadableDevice] = {}
861-
self.node_prop_devices: Dict[str, SignalR] = {}
862-
863-
self.genCode: GenNodeCode
864-
865-
# Name is set to sec-node equipment_id
866-
name = self._secclient.properties[EQUIPMENT_ID].replace(".", "-")
867-
868-
config = []
869-
870-
self.logger: Logger = logger
871-
872-
self.logger.info(
873-
"Initializing SECoPNodeDevice "
874-
+ f"({self._secclient.host}:{self._secclient.port})"
875-
)
876-
877-
with self.add_children_as_readables(
878-
format=StandardReadableFormat.CONFIG_SIGNAL
879-
):
880-
for property in self._secclient.properties:
881-
propb = PropertyBackend(property, self._secclient.properties, secclient)
882-
setattr(self, property, SignalR(backend=propb))
883-
config.append(getattr(self, property))
884-
self.node_prop_devices[property] = getattr(self, property)
885-
886-
with self.add_children_as_readables(format=StandardReadableFormat.CHILD):
887-
for module, module_desc in self._secclient.modules.items():
862+
self.host, self.port = sec_node_uri.rsplit(":", maxsplit=1)
888863

889-
secop_dev_class = self.class_from_interface(module_desc["properties"])
890-
891-
if secop_dev_class is not None:
892-
setattr(
893-
self,
894-
module,
895-
secop_dev_class(
896-
self._secclient,
897-
module,
898-
loglevel=self.logger.level,
899-
logdir=logdir,
900-
),
901-
)
902-
self.mod_devices[module] = getattr(self, module)
903-
904-
# register secclient callbacks (these are useful if sec node description
905-
# changes after a reconnect)
906-
secclient.client.register_callback(
907-
None, self.descriptiveDataChange, self.nodeStateChange
864+
self.logger: Logger = setup_logging(
865+
name=f"frappy:{self.host}:{self.port}",
866+
level=LOG_LEVELS[loglevel],
867+
log_dir=logdir,
908868
)
869+
self.logdir = logdir
909870

910-
super().__init__(name=name)
911-
912-
@classmethod
913-
def create(
914-
cls,
915-
host: str,
916-
port: str,
917-
loop,
918-
loglevel: str = "INFO",
919-
logdir: str | None = None,
920-
) -> "SECoPNodeDevice":
921-
922-
secclient: AsyncFrappyClient
923-
924-
logger: Logger = setup_logging(
925-
name=f"frappy:{host}:{port}", level=LOG_LEVELS[loglevel], log_dir=logdir
926-
)
871+
self.name = name
927872

928-
if not loop.is_running():
929-
raise Exception("The provided Eventloop is not running")
873+
async def connect(
874+
self,
875+
mock: bool | LazyMock = False,
876+
timeout: float = DEFAULT_TIMEOUT,
877+
force_reconnect: bool = False,
878+
):
879+
if not hasattr(self, "_secclient"):
880+
secclient: AsyncFrappyClient
930881

931-
if loop._thread_id != threading.current_thread().ident:
932-
client_future = asyncio.run_coroutine_threadsafe(
933-
AsyncFrappyClient.create(host=host, port=port, loop=loop, log=logger),
934-
loop,
882+
secclient = await AsyncFrappyClient.create(
883+
host=self.host,
884+
port=self.port,
885+
loop=asyncio.get_running_loop(),
886+
log=self.logger,
935887
)
936-
secclient = client_future.result()
937888

938-
secclient.external = True
889+
self.equipment_id: SignalR
890+
self.description: SignalR
891+
self.version: SignalR
939892

940-
else:
941-
raise Exception(
942-
"should be calles with an eventloop that is"
943-
"running in a seperate thread"
944-
)
893+
self._secclient: AsyncFrappyClient = secclient
945894

946-
return SECoPNodeDevice(secclient=secclient, logger=logger, logdir=logdir)
895+
self._module_name: str = ""
896+
self._node_cls_name: str = ""
897+
self.mod_devices: Dict[str, SECoPReadableDevice] = {}
898+
self.node_prop_devices: Dict[str, SignalR] = {}
947899

948-
@classmethod
949-
async def create_async(
950-
cls,
951-
host: str,
952-
port: str,
953-
loop,
954-
loglevel: str = "INFO",
955-
logdir: str | None = None,
956-
) -> "SECoPNodeDevice":
900+
self.genCode: GenNodeCode
957901

958-
logger: Logger = setup_logging(
959-
name=f"frappy:{host}:{port}", level=LOG_LEVELS[loglevel], log_dir=logdir
960-
)
902+
if self.name == "":
903+
self.name = self._secclient.properties[EQUIPMENT_ID].replace(".", "-")
961904

962-
secclient: AsyncFrappyClient
963-
964-
if not loop.is_running():
965-
raise Exception("The provided Eventloop is not running")
905+
config = []
966906

967-
if loop._thread_id == threading.current_thread().ident:
968-
secclient = await AsyncFrappyClient.create(
969-
host=host, port=port, loop=loop, log=logger
907+
self.logger.info(
908+
"Initializing SECoPNodeDevice "
909+
+ f"({self._secclient.host}:{self._secclient.port})"
970910
)
971911

972-
secclient.external = False
912+
with self.add_children_as_readables(
913+
format=StandardReadableFormat.CONFIG_SIGNAL
914+
):
915+
for property in self._secclient.properties:
916+
propb = PropertyBackend(
917+
property, self._secclient.properties, secclient
918+
)
919+
setattr(self, property, SignalR(backend=propb))
920+
config.append(getattr(self, property))
921+
self.node_prop_devices[property] = getattr(self, property)
973922

974-
else:
975-
# Event loop is running in a different thread
976-
client_future = asyncio.run_coroutine_threadsafe(
977-
AsyncFrappyClient.create(host=host, port=port, loop=loop, log=logger),
978-
loop,
979-
)
980-
secclient = await asyncio.wrap_future(future=client_future)
981-
secclient.external = True
923+
with self.add_children_as_readables(format=StandardReadableFormat.CHILD):
924+
for module, module_desc in self._secclient.modules.items():
982925

983-
return SECoPNodeDevice(secclient=secclient, logger=logger, logdir=logdir)
926+
secop_dev_class = self.class_from_interface(
927+
module_desc["properties"]
928+
)
984929

985-
def disconnect(self):
986-
"""shuts down secclient, eventloop must be running in external thread"""
987-
if (
988-
self._secclient.loop._thread_id == threading.current_thread().ident
989-
and self._secclient.loop.is_running()
990-
):
991-
raise Exception(
992-
"Eventloop must be running in external thread,"
993-
" try await node.disconnect_async()"
994-
)
995-
else:
996-
future = asyncio.run_coroutine_threadsafe(
997-
self._secclient.disconnect(True), self._secclient.loop
930+
if secop_dev_class is not None:
931+
setattr(
932+
self,
933+
module,
934+
secop_dev_class(
935+
self._secclient,
936+
module,
937+
loglevel=self.logger.level,
938+
logdir=self.logdir,
939+
),
940+
)
941+
self.mod_devices[module] = getattr(self, module)
942+
943+
# register secclient callbacks (these are useful if sec node description
944+
# changes after a reconnect)
945+
secclient.client.register_callback(
946+
None, self.descriptiveDataChange, self.nodeStateChange
998947
)
999948

1000-
future.result(2)
949+
super().__init__(name=self.name)
1001950

1002-
async def disconnect_async(self):
1003-
"""shuts down secclient using asyncio, eventloop can be running in same or
1004-
external thread
1005-
"""
1006-
if (
1007-
self._secclient.loop._thread_id == threading.current_thread().ident
1008-
and self._secclient.loop.is_running()
1009-
):
951+
elif force_reconnect or self._secclient.client.online is False:
1010952
await self._secclient.disconnect(True)
1011-
else:
1012-
disconn_future = asyncio.run_coroutine_threadsafe(
1013-
self._secclient.disconnect(True), self._secclient.loop
1014-
)
1015-
await asyncio.wrap_future(future=disconn_future)
953+
await self._secclient.connect(try_period=DEFAULT_TIMEOUT)
1016954

1017955
def class_from_instance(self, path_to_module: str | None = None):
1018956
"""Dynamically generate python class file for the SECoP_Node_Device, this

tests/conftest.py

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
StructOf,
1414
TupleOf,
1515
)
16+
from ophyd_async.core import init_devices
1617
from xprocess import ProcessStarter
1718

1819
from secop_ophyd.AsyncFrappyClient import AsyncFrappyClient
@@ -151,38 +152,49 @@ async def nested_client(nested_struct_sim, logger, port="10771"):
151152

152153

153154
@pytest.fixture
154-
async def run_engine():
155+
async def RE(): # noqa: N802
155156
re = RunEngine({})
156157
return re
157158

158159

159160
@pytest.fixture
160-
async def cryo_node(run_engine):
161-
return SECoPNodeDevice.create(
162-
host="localhost", port="10769", loop=run_engine.loop, loglevel="INFO"
163-
)
161+
async def nested_node_no_re():
162+
async with init_devices():
163+
nested = SECoPNodeDevice(
164+
sec_node_uri="localhost:10771",
165+
)
166+
167+
return nested
164168

165169

166170
@pytest.fixture
167-
def nested_node_re(run_engine):
168-
return SECoPNodeDevice.create(host="localhost", port="10771", loop=run_engine.loop)
171+
def nested_node(RE): # noqa: N803
172+
with init_devices():
173+
nested = SECoPNodeDevice(
174+
sec_node_uri="localhost:10771",
175+
)
176+
177+
return nested
169178

170179

171180
@pytest.fixture
172-
async def cryo_node_internal_loop():
173-
return await SECoPNodeDevice.create_async(
174-
host="localhost",
175-
port="10769",
176-
loop=asyncio.get_running_loop(),
177-
loglevel="DEBUG",
178-
)
181+
async def cryo_node_no_re():
182+
async with init_devices():
183+
cryo = SECoPNodeDevice(
184+
sec_node_uri="localhost:10769",
185+
)
186+
187+
return cryo
179188

180189

181190
@pytest.fixture
182-
async def nested_node():
183-
return await SECoPNodeDevice.create_async(
184-
host="localhost", port="10771", loop=asyncio.get_running_loop()
185-
)
191+
def cryo_node(RE): # noqa: N803
192+
with init_devices():
193+
cryo = SECoPNodeDevice(
194+
sec_node_uri="localhost:10769",
195+
)
196+
197+
return cryo
186198

187199

188200
@pytest.fixture

tests/test_ classgen.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,15 @@
44
from secop_ophyd.SECoPDevices import SECoPNodeDevice
55

66

7-
async def test_class_gen(nested_struct_sim, nested_node: SECoPNodeDevice):
8-
nested_node.class_from_instance()
7+
async def test_class_gen(nested_struct_sim, nested_node_no_re: SECoPNodeDevice):
8+
nested_node_no_re.class_from_instance()
99

1010
if os.path.exists("genNodeClass.py"):
1111
os.remove("genNodeClass.py")
1212

13-
await nested_node.disconnect_async()
1413

15-
16-
async def test_class_gen_path(nested_struct_sim, nested_node: SECoPNodeDevice):
17-
nested_node.class_from_instance("tests")
14+
async def test_class_gen_path(nested_struct_sim, nested_node_no_re: SECoPNodeDevice):
15+
nested_node_no_re.class_from_instance("tests")
1816

1917
if os.path.exists("tests/genNodeClass.py"):
2018
os.remove("tests/genNodeClass.py")
21-
22-
await nested_node.disconnect_async()

0 commit comments

Comments
 (0)