Skip to content

Commit e46b028

Browse files
committed
Add info tags to define PVI structure from controllers
Refactor helper functions to preserve PV prefix until later so that PVI PV can use it. Remove call to SetDeviceName and construct full PVs when creating records. Add call to create info tags on all records as they are created to add them to the PVI of the controller. Add call to add sub controller PVI to parent PVI
1 parent 4370011 commit e46b028

File tree

2 files changed

+205
-52
lines changed

2 files changed

+205
-52
lines changed

src/fastcs/backends/epics/ioc.py

Lines changed: 115 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
from collections.abc import Callable
22
from dataclasses import dataclass
33
from types import MethodType
4-
from typing import Any
4+
from typing import Any, Literal
55

66
from softioc import builder, softioc
77
from softioc.asyncio_dispatcher import AsyncioDispatcher
88
from softioc.pythonSoftIoc import RecordWrapper
99

1010
from fastcs.attributes import AttrR, AttrRW, AttrW
11+
from fastcs.controller import BaseController
1112
from fastcs.datatypes import Bool, DataType, Float, Int, String
1213
from fastcs.exceptions import FastCSException
1314
from fastcs.mapping import Mapping
@@ -20,10 +21,10 @@ class EpicsIOCOptions:
2021

2122
class EpicsIOC:
2223
def __init__(self, pv_prefix: str, mapping: Mapping):
23-
builder.SetDeviceName(pv_prefix)
24+
_create_and_link_attribute_pvs(pv_prefix, mapping)
25+
_create_and_link_command_pvs(pv_prefix, mapping)
2426

25-
_create_and_link_attribute_pvs(mapping)
26-
_create_and_link_command_pvs(mapping)
27+
_add_pvi_refs(pv_prefix, mapping.controller)
2728

