From 17211a81dd73d276b87861f6998501a2a512769e Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Wed, 10 Dec 2025 14:39:44 +0000 Subject: [PATCH 1/6] Add opis URL to Beamline model --- src/techui_builder/models.py | 13 +++++++++++++ tests/test_models.py | 7 ++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/techui_builder/models.py b/src/techui_builder/models.py index 81091ef4..16505235 100644 --- a/src/techui_builder/models.py +++ b/src/techui_builder/models.py @@ -46,6 +46,7 @@ ) _LONG_DOM_RE = re.compile(r"^[a-zA-Z]{2}\d{2}[a-zA-Z]$") _SHORT_DOM_RE = re.compile(r"^[a-zA-Z]{1}\d{2}(-[0-9]{1})?$") +_OPIS_URL_RE = re.compile(r"^[a-z0-9]{3}-(?:[0-9]-)?opis(?:.[a-z0-9]*)*") class Beamline(BaseModel): @@ -53,6 +54,7 @@ class Beamline(BaseModel): long_dom: str = Field(description="Full BL domain e.g. bl23b") desc: str = Field(description="Description") model_config = ConfigDict(extra="forbid") + url: str = Field(description="URL of ixx-opis") @field_validator("short_dom") @classmethod @@ -75,6 +77,17 @@ def normalize_long_dom(cls, v: str) -> str: raise ValueError("Invalid long dom.") + @field_validator("url") + @classmethod + def check_url(cls, url: str) -> str: + url = url.strip().lower() + if _OPIS_URL_RE.fullmatch(url): + # url in correct format + # e.g. t01-opis.diamond.ac.uk + return url + + raise ValueError("Invalid opis URL.") + class Component(BaseModel): prefix: str diff --git a/tests/test_models.py b/tests/test_models.py index 67e4652e..7e66e00f 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -10,7 +10,12 @@ @pytest.fixture def beamline() -> Beamline: - return Beamline(short_dom="t01", long_dom="bl01t", desc="Test Beamline") + return Beamline( + short_dom="t01", + long_dom="bl01t", + desc="Test Beamline", + url="t01-opis.diamond.ac.uk", + ) @pytest.fixture From 2ee88081303a9e6b747de0a779480d8128a3a732 Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Wed, 10 Dec 2025 14:43:34 +0000 Subject: [PATCH 2/6] Add url to t01-services techui.yaml --- example/t01-services/synoptic/techui.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/example/t01-services/synoptic/techui.yaml b/example/t01-services/synoptic/techui.yaml index ad5a822b..616bc733 100644 --- a/example/t01-services/synoptic/techui.yaml +++ b/example/t01-services/synoptic/techui.yaml @@ -3,6 +3,7 @@ beamline: short_dom: t01 long_dom: bl01t desc: Test Beamline + url: t01-opis.diamond.ac.uk components: fshtr: From d4d936274820d06dd741e2f1369bff385712aaa9 Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Wed, 10 Dec 2025 14:44:15 +0000 Subject: [PATCH 3/6] Add jinja2 dependency --- pyproject.toml | 1 + uv.lock | 2 ++ 2 files changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 0501d3f6..1236b1ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ dependencies = [ "typer>=0.16.0", "rich>=14.1.0", "pydantic>=2.11.7", + "jinja2>=3.1.6", ] scripts = { techui-builder = "techui_builder.__main__:app" } diff --git a/uv.lock b/uv.lock index bdce5533..4e06362c 100644 --- a/uv.lock +++ b/uv.lock @@ -981,6 +981,7 @@ wheels = [ name = "techui-builder" source = { editable = "." } dependencies = [ + { name = "jinja2" }, { name = "lxml" }, { name = "phoebusgen" }, { name = "pydantic" }, @@ -1011,6 +1012,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "jinja2", specifier = ">=3.1.6" }, { name = "lxml", specifier = ">=5.4.0" }, { name = "phoebusgen", specifier = ">=3.0.0" }, { name = "pydantic", specifier = ">=2.11.7" }, From 4ee49f7c41c7019896d1f457cada05bd32626663 Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Wed, 10 Dec 2025 14:46:30 +0000 Subject: [PATCH 4/6] Delete MOTOR.bob and add motor_embed.bob.jinja with some jinja tags --- .../techui-support/bob/pmac/MOTOR.bob | 1585 ----------------- .../bob/pmac/motor_embed.bob.jinja | 241 +++ 2 files changed, 241 insertions(+), 1585 deletions(-) delete mode 100644 example-synoptic/b23-services/synoptic/techui-support/bob/pmac/MOTOR.bob create mode 100644 example-synoptic/b23-services/synoptic/techui-support/bob/pmac/motor_embed.bob.jinja diff --git a/example-synoptic/b23-services/synoptic/techui-support/bob/pmac/MOTOR.bob b/example-synoptic/b23-services/synoptic/techui-support/bob/pmac/MOTOR.bob deleted file mode 100644 index 47b79730..00000000 --- a/example-synoptic/b23-services/synoptic/techui-support/bob/pmac/MOTOR.bob +++ /dev/null @@ -1,1585 +0,0 @@ - - - MOTOR Subscreen - 832 - 800 - - - - - 4 - 4 - - Title - TITLE - MOTOR - 0 - 0 - 779 - 25 - - - - - - - - - true - 1 - - - Eloss - 5 - 30 - 352 - 56 - - - - - true - - Label - Eloss Clear - 2 - 1 - - - WritePV - - - $(pv_name) - 1 - $(name) - - - $(P):$(M):ELOSS:ELOSS-CLEAR - Go - 105 - 205 - 20 - - - - - - - - - $(actions) - - - - Limit Violation - 5 - 91 - 352 - 131 - - - - - true - - Label - User High Limit - 2 - 1 - - - Label - User Low Limit - 25 - 2 - 1 - - - Label - Dial High Limit - 50 - 2 - 1 - - - Label - Dial Low Limit - 75 - 2 - 1 - - - TextEntry_33 - $(P):$(M):LIMIT_VIOLATION:USER-HIGH-LIMIT - 105 - 205 - - - - - - - - - 1 - 1 - - - - - - - TextEntry_35 - $(P):$(M):LIMIT_VIOLATION:USER-LOW-LIMIT - 105 - 25 - 205 - - - - - - - - - 1 - 1 - - - - - - - TextEntry_37 - $(P):$(M):LIMIT_VIOLATION:DIAL-HIGH-LIMIT - 105 - 50 - 205 - - - - - - - - - 1 - 1 - - - - - - - TextEntry_38 - $(P):$(M):LIMIT_VIOLATION:DIAL-LOW-LIMIT - 105 - 75 - 205 - - - - - - - - - 1 - 1 - - - - - - - Rectangle_49 - 72 - 310 - 1 - 1 - - - - - - - - - - - Rectangle_50 - 47 - 310 - 1 - 1 - - - - - - - - - - - Rectangle_51 - 22 - 310 - 1 - 1 - - - - - - - - - - - - Kill - 5 - 227 - 352 - 56 - - - - - true - - Label - Kill - 2 - 1 - - - WritePV - - - $(pv_name) - 1 - $(name) - - - $(P):$(M):KILL:KILL - Go - 105 - 205 - 20 - - - - - - - - - $(actions) - - - - Sync Val Rbv - 5 - 288 - 352 - 56 - - - - - true - - Label - Sync Val RBV - 2 - 1 - - - WritePV - - - $(pv_name) - 1 - $(name) - - - $(P):$(M):SYNC_VAL_RBV:SYNC-VAL-RBV - Go - 105 - 205 - 20 - - - - - - - - - $(actions) - - - - Commands - 5 - 349 - 352 - 206 - - - - - true - - Label - Home Forward - 2 - 1 - - - WritePV - - - $(pv_name) - 1 - $(name) - - - $(P):$(M):COMMANDS:HOME-FORWARD - Go - 105 - 205 - 20 - - - - - - - - - $(actions) - - - Label - Home Reverse - 25 - 2 - 1 - - - WritePV - - - $(pv_name) - 1 - $(name) - - - $(P):$(M):COMMANDS:HOME-REVERSE - Go - 105 - 25 - 205 - 20 - - - - - - - - - $(actions) - - - Label - Jog Forward - 50 - 2 - 1 - - - WritePV - - - $(pv_name) - 1 - $(name) - - - $(P):$(M):COMMANDS:JOG-FORWARD - Go - 105 - 50 - 205 - 20 - - - - - - - - - $(actions) - - - Label - Jog Reverse - 75 - 2 - 1 - - - WritePV - - - $(pv_name) - 1 - $(name) - - - $(P):$(M):COMMANDS:JOG-REVERSE - Go - 105 - 75 - 205 - 20 - - - - - - - - - $(actions) - - - Label - Tweak Forward - 100 - 2 - 1 - - - WritePV - - - $(pv_name) - 1 - $(name) - - - $(P):$(M):COMMANDS:TWEAK-FORWARD - Go - 105 - 100 - 205 - 20 - - - - - - - - - $(actions) - - - Label - Tweak Reverse - 125 - 2 - 1 - - - WritePV - - - $(pv_name) - 1 - $(name) - - - $(P):$(M):COMMANDS:TWEAK-REVERSE - Go - 105 - 125 - 205 - 20 - - - - - - - - - $(actions) - - - Label - Tweak Step - 150 - 2 - 1 - - - TextEntry_40 - $(P):$(M):COMMANDS:TWEAK-STEP - 105 - 150 - 205 - - - - - - - - - 1 - 1 - - - - - - - Rectangle_43 - 147 - 310 - 1 - 1 - - - - - - - - - - - Rectangle_44 - 122 - 310 - 1 - 1 - - - - - - - - - - - Rectangle_45 - 97 - 310 - 1 - 1 - - - - - - - - - - - Rectangle_46 - 72 - 310 - 1 - 1 - - - - - - - - - - - Rectangle_47 - 47 - 310 - 1 - 1 - - - - - - - - - - - Rectangle_48 - 22 - 310 - 1 - 1 - - - - - - - - - - - - Calibration - 5 - 560 - 352 - 156 - - - - - true - - Label - Direction - 2 - 1 - - - ComboBox - $(P):$(M):CALIBRATION:DIRECTION - 105 - 205 - 20 - - - - - - - - - - Neg - Pos - - false - - - Label - User Offset - 25 - 2 - 1 - - - Label - Set Use - 50 - 2 - 1 - - - ComboBox - $(P):$(M):CALIBRATION:SET-USE - 105 - 50 - 205 - 20 - - - - - - - - - - Set - Use - - false - - - Label - Offset - 75 - 2 - 1 - - - ComboBox - $(P):$(M):CALIBRATION:OFFSET - 105 - 75 - 205 - 20 - - - - - - - - - - Variable - Fixed - - false - - - Label - Use Encoder - 100 - 2 - 1 - - - ComboBox - $(P):$(M):CALIBRATION:USE-ENCODER - 105 - 100 - 205 - 20 - - - - - - - - - - No - Yes - - false - - - TextEntry_41 - $(P):$(M):CALIBRATION:USER-OFFSET - 105 - 25 - 205 - - - - - - - - - 1 - 1 - - - - - - - Rectangle_39 - 97 - 310 - 1 - 1 - - - - - - - - - - - Rectangle_40 - 72 - 310 - 1 - 1 - - - - - - - - - - - Rectangle_41 - 47 - 310 - 1 - 1 - - - - - - - - - - - Rectangle_42 - 22 - 310 - 1 - 1 - - - - - - - - - - - - Resolution - 5 - 721 - 352 - 106 - - - - - true - - Label - Resolution - 2 - 1 - - - ComboBox - $(P):$(M):RESOLUTION:RESOLUTION - 105 - 205 - 20 - - - - - - - - - - 1 - 10 - 100 - - false - - - Label - Motor Step Size - 25 - 2 - 1 - - - Label - Encode Step Size - 50 - 2 - 1 - - - TextEntry_42 - $(P):$(M):RESOLUTION:MOTOR-STEP-SIZE - 105 - 25 - 205 - - - - - - - - - 1 - 1 - - - - - - - TextEntry_43 - $(P):$(M):CALIBRATION:USER-OFFSET - 105 - 50 - 205 - - - - - - - - - 1 - 1 - - - - - - - Rectangle_37 - 47 - 310 - 1 - 1 - - - - - - - - - - - Rectangle_38 - 22 - 310 - 1 - 1 - - - - - - - - - - - - Motion - 357 - 30 - 422 - 331 - - - - - true - - Label - Max Velocity - 170 - 2 - 1 - - - Label - Base Velocity - 25 - 170 - 2 - 1 - - - Label - Velocity - 50 - 170 - 2 - 1 - - - Label - Secs To Velocity - 75 - 170 - 2 - 1 - - - Label - JVEL - 100 - 170 - 2 - 1 - - - Label - Jog Acceleration - 125 - 170 - 2 - 1 - - - Label - Backlash Distance - 150 - 170 - 2 - 1 - - - Label - Backlash Velocity - 175 - 170 - 2 - 1 - - - Label - Backlash Secs To Velocity - 200 - 170 - 2 - 1 - - - Label - Move Fraction - 225 - 170 - 2 - 1 - - - Label - Retry Deadband - 250 - 170 - 2 - 1 - - - Label - Max Retries - 275 - 170 - 2 - 1 - - - TextEntry_20 - $(P):$(M):MOTION:BASE-VELOCITY - 175 - 25 - 205 - - - - - - - - - 1 - 1 - - - - - - - TextEntry_19 - $(P):$(M):MOTION:MAX-VELOCITY - 175 - 205 - - - - - - - - - 1 - 1 - - - - - - - TextEntry_21 - $(P):$(M):MOTION:VELOCITY - 175 - 50 - 205 - - - - - - - - - 1 - 1 - - - - - - - TextEntry_22 - $(P):$(M):MOTION:SECS-TO-VELOCITY - 175 - 75 - 205 - - - - - - - - - 1 - 1 - - - - - - - TextEntry_23 - $(P):$(M):MOTION:JVEL - 175 - 100 - 205 - - - - - - - - - 1 - 1 - - - - - - - TextEntry_24 - $(P):$(M):MOTION:JOG-ACCELERATION - 175 - 125 - 205 - - - - - - - - - 1 - 1 - - - - - - - TextEntry_25 - $(P):$(M):MOTION:BACKLASH-DISTANCE - 175 - 150 - 205 - - - - - - - - - 1 - 1 - - - - - - - TextEntry_26 - $(P):$(M):MOTION:BACKLASH-VELOCITY - 175 - 175 - 205 - - - - - - - - - 1 - 1 - - - - - - - TextEntry_27 - $(P):$(M):MOTION:BACKLASH-SECS-TO-VELOCITY - 175 - 200 - 205 - - - - - - - - - 1 - 1 - - - - - - - TextEntry_28 - $(P):$(M):MOTION:MOVE-FRACTION - 175 - 225 - 205 - - - - - - - - - 1 - 1 - - - - - - - TextEntry_29 - $(P):$(M):MOTION:RETRY-DEADBAND - 175 - 250 - 205 - - - - - - - - - 1 - 1 - - - - - - - TextEntry_30 - $(P):$(M):MOTION:MAX-RETRIES - 175 - 275 - 205 - - - - - - - - - 1 - 1 - - - - - - - Rectangle_26 - 272 - 380 - 1 - 1 - - - - - - - - - - - Rectangle_27 - 247 - 380 - 1 - 1 - - - - - - - - - - - Rectangle_28 - 222 - 380 - 1 - 1 - - - - - - - - - - - Rectangle_29 - 197 - 380 - 1 - 1 - - - - - - - - - - - Rectangle_30 - 172 - 380 - 1 - 1 - - - - - - - - - - - Rectangle_31 - 147 - 380 - 1 - 1 - - - - - - - - - - - Rectangle_32 - 122 - 380 - 1 - 1 - - - - - - - - - - - Rectangle_33 - 97 - 380 - 1 - 1 - - - - - - - - - - - Rectangle_34 - 72 - 380 - 1 - 1 - - - - - - - - - - - Rectangle_35 - 47 - 380 - 1 - 1 - - - - - - - - - - - Rectangle_36 - 22 - 380 - 1 - 1 - - - - - - - - - - - - Other - 357 - 361 - 422 - 81 - - - - - true - - Label - PREC - 170 - 2 - 1 - - - Label - EGU - 25 - 170 - 2 - 1 - - - TextEntry_31 - $(P):$(M):OTHER:PREC - 175 - 205 - - - - - - - - - 1 - 1 - - - - - - - TextEntry_32 - $(P):$(M):OTHER:EGU - 175 - 25 - 205 - - - - - - - - - 1 - 1 - - - - - - - Rectangle_25 - 22 - 380 - 1 - 1 - - - - - - - - - - - diff --git a/example-synoptic/b23-services/synoptic/techui-support/bob/pmac/motor_embed.bob.jinja b/example-synoptic/b23-services/synoptic/techui-support/bob/pmac/motor_embed.bob.jinja new file mode 100644 index 00000000..1dcdc1bb --- /dev/null +++ b/example-synoptic/b23-services/synoptic/techui-support/bob/pmac/motor_embed.bob.jinja @@ -0,0 +1,241 @@ + + + Main + 205 + 120 + + + + + + $(M) + 205 + 120 + + + + + + Tweak Left + + + $(pv_name) + value + $(name) + + + $(P):$(M).TWR + - + 10 + 30 + 20 + + + + + + + + + + + + + $(tooltip) + + + Tweak Right + + + $(pv_name) + value + $(name) + + + $(P):$(M).TWF + + + 140 + 10 + 30 + 20 + + + + + + + + + + + + + $(tooltip) + + + OpenDisplay + + + Open Display + {{url}}/{{p_lower}}/pmacAxis.pvi.bob + + :$(M) +

