Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 41 additions & 10 deletions src/techui_builder/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@

import yaml
from epicsdbbuilder.recordbase import Record
from jinja2 import Template
from lxml import etree, objectify
from lxml.objectify import ObjectifiedElement
from softioc.builder import records

from techui_builder.generate import Generator
from techui_builder.models import Entity, TechUi
from techui_builder.models import Entity, SupportEntity, TechUi, TechUiSupport
from techui_builder.validator import Validator

logger_ = logging.getLogger(__name__)
Expand Down Expand Up @@ -49,9 +50,10 @@ class Builder:
default_factory=lambda: defaultdict(list), init=False
)
status_pvs: dict[str, Record] = field(default_factory=dict, init=False)

# These are global params for the class (not accessible by user)
_services_dir: Path = field(init=False, repr=False)
_gui_map: dict = field(init=False, repr=False)
_write_directory: Path = field(default=Path("opis"), init=False, repr=False)
_write_directory: Path = field(init=False, repr=False)

def __post_init__(self):
# Populate beamline and components
Expand All @@ -61,12 +63,30 @@ def __post_init__(self):

def setup(self):
"""Run intial setup, e.g. extracting entries from service ioc.yaml."""
# This needs to be before _read_map()
self.support_path = self._write_directory.joinpath("techui-support")

self._read_map()

self._extract_services()
synoptic_dir = self._write_directory

self.clean_files()

self.generator = Generator(synoptic_dir, self.conf.beamline.url)
self.generator = Generator(
self._write_directory,
self.conf.beamline.url,
self.support_path,
self.techui_support,
)

def _read_map(self):
"""Read the techui-support.yaml file from techui-support."""
support_yaml = self.support_path.joinpath("techui-support.yaml").absolute()
logger_.debug(f"techui-support.yaml location: {support_yaml}")

self.techui_support = TechUiSupport.model_validate(
yaml.safe_load(support_yaml.read_text(encoding="utf-8"))
)

def clean_files(self):
exclude = {"index.bob"}
Expand Down Expand Up @@ -177,17 +197,28 @@ def _extract_entities(self, service_name: str, ioc_yaml: Path):
with open(ioc_yaml) as ioc:
ioc_conf: dict[str, list[dict[str, str]]] = yaml.safe_load(ioc)
for entity in ioc_conf["entities"]:
if "P" in entity.keys():
if entity["type"] in self.techui_support.support_modules:
support_mapping: SupportEntity = (
self.techui_support.support_modules[entity["type"]]
)
support_macros = support_mapping.macros

macros = {k: v for k, v in entity.items() if k in support_macros}

prefix_template = Template(support_mapping.prefix)
prefix: str = prefix_template.render(macros)

# Create Entity and append to entity list
new_entity = Entity(
service_name=service_name,
type=entity["type"],
desc=entity.get("desc", None),
P=entity["P"],
M=None if (val := entity.get("M")) is None else val,
R=None if (val := entity.get("R")) is None else val,
prefix=prefix,
macros=macros,
)
self.entities[new_entity.P].append(new_entity)

pv_root = prefix.split(":", maxsplit=1)[0]
self.entities[pv_root].append(new_entity)

def _generate_screen(self, screen_name: str):
self.generator.build_screen(screen_name)
Expand Down
80 changes: 25 additions & 55 deletions src/techui_builder/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@
import os
import re
from collections import defaultdict
from collections.abc import Mapping, Sequence
from collections.abc import Mapping
from dataclasses import dataclass, field
from pathlib import Path

import yaml
from lxml import objectify
from phoebusgen import screen as pscreen
from phoebusgen import widget as pwidget
from phoebusgen.widget.widgets import ActionButton, EmbeddedDisplay, Group

from techui_builder.models import Component, Entity
from techui_builder.models import Component, Entity, TechUiSupport

logger_ = logging.getLogger(__name__)

Expand All @@ -23,12 +22,10 @@ class Generator:
beamline_url: str = field(repr=False)

# These are global params for the class (not accessible by user)
support_path: Path = field(init=False, repr=False)
techui_support: dict = field(init=False, repr=False)
support_path: Path = field(repr=False)
techui_support: TechUiSupport = field(repr=False)
default_size: int = field(default=100, init=False, repr=False)
P: str = field(default="P", init=False, repr=False)
M: str = field(default="M", init=False, repr=False)
R: str = field(default="R", init=False, repr=False)
prefix: str = field(default="P", init=False, repr=False)
widgets: list[ActionButton | EmbeddedDisplay] = field(
default_factory=list[ActionButton | EmbeddedDisplay], init=False, repr=False
)
Expand All @@ -42,20 +39,6 @@ class Generator:
group_padding: int = field(default=50, init=False, repr=False)
label_flag: bool = field(default=False, init=False, repr=False)

def __post_init__(self):
# This needs to be before _read_map()
self.support_path = self.synoptic_dir.joinpath("techui-support")

self._read_map()

def _read_map(self):
"""Read the techui-support.yaml file from techui-support."""
support_yaml = self.support_path.joinpath("techui-support.yaml").absolute()
logger_.debug(f"techui-support.yaml location: {support_yaml}")

with open(support_yaml) as map:
self.techui_support = yaml.safe_load(map)

