Thread Tools
This thread is privately moderated by l shems, who may elect to delete unwanted replies.
Oct 29, 2017, 05:46 AM
Have Fun and Just Fly!
l shems's Avatar
Discussion

Tips and Tricks for LUA on OpenTX


OK, new blog entry.
I will share some of my experiences and solutions to problems I encountered using the LUA scripting on openTX. Please feel free to comment. It is meant to become a help to anyone wanting to create, adapt or migrate any LUA script for OpenTX. Tutorial files are added in this first post, and in the last of this thread.

It is assumed that you have read and at least partially understood the OpenTX LUA reference guide. I will assume OpenTX 2.2, OpenTX 2.1 should work as well for all examples, unless stated otherwise, but OpenTX 2.0 is not going to work without extra lines managing the lcd.lock() functionality. Some special article will covering the OpenTX 2.0 specialties, to allow creating interoperability.

Also, if you intend to use or change the scripts, or follow some instructions on this site, I will assume you have ZeroBrane installed. This helps enourmously in developing and testing LUA scripts.

The on-line LUA manual is used as a reference as well. We use the LUA 5.2 version, as OpenTX is build around this.

Be aware that OpenTX is not designed to be LUA friendly! ! !

OpenTX has a LUA API, but that is about it. The people behind OpenTX probably didn't imagine how powerful LUA can be, and that it opens the possibilities for completely different approaches towards using an OpenTX radio. They probably don't even have any interest in the possibilities it opens, and is for them just an add-on provided to complete their framework.
Be aware of that when developing OpenTX LUA scripts. Chances are, they will work today, but not tomorrow.

Disclaimer:
As I am just a user, and in no way related nor involved in OpenTX. I will be open and unbiased in any comments I give. These comments can both be positive and negative.

That also means that some people might feel offended, since I will often make remarks that things are not properly, or inconveniently, implemented in the LUA API for OpenTX. These comments mean in no way that I WANT to critisize or WANT to offend the people building OpenTX. It just might happen along the way, and is not intended as such. But I won't choose my words in order to prevent this happening. Sorry, but that is the way I want to be able to proceed in my personal Blog. If you don't like it, don' read it.

Since OpenTX is open source, and everyone can add or change whatever they want by forking the code or adding to the existing code, nobody can make any claim on what the developers could or should develop or not develop. So me neither.

At the same time, since it is opensource, also the developers of OpenTX are bound to the same open source paradigma: everyone can do with OpenTX whatever he wants, even commercially. If as a developer you have a problem with that, stop sharing your code under the license you are sharing it, since that explicitly allows for that.

Special remark to LUA scripts: every LUA script is a piece of intellectual property (IP) of it's own, whether it is written for use on an OpenTX radio, or for any other device that implemented LUA. The developers of OpenTX can as such never claim nor object any script, shared under any license, open source or licensed under whatever license. This is a consequence of exactly that which makes opensource possible: the protection of intellectual property.

All scripts I post on this site are free to use, and are for you to copy, share, adapt, whatever you wish to do with it. If you think that I posted something that is already bound to some license, please post that info, and I will take proper action.
Last edited by l shems; Nov 05, 2017 at 06:08 AM.
Sign up now
to remove ads between posts
Oct 29, 2017, 07:06 AM
Have Fun and Just Fly!
l shems's Avatar

The OpenTX LUA API explained


Ok, so how does OpenTX use the code that you tell it to load?

The main principle is that it will load the script file as a function, and therefore it will expect the script to return some predefined information. You can check the reference guide for specifics on that, but I will use the 'One-time' script as an example.

A 'One-time' script, when loaded, needs to return at least the 'run' function.

LUA itself works a lot like the oldschool 'BASIC' language, so the following code should look familiar for those knowing 'BASIC':

Code:
local run = function (event)
  local i = 0
  while true do
    lcd.clear()
    lcd.drawText(i,10,i,MIDSIZE)
    i=i+1
    if i>=LCD_W then
      i=0
    end
  end
  return 0
end

return {run=run}
What happens if we try to run that code? On first glance it looks fine, doing the following steps:
  1. clear the screen
  2. put the counter on the screen at the position of the counter
  3. untill the counter has reached the end of the screen
  4. at which it will restart the counter

Try to load that code into a 'One-time' script, save it as "/SCRIPTS/movingCounter.lua", and run it by long pressing the 'menu' button to enter the radio menu, pressing 'page' once to get to the SDcard content, and then browse to the script. 'long press enter' and the transmitter will ask to execute it. 'enter' again, and of you go.

Ouch, error message: 'Script Killed, CPU limit'.
Name: screenshot_x9d+_17-10-29_13-34-11.png
Views: 11
Size: 1.4 KB
Description:
Well, that is not nice. They just killed your script! The reason is, that OpenTX needs to do other things than just running your code. How is that accomplished?

Well, the 'run' function is called every xx milliseconds for execution. It is allowed to run for a certain time, performing a certain number of transactions. Well, we didn't let go of the function. We told it to run forever. So therefore, OpenTX will detect it is running to long, or running to many instructions, and stop it from running. In this case by "killing" it.

In the next post, how to properly implement this.
Last edited by l shems; Oct 31, 2017 at 05:15 AM.
Oct 29, 2017, 07:32 AM
Have Fun and Just Fly!
l shems's Avatar

OpenTX LUA API explained part II


Ok, so we didn't succed in our first try. Lets use the information we obtained to change the script. The 'run' function is called periodically, and needs to 'let go' of the processor without exceeding a certain maximum number of transactions (my practical experience is about 1000-4000 transactions are allowed, so it is not that limiting ).

We need to eliminate the loop, since the OpenTX API is already looping the 'run' function itself:
Code:
local run = function (event)
  local i = 0
  lcd.clear()
  lcd.drawText(i,10,i,MIDSIZE)
  i=i+1
  if i>=LCD_W then
    i=0
  end
  return 0
