Skip to content

feat(lua): describe the RGB LED layout to scripts, fix custom switch LED mapping - #7864

Draft
pfeerick wants to merge 2 commits into
EdgeTX:mainfrom
pfeerick:pfeerick/lua-rgbled-layout
Draft

pfeerick wants to merge 2 commits into
EdgeTX:mainfrom
pfeerick:pfeerick/lua-rgbled-layout

Conversation

@pfeerick

@pfeerick pfeerick commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

Closes #7716, and follows on from the discussion in #7742.

Summary

LED_STRIP_LENGTH and setRGBLedColor() keep their current meaning: Lua indexes 0 .. BLING-1 are the decorative LEDs, and the following indexes are the custom switch LEDs, which a script can only set while that switch is NONE. Nothing existing changes meaning.

This PR has two commits:

  1. fix(lua): custom switch LEDs mapped wrongly on PA01 is a bug fix with no new Lua API. It will be cherry-picked into the 2.12.5 backport (chore: 2.12.5 backports #7817).
  2. feat(lua): describe the RGB LED layout to scripts adds the new API, and is targeted at 3.0.
    • It only adds API and doesn't change existing behaviour, so it could also go into 2.12 if desired.
    • On 2.12 the LED layout is defined in each target's hal.h rather than in the hw_defs JSON. The gimbal group data would therefore need a small hal.h-based variant there.
    • The Lua side would be the same.

1. Fix: custom switch LED mapping

  • PA01: the custom switch LEDs are not in switch order (ledMapping = {4, 6, 0, 2}), but setRGBLedColor() assumed they were. A script therefore checked whether one switch was NONE, then wrote the LEDs of another, configured, switch.
  • Diagnostics page: fsGetLedRGB() made the same assumption, so the custom switch diagnostics page showed the wrong colours on PA01.
  • getFSLedState(): this ignored the strip offset altogether.
  • The fix: a weak fsLedFirstIndex() hook is now used for every custom switch LED access (fsLedRGB, fsGetLedRGB, getFSLedState, setRGBLedColor), and PA01 overrides it with its mapping.
  • ST16: the generic fsLedRGB() now handles CFS_LEDS_PER_SWITCH, so ST16's duplicate LED driver is removed.
  • Unchecked lookup: the result of switchGetSwitchFromCustomIdx() is now checked before it is used.
  • luadoc: corrected the setRGBLedColor() parameter names.

The firmware changes apply cleanly to 2.12. The only conflict is in tests/lua.cpp, where the new test is appended after tests that only exist on main.

2. Feature: LED layout for scripts (3.0)

  • BLING_LED_STRIP_LENGTH: the number of decorative LEDs. The custom switch LED count is LED_STRIP_LENGTH - BLING_LED_STRIP_LENGTH, so it gets no constant of its own.
  • FUNC_RGB_LED: this constant has been missing since feat: RGB leds supports #3909. Thanks to @davidbitton for spotting it.
  • getRGBLedInfo() returns { length, bling, cfs = { first, count, perSwitch }, groups = { <name> = { first, count, startAngle, direction } } }. The table is built only when called, so it adds one function entry to the ROTable.
  • leds.bling_groups in the hw_defs JSON: a new optional field, generated into lua_leds.inc. Each entry has a name, first, count, and optionally start_angle/direction for evenly spaced rings.
    • Populated for TX15, TX16S Mk3, GX15 and ST16 with the values from the SD card gimbal.lua.
    • The schema checks that each group fits within the bling LEDs, and that start_angle and direction are given together.
  • luadoc: documents the setRGBLedColor() index space.

Scripts that want only the decorative LEDs, and must still run on older firmware, can use (BLING_LED_STRIP_LENGTH or LED_STRIP_LENGTH).

SD card scripts

The companion SD card PR is EdgeTX/edgetx-sdcard#306:

  • gimbal.lua prefers getRGBLedInfo().groups, keeping its table as a fallback for older firmware.
  • The pattern scripts get a USE_FULL_STRIP option. It defaults to the decorative LEDs only, and can be switched to also drive unused custom switch LEDs.

Testing

  • Firmware builds: TX16S Mk3, GX15, ST16, PA01, GX12, V16, T15 Pro, MT12. The fix commit also builds and passes the gtests on its own.
  • Codegen: the hw_defs templates render for all 57 boards, and the generated hal_settings.h is byte-identical to main for every board.
  • Lua gtests:
    • Lua.RGBLedIndexes (fix commit) checks that bling writes land at BLING_LED_STRIP_START, that an unused custom switch slot is written through the mapping hook, and that a configured one is refused.
    • Lua.RGBLedInfo (feature commit) checks the constants and the table shape.
    • Both run on each LED target in CI.
  • Hardware, PA01: with a custom switch set to NONE, each switch's Lua slot lights that switch's own LED pair, configured switches are refused, and the diagnostics swatches match the real LEDs.
  • Hardware, TX16S Mk3: with ledstep.lua and ledmap.lua, Lua indexes 0-9 and 10-19 light the right and left gimbal rings, with the start LED and direction as reported by getRGBLedInfo(). Indexes 20-25 are SW1-SW6, and only a switch set to NONE accepts writes.
  • SD card scripts on hardware: the updated police.lua and gimbal.lua from feat(rgbled): drive the decorative LEDs by default, use firmware LED groups edgetx-sdcard#306 were tested on the TX16S Mk3. police.lua was run with both the default and USE_FULL_STRIP = true.

Test scripts

ledstep.lua is a Tools script (/SCRIPTS/TOOLS/). It lights one Lua index at a time and shows what that index is, and what setRGBLedColor() returned:

-- Tools script: step through every setRGBLedColor() index one at a time
-- Copy to /SCRIPTS/TOOLS/ and run it from the Tools menu.
--
-- Lights only the selected Lua index (blue), and shows what that index is
-- (bling LED / custom switch slot / group) and what setRGBLedColor()
-- returned. Configured custom switches should return false and keep their
-- model colour; switches set to NONE should return true and light up.
--
-- Next / Prev (rotary or +/-) move the index, Exit clears the LEDs and quits.
-- Turn off any "RGB leds" special function while using this.

local idx = 0
local result
local info

local function describe(i)
  if not info then
    return "no getRGBLedInfo() (old firmware)"
  end
  if i < info.bling then
    local s = "bling LED " .. i
    for name, g in pairs(info.groups) do
      if i >= g.first and i < g.first + g.count then
        s = s .. ", " .. name .. " #" .. (i - g.first)
      end
    end
    return s
  end
  local n = i - info.cfs.first
  return "custom switch slot " .. math.floor(n / info.cfs.perSwitch) ..
         " (LED " .. (n % info.cfs.perSwitch) .. " of " .. info.cfs.perSwitch .. ")"
end

local function light()
  for i = 0, LED_STRIP_LENGTH - 1 do
    setRGBLedColor(i, 0, 0, 0)
  end
  result = setRGBLedColor(idx, 0, 0, 255)
  applyRGBLedColors()
end

local function init()
  if getRGBLedInfo then
    info = getRGBLedInfo()
  end
  light()
end

local function run(event)
  if event == EVT_VIRTUAL_NEXT or event == EVT_VIRTUAL_INC then
    idx = (idx + 1) % LED_STRIP_LENGTH
    light()
  elseif event == EVT_VIRTUAL_PREV or event == EVT_VIRTUAL_DEC then
    idx = (idx - 1) % LED_STRIP_LENGTH
    light()
  elseif event == EVT_VIRTUAL_EXIT then
    for i = 0, LED_STRIP_LENGTH - 1 do
      setRGBLedColor(i, 0, 0, 0)
    end
    applyRGBLedColors()
    return 1
  end

  lcd.clear()
  lcd.drawText(5, 5, "Lua index " .. idx .. " / " .. (LED_STRIP_LENGTH - 1))
  lcd.drawText(5, 30, describe(idx))
  lcd.drawText(5, 55, "setRGBLedColor: " .. tostring(result))
  if info then
    lcd.drawText(5, 80, "bling " .. info.bling .. ", length " .. info.length)
  end
  lcd.drawText(5, 105, "Next/Prev: move, Exit: quit")
  return 0
end

return { init = init, run = run }

ledmap.lua is an RGBLED script (/SCRIPTS/RGBLED/, run from an "RGB leds" special function). It paints each group: the first LED white, the second full colour to show the direction, and the rest dim:

-- RGBLED test script for getRGBLedInfo() / setRGBLedColor() index mapping
-- Copy to /SCRIPTS/RGBLED/ and select it in an "RGB leds" special function.
--
--   gimbal_right ring : red, first LED white, second LED bright red (shows
--                       the direction), the rest dim red
--   gimbal_left ring  : blue, same pattern
--   other groups      : yellow
--   bling not in any group : dim white
--   custom switch LEDs: green, one shade per switch (only switches set to
--                       NONE light up, setRGBLedColor() refuses the others)
--
-- A one-off dump of the table is printed to the debug output in init().

local info

local function dump(t, indent)
  indent = indent or ""
  for k, v in pairs(t) do
    if type(v) == "table" then
      print(indent .. tostring(k) .. " = {")
      dump(v, indent .. "  ")
      print(indent .. "}")
    else
      print(indent .. tostring(k) .. " = " .. tostring(v))
    end
  end
end

local function init()
  if not getRGBLedInfo then
    print("ledmap: getRGBLedInfo() not available")
    return
  end
  info = getRGBLedInfo()
  print("ledmap: getRGBLedInfo()")
  dump(info, "  ")
end

local function run()
  if not info then return end

  for i = 0, info.bling - 1 do
    setRGBLedColor(i, 8, 8, 8)
  end

  for name, g in pairs(info.groups) do
    local r, gr, b = 255, 255, 0
    if name == "gimbal_right" then
      r, gr, b = 255, 0, 0
    elseif name == "gimbal_left" then
      r, gr, b = 0, 0, 255
    end
    for n = 0, g.count - 1 do
      if n == 0 then
        -- first LED of the group: white
        setRGBLedColor(g.first + n, 255, 255, 255)
      elseif n == 1 then
        -- second LED: full colour, shows the direction
        setRGBLedColor(g.first + n, r, gr, b)
      else
        setRGBLedColor(g.first + n, math.floor(r / 12), math.floor(gr / 12), math.floor(b / 12))
      end
    end
  end

  if info.cfs then
    local switches = info.cfs.count / info.cfs.perSwitch
    for n = 0, info.cfs.count - 1 do
      local sw = math.floor(n / info.cfs.perSwitch)
      setRGBLedColor(info.cfs.first + n, 0, math.floor(40 + sw * 215 / switches), 0)
    end
  end

  applyRGBLedColors()
end

local function background()
end

return { run=run, background=background, init=init }

🤖 Generated with Claude Code

pfeerick and others added 2 commits October 5, 2026 04:39
setRGBLedColor() assumed the custom switch LEDs follow each other in
switch order from CFS_LED_STRIP_START. PA01 maps them through
ledMapping = {4, 6, 0, 2}, so a script checked whether one switch was
set to NONE but then wrote the LEDs of another, configured, switch.
fsGetLedRGB() made the same assumption, so the custom switch
diagnostics page showed the wrong colours on PA01, and getFSLedState()
ignored the strip offset altogether.

Add a weak fsLedFirstIndex() hook giving the first strip LED of a
custom switch, and use it for every custom switch LED access:
fsLedRGB(), fsGetLedRGB(), getFSLedState() and setRGBLedColor(). PA01
overrides it with its mapping. The generic fsLedRGB() now handles
CFS_LEDS_PER_SWITCH, so ST16's copy of the driver is removed.

Also check the switchGetSwitchFromCustomIdx() result before using it,
and fix the setRGBLedColor() parameter names in the luadoc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LED_STRIP_LENGTH counts the decorative LEDs followed by the custom
switch LEDs, which scripts can only set while that switch is set to
NONE. Scripts had no way to tell the two ranges apart, or to find where
the gimbal ring LEDs are without a hard-coded per-radio table.

Without changing the meaning of LED_STRIP_LENGTH or setRGBLedColor(),
add:
- BLING_LED_STRIP_LENGTH, the number of decorative LEDs
- FUNC_RGB_LED, missing from the Lua constants until now
- getRGBLedInfo(), returning the strip length, the decorative and
  custom switch LED ranges, and named groups of decorative LEDs with
  their ring geometry

The groups come from a new optional leds.bling_groups list in the
hw_defs JSON, generated into lua_leds.inc, and are populated for the
TX15, TX16S Mk3, GX15 and ST16 gimbal rings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pfeerick

pfeerick commented Oct 5, 2026 •

Copy link
Copy Markdown
Member Author

@philmoz @3djc What do you think?? (Part B / second commit, Part A / first commit is just fixes for PA01 that I hit since I chose that as a test target lol)

This allows for lua to readily determine "bling" vs cfs leds without changing things for older scripts, as well as allowing future lua scripts to "just work" with any radio's pre-defined gimbal leds. With a minor tweak to the sdcard content, there is also a flag at the top of the various RGBLED scripts that allows for them to be used on "disabled" CFS switches. i.e. thus allowing, for instance, for the police.lua to run on the GX12 CFS leds if they are all turned off, etc.

@philmoz

philmoz commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

With respect to the 'getRGBLedInfo' function, I considered this when I rewrote the gimbal script; but decided against it:

  • limited application, the percentage of radios with gimbal LED's is small
  • the SD card scripts can be updated more easily than firmware to support new radios
  • the SD card scripts still need the fallback so it's duplicated effort to be maintained.

I don't have any serious objection if you want to add it.

@pfeerick

pfeerick commented Oct 5, 2026 •

Copy link
Copy Markdown
Member Author

It is small, but increasing... it appears to be becoming somewhat a standard feature for most new radios, or at least the flagship version of a given model (the same could have been said a year ago about customisable switches).

Agreed, the SD card content can be updated easier, but it should not be necessary for something the firmware itself knows about, and could (and should IMO) be informing of, so Lua authors don't need to handcraft the complex mapping tables ... no, not ranting on about that any further... I'll start griping about the should-be-unnecessary complex version and radios specific tables in lua scripts.

As far as the fallback, no, it won't be needed for radios introduced from say 3.0 onwards if this only goes into 3.0 (if someone wants to add them, that's fine, but we certainly won't need to do it any more) - which is where part of this becomes is this 3.0 only or 2.12-backported question.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug 🪲 Something isn't working lua-api Lua API related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants