Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement grouping of Attributes on generated UIs #19

Merged
merged 3 commits into from
Jan 23, 2024
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 @@ -14,7 +14,7 @@ description = "Control system agnostic framework for building Device support in
dependencies = [
"numpy",
"pydantic",
"pvi",
"pvi~=0.7.1",
"softioc",
] # Add project dependencies here, e.g. ["click", "numpy"]
dynamic = ["version"]
Expand Down
39 changes: 35 additions & 4 deletions src/fastcs/attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,25 @@


class AttrMode(Enum):
"""Access mode of an `Attribute`."""

READ = 1
WRITE = 2
READ_WRITE = 3


@runtime_checkable
class Sender(Protocol):
"""Protocol for setting the value of an `Attribute`."""

async def put(self, controller: Any, attr: AttrW, value: Any) -> None:
pass


@runtime_checkable
class Updater(Protocol):
"""Protocol for updating the cached readback value of an `Attribute`."""

update_period: float

async def update(self, controller: Any, attr: AttrR) -> None:
Expand All @@ -28,18 +34,30 @@

@runtime_checkable
class Handler(Sender, Updater, Protocol):
"""Protocol encapsulating both `Sender` and `Updater`."""

pass


class Attribute(Generic[T]):
"""Base FastCS attribute.

Instances of this class added to a `Controller` will be used by the backend.
"""

def __init__(
self, datatype: DataType[T], access_mode: AttrMode, handler: Any = None
self,
datatype: DataType[T],
access_mode: AttrMode,
group: str | None = None,
handler: Any = None,
) -> None:
assert (
datatype.dtype in ATTRIBUTE_TYPES
), f"Attr type must be one of {ATTRIBUTE_TYPES}, received type {datatype.dtype}"
self._datatype: DataType[T] = datatype
self._access_mode: AttrMode = access_mode
self._group = group

Check warning on line 60 in src/fastcs/attributes.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/attributes.py#L60

Added line #L60 was not covered by tests

@property
def datatype(self) -> DataType[T]:
Expand All @@ -53,15 +71,22 @@
def access_mode(self) -> AttrMode:
return self._access_mode

@property
def group(self) -> str | None:
return self._group

Check warning on line 76 in src/fastcs/attributes.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/attributes.py#L76

Added line #L76 was not covered by tests


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

def __init__(
self,
datatype: DataType[T],
access_mode=AttrMode.READ,
group: str | None = None,
handler: Updater | None = None,
) -> None:
super().__init__(datatype, access_mode, handler) # type: ignore
super().__init__(datatype, access_mode, group, handler) # type: ignore

Check warning on line 89 in src/fastcs/attributes.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/attributes.py#L89

Added line #L89 was not covered by tests
self._value: T = datatype.dtype()
self._update_callback: AttrCallback[T] | None = None
self._updater = handler
Expand All @@ -84,13 +109,16 @@


class AttrW(Attribute[T]):
"""A write-only `Attribute`."""

def __init__(
self,
datatype: DataType[T],
access_mode=AttrMode.WRITE,
group: str | None = None,
handler: Sender | None = None,
) -> None:
super().__init__(datatype, access_mode, handler) # type: ignore
super().__init__(datatype, access_mode, group, handler) # type: ignore

Check warning on line 121 in src/fastcs/attributes.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/attributes.py#L121

Added line #L121 was not covered by tests
self._process_callback: AttrCallback[T] | None = None
self._write_display_callback: AttrCallback[T] | None = None
self._sender = handler
Expand Down Expand Up @@ -120,13 +148,16 @@


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

def __init__(
self,
datatype: DataType[T],
access_mode=AttrMode.READ_WRITE,
group: str | None = None,
handler: Handler | None = None,
) -> None:
super().__init__(datatype, access_mode, handler) # type: ignore
super().__init__(datatype, access_mode, group, handler) # type: ignore

Check warning on line 160 in src/fastcs/attributes.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/attributes.py#L160

Added line #L160 was not covered by tests

async def process(self, value: T) -> None:
await self.set(value)
Expand Down
98 changes: 67 additions & 31 deletions src/fastcs/backends/epics/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
from enum import Enum
from pathlib import Path

from pvi._format.base import Formatter
from pvi._yaml_utils import deserialize_yaml
from pvi._format.dls import DLSFormatter
from pvi.device import (
LED,
CheckBox,
Expand All @@ -16,6 +15,7 @@
SignalRW,
SignalW,
SignalX,
SubScreen,
TextFormat,
TextRead,
TextWrite,
Expand All @@ -24,11 +24,10 @@
)

