Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
This section will give some technical details for radios with a color screen.
An argument with drawing flags can be given to the various functions that draw on the LCD screen. The lower half of the flags (bits 1-16) are the flag attributes shown below, and the upper half of the flags (bits 17-32) are a color value.
Not all of the flags below should be directly manipulated from Lua, but those that are meant to be set directly by Lua, are accessible by the Lua flag constants.
Since the flags are bits, you can add them up to combine, as long as you only add each flag one time. If you add the same flag twice, then it will add up to the next bit over, e.g. INVERS + INVERS = VCENTER
. If you want to add a flag to a value where it may already be set, then use flags = bit32.bor(flags, INVERS)
.
RGB_FLAG decides how the color value is encoded into the upper half (bits 17-32).
If RGB_FLAG = 0, then an index into a color table is stored. This color table holds a default color (index 0), the theme colors, and CUSTOM_COLOR. The entries in the color table can be changed with the function lcd.setColor
. The advantage of this system is that the color changes everywhere that this indexed color is used, and this is how different color themes are created. Notice that changing the theme colors affects the entire user interface of your radio!!
If RGB_FLAG = 1, then a 16-bit RGB565 color is stored. This is used directly by the system to draw a color on the screen.
You should not change RGB_FLAG explicitly; this is handled automatically by the various functions and Lua constants. But you should be aware of the following.
lcd.setColor
must have an indexed color as its first argument, because this will be the index of the color in the table being changed. Giving another color, e.g. ORANGE, as the first argument will result in nothing.
If no color is given to the flags with a drawing function, RGB_FLAGS = 0 and the color index = 0. Therefore, the default color is stored in the color table under this index, and you can change the default color with lcd.setColor(0, color)
.
lcd.getColor
always returns a RGB color. This can be used to "save" an indexed color before you change it.
lcd.RGB
obviously returns a RGB color.
OpenTX only supports indexed colors in drawing functions, so you must first call lcd.setColor
to change e.g. CUSTOM_COLOR, and then call the LCD drawing function with that indexed color. In EdgeTX, you can use either type of color for drawing functions, so you are no longer forced to constantly call lcd.setColor
. You can also store any color in local variables, and then use these when drawing, thus effectively creating your own color theme.
In OpenTX, lcd.RGB
returns a 16 bit RGB565 value, but in EdgeTX it returns a 32 bit flags value with the 16 bit RGB565 value in the upper half (bits 17-32). Therefore, colors in EdgeTX are not binary compatible with colors in OpenTX. But if you use the functions lcd.RGB
, lcd.setColor
, and lcd.getColor
, then your code should work the same way in EdgeTX as in OpenTX.
Unfortunately, OpenTX has disabled the function lcd.RGB
when the screen is not available for drawing, so it can only be called successfully from the refresh
function in widgets and from the run
function in One-Time scripts. Therefore, some existing widget scripts set up colors with hard coded constants instead of calling lcd.RGB
during initialization, and this is not going to work with EdgeTX, because of the different binary format.
A pull request has been submitted to OpenTX, allowing lcd.RGB
to work also during widget script initialization, and hopefully, it will be merged into OpenTX 2.3.15. If that happens, then the obvious way to solve the problem is to use lcd.RGB
values instead of hard coded color constants. But in the meantime, the following RGB function can be used for setting up colors in a way that works for both EdgeTX and OpenTX.
This functions calls lcd.RGB
, and if it gets a nil value (because we are running a widget script under OpenTX, and it is not called from the refresh
function) then it creates the 16-bit RGB565 value that OpenTX wants.
This chapter will show you some ways that large script projects can be fitted into the limited memory of our radios.
Regarding memory, the situation is a bit different for the radios with black/white or grey scale screens and telemetry scripts, and the radios with color screens and widget scripts. The telemetry script radios only have 128-192 KB RAM memory - that is very small! The widget script radios have 8 MB RAM memory. But the way that widgets are designed means that all widget scripts present on the SD card will be loaded into memory, whether or not they are actually used. Therefore, different strategies should be applied to save memory for the two different types of radios and scripts.
Radios with black/white or grey scale screens and telemetry scripts such as e.g. FrSky Taranis, Xlite, Jumper T12 and Radiomaster TX12 have extremely small RAM memories, and therefore it may be necessary to divide up your script into smaller loadable modules.
The following simple example demonstrates how different screens can be loaded on demand, and how shared data can be stored in a table.
The table shared
contains data that is shared between the main telemetry script and the loadable screens. Notice that the functions shared.changeScreen
and shared.run
are also shared this way.
Code is loaded by shared.changeScreen
with the loadScript
function, which returns the loadable script as a chunk of code. The code is executed with shared
as the argument, and the loadable script adds a new run
function to the shared
table. shared.run
is called by run
in the main script.
Radios with color screens and widget scripts such as e.g. FrSky Horus, Jumper T16 and Radiomaster TX16S have fairly large RAM memories, but since all widget scripts present on the SD card are always loaded into memory, they could run out of memory if many big widget scripts are present on the card - even if they are not being used by the selected model. Therefore, large widget scripts should be divided into a small main script and a large loadable part. One way to accomplish this is the following.
The create
function loads the file loadable.lua
in the folder /WIDGETS/<widget name>/, and calls it immediately as described in the previous section. It passes zone
and options
as arguments to loadable.lua
. This scripts adds the functions refresh
, update
and (optionally) background
to the widget
table:
zone
and options
are stored in the closure of loadable.lua
, therefore they do not need to be added to the widget
table, as is commonly done.
Obviously, the bulk of the widget's code goes in loadable.lua
, and is only loaded if the widgets is in fact being used. Therefore, if the widget is not used, only the small amount of code in main.lua
is loaded into the radio's memory.
For an example of a widget that uses the above design pattern, please have a look at the EventDemo widget that is included on the SD card with EdgeTX for color screen radios.
Lua makes it easy to load and unload code modules on the fly, to save memory or to provide program extensions.
The loadScript(<file>)
function will load a script from a the file and return a function that is the body of the script, as described in the previous section. So you could have the following Lua script file saved on the SD card:
You can load and use the above file with the following code:
So here we put together what we learned in the previous section. The body of the script is an anonymous function returned by loadScript
and stored in the variable chunk
. It returns the function f
when it is called. The local variable c
in the script is assigned to the first vararg passed to the call. Since a new closure is created every time we call chunk
, f1
and f2
have different closures with different values of c
.
Lua is a small but powerful language. This section will explain a few of the most important concepts.
Lua was chosen for OpenTX, and hence also EdgeTX, because it is a small language designed to be highly extensible by libraries written in C and C++, so it can be integrated into other systems like EdgeTX. It is also relatively efficient, both in terms of memory and CPU usage, and hence well suited for the radios.
In addition to the provided libraries, Lua has a very elegant mechanism for loading new Lua code modules during run-time. A lot of the elegance comes from the way that the loading mechanism meshes with another concept supported by Lua: first class functions with closures.
Computer science pioneer Christopher Strachey introduced the concept of functions as first-class objects in his paper Fundamental Concepts in Programming Languages from 1967. What it means is that functions can be treated as other variables: as arguments passed in function calls, as results returned from function calls, and a function identifier can be re-assigned to another chunk of function code, just like a variable ca be assigned to a new value.
In Lua, a function declaration is really "syntactic sugar" for assigning a variable to the chunk of code that is called when the function is invoked, i.e.
local function f(x) return x + 1 end
is the same as
local f = function(x) return x + 1 end
You can even add functions to Lua tables, i.e.
t = { f = f }
will add the above function to the table t
, as a field that is also called f
. Does that look familiar to the return statement required at the end of a Lua script?
Yes indeed, because a script is really an anonymous function that returns a list of functions to the system. The function declarations assign variables to chunks of function code, these variables are added to the list returned at the end of the script, and the system then calls the functions periodically to run the script. So the script itself is run one time initially, and subsequently the functions returned by the last statement are called periodically.
Another important concept that goes with first-class functions, is closures. This is the environment of the function with the variable names that the function can see. Please consider the following function counter
that returns another function:
The function is returned directly without being assigned to a variable name. The closure of the function returned is the body of the function counter
, containing both the arguments start
and step
, and the local variable x
. So if c1 = counter(1, 1)
then c1()
will return 1, 2, 3, ... when called repeatedly, and if c2 = counter(2, 3)
then c2()
will return 2, 5, 8, ...
Likewise, the local variables that you declare outside the functions of your script can be used by all of the functions in your script, and they persist between function calls, but they are not visible to other scripts.
The widget scripts are a little trickier, as you can register multiple instances of the same widget script, and all of these instances run within the same Lua closure. Therefore, local variables declared outside any functions in a widget script are shared among all of the instances of that script. But each call to the create(...)
function returns a new widget
list to the system. And since this list is unique to each instance, you can add private instance variables to it.
Please consider this function:
It takes an argument x
and a vararg list ...
A vararg list is just like a list of arguments separated by commas, and you can call the function with any number of arguments greater than 1. Inside the function, you can also treat ...
like a comma separated list of variables, e.g. in the above function, ...
is converted to a table by {...}
just like you could construct a table by e.g. {a, b, c}
. The table is iterated with the ipairs
function to look for an element matching the first argument x
. So e.g. the following statement
if event == EVT_VIRTUAL_ENTER or event == EVT_VIRTUAL_EXIT then
can be replaced by
if match(event, EVT_VIRTUAL_ENTER, EVT_VIRTUAL_EXIT) then
You can also use ...
directly as a comma separated list of values, e.g. local a, b, c = ...
will assign the three variables to the three first arguments following x
, or nil
if none are given.
The Lua 5.2 Reference Manual helpful, both if you want to learn more about Lua, and if you want to search for answers to specific questions.
The io.seek() function is used to move the current read/write position.
Notice: other read standard seek bases (like "cur", "end") are not supported.
file object
a file object that was returned by the io.open() function.
offset
position the read/write file pointer at the specified offset from the beginning of the file. If specified offset is bigger than the file size, then the pointer is moved to the end of the file.
0
success
<number>
any other value means failure.
The io.open() function is used to open the file on SD card for subsequent reading or writing. After the script is done with the file manipulation io.close() function should be used.
filename
full path to the file starting from the SD card root directory. This function can't create a new file in non-existing directory.
mode
supported mode strings are:
"r"
read access. File must exist beforehand. The read pointer is located at the beginning of file. This is the default mode if is omitted.
"w"
write access. File is opened or created (if it didn't exist) and truncated (all existing file contents are lost).
"a" write access. File is opened or created (if it didn't exist) and write pointer is located at the end of the file. The existing file contents are preserved.
<file object>
if file was successfully opened.
nil
if file could not be opened.
The io.read() function is used to read data from the file on SD card.
Notice: other read commands (like "all", etc..) are *not supported.
file object
a file object that was returned by the io.open() function. The file must be opened in read mode.
length
number of characters/bytes to read. The number of actual read/returned characters can be less if the file end is reached.
<string>
a string with a length equal or less than
""
a zero length string if the end of file was reached
This section provides more specifics on the EdgeTX Lua implementation. Here you will find syntax rules for interface tables and functions.
This section will discuss how interactive scripts receiving user inputs via key and touch events can be created.
The two Lua widgets EventDemo and LibGUI are provided on the SD card content for color screen radios. EventDemo is just a small widget showing off how key, and especially touch events, can be used. LibGUI contains a globally available library, and a widget showing how the library can be used by other widgets. This section will discuss these two widgets for color screen radios, but generally, what is stated about key events here also applies to the run
function in Telemetry and One-Time scripts.
This widget uses the design pattern for saving memory by loadable files discussed in the previous section, so all of the action takes place in the file loadable.lua. The following code listing is an outline of the refresh
function with comments explaining what is going on.
LibGUI was created to support the SoarETX widgets, but it was made separate so everyone can use it, if they so desire. Since it is all Lua, the code is visible for everyone to study and use as is, or modify for their own use (as long as they comply with the terms of the Gnu Public license, of course).
It is a widget that comes with a global library. Since all widgets are loaded, whether or not they are being used, global functions declared in the body of a widget script will always be available in any widget script in the create
and refresh
functions. It is not necessary to setup the widget to use the library, and the only purpose of the widget is to show how LibGUI
can be used to create widget apps.
The library is implemented in the loadable file /WIDGETS/LibGUI/libgui.lua. The widget that demonstrates how to use the library is implemented in the loadable file /WIDGETS/LibGUI/loadable.lua. The file /WIDGETS/LibGUI/main.lua contains the standard functions needed for a widget and handles the loading of the two other files.
The global function loadGUI()
returns a new libGUI
object. This object can be used to create new GUI
objects, which are used to create screens with elements like buttons, menus, labels, numbers etc.
libGUI
has the following properties controlling general settings for the library.
These are default drawing flags to be applied if no flags are given at creation of screen elements.
Note: these flags should not contain color values, as colors are added by the following.
This is a table of the colors used for drawing the GUI elements. These are all theme colors by default, but they can be changed for the libGUI
instance without changing the theme colors anywhere else.
A function f()
to draw the zone of the screen in widget mode. It takes no arguments.
This is a small utility function that returns true
if the first argument x
matches any of the following arguments. It is useful for comparing values of event
, e.g. if we want to test if the user pressed either of the following buttons, we can use:libGUI.match(event, EVT_VIRTUAL_ENTER, EVT_VIRTUAL_EXIT, EVT_VIRTUAL_MENU)
This is the main function that creates a new GUI
object. If an application has different screens, then a GUI
object is created for each screen.
A function f()
to draw the screen background in full screen mode. The GUI elements are drawn afterwards on top.
Redraws the screen and processes key and touch events. It can directly replace a widget's refresh
function, or it can be called by refresh
.
Sets a function f(event, touchState)
to handle an event. If no GUI element is being edited, then this can trap events before they are passed to the GUI, e.g. to press EXIT to go back to the previous screen. If f
is nil
, then the event handler is removed.
If the function returns another event value, then it is passed to the active screen element (i.e. the handler can modify certain events).
It cannot trap the events EVT_VIRTUAL_PREV
, EVT_VIRTUAL_NEXT
as these events are used for navigation. If elements are being edited, then this also takes precedence over the event handler.
This function can be used to show a modal prompt window, or trap events (alternative to setEventHandler
). guiPrompt
is a GUI
or another Lua table with a function run(event, touchState
).
When it is set, the main GUI will first be drawn, and then it will call guiPrompt.run(event, touchState)
instead of its own onEvent
function.
Dismisses the prompt.
The screen elements are drawn in the order that they are added to the GUI, and touch events are sent to the first element that covers the touched point of the screen. GUI elements therefore should never overlap.
There are some common properties that can be set for all or most of the GUI elements.
element.disabled = true
prevents the element from taking focus and receiving events, and disabled buttons are greyed out.
element.hidden = true
in addition to the above, the element is not drawn.
element.title
can be changed for elements with a title.
element.value
can be changed for elements with a value.
element.flags
drawing flags for the element's text. If no flags were given at element creation, it defaults to GUI.flags
.
The various screen elements are added to the GUI with the functions described below. The functions all add the element to the GUI and returns a reference so the element subsequently can be accessed by the client.
Add a button to the GUI.
When tapped, it calls callBack(self)
so a call back function can tell which button activated it.
Add a toggle button to the GUI.
The value
is either true
or false
.
When tapped, it calls callBack(self)
so a call back function can tell which toggle button activated it, and what self.value
is.
Add an editable number to the GUI.
The value
can be either a number or text. By setting the value to a text, it can be shown that the number is not present e.g. by "- -" or similar.
When tapped, the number will go to edit mode. In edit mode, and changeValue(d, self)
is called if the increase/decrease buttons are pressed, or a finger is slided up or down from the number. The parameter d
is the number of "ticks" by which the number is being changed, and changeValue
must return the new value for the number. In a simple case, the new value would just be self.value + d
, but it could also select new values from a table etc.
Add a timer to the GUI.
If no value
is present, then the model timer tmr
will be shown. If value
is a number, then it indicates the time in seconds, and it will be shown as MM:SS. The value
can also be text, e.g. "- - : - -" to show that the timer is disabled.
When tapped, the timer will go to edit mode, as described above for number.
Add a text label to the GUI.
The label does not respond to any events, but its title
and flags
can be changed.
Add a scrollable menu to the GUI.
items
is a table with the menu item texts.
When a menu item is tapped, it calls callBack(self)
and self.selected
gets the index of the selected item.
Add a field with values that can be selected on a drop down menu.
items
is a table with the items that can be selected from.
selected
is the index of the initially selected item.
When an item has been selected, it calls callBack(self)
. The index of the selected item is given by self.selected
.
Adds a horizontal slider that starts at (x, y)
and has the width w
.
value
is the initial value, and min - max
is the interval of values that can be selected with step size delta
.
When the value is changed, callBack(self)
is called, and the value is given by self.value
.
The same as the above, just vertical.
This can be used to create your own custom GUI elements. self
is a table containing the element. You must define the functions self.draw(focused)
and self.onEvent(event, touchState)
. There is a function self.drawFocus(color)
defined that can draw a border around the element (use if focused == true).
Create a nested sub-GUI. This can be used to group GUI elements which will be offset by (x, y)
relative to the parent GUI
.
An EdgeTX model has several data components that can be exchanged with Lua scripts. This section gives an overview.
In general terms, there are two different types of data that can be exchanged with EdgeTX:
Values, which generally fall on a scale between -1024 and 1024, also sometimes reported as -100% to 100%, e.g. for mixer values and channel outputs.
Switches, which are either true
or false.
A specific data source type, e.g. Global Variables, can be indexed directly with an index that starts with 0 for the first one. This can be a little confusing, as e.g. GV1 for FM0 is read with model.getGlobalVariable(0, 0)
, because GV1 is the first global variable, and FM0 is the first flight mode. But as long as you remember that all direct indices start with 0 for the first one, you should be fine!
Global Variables is a value data source that can return a value between -1024 and 1024. Examples of value sources are:
Global Variables
Sticks, sliders and knobs
Trim values
Input lines
Channel outputs
Trainer channel inputs
Telemetry sensors
There is a way to access a value of any such type with a meta-index. You use the function getFieldInfo(name)
where name
can be any of the valid source names listed here. It returns a table with a description of the value source, including the field id,
which is the meta-index.
You can use id
with the function getValue(id)
to read the current value. You can also use name
directly, but this is less efficient, as EdgeTX does a linear search for the name every time it is called. Therefore, the procedure of first extracting the meta-index from getFieldInfo(name).id
, and then using that, is recommended.
For switches, getLogicalSwitchValue(index)
uses a direct index to read this specific type of switch sources, again using the index
0 for LS1 etc. But there are also several types of switch sources, such as:
Logical switches
Switch positions, testing if a physical switch is in a particular position, e.g. SA↑
.
Trim buttons
Transmitter and telemetry activity
You can find the meta-index of a particular switch source with getSwitchIndex(name)
, where name
is exactly the name that you see in the radio menus where you can select a switch source.
You can read the current value of a switch source with getSwitchValue(index)
as a true
/false
value.
It can be very confusing that some source can be read both as a value and as a switch. As an example, elevator trim can be read as the current value of the trim:
Or it can be read as the current value of the trim button position:
These are really two different things, as the elevator trim is an internal value stored by the radio, which is changed with the trim button, and the button is a physical button.
But it can also be a more direct comparison, e.g. the 3-position switch B, which can be read as a value source with:
Or the current position of switch B can be read with:
Hopefully, these examples illustrated the differences between values and switches, and showed how these can be retrieved in a Lua script.
You can also send data the other way: from Lua to the EdgeTX model setup.
To send a value, use a global variable: model.setGlobalVariable(index, fm, value)
. If you use the default GV setting, where all other flight modes use the value of FM0, then you can use 0 for fm
.
To send a switch, setup a STICKY
type logical switch, and then use setStickySwitch(index, true/false)
.
The following table gives an overview of the Lua API functions that can be used to exchange data with the EdgeTX model setup.
Lua Standard Libraries | Included |
---|---|
The io library has been simplified and only a subset of functions and their functionality is available. What follows is a complete reference of io functions that are available to EdgeTX scripts
io.open()
io.close()
io.read()
io.write()
io.seek()
Color | Default value | Used for |
---|---|---|
Function | Description |
---|---|
package
-
coroutine
-
table
since OpenTX 2.3.8
(color LCD radios only)
since OpenTX 2.1.0 (with limitations)
os
-
string
since OpenTX 2.1.7
bit32
since OpenTX 2.1.0
math
since OpenTX 2.0.0
debug
-
primary1
COLOR_THEME_PRIMARY1
Text on dropDown menu
primary2
COLOR_THEME_PRIMARY2
Text on buttons and numbers/timers being edited
primary3
COLOR_THEME_PRIMARY3
Text on labels, menus, and numbers/timers
focus
COLOR_THEME_FOCUS
Background on buttons and numbers/timers being edited.
edit
COLOR_THEME_EDIT
Background when a value is being edited.
active
COLOR_THEME_ACTIVE
Background on active toggle buttons and the border around selected elements.
getFieldInfo(name)
Gets the meta-index of a value source with name
.
getValue(index)
Gets the current value of a source, where index
is from the above.
getFlightMode()
Returns the current flight mode number and name. Do not confuse with model.getFlightMode
, which is a completely different function!
model.getGlobalVariable( index, fm)
Read the value of a global variable between -1024 and 1024. You can use the above to get the current flioght mode for fm
, or you can setup the GV to always use the value from FM0.
model.setGlobalVariable( index, fm, value)
Sets the value of a global variable.
setTelemetryValue(id, subID, instance, value, ...)
This function can send data from Lua to a telemetry sensor. If it is running, then the sensor will show up with Discover Sensors. This can be used to log values generated by Lua.
getLogicalSwitchValue( index)
Get the value of a logical switch.
setStickySwitch(id, value)
Set the value of a STICKY
type logical switch.
getSwitchIndex(name)
Get the meta-index of a switch source with name
.
getSwitchName(index)
Get the name of a switch source.
getSwitchValue(index)
Get the value of a switch source.
getPhysicalSwitches()
Get a list of physical switches on the radio. Can be used to make a selection list.
getShmVar(index)
Get the value of a shared memory variable. These are little "mail boxes" provided as a way to send data between Lua widgets and other Lua scripts like Mixer and Function scripts.
setShmVar(index, value)
Set the value of a shared memory variable.
model.setTimer(timer, value)
Set the timer to value
.
model.getTimer(timer)
Read the timer.
The io.write() function is used to write data to the file on SD card.
file object
a file object that was returned by the io.open() function. The file must be opened in write or append mode.
data
any Lua type that can be converted into string. If more than one data parameter is used their contents are written to the file by one in the same order as they are specified.
<file object>
if data was successfully opened.
nil, <error string>, <error number>
if the data can't be written.