Skip to content

Commit 1ee470b

Browse files
committed
Implement Waveform
1 parent 9ebdf4d commit 1ee470b

File tree

8 files changed

+75
-42
lines changed

8 files changed

+75
-42
lines changed

src/fastcs/datatypes/waveform.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def initial_value(self) -> np.ndarray:
2020
return np.zeros(self.shape, dtype=self.array_dtype)
2121

2222
def validate(self, value: np.ndarray) -> np.ndarray:
23-
_value = super().validate(value)
23+
_value = super().validate(np.asarray(value).astype(self.array_dtype))
2424

2525
if self.array_dtype != _value.dtype:
2626
raise ValueError(

src/fastcs/demo/controllers.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
from dataclasses import KW_ONLY, dataclass
55
from typing import TypeVar
66

7+
import numpy as np
8+
79
from fastcs.attributes import AttributeIO, AttributeIORef, AttrR, AttrRW, AttrW
810
from fastcs.connections import IPConnection, IPConnectionSettings
911
from fastcs.controllers import Controller
10-
from fastcs.datatypes import Enum, Float, Int
12+
from fastcs.datatypes import Enum, Float, Int, Waveform
1113
from fastcs.methods import command, scan
1214

1315
NumberT = TypeVar("NumberT", int, float)
@@ -66,6 +68,7 @@ async def update(
6668
class TemperatureController(Controller):
6769
ramp_rate = AttrRW(Float(), io_ref=TemperatureControllerAttributeIORef(name="R"))
6870
power = AttrR(Float(), io_ref=TemperatureControllerAttributeIORef(name="P"))
71+
voltages = AttrR(Waveform(np.int32, shape=(4,)))
6972

7073
def __init__(self, settings: TemperatureControllerSettings) -> None:
7174
self.connection = IPConnection()
@@ -101,6 +104,9 @@ async def update_voltages(self):
101104
voltages = json.loads(
102105
(await self.connection.send_query(f"{query}\r\n")).strip("\r\n")
103106
)
107+
108+
await self.voltages.update(voltages)
109+
104110
for index, controller in enumerate(self._ramp_controllers):
105111
self.log_event(
106112
"Update voltages",

src/fastcs/transports/epics/gui.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from pvi._format.dls import DLSFormatter # type: ignore
22
from pvi.device import (
33
LED,
4+
ArrayTrace,
45
ButtonPanel,
56
ComboBox,
67
ComponentUnion,
@@ -66,8 +67,12 @@ def _get_read_widget(self, fastcs_datatype: DataType) -> ReadWidgetUnion | None:
6667
return TextRead(format=TextFormat.string)
6768
case Enum():
6869
return TextRead(format=TextFormat.string)
69-
case Waveform():
70-
return None
70+
case Waveform() as waveform:
71+
if len(waveform.shape) > 1:
72+
logger.warning("EPICS CA transport only supports 1D waveforms")
73+
return None
74+
75+
return ArrayTrace(axis="x")
7176
case datatype:
7277
raise TypeError(f"Unsupported type {type(datatype)}: {datatype}")
7378

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from pvi.device import (
22
CheckBox,
3+
ImageRead,
34
ReadWidgetUnion,
45
TableRead,
56
TableWrite,
67
WriteWidgetUnion,
78
)
89

9-
from fastcs.datatypes import Bool, DataType, Table, numpy_to_fastcs_datatype
10+
from fastcs.datatypes import Bool, DataType, Table, Waveform, numpy_to_fastcs_datatype
1011
from fastcs.transports.epics.gui import EpicsGUI
1112

1213

@@ -18,31 +19,37 @@ class PvaEpicsGUI(EpicsGUI):
1819
def _get_pv(self, attr_path: list[str], name: str):
1920
return f"pva://{super()._get_pv(attr_path, name)}"
2021

21-
def _get_read_widget(self, fastcs_datatype: DataType) -> ReadWidgetUnion | None: # noqa: F821
22-
if isinstance(fastcs_datatype, Table):
23-
fastcs_datatypes = [
24-
numpy_to_fastcs_datatype(datatype)
25-
for _, datatype in fastcs_datatype.structured_dtype
26-
]
27-
28-
base_get_read_widget = super()._get_read_widget
29-
widgets = [base_get_read_widget(datatype) for datatype in fastcs_datatypes]
30-
31-
return TableRead(widgets=widgets) # type: ignore
32-
else:
33-
return super()._get_read_widget(fastcs_datatype)
22+
def _get_read_widget(self, fastcs_datatype: DataType) -> ReadWidgetUnion | None:
23+
match fastcs_datatype:
24+
case Table():
25+
fastcs_datatypes = [
26+
numpy_to_fastcs_datatype(datatype)
27+
for _, datatype in fastcs_datatype.structured_dtype
28+
]
29+
30+
base_get_read_widget = super()._get_read_widget
31+
widgets = [
32+
base_get_read_widget(datatype) for datatype in fastcs_datatypes
33+
]
34+
35+
return TableRead(widgets=widgets) # type: ignore
36+
case Waveform(shape=(height, width)):
37+
return ImageRead(height=height, width=width, grayscale=True)
38+
case _:
39+
return super()._get_read_widget(fastcs_datatype)
3440

3541
def _get_write_widget(self, fastcs_datatype: DataType) -> WriteWidgetUnion | None:
36-
if isinstance(fastcs_datatype, Table):
37-
widgets = []
38-
for _, datatype in fastcs_datatype.structured_dtype:
39-
fastcs_datatype = numpy_to_fastcs_datatype(datatype)
40-
if isinstance(fastcs_datatype, Bool):
41-
# Replace with compact version for Table row
42-
widget = CheckBox()
43-
else:
44-
widget = super()._get_write_widget(fastcs_datatype)
45-
widgets.append(widget)
46-
return TableWrite(widgets=widgets)
47-
else:
48-
return super()._get_write_widget(fastcs_datatype)
42+
match fastcs_datatype:
43+
case Table():
44+
widgets = []
45+
for _, datatype in fastcs_datatype.structured_dtype:
46+
fastcs_datatype = numpy_to_fastcs_datatype(datatype)
47+
if isinstance(fastcs_datatype, Bool):
48+
# Replace with compact version for Table row
49+
widget = CheckBox()
50+
else:
51+
widget = super()._get_write_widget(fastcs_datatype)
52+
widgets.append(widget)
53+
return TableWrite(widgets=widgets)
54+
case _:
55+
return super()._get_write_widget(fastcs_datatype)

src/fastcs/transports/graphql/graphql.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@
88
from strawberry.types.field import StrawberryField
99

1010
from fastcs.attributes import AttrR, AttrRW, AttrW
11+
from fastcs.datatypes import Waveform
1112
from fastcs.datatypes.datatype import DType_T
1213
from fastcs.exceptions import FastCSError
13-
from fastcs.logging import intercept_std_logger
14-
from fastcs.transports.controller_api import ControllerAPI
14+
from fastcs.logging import bind_logger, intercept_std_logger
15+
from fastcs.transports import ControllerAPI
1516

1617
from .options import GraphQLServerOptions
1718

19+
logger = bind_logger(__name__)
20+
1821

1922
class GraphQLServer:
2023
"""A GraphQL server which handles a controller"""
@@ -61,6 +64,13 @@ def __init__(self, controller_api: ControllerAPI):
6164
def _process_attributes(self, api: ControllerAPI):
6265
"""Create queries and mutations from api attributes."""
6366
for attr_name, attribute in api.attributes.items():
67+
if isinstance(attribute.datatype, Waveform):
68+
logger.warning(
69+
"Waveform attributes are not supported in GraphQL transport",
70+
attribute=attribute,
71+
)
72+
continue
73+
6474
match attribute:
6575
# mutation for server changes https://graphql.org/learn/queries/
6676
case AttrRW():

src/fastcs/transports/rest/rest.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
from pydantic import create_model
77

88
from fastcs.attributes import AttrR, AttrRW, AttrW
9+
from fastcs.datatypes import Waveform
910
from fastcs.datatypes.datatype import DType_T
10-
from fastcs.logging import intercept_std_logger
11+
from fastcs.logging import bind_logger, intercept_std_logger
1112
from fastcs.methods import CommandCallback
1213
from fastcs.transports.controller_api import ControllerAPI
1314

@@ -18,6 +19,8 @@
1819
convert_datatype,
1920
)
2021

22+
logger = bind_logger(__name__)
23+
2124

2225
class RestServer:
2326
"""A Rest Server which handles a controller"""
@@ -105,6 +108,13 @@ def _add_attribute_api_routes(app: FastAPI, root_controller_api: ControllerAPI)
105108
path = controller_api.path
106109

107110
for attr_name, attribute in controller_api.attributes.items():
111+
if isinstance(attribute.datatype, Waveform):
112+
logger.warning(
113+
"Waveform attributes are not supported in GraphQL transport",
114+
attribute=attribute,
115+
)
116+
continue
117+
108118
attr_name = attr_name.replace("_", "-")
109119
route = f"{'/'.join(path)}/{attr_name}" if path else attr_name
110120

tests/test_datatypes.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@ class MyIntEnum(IntEnum):
3434
(Float, {"min": 1}, 0.0),
3535
(Float, {"max": -1}, 0.0),
3636
(Enum, {"enum_cls": int}, 0),
37-
(Waveform, {"array_dtype": "U64", "shape": (1,)}, np.ndarray([1])),
38-
(Waveform, {"array_dtype": "float64", "shape": (1, 1)}, np.ndarray([1])),
37+
(Waveform, {"array_dtype": "uint64", "shape": (1, 1)}, np.ndarray([1])),
3938
],
4039
)
4140
def test_validate(datatype, init_args, value):

tests/transports/epics/ca/test_gui.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import pytest
33
from pvi.device import (
44
LED,
5+
ArrayTrace,
56
ButtonPanel,
67
ComboBox,
78
Group,
@@ -39,7 +40,7 @@ def test_get_pv():
3940
(Float(), TextRead()),
4041
(String(), TextRead(format=TextFormat.string)),
4142
(Enum(ColourEnum), TextRead(format=TextFormat.string)),
42-
# (Waveform(array_dtype=np.int32), None),
43+
(Waveform(array_dtype=np.int32), ArrayTrace(axis="x")),
4344
],
4445
)
4546
def test_get_attribute_component_r(datatype, widget):
@@ -78,11 +79,6 @@ def test_get_attribute_component_none(mocker):
7879
assert gui._get_attribute_component([], "Attr", AttrRW(Int())) is None
7980

8081

81-
def test_get_read_widget_none():
82-
gui = EpicsGUI(ControllerAPI(), "DEVICE")
83-
assert gui._get_read_widget(fastcs_datatype=Waveform(np.int32)) is None
84-
85-
8682
def test_get_write_widget_none():
8783
gui = EpicsGUI(ControllerAPI(), "DEVICE")
8884
assert gui._get_write_widget(fastcs_datatype=Waveform(np.int32)) is None

0 commit comments

Comments
 (0)