Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
This section provides more specifics on the EdgeTX Lua implementation. Here you will find syntax rules for interface tables and functions.
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:
-- /SCRIPTS/TestScript.lua
local c = ...
local function f(x)
return x + c
end
return fYou can load and use the above file with the following code:
local chunk = loadScript("/SCRIPTS/TestScript.lua")
local f1 = chunk(1)
local y = f1(5)
-- y = 5 + 1
local f2 = chunk(3)
local z = f2(5)
-- z = 5 + 3So 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.
local function counter(start, step)
local x = start
return function()
local y = x
x = x + step
return y
end
endlocal function match(x, ...)
for i, y in ipairs({...}) do
if x == y then
return true
end
end
return false
endThis 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 TX16 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 TX16 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 . 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 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.
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 .
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
string
since OpenTX 2.1.7
bit32
since OpenTX 2.1.0
math
since OpenTX 2.0.0
debug
-
Lua Standard Libraries
Included
package
-
coroutine
-
table
-
since OpenTX 2.1.0 (with limitations)
os
-
-- Main telemetry script
local shared = { }
shared.screens = {
"/SCRIPTS/Test/menu1.lua",
"/SCRIPTS/Test/menu2.lua",
"/SCRIPTS/Test/menu3.lua"
}
function shared.changeScreen(delta)
shared.current = shared.current + delta
if shared.current > #shared.screens then
shared.current = 1
elseif shared.current < 1 then
shared.current = #shared.screens
end
local chunk = loadScript(shared.screens[shared.current])
chunk(shared)
end
local function init()
shared.current = 1
shared.changeScreen(0)
end
local function run(event)
shared.run(event)
end
return { run = run, init = init }-- /SCRIPTS/Test/menu1.lua
local shared = ...
function shared.run(event)
lcd.clear()
lcd.drawText(20, 10, "Screen 1", MIDSIZE)
if event == EVT_VIRTUAL_NEXT then
shared.changeScreen(1)
elseif event == EVT_VIRTUAL_PREV then
shared.changeScreen(-1)
end
end-- /SCRIPTS/Test/menu2.lua
local shared = ...
function shared.run(event)
lcd.clear()
lcd.drawText(20, 10, "Screen 2", MIDSIZE)
if event == EVT_VIRTUAL_NEXT then
shared.changeScreen(1)
elseif event == EVT_VIRTUAL_PREV then
shared.changeScreen(-1)
end
end-- /SCRIPTS/Test/menu3.lua
local shared = ...
function shared.run(event)
lcd.clear()
lcd.drawText(20, 10, "Screen 3", MIDSIZE)
if event == EVT_VIRTUAL_NEXT then
shared.changeScreen(1)
elseif event == EVT_VIRTUAL_PREV then
shared.changeScreen(-1)
end
end-- main.lua
local name = "<widget name>"
local function create(zone, options)
-- Loadable code is called immediately and returns a widget table
return loadScript("/WIDGETS/" .. name .. "/loadable.lua")(zone, options)
end
local function refresh(widget, event, touchState)
widget.refresh(event, touchState)
end
local options = {
-- default options here
}
local function update(widget, options)
widget.update(options)
end
local function background(widget)
widget.background()
end
return {
name = name,
create = create,
refresh = refresh,
options = options,
update = update,
background = background
}-- loadable.lua
local zone, options = ...
-- The widget table will be returned to the main script.
local widget = { }
function widget.refresh(event, touchState)
-- refresh code here
end
function widget.update(opt)
options = opt
-- update code here
end
function widget.background()
-- background code here
end
-- Return to the create(...) function in the main script
return widgetIf 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.
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.
local function RGB(r, g, b)
local rgb = lcd.RGB(r, g, b)
if not rgb then
rgb = bit32.lshift(bit32.rshift(bit32.band(r, 0xFF), 3), 11)
rgb = rgb + bit32.lshift(bit32.rshift(bit32.band(g, 0xFF), 2), 5)
rgb = rgb + bit32.rshift(bit32.band(b, 0xFF), 3)
end
return rgb
endThis 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.
This 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. 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 apps.
The library is implemented in the /WIDGETS/LibGUI/libgui.lua. The widget that demonstrates how to use the library is implemented in the /WIDGETS/LibGUI/loadable.lua. The file /WIDGETS/LibGUI/main.lua contains a global function that loads the library and the standard functions needed for a widget.
The global function loadGUI() returns a new libGUI object. This object can be used to create new GUIobjects, which are used to create screens with elements like buttons, menus, labels, numbers that can be edited, and timers.
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 colors used for drawing the GUI elements. The following colors are available:
Notice that all of the default colors are theme colors. This will make the GUI screens use the contemporary color theme.
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 zone of the screen in widget mode. It takes no arguments.
A function f(event, touchState) to draw the screen background in full screen mode. The GUI elements are drawn afterwards on top.
A GUI (or another table with a function run(event, touchState). When this is set, the GUI will first be drawn, and then it will call prompt.run(event, touchState) instead of running itself. That way, the prompt can implement a modal prompt window.
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.
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.
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, events are passed to callBack(self, event, touchState). Thereby, the call back function can use events to edit the number, e.g. sliding a finger up and down can increase and decrease the value. You can look in the LibGUI widget's loadable file for an example of this.
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. This function returns a table with each of the menu's line elements.
visibleCount is the number of visible menu items.
items is a table with the menu item texts.
When a menu item is tapped, it calls callBack(self). Each menu element has a field self.idx giving the index in the menu, and this can be used by callBack to see which menu item was selected.
Notice that the menu's width is decided by the item texts and the font flags, and the height is decided by visibleCount and the font flags.
Background when a value is being edited.
active
COLOR_THEME_ACTIVE
Background on active toggle buttons and the border around selected elements.
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.
elements.blink will make non-button elements blink
elements.invers will make non-button elements draw in inversed colors.
Color
Default value
Used for
text
COLOR_THEME_PRIMARY3
Text on labels, menus etc.
focusText
COLOR_THEME_PRIMARY2
Text on buttons and numbers/timers being edited.
buttonBackground
COLOR_THEME_FOCUS
Background on buttons and numbers/timers being edited.
editBackground
COLOR_THEME_EDIT
-- This code chunk is loaded on demand by the widget's main script
-- when the create(...) function is run. Hence, the body of this
-- file is executed by the widget's create(...) function.
-- zone and options were passed as arguments to chunk(...)
local zone, options = ...
-- The widget table will be returned to the main script
local widget = { }
function widget.refresh(event, touchState)
if event == nil then -- Widget mode
-- Draw in widget mode. The size equals zone.w by zone.h
else -- Full screen mode
-- Draw in full screen mode. The size equals LCD_W by LCD_H
if event ~= 0 then -- Do we have an event?
if touchState then -- Only touch events come with a touchState
if event == EVT_TOUCH_FIRST then
-- When the finger first hits the screen
elseif event == EVT_TOUCH_BREAK then
-- When the finger leaves the screen and did not slide on it
elseif event == EVT_TOUCH_TAP then
-- A short tap gives TAP instead of BREAK
-- touchState.tapCount shows number of taps
elseif event == EVT_TOUCH_SLIDE then
-- Sliding the finger gives a SLIDE instead of BREAK or TAP
if touchState.swipeRight then
-- Is true if user swiped right
elseif touchState.swipeLeft then
elseif touchState.swipeUp then
elseif touchState.swipeDown then
-- etc.
else
-- Sliding but not swiping
end
end
else -- event ~= 0 and touchState == nil: key event
if event == EVT_VIRTUAL_ENTER then
-- User hit ENTER
elseif event == EVT_VIRTUAL_EXIT then
-- etc.
end
end
end
end
end