-
Notifications
You must be signed in to change notification settings - Fork 0
Home
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, version0.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+coretempand nopwm*attributes, sofancontrol/pwmconfigcannot 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
LICENSEin the source repo (SPDXGPL-3.0-onlyinCargo.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,SIGKILLrescue in curve mode, 600-sampledoctor --comparetable, polkit-drivenhold, 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 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.
# 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)| 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) |
| 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 |
- File paths, sysfs attributes, JSON keys, CLI verbs/flags, and constant names
are in
codeformatting. - Temperatures are °C at the UI boundary and milli-°C (
MilliC) inside the policy; rpm are bareu32. - Every behavior names its source module and, where useful, the exact constant
(e.g.
VERIFY_TOLERANCE_RPM = 150inpolicy.rs). - Shell examples that touch hardware use
sudo/pkexecwhere required; pure reads and fixture examples do not need root.
- Home
- Installation
- CLI Reference
- Configuration
- Control Policy
- Supervisor and Run Modes
- Safety Model
- Hardware Interface
- Diagnostics (doctor)
- Plugin Surface (omafan)
- JSON Schemas
- systemd and Packaging
- Development and Testing
- Architecture and Internals
- Troubleshooting and FAQ
- Background and Verification
- afanctl-vs-mbpfan