Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 57 additions & 8 deletions PROJECT_MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
关联时间戳记忆:`/Users/puremilk/Documents/mac fancontrol/macfan-control-v2/PROJECT_TIMELINE.md`
读取建议:先读本文件,再读时间戳记忆,避免遗漏最新变更。

更新时间:2026-05-04
更新时间:2026-07-23

## 1. 项目当前基线

Expand All @@ -18,9 +18,57 @@ iFanControl 是一个面向 Apple Silicon 带风扇机型的风扇控制工具
- 匿名活跃统计(每日上报,可关闭)
- 告诉开发者(菜单栏反馈入口)
- 开机自启动(SMAppService 原生登录项 API,系统设置中可直接管理)
- 信息栏展示设置(完整/简洁两种模式,存 `ifancontrol.ui.display_mode`,切换即时生效)
- 信息栏展示设置(完整/简洁/迷你三种模式,弹窗设置,存 `ifancontrol.ui.display_mode`,切换即时生效)

当前版本:`2.9.7` / build 43
当前版本:`2.9.8` / build 50

### build 50 说明

- **曲线编辑器**:新增 Y 轴「正向 / 反向」显示选项,默认保持正向
- **数据兼容性**:Y 轴方向只影响编辑器显示与拖拽坐标换算,不修改曲线数据、预设或实际控温逻辑
- **发布口径**:新增曲线编辑器 Y 轴方向切换,保留 CPU / GPU 平均温度来源,并修复部分问题

### build 48 说明

- **新增温度源**:支持分别计算 CPU 传感器平均温度与 GPU 传感器平均温度,并作为独立温度源驱动风扇曲线
- **传感器兼容性**:保留静态 catalog,同时支持动态发现可读取的 `T*` 温度 key
- **发布口径**:小版本迭代,公开说明聚焦温度源选择、问题修复与稳定性提升

### build 47 说明

- **对外版本基线**:`2.9.8 / build 47`
- **发布口径**:本次对外文案继续保持克制,统一表述为“修复部分 bug,提高稳定性”
- **本地基线变化**:版本号与 app bundle 已提升到 build 47;官网 `timeline.json` 与 `update-manifest.json` 已同步到 build 47
- **文档状态**:本文件与 `PROJECT_TIMELINE.md` 于 2026-06-10 补齐到 build 47,和当前已发布版本重新对齐

### build 46 说明

- **对外版本基线**:`2.9.8 / build 46`
- **菜单栏不显示修复**:修复少数设备上 App 进程已启动但菜单栏没有显示的问题;启动阶段不再因为同步动态传感器发现阻塞主线程,菜单栏与控制循环可先正常起来
- **传感器发现增强**:温度传感器从“纯静态白名单”升级为“静态 catalog + 动态发现”双轨策略,未知但可稳定读取的 `T*` key 不再被直接忽略
- **遥测与安全控制稳健性提升**:后台单轮温度 / RPM 读取全失败时不再立即清空最后成功快照;控制循环改为区分 `fresh / cached_stale / expired` 三种状态,短时遥测空窗优先继续使用最后成功快照并进入更保守控制
- **诊断增强**:fallback / fail-safe 日志补充 `model / os / telemetryState / tempAge / rpmAge / reason code`,便于后续定位 M4 / M5 / MacBook Pro 机型上的真实根因
- **发布状态**:`2.9.8 / build 46` 作为当前对外版本发布;GitHub Release、官网 ZIP、`update-manifest.json`、`timeline.json` 四项保持同步

### build 45 说明

- **本地版本基线**:`2.9.8 / build 45`
- **状态栏遥测失效显示修复**:后台遥测超过 stale 阈值后,完整 / 简洁 / 迷你三种模式不再沿用旧温度或旧 RPM,统一改为 `--` 占位;tooltip 同步显示占位值
- **信息栏展示重构**:双层信息栏改为“上层 / 下层内容可多选 + 统一样式”结构,并新增核心功率展示项
- **默认曲线适配修复**:`defaultFanCurve(maxRPM:)` 改为随机型 `maxRPM` 比例缩放,首次生成配置与重置曲线时更符合不同机型
- **新增观测与提醒**:新增“电力消耗...”窗口(核心功率 / DC In 等实时观测)与规则型散热异常提醒
- **发布状态**:`2.9.8 / build 45` 已作为上一版对外版本发布

