Skip to content

Commit 3ec51ea

Browse files
authored
Add table support in pva and fix gui pv naming (#167)
1 parent aec213b commit 3ec51ea

File tree

6 files changed

+185
-21
lines changed

6 files changed

+185
-21
lines changed

src/fastcs/transport/epics/gui.py

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from pvi.device import (
33
LED,
44
ButtonPanel,
5+
CheckBox,
56
ComboBox,
67
ComponentUnion,
78
Device,
@@ -13,6 +14,8 @@
1314
SignalW,
1415
SignalX,
1516
SubScreen,
17+
TableRead,
18+
TableWrite,
1619
TextFormat,
1720
TextRead,
1821
TextWrite,
@@ -25,9 +28,18 @@
2528
from fastcs.attributes import Attribute, AttrR, AttrRW, AttrW
2629
from fastcs.controller_api import ControllerAPI
2730
from fastcs.cs_methods import Command
28-
from fastcs.datatypes import Bool, Enum, Float, Int, String, Waveform
31+
from fastcs.datatypes import (
32+
Bool,
33+
DataType,
34+
Enum,
35+
Float,
36+
Int,
37+
String,
38+
Table,
39+
Waveform,
40+
)
2941
from fastcs.exceptions import FastCSException
30-
from fastcs.util import snake_to_pascal
42+
from fastcs.util import numpy_to_fastcs_datatype, snake_to_pascal
3143

3244
from .options import EpicsGUIFormat, EpicsGUIOptions
3345

@@ -40,12 +52,13 @@ def __init__(self, controller_api: ControllerAPI, pv_prefix: str) -> None:
4052
self._pv_prefix = pv_prefix
4153

4254
def _get_pv(self, attr_path: list[str], name: str):
43-
attr_prefix = ":".join([self._pv_prefix] + attr_path)
55+
attr_prefix = ":".join(
56+
[self._pv_prefix] + [snake_to_pascal(node) for node in attr_path]
57+
)
4458
return f"{attr_prefix}:{snake_to_pascal(name)}"
4559

46-
@staticmethod
47-
def _get_read_widget(attribute: AttrR) -> ReadWidgetUnion | None:
48-
match attribute.datatype:
60+
def _get_read_widget(self, fastcs_datatype: DataType) -> ReadWidgetUnion | None:
61+
match fastcs_datatype:
4962
case Bool():
5063
return LED()
5164
case Int() | Float():
@@ -59,17 +72,16 @@ def _get_read_widget(attribute: AttrR) -> ReadWidgetUnion | None:
5972
case datatype:
6073
raise FastCSException(f"Unsupported type {type(datatype)}: {datatype}")
6174

62-
@staticmethod
63-
def _get_write_widget(attribute: AttrW) -> WriteWidgetUnion | None:
64-
match attribute.datatype:
75+
def _get_write_widget(self, fastcs_datatype: DataType) -> WriteWidgetUnion | None:
76+
match fastcs_datatype:
6577
case Bool():
6678
return ToggleButton()
6779
case Int() | Float():
6880
return TextWrite()
6981
case String():
7082
return TextWrite(format=TextFormat.string)
7183
case Enum():
72-
return ComboBox(choices=attribute.datatype.names)
84+
return ComboBox(choices=fastcs_datatype.names)
7385
case Waveform():
7486
return None
7587
case datatype:
@@ -82,8 +94,8 @@ def _get_attribute_component(
8294
name = snake_to_pascal(name)
8395
match attribute:
8496
case AttrRW():
85-
read_widget = self._get_read_widget(attribute)
86-
write_widget = self._get_write_widget(attribute)
97+
read_widget = self._get_read_widget(attribute.datatype)
98+
write_widget = self._get_write_widget(attribute.datatype)
8799
if write_widget is None or read_widget is None:
88100
return None
89101
return SignalRW(
@@ -94,12 +106,12 @@ def _get_attribute_component(
94106
read_widget=read_widget,
95107
)
96108
case AttrR():
97-
read_widget = self._get_read_widget(attribute)
109+
read_widget = self._get_read_widget(attribute.datatype)
98110
if read_widget is None:
99111
return None
100112
return SignalR(name=name, read_pv=pv, read_widget=read_widget)
101113
case AttrW():
102-
write_widget = self._get_write_widget(attribute)
114+
write_widget = self._get_write_widget(attribute.datatype)
103115
if write_widget is None:
104116
return None
105117
return SignalW(name=name, write_pv=pv, write_widget=write_widget)
@@ -188,3 +200,39 @@ def extract_api_components(self, controller_api: ControllerAPI) -> Tree:
188200
components.append(Group(name=name, layout=Grid(), children=children))
189201

190202
return components
203+
204+
205+
class PvaEpicsGUI(EpicsGUI):
206+
"""For creating gui in the PVA EPICS transport."""
207+
208+
def _get_pv(self, attr_path: list[str], name: str):
209+
return f"pva://{super()._get_pv(attr_path, name)}"
210+
211+
def _get_read_widget(self, fastcs_datatype: DataType) -> ReadWidgetUnion | None:
212+
if isinstance(fastcs_datatype, Table):
213+
fastcs_datatypes = [
214+
numpy_to_fastcs_datatype(datatype)
215+
for _, datatype in fastcs_datatype.structured_dtype
216+
]
217+
218+
base_get_read_widget = super()._get_read_widget
219+
widgets = [base_get_read_widget(datatype) for datatype in fastcs_datatypes]
220+
221+
return TableRead(widgets=widgets) # type: ignore
222+
else:
223+
return super()._get_read_widget(fastcs_datatype)
224+
225+
def _get_write_widget(self, fastcs_datatype: DataType) -> WriteWidgetUnion | None:
226+
if isinstance(fastcs_datatype, Table):
227+
widgets = []
228+
for _, datatype in fastcs_datatype.structured_dtype:
229+
fastcs_datatype = numpy_to_fastcs_datatype(datatype)
230+
if isinstance(fastcs_datatype, Bool):
231+
# Replace with compact version for Table row
232+
widget = CheckBox()
233+
else:
234+
widget = super()._get_write_widget(fastcs_datatype)
235+
widgets.append(widget)
236+
return TableWrite(widgets=widgets)
237+
else:
238+
return super()._get_write_widget(fastcs_datatype)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from fastcs.controller_api import ControllerAPI
22
from fastcs.transport.adapter import TransportAdapter
33
from fastcs.transport.epics.docs import EpicsDocs
4-
from fastcs.transport.epics.gui import EpicsGUI
4+
from fastcs.transport.epics.gui import PvaEpicsGUI
55
from fastcs.transport.epics.pva.options import EpicsPVAOptions
66

77
from .ioc import P4PIOC
@@ -32,4 +32,4 @@ def create_docs(self) -> None:
3232
EpicsDocs(self._controller_api).create_docs(self.options.docs)
3333

3434
def create_gui(self) -> None:
35-
EpicsGUI(self._controller_api, self._pv_prefix).create_gui(self.options.gui)
35+
PvaEpicsGUI(self._controller_api, self._pv_prefix).create_gui(self.options.gui)

src/fastcs/util.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import re
22

3+
import numpy as np
4+
5+
from fastcs.datatypes import Bool, DataType, Float, Int, String
6+
37

48
def snake_to_pascal(name: str) -> str:
59
"""Converts string from snake case to Pascal case.
@@ -8,3 +12,17 @@ def snake_to_pascal(name: str) -> str:
812
if re.fullmatch(r"[a-z][a-z0-9]*(?:_[a-z0-9]+)*", name):
913
name = re.sub(r"(?:^|_)([a-z0-9])", lambda match: match.group(1).upper(), name)
1014
return name
15+
16+
17+
def numpy_to_fastcs_datatype(np_type) -> DataType:
18+
"""Converts numpy types to fastcs types for widget creation.
19+
Only types important for widget creation are explicitly converted
20+
"""
21+
if np.issubdtype(np_type, np.integer):
22+
return Int()
23+
elif np.issubdtype(np_type, np.floating):
24+
return Float()
25+
elif np.issubdtype(np_type, np.bool_):
26+
return Bool()
27+
else:
28+
return String()

tests/test_util.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import numpy as np
12
import pytest
23
from pvi.device import SignalR
34
from pydantic import ValidationError
45

5-
from fastcs.util import snake_to_pascal
6+
from fastcs.datatypes import Bool, Float, Int, String
7+
from fastcs.util import numpy_to_fastcs_datatype, snake_to_pascal
68

79

810
def test_snake_to_pascal():
@@ -36,3 +38,21 @@ def test_pvi_validation_error():
3638
name = snake_to_pascal("Name-With_%_Invalid-&-Symbols_£_")
3739
with pytest.raises(ValidationError):
3840
SignalR(name=name, read_pv="test")
41+
42+
43+
@pytest.mark.parametrize(
44+
"numpy_type, fastcs_datatype",
45+
[
46+
(np.float16, Float()),
47+
(np.float32, Float()),
48+
(np.int16, Int()),
49+
(np.int32, Int()),
50+
(np.bool, Bool()),
51+
(np.dtype("S1000"), String()),
52+
(np.dtype("U25"), String()),
53+
(np.dtype(">i4"), Int()),
54+
(np.dtype("d"), Float()),
55+
],
56+
)
57+
def test_numpy_to_fastcs_datatype(numpy_type, fastcs_datatype):
58+
assert fastcs_datatype == numpy_to_fastcs_datatype(numpy_type)

tests/transport/epics/ca/test_gui.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,14 @@ def test_get_attribute_component_none(mocker, controller_api):
7878
assert gui._get_attribute_component([], "Attr", AttrRW(Int())) is None
7979

8080

81-
def test_get_read_widget_none():
82-
assert EpicsGUI._get_read_widget(AttrR(Waveform(np.int32))) is None
81+
def test_get_read_widget_none(controller_api):
82+
gui = EpicsGUI(controller_api, "DEVICE")
83+
assert gui._get_read_widget(fastcs_datatype=Waveform(np.int32)) is None
8384

8485

85-
def test_get_write_widget_none():
86-
assert EpicsGUI._get_write_widget(AttrW(Waveform(np.int32))) is None
86+
def test_get_write_widget_none(controller_api):
87+
gui = EpicsGUI(controller_api, "DEVICE")
88+
assert gui._get_write_widget(fastcs_datatype=Waveform(np.int32)) is None
8789

8890

8991
def test_get_components(controller_api):
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import numpy as np
2+
from pvi.device import (
3+
LED,
4+
CheckBox,
5+
SignalR,
6+
SignalW,
7+
TableRead,
8+
TableWrite,
9+
TextFormat,
10+
TextRead,
11+
TextWrite,
12+
)
13+
14+
from fastcs.attributes import AttrR, AttrW
15+
from fastcs.datatypes import Table
16+
from fastcs.transport.epics.gui import PvaEpicsGUI
17+
18+
19+
def test_get_pv_in_pva(controller_api):
20+
gui = PvaEpicsGUI(controller_api, "DEVICE")
21+
22+
assert gui._get_pv([], "A") == "pva://DEVICE:A"
23+
assert gui._get_pv(["B"], "C") == "pva://DEVICE:B:C"
24+
assert gui._get_pv(["D", "E"], "F") == "pva://DEVICE:D:E:F"
25+
26+
27+
def test_get_attribute_component_table_write(controller_api):
28+
gui = PvaEpicsGUI(controller_api, "DEVICE")
29+
30+
attribute_component = gui._get_attribute_component(
31+
[],
32+
"Table",
33+
AttrW(
34+
Table(
35+
structured_dtype=[
36+
("FIELD1", np.uint32),
37+
("FIELD2", np.bool),
38+
("FIELD3", np.dtype("S1000")),
39+
]
40+
)
41+
),
42+
)
43+
44+
assert isinstance(attribute_component, SignalW)
45+
assert isinstance(attribute_component.write_widget, TableWrite)
46+
assert attribute_component.write_widget.widgets == [
47+
TextWrite(),
48+
CheckBox(),
49+
TextWrite(format=TextFormat.string),
50+
]
51+
52+
53+
def test_get_attribute_component_table_read(controller_api):
54+
gui = PvaEpicsGUI(controller_api, "DEVICE")
55+
56+
attribute_component = gui._get_attribute_component(
57+
[],
58+
"Table",
59+
AttrR(
60+
Table(
61+
structured_dtype=[
62+
("FIELD1", np.uint32),
63+
("FIELD2", np.bool),
64+
("FIELD3", np.dtype("S1000")),
65+
]
66+
)
67+
),
68+
)
69+
70+
assert isinstance(attribute_component, SignalR)
71+
assert isinstance(attribute_component.read_widget, TableRead)
72+
assert attribute_component.read_widget.widgets == [
73+
TextRead(),
74+
LED(),
75+
TextRead(format=TextFormat.string),
76+
]

0 commit comments

Comments
 (0)