Skip to content

Instantly share code, notes, and snippets.

@ndevenish
Created April 16, 2026 14:49
Show Gist options
  • Select an option

  • Save ndevenish/69882efe95fcd7086ee2841c972bbe76 to your computer and use it in GitHub Desktop.

Select an option

Save ndevenish/69882efe95fcd7086ee2841c972bbe76 to your computer and use it in GitHub Desktop.
Convert phoebus screen to EDL
#!/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