Skip to content

Keyboard backlight: drive the ENE K5130 directly on Predator PHN16S-71 - #8

Open
HectorHuaripata wants to merge 7 commits into
otectus:mainfrom
HectorHuaripata:ene-keyboard
Open

Keyboard backlight: drive the ENE K5130 directly on Predator PHN16S-71#8
HectorHuaripata wants to merge 7 commits into
otectus:mainfrom
HectorHuaripata:ene-keyboard

Conversation

@HectorHuaripata

Copy link
Copy Markdown

Keyboard backlight: drive the ENE K5130 directly on Predator PHN16S-71

AI assistance disclosure. This work was done in a session assisted by an
AI agent (Claude): it proposed the experiments, wrote the probing tools and
authored the code. I ran everything on the affected machine and reported what
the hardware actually did. Every behavioural claim below was verified by a
human watching the keyboard
, not inferred by the model — the firmware's
failure mode here is the silent no-op, so nothing was accepted on the basis
of a successful write or a matching read-back. Several intermediate
conclusions were wrong and are documented as such in docs/ENE_PROTOCOL.md.

Summary

On the Acer Predator Helios Neo 16S AI (PHN16S-71) the keyboard lighting page
looks like it works and does nothing. Setting zone colours succeeds, the value
is stored, the sysfs read-back returns exactly what was written — and the
keyboard keeps running the factory rainbow.

The cause is in firmware, not in Archer: the ACPI-WMI lighting path on this
model is partially implemented. Measured, with a single WMI call:

Field Result
brightness applied
per-zone RGB stored, read back faithfully, never applied to the LEDs
effect mode ignored

Because the read-back is faithful, every layer above it reports success. That
is what makes this bug so quiet, and it is worth stating plainly: on this model
the sysfs read-back is not evidence that anything happened.

The LEDs are actually driven by an ENE K5130 on I2C-1 at address 0x50,
exposed as a standard HID-over-I2C device (0018:0CF2:5130) carrying four
vendor feature reports. Writing those reports directly does work.

This PR adds that backend and routes the two keyboard setters through it,
keeping the existing sysfs write as a fallback for other hardware.

Changes

File Change
gui/archer_ene.py new — ENE K5130 protocol backend
gui/archer_daemon.py both keyboard setters prefer ENE, sysfs fallback kept
gui/archer/pages/keyboard.py effect list replaced with the verified effects
docs/ENE_PROTOCOL.md new — full reverse-engineering notes
install-ene-keyboard.sh optional deploy/rollback helper for existing installs

Nothing changes for hardware where the ENE is absent: archer_ene is imported
in a try, available() gates the feature, and the old code path is untouched.

The protocol, briefly

0xA1   4 bytes, read-only   "03 65 21 83" = device count, then device ids
0xA2   1 byte               select device            <- MANDATORY before 0xA4
0xA4  10 bytes              dev, mode, brightness, ?, ?, R, G, B, zone(16b LE)

Device ids: 0x21 keyboard (4 zones), 0x65 performance-mode button LED,
0x83 lid logo.

Two things that cost the most time to find, and that anyone extending this
should know:

Mode semantics are per device and do not carry over. On the keyboard mode 1
turns it off and mode 2 is static colour. On the button LED mode 1 is static
colour. Generalising from one device to another produces silent no-ops.

The zone field is a bitmask of the low four bits, so zones combine: 0x3
paints the left half in a single write. The high byte is ignored.

Verified keyboard effects: 2 static, 4 fade, 5 colour cycle, 6 colour cycle
fast, 7 rainbow wave. Modes 5 and 6 are not exposed by any existing tool for
this machine.

Why speed and direction are hidden in the UI

They correspond to bytes 3 and 4 of report 0xA4, whose meaning is not
established. The daemon accepts them for signature compatibility and ignores
them rather than guessing. A control that silently does nothing is worse than
no control, so both are hidden until decoded.

Safety notes

  • The target is resolved by HID identity, never by hidraw number. The
    numbering is not stable on this machine: the ENE's reset times out during
    probe so it enumerates last, and the touchpad can rebind and reclaim
    hidraw0.
  • The internal keyboard and the touchpad are on an explicit blacklist.
  • Every value is range-checked against what the report descriptor declares.
  • Modes >= 8 are rejected. While sweeping the button LED they coincided
    with fans starting and stopping, so in that range the report probably reaches
    the performance profile and not just the LED. Not thermally dangerous — the
    EC governs the fan curve — but out of scope for a lighting control.
  • POLKIT_ACTIONS already maps both keyboard methods, so authorisation is
    unchanged and no new method bypasses it.

Testing

Verified on Predator PHN16S-71, BIOS V1.26, kernel 6.17 series, Arch Linux with
linuwu-sense 1.0.0 (DKMS).

Every result below was taken against a fully dark baseline — keyboard,
button LED and lid logo all off — so that anything lighting up admits no
alternative reading. That matters: an earlier round of this work reached a
wrong conclusion precisely because no baseline was recorded before the first
write.

  • Zone addressing swept exhaustively: 0x1/0x2/0x4/0x8 each paint their
    own quarter, 0x3 paints the left half, 0xF and 0xFFFF the whole board,
    0x0100 does nothing.
  • All five effects observed on the physical keyboard.
  • Daemon restart reapplies saved lighting correctly through the new backend.

Known gaps

  • 0x83 (lid logo) responds, but returned green/yellow/red for three
    consecutive writes of the same colour, so it does not interpret the RGB
    bytes the way the keyboard does. Not wired into the UI.
  • Bytes 3 and 4 of 0xA4 undecoded.
  • Lighting is not reapplied after suspend/resume; acer_suspend/acer_resume
    do not touch RGB.

HectorHuaripata and others added 7 commits August 5, 2026 02:04
On the Predator PHN16S-71 the ACPI-WMI lighting path is only partially
implemented in firmware. Measured behaviour:

    brightness   applied
    per-zone RGB stored, read back faithfully, never applied to the LEDs
    effect mode  ignored

This is why per_zone_mode appeared to work: writing it succeeds and the
sysfs read-back returns exactly what was written, while the keyboard keeps
running the factory rainbow. On this model the read-back is not evidence.

The LEDs are actually driven by an ENE K5130 on I2C-1 at 0x50, exposed as
HID-over-I2C 0018:0CF2:5130 with four vendor feature reports. Writing them
directly does work, verified against a fully dark baseline.

  archer_ene.py      new backend: select device via 0xA2, configure via
                     0xA4. Resolves the target by HID identity rather than
                     hidraw number, which is not stable across boots, and
                     refuses the internal keyboard and the touchpad.
  archer_daemon.py   both keyboard setters prefer the ENE backend and keep
                     the sysfs write as a fallback for other hardware.
  keyboard.py        the effect list now holds the five effects actually
                     verified on this controller instead of the eight from
                     the WMI documentation, most of which did nothing.
                     Speed and direction are hidden: they map to bytes 3
                     and 4 of report 0xA4, whose meaning is not yet known,
                     and a control that silently does nothing is worse than
                     no control.

Zones are addressed by a bitmask of the low four bits, so they combine:
0x3 paints the left half in one write. Full notes in docs/ENE_PROTOCOL.md.

Reverse-engineered and written with AI assistance; every behavioural claim
was verified by a human observing the hardware.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bytes 3 and 4 of report 0xA4 are now identified, and the keyboard mode
sweep was extended past 7.

  byte 3  speed, 0-9, rising monotonically
  byte 4  direction, 1 = left-to-right, 2 = right-to-left

Note the direction encoding is the REVERSE of the convention Archer and the
Linuwu-Sense docs use, where 2 means left-to-right. set_effect() takes
Archer's value and flips it for the wire, so the UI label stays correct.

Four further effects were observed on the keyboard and are now exposed:
mode 8 (a random segment, then a full-board flash), 9 (circular, outside
inward), 10 (a streak crossing and returning over a dark board) and 11 (two
of the four segments lit at random). Modes 13-31 produced nothing visible.

Names are matched to the effects PredatorSense advertises, by behaviour
rather than by any documented mapping; "Shifting" is the least certain and
is marked as tentative in the docs.

The mode ceiling is now per device: the keyboard accepts the full verified
range, while other device ids stay capped at 7, because on the
performance-mode button LED modes >= 8 coincided with fans starting and
stopping.

Speed and direction return to the UI as working controls.

Verified on hardware against a dark baseline, one variable per step.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mode 10 is the one with a visible direction: light crosses the board and
returns. That matches the documented description of Shifting, "shifting
light effect, full control over speed, direction, and color", far better
than Meteor does. Mode 8 -- a point flaring at random followed by a
full-board flash -- reads as Meteor instead.

Both remain marked tentative: there is no documented mapping between the
ENE's mode numbers and the effect names PredatorSense advertises, so these
are behavioural matches, not authority.

Effect list order is unchanged, so saved settings indices stay valid.

Also refreshes the module docstring, which still described bytes 3 and 4 as
unknown and the mode ceiling as a flat limit rather than per device.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…file

The lid logo (device 0x83) does follow the requested RGB. An earlier round
concluded otherwise because the same green was sent with modes 1, 2 and 4
and produced off, green and red: three modes with three different
behaviours, read as one capricious colour. With the mode held at 2 the logo
matches red, green, blue and white exactly, and repeating an identical write
repeats the identical result, so there is no hidden counter either.

Logo modes: 1 off, 2 static honouring RGB, 3 off, 4 fixed red, 5 slow
cycle, 6 fixed yellow, 7 off.

The performance-mode button LED (0x65) is lit from the factory in a colour
that tracks the active platform profile. Once this backend takes the
controller over the firmware stops driving it and the button just goes
dark, so the daemon now keeps it in step: set_thermal_profile() colours it,
which also covers the restore path at startup since that goes through the
same call.

  low-power  cyan · quiet green · balanced white
  balanced-performance purple (the factory colour here) · performance red

Unknown profile names are left alone rather than guessed at, so a kernel
that grows a new profile does not silently get the wrong colour. The
behaviour is gated on button_follows_profile in settings.json, default on,
and any failure is logged and swallowed: a lighting detail must never make
a thermal profile change report failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rable

Lighting did not survive suspend. Neither acer_suspend() nor acer_resume()
touch RGB, and the ENE loses its state across the sleep, so the keyboard
came back under whatever the EC decided.

The daemon now listens for logind's PrepareForSleep and reapplies the saved
state on the way back up. Hooking that signal keeps this self-contained:
no extra systemd unit to install, and the daemon already runs a GLib loop.
The write is deferred two seconds because the I2C-HID controller
re-enumerates shortly after resume and PrepareForSleep(False) arrives before
that.

reapply_lighting() is deliberately narrow: lighting only. A resume is not
the moment to start rewriting the thermal profile or the fans, so those are
left alone even though the same settings file holds them.

Also: the per-profile button colours are now overridable through
button_colours in settings.json, and the defaults are documented for what
they are -- a choice made here, not Acer's mapping. Acer does not publish
the per-mode colours, and once this backend takes the controller over the
factory firmware stops driving the LED, so there is nothing left to read
them from. Only the purple on balanced-performance came from observed
hardware. The balanced default is now blue, to match Archer's own palette.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every write to the performance-mode button LED silently did nothing,
because BUTTON_STATIC was 1 and mode 1 is a no-op on that device. A fresh
sweep from a known state gives:

    1 nothing · 2 static, honours RGB · 3 nothing · 4 breathing
    5 colour cycle · 6 off · 7 nothing

So mode 2 is static colour on ALL THREE devices -- keyboard, button and lid
logo alike. That is a simpler rule than the one documented before, and the
earlier claim that the button's static lived at mode 1 came from a single
observation during the first sweep that later evidence does not support.

What remains per device is the rest of the map: on the keyboard mode 1
turns it off, on the button mode 1 does nothing and mode 6 does. Mode 6 on
the button also briefly darkens the keyboard, which the EC restores a
second or two later.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
set_thermal_profile() only covers changes made through Archer. The Plasma
widget, powerprofilesctl, the driver's own restore at module load and the
hardware mode button all write platform_profile directly, and the LED would
sit there showing a profile the machine is no longer in.

A three-second poll of one small sysfs file catches every writer without
needing to know who they are, which matters here because there are several
and they do not coordinate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant