Summary
When the daemon applies a fan curve target, the post-write read-back verification of F0Tg does an exact byte comparison. The SMC quantizes the written float (low mantissa bits are zeroed), so whenever the interpolated target has precision beyond what the SMC stores, verification fails — and Fan policy evaluation failed is logged on every policy cycle (roughly once per second under my curve).
The write actually lands: fan actual RPM tracks the intended target fine. This is a false positive that floods the unified log with Error-level entries.
Environment
- smctl 0.1.8 (Homebrew, built from source)
- MacBook Pro (M5 Pro), macOS 26.4.1 (25E253)
- Custom curve profile applied via
smctl fan profile <name> (daemon mode manual)
Log sample
smctld: [one.leaper.smctl:daemon] Fan policy evaluation failed: SMC write verification failed for F0Tg: expected 00566f45, read back 00506f45
smctld: [one.leaper.smctl:daemon] Fan policy evaluation failed: SMC write verification failed for F0Tg: expected 00147545, read back 00107545
smctld: [one.leaper.smctl:daemon] Fan policy evaluation failed: SMC write verification failed for F0Tg: expected c0f58545, read back 00f08545
Decoding the bytes (little-endian IEEE-754 float)
| written |
value (RPM) |
read back |
value (RPM) |
delta |
00 56 6f 45 → 0x456F5600 |
3829.5 |
00 50 6f 45 → 0x456F5000 |
3829.125 |
0.375 |
00 14 75 45 → 0x45751400 |
3921.25 |
00 10 75 45 → 0x45751000 |
3921.0 |
0.25 |
c0 f5 85 45 → 0x4585F5C0 |
4286.72 |
00 f0 85 45 → 0x4585F000 |
4286.0 |
0.72 |
In every case the read-back is the written value with the low ~12 mantissa bits zeroed — the SMC stores the target at reduced precision. The delta is always < 1 RPM, far below anything a fan can physically resolve.
Observed impact
- Unified log gets one Error entry per policy cycle while a curve is active (~17 entries in 2 minutes on my machine).
- Harder to spot real failures: during a genuine incident (safety-ceiling latch), these false positives were interleaved with the real error and initially looked like the cause.
- Fan control itself works —
smctl fan status shows actual converging to target.
Suggested fix
Compare with a tolerance instead of exact bytes, e.g. accept when abs(readBack - written) < 1.0 RPM — or quantize the written value the same way before comparing (mask the low mantissa bits). Alternatively, downgrade this specific mismatch to debug-level once the read-back is within tolerance.
Happy to test a fix on this machine.
Summary
When the daemon applies a fan curve target, the post-write read-back verification of
F0Tgdoes an exact byte comparison. The SMC quantizes the written float (low mantissa bits are zeroed), so whenever the interpolated target has precision beyond what the SMC stores, verification fails — andFan policy evaluation failedis logged on every policy cycle (roughly once per second under my curve).The write actually lands: fan
actualRPM tracks the intended target fine. This is a false positive that floods the unified log with Error-level entries.Environment
smctl fan profile <name>(daemon modemanual)Log sample
Decoding the bytes (little-endian IEEE-754 float)
00 56 6f 45→ 0x456F560000 50 6f 45→ 0x456F500000 14 75 45→ 0x4575140000 10 75 45→ 0x45751000c0 f5 85 45→ 0x4585F5C000 f0 85 45→ 0x4585F000In every case the read-back is the written value with the low ~12 mantissa bits zeroed — the SMC stores the target at reduced precision. The delta is always < 1 RPM, far below anything a fan can physically resolve.
Observed impact
smctl fan statusshowsactualconverging totarget.Suggested fix
Compare with a tolerance instead of exact bytes, e.g. accept when
abs(readBack - written) < 1.0RPM — or quantize the written value the same way before comparing (mask the low mantissa bits). Alternatively, downgrade this specific mismatch to debug-level once the read-back is within tolerance.Happy to test a fix on this machine.