Skip to content

2.2 Level II : read & show telemetry

Udo edited this page Nov 21, 2024 · 17 revisions

1. The Task:

Now, I hope it’s getting really interesting for you.
Building on the very first widget, I want to expand its functionality to display two sensor values instead of static text.
The layout should no longer be "hardcoded" for specific displays.

The final result should look like this:
image



This introduces a new level of difficulty with topics such as:

  • How to read telemetry (or, more generally, "values from a source")
  • How to handle different displays/transmitter types
  • How to refresh the screen

Each of these points may initially seem trivial, but they are essential for all widgets.
In the past, there were many examples of how easily one can fall into pitfalls with these topics, leading to strange widget behavior or significantly reduced transmitter performance.

I will first address each of these points separately before diving into the "complete widget."



2. One of the Most Important Lua Topics: Sensor & Source Values

Displaying telemetry values individually is certainly one of the main use cases for Lua widgets.
Here’s a few pages of explanation on the topic.

In general, Ethos Lua doesn’t distinguish whether you want to "query" telemetry sensors, physical switches, analog inputs, trims, logical switches, or anything else.
Lua treats these in an "object-oriented" way as a "source" (the object) with properties (attributes).

Key Points:

  • Properties can be names, but also values.
  • When querying a telemetry value, two steps are necessary:
    1. Step 1: Define the source ONCE in the entire script (by assigning it to a variable).
    2. Step 2: Query the source for its value whenever needed.



Defining a Source or Sensor:

Important: Each source should be defined only once throughout the entire runtime!

But what is the correct method?
We remember that possible sources include analog inputs, switches, sensors, etc.

In the guide, you’ll find the corresponding entry under system.

image

Don’t be confused by the many examples.
The first example is actually quite poor and should never be used that way, as a logical switch of the same name could exist, leading to ambiguity about which source is returned.



Proper Method:

Typically, you specify the name-category or "member" pair.
The member indicates where in the category list the source lies.
In the base section, you can get a good overview of the available categories.

  • Our desired value, RSSI, is generated by the receiver as a telemetry sensor, and thus belongs to the CATEGORY_TELEMETRY_SENSOR category.
  • The transmitter voltage is "internal" and comes from CATEGORY_SYSTEM.

Thus, I define the following two sources:

srcValue1 = system.getSource({category=CATEGORY_TELEMETRY_SENSOR, name="RSSI"})
srcValue2 = system.getSource({category=CATEGORY_SYSTEM, member=SYSTEM_MAIN_VOLTAGE})

(Example code to follow later in the widget)

and now, which handler?

Where do I place the definition within the widget?
Remember: Sources should only be defined once.

If it’s clear at program startup which sources will be used, it’s best to use the create handler for this, as it is only called once when the widget starts. If you dynamically determine which sources to query during runtime, you can check in the wakeup handler whether the source variable is already defined (not nil) and assign the source only once.



By the way, nil is Lua terminology meaning "nothing" or "empty."
A simple declaration like local test = nil is possible. The variable will be assigned a value later but already "exists" in the local address space/namespace.



The Typical "Source Error"

One of the most common mistakes is defining the source in the wakeup handler on every run (or at least multiple times) and then querying it.

	local srcRSSI =system.getSource(„RSSI“)
	local rssiValue = srcRSSI:value()

Or even more elegantly, all in one line:

local rssiValue = system.getSource(„RSSI“):value()

One reason you might see something like this is that many oTx users have switched to Ethos and may think they can replicate the query style they’re used to.
In openTx, it was possible to query a simple telemetry value in one line, for example, using getValue(source).
This might quickly lead someone to pack source definition and value querying into one line.



Why is this wrong? Why should the source only be defined once in Ethos Lua?

Ethos is not openTx and indeed works differently in some areas.
Here, the object-oriented approach in Ethos becomes clear:

  • You define the object once (in this case, a sensor).
  • Then you query the desired attribute, such as the value, using value().

An attribute can also be the name, and you’d get the name back with source:name(), and so on.
Often, an argument (value) can be included within the parentheses, which "writes" the corresponding value to the attribute.
This is implemented uniformly and cross-functionally in Lua.

If you assign a source to a variable multiple times (worst-case in every wakeup call), Lua has to locate this source in its "index," which costs a fair amount of CPU time.
Furthermore, it can happen that the "old" memory area of the "previous" source isn’t immediately freed up but only after several dozen loops.
So with each wakeup loop, the available memory becomes less until finally the "cleanup crew," or the so-called "garbage collector," clears the memory.




3. Working with Different Displays & Resolutions

