Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ dependencies = [
"pydantic",
"pvi~=0.10.0",
"pytango",
"softioc",
"softioc>=4.5.0",
]
dynamic = ["version"]
license.file = "LICENSE"
Expand Down
51 changes: 42 additions & 9 deletions src/fastcs/attributes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from collections.abc import Callable
from enum import Enum
from typing import Any, Generic, Protocol, runtime_checkable

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

update_period: float
# If update period is None then the attribute will not be updated as a task.
update_period: float | None = None

async def update(self, controller: Any, attr: AttrR) -> None:
pass
Expand All @@ -52,6 +54,7 @@ def __init__(
group: str | None = None,
handler: Any = None,
allowed_values: list[T] | None = None,
description: str | None = None,
) -> None:
assert (
datatype.dtype in ATTRIBUTE_TYPES
Expand All @@ -61,6 +64,11 @@ def __init__(
self._group = group
self.enabled = True
self._allowed_values: list[T] | None = allowed_values
self.description = description

# A callback to use when setting the datatype to a different value, for example
# changing the units on an int. This should be implemented in the backend.
self._update_datatype_callbacks: list[Callable[[DataType[T]], None]] = []

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

def add_update_datatype_callback(
self, callback: Callable[[DataType[T]], None]
) -> None:
self._update_datatype_callbacks.append(callback)

def update_datatype(self, datatype: DataType[T]) -> None:
if not isinstance(self._datatype, type(datatype)):
raise ValueError(
f"Attribute datatype must be of type {type(self._datatype)}"
)
self._datatype = datatype
for callback in self._update_datatype_callbacks:
callback(datatype)


class AttrR(Attribute[T]):
"""A read-only ``Attribute``."""
Expand All @@ -92,24 +114,29 @@ def __init__(
access_mode=AttrMode.READ,
group: str | None = None,
handler: Updater | None = None,
initial_value: T | None = None,
allowed_values: list[T] | None = None,
description: str | None = None,
) -> None:
super().__init__(
datatype, # type: ignore
access_mode,
group,
handler,
allowed_values=allowed_values, # type: ignore
description=description,
)
self._value: T = (
datatype.initial_value if initial_value is None else initial_value
)
self._value: T = datatype.dtype()
self._update_callback: AttrCallback[T] | None = None
self._updater = handler

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

async def set(self, value: T) -> None:
self._value = self._datatype.dtype(value)
self._value = self._datatype.validate(value)

if self._update_callback is not None:
await self._update_callback(self._value)
Expand All @@ -132,13 +159,15 @@ def __init__(
group: str | None = None,
handler: Sender | None = None,
allowed_values: list[T] | None = None,
description: str | None = None,
) -> None:
super().__init__(
datatype, # type: ignore
access_mode,
group,
handler,
allowed_values=allowed_values, # type: ignore
description=description,
)
self._process_callback: AttrCallback[T] | None = None
self._write_display_callback: AttrCallback[T] | None = None
Expand All @@ -150,11 +179,11 @@ async def process(self, value: T) -> None:

async def process_without_display_update(self, value: T) -> None:
if self._process_callback is not None:
await self._process_callback(self._datatype.dtype(value))
await self._process_callback(self._datatype.validate(value))

async def update_display_without_process(self, value: T) -> None:
if self._write_display_callback is not None:
await self._write_display_callback(self._datatype.dtype(value))
await self._write_display_callback(self._datatype.validate(value))

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


class AttrRW(AttrW[T], AttrR[T]):
class AttrRW(AttrR[T], AttrW[T]):
"""A read-write ``Attribute``."""

def __init__(
Expand All @@ -179,14 +208,18 @@ def __init__(
access_mode=AttrMode.READ_WRITE,
group: str | None = None,
handler: Handler | None = None,
initial_value: T | None = None,
allowed_values: list[T] | None = None,
description: str | None = None,
) -> None:
super().__init__(
datatype, # type: ignore
access_mode,
group,
handler,
allowed_values, # type: ignore
group=group,
handler=handler,
initial_value=initial_value,
allowed_values=allowed_values, # type: ignore
description=description,
)

async def process(self, value: T) -> None:
Expand Down
3 changes: 2 additions & 1 deletion src/fastcs/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ def _add_attribute_updater_tasks(
callback = _create_updater_callback(
attribute, single_mapping.controller
)
scan_dict[update_period].append(callback)
if update_period is not None:
scan_dict[update_period].append(callback)


def _create_updater_callback(attribute, controller):
Expand Down
108 changes: 90 additions & 18 deletions src/fastcs/backends/epics/ioc.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from collections.abc import Callable
from dataclasses import dataclass
from dataclasses import asdict, dataclass
from types import MethodType
from typing import Any, Literal

Expand All @@ -15,7 +15,7 @@
enum_value_to_index,
)
from fastcs.controller import BaseController
from fastcs.datatypes import Bool, Float, Int, String, T
from fastcs.datatypes import Bool, DataType, Float, Int, String, T
from fastcs.exceptions import FastCSException
from fastcs.mapping import Mapping

Expand All @@ -27,6 +27,26 @@ class EpicsIOCOptions:
terminal: bool = True


DATATYPE_NAME_TO_RECORD_FIELD = {
"prec": "PREC",
"units": "EGU",
"min": "DRVL",
"max": "DRVH",
"min_alarm": "LOPR",
"max_alarm": "HOPR",
"znam": "ZNAM",
"onam": "ONAM",
}


def datatype_to_epics_fields(datatype: DataType) -> dict[str, Any]:
return {
DATATYPE_NAME_TO_RECORD_FIELD[field]: value
for field, value in asdict(datatype).items()
if field in DATATYPE_NAME_TO_RECORD_FIELD
}


class EpicsIOC:
def __init__(self, pv_prefix: str, mapping: Mapping):
_add_pvi_info(f"{pv_prefix}:PVI")
Expand Down Expand Up @@ -172,27 +192,50 @@ async def async_record_set(value: T):


def _get_input_record(pv: str, attribute: AttrR) -> RecordWrapper:
attribute_fields = {}
if attribute.description is not None:
attribute_fields.update({"DESC": attribute.description})

if attr_is_enum(attribute):
assert attribute.allowed_values is not None and all(
isinstance(v, str) for v in attribute.allowed_values
)
state_keys = dict(zip(MBB_STATE_FIELDS, attribute.allowed_values, strict=False))
return builder.mbbIn(pv, **state_keys)
return builder.mbbIn(pv, **state_keys, **attribute_fields)

match attribute.datatype:
case Bool(znam, onam):
return builder.boolIn(pv, ZNAM=znam, ONAM=onam)
case Bool():
record = builder.boolIn(
pv, **datatype_to_epics_fields(attribute.datatype), **attribute_fields
)
case Int():
return builder.longIn(pv)
case Float(prec):
return builder.aIn(pv, PREC=prec)
record = builder.longIn(
pv,
**datatype_to_epics_fields(attribute.datatype),
**attribute_fields,
)
case Float():
record = builder.aIn(
pv,
**datatype_to_epics_fields(attribute.datatype),
**attribute_fields,
)
case String():
return builder.longStringIn(pv)
record = builder.longStringIn(
pv, **datatype_to_epics_fields(attribute.datatype), **attribute_fields
)
case _:
raise FastCSException(
f"Unsupported type {type(attribute.datatype)}: {attribute.datatype}"
)

def datatype_updater(datatype: DataType):
for name, value in datatype_to_epics_fields(datatype).items():
record.set_field(name, value)

attribute.add_update_datatype_callback(datatype_updater)
return record


def _create_and_link_write_pv(
pv_prefix: str, pv_name: str, attr_name: str, attribute: AttrW[T]
Expand Down Expand Up @@ -224,33 +267,62 @@ async def async_write_display(value: T):


def _get_output_record(pv: str, attribute: AttrW, on_update: Callable) -> Any:
attribute_fields = {}
if attribute.description is not None:
attribute_fields.update({"DESC": attribute.description})
if attr_is_enum(attribute):
assert attribute.allowed_values is not None and all(
isinstance(v, str) for v in attribute.allowed_values
)
state_keys = dict(zip(MBB_STATE_FIELDS, attribute.allowed_values, strict=False))
return builder.mbbOut(pv, always_update=True, on_update=on_update, **state_keys)
return builder.mbbOut(
pv,
always_update=True,
on_update=on_update,
**state_keys,
**attribute_fields,
)

match attribute.datatype:
case Bool(znam, onam):
return builder.boolOut(
case Bool():
record = builder.boolOut(
pv,
ZNAM=znam,
ONAM=onam,
**datatype_to_epics_fields(attribute.datatype),
always_update=True,
on_update=on_update,
)
case Int():
return builder.longOut(pv, always_update=True, on_update=on_update)
case Float(prec):
return builder.aOut(pv, always_update=True, on_update=on_update, PREC=prec)
record = builder.longOut(
pv,
always_update=True,
on_update=on_update,
**datatype_to_epics_fields(attribute.datatype),
**attribute_fields,
)
case Float():
record = builder.aOut(
pv,
always_update=True,
on_update=on_update,
**datatype_to_epics_fields(attribute.datatype),
**attribute_fields,
)
case String():
return builder.longStringOut(pv, always_update=True, on_update=on_update)
record = builder.longStringOut(
pv, always_update=True, on_update=on_update, **attribute_fields
)
case _:
raise FastCSException(
f"Unsupported type {type(attribute.datatype)}: {attribute.datatype}"
)

def datatype_updater(datatype: DataType):
for name, value in datatype_to_epics_fields(datatype).items():
record.set_field(name, value)

attribute.add_update_datatype_callback(datatype_updater)
return record


def _create_and_link_command_pvs(pv_prefix: str, mapping: Mapping) -> None:
for single_mapping in mapping.get_controller_mappings():
Expand Down
Loading
Loading