# 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//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`.