### build 46 技术备注(2026-05-10)

- **根因定位**:本地成功复现“App 进程已启动,但菜单栏完全不显示”。最终确认根因是启动阶段同步执行动态传感器发现(`kentsmc -l`)阻塞主线程,导致 `applicationDidFinishLaunching` 未及时完成,`NSStatusItem` 没有正常挂上去。
- **重要教训**:为临时兜菜单栏显示,曾尝试给迷你双行模式增加文本 fallback;该方案会破坏原本稳定的迷你双行显示,已确认撤回。**最终保留的正确修复只有“异步发现 + 启动兜底 + 遥测容错增强”**。

### build 44 说明

- **GitHub / 官网 / App 内更新真实版本**:`2.9.8 / build 44`
- **新增功能**:迷你模式上下行内容自定义(7 种选项自由组合)、交换按钮、转速简称单位(`2400R`);信息栏展示改为独立弹窗(`DisplaySettingsWindowController`)
- **清理**:移除 `menuKeepOpen`、`menuDidClose`、`NSMenuDelegate`、9 个死属性、5 个旧 action

### build 43 说明

Expand Down Expand Up @@ -76,9 +124,10 @@ npx wrangler pages deploy ./ --project-name ifan --no-bundle --skip-caching

1. Landing Screen(首页)
2. Iteration Section(开发者也是用户本身)
3. **Timeline Section(项目时间线)** — 默认展示最近 1 天,可展开查看全部
4. Showcase Section(界面展示)
5. Support Modal(支持作者)
3. **Voices Section(用户声音)** — 左右分栏:统计卡片 + 反馈轮播,fetch `voices.json` 加载
4. **Timeline Section(项目时间线)** — 默认展示最近 1 天,可展开查看全部
5. Showcase Section(界面展示)
6. Support Modal(支持作者)

### 时间线维护规范

Expand Down Expand Up @@ -181,8 +230,8 @@ cd /tmp/staging && zip -r /tmp/iFanControl-macOS-X.Y.Z.zip .
- `FanManager` 是 `@MainActor`,硬件调用必须在后台线程
- **Process 超时保护(已撤回)**:v2.9.5/build 37 曾添加 `DispatchSemaphore` 8 秒超时,但实际导致温度读取变慢/卡住。已撤回为原始 `waitUntilExit()` 直接等待。教训见 `CODE_AUDIT.md#6`
- **菜单栏恢复**:监听 `NSApplication.didChangeScreenParametersNotification` 检测 SystemUIServer 重启,自动重建 `NSStatusItem`;事件触发时还会重新探测硬件(`reprobeHardware`)并刷新遥测
- **信息栏展示设置(build 41)**:`displayMode` 存 `UserDefaults("ifancontrol.ui.display_mode")`,值为 `"full"`(默认)或 `"compact"`;简洁模式 `53|981|A` 无单位、全角竖线分隔、模式首字母;菜单「信息栏展示」子菜单带选中态,切换即时生效
- **迷你双行显示模式(build 43)**:`displayMode` 新增 `"mini"` 值,存 `UserDefaults("ifancontrol.ui.display_mode")`;NSStatusBarButton 不支持多行文字,改用 `renderMiniStatusImage()` 渲染 NSBitmapImageRep(Retina 感知,26px 高度),上下两行独立居中(避免 monospacedDigitSystemFont 空格/数字宽度不一致问题);A/M 颜色从 `ControlStatusStore.load()` 读取,正常绿色、异常红色(nil 时不标红)
- **信息栏展示设置(build 41→44)**:`displayMode` 存 `UserDefaults("ifancontrol.ui.display_mode")`,值为 `"full"`(默认)/ `"compact"` / `"mini"`;菜单「信息栏展示...」点击弹出 `DisplaySettingsWindowController` 独立窗口,切换即时生效;简洁模式 `53|981|A` 无单位、全角竖线分隔、模式首字母
- **迷你双行显示模式(build 43→44)**:NSStatusBarButton 不支持多行文字,改用自定义 `MiniStatusView`(`NSView` + `draw(_:)` Core Text 渲染,与原生菜单栏文字同一管线,锐利);上下行均可选择任意内容类型(温度/转速/单位/模式,共 7 种),一键交换;默认上行 `65℃|A`,下行 `2400 RPM`;`miniTopLine` / `miniBottomLine` 存 `UserDefaults`;A/M 颜色从 `ControlStatusStore.load()` 读取,正常绿色、异常红色(nil 时不标红)
- **开机自启动**:使用 `SMAppService.mainApp.register()` 原生 API,在系统设置「登录项与扩展」中显示为 App 类型开关;旧的 LaunchAgent plist 在首次启动时自动迁移清理
- **更新校验**:仅依赖 SHA256 校验,不校验文件大小(Cloudflare CDN 传输时大小可能变化 5 字节导致误判)
- **GCD 定时器(build 40)**:所有 `Timer.scheduledTimer` 改为 `DispatchSource.makeTimerSource`,不依赖 RunLoop,无显示器场景也能正常运行(解决 Mac mini 无显示器风扇控制失效根因)
Expand Down
69 changes: 69 additions & 0 deletions PROJECT_TIMELINE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,77 @@
关联固定记忆:`/Users/puremilk/Documents/mac fancontrol/macfan-control-v2/PROJECT_MEMORY.md`
读取建议:先读固定记忆,再读本文件,用本文件补足最近发生的变化。

