Best practices and coaching

Help creating logic scripts for Air Manager Instruments

Moderators: russ, Ralph

Message
Author
JackZero
Posts: 47
Joined: Tue Dec 29, 2015 12:31 pm

Best practices and coaching

#1 Post by JackZero »

Hi fellow instrument developers,

I think it's time to finally start a thread about best practices in regards to instrument development. Most of the people working on instruments aren't professional software developers and while Air Manager makes it easy to start working on simple gauges (which is great), as soon as things get a bit more complex, there are quite a few pitfalls on the way. And I think with a bit of guidance from more seasoned developers, many of these could be avoided.

As a preliminary note: These practices are common coding practices, but they aren't meant set in stone... they represent my own experience as someone working with Air Manager for a few years and being a professional software developer for 20 years. There will always be cases where following the rules won't make much sense. And hopefully, other seasoned developers will join me and share their thoughts as well.

In addition, I'd like to offer coaching to anyone starting with the development of complex instruments or panels and would like to invite other seasoned developers to do the same. In many cases, having someone experienced to talk to about problems, concepts, ideas and who'll take a look at the code (especially at the early stages) can vastly speed up the learning curve. So if your interested, just send me a PM and we'll work something out. Often, if your new to programming, you sometimes need hours to solve a problem where others could have told you the solution within minutes. A forum alone isn't the perfect solution to address this, as response times will often be too long.

Documentation:
The right amount of documentation within code is something that can be wildly discussed. Commenting code is necessary so others (or you in a few month or years) still have the chance to understand not only what the code does, but also why. In my opinion, comments should focus on two parts. First of, there should be a preliminary comment about what the code does (in general) as well as limitations, known errors and the like. The second thing that should be documented is when code gets either too complex to be easily readable (e.g. on performance critical parts) or does things that aren't obvious at first glance. And in this case, you shouldn't only comment what the code does, but more importantly why.
For example, if you have a block of complicated code which could be easily simplified if it weren't for this one seldom case where the simple solution won't work that you only found after two hours of testing, this is the typical case where you should document not only the what, but the why. Because another developer (or you, two years from now) might stumble over the code again and will say: "Dang, why did I do this... this can be implemented much easier...".

Fast code vs. readable code
When coding, you will often have to find a balance between a (computational) fast solution and a solution that might be a bit slower but is much easier to read and maintain. Always ask yourself, how often the code is run: If it's run only once when the instrument starts or very seldom (e.g. if the user changes a display mode) take the slow but readable solution. If the code is run at every simulator frame (e.g. in a dataref callback for a float dataref which will permanently change by small amounts, like bus voltage or rpm), take the fast solution. And if it's somewhere in between (e.g. code that is run a few times per second), try to find a balanced solution.

Basic coding
Try to stick to simple naming conventions and don't make things to complicated. This will directly reduce the time you spent debugging and finding typos. And only use prefixes or suffixes when necessary (e.g. if you have both a string and a float variable for the same thing). It's not necessary to type "gbl_SelectedDisplayMode" a hundred times when a simple "mode" would do the trick as well. If necessary, you can add a comment to the declaration of global variables that explains what "mode" is meant in case it's not evident.

Also, use constants, where appropriate. Lua doesn't support constants and enums, but we can just use variables instead and give them upper case names (so they are distinguishable from regular variables). If you have a constant number that you need several times within the code, use a constant. If you have fixed numbers that represent a certain state (e.g. different modes), use constants.

In addition, use arrays instead of numbered variables. Arrays have the advantage, that you can easily loop over them which makes code that much more readable.

Just take a look at the following code snips, is this more readable:

Code: Select all

local img_RedBar1 = img_add("redbar.png", get_column_x(1), 232, nil, nil)
local img_RedBar2 = img_add("redbar.png", get_column_x(2), 232, nil, nil)
local img_RedBar3 = img_add("redbar.png", get_column_x(3), 232, nil, nil)
local img_RedBar4 = img_add("redbar.png", get_column_x(4), 232, nil, nil)
local img_RedBar5 = img_add("redbar.png", get_column_x(5), 232, nil, nil)
local img_RedBar6 = img_add("redbar.png", get_column_x(6), 232, nil, nil)

visible(img_RedBar1, false)
visible(img_RedBar2, false)
visible(img_RedBar3, false)
visible(img_RedBar4, false)
visible(img_RedBar5, false)
visible(img_RedBar6, false)
or

Code: Select all

local redBars = {}
for i = 1,6 do
    redBars[i] = img_add("redbar.png", get_column_x(i), 232, nil, nil)
    visible(redBars[i], false)
end

This

Code: Select all

