Skip to content

Telemetry value widget

A widget that shows any source in the largest font that fits, turns a warning color below a threshold, tracks min/max, and adds Reset min/max to its long-press menu.

The widget's configuration page

What it demonstrates

  • Change detection at display precision. The value is quantized to the source's decimals before comparing, so sensor noise doesn't cause redraws.
  • Cached font fitting. Measuring text in several fonts is done only when the widget size or the text length changes.
  • File-level constants for colors and the font list.
  • Versioned settings (storage.write("v", ...)) so later releases can migrate.
  • A context menu through the menu callback.

The script

scripts/valuewgt/main.lua
-- Value widget: one source in large text, with min/max tracking,
-- a warning threshold and a long-press menu to reset the statistics.
-- https://frskyrc.github.io/ethos-lua-doc/cookbook/value-widget/

local SETTINGS_VERSION = 1

-- colors and fonts are computed once, at file level, not in paint()
local COLOR_WARN = lcd.RGB(230, 50, 50)
local FONTS = { FONT_XXL, FONT_XL, FONT_L, FONT_M, FONT_S }

local function create()
  return {
    source = nil,         -- chosen in configure()
    warnBelow = 0,        -- 0 = no warning; stored in tenths (e.g. 105 = 10.5)
    color = lcd.RGB(0, 190, 80),
    -- runtime state
    shown = nil,          -- last value as displayed (quantized), for change detection
    text = "--", minText = "", maxText = "",
    min = nil, max = nil,
    warn = false,
    fitKey = nil, fitFont = FONT_M,  -- cached font choice for the current size + text length
  }
end

local function fmt(widget, v)
  local d = widget.source:decimals()
  return string.format("%." .. d .. "f", v)
end

local function wakeup(widget)
  local src = widget.source
  if not src then return end

  local v = src:value()
  if v == nil then
    if widget.shown ~= nil then
      widget.shown, widget.text = nil, "--"
      lcd.invalidate()
    end
    return
  end

  -- compare at display precision so sensor noise doesn't cause redraws
  local scale = 10 ^ src:decimals()
  local shown = math.floor(v * scale + 0.5)
  if shown == widget.shown then return end
  widget.shown = shown

  widget.text = src:stringValue()
  if not widget.min or v < widget.min then widget.min = v; widget.minText = "min " .. fmt(widget, v) end
  if not widget.max or v > widget.max then widget.max = v; widget.maxText = "max " .. fmt(widget, v) end
  widget.warn = widget.warnBelow > 0 and v * 10 < widget.warnBelow
  lcd.invalidate()
end

local function pickFont(widget, w, h)
  -- re-fit only when the size or the text length changes
  local key = w * 100000 + h * 100 + #widget.text
  if key == widget.fitKey then return widget.fitFont end
  widget.fitKey = key
  for i = 1, #FONTS do
    lcd.font(FONTS[i])
    local tw, th = lcd.getTextSize(widget.text)
    if tw <= w - 8 and th <= h * 0.7 then
      widget.fitFont = FONTS[i]
      return FONTS[i]
    end
  end
  widget.fitFont = FONTS[#FONTS]
  return widget.fitFont
end

local function paint(widget)
  local w, h = lcd.getWindowSize()
  local fg = lcd.themeColor(THEME_DEFAULT_COLOR)

  if not widget.source then
    lcd.font(FONT_S)
    lcd.color(fg)
    lcd.drawText(w / 2, h / 2 - 8, "Configure a source", CENTERED)
    return
  end

  lcd.font(pickFont(widget, w, h))
  local _, th = lcd.getTextSize(widget.text)
  lcd.color(widget.warn and COLOR_WARN or widget.color)
  lcd.drawText(w / 2, (h - th) / 2, widget.text, CENTERED)

  if h > 80 then
    lcd.font(FONT_XS)
    lcd.color(fg)
    lcd.drawText(4, h - 16, widget.minText)
    lcd.drawText(w - 4, h - 16, widget.maxText, RIGHT)
  end
end

local function resetStats(widget)
  widget.min, widget.max, widget.minText, widget.maxText = nil, nil, "", ""
  widget.shown = nil  -- force a refresh on the next wakeup
end

local function menu(widget)
  return { { "Reset min/max", function() resetStats(widget) end } }
end

local function configure(widget)
  local line = form.addLine("Source")
  form.addSourceField(line, nil, function() return widget.source end,
    function(v) widget.source = v; resetStats(widget) end)

  line = form.addLine("Warn below")
  local f = form.addNumberField(line, nil, 0, 10000,
    function() return widget.warnBelow end,
    function(v) widget.warnBelow = v; widget.shown = nil end)
  f:decimals(1)
  f:help("0 disables the warning color")

  line = form.addLine("Color")
  form.addColorField(line, nil, function() return widget.color end,
    function(v) widget.color = v end)
end

local function read(widget)
  local _ = storage.read("v")  -- settings version, for future migrations
  widget.source = storage.read("source")
  widget.warnBelow = storage.read("warn") or 0
  widget.color = storage.read("color") or widget.color
end

local function write(widget)
  storage.write("v", SETTINGS_VERSION)
  storage.write("source", widget.source)
  storage.write("warn", widget.warnBelow)
  storage.write("color", widget.color)
end

local function init()
  system.registerWidget({
    key = "valuewg", name = "Big value",  -- "Value" would clash with the built-in widget
    create = create, wakeup = wakeup, paint = paint,
    configure = configure, read = read, write = write, menu = menu,
  })
end

return { init = init }

Variations

  • Show stale telemetry: grey the text when widget.source:age() exceeds a few seconds (see Sources & telemetry).
  • Warn above instead of below: add a choice field for the direction.
  • Use the theme's warning color: replace COLOR_WARN with lcd.themeColor(THEME_WARNING_COLOR).

API used

system.registerWidget · lcd.getWindowSize · lcd.getTextSize · lcd.themeColor · Source:value · Source:stringValue · Source:decimals · form.addSourceField · form.addNumberField · storage.read