## 2026-07-23

### 2026-07-23 — v2.9.8 / build 50 发布(曲线 Y 轴方向切换)

- 风扇曲线编辑器新增 Y 轴「正向 / 反向」显示选项,默认保持正向
- 只改变坐标显示与拖拽换算,不修改曲线数据、预设或实际控温逻辑
- 保留 CPU / GPU 平均温度来源,并修复部分问题、提升稳定性

## 2026-07-16

### 2026-07-16 — v2.9.8 / build 48 发布(CPU/GPU 平均温度源)

- **新增温度源**:支持分别按 CPU 传感器平均值或 GPU 传感器平均值驱动风扇曲线
- **传感器兼容性**:静态传感器目录与动态发现并行,未知但可读取的温度 key 仍可参与选择
- **对外说明**:新增 CPU / GPU 平均温度来源,修复部分问题并提升稳定性

## 2026-05-27

### 2026-05-27 — v2.9.8 / build 47 发布

- **对外说明**:修复部分 bug,提高稳定性。
- **发布渠道状态**:官网 `timeline.json`、`update-manifest.json` 与 App 内更新基线已同步到 `v2.9.8 / build 47`
- **文档补记**:主仓项目文档在 2026-06-10 补齐本条,避免固定记忆继续停留在 build 46

## 2026-05-11

### 2026-05-11 — 官网新增「用户声音」板块

- **新板块上线**:在 Iteration Section 和 Timeline Section 之间新增 voices-section,展示真实用户反馈如何推动产品改进
- **板块布局**:左栏三张统计卡片(已采纳建议 2 条 / 平均响应 <24h / 采纳到上线 ≈0.5d)+ 右栏反馈轮播卡片
- **卡片设计**:方正上下分区结构——上部「用户反馈」标签 + 引用正文(display font),下部「我们做了什么」标签 + 改进描述(sans font),底部时间线 meta 栏
- **两条反馈内容**:风扇曲线三预设系统(开发者自身) + 散热异常提醒(1836654086@qq.com 已授权匿名引用)
- **轮播机制**:圆点导航 + 6 秒自动切换 + hover 暂停 + 中英双语渲染
- **响应式**:四大断点适配(>1179px 左右并排 / <819px 上下堆叠 / <640px 统计上一下二排列)
- **技术细节**:数据通过 fetch `voices.json` 加载(部署后生效)、CSS 按规范重命名为 `styles.20260511a.css` 避免 CDN 缓存
- 涉及文件:`docs/index.html`、`docs/voices.json`、`docs/styles.20260511a.css`
- 已部署到 Cloudflare Pages

## 2026-05-10

### 2026-05-10 — v2.9.8 / build 46 发布(菜单栏显示修复 + 遥测稳健性提升)

