FrontPanel Scripting

FrontPanel XML Lua Scripting is provided as a beta feature in FrontPanel 5.1.

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:

Lua:

function OnSayHi(event)
	-- This is just a comment
	okUI:MessageBox("Hello FrontPanel!")
endCode language: JavaScript (javascript)

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 okScriptFile and okScriptCode elements that, in turn, contain either a reference to the file containing the functions or the function definitions directly inside, respectively:

XML:

<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 &lt; 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: 

XML:

<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).

The 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:

Lua:

function OnDigitDisplay(source, event)
	local device = event:GetDevice()
	local value_wire_33 = device:GetWireOutValue(0x21)

	okUI:FindPanel("panel1"):FindDigitDisplay("digitDisplay2"):SetValue(2*value_wire_33)
endCode language: JavaScript (javascript)

Default Behaviour

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:

XML:

<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 endpoint and bit elements and functionname are used depends on the script function behavior. By default, the scripting function is executed first and then, if the endpoint and 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 endpoint/bit or 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 endpoint and 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 endpoint nor 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 endpoint and 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.

Initialization Script

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.

Debugging

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:

Lua:

-- The simplest function is DebugLog(), which just shows it output in the
-- debug window:
DebugLog("Button is pressed")

-- A useful helper for quickly showing the value and the type of some value:
-- this will show something like "answer (number) = 17"
n = 17
DebugLogValue("counter", n)

-- Finally there is a convenient wrapper taking string.format()-like format:
DebugPrintf("This is event #%d from the button %s", count, button)Code language: JavaScript (javascript)

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.

Scripting API

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

The global 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 nil if 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 OKCANCEL and YES_NO constants. Returns the button pressed in the dialog, which can be OKCANCELYES or 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.YES in 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 true only 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)

Panel objects

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 nil.

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:

  • FindCombobox()
  • FindDigitDisplay()
  • FindDigitEntry()
  • FindGauge()
  • FindHexDisplay()
  • FindLED()
  • FindLog()
  • FindMessageLog()
  • FindSlider()
  • FindToggleCheck()

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.

Control class

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 #RRGGBB format, e.g. OpalKellyUI.Color("#ffff00").
  • Using one of predefined constants, which are:
    • Black
    • Blue
    • Cyan
    • Green
    • LightGrey
    • Red
    • White
    • Yellow
    (as all the other constants, they must be prefixed by OpalKellyUI. when used).

Color objects also have Red()Green() and Blue() methods for retrieving the corresponding channels values.

Event class

The base 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.