Skip to content

Commit 5256be3

Browse files
authored
Merge pull request #82 from DiamondLightSource/81-add-features-to-attributes-and-datatypes
81 add features to attributes and datatypes
2 parents 4c9d82a + 546934a commit 5256be3

File tree

6 files changed

+227
-38
lines changed

6 files changed

+227
-38
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ dependencies = [
1616
"pydantic",
1717
"pvi~=0.10.0",
1818
"pytango",
19-
"softioc",
19+
"softioc>=4.5.0",
2020
]
2121
dynamic = ["version"]
2222
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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,8 @@ def _add_attribute_updater_tasks(
137137
callback = _create_updater_callback(
138138
attribute, single_mapping.controller
139139
)
140-
scan_dict[update_period].append(callback)
140+
if update_period is not None:
141+
scan_dict[update_period].append(callback)
141142

142143

143144
def _create_updater_callback(attribute, controller):

src/fastcs/backends/epics/ioc.py

Lines changed: 90 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from collections.abc import Callable
2-
from dataclasses import dataclass
2+
from dataclasses import asdict, dataclass
33
from types import MethodType
44
from typing import Any, Literal
55

@@ -15,7 +15,7 @@
1515
enum_value_to_index,
1616
)
1717
from fastcs.controller import BaseController
18-
from fastcs.datatypes import Bool, Float, Int, String, T
18+
from fastcs.datatypes import Bool, DataType, Float, Int, String, T
1919
from fastcs.exceptions import FastCSException
2020
from fastcs.mapping import Mapping
2121

@@ -27,6 +27,26 @@ class EpicsIOCOptions:
2727
terminal: bool = True
2828

2929

30+
DATATYPE_NAME_TO_RECORD_FIELD = {
31+
"prec": "PREC",
32+
"units": "EGU",
33+
"min": "DRVL",
34+
"max": "DRVH",
35+
"min_alarm": "LOPR",
36+
"max_alarm": "HOPR",
37+
"znam": "ZNAM",
38+
"onam": "ONAM",
39+
}
40+
41+
42+
def datatype_to_epics_fields(datatype: DataType) -> dict[str, Any]:
43+
return {
44+
DATATYPE_NAME_TO_RECORD_FIELD[field]: value
45+
for field, value in asdict(datatype).items()
46+
if field in DATATYPE_NAME_TO_RECORD_FIELD
47+
}
48+
49+
3050
class EpicsIOC:
3151
def __init__(
3252
self, pv_prefix: str, mapping: Mapping, options: EpicsIOCOptions | None = None
@@ -172,27 +192,50 @@ async def async_record_set(value: T):
172192

173193

174194
def _get_input_record(pv: str, attribute: AttrR) -> RecordWrapper:
195+
attribute_fields = {}
196+
if attribute.description is not None:
197+
attribute_fields.update({"DESC": attribute.description})
198+
175199
if attr_is_enum(attribute):
176200
assert attribute.allowed_values is not None and all(
177201
isinstance(v, str) for v in attribute.allowed_values
178202
)
179203
state_keys = dict(zip(MBB_STATE_FIELDS, attribute.allowed_values, strict=False))
180-
return builder.mbbIn(pv, **state_keys)
204+
return builder.mbbIn(pv, **state_keys, **attribute_fields)
181205

182206
match attribute.datatype:
183-
case Bool(znam, onam):
184-
return builder.boolIn(pv, ZNAM=znam, ONAM=onam)
207+
case Bool():
208+
record = builder.boolIn(
209+
pv, **datatype_to_epics_fields(attribute.datatype), **attribute_fields
210+
)
185211
case Int():
186-
return builder.longIn(pv)
187-
case Float(prec):
188-
return builder.aIn(pv, PREC=prec)
212+
record = builder.longIn(
213+
pv,
214+
**datatype_to_epics_fields(attribute.datatype),
215+
**attribute_fields,
216+
)
217+
case Float():
218+
record = builder.aIn(
219+
pv,
220+
**datatype_to_epics_fields(attribute.datatype),
221+
**attribute_fields,
222+
)
189223
case String():
190-
return builder.longStringIn(pv)
224+
record = builder.longStringIn(
225+
pv, **datatype_to_epics_fields(attribute.datatype), **attribute_fields
226+
)
191227
case _:
192228
raise FastCSException(
193229
f"Unsupported type {type(attribute.datatype)}: {attribute.datatype}"
194230
)
195231

232+
def datatype_updater(datatype: DataType):
233+
for name, value in datatype_to_epics_fields(datatype).items():
234+
record.set_field(name, value)
235+
236+
attribute.add_update_datatype_callback(datatype_updater)
237+
return record
238+
196239

197240
def _create_and_link_write_pv(
198241
pv_prefix: str, pv_name: str, attr_name: str, attribute: AttrW[T]
@@ -225,33 +268,62 @@ async def async_write_display(value: T):
225268

226269

227270
def _get_output_record(pv: str, attribute: AttrW, on_update: Callable) -> Any:
271+
attribute_fields = {}
272+
if attribute.description is not None:
273+
attribute_fields.update({"DESC": attribute.description})
228274
if attr_is_enum(attribute):
229275
assert attribute.allowed_values is not None and all(
230276
isinstance(v, str) for v in attribute.allowed_values
231277
)
232278
state_keys = dict(zip(MBB_STATE_FIELDS, attribute.allowed_values, strict=False))
233-
return builder.mbbOut(pv, always_update=True, on_update=on_update, **state_keys)
279+
return builder.mbbOut(
280+
pv,
281+
always_update=True,
282+
on_update=on_update,
283+
**state_keys,
284+
**attribute_fields,
285+
)
234286

235287
match attribute.datatype:
236-
case Bool(znam, onam):
237-
return builder.boolOut(
288+
case Bool():
289+
record = builder.boolOut(
238290
pv,
239-
ZNAM=znam,
240-
ONAM=onam,
291+
**datatype_to_epics_fields(attribute.datatype),
241292
always_update=True,
242293
on_update=on_update,
243294
)
244295
case Int():
245-
return builder.longOut(pv, always_update=True, on_update=on_update)
246-
case Float(prec):
247-
return builder.aOut(pv, always_update=True, on_update=on_update, PREC=prec)
296+
record = builder.longOut(
297+
pv,
298+
always_update=True,
299+
on_update=on_update,
300+
**datatype_to_epics_fields(attribute.datatype),
301+
**attribute_fields,
302+
)
303+
case Float():
304+
record = builder.aOut(
305+
pv,
306+
always_update=True,
307+
on_update=on_update,
308+
**datatype_to_epics_fields(attribute.datatype),
309+
**attribute_fields,
310+
)
248311
case String():
249-
return builder.longStringOut(pv, always_update=True, on_update=on_update)
312+
record = builder.longStringOut(
313+
pv, always_update=True, on_update=on_update, **attribute_fields
314+
)
250315
case _:
251316
raise FastCSException(
252317
f"Unsupported type {type(attribute.datatype)}: {attribute.datatype}"
253318
)
254319

320+
def datatype_updater(datatype: DataType):
321+
for name, value in datatype_to_epics_fields(datatype).items():
322+
record.set_field(name, value)
323+
324+
attribute.add_update_datatype_callback(datatype_updater)
325+
return record
326+
255327

256328
def _create_and_link_command_pvs(pv_prefix: str, mapping: Mapping) -> None:
257329
for single_mapping in mapping.get_controller_mappings():

0 commit comments

Comments
 (0)