end

return {run=run}
Actually, the code became simpler . Lets try it:
Name: screenshot_x9d+_17-10-29_13-18-56.png
Views: 13
Size: 565 Bytes
Description:

OK, no running text, no increase of the counter. Just a zero. But at least we got something

What's wrong?

Nothing, but we need a way to initiate the counter 'i' outside of the function. Otherwise, it get's reset every time the function is called. Normally, we would do that by passing the counter to the function, increasing it there, and then return the result to the calling script so that it can update this parameter. Something like this:
Code:
local i = 0

local run = function (i)
  lcd.clear()
  lcd.drawText(i,10,i,MIDSIZE)
  i=i+1
  if i>=LCD_W then
    i=0
  end
  return i
end

return {run=run}
But how is OpenTX to know that it needs to pass 'i' to the 'run' function? And even if we could get that part solved, how to assign the result of the 'run' function adding transactions on 'i' again after execution? Since 'we' can't control the way the 'run' function is called, nor how it's result is processed, there needs ot be an other way around.

This is where the LUA concept of 'upvalues' comes into play. An 'upvalue' is a local variable that is declared in the same 'chuck' of code, and within LUA, this local is known within all functions declared within the same 'chunk'. Using this concept, the code should look like this:
Code:
local i = 0

local run = function ()
  lcd.clear()
  lcd.drawText(i,10,i,MIDSIZE)
  i=i+1
  if i>=LCD_W then
    i=0
  end
  return 0
end

return {run=run}
Actually, this is again a simplification. Well, this LUA is interesting stuff. Just some lines of code and we have a running "One-time' script.
Name: screenshot_x9d+_17-10-29_13-32-12.png
Views: 8
Size: 633 Bytes
Description:
Last edited by l shems; Oct 31, 2017 at 05:16 AM.
Oct 29, 2017, 08:15 AM
Have Fun and Just Fly!
l shems's Avatar

Running a 'telemetry' script as a 'One-time' script.


First practical use of our knowledge: Running a 'telemetry' script as a 'One-time' script.

The structure of a 'One-time' script is only slightly different from a 'telemetry' script. Still, you can't run a 'telemetry' script as a 'One-time' script. (In theory you could, but most 'telemetry' scripts don't strictly follow the rules. 'One time' script execution crashes on that).

This small LUA script solves that problem: it can run any 'telemetry' script as a 'One-time' script. This means that you can theoretically also run Taranis' 'telemetry' scripts as 'One-time' scripts on the Horus. Isn't that nice ?
More on that later.

Code:
Code:
local script =  loadfile("/SCRIPTS/Telemetry/yourscript.lua")()
local Legacy = false

local run = function (event,...)
  
  if not Legacy then --legacy scripts will call the background from the run themselves
    if script.background then 
       script.background()
    end
  end

  return script.run(event,...) or 0
end

return {run=run, init=script.init}
You have to set the script to be loaded in the first line. In the second line, you have to set 'Legacy' to 'True' when you are using a version of OpenTX before 2.1.

How does this work:
First, we load the code of the 'telemetry' script with 'loadfile' into the workspace of this script. Then, by adding the '()', we immediatly execute that code as a function, and the results of that execution returns are stored in the local variable 'script'.

Remember, a 'telemetry' script returns '{run=somefunction, init=someotherfunction, background=yetsomeotherfunction}' when called as a function, so the local 'script' variable now contains the three functions from the original script, accessible by 'script.init', 'script.run' and 'script.background'.

Now we can 'reconstruct' the intended operation of the 'telemetry' script in our 'One-time' script.

We first need to decide what to do with the 'background' function. In the first implementation of LUA for OpenTX, for version 2.0, the 'background' function from a 'telemetry' script is NOT called when the 'run' function is called (so when the script is running in the active, visible, screen). So, if the 'run' function needs the 'background' function to be run, it will call it himself.
Later, as from 2.1 on, they decided that the 'background' function will be called always, so since this is a 'One-time' script with only a 'run' and 'init' function, you need to call the 'background' function within the 'run' function to mimic the behaviour of OpenTX 2.1 and on for 'telemetry' scripts. But we need to make sure it exists first off course. Therefore the 'if background then'.

This is done as first action in the new 'run' function.

If it is a 'telemetry' script written for 2.0, this is actually not neccesary, since it will call the 'background' function itself in the 'run' function if neccesary. But normally it doesn't hurt either to have it run twice.
Since you cannot know upfront for which version the script was written, this is the best solution I found.

Then, we need the new 'one-time' script 'run' function to execute the 'telemetry' script 'run' function, and return the same result. So we simply execute the 'telemery' script 'run' function and pass the result as return from the 'one-time' script 'run' function.

