Skip to content

Commit db35461

Browse files
committed
added a callback to Attribute enabling backend to change values
For example, updating an attribute to have an `Int` with different units will make the epics backend update `EGU` of the corresponding record.
1 parent 2173531 commit db35461

File tree

3 files changed

+79
-35
lines changed

3 files changed

+79
-35
lines changed

src/fastcs/attributes.py

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

93+
def set_update_datatype_callback(
94+
self, callback: Callable[[DataType[T]], None] | None
95+
) -> None:
96+
self._update_datatype_callback = callback
97+
98+
def update_datatype(self, datatype: DataType[T]) -> None:
99+
if not isinstance(self._datatype, type(datatype)):
100+
raise ValueError(f"Attribute datatype must be of type {type(datatype)}")
101+
self._datatype = datatype
102+
if self._update_datatype_callback is not None:
103+
self._update_datatype_callback(datatype)
104+
88105

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

src/fastcs/backends/epics/ioc.py

Lines changed: 61 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
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

6-
from softioc import builder, softioc
6+
import numpy as np
7+
from softioc import builder, fields, softioc
78
from softioc.asyncio_dispatcher import AsyncioDispatcher
9+
from softioc.imports import db_put_field
810
from softioc.pythonSoftIoc import RecordWrapper
911

1012
from fastcs.attributes import AttrR, AttrRW, AttrW
@@ -17,7 +19,7 @@
1719
enum_value_to_index,
1820
)
1921
from fastcs.controller import BaseController
20-
from fastcs.datatypes import Bool, Float, Int, String, T
22+
from fastcs.datatypes import Bool, DataType, Float, Int, String, T
2123
from fastcs.exceptions import FastCSException
2224
from fastcs.mapping import Mapping
2325

@@ -30,6 +32,35 @@ class EpicsIOCOptions:
3032
name_options: EpicsNameOptions = EpicsNameOptions()
3133

3234

35+
DATATYPE_NAME_TO_RECORD_FIELD = {
36+
"prec": "PREC",
37+
"units": "EGU",
38+
"min": "DRVL",
39+
"max": "DRVH",
40+
"min_alarm": "LOPR",
41+
"max_alarm": "HOPR",
42+
"znam": "ZNAM",
43+
"onam": "ONAM",
44+
}
45+
46+
47+
def datatype_to_epics_fields(datatype: DataType) -> dict[str, Any]:
48+
return {
49+
DATATYPE_NAME_TO_RECORD_FIELD[field]: value
50+
for field, value in asdict(datatype).items()
51+
}
52+
53+
54+
def reload_attribute_fields(pv: str, DataType: DataType):
55+
"""If the ioc side changes a field on the attribute
56+
e.g ``units`` then this method will update it on the attribute"""
57+
58+
for name, value in datatype_to_epics_fields(DataType):
59+
# TODO: can we just make every dtype a string and have the ioc convert?
60+
array = np.require(value, dtype=np.dtype("S40"))
61+
db_put_field(f"{pv}.{name}", fields.DBR_STRING, array, 1)
62+
63+
3364
class EpicsIOC:
3465
def __init__(
3566
self, pv_prefix: str, mapping: Mapping, options: EpicsIOCOptions | None = None
@@ -327,32 +358,32 @@ def _get_input_record(pv: str, attribute: AttrR) -> RecordWrapper:
327358
state_keys = dict(zip(MBB_STATE_FIELDS, attribute.allowed_values, strict=False))
328359
return builder.mbbIn(pv, **state_keys, **attribute_fields)
329360

361+
def datatype_updater(datatype: DataType):
362+
reload_attribute_fields(pv, datatype)
363+
364+
attribute.set_update_datatype_callback(datatype_updater)
365+
330366
match attribute.datatype:
331-
case Bool(znam, onam):
332-
return builder.boolIn(pv, ZNAM=znam, ONAM=onam, **attribute_fields)
333-
case Int(units, min, max, min_alarm, max_alarm):
367+
case Bool():
368+
return builder.boolIn(
369+
pv, **datatype_to_epics_fields(attribute.datatype), **attribute_fields
370+
)
371+
case Int():
334372
return builder.longIn(
335373
pv,
336-
EGU=units,
337-
DRVL=min,
338-
DRVH=max,
339-
LOPR=min_alarm,
340-
HOPR=max_alarm,
374+
**datatype_to_epics_fields(attribute.datatype),
341375
**attribute_fields,
342376
)
343-
case Float(prec, units, min, max, min_alarm, max_alarm):
377+
case Float():
344378
return builder.aIn(
345379
pv,
346-
PREC=prec,
347-
EGU=units,
348-
DRVL=min,
349-
DRVH=max,
350-
LOPR=min_alarm,
351-
HOPR=max_alarm,
380+
**datatype_to_epics_fields(attribute.datatype),
352381
**attribute_fields,
353382
)
354383
case String():
355-
return builder.longStringIn(pv, **attribute_fields)
384+
return builder.longStringIn(
385+
pv, **datatype_to_epics_fields(attribute.datatype), **attribute_fields
386+
)
356387
case _:
357388
raise FastCSException(
358389
f"Unsupported type {type(attribute.datatype)}: {attribute.datatype}"
@@ -376,38 +407,33 @@ def _get_output_record(pv: str, attribute: AttrW, on_update: Callable) -> Any:
376407
**attribute_fields,
377408
)
378409

410+
def datatype_updater(datatype: DataType):
411+
reload_attribute_fields(pv, datatype)
412+
413+
attribute.set_update_datatype_callback(datatype_updater)
414+
379415
match attribute.datatype:
380-
case Bool(znam, onam):
416+
case Bool():
381417
return builder.boolOut(
382418
pv,
383-
ZNAM=znam,
384-
ONAM=onam,
419+
**datatype_to_epics_fields(attribute.datatype),
385420
always_update=True,
386421
on_update=on_update,
387422
)
388-
case Int(units, min, max, min_alarm, max_alarm):
423+
case Int():
389424
return builder.longOut(
390425
pv,
391426
always_update=True,
392427
on_update=on_update,
393-
EGU=units,
394-
DRVL=min,
395-
DRVH=max,
396-
LOPR=min_alarm,
397-
HOPR=max_alarm,
428+
**datatype_to_epics_fields(attribute.datatype),
398429
**attribute_fields,
399430
)
400-
case Float(prec, units, min, max, min_alarm, max_alarm):
431+
case Float():
401432
return builder.aOut(
402433
pv,
403434
always_update=True,
404435
on_update=on_update,
405-
PREC=prec,
406-
EGU=units,
407-
DRVL=min,
408-
DRVH=max,
409-
LOPR=min_alarm,
410-
HOPR=max_alarm,
436+
**datatype_to_epics_fields(attribute.datatype),
411437
**attribute_fields,
412438
)
413439
case String():

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

0 commit comments

Comments
 (0)