Lua for Jeti – Considerations and Best Practices

“Don’t do like I do, do like I say” did some wise guy from past say. It applies to many things and this is one.

I’ve been writing Lua-apps from “hello world” to somewhat more complex operations ever since I got the DC-24 home just over a month ago. And while it have been a great fun to do so I have to be honest there have been moments when “this crap just does not work!”.

So I thought to share some simple information and advice to make stepping in to Jeti-oriented Lua a bit easier if possible.

New transmitter, what’s next?

Jetimodel have done a great job with both the API and the documentation of it. It’s available here, always up-to-date Jetimodels sample app’s are here, go and grab them.

Basic Stuff

We have to understand a few basic rules before even planning on what to do with Lua:

  • We are not allowed to use any functions from transmitters operating system (Example: os.time() does not work, we are not getting the time that way. (system.getTime() works)
  • It is strongly advised not to operate any flight-critical functions with Lua. After all the thing that messes up Lua apps is the user with faulty code.
  • Speed of Lua in transmitter is relatively good so it’s very fast, more on that later.
  • Make comments in the code – Easier to read your own stuff too later on.

And for once in your lifetime: Stop being such a man, read the manual. Trust me, you will search through it many times when thinking “how would I do that…”

Construction of a Lua app for Jeti

Lua application is in my mind a module-built app with different modules, some are mandatory, some are not.

For a Lua app to work there are a few things that need to happen. Let’s try to go through them a bit.

The application is initialized when

  • Model is selected (changing model from list)
  • Application is added in “Applications” -> “User Applications”
  • Transmitter is started to a model, either straight away or via model select

In all of the above cases the application is running (if it is added to that specific model). What happens in that moment is the part in Lua app that is called init. Basically this is mandatory, something has to be done when app is started to run.

Here’s an example of  it:

local function init()
 system.setControl(8, 0, 0, 0)

The example makes a few things, on first line we register a Form (the “window” for settings, values etc) with name MyFirstApp. Second line registers a new telemetry control to transmitter. That is a switch you can find in switch selection, under the F5 marked “Apps”. We can see that our App-switch is called TelemetryControl and the short-name in menus etc is TC1. In third line we are setting the telemetry control to value zero.

Jeti have gone to great lengths in telling us what each value does, for example the system.setControl(8,0,0,0) means we are setting telemetry control number 8 to OFF (first 0), with no delay (second 0) and we have smooth-type of zero (third 0). So, once again, you really need to be humble and read the manual….

Another mandatory line found in apps is the ID-line. As an example:

return {init=init, loop=loop, author="Tero", version="1.0", name="MyFirstApp"}

This does a few things, and also tells us something. From left to right:

  • Tells initialization to run function named init. That means “local function init()”
  • We have a loop we want to be run over and over when app is running. We also tell that the loop is named as loop so that means “local function loop()”
  • Name of author is self-explanatory, so is version.
  • Application name here is the name that appears in the User Applications list on your transmitter. It is not the same as the name for form in init, that name is the name visible for your app in menus.

Alright. Moving on. Complete example.

Now, I assume you have downloaded the API-docs and sample apps already. If not, do so, we are talking about the “05_avgtm.lua” application. This is a very useful app to use in your own debugging later on.

We learn a few things from this small app, first we get some understanding how the app works when looking at the code and also we get an understanding on what to expect from Lua when looking at performance.

Let’s have a look, nicely commented also (Made small changes due clarity):

-- Locals for the AvgTime App
local lastTime
local avgTime
-- Calculates the average period
local function loop()
 local newTime = system.getTimeCounter()
 local delta = newTime - lastTime
 lastTime = newTime
 if (avgTime == 0) then 
 avgTime = delta
 avgTime = avgTime * 0.95 + delta * 0.05
-- Displays the screen
local function printForm() 
 lcd.drawText(10,30,string.format("Avg. time: %.2fms",avgTime),FONT_MAXI) 
local function init() 
 system.registerForm(1,MENU_MAIN,"Test 5 - Avg. time",nil, nil,printForm) 
 lastTime = system.getTimeCounter()
 avgTime = 0
return {init=init, loop=loop, author="JETI model", version="1.0"}

Starting from top we are defining some local variables we are using in application. A variable has always have to be defined before we can call it. For example if we do not have a line “local variable = 0” before we want to check if variable is zero like “if(variable == 0)” the application will halt to an error since it cannot find the variable. So, what we can learn from this? Yes, order of things do serve a purpose.

The same thing does apply to whole functions. We cannot call for init() unless it’s defined first.


Then there is the loop. Well, to be strict it’s a local function loop, not a system-wide function. In my mind the loop is the workhorse, in loop we are defining what we want to have done over and over again when application is running.

In our example application is getting new time-information on start of every loop, this is then stored to memory as “newTime”. Next what happens is that we are calculating the difference between newTime and lastTime and storing that result to “delta”. Next line is that we are telling the lastTime to be the most recent time we have, that is the newTime. This is done so when we come to line 2 inside the loop again we have the right lastTime. Are you still with me? Good.

There seems to be one comparison. what happens next is “if average time is zero then make the average time to me same as delta, else make average time to be average time times 0.95 added with delta times 0.95 and then end.

That’s the end of the loop, after that it starts all over again. What that loop does is that it counts the time it takes for every loop. The 0.95’s are for a small correction in time.

What do we see, the form?

Now that we have done everything in the background we actually have something to show on the screen. So, let’s make the screen, or form as it is called. That is the local function printForm() -part.

There’s only one line there, we are telling the app to write a text-line in position 10 pixels to right from left upper corner and 30 pixels down. The text should display a text string starting with Avg. time: and have the average time presented with two decimals and last with ms written behind it. The really last part tells that we want the font to be big.

Here’s the result of that app:


When we sit down and read the code with a little thought it is opening up. That is one of the beauties of script-languages like Lua.

Performance considerations

When looking at the picture above and reading the code we know a few things:

  • One loop takes ~22 milliseconds
  • That means the delay in anything we control inside the app is at least that time

Why does this matter? It does and it doesn’t. It doesn’t in the way that 20-22ms is the shortest loop-time we can achieve with this configuration and firmware. That is the “full throttle”.

This is good to keep in mind since trust me, you CAN f**k the code up quite easily. One thing I do before I release an app is that I copy that average time-app to my new app. Locals in the beginning to locals in my apps beginning, loop-stuff inside my loop etc. Only difference is that I’m not printing the value to screen, so I’m not using this part:

local function printForm() 
 lcd.drawText(10,30,string.format("Avg. time: %.2fms",avgTime),FONT_MAXI) 

Instead I’m adding a line in the bottom of my loop() like this:


This makes the same average time to be printed to console instead. (“Applications” -> “User Applications” -> F1), here’s an example:


If it happens that the looptime is too big there is something not quite right in the code. For example, the less string formatting you do in the loop the faster your code is. My first attempt to percentage to telemetry screen was horrible, loop-times were up to half a second. So yes, it does matter, and yes it’s quite easy to test.

To local or not to local?

There’s a lot of “local” here. I can define a variable named “speed” in two ways in an application “App One”:

speed= 100


local speed= 100

Let’s say we use the first option. If I have a second application “App Two” and I’m asking for variable speed I will get the value from “App One” instead. The variable “speed” is global, it’s not app-specific. And yes, this will create issues if not watched upon.  Then if you have used the latter way you would get a “nil” since the variable “speed” is not known to “App Two” due it’s being “local for only the App One”.

Unfortunately there are places where you have to use global variables. And what this means is that you look out.

In my opinion best ways to minimize possibilities for issues is to use variable-names that are a bit more complex like “spdMdl” instead of “speed”. I like the idea of making variables “of your own” in those places where they are needed to be global.


I work a lot with customers, I talk three languages in my work and use at least two of them almost daily. I believe in good service and with LUA Apps for Jeti this is easy to achieve, use a translation-file. If you will not provide any translations then use the translation-file file with only one language.

If you have no meaning whatsoever to share your file then who cares. But hey, what’s fun in open source language and apps if they’re not shared?

What to write with? Does it matter?

Writing can be done in plain text-editors like Notepad, Notepad++ and so on in Windows. Do NOT use Word or Wordpad etc formatting editors. In other OS’s you could use nano, pico, vi, emacs or whatever you like and have. One important note is that both LUA Apps (.lua) and language-files (.jsn) should be in UTF-8 encoding.

My personal favorite have been Notepad++ for years, using it daily at work too.

How to test apps?

One important advice: Make a test-model in your transmitter to test your apps with. Just in case.


We men can have a playground too :)

