Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/SCRIPTS/RF2/COMPILE/scripts.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ local scripts = {
"/SCRIPTS/RF2/LCD/shared.lua",
"/SCRIPTS/RF2/LCD/waitMessage.lua",
"/SCRIPTS/RF2/F/canUseLvgl.lua",
"/SCRIPTS/RF2/F/fontTools.lua",
"/SCRIPTS/RF2/F/formatSeconds.lua",
"/SCRIPTS/RF2/F/getBit.lua",
"/SCRIPTS/RF2/F/getLvglSubtitle.lua",
Expand Down
93 changes: 93 additions & 0 deletions src/SCRIPTS/RF2/F/fontTools.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
-- Usage: local fontTools = rf2.executeScript("F/fontTools")()

local fontSizes = { XXLSIZE, DBLSIZE, MIDSIZE, STDSIZE, SMLSIZE, TINSIZE }
local fontNames = { "XXLSIZE", "DBLSIZE", "MIDSIZE", "STDSIZE", "SMLSIZE", "TINSIZE" }

local xlSize = _G["XLSIZE"]
if type(xlSize) == "number" then
table.insert(fontSizes, 2, xlSize)
table.insert(fontNames, 2, "XLSIZE")
end

-- Return the measured font height, or -1 when the test string exceeds maxW
local function measureFont(fontConst, maxW, testString)
local testText = testString or "X"
local textW, textH = lcd.sizeText(testText, fontConst)

if maxW and textW > maxW then return -1 end

return textH
end

local function getFontIndex(fontConst)
for i = 1, #fontSizes do
if fontSizes[i] == fontConst then return i end
end

return nil
end

-- Pick the largest fitting font, optionally limited to fonts smaller than
-- smallerThanFont
-- heightTolerance keeps the height fit check slightly forgiving because rendered text
-- can end up a couple of pixels taller than a strict sizeText limit suggests
-- If nothing fits, return the smallest available font anyway so callers do
-- not need a separate nil fallback path
local function selectFont(availableH, availableW, testString, smallerThanFont, heightTolerance)
local maxH = availableH + (heightTolerance or 2)
local startIndex = 1

if smallerThanFont then
local maxIndex = getFontIndex(smallerThanFont)
if maxIndex then startIndex = math.min(maxIndex + 1, #fontSizes) end
end

for i = startIndex, #fontSizes do
local fontConst = fontSizes[i]
if fontConst then
local fontH = measureFont(fontConst, availableW, testString)
if fontH > 0 and fontH <= maxH then return fontConst end
end
end

return fontSizes[#fontSizes]
end

-- Return the smallest font from a list of already selected font constants
-- Pass the font values returned by selectFont(...); because fontSizes is
-- ordered from largest to smallest, this just keeps the highest matching index
local function pickSmallestFont(...)
local selectedFont = nil
local selectedIndex = nil

for i = 1, select("#", ...) do
local fontConst = select(i, ...)
if fontConst then
for j = 1, #fontSizes do
if fontSizes[j] == fontConst and (not selectedIndex or j > selectedIndex) then
selectedFont = fontConst
selectedIndex = j
break
end
end
end
end

return selectedFont or STDSIZE
end

-- Return the symbolic EdgeTX font name for a font constant when needed
-- for e.g. debug output
local function getFontName(fontConst)
local index = getFontIndex(fontConst)
if index then return fontNames[index] end

return nil
end

return {
measureFont = measureFont,
selectFont = selectFont,
pickSmallestFont = pickSmallestFont,
getFontName = getFontName
}
209 changes: 188 additions & 21 deletions src/WIDGETS/RfTool/app.lua
Original file line number Diff line number Diff line change
@@ -1,18 +1,45 @@
---@diagnostic disable: undefined-global
-- RfTool widget
local zone, options = ...
local zone, options, warning_duplicate = ...
warning_duplicate = warning_duplicate == true

local w = {
zone = zone,
options = options
}

local font_tools = assert(loadScript("/SCRIPTS/RF2/F/fontTools.lua"))()

local scriptsCompiled = assert(loadScript("/SCRIPTS/RF2/COMPILE/scripts_compiled.lua"))()
if scriptsCompiled then
w.state = "loading"
else
w.state = "compiling"
end

-- Longest possible state string
local STATE_MEASURE_TEXT = "Unknown Protocol"

local function getTelemetryText(options, measure)
local source = options.sourceName
if not source or source == "" then return "No source" end

if measure then
if #source < 4 then
source = string.rep("W", 4 - #source) .. source
end
return source .. ": 0000" .. (options.Suffix or "")
end

-- Not available at boot time
if not getValue then return source .. ":" end

local value = getValue(source)
if value == nil then return source .. ": -" end

return source .. ": " .. tostring(value) .. (options.Suffix or "")
end

w.options.getText = function(options)
if not options.sourceName then return "" end
if not getValue then return " - " .. options.sourceName .. ": " end
Expand Down Expand Up @@ -48,7 +75,8 @@ local function setArmState(widget)
--]]
if armState ~= previousArmState then
previousArmState = armState
local state = bit32.btest(armState, 1) and "armed" or "disarmed"
-- bit32 is marked as deprecated, so switch to native bit operators
local state = (armState & 1) ~= 0 and "armed" or "disarmed"
widget:setState(state)
end
end
Expand Down Expand Up @@ -92,27 +120,140 @@ local function getModelName()
return modelName or "Unknown"
end

local function getStateText(widget)
local state = widget.state
return string.upper(string.sub(state, 1, 1)) .. string.sub(state, 2)
end

local function getDisplayedModelName(widget)
if widget.options.HideModel == 1 then return nil end
return getModelName()
end

local function showWidget(widget)
lvgl.clear();
lvgl.build({
{
type = "box", flexFlow = lvgl.FLOW_COLUMN, children =
{
{ type = "label", text = function() return getModelName() end, w = widget.zone.x, font = DBLSIZE, align = CENTER },
{
type = "label",
text = function()
return string.upper(string.sub(widget.state, 1, 1))
.. string.sub(widget.state, 2)
.. widget.options:getText()
end,
w = widget.zone.x,
align = CENTER
},
local widget_w = widget.zone.w or widget.zone.x
local widget_h = widget.zone.h or widget.zone.y
local displayed_model_name = getDisplayedModelName(widget)
local show_model = displayed_model_name ~= nil
local show_state = widget.options.HideState ~= 1
local show_telemetry = widget.options.HideTelemetry ~= 1
local telemetry_measure = show_telemetry and getTelemetryText(widget.options, true) or nil
local row1_text = nil
local row1_measure = nil
local row2_text = nil
local row2_measure = nil
local text_color = widget.options.TextColor or COLOR_THEME_PRIMARY1

-- Determine what text to show in row 1 and row 2 based on the options
if show_model then
row1_text = displayed_model_name
row1_measure = displayed_model_name
end

if show_state and show_telemetry then
if row1_text then
row2_text = function()
return getStateText(widget) .. " - " .. getTelemetryText(widget.options)
end
row2_measure = STATE_MEASURE_TEXT .. " - " .. telemetry_measure
else
row1_text = function() return getStateText(widget) end
row1_measure = STATE_MEASURE_TEXT
row2_text = function() return getTelemetryText(widget.options) end
row2_measure = telemetry_measure
end
elseif show_state then
if row1_text then
row2_text = function() return getStateText(widget) end
row2_measure = STATE_MEASURE_TEXT
else
row1_text = function() return getStateText(widget) end
row1_measure = STATE_MEASURE_TEXT
end
elseif show_telemetry then
if row1_text then
row2_text = function() return getTelemetryText(widget.options) end
row2_measure = telemetry_measure
else
row1_text = function() return getTelemetryText(widget.options) end
row1_measure = telemetry_measure
end
end

if warning_duplicate then
row1_text = "Warning"
row1_measure = row1_text
row2_text = "Use only one RfTool widget"
row2_measure = row2_text
text_color = COLOR_THEME_WARNING
end

local children = {}

if row1_text then
-- Build the lvgl object tree. If no rows are enabled we leave
-- children empty, which clears the widget while still flowing through
-- the same render tail.
local pad_x = 2
local pad_y = 2
local row_gap = 1
local content_w = math.max(1, widget_w - 2 * pad_x)
local content_h = math.max(1, widget_h - 2 * pad_y)

if row2_text then
local top_h = math.max(1, math.floor((content_h - row_gap + 1) / 2))
local top_font = font_tools.selectFont(top_h, content_w, row1_measure)
local top_font_h = font_tools.measureFont(top_font)
local detail_h = math.max(1, content_h - top_font_h - row_gap)
local detail_font = font_tools.selectFont(detail_h, content_w, row2_measure, top_font)
local detail_font_h = font_tools.measureFont(detail_font)
local detail_y = pad_y + top_font_h + row_gap

children[#children + 1] = {
type = "label",
x = pad_x,
y = pad_y,
w = content_w,
h = top_font_h,
text = row1_text,
font = top_font,
color = text_color,
align = LEFT
}
children[#children + 1] = {
type = "label",
x = pad_x,
y = detail_y,
w = content_w,
h = detail_font_h,
text = row2_text,
font = detail_font,
color = text_color,
align = LEFT
}
}
});
else
local row_font = font_tools.selectFont(content_h, content_w, row1_measure)
local row_font_h = font_tools.measureFont(row_font)
local row_y = pad_y + math.max(0, math.floor((content_h - row_font_h) / 2))

children[#children + 1] = {
type = "label",
x = pad_x,
y = row_y,
w = content_w,
h = row_font_h,
text = row1_text,
font = row_font,
color = text_color,
align = LEFT
}
end
end

lvgl.clear()
lvgl.build(children)

widget.renderedModelName = displayed_model_name
widget.visible = true
end

Expand All @@ -125,6 +266,11 @@ w.update = function(widget, options)
end
end

if warning_duplicate then
showWidget(widget)
return
end

if lvgl.isFullScreen() or lvgl.isAppMode() then
rf2.restartUi()
else
Expand All @@ -133,6 +279,12 @@ w.update = function(widget, options)
end

w.background = function(widget, calledFromRefresh)
if warning_duplicate then
return
end

rf2.rfToolInstanceSeenAt = getTime()

if widget.state == "compiling" then
compileTask = compileTask or assert(loadScript("/SCRIPTS/RF2/COMPILE/compile.lua"))()
if compileTask() == 1 then
Expand Down Expand Up @@ -173,8 +325,17 @@ end

local redrawWidget = false
w.refresh = function(widget, event, touchState)
if warning_duplicate then
if not widget.visible then
showWidget(widget)
end
return
end

rf2.rfToolInstanceSeenAt = getTime()

if uiTask ~= nil then
if redrawWidget or not widget.visible then
if redrawWidget or not widget.visible or widget.renderedModelName ~= getDisplayedModelName(widget) then
-- If we immediately show the widget after lvgl.exitFullScreen(), the widget if briefly
-- displayed in full screen mode. Using redrawWidget prevents that.
showWidget(widget)
Expand All @@ -192,7 +353,13 @@ w.refresh = function(widget, event, touchState)
w.background(widget, true)
end

if warning_duplicate then
-- Duplicate instances stay warning-only so they do not reinitialize shared RF2 state.
return w
end

initializeRf2GlobalVar()
rf2.rfToolInstanceSeenAt = getTime()
rf2.registerWidget = registerWidget
rf2.rfToolApiVersion = 1.00

Expand Down
Loading