diff --git a/pyproject.toml b/pyproject.toml index 8dcb22805..f23b2f0fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,8 +30,8 @@ requires-python = ">=3.11" [project.optional-dependencies] demo = ["tickit~=0.4.3"] -epicsca = ["pvi~=0.11.0", "softioc>=4.5.0"] -epicspva = ["p4p", "pvi~=0.11.0"] +epicsca = ["pvi~=0.12.0", "softioc>=4.5.0"] +epicspva = ["p4p", "pvi~=0.12.0"] epics = ["fastcs[epicsca]", "fastcs[epicspva]"] tango = ["pytango"] graphql = ["strawberry-graphql", "uvicorn[standard]>=0.12.0"] diff --git a/src/fastcs/datatypes/waveform.py b/src/fastcs/datatypes/waveform.py index e9ca0ada5..34b97b2cb 100644 --- a/src/fastcs/datatypes/waveform.py +++ b/src/fastcs/datatypes/waveform.py @@ -20,7 +20,7 @@ def initial_value(self) -> np.ndarray: return np.zeros(self.shape, dtype=self.array_dtype) def validate(self, value: np.ndarray) -> np.ndarray: - _value = super().validate(value) + _value = super().validate(np.asarray(value).astype(self.array_dtype)) if self.array_dtype != _value.dtype: raise ValueError( diff --git a/src/fastcs/demo/controllers.py b/src/fastcs/demo/controllers.py index 3946769c1..51383e0fb 100755 --- a/src/fastcs/demo/controllers.py +++ b/src/fastcs/demo/controllers.py @@ -4,10 +4,12 @@ from dataclasses import KW_ONLY, dataclass from typing import TypeVar +import numpy as np + from fastcs.attributes import AttributeIO, AttributeIORef, AttrR, AttrRW, AttrW from fastcs.connections import IPConnection, IPConnectionSettings from fastcs.controllers import Controller -from fastcs.datatypes import Enum, Float, Int +from fastcs.datatypes import Enum, Float, Int, Waveform from fastcs.methods import command, scan NumberT = TypeVar("NumberT", int, float) @@ -66,6 +68,7 @@ async def update( class TemperatureController(Controller): ramp_rate = AttrRW(Float(), io_ref=TemperatureControllerAttributeIORef(name="R")) power = AttrR(Float(), io_ref=TemperatureControllerAttributeIORef(name="P")) + voltages = AttrR(Waveform(np.int32, shape=(4,))) def __init__(self, settings: TemperatureControllerSettings) -> None: self.connection = IPConnection() @@ -101,6 +104,9 @@ async def update_voltages(self): voltages = json.loads( (await self.connection.send_query(f"{query}\r\n")).strip("\r\n") ) + + await self.voltages.update(voltages) + for index, controller in enumerate(self._ramp_controllers): self.log_event( "Update voltages", diff --git a/src/fastcs/transports/epics/gui.py b/src/fastcs/transports/epics/gui.py index ebcd78d54..8ac52f4bd 100644 --- a/src/fastcs/transports/epics/gui.py +++ b/src/fastcs/transports/epics/gui.py @@ -1,6 +1,7 @@ from pvi._format.dls import DLSFormatter # type: ignore from pvi.device import ( LED, + ArrayTrace, ButtonPanel, ComboBox, ComponentUnion, @@ -66,8 +67,12 @@ def _get_read_widget(self, fastcs_datatype: DataType) -> ReadWidgetUnion | None: return TextRead(format=TextFormat.string) case Enum(): return TextRead(format=TextFormat.string) - case Waveform(): - return None + case Waveform() as waveform: + if len(waveform.shape) > 1: + logger.warning("EPICS CA transport only supports 1D waveforms") + return None + + return ArrayTrace(axis="x") case datatype: raise TypeError(f"Unsupported type {type(datatype)}: {datatype}") diff --git a/src/fastcs/transports/epics/pva/gui.py b/src/fastcs/transports/epics/pva/gui.py index 7145e5791..b305b9814 100644 --- a/src/fastcs/transports/epics/pva/gui.py +++ b/src/fastcs/transports/epics/pva/gui.py @@ -1,12 +1,14 @@ from pvi.device import ( CheckBox, + ImageColorMap, + ImageRead, ReadWidgetUnion, TableRead, TableWrite, WriteWidgetUnion, ) -from fastcs.datatypes import Bool, DataType, Table, numpy_to_fastcs_datatype +from fastcs.datatypes import Bool, DataType, Table, Waveform, numpy_to_fastcs_datatype from fastcs.transports.epics.gui import EpicsGUI @@ -18,31 +20,39 @@ class PvaEpicsGUI(EpicsGUI): def _get_pv(self, attr_path: list[str], name: str): return f"pva://{super()._get_pv(attr_path, name)}" - def _get_read_widget(self, fastcs_datatype: DataType) -> ReadWidgetUnion | None: # noqa: F821 - if isinstance(fastcs_datatype, Table): - fastcs_datatypes = [ - numpy_to_fastcs_datatype(datatype) - for _, datatype in fastcs_datatype.structured_dtype - ] - - base_get_read_widget = super()._get_read_widget - widgets = [base_get_read_widget(datatype) for datatype in fastcs_datatypes] - - return TableRead(widgets=widgets) # type: ignore - else: - return super()._get_read_widget(fastcs_datatype) + def _get_read_widget(self, fastcs_datatype: DataType) -> ReadWidgetUnion | None: + match fastcs_datatype: + case Table(): + fastcs_datatypes = [ + numpy_to_fastcs_datatype(datatype) + for _, datatype in fastcs_datatype.structured_dtype + ] + + base_get_read_widget = super()._get_read_widget + widgets = [ + base_get_read_widget(datatype) for datatype in fastcs_datatypes + ] + + return TableRead(widgets=widgets) # type: ignore + case Waveform(shape=(height, width)): + return ImageRead( + height=height, width=width, color_map=ImageColorMap.GRAY + ) + case _: + return super()._get_read_widget(fastcs_datatype) def _get_write_widget(self, fastcs_datatype: DataType) -> WriteWidgetUnion | None: - if isinstance(fastcs_datatype, Table): - widgets = [] - for _, datatype in fastcs_datatype.structured_dtype: - fastcs_datatype = numpy_to_fastcs_datatype(datatype) - if isinstance(fastcs_datatype, Bool): - # Replace with compact version for Table row - widget = CheckBox() - else: - widget = super()._get_write_widget(fastcs_datatype) - widgets.append(widget) - return TableWrite(widgets=widgets) - else: - return super()._get_write_widget(fastcs_datatype) + match fastcs_datatype: + case Table(): + widgets = [] + for _, datatype in fastcs_datatype.structured_dtype: + fastcs_datatype = numpy_to_fastcs_datatype(datatype) + if isinstance(fastcs_datatype, Bool): + # Replace with compact version for Table row + widget = CheckBox() + else: + widget = super()._get_write_widget(fastcs_datatype) + widgets.append(widget) + return TableWrite(widgets=widgets) + case _: + return super()._get_write_widget(fastcs_datatype) diff --git a/tests/test_datatypes.py b/tests/test_datatypes.py index b3a41f31d..e49e80ebb 100644 --- a/tests/test_datatypes.py +++ b/tests/test_datatypes.py @@ -34,8 +34,7 @@ class MyIntEnum(IntEnum): (Float, {"min": 1}, 0.0), (Float, {"max": -1}, 0.0), (Enum, {"enum_cls": int}, 0), - (Waveform, {"array_dtype": "U64", "shape": (1,)}, np.ndarray([1])), - (Waveform, {"array_dtype": "float64", "shape": (1, 1)}, np.ndarray([1])), + (Waveform, {"array_dtype": "uint64", "shape": (1, 1)}, np.ndarray([1])), ], ) def test_validate(datatype, init_args, value): diff --git a/tests/transports/epics/ca/test_gui.py b/tests/transports/epics/ca/test_gui.py index 942feed5c..68ea0d832 100644 --- a/tests/transports/epics/ca/test_gui.py +++ b/tests/transports/epics/ca/test_gui.py @@ -2,6 +2,7 @@ import pytest from pvi.device import ( LED, + ArrayTrace, ButtonPanel, ComboBox, Group, @@ -39,7 +40,7 @@ def test_get_pv(): (Float(), TextRead()), (String(), TextRead(format=TextFormat.string)), (Enum(ColourEnum), TextRead(format=TextFormat.string)), - # (Waveform(array_dtype=np.int32), None), + (Waveform(array_dtype=np.int32), ArrayTrace(axis="x")), ], ) def test_get_attribute_component_r(datatype, widget): @@ -50,6 +51,18 @@ def test_get_attribute_component_r(datatype, widget): ) +@pytest.mark.parametrize( + "datatype", + [ + (Waveform(array_dtype=np.int32, shape=(10, 10))), + ], +) +def test_get_attribute_component_r_signal_none(datatype): + gui = EpicsGUI(ControllerAPI(), "DEVICE") + + assert gui._get_attribute_component([], "Attr", AttrR(datatype)) is None + + @pytest.mark.parametrize( "datatype, widget", [ @@ -78,11 +91,6 @@ def test_get_attribute_component_none(mocker): assert gui._get_attribute_component([], "Attr", AttrRW(Int())) is None -def test_get_read_widget_none(): - gui = EpicsGUI(ControllerAPI(), "DEVICE") - assert gui._get_read_widget(fastcs_datatype=Waveform(np.int32)) is None - - def test_get_write_widget_none(): gui = EpicsGUI(ControllerAPI(), "DEVICE") assert gui._get_write_widget(fastcs_datatype=Waveform(np.int32)) is None diff --git a/tests/transports/epics/pva/test_pva_gui.py b/tests/transports/epics/pva/test_pva_gui.py index 4f0484dcd..793a85908 100644 --- a/tests/transports/epics/pva/test_pva_gui.py +++ b/tests/transports/epics/pva/test_pva_gui.py @@ -1,8 +1,10 @@ import numpy as np +import pytest from pvi.device import ( LED, ButtonPanel, CheckBox, + ImageRead, SignalR, SignalW, SignalX, @@ -14,11 +16,26 @@ ) from fastcs.attributes import AttrR, AttrW -from fastcs.datatypes import Table +from fastcs.datatypes import Table, Waveform from fastcs.transports import ControllerAPI +from fastcs.transports.epics.gui import EpicsGUI from fastcs.transports.epics.pva.gui import PvaEpicsGUI +@pytest.mark.parametrize( + "datatype, widget", + [ + (Waveform(array_dtype=np.int32), ImageRead()), + ], +) +def test_pva_get_attribute_component_r(datatype, widget): + gui = EpicsGUI(ControllerAPI(), "DEVICE") + + assert gui._get_attribute_component([], "Attr", AttrR(datatype)) == SignalR( + name="Attr", read_pv="Attr", read_widget=widget + ) + + def test_get_pv_in_pva(): gui = PvaEpicsGUI(ControllerAPI(), "DEVICE")