Skip to content

Hardware Interface

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

Hardware Interface

All sysfs path knowledge lives in one module (src/smc.rs); nothing outside it contains a path string (grep-gated). Two backends implement the frozen Smc trait: SysfsSmc (real) and MockSmc (scriptable faults for tests).

Sysfs layout (measured on the A1708)

Sysfs root: /sys/devices/platform/applesmc.768/ — fan files sit directly there, not under hwmon.

File SMC key Meaning Access
fan1_label Exhaust (single-fan scope) RO
fan1_input F0Ac Actual rpm (tachometer), read-only RO
fan1_output F0Tg Target (command register), read-write RW
fan1_manual bit 0 of FS! 0 = firmware curve, 1 = our command RW
fan1_min / fan1_max F0Mn / F0Mx Hardware range: 1200 / 7200 rpm RO
fan1_safe Present, reads empty, semantics unknown — ignored (Q3) RO

Sensors: coretemp → dynamic hwmon index (hwmon4 on the verified machine): Package id 0, Core 0, Core 1; temp*_crit (Tjmax) = 100 °C. The applesmc hwmon dir (hwmon3) exists but is an attribute-less husk — upstream applesmc is mid-conversion to standard hwmon attrs. applesmc also exposes 33 raw SMC temp keys (temp1..33, labels like TA0V, TB0T) — out of scope for control; coretemp is the trusted set.

Measured dynamics (2026-09-14, calibrates the code and the mocks): the SMC adopts a written target within ≤ 1 s on a ~1 s internal tick; a full swing takes ~5 s (6688 → 2664 → 1632 → … → 2001 rpm); steady-state jitter ±20 rpm (worst sampled second-to-second delta 26 rpm). fan1_output mirrors the SMC's own target while in AUTO, so a write is always preceded by a verified manual-mode write.

Discovery (never hardcode hwmonN)

Fan discovery: devices/platform/applesmc.768 → if fan1_input exists there, use it (classic layout); else walk hwmon/ subdirs (sorted) and use the first one containing fan1_input (post-conversion layout).

Coretemp discovery: walk /sys/devices/platform/coretemp.0/hwmon/hwmon*/, scan tempN_input files (N parsed as u32, sorted), read optional tempN_label (fallback tempN), in discovery order. No hardcoded hwmonN anywhere (Q5).

Read-back-verify invariant (module invariant, not a feature)

Every state-changing write (fan1_output, fan1_manual) is followed by a read-back of the attribute just written, within tolerance; logical state (last_written, manual_armed) commits only on verified read-back; failed verification re-issues within a settle window, then surfaces VerifyFailed upward (which triggers fail-toward-AUTO).

  • RULING F16: fan1_output echo is the verification source (tolerance WRITE_ECHO_TOLERANCE_RPM = 50); fan1_input is the tachometer and is never a write-verification source — a fan still spinning down is not a failed write.
  • RULING F19 settle windows: the echo is verified inside ECHO_SETTLE_MS = 1500 (sampled 10 × 150 ms; MODE_SETTLE_MS = 1000 for set_mode, exact 0/1 match) because the SMC adopts F0Tg asynchronously on a ~1 s tick. One write per window, no microsecond retries; at most WRITE_RETRY_MAX = 1 re-issue after a dead window, then VerifyFailed.
  • All rpm writes clamped to [fan1_min, fan1_max] read from hardware at discovery; hw_min > hw_max at open is InvalidValue.
  • Sensor readings < 0 °C or > 120 °C (also empty/unparseable) are rejected as failed reads (outlier rejection).
  • --sysfs-root <dir> redirects all sysfs access (the entire test suite runs against fixtures this way).

panic_fd (L2 handle)

SysfsSmc pre-opens O_WRONLY on fan1_manual at discovery and exposes it via Smc::panic_fd() -> Option<i32> (None for MockSmc and for read-only trees, with a debug log). safety::install_death_path(fd) consumes it.

sd_notify (hand-rolled, no systemd crate)

src/notify.rs: sd_ready() (READY=1), sd_watchdog() (WATCHDOG=1), sd_status(msg) (STATUS=…) via unix datagram to $NOTIFY_SOCKET (filesystem or @-abstract path, newline-terminated). Unset socket → no-op false; send failures → false, never a panic.

Layout-change coupling (deliberate)

ReadWritePaths pins today's platform path, and doctor detects a layout change (fan attrs appearing under the hwmon husk → "conversion in flight") and tells the user to update the unit. A kernel bump changing layout = a one-module fix, detected by doctor (Q4).

Clone this wiki locally