Skip to content

Commit ff7da53

Browse files
authored
Merge branch 'main' into rest-backend
2 parents 262fde4 + 5256be3 commit ff7da53

File tree

10 files changed

+409
-141
lines changed

10 files changed

+409
-141
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ dependencies = [
1717
"pydantic",
1818
"pvi~=0.10.0",
1919
"pytango",
20-
"softioc",
20+
"softioc>=4.5.0",
2121
]
2222
dynamic = ["version"]
2323
license.file = "LICENSE"

src/fastcs/attributes.py

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
from collections.abc import Callable
34
from enum import Enum
45
from typing import Any, Generic, Protocol, runtime_checkable
56

@@ -26,7 +27,8 @@ async def put(self, controller: Any, attr: AttrW, value: Any) -> None:
2627
class Updater(Protocol):
2728
"""Protocol for updating the cached readback value of an ``Attribute``."""
2829

29-
update_period: float
30+
# If update period is None then the attribute will not be updated as a task.
31+
update_period: float | None = None
3032

3133
async def update(self, controller: Any, attr: AttrR) -> None:
3234
pass
@@ -52,6 +54,7 @@ def __init__(
5254
group: str | None = None,
5355
handler: Any = None,
5456
allowed_values: list[T] | None = None,
57+
description: str | None = None,
5558
) -> None:
5659
assert (
5760
datatype.dtype in ATTRIBUTE_TYPES
@@ -61,6 +64,11 @@ def __init__(
6164
self._group = group
6265
self.enabled = True
6366
self._allowed_values: list[T] | None = allowed_values
67+
self.description = description
68+
69+
# A callback to use when setting the datatype to a different value, for example
70+
# changing the units on an int. This should be implemented in the backend.
71+
self._update_datatype_callbacks: list[Callable[[DataType[T]], None]] = []
6472

6573
@property
6674
def datatype(self) -> DataType[T]:
@@ -82,6 +90,20 @@ def group(self) -> str | None:
8290
def allowed_values(self) -> list[T] | None:
8391
return self._allowed_values
8492

93+
def add_update_datatype_callback(
94+
self, callback: Callable[[DataType[T]], None]
95+
) -> None:
96+
self._update_datatype_callbacks.append(callback)
97+
98+
def update_datatype(self, datatype: DataType[T]) -> None:
99+
if not isinstance(self._datatype, type(datatype)):
100+
raise ValueError(
101+
f"Attribute datatype must be of type {type(self._datatype)}"
102+
)
103+
self._datatype = datatype
104+
for callback in self._update_datatype_callbacks:
105+
callback(datatype)
106+
85107

86108
class AttrR(Attribute[T]):
87109
"""A read-only ``Attribute``."""
@@ -92,24 +114,29 @@ def __init__(
92114
access_mode=AttrMode.READ,
93115
group: str | None = None,
94116
handler: Updater | None = None,
117+
initial_value: T | None = None,
95118
allowed_values: list[T] | None = None,
119+
description: str | None = None,
96120
) -> None:
97121
super().__init__(
98122
datatype, # type: ignore
99123
access_mode,
100124
group,
101125
handler,
102126
allowed_values=allowed_values, # type: ignore
127+
description=description,
128+
)
129+
self._value: T = (
130+
datatype.initial_value if initial_value is None else initial_value
103131
)
104-
self._value: T = datatype.dtype()
105132
self._update_callback: AttrCallback[T] | None = None
106133
self._updater = handler
107134

108135
def get(self) -> T:
109136
return self._value
110137

111138
async def set(self, value: T) -> None:
112-
self._value = self._datatype.dtype(value)
139+
self._value = self._datatype.validate(value)
113140

114141
if self._update_callback is not None:
115142
await self._update_callback(self._value)
@@ -132,13 +159,15 @@ def __init__(
132159
group: str | None = None,
133160
handler: Sender | None = None,
134161
allowed_values: list[T] | None = None,
162+
description: str | None = None,
135163
) -> None:
136164
super().__init__(
137165
datatype, # type: ignore
138166
access_mode,
139167
group,
140168
handler,
141169
allowed_values=allowed_values, # type: ignore
170+
description=description,
142171
)
143172
self._process_callback: AttrCallback[T] | None = None
144173
self._write_display_callback: AttrCallback[T] | None = None
@@ -150,11 +179,11 @@ async def process(self, value: T) -> None:
150179

151180
async def process_without_display_update(self, value: T) -> None:
152181
if self._process_callback is not None:
153-
await self._process_callback(self._datatype.dtype(value))
182+
await self._process_callback(self._datatype.validate(value))
154183

155184
async def update_display_without_process(self, value: T) -> None:
156185
if self._write_display_callback is not None:
157-
await self._write_display_callback(self._datatype.dtype(value))
186+
await self._write_display_callback(self._datatype.validate(value))
158187

159188
def set_process_callback(self, callback: AttrCallback[T] | None) -> None:
160189
self._process_callback = callback
@@ -170,7 +199,7 @@ def sender(self) -> Sender | None:
170199
return self._sender
171200

172201

173-
class AttrRW(AttrW[T], AttrR[T]):
202+
class AttrRW(AttrR[T], AttrW[T]):
174203
"""A read-write ``Attribute``."""
175204

176205
def __init__(
@@ -179,14 +208,18 @@ def __init__(
179208
access_mode=AttrMode.READ_WRITE,
180209
group: str | None = None,
181210
handler: Handler | None = None,
211+
initial_value: T | None = None,
182212
allowed_values: list[T] | None = None,
213+
description: str | None = None,
183214
) -> None:
184215
super().__init__(
185216
datatype, # type: ignore
186217
access_mode,
187-
group,
188-
handler,
189-
allowed_values, # type: ignore
218+
group=group,
219+
handler=handler,
220+
initial_value=initial_value,
221+
allowed_values=allowed_values, # type: ignore
222+
description=description,
190223
)
191224

192225
async def process(self, value: T) -> None:

src/fastcs/backend.py

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ def __init__(
2020
self._loop = self._dispatcher.loop
2121
self._controller = controller
2222

23-
self._initial_tasks = [controller.connect]
24-
self._scan_tasks: list[Future] = []
23+
self._initial_coros = [controller.connect]
24+
self._scan_futures: set[Future] = set()
2525

2626
asyncio.run_coroutine_threadsafe(
2727
self._controller.initialise(), self._loop
@@ -41,22 +41,32 @@ def _link_process_tasks(self):
4141
_link_single_controller_put_tasks(single_mapping)
4242
_link_attribute_sender_class(single_mapping)
4343

44-
def run(self):
45-
self._run_initial_tasks()
46-
self._start_scan_tasks()
44+
def __del__(self):
45+
self.stop_scan_futures()
4746

47+
def run(self):
48+
self._run_initial_futures()
49+
self.start_scan_futures()
4850
self._run()
4951

50-
def _run_initial_tasks(self):
51-
for task in self._initial_tasks:
52-
future = asyncio.run_coroutine_threadsafe(task(), self._loop)
52+
def _run_initial_futures(self):
53+
for coro in self._initial_coros:
54+
future = asyncio.run_coroutine_threadsafe(coro(), self._loop)
5355
future.result()
5456

55-
def _start_scan_tasks(self):
56-
scan_tasks = _get_scan_tasks(self._mapping)
57+
def start_scan_futures(self):
58+
self._scan_futures = {
59+
asyncio.run_coroutine_threadsafe(coro(), self._loop)
60+
for coro in _get_scan_coros(self._mapping)
61+
}
5762

58-
for task in scan_tasks:
59-
asyncio.run_coroutine_threadsafe(task(), self._loop)
63+
def stop_scan_futures(self):
64+
for future in self._scan_futures:
65+
if not future.done():
66+
try:
67+
future.cancel()
68+
except asyncio.CancelledError:
69+
pass
6070

6171
def _run(self):
6272
raise NotImplementedError("Specific Backend must implement _run")
@@ -98,15 +108,15 @@ async def callback(value):
98108
return callback
99109

100110

101-
def _get_scan_tasks(mapping: Mapping) -> list[Callable]:
111+
def _get_scan_coros(mapping: Mapping) -> list[Callable]:
102112
scan_dict: dict[float, list[Callable]] = defaultdict(list)
103113

104114
for single_mapping in mapping.get_controller_mappings():
105115
_add_scan_method_tasks(scan_dict, single_mapping)
106116
_add_attribute_updater_tasks(scan_dict, single_mapping)
107117

108-
scan_tasks = _get_periodic_scan_tasks(scan_dict)
109-
return scan_tasks
118+
scan_coros = _get_periodic_scan_coros(scan_dict)
119+
return scan_coros
110120

111121

112122
def _add_scan_method_tasks(
@@ -127,7 +137,8 @@ def _add_attribute_updater_tasks(
127137
callback = _create_updater_callback(
128138
attribute, single_mapping.controller
129139
)
130-
scan_dict[update_period].append(callback)
140+
if update_period is not None:
141+
scan_dict[update_period].append(callback)
131142

132143

133144
def _create_updater_callback(attribute, controller):
@@ -144,18 +155,18 @@ async def callback():
144155
return callback
145156

146157

147-
def _get_periodic_scan_tasks(scan_dict: dict[float, list[Callable]]) -> list[Callable]:
148-
periodic_scan_tasks: list[Callable] = []
158+
def _get_periodic_scan_coros(scan_dict: dict[float, list[Callable]]) -> list[Callable]:
159+
periodic_scan_coros: list[Callable] = []
149160
for period, methods in scan_dict.items():
150-
periodic_scan_tasks.append(_create_periodic_scan_task(period, methods))
161+
periodic_scan_coros.append(_create_periodic_scan_coro(period, methods))
151162

152-
return periodic_scan_tasks
163+
return periodic_scan_coros
153164

154165

155-
def _create_periodic_scan_task(period, methods: list[Callable]) -> Callable:
156-
async def scan_task() -> None:
166+
def _create_periodic_scan_coro(period, methods: list[Callable]) -> Callable:
167+
async def scan_coro() -> None:
157168
while True:
158169
await asyncio.gather(*[method() for method in methods])
159170
await asyncio.sleep(period)
160171

161-
return scan_task
172+
return scan_coro

src/fastcs/backends/epics/backend.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,22 @@
77

88

99
class EpicsBackend(Backend):
10-
def __init__(self, controller: Controller, pv_prefix: str = "MY-DEVICE-PREFIX"):
10+
def __init__(
11+
self,
12+
controller: Controller,
13+
pv_prefix: str = "MY-DEVICE-PREFIX",
14+
options: EpicsIOCOptions | None = None,
15+
):
1116
super().__init__(controller)
1217

1318
self._pv_prefix = pv_prefix
14-
self._ioc = EpicsIOC(pv_prefix, self._mapping)
19+
self._ioc = EpicsIOC(pv_prefix, self._mapping, options=options)
1520

1621
def create_docs(self, options: EpicsDocsOptions | None = None) -> None:
1722
EpicsDocs(self._mapping).create_docs(options)
1823

1924
def create_gui(self, options: EpicsGUIOptions | None = None) -> None:
2025
EpicsGUI(self._mapping, self._pv_prefix).create_gui(options)
2126

22-
def _run(self, options: EpicsIOCOptions | None = None):
23-
self._ioc.run(self._dispatcher, self._context, options)
27+
def _run(self):
28+
self._ioc.run(self._dispatcher, self._context)

0 commit comments

Comments
 (0)