local gbl_flt_TotalFuel = (flt_arr_SimFuel[1] + flt_arr_SimFuel[2]) * 0.3555843
local gbl_flt_FuelFlow  = flt_arr_SimFF[1] * 1280.10348
or

Code: Select all

local totalFuel = (simFuel[1] + simFuel[2]) * KGS_TO_LBS
local fuelFlow  = simFuelFlow[i] * KGS_S_TO_GAL_H
This

Code: Select all

if gbl_int_SelectedMode == 3 or (gbl_int_SelectedMode == 5 and gbl_bool_CylinderHasPeaked == false) then
or

Code: Select all

if mode == MODE_PERCENT or (mode == MODE_LEAN_FIND and not isPeaked) then
And just in case you didn't know, in an if-clause, you don't need to compare booleans with true or false (a == true/false) but instead just use them directly (a / not a). And another thing beginners often don't notice is "elseif". Often I see code like this:

Code: Select all

if time < 8 then
    -- a
end

if time >= 8 and time < 16 then
    -- b
end

if time >= 16 and time < 24 then
    -- c
end
Using

Code: Select all

if time < 8 then
    -- a
elseif time < 16 then
    -- b
elseif time < 24 then
    -- c
end
is not only shorter but also faster because if time < 8, the next to if statements don't need to be evaluated.


Redundancy
Try to reduce redundancy as much as possible. If you have two methods that do almost the same, it might be best, to merge them to one method which distinguishes both cases internally. Also, try not to having multiple variables for the same thing. For example, in the following code:

Code: Select all

local btn_pressed = false
local btn_count   = 0
local btn_ignore  = false

function button_pressed()
    btn_pressed = true
end

function button_released()
    btn_pressed = false
    if not btn_ignore release then
        -- do short press action
    end
end

-- called by timer
function update()
    -- ...
    if btn_pressed and not btn_ignore then
        btn_count = btn_count + 1 
    
        if btn_count > 10 then
            -- do long press action
            btn_ignore = true
        end
    end
    -- ...
end
the three variables can be replaced by a single one, we only need btn_count:

Code: Select all

local btn_count = 0 -- -1 pressed, but already handled, 0 depressed, 1+ pressed (duration + 1)

function button_pressed()
    btn_count = 1
end

function button_released()
    if btn_count > 0 then
        -- do short press action
    end
    btn_count = 0
end

-- called by timer
function update()
    -- ...
    if btn_count > 0 then
        btn_count = btn_count + 1 
        -- note that btn_count will be 1 higher than in the previous example, as it is incremented once on the button press:
        if btn_count > 11 then
            -- do long press action
            btn_count = -1
        end
    end
    -- ...
end
If you only have one variable, the code will be less error prone. For example, in the above code, we forgot to reset btn_ignore to false, either in btn_pressed or btn_released, so it will only work once.

On the other hand, don't overdue this. For example, rather then this

Code: Select all

local ticks = 0

function draw()
    if ticks < 8 then
        -- draw used fuel
    elseif ticks < 16 then
        -- draw remaining fuel
    else
        -- draw remaining time
    end
end

function update()
    if isAutoCycle then
        ticks = ticks + 1
        if ticks >= 24 then 
            ticks = 0
        end
    end
    ...
end
I suggest this

Code: Select all

local FPS         = 4
local PAGE_USED   = 1
local PAGE_REMAIN = 2
local PAGE_TIME   = 3

local ticks = 0
local page  = PAGE_USED

function draw()
    if page == PAGE_USED then
        -- draw used fuel
    elseif page == PAGE_REMAIN then
        -- draw remaining fuel
    elseif page == PAGE_TIME then
        -- draw remaining time
    else
        print("Cannot be, trying to draw invalid page!")
    end
end

function update()
    if isAutoCycle then
        ticks = ticks + 1
        if ticks == FPS * 2 then 
            ticks = 0
            page = fif(page == PAGE_TIME, PAGE_USED, page + 1)
        end
    end
    ...
end

While this code is a bit longer and technically, page and ticks are redundant, it will be much more readable and extendable, as it separates the timekeeping (tick) from the visualization (page). And if you ever decide to change the FPS, you just need to adjust one constant.

KISS - Keep it short and simple
Try keeping the code simple. That also means keeping functions short. If your function is longer than about 30 lines, move code into subfunctions. If a simple solution fits 99% of the time, make one function for these case and another one for that one special case where it doesn't fit. This is most often more simple than trying to implement something that fits all cases.
Having to review or debug a 500 line spaghetti function!

Init-Function
Put all the code that creates drawable objects, registers callbacks, etc. into a single init()-Function. That way (at least if you use a serious editor for coding), you can collapse this whole initialization logic out of view, once you begin working on the runtime logic. This is the one case, where you can consequently ignore the "rule of 30" (see above). While we're at it, use a serious code editor (Notepad++, VSCode, ...) with syntax highlighting, code collapsing and split views for coding and use the built-in editor only for debugging.

Prepare for expansion
Imagine you code something like an engine monitor, where you need dedicated EGT values for each cylinder, but the simulator you use only supplies one value per engine. So you come up with a smart solution to extrapolate values for other cylinders base on what you have. The important part is that you put that logic in some dedicated place, for example in the callback code that gets the simulator values and build the rest of the instrument as if you would have dedicated values per cylinder. That way, if you (or any other user) changes to a simulator that has these dedicated values, you just need to remove that single block of code. Or if you come up with an even better solution on how to extrapolate the values, it's easily changed.

Modell View Controller
This is a design pattern that is perfectly suited for more complex instruments, like an PFD or Engine Monitor. The idea of the MVC pattern is to split the code into three parts, the model (=data), the view (=visual representation) and control (=program workflow).
Lets stick to the example of an fuel computer. You'll need to subscribe to a lot of datarefs (voltage, fuel amount, fuel flow) and depending on the state the instrument is in (turned off, self test during power up, normal display or even programming modes) draw different things to the display.
Now we could try to put this all into a huge method (or group of methods) that is registered as a simulator callback, but this will have a huge performance impact. As float values like voltages or fuel flow will constantly changed, our callback would be called for every frame of the simulator.

Instead, we use the MVC pattern: The callbacks from the simulator will store the data in global variables (Model), e.g. update the used fuel. Functions triggered by buttons or timers (e.g. for periodically changing views) will just implement the instruments logic and store the state of the instrument in the same and more global variables (Control), e.g. set the instrument mode (OFF, Normal, Programming) or clear the used fuel. And another function called by a timer at an interval matching the appropriate FPS for the instrument type (e.g. 25 for an PFD or 5 for a fuel computer) will update the displays (View), e.g. write the used fuel to the seven segment display if we're in used fuel mode.

This will keep the code tidy and untangled (no more txt_set in the middle of a calculation block) and reduce load as the expensive draw calls are reduced to an acceptable rate.


Best regards
Florian

User avatar
Ralph
Posts: 7878
Joined: Tue Oct 27, 2015 7:02 pm
Location: De Steeg
Contact:

Re: Best practices and coaching

#2 Post by Ralph »

An interesting feature might worth mentioning. You can add an image and set its default visibility, that saves you from doing visible() after adding them.
So for exampe:

Code: Select all

img_my_cat = img_add("my_adorable_cat.png", 100, 100, nil, nil, "visible:false")

User avatar
Keith Baxter
Posts: 4674
Joined: Wed Dec 20, 2017 11:00 am
Location: Botswana

Re: Best practices and coaching

#3 Post by Keith Baxter »

Hi ,

Awesome thread Florian.

@JackZero I will shortly be on the PM to discuss a few things.

Keith
AMD RYZEN 9 5950X CPU, Corsair H80I cooler, ASUS TUF GAMING B550-PLUS AMD Ryzen Mother Board,  32Gb ram Corsair Vengeance 3000Mh, MSI GTX960 4G graphics card 

User avatar
Keith Baxter
Posts: 4674
Joined: Wed Dec 20, 2017 11:00 am
Location: Botswana

Re: Best practices and coaching

#4 Post by Keith Baxter »

Ralph wrote: Thu Mar 04, 2021 3:58 pm An interesting feature might worth mentioning. You can add an image and set its default visibility, that saves you from doing visible() after adding them.
So for exampe:

Code: Select all

img_my_cat = img_add("my_adorable_cat.png", 100, 100, nil, nil, "visible:false")
Ralph,

Is that specific to img_add() ?

Or Is there a bug?? Because this below don't work.
It would be nice if things were consistent. If "visible:false" is not, then better not confuse us, I am sure you will get many "Q" and bug reports.

OK so now where is the list of all these features. Somewhere in the API? You got a job to do now that you opened your mouth. We are fast here . :mrgreen: :mrgreen:

Code: Select all

gdu_bg_style2                       =canvas_add(0,0,1412,917, "visible:false")

services_sd_card_group            =group_add(services_panel,sv_sd_group, "visible:false")

Keith
AMD RYZEN 9 5950X CPU, Corsair H80I cooler, ASUS TUF GAMING B550-PLUS AMD Ryzen Mother Board,  32Gb ram Corsair Vengeance 3000Mh, MSI GTX960 4G graphics card 

JackZero
Posts: 47
Joined: Tue Dec 29, 2015 12:31 pm

Re: Best practices and coaching

#5 Post by JackZero »

Keith Baxter wrote: Thu Mar 04, 2021 4:56 pm Hi ,

Awesome thread Florian.

@JackZero I will shortly be on the PM to discuss a few things.

Keith
Sure, I'm looking forward to it.

Florian

User avatar
Ralph
Posts: 7878
Joined: Tue Oct 27, 2015 7:02 pm
Location: De Steeg
Contact:

Re: Best practices and coaching

#6 Post by Ralph »

@Keith Baxter it only works with images.

User avatar
Keith Baxter
Posts: 4674
Joined: Wed Dec 20, 2017 11:00 am
Location: Botswana

Re: Best practices and coaching

#7 Post by Keith Baxter »

Hi

OK then I will not use it much hay. Most of my stuff is canvas. ;) ;)

Keith
AMD RYZEN 9 5950X CPU, Corsair H80I cooler, ASUS TUF GAMING B550-PLUS AMD Ryzen Mother Board,  32Gb ram Corsair Vengeance 3000Mh, MSI GTX960 4G graphics card 

User avatar
Keith Baxter
Posts: 4674
Joined: Wed Dec 20, 2017 11:00 am
Location: Botswana

Re: Best practices and coaching

#8 Post by Keith Baxter »

Hi

Something that might interest some.
Using tables instead of if...else statements. This example utilizes a table to send a command.
NOTE: The table can consist of complete commands or portions of a command that can be concatenated. Also note the use of visible to show hide the display and rotate to maneuver a signal canvas or image.

Code: Select all

---Temporary dial
temp_dial=canvas_add(590,40,80,80, function()
   _circle(40,40,40)
   _fill("#000000")
   _move_to(40,40)
   _line_to(80,40)
   _stroke("#ffffff",4)
end)
rotate(temp_dial,125)



dial_table={"off","standby","test","on","alt"}
dial_pos=1
function dial_callback(dir)
   dial_pos=var_cap(dial_pos+dir,1,5) 
   xpl_command("sim/transponder/transponder_"..dial_table[dial_pos]) 
   rotate(temp_dial,70+(dial_pos*55))
   visible(text_canvas,dial_pos ~= 1)   
end
dial_add(nil,590,40,80,80,dial_callback)

Yes @Sling . You a good teach. :mrgreen:

Keith

EDIT: I need to add that this technique is useful when using a dynamic table and a "hw_button_array_add" .

It could also be a .json file...

I will do an example of a .json using a "hw_button_array_add" on a gcu478 keyboard. I expect the code to be < 12 lines and the .json a few more than the number of buttons.

Not sure what is better practice and i am sure there are pro's and con's. Tables or small .json files. I suspect the difference might not be worth worrying about due to the scale of the data.
AMD RYZEN 9 5950X CPU, Corsair H80I cooler, ASUS TUF GAMING B550-PLUS AMD Ryzen Mother Board,  32Gb ram Corsair Vengeance 3000Mh, MSI GTX960 4G graphics card 

toneill
Posts: 58
Joined: Tue Dec 03, 2019 5:46 pm

Re: Best practices and coaching

#9 Post by toneill »

Hello all.

This is an excellent idea! Back in the day I used to develop software and got pretty good at efficient code. Unfortunately, I have forgotten so much more than I currently know! I find writing software very satisfying. Now that my eyesight has been restored (cataract surgery successful - yeah!) I am definitely getting more involved with air manager software and hardware development for Hot Start's TBM900, as this combines my love of flight sim and computer programming.

I used to program in Visual Basic (Net) and C++ and some other languages as the need arose. LUA is new to me, but many of the concepts are familiar. Personally, I learn best from a book. I have several soft cover books on best programming practices, tips etc. that I use constantly for reference.

My question is 'what books(s) would you recommend for acquiring a deeper understanding of LUA - one for reference and one for programming practices.

For example, I am currently using brute force if-then , elseif -then lines of code to do some simple things, where as in Visual Basic there is the Select Case method which greatly simplifies code. Alternatively I could write my own helper functions to make coding easy. Stuff I use over and over again. So please suggest some good written references.

Thanks so much
Ted

User avatar
Ralph
Posts: 7878
Joined: Tue Oct 27, 2015 7:02 pm
Location: De Steeg
Contact:

Re: Best practices and coaching

#10 Post by Ralph »

The Lua organisation has their own book:
https://www.lua.org/pil/
It's pretty good. You can do a lot of complex things with Lua. I'm probably applying just 1% of its capabilities :)

Post Reply