- **菜单栏不显示修复**:修复少数设备上 App 进程已启动但菜单栏没有显示的问题。启动阶段不再因为同步动态传感器发现阻塞主线程,菜单栏与控制循环可先正常起来。
- **传感器发现增强**:温度传感器从“纯静态白名单”升级为“静态 catalog + 动态发现”双轨策略;未知但可稳定读取的 `T*` key 不再被直接忽略。
- **遥测容错与安全控制增强**:后台单轮温度 / RPM 读取全失败时不再立即清空最后成功快照;控制循环改为区分 `fresh / cached_stale / expired` 三种状态,短时遥测空窗优先继续使用最后成功快照并进入更保守控制。
- **诊断增强**:fallback / fail-safe 日志新增 `model / os / telemetryState / tempAge / rpmAge / reason code`,便于后续定位 M4 / M5 / MacBook Pro 机型上的真实根因。
- **发布渠道状态**:本次按新 build 46 正式发布,不覆盖 build 45;已同步新 ZIP、官网 ZIP、`update-manifest.json`、`timeline.json` 与 GitHub Release。

## 2026-05-05

### 2026-05-05 — v2.9.8 / build 45 发布(功率展示 + 规则型散热异常提醒)

- **状态栏遥测失效显示修复**:后台温度 / RPM 遥测超过 stale 阈值后,菜单栏完整 / 简洁 / 迷你三种模式统一显示 `--` 占位,不再继续挂旧数值
- **信息栏展示重构**:双层信息栏改为上层 / 下层内容可多选,并新增核心功率展示;样式统一作用于每层已选内容
- **电力消耗窗口**:新增“电力消耗...”窗口,可直接查看 CPU / GPU / ANE / RAM / PCI / DC In 等实时功率
- **散热异常提醒**:新增基于 10 分钟窗口、低输入功率 + 高风扇 + 高温条件的规则型异常提醒,带 12 小时冷却期
- **发布渠道状态**:GitHub Release、官网 ZIP、`update-manifest.json`、`timeline.json` 已同步到 `v2.9.8 / build 45`

## 2026-05-04

### 2026-05-04 — v2.9.8 / build 44 发布(迷你模式自定义 + 信息栏弹窗设置)

- **迷你模式上下行自定义**:上行/下行均可选择温度或转速的任意组合(含单位、模式),不再固定「温度在上、转速在下」
- 7 种内容选项:温度、温度+单位、温度+模式、温度+全部、转速、转速+单位、转速+简称(`2400R`)
- 交换按钮:一键互换上下行内容
- 默认配置:上行 `65℃|A`,下行 `2400 RPM`(带单位)
- **信息栏展示改为弹窗设置**:新增 `DisplaySettingsWindowController`,菜单栏「信息栏展示...」点击后弹出独立窗口,显示模式(完整/简洁/迷你)+ 迷他选项均在弹窗内操作,设置即时生效
- 迷你选项区缩进 + 次级标签(小字灰色),与主选项视觉分层
- **清理死代码**:移除 `menuKeepOpen`、`menuDidClose`、9 个未赋值的 `NSMenuItem?` 属性、5 个旧 action 方法、`NSMenuDelegate` 协议遵循
- **Release 发布**:GitHub Release、官网 ZIP、update-manifest.json、timeline.json 已同步到 build 44

### 2026-05-04 — v2.9.7 / build 43 发布(迷你双行显示模式)

