Skip to content

Commit f6eb5d2

Browse files
Producer GuyProducer Guy
authored andcommitted
Profile redesign v2: per-profile curve shapes, 100ms tick, attack-dog Max
Thermal logs showed the uniform profile design failed — sustained trigger prevented Max from engaging fans while temps hit 98°C in 6 seconds. - Added CurveShape enum (easeIn, linear, easeOut, sCurve) with per-profile curve shape application in targetPercent() - Per-profile ramp rates, sustained triggers, and instantEngage flag - Thermal tick 2000ms → 100ms (matching Apple's thermalmonitord) - Dual-cadence: 100ms fan control, 2s process capture + anomaly detection - Max: instant 100% at 65°C after 5s sustained, S-curve ramp-down - Balanced: ease-in curve (quiet low temps), 8s trigger - Performance: linear curve, 2× ramp-up, 4s trigger - Smart: S-curve, 6s trigger (was 8s) - UI updates gated to 500ms cadence - 24 tests pass, zero warnings
1 parent dcc1730 commit f6eb5d2

8 files changed

Lines changed: 595 additions & 231 deletions

File tree

PLAN.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Profile Redesign Build Plan
2+
3+
## Infrastructure Change: Dual-Cadence Tick
4+
5+
**`ThermalMonitor.swift`** — split the single 2s tick into two cadences:
6+
7+
| Cadence | Interval | What It Does |
8+
|---|---|---|
9+
| **Thermal tick** | 100ms | Read temps, calculate curve, apply ramp governor, write fan speed |
10+
| **Monitor tick** | Every 20th thermal tick (2s) | Process capture, anomaly detection, history logging |
11+
12+
New state: `private var tickCounter: Int = 0` — increments every thermal tick, runs monitor logic when `tickCounter % 20 == 0`.
13+
14+
## Architecture Change: Per-Profile Personality
15+
16+
**`Profile.swift`** — add to `Curve` struct:
17+
18+
```swift
19+
enum CurveShape { case linear, easeIn, easeOut, sCurve }
20+
21+
let curveShape: CurveShape // how targetPercent maps temp to speed
22+
let rampUpPerSec: Float // max fan speed increase per second (0.0-1.0)
23+
let rampDownPerSec: Float // max fan speed decrease per second
24+
let sustainedTriggerSec: Float // seconds above startTemp before engaging
25+
let instantEngage: Bool // skip ramp-up entirely (MAX)
26+
```
27+
28+
`targetPercent()` updated to apply curve shape:
29+
- **easeIn**: `pos² * maxRPM` — quiet start, accelerates
30+
- **linear**: `pos * maxRPM` — current behavior
31+
- **easeOut**: `√pos * maxRPM` — fast start, levels off
32+
- **sCurve**: `pos² * (3 - 2*pos) * maxRPM` — smooth both ends
33+
34+
---
35+
36+
## Profile Definitions
37+
38+
### Silent (Apple Default)
39+
No changes. Hands-off monitoring only. Apple controls fans.
40+
41+
### Balanced — "Everyday, keep it quiet"
42+
| Parameter | Value | Rationale |
43+
|---|---|---|
44+
| Stop | 50°C | Same as Apple's observed off range |
45+
| Start | 55°C | 5°C hysteresis |
46+
| Ceiling | 70°C | Reaches max fan speed at 70°C |
47+
| Max fan | 60% | Caps noise |
48+
| Curve shape | **Ease-in** (`pos²`) | Quiet at low temps, ramps faster as heat builds |
49+
| Ramp up | ~400 RPM/s | Gentle, not jarring |
50+
| Ramp down | ~200 RPM/s | Smooth deceleration |
51+
| Sustained trigger | **8 seconds** | Filters all transients — this profile prioritizes quiet |
52+
53+
Balanced is the "don't bother me" profile. It accepts higher temps in exchange for less fan noise. The ease-in curve means at 60°C you're barely hearing the fans; at 65°C+ they start pulling harder.
54+
55+
### Performance — "Keep it cool, noise is fine"
56+
| Parameter | Value | Rationale |
57+
|---|---|---|
58+
| Stop | 50°C | Unified off threshold |
59+
| Start | 55°C | Same start, but faster response |
60+
| Ceiling | 65°C | Reaches max speed 5°C earlier than Balanced |
61+
| Max fan | 85% | High but not ear-splitting |
62+
| Curve shape | **Linear** | Direct, proportional, responsive |
63+
| Ramp up | ~800 RPM/s | 2× Balanced — gets to cooling faster |
64+
| Ramp down | ~300 RPM/s | Moderate deceleration |
65+
| Sustained trigger | **4 seconds** | Filters brief spikes but responds to real work quickly |
66+
67+
Performance is for compiles, renders, LLM inference. It doesn't wait around. Linear curve means the cooling response is proportional and predictable. 85% cap because 100% on these fans is loud and the last 15% of RPM gives diminishing thermal returns.
68+
69+
### Max — "Attack dog"
70+
| Parameter | Value | Rationale |
71+
|---|---|---|
72+
| Stop | 50°C | Unified off threshold |
73+
| Start | **65°C** | Higher start because the response is instant — no need to engage early |
74+
| Ceiling | N/A | No curve — it's binary |
75+
| Max fan | 100% | Full send |
76+
| Curve shape | N/A up / **S-curve down** | Instant on, gentle off |
77+
| Ramp up | **Instant** | Single reading above 65°C sustained → 100% immediately |
78+
| Ramp down | ~200 RPM/s with S-curve | Give temps time to stabilize |
79+
| Sustained trigger | **5 seconds** (50 ticks at 100ms) | Filters transient spikes but still catches real events |
80+
81+
**Up behavior**: `sustainedAboveCount >= 50 && peakTemp >= 65` → instant `setMax()`. No ramp governor. They spike, we spike.
82+
83+
**Down behavior**: Once temp drops below 65°C, S-curve governor ramps down at ~200 RPM/s. Below 50°C with rate-of-change ≤ 0 → fans off.
84+
85+
**Why this fixes the logs**: The Apr 7 22:12 event hit 56°C at tick 1, then 72°C at tick 2. With Option C at 65°C, the 72°C reading (2 seconds later) would have instantly triggered 100% fans. Instead of waiting for safety override at 98°C, fans would have been at 7800+ RPM 26°C earlier.
86+
87+
### Smart — "Proactive adaptive"
88+
| Parameter | Value | Rationale |
89+
|---|---|---|
90+
| Stop | 50°C | Unified off threshold |
91+
| Start | 53°C | 2°C earlier than others — gets ahead of rising temps |
92+
| Ceiling | 85°C | Wide range for proportional control |
93+
| Max fan | 100% | Uncapped when needed |
94+
| Curve shape | **S-curve** | Smooth across the full range (existing) |
95+
| Ramp up | Adaptive (rate-of-change based) | Existing logic — faster when temps rising fast |
96+
| Ramp down | ~200 RPM/s | Smooth deceleration |
97+
| Sustained trigger | **6 seconds** (60 ticks) | Proactive but filtered |
98+
99+
Smart keeps its existing rate-of-change awareness and calibration data support. The main improvements it gets are the 100ms tick (10× smoother fan transitions) and the non-linear curve shapes.
100+
101+
---
102+
103+
## Files to Modify
104+
105+
| File | Changes |
106+
|---|---|
107+
| **Profile.swift** | Add `CurveShape` enum, add ramp/trigger/shape fields to `Curve`, update `targetPercent()` for curve shapes, update all 5 profile definitions, update `Codable` conformance |
108+
| **ThermalMonitor.swift** | 100ms tick with monitor cadence at 2s, per-profile ramp rates from `Curve`, per-profile sustained trigger from `Curve`, MAX-specific instant engage path, remove hardcoded `maxRampUp`/`maxRampDown`/`sustainedTriggerCount` |
109+
| **ProfileTests.swift** | Update all threshold/parameter tests, add curve shape tests, add per-profile ramp rate tests, add MAX instant-engage test, add sustained trigger per-profile tests |
110+
| **MenuBarView.swift** | Update labels — MAX shows "65°C instant", others show start→ceiling range |
111+
112+
## What Will NOT Change
113+
- Safety override (95°C) — working correctly, stays as-is
114+
- Anomaly detection — working, just moves to 2s cadence
115+
- Process capture — working, stays at 2s cadence
116+
- Smart rate-of-change logic — working, just benefits from faster tick
117+
- Calibration system — unchanged
118+
- Silent profile — unchanged
119+
- Daemon/heartbeat/SMC layer — unchanged

README.md

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ Tools like **Macs Fan Control** and **TG Pro** charge $15–$20 for fan control
1818
| Feature | ThermalForge | Macs Fan Control | TG Pro |
1919
|---|---|---|---|
2020
| Smart adaptive fan curve | **Yes** | No | No |
21-
| Machine-specific calibration | **Yes** | No | No |
21+
| Machine-specific calibration | **Coming soon** | No | No |
2222
| Multi-sensor safety | **Yes — all sensors** | [One sensor per fan](https://github.com/crystalidea/macs-fan-control/issues/266) | Manual rules only |
2323
| Proactive cooling (ramps before throttle) | **Yes** | No | No |
24-
| Fan curve type | Graduated continuous | Linear between 2 points | Manual step-function |
24+
| Fan curve type | Per-profile shapes (ease-in, linear, S-curve, instant) | Linear between 2 points | Manual step-function |
2525
| Real-time temp monitoring | Yes | Yes | Yes |
2626
| Menu bar app | Yes | Yes | Yes |
2727
| CLI access | **Yes** | No | No |
@@ -56,25 +56,25 @@ Tools like **Macs Fan Control** and **TG Pro** charge $15–$20 for fan control
5656

5757
## Profiles
5858

59-
Every profile uses a proportional curve — fans ramp gradually with temperature, not as binary switches. All profiles share a unified 50°C off threshold (matching Apple's observed behavior). Fans only engage after temperature stays above the start threshold for 8 consecutive seconds transient 2-second spikes are ignored because they resolve on their own and reacting to them would cause the start/stop cycling that is the #1 cause of fan bearing wear (source: [Analog Devices fan control](https://www.analog.com/en/analog-dialogue/articles/how-to-control-fan-speed.html)).
59+
Every profile uses a proportional curve with a per-profile curve shape — fans ramp gradually with temperature, not as binary switches. All profiles share a unified 50°C off threshold (matching Apple's observed behavior). Each profile has its own sustained trigger duration — fans only engage after temperature stays above the start threshold for a profile-specific number of seconds, filtering transient spikes that resolve on their own. Reacting to transient spikes would cause the start/stop cycling that is the #1 cause of fan bearing wear (source: [Analog Devices fan control](https://www.analog.com/en/analog-dialogue/articles/how-to-control-fan-speed.html)).
6060

61-
Ramp rates match Apple's hardware behavior (~400 RPM/sec up, ~200 RPM/sec down) for acoustic comfort (source: [MAX31760 datasheet](https://www.analog.com/media/en/technical-documentation/data-sheets/max31760.pdf), [Microchip AN771](https://ww1.microchip.com/downloads/en/appnotes/00771a.pdf)).
61+
Thermal polling runs at 100ms (matching Apple's own thermalmonitord cadence) for smooth fan transitions. Each profile has its own ramp rates and curve shape tuned to its purpose. Ramp governor design sourced from [MAX31760 datasheet](https://www.analog.com/media/en/technical-documentation/data-sheets/max31760.pdf) and [Microchip AN771](https://ww1.microchip.com/downloads/en/appnotes/00771a.pdf).
6262

63-
| Profile | Fans off | Fans start | Ceiling | Max fan | Behavior |
64-
|---|---|---|---|---|---|
65-
| **Silent (Apple Default)** | N/A | N/A | N/A | Apple | Monitoring only. Apple controls fans. |
66-
| **Balanced** | 50°C | 55°C | 70°C | 60% | Gentle ramp for everyday use. |
67-
| **Performance** | 50°C | 55°C | 65°C | 85% | Steeper curve, lower ceiling. |
68-
| **Max** | 50°C | 55°C | 65°C | 100% | Full cooling with ramp governor. |
69-
| **Smart** | 50°C | 53°C | 85°C | 100% | Proactive curve with rate-of-change awareness. Starts 2°C earlier. |
63+
| Profile | Fans off | Fans start | Ceiling | Max fan | Curve | Sustained trigger | Behavior |
64+
|---|---|---|---|---|---|---|---|
65+
| **Silent (Apple Default)** | N/A | N/A | N/A | Apple | N/A | N/A | Monitoring only. Apple controls fans. |
66+
| **Balanced** | 50°C | 55°C | 70°C | 60% | Ease-in (pos²) | 8 seconds | Quiet at low temps, ramps harder as heat builds. |
67+
| **Performance** | 50°C | 55°C | 65°C | 85% | Linear | 4 seconds | Direct proportional response, 2× ramp-up speed. |
68+
| **Max** | 50°C | 65°C | | 100% | Instant | 5 seconds | Attack dog: instant 100% when triggered, S-curve ramp-down. |
69+
| **Smart** | 50°C | 53°C | 85°C | 100% | S-curve | 6 seconds | Proactive with rate-of-change awareness. Starts 2°C earlier. |
7070

7171
**How profiles work:**
7272
- **Below 50°C:** All fans off. Machine is at idle.
73-
- **50–55°C (hysteresis zone):** Fans maintain current state. Already running → stay at minimum. Already off → stay off.
74-
- **Above 55°C for 8+ seconds:** Fans engage at minimum RPM and begin proportional ramp toward ceiling.
75-
- **Between start and ceiling:** Fan speed scales proportionally. At 62.5°C on Balanced (midpoint of 55–70°C), fans run at 30% of max RPM.
73+
- **50°C–start (hysteresis zone):** Fans maintain current state. Already running → stay at minimum. Already off → stay off.
74+
- **Above start for N seconds:** Fans engage. Balanced and Performance ramp proportionally using their curve shape. Max jumps instantly to 100%.
75+
- **Between start and ceiling:** Fan speed scales based on the profile's curve shape. Balanced (ease-in) is quiet at low temps — at 62.5°C (midpoint of 55–70°C), fans run at only 15% of max RPM instead of 30% linear. Performance (linear) is proportional. Max has no proportional zone — it's binary.
7676
- **At ceiling and above:** Fan speed at the profile's maximum (60%/85%/100%).
77-
- **Ramp down:** When temperature drops, fans reduce speed at ~200 RPM/sec — slower than ramp-up for smoother acoustics.
77+
- **Ramp down:** Each profile has its own ramp-down rate. Max uses a gentle governor to let temps stabilize before backing off.
7878

7979
## Install
8080

@@ -127,13 +127,13 @@ Apple doesn't do this because silence sells in store demos and most users never
127127

128128
### How Smart works
129129

130-
**The curve:** Smart maps temperature to fan speed across a 53–85°C range. Below 50°C, fans turn off. Between 50–53°C, fans maintain current state (hysteresis). Above 85°C, fans go to max. Between those points, fan speed scales proportionally using an S-curve (gentle at low temps, steeper approaching the ceiling).
130+
**The curve:** Smart maps temperature to fan speed across a 53–85°C range using an S-curve (gentle at both ends, steeper in the middle). Below 50°C, fans turn off. Between 50–53°C, fans maintain current state (hysteresis). Above 85°C, fans go to max.
131131

132132
**Rate-of-change awareness:** Smart doesn't just look at where temperature is — it looks at how fast it's moving. If temp is rising at 1°C/sec, Smart boosts fan speed proportionally to get ahead of the climb. If temp is stable or falling, Smart holds steady or eases off gradually.
133133

134-
**Ramp governors:** Fan speed changes are rate-limited to match Apple's hardware behavior. Ramp up at ~400 RPM/sec, ramp down at ~200 RPM/sec. This prevents acoustic shock, reduces mechanical stress, and extends fan bearing lifespan by up to 50% compared to abrupt speed changes (source: [NMB fan engineering](https://nmbtc.com/white-papers/dc-brushless-cooling-fan-behavior/), [Analog Devices ADM1031 datasheet](https://www.onsemi.com/download/data-sheet/pdf/adm1031-d.pdf)).
134+
**Ramp governors:** Fan speed changes are rate-limited for acoustic comfort. Each profile has its own ramp rates — Smart uses ~400 RPM/sec up, ~200 RPM/sec down. This prevents acoustic shock, reduces mechanical stress, and extends fan bearing lifespan by up to 50% compared to abrupt speed changes (source: [NMB fan engineering](https://nmbtc.com/white-papers/dc-brushless-cooling-fan-behavior/), [Analog Devices ADM1031 datasheet](https://www.onsemi.com/download/data-sheet/pdf/adm1031-d.pdf)).
135135

136-
**Hysteresis:** Fans turn on at 53°C (after 8 seconds sustained) and turn off at 50°C — a 3°C gap. All other profiles use 55°C start with a 5°C gap. This prevents rapid on/off cycling, which is the #1 cause of fan bearing wear in fluid dynamic bearing fans (source: [Nidec FDB technology](https://www.nidec.com/en/technology/capability/fdb/), [AnandTech fan lifespan discussion](https://forums.anandtech.com/threads/fan-stop-start-effect-on-lifespan.2284098/)).
136+
**Hysteresis:** Fans turn on at 53°C (after 6 seconds sustained) and turn off at 50°C — a 3°C gap. Balanced and Performance use 55°C start with a 5°C gap. Max uses 65°C start with a 15°C gap. This prevents rapid on/off cycling, which is the #1 cause of fan bearing wear in fluid dynamic bearing fans (source: [Nidec FDB technology](https://www.nidec.com/en/technology/capability/fdb/), [AnandTech fan lifespan discussion](https://forums.anandtech.com/threads/fan-stop-start-effect-on-lifespan.2284098/)).
137137

138138
**0 to minimum RPM is binary:** Apple Silicon MacBook fans cannot spin below their minimum RPM (2317 on M5 Max, 1200 on M1 Max). When Smart decides fans should run, they jump directly to minimum — this is a hardware limitation of brushless DC motors that require a startup burst to overcome static friction. Above minimum, all speed changes are smooth and governed.
139139

0 commit comments

Comments
 (0)