diff --git a/src/SCRIPTS/RF2/COMPILE/scripts.lua b/src/SCRIPTS/RF2/COMPILE/scripts.lua index c6b734d..e9c337a 100644 --- a/src/SCRIPTS/RF2/COMPILE/scripts.lua +++ b/src/SCRIPTS/RF2/COMPILE/scripts.lua @@ -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", diff --git a/src/SCRIPTS/RF2/F/fontTools.lua b/src/SCRIPTS/RF2/F/fontTools.lua new file mode 100644 index 0000000..016d850 --- /dev/null +++ b/src/SCRIPTS/RF2/F/fontTools.lua @@ -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 +} diff --git a/src/WIDGETS/RfTool/app.lua b/src/WIDGETS/RfTool/app.lua index e2422dc..3c9a8bf 100644 --- a/src/WIDGETS/RfTool/app.lua +++ b/src/WIDGETS/RfTool/app.lua @@ -1,11 +1,15 @@ +---@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" @@ -13,6 +17,29 @@ 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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) @@ -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 diff --git a/src/WIDGETS/RfTool/main.lua b/src/WIDGETS/RfTool/main.lua index 818b8ff..c5ca9c1 100644 --- a/src/WIDGETS/RfTool/main.lua +++ b/src/WIDGETS/RfTool/main.lua @@ -1,11 +1,23 @@ +---@diagnostic disable: undefined-global -- Keep main.lua as lightweight as possible, since main.lua gets loaded for **all** widgets at boot time. -- Even if a widget isn't used by a particular model. local name = "RF Tool" +local ACTIVE_WIDGET_TIMEOUT = 100 -- 1 second timeout +--- this is for VSCode extnension 'EdgeTX Dev Kit' +---@type WidgetScript + +---@type WidgetOptions local options = { { "Source", SOURCE, "Vcel" }, - { "Suffix", STRING, "" } + { "Suffix", STRING, "" }, + { "HideModel", BOOL, 0}, + { "HideState", BOOL, 0 }, + { "HideTelemetry", BOOL, 0 }, + { "TextColor", COLOR, COLOR_THEME_PRIMARY1 } } +-- newly added options in EdgeTX don't get their set default value, +-- so the color is black by default when "updating" the widget. if lvgl == nil then return { @@ -19,8 +31,10 @@ if lvgl == nil then end local function create(zone, options) - local widget = loadScript("/WIDGETS/RfTool/app.lua")(zone, options) - return widget + local now = getTime() + local warning_duplicate = rf2 ~= nil and rf2.rfToolInstanceSeenAt ~= nil and now - rf2.rfToolInstanceSeenAt <= ACTIVE_WIDGET_TIMEOUT + + return loadScript("/WIDGETS/RfTool/app.lua")(zone, options, warning_duplicate) end local function update(widget, options)