fix: fixed fan control helper on M5 (#3051)

M5 gen uses lowercase fan mode key (F%dmd) instead of F%dMd, and lacks
the Ftst key used for fan unlock on M1-M4. Runtime-detect the correct
key name via SMC probe, and restructure unlockFanControl/resetFanControl
to work without Ftst in the case of M5 gen.
This commit is contained in:
Charlie Herz
2026-03-22 14:40:26 -04:00
committed by GitHub
parent 4511a96c3a
commit fb95e33cb6
3 changed files with 54 additions and 37 deletions

View File

@@ -332,7 +332,7 @@ extension SensorsReader {
}
}
if let md = SMC.shared.getValue("F\(i)Md"), let parsed = FanMode(rawValue: Int(md)) {
if let md = SMC.shared.getValue(SMC.shared.fanModeKey(i)), let parsed = FanMode(rawValue: Int(md)) {
mode = parsed.isAutomatic ? .automatic : parsed
} else {
mode = self.getFanMode(i)
@@ -356,7 +356,7 @@ extension SensorsReader {
#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)
let modeValue = Int(SMC.shared.getValue(SMC.shared.fanModeKey(id)) ?? 0)
return modeValue == 1 ? .forced : .automatic
#else
// Legacy Intel: Use FS! bitmask

View File

@@ -134,16 +134,16 @@ func main() {
print("Minimal speed:", SMC.shared.getValue("F\(i)Mn") ?? -1)
print("Maximum speed:", SMC.shared.getValue("F\(i)Mx") ?? -1)
print("Target speed:", SMC.shared.getValue("F\(i)Tg") ?? -1)
print("Mode:", FanMode(rawValue: Int(SMC.shared.getValue("F\(i)Md") ?? -1)) ?? .forced)
print("Mode:", FanMode(rawValue: Int(SMC.shared.getValue(SMC.shared.fanModeKey(i)) ?? -1)) ?? .forced)
print()
}
case .reset:
#if arch(arm64)
if SMC.shared.resetFanControl() {
print("[reset] Ftst reset to 0, thermalmonitord has control")
print("[reset] fan control restored to automatic")
} else {
print("[reset] Ftst reset FAILED")
print("[reset] fan control reset FAILED")
}
#else
print("[reset] not needed on Intel Macs")

View File

@@ -160,6 +160,7 @@ extension Float {
public class SMC {
public static let shared = SMC()
private var conn: io_connect_t = 0
private var _fanModeKeyIsLower: Bool?
public init() {
var result: kern_return_t
@@ -210,7 +211,7 @@ public class SMC {
}
if val.dataSize > 0 {
if val.bytes.first(where: { $0 != 0 }) == nil && val.key != "FS! " && val.key != "F0Md" && val.key != "F1Md" {
if val.bytes.first(where: { $0 != 0 }) == nil && val.key != "FS! " && val.key != "F0Md" && val.key != "F1Md" && val.key != "F0md" && val.key != "F1md" {
return nil
}
@@ -355,13 +356,25 @@ public class SMC {
}
// MARK: - fans
public func fanModeKey(_ id: Int) -> String {
#if arch(arm64)
if _fanModeKeyIsLower == nil {
var probe = SMCVal_t("F0md")
_fanModeKeyIsLower = read(&probe) == kIOReturnSuccess && probe.dataSize > 0
}
return _fanModeKeyIsLower! ? "F\(id)md" : "F\(id)Md"
#else
return "F\(id)Md"
#endif
}
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 modeKey = fanModeKey(id)
let targetKey = "F\(id)Tg"
if self.getValue(modeKey) != nil {
@@ -474,7 +487,7 @@ public class SMC {
}
#if arch(arm64)
var modeVal = SMCVal_t("F\(id)Md")
var modeVal = SMCVal_t(fanModeKey(id))
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"))
@@ -556,20 +569,8 @@ public class SMC {
}
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"
// Try direct mode write first (works on M5+ without Ftst)
let modeKey = fanModeKey(fanId)
var modeVal = SMCVal_t(modeKey)
let modeRead = read(&modeVal)
guard modeRead == kIOReturnSuccess else {
@@ -580,27 +581,31 @@ public class SMC {
if write(modeVal) == kIOReturnSuccess {
return true
}
// Direct failed; fall back to Ftst unlock
// Direct failed; try Ftst unlock (M1-M4)
var ftstVal = SMCVal_t("Ftst")
let ftstRead = read(&ftstVal)
guard ftstRead == kIOReturnSuccess else {
print(smcError("read", key: "Ftst", result: ftstRead))
let ftstResult = read(&ftstVal)
guard ftstResult == kIOReturnSuccess, ftstVal.dataSize > 0 else {
return false
}
if ftstVal.bytes[0] == 1 {
return retryModeWrite(fanId: fanId, maxAttempts: 20)
}
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"
let modeKey = fanModeKey(fanId)
var modeVal = SMCVal_t(modeKey)
let result = read(&modeVal)
guard result == kIOReturnSuccess else {
@@ -614,13 +619,25 @@ public class SMC {
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 result == kIOReturnSuccess && value.dataSize > 0 {
if value.bytes[0] == 0 { return true }
value.bytes[0] = 0
return writeWithRetry(value)
}
if value.bytes[0] == 0 { return true }
value.bytes[0] = 0
return writeWithRetry(value)
// Ftst absent (M5+): reset fan modes directly
guard let count = getValue("FNum") else { return false }
var success = true
for i in 0..<Int(count) {
let modeKey = fanModeKey(i)
var modeVal = SMCVal_t(modeKey)
let readResult = read(&modeVal)
guard readResult == kIOReturnSuccess else { continue }
if modeVal.bytes[0] == 0 { continue }
modeVal.bytes[0] = 0
if !writeWithRetry(modeVal) { success = false }
}
return success
}
#endif