Skip to content

Commit 62ff4ab

Browse files
committed
Add update datatype logic to Attribute
1 parent eaf23e9 commit 62ff4ab

File tree

4 files changed

+115
-40
lines changed

4 files changed

+115
-40
lines changed

src/fastcs/attributes.py

Lines changed: 19 additions & 0 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

@@ -65,6 +66,10 @@ def __init__(
6566
self._allowed_values: list[T] | None = allowed_values
6667
self.description = description
6768

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]] = []
72+
6873
@property
6974
def datatype(self) -> DataType[T]:
7075
return self._datatype
@@ -85,6 +90,20 @@ def group(self) -> str | None:
8590
def allowed_values(self) -> list[T] | None:
8691
return self._allowed_values
8792

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+
88107

89108
class AttrR(Attribute[T]):
90109
"""A read-only ``Attribute``."""

src/fastcs/backends/epics/ioc.py

Lines changed: 59 additions & 40 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__(self, pv_prefix: str, mapping: Mapping):
3252
_add_pvi_info(f"{pv_prefix}:PVI")
@@ -184,36 +204,38 @@ def _get_input_record(pv: str, attribute: AttrR) -> RecordWrapper:
184204
return builder.mbbIn(pv, **state_keys, **attribute_fields)
185205

186206
match attribute.datatype:
187-
case Bool(znam, onam):
188-
return builder.boolIn(pv, ZNAM=znam, ONAM=onam, **attribute_fields)
189-
case Int(units, min, max, min_alarm, max_alarm):
190-
return builder.longIn(
207+
case Bool():
208+
record = builder.boolIn(
209+
pv, **datatype_to_epics_fields(attribute.datatype), **attribute_fields
210+
)
211+
case Int():
212+
record = builder.longIn(
191213
pv,
192-
EGU=units,
193-
DRVL=min,
194-
DRVH=max,
195-
LOPR=min_alarm,
196-
HOPR=max_alarm,
214+
**datatype_to_epics_fields(attribute.datatype),
197215
**attribute_fields,
198216
)
199-
case Float(prec, units, min, max, min_alarm, max_alarm):
200-
return builder.aIn(
217+
case Float():
218+
record = builder.aIn(
201219
pv,
202-
PREC=prec,
203-
EGU=units,
204-
DRVL=min,
205-
DRVH=max,
206-
LOPR=min_alarm,
207-
HOPR=max_alarm,
220+
**datatype_to_epics_fields(attribute.datatype),
208221
**attribute_fields,
209222
)
210223
case String():
211-
return builder.longStringIn(pv, **attribute_fields)
224+
record = builder.longStringIn(
225+
pv, **datatype_to_epics_fields(attribute.datatype), **attribute_fields
226+
)
212227
case _:
213228
raise FastCSException(
214229
f"Unsupported type {type(attribute.datatype)}: {attribute.datatype}"
215230
)
216231

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+
217239

218240
def _create_and_link_write_pv(
219241
pv_prefix: str, pv_name: str, attr_name: str, attribute: AttrW[T]
@@ -262,48 +284,45 @@ def _get_output_record(pv: str, attribute: AttrW, on_update: Callable) -> Any:
262284
)
263285

264286
match attribute.datatype:
265-
case Bool(znam, onam):
266-
return builder.boolOut(
287+
case Bool():
288+
record = builder.boolOut(
267289
pv,
268-
ZNAM=znam,
269-
ONAM=onam,
290+
**datatype_to_epics_fields(attribute.datatype),
270291
always_update=True,
271292
on_update=on_update,
272293
)
273-
case Int(units, min, max, min_alarm, max_alarm):
274-
return builder.longOut(
294+
case Int():
295+
record = builder.longOut(
275296
pv,
276297
always_update=True,
277298
on_update=on_update,
278-
EGU=units,
279-
DRVL=min,
280-
DRVH=max,
281-
LOPR=min_alarm,
282-
HOPR=max_alarm,
299+
**datatype_to_epics_fields(attribute.datatype),
283300
**attribute_fields,
284301
)
285-
case Float(prec, units, min, max, min_alarm, max_alarm):
286-
return builder.aOut(
302+
case Float():
303+
record = builder.aOut(
287304
pv,
288305
always_update=True,
289306
on_update=on_update,
290-
PREC=prec,
291-
EGU=units,
292-
DRVL=min,
293-
DRVH=max,
294-
LOPR=min_alarm,
295-
HOPR=max_alarm,
307+
**datatype_to_epics_fields(attribute.datatype),
296308
**attribute_fields,
297309
)
298310
case String():
299-
return builder.longStringOut(
311+
record = builder.longStringOut(
300312
pv, always_update=True, on_update=on_update, **attribute_fields
301313
)
302314
case _:
303315
raise FastCSException(
304316
f"Unsupported type {type(attribute.datatype)}: {attribute.datatype}"
305317
)
306318

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

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

src/fastcs/datatypes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
AttrCallback = Callable[[T], Awaitable[None]]
1313

1414

15+
@dataclass(frozen=True) # So that we can type hint with dataclass methods
1516
class DataType(Generic[T]):
1617
"""Generic datatype mapping to a python type, with additional metadata."""
1718

tests/backends/epics/test_ioc.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,3 +459,39 @@ def test_long_pv_names_discarded(mocker: MockerFixture):
459459
always_update=True,
460460
on_update=mocker.ANY,
461461
)
462+
463+
464+
def test_update_datatype(mocker: MockerFixture):
465+
builder = mocker.patch("fastcs.backends.epics.ioc.builder")
466+
467+
pv_name = f"{DEVICE}:Attr"
468+
469+
attr_r = AttrR(Int())
470+
record_r = _get_input_record(pv_name, attr_r)
471+
472+
builder.longIn.assert_called_once_with(pv_name, **DEFAULT_SCALAR_FIELD_ARGS)
473+
record_r.set_field.assert_not_called()
474+
attr_r.update_datatype(Int(units="m", min=-3))
475+
record_r.set_field.assert_any_call("EGU", "m")
476+
record_r.set_field.assert_any_call("DRVL", -3)
477+
478+
with pytest.raises(
479+
ValueError,
480+
match="Attribute datatype must be of type <class 'fastcs.datatypes.Int'>",
481+
):
482+
attr_r.update_datatype(String()) # type: ignore
483+
484+
attr_w = AttrW(Int())
485+
record_w = _get_output_record(pv_name, attr_w, on_update=mocker.ANY)
486+
487+
builder.longIn.assert_called_once_with(pv_name, **DEFAULT_SCALAR_FIELD_ARGS)
488+
record_w.set_field.assert_not_called()
489+
attr_w.update_datatype(Int(units="m", min=-3))
490+
record_w.set_field.assert_any_call("EGU", "m")
491+
record_w.set_field.assert_any_call("DRVL", -3)
492+
493+
with pytest.raises(
494+
ValueError,
495+
match="Attribute datatype must be of type <class 'fastcs.datatypes.Int'>",
496+
):
497+
attr_w.update_datatype(String()) # type: ignore

0 commit comments

Comments
 (0)