$(P)

+
+ tab +
+
+ More + 60 + 40 + 20 + + + + + + + + + + + + + $(actions) +
+ + WritePV_28 + + + $(pv_name) + value + $(name) + + + $(P):$(M).STOP + STOP + 130 + 60 + 40 + 20 + + + + + + + + + + + + + $(tooltip) + + + TextEntry_27 + $(P):$(M).TWV + 45 + 60 + 80 + + + + + + + + + 1 + 1 + + + + + + + Moving + $(P):$(M).DMOV + 150 + 35 + 20 + 1 + + + + + + + + + + + + + + + Serverity + $(P):$(M).SEVR + 35 + 20 + 1 + + + + + + + + + + + + + + + PV + $(P):$(M) + 35 + 10 + + + + + + + + + 1 + 1 + + + + + + + Readback PV + $(P):$(M).RBV + 25 + 35 + 120 + + + + + + + + + + + + + 1 + 1 + + + + + +
+
From 4dcfea1739c0110034fe4d98f7142ea6e26855dd Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Wed, 10 Dec 2025 15:08:41 +0000 Subject: [PATCH 5/6] Add basic Renderer class --- src/techui_builder/render.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/techui_builder/render.py diff --git a/src/techui_builder/render.py b/src/techui_builder/render.py new file mode 100644 index 00000000..d4497a74 --- /dev/null +++ b/src/techui_builder/render.py @@ -0,0 +1,19 @@ +from dataclasses import dataclass +from pathlib import Path + +from jinja2 import Environment, FileSystemLoader + +from techui_builder.models import Beamline + + +@dataclass +class Renderer: + support_screen_path: Path + screen_path: Path + beamline: Beamline + + def __post_init__(self): + self.env = Environment(loader=FileSystemLoader(self.support_screen_path)) + + def load_screen(self): + self.screen_template = self.env.get_template(self.screen_path.name) From e6e3eb4a818ad5fcf81236dabdca48d3eeb5d273 Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Thu, 11 Dec 2025 09:21:08 +0000 Subject: [PATCH 6/6] WIP --- src/techui_builder/autofill.py | 18 ++---------------- src/techui_builder/render.py | 3 +++ src/techui_builder/utils.py | 24 ++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/techui_builder/autofill.py b/src/techui_builder/autofill.py index 7789e633..687dd6c0 100644 --- a/src/techui_builder/autofill.py +++ b/src/techui_builder/autofill.py @@ -1,12 +1,11 @@ import logging -import os from collections import defaultdict from dataclasses import dataclass, field from pathlib import Path -from lxml import objectify from lxml.objectify import ObjectifiedElement +from techui_builder import utils from techui_builder.builder import Builder, _get_action_group from techui_builder.models import Component from techui_builder.utils import read_bob @@ -46,20 +45,7 @@ def autofill_bob(self, gui: "Builder"): child["run_actions_on_mouse_click"] = "true" def write_bob(self, filename: Path): - # Check if data/ dir exists and if not, make it - data_dir = filename.parent - if not data_dir.exists(): - os.mkdir(data_dir) - - # Remove any unnecessary xmlns:py and py:pytype metadata from tags - objectify.deannotate(self.tree, cleanup_namespaces=True) - - self.tree.write( - filename, - pretty_print=True, - encoding="utf-8", - xml_declaration=True, - ) + utils.write_bob(self.tree, filename) logger_.debug(f"Screen filled for {filename}") def replace_content( diff --git a/src/techui_builder/render.py b/src/techui_builder/render.py index d4497a74..86de96fa 100644 --- a/src/techui_builder/render.py +++ b/src/techui_builder/render.py @@ -17,3 +17,6 @@ def __post_init__(self): def load_screen(self): self.screen_template = self.env.get_template(self.screen_path.name) + + def render_screen(self): + rendered_screen = self.screen_template.render(url=self.beamline.url) diff --git a/src/techui_builder/utils.py b/src/techui_builder/utils.py index 11b1304a..1d4b5a8f 100644 --- a/src/techui_builder/utils.py +++ b/src/techui_builder/utils.py @@ -1,4 +1,9 @@ +import logging +import os +from pathlib import Path + from lxml import objectify +from lxml.etree import _ElementTree from lxml.objectify import ObjectifiedElement @@ -14,6 +19,25 @@ def read_bob(path): return tree, widgets +def write_bob(tree: _ElementTree[ObjectifiedElement], filename: Path): + # Check if data/ dir exists and if not, make it + data_dir = filename.parent + if not data_dir.exists(): + os.mkdir(data_dir) + + # Remove any unnecessary xmlns:py and py:pytype metadata from tags + objectify.deannotate(tree, cleanup_namespaces=True) + + tree.write( + filename, + pretty_print=True, + encoding="utf-8", + xml_declaration=True, + ) + logger_ = logging.getLogger() + logger_.debug(f"Screen filled for {filename}") + + def get_widgets(root: ObjectifiedElement): widgets: dict[str, ObjectifiedElement] = {} # Loop over objects in the xml