// // FanControl.swift // OpenStats // // 风扇读写。Apple Silicon 解锁流程参考 exelban/stats (MIT): // Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved. // import Foundation public struct FanState: Sendable, Equatable, Identifiable { public let id: Int public var current: Double public var minimum: Double public var maximum: Double public var target: Double public var isManual: Bool /// 风扇从停转起步:Apple 芯片的固件先低速拖转约 5 秒,期间转速读数为 0 public var isStarting: Bool public init(id: Int, current: Double, minimum: Double, maximum: Double, target: Double, isManual: Bool, isStarting: Bool = false) { self.id = id self.current = current self.minimum = minimum self.maximum = maximum self.target = target self.isManual = isManual self.isStarting = isStarting } /// 当前转速在 min...max 区间的占比 public var fraction: Double { guard maximum > minimum else { return 0 } return min(1, max(0, (current - minimum) / (maximum - minimum))) } } public final class FanControl { private let smc: SMCConnection private var lowercaseModeKey: Bool? public init(smc: SMCConnection) { self.smc = smc } public var fanCount: Int { Int(smc.double(SMCKey("FNum")) ?? 0) } public func read() -> [FanState] { (0.. 0) return FanState(id: id, current: current, minimum: minimum, maximum: maximum, target: target, isManual: mode == 1, isStarting: starting && current == 0) } } // MARK: 写入(需要 root) /// 设为手动并指定目标转速;转速会被限制在固件给出的 min...max 之间。 public func setManual(fan id: Int, rpm: Double) throws { let minimum = smc.double(SMCKey("F\(id)Mn")) ?? 0 let maximum = smc.double(SMCKey("F\(id)Mx")) ?? rpm let clamped = min(max(rpm, minimum), maximum) #if arch(arm64) if smc.double(modeKey(id)) != 1 { try unlockManualMode(fan: id) } try writeWithRetry(SMCKey("F\(id)Tg"), value: clamped) #else try smc.write(SMCKey("F\(id)Md"), value: 1) try smc.write(SMCKey("F\(id)Tg"), value: clamped) #endif } public func setAutomatic(fan id: Int) throws { #if arch(arm64) if smc.double(modeKey(id)) != 0 { try writeWithRetry(modeKey(id), value: 0) } #else try smc.write(SMCKey("F\(id)Md"), value: 0) #endif } /// 全部风扇恢复系统自动控制 public func resetAll() throws { var firstError: Error? for id in 0.. SMCKey { if lowercaseModeKey == nil { lowercaseModeKey = smc.exists(SMCKey("F0md")) } return SMCKey(lowercaseModeKey == true ? "F\(id)md" : "F\(id)Md") } #if arch(arm64) private func unlockManualMode(fan id: Int) throws { // M5 及以后:直接写模式键即可 if (try? smc.write(modeKey(id), value: 1)) != nil { return } // M1–M4:先写 Ftst=1,等待 thermalmonitord 让出控制权 let ftst = SMCKey("Ftst") guard smc.exists(ftst) else { throw SMCError.firmwareRejected(modeKey(id), 0) } if smc.double(ftst) != 1 { try writeWithRetry(ftst, value: 1, attempts: 100) Thread.sleep(forTimeInterval: 3) } try writeWithRetry(modeKey(id), value: 1, attempts: 300, delay: 0.1) } private func writeWithRetry(_ key: SMCKey, value: Double, attempts: Int = 10, delay: TimeInterval = 0.05) throws { var lastError: Error? for attempt in 0..