Skip to content

Commit afd4180

Browse files
authored
Implement SubControllerVector (#192)
1 parent bced85d commit afd4180

File tree

12 files changed

+408
-315
lines changed

12 files changed

+408
-315
lines changed

src/fastcs/controller.py

Lines changed: 63 additions & 6 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

@@ -17,6 +17,7 @@ class BaseController(Tracer):
1717

1818
#: Attributes passed from the device at runtime.
1919
attributes: dict[str, Attribute]
20+
root_attribute: Attribute | None = None
2021

2122
description: str | None = None
2223

@@ -36,7 +37,7 @@ def __init__(
3637
if not hasattr(self, "attributes"):
3738
self.attributes = {}
3839
self._path: list[str] = path or []
39-
self.__sub_controller_tree: dict[str, Controller] = {}
40+
self.__sub_controller_tree: dict[str, BaseController] = {}
4041

4142
self._bind_attrs()
4243

@@ -144,7 +145,7 @@ def add_attribute(self, name, attribute: Attribute):
144145
self.attributes[name] = attribute
145146
super().__setattr__(name, attribute)
146147

147-
def add_sub_controller(self, name: str, sub_controller: Controller):
148+
def add_sub_controller(self, name: str, sub_controller: BaseController):
148149
if name in self.__sub_controller_tree.keys():
149150
raise ValueError(
150151
f"Cannot add sub controller {sub_controller}. "
@@ -166,7 +167,7 @@ def add_sub_controller(self, name: str, sub_controller: Controller):
166167
self.attributes[name] = sub_controller.root_attribute
167168

168169
@property
169-
def sub_controllers(self) -> dict[str, Controller]:
170+
def sub_controllers(self) -> dict[str, BaseController]:
170171
return self.__sub_controller_tree
171172

172173
def __repr__(self):
@@ -194,17 +195,73 @@ class Controller(BaseController):
194195
such as generating a UI or creating parameters for a control system.
195196
"""
196197

197-
root_attribute: Attribute | None = None
198-
199198
def __init__(
200199
self,
201200
description: str | None = None,
202201
ios: Sequence[AttributeIO[T, AttributeIORefT]] | None = None,
203202
) -> None:
204203
super().__init__(description=description, ios=ios)
205204

205+
def add_sub_controller(self, name: str, sub_controller: BaseController):
206+
if name.isdigit():
207+
raise ValueError(
208+
f"Cannot add sub controller {name}. "
209+
"Numeric-only names are not allowed; use ControllerVector instead"
210+
)
211+
return super().add_sub_controller(name, sub_controller)
212+
206213
async def connect(self) -> None:
207214
pass
208215

209216
async def disconnect(self) -> None:
210217
pass
218+
219+
220+
class ControllerVector(MutableMapping[int, Controller], BaseController):
221+
"""A controller with a collection of identical sub controllers distinguished
222+
by a numeric value"""
223+
224+
def __init__(
225+
self,
226+
children: Mapping[int, Controller],
227+
description: str | None = None,
228+
ios: Sequence[AttributeIO[T, AttributeIORefT]] | None = None,
229+
) -> None:
230+
super().__init__(description=description, ios=ios)
231+
self._children: dict[int, Controller] = {}
232+
for index, child in children.items():
233+
self[index] = child
234+
235+
def add_sub_controller(self, name: str, sub_controller: BaseController):
236+
raise NotImplementedError(
237+
"Cannot add named sub controller to ControllerVector. "
238+
"Use __setitem__ instead, for indexed sub controllers. "
239+
"E.g., vector[1] = Controller()"
240+
)
241+
242+
def __getitem__(self, key: int) -> Controller:
243+
try:
244+
return self._children[key]
245+
except KeyError as exception:
246+
raise KeyError(
247+
f"ControllerVector does not have Controller with key {key}"
248+
) from exception
249+
250+
def __setitem__(self, key: int, value: Controller) -> None:
251+
if not isinstance(key, int):
252+
msg = f"Expected int, got {key}"
253+
raise TypeError(msg)
254+
if not isinstance(value, Controller):
255+
msg = f"Expected Controller, got {value}"
256+
raise TypeError(msg)
257+
self._children[key] = value
258+
super().add_sub_controller(str(key), value)
259+
260+
def __delitem__(self, key: int) -> None:
261+
raise NotImplementedError("Cannot delete sub controller from ControllerVector.")
262+
263+
def __iter__(self) -> Iterator[int]:
264+
yield from self._children
265+
266+
def __len__(self) -> int:
267+
return len(self._children)

src/fastcs/controller_api.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,12 @@ def walk_api(self) -> Iterator["ControllerAPI"]:
3838
yield from api.walk_api()
3939

4040
def __repr__(self):
41-
return f"""\
42-
ControllerAPI(path={self.path}, sub_apis=[{", ".join(self.sub_apis.keys())}])\
43-
"""
41+
return (
42+
f"ControllerAPI("
43+
f"path={self.path}, "
44+
f"sub_apis=[{', '.join(self.sub_apis.keys())}]"
45+
f")"
46+
)
4447

4548
def get_scan_and_initial_coros(
4649
self,

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,14 @@ 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 = (
117+
f"__{child.path[-1]}" # Sub-Controller of ControllerVector
118+
if child.path[-1].isdigit()
119+
else child.path[-1]
120+
)
121+
122+
_add_pvi_info(child_pvi, parent_pvi, child_name.lower())
117123

118-
_add_pvi_info(child_pvi, parent_pvi, child_name)
119124
_add_sub_controller_pvi_info(pv_prefix, child)
120125

121126

src/fastcs/transport/epics/gui.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 name.isdigit():
164+
name = f"{controller_api.path[-1]}{name}"
163165
components.append(
164166
Group(
165167
name=snake_to_pascal(name),

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

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from p4p.server import Server, StaticProvider
44

5-
from fastcs.attributes import Attribute, AttrR, AttrRW, AttrW
5+
from fastcs.attributes import AttrR, AttrRW, AttrW
66
from fastcs.controller_api import ControllerAPI
77
from fastcs.transport.epics.util import controller_pv_prefix
88
from fastcs.util import snake_to_pascal
@@ -12,32 +12,23 @@
1212
make_shared_read_pv,
1313
make_shared_write_pv,
1414
)
15-
from .pvi_tree import AccessModeType, PviTree
16-
17-
18-
def _attribute_to_access(attribute: Attribute) -> AccessModeType:
19-
match attribute:
20-
case AttrRW():
21-
return "rw"
22-
case AttrR():
23-
return "r"
24-
case AttrW():
25-
return "w"
26-
case _:
27-
raise ValueError(f"Unknown attribute type {type(attribute)}")
15+
from .pvi import add_pvi_info
2816

2917

3018
async def parse_attributes(
3119
root_pv_prefix: str, root_controller_api: ControllerAPI
32-
) -> list[StaticProvider]:
20+
) -> StaticProvider:
3321
"""Parses `Attribute` s into p4p signals in handlers."""
34-
pvi_tree = PviTree(root_pv_prefix)
3522
provider = StaticProvider(root_pv_prefix)
3623

3724
for controller_api in root_controller_api.walk_api():
3825
pv_prefix = controller_pv_prefix(root_pv_prefix, controller_api)
39-
40-
pvi_tree.add_sub_device(pv_prefix, controller_api.description)
26+
add_pvi_info(
27+
provider=provider,
28+
pv_prefix=pv_prefix,
29+
controller_api=controller_api,
30+
description=controller_api.description,
31+
)
4132

4233
for attr_name, attribute in controller_api.attributes.items():
4334
full_pv_name = f"{pv_prefix}:{snake_to_pascal(attr_name)}"
@@ -47,23 +38,19 @@ async def parse_attributes(
4738
attribute_pv_rbv = make_shared_read_pv(attribute)
4839
provider.add(f"{full_pv_name}", attribute_pv)
4940
provider.add(f"{full_pv_name}_RBV", attribute_pv_rbv)
50-
pvi_tree.add_signal(f"{full_pv_name}", "rw")
5141
case AttrR():
5242
attribute_pv = make_shared_read_pv(attribute)
5343
provider.add(f"{full_pv_name}", attribute_pv)
54-
pvi_tree.add_signal(f"{full_pv_name}", "r")
5544
case AttrW():
5645
attribute_pv = make_shared_write_pv(attribute)
5746
provider.add(f"{full_pv_name}", attribute_pv)
58-
pvi_tree.add_signal(f"{full_pv_name}", "w")
5947

6048
for attr_name, method in controller_api.command_methods.items():
6149
full_pv_name = f"{pv_prefix}:{snake_to_pascal(attr_name)}"
6250
command_pv = make_command_pv(method.fn)
6351
provider.add(f"{full_pv_name}", command_pv)
64-
pvi_tree.add_signal(f"{full_pv_name}", "x")
6552

66-
return [provider, pvi_tree.make_provider()]
53+
return provider
6754

6855

6956
class P4PIOC:
@@ -74,8 +61,8 @@ def __init__(self, pv_prefix: str, controller_api: ControllerAPI):
7461
self.controller_api = controller_api
7562

7663
async def run(self):
77-
providers = await parse_attributes(self.pv_prefix, self.controller_api)
64+
provider = await parse_attributes(self.pv_prefix, self.controller_api)
7865

7966
endless_event = asyncio.Event()
80-
with Server(providers):
67+
with Server([provider]):
8168
await endless_event.wait()
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
from collections import defaultdict
2+
from typing import Literal
3+
4+
from p4p import Type, Value
5+
from p4p.nt.common import alarm, timeStamp
6+
from p4p.server import StaticProvider
7+
from p4p.server.asyncio import SharedPV
8+
9+
from fastcs.attributes import Attribute, AttrR, AttrRW, AttrW
10+
from fastcs.controller_api import ControllerAPI
11+
from fastcs.util import snake_to_pascal
12+
13+
from .types import p4p_alarm_states, p4p_timestamp_now
14+
15+
AccessModeType = Literal["r", "w", "rw", "d", "x"]
16+
17+
18+
# TODO: This should be removed after https://github.com/DiamondLightSource/FastCS/issues/260
19+
def _attribute_to_access(attribute: Attribute) -> AccessModeType:
20+
match attribute:
21+
case AttrRW():
22+
return "rw"
23+
case AttrR():
24+
return "r"
25+
case AttrW():
26+
return "w"
27+
case _:
28+
raise ValueError(f"Unknown attribute type {type(attribute)}")
29+
30+
31+
def add_pvi_info(
32+
provider: StaticProvider,
33+
pv_prefix: str,
34+
controller_api: ControllerAPI,
35+
description: str | None = None,
36+
) -> None:
37+
"""Add PVI information to given provider."""
38+
provider.add(
39+
f"{pv_prefix}:PVI",
40+
SharedPV(initial=_make_p4p_value(pv_prefix, controller_api, description)),
41+
)
42+
43+
44+
def _make_p4p_value(
45+
pv_prefix: str, controller_api: ControllerAPI, description: str | None
46+
) -> Value:
47+
display = (
48+
{"display": {"description": description}} if description is not None else {}
49+
) # Defined here so the value can be (none)
50+
51+
raw_value = _make_p4p_raw_value(pv_prefix, controller_api)
52+
p4p_type = _make_type_for_raw_value(raw_value)
53+
54+
try:
55+
return Value(
56+
p4p_type,
57+
{
58+
**p4p_alarm_states(),
59+
**p4p_timestamp_now(),
60+
**display,
61+
"value": raw_value,
62+
},
63+
)
64+
except KeyError as e:
65+
raise ValueError(f"Failed to create p4p Value from {raw_value}") from e
66+
67+
68+
def _make_p4p_raw_value(pv_prefix: str, controller_api: ControllerAPI) -> dict:
69+
p4p_raw_value = defaultdict(dict)
70+
# Sub-controller api returned if current item is a Controller
71+
for pv_leaf, sub_controller_api in controller_api.sub_apis.items():
72+
# Add Controller entry
73+
pv = f"{pv_prefix}:{snake_to_pascal(pv_leaf)}:PVI"
74+
if sub_controller_api.path[-1].isdigit():
75+
# Sub-device of a ControllerVector
76+
p4p_raw_value[f"__{int(pv_leaf)}"]["d"] = pv
77+
else:
78+
p4p_raw_value[pv_leaf]["d"] = pv
79+
for pv_leaf, attribute in controller_api.attributes.items():
80+
# Add attribute entry
81+
pv = f"{pv_prefix}:{snake_to_pascal(pv_leaf)}"
82+
p4p_raw_value[pv_leaf][_attribute_to_access(attribute)] = pv
83+
for pv_leaf, _ in controller_api.command_methods.items():
84+
pv = f"{pv_prefix}:{snake_to_pascal(pv_leaf)}"
85+
p4p_raw_value[pv_leaf]["x"] = pv
86+
87+
return p4p_raw_value
88+
89+
90+
def _make_type_for_raw_value(raw_value: dict) -> Type:
91+
p4p_raw_type = []
92+
for pvi_group_name, access_to_field in raw_value.items():
93+
pvi_group_structure = []
94+
for access, field in access_to_field.items():
95+
if isinstance(field, str):
96+
pvi_group_structure.append((access, "s"))
97+
elif isinstance(field, dict):
98+
pvi_group_structure.append(
99+
(
100+
access,
101+
(
102+
"S",
103+
None,
104+
[(v, "s") for v, _ in field.items()],
105+
),
106+
)
107+
)
108+
109+
p4p_raw_type.append((pvi_group_name, ("S", "structure", pvi_group_structure)))
110+
111+
return Type(
112+
[
113+
("alarm", alarm),
114+
("timeStamp", timeStamp),
115+
("display", ("S", None, [("description", "s")])),
116+
("value", ("S", "structure", p4p_raw_type)),
117+
]
118+
)

0 commit comments

Comments
 (0)