Skip to content

Commit

Permalink
wip: merged blocks and fields into a single recursive controller
Browse files Browse the repository at this point in the history
  • Loading branch information
evalott100 committed Oct 31, 2024
1 parent 139b705 commit 9a9552e
Show file tree
Hide file tree
Showing 9 changed files with 93 additions and 281 deletions.
5 changes: 2 additions & 3 deletions src/fastcs_pandablocks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@
from ._version import __version__
from .gui import PandaGUIOptions
from .panda.controller import PandaController
from .types import EpicsName

DEFAULT_POLL_PERIOD = 0.1


def ioc(
epics_prefix: EpicsName,
epics_prefix: str,
hostname: str,
screens_directory: Path | None = None,
clear_bobfiles: bool = False,
Expand All @@ -31,7 +30,7 @@ def ioc(

controller = PandaController(hostname, poll_period)
backend = EpicsBackend(
controller, pv_prefix=str(epics_prefix), ioc_options=epics_ioc_options
controller, pv_prefix=epics_prefix, ioc_options=epics_ioc_options
)

if clear_bobfiles and not screens_directory:
Expand Down
3 changes: 1 addition & 2 deletions src/fastcs_pandablocks/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from fastcs.backends.epics.util import PvNamingConvention

from fastcs_pandablocks import DEFAULT_POLL_PERIOD, ioc
from fastcs_pandablocks.types import EpicsName

from . import __version__

Expand Down Expand Up @@ -83,7 +82,7 @@ def main():
logging.basicConfig(format="%(levelname)s:%(message)s", level=level)

ioc(
EpicsName(prefix=parsed_args.prefix),
parsed_args.prefix,
parsed_args.hostname,
screens_directory=Path(parsed_args.screens_dir),
clear_bobfiles=parsed_args.clear_bobfiles,
Expand Down
92 changes: 15 additions & 77 deletions src/fastcs_pandablocks/panda/blocks.py
Original file line number Diff line number Diff line change
@@ -1,86 +1,21 @@
from collections.abc import Generator

from fastcs.attributes import Attribute, AttrR, AttrRW, AttrW
from fastcs.controller import SubController

from fastcs_pandablocks.types import (
EpicsName,
PandaName,
RawBlocksType,
RawFieldsType,
RawInitialValuesType,
)
from fastcs_pandablocks.types.annotations import ResponseType

from .fields import (
FieldControllerType,
get_field_controller_from_field_info,
FieldController,
)


def _def_pop_up_to_block_or_field(name: PandaName, dictionary: RawInitialValuesType):
extracted_members = {}
resolution_method = (
PandaName.up_to_block if name.field is None else PandaName.up_to_field
)

# So the dictionary can be changed during iteration.
for sub_name in list(dictionary):
if resolution_method(sub_name) == name:
extracted_members[sub_name] = dictionary.pop(sub_name)
return extracted_members


class BlockController(SubController):
fields: dict[PandaName, FieldControllerType]

def __init__(
self,
panda_name: PandaName,
description: str | None | None,
field_infos: dict[PandaName, ResponseType],
initial_values: RawInitialValuesType,
label: str | None,
):
self.panda_name = panda_name
self.description = description
self.label = label

self._additional_attributes: dict[str, Attribute] = {}
self.fields: dict[PandaName, FieldControllerType] = {}

for field_name, field_info in field_infos.items():
field_name = panda_name + field_name
field_initial_values = _def_pop_up_to_block_or_field(
field_name, initial_values
)
self.fields[field_name] = get_field_controller_from_field_info(
field_name, field_info, field_initial_values, label
)

super().__init__()

def initialise(self):
for field_name, field in self.fields.items():
if field.additional_attributes:
self.register_sub_controller(
field_name.attribute_name, sub_controller=field
)
field.initialise() # Registers `field.sub_contollers`.
if field.top_level_attribute:
assert field_name.field is not None
self._additional_attributes[field_name.field] = (
field.top_level_attribute
)

@property
def additional_attributes(self) -> dict[str, Attribute]:
return self._additional_attributes


class Blocks:
_blocks: dict[PandaName, BlockController]
epics_prefix: EpicsName
_blocks: dict[PandaName, FieldController]

def __init__(self):
self._blocks = {}
Expand All @@ -107,17 +42,19 @@ def parse_introspected_data(
)

for numbered_block_name in numbered_block_names:
block_initial_values = _def_pop_up_to_block_or_field(
numbered_block_name, initial_values
)
block_initial_values = {
key: value
for key, value in initial_values.items()
if key in numbered_block_name
}
label = labels.get(numbered_block_name, None)

self._blocks[numbered_block_name] = BlockController(
self._blocks[numbered_block_name] = FieldController(
numbered_block_name,
block_info.description,
field_info,
block_initial_values,
label,
field_infos=field_info,
label=label,
)

async def update_field_value(self, panda_name: PandaName, value: str):
Expand All @@ -132,15 +69,16 @@ async def update_field_value(self, panda_name: PandaName, value: str):

def flattened_attribute_tree(
self,
) -> Generator[tuple[str, BlockController], None, None]:
) -> Generator[tuple[str, FieldController], None, None]:
for block in self._blocks.values():
yield (block.panda_name.attribute_name, block)

def __getitem__(self, name: PandaName) -> BlockController | Attribute | None:
def __getitem__(self, name: PandaName) -> FieldController | Attribute:
block = self._blocks[name.up_to_block()]
if name.field is None:
return block
field = block.fields[name.up_to_field()]
field = block.sub_fields[name.up_to_field()]
if name.sub_field is None:
assert field.top_level_attribute
return field.top_level_attribute
return field.additional_attributes[name.sub_field]
return field.additional_attributes[name.attribute_name]
2 changes: 1 addition & 1 deletion src/fastcs_pandablocks/panda/client_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
RawFieldsType,
RawInitialValuesType,
)
from fastcs_pandablocks.types.string_types import PandaName
from fastcs_pandablocks.types import PandaName


class RawPanda:
Expand Down
86 changes: 53 additions & 33 deletions src/fastcs_pandablocks/panda/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,61 +29,81 @@
DefaultFieldUpdater,
EguSender,
)
from fastcs_pandablocks.types.annotations import RawInitialValuesType, ResponseType
from fastcs_pandablocks.types.string_types import PandaName


class WidgetGroup(Enum):
"""Purposely not an enum since we only ever want the string."""

NONE = None
PARAMETERS = "Parameters"
OUTPUTS = "Outputs"
INPUTS = "Inputs"
READBACKS = "Readbacks"
CAPTURE = "Capture"
from fastcs_pandablocks.types import (
RawInitialValuesType,
ResponseType,
PandaName,
WidgetGroup,
)


# EPICS hardcoded. TODO: remove once we switch to pvxs.
MAXIMUM_DESCRIPTION_LENGTH = 40


def _strip_description(description: str | None) -> str:
def _strip_description(description: str | None) -> str | None:
if description is None:
return ""
return description
return description[:MAXIMUM_DESCRIPTION_LENGTH]


class FieldController(SubController):
def __init__(self):
"""
Since fields contain an attribute for the field itself
`PREFIX:BLOCK:FIELD`, but also subfields, `PREFIX:BLOCK:FIELD:SUB_FIELD`,
have a top level attribute set in the `BlockController`, and
further attributes which are used in the field as a `SubController`.
"""
def __init__(
self,
panda_name: PandaName,
description: str | None | None,
initial_values: RawInitialValuesType,
label: str | None = None,
):
self.panda_name = panda_name
self.description = description
self._initial_values = initial_values
self.label = label

self.top_level_attribute: Attribute | None = None
self._additional_attributes = {}
self.sub_controllers: dict[str, FieldController] = {}
self._additional_attributes: dict[str, Attribute] = {}
self.sub_fields: dict[PandaName, FieldController] = {}

def make_sub_fields_from_field_infos(self, field_infos: RawInitialValuesType):
for sub_field_name, sub_field_info in field_infos.items():
full_sub_field_name = self.panda_name + sub_field_name
field_initial_values = {
key: value
for key, value in self._initial_values.items()
if key in sub_field_name
}
self.sub_fields[full_sub_field_name] = get_field_controller_from_field_info(
full_sub_field_name, sub_field_info, field_initial_values, label
)

super().__init__(search_device_for_attributes=False)

def initialise(self):
for field_name, field in self.sub_fields.items():
self.register_sub_controller(
field_name.attribute_name, sub_controller=field
)
field.initialise()
if field.top_level_attribute:
self._additional_attributes[field_name.attribute_name] = (
field.top_level_attribute
)

@property
def additional_attributes(self) -> dict[str, Attribute]:
"""
Used by the FastCS mapping parser to get attributes since
we're not searching for device attributes.
"""
return self._additional_attributes

def initialise(self):
for sub_field_name, sub_field_controller in self.sub_controllers.items():
self.register_sub_controller(sub_field_name, sub_field_controller)
sub_field_controller.initialise()
self._additional_attributes[sub_field_name] = (
sub_field_controller.top_level_attribute
)


class TableFieldController(FieldController):
def __init__(self, panda_name: PandaName, field_info: TableFieldInfo):
super().__init__()
super().__init__(
panda_name,
field_info.description,
)

self.top_level_attribute = AttrR(
Float(),
Expand Down
18 changes: 14 additions & 4 deletions src/fastcs_pandablocks/types/__init__.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
from .annotations import (
from ._annotations import (
RawBlocksType,
RawFieldsType,
RawInitialValuesType,
ResponseType,
)
from .string_types import (
from ._string_types import (
EPICS_SEPARATOR,
PANDA_SEPARATOR,
EpicsName,
PandaName,
)
from enum import Enum


class WidgetGroup(Enum):
NONE = None
PARAMETERS = "Parameters"
OUTPUTS = "Outputs"
INPUTS = "Inputs"
READBACKS = "Readbacks"
CAPTURE = "Capture"


__all__ = [
"EPICS_SEPARATOR",
"EpicsName",
"PANDA_SEPARATOR",
"PandaName",
"ResponseType",
"RawBlocksType",
"RawFieldsType",
"RawInitialValuesType",
"WidgetGroup",
]
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
UintFieldInfo,
)

from .string_types import PandaName
from ._string_types import PandaName

# Pyright gives us variable not allowed in type expression error
# if we try to use the new (|) syntax
Expand Down
Loading

0 comments on commit 9a9552e

Please sign in to comment.