Skip to content
Prakhar Yadav edited this page Sep 16, 2026 · 3 revisions

afanctl — applesmc fan supervisor for the pre-T2 Intel Mac (A1708)

afanctl is a small, safety-first fan supervisor for the MacBook Pro 2017 A1708 (MacBookPro14,1, pre-T2 Intel) running Linux. It reads coretemp sensors, drives the single exhaust fan through the applesmc driver's fan1_manual / fan1_output files, and — when anything goes wrong — gets out of the way and lets the SMC firmware run the fan again (fail toward the firmware).

  • Binary / crate / AUR package: afanctl, version 0.1.0
  • Source: https://github.com/yadav-prakhar/afanctl
  • This wiki's repo: https://github.com/yadav-prakhar/afanctl.wiki.git
  • Scope: one fan, one machine class (pre-T2 Intel Macs with applesmc + coretemp and no pwm* attributes, so fancontrol/pwmconfig cannot work).
  • Language: Rust, edition 2021, rustc 1.98.1, single-threaded, no async, no tokio. Release profile: strip = true, lto = true (binary < 2 MB, RSS < 5 MB budget).
  • License: GPL-3.0-only — see LICENSE in the source repo (SPDX GPL-3.0-only in Cargo.toml / PKGBUILD).
  • Status: shipped and verified on hardware (MacBookPro14,1, kernel 7.2.3-arch1-3, systemd). The full acceptance gate (PRD §9: doctor, 2-second roundtrip write test, deliberate-panic death-path test, 1 h+ observe soak at 0.05 % CPU / 2.4 MB peak, SIGKILL rescue in curve mode, 600-sample doctor --compare table, polkit-driven hold, reboot test) all PASSED.

Start here, then follow the sidebar. Every page below documents one aspect exhaustively — nothing lives only in chat logs or commit messages.

The one-paragraph mental model

The daemon polls once per interval_s (default 1 s). Each poll: re-read the plugin command file → read sensors → run the pure control policy (t_eff = hottest sensor, absolute curve, slew limiter, overshoot guard) → act through the verified sysfs write path → re-assert mode/tracking (L1) → publish state.json → ping the systemd watchdog twice (start and end of the poll). Any write failure, sensor loss, crash, hang, SIGKILL, or bad config ends with the SMC firmware back in charge, loudly logged. The CLI never writes sysfs directly (except doctor --roundtrip's supervised 2 s test); mode changes flow through /run/afanctl/cmd.json, and status --json is the render feed.

Quick start

# build the package from a checkout (does not install)
cd packaging && makepkg

# build + install (requires root; part of the supervised hardware gate)
cd packaging && makepkg -si

# the unit installs DISABLED and starts in observe mode — nothing changes yet
sudo systemctl enable --now afanctl
systemctl status afanctl

# everyday commands
afanctl status                 # temps, t_eff, mode, fan, config provenance
sudo afanctl doctor            # 10 checks; exit 1 on any FAIL (run as root!)
afanctl once --dry-run         # one control decision, writes nothing
sudo afanctl curve             # opt into continuous control
sudo afanctl observe           # release the fan back to firmware
sudo afanctl hold 3000         # hold a fixed rpm (plugin presets/slider)

Wiki map (every aspect, no gaps)

Page What it covers
Installation Packaging, makepkg -si, installed paths, enable/start, reinstall-then-restart discipline
CLI Reference Every verb, every flag, exit codes 0/1/2/101, env var, real output examples
Configuration The exactly-five keys, defaults, validation table, unknown-key warnings, provenance, watchdog coupling
Control Policy Curve formula, hysteresis, slew limiter, overshoot guard, sensor loss, hold semantics, every constant
Supervisor and Run Modes observe/curve/hold, exact poll order, startup reconcile, cmd/state files, polls vs watchdog_pings
Safety Model L1 verify/re-assert + stall detector + off-target WARNs, L2 death path, L3 systemd, sensor loss, honest AUTO
Hardware Interface sysfs paths, SMC key mapping (F0Ac/F0Tg/FS!/F0Mn/F0Mx), discovery walks, settle-window verify, clamping
Diagnostics (doctor) All 10 checks, root-required rule, --roundtrip 2 s procedure, --compare stats + verdicts
Plugin Surface (omafan) cmd.json channel, freshness gate, preset mapping, polkit allowlist, monitor_only rendering rules
JSON Schemas afanctl.status.v1, afanctl.cmd.v1, afanctl.state.v1, afanctl.doctor.v1 — every field
systemd and Packaging Unit file annotated, PKGBUILD, polkit rule annotated, backup= semantics
Development and Testing Quality gates, test inventory, fixture trees, MockSmc faults, hw feature guard
Architecture and Internals Crate layout, module contracts, public signatures, error model, MilliC, conventions
Troubleshooting and FAQ Every failure mode, every doctor line, recovery recipes, FAQ
Background and Verification Why it exists (mbpfan defects), evidence base, hardware-gate results, performance, LOC note, deferred work
afanctl-vs-mbpfan How afanctl differs from mbpfan and the benefits it offers (algorithm, safety, config, ops)

Key facts at a glance

Fact Value
Fan 1 (fan1_label = Exhaust), fan1_min 1200 / fan1_max 7200 rpm
fan1_manual 0 = firmware curve, 1 = our command; 0 restores the safety net
Sensors coretemp hwmon walk: Package id 0, Core 0, Core 1; Tjmax 100 °C
Config path /etc/afanctl/afanctl.toml (missing → built-in defaults; present → all 5 keys required)
Runtime dir /run/afanctl (cmd.json, state.json); override via AFANCTL_RUNTIME_DIR (tests only)
Sysfs root /sys (/sys/devices/platform/applesmc.768/); override via --sysfs-root (tests/fixtures only)
Daemon default observe (writes nothing); /run is tmpfs so reboot always returns to observe
t_eff max over valid sensors (never the average)
Defaults high = 66, max = 86, low = high − 3 = 63, min_rpm = 1200, max_rpm = 6200, interval_s = 1 (max 12)
Exit codes 0 success · 1 runtime failure · 2 usage error · 101 deliberate selftest-panic
Authoritative docs PRD.md (requirements) · DESIGN.md (binding contracts) · PLAN.md (build plan) · HANDOFF.md (operator handoff) · README.md (user entry) in the source repo

Conventions used across this wiki

  • File paths, sysfs attributes, JSON keys, CLI verbs/flags, and constant names are in code formatting.
  • Temperatures are °C at the UI boundary and milli-°C (MilliC) inside the policy; rpm are bare u32.
  • Every behavior names its source module and, where useful, the exact constant (e.g. VERIFY_TOLERANCE_RPM = 150 in policy.rs).
  • Shell examples that touch hardware use sudo/pkexec where required; pure reads and fixture examples do not need root.

Clone this wiki locally