In case there is no value returned from the 'telemetry' script 'run' function, we will return a 0 (zero), hence the 'or 0'. Otherwise, OpenTX will probably give an error, because it wants to have a result from the 'run' function in a 'One-time' script. (According to the documentation, a 'telemetry' script should also return something different from 0 at the 'run' function, but this is not enforced. A LOT, if not all, of the telemetry scripts I know don't return anything on the 'run' function, hence the error when trying to run it as a 'One-time' script.)

Finally, we need to return the already existing 'init' function, and the newly constructed 'run' function, to the OpenTX API.

So now you can run whatever 'telemetry' script as a 'One-time' script.

I have a file named 'ZGo.lua' with this code in my SDcard root directory. If I want to test a 'telemetry' script, i adapt this 'ZGo.lua' to load my 'telemetry' script to test. If it fails, the simulator will give an error, and I can immediatly adapt the script that is under test, and run it again by launching this 'One-time' script.

Since the name I gave starts with a 'Z', it will be the last entry on the SDcard that I browse from my simulator or transmitter. So start the file browser in the radio menu, press 'up' one time, and it is selected. Then just long press 'enter', select 'execute', and off you go!



P.S. if you wonder what the three dots are: they are all arguments following the 'event' argument, passed to the function. If you write a function to replace another, you must always make sure everything is passed through again , even if you don't expect it.
Last edited by l shems; Oct 31, 2017 at 04:49 AM.
Oct 29, 2017, 09:06 AM
Have Fun and Just Fly!
l shems's Avatar

Saving memory? Yes, neccesary !


The transmitters that run OpenTX are no laptops. They have limited resources available. That's probably one of the reasons for the LUA scripts being run in cycles, and not having a parellel operating system in place, doing two things 'at the same time'.

The simplest way to save memory, is to load the code you need only when you need it. The SDcard is available for that, and as of the first implementation of LUA on OpenTX, we can load code from the SDCard.
In the newest version of OpenTX, we even have support for on the fly compilation of our code, which makes it run faster and with less memory after the first run. We will not use this in our scripts however, since it will render them useless for older versions of OpenTX. There is a simple workaround for that that I will discuss later.

My main filosofy, which is unfortunately not shared by the OpenTX developers, is that every LUA script should be able to be run on any OpenTX version, on any OpenTX radio. Even the less restrictive filosofy of 'backward compatibility' is not supported by the OpenTX developers.
I will do anything possible however, to facilitate portability of scripts from version to version and from platform to platform (Taranis to Horus for example).

So back to saving memory: loading a function when you need it.

I personally use it a LOT. I created a library, where I keep all functions I need on a repetitive basis. In order to reference to it quickly and easily, it is called 'L' and placed inside the 'SCRIPTS' directory. Then, another subdivision is used for organising them.

So how does that work? Well, just make a script, and save it somewhere. It needs to contain some code, and perhaps some input and some output if you want to use it as a function.
In essence, all scripts are loaded as a function, but sometimes with no input nor output. In that case it either does something completely on it's own, or it set's some GLOBAL variables. Three examples:

1) Setting a global variable to 'l Shems': create a file '/SCRIPTS/L/Tutorial/myName.lua' containing:
Code:
NAME='l Shems'
in order to use it, create a file '/SCRIPTS/TELEM/Onetime.lua':
Code:
local chunck=loadfile('/SCRIPTS/L/Tutorial/myName.lua')
chunck()
chunck=nil
local function run()
  lcd.clear()
  lcd.drawText(10,10,NAME,0)
  return 0
end

return {run = run}
as you can see, we only needed the 'chunck' variable to temporarily store the chunck to be able to execute it as a function calling 'chunck()'. After that, we can clean it up again, as the global variable NAME has already been created.

2) Setting a global variable to some value, create a file '/SCRIPTS/L/Tutorial/anyName.lua' containing:
Code:
local name=...
NAME=name
in order to use it, create a file '/SCRIPTS/TELEM/Onetime2.lua':
Code:
local chunck=loadfile('/SCRIPTS/L/Tutorial/anyName.lua')
chunck('anyName')
chunck=nil
local function run()
  lcd.clear()
  lcd.drawText(10,10,NAME,0)
  return 0
end

return {run = run}
as you can see, we passed a string containg 'anyName' to the function stored in the chunck to get this string assigned to the global variable NAME. Afterwards we can clean it up again..

3) Adding a value to some value, create a file '/SCRIPTS/L/Tutorial/combinedName.lua' containing:
Code:
local otherName=...
local myName = "anyName"
return otherName .. " " .. myName
in order to use it, create a file '/SCRIPTS/TELEM/Onetime3.lua':
Code:
local combinedName=loadfile('/SCRIPTS/L/Tutorial/combinedName.lua')('l Shems')
local function run()
  lcd.clear()
  lcd.drawText(10,10,combinedName,0)
  return 0
end

return {run=run}
As you can see, we have skipped the use of the temporary variable 'chunck'. We just loaded the file, and passed the arguments directly, and stored the returned result in the local 'combinedName'. This saves us the trouble of declaring and cleaning up the chunck.
Last edited by l shems; Oct 31, 2017 at 01:32 PM.
Oct 29, 2017, 10:24 AM
Have Fun and Just Fly!
l shems's Avatar

Yet another way to save memory: staging


In the previous example we saw how to use loadscript for keeping our memory usage in the main script low.

Another technique that is quite powerfull is 'staging'. You have to define the workflow of your script first, and then ypou only load the code that is needed for each 'stage' in your workflow.

Suppose you want to draw a frame, and then some graphs within them. Both functions don't need to act at the same time, so we can apply staging here to save memory (and make the program more understandable as well ).

function 1 needed, save to '/SCRIPTS/L/Tutorial/drawFrame.lua':
Code:
local x,y,w,h=...

--Display starts left upper at x,y; ends at right lower at w-1,h-1

lcd.drawLine(x,y,x+w-1,y,SOLID,0)
lcd.drawLine(x+w-1,y,x+w-1,y+h-1,SOLID,0)
lcd.drawLine(x+w-1,y+h-1,x,y+h-1,SOLID,0)
lcd.drawLine(x,y+h-1,x,y,SOLID,0)

return "drawGraph"
function 2 needed, save to '/SCRIPTS/L/Tutorial/drawGraph.lua':
Code:
local x,y,w,h,a,b=...

local fx, fy

--Display starts left upper at x,y; ends at right lower at w-1,h-1

for fx=0,w - 1 do -- loop through all fx within the framewidth w
  fy=a*fx^2 + b -- calculate the fy
  fy=math.min(h-1,fy) --make sure fy is not exceeding the frame height h-1 
  fy=math.max(fy,0) --but not smaller then 0 either
  fy=h-1 - fy  --inverse the Y to make it mathematically correct.
  lcd.drawLine(x+fx,y+fy,x+fx,y+fy,SOLID,(GREY_DEFAULT or GREY or 0) + (FORCE or 0)) 
  --using drawLine because drawPixel doesn't support greyscale :)
end

