From 4643567dfc55e7553a7450f295db61b1025df27f Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Mon, 7 Jul 2025 10:32:59 +0000 Subject: [PATCH 01/13] Apply snake to pascal to attr path --- src/fastcs/transport/epics/gui.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/fastcs/transport/epics/gui.py b/src/fastcs/transport/epics/gui.py index c7fbff30a..7a20b7fbd 100644 --- a/src/fastcs/transport/epics/gui.py +++ b/src/fastcs/transport/epics/gui.py @@ -40,7 +40,9 @@ def __init__(self, controller_api: ControllerAPI, pv_prefix: str) -> None: self._pv_prefix = pv_prefix def _get_pv(self, attr_path: list[str], name: str): - attr_prefix = ":".join([self._pv_prefix] + attr_path) + attr_prefix = ":".join( + [self._pv_prefix] + [snake_to_pascal(attr) for attr in attr_path] + ) return f"{attr_prefix}:{snake_to_pascal(name)}" @staticmethod From aad920610ec14809ba15b890fde34bc873bb968b Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Mon, 7 Jul 2025 10:33:44 +0000 Subject: [PATCH 02/13] Add Table support for pva --- src/fastcs/transport/epics/gui.py | 52 ++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/fastcs/transport/epics/gui.py b/src/fastcs/transport/epics/gui.py index 7a20b7fbd..c529dec24 100644 --- a/src/fastcs/transport/epics/gui.py +++ b/src/fastcs/transport/epics/gui.py @@ -13,6 +13,8 @@ SignalW, SignalX, SubScreen, + TableRead, + TableWrite, TextFormat, TextRead, TextWrite, @@ -25,7 +27,7 @@ from fastcs.attributes import Attribute, AttrR, AttrRW, AttrW from fastcs.controller_api import ControllerAPI from fastcs.cs_methods import Command -from fastcs.datatypes import Bool, Enum, Float, Int, String, Waveform +from fastcs.datatypes import Bool, Enum, Float, Int, String, Table, Waveform from fastcs.exceptions import FastCSException from fastcs.util import snake_to_pascal @@ -190,3 +192,51 @@ def extract_api_components(self, controller_api: ControllerAPI) -> Tree: components.append(Group(name=name, layout=Grid(), children=children)) return components + + +class PvaEpicsGUI(EpicsGUI): + """For creating gui in the PVA transport.""" + + def _get_pv(self, attr_path: list[str], name: str): + attr_prefix = ":".join( + [self._pv_prefix] + [snake_to_pascal(attr) for attr in attr_path] + ) + return f"pva://{attr_prefix}:{snake_to_pascal(name)}" + + @staticmethod + def _get_read_widget(attribute: AttrR) -> ReadWidgetUnion | None: + match attribute.datatype: + case Bool(): + return LED() + case Int() | Float(): + return TextRead() + case String(): + return TextRead(format=TextFormat.string) + case Enum(): + return TextRead(format=TextFormat.string) + case Waveform(): + return None + case Table(): + return TableRead( + widgets=[TextRead()] * 4 + [LED()] * 6 + [TextRead()] + [LED()] * 6 + ) + case datatype: + raise FastCSException(f"Unsupported type {type(datatype)}: {datatype}") + + @staticmethod + def _get_write_widget(attribute: AttrW) -> WriteWidgetUnion | None: + match attribute.datatype: + case Bool(): + return ToggleButton() + case Int() | Float(): + return TextWrite() + case String(): + return TextWrite(format=TextFormat.string) + case Enum(): + return ComboBox(choices=attribute.datatype.names) + case Waveform(): + return None + case Table(): + return TableWrite(widgets=[TextWrite()]) + case datatype: + raise FastCSException(f"Unsupported type {type(datatype)}: {datatype}") From 8820b6e6ca78d30a2f8bcc3d94bdeabab8efcde0 Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Mon, 7 Jul 2025 10:34:22 +0000 Subject: [PATCH 03/13] Use PvaEpicsGui for pva epics adapter gui --- src/fastcs/transport/epics/pva/adapter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fastcs/transport/epics/pva/adapter.py b/src/fastcs/transport/epics/pva/adapter.py index d07aafa00..569442e23 100644 --- a/src/fastcs/transport/epics/pva/adapter.py +++ b/src/fastcs/transport/epics/pva/adapter.py @@ -1,7 +1,7 @@ from fastcs.controller_api import ControllerAPI from fastcs.transport.adapter import TransportAdapter from fastcs.transport.epics.docs import EpicsDocs -from fastcs.transport.epics.gui import EpicsGUI +from fastcs.transport.epics.gui import PvaEpicsGUI from fastcs.transport.epics.pva.options import EpicsPVAOptions from .ioc import P4PIOC @@ -32,4 +32,4 @@ def create_docs(self) -> None: EpicsDocs(self._controller_api).create_docs(self.options.docs) def create_gui(self) -> None: - EpicsGUI(self._controller_api, self._pv_prefix).create_gui(self.options.gui) + PvaEpicsGUI(self._controller_api, self._pv_prefix).create_gui(self.options.gui) From 4225e2782b25ea17879ce1b6a97aa6177374df20 Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Mon, 7 Jul 2025 10:51:35 +0000 Subject: [PATCH 04/13] Simplify PvaEpicsGui --- src/fastcs/transport/epics/gui.py | 49 ++++++++----------------------- 1 file changed, 12 insertions(+), 37 deletions(-) diff --git a/src/fastcs/transport/epics/gui.py b/src/fastcs/transport/epics/gui.py index c529dec24..14515e371 100644 --- a/src/fastcs/transport/epics/gui.py +++ b/src/fastcs/transport/epics/gui.py @@ -195,48 +195,23 @@ def extract_api_components(self, controller_api: ControllerAPI) -> Tree: class PvaEpicsGUI(EpicsGUI): - """For creating gui in the PVA transport.""" + """For creating gui in the PVA EPICS transport.""" def _get_pv(self, attr_path: list[str], name: str): - attr_prefix = ":".join( - [self._pv_prefix] + [snake_to_pascal(attr) for attr in attr_path] - ) - return f"pva://{attr_prefix}:{snake_to_pascal(name)}" + return f"pva://{super()._get_pv(attr_path, name)}" @staticmethod def _get_read_widget(attribute: AttrR) -> ReadWidgetUnion | None: - match attribute.datatype: - case Bool(): - return LED() - case Int() | Float(): - return TextRead() - case String(): - return TextRead(format=TextFormat.string) - case Enum(): - return TextRead(format=TextFormat.string) - case Waveform(): - return None - case Table(): - return TableRead( - widgets=[TextRead()] * 4 + [LED()] * 6 + [TextRead()] + [LED()] * 6 - ) - case datatype: - raise FastCSException(f"Unsupported type {type(datatype)}: {datatype}") + if isinstance(attribute.datatype, Table): + return TableRead( + widgets=[TextRead()] * 4 + [LED()] * 6 + [TextRead()] + [LED()] * 6 + ) + else: + return EpicsGUI._get_read_widget(attribute) # noqa: SLF001 @staticmethod def _get_write_widget(attribute: AttrW) -> WriteWidgetUnion | None: - match attribute.datatype: - case Bool(): - return ToggleButton() - case Int() | Float(): - return TextWrite() - case String(): - return TextWrite(format=TextFormat.string) - case Enum(): - return ComboBox(choices=attribute.datatype.names) - case Waveform(): - return None - case Table(): - return TableWrite(widgets=[TextWrite()]) - case datatype: - raise FastCSException(f"Unsupported type {type(datatype)}: {datatype}") + if isinstance(attribute.datatype, Table): + return TableWrite(widgets=[TextWrite()]) + else: + return EpicsGUI._get_write_widget(attribute) # noqa: SLF001 From 6f8a37e86f68644e5c39d351ede16a20c71ebf8b Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Mon, 7 Jul 2025 15:57:29 +0000 Subject: [PATCH 05/13] Add tests for pva tables and prefix --- tests/transport/epics/pva/test_pva_gui.py | 46 +++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 tests/transport/epics/pva/test_pva_gui.py diff --git a/tests/transport/epics/pva/test_pva_gui.py b/tests/transport/epics/pva/test_pva_gui.py new file mode 100644 index 000000000..1c27cdd2e --- /dev/null +++ b/tests/transport/epics/pva/test_pva_gui.py @@ -0,0 +1,46 @@ +from numpy import uint32 +from pvi.device import ( + LED, + SignalR, + SignalW, + TableRead, + TableWrite, + TextRead, + TextWrite, +) + +from fastcs.attributes import AttrR, AttrW +from fastcs.datatypes import Table +from fastcs.transport.epics.gui import PvaEpicsGUI + + +def test_get_pv_in_pva(controller_api): + gui = PvaEpicsGUI(controller_api, "DEVICE") + + assert gui._get_pv([], "A") == "pva://DEVICE:A" + assert gui._get_pv(["B"], "C") == "pva://DEVICE:B:C" + assert gui._get_pv(["D", "E"], "F") == "pva://DEVICE:D:E:F" + + +def test_get_attribute_component_table_write(controller_api): + gui = PvaEpicsGUI(controller_api, "DEVICE") + + assert gui._get_attribute_component( + [], "Table", AttrW(Table(structured_dtype=[("FIELD", uint32)])) + ) == SignalW( + name="Table", write_pv="Table", write_widget=TableWrite(widgets=[TextWrite()]) + ) + + +def test_get_attribute_component_table_read(controller_api): + gui = PvaEpicsGUI(controller_api, "DEVICE") + + assert gui._get_attribute_component( + [], "Table", AttrR(Table(structured_dtype=[("FIELD", uint32)])) + ) == SignalR( + name="Table", + read_pv="Table", + read_widget=TableRead( + widgets=[TextRead()] * 4 + [LED()] * 6 + [TextRead()] + [LED()] * 6 + ), + ) From a100fc3e2cd30d37f30f6df8804c7ae5178f3946 Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Fri, 18 Jul 2025 13:17:46 +0000 Subject: [PATCH 06/13] Get widgets from Table column datatypes --- src/fastcs/transport/epics/gui.py | 67 +++++++++++++++++++------------ src/fastcs/util.py | 15 +++++++ 2 files changed, 56 insertions(+), 26 deletions(-) diff --git a/src/fastcs/transport/epics/gui.py b/src/fastcs/transport/epics/gui.py index 14515e371..414abf863 100644 --- a/src/fastcs/transport/epics/gui.py +++ b/src/fastcs/transport/epics/gui.py @@ -27,9 +27,18 @@ from fastcs.attributes import Attribute, AttrR, AttrRW, AttrW from fastcs.controller_api import ControllerAPI from fastcs.cs_methods import Command -from fastcs.datatypes import Bool, Enum, Float, Int, String, Table, Waveform +from fastcs.datatypes import ( + Bool, + DataType, + Enum, + Float, + Int, + String, + Table, + Waveform, +) from fastcs.exceptions import FastCSException -from fastcs.util import snake_to_pascal +from fastcs.util import numpy_to_fastcs_datatype, snake_to_pascal from .options import EpicsGUIFormat, EpicsGUIOptions @@ -47,9 +56,8 @@ def _get_pv(self, attr_path: list[str], name: str): ) return f"{attr_prefix}:{snake_to_pascal(name)}" - @staticmethod - def _get_read_widget(attribute: AttrR) -> ReadWidgetUnion | None: - match attribute.datatype: + def _get_read_widget(self, fastcs_datatype: DataType) -> ReadWidgetUnion | None: + match fastcs_datatype: case Bool(): return LED() case Int() | Float(): @@ -63,9 +71,8 @@ def _get_read_widget(attribute: AttrR) -> ReadWidgetUnion | None: case datatype: raise FastCSException(f"Unsupported type {type(datatype)}: {datatype}") - @staticmethod - def _get_write_widget(attribute: AttrW) -> WriteWidgetUnion | None: - match attribute.datatype: + def _get_write_widget(self, fastcs_datatype: DataType) -> WriteWidgetUnion | None: + match fastcs_datatype: case Bool(): return ToggleButton() case Int() | Float(): @@ -73,7 +80,7 @@ def _get_write_widget(attribute: AttrW) -> WriteWidgetUnion | None: case String(): return TextWrite(format=TextFormat.string) case Enum(): - return ComboBox(choices=attribute.datatype.names) + return ComboBox(choices=fastcs_datatype.names) case Waveform(): return None case datatype: @@ -86,8 +93,8 @@ def _get_attribute_component( name = snake_to_pascal(name) match attribute: case AttrRW(): - read_widget = self._get_read_widget(attribute) - write_widget = self._get_write_widget(attribute) + read_widget = self._get_read_widget(attribute.datatype) + write_widget = self._get_write_widget(attribute.datatype) if write_widget is None or read_widget is None: return None return SignalRW( @@ -98,12 +105,12 @@ def _get_attribute_component( read_widget=read_widget, ) case AttrR(): - read_widget = self._get_read_widget(attribute) + read_widget = self._get_read_widget(attribute.datatype) if read_widget is None: return None return SignalR(name=name, read_pv=pv, read_widget=read_widget) case AttrW(): - write_widget = self._get_write_widget(attribute) + write_widget = self._get_write_widget(attribute.datatype) if write_widget is None: return None return SignalW(name=name, write_pv=pv, write_widget=write_widget) @@ -200,18 +207,26 @@ class PvaEpicsGUI(EpicsGUI): def _get_pv(self, attr_path: list[str], name: str): return f"pva://{super()._get_pv(attr_path, name)}" - @staticmethod - def _get_read_widget(attribute: AttrR) -> ReadWidgetUnion | None: - if isinstance(attribute.datatype, Table): - return TableRead( - widgets=[TextRead()] * 4 + [LED()] * 6 + [TextRead()] + [LED()] * 6 - ) + def _get_read_widget(self, fastcs_datatype: DataType) -> ReadWidgetUnion | None: + 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 EpicsGUI._get_read_widget(attribute) # noqa: SLF001 - - @staticmethod - def _get_write_widget(attribute: AttrW) -> WriteWidgetUnion | None: - if isinstance(attribute.datatype, Table): - return TableWrite(widgets=[TextWrite()]) + return super()._get_read_widget(fastcs_datatype) + + def _get_write_widget(self, fastcs_datatype: DataType) -> WriteWidgetUnion | None: + if isinstance(fastcs_datatype, Table): + fastcs_datatypes = [ + numpy_to_fastcs_datatype(datatype) + for _, datatype in fastcs_datatype.structured_dtype + ] + base_get_write_widget = super()._get_write_widget + widgets = [base_get_write_widget(datatype) for datatype in fastcs_datatypes] + return TableWrite(widgets=widgets) # type: ignore else: - return EpicsGUI._get_write_widget(attribute) # noqa: SLF001 + return super()._get_write_widget(fastcs_datatype) diff --git a/src/fastcs/util.py b/src/fastcs/util.py index 1ec8c3925..2241c4d2d 100644 --- a/src/fastcs/util.py +++ b/src/fastcs/util.py @@ -1,5 +1,9 @@ import re +import numpy as np + +from fastcs.datatypes import Bool, DataType, Float, Int, String + def snake_to_pascal(name: str) -> str: """Converts string from snake case to Pascal case. @@ -8,3 +12,14 @@ def snake_to_pascal(name: str) -> str: if re.fullmatch(r"[a-z][a-z0-9]*(?:_[a-z0-9]+)*", name): name = re.sub(r"(?:^|_)([a-z0-9])", lambda match: match.group(1).upper(), name) return name + + +def numpy_to_fastcs_datatype(np_type) -> DataType: + if np.issubdtype(np_type, np.integer): + return Int() + elif np.issubdtype(np_type, np.floating): + return Float() + elif np.issubdtype(np_type, np.bool_): + return Bool() + else: + return String() From e3be5e12751231838d6f740241ff213498e095d0 Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Fri, 18 Jul 2025 13:29:25 +0000 Subject: [PATCH 07/13] Amend EpicGui tests to pass datatype --- tests/transport/epics/ca/test_gui.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/transport/epics/ca/test_gui.py b/tests/transport/epics/ca/test_gui.py index cf1d47b8c..56ac3fb6d 100644 --- a/tests/transport/epics/ca/test_gui.py +++ b/tests/transport/epics/ca/test_gui.py @@ -78,12 +78,14 @@ def test_get_attribute_component_none(mocker, controller_api): assert gui._get_attribute_component([], "Attr", AttrRW(Int())) is None -def test_get_read_widget_none(): - assert EpicsGUI._get_read_widget(AttrR(Waveform(np.int32))) is None +def test_get_read_widget_none(controller_api): + gui = EpicsGUI(controller_api, "DEVICe") + assert gui._get_read_widget(fastcs_datatype=Waveform(np.int32)) is None -def test_get_write_widget_none(): - assert EpicsGUI._get_write_widget(AttrW(Waveform(np.int32))) is None +def test_get_write_widget_none(controller_api): + gui = EpicsGUI(controller_api, "DEVICe") + assert gui._get_write_widget(fastcs_datatype=Waveform(np.int32)) is None def test_get_components(controller_api): From 2f75e398516250651ef9d1ea08c5e15b8376cfc0 Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Fri, 18 Jul 2025 14:00:45 +0000 Subject: [PATCH 08/13] Add test for np to fastcs dtype conversion --- tests/test_util.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/test_util.py b/tests/test_util.py index c60711386..f6cb87b3c 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,8 +1,10 @@ +import numpy as np import pytest from pvi.device import SignalR from pydantic import ValidationError -from fastcs.util import snake_to_pascal +from fastcs.datatypes import Bool, Float, Int, String +from fastcs.util import numpy_to_fastcs_datatype, snake_to_pascal def test_snake_to_pascal(): @@ -36,3 +38,21 @@ def test_pvi_validation_error(): name = snake_to_pascal("Name-With_%_Invalid-&-Symbols_£_") with pytest.raises(ValidationError): SignalR(name=name, read_pv="test") + + +@pytest.mark.parametrize( + "numpy_type, fastcs_datatype", + [ + (np.float16, Float()), + (np.float32, Float()), + (np.int16, Int()), + (np.int32, Int()), + (np.bool, Bool()), + (np.dtype("S1000"), String()), + (np.dtype("U25"), String()), + (np.dtype(">i4"), Int()), + (np.dtype("d"), Float()), + ], +) +def test_numpy_to_fastcs_datatype(numpy_type, fastcs_datatype): + assert fastcs_datatype == numpy_to_fastcs_datatype(numpy_type) From abae72da79fc32913a899ce93252a43d57f5c148 Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Fri, 18 Jul 2025 14:05:17 +0000 Subject: [PATCH 09/13] Add docstring to util function --- src/fastcs/util.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/fastcs/util.py b/src/fastcs/util.py index 2241c4d2d..e4f526a49 100644 --- a/src/fastcs/util.py +++ b/src/fastcs/util.py @@ -15,6 +15,9 @@ def snake_to_pascal(name: str) -> str: def numpy_to_fastcs_datatype(np_type) -> DataType: + """Converts numpy types to fastcs types for widget creation. + Only types important for widget creation are explicitly converted + """ if np.issubdtype(np_type, np.integer): return Int() elif np.issubdtype(np_type, np.floating): From 839c39de547a12263be0fc43e974c63e03955810 Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Fri, 18 Jul 2025 14:16:54 +0000 Subject: [PATCH 10/13] Amend get_pv variable name from attr to node --- src/fastcs/transport/epics/gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fastcs/transport/epics/gui.py b/src/fastcs/transport/epics/gui.py index 414abf863..26c5362b2 100644 --- a/src/fastcs/transport/epics/gui.py +++ b/src/fastcs/transport/epics/gui.py @@ -52,7 +52,7 @@ def __init__(self, controller_api: ControllerAPI, pv_prefix: str) -> None: def _get_pv(self, attr_path: list[str], name: str): attr_prefix = ":".join( - [self._pv_prefix] + [snake_to_pascal(attr) for attr in attr_path] + [self._pv_prefix] + [snake_to_pascal(node) for node in attr_path] ) return f"{attr_prefix}:{snake_to_pascal(name)}" From 074eb5d1ce120bb12217ecda37fbb250f1b77738 Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Wed, 23 Jul 2025 15:15:52 +0000 Subject: [PATCH 11/13] Replace row ToggleButton with Checkbox widget --- src/fastcs/transport/epics/gui.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/fastcs/transport/epics/gui.py b/src/fastcs/transport/epics/gui.py index 26c5362b2..c83db2b7e 100644 --- a/src/fastcs/transport/epics/gui.py +++ b/src/fastcs/transport/epics/gui.py @@ -2,6 +2,7 @@ from pvi.device import ( LED, ButtonPanel, + CheckBox, ComboBox, ComponentUnion, Device, @@ -213,20 +214,24 @@ def _get_read_widget(self, fastcs_datatype: DataType) -> ReadWidgetUnion | None: 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_write_widget(self, fastcs_datatype: DataType) -> WriteWidgetUnion | None: if isinstance(fastcs_datatype, Table): - fastcs_datatypes = [ - numpy_to_fastcs_datatype(datatype) - for _, datatype in fastcs_datatype.structured_dtype - ] - base_get_write_widget = super()._get_write_widget - widgets = [base_get_write_widget(datatype) for datatype in fastcs_datatypes] - return TableWrite(widgets=widgets) # type: ignore + widgets = [] + for _, datatype in fastcs_datatype.structured_dtype: + fastcs_datatype = numpy_to_fastcs_datatype(datatype) + widget = super()._get_write_widget(fastcs_datatype) + if isinstance(widget, ToggleButton): + # Replace with compact version for Table row + widget = CheckBox() + widgets.append(widget) + return TableWrite(widgets=widgets) else: return super()._get_write_widget(fastcs_datatype) From 0509505be74075fc4080448f4d786599aa7c3e67 Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Wed, 23 Jul 2025 15:38:05 +0000 Subject: [PATCH 12/13] Fix typo and test for table row widgets --- tests/transport/epics/ca/test_gui.py | 4 +- tests/transport/epics/pva/test_pva_gui.py | 54 ++++++++++++++++++----- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/tests/transport/epics/ca/test_gui.py b/tests/transport/epics/ca/test_gui.py index 56ac3fb6d..80a6da4dc 100644 --- a/tests/transport/epics/ca/test_gui.py +++ b/tests/transport/epics/ca/test_gui.py @@ -79,12 +79,12 @@ def test_get_attribute_component_none(mocker, controller_api): def test_get_read_widget_none(controller_api): - gui = EpicsGUI(controller_api, "DEVICe") + gui = EpicsGUI(controller_api, "DEVICE") assert gui._get_read_widget(fastcs_datatype=Waveform(np.int32)) is None def test_get_write_widget_none(controller_api): - gui = EpicsGUI(controller_api, "DEVICe") + gui = EpicsGUI(controller_api, "DEVICE") assert gui._get_write_widget(fastcs_datatype=Waveform(np.int32)) is None diff --git a/tests/transport/epics/pva/test_pva_gui.py b/tests/transport/epics/pva/test_pva_gui.py index 1c27cdd2e..b83e8a102 100644 --- a/tests/transport/epics/pva/test_pva_gui.py +++ b/tests/transport/epics/pva/test_pva_gui.py @@ -1,10 +1,12 @@ -from numpy import uint32 +import numpy as np from pvi.device import ( LED, + CheckBox, SignalR, SignalW, TableRead, TableWrite, + TextFormat, TextRead, TextWrite, ) @@ -25,22 +27,50 @@ def test_get_pv_in_pva(controller_api): def test_get_attribute_component_table_write(controller_api): gui = PvaEpicsGUI(controller_api, "DEVICE") - assert gui._get_attribute_component( - [], "Table", AttrW(Table(structured_dtype=[("FIELD", uint32)])) - ) == SignalW( - name="Table", write_pv="Table", write_widget=TableWrite(widgets=[TextWrite()]) + attribute_component = gui._get_attribute_component( + [], + "Table", + AttrW( + Table( + structured_dtype=[ + ("FIELD1", np.uint32), + ("FIELD2", np.bool), + ("FIELD3", np.dtype("S1000")), + ] + ) + ), ) + assert isinstance(attribute_component, SignalW) + assert isinstance(attribute_component.write_widget, TableWrite) + assert attribute_component.write_widget.widgets == [ + TextWrite(), + CheckBox(), + TextWrite(format=TextFormat.string), + ] + def test_get_attribute_component_table_read(controller_api): gui = PvaEpicsGUI(controller_api, "DEVICE") - assert gui._get_attribute_component( - [], "Table", AttrR(Table(structured_dtype=[("FIELD", uint32)])) - ) == SignalR( - name="Table", - read_pv="Table", - read_widget=TableRead( - widgets=[TextRead()] * 4 + [LED()] * 6 + [TextRead()] + [LED()] * 6 + attribute_component = gui._get_attribute_component( + [], + "Table", + AttrR( + Table( + structured_dtype=[ + ("FIELD1", np.uint32), + ("FIELD2", np.bool), + ("FIELD3", np.dtype("S1000")), + ] + ) ), ) + + assert isinstance(attribute_component, SignalR) + assert isinstance(attribute_component.read_widget, TableRead) + assert attribute_component.read_widget.widgets == [ + TextRead(), + LED(), + TextRead(format=TextFormat.string), + ] From a37abf2f82819785ba7cbb95d3e51233d788c8da Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Thu, 24 Jul 2025 11:16:08 +0000 Subject: [PATCH 13/13] Implement review changes --- src/fastcs/transport/epics/gui.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/fastcs/transport/epics/gui.py b/src/fastcs/transport/epics/gui.py index c83db2b7e..698102b9a 100644 --- a/src/fastcs/transport/epics/gui.py +++ b/src/fastcs/transport/epics/gui.py @@ -227,10 +227,11 @@ def _get_write_widget(self, fastcs_datatype: DataType) -> WriteWidgetUnion | Non widgets = [] for _, datatype in fastcs_datatype.structured_dtype: fastcs_datatype = numpy_to_fastcs_datatype(datatype) - widget = super()._get_write_widget(fastcs_datatype) - if isinstance(widget, ToggleButton): + 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: