From ce91ed793389d3e9a149420c5f3d208c92d6c77f Mon Sep 17 00:00:00 2001 From: "Sode, Adedamola (DLSLtd,RAL,LSCI)" Date: Tue, 25 Aug 2026 09:34:14 +0000 Subject: [PATCH 1/5] Added the ability to template for fastcs modules --- src/techui_builder/builder.py | 4 ++++ src/techui_builder/generate.py | 19 +++++++++++++------ src/techui_builder/models.py | 4 ++++ 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/techui_builder/builder.py b/src/techui_builder/builder.py index 18d84909..e13851f9 100644 --- a/src/techui_builder/builder.py +++ b/src/techui_builder/builder.py @@ -158,6 +158,9 @@ def _extract_entities(self, service_name: str, service_yaml: Path): entity_key = match.group() for entity in ioc_conf[entity_key]: + if entity["type"].startswith("fastcs"): + entity["type"] = "fastcs*" + if entity["type"] in self.techui_support.support_modules: support_mapping: SupportEntity = ( self.techui_support.support_modules[entity["type"]] @@ -216,6 +219,7 @@ def create_screens(self): # with the same prefix as the component for entity in self.entities[component.prefix]: entity.child_labels = component.child_labels + entity.file = component.file screen_entities.extend(self.entities[component.prefix]) diff --git a/src/techui_builder/generate.py b/src/techui_builder/generate.py index b1dee7f9..b48bd9fc 100644 --- a/src/techui_builder/generate.py +++ b/src/techui_builder/generate.py @@ -199,13 +199,17 @@ def _allocate_widget( "Only related displays can have remote screens" ) else: - screen_path = self.support_path / f"bob/{file}" - logger_.debug(f"Screen path: {screen_path}") + support_bob = (self.support_path / "bob").resolve() + configured_path = Path(file) - # Path of screen relative to synoptic/ - support_screen_path = screen_path.relative_to( - self.synoptic_dir, walk_up=True - ) + if configured_path.is_absolute(): + screen_path = configured_path.resolve() + elif configured_path.parts[:2] == ("techui-support", "bob"): + screen_path = (self.synoptic_dir / configured_path).resolve() + else: + screen_path = (support_bob / configured_path).resolve() + + support_screen_path = screen_path.relative_to(self.synoptic_dir.resolve()) # For Gui Components with multiple components embedded, we add a suffix field # to the components, and adjust the name and suffix accordingly @@ -283,6 +287,9 @@ def _create_widgets( {name}. Skipping..." ) return None + # if component is fastcs, and has the field of file, add it to the support + if component.file: + screen_mapping.append({"file": component.file, "type": "embedded"}) for screen_dict in screen_mapping: new_widget.append(self._allocate_widget(screen_dict, component)) diff --git a/src/techui_builder/models.py b/src/techui_builder/models.py index 2cfc27cc..23fa4a74 100644 --- a/src/techui_builder/models.py +++ b/src/techui_builder/models.py @@ -306,6 +306,10 @@ class Entity(BaseModel): dict[str, Any], Field(description="Macros for the matching screen (can be empty)"), ] + file: Annotated[ + str | None, + Field(description="File path for custom screen"), + ] = None class SupportEntity(BaseModel): From 22048fe65a385f07d0fdcdff167153e842093fe0 Mon Sep 17 00:00:00 2001 From: "Sode, Adedamola (DLSLtd,RAL,LSCI)" Date: Thu, 27 Aug 2026 08:36:26 +0000 Subject: [PATCH 2/5] Added remote fetching of files to be able to determine their sizes --- src/techui_builder/generate.py | 36 ++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/techui_builder/generate.py b/src/techui_builder/generate.py index b48bd9fc..9b54a8aa 100644 --- a/src/techui_builder/generate.py +++ b/src/techui_builder/generate.py @@ -4,8 +4,13 @@ from collections import defaultdict from collections.abc import Mapping from dataclasses import dataclass, field +from io import BytesIO from pathlib import Path +<<<<<<< HEAD +======= +import requests +>>>>>>> 28634f6 (Added remote fetching of files to be able to determine their sizes) from jinja2 import Template from lxml import objectify from phoebusgen import screen as pscreen @@ -42,13 +47,17 @@ class Generator: group_padding: int = field(default=50, init=False, repr=False) label_flag: bool = field(default=False, init=False, repr=False) - def _get_screen_dimensions(self, file: str) -> tuple[int, int]: + def _get_screen_dimensions(self, file: Path | bytes) -> tuple[int, int]: """ Parses the bob files for information on the height and width of the screen """ # Read the bob file - tree = objectify.parse(file) + if isinstance(file, bytes): + tree = objectify.parse(BytesIO(file)) + else: + tree = objectify.parse(str(file)) + root = tree.getroot() try: height_element = root.height @@ -189,15 +198,26 @@ def _allocate_widget( # Get relative path to screen file = Template(screen_mapping["file"]).render(component.macros) +<<<<<<< HEAD +======= + + # IF the file starts with IOC, and needs macro expansion +>>>>>>> 28634f6 (Added remote fetching of files to be able to determine their sizes) if file.startswith("$(IOC)"): screen_path = support_screen_path = file.replace( "$(IOC)", f"{self.beamline_url}/{component.service_name}" - ) # Only works with related displays as - # embedded displays need to access the file to get dimensions - - assert screen_mapping["type"] == "related", ( - "Only related displays can have remote screens" ) + # For embedded screens, that need to be placed on screen and dimensions, + # it is required to fetch the screen from remote + if screen_mapping["type"] == "embedded" and str( + support_screen_path + ).startswith("https"): + try: + screen_path = requests.get(str(support_screen_path)).content + except requests.RequestException: + logger_.warning( + f"Could not retrieve file from link {support_screen_path}" + ) else: support_bob = (self.support_path / "bob").resolve() configured_path = Path(file) @@ -232,7 +252,7 @@ def _allocate_widget( pass if screen_mapping["type"] == "embedded": - height, width = self._get_screen_dimensions(str(screen_path)) + height, width = self._get_screen_dimensions(screen_path) new_widget = pwidget.EmbeddedDisplay( component_name, str(support_screen_path), From 53b87133455fac263c0e1d7610001a64006451cc Mon Sep 17 00:00:00 2001 From: "Sode, Adedamola (DLSLtd,RAL,LSCI)" Date: Fri, 28 Aug 2026 08:06:29 +0000 Subject: [PATCH 3/5] Ensured the labels are treated correctly for the fastcs screens --- src/techui_builder/builder.py | 2 ++ src/techui_builder/generate.py | 6 +++++- src/techui_builder/models.py | 4 ++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/techui_builder/builder.py b/src/techui_builder/builder.py index e13851f9..67d424f5 100644 --- a/src/techui_builder/builder.py +++ b/src/techui_builder/builder.py @@ -159,6 +159,7 @@ def _extract_entities(self, service_name: str, service_yaml: Path): for entity in ioc_conf[entity_key]: if entity["type"].startswith("fastcs"): + entity["name"] = entity["type"] entity["type"] = "fastcs*" if entity["type"] in self.techui_support.support_modules: @@ -185,6 +186,7 @@ def _extract_entities(self, service_name: str, service_yaml: Path): pv_root = prefix.split(":", maxsplit=1)[0] self.entities[pv_root].append(new_entity) + break def _generate_screen(self, screen_name: str): diff --git a/src/techui_builder/generate.py b/src/techui_builder/generate.py index 9b54a8aa..9ad8d821 100644 --- a/src/techui_builder/generate.py +++ b/src/techui_builder/generate.py @@ -171,7 +171,11 @@ def _update_macros(self, component: Entity) -> tuple[str, dict[str, str]]: suffix_key = next(k for k, v in component.macros.items() if v == suffix) except (IndexError, ValueError): prefix = component.prefix - component_name = component.type + component_name = ( + component.name + if component.type == "fastcs*" and component.name is not None + else component.type + ) suffix_key = suffix = "" # Try to get name from child labels if they exist, diff --git a/src/techui_builder/models.py b/src/techui_builder/models.py index 23fa4a74..1a2277b8 100644 --- a/src/techui_builder/models.py +++ b/src/techui_builder/models.py @@ -310,6 +310,10 @@ class Entity(BaseModel): str | None, Field(description="File path for custom screen"), ] = None + name: Annotated[ + str | None, + Field(description="placeholder name for fastcs screens"), + ] = None class SupportEntity(BaseModel): From 7e643e0c52221d12308d913238b67b874de29313 Mon Sep 17 00:00:00 2001 From: "Sode, Adedamola (DLSLtd,RAL,LSCI)" Date: Fri, 28 Aug 2026 10:32:53 +0000 Subject: [PATCH 4/5] sorted merge conflicts --- src/techui_builder/generate.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/techui_builder/generate.py b/src/techui_builder/generate.py index 9ad8d821..55f0db6d 100644 --- a/src/techui_builder/generate.py +++ b/src/techui_builder/generate.py @@ -7,10 +7,7 @@ from io import BytesIO from pathlib import Path -<<<<<<< HEAD -======= import requests ->>>>>>> 28634f6 (Added remote fetching of files to be able to determine their sizes) from jinja2 import Template from lxml import objectify from phoebusgen import screen as pscreen @@ -202,11 +199,8 @@ def _allocate_widget( # Get relative path to screen file = Template(screen_mapping["file"]).render(component.macros) -<<<<<<< HEAD -======= # IF the file starts with IOC, and needs macro expansion ->>>>>>> 28634f6 (Added remote fetching of files to be able to determine their sizes) if file.startswith("$(IOC)"): screen_path = support_screen_path = file.replace( "$(IOC)", f"{self.beamline_url}/{component.service_name}" From 5e18f38b87b927e95203f642c3568640f44bb0a2 Mon Sep 17 00:00:00 2001 From: "Sode, Adedamola (DLSLtd,RAL,LSCI)" Date: Fri, 28 Aug 2026 12:49:36 +0000 Subject: [PATCH 5/5] Fixed tests --- .../services/bl01t-ea-temp-01/config/fastcs.yaml | 2 +- src/techui_builder/builder.py | 4 +++- tests/conftest.py | 4 +--- tests/test_builder.py | 8 +++++--- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/example/t01-services/services/bl01t-ea-temp-01/config/fastcs.yaml b/example/t01-services/services/bl01t-ea-temp-01/config/fastcs.yaml index 790d6aed..a06071fc 100644 --- a/example/t01-services/services/bl01t-ea-temp-01/config/fastcs.yaml +++ b/example/t01-services/services/bl01t-ea-temp-01/config/fastcs.yaml @@ -1,6 +1,6 @@ # yaml-language-server: $schema=schema.json controllers: - - name: BL01T-EA-TEMP-01 + - id: BL01T-EA-TEMP-01 type: fastcs.TemperatureController ip_settings: ip: "localhost" diff --git a/src/techui_builder/builder.py b/src/techui_builder/builder.py index 67d424f5..0e8e286b 100644 --- a/src/techui_builder/builder.py +++ b/src/techui_builder/builder.py @@ -158,8 +158,9 @@ def _extract_entities(self, service_name: str, service_yaml: Path): entity_key = match.group() for entity in ioc_conf[entity_key]: + component_name = None if entity["type"].startswith("fastcs"): - entity["name"] = entity["type"] + component_name = entity["type"] entity["type"] = "fastcs*" if entity["type"] in self.techui_support.support_modules: @@ -182,6 +183,7 @@ def _extract_entities(self, service_name: str, service_yaml: Path): desc=entity.get("desc", None), prefix=prefix, macros=macros, + name=component_name, ) pv_root = prefix.split(":", maxsplit=1)[0] diff --git a/tests/conftest.py b/tests/conftest.py index 28eb8175..985f8765 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -102,9 +102,7 @@ def techui_support(): }, ], ), - "fastcs.TemperatureController": SupportEntity( - prefix="{{ name }}", macros=["name"], screens=[{}] - ), + "fastcs*": SupportEntity(prefix="{{ id }}", macros=["id"], screens=[{}]), } return ts diff --git a/tests/test_builder.py b/tests/test_builder.py index bc9766f1..f5e64720 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -125,19 +125,20 @@ def test_gb_extract_entities_ioc_yaml( @pytest.mark.parametrize( - "index, type, desc, pv, macros", + "index, type, name, desc, pv, macros", [ ( 0, + "fastcs*", "fastcs.TemperatureController", None, "BL01T-EA-TEMP-01", - {"name": "BL01T-EA-TEMP-01"}, + {"id": "BL01T-EA-TEMP-01"}, ), ], ) def test_gb_extract_entities_fastcs_yaml( - builder, techui_support, index, type, desc, pv, macros + builder, techui_support, index, type, name, desc, pv, macros ): # We don't want to use builder_with_setup as that calls _extract_services() # and in turn that calls _extract_entities() @@ -151,6 +152,7 @@ def test_gb_extract_entities_fastcs_yaml( ) entity = builder.entities[prefix][index] assert entity.type == type + assert entity.name == name assert entity.desc == desc assert entity.prefix == pv assert entity.macros == macros