return "finish"
main script, save to '/SCRIPTS/TELEM/staging.lua':
Code:
local a = 0.005
local b = 10
local x = 10
local y = 10
local w = LCD_W/2
local h = LCD_H/2

local stage = 'start'

local run = function(event)
  
  if stage == "start" then
    lcd.clear()
    stage = "drawFrame"
  elseif stage == "finish" then
    --doNothing but keep the script running to show the results
  else
    stage = loadfile("/SCRIPTS/L/Tutorial/" .. stage .. ".lua")(x,y,w,h,a,b)
  end
  
  if lcd.lock then 
    lcd.lock()
  end
  
  return 0
end

return {run=run}
So, we can now draw some nice graphs in two stages, where the run function contains no more code than that to pass through all the stages. We needed to use two special things to get it working properly:
  1. The 'lcd.lock()', that was made obsolete as of version 2.1 of OpenTX. Since we are not sure if the function still exists, we check for that. Any variable returns true if it is not false or nil, so this is fine as we did it. Without the lock, the lcd screen will be handed over to OpenTX every cycle, clearing the screen and rebuilding it with the OpenTX content.
  2. The 'do nothing' block. Since this is a 'One-time' script, if we do not keep it 'alive', the results will be gone and OpenTX takes over again before we were able to see the graphs.

This particular example has a lot of problems when trying to expand it's usage. The variables passed for example, can vary from stage to stage. In this case we have chosen them to be the same, and in the same order. But this will in general not be the case. We'll come later to a more convenient way of passing variables.
Name: screenshot_x9d+_17-10-29_16-31-15.png
Views: 12
Size: 773 Bytes
Description:
But, the idea works
Last edited by l shems; Oct 31, 2017 at 01:39 PM.
Oct 29, 2017, 11:15 AM
Have Fun and Just Fly!
l shems's Avatar

Passing variable: the easy way


When passing variables, you can do that explicitly, or implicitly. What do we mean by that?

Well, normal way of passing variables to a function is simply listing them, separated by comma's:
Code:
local calc = function (a,b,c,d)
  return (a or 2) + b*c*d
end
This is implicitly, because I need to know the order of the arguments. To call the function, I would use
Code:
local result = calc(nil,1,2,3)
you can also do it more explicitly, by using the array constructors:
Code:
local calc = function (parameters)
  return (parameters.a or 2) + parameters.b*parameters.c*parameters.d
end
This is explicitly, because I DO NOT need to know the ORDER of the arguments. To call the function, I would use
Code:
local result = calc({a=nil,c=2,b=1,d=3})
. The advantage is that I can pass exactly those parameters that I wish, in any order. I can even have these parameters contained within one variable:
Code:
local P = {a=nil,b=1,c=2,d=3}
 local result = calc(P)
.
One extra thing I get for free: I can add to P the result without returning it:
Code:
local calc = function (parameters)
  parameters.result =  (parameters.a or 2) + parameters.b*parameters.c*parameters.d
end
Since parameters is already local, so will all extra elements in it. Never to worry about local or global declarations. Be aware that an 'table' variable is only a reference to it's content. so if I pass 'P' to the function, within that function the 'parameters' variable is referring to exactly the same table! (In 'VBA' the 'pass by reference' principle.)

OK, but where is that useful for? Well, this is a very convenient way of keeping all variables belonging to the same OpenTX script together. Look at the following script, which is another version of the staging script (I saved it as "/SCRIPTS/TELEM/staging2.lua")
Code:
local script = {}

script.run = function(event)
  
  script.stage = script.stage or 'start'
  
  if script.stage == "start" then
    lcd.clear()
    script.stage = "drawFrame"
  elseif script.stage == "finish" then
    --doNothing but keep the script running to show the results
  else
    loadfile("/SCRIPTS/L/Tutorial/" .. script.stage .. "2.lua")(script)
  end
  
  if lcd.lock then 
    lcd.lock()
  end
  
  return 0
end

return script
and the two corresponding scripts (added version 2, so "/SCRIPTS/L/Tutorial/drawFrame2.lua")
Code:
local S=...

S.x =S.x or 10
S.y = S.y or 10
S.w = S.w or LCD_W/2
S.h = S.h or LCD_H/2

--Display starts left upper at S.x,S.y; ends at right lower at S.w-1,S.h-1

lcd.drawLine(S.x,S.y,S.x+S.w-1,S.y,SOLID,0)
lcd.drawLine(S.x+S.w-1,S.y,S.x+S.w-1,S.y+S.h-1,SOLID,0)
lcd.drawLine(S.x+S.w-1,S.y+S.h-1,S.x,S.y+S.h-1,SOLID,0)
lcd.drawLine(S.x,S.y+S.h-1,S.x,S.y,SOLID,0)

S.stage="drawGraph"
and (added version 2, so "/SCRIPTS/L/Tutorial/drawGraph2.lua")
Code:
local S=...

S.a = S.a or 0.005
S.b = S.b or 10

local fx, fy

--Display starts left upper at S.x,S.y; ends at right lower at S.w-1,S.h-1

for fx=0,S.w - 1 do -- loop through all fx within the framewidth S.w
  fy=S.a*fx^2 + S.b -- calculate the fy
  fy=math.min(S.h - 1,fy) --make sure fy is not exceeding the frame height S.h
  fy=math.max(fy,0) --but not smaller then 0 either
  fy=S.h-1 - fy  --inverse the fy to make it mathematically correct. Frame starts left uppper at S.x,S.y
  lcd.drawLine(S.x+fx,S.y+fy,S.x+fx,S.y+fy,SOLID,(GREY_DEFAULT or GREY or 0) + (FORCE or 0)) 
  --using drawLine because drawPixel doesn't support greyscale :), the 'or' is to fall back to Horus constants when run on Horus.
end