In the first small widget, we hardcoded variables for layout parameters, such as x/y coordinates for placement, and had to activate certain ones depending on the display.
How can we support different display resolutions or even different widget sizes without changing the code?
Several tools are available for this.


1. Querying Widget Size

A relatively simple way is to query the widget size (x & y in pixels) and set positions in relation to it, for example, at 0.2 times the widget width.

The method for this is lcd.getWindowSize(), which returns the widget’s width and height.
The size can already be queried in the create handler.

Code Example

    local w, h = lcd.getWindowSize()

w = width h = height


A line like this: ```lua Lcd.drawText( w*0.1, h*0.2 , „TestText“) ```

would always set the text in relation to the widget size.



2. Querying Transmitter Type (Display Type)

Some widgets are intended to be displayed in Fullscreen mode, taking up the entire screen.
In such cases, using the lcd.getWindowSize() method makes sense. However, you can also indirectly query the display by checking the transmitter type.

For this, the system.getVersion() method exists.
This method returns a table with various elements.
The "board" element specifically returns a string representing the transmitter hardware, such as "X20", "X18", "X12".

A small function to query the transmitter hardware could look like this:

local DISP_X20 <const> 		= 1
local DISP_X18 <const> 		= 2
local DISP_HORUS <const> 	= 3

function evaluate_display()
    local detectSys = system.getVersion()							
    if detectSys.board == "X12" or detectSys.board == "X10EXPRESS" then return(DISP_HORUS) end
    if detectSys.board == "X18S" or detectSys.board == "X18" then return(DISP_X18) end
    return(DISP_X20)
end

First, I define constants with "self-explanatory names." A function retrieves the board type and returns the corresponding constant.

Now, I can create a multidimensional table, for example. The table could store positions for different objects, like text, values, or images, depending on screen (or widget) sizes.

local masterLayout = {	
	{hLine=46, offset=2, tabTxt=180,	tabValue=290},		-- x20
	{hLine=28, offset=0, tabTxt=100,	tabValue= 80},		-- x18
	{hLine=20, offset=1, tabTxt=100,	tabValue= 80},		-- horus
}

Here, I define a table named "masterLayout" and store different values for line height (hLine), y-offsets for positions (offset), and the horizontal positions for text and values (tabTxt, tabValue) for different display types.

The way tables are defined might not be immediately obvious to beginners, so here’s a brief explanation of Lua tables:


3. Tables (Arrays) in Lua

Let’s take a small detour to explore tables in Lua.
I assume at least a basic understanding of what a multidimensional table is.
When discussing multidimensional tables, the terms "matrix" or "array" are often used.

Most people are familiar with programs like Excel, which are used for editing two-dimensional tables (rows and columns).
A table has multiple entries in each dimension.

  • A specific element in a two-dimensional Ethos table could be accessed as follows:
value = table[row][column]

For three dimensions (and so on), it would look like this:

value = table[row][column][indexDimension3]

However, any element of a table cannot simply be assigned a value!

Initialization Comes First for Every Dimension!

For a one-dimensional table (a "list"), the table is initialized like this, after which values can be assigned:

local array = {}
array[1] = 15
array[2] = 17

For a two-dimensional table, multiple initializations are necessary, for example, for a table with 3 "rows" and 2 "columns":

local array = {}
for i= 1,3 do
	array[i] ={}
	array[i][1] = i*10
	array[i][2] = i*10+1
end

Another way to directly initialize would be through nesting:

local array = {
  {10,11},
  {20,21},
  {30,31}
}

The result would be the same; the specific use case dictates the approach.
In my opinion, it’s also worth considering that you’ll want to be able to read and understand your code long after it was initially written, so personal preferences can also play a role.



Arrays, the Next Level

Now it’s getting really interesting:
In Lua, tables are "associative arrays." Clear as day, right?

This means that indices don’t just have to be numbers, as shown above; they can also be text labels.

Example

local array = {}
array[„height“] = 12		-- height
array[„width“] = 40- width

print(„Height:“, array[„height“])

The array element doesn’t get a numerical index but is identified by a text label:



You can achieve this with even less code by assigning the label and its values directly during initialization:

local array = {
	height	= 12,
	width	= 40
	}

print(„Höhe:“, array[„height“])



Multidimensional Arrays with Labels

Now, let’s make this multidimensional, with two "rows," each assigning different height and width values:

local array = {
	{height= 12,	width	= 40},
	{height= 20,	width	= 60},
	}

print(„height 1:“, array[1][„height“])
print(„height 2:“, array[2][„height“])



Why this?

This can significantly improve code readability, especially when using the concise syntax where the element label is separated by a dot.

In the first example above, you could modify the print statement to get the same result as follows:

print(„height:“, array.height )

Somehow better, right?



The Ultimate Challenge: Thinking of It as Multidimensional


Remember "my" layout definition?

local masterLayout = {	
	{hLine=46, offset=2, tabTxt=180,	tabValue=290},		-- x20
	{hLine=28, offset=0, tabTxt=100,	tabValue= 80},		-- x18
	{hLine=20, offset=1, tabTxt=100,	tabValue= 80},		-- horus
}

What value do you think this function outputs?

print( masterLayout[DISP_X20].offset  ) 

Would you be able to "see" as quickly what’s hidden in the middle of the code if the array was generated purely with numerical indices, like this?

print(  masterLayout[1][2] )  



Finally, here are two links from the official Lua documentation that also explain these concepts:





4. Refreshing the Screen

To recap,
in Chapter 1 under handlers, I wrote the following:

  • wakeup is responsible for general background processing and, as much as possible, for preprocessing data used in paint. Wakeup is called very frequently by the system.

  • paint is also called frequently by the system and essentially "follows" the wakeup handler, focusing on graphical presentation.
    VERY IMPORTANT: Calling a graphical method—such as drawing lines or "printing" text or values—does NOT necessarily mean it will be displayed directly on the screen. Instead, it fills a very fast, temporary image buffer.

  • lcd.invalidate(x, y, w, h): Only this call triggers the screen (or the specified area) to be cleared on the display, and the relatively "slower" screen buffer is filled with new data.
    This method should only be called from wakeup, ideally when it’s detected that something on the display has changed and needs updating!



In the very first example, the content didn’t change; the same text was always displayed, so we didn’t necessarily need a refresh.

Now, however, we are constantly reading values that change.



How can we ensure the display is refreshed appropriately?

To keep it simple, we’ll have the wakeup handler trigger a full refresh of the widget on each loop, using the following call:

lcd.invalidate(0, 0, lcd.getWindowSize())





5. Finally, the Script

Preparation

As in the previous script:

  • Create a new subfolder in the scripts directory and copy the template file there.
  • Assign a name and key (UID) to the script.



The Header

The code section mostly consists of the handlers required by Ethos.
Typically, functions are also defined to keep the code within a handler concise and easy to read.
Functions have the added benefit of being callable from various parts of the program.

What is the "Header Section"?

The area "above" this is called the header or "header section."
Here, local variables and constants can be declared.

  • Most of the time, certain variables are only needed within loops or within a specific function or handler.
    These variables are created as local, meaning they lose their validity at the end of the loop or function, saving memory and improving performance through more direct access.

  • If variables or constants are needed across multiple handlers, they CAN be declared in the header section.
    (An alternative method will be explained later.)
    Variables declared here are available to all following handlers and functions.



First Step: Declaration in the Header

The first step is to declare the necessary variables and constants in the header.

-- sources
local srcValue1 = nil
local srcValue2 = nil

-- source-values
local value1 = nil
local value2 = nil



-- System type constants
local DISP_X20 <const> 		= 1
local DISP_X18 <const> 		= 2
local DISP_HORUS <const> 	= 3

-- actual system type
local txType = 1					-- standard = x20



-- layout, system dependent
local masterLayout = {	
	{hLine=46, offset=2, tabTxt=180,	tabValue=290},		-- x20
	{hLine=28, offset=0, tabTxt=100,	tabValue= 80},		-- x18
	{hLine=20, offset=1, tabTxt=100,	tabValue= 80},		-- horus
}

-- actual layout
local layout = {}

Explanation:

  • Variables for Sources and Source-Values:
    First, variables for the sources and their values are declared.

  • Constants for Transmitter/Display Types:
    Then, constants for the different transmitter/display types are defined, along with a variable for the current transmitter type.
    This variable is preset and will be reevaluated later.

  • Layout Parameters:
    Finally, under masterLayout, the layout parameters are defined depending on the display.
    This includes a table that will later use the actual layout based on the display type.




5.2 create()

We know that create is executed only once when the widget is first called.


Ideal Use Cases for create:

  • Determining the Display Type:
    Evaluate the display type to adjust the widget dynamically.

  • Setting the Layout:
    Configure the layout based on the evaluated display type.

  • Defining Sources:
    Define the sources for RSSI and transmitter voltage.



Routine for Evaluating Display Type

Evaluating the display type will likely be useful in other widgets as well.
To improve readability and reusability, this routine is placed in its own function called evaluate_display().
The function assigns the corresponding constant for the transmitter type to the variable txType.



