Architecture: centralize fan-control lifecycle and hardware state transitions
- Dominant language
- Swift
- Stars
- 4
- Forks
- 1
- PR merge metrics
- No merged PRs in 30d
Description
## Context
While investigating MAX mode on an M1 Max, the immediate backend issue was that M1–M4 fan control needs the global `Ftst` gate before per-fan mode/target writes are effective.
Reviewing the surrounding lifecycle exposed a broader concern that I intentionally did **not** include in the focused backend PR: desired profile state, applied SMC state, power transitions, and daemon auto-suspend are currently coordinated by separate components.
## Current risk areas
- `ProfileStore` can persist System mode independently of a confirmed hardware release.
- Profile selection/deletion, polling, sleep/wake, startup/shutdown, and auto-suspend do not share one transition owner.
- `SMCController.resetAllFans()` is fail-fast, so one failed reset can prevent later fans from being attempted.
- Apple Silicon `Ftst` is global, while the public control API is per-fan.
- Safety paths sometimes treat profile state as proof of applied hardware state.
- Sleep is a multi-message protocol (`CanSystemSleep`, `WillSleep`, `WillNotSleep`, wake), not a single callback.
These can produce partial/manual/unknown fan states even when the persisted profile says System.
## Suggested direction
Introduce a daemon-level `FanControlCoordinator` (an actor or one serial executor) as the only mutation gateway for:
- profile activation and active-profile deletion;
- periodic target application;
- startup and shutdown recovery;
- sleep cancellation, sleep entry, and wake;
- daemon auto-suspend eligibility.
The coordinator should track applied state explicitly, for example:
- `automatic`
- `manual(profileID)`
- `releasing(reason)`
- `sleepPending`
- `sleeping`
- `faulted/unknown`
### Transaction ordering
For a transition to System:
1. quiesce new fan writes;
2. record a pending release;
3. release hardware control and attempt every fan where control is per-fan;
4. only after success persist/report System and permit auto-suspend;
5. on failure remain active in a visible fault/retry state rather than exiting.
For Apple Silicon, the backend should expose whole-operation semantics such as `applyFanTargets` and `releaseAllControl`, so global `Ftst` ownership is not composed accidentally through repeated per-fan calls.
## Acceptance criteria
- System is reported and eligible for auto-suspend only after hardware release succeeds.
- A failed release cannot later trigger daemon exit merely because the persisted profile is System.
- Deleting the active profile follows the same System transition.
- Reset attempts continue across independently controlled fans and aggregate failures.
- No target writes occur while releasing, sleeping, or in unknown state.
- `CanSystemSleep -> WillNotSleep` resumes the prior desired state.
- Restart recovers an interrupted release before applying another manual profile.
- Fault-injection tests cover each fan failing, partial apply/reset, profile persistence failure, sleep cancellation, and auto-suspend during a failed release.
This is deliberately proposed as a separate architecture change rather than expanding the focused M1–M4 `Ftst` fix.
Contributor guide
Research direction
Start by tracing lifecycle mutations through ProfileStore and SMCController.resetAllFans(), then map profile changes, sleep/wake handling, startup/shutdown, and auto-suspend. Use the proposed FanControlCoordinator and listed acceptance criteria as the boundary for the architecture. Done means transitions are serialized, failed releases remain visible and retryable, and fault-injection tests cover the listed partial and sleep scenarios.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- macos, swift
- Domain
- desktop, operating-systems
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100