S.stage = "finish"
We have solved the problem of different parameters for different stages, kept all script variables neatly together, and never have to worry about an accidental global variable introduction (more on globals and locals declaration later).

At the same time, we created the entries for the position of the frame in the code for creating the frame, and the entries for the formula used in the graph in the code for the graph. After the creation, these entries are all available to the main script as well. Much tidier .



P.S. it is a very handy trick to use the construction of
Code:
S.a = S.a or 0.005
If 'S.a' has a value, it will keep the value; if not, it will get the specified value. In this way, you can choose where to set or initialise these variables, without checking the other parts of your code again.
Last edited by l shems; Oct 31, 2017 at 01:50 PM.
Oct 29, 2017, 12:47 PM
Have Fun and Just Fly!
l shems's Avatar

locals, globals and the _ENV


LUA simply declares all variables that are used without explicit local declaration as global variable. Functions are considered variables as well.

It is very tricky to use globals in 'telemetry' scripts or 'widgets', or actually all scripts other then 'One-time' scripts, since several scripts all share the same LUA environment.

In principle, you are never sure your script will work if you use global variables, in combination with scripts you didn't write. Because anyone could be using the same globals, and assigning values to it that interfere with your script.

As explained previously, nothing prevents you from creating a local array, and making each and every piece of code part of that array. The example of the 'script' variable in the previous post does just that. In LUA, there is however a more convenient way for doing that: using the _ENV variable.

The _ENV variable contains all global variables. Each function is run within the global environment, unless you change the environment explicitly. Let's see how that works, using the staging script example (version 3):
Code:
local script = {}

script.run = function(event)
  GLOBAL = "_ENV GLOBAL"
  script.stage = script.stage or 'start'
  
  if script.stage == "start" then
    lcd.clear()
    script.stage = "drawFrame"
  elseif script.stage == "finish" then
   lcd.drawText(10,10,GLOBAL,0)
    --doNothing but keep the script running to show the results
  else
    loadfile("/SCRIPTS/L/Tutorial/" .. script.stage .. "3.lua")(script)
  end
  
  if lcd.lock then 
    lcd.lock()
  end
  
  return 0
end

return script
and for the frame drawing "/SCRIPTS/L/Tutorial/drawFrame3.lua":
Code:
local S=...

S.a = S.a or 0.005
S.b = S.b or 10

local fx, fy

--Display starts left upper at S.x,S.y; ends at right lower at S.w-1,S.h-1

for fx=0,S.w - 1 do -- loop through all fx within the framewidth S.w
  fy=S.a*fx^2 + S.b -- calculate the fy
  fy=math.min(S.h - 1,fy) --make sure fy is not exceeding the frame height S.h
  fy=math.max(fy,0) --but not smaller then 0 either
  fy=S.h-1 - fy  --inverse the fy to make it mathematically correct. Frame starts left uppper at S.x,S.y
  lcd.drawLine(S.x+fx,S.y+fy,S.x+fx,S.y+fy,SOLID,(GREY_DEFAULT or GREY or 0) + (FORCE or 0)) 
  --using drawLine because drawPixel doesn't support greyscale :), the 'or' is to fall back to Horus constants when run on Horus.
end

S.stage = "finish"

lcd.drawText(10,20,GLOBAL or "_ENV DRAWFRAME",0)
[/CODE]

Don't forget to create also a version 3 for the 'drawGraph' function .

The result of this is of course that the global variable 'GLOBAL', containing "_ENV GLOBAL", is drawn in both functions, as they both use the global '_ENV'.
Name: screenshot_x9d+_17-10-29_18-15-15.png
Views: 11
Size: 950 Bytes
Description:

Now, we will set the '_ENV' variable of the first script to '{}', meaning 'empty', after first setting the 'GLOBAL' variable to "_ENV GLOBAL", and after that setting the 'GLOBAL' variable to "_ENV STAGING". "/SCRIPTS/TELE/staging3b.lua"
Code:
local script = {}

script.run = function(event)
  GLOBAL = "_ENV GLOBAL"
  _ENV={}
  GLOBAL = "_ENV STAGING"
  script.stage = script.stage or 'start'
  
  if script.stage == "start" then
    lcd.clear()
    script.stage = "drawFrame"
  elseif script.stage == "finish" then
   lcd.drawText(10,10,GLOBAL,0)
    --doNothing but keep the script running to show the results
  else
    loadfile("/SCRIPTS/L/Tutorial/" .. script.stage .. "3.lua")(script)
  end
  
  if lcd.lock then 
    lcd.lock()
  end
  
  return 0
end

return script
Name: screenshot_x9d+_17-10-29_18-21-26.png
Views: 8
Size: 853 Bytes
Description:
So what happened? By setting _ENV to {}, we have actually created a new environment for the staging function. The previously created global 'GLOBAL' is still there, but in the global environment 'above' the new one we created for the staging function. But the 'drawFrame' function is still using the 'old' global environment, so it sees the 'old' global 'GLOBAL'.

What does this imply? Well, that it is quite simple to run scripts from other sources in a safe way: just load them using a script that executes them after having set a "personal" global environment; So if you add this line as first line in the script, you're safe:
Code:
_ENV = {}
But, instead of changing scripts from others, which is always tedious if they provide an update, why not just load it like this:
Save as 'ZZGO.lua'
Code:
return loadfile("ZGo.lua",'bt',{})(...)
and have your 'ZGo.lua' point to the script under test.

LUA, that's fun. They have foreseen our problem, and it is solved so neatly by calling a script after loading it with it's personal (empty ) environment.

Last edited by l shems; Oct 31, 2017 at 02:01 PM.
Oct 29, 2017, 01:59 PM
Have Fun and Just Fly!
l shems's Avatar

Something more about environments


Just a short add-on here on environments.

