Files

149 lines
5.1 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// 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..<fanCount).compactMap { id in
guard let current = smc.double(SMCKey("F\(id)Ac")) else { return nil }
let minimum = smc.double(SMCKey("F\(id)Mn")) ?? 0
let maximum = smc.double(SMCKey("F\(id)Mx")) ?? 0
let target = smc.double(SMCKey("F\(id)Tg")) ?? current
let mode = smc.double(modeKey(id)) ?? 0
// F0St:3 停转、1 起步、5 运行;没有这个键的机型按“有目标转速但还没转起来”判断
let starting = smc.double(SMCKey("F\(id)St")).map { $0 == 1 } ?? (mode == 1 && target > 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..<fanCount {
do { try setAutomatic(fan: id) } catch { firstError = firstError ?? error }
}
#if arch(arm64)
// M1–M4 需要复位 Ftst,让 thermalmonitord 接管
let ftst = SMCKey("Ftst")
if smc.exists(ftst), smc.double(ftst) == 1 {
do { try writeWithRetry(ftst, value: 0) } catch { firstError = firstError ?? error }
}
#endif
if let firstError { throw firstError }
}
// MARK: 私有
private func modeKey(_ id: Int) -> 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..<attempts {
do {
try smc.write(key, value: value)
return
} catch {
lastError = error
if attempt < attempts - 1 { Thread.sleep(forTimeInterval: delay) }
}
}
throw lastError ?? SMCError.firmwareRejected(key, 0)
}
#endif
}