2829
def run(
2930
self,
@@ -40,95 +41,174 @@ def run(
4041
softioc.interactive_ioc(context)
4142

4243

43-
def _create_and_link_attribute_pvs(mapping: Mapping) -> None:
44+
def _create_and_link_attribute_pvs(pv_prefix: str, mapping: Mapping) -> None:
4445
for single_mapping in mapping.get_controller_mappings():
4546
path = single_mapping.controller.path
4647
for attr_name, attribute in single_mapping.attributes.items():
47-
attr_name = attr_name.title().replace("_", "")
48-
pv_name = f"{':'.join(path)}:{attr_name}" if path else attr_name
48+
pv_name = attr_name.title().replace("_", "")
49+
_pv_prefix = ":".join([pv_prefix] + path)
4950

5051
match attribute:
5152
case AttrRW():
52-
_create_and_link_read_pv(pv_name + "_RBV", attribute)
53-
_create_and_link_write_pv(pv_name, attribute)
53+
_create_and_link_read_pv(
54+
_pv_prefix, f"{pv_name}_RBV", attr_name, attribute
55+
)
56+
_create_and_link_write_pv(_pv_prefix, pv_name, attr_name, attribute)
5457
case AttrR():
55-
_create_and_link_read_pv(pv_name, attribute)
58+
_create_and_link_read_pv(_pv_prefix, pv_name, attr_name, attribute)
5659
case AttrW():
57-
_create_and_link_write_pv(pv_name, attribute)
60+
_create_and_link_write_pv(_pv_prefix, pv_name, attr_name, attribute)
5861

5962

60-
def _create_and_link_read_pv(pv_name: str, attribute: AttrR) -> None:
61-
record = _get_input_record(pv_name, attribute.datatype)
63+
def _create_and_link_read_pv(
64+
pv_prefix: str, pv_name: str, attr_name: str, attribute: AttrR
65+
) -> None:
66+
record = _get_input_record(f"{pv_prefix}:{pv_name}", attribute.datatype)
67+
68+
_add_pvi_info(record, pv_prefix, attr_name, "r")
6269

6370
async def async_wrapper(v):
6471
record.set(v)
6572

6673
attribute.set_update_callback(async_wrapper)
6774

6875

69-
def _get_input_record(pv_name: str, datatype: DataType) -> RecordWrapper:
76+
def _get_input_record(pv: str, datatype: DataType) -> RecordWrapper:
7077
match datatype:
7178
case Bool(znam, onam):
72-
return builder.boolIn(pv_name, ZNAM=znam, ONAM=onam)
79+
return builder.boolIn(pv, ZNAM=znam, ONAM=onam)
7380
case Int():
74-
return builder.longIn(pv_name)
81+
return builder.longIn(pv)
7582
case Float(prec):
76-
return builder.aIn(pv_name, PREC=prec)
83+
return builder.aIn(pv, PREC=prec)
7784
case String():
78-
return builder.longStringIn(pv_name)
85+
return builder.longStringIn(pv)
7986
case _:
8087
raise FastCSException(f"Unsupported type {type(datatype)}: {datatype}")
8188

8289

83-
def _create_and_link_write_pv(pv_name: str, attribute: AttrW) -> None:
90+
def _create_and_link_write_pv(
91+
pv_prefix: str, pv_name: str, attr_name: str, attribute: AttrW
92+
) -> None:
8493
record = _get_output_record(
85-
pv_name, attribute.datatype, on_update=attribute.process_without_display_update
94+
f"{pv_prefix}:{pv_name}",
95+
attribute.datatype,
96+
on_update=attribute.process_without_display_update,
8697
)
8798

99+
_add_pvi_info(record, pv_prefix, attr_name, "w")
100+
88101
async def async_wrapper(v):
89102
record.set(v, process=False)
90103

91104
attribute.set_write_display_callback(async_wrapper)
92105

93106

94-
def _get_output_record(pv_name: str, datatype: DataType, on_update: Callable) -> Any:
107+
def _get_output_record(pv: str, datatype: DataType, on_update: Callable) -> Any:
95108
match datatype:
96109
case Bool(znam, onam):
97110
return builder.boolOut(
98-
pv_name,
111+
pv,
99112
ZNAM=znam,
100113
ONAM=onam,
101114
always_update=True,
102115
on_update=on_update,
103116
)
104117
case Int():
105-
return builder.longOut(pv_name, always_update=True, on_update=on_update)
118+
return builder.longOut(pv, always_update=True, on_update=on_update)
106119
case Float(prec):
107-
return builder.aOut(
108-
pv_name, always_update=True, on_update=on_update, PREC=prec
109-
)
120+
return builder.aOut(pv, always_update=True, on_update=on_update, PREC=prec)
110121
case String():
111-
return builder.longStringOut(
112-
pv_name, always_update=True, on_update=on_update
113-
)
122+
return builder.longStringOut(pv, always_update=True, on_update=on_update)
114123
case _:
115124
raise FastCSException(f"Unsupported type {type(datatype)}: {datatype}")
116125

117126

118-
def _create_and_link_command_pvs(mapping: Mapping) -> None:
127+
def _create_and_link_command_pvs(pv_prefix: str, mapping: Mapping) -> None:
119128
for single_mapping in mapping.get_controller_mappings():
120129
path = single_mapping.controller.path
121130
for attr_name, method in single_mapping.command_methods.items():
122-
attr_name = attr_name.title().replace("_", "")
123-
pv_name = f"{':'.join(path)}:{attr_name}" if path else attr_name
131+
pv_name = attr_name.title().replace("_", "")
132+
_pv_prefix = ":".join([pv_prefix] + path)
124133

125134
_create_and_link_command_pv(
126-
pv_name, MethodType(method.fn, single_mapping.controller)
135+
_pv_prefix,
136+
pv_name,
137+
attr_name,
138+
MethodType(method.fn, single_mapping.controller),
127139
)
128140

129141

130-
def _create_and_link_command_pv(pv_name: str, method: Callable) -> None:
142+
def _create_and_link_command_pv(
143+
pv_prefix: str, pv_name: str, attr_name: str, method: Callable
144+
) -> None:
131145
async def wrapped_method(_: Any):
132146
await method()
133147

134-
builder.aOut(pv_name, initial_value=0, always_update=True, on_update=wrapped_method)
148+
record = builder.aOut(
149+
f"{pv_prefix}:{pv_name}",
150+
initial_value=0,
151+
always_update=True,
152+
on_update=wrapped_method,
153+
)
154+
155+
_add_pvi_info(record, pv_prefix, attr_name, "x")
156+
157+
158+
def _add_pvi_info(
159+
record: RecordWrapper,
160+
prefix: str,
161+
name: str,
162+
access_mode: Literal["r", "w", "rw", "x"],
163+
):
164+
"""Add an info tag to a record to include it in the PVI for the controller.
165+
166+
Args:
167+
record: Record to add info tag to
168+
prefix: PV prefix of controller
169+
name: Name of parameter to add to PVI
170+
access_mode: Access mode of parameter
171+
"""
172+
record.add_info(
173+
"Q:group",
174+
{
175+
f"{prefix}:PVI": {
176+
f"pvi.{name}.{access_mode}": {
177+
"+channel": "NAME",
178+
"+type": "plain",
179+
"+trigger": f"pvi.{name}.{access_mode}",
180+
}
181+
}
182+
},
183+
)
184+
185+
186+
def _add_pvi_refs(pv_prefix: str, parent: BaseController):
187+
"""Add PVI references from controller to its sub controllers, recursively.
188+
189+
Args:
190+
pv_prefix: PV Prefix of IOC
191+
parent: Controller to add PVI refs for
192+
"""
193+
parent_pvi = ":".join([pv_prefix] + parent.path + ["PVI"])
194+
195+
for child in parent.get_sub_controllers().values():
196+
child_pvi = ":".join([pv_prefix] + child.path + ["PVI"])
197+
child_name = f"pvi.{child.path[-1].lower()}.d"
198+
199+
# Make a record with an info tag to add child to parent PVI structure
200+
dummy_record = builder.longStringIn(f"{child_pvi}_PV", initial_value=child_pvi)
201+
dummy_record.add_info(
202+
"Q:group",
203+
{
204+
parent_pvi: {
205+
child_name: {
206+
"+channel": "VAL",
207+
"+type": "plain",
208+
"+trigger": child_name,
209+
}
210+
}
211+
},
212+
)
213+
214+
_add_pvi_refs(pv_prefix, child)

tests/backends/epics/test_ioc.py

Lines changed: 90 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,104 @@
11
from pytest_mock import MockerFixture
22

3-
from fastcs.backends.epics.ioc import EpicsIOC
3+
from fastcs.backends.epics.ioc import EpicsIOC, _add_pvi_info, _add_pvi_refs
44
from fastcs.mapping import Mapping
55

66

77
def test_ioc(mocker: MockerFixture, mapping: Mapping):
8-
builder_mock = mocker.patch("fastcs.backends.epics.ioc.builder")
8+
builder = mocker.patch("fastcs.backends.epics.ioc.builder")
9+
add_pvi_info = mocker.patch("fastcs.backends.epics.ioc._add_pvi_info")
910

1011
EpicsIOC("DEVICE", mapping)
1112

12-
builder_mock.aIn.assert_called_once_with("ReadWriteFloat_RBV", PREC=2)
13-
builder_mock.aOut.assert_any_call(
14-
"ReadWriteFloat", always_update=True, on_update=mocker.ANY, PREC=2
13+
# Check records are created
14+
builder.aIn.assert_called_once_with("DEVICE:ReadWriteFloat_RBV", PREC=2)
15+
builder.aOut.assert_any_call(
16+
"DEVICE:ReadWriteFloat", always_update=True, on_update=mocker.ANY, PREC=2
1517
)
16-
builder_mock.boolIn.assert_called_once_with("ReadBool", ZNAM="OFF", ONAM="ON")
17-
builder_mock.boolOut.assert_called_once_with(
18-
"WriteBool", ZNAM="OFF", ONAM="ON", always_update=True, on_update=mocker.ANY
18+
builder.boolIn.assert_called_once_with("DEVICE:ReadBool", ZNAM="OFF", ONAM="ON")
19+
builder.boolOut.assert_called_once_with(
20+
"DEVICE:WriteBool",
21+
ZNAM="OFF",
22+
ONAM="ON",
23+
always_update=True,
24+
on_update=mocker.ANY,
1925
)
20-
builder_mock.longIn.assert_any_call("ReadInt")
21-
builder_mock.longIn.assert_any_call("ReadWriteInt_RBV")
22-
builder_mock.longOut.assert_called_with(
23-
"ReadWriteInt", always_update=True, on_update=mocker.ANY
26+
builder.longIn.assert_any_call("DEVICE:ReadInt")
27+
builder.longIn.assert_any_call("DEVICE:ReadWriteInt_RBV")
28+
builder.longOut.assert_called_with(
29+
"DEVICE:ReadWriteInt", always_update=True, on_update=mocker.ANY
2430
)
25-
builder_mock.longStringIn.assert_called_once_with("StringEnum_RBV")
26-
builder_mock.longStringOut.assert_called_once_with(
27-
"StringEnum", always_update=True, on_update=mocker.ANY
31+
builder.longStringIn.assert_called_once_with("DEVICE:StringEnum_RBV")
32+
builder.longStringOut.assert_called_once_with(
33+
"DEVICE:StringEnum", always_update=True, on_update=mocker.ANY
2834
)
29-
builder_mock.aOut.assert_any_call(
30-
"Go", initial_value=0, always_update=True, on_update=mocker.ANY
35+
builder.aOut.assert_any_call(
36+
"DEVICE:Go", initial_value=0, always_update=True, on_update=mocker.ANY
37+
)
38+
39+
# Check info tags are added
40+
add_pvi_info.assert_has_calls(
41+
[
42+
mocker.call(builder.boolIn.return_value, "DEVICE", "read_bool", "r"),
43+
mocker.call(builder.longIn.return_value, "DEVICE", "read_int", "r"),
44+
mocker.call(builder.aIn.return_value, "DEVICE", "read_write_float", "r"),
45+
mocker.call(builder.aOut.return_value, "DEVICE", "read_write_float", "w"),
46+
mocker.call(builder.longIn.return_value, "DEVICE", "read_write_int", "r"),
47+
mocker.call(builder.longOut.return_value, "DEVICE", "read_write_int", "w"),
48+
mocker.call(
49+
builder.longStringIn.return_value, "DEVICE", "string_enum", "r"
50+
),
51+
mocker.call(
52+
builder.longStringOut.return_value, "DEVICE", "string_enum", "w"
53+
),
54+
mocker.call(builder.boolOut.return_value, "DEVICE", "write_bool", "w"),
55+
mocker.call(builder.aOut.return_value, "DEVICE", "go", "x"),
56+
]
57+
)
58+
59+
60+
def test_add_pvi_info(mocker: MockerFixture):
61+
record = mocker.MagicMock()
62+
63+
_add_pvi_info(record, "DEVICE", "attr", "r")
64+
65+
record.add_info.assert_called_once_with(
66+
"Q:group",
67+
{
68+
"DEVICE:PVI": {
69+
"pvi.attr.r": {
70+
"+channel": "NAME",
71+
"+type": "plain",
72+
"+trigger": "pvi.attr.r",
73+
}
74+
}
75+
},
76+
)
77+
78+
79+
def test_add_pvi_refs(mocker: MockerFixture):
80+
builder = mocker.patch("fastcs.backends.epics.ioc.builder")
81+
controller = mocker.MagicMock()
82+
controller.path = []
83+
child = mocker.MagicMock()
84+
child.path = ["Child"]
85+
controller.get_sub_controllers.return_value = {"d": child}
86+
87+
_add_pvi_refs("DEVICE", controller)
88+
89+
builder.longStringIn.assert_called_once_with(
90+
"DEVICE:Child:PVI_PV", initial_value="DEVICE:Child:PVI"
91+
)
92+
record = builder.longStringIn.return_value
93+
record.add_info.assert_called_once_with(
94+
"Q:group",
95+
{
96+
"DEVICE:PVI": {
97+
"pvi.child.d": {
98+
"+channel": "VAL",
99+
"+type": "plain",
100+
"+trigger": "pvi.child.d",
101+
}
102+
}
103+
},
31104
)

0 commit comments

Comments
 (0)