Skip to content

Display Builder Script Compatibility

Kay Kasemir edited this page Dec 18, 2023 · 16 revisions

Display Builder Script Compatibility

Scripts allow BOY or Display Builder panels to perform nearly arbitrary functions which would ordinarily require a separate application.

Compared to BOY, Display Builder scripts are more powerful because they are executed off the UI thread. While BOY could only execute one script at a time, blocking the complete UI while any script is running, the Display Builder can execute one script per display without directly impacting the UI responsiveness.

At the same time, each script is in fact a (small) application, implemented based on the BOY or Display Builder API. Like any application code, a script needs to be updated as the API changes, and we do not offer any guarantee regarding API stability. Since a script can access the complete Display Builder API, you will have to read the Java source code to familiarize yourself with that API. If you have to ask how to do something now, you will have to ask again in a few years as the API changes. A script implemented for BOY will need to be rewritten for the Display Builder. Check the Display Builder examples under script_util for hints on porting scripts, because for BOY and the Display Builder there are at least some similarities, but note that the Display Builder web runtime, which can display most of the basic Display Builder widgets and their properties, will never be able to execute Display Builder scripts because it's using a totally different environment (JavaScript in web browser DOM as opposed to Jython in JVM).

Apart from the maintainability, there are other caveats. A display script should never 'do' anything. If your control system depends on a script in a display to perform a function, for example to ramp a power supply voltage up or to open a relief valve in case of overpressure, you are mis-using scripts. Always ask yourself: What will happen if the user closes the display, will I then lose some necessary control system functionality like overpressure protection or power supply ramp-up? What happens if multiple users open copies of the same display, will several scripts then try to operate on the same subsystem, the power supply will ramp up twice as fast? Any script that performs a function on the control system needs to be turned into an IOC, or maybe a python-based service that uses Channel Access or PVAccess. Scripts in a display may be used to help display something, but they must not operate on the control system.

Finally, remember that the Display Builder is meant to be a display tool, showing the value of PVs to users and allowing users to enter new values, which are then written to PVs. The fact that there is a scripting interface doesn't mean that you must use it. It is not meant to improve your overall productivity, golf score or general happiness. If you wonder how a Display Builder script can do a specific thing and want to ask for help, check https://en.wikipedia.org/wiki/XY_problem to see if maybe the problem you're trying to solve should use a different approach.

Below are some specific script porting hints.

Jython vs JavaScript

BOY has supported both Jython and JavaScript, but the JavaScript engine has changed.

Initially, BOY used the Rhino JavaScript engine. With Java 8, we switched to the Nashorn JS engine because it was included in JDK 8. With Java 15, Nashorn will be removed from the JDK, so on 2020-06-20 we reverted back to Rhino.

Nashorn and Rhino differ in the way they import Java API. Example:

# Rhino
importPackage(Packages.org.csstudio.opibuilder.scriptUtil);
widget.setPropertyValue("text", PVUtil.getString(pvs[0]));
# Nashorn
PVUtil = org.csstudio.display.builder.runtime.script.PVUtil;
widget.setPropertyValue("text", PVUtil.getString(pvs[0]));

While the long-term future of either scripting support in Java is not known, Jython is for now preferred because it has been longer-lived than the changing JS script support. The following hints are thus for Jython.

Jython vs. C-Python

Jython has the same syntax as C-Python. Jython is executed within the Java runtime and thus has full access to the Display Builder API, but it cannot use many of the popular C-Python libraries like Numpy because those depend on native code. C-Python on the other hand has access to libraries like Numpy, but it cannot directly access the Display Builder API because it executes as a separate process, distinct from the Java runtime.

The Py4J project offers an approach for starting an external C-Python process with a "JavaGateway". The Java runtime then communicates with the C-Python process via that gateway. In principle, this solves the desire of running C-Python with access to both popular libraries like Numpy and access to the Display Builder API.

When widgets refer to script files named *.py, they are by default executed in Jython. If the first line, however, includes the text python, it is executed by C-Python. If Py4J has been installed, that C-Python script can then connect back to the Display Builder API.

A "connect2j" helper has been created to assist with this approach, and the examples provide some Jython vs. Python scripts:

Regrettably, the C-Python approach using Py4J has never been as practical as the Jython support, so while we keep the C-Python/Py4y examples around, it is suggested to use Jython. The following hints are thus for Jython.

Imports

These imports are patched automatically when opening *.opi files, replacing

from org.csstudio.opibuilder.scriptUtil import PVUtil
from org.csstudio.opibuilder.scriptUtil import ScriptUtil

with

from org.csstudio.display.builder.runtime.script import PVUtil
from org.csstudio.display.builder.runtime.script import ScriptUtil

There will be a warning message, though, which you best avoid by updating your script imports.

Debugging Scripts

You can now simply

print "Whatever"

in a script, and the output appears in the console where Phoebus was started. You can also open the Debug, Error Log application to see these console messages.

Widget Properties

Scripts are invoked with widget set to the Widget to which the script is attached. Compared to BOY, this widget is now a Display Builder widget with very different API. About the only common method is

# Assume script is attached to a Label
widget.setPropertyValue("text", "Hello!")

Many widgets have the same property names, but you will need to check for each widget.

Widgets vs. Display

BOY merged all 'linked' (embedded) displays into one large display model, delaying the UI until the complete model had been obtained.

The Display Builder treats embedded displays as a black box. Each embedded widget asynchronously loads its content.

BOY published a display variable for accessing the root of the display model. In the Display Builder, scripts have access to two related pieces of information.

