From 50bc8dfc9a510656fa0855bd38738c767f398468 Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Fri, 15 Aug 2025 13:36:04 +0000 Subject: [PATCH 1/9] feat: implement initial subcontroller vector processing --- src/fastcs/backend.py | 1 + src/fastcs/controller.py | 59 +++++++++++++++++++++- src/fastcs/controller_api.py | 1 + src/fastcs/transport/epics/pva/ioc.py | 4 +- src/fastcs/transport/epics/pva/pvi_tree.py | 22 +++++--- tests/transport/epics/pva/test_p4p.py | 20 ++++---- 6 files changed, 89 insertions(+), 18 deletions(-) diff --git a/src/fastcs/backend.py b/src/fastcs/backend.py index 8312b410..4e057f5b 100644 --- a/src/fastcs/backend.py +++ b/src/fastcs/backend.py @@ -203,4 +203,5 @@ def _build_controller_api(controller: BaseController, path: list[str]) -> Contro for name, sub_controller in controller.get_sub_controllers().items() }, description=controller.description, + vector_name=controller.get_sub_controller_vector(), ) diff --git a/src/fastcs/controller.py b/src/fastcs/controller.py index c0138000..272d20ed 100755 --- a/src/fastcs/controller.py +++ b/src/fastcs/controller.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +from collections.abc import Iterator, Mapping, MutableMapping from copy import deepcopy from typing import get_type_hints @@ -27,6 +28,7 @@ def __init__( self.attributes = {} self._path: list[str] = path or [] self.__sub_controller_tree: dict[str, SubController] = {} + self.__sub_controller_vector: str | None = None self._bind_attrs() @@ -98,7 +100,15 @@ class method and a controller instance, so that it can be called from any elif isinstance(attr, UnboundPut | UnboundScan | UnboundCommand): setattr(self, attr_name, attr.bind(self)) - def register_sub_controller(self, name: str, sub_controller: SubController): + def register_sub_controller( + self, name: str, sub_controller: SubController | SubControllerVector + ): + if isinstance(sub_controller, SubControllerVector): + for index, child in sub_controller.children(): + child.__sub_controller_vector = name # noqa: SLF001 + self.register_sub_controller(f"{name}{index}", child) + return + if name in self.__sub_controller_tree.keys(): raise ValueError( f"Controller {self} already has a SubController registered as {name}" @@ -119,6 +129,9 @@ def register_sub_controller(self, name: str, sub_controller: SubController): def get_sub_controllers(self) -> dict[str, SubController]: return self.__sub_controller_tree + def get_sub_controller_vector(self) -> str | None: + return self.__sub_controller_vector + class Controller(BaseController): """Top-level controller for a device. @@ -144,6 +157,50 @@ class SubController(BaseController): """ root_attribute: Attribute | None = None + _pvi_group: str | None = None def __init__(self, description: str | None = None) -> None: super().__init__(description=description) + + +class SubControllerVector(MutableMapping[int, SubController]): + """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, SubController], description: str | None = None + ) -> None: + self._children: dict[int, SubController] = {} + self.update(children) + + def __getitem__(self, key: int) -> SubController: + return self._children[key] + + def __setitem__(self, key: int, value: SubController) -> None: + if not isinstance(key, int): + msg = f"Expected int, got {key}" + raise TypeError(msg) + if not isinstance(value, SubController): + msg = f"Expected SubController, 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, SubController]]: + for key, child in self._children.items(): + yield str(key), child + + def __hash__(self): + return hash(id(self)) diff --git a/src/fastcs/controller_api.py b/src/fastcs/controller_api.py index ac5d1af3..c28912d9 100644 --- a/src/fastcs/controller_api.py +++ b/src/fastcs/controller_api.py @@ -18,6 +18,7 @@ class ControllerAPI: sub_apis: dict[str, "ControllerAPI"] = field(default_factory=dict) """APIs of the sub controllers of the `Controller` this API was built from""" description: str | None = None + vector_name: str | None = None def walk_api(self) -> Iterator["ControllerAPI"]: """Walk through all the nested `ControllerAPI` s of this `ControllerAPI`. diff --git a/src/fastcs/transport/epics/pva/ioc.py b/src/fastcs/transport/epics/pva/ioc.py index d17e6083..ba713643 100644 --- a/src/fastcs/transport/epics/pva/ioc.py +++ b/src/fastcs/transport/epics/pva/ioc.py @@ -42,7 +42,9 @@ async def parse_attributes( for controller_api in root_controller_api.walk_api(): pv_prefix = get_pv_name(root_pv_prefix, *controller_api.path) - pvi_tree.add_sub_device(pv_prefix, controller_api.description) + pvi_tree.add_sub_device( + pv_prefix, controller_api.description, controller_api.vector_name + ) for attr_name, attribute in controller_api.attributes.items(): pv_name = get_pv_name(pv_prefix, attr_name) diff --git a/src/fastcs/transport/epics/pva/pvi_tree.py b/src/fastcs/transport/epics/pva/pvi_tree.py index e4f8726f..7b7ce7c7 100644 --- a/src/fastcs/transport/epics/pva/pvi_tree.py +++ b/src/fastcs/transport/epics/pva/pvi_tree.py @@ -42,16 +42,19 @@ class PviDevice(dict[str, "PviDevice"]): pv_prefix: str description: str | None device_signal_info: _PviSignalInfo | None + pvi_group: str | None def __init__( self, pv_prefix: str, description: str | None = None, device_signal_info: _PviSignalInfo | None = None, + pvi_group: str | None = None, ): self.pv_prefix = pv_prefix self.description = description self.device_signal_info = device_signal_info + self.pvi_group = pvi_group def __missing__(self, key: str) -> "PviDevice": new_device = PviDevice(pv_prefix=f"{self.pv_prefix}:{key}") @@ -83,10 +86,13 @@ 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}"] = ( + pvi_group = self[stripped_leaf].pvi_group + + if is_controller and pvi_group is not None and number is not None: + pvi_group = _pascal_to_snake(pvi_group) + if signal_info.access not in p4p_raw_value[pvi_group]: + p4p_raw_value[pvi_group][signal_info.access] = {} + p4p_raw_value[pvi_group][signal_info.access][f"v{number}"] = ( signal_info.pv ) elif is_controller: @@ -179,14 +185,18 @@ def add_sub_device( self, device_pv: str, description: str | None, + pvi_group: str | None, ): if ":" not in device_pv: assert device_pv == self._pvi_tree_root.pv_prefix self._pvi_tree_root.description = description + self._pvi_tree_root.pvi_group = pvi_group else: - self._pvi_tree_root.get_recursively( + pvi_device = self._pvi_tree_root.get_recursively( *device_pv.split(":")[1:] # To remove the prefix - ).description = description + ) + pvi_device.description = description + pvi_device.pvi_group = pvi_group def add_signal( self, diff --git a/tests/transport/epics/pva/test_p4p.py b/tests/transport/epics/pva/test_p4p.py index 12460613..a41f4a8e 100644 --- a/tests/transport/epics/pva/test_p4p.py +++ b/tests/transport/epics/pva/test_p4p.py @@ -14,7 +14,7 @@ from p4p.nt import NTTable from fastcs.attributes import AttrR, AttrRW, AttrW -from fastcs.controller import Controller, SubController +from fastcs.controller import Controller, SubController, SubControllerVector from fastcs.datatypes import Bool, Enum, Float, Int, String, Table, Waveform from fastcs.launch import FastCS from fastcs.transport.epics.options import EpicsIOCOptions @@ -290,15 +290,15 @@ class SomeController(Controller): controller = SomeController() - sub_controller = ChildController() - controller.register_sub_controller("Child0", sub_controller) - sub_controller.register_sub_controller("ChildChild", ChildChildController()) - sub_controller = ChildController() - controller.register_sub_controller("Child1", sub_controller) - sub_controller.register_sub_controller("ChildChild", ChildChildController()) - sub_controller = ChildController() - controller.register_sub_controller("Child2", sub_controller) - sub_controller.register_sub_controller("ChildChild", ChildChildController()) + sub_controller_vector = SubControllerVector( + {i: ChildController() for i in range(3)} + ) + + for _, child in sub_controller_vector.children(): + child.register_sub_controller("ChildChild", ChildChildController()) + + controller.register_sub_controller("Child", sub_controller_vector) + sub_controller = ChildController() controller.register_sub_controller("another_child", sub_controller) sub_controller.register_sub_controller("ChildChild", ChildChildController()) From 489b35b3630e006bd86296de7880345f872de5df Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Fri, 15 Aug 2025 13:41:21 +0000 Subject: [PATCH 2/9] tests: amend test p4p process to use vector --- tests/example_p4p_ioc.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/example_p4p_ioc.py b/tests/example_p4p_ioc.py index 0736f782..138a8a31 100644 --- a/tests/example_p4p_ioc.py +++ b/tests/example_p4p_ioc.py @@ -4,7 +4,7 @@ import numpy as np from fastcs.attributes import AttrHandlerW, AttrR, AttrRW, AttrW -from fastcs.controller import Controller, SubController +from fastcs.controller import Controller, SubController, SubControllerVector from fastcs.datatypes import Bool, Enum, Float, Int, Table, Waveform from fastcs.launch import FastCS from fastcs.transport.epics.options import ( @@ -77,10 +77,13 @@ def run(pv_prefix="P4P_TEST_DEVICE"): p4p_options = EpicsPVAOptions(pva_ioc=EpicsIOCOptions(pv_prefix=pv_prefix)) controller = ParentController() controller.register_sub_controller( - "Child1", ChildController(description="some sub controller") - ) - controller.register_sub_controller( - "Child2", ChildController(description="another sub controller") + "Child", + SubControllerVector( + { + 1: ChildController(description="some sub controller"), + 2: ChildController(description="another sub controller"), + } + ), ) fastcs = FastCS(controller, [p4p_options]) fastcs.run() From 0dea4cdf4e1556b4a4359f4245c0c2ba760f73dc Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Fri, 15 Aug 2025 15:01:45 +0000 Subject: [PATCH 3/9] refactor: remove description from vector --- src/fastcs/controller.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/fastcs/controller.py b/src/fastcs/controller.py index 272d20ed..f9837d11 100755 --- a/src/fastcs/controller.py +++ b/src/fastcs/controller.py @@ -171,9 +171,7 @@ class SubControllerVector(MutableMapping[int, SubController]): a string name of the vector. """ - def __init__( - self, children: Mapping[int, SubController], description: str | None = None - ) -> None: + def __init__(self, children: Mapping[int, SubController]) -> None: self._children: dict[int, SubController] = {} self.update(children) From bd5f50d95e3455be46a822675120eb05cec415eb Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Mon, 18 Aug 2025 09:34:31 +0000 Subject: [PATCH 4/9] chore: remove redundant controller attribute --- src/fastcs/controller.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/fastcs/controller.py b/src/fastcs/controller.py index f9837d11..4677105b 100755 --- a/src/fastcs/controller.py +++ b/src/fastcs/controller.py @@ -157,7 +157,6 @@ class SubController(BaseController): """ root_attribute: Attribute | None = None - _pvi_group: str | None = None def __init__(self, description: str | None = None) -> None: super().__init__(description=description) From 9dcd4ec25822e22fa1326684c803acc5bff8e9e1 Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Wed, 20 Aug 2025 13:35:42 +0000 Subject: [PATCH 5/9] wip: add initial controller vector feature --- src/fastcs/backend.py | 5 ++- src/fastcs/controller.py | 38 +++++++---------- src/fastcs/controller_api.py | 5 +-- src/fastcs/transport/epics/gui.py | 12 +++--- src/fastcs/transport/epics/pva/ioc.py | 8 ++-- src/fastcs/transport/epics/pva/pvi_tree.py | 41 ++++++++++-------- tests/example_p4p_ioc.py | 27 ++++++++---- tests/transport/epics/pva/test_p4p.py | 48 +++++++++++++--------- 8 files changed, 100 insertions(+), 84 deletions(-) diff --git a/src/fastcs/backend.py b/src/fastcs/backend.py index 4e057f5b..46442d47 100644 --- a/src/fastcs/backend.py +++ b/src/fastcs/backend.py @@ -175,7 +175,9 @@ def build_controller_api(controller: Controller) -> ControllerAPI: return _build_controller_api(controller, []) -def _build_controller_api(controller: BaseController, path: list[str]) -> ControllerAPI: +def _build_controller_api( + controller: BaseController, path: list[str | int] +) -> ControllerAPI: """Build a `ControllerAPI` for a `BaseController` and its sub controllers""" scan_methods: dict[str, Scan] = {} put_methods: dict[str, Put] = {} @@ -203,5 +205,4 @@ def _build_controller_api(controller: BaseController, path: list[str]) -> Contro for name, sub_controller in controller.get_sub_controllers().items() }, description=controller.description, - vector_name=controller.get_sub_controller_vector(), ) diff --git a/src/fastcs/controller.py b/src/fastcs/controller.py index 4677105b..5815ad5f 100755 --- a/src/fastcs/controller.py +++ b/src/fastcs/controller.py @@ -17,7 +17,7 @@ class BaseController: description: str | None = None def __init__( - self, path: list[str] | None = None, description: str | None = None + self, path: list[str | int] | None = None, description: str | None = None ) -> None: if ( description is not None @@ -26,9 +26,8 @@ def __init__( if not hasattr(self, "attributes"): self.attributes = {} - self._path: list[str] = path or [] - self.__sub_controller_tree: dict[str, SubController] = {} - self.__sub_controller_vector: str | None = None + self._path: list[str | int] = path or [] + self.__sub_controller_tree: dict[str | int, SubController] = {} self._bind_attrs() @@ -47,11 +46,11 @@ async def attribute_initialise(self) -> None: await controller.attribute_initialise() @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"SubController is already registered under {self.path}") @@ -100,15 +99,7 @@ class method and a controller instance, so that it can be called from any elif isinstance(attr, UnboundPut | UnboundScan | UnboundCommand): setattr(self, attr_name, attr.bind(self)) - def register_sub_controller( - self, name: str, sub_controller: SubController | SubControllerVector - ): - if isinstance(sub_controller, SubControllerVector): - for index, child in sub_controller.children(): - child.__sub_controller_vector = name # noqa: SLF001 - self.register_sub_controller(f"{name}{index}", child) - return - + def register_sub_controller(self, name: str | int, sub_controller: SubController): if name in self.__sub_controller_tree.keys(): raise ValueError( f"Controller {self} already has a SubController registered as {name}" @@ -124,14 +115,11 @@ def register_sub_controller( f"on the parent controller `{type(self).__name__}` " f"as it already has an attribute of that name." ) - self.attributes[name] = sub_controller.root_attribute + self.attributes[str(name)] = sub_controller.root_attribute - def get_sub_controllers(self) -> dict[str, SubController]: + def get_sub_controllers(self) -> dict[str | int, SubController]: return self.__sub_controller_tree - def get_sub_controller_vector(self) -> str | None: - return self.__sub_controller_vector - class Controller(BaseController): """Top-level controller for a device. @@ -162,17 +150,21 @@ def __init__(self, description: str | None = None) -> None: super().__init__(description=description) -class SubControllerVector(MutableMapping[int, SubController]): +class SubControllerVector(MutableMapping[int, SubController], SubController): """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, SubController]) -> None: + def __init__( + self, children: Mapping[int, SubController], description: str | None = None + ) -> None: self._children: dict[int, SubController] = {} self.update(children) + super().__init__(description=description) + for index, child in children.items(): + self.register_sub_controller(index, child) def __getitem__(self, key: int) -> SubController: return self._children[key] diff --git a/src/fastcs/controller_api.py b/src/fastcs/controller_api.py index c28912d9..eb6b8e68 100644 --- a/src/fastcs/controller_api.py +++ b/src/fastcs/controller_api.py @@ -9,16 +9,15 @@ class ControllerAPI: """Attributes, bound methods and sub APIs of a `Controller` / `SubController`""" - 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) put_methods: dict[str, Put] = 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 - vector_name: str | None = None def walk_api(self) -> Iterator["ControllerAPI"]: """Walk through all the nested `ControllerAPI` s of this `ControllerAPI`. diff --git a/src/fastcs/transport/epics/gui.py b/src/fastcs/transport/epics/gui.py index 698102b9..5931ce07 100644 --- a/src/fastcs/transport/epics/gui.py +++ b/src/fastcs/transport/epics/gui.py @@ -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)}" @@ -88,7 +88,7 @@ def _get_write_widget(self, fastcs_datatype: DataType) -> WriteWidgetUnion | Non raise FastCSException(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) @@ -118,7 +118,7 @@ def _get_attribute_component( case _: raise FastCSException(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) @@ -149,6 +149,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), @@ -205,7 +207,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: diff --git a/src/fastcs/transport/epics/pva/ioc.py b/src/fastcs/transport/epics/pva/ioc.py index ba713643..1ae4d5d2 100644 --- a/src/fastcs/transport/epics/pva/ioc.py +++ b/src/fastcs/transport/epics/pva/ioc.py @@ -26,9 +26,9 @@ def _attribute_to_access(attribute: Attribute) -> AccessModeType: raise ValueError(f"Unknown attribute type {type(attribute)}") -def get_pv_name(pv_prefix: str, *attribute_names: str) -> str: +def get_pv_name(pv_prefix: str, *attribute_names: str | int) -> str: """Converts from an attribute name to a pv name.""" - pv_formatted = ":".join([snake_to_pascal(attr) for attr in attribute_names]) + pv_formatted = ":".join([snake_to_pascal(str(attr)) for attr in attribute_names]) return f"{pv_prefix}:{pv_formatted}" if pv_formatted else pv_prefix @@ -42,9 +42,7 @@ async def parse_attributes( for controller_api in root_controller_api.walk_api(): pv_prefix = get_pv_name(root_pv_prefix, *controller_api.path) - pvi_tree.add_sub_device( - pv_prefix, controller_api.description, controller_api.vector_name - ) + pvi_tree.add_sub_device(pv_prefix, controller_api.description) for attr_name, attribute in controller_api.attributes.items(): pv_name = get_pv_name(pv_prefix, attr_name) diff --git a/src/fastcs/transport/epics/pva/pvi_tree.py b/src/fastcs/transport/epics/pva/pvi_tree.py index 7b7ce7c7..26c379a8 100644 --- a/src/fastcs/transport/epics/pva/pvi_tree.py +++ b/src/fastcs/transport/epics/pva/pvi_tree.py @@ -42,19 +42,16 @@ class PviDevice(dict[str, "PviDevice"]): pv_prefix: str description: str | None device_signal_info: _PviSignalInfo | None - pvi_group: str | None def __init__( self, pv_prefix: str, description: str | None = None, device_signal_info: _PviSignalInfo | None = None, - pvi_group: str | None = None, ): self.pv_prefix = pv_prefix self.description = description self.device_signal_info = device_signal_info - self.pvi_group = pvi_group def __missing__(self, key: str) -> "PviDevice": new_device = PviDevice(pv_prefix=f"{self.pv_prefix}:{key}") @@ -86,17 +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) - pvi_group = self[stripped_leaf].pvi_group - - if is_controller and pvi_group is not None and number is not None: - pvi_group = _pascal_to_snake(pvi_group) - if signal_info.access not in p4p_raw_value[pvi_group]: - p4p_raw_value[pvi_group][signal_info.access] = {} - p4p_raw_value[pvi_group][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 @@ -185,18 +194,14 @@ def add_sub_device( self, device_pv: str, description: str | None, - pvi_group: str | None, ): if ":" not in device_pv: assert device_pv == self._pvi_tree_root.pv_prefix self._pvi_tree_root.description = description - self._pvi_tree_root.pvi_group = pvi_group else: - pvi_device = self._pvi_tree_root.get_recursively( + self._pvi_tree_root.get_recursively( *device_pv.split(":")[1:] # To remove the prefix - ) - pvi_device.description = description - pvi_device.pvi_group = pvi_group + ).description = description def add_signal( self, diff --git a/tests/example_p4p_ioc.py b/tests/example_p4p_ioc.py index 138a8a31..a8f74928 100644 --- a/tests/example_p4p_ioc.py +++ b/tests/example_p4p_ioc.py @@ -76,16 +76,27 @@ async def i(self): def run(pv_prefix="P4P_TEST_DEVICE"): p4p_options = EpicsPVAOptions(pva_ioc=EpicsIOCOptions(pv_prefix=pv_prefix)) controller = ParentController() - controller.register_sub_controller( - "Child", - SubControllerVector( - { - 1: ChildController(description="some sub controller"), - 2: ChildController(description="another sub controller"), - } - ), + # controller.register_sub_controller( + # "Child1", ChildController(description="some sub controller") + # ) + # controller.register_sub_controller( + # "Child2", ChildController(description="another sub controller") + # ) + + class Vector(SubControllerVector): + int: AttrR = AttrR(Int()) + + sub_controller = Vector( + { + 1: ChildController(description="some sub controller"), + 2: ChildController(description="another sub controller"), + } ) + + controller.register_sub_controller("Child", sub_controller) + fastcs = FastCS(controller, [p4p_options]) + fastcs.create_gui() fastcs.run() diff --git a/tests/transport/epics/pva/test_p4p.py b/tests/transport/epics/pva/test_p4p.py index a41f4a8e..fb751ae7 100644 --- a/tests/transport/epics/pva/test_p4p.py +++ b/tests/transport/epics/pva/test_p4p.py @@ -313,13 +313,21 @@ class SomeController(Controller): ctxt = ThreadContext("pva") - controller_pvi, child_controller_pvi, child_child_controller_pvi = [], [], [] + ( + controller_pvi, + child_vector_controller_pvi, + child_child_controller_pvi, + child_child_child_controller_pvi, + ) = [], [], [], [] controller_monitor = ctxt.monitor(f"{pv_prefix}:PVI", controller_pvi.append) - child_controller_monitor = ctxt.monitor( - f"{pv_prefix}:Child0:PVI", child_controller_pvi.append + child_vector_controller_monitor = ctxt.monitor( + f"{pv_prefix}:Child:PVI", child_vector_controller_pvi.append ) child_child_controller_monitor = ctxt.monitor( - f"{pv_prefix}:Child0:ChildChild:PVI", child_child_controller_pvi.append + f"{pv_prefix}:Child:0:PVI", child_child_controller_pvi.append + ) + child_child_child_controller_monitor = ctxt.monitor( + f"{pv_prefix}Child:0:ChildChild:PVI", child_child_child_controller_pvi.append ) serve = asyncio.ensure_future(fastcs.serve()) @@ -331,8 +339,9 @@ class SomeController(Controller): ... finally: controller_monitor.close() - child_controller_monitor.close() + child_vector_controller_monitor.close() child_child_controller_monitor.close() + child_child_child_controller_monitor.close() serve.cancel() assert len(controller_pvi) == 1 @@ -351,21 +360,15 @@ class SomeController(Controller): "another_attr1000": {"rw": f"{pv_prefix}:AnotherAttr1000"}, "a_third_attr": {"w": f"{pv_prefix}:AThirdAttr"}, "attr1": {"rw": f"{pv_prefix}:Attr1"}, - "child": { - "d": { - "v0": f"{pv_prefix}:Child0:PVI", - "v1": f"{pv_prefix}:Child1:PVI", - "v2": f"{pv_prefix}:Child2:PVI", - } - }, + "child": {"d": f"{pv_prefix}:Child:PVI"}, "child_attribute_same_name": { "d": f"{pv_prefix}:ChildAttributeSameName:PVI", "r": f"{pv_prefix}:ChildAttributeSameName", }, }, } - assert len(child_controller_pvi) == 1 - assert child_controller_pvi[0].todict() == { + assert len(child_vector_controller_pvi) == 1 + assert child_vector_controller_pvi[0].todict() == { "alarm": {"message": "", "severity": 0, "status": 0}, "display": {"description": ""}, "timeStamp": { @@ -374,11 +377,13 @@ class SomeController(Controller): "userTag": 0, }, "value": { - "attr_c": {"w": f"{pv_prefix}:Child0:AttrC"}, - "attr_d": { - "w": f"{pv_prefix}:Child0:AttrD", + "child": { + "d": { + "v0": f"{pv_prefix}:Child:0:PVI", + "v1": f"{pv_prefix}:Child:1:PVI", + "v2": f"{pv_prefix}:Child:2:PVI", + }, }, - "child_child": {"d": f"{pv_prefix}:Child0:ChildChild:PVI"}, }, } assert len(child_child_controller_pvi) == 1 @@ -391,8 +396,11 @@ class SomeController(Controller): "userTag": 0, }, "value": { - "attr_e": {"rw": f"{pv_prefix}:Child0:ChildChild:AttrE"}, - "attr_f": {"r": f"{pv_prefix}:Child0:ChildChild:AttrF"}, + "attr_c": {"w": f"{pv_prefix}:Child:0:AttrC"}, + "attr_d": { + "w": f"{pv_prefix}:Child:0:AttrD", + }, + "child_child": {"d": f"{pv_prefix}:Child:0:ChildChild:PVI"}, }, } From 4b383db751749b826ee98885aec783646a3ffc69 Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Wed, 20 Aug 2025 13:41:19 +0000 Subject: [PATCH 6/9] wip: cast int to str for nodes in ca path --- src/fastcs/transport/epics/ca/ioc.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/fastcs/transport/epics/ca/ioc.py b/src/fastcs/transport/epics/ca/ioc.py index 9653328e..468e4dde 100644 --- a/src/fastcs/transport/epics/ca/ioc.py +++ b/src/fastcs/transport/epics/ca/ioc.py @@ -104,11 +104,11 @@ def _add_sub_controller_pvi_info(pv_prefix: str, parent: ControllerAPI): parent: Controller to add PVI refs for """ - parent_pvi = ":".join([pv_prefix] + parent.path + ["PVI"]) + parent_pvi = ":".join([pv_prefix] + [str(node) for node in parent.path] + ["PVI"]) for child in parent.sub_apis.values(): - child_pvi = ":".join([pv_prefix] + child.path + ["PVI"]) - child_name = child.path[-1].lower() + child_pvi = ":".join([pv_prefix] + [str(node) for node in child.path] + ["PVI"]) + child_name = str(child.path[-1]).lower() _add_pvi_info(child_pvi, parent_pvi, child_name) @@ -119,7 +119,7 @@ def _create_and_link_attribute_pvs( pv_prefix: str, 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(): pv_name = snake_to_pascal(attr_name) _pv_prefix = ":".join([pv_prefix] + path) @@ -218,7 +218,7 @@ def _create_and_link_command_pvs( pv_prefix: str, 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, method in controller_api.command_methods.items(): pv_name = snake_to_pascal(attr_name) _pv_prefix = ":".join([pv_prefix] + path) From 3c042947c2699260ad423695c06f69a4e39526b1 Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Wed, 20 Aug 2025 13:52:03 +0000 Subject: [PATCH 7/9] wip: cast int to str for tango nodes --- src/fastcs/transport/tango/dsr.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/fastcs/transport/tango/dsr.py b/src/fastcs/transport/tango/dsr.py index 7686ef36..82193d4b 100644 --- a/src/fastcs/transport/tango/dsr.py +++ b/src/fastcs/transport/tango/dsr.py @@ -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("_", "") @@ -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() @@ -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("_", "") From d067000a470f74014390080e58a3b392856e203a Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Wed, 20 Aug 2025 13:57:53 +0000 Subject: [PATCH 8/9] wip: cast int to str for rest nodes --- src/fastcs/transport/rest/rest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fastcs/transport/rest/rest.py b/src/fastcs/transport/rest/rest.py index db31247c..3aa08fe3 100644 --- a/src/fastcs/transport/rest/rest.py +++ b/src/fastcs/transport/rest/rest.py @@ -99,7 +99,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("_", "-") @@ -149,7 +149,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("_", "-") From f67f90eb9ae6d833cad295ece148e2998fcc528e Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Wed, 20 Aug 2025 14:03:43 +0000 Subject: [PATCH 9/9] wip: cast int to str for graphql nodes --- src/fastcs/transport/graphQL/graphQL.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fastcs/transport/graphQL/graphQL.py b/src/fastcs/transport/graphQL/graphQL.py index 69743701..96db742c 100644 --- a/src/fastcs/transport/graphQL/graphQL.py +++ b/src/fastcs/transport/graphQL/graphQL.py @@ -84,7 +84,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(