- **迷你双行显示模式**:菜单栏可显示两行紧凑信息,大幅减少横向占用
Expand Down
10 changes: 8 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ let package = Package(
.macOS(.v13)
],
targets: [
.target(
name: "IOReportBridge",
publicHeadersPath: "include"
),
.target(
name: "FanCurveEditor",
linkerSettings: [
Expand All @@ -18,10 +22,12 @@ let package = Package(
),
.executableTarget(
name: "MacFanControl",
dependencies: ["FanCurveEditor"],
dependencies: ["FanCurveEditor", "IOReportBridge"],
linkerSettings: [
.linkedFramework("AppKit"),
.linkedFramework("Foundation")
.linkedFramework("Foundation"),
.linkedFramework("IOKit"),
.unsafeFlags(["-lIOReport"])
]
),
]
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ English readers can use [README_EN.md](README_EN.md).
- 菜单栏显示当前温度、风扇转速和控制模式
- 自动模式:根据 5 点风扇曲线调速
- 手动模式:固定目标 RPM
- 曲线编辑器支持 Y 轴正向 / 反向显示,方便按不同习惯编辑 RPM 曲线
- 安全兜底转速:高温时只托底,不压低用户更高转速
- 温度源选择:默认选择最热传感器,也支持手动指定
- 温度源选择:默认选择最热传感器,也支持 CPU / GPU 分别计算平均温度或手动指定传感器
- 应用内更新:支持手动检查,也支持后台自动检查
- 关于窗口:整合版本号、检查更新、自动检查更新、GitHub 和重启入口
- 匿名统计用户量:按固定间隔上报,可在关于/帮助中关闭
Expand Down
3 changes: 2 additions & 1 deletion README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ This repository now keeps only source code, scripts, and essential resources. Re
- Menu bar display for current temperature, RPM, and control mode
- Automatic mode based on a 5-point fan curve
- Manual mode with a fixed RPM target
- Fan curve editor with normal/reversed Y-axis display options
- Safety floor RPM at critical temperature without overriding higher user RPM
- Temperature source selection with automatic hottest-sensor mode by default
- Temperature source selection with automatic hottest-sensor mode by default, plus separate CPU/GPU average-temperature and manual sensor modes
- In-app updates with manual and scheduled checks
- Unified “About iFanControl” window with version, update controls, GitHub, and restart
- Anonymous user-count stats sent at a fixed interval, configurable in About / Help
Expand Down
25 changes: 20 additions & 5 deletions Sources/FanCurveEditor/FanCurveView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,23 @@ class FanCurveView: NSView {
setNeedsDisplay(bounds)
}
}

var isYAxisReversed = false {
didSet {
setNeedsDisplay(bounds)
}
}

private let minTemp = 20.0
private let maxTemp = 100.0
private let padding = 40.0

private var selectedPointIndex: Int? = nil

private func yPosition(forRPM rpm: Int, height: CGFloat) -> CGFloat {
let normalizedRPM = CGFloat(rpm) / CGFloat(maxRPM)
return padding + (isYAxisReversed ? normalizedRPM : 1 - normalizedRPM) * height
}

override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
Expand Down Expand Up @@ -106,7 +118,7 @@ class FanCurveView: NSView {

// RPM标签
for rpm in stride(from: 0, through: maxRPM, by: maxRPM / 4) {
let y = padding + height - (CGFloat(rpm) / CGFloat(maxRPM)) * height
let y = yPosition(forRPM: rpm, height: height)
let label = String(format: "%d", rpm)
let size = label.size(withAttributes: attributes)
label.draw(at: CGPoint(x: padding - size.width - 5, y: y - size.height / 2), withAttributes: attributes)
Expand All @@ -122,7 +134,7 @@ class FanCurveView: NSView {
var points: [CGPoint] = []
for point in fanCurve {
let x = padding + (point.temperature - minTemp) / (maxTemp - minTemp) * width
let y = padding + height - (CGFloat(point.rpm) / CGFloat(maxRPM)) * height
let y = yPosition(forRPM: point.rpm, height: height)
points.append(CGPoint(x: x, y: y))
}

Expand Down Expand Up @@ -179,7 +191,7 @@ class FanCurveView: NSView {

for (index, point) in fanCurve.enumerated() {
let x = padding + (point.temperature - minTemp) / (maxTemp - minTemp) * width
let y = padding + height - (CGFloat(point.rpm) / CGFloat(maxRPM)) * height
let y = yPosition(forRPM: point.rpm, height: height)

let circlePath = CGMutablePath()
circlePath.addArc(center: CGPoint(x: x, y: y), radius: 6, startAngle: 0, endAngle: .pi * 2, clockwise: false)
Expand Down Expand Up @@ -247,7 +259,7 @@ class FanCurveView: NSView {

for (index, fanPoint) in fanCurve.enumerated() {
let x = padding + (fanPoint.temperature - minTemp) / (maxTemp - minTemp) * width
let y = padding + height - (CGFloat(fanPoint.rpm) / CGFloat(maxRPM)) * height
let y = yPosition(forRPM: fanPoint.rpm, height: height)

let distance = hypot(point.x - x, point.y - y)
if distance < 10 {
Expand All @@ -271,7 +283,10 @@ class FanCurveView: NSView {
let height = bounds.height - 2 * padding

let newTemp = minTemp + (point.x - padding) / width * (maxTemp - minTemp)
let newRPM = Int((1 - (point.y - padding) / height) * Double(maxRPM))
let normalizedRPM = isYAxisReversed
? (point.y - padding) / height
: 1 - (point.y - padding) / height
let newRPM = Int(normalizedRPM * Double(maxRPM))

if index == 0 {
fanCurve[index].temperature = minTemp
Expand Down
Loading