from fastcs.attributes import Attribute, AttrR, AttrRW, AttrW
from fastcs.cs_methods import Command
from fastcs.datatypes import Bool, DataType, Float, Int, String
from fastcs.exceptions import FastCSException
from fastcs.mapping import Mapping

FORMATTER_YAML = Path.cwd() / ".." / "pvi" / "formatters" / "dls.bob.pvi.formatter.yaml"
from fastcs.mapping import Mapping, SingleMapping


class EpicsGUIFormat(Enum):
Expand Down Expand Up @@ -83,26 +82,32 @@
@classmethod
def _get_attribute_component(cls, attr_path: str, name: str, attribute: Attribute):
pv = cls._get_pv(attr_path, name)
name = name.title().replace("_", " ")
name = name.title().replace("_", "")

Check warning on line 85 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L85

Added line #L85 was not covered by tests

match attribute:
case AttrRW():
read_widget = cls._get_read_widget(attribute.datatype)
write_widget = cls._get_write_widget(attribute.datatype)
return SignalRW(name, pv, write_widget, pv + "_RBV", read_widget)
return SignalRW(

Check warning on line 91 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L91

Added line #L91 was not covered by tests
name=name,
pv=pv,
widget=write_widget,
read_pv=pv + "_RBV",
read_widget=read_widget,
)
case AttrR():
read_widget = cls._get_read_widget(attribute.datatype)
return SignalR(name, pv, read_widget)
return SignalR(name=name, pv=pv, widget=read_widget)

Check warning on line 100 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L100

Added line #L100 was not covered by tests
case AttrW():
write_widget = cls._get_write_widget(attribute.datatype)
return SignalW(name, pv, TextWrite())
return SignalW(name=name, pv=pv, widget=TextWrite())

Check warning on line 103 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L103

Added line #L103 was not covered by tests

@classmethod
def _get_command_component(cls, attr_path: str, name: str):
pv = cls._get_pv(attr_path, name)
name = name.title().replace("_", " ")
name = name.title().replace("_", "")

Check warning on line 108 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L108

Added line #L108 was not covered by tests

return SignalX(name, pv, value=1)
return SignalX(name=name, pv=pv, value="1")

Check warning on line 110 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L110

Added line #L110 was not covered by tests

def create_gui(self, options: EpicsGUIOptions | None = None) -> None:
if options is None:
Expand All @@ -113,29 +118,60 @@

assert options.output_path.suffix == options.file_format.value

formatter = deserialize_yaml(Formatter, FORMATTER_YAML)
formatter = DLSFormatter()

Check warning on line 121 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L121

Added line #L121 was not covered by tests

components: Tree[Component] = []
for single_mapping in self._mapping.get_controller_mappings():
attr_path = single_mapping.controller.path

group_name = type(single_mapping.controller).__name__ + " " + attr_path
group_children: list[Component] = []

for attr_name, attribute in single_mapping.attributes.items():
group_children.append(
self._get_attribute_component(
attr_path,
attr_name,
attribute,
)
)
controller_mapping = self._mapping.get_controller_mappings()[0]
sub_controller_mappings = self._mapping.get_controller_mappings()[1:]

Check warning on line 124 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L123-L124

Added lines #L123 - L124 were not covered by tests

for name in single_mapping.command_methods:
group_children.append(self._get_command_component(attr_path, name))
components = self.extract_mapping_components(controller_mapping)

Check warning on line 126 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L126

Added line #L126 was not covered by tests

components.append(Group(group_name, Grid(), group_children))
for sub_controller_mapping in sub_controller_mappings:
components.append(

Check warning on line 129 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L128-L129

Added lines #L128 - L129 were not covered by tests
Group(
name=sub_controller_mapping.controller.path,
layout=SubScreen(),
children=self.extract_mapping_components(sub_controller_mapping),
)
)

device = Device("Simple Device", children=components)
device = Device(label="Simple Device", children=components)

Check warning on line 137 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L137

Added line #L137 was not covered by tests

formatter.format(device, "MY-DEVICE-PREFIX", options.output_path)

def extract_mapping_components(self, mapping: SingleMapping) -> list[Component]:
components: Tree[Component] = []
attr_path = mapping.controller.path

Check warning on line 143 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L142-L143

Added lines #L142 - L143 were not covered by tests

groups: dict[str, list[Component]] = {}
for attr_name, attribute in mapping.attributes.items():
signal = self._get_attribute_component(

Check warning on line 147 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L145-L147

Added lines #L145 - L147 were not covered by tests
attr_path,
attr_name,
attribute,
)

