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
+
+
+
+
+
+
+
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:
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/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/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/src/techui_builder/render.py b/src/techui_builder/render.py
new file mode 100644
index 00000000..86de96fa
--- /dev/null
+++ b/src/techui_builder/render.py
@@ -0,0 +1,22 @@
+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)
+
+ 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
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
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" },