You can access all global variables not only directly by their name, but also by their presence in the '_ENV' variable. So the following code snippets are exactly the same:
Code:
GLOBAL = "someglobalvariable"
and
Code:
_ENV.GLOBAL = "someglobalvariable"
That also means that you can 'create' predefined environments, using 'selected' parameters from another environment. This allows for 'controlled' sharing of globals between different 'telemetry' scripts for example.

The concept is that you can create global variables on YOUR transmitter, and have them selectively transferred to the script YOU want that for.

Suppose I want to share some info between two 'telemetry' scripts. First, make sure you have a global variable in both scripts that is used a a shared variable (array), say 'SHARED1' in 'telemetry' script 1 and 'SHARED2' in 'telemetry' script 2. Then, create a loader script for each 'telemetry' script, replacing the original script with this 'loader' function.
'telemetry' script 1
Code:
return loadfile("script1.lua,"bt",{SHARED1=GLOBALSHARED}(...)
'telemetry' script 2
Code:
return loadfile("script2.lua,"bt",{SHARED2=GLOBALSHARED}(...)
What will happen? The first script that is launched will try to assign 'GLOBALSHARED' from the global environment as reference for the 'SHAREDX' global to be used in the newly created environment within which the 'telemetry' script is loaded. The second script that is loaded will do the same, but then the global GLOBALSHARED is already declared globally by the first script being loaded, so it will point to the same global variable 'GLOBALSHARED'.

if all other widgets and scripts you use are loaded with an empty environment ({}), you are in full control.

What about other script consisting of multiple scripts loaded using 'staging' or a function library as described above? Well, if they are properly written, nothing happens. They will function as before. But if within these scripts some global variables are declared, the staging will stop to work. You can work around that by looking for all places where a script is loaded, and have the load script pass the '_ENV' variable
Code:
??? = loadscript("something","bt",_ENV)(???)
. In that way, the specific environment you loaded the script with, is propagated to the other scripts. Tedious, but the only solution.

So IF you want to share global variables between scripts, you can only limit the risk by passing only ONE variable, but it is never secure.

I would strongly suggest to write your own scripts without the use of global variables. It is not neccesary, and will in the end cause problems.
Last edited by l shems; Oct 29, 2017 at 02:15 PM.
Oct 29, 2017, 02:31 PM
Have Fun and Just Fly!
l shems's Avatar

Creating a telemetry script


Well, we have successfully created a one time script to draw a frame and a graph, using staging. It will run on both Taranis, Q7 and Horus, or perhaps I should say 'black & white', 'greyscale' and 'color' displays, because we carefully chose the size and draw parameters to be valid for all. We used the 'or' function to catch an empty constant, and replaced it with a valid one for the other type of display:
Code:
(GREY_DEFAULT or GREY or 0) + (FORCE or 0)
So far so good.

Now it is time to write some telemetry scripts, and then later some widgets .

Telemetry scripts is actually quite simple. Just create a local 'init', 'run', and 'background' function, and return them in the final line, and you're done. Let's add a nice 'background' function to the already created 'One-time' 'staging' script, add the new 'background' function to the return statement, and save it as a 'telemetry' script:
Save as "/SCRIPTS/TELEM/staging2b.lua":
Code:
local script = {}

script.run = function(event)
  
  script.allowLocks = script.allowLocks or true
  script.stage = script.stage or 'start'
  
  if script.stage == "start" then
    lcd.clear()
    script.stage = "drawFrame"
  elseif script.stage == "finish" then
    if event==EVT_ENTER_BREAK then
      script.stage = 'start'
      script.allowLocks = true
    elseif event==EVT_EXIT_BREAK then
      script.allowLocks = false
    end
  else
    loadfile("/SCRIPTS/L/Tutorial/" .. script.stage .. "2.lua")(script)
  end
  
  if lcd.lock and script.allowLocks then
    lcd.lock()
  end
  
  return 0
end

script.background = function()
  if script.stage == "finish" then
    script.b = (script.b + 1) % script.h
  end
end

return script
What will this background function do? Well, it will add 1 to the 'b' parameter of the graph after it has been finished, until the staging is reset to the 'start'
stage.
We added a smal 'wait' routine to restart the graph when in 'finish' state and 'enter' was pressed.

If the 'b' will hit the height 'h' in one of the next restarts, the modulo '%' operator, will reset it to 0, to start all over again. So if you load it as a telemetry script, and you enter the telemetry screen, it will show a graph with an y-axis offset between 0 and the frame height, depending on how long it took before you clicked the 'enter' button .

We didn't want the background to kick in before there where some graphs drawn, and the graph parameters where known. (actually, we got an error ourselves first ) So we added the 'finish' condition for the graph parameters to be changed.

Well, since I suggested to load it in the TELEM directory, we cannot load it as a 'telemetry' script yet. It is in the wrong place. Just create a new file "/SCRIPTS/telemetry/stage.lua", containing the following code to solve that:
Code:
return loadfile("/SCRIPTS/TELEM/staging2b.lua","bt",{})(...)
if you are still on OpenTX 2.0 (as I prefer ), you must create a file with the name "telem1.lua" (or telem2.lua if that already exists, until telem7.lua) and place it in the a directory with the same name as your model.

In this way, we build a library of 'telemetry' scripts, that we can use to create "safe" loader scripts in the load directory with a short enough name (6 characters is the limit for OpenTX 2.2!)
Last edited by l shems; Oct 31, 2017 at 02:16 PM.
Oct 30, 2017, 04:07 PM
Have Fun and Just Fly!
l shems's Avatar

lcd.lock and OpenTX 2.0


If you where on OpenTX 2.0 and you tested the previous script, you probably noticed it didn't work as you expected. When activating the telemetry screens by 'long press page', the graph showed up. But nothing happened on pressing enter?? Well, the background function isn't called by itself, remember?

Next thing is that we had to put some extra logic in for the 'lcd.lock'. This lcd.lock is a little bit tricky. We made a variable 'lockAllowed' that is unset if the 'exit' event is detected. If we wouldn't do it that way, we would never get control on the lcd and buttons again when running the 'telemetry' script. In some way or another however, instead of exiting the lcd screen directly, it first becomes empty. You can skip now to the other 'telemetry' pages with the 'page' button.

When pressing the 'page' button in this stage, the next telemetry screen will become active, triggering the 'background' function. when returning, and pressing 'enter' again, the effect will be seen of the 'background' function: the shifting of the graph to another position.

If you want it to run as it would on openTX 2.1, you need to enter a call to the 'background' function in the script.

I don't know how the overcome the nuisance with the 'lcd.lock' giving a blank screen in this script. If someone knows what could be improved, please post it here.

For now, we will just skip the 'lcd.lock' in future scripts, and assume all will be working fine when we need to introduce it again. Apart from that stupid blank screen, that is.
Last edited by l shems; Oct 31, 2017 at 05:49 AM.
Oct 30, 2017, 05:08 PM
Have Fun and Just Fly!
l shems's Avatar

Ok, and now for a widget


Well, some of you perhaps know I am not so fond of widgets. To complex, to tedious. But there are reasons to change my mind. Perhaps .

Widgets are not that different from 'telemetry' scripts. The have a 'background' function, and a 'run' function, that is now called 'refresh'. So far, nearly the same. Only difference for those two 'funtions', is that the widget 'options', 'zone', and any other parameters you created for the widget, are passed in one array to these functions. The 'event' parameter for the 'run' function in 'telemetry' and 'One-time' is missing, probably because you can activate more than one widget on the same screen, and those widgets might start fighting for the 'events', your transmitter not knowing which widget is entitled to 'use' them.

Then there are two other functions that we need to define: one to 'create' the widget, given the 'zone' it is located and the 'options' that have been saved. The other to 'update' the widget if the 'options' get changed.

Finally, we need to define the options that can be set in the screen setup for each active widget on that screen, and define a 'name' for the widget.

Let's have a look at the most simple widget, having all options, doing what our first 'One-time' script was doing, showing a counter moving over the screen:
Code:
local WIDGET={}

WIDGET.name = "MovingCntr"

WIDGET.options = 
  {{"height",VALUE,10,0,30}
}

WIDGET.create = function(zone, options)
  return {zone=zone, options=options, i=0}
end

WIDGET.update = function(widgetToUpdate, newOptions)
  widgetToUpdate.options = newOptions
end

WIDGET.background = function(widgetToProcessInBackground)
end

WIDGET.refresh = function(widgetToRefresh)
  lcd.clear()
  lcd.drawText
    (widgetToRefresh.zone.x + widgetToRefresh.i
    ,widgetToRefresh.zone.y + widgetToRefresh.options.height
    ,widgetToRefresh.i 
    ,MIDSIZE
  )
  widgetToRefresh.i=widgetToRefresh.i+1
  if widgetToRefresh.i>=widgetToRefresh.zone.w then
    widgetToRefresh.i=0
  end
end

return WIDGET
In order to make it perfectly clear that the functions are actually all using their local reference to the same widget that is passed as an argument, I have named it differently in each function. In further examples, we will use a shorter variable name, and the same one in all functions, because that is more convenient.

Secondly, I have not used 'upvalues' to declare the counter 'i'. I have actually made it part of the widget we created in the 'WIDGET.create' function. It is this function that needs to return the widget, containing the definitions and options and all other characteristics we want in the widget. In that way, it doesn't exist outside of the widget created, which makes it very clean. No upvalues here yet, no globals, all is contained within the definition of the widget.

Ok, you need to place the widget script, named as "main.lua", in a directory within the WIDGETS directory. So we will put this one in "/WIDGETS/MOVINGCOUNTER/main.lua".

Now you can power up (your simulated) Horus, and open the screen setup to assign this widget to some zone:
Name: screenshot_x12s_17-10-30_23-12-53.png
Views: 11
Size: 6.5 KB
Description:

MMM, al my other widgets gone, and I was sure I gave it only 1 of the four zones. Turns out to be the 'lcd.clear' function. Yet another one to take care of later. Without the 'lcd.clear' it all worked fine. Our first widget.


P.S. It seems that the widget lcd zone is cleaned after every cycle. MMM, sure hope that doesn't ruin our graphical posibilities when creating complex screens. We are limited to how many transactions we can do, remember.
Last edited by l shems; Oct 31, 2017 at 02:29 PM.
Oct 30, 2017, 05:25 PM
Have Fun and Just Fly!
l shems's Avatar

Nearly forgotten: the 'init' function.


Well, in a lot of scripts types we have this function called 'init'. It is called one time only on startup of the radio, and is supposed to do all kind of initialising stuff if you need that.

Until now, we didn't need it. It's use is rather limited in first view, since we can just as well do any actions outside the 'run' and 'background' functions in the main body of the script, and it get's executed as first thing when after it gets loaded and executed by OpenTX.

Also, since no parameters go in or out, it doesn't work like the 'create' function, or the 'update' function for widgets. You can only use upvalues to have it actually do something that lasts after it is finished.

Is there no use at all then? Yes, there is. If you want to restart a script from scratch, without reloading the running functions and loosing the values of the current upvalues, it is rather nice you can call the 'init' function from the 'run' or 'background' function.

Keep that in mind. It will come in handy.

Perhaps you feel already something coming, related to 'staging'
Last edited by l shems; Oct 31, 2017 at 05:03 AM.
Oct 30, 2017, 06:02 PM
Have Fun and Just Fly!
l shems's Avatar

Hey, why not load a "telemetry" script as widget :)


Yeah, why not? That would save a lot of hassle. Think of the advantages:
  1. only one script to create
  2. only one script to maintain
  3. portability over platforms
  4. portability over OpenTX versions
  5. ...

Let's try it:

Code:
local WIDGET = {}

local LUAscript,options = ...
LUAscript = LUAscript or "/SCRIPTS/movingCounter.lua" 

WIDGET.name = string.sub(LUAscript,0,10)

WIDGET.options = options or {}

WIDGET.create = function(zone, options)
  local TELEM = loadfile(LUAscript)()
  if TELEM.init then
    TELEM.init()
  end
  
  return 
    {zone=zone
    ,options=options
    ,TELEM=TELEM
  }
end

WIDGET.update=function(widget, newOptions)
  widget.options = newOptions
  if widget.TELEM.init then
    widget.TELEM.init()
  end
end

WIDGET.background=function(widget)
  if widget.TELEM.background then
    widget.TELEM.background()
  end
end


WIDGET.refresh = function(widget)
  if widget.TELEM.run then
    widget.TELEM.run()
  end
  if widget.TELEM.background then
    widget.TELEM.background()
  end
end

return WIDGET
Just save it as "/WIDGETS/MCtelem/main.lua", and try to register it as a widget.

Name: screenshot_x12s_17-10-31_20-39-16.png
Views: 10
Size: 1.8 KB
Description:
well, it is writing on the screen. But in the titlebar. Yeah, well, it is still thinking it HAS the entire screen at its disposal. It has covered my other widgets as well. That 'lcd.clear' again !! We will need to fix that some time. More on that later .

But the basics work Lets go on in the next posts.
opc orn:

P.S. did you notice the call to the 'widget.TELEM.background' in the 'refresh' function? It is needed because a 'telemetry' script expects that the background is being called always. A 'widget' doesn't call the background itself when the refresh is active, so we have to do it ourselves !
Last edited by l shems; Oct 31, 2017 at 02:40 PM.
Oct 31, 2017, 06:54 AM
Have Fun and Just Fly!
l shems's Avatar

Combining stuff :)


