Files
mesh-catalog/modules/asus-zephyrus-g14/README.md
T
jochen 9c84cd9e6c Phase 3: asus-zephyrus-g14 and memory-pressure, with Go tools and long-running code
The laptop model's hardware module and a memory-pressure module for any
machine (hq research 027/03, 026/05, to-be 42 phase 3). The predecessor's
polling auto-profile and mem-guard user scripts become each module's own
Go code launched by the node runtime (ADR 0198): a profile switcher woken
by the kernel's power-supply uevents, and a guard that warns on RAM, swap
or PSI before systemd-oomd acts, on the desktop over the account's bus and
always as an event. supergfxctl and triggerhappy are kept as found
(research 027 Q1).
2026-10-04 12:56:23 +02:00

19 KiB
Raw Blame History

asus-zephyrus-g14

The hardware module for the ASUS ROG Zephyrus G14 laptop: its vendor daemon and platform profiles, the hybrid GPU's mode and driver options, suspend, the lid and power key, low battery, the backlights, the vendor keys and the touchpad (novox/hq research 027/03 Power management on the laptop, research 026/05, to-be 42 phase 3).

Why this name

A module is named after the hardware model, never the node (novox/hq ADR 0112; research 026/03: no flavors, no machine names). asus-zephyrus-g14 is the model family exactly as the firmware reports it (/sys/class/dmi/id/product_family = ROG Zephyrus G14). The module's code checks that value and its switcher does nothing on any other model, and zephyrus_check reports it.

A wider name such as asus-rog-laptop would promise what this module cannot keep. Its contents belong to this family: the vendor-key scan codes, the eDP panel beside an NVIDIA dGPU, and the NVIDIA D3 workaround. A second G14 is assigned the same module. Another ROG model gets its own.

Written against the GA403 (2024, Ryzen 8945HS, RTX 4070 Laptop, hybrid). Older G14 years have the same daemons and probably the same keys. Their GPU options are unverified.

What it owns

what how
package asusctl (asusd + client) the distribution's package (extra). The machine was found with a local build of 6.4.0. The host only asserts present, so the switch to 6.5.0 from extra happens at the next pacman -Syu (or pacman -S asusctl). zephyrus_check flags a local build
package upower, playerctl, xorg-xinput what the low-battery drop-in, the media keys and the touchpad key use
service asusd running (static unit: no boot state to declare), supergfxd running and enabled
archive /usr/local/lib/asus-zephyrus-g14/bin/ the module's scripts, from files/bin (below)
file /etc/modprobe.d/g14-nvidia-power.conf NVreg_DynamicPowerManagement=0x00 (runtime D3 off: the ACPI D-Notifier hang) and NVreg_PreserveVideoMemoryAllocations=1. The path is adopted (ADR 0182)
file /etc/modprobe.d/video-brightness-switch.conf video.brightness_switch_enabled=0, so the ACPI video driver does not also move a backlight on the keys. The file was on the machine and owned by nothing
file ×3 systemd-{suspend,hibernate,suspend-then-hibernate}.service.d/asus-zephyrus-g14-nvidia.conf Wants= the matching nvidia-* sleep units and nvidia-resume (see suspend units below)
file nvidia-powerd.service.d/asus-zephyrus-g14.conf ConditionKernelCommandLine=zephyrus.nvidia-powerd: Dynamic Boost runs only when the operator opts in at boot
file /etc/systemd/logind.conf.d/power.conf the power key and the lid suspend, on battery, on mains and docked. systemd-logind is reloaded, never restarted
file /etc/udev/rules.d/90-backlight.rules backlights writable by the video group. systemd-udevd is reloaded
file triggerhappy.service.d/asus-zephyrus-g14.conf thd … --user ${machine:account}: the triggers run as the operator's account (below)
file /etc/triggerhappy/triggers.d/asus-g14.conf the vendor keys: media (KEY_PROG1/3/4), panel brightness, touchpad (KEY_F21). The path is adopted, because two trigger files would fire every key twice
file /etc/UPower/UPower.conf.d/50-asus-zephyrus-g14.conf low battery at 15/10/7 %; at 7 % suspend, not power off. A drop-in over the package's own file
file /etc/X11/xorg.conf.d/30-asus-zephyrus-g14-touchpad.conf tap to click, natural scrolling, acceleration 0.15, as an X input class

What it does not own, on purpose:

  • /etc/asusd/*.ron belong to asusd, which rewrites them whenever a setting changes. RON is not a format the host writes into (ADR 0102 speaks JSON and marked blocks). Owning the file whole would repeat the predecessor's freeze: the measured file already differs from the one the predecessor shipped. The settings the module needs are set through asusd, by its code (below).
  • /etc/supergfxd.conf and /etc/modprobe.d/supergfxd.conf belong to supergfxd, which writes both.
  • The swap file, its unit and the swap partition are the machine's swap layout (research 027, question 3). They are not this module's, nor memory-pressure's.
  • The i3 fragments (~/.config/i3/config.d/10-asus.conf, 20-g14.conf) and the keyboard-backlight notifier they start belong to phase 2 (the i3 module).

Software outside the distribution (ADR 0205, research 027 question 1)

supergfxctl (5.2.7, from the asus-linux repository, which is no longer configured) and triggerhappy (AUR) are kept as found, and depended on. The module declares no package for either, because the host installs from the official repositories only. It declares their services (supergfxd running, triggerhappy running), so on a machine without them the host refuses the service by name: does not exist on this machine. The refusal is loud, never a silent pass. zephyrus_check names both as foreign.

This module does not choose between the options of research 027 question 1. Under the starting position (P2: the build machine builds AUR packages into a repository the mesh serves), both become package resources here, and a fresh G14 installs them. Until P2 exists, a fresh G14 is blocked on installing these two by hand. ADR 0205's vendored archive (P1) does not fit: supergfxctl is a daemon with a system-bus policy and udev rules, and triggerhappy is C.

A later option for the keys: the module's own Go code could read the vendor keys from evdev, which the operator's account may do through the input group. That would retire triggerhappy entirely. It is not done here, because it would put the keys behind the node's runtime, and the runtime restarts a bundle that dies only on its next call (below).

The long-running code: the profile switcher (ADR 0198)

The module's Go bundle serves the tools and runs the platform-profile switcher in the same process. The node's runtime launches the bundle at the runtime's start. It replaces the predecessor's auto-profile, a user unit that woke every five seconds, on battery too.

  • Policy (constants until settings exist, issue 168): battery → Quiet; mains → Balanced; mains with the CPU at or above 50 % for 3 samples of 10 s → Performance, back to Balanced after 3 samples at or below 20 %. Between the lines nothing moves (hysteresis). iowait counts as idle.
  • Woken by events, not a poll. The kernel's power-supply uevents (netlink, group 1) wake the switcher. Any account may listen on that group, and it needs no daemon, bus client or dependency; upower re-announces the same changes but would need a D-Bus client in the bundle. The CPU is sampled only on mains, every 10 s, because only there does the answer depend on it. On battery, a safety re-read every 5 minutes covers an event lost across a suspend. If the uevent socket cannot be opened, the switcher polls every 10 s and says so in zephyrus_profile_policy.
  • The battery decides the source. A battery that is discharging means battery, whatever any adapter says. The predecessor took any online file reading 1 as mains, and on this model the USB-C ports report online. Batteries of scope=Device (a mouse, a headset) are ignored.
  • It acts on a change of its decision, never to restore one. A profile someone chose by hand (the profile key, asusctl, zephyrus_profile) stays until the power source changes or the load crosses a line. The predecessor re-asserted its choice every five seconds, which made the profile key useless. Starting is not a decision: the runtime restarts the bundle on every push that changes one, and a push must not reset the operator's profile.
  • A hold. zephyrus_profile holds the profile it sets for 60 min (hold_minutes). A change of power source ends the hold.
  • One assertion at start: through asusctl, the charge limit (80 %) and asusd's own on-mains and on-battery profiles (Balanced, Quiet), each read first and set only if it differs. asusd's own switching on a change of power source then agrees with the switcher's. A limit set later with zephyrus_charge_limit stands until the bundle next starts. For a one-off full charge, use its oneshot.
  • Events: profile.switched (profile, from, reason, source), published through the runtime.

No root is involved. asusd's and supergfxd's bus policies admit the users and wheel groups, and the runtime runs as the operator's account. The one write that may escalate is the panel's backlight, when the udev rule has not run yet. It uses sudo -n and never prompts. Every command is bounded at 20 s.

Known limit. The runtime restarts a launched bundle that exits on its next tool call, not at once (mesh-tools launch.ts), so a crashed switcher stays down until a tool is called. ADR 0198 §1 says started again when it exits. The switcher recovers from a panic and reports it in zephyrus_profile_policy and zephyrus_check, but a crash of the process is the runtime's to restart.

The vendor keys and the scripts

triggerhappy opens the input devices as root, then drops to the operator's account with its groups (initgroups: input, video). The packaged unit already drops to nobody, and the module's drop-in names the account instead. The predecessor replaced the packaged unit with one that ran every trigger as root, then su-ed to a named person with a hard-coded uid and display, and sourced a file of secrets on the way (research 027 question 2). Now:

  • zephyrus-session CMD…: runs a command in the account's graphical session. It sets the account's own bus (/run/user/<uid>/bus) and finds the display from logind, or from a process of the account that has one. Nothing is sourced.
  • zephyrus-backlight + | - | N: the panel in 5 % steps, never below 1 %. The panel is the backlight under the eDP connector, because this model also registers nvidia_0, which moves nothing. The predecessor named amdgpu_bl1 literally.
  • zephyrus-notify ID TEXT: one replacing notification, through busctl (the service manager's client, so no libnotify).
  • zephyrus-touchpad reset | toggle: bound to the touchpad key (KEY_F21).

Media keys go to MPRIS through playerctl. The predecessor's Plex fallback needed a Plex token from the secrets file and is dropped until a module can be handed a secret (research 027 question 2).

The touchpad: an input class instead of a sleep hook

The predecessor re-ran xinput from /etc/systemd/system-sleep/ after every resume, as a named person on a guessed display, because settings made with xinput are lost when the device initialises again. An X input class is applied by X every time the device appears: at login, on hotplug and after a resume. So the cause is fixed, and the hook is gone. The class matches any touchpad on the machine, which is the model's, so it holds across G14 years whose touchpads differ. It takes effect at the next X start. zephyrus-touchpad reset stays as the manual form.

Suspend units without enabling them

nvidia-suspend, -hibernate, -suspend-then-hibernate and -resume are enabled with links in the sleep services' .wants directories. The mesh makes no links (ADR 0012). The host's service shape cannot declare them either: it may only say running or stopped, and running on a one-shot that last failed would start nvidia-sleep.sh suspend with the machine awake. So the module asks for them from the other side: a drop-in on each sleep service that Wants= them. The units' own Before=/After= order them. The found links stay and are harmless.

suspend-then-hibernate now also gets nvidia-suspend-then-hibernate, which the machine lacked.

The drop-ins take effect at the service manager's next daemon-reload. In the same apply, the restart of triggerhappy (whose drop-in changes) performs one.

Tools

tool r/a what
zephyrus_brightness r/a panel (percent or ±step, floor 1 %) and keyboard (off/low/med/high, 0-3, ±) through asusd
zephyrus_battery r charge, energy in Wh, health (full ÷ design), cycles (the firmware reports 0, and this is said), limit, watts, hours left
zephyrus_charge_limit r/a 20-100 through asusd; oneshot
zephyrus_gpu_mode r/a mode, supported modes, dGPU power, the pending mode and action; says that asusd switches the mode on every change of power source
zephyrus_profile r/a active, on-mains and on-battery profile, kernel platform profile; set with a hold
zephyrus_thermals r every hwmon temperature and fan, the hottest, the dGPU's temperature only when it is awake (nvidia-smi wakes a suspended GPU)
zephyrus_power_draw r battery flow, APU package power (PPT), dGPU draw when awake, power source and why
zephyrus_profile_policy r what the switcher would choose now and why: source, recent load against the thresholds, decision, hold, last switch, what woke it, what it asserted at start, and whether the predecessor's switcher still runs
zephyrus_fan_curves r asusd's curves per profile and fan
zephyrus_check r every expectation: model, packages (local or foreign), daemons, nvidia-powerd, sleep units, the NVIDIA options in force (/proc/driver/nvidia/params), charge limit, one authority each over the profile and the GPU mode, predecessor leftovers. It also lists what it did not check

profile is a candidate verb for a future node-power-profile seat (research 027/03). That seat has no record yet, so this is the module's own tool.

Found on the laptop, 2026-10-04 (read-only)

  • Two authorities over the GPU mode. asusd.ron has ac_command: "supergfxctl -m Hybrid" and bat_command: "supergfxctl -m Integrated", so asusd switches the GPU mode on every change of power source. supergfxd 5.2.7 cannot read logind's sessions (manager is an invalid variant, every boot), so a switch that needs a logout times out. zephyrus_check reports both. The fix is the operator's, in asusd's file: clear both commands, or update supergfxctl once it can be packaged.
  • brightness.conf did nothing. HandleBrightnessKey is not a logind key, and logind logs Unknown key … ignoring at every start. The module does not carry it. The brightness keys were always triggerhappy's, with the ACPI video switch off.
  • Two profile switchers would run at once until auto-profile is stopped (below).
  • asusctl is a local build (6.4.0, Unknown Packager) beside a foreign asusctl-debug.

When assigned to the laptop: what changes

  1. /usr/local/lib/asus-zephyrus-g14/ appears (four scripts).
  2. Written over found files (each original kept once by the host): g14-nvidia-power.conf and video-brightness-switch.conf (same options, so no change until the next boot either), logind.conf.d/power.conf (same keys; logind reloaded), 90-backlight.rules (same effect; udevd reloaded), triggers.d/asus-g14.conf (now the module's scripts).
  3. New: the three sleep drop-ins (behaviour gained: nvidia-suspend-then-hibernate), the nvidia-powerd drop-in (no effect while it is masked), the triggerhappy drop-in, the UPower drop-in (same values as today), and the touchpad input class (at the next X start).
  4. daemon-reload and a triggerhappy restart. thd now runs as the account and the keys run the module's scripts. upower restarts.
  5. Packages, asusd and supergfxd: already as declared, so nothing changes. asusctl stays the local 6.4.0 until the next upgrade.
  6. The node runtime restarts with the new bundle. The switcher asserts the limit (80, already) and asusd's profiles (Balanced and Quiet, already), so it sets nothing. It takes the current decision as applied and acts from the first event on.

Predecessor files this module makes redundant — the operator removes them once (ADR 0182)

On the laptop:

  1. systemctl --user disable --now auto-profile.service, then delete ~/.config/systemd/user/auto-profile.service and ~/scripts/auto-profile. Do this right after the push, or two switchers run at once.
  2. ~/scripts/asus-bright, ~/scripts/asusctl-kbd-bright, ~/scripts/xrandr-bright, ~/scripts/as-user, ~/scripts/media-control: no trigger and no i3 binding uses them any more.
  3. ~/scripts/xinput-reset-touchpad: still started and bound by ~/.config/i3/config.d/20-g14.conf. Point those two lines at /usr/local/lib/asus-zephyrus-g14/bin/zephyrus-touchpad reset, or wait for the i3 module (phase 2) to rewrite the fragment.
  4. /etc/systemd/system/triggerhappy.service: the predecessor's replacement of the packaged unit. The module's drop-in works over either, so delete it and systemctl daemon-reload to return to the packaged unit (Type=notify, socket).
  5. /etc/systemd/logind.conf.d/brightness.conf: the unknown key, which does nothing.
  6. /etc/systemd/system-sleep/xinput-reset-touchpad.sh: replaced by the input class.
  7. /etc/UPower/UPower.conf: the predecessor's replacement of the package's file. Its values are now the module's drop-in. Restore the package's copy (rm it, then pacman -S upower).
  8. Optional: systemctl disable nvidia-suspend nvidia-resume nvidia-hibernate (the drop-ins carry them now), /etc/asusd/*.ron-old and fan_curves.ron.bak, the foreign asusctl-debug package, and pacman -S asusctl for the distribution's build.

Stays the machine's: /swapfile and /etc/systemd/system/swapfile.swap (the swap layout), /etc/udev/rules.d/91-monitor-hotplug.rules (the display's, phase 2), and the i3 fragments.

On the desktop (the predecessor's G14 flavor reached it; part was removed on 2026-10-04): none of this module applies there. Still present and to be deleted: /etc/systemd/logind.conf.d/brightness.conf, /etc/systemd/system-sleep/xinput-reset-touchpad.sh, ~/scripts/xinput-reset-touchpad, ~/scripts/xrandr-bright and ~/.config/i3/scripts/kbd-brightness-notify.sh.

Tests

go test ./... in this directory. Every tool runs against a tree standing in for /sys, /proc and /etc, and an injected runner answering with what asusctl 6.4 and supergfxctl 5.2 said on the laptop. The tests cover:

  • the power-source rule;
  • battery arithmetic from charge_*;
  • the eDP panel choice;
  • brightness bounds;
  • the policy's sustain, relax and hysteresis, with iowait counted as idle;
  • the switcher: no act at start, one switch per change of source, a published event, boost from samples, holds, retry after failure, start-up assertions only where they differ, inert on another model;
  • the uevent filter;
  • the manifest: tools listed equal tools served, no machine named, triggers exist, every key runs a shipped executable script, every script passes bash -n.