def _get_screen_dimensions(self, file: str) -> tuple[int, int]:
"""
Parses the bob files for information on the height
Expand Down Expand Up @@ -161,33 +144,23 @@ def _get_group_dimensions(self, widget_list: list[EmbeddedDisplay | ActionButton
)

def _initialise_name_suffix(self, component: Entity) -> tuple[str, str, str | None]:
if component.M is not None:
name: str = component.M
suffix: str = component.M
suffix_label: str | None = self.M
elif component.R is not None:
name = component.R
suffix = component.R
suffix_label = self.R
else:
name = component.P
suffix = ""
suffix_label = ""
try:
component_name = component.prefix.split(":", maxsplit=1)[1]
raw_name = component_name.removesuffix(":")
except IndexError:
component_name = ""
raw_name = ""

suffix = component_name

name = name.removeprefix(":").removesuffix(":")
# Try to get name from child labels if they exist,
# if not, just use the name as it is.
if component.child_labels is not None:
if name in component.child_labels.keys():
name = component.child_labels[name]
if raw_name in component.child_labels.keys():
component_name = component.child_labels[raw_name]
self.label_flag = True

return (name, suffix, suffix_label)

def _is_list_of_dicts(self, scrn_mapping: Mapping) -> bool:
return isinstance(scrn_mapping, Sequence) and all(
isinstance(scrn, Mapping) for scrn in scrn_mapping
)
return (component_name, suffix, raw_name)

def _allocate_widget(
self, scrn_mapping: Mapping, component: Entity
Expand Down Expand Up @@ -238,7 +211,7 @@ def _allocate_widget(
height,
)
# Add macros to the widgets
new_widget.macro(self.P, component.P)
new_widget.macro(self.prefix, component.prefix)
if suffix_label != "":
new_widget.macro(f"{suffix_label}", suffix)
new_widget.macro("label", name.removeprefix(":").removesuffix(":"))
Expand Down Expand Up @@ -266,7 +239,7 @@ def _allocate_widget(
file=str(data_scrn_path),
target="tab",
macros={
"P": component.P,
"P": component.prefix,
f"{suffix_label}": suffix,
},
)
Expand All @@ -275,7 +248,7 @@ def _allocate_widget(
file=str(data_scrn_path),
target="tab",
macros={
"P": component.P,
"P": component.prefix,
},
)

Expand All @@ -294,19 +267,16 @@ def _create_widget(
new_widget = []

try:
scrn_mapping = self.techui_support[component.type]
scrn_mapping = self.techui_support.support_modules[component.type].screens
except KeyError:
logger_.warning(
f"No available widget for {component.type} in screen \
{name}. Skipping..."
)
return None

if self._is_list_of_dicts(scrn_mapping):
for value in scrn_mapping:
new_widget.append(self._allocate_widget(value, component))
else:
new_widget = self._allocate_widget(scrn_mapping, component)
for value in scrn_mapping:
new_widget.append(self._allocate_widget(value, component))

return new_widget

Expand Down Expand Up @@ -374,14 +344,14 @@ def layout_widgets(self, widgets: list[EmbeddedDisplay | ActionButton]):

return sorted_widgets

def build_widgets(self, screen_name: str, screen_components: list[Entity]):
def build_widgets(self, screen_name: str, screen_entities: list[Entity]):
# Empty widget buffer
self.widgets = []

# order is an enumeration of the components, used to list them,
# and serves as functionality in the math for formatting.
for component in screen_components:
new_widget = self._create_widget(name=screen_name, component=component)
for entity in screen_entities:
new_widget = self._create_widget(name=screen_name, component=entity)
if new_widget is None:
continue
if isinstance(new_widget, list):
Expand Down
33 changes: 28 additions & 5 deletions src/techui_builder/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging
import re
from typing import Annotated, Literal
from typing import Annotated, Any, Literal

from pydantic import (
BaseModel,
Expand Down Expand Up @@ -272,15 +272,38 @@ class Entity(BaseModel):
"ADAravis.aravisCamera"
),
]
P: Annotated[str, Field(description="PV Prefix for module entity")]
prefix: Annotated[str, Field(description="PV Prefix for module entity")]
desc: Annotated[
str | None, Field(description="Optional description of module entity")
] = None
child_labels: Annotated[
dict[str, str] | None,
Field(description="Optional child labels for module entity"),
] = None
M: Annotated[str | None, Field(description="Optional PV suffix for a motor")]
R: Annotated[
str | None, Field(description="Optional PV suffix for an ADAravis plugin")
macros: Annotated[
dict[str, Any],
Field(description="Macros for the matching screen (can be empty)"),
]


class SupportEntity(BaseModel):
"""
Table of variables from corresponding support module in techui-support.yaml file
"""

prefix: Annotated[str, Field(description="Prefix for techui-support screen")]
macros: Annotated[
list[str],
Field(description="Macros for the matching screen (can be empty)"),
]
screens: Annotated[
list[dict[str, str]],
Field(description="Dictionary of available screens for the support module"),
]


class TechUiSupport(BaseModel):
support_modules: Annotated[
dict[str, SupportEntity],
Field(description="The dictionary of techui-support.yaml entities"),
]
Loading