All FrontPanel XML components have a built-in default behavior, for example an
okPushbutton is associated with a Wire In endpoint and pressing it results in asserting this wire while a
okLED is associated with a Wire Out and updated automatically when the value of the wire changes. However it is also possible to augment, or even replace, this default action with a user-defined script function, i.e. a small piece of code executed when the button is pressed or the corresponding wire out value changes.
Currently, FrontPanel scripts are written in the Lua programming language. Describing Lua is out of scope of this documentation, but it is a simple language with Pascal-like syntax and the provided examples of Lua scripts should be easy to read even for those completely unfamiliar with it. One unusual syntax element is that while accessing fields of Lua objects is done using the usual dot (
.) character, colon (
:) must be used to call object methods, e.g. here is a simple Lua event handler calling
MessageBox() method of the predefined
okUI global object:
The Lua home page contains links to Lua books and tutorials to find more information about the language.
Defining Script Functions
Scripting functions definitions can be written either directly in the FrontPanel XML file or in a standalone file with the
.lua extension. In either case, because the same scripting function can be used by different panels, they are defined inside a top-level element of the class
okProfile, which can contain
okScriptCode elements that, in turn, contain either a reference to the file containing the functions or the function definitions directly inside, respectively:
<object class="okProfile"> <object class="okScriptFile"> <file>test.lua</file> </object> <object class="okScriptCode" name="script_from_data"> <![CDATA[ -- Lua code here, e.g. define this simple button event handler function OnSayHiFromButton(button, event) okUI:MessageBox("Hello from button!") end ]]> </object> <object class="okInitScript"> <functionname>OnSayHi</functionname> </object> </object>Code language: HTML, XML (xml)
Because Lua code often contains characters that need to be escaped in XML, it’s convenient to use the XML CDATA mechanism when embedding the code directly in the XML file, however this is a matter of preference and it is also possible to embed the code and write
< instead of
< and so on instead.
Defining scripting functions directly in the FrontPanel XML is simpler for short examples, but it is a better idea to define more complex scripts in a separate file. If nothing else, this allows editing them conveniently in any text editor with Lua syntax highlighting support. Finally, note that both methods can be mixed and matched as desired and it is always possible to move some functions from the
.xfp to the
.lua file or in the other direction.
Associating a Script Function with a Component
Just defining a scripting function is not enough, as it won’t be called unless associated with an XML component (or run on profile initialization, see the corresponding section below). To do this, use the function name inside the
<functionname> XML tag supported by all scriptable XML components (i.e. almost all of them, with the exception of static and PLL ones). Here is an example of a component definition using the
OnSayHiFromButton() function shown above:
<object class="okPushbutton"> <label>Say _hi!</label> <position>20,30</position> <size>75,20</size> <functionname>OnSayHiFromButton</functionname> </object>Code language: HTML, XML (xml)
Pressing the button now will show the expected message box.
Writing Event Handlers
To write a useful handler, it is necessary to know about the context in which it is called and to be able to perform actions on the UI elements and the associated device. This is done by using either the handler arguments or the global
okUI object seen above. This section covers both of them in more details.
All event handlers take exactly two arguments, the first argument is the
source object representing the component which generated the event (note that it may be useful to call this argument according to the type of the component with which this scripting function is associated, i.e.
button if it’s a pushbutton,
gauge if it’s a progress bar and so on instead of just
source, but the event handler can use any name whatsoever for it). The second argument is the
event object (again, this name is just a convention and anything else can be used instead).
source object allows retrieval of information about the current object state and the ability to modify it if necessary. The
event object has several useful functions. First, it can be used to obtain the details of the event, for example, the current value of the associated Wire Out for the controls corresponding to a Wire Out or the key code which has been pressed for an
okKeyPanel component. Second, it provides the
GetDevice() method giving access to the FrontPanel device object that this profile is used with. Most of the usual methods of FrontPanel device API can be used on this object, e.g. it’s possible to get or modify the values of its wires, perform pipe transfers etc. Finally, the
event object can also be used to affect the default behavior of the component as explained in the next section.
In addition to these function arguments, there is also the global
okUI object representing the entire FrontPanel UI. It provides the
MessageBox() method that described above, as well as other similar simple methods, such as
PlaySound() to play the given WAV file, and debug logging helpers, covered in their own section below. However the most useful method defined by this object is
FindPanel(), which allows retrieval of the panel with the given name (this is the name used in the XFP file, e.g.
"panel1"). And the panel object, in turn, provides
FindControl() and other, more specific,
FindXXX() methods, that return the UI element with the specified name, finding any of the components defined in the XFP file.
So a typical handler might be written like this:
Note that in the previous example using
OnSayHiFromButton() function, pressing the button only showed the message box and didn’t do anything else, however usually a pushbutton is used to assert a wire at the given endpoint, e.g. this pushbutton will set the first bit of the first endpoint when pressed:
<object class="okPushbutton"> <label>Say _hi!</label> <position>20,30</position> <size>75,20</size> <endpoint>0x00</endpoint> <bit>0</bit> </object>Code language: HTML, XML (xml)
What happens if both the
bit elements and
functionname are used depends on the script function behavior. By default, the scripting function is executed first and then, if the
bit are specified, the corresponding wire is asserted. However the function can call
event:PreventDefault() to prevent this default action from taking place.
While this explanation only used the pushbutton for illustration, the same logic applies to all the other “input” scriptable XML components, i.e. those that are usually used for transmitting user input to the device. The only minor difference is that a pushbutton must have either
functionname inside it, as it would be useless otherwise, while some other components allow having neither of these tags, i.e. they can be completely passive and only exist in order to be modified from the script functions and, because of this, must have a name specified for them (as otherwise they would be impossible to use from script code and would be perfectly useless).
For the “output” XML components, i.e. those that show the device outputs to the user, the situation is slightly different, the
functionname element can be used only if
bit are specified, as the script function is called when the corresponding wire or trigger is changed and it would be never called if no device endpoint is associated with the component. On the other hand, the
name attribute of can only be specified if neither
bit (nor, necessarily,
functionname) are used, as using the component name it’s possible to update its state from the script functions and this could conflict with the updates due to the changes on the device — so having both the
name and the associated wire/trigger is simply forbidden.
To summarize, “output” component, such as
okLED, can be used in either “automatic” mode, where they update on their own, following the device state, or in “manual” mode, in which they are updated by the scripting code. In the automatic mode, the component must have
bit elements and may also have
functionname one. Moreover, in the latter case, the script function may call
event:PreventDefault() to prevent the default component UI update from taking place.
It can be useful to perform some initialization when loading the profile initially and the special
okInitScript pseudo-component can be used for this. This is not a real component, as it doesn’t appear on the screen, and hence it doesn’t belong to any panel but needs to be defined in the top-level element defining all the scripting functions.
As shown in the example above, this element has only a single required child element
functionname which specifies the function to call when the profile is loaded.
Note that multiple
okInitScript elements can be used and the functions corresponding to all of them will be called, in order of their appearance.
Scripting functions are programs, albeit small ones, and so will inevitably have bugs. FrontPanel doesn’t have any debugger for Lua code, so when things don’t work the only available debugging technique is printf debugging using the provided
DebugLog() and related functions:
The messages logged by these functions are not visible by default, which allows XFP developers to leave them in their code without any ill effects. To view debug log messages, select the “Show script debug log” menu item from the profile popup menu, accessible by clicking the colored sphere button in the main FrontPanel window.
Also note that
DebugLogValue() can be used with values of any type, including Lua tables and FrontPanel buffers, for which their hexadecimal dump is shown.
This section contains reference documentation for the Lua API exposed by FrontPanel, i.e. classes, functions and constants that can be used from the scripting functions. All of them are defined in
OpalKellyUI module, however it is mostly necessary to specify it only when using the constants, as object methods don’t need to be qualified and the only functions exposed by FrontPanel API are the debug log helpers which are defined outside of the module for convenience.
Global UI object
okUI object is accessible from all functions and provides the following methods:
Panel FindPanel(string name): Returns the panel with the given name (i.e. a string of the form “panel1”, “panel2” etc) or
nilif not found.
ClearDebugLog(): clears the debug log window.
DebugLog(string message): shown the given string in the debug log.
MessageBox(string message, number buttons = OpalKellyUI.OK): shows a modal dialog with the specified message and buttons, which can be a combination of
YES_NOconstants. Returns the button pressed in the dialog, which can be
NO. Please remember that the constants have to be qualified with the module name, so this must be written as
if okUI:MessageBox("Do it?", OpalKellyUI.YES_NO) == OpalKellyUI.YESin the code.
PlaySound(string filename): try to load a sound from the given file in WAV format and play it asynchronously (i.e. the function returns immediately, without waiting for the sound to finish playing).
boolean IsSystemAppearenceDark(): this function returns
trueonly when using dark mode under macOS 10.14+ and can be used by the scripts using custom colors to adjust the colors used depending on the system color mode.
string GetProfileDirectory(): this function returns the directory that the current XFP file was loaded from. (only available in FrontPanel version 5.1.1 and greater)
Panels represent a single window defined in the XFP file. Panel objects are obtained by calling
okUI:FindPanel() and can, in turn, be used to retrieve any of the components defined in them. The panel also provides access to the device object, which can be used to communicate with the FPGA (see FrontPanel API documentation) via its
GetDevice() method. If there is no connected device, this method returns
The panel object provides a generic
Control FindControl(string name) method for locating the object representing the component with the given name on this panel (if there is no component with such name,
nil is returned) as well as specific methods for the various components:
All of these methods also take the
string name parameter and return
nil if there is no component with the given name or if it’s not of the requested kind.
All components inherit from the base
Control class which provides the following methods:
SetLabel(string label): changes the label of the component, if it has one.
Move(number x, number y): moves the component to the specified position, in pixels.
SetSize(int width, int height): sets the size of the component.
SetBackgroundColour(Color color): sets the component background color.
SetForegroundColour(Color color): sets the component foreground or text color.
SetToolTip(string tooltip): shows the given tooltip when mouse hovers over the component.
The methods working with colors use the
Color class. Objects of this color can be constructed from
- Numeric RGB values, e.g.
OpalKellyUI.Color(0xff, 0xff, 0)will create yellow color.
- Color names, e.g.
OpalKellyUI.Color("yellow")will do the same thing.
- HTML-like color specifications in
- Using one of predefined constants, which are:
Color objects also have
Blue() methods for retrieving the corresponding channels values.
Event class provides a single
GetDevice() method returning the device currently used by FrontPanel. Classes deriving from
Event that provide additional methods are documented in the section of the component generating or using these events.