Skip to content

Configuration

Prakhar Yadav edited this page Sep 16, 2026 · 1 revision

Configuration

/etc/afanctl/afanctl.toml is typed TOML with exactly five honoured keys (src/config.rs). Unknown keys produce a warning (not an error) and are ignored. Every error names the key and the fix.

The file

Pristine copy: packaging/afanctl.toml.default → installed to both /etc/afanctl/afanctl.toml (first install) and /usr/share/afanctl/afanctl.toml.default. Pacman backup= protects user edits across upgrades.

[thresholds]          # °C, integers
high = 66             # ramp starts here; low is derived: high - 3
max  = 86             # full speed from here; guard: max <= 95 (Tjmax 100 - 5)

[curve]
min_rpm = 1200        # clamped to >= fan1_min at load
max_rpm = 6200        # clamped to <= fan1_max at load

[poll]
interval_s = 1        # 1..=12; must stay well below the unit's WatchdogSec=15
                      # (two watchdog pings per poll — start and end of each
                      # one; starving it crash-loops)

The five keys

Dotted key Type Default Meaning
thresholds.high integer °C 66 Ramp starts here. low is derived: high − 3 (= 63 by default).
thresholds.max integer °C 86 Full speed from here. Guard: max ≤ 95 (Tjmax 100 − 5).
curve.min_rpm u32 rpm 1200 Curve floor. Clamped up to fan1_min at load.
curve.max_rpm u32 rpm 6200 Curve ceiling (deliberately below the 7200 hardware max). Clamped down to fan1_max at load.
poll.interval_s u64 s 1 Poll period. Range 1..=12 (see watchdog coupling below).

Safety tunables are compiled-in constants, not config: verify tolerance, sensor-loss polls, overshoot polls, slew rate, write-fail fallback, retry counts, settle windows (full table in Control Policy and Hardware Interface).

Semantics

  • Missing file → falls back to the built-in defaults above (provenance source: "defaults" in status --json).
  • Present file → must contain all five keys. A missing key is refused (not defaulted) with the key name and the fix — a typo can never silently select a different curve. The daemon exits nonzero.
  • Present-but-broken → refuse to start (exit nonzero, loud error naming the key and the fix). Never runs on defaults after a bad edit.
  • Unknown keys → warning strings, sorted, e.g. unknown config key 'thresholds.low' ignored; the file still loads (forward-compatible).
  • Load-time RPM clamping against hardware: min_rpm.max(fan1_min), max_rpm.min(fan1_max). If the band empties after clamping (clamped_min >= clamped_max), the config is refused under curve.min_rpm with key+fix.

Validation table

Rule Rejects Error key reported
thresholds.max outside 0..=95 max > 95 or max < 0 thresholds.max
thresholds.high outside 0..=95 any value outside the band thresholds.high
high >= max equal (the H3 landmine) or inverted thresholds thresholds.high
curve.min_rpm >= curve.max_rpm empty rpm band curve.min_rpm
poll.interval_s < 1 0 poll.interval_s
poll.interval_s > 12 13+ (would starve the watchdog) poll.interval_s
Non-integer value float, string, bool where an integer belongs respective key
Missing key / missing section (file present) absent key or absent [section] respective key / section name
Band empties after hw clamp clamped_min >= clamped_max curve.min_rpm

Temperatures that "can't reproduce a real temperature" (outside 0..=95 °C) are a config error by construction. high == max (mbpfan's UB trigger) is rejected outright.

Watchdog coupling (interval_s ≤ 12)

The bound is the compiled-in constant config::MAX_INTERVAL_S = 12, mirroring packaging/afanctl.service (WATCHDOG_UNIT_SEC = 15, WatchdogSec=15). Rationale: the watchdog pings ride the poll — two per poll, at its start and end — and the F19 settle windows block up to ≈2.7 s inside a failing write; a poll period without ≥3 s of watchdog headroom crash-loops the daemon (hang → SIGABRT → L2 → restart, noisily but safely). 12 is legal, 14 is rejected (pinned by test).

Provenance and observability

  • status / status --json always show where the config came from: source: /etc/afanctl/afanctl.toml (or your --config path) vs source: "defaults".
  • The daemon's startup evidence INFO line in the journal names the effective mode, L2 armed state, watchdog notify path, state-file path, config source path, and hardware band.
  • doctor's config validation check reports valid / invalid (key+fix) / unknown-key WARNs; an invalid config FAILs the whole doctor run (exit 1).

Tuning guidance (from the 600-sample hardware comparison)

The empirical verdict: at t_eff ≤ 64 °C ours ≈ the SMC within ±20 rpm (both ~1190–1220); during short coretemp spikes (66–85 °C) ours ramps toward the linear target at the 750 rpm/poll slew while the SMC stayed near 1.2k rpm (its own sensors did not register those spikes). Ours is more responsive to coretemp and verifiable, not quieter. Knobs if quiet is the goal: raise [thresholds].high (start ramping later) and/or lower [curve].max_rpm. Re-verify with doctor --compare N after any change, then sudo systemctl restart afanctl (no in-process reload — restart is the reload, by design).

Clone this wiki locally