Skip to content

Commit 7f0d4b7

Browse files
shihab-dlsGDYendell
authored andcommitted
feat: implement initial subcontroller vector
1 parent b0ff1f8 commit 7f0d4b7

File tree

10 files changed

+178
-67
lines changed

10 files changed

+178
-67
lines changed

src/fastcs/controller.py

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
from collections import Counter
4-
from collections.abc import Sequence
4+
from collections.abc import Iterator, Mapping, MutableMapping, Sequence
55
from copy import deepcopy
66
from typing import get_type_hints
77

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

2323
def __init__(
2424
self,
25-
path: list[str] | None = None,
25+
path: list[str | int] | None = None,
2626
description: str | None = None,
2727
ios: Sequence[AttributeIO[T, AttributeIORefT]] | None = None,
2828
) -> None:
@@ -35,8 +35,8 @@ def __init__(
3535

3636
if not hasattr(self, "attributes"):
3737
self.attributes = {}
38-
self._path: list[str] = path or []
39-
self.__sub_controller_tree: dict[str, Controller] = {}
38+
self._path: list[str | int] = path or []
39+
self.__sub_controller_tree: dict[str | int, BaseController] = {}
4040

4141
self._bind_attrs()
4242

@@ -71,11 +71,11 @@ def connect_attribute_ios(self) -> None:
7171
controller.connect_attribute_ios()
7272

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

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

@@ -142,7 +142,7 @@ def add_attribute(self, name, attribute: Attribute):
142142
self.attributes[name] = attribute
143143
super().__setattr__(name, attribute)
144144

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

157157
sub_controller.set_path(self.path + [name])
158158
self.__sub_controller_tree[name] = sub_controller
159-
super().__setattr__(name, sub_controller)
159+
super().__setattr__(str(name), sub_controller)
160160

161161
if isinstance(sub_controller.root_attribute, Attribute):
162-
self.attributes[name] = sub_controller.root_attribute
162+
self.attributes[str(name)] = sub_controller.root_attribute
163163

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

168168
def __repr__(self):
169169
name = self.__class__.__name__
170-
path = ".".join(self.path) or None
170+
path = ".".join([str(p) for p in self.path]) or None
171171
sub_controllers = list(self.sub_controllers.keys()) or None
172172

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

205205
async def disconnect(self) -> None:
206206
pass
207+
208+
209+
class SubControllerVector(MutableMapping[int, Controller], Controller):
210+
"""A collection of SubControllers, with an arbitrary integer index.
211+
An instance of this class can be registered with a parent ``Controller`` to include
212+
it's children as part of a larger controller. Each child of the vector will keep
213+
a string name of the vector.
214+
"""
215+
216+
def __init__(
217+
self, children: Mapping[int, Controller], description: str | None = None
218+
) -> None:
219+
self._children: dict[int, Controller] = {}
220+
self.update(children)
221+
super().__init__(description=description)
222+
for index, child in children.items():
223+
self.add_sub_controller(index, child)
224+
225+
def __getitem__(self, key: int) -> Controller:
226+
return self._children[key]
227+
228+
def __setitem__(self, key: int, value: Controller) -> None:
229+
if not isinstance(key, int):
230+
msg = f"Expected int, got {key}"
231+
raise TypeError(msg)
232+
if not isinstance(value, Controller):
233+
msg = f"Expected Controller, got {value}"
234+
raise TypeError(msg)
235+
self._children[key] = value
236+
237+
def __delitem__(self, key: int) -> None:
238+
del self._children[key]
239+
240+
def __iter__(self) -> Iterator[int]:
241+
yield from self._children
242+
243+
def __len__(self) -> int:
244+
return len(self._children)
245+
246+
def children(self) -> Iterator[tuple[str, Controller]]:
247+
for key, child in self._children.items():
248+
yield str(key), child
249+
250+
def __hash__(self):
251+
return hash(id(self))

src/fastcs/controller_api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@
1717
class ControllerAPI:
1818
"""Attributes, bound methods and sub APIs of a `Controller`"""
1919

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

src/fastcs/transport/epics/ca/ioc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def _add_sub_controller_pvi_info(pv_prefix: str, parent: ControllerAPI):
113113

114114
for child in parent.sub_apis.values():
115115
child_pvi = f"{controller_pv_prefix(pv_prefix, child)}:PVI"
116-
child_name = child.path[-1].lower()
116+
child_name = str(child.path[-1]).lower()
117117

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

src/fastcs/transport/epics/gui.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ def __init__(self, controller_api: ControllerAPI, pv_prefix: str) -> None:
5151
self._controller_api = controller_api
5252
self._pv_prefix = pv_prefix
5353

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

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

9090
def _get_attribute_component(
91-
self, attr_path: list[str], name: str, attribute: Attribute
91+
self, attr_path: list[str | int], name: str, attribute: Attribute
9292
) -> SignalR | SignalW | SignalRW | None:
9393
pv = self._get_pv(attr_path, name)
9494
name = snake_to_pascal(name)
@@ -129,7 +129,7 @@ def _get_attribute_component(
129129
case _:
130130
raise FastCSError(f"Unsupported attribute type: {type(attribute)}")
131131

132-
def _get_command_component(self, attr_path: list[str], name: str):
132+
def _get_command_component(self, attr_path: list[str | int], name: str):
133133
pv = self._get_pv(attr_path, name)
134134
name = snake_to_pascal(name)
135135

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

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

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

222224
def _get_read_widget(self, fastcs_datatype: DataType) -> ReadWidgetUnion | None:

src/fastcs/transport/epics/pva/pvi_tree.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,29 @@ def _make_p4p_raw_value(self) -> dict:
8383
stripped_leaf = pv_leaf.rstrip(":PVI")
8484
is_controller = stripped_leaf != pv_leaf
8585
pvi_name, number = _pv_to_pvi_name(stripped_leaf or pv_leaf)
86-
if is_controller and number is not None:
87-
if signal_info.access not in p4p_raw_value[pvi_name]:
88-
p4p_raw_value[pvi_name][signal_info.access] = {}
89-
p4p_raw_value[pvi_name][signal_info.access][f"v{number}"] = (
86+
if is_controller and number is not None and not pvi_name:
87+
pattern = rf"(?:(?<=:)|^)([^:]+)(?=:{re.escape(str(number))}(?:[:]|$))"
88+
match = re.search(pattern, signal_info.pv)
89+
90+
if not match:
91+
raise RuntimeError(
92+
"Failed to extract parent SubControllerVector name "
93+
f"from Subcontroller pv {signal_info.pv}"
94+
)
95+
if (
96+
signal_info.access
97+
not in p4p_raw_value[_pascal_to_snake(match.group(1))]
98+
):
99+
p4p_raw_value[_pascal_to_snake(match.group(1))][
100+
signal_info.access
101+
] = {}
102+
p4p_raw_value[_pascal_to_snake(match.group(1))][signal_info.access][
103+
f"v{number}"
104+
] = signal_info.pv
105+
elif is_controller:
106+
p4p_raw_value[_pascal_to_snake(stripped_leaf)][signal_info.access] = (
90107
signal_info.pv
91108
)
92-
elif is_controller:
93-
p4p_raw_value[pvi_name][signal_info.access] = signal_info.pv
94109
else:
95110
attr_pvi_name = f"{pvi_name}{'' if number is None else number}"
96111
p4p_raw_value[attr_pvi_name][signal_info.access] = signal_info.pv

src/fastcs/transport/graphql/graphql.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def _process_commands(self, controller_api: ControllerAPI):
8686
def _process_sub_apis(self, root_controller_api: ControllerAPI):
8787
"""Recursively add fields from the queries and mutations of sub apis"""
8888
for controller_api in root_controller_api.sub_apis.values():
89-
name = "".join(controller_api.path)
89+
name = "".join([str(node) for node in controller_api.path])
9090
child_tree = GraphQLAPI(controller_api)
9191
if child_tree.queries:
9292
self.queries.append(

src/fastcs/transport/rest/rest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ async def attr_get() -> Any: # Must be any as response_model is set
101101

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

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

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

156156
for name, method in root_controller_api.command_methods.items():
157157
cmd_name = name.replace("_", "-")

src/fastcs/transport/tango/dsr.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def _collect_dev_attributes(
6161
) -> dict[str, Any]:
6262
collection: dict[str, Any] = {}
6363
for controller_api in root_controller_api.walk_api():
64-
path = controller_api.path
64+
path = [str(node) for node in controller_api.path]
6565

6666
for attr_name, attribute in controller_api.attributes.items():
6767
attr_name = attr_name.title().replace("_", "")
@@ -109,7 +109,8 @@ def _wrap_command_f(
109109
) -> Callable[..., Awaitable[None]]:
110110
async def _dynamic_f(tango_device: Device) -> None:
111111
tango_device.info_stream(
112-
f"called {'_'.join(controller_api.path)} f method: {method_name}"
112+
f"called {'_'.join([str(node) for node in controller_api.path])} "
113+
f"f method: {method_name}"
113114
)
114115

115116
coro = method()
@@ -125,7 +126,7 @@ def _collect_dev_commands(
125126
) -> dict[str, Any]:
126127
collection: dict[str, Any] = {}
127128
for controller_api in root_controller_api.walk_api():
128-
path = controller_api.path
129+
path = [str(node) for node in controller_api.path]
129130

130131
for name, method in controller_api.command_methods.items():
131132
cmd_name = name.title().replace("_", "")

tests/example_p4p_ioc.py

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,22 @@
33

44
import numpy as np
55

6-
from fastcs.attributes import AttrR, AttrRW, AttrW
7-
from fastcs.controller import Controller
6+
from fastcs.attributes import AttrHandlerW, AttrR, AttrRW, AttrW
7+
from fastcs.controller import Controller, SubController, SubControllerVector
88
from fastcs.datatypes import Bool, Enum, Float, Int, Table, Waveform
99
from fastcs.launch import FastCS
1010
from fastcs.transport.epics.options import (
1111
EpicsIOCOptions,
1212
)
13-
from fastcs.transport.epics.pva.transport import EpicsPVATransport
13+
from fastcs.transport.epics.pva.options import EpicsPVAOptions
1414
from fastcs.wrappers import command, scan
1515

1616

17+
class SimpleAttributeSetter(AttrHandlerW):
18+
async def put(self, attr, value):
19+
await attr.update_display_without_process(value)
20+
21+
1722
class FEnum(enum.Enum):
1823
A = 0
1924
B = 1
@@ -25,29 +30,29 @@ class FEnum(enum.Enum):
2530
class ParentController(Controller):
2631
description = "some controller"
2732
a: AttrRW = AttrRW(Int(max=400_000, max_alarm=40_000))
28-
b: AttrW = AttrW(Float(min=-1, min_alarm=-0.5))
33+
b: AttrW = AttrW(Float(min=-1, min_alarm=-0.5), handler=SimpleAttributeSetter())
2934

3035
table: AttrRW = AttrRW(
3136
Table([("A", np.int32), ("B", "i"), ("C", "?"), ("D", np.float64)])
3237
)
3338

3439

35-
class ChildController(Controller):
40+
class ChildController(SubController):
3641
fail_on_next_e = True
37-
c: AttrW = AttrW(Int())
42+
c: AttrW = AttrW(Int(), handler=SimpleAttributeSetter())
3843

3944
@command()
4045
async def d(self):
4146
print("D: RUNNING")
4247
await asyncio.sleep(0.1)
4348
print("D: FINISHED")
44-
await self.j.update(self.j.get() + 1)
49+
await self.j.set(self.j.get() + 1)
4550

4651
e: AttrR = AttrR(Bool())
4752

4853
@scan(1)
4954
async def flip_flop(self):
50-
await self.e.update(not self.e.get())
55+
await self.e.set(not self.e.get())
5156

5257
f: AttrRW = AttrRW(Enum(FEnum))
5358
g: AttrRW = AttrRW(Waveform(np.int64, shape=(3,)))
@@ -63,21 +68,36 @@ async def i(self):
6368
else:
6469
self.fail_on_next_e = True
6570
print("I: FINISHED")
66-
await self.j.update(self.j.get() + 1)
71+
await self.j.set(self.j.get() + 1)
6772

6873
j: AttrR = AttrR(Int())
6974

7075

7176
def run(pv_prefix="P4P_TEST_DEVICE"):
77+
p4p_options = EpicsPVAOptions(pva_ioc=EpicsIOCOptions(pv_prefix=pv_prefix))
7278
controller = ParentController()
73-
controller.a.enable_tracing()
74-
controller.child1 = ChildController(description="some sub controller")
75-
controller.child2 = ChildController(description="another sub controller")
76-
77-
fastcs = FastCS(
78-
controller, [EpicsPVATransport(pva_ioc=EpicsIOCOptions(pv_prefix=pv_prefix))]
79+
# controller.register_sub_controller(
80+
# "Child1", ChildController(description="some sub controller")
81+
# )
82+
# controller.register_sub_controller(
83+
# "Child2", ChildController(description="another sub controller")
84+
# )
85+
86+
class Vector(SubControllerVector):
87+
int: AttrR = AttrR(Int())
88+
89+
sub_controller = Vector(
90+
{
91+
1: ChildController(description="some sub controller"),
92+
2: ChildController(description="another sub controller"),
93+
}
7994
)
80-
fastcs.run(interactive=False)
95+
96+
controller.register_sub_controller("Child", sub_controller)
97+
98+
fastcs = FastCS(controller, [p4p_options])
99+
fastcs.create_gui()
100+
fastcs.run()
81101

82102

83103
if __name__ == "__main__":

0 commit comments

Comments
 (0)