match attribute:
case Attribute(group=group) if group is not None:
if group not in groups:
groups[group] = []

Check warning on line 156 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L153-L156

Added lines #L153 - L156 were not covered by tests

groups[group].append(signal)
case _:
components.append(signal)

Check warning on line 160 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L158-L160

Added lines #L158 - L160 were not covered by tests

for name, command in mapping.command_methods.items():
signal = self._get_command_component(attr_path, name)

Check warning on line 163 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L162-L163

Added lines #L162 - L163 were not covered by tests

match command:
case Command(group=group) if group is not None:
if group not in groups:
groups[group] = []

Check warning on line 168 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L165-L168

Added lines #L165 - L168 were not covered by tests

groups[group].append(signal)
case _:
components.append(signal)

Check warning on line 172 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L170-L172

Added lines #L170 - L172 were not covered by tests

for name, children in groups.items():
components.append(Group(name=name, layout=Grid(), children=children))

Check warning on line 175 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L174-L175

Added lines #L174 - L175 were not covered by tests

return components

Check warning on line 177 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L177

Added line #L177 was not covered by tests
14 changes: 14 additions & 0 deletions src/fastcs/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ def _bind_attrs(self) -> None:


class Controller(BaseController):
"""Top-level controller for a device.

This is the primary class for implementing device support in FastCS. Instances of
this class can be loaded into a backend to access its `Attribute`s. The backend can
then perform a specific function with the set of `Attributes`, such as generating a
UI or creating parameters for a control system.
"""

def __init__(self) -> None:
super().__init__()
self.__sub_controllers: list[SubController] = []
Expand All @@ -38,5 +46,11 @@ def get_sub_controllers(self) -> list[SubController]:


class SubController(BaseController):
"""A subordinate to a `Controller` for managing a subset of a device.

An instance of this class can be registered with a parent `Controller` to include it
as part of a larger device.
"""

def __init__(self, path: str) -> None:
super().__init__(path)
11 changes: 8 additions & 3 deletions src/fastcs/cs_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@


class Method:
def __init__(self, fn: Callable) -> None:
def __init__(self, fn: Callable, *, group: str | None = None) -> None:
self._docstring = getdoc(fn)

sig = signature(fn, eval_str=True)
Expand All @@ -17,6 +17,7 @@
self._validate(fn)

self._fn = fn
self._group = group

Check warning on line 20 in src/fastcs/cs_methods.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/cs_methods.py#L20

Added line #L20 was not covered by tests

def _validate(self, fn: Callable) -> None:
if self.return_type not in (None, Signature.empty):
Expand All @@ -41,6 +42,10 @@
def fn(self):
return self._fn

@property
def group(self):
return self._group

Check warning on line 47 in src/fastcs/cs_methods.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/cs_methods.py#L47

Added line #L47 was not covered by tests


class Scan(Method):
def __init__(self, fn: Callable, period) -> None:
Expand Down Expand Up @@ -71,8 +76,8 @@


class Command(Method):
def __init__(self, fn: Callable) -> None:
super().__init__(fn)
def __init__(self, fn: Callable, *, group: str | None = None) -> None:
super().__init__(fn, group=group)

Check warning on line 80 in src/fastcs/cs_methods.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/cs_methods.py#L80

Added line #L80 was not covered by tests

def _validate(self, fn: Callable) -> None:
super()._validate(fn)
Expand Down
10 changes: 10 additions & 0 deletions src/fastcs/datatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@


class DataType(Generic[T]):
"""Generic datatype mapping to a python type, with additional metadata."""

@property
@abstractmethod
def dtype(self) -> type[T]: # Using property due to lack of Generic ClassVars
Expand All @@ -20,13 +22,17 @@ def dtype(self) -> type[T]: # Using property due to lack of Generic ClassVars

@dataclass(frozen=True)
class Int(DataType[int]):
"""`DataType` mapping to builtin `int`."""

@property
def dtype(self) -> type[int]:
return int


@dataclass(frozen=True)
class Float(DataType[float]):
"""`DataType` mapping to builtin `float`."""

prec: int = 2

@property
Expand All @@ -36,6 +42,8 @@ def dtype(self) -> type[float]:

@dataclass(frozen=True)
class Bool(DataType[bool]):
"""`DataType` mapping to builtin `bool`."""

znam: str = "OFF"
onam: str = "ON"

Expand All @@ -46,6 +54,8 @@ def dtype(self) -> type[bool]:

@dataclass(frozen=True)
class String(DataType[str]):
"""`DataType` mapping to builtin `str`."""

@property
def dtype(self) -> type[str]:
return str
Loading
Loading