feat: full fan control — all fans or individual fans #8

Merged
Pakobbix merged 1 commits from feat/multi-fan-control into main 2026-09-10 13:50:56 +00:00
Collaborator

The fan curve previously only controlled fan index 0; secondary fans (fan 2/3) stayed on driver control. The curve can now target all fans (new default) or any individual fan(s).

Backend

  • hal/fans.py: get_num_fans() via nvmlDeviceGetNumFans; get_fan_info() returns per-fan speeds; set_fan_speed() accepts a fan index list (None = all fans; all-fans mode is lenient toward driver-locked fans, explicit lists are strict); reset_fan() restores all fans to automatic.
  • server.py: per-GPU fan_targets state; the poller applies the curve to all target fans and logs write failures (once per distinct error); activation validates targets against the hardware (stale indices, e.g. a profile from a 2-fan GPU applied to a 1-fan GPU, fall back to all fans with a warning); POST /api/fans accepts fans, POST /api/fans/speed accepts a fan index, GET /api/fans returns num_fans/fans/fan_targets.
  • Persistence format is now {"curve": ..., "fans": ...}; legacy bare-curve entries in config.json migrate to "all fans" at startup.
  • Profiles save/apply fan_targets alongside fan_curve (old profiles load fine, defaulting to all fans).
  • MonitoringSample carries per-fan speeds for live gauges.

Frontend

  • Fans tab: All / Fan 1 / Fan 2 / … selector with live per-fan %; the selection is applied together with the curve (Discard works for either pending change).
  • Live monitor: per-fan gauges with sparklines for multi-fan GPUs.

Notes

  • On cards where fan 2 is a driver-mirrored slave (writes rejected with NOT_SUPPORTED), all-fans mode skips the locked write gracefully — fan 2 follows fan 1 via the driver anyway. Explicitly selecting a locked fan fails with a clear error.
  • Verified: pyright 0 errors, frontend build passes, HAL behavior tested against mocked NVML (2-fan GPU with driver-locked fan 2).
The fan curve previously only controlled fan index 0; secondary fans (fan 2/3) stayed on driver control. The curve can now target **all fans** (new default) or **any individual fan(s)**. ## Backend - `hal/fans.py`: `get_num_fans()` via `nvmlDeviceGetNumFans`; `get_fan_info()` returns per-fan speeds; `set_fan_speed()` accepts a fan index list (`None` = all fans; all-fans mode is lenient toward driver-locked fans, explicit lists are strict); `reset_fan()` restores all fans to automatic. - `server.py`: per-GPU `fan_targets` state; the poller applies the curve to all target fans and logs write failures (once per distinct error); activation validates targets against the hardware (stale indices, e.g. a profile from a 2-fan GPU applied to a 1-fan GPU, fall back to all fans with a warning); `POST /api/fans` accepts `fans`, `POST /api/fans/speed` accepts a `fan` index, `GET /api/fans` returns `num_fans`/`fans`/`fan_targets`. - Persistence format is now `{"curve": ..., "fans": ...}`; legacy bare-curve entries in `config.json` migrate to "all fans" at startup. - Profiles save/apply `fan_targets` alongside `fan_curve` (old profiles load fine, defaulting to all fans). - `MonitoringSample` carries per-fan speeds for live gauges. ## Frontend - **Fans tab**: `All` / `Fan 1` / `Fan 2` / … selector with live per-fan %; the selection is applied together with the curve (Discard works for either pending change). - **Live monitor**: per-fan gauges with sparklines for multi-fan GPUs. ## Notes - On cards where fan 2 is a driver-mirrored slave (writes rejected with `NOT_SUPPORTED`), all-fans mode skips the locked write gracefully — fan 2 follows fan 1 via the driver anyway. Explicitly selecting a locked fan fails with a clear error. - Verified: pyright 0 errors, frontend build passes, HAL behavior tested against mocked NVML (2-fan GPU with driver-locked fan 2).
ARIA added 1 commit 2026-09-10 13:48:32 +00:00
The fan curve previously only controlled fan index 0; secondary fans
stayed on driver control. The curve can now target all fans (new
default) or any individual fan(s).

Backend:
- hal/fans.py: get_num_fans() via nvmlDeviceGetNumFans; get_fan_info()
  returns per-fan speeds; set_fan_speed() accepts a fan index list
  (None = all fans; all-fans mode is lenient toward driver-locked
  fans, explicit lists are strict); reset_fan() restores all fans.
- server.py: per-GPU fan_targets state; the poller applies the curve to
  all target fans and logs write failures (once per distinct error);
  activation validates targets against the hardware (stale indices fall
  back to all fans); POST /api/fans accepts fans, POST /api/fans/speed
  accepts a fan index, GET /api/fans returns num_fans/fans/fan_targets.
- Persistence format is now {"curve": ..., "fans": ...}; legacy
  bare-curve entries migrate to "all fans" at startup.
- Profiles save/apply fan_targets alongside fan_curve.
- MonitoringSample carries per-fan speeds for live gauges.

Frontend:
- Fans tab: All / Fan 1 / Fan 2 / ... selector with live per-fan %;
  the selection is applied together with the curve.
- Live monitor: per-fan gauges with sparklines for multi-fan GPUs.
Collaborator

✅ No issues found — changes look consistent with the stated intent. Ready to be merged.

✅ No issues found — changes look consistent with the stated intent. Ready to be merged.
Pakobbix merged commit 8956fc9d7b into main 2026-09-10 13:50:56 +00:00
Pakobbix deleted branch feat/multi-fan-control 2026-09-10 13:50:56 +00:00
Sign in to join this conversation.