Creating a graphing script for OpenTX using LUA - RC Groups
Thread Tools
This thread is privately moderated by l shems, who may elect to delete unwanted replies.
Nov 05, 2017, 07:09 AM
Have Fun and Just Fly!
l shems's Avatar
Discussion

Creating a graphing script for OpenTX using LUA


We are going to create a graphing script from scratch, using the widget framework to support Horus as well . Main requirements:
  1. Any source, selectable as option
  2. Fully scalable to support different lcd and widget sizes
  3. First-In-First-Out fixed timeframe drawing buffer
  4. Adjustable timeframe width with 'plus' and 'minus'
  5. Autoscale on height
  6. Reset on 'exit' and restart on 'enter'

Name: screenshot_x12s_17-11-15_19-01-22.png
Views: 110
Size: 35.3 KB
Description:

Well, that sounds nice, no?

Let's start .....
Last edited by l shems; Nov 15, 2017 at 12:03 PM.
Sign up now
to remove ads between posts
Nov 12, 2017, 03:36 PM
Have Fun and Just Fly!
l shems's Avatar

First the 'One-time' script


Three things we need first:

Init, background and run.

"/SCRIPTS/TELEM/graph.lua":
Code:
--the default options:
local options = 
  {source = getFieldInfo("ele").id
  ,timeframe  = 6 --sec
}

local script  = {}                             --stores the script
local P       = {}                             --fi-fo buffer to store the graph points
local now     = getTime() / 100       
local last    = now

script.init=function()
  P.dp        = math.ceil(LCD_W / 128 )        --pixels per graphPoint 128 points (MAX!!) 
  P.y0        = (LCD_H-1)/2                    --the pixelWise zero line. 
  for t = 1, math.floor((LCD_W-1)/P.dp) do     --fill fi-fo
    P[t]=P.y0       
  end
  P.val       = getValue(options.source)/10.24
  P.scale     = (LCD_H-1)/200                  --the pixel scale factor: value -100->100 scales on entire screen 
  P.t         = #P            --t is time index. Set to last in fi-fo buffer for next cycle start at first
  P.t0        = math.floor( 1 * (#P-1))        --t0 zero offset of graph. Just fun to play with. 
  P.dt        = options.timeframe/#P           --time between taking measurements
end

script.background=function()
  now = getTime()/100
  if now >= last + P.dt   then
    P.val = getValue(options.source)/10.24
    P.t= P.t % #P + 1 
    P[P.t]=P.y0 - P.val*P.scale
    last = now
  end
end

script.run=function(event)
  lcd.clear()
  script.background()
  for i = 0, #P-1 - 1 do
    local j = (i+P.t-P.t0 + #P-1 )
    if i ~= P.t0 then
      lcd.drawLine(i*P.dp ,P[j % #P+1] ,(i+1)*P.dp ,P[(j+1) % #P+1] ,SOLID ,CURVE_COLOR)
    end
  end
  return 0
end

return script
Nov 12, 2017, 04:11 PM
Have Fun and Just Fly!
l shems's Avatar

Run it as a widget


Well, use the script from the other thread to load the 'One-time' as a widget.

"/WIDGETS/GRAPH/main.lua":
Code:
local LUAscript = "graph.lua" --name of the telemetry or one-time script
local LUApath = "/SCRIPTS/TELEM/"
return loadfile("/SCRIPTS/L/Tutorial/Script2Widget.lua","bt",_ENV)(LUAscript,LUApath)
Make sure you have installed the "/SCRIPTS/L/Tutorial/Script2widget.lua".

MMM. Error message: Error in widget graph.lua refresh() function: /SCRIPTS/TELEM/graph.lua:41: attempt to call field 'clear' (a nil value)
Widget disabled

Oh, well, several things. One by one.
First, we need to add the LCD function loader lines in the telemetry script.
Code:
--neccesary to support the widget localisation and autoscaling
local lcd=LCD or lcd
local LCD_W = lcd.W or LCD_W
local LCD_H = lcd.H or LCD_H
We need to add the lcd.clear to list of commands in the "/SCRIPTS/Tutorial/lcdZone.lua"
Code:
LCD.clear = lcd.clear
Yes; it works, bt the lcd.clear is troubling us:
Name: screenshot_x12s_17-11-12_22-58-45.png
Views: 18
Size: 1.9 KB
Description:

So let's remove the lcd.clear, and while we are at it, also remove the call to the background script. Then we have set it up as a proper OpenTX 2.1 plus 'telemetry' script. I know, there are no telemetry script on the Horus, but let's try to be prepared for when we want to. It is always easier to add a background call than to remove it (you need some nasty signalling logic then).

That looks better:
Name: screenshot_x12s_17-11-12_22-53-16.png
Views: 19
Size: 38.2 KB
Description:

Success, but we are a little bit cheating here. We changed the one-time script to a telemetry script, but we removed the lcd.clear command.

Why not put something like this in the widgetLCD script, and leave the lcd.clear command in the telemetry script:
Code:
LCD.clear = function() end
Same result, but now we have no nice white background.

So when we add a drawFilledRectangle to the telemetry script, we can draw it ourselves. Also, put a corrresponding command in the widgetLCD script:

"/SCRIPTS/L/Tutorial/widgetLCD.lua":
Code:
LCD.drawFilledRectangle = function(x,y,w,h,f)
  lcd.drawFilledRectangle(zone.x,zone.y,zone.w,zone.h,f)
end
and in our graph script:
Code:
  lcd.clear()
  lcd.drawFilledRectangle(0,0,LCD_W,LCD_H,MAINVIEW_PANES_COLOR )
  for i = 0, #P-1 - 1 do
Tha's it:
Name: screenshot_x12s_17-11-12_23-12-08.png
Views: 15
Size: 17.4 KB
Description:
Nov 12, 2017, 04:24 PM
Have Fun and Just Fly!
l shems's Avatar

Adding Info


We need some axis info, and labels. Just change the run function:

Code:
  lcd.drawLine(0          ,P.y0    ,LCD_W-1    ,P.y0      ,DOTTED ,CURVE_AXIS_COLOR)
  lcd.drawLine(P.t0*P.dp  ,0       ,P.t0*P.dp  ,LCD_H-1   ,DOTTED ,CURVE_AXIS_COLOR)
  lcd.drawLine(0          ,P[P.t]  ,LCD_W-1    ,P[P.t]    ,SOLID  ,LINE_COLOR)
Two dotted axis lines, and one value indicator. Make sure to draw those before the graph curve is drawn. They need to be below.
Name: screenshot_x12s_17-11-12_23-16-51.png
Views: 13
Size: 18.0 KB
Description:

And now the axis labels and the value number, and oh, don't forget the source on the left top corner:
Code:
  lcd.drawSource(1 ,1                                                ,options.source ,TEXT_COLOR + 0 + LEFT)
  lcd.drawNumber(LCD_W-1 , 0                                         ,100     ,TEXT_COLOR + axisFont + RIGHT + INVERS)
  lcd.drawNumber(LCD_W-1 ,LCD_H-1 - 1 - axisFontSize                 ,-100       ,TEXT_COLOR + axisFont + RIGHT + INVERS)
  lcd.drawNumber(2 , 2 + P[P.t] - P[P.t]/(LCD_H-1)*(valueFontSize+2) ,P.val          ,TEXT_COLOR + valueFont + LEFT + INVERS)
We declared 4 variables for the fonts and their sizes, and did some math to draw within the zone.

Code:
local valueFont      = MIDSIZE
local valueFontSize  = 24
local axisFont       = SMLSIZE
local axisFontSize   = 16
Name: screenshot_x12s_17-11-12_23-24-06.png
Views: 16
Size: 20.6 KB
Description:

That's nice. Almost done.
Nov 13, 2017, 01:12 PM
Have Fun and Just Fly!
l shems's Avatar

Lets check the requirements: autoscale??


Ok,

we have to put in autoscale. If you have looked at the script closely, you have seen that the fifo buffer is filled with points values already scaled to the lcd screen. This is to do as little as possible calculations in the 'run' function, since the 'drawline' function is eating cycles, and we don't want the CPU limit to haunt us.

I have chosen to do the scaling for the entire set of points in the 'background' function, as soon as the scale needs to change. Then the 'run' function has only to plot them, and remains the same as far as load concerned.

So first define the min and max. Since we don't know the range yet, we want to set it as small as possible. We cannot set the min and max equal, since we would create a division by zero problem later on on the code (scaling becomes infinite then)

Code:
  P.val       = getValue(options.source)/10.24
  P.valMax    = P.val*1.01                     --initial max value to use for graph scale
  P.valMin    = P.val*0.99                     --initial min value to use for graph scale
  P.scale     = (LCD_H-1)/1                    --the pixel scale factor: value 1 scales on entire screen
And th emain block of the 'run' function:
Code:
   P.val = getValue(options.source)/10.24
    P.valMin,P.valMax = math.min(P.valMin,P.val),math.max(P.valMax,P.val)
    P.scaleNew = (LCD_H-1)/(1+P.valMax-P.valMin)
    if P.scaleNew < P.scale then
      P.y0n = (P.valMax+P.valMin)*P.scaleNew/2 + LCD_H/2
      for i,point in ipairs(P) do
        P[i] = (point-P.y0)*P.scaleNew/P.scale + P.y0n
      end
      P.scale = P.scaleNew
      P.y0 = P.y0n
    end
    P.t= P.t % #P + 1 
    P[P.t]=P.y0 - P.val*P.scale
    last = now
What we do here is determine the new 'valMin' and 'valMax', and if the resulting 'scaleNew' is different from the old (it can only decrease ), we will scale back the original scaled value that was stored in the fifo 'P', and scale it with the 'newScale'.

Now we only need to make the two labels show the actual min and max values:
Code:
  lcd.drawNumber(LCD_W-1    ,0                                       ,P.valMax        ,TEXT_COLOR + axisFont + RIGHT + INVERS)
  lcd.drawNumber(LCD_W-1    ,LCD_H-1 - 1 - axisFontSize              ,P.valMin        ,TEXT_COLOR + axisFont + RIGHT + INVERS)
That's it. Scaling is in.
Name: screenshot_x12s_17-11-13_20-11-40.png
Views: 11
Size: 20.6 KB
Description:
Nov 13, 2017, 01:54 PM
Have Fun and Just Fly!
l shems's Avatar

Adding options to our own widgets :)


Ha, we saw that already in the other thread.

Let's add them to the "/WIDGETS/Graph2widget/main.lua" file:
Code:
local LUAscript = "graph.lua" --name of the telemetry or one-time script
local LUApath = "/SCRIPTS/TELEM/"

--the default options:
local options = {}

options[#options+1] = {"source",SOURCE,getFieldInfo("rud").id}
options[#options+1] = {"timeframe",VALUE,60}

return loadfile("/SCRIPTS/L/Tutorial/Script2Widget.lua","bt",_ENV)(LUAscript,LUApath,options)
We have used different values as in the 'Graph' script, so we can see if it works:
Name: screenshot_x12s_17-11-13_20-25-56.png
Views: 12
Size: 8.1 KB
Description:

Ah, ok, the parameters are added, but mmm, some stuff shifted. But this isn't reflected in the graph. That is still the same, showing elevator. Deleting and redefining will make them look better. But we forgot something. The graph script itself needs to know the new values:
Code:
--updating with passed options
local newOptions = ... or {}
for k,v in pairs(options) do
  options[k]  = newOptions[k] or v
end
This bit of code checks if there are options passed, and need to be added.

And the script2widget needs to pass them to the 'telemetry' script:
Code:
WIDGET.create = function(zone, options)
  local TELEM = loadfile
    (LUApath .. LUAscript
    ,"bt"
    ,loadfile("/SCRIPTS/L/Tutorial/widgetLCD.lua")(zone)
  )(options)
Ok, all fine now, but only after reboot do we get the new values. That is because we have now loaded the 'telemetry' script in the create function WITH the options, but we never update the loaded script with the NEW options.

Let's add it:
Code:
  widget.TELEM = loadfile
    (LUApath .. LUAscript
    ,"bt"
    ,loadfile("/SCRIPTS/L/Tutorial/widgetLCD.lua")(widget.zone)
  )(widget.options)
Ok, ready to test:
Name: screenshot_x12s_17-11-13_20-52-30.png
Views: 14
Size: 9.1 KB
Description:
Name: screenshot_x12s_17-11-13_20-53-10.png
Views: 12
Size: 20.3 KB
Description:

Hurray!!
Last edited by l shems; Nov 13, 2017 at 04:32 PM.
Nov 13, 2017, 04:28 PM
Have Fun and Just Fly!
l shems's Avatar

One requirement to fulfil: reset and restart


What's the difference between reset and restart?

Reset will set the scale back to 1, and clear the fifo buffer.
Restart will KEEP the scaling, but only clear the fifo.

So we have the events 'simulator' to do all this. We didn't use the 'PlusMinus' simulator for adjusting the timeframe, but created one for our own. These events can be used to assign to some other functionality later. Perhaps shift through the sources quickly, without going to the widget setup. Whatever.

Let's start and catch some events:
Code:
script.run=function(event)
  if event == EVT_ENTER_BREAK then
    --restart
  elseif event == EVT_EXIT_BREAK then
    --reset
  end
Now we only have to come up with the proper actions. For the reset, it is easy: just call the 'script.init' . That what it is there fore! For the restart, we need to empty the fifo, nothing more. Since it is a fifo, we don't need to bother to reset anything else. It will just resume where it was.
Code:
    for i in ipairs(P) do
      P[i] = P[P.t]
    end
We have set it to the last value for the entire fifo. This is not correct of course, but neither is putting a zero in. At least the graph starts flat now, so it looks as a new start.

Again, things don't go as planned. Crash over crash in the simulator.
After quite some searching: found it. We cannot draw the zero line if it is not within the LCD zone. So we need to check for it:
Code:
  if (0 <= P.y0) and (P.y0 <= LCD_H-1) then
    lcd.drawLine(0          ,P.y0    ,LCD_W-1    ,P.y0      ,DOTTED ,CURVE_AXIS_COLOR)
  end
remark that we have put both parts of code for the two events to be handled in the if and elseif, but we have put the normal graph drawing in the else. In this way, it is either the reseting or restarting of the script that is going to take CPU time, or the drawing of the graph. But never both, so hopefully no CPU limits reached here.
Code:
  if event == EVT_ENTER_BREAK then
    for i in ipairs(P) do
      P[i] = P[P.t]
    end
  elseif event == EVT_EXIT_BREAK then
    script.init()
  else
    for i = 0, #P-1 - 1 do
      local j = (i+P.t-P.t0 + #P-1 )
      if i ~= P.t0 then
        lcd.drawLine(i*P.dp ,P[j % #P+1] ,(i+1)*P.dp ,P[(j+1) % #P+1] ,SOLID ,CURVE_COLOR)
      end
    end
  end
Well, that's it. Done!
Name: screenshot_x12s_17-11-13_23-27-52.png
Views: 12
Size: 18.9 KB
Description:


Scripts on the first page.

Have fun and just fly!
Nov 13, 2017, 04:58 PM
Have Fun and Just Fly!
l shems's Avatar

Ah, sorry guys, forgot about the plus and minus


The timeframe is adjustable in the widget options, so you can use that.

You just can't adjust it using the switch assigned to the plus and minus events. Would be temporary anyway, so .....

Up to the next topic: Getting this exact script to work on the Taranis and Q7.



Quick Reply
Message:
Thread Tools

Similar Threads
Category Thread Thread Starter Forum Replies Last Post
Question OpenTx Horus LUA Scripts sten1860 Multirotor Drone Electronics 1 Nov 05, 2017 07:13 AM
Question OpenTx Horus LUA Scripts sten1860 Radios 0 Oct 31, 2017 04:11 AM
New Product Taranis Q X7 OpenTX Battery Capacity Remaining Lua Script RcRav Radios 0 Mar 31, 2017 03:17 PM
Discussion OpenTx Lua Script Lap Timer RcRav Radios 0 Jan 10, 2017 01:08 PM
Discussion L/D and Sink rate LUA script for OpenTx FabFlight Sailplane Talk 2 Mar 23, 2016 07:28 PM