Skip to content

Add stick lighting control for the AYN Odin 3 - #255

Open
Rayekkk wants to merge 1 commit into
armada-os:mainfrom
Rayekkk:odin3-stick-lighting
Open

Add stick lighting control for the AYN Odin 3#255
Rayekkk wants to merge 1 commit into
armada-os:mainfrom
Rayekkk:odin3-stick-lighting

Conversation

@Rayekkk

@Rayekkk Rayekkk commented Aug 15, 2026

Copy link
Copy Markdown

Closes #167 for the Odin 3.

The Odin 3 has two HTR3212 controllers driving four RGB emitters around each stick. The kernel already exposes all 24 channels at /sys/class/leds/{l,r}:{r,g,b}{1..4}, but nothing in userspace ever wrote to them, so the rings have always been dark. This adds a small daemon plus a Stick Lighting section in Armada Control's Settings tab: colour, brightness, and a static or breathing mode, with an optional separate colour per stick.

Off by default, so an update does not change how anyone's device looks without being asked.

Verified on hardware, not read off the device tree

Everything below was measured on an Odin 3 running beta (20260729.d812101), not inferred:

  • Channel labels match the emitted colours. The node labels in the DTS disagree with the exported labels (ledr_b1 carries label = "r:r1"), and it is the exported labels that are right.
  • Both rings run clockwise, but the right ring is offset by one position: upper left is l:*4 and r:*1, upper right l:*1 and r:*2, lower right l:*2 and r:*3, lower left l:*3 and r:*4.
  • The four emitters are not evenly spaced. There is a wider gap on the left of each ring, which is why the right side of a ring visibly lights first on a ramp. Anything running around the circumference looks broken, so the modes are static and breathing only.
  • Colours and mode survive suspend and resume, so no restore hook is needed.

Implementation notes

  • Levels are gamma corrected before they reach the PWM registers: duty is linear in current, perception is not. Breathing runs at 60 Hz because 20 Hz visibly bands on a four second ramp.
  • armada-ledd polls its config file rather than taking a signal, so the Quick Access Menu can preview a colour while a slider is being dragged.
  • The unit deliberately carries no ConditionPathExists. systemd evaluates conditions once, and the i2c controllers can probe after the unit starts, so the daemon waits for the LED devices itself and exits cleanly if they never appear.
  • Gated on ayn-odin-3. AYN Thor, Odin 2 Portal and Retroid Pocket 6 declare the same controllers in their device trees, but nobody has confirmed their ring layout, so they are deliberately left out. Enabling them is a one line change once someone with the hardware maps them.

Why Armada Control and not Steam's own UI

Steam has no general device lighting API, and steamos-manager exposes nothing for LEDs either. The only native route is the DualSense lightbar: InputPlumber 0.77.2 ships an LED source driver that forwards a target device's colour to a LED class device. Taking it would mean:

  • a device tree change grouping the mono channels with leds-group-multicolor (the kconfig option is already enabled), because that driver needs multi_intensity and multi_index, which the Odin 3 does not expose;
  • forcing controller emulation to DualSense, which changes button glyphs across the whole Steam UI and fights the existing Controller Emulation setting;
  • one colour for both sticks, since Steam sends a single lightbar value;
  • no breathing, since Steam only sets a static colour.

That route is worth having later, and the recipe above is the whole of it, but the two must be made mutually exclusive if it ever lands, because they would fight over the same LEDs.

Tests

tests/leds-test.sh covers config clamping and fallbacks, the gamma conversion, both daemon exit paths, and asserts the unit has no Condition and that the image actually enables the service. It was checked by mutation: reverting the fixes turns it red.

The rest of tests/ passes, except abl-update-test.sh and perf-settings-test.sh, which fail the same way on a pristine main and are unrelated to this change.

The Odin 3 has two HTR3212 controllers driving four RGB emitters around
each stick, and the kernel already exposes all 24 channels, but nothing in
userspace ever wrote to them, so the rings stayed dark. Add a small daemon
that applies a saved colour, brightness and mode, and a Stick Lighting
section in Armada Control's Settings tab to drive it.

The layout was mapped on hardware rather than read off the device tree.
Channel labels match the emitted colours, both rings run clockwise, and the
right ring is offset by one position from the left: upper left is l:*4 and
r:*1, upper right l:*1 and r:*2, lower right l:*2 and r:*3, lower left
l:*3 and r:*4. The four emitters are not evenly spaced, with a wider gap on
the left of each ring, so no animation runs around the circumference; the
modes are static and breathing only.

Levels are gamma corrected before they reach the PWM registers because duty
is linear in current and perception is not, and breathing runs at 60 Hz
because 20 Hz visibly bands on a four second ramp.

armada-ledd polls its config file instead of taking a signal so the Quick
Access Menu can preview a colour while a slider is being dragged. The unit
carries no ConditionPathExists: systemd evaluates conditions once, and the
i2c controllers can probe after the unit starts, so the daemon waits for the
LED devices itself.

Lighting is gated on ayn-odin-3, the only device whose ring wiring has been
verified. The AYN Thor, Odin 2 Portal and Retroid Pocket 6 declare the same
controllers in their device trees, but nobody has confirmed their layout, so
they are deliberately left out.

Off by default, so an update does not change how anyone's device looks
without being asked.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

Joystick lighting controls

2 participants