-
Notifications
You must be signed in to change notification settings - Fork 95
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.
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 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:
- https://github.com/ControlSystemStudio/phoebus/tree/master/app/display/model/src/main/resources/examples/connect2j
- https://github.com/ControlSystemStudio/phoebus/blob/master/app/display/model/src/main/resources/examples/python
- https://github.com/ControlSystemStudio/phoebus/tree/master/app/display/model/src/main/resources/examples/scripts
- https://github.com/ControlSystemStudio/phoebus/tree/master/app/display/model/src/main/resources/examples/scripts/python
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.
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.
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.
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.
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.
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")
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.
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")
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.
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
toloc://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.
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)
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!")
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
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.