Jeti has an software release planned up sometime in near future. One great thing about is that it has DC/DS-24 emulator, perfect to test Lua with, in fact all screenshots on this page is from that app and not my DC-24. I’ve been using it for some weeks now and I would not give it away anymore. So watch Jeti webpage for any release. Unfortunately I have no release date or are allowed to give any other information about it, it’s still in development.

Optimizing Lua

Well. Two-sided sword actually. Most often it is not needed. But some things do apply, like the one with writing code so that string.format would be used so little as possible in the loop. On the other hand sometimes you have to.

I found this on the interwebs, and it was easy to read and gives a few good hints, if you have made an app and your loop-time is poor you might get some help reading it. A really good write-up from Roberto Ierusalimschy, get it here.

Since memory is short of in 14/16 there’s a few things we need to know:

  • Use globals only where you need to – Globals use more memory
  • Use compiled versions of your apps – Use the dumper found in Jetimodels Lua apps
  • Use collectgarbage()

All these help remarkably to get your code to run within tight limits of 14/16-models 50KB hard-limit. Use them!


What is it? Simply put, it collects garbage! It’s a built-in function in Lua, it catches unused memory and free’s it for the system to use. Especially useful when you for some reason have a hard time using locals and have to (or want to) use globals.

I have found that there’s a few good places where to put a line with collectgarbage(), one is at the start of file, one is as the last line inside init() -function, and one is last line before the “return {init=init…” line. That means second to last line should be collctgarbage()

if you are having trouble getting an app running within tight memorylimits then collectgarbage() might help.

Lua safety

Trust me, you will make your transmitter very slow when writing a app from scratch and “just trying something”. The good thing is that Jeti did think of that. If you write so bad code that it is eating up all the available resources the app will be killed by transmitter. Yes, shot dead. This is a really good to know, after this happened to me I purposely made an app that would eat up the resources. I then put a receiver with some servos up, started the app and moved the sticks around. No interrupts, no disturbances. And the transmitter didn’t even letting me into menus without a second or so delay, it was so slow. But the most important thing, control of the RC and transmission was uninterrupted the whole time.

Thanks Jeti. For thinking us wannabe-coders too :)

Obligatory stuff

This is a car-mechanic’s ideas of coding. The hard-core coders and software dev’s will most likely laugh on this on their coffee-break. But one fact remains, major part of you are like me, not a coder. So some basic info is good and sometimes we do not hit it spot on. But everything is forward!

Also the thing that “I’m not responsible of yadda yaada yaa…” You know it, no need to repeat :)

Enjoy the new possibilities!

One thought on “Lua for Jeti – Considerations and Best Practices

  1. Tero, I cannot think of a single place where a variable would NOT be declared local. Where have you had a use-case for a non-local variable?

    Keep in mind that "local" can be scoped to:
    The file containing the declaration
    A function containing a declaration
    A loop containing a declaration

    For a Jeti LUA app, it's all in one file, so a file scoped local is global to the app, but not global to other apps! This is the right way to handle variables!

Leave a Reply

Your email address will not be published. Required fields are marked *