Well, what we tried to achieve was to load a 'telemetry' script as a 'widget'. But we need to figure out how to 'fool' the 'telemetry' script into thinking it has an LCD display the size of the widget zone.
And of course the 'telemetry' script has to be written in a smart way to take the LCD size into account by using the 'LCD_W' and 'LCD_H'. It's therefore that they are there .

Can't we use the environments to do just that? Just load the script with an environment that has some other values of 'LCD_W' and 'LCD_H' !

Let's try:
Code:
local WIDGET = {}

local LUAscript,options = ...
LUAscript = LUAscript or "/SCRIPTS/movingCounter.lua" 

WIDGET.name = string.sub(LUAscript,0,10)

WIDGET.options = options or {}

WIDGET.create = function(zone, options)
  local TELEM = loadfile(LUAscript,"bt",{LCD_W=zone.w,LCD_H=zone.h})()
  if TELEM.init then
    TELEM.init()
  end
  
  return 
    {zone=zone
    ,options=options
    ,TELEM=TELEM
  }
end

WIDGET.update=function(widget, newOptions)
  widget.options = newOptions
  if widget.TELEM.init then
    widget.TELEM.init()
  end
end

WIDGET.background=function(widget)
  if widget.TELEM.background then
    widget.TELEM.background()
  end
