Created
April 16, 2026 14:49
-
-
Save ndevenish/69882efe95fcd7086ee2841c972bbe76 to your computer and use it in GitHub Desktop.
Convert phoebus screen to EDL
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """Convert Phoebus .bob to EDM .edl screen format, using DLS house style.""" | |
| import argparse | |
| import os | |
| import xml.etree.ElementTree as ET | |
| GROUP_TITLE_HEIGHT = 25 | |
| SECTION_LABEL_HEIGHT = 13 | |
| RECT_INSET = 10 # how far below the section label the rect starts | |
| def get_text(elem, tag, default=""): | |
| child = elem.find(tag) | |
| if child is not None and child.text: | |
| return child.text.strip() | |
| return default | |
| def get_int(elem, tag, default=0): | |
| child = elem.find(tag) | |
| if child is not None and child.text: | |
| try: | |
| return int(float(child.text.strip())) | |
| except ValueError: | |
| return default | |
| return default | |
| def get_align(elem): | |
| val = get_int(elem, "horizontal_alignment", 0) | |
| if val == 1: | |
| return "center" | |
| elif val == 2: | |
| return "right" | |
| return "" | |
| # --------------------------------------------------------------------------- | |
| # Widget renderers – each returns a list of EDL text blocks | |
| # --------------------------------------------------------------------------- | |
| def edl_title_group(x, y, w, h, text): | |
| """DLS-style title bar: rectangle, 3D lines, centred bold text, all in a group.""" | |
| return f"""# (Group) | |
| object activeGroupClass | |
| beginObjectProperties | |
| major 4 | |
| minor 0 | |
| release 0 | |
| x {x} | |
| y {y} | |
| w {w} | |
| h {h} | |
| beginGroup | |
| # (Rectangle) | |
| object activeRectangleClass | |
| beginObjectProperties | |
| major 4 | |
| minor 0 | |
| release 0 | |
| x {x} | |
| y {y} | |
| w {w} | |
| h {h} | |
| lineColor index 3 | |
| fill | |
| fillColor index 3 | |
| endObjectProperties | |
| # (Lines) | |
| object activeLineClass | |
| beginObjectProperties | |
| major 4 | |
| minor 0 | |
| release 1 | |
| x {x} | |
| y {y} | |
| w {w} | |
| h {h} | |
| lineColor index 11 | |
| fillColor index 0 | |
| numPoints 3 | |
| xPoints {{ | |
| 0 {x} | |
| 1 {x + w} | |
| 2 {x + w} | |
| }} | |
| yPoints {{ | |
| 0 {y + h} | |
| 1 {y + h} | |
| 2 {y} | |
| }} | |
| endObjectProperties | |
| # (Static Text) | |
| object activeXTextClass | |
| beginObjectProperties | |
| major 4 | |
| minor 1 | |
| release 1 | |
| x {x} | |
| y {y} | |
| w {w} | |
| h {h} | |
| font "arial-bold-r-16.0" | |
| fontAlign "center" | |
| fgColor index 14 | |
| bgColor index 48 | |
| value {{ | |
| "{text}" | |
| }} | |
| endObjectProperties | |
| # (Lines) | |
| object activeLineClass | |
| beginObjectProperties | |
| major 4 | |
| minor 0 | |
| release 1 | |
| x {x} | |
| y {y} | |
| w {w} | |
| h {h} | |
| lineColor index 1 | |
| fillColor index 0 | |
| numPoints 3 | |
| xPoints {{ | |
| 0 {x} | |
| 1 {x} | |
| 2 {x + w} | |
| }} | |
| yPoints {{ | |
| 0 {y + h} | |
| 1 {y} | |
| 2 {y} | |
| }} | |
| endObjectProperties | |
| endGroup | |
| endObjectProperties""" | |
| def edl_label(x, y, w, h, text, font="arial-medium-r-12.0", align="", fg=14): | |
| lines = [ | |
| "# (Static Text)", | |
| "object activeXTextClass", | |
| "beginObjectProperties", | |
| "major 4", | |
| "minor 1", | |
| "release 1", | |
| f"x {x}", | |
| f"y {y}", | |
| f"w {w}", | |
| f"h {h}", | |
| f'font "{font}"', | |
| f"fgColor index {fg}", | |
| "bgColor index 3", | |
| "useDisplayBg", | |
| "value {", | |
| f' "{text}"', | |
| "}", | |
| ] | |
| if align: | |
| lines.append(f'fontAlign "{align}"') | |
| lines.append("endObjectProperties") | |
| return "\n".join(lines) | |
| def edl_text_monitor(x, y, w, h, pv, precision=0, fmt=None): | |
| lines = [ | |
| "# (Text Monitor)", | |
| "object activeXTextDspClass:noedit", | |
| "beginObjectProperties", | |
| "major 4", | |
| "minor 6", | |
| "release 0", | |
| f"x {x}", | |
| f"y {y}", | |
| f"w {w}", | |
| f"h {h}", | |
| f'controlPv "{pv}"', | |
| ] | |
| if fmt == "6": | |
| lines.append('format "string"') | |
| else: | |
| lines.append('format "float"') | |
| lines.append(f"precision {precision}") | |
| lines += [ | |
| 'font "arial-medium-r-12.0"', | |
| 'fontAlign "center"', | |
| "fgColor index 16", | |
| "fgAlarm", | |
| "bgColor index 10", | |
| "limitsFromDb", | |
| "nullColor index 30", | |
| "useAlarmBorder", | |
| "newPos", | |
| 'objType "monitors"', | |
| "noExecuteClipMask", | |
| "endObjectProperties", | |
| ] | |
| return "\n".join(lines) | |
| def edl_text_entry(x, y, w, h, pv, precision=0): | |
| lines = [ | |
| "# (Textentry)", | |
| "object TextentryClass", | |
| "beginObjectProperties", | |
| "major 10", | |
| "minor 0", | |
| "release 0", | |
| f"x {x}", | |
| f"y {y}", | |
| f"w {w}", | |
| f"h {h}", | |
| f'controlPv "{pv}"', | |
| 'displayMode "decimal"', | |
| f"precision {precision}", | |
| "fgColor index 25", | |
| "bgColor index 3", | |
| "fill", | |
| 'font "arial-medium-r-12.0"', | |
| "lineWidth 0", | |
| "endObjectProperties", | |
| ] | |
| return "\n".join(lines) | |
| def edl_menu_button(x, y, w, h, pv): | |
| lines = [ | |
| "# (Menu Button)", | |
| "object activeMenuButtonClass", | |
| "beginObjectProperties", | |
| "major 4", | |
| "minor 0", | |
| "release 0", | |
| f"x {x}", | |
| f"y {y}", | |
| f"w {w}", | |
| f"h {h}", | |
| "fgColor index 25", | |
| "bgColor index 3", | |
| "inconsistentColor index 3", | |
| "topShadowColor index 1", | |
| "botShadowColor index 11", | |
| f'controlPv "{pv}"', | |
| 'font "arial-medium-r-14.0"', | |
| "endObjectProperties", | |
| ] | |
| return "\n".join(lines) | |
| def edl_message_button(x, y, w, h, pv, text, value="1"): | |
| lines = [ | |
| "# (Message Button)", | |
| "object activeMessageButtonClass", | |
| "beginObjectProperties", | |
| "major 4", | |
| "minor 1", | |
| "release 0", | |
| f"x {x}", | |
| f"y {y}", | |
| f"w {w}", | |
| f"h {h}", | |
| "fgColor index 25", | |
| "onColor index 4", | |
| "offColor index 3", | |
| "topShadowColor index 1", | |
| "botShadowColor index 11", | |
| f'controlPv "{pv}"', | |
| f'pressValue "{value}"', | |
| f'onLabel "{text}"', | |
| f'offLabel "{text}"', | |
| "3d", | |
| 'font "arial-bold-r-12.0"', | |
| "endObjectProperties", | |
| ] | |
| return "\n".join(lines) | |
| def edl_related_display(x, y, w, h, text, filename): | |
| if filename.endswith(".bob"): | |
| filename = filename[:-4] + ".edl" | |
| lines = [ | |
| "# (Related Display)", | |
| "object relatedDisplayClass", | |
| "beginObjectProperties", | |
| "major 4", | |
| "minor 4", | |
| "release 0", | |
| f"x {x}", | |
| f"y {y}", | |
| f"w {w}", | |
| f"h {h}", | |
| "fgColor index 25", | |
| "bgColor index 3", | |
| "topShadowColor index 1", | |
| "botShadowColor index 11", | |
| 'font "arial-bold-r-12.0"', | |
| f'buttonLabel "{text}"', | |
| "numDsps 1", | |
| "displayFileName {", | |
| f' 0 "{filename}"', | |
| "}", | |
| "endObjectProperties", | |
| ] | |
| return "\n".join(lines) | |
| def edl_byte(x, y, w, h, pv, num_bits=1): | |
| """LED indicator using ByteClass (DLS style).""" | |
| lines = [ | |
| "# (Byte)", | |
| "object ByteClass", | |
| "beginObjectProperties", | |
| "major 4", | |
| "minor 0", | |
| "release 0", | |
| f"x {x}", | |
| f"y {y}", | |
| f"w {w}", | |
| f"h {h}", | |
| f'controlPv "{pv}"', | |
| "lineColor index 14", | |
| "onColor index 20", | |
| "offColor index 24", | |
| "lineWidth 2", | |
| f"numBits {num_bits}", | |
| "endObjectProperties", | |
| ] | |
| return "\n".join(lines) | |
| def edl_choice_button(x, y, w, h, pv): | |
| lines = [ | |
| "# (Choice Button)", | |
| "object activeChoiceButtonClass", | |
| "beginObjectProperties", | |
| "major 4", | |
| "minor 0", | |
| "release 0", | |
| f"x {x}", | |
| f"y {y}", | |
| f"w {w}", | |
| f"h {h}", | |
| f'controlPv "{pv}"', | |
| 'font "arial-medium-r-10.0"', | |
| "fgColor index 25", | |
| "bgColor index 3", | |
| "selectColor index 6", | |
| "inconsistentColor index 3", | |
| "topShadowColor index 1", | |
| "botShadowColor index 11", | |
| 'orientation "horizontal"', | |
| "endObjectProperties", | |
| ] | |
| return "\n".join(lines) | |
| def edl_section_header(x, y, w, content_h, name): | |
| """DLS-style section: rectangle with a section-label overlapping its top edge. | |
| Returns a list of EDL blocks: the rectangle then the label. | |
| The rectangle starts SECTION_LABEL_HEIGHT/2 below y so the label | |
| straddles the top edge of the box. | |
| """ | |
| parts = [] | |
| rect_y = y + SECTION_LABEL_HEIGHT // 2 | |
| # Rectangle | |
| parts.append( | |
| "\n".join( | |
| [ | |
| "# (Rectangle)", | |
| "object activeRectangleClass", | |
| "beginObjectProperties", | |
| "major 4", | |
| "minor 0", | |
| "release 0", | |
| f"x {x}", | |
| f"y {rect_y}", | |
| f"w {w}", | |
| f"h {content_h}", | |
| "lineColor index 14", | |
| "fill", | |
| "fillColor index 5", | |
| "endObjectProperties", | |
| ] | |
| ) | |
| ) | |
| # Section label (overlaps top edge of rectangle) | |
| padded = f" {name} " | |
| parts.append( | |
| "\n".join( | |
| [ | |
| "# (Static Text)", | |
| "object activeXTextClass", | |
| "beginObjectProperties", | |
| "major 4", | |
| "minor 1", | |
| "release 1", | |
| f"x {x}", | |
| f"y {y}", | |
| f"w {len(padded) * 7}", | |
| f"h {SECTION_LABEL_HEIGHT}", | |
| 'font "arial-medium-r-12.0"', | |
| "fgColor index 14", | |
| "bgColor index 8", | |
| "value {", | |
| f' "{padded}"', | |
| "}", | |
| "autoSize", | |
| "border", | |
| "endObjectProperties", | |
| ] | |
| ) | |
| ) | |
| return parts | |
| # --------------------------------------------------------------------------- | |
| # Widget processing | |
| # --------------------------------------------------------------------------- | |
| def process_widget(widget, off_x=0, off_y=0): | |
| """Process a single widget and return list of EDL text blocks.""" | |
| results = [] | |
| wtype = widget.get("type") | |
| x = get_int(widget, "x", 0) + off_x | |
| y = get_int(widget, "y", 0) + off_y | |
| w = get_int(widget, "width", 100) | |
| h = get_int(widget, "height", 20) | |
| if wtype == "label": | |
| text = get_text(widget, "text", "") | |
| cls = get_text(widget, "class", "") | |
| align = get_align(widget) | |
| if cls == "TITLE": | |
| # Skip – we render the DLS-style title group separately | |
| pass | |
| else: | |
| results.append(edl_label(x, y, w, h, text, "arial-medium-r-12.0", align)) | |
| elif wtype == "textupdate": | |
| pv = get_text(widget, "pv_name", "") | |
| precision = get_int(widget, "precision", 0) | |
| fmt = get_text(widget, "format", "") | |
| results.append( | |
| edl_text_monitor(x, y, w, h, pv, precision, fmt if fmt else None) | |
| ) | |
| elif wtype == "textentry": | |
| pv = get_text(widget, "pv_name", "") | |
| precision = get_int(widget, "precision", 0) | |
| results.append(edl_text_entry(x, y, w, h, pv, precision)) | |
| elif wtype == "combo": | |
| pv = get_text(widget, "pv_name", "") | |
| results.append(edl_menu_button(x, y, w, h, pv)) | |
| elif wtype == "action_button": | |
| text = get_text(widget, "text", "Button") | |
| text = text.replace("\u29c9", "->").replace("⧉", "->") | |
| actions = widget.find("actions") | |
| if actions is not None: | |
| action = actions.find("action") | |
| if action is not None: | |
| atype = action.get("type", "") | |
| if atype == "open_display": | |
| fname = get_text(action, "file", "") | |
| results.append(edl_related_display(x, y, w, h, text, fname)) | |
| elif atype == "write_pv": | |
| pv = get_text(action, "pv_name", "") | |
| if pv == "$(pv_name)": | |
| pv = get_text(widget, "pv_name", "") | |
| value = get_text(action, "value", "1") | |
| results.append(edl_message_button(x, y, w, h, pv, text, value)) | |
| elif wtype == "slide_button": | |
| pv = get_text(widget, "pv_name", "") | |
| results.append(edl_choice_button(x, y, w, h, pv)) | |
| elif wtype == "led": | |
| pv = get_text(widget, "pv_name", "") | |
| results.append(edl_byte(x, y, w, h, pv)) | |
| elif wtype == "group": | |
| name = get_text(widget, "name", "Group") | |
| group_w = w | |
| # Collect children first to work out content height | |
| child_blocks = [] | |
| child_off_x = x | |
| child_off_y = y + GROUP_TITLE_HEIGHT | |
| for child in widget: | |
| if child.tag == "widget": | |
| child_blocks.extend( | |
| process_widget(child, child_off_x, child_off_y) | |
| ) | |
| content_h = h - SECTION_LABEL_HEIGHT // 2 | |
| # Section header (rect + label) | |
| results.extend(edl_section_header(x, y, group_w, content_h, name)) | |
| # Children | |
| results.extend(child_blocks) | |
| return results | |
| def convert(bob_file, edl_file): | |
| tree = ET.parse(bob_file) | |
| root = tree.getroot() | |
| screen_w = get_int(root, "width", 800) | |
| screen_h = get_int(root, "height", 600) | |
| screen_title = get_text(root, "name", "Display") | |
| header = f"""4 0 1 | |
| beginScreenProperties | |
| major 4 | |
| minor 0 | |
| release 1 | |
| x 0 | |
| y 0 | |
| w {screen_w} | |
| h {screen_h} | |
| font "arial-medium-r-18.0" | |
| ctlFont "arial-medium-r-18.0" | |
| btnFont "arial-medium-r-18.0" | |
| fgColor index 14 | |
| bgColor index 3 | |
| textColor index 14 | |
| ctlFgColor1 index 25 | |
| ctlFgColor2 index 30 | |
| ctlBgColor1 index 3 | |
| ctlBgColor2 index 3 | |
| topShadowColor index 1 | |
| botShadowColor index 11 | |
| showGrid | |
| snapToGrid | |
| gridSize 5 | |
| title "{screen_title}" | |
| endScreenProperties""" | |
| widgets = [] | |
| # DLS-style title bar | |
| widgets.append(edl_title_group(0, 0, screen_w, 30, screen_title)) | |
| # Process top-level widgets | |
| for child in root: | |
| if child.tag == "widget": | |
| widgets.extend(process_widget(child)) | |
| with open(edl_file, "w") as f: | |
| f.write(header) | |
| for w in widgets: | |
| f.write("\n\n") | |
| f.write(w) | |
| f.write("\n") | |
| print(f"Converted {bob_file} -> {edl_file}") | |
| print(f" {len(widgets)} EDM objects generated") | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser( | |
| description="Convert a Phoebus .bob screen to an EDM .edl screen" | |
| ) | |
| parser.add_argument("bob_file", help="Input .bob file") | |
| parser.add_argument( | |
| "-o", "--output", help="Output .edl file (default: same name with .edl extension)" | |
| ) | |
| args = parser.parse_args() | |
| edl_file = args.output or os.path.splitext(args.bob_file)[0] + ".edl" | |
| convert(args.bob_file, edl_file) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment