Skip to content

feat: add YAML-configured hwmon fan control - #54

Open
sknowledge1 wants to merge 15 commits into
ShadowBlip:mainfrom
sknowledge1:feat/generic-fan-control
Open

sknowledge1 wants to merge 15 commits into
ShadowBlip:mainfrom
sknowledge1:feat/generic-fan-control

Conversation

@sknowledge1

@sknowledge1 sknowledge1 commented Sep 22, 2026 •

Copy link
Copy Markdown

PowerStation currently has no standard fan interface, so handheld fan controls
require device-specific daemons and UI integrations. This adds a generic,
YAML-configured hwmon subsystem that can serve native clients over D-Bus while
keeping hardware selection and safety policy reviewable as platform data.

Result

  • Match one reviewed platform through exact DMI alternatives before opening a
    writable fan attribute.
  • Discover fan and temperature paths by hwmon name, parent driver, channel, and
    sensor label instead of hwmonN numbering.
  • Support one-second software curves and complete contiguous hardware auto-point
    curves, with verified writes and driver-specific mode values.
  • Expose /org/shadowblip/Performance/Fan and atomic Automatic, Manual, Curve,
    and named-preset operations on org.shadowblip.Fan.Device objects.
  • Persist global fan state in PowerStation and migrate the legacy AYANEO Quiet
    selection once.
  • Add AYANEO 3 as the first platform entry, matching product or board identity
    across CPU variants and using ayaneo_ec channel 1 with k10temp/Tctl.

Safety and lifecycle

  • Ambiguous DMI/hwmon matches, malformed telemetry, incomplete auto points,
    ownership changes, stalls, and failed readback all fall back to verified
    firmware automatic mode.
  • PrepareForSleep uses a logind delay inhibitor. PowerStation verifies automatic
    mode before releasing sleep, rediscovers hwmon paths on resume, and restores
    saved control only after telemetry validation.
  • Loss or replacement of logind's D-Bus owner makes the daemon unhealthy.
  • Fan database or controller initialization errors fail the supervised service
    instead of being hidden behind an empty fan collection.
  • The systemd watchdog follows controller progress. ExecStopPost uses a narrow
    DMI/name/driver/channel recovery path that can restore automatic mode even when
    temperature or RPM telemetry is unavailable.
  • A failed service run persists Automatic so restart cannot repeat unsafe custom
    control.

The AYANEO entry permits 0% while cool, applies a 10% running floor, stops at or
below 42 C, restarts above 45 C, and includes the seven-point Quiet preset. The
95 C full-speed and 98 C firmware-handoff values are proposed for maintainer
review; they are not presented as already approved.

Validation

  • cargo fmt --check
  • cargo test --locked --all: 28 passed
  • warning-strict Clippy across all targets/features, with only the two lint
    classes already present in the upstream base allowed
  • cargo build --locked --all
  • clean git archive rebuild at commit
    e6baca4e9ef4a04df2884b8803a6ca6deb167ec0
  • fake-sysfs and private-D-Bus coverage for DMI precedence, renumbering,
    conflicting devices, malformed telemetry, persistence, both curve backends,
    interpolation, zero-duty hysteresis, stalls, readback/sensor faults, 95/98 C
    boundaries, and the D-Bus contract
  • transient-systemd lifecycle coverage for clean stop, restart, SIGKILL,
    sensor loss, SIGSTOP/watchdog, failed sleep handoff, logind owner loss, and
    ExecStopPost recovery
  • controlled real-hardware handoff on the connected Ryzen 7 8840U AYANEO 3:
    one fan object, software-curve backend, Quiet and 20% manual roundtrips,
    verified SIGKILL/watchdog recovery, and verified automatic mode on clean stop

During the final-commit physical test, Quiet at 49.625 C applied the 10% running
floor (pwm1=25, 1061 RPM), and 20% manual control read back as pwm1=51 at
1502 RPM.
Both a SIGKILL and a five-second systemd watchdog timeout ran ExecStopPost,
persisted Automatic after the failed run, and restarted in verified firmware
automatic mode. The review service was then removed; stock PowerStation and the
v0.5.1 rollback service are active again with pwm1_enable=2. No deliberate
high-temperature test was performed. Ryzen AI 9 HX 370 support remains DMI/schema
coverage until tested on that hardware.

Addresses #29. The dependent native client is OpenGamepadUI PR #550:
ShadowBlip/OpenGamepadUI#550

semantic-release-bot and others added 11 commits September 20, 2026 14:28
## [0.8.2](ShadowBlip/PowerStation@v0.8.1...v0.8.2) (2026-08-27)

### Bug Fixes

* add aimax+395 ([93c5b59](ShadowBlip@93c5b59))
* Add ONEXPLAYER 3 TDP support ([0813052](ShadowBlip@0813052))
* **cpu:** don't panic when SMT cannot be enabled ([ad0c115](ShadowBlip@ad0c115))
* Update rust to 1.88 ([341c9a9](ShadowBlip@341c9a9))
The PCI class code was only ever a guess at whether a GPU is integrated.
It says whether the GPU is the boot VGA device, not where it lives: an
AMD APU enumerates as 030000 when its iGPU is the boot VGA device and as
038000 when it is not, so any APU that is not the boot VGA device was
treated as discrete and never got a TDP interface.

Ask the driver instead. amdgpu reports AMDGPU_IDS_FLAGS_FUSION for APUs,
and i915 and xe both report whether the GPU has memory of its own. The
class code is kept only as a fallback, for drivers that answer none of
these.

Fixes ShadowBlip#28
## [0.8.3](ShadowBlip/PowerStation@v0.8.2...v0.8.3) (2026-08-28)

### Bug Fixes

* Add all missing Strix Point APUs to database ([40c96dc](ShadowBlip@40c96dc))
* Add Ryzen Z2 Extreme ([319e540](ShadowBlip@319e540))
* **gpu:** ask the driver whether a GPU is integrated ([847d508](ShadowBlip@847d508)), closes [ShadowBlip#28](ShadowBlip#28)
* Update libryzenadj-rs to v0.19.0 ([740967a](ShadowBlip@740967a))
Handles cases like AMD Ryzen AI Max+ 395 vs AMD RYZEN AI MAX+ 395
## [0.8.4](ShadowBlip/PowerStation@v0.8.3...v0.8.4) (2026-09-10)

### Bug Fixes

* Use case insensitive model_name check ([0204f95](ShadowBlip@0204f95))
## [0.8.2](ShadowBlip/PowerStation@v0.8.1...v0.8.2) (2026-09-21)

### Bug Fixes

* add aimax+395 ([7f7f936](ShadowBlip@7f7f936))
* Add all missing Strix Point APUs to database ([c40f718](ShadowBlip@c40f718))
* Add ONEXPLAYER 3 TDP support ([0813052](ShadowBlip@0813052))
* Add Ryzen Z2 Extreme ([76d363b](ShadowBlip@76d363b))
* **cpu:** don't panic when SMT cannot be enabled ([a4b0650](ShadowBlip@a4b0650))
* **gpu:** ask the driver whether a GPU is integrated ([ecf1c54](ShadowBlip@ecf1c54)), closes [ShadowBlip#28](ShadowBlip#28)
* **TDP:** Add DMI overrides for Xbox Ally/X, OXP APEX, GPD Win5 ([4140275](ShadowBlip@4140275))
* Update libryzenadj-rs to v0.19.0 ([4bfcfea](ShadowBlip@4bfcfea))
* Update rust to 1.88 ([aa376ec](ShadowBlip@aa376ec))
* Use case insensitive model_name check ([57f1e3a](ShadowBlip@57f1e3a))
@pastaq

pastaq commented Oct 10, 2026

Copy link
Copy Markdown
Contributor

Please rebase on latest main

@pastaq

pastaq commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

This can't be merged or reviewed as is. The history is all messed up.

I'd suggest you delete your local branch, check out an pull main, remake the branch, cherry pick your commits, and then force push

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.

5 participants