Skip to content

Commit

Permalink
Implement grouping of Attributes on generated UIs
Browse files Browse the repository at this point in the history
Attributes and Methods take an optional group to be placed in. If group is unset it will appear at the root of the UI.
SubControllers are displayed on a SubScreen of the parent Controller.
  • Loading branch information
GDYendell committed Jan 18, 2024
1 parent f498de7 commit 0d81daf
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 36 deletions.
21 changes: 16 additions & 5 deletions src/fastcs/attributes.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

from enum import Enum
from typing import Any, Generic, Protocol, runtime_checkable
from typing import Any, Generic, Optional, Protocol, runtime_checkable

from .datatypes import ATTRIBUTE_TYPES, AttrCallback, DataType, T

Expand Down Expand Up @@ -51,13 +51,17 @@ class Attribute(Generic[T]):
"""

def __init__(
self, datatype: DataType[T], access_mode: AttrMode, handler: Any = None
self,
datatype: DataType[T],
access_mode: AttrMode,
group: Optional[str] = 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

@property
def datatype(self) -> DataType[T]:
Expand All @@ -71,6 +75,10 @@ def dtype(self) -> type[T]:
def access_mode(self) -> AttrMode:
return self._access_mode

@property
def group(self) -> Optional[str]:
return self._group


class AttrR(Attribute[T]):
"""A read-only `Attribute`."""
Expand All @@ -79,9 +87,10 @@ def __init__(
self,
datatype: DataType[T],
access_mode=AttrMode.READ,
group: Optional[str] = None,
handler: Updater | None = None,
) -> None:
super().__init__(datatype, access_mode, handler) # type: ignore
super().__init__(datatype, access_mode, group) # type: ignore
self._value: T = datatype.dtype()
self._update_callback: AttrCallback[T] | None = None
self._updater = handler
Expand Down Expand Up @@ -110,9 +119,10 @@ def __init__(
self,
datatype: DataType[T],
access_mode=AttrMode.WRITE,
group: Optional[str] = None,
handler: Sender | None = None,
) -> None:
super().__init__(datatype, access_mode, handler) # type: ignore
super().__init__(datatype, access_mode, group, handler) # type: ignore
self._process_callback: AttrCallback[T] | None = None
self._write_display_callback: AttrCallback[T] | None = None
self._sender = handler
Expand Down Expand Up @@ -148,9 +158,10 @@ def __init__(
self,
datatype: DataType[T],
access_mode=AttrMode.READ_WRITE,
group: Optional[str] = None,
handler: Handler | None = None,
) -> None:
super().__init__(datatype, access_mode, handler) # type: ignore
super().__init__(datatype, access_mode, group, handler) # type: ignore

async def process(self, value: T) -> None:
await self.set(value)
Expand Down
77 changes: 55 additions & 22 deletions src/fastcs/backends/epics/gui.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import Callable

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 +16,7 @@
SignalRW,
SignalW,
SignalX,
SubScreen,
TextFormat,
TextRead,
TextWrite,
Expand All @@ -24,6 +25,7 @@
)

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
Expand Down Expand Up @@ -113,29 +115,60 @@ def create_gui(self, options: EpicsGUIOptions | None = None) -> None:

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

formatter = deserialize_yaml(Formatter, FORMATTER_YAML)
formatter = DLSFormatter(label_width=150, widget_width=200)

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:]

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

components.append(Group(group_name, Grid(), group_children))
for sub_controller_mapping in sub_controller_mappings:
components.append(
Group(
sub_controller_mapping.controller.path,
SubScreen(),
self.extract_mapping_components(sub_controller_mapping),
)
)

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

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

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

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

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

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

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

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

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

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

return components
2 changes: 1 addition & 1 deletion src/fastcs/backends/epics/ioc.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def run(self, options: EpicsIOCOptions | None = None) -> None:
backend = Backend(self._mapping, dispatcher.loop)

# Set the record prefix
builder.SetDeviceName("MY-DEVICE-PREFIX")
builder.SetDeviceName("GARY")

_create_and_link_attribute_pvs(self._mapping)

Expand Down
13 changes: 9 additions & 4 deletions src/fastcs/cs_methods.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from asyncio import iscoroutinefunction
from inspect import Signature, getdoc, signature
from typing import Awaitable, Callable
from typing import Awaitable, Callable, Optional

from .exceptions import FastCSException

ScanCallback = Callable[..., Awaitable[None]]


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

sig = signature(fn, eval_str=True)
Expand All @@ -17,6 +17,7 @@ def __init__(self, fn: Callable) -> None:
self._validate(fn)

self._fn = fn
self._group = group

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

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


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


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

def _validate(self, fn: Callable) -> None:
super()._validate(fn)
Expand Down
18 changes: 14 additions & 4 deletions src/fastcs/wrappers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Protocol, runtime_checkable
from typing import Any, Optional, Protocol, runtime_checkable

from .cs_methods import Command, Method, Put, Scan
from .exceptions import FastCSException
Expand Down Expand Up @@ -26,6 +26,16 @@ def put(fn) -> Any:
return fn


def command(fn) -> Any:
fn.fastcs_method = Command(fn)
return fn
def command(*, group: Optional[str] = None) -> Any:
"""Decorator to turn a Controller method into a Command.
Args:
group: Group to display the widget for this command in on the UI
"""

def wrapper(fn):
fn.fastcs_method = Command(fn, group=group)
return fn

return wrapper

0 comments on commit 0d81daf

Please sign in to comment.