mirror of
https://github.com/morgan9e/macos-stats
synced 2026-04-14 00:04:15 +09:00
feat: added better fan control for M3/M4 Apple Silicon (#2924)
* Fix Ftst key handling for Apple Silicon fan control * Update CFBundleVersion to 747 in Info.plist Signed-off-by: Alex Goodkind <alex@goodkind.io> * Update TeamId and SMC.Helper certificate identifier in Info.plist Signed-off-by: Alex Goodkind <alex@goodkind.io> * Add debug logging to SMC fan control functions * Use writeWithRetry for Apple Silicon fan control writes and bump helper version to 1.0.3 * SMC fan control: serialize ops, Ftst timing, verification, logging - Helper: serial queue for setFanMode/setFanSpeed/resetFanControl - smc.swift: 3s wait after Ftst=1, longer mode retry (100ms), SMC result logging - helpers: per-fan verification with cancel-on-supersede, clearer logs - smc.swift: neutral write logs (no 'succeeded'), FAILED on error * - Updated error handling in SMCHelper to suppress expected XPC errors (codes 4097 and 4099) during helper updates/restarts. - Removed unnecessary debug print statement in ModeButtons for improved log clarity. * Update version numbers in Info.plist files to 752 and change TeamId for SMC.Helper * Add FanMode.auto3 and isAutomatic, re-add F%dMd write in automatic path, use isAutomatic in countManualFans; bump SMC Helper to 1.0.24 * Apple Silicon fan control: direct-first writes, strip diagnostic bloat Try direct F%dMd=1 write before Ftst unlock (instant on M1, fallback on M3/M4). Remove verification system, diagnostic prints, dead code. * Apple Silicon fan control: direct-first writes Try direct F%dMd=1 write before Ftst unlock (instant on M1, fallback on M3/M4). Bump helper to 1.0.2/3, Stats/Widgets to 751. --------- Signed-off-by: Alex Goodkind <alex@goodkind.io>
This commit is contained in:
@@ -894,6 +894,11 @@ public class SMCHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func resetFanControl() {
|
||||||
|
guard let helper = self.helper(nil) else { return }
|
||||||
|
helper.resetFanControl { _ in }
|
||||||
|
}
|
||||||
|
|
||||||
public func isActive() -> Bool {
|
public func isActive() -> Bool {
|
||||||
return self.connection != nil
|
return self.connection != nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -271,6 +271,7 @@ public extension Notification.Name {
|
|||||||
static let refreshPublicIP = Notification.Name("refreshPublicIP")
|
static let refreshPublicIP = Notification.Name("refreshPublicIP")
|
||||||
static let resetTotalNetworkUsage = Notification.Name("resetTotalNetworkUsage")
|
static let resetTotalNetworkUsage = Notification.Name("resetTotalNetworkUsage")
|
||||||
static let syncFansControl = Notification.Name("syncFansControl")
|
static let syncFansControl = Notification.Name("syncFansControl")
|
||||||
|
static let checkFanModes = Notification.Name("checkFanModes")
|
||||||
static let fanHelperState = Notification.Name("fanHelperState")
|
static let fanHelperState = Notification.Name("fanHelperState")
|
||||||
static let toggleOneView = Notification.Name("toggleOneView")
|
static let toggleOneView = Notification.Name("toggleOneView")
|
||||||
static let widgetRearrange = Notification.Name("widgetRearrange")
|
static let widgetRearrange = Notification.Name("widgetRearrange")
|
||||||
|
|||||||
@@ -71,8 +71,24 @@ internal class Popup: PopupWrapper {
|
|||||||
selected: self.fanValueState.rawValue
|
selected: self.fanValueState.rawValue
|
||||||
))
|
))
|
||||||
]))
|
]))
|
||||||
|
#if arch(arm64)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(self.checkFanModesAndResetFtst), name: .checkFanModes, object: nil)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if arch(arm64)
|
||||||
|
@objc private func checkFanModesAndResetFtst() {
|
||||||
|
let fanViews = self.list.values.compactMap { $0 as? FanView }
|
||||||
|
guard !fanViews.isEmpty else { return }
|
||||||
|
guard fanViews.allSatisfy({ $0.fan.mode == .automatic }) else { return }
|
||||||
|
SMCHelper.shared.resetFanControl()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
@@ -455,7 +471,7 @@ internal class ChartSensorView: NSStackView {
|
|||||||
internal class FanView: NSStackView {
|
internal class FanView: NSStackView {
|
||||||
public var sizeCallback: (() -> Void)
|
public var sizeCallback: (() -> Void)
|
||||||
|
|
||||||
private var fan: Fan
|
internal var fan: Fan
|
||||||
private var ready: Bool = false
|
private var ready: Bool = false
|
||||||
|
|
||||||
private var helperView: NSView? = nil
|
private var helperView: NSView? = nil
|
||||||
@@ -638,7 +654,7 @@ internal class FanView: NSStackView {
|
|||||||
height: view.frame.height - 8
|
height: view.frame.height - 8
|
||||||
), mode: self.fan.mode)
|
), mode: self.fan.mode)
|
||||||
buttons.callback = { [weak self] (mode: FanMode) in
|
buttons.callback = { [weak self] (mode: FanMode) in
|
||||||
if let fan = self?.fan, fan.mode != mode {
|
if let fan = self?.fan, mode == .automatic || fan.mode != mode {
|
||||||
self?.fan.mode = mode
|
self?.fan.mode = mode
|
||||||
self?.fan.customMode = mode
|
self?.fan.customMode = mode
|
||||||
SMCHelper.shared.setFanMode(fan.id, mode: mode.rawValue)
|
SMCHelper.shared.setFanMode(fan.id, mode: mode.rawValue)
|
||||||
@@ -1072,6 +1088,7 @@ private class ModeButtons: NSStackView {
|
|||||||
self.callback(.automatic)
|
self.callback(.automatic)
|
||||||
|
|
||||||
NotificationCenter.default.post(name: .syncFansControl, object: nil, userInfo: ["mode": "automatic"])
|
NotificationCenter.default.post(name: .syncFansControl, object: nil, userInfo: ["mode": "automatic"])
|
||||||
|
NotificationCenter.default.post(name: .checkFanModes, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func manualMode(_ sender: NSButton) {
|
@objc private func manualMode(_ sender: NSButton) {
|
||||||
|
|||||||
@@ -352,6 +352,14 @@ extension SensorsReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func getFanMode(_ id: Int) -> FanMode {
|
private func getFanMode(_ id: Int) -> FanMode {
|
||||||
|
#if arch(arm64)
|
||||||
|
// Apple Silicon: Read F%dMd directly
|
||||||
|
// Mode values: 0 = auto, 1 = manual, 3 = system (treated as auto for UI)
|
||||||
|
let modeValue = Int(SMC.shared.getValue("F\(id)Md") ?? 0)
|
||||||
|
return modeValue == 1 ? .forced : .automatic
|
||||||
|
#else
|
||||||
|
// Legacy Intel: Use FS! bitmask
|
||||||
|
// Bitmask: 0 = all auto, 1 = fan 0 forced, 2 = fan 1 forced, 3 = both forced
|
||||||
let fansMode: Int = Int(SMC.shared.getValue("FS! ") ?? 0)
|
let fansMode: Int = Int(SMC.shared.getValue("FS! ") ?? 0)
|
||||||
var mode: FanMode = .automatic
|
var mode: FanMode = .automatic
|
||||||
|
|
||||||
@@ -366,6 +374,7 @@ extension SensorsReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return mode
|
return mode
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>eu.exelban.Stats.SMC.Helper</string>
|
<string>eu.exelban.Stats.SMC.Helper</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0.1</string>
|
<string>1.1.0</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>2</string>
|
<string>3</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>SMAuthorizedClients</key>
|
<key>SMAuthorizedClients</key>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ helper.run()
|
|||||||
|
|
||||||
class Helper: NSObject, NSXPCListenerDelegate, HelperProtocol {
|
class Helper: NSObject, NSXPCListenerDelegate, HelperProtocol {
|
||||||
private let listener: NSXPCListener
|
private let listener: NSXPCListener
|
||||||
|
private let smcQueue = DispatchQueue(label: "eu.exelban.Stats.SMC.Helper.smcQueue")
|
||||||
|
|
||||||
private var connections = [NSXPCConnection]()
|
private var connections = [NSXPCConnection]()
|
||||||
private var shouldQuit = false
|
private var shouldQuit = false
|
||||||
@@ -115,6 +116,7 @@ extension Helper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func setFanMode(id: Int, mode: Int, completion: (String?) -> Void) {
|
func setFanMode(id: Int, mode: Int, completion: (String?) -> Void) {
|
||||||
|
smcQueue.sync {
|
||||||
guard let smc = self.smc else {
|
guard let smc = self.smc else {
|
||||||
completion("missing smc tool")
|
completion("missing smc tool")
|
||||||
return
|
return
|
||||||
@@ -129,8 +131,10 @@ extension Helper {
|
|||||||
|
|
||||||
completion(result.output)
|
completion(result.output)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func setFanSpeed(id: Int, value: Int, completion: (String?) -> Void) {
|
func setFanSpeed(id: Int, value: Int, completion: (String?) -> Void) {
|
||||||
|
smcQueue.sync {
|
||||||
guard let smc = self.smc else {
|
guard let smc = self.smc else {
|
||||||
completion("missing smc tool")
|
completion("missing smc tool")
|
||||||
return
|
return
|
||||||
@@ -146,6 +150,23 @@ extension Helper {
|
|||||||
|
|
||||||
completion(result.output)
|
completion(result.output)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetFanControl(completion: (String?) -> Void) {
|
||||||
|
smcQueue.sync {
|
||||||
|
guard let smc = self.smc else {
|
||||||
|
completion("missing smc tool")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let result = syncShell("\(smc) reset")
|
||||||
|
if let error = result.error, !error.isEmpty {
|
||||||
|
NSLog("error reset fan control: \(error)")
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
completion(result.output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func powermetrics(_ samplers: [String], completion: @escaping (String?) -> Void) {
|
func powermetrics(_ samplers: [String], completion: @escaping (String?) -> Void) {
|
||||||
let result = syncShell("powermetrics -n 1 -s \(samplers.joined(separator: ",")) --sample-rate 1000")
|
let result = syncShell("powermetrics -n 1 -s \(samplers.joined(separator: ",")) --sample-rate 1000")
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import Foundation
|
|||||||
|
|
||||||
func setFanMode(id: Int, mode: Int, completion: @escaping (String?) -> Void)
|
func setFanMode(id: Int, mode: Int, completion: @escaping (String?) -> Void)
|
||||||
func setFanSpeed(id: Int, value: Int, completion: @escaping (String?) -> Void)
|
func setFanSpeed(id: Int, value: Int, completion: @escaping (String?) -> Void)
|
||||||
|
func resetFanControl(completion: @escaping (String?) -> Void)
|
||||||
func powermetrics(_ samplers: [String], completion: @escaping (String?) -> Void)
|
func powermetrics(_ samplers: [String], completion: @escaping (String?) -> Void)
|
||||||
|
|
||||||
func uninstall()
|
func uninstall()
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ enum CMDType: String {
|
|||||||
case set
|
case set
|
||||||
case fan
|
case fan
|
||||||
case fans
|
case fans
|
||||||
|
case reset
|
||||||
case help
|
case help
|
||||||
case unknown
|
case unknown
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ enum CMDType: String {
|
|||||||
case "set": self = .set
|
case "set": self = .set
|
||||||
case "fan": self = .fan
|
case "fan": self = .fan
|
||||||
case "fans": self = .fans
|
case "fans": self = .fans
|
||||||
|
case "reset": self = .reset
|
||||||
case "help": self = .help
|
case "help": self = .help
|
||||||
default: self = .unknown
|
default: self = .unknown
|
||||||
}
|
}
|
||||||
@@ -136,6 +138,16 @@ func main() {
|
|||||||
|
|
||||||
print()
|
print()
|
||||||
}
|
}
|
||||||
|
case .reset:
|
||||||
|
#if arch(arm64)
|
||||||
|
if SMC.shared.resetFanControl() {
|
||||||
|
print("[reset] Ftst reset to 0, thermalmonitord has control")
|
||||||
|
} else {
|
||||||
|
print("[reset] Ftst reset FAILED")
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
print("[reset] not needed on Intel Macs")
|
||||||
|
#endif
|
||||||
case .help, .unknown:
|
case .help, .unknown:
|
||||||
print("SMC tool\n")
|
print("SMC tool\n")
|
||||||
print("Usage:")
|
print("Usage:")
|
||||||
@@ -145,6 +157,7 @@ func main() {
|
|||||||
print(" set set value to a key")
|
print(" set set value to a key")
|
||||||
print(" fan set fan speed")
|
print(" fan set fan speed")
|
||||||
print(" fans list of fans")
|
print(" fans list of fans")
|
||||||
|
print(" reset reset Ftst (Apple Silicon only)")
|
||||||
print(" help help menu\n")
|
print(" help help menu\n")
|
||||||
print("Available Flags:")
|
print("Available Flags:")
|
||||||
print(" -t list temperature sensors")
|
print(" -t list temperature sensors")
|
||||||
|
|||||||
166
SMC/smc.swift
166
SMC/smc.swift
@@ -46,6 +46,11 @@ internal enum SMCKeys: UInt8 {
|
|||||||
public enum FanMode: Int, Codable {
|
public enum FanMode: Int, Codable {
|
||||||
case automatic = 0
|
case automatic = 0
|
||||||
case forced = 1
|
case forced = 1
|
||||||
|
case auto3 = 3
|
||||||
|
|
||||||
|
public var isAutomatic: Bool {
|
||||||
|
self == .automatic || self == .auto3
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal struct SMCKeyData_t {
|
internal struct SMCKeyData_t {
|
||||||
@@ -352,6 +357,43 @@ public class SMC {
|
|||||||
// MARK: - fans
|
// MARK: - fans
|
||||||
|
|
||||||
public func setFanMode(_ id: Int, mode: FanMode) {
|
public func setFanMode(_ id: Int, mode: FanMode) {
|
||||||
|
#if arch(arm64)
|
||||||
|
if mode == .forced {
|
||||||
|
if !unlockFanControl(fanId: id) { return }
|
||||||
|
} else {
|
||||||
|
let modeKey = "F\(id)Md"
|
||||||
|
let targetKey = "F\(id)Tg"
|
||||||
|
|
||||||
|
if self.getValue(modeKey) != nil {
|
||||||
|
var modeVal = SMCVal_t(modeKey)
|
||||||
|
let readResult = read(&modeVal)
|
||||||
|
guard readResult == kIOReturnSuccess else {
|
||||||
|
print(smcError("read", key: modeKey, result: readResult))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if modeVal.bytes[0] != 0 {
|
||||||
|
modeVal.bytes[0] = 0
|
||||||
|
if !writeWithRetry(modeVal) { return }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetValue = SMCVal_t(targetKey)
|
||||||
|
let result = read(&targetValue)
|
||||||
|
guard result == kIOReturnSuccess else {
|
||||||
|
print(smcError("read", key: targetKey, result: result))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = Float(0).bytes
|
||||||
|
targetValue.bytes[0] = bytes[0]
|
||||||
|
targetValue.bytes[1] = bytes[1]
|
||||||
|
targetValue.bytes[2] = bytes[2]
|
||||||
|
targetValue.bytes[3] = bytes[3]
|
||||||
|
|
||||||
|
if !writeWithRetry(targetValue) { return }
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
// Intel
|
||||||
if self.getValue("F\(id)Md") != nil {
|
if self.getValue("F\(id)Md") != nil {
|
||||||
var result: kern_return_t = 0
|
var result: kern_return_t = 0
|
||||||
var value = SMCVal_t("F\(id)Md")
|
var value = SMCVal_t("F\(id)Md")
|
||||||
@@ -422,15 +464,26 @@ public class SMC {
|
|||||||
print("Error write: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
|
print("Error write: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
public func setFanSpeed(_ id: Int, speed: Int) {
|
public func setFanSpeed(_ id: Int, speed: Int) {
|
||||||
let maxSpeed = Int(self.getValue("F\(id)Mx") ?? 4000)
|
if let maxSpeed = self.getValue("F\(id)Mx"),
|
||||||
|
speed > Int(maxSpeed) {
|
||||||
|
return setFanSpeed(id, speed: Int(maxSpeed))
|
||||||
|
}
|
||||||
|
|
||||||
if speed > maxSpeed {
|
#if arch(arm64)
|
||||||
print("new fan speed (\(speed)) is more than maximum speed (\(maxSpeed))")
|
var modeVal = SMCVal_t("F\(id)Md")
|
||||||
|
let modeResult = read(&modeVal)
|
||||||
|
guard modeResult == kIOReturnSuccess else {
|
||||||
|
print("Error read fan mode: " + (String(cString: mach_error_string(modeResult), encoding: String.Encoding.ascii) ?? "unknown error"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if modeVal.bytes[0] != 1 {
|
||||||
|
if !unlockFanControl(fanId: id) { return }
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
var result: kern_return_t = 0
|
var result: kern_return_t = 0
|
||||||
var value = SMCVal_t("F\(id)Tg")
|
var value = SMCVal_t("F\(id)Tg")
|
||||||
@@ -454,11 +507,17 @@ public class SMC {
|
|||||||
value.bytes[3] = UInt8(0)
|
value.bytes[3] = UInt8(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if arch(arm64)
|
||||||
|
if !writeWithRetry(value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
#else
|
||||||
result = write(value)
|
result = write(value)
|
||||||
if result != kIOReturnSuccess {
|
if result != kIOReturnSuccess {
|
||||||
print("Error write: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
|
print("Error write: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
public func resetFans() {
|
public func resetFans() {
|
||||||
@@ -471,6 +530,101 @@ public class SMC {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Apple Silicon Fan Control
|
||||||
|
|
||||||
|
#if arch(arm64)
|
||||||
|
/// Format SMC error for logging with context
|
||||||
|
private func smcError(_ operation: String, key: String, result: kern_return_t) -> String {
|
||||||
|
let errorDesc = String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"
|
||||||
|
return "[\(key)] \(operation) failed: \(errorDesc) (0x\(String(result, radix: 16)))"
|
||||||
|
}
|
||||||
|
|
||||||
|
private func writeWithRetry(_ value: SMCVal_t, maxAttempts: Int = 10, delayMicros: UInt32 = 50_000) -> Bool {
|
||||||
|
let mutableValue = value
|
||||||
|
var lastResult: kern_return_t = kIOReturnSuccess
|
||||||
|
for attempt in 0..<maxAttempts {
|
||||||
|
lastResult = write(mutableValue)
|
||||||
|
if lastResult == kIOReturnSuccess {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if attempt < maxAttempts - 1 {
|
||||||
|
usleep(delayMicros)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print(smcError("write", key: value.key, result: lastResult))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func unlockFanControl(fanId: Int) -> Bool {
|
||||||
|
var ftstCheck = SMCVal_t("Ftst")
|
||||||
|
let ftstResult = read(&ftstCheck)
|
||||||
|
guard ftstResult == kIOReturnSuccess else {
|
||||||
|
print(smcError("read", key: "Ftst", result: ftstResult))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let ftstActive = ftstCheck.bytes[0] == 1
|
||||||
|
|
||||||
|
if ftstActive {
|
||||||
|
return retryModeWrite(fanId: fanId, maxAttempts: 20)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try direct write first (works on M1 without Ftst)
|
||||||
|
let modeKey = "F\(fanId)Md"
|
||||||
|
var modeVal = SMCVal_t(modeKey)
|
||||||
|
let modeRead = read(&modeVal)
|
||||||
|
guard modeRead == kIOReturnSuccess else {
|
||||||
|
print(smcError("read", key: modeKey, result: modeRead))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
modeVal.bytes[0] = 1
|
||||||
|
if write(modeVal) == kIOReturnSuccess {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct failed; fall back to Ftst unlock
|
||||||
|
var ftstVal = SMCVal_t("Ftst")
|
||||||
|
let ftstRead = read(&ftstVal)
|
||||||
|
guard ftstRead == kIOReturnSuccess else {
|
||||||
|
print(smcError("read", key: "Ftst", result: ftstRead))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ftstVal.bytes[0] = 1
|
||||||
|
if !writeWithRetry(ftstVal, maxAttempts: 100) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for thermalmonitord to yield control
|
||||||
|
usleep(3_000_000)
|
||||||
|
|
||||||
|
return retryModeWrite(fanId: fanId, maxAttempts: 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func retryModeWrite(fanId: Int, maxAttempts: Int) -> Bool {
|
||||||
|
let modeKey = "F\(fanId)Md"
|
||||||
|
var modeVal = SMCVal_t(modeKey)
|
||||||
|
let result = read(&modeVal)
|
||||||
|
guard result == kIOReturnSuccess else {
|
||||||
|
print(smcError("read", key: modeKey, result: result))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
modeVal.bytes[0] = 1
|
||||||
|
return writeWithRetry(modeVal, maxAttempts: maxAttempts, delayMicros: 100_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func resetFanControl() -> Bool {
|
||||||
|
var value = SMCVal_t("Ftst")
|
||||||
|
let result = read(&value)
|
||||||
|
guard result == kIOReturnSuccess else {
|
||||||
|
print(smcError("read", key: "Ftst", result: result))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if value.bytes[0] == 0 { return true }
|
||||||
|
value.bytes[0] = 0
|
||||||
|
return writeWithRetry(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
// MARK: - internal functions
|
// MARK: - internal functions
|
||||||
|
|
||||||
private func read(_ value: UnsafeMutablePointer<SMCVal_t>) -> kern_return_t {
|
private func read(_ value: UnsafeMutablePointer<SMCVal_t>) -> kern_return_t {
|
||||||
@@ -520,6 +674,12 @@ public class SMC {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IOKit can return kIOReturnSuccess but SMC firmware may still reject the write.
|
||||||
|
// Check SMC-level result code (0x00 = success, non-zero = error)
|
||||||
|
if output.result != 0x00 {
|
||||||
|
return kIOReturnError
|
||||||
|
}
|
||||||
|
|
||||||
return kIOReturnSuccess
|
return kIOReturnSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user