Defining Sources

Next, the two sources (srcValue1 and srcValue2) are defined. These sources will later be used to read the values for RSSI and transmitter voltage.

... a lot of text for little code (-;

The create handler looks like this:

local function create()
	txType = evaluate_display()				-- determine display
	layout = masterLayout[txType]				-- set layout	

	srcValue1 = system.getSource({category=CATEGORY_TELEMETRY_SENSOR, name="RSSI"})		
	srcValue2 = system.getSource({category=CATEGORY_SYSTEM,   member=SYSTEM_MAIN_VOLTAGE})	
  return{}
end



5.3 paint(widget)

Now follows the most important handler for our widget: paint(widget).

local function paint(widget)
	lcd.font(FONT_XXL)						-- set font sizelcd.font(FONT_XL)						-- X10 / horus
	local xText = layout.tabTxt					-- x-coordinate text (both lines)-     #####   line1   #####

	local tmpColor = lcd.RGB( 170,  170,  200)				-- color definition line 1
	lcd.color(tmpColor)						-- set color
  	local y=layout.offset + layout.hLine*0				-- y-coordinate line 1

	lcd.drawText(xText,y,"RSSI:",RIGHT)				-- text value 1
	value1 = srcValue1:value()					-- get value 1
	lcd.drawNumber(layout.tabValue,	y,	value1,	nil,1,RIGHT)	-- draw value 1-     #####   line2   #####

	tmpColor = lcd.RGB( 170,  200,  170)				-- color definition line 2
	lcd.color(tmpColor)						-- set color
	y=layout.offset + layout.hLine*1					-- y-coordinate line 2

	lcd.drawText(xText, y,"TxBat:",RIGHT)				-- text value 2
	value2 = srcValue2:value()					-- get value 2
	lcd.drawNumber(layout.tabValue,	y,	value2,	nil,1,RIGHT)	-- draw value 2	

end

Steps in paint(widget)

  • Set the Font Size:
    The font size is set first.

  • Assign Horizontal Position:
    The horizontal position for the text in the widget is assigned to a variable xText.
    (This improves readability; alternatively, the layout variable layout.tabTxt could be used directly in the lcd.drawText method.)



First Line Displayed:

  1. Determine Color
  2. Set Y Position
  3. Display Text
  4. Retrieve Telemetry Value
  5. Display Value



Second Line:

This process repeats for the second line of values.






5.4 wakeup(widget)

As previously described, we want to refresh the widget regularly to display changing values as quickly as possible.


Enforcing Refresh

This is enforced in the wakeup handler (and only there!) using the lcd.invalidate() method:

local function wakeup(widget)
	lcd.invalidate()
end



5.5 Subfunctions

As mentioned in the create handler, it’s always a good idea to break a larger function into smaller, manageable tasks (functions).
This improves clarity and readability.
Reusing functions also makes the code more flexible.



Example: evaluate_display()

In create, the function evaluate_display() is called to determine the display or transmitter type.

function evaluate_display()
		local detectSys = system.getVersion()	-- get system type string

		if detectSys.board == "X12" or detectSys.board == "X10EXPRESS" then 
			return(DISP_HORUS) 
		elseif detectSys.board == "X18S" or detectSys.board == "X18" then 
			return(DISP_X18) 
		else
			return(DISP_X20)
		end
end

This function works as follows:

  1. Use system.getVersion:
    The hardware-specific "type string" is read.

  2. Decision Structure:
    Based on this string, the appropriate constant is returned.



Important Note:

When a function is called in Lua, it must be defined before it is called in the code.
Failing to do so will result in a script error!



5.6 The Complete Script

Now we have all the building blocks:

  • A "basic structure" of the widget was created with:
    • All necessary handlers
    • The correct name
    • A unique key in a folder
  • Necessary constants and shared variables are declared in the header.
  • create generates all sources that need to be defined once.
  • The paint handler displays the desired data.
  • wakeup ensures that the widget’s display is updated regularly.
  • Specific functions called in the handlers are coded before the handlers in the script.



Insights Gained

We also gained insights into:

  • How to Find Information in the Reference Guide:
    Look up necessary methods and syntax.

  • Handling Sources:
    Including telemetry values.

  • Evaluating the Transmitter Type:
    Determine which display is being used.

  • Querying Widget Size:
    Adjust layouts dynamically.

  • Understanding Multidimensional Tables in Lua:
    Work with more complex data structures.



The complete script is included in the "tut 2.2" folder in the attachment. It can easily be configured in a small widget frame on the standard home screen.

image

Feel free to adjust it to your own needs!

Clone this wiki locally