end


WIDGET.refresh = function(widget)
  if widget.TELEM.run then
    widget.TELEM.run()
  end
  if widget.TELEM.background then
    widget.TELEM.background()
  end
end

return WIDGET
So, save this stuff, and try to register it to the left upper zone in a 4 zone screen layout.

Nothing changed, still using the entire screen. Well, mmm, this didn't work. Why?

I can only conclude that the OpenTX LUA implementation is not respecting the general LUA rules about environments. If I pass an environment, it should be used by the receiving chunk. OpenTX doesn't.

So OpenTX doesn't allow us to override any of the globally available OpenTX variables, that are also of a type that standard LUA doesn't know. Try the following for instance (I saved it at "/SCRIPTS/TELEM/lcdInfo.lua"):
Code:
local script = {}

script.run = function ()
  lcd.clear()
  lcd.drawText(10,10,"data type LCD_W: " .. type(LCD_W),0)
  lcd.drawText(10,20,"data type lcd: " .. type(lcd),0)
  lcd.drawText(10,30,"data type lcd.drawText: " .. type(lcd.drawText),0)
  return 0
end

return script
Ok, some 'rotables' and 'lighthouse' functions pop up. So they are not even proper 'tables' or 'functions'. That might explain a lot.

Well, that was it. Stuck. we can't use this. It was such a nice idea .

But wait, can't I declare a local with the same name, replacing the OpenTX stuff?
Code:
local script = {}

local lcd = 
  {clear = lcd.clear
  , drawText = function(x,y,text,f) return lcd.drawText(x,y,"some new feature " .. text,f) end
}

script.run = function ()
  lcd.clear()
  lcd.drawText(10,20,"data type lcd: " .. type(lcd),0)
  return 0
end

return script
(I saved it at "/SCRIPTS/TELEM/lcdMagic.lua")

Name: screenshot_x12s_17-10-31_21-10-17.png
Views: 12
Size: 3.3 KB
Description:

What happens here??!! Well, we COULD define a local 'lcd', overriding the global 'lcd'.

Hey, we can use that, can't we?
Last edited by l shems; Oct 31, 2017 at 03:11 PM.


Quick Reply
Message:
Thread Tools

Similar Threads
Category Thread Thread Starter Forum Replies Last Post
FAQ Tiny QX/EX Series Tips & Tricks Thread (FAQ info on first page) SoloProFan Micro Multirotor Drones 1003 Aug 27, 2018 02:27 PM
Discussion Tips and tricks for getting an initial flyable trim rhodesengr Sailplane Talk 15 Apr 28, 2016 02:11 PM
Discussion L/D and Sink rate LUA script for OpenTx FabFlight Sailplane Talk 2 Mar 23, 2016 07:28 PM
Discussion Tips and tricks for building out an EPP foam basher BigBadBry Foamies (Kits) 3 Oct 15, 2015 03:49 PM