widget.getDisplayModel() provides access to the closest 'root' of a widget's display model. For content within an embedded widget, this would be the model root of that child model.

widget.getTopDisplayModel() provides access to the top-level model, i.e. the overall display root that is shown in a panel.

Locating Widgets by Name

Ideally, scripts are attached to a widget, so they can use the widget variable to access the widget to which they are attached.

Scripts that operate on multiple widgets can locate those via their name.

In BOY, this was done via

other_widget = display.getWidget("NameOfOtherWidget")

In the display builder, this is done via

other_widget = ScriptUtil.findWidgetByName(widget, "NameOfOtherWidget")

Label, Text Update

Some legacy displays use Labels where the text is updated by a script. Ideally, those can be removed by instead creating suitable PVs which provide the necessary text, and then simply display them with a Text Update widget.

Read and Write PVs

Widgets are invoked with pvs[] set to the script's PVs. The PVUtil is very similar, no change to for example

value = PVUtil.getDouble(pvs[0])

To write a value to the PV, use

pvs[0].write(3.14)
pvs[1].write("String PV")

Script Triggers

Scripts are executed whenever any of the attached PVs sends a value, except for PVs that are configured to not trigger the script.

This includes the initial value that a PV sends when we first connect, so each script will trigger at least once when a display is opened and all PVs connect.

In BOY, scripts ran within the UI thread, which tended to suppress that initial update.

Triggers are cached, not queued.

When a PV triggers a script, it is scheduled for execution. Additional triggers from the same or other PVs while the script is already scheduled have no effect. Once the script executes, triggers will re-schedule the script.

Only connected PVs can send updates which trigger a script. When reading the value of a PV inside the script, that is the 'current' value of the PV. For a rapidly changing PV, the following is possible:

  • PV sends value 1, which triggers the script
  • PV sends values 2, 3, 4, but script is already scheduled
  • Script executes, and PVUtil.getInt(pvs[0]) now returns 4

Though unlikely, it is also possible that

  • PV sends value 1, which triggers the script
  • PV disconnects
  • Script executes, and PVUtil.getInt(pvs[0]) now throws exception because PV has no value

Script can thus be used to support a generic user display, using a best effort to indicate the most recent value of PVs. Scripts cannot implement a data acquisition system which dependably handles every value sent by a PV.

Trigger Script via Button

Scripts can be attached to Action Button actions. Pressing the button will then invoke the script. But such scripts cannot receive additional PVs.

To trigger a script from a button, and receive additional PVs in the script, consider the following setup:

  • Button with action to write 1 to loc://do_it(0)
  • Script is triggered by that PV, and maybe uses additional non-triggering PVs. The script can be attached to any widget, but the button is a suggested place to simplify locating the script.

In BOY, such a script would often only run when the button is pressed, because it is executed by the UI thread, but exact behavior is not predictable.

In the Display Builder, the script will run whenever the PV sends a value, which includes the initial value when the display is first opened. Scripts that are supposed to run when a button is pressed thus need to be implemented like this:

# Script is triggered whenever 'do_it' changes
do_it = PVUtil.getInt(pvs[0])
if do_it:
    # Do what you need to do
    # ...

    # Reset trigger
    pvs[0].write(0)
# else: Display just opened, or do_it reset to 0

This script will be triggered when the display is opened and the local PV connects with initial value 0. When the button is pressed and writes 1 to the PV, the PV triggers the script with value 1 and we do what we need to do, finally resetting the PV. The script will be called again because of that new value 0, but not do anything.

Opening Displays

Replace

replace = 1
ScriptUtil.openOPI(widget, "SomeDisplay.opi", replace, MacrosInput(LinkedHashMap(), True))

with

ScriptUtil.openDisplay(widget, "SomeDisplay.bob", "REPLACE", None)

If you need to pass macros, you can pass a plain Python map.

macros = { "NAME": "Value", "Other": "18" } 
ScriptUtil.openDisplay(widget, "SomeDisplay.bob", "REPLACE", macros)

Message Boxes, UI interactions

BOY scripts sometimes used SWT or JFace to open dialog boxes. In general, that should be avoided. While it is acceptable for the display editor to open dialogs, the display runtime must be able to simply keep running the display. A running display should never open dialogs which then prevent access to the rest of the user interface until somebody interactively completes the dialog.

Technically, the Display Builder uses JavaFX graphics, and the scripts run in background threads, so if you need to directly call JavaFX, you need to do this via Platform.runLater(...).

The ScriptUtil has helpers for common dialogs:

if ScriptUtil.showConfirmationDialog(widget, "Are you sure?"):
    ScriptUtil.showMessageDialog(widget, "OK then!")

Embedded Displays

To update the file shown in an embedded display widget, or to update the macros, see https://github.com/ControlSystemStudio/phoebus/blob/master/app/display/model/src/main/resources/examples/embedded/update_display.py

Table Widgets

BOY Display Builder
setColumnsCount(2) (just set headers)
setColumnHeaders([ "A", "B" ]) setHeaders([ "A", "B" ])
setContent(list_of_rows) setValue(list_of_rows)
setCellBackground(row, col, ColorFontUtil.getColorFromRGB(r, g, b)) setCellColor(row, column, WidgetColor(r, g, b))

BOY tables required attaching an ITableSelectionChangedListener when you want to track table selections.

The Display Builder table has a selection_pv property. It writes information about the currently selected table cells to that PV as a VTable, and you can then trigger a script by changes in that PV. See "Table" in example displays.