Skip to content
Draft
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
67 changes: 56 additions & 11 deletions src/fastcs/controller.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

from collections import Counter
from collections.abc import Sequence
from collections.abc import Iterator, Mapping, MutableMapping, Sequence
from copy import deepcopy
from typing import get_type_hints

Expand All @@ -22,7 +22,7 @@ class BaseController(Tracer):

def __init__(
self,
path: list[str] | None = None,
path: list[str | int] | None = None,
description: str | None = None,
ios: Sequence[AttributeIO[T, AttributeIORefT]] | None = None,
) -> None:
Expand All @@ -35,8 +35,8 @@ def __init__(

if not hasattr(self, "attributes"):
self.attributes = {}
self._path: list[str] = path or []
self.__sub_controller_tree: dict[str, Controller] = {}
self._path: list[str | int] = path or []
self.__sub_controller_tree: dict[str | int, BaseController] = {}

self._bind_attrs()

Expand Down Expand Up @@ -71,11 +71,11 @@ def connect_attribute_ios(self) -> None:
controller.connect_attribute_ios()

@property
def path(self) -> list[str]:
def path(self) -> list[str | int]:
"""Path prefix of attributes, recursively including parent Controllers."""
return self._path

def set_path(self, path: list[str]):
def set_path(self, path: list[str | int]):
if self._path:
raise ValueError(f"sub controller is already registered under {self.path}")

Expand Down Expand Up @@ -142,7 +142,7 @@ def add_attribute(self, name, attribute: Attribute):
self.attributes[name] = attribute
super().__setattr__(name, attribute)

def add_sub_controller(self, name: str, sub_controller: Controller):
def add_sub_controller(self, name: str | int, sub_controller: BaseController):
if name in self.__sub_controller_tree.keys():
raise ValueError(
f"Cannot add sub controller {name}. "
Expand All @@ -156,18 +156,18 @@ def add_sub_controller(self, name: str, sub_controller: Controller):

sub_controller.set_path(self.path + [name])
self.__sub_controller_tree[name] = sub_controller
super().__setattr__(name, sub_controller)
super().__setattr__(str(name), sub_controller)

if isinstance(sub_controller.root_attribute, Attribute):
self.attributes[name] = sub_controller.root_attribute
self.attributes[str(name)] = sub_controller.root_attribute

@property
def sub_controllers(self) -> dict[str, Controller]:
def sub_controllers(self) -> dict[str | int, BaseController]:
return self.__sub_controller_tree

def __repr__(self):
name = self.__class__.__name__
path = ".".join(self.path) or None
path = ".".join([str(p) for p in self.path]) or None
sub_controllers = list(self.sub_controllers.keys()) or None

return f"{name}(path={path}, sub_controllers={sub_controllers})"
Expand Down Expand Up @@ -204,3 +204,48 @@ async def connect(self) -> None:

async def disconnect(self) -> None:
pass


class SubControllerVector(MutableMapping[int, Controller], Controller):
"""A collection of SubControllers, with an arbitrary integer index.
An instance of this class can be registered with a parent ``Controller`` to include
it's children as part of a larger controller. Each child of the vector will keep
a string name of the vector.
"""

def __init__(
self, children: Mapping[int, Controller], description: str | None = None
) -> None:
self._children: dict[int, Controller] = {}
self.update(children)
super().__init__(description=description)
for index, child in children.items():
self.add_sub_controller(index, child)

def __getitem__(self, key: int) -> Controller:
return self._children[key]

def __setitem__(self, key: int, value: Controller) -> None:
if not isinstance(key, int):
msg = f"Expected int, got {key}"
raise TypeError(msg)
if not isinstance(value, Controller):
msg = f"Expected Controller, got {value}"
raise TypeError(msg)
self._children[key] = value

def __delitem__(self, key: int) -> None:
del self._children[key]

def __iter__(self) -> Iterator[int]:
yield from self._children

def __len__(self) -> int:
return len(self._children)

def children(self) -> Iterator[tuple[str, Controller]]:
for key, child in self._children.items():
yield str(key), child

def __hash__(self):
return hash(id(self))
4 changes: 2 additions & 2 deletions src/fastcs/controller_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
class ControllerAPI:
"""Attributes, bound methods and sub APIs of a `Controller`"""

path: list[str] = field(default_factory=list)
path: list[str | int] = field(default_factory=list)
"""Path within controller tree (empty if this is the root)"""
attributes: dict[str, Attribute] = field(default_factory=dict)
command_methods: dict[str, Command] = field(default_factory=dict)
scan_methods: dict[str, Scan] = field(default_factory=dict)
sub_apis: dict[str, "ControllerAPI"] = field(default_factory=dict)
sub_apis: dict[str | int, "ControllerAPI"] = field(default_factory=dict)
"""APIs of the sub controllers of the `Controller` this API was built from"""
description: str | None = None

Expand Down
9 changes: 7 additions & 2 deletions src/fastcs/transport/epics/ca/ioc.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,14 @@ def _add_sub_controller_pvi_info(pv_prefix: str, parent: ControllerAPI):

for child in parent.sub_apis.values():
child_pvi = f"{controller_pv_prefix(pv_prefix, child)}:PVI"
child_name = child.path[-1].lower()
child_name = (
f"{child.path[-2]}{child.path[-1]}" # Parent name + child idx
if isinstance(child.path[-1], int)
else str(child.path[-1]) # Child name
)

_add_pvi_info(child_pvi, parent_pvi, child_name.lower())

_add_pvi_info(child_pvi, parent_pvi, child_name)
_add_sub_controller_pvi_info(pv_prefix, child)


Expand Down
3 changes: 3 additions & 0 deletions src/fastcs/transport/epics/ca/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ def _verify_in_datatype(_, value):
return value in datatype.names

arguments["validate"] = _verify_in_datatype
case Bool():
arguments["ZNAM"] = "False"
arguments["ONAM"] = "True"

return arguments

Expand Down
12 changes: 7 additions & 5 deletions src/fastcs/transport/epics/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ def __init__(self, controller_api: ControllerAPI, pv_prefix: str) -> None:
self._controller_api = controller_api
self._pv_prefix = pv_prefix

def _get_pv(self, attr_path: list[str], name: str):
def _get_pv(self, attr_path: list[str | int], name: str):
attr_prefix = ":".join(
[self._pv_prefix] + [snake_to_pascal(node) for node in attr_path]
[self._pv_prefix] + [snake_to_pascal(str(node)) for node in attr_path]
)
return f"{attr_prefix}:{snake_to_pascal(name)}"

Expand Down Expand Up @@ -88,7 +88,7 @@ def _get_write_widget(self, fastcs_datatype: DataType) -> WriteWidgetUnion | Non
raise FastCSError(f"Unsupported type {type(datatype)}: {datatype}")

def _get_attribute_component(
self, attr_path: list[str], name: str, attribute: Attribute
self, attr_path: list[str | int], name: str, attribute: Attribute
) -> SignalR | SignalW | SignalRW | None:
pv = self._get_pv(attr_path, name)
name = snake_to_pascal(name)
Expand Down Expand Up @@ -129,7 +129,7 @@ def _get_attribute_component(
case _:
raise FastCSError(f"Unsupported attribute type: {type(attribute)}")

def _get_command_component(self, attr_path: list[str], name: str):
def _get_command_component(self, attr_path: list[str | int], name: str):
pv = self._get_pv(attr_path, name)
name = snake_to_pascal(name)

Expand Down Expand Up @@ -160,6 +160,8 @@ def extract_api_components(self, controller_api: ControllerAPI) -> Tree:
components: Tree = []

for name, api in controller_api.sub_apis.items():
if isinstance(name, int):
name = f"{controller_api.path[-1]}{name}"
components.append(
Group(
name=snake_to_pascal(name),
Expand Down Expand Up @@ -216,7 +218,7 @@ def extract_api_components(self, controller_api: ControllerAPI) -> Tree:
class PvaEpicsGUI(EpicsGUI):
"""For creating gui in the PVA EPICS transport."""

def _get_pv(self, attr_path: list[str], name: str):
def _get_pv(self, attr_path: list[str | int], name: str):
return f"pva://{super()._get_pv(attr_path, name)}"

def _get_read_widget(self, fastcs_datatype: DataType) -> ReadWidgetUnion | None:
Expand Down
27 changes: 21 additions & 6 deletions src/fastcs/transport/epics/pva/pvi_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,29 @@ def _make_p4p_raw_value(self) -> dict:
stripped_leaf = pv_leaf.rstrip(":PVI")
is_controller = stripped_leaf != pv_leaf
pvi_name, number = _pv_to_pvi_name(stripped_leaf or pv_leaf)
if is_controller and number is not None:
if signal_info.access not in p4p_raw_value[pvi_name]:
p4p_raw_value[pvi_name][signal_info.access] = {}
p4p_raw_value[pvi_name][signal_info.access][f"v{number}"] = (
if is_controller and number is not None and not pvi_name:
pattern = rf"(?:(?<=:)|^)([^:]+)(?=:{re.escape(str(number))}(?:[:]|$))"
match = re.search(pattern, signal_info.pv)

if not match:
raise RuntimeError(
"Failed to extract parent SubControllerVector name "
f"from Subcontroller pv {signal_info.pv}"
)
if (
signal_info.access
not in p4p_raw_value[_pascal_to_snake(match.group(1))]
):
p4p_raw_value[_pascal_to_snake(match.group(1))][
signal_info.access
] = {}
p4p_raw_value[_pascal_to_snake(match.group(1))][signal_info.access][
f"v{number}"
] = signal_info.pv
elif is_controller:
p4p_raw_value[_pascal_to_snake(stripped_leaf)][signal_info.access] = (
signal_info.pv
)
elif is_controller:
p4p_raw_value[pvi_name][signal_info.access] = signal_info.pv
else:
attr_pvi_name = f"{pvi_name}{'' if number is None else number}"
p4p_raw_value[attr_pvi_name][signal_info.access] = signal_info.pv
Expand Down
2 changes: 1 addition & 1 deletion src/fastcs/transport/graphql/graphql.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def _process_commands(self, controller_api: ControllerAPI):
def _process_sub_apis(self, root_controller_api: ControllerAPI):
"""Recursively add fields from the queries and mutations of sub apis"""
for controller_api in root_controller_api.sub_apis.values():
name = "".join(controller_api.path)
name = "".join([str(node) for node in controller_api.path])
child_tree = GraphQLAPI(controller_api)
if child_tree.queries:
self.queries.append(
Expand Down
4 changes: 2 additions & 2 deletions src/fastcs/transport/rest/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ async def attr_get() -> Any: # Must be any as response_model is set

def _add_attribute_api_routes(app: FastAPI, root_controller_api: ControllerAPI) -> None:
for controller_api in root_controller_api.walk_api():
path = controller_api.path
path = [str(node) for node in controller_api.path]

for attr_name, attribute in controller_api.attributes.items():
attr_name = attr_name.replace("_", "-")
Expand Down Expand Up @@ -151,7 +151,7 @@ async def command() -> None:

def _add_command_api_routes(app: FastAPI, root_controller_api: ControllerAPI) -> None:
for controller_api in root_controller_api.walk_api():
path = controller_api.path
path = [str(node) for node in controller_api.path]

for name, method in root_controller_api.command_methods.items():
cmd_name = name.replace("_", "-")
Expand Down
7 changes: 4 additions & 3 deletions src/fastcs/transport/tango/dsr.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def _collect_dev_attributes(
) -> dict[str, Any]:
collection: dict[str, Any] = {}
for controller_api in root_controller_api.walk_api():
path = controller_api.path
path = [str(node) for node in controller_api.path]

for attr_name, attribute in controller_api.attributes.items():
attr_name = attr_name.title().replace("_", "")
Expand Down Expand Up @@ -109,7 +109,8 @@ def _wrap_command_f(
) -> Callable[..., Awaitable[None]]:
async def _dynamic_f(tango_device: Device) -> None:
tango_device.info_stream(
f"called {'_'.join(controller_api.path)} f method: {method_name}"
f"called {'_'.join([str(node) for node in controller_api.path])} "
f"f method: {method_name}"
)

coro = method()
Expand All @@ -125,7 +126,7 @@ def _collect_dev_commands(
) -> dict[str, Any]:
collection: dict[str, Any] = {}
for controller_api in root_controller_api.walk_api():
path = controller_api.path
path = [str(node) for node in controller_api.path]

for name, method in controller_api.command_methods.items():
cmd_name = name.title().replace("_", "")
Expand Down
Loading
Loading