260 lines
7.4 KiB
Swift
260 lines
7.4 KiB
Swift
import Foundation
|
||
import IOKit
|
||
|
||
// MARK: - SMC 温度传感器(通过 AppleSMC + IOKit 读取 CPU / GPU 温度)
|
||
|
||
// ── SMC 结构体定义(必须精确匹配 AppleSMC kext 的 SMCParamStruct)──
|
||
|
||
private struct SMCVersion {
|
||
var major: UInt8 = 0
|
||
var minor: UInt8 = 0
|
||
var build: UInt8 = 0
|
||
var reserved: UInt8 = 0
|
||
var release: UInt16 = 0
|
||
}
|
||
|
||
private struct SMCPLimitData {
|
||
var version: UInt16 = 0
|
||
var length: UInt16 = 0
|
||
var cpuPLimit: UInt32 = 0
|
||
var gpuPLimit: UInt32 = 0
|
||
var memPLimit: UInt32 = 0
|
||
}
|
||
|
||
private struct SMCKeyInfoData {
|
||
var dataSize: UInt32 = 0
|
||
var dataType: UInt32 = 0
|
||
var dataAttributes: UInt8 = 0
|
||
}
|
||
|
||
private typealias SMCBytes = (
|
||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8
|
||
)
|
||
|
||
private struct SMCParamStruct {
|
||
var key: UInt32 = 0
|
||
var vers = SMCVersion()
|
||
var pLimitData = SMCPLimitData()
|
||
var keyInfo = SMCKeyInfoData()
|
||
var padding: UInt16 = 0 // 关键!对齐填充
|
||
var result: UInt8 = 0
|
||
var status: UInt8 = 0
|
||
var data8: UInt8 = 0 // 子 selector(5=读key, 9=查询key信息, 8=按索引取key)
|
||
var data32: UInt32 = 0
|
||
var bytes: SMCBytes = (
|
||
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
|
||
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0
|
||
)
|
||
}
|
||
|
||
// SMC 常量
|
||
private let kSMCHandleYPCEvent: UInt32 = 2
|
||
private let kSMCReadKey: UInt8 = 5
|
||
private let kSMCGetKeyInfo: UInt8 = 9
|
||
private let kSMCGetKeyFromIndex: UInt8 = 8
|
||
private let kSMCSuccess: UInt8 = 0
|
||
|
||
/// 非 actor 工具类:封装 SMC 通信细节
|
||
final class SMCSensor {
|
||
static let shared = SMCSensor()
|
||
|
||
private var conn: io_connect_t = 0
|
||
private var cpuKeys: [String] = []
|
||
private var gpuKeys: [String] = []
|
||
private var scanned = false
|
||
|
||
private init() {}
|
||
|
||
deinit {
|
||
if conn != 0 { IOServiceClose(conn) }
|
||
}
|
||
|
||
// MARK: - 公开接口
|
||
|
||
func readCPUTemperature() -> Double? {
|
||
return readMax(keys: cpuKeys)
|
||
}
|
||
|
||
func readGPUTemperature() -> Double? {
|
||
return readMax(keys: gpuKeys)
|
||
}
|
||
|
||
// MARK: - 内部
|
||
|
||
private func readMax(keys: [String]) -> Double? {
|
||
guard openSMC() else { return nil }
|
||
if !scanned { scanKeys() }
|
||
|
||
var maxTemp: Double? = nil
|
||
for key in keys {
|
||
if let temp = readKey(key) {
|
||
maxTemp = max(maxTemp ?? temp, temp)
|
||
}
|
||
}
|
||
return maxTemp
|
||
}
|
||
|
||
private func openSMC() -> Bool {
|
||
if conn != 0 { return true }
|
||
|
||
let service = IOServiceGetMatchingService(
|
||
kIOMainPortDefault,
|
||
IOServiceMatching("AppleSMC")
|
||
)
|
||
guard service != 0 else { return false }
|
||
|
||
let status = IOServiceOpen(service, mach_task_self_, 0, &conn)
|
||
IOObjectRelease(service)
|
||
|
||
return status == KERN_SUCCESS && conn != 0
|
||
}
|
||
|
||
// MARK: - 扫描可用温度 key
|
||
|
||
private func scanKeys() {
|
||
scanned = true
|
||
guard conn != 0 else { return }
|
||
|
||
// 先获取 key 总数
|
||
let totalKeys = readKeyCount()
|
||
guard totalKeys > 0 else { return }
|
||
|
||
var cpu: [String] = []
|
||
var gpu: [String] = []
|
||
|
||
for i in 0..<totalKeys {
|
||
guard let keyName = keyAtIndex(i),
|
||
let (dataType, _) = keyInfo(keyName) else { continue }
|
||
|
||
// 只关心浮点或 sp78 类型的温度 key
|
||
let isTempType = (dataType == "flt " || dataType == "sp78")
|
||
guard isTempType else { continue }
|
||
|
||
if keyName.hasPrefix("Tp") {
|
||
cpu.append(keyName)
|
||
} else if keyName.hasPrefix("Tg") {
|
||
gpu.append(keyName)
|
||
}
|
||
}
|
||
|
||
if !cpu.isEmpty { cpuKeys = cpu }
|
||
if !gpu.isEmpty { gpuKeys = gpu }
|
||
}
|
||
|
||
// MARK: - SMC 通信原语
|
||
|
||
private func readKeyCount() -> Int {
|
||
var input = SMCParamStruct()
|
||
input.key = fourCharCode("#KEY")
|
||
input.keyInfo.dataSize = 4
|
||
input.keyInfo.dataType = fourCharCode("ui32")
|
||
input.data8 = kSMCReadKey
|
||
|
||
guard let output = callSMC(&input),
|
||
output.result == kSMCSuccess else { return 0 }
|
||
|
||
let raw = UInt32(output.bytes.0) << 24
|
||
| UInt32(output.bytes.1) << 16
|
||
| UInt32(output.bytes.2) << 8
|
||
| UInt32(output.bytes.3)
|
||
return Int(raw)
|
||
}
|
||
|
||
private func keyAtIndex(_ index: Int) -> String? {
|
||
var input = SMCParamStruct()
|
||
input.data8 = kSMCGetKeyFromIndex
|
||
input.data32 = UInt32(index)
|
||
|
||
guard let output = callSMC(&input),
|
||
output.result == kSMCSuccess else { return nil }
|
||
|
||
return charCodeToString(output.key)
|
||
}
|
||
|
||
/// 查询 key 的类型和大小,返回 (dataType, dataSize)
|
||
private func keyInfo(_ keyName: String) -> (String, UInt32)? {
|
||
var input = SMCParamStruct()
|
||
input.key = fourCharCode(keyName)
|
||
input.data8 = kSMCGetKeyInfo
|
||
|
||
guard let output = callSMC(&input),
|
||
output.result == kSMCSuccess else { return nil }
|
||
|
||
return (charCodeToString(output.keyInfo.dataType),
|
||
output.keyInfo.dataSize)
|
||
}
|
||
|
||
/// 读取 key 的值(自动适配 flt / sp78 类型)
|
||
private func readKey(_ keyName: String) -> Double? {
|
||
guard let (dataType, dataSize) = keyInfo(keyName) else { return nil }
|
||
|
||
var input = SMCParamStruct()
|
||
input.key = fourCharCode(keyName)
|
||
input.keyInfo.dataSize = dataSize
|
||
input.keyInfo.dataType = fourCharCode(dataType)
|
||
input.data8 = kSMCReadKey
|
||
|
||
guard let output = callSMC(&input),
|
||
output.result == kSMCSuccess else { return nil }
|
||
|
||
switch dataType {
|
||
case "flt ":
|
||
// 32-bit IEEE 754 float(小端序)
|
||
let raw = UInt32(output.bytes.0)
|
||
| (UInt32(output.bytes.1) << 8)
|
||
| (UInt32(output.bytes.2) << 16)
|
||
| (UInt32(output.bytes.3) << 24)
|
||
return Double(Float(bitPattern: raw))
|
||
|
||
case "sp78":
|
||
// 16-bit signed fixed-point
|
||
let sign = (output.bytes.0 & 0x80) == 0 ? 1.0 : -1.0
|
||
return sign * (Double(output.bytes.0 & 0x7F) + Double(output.bytes.1) / 256.0)
|
||
|
||
default:
|
||
return nil
|
||
}
|
||
}
|
||
|
||
/// 发送 SMCParamStruct 到 AppleSMC
|
||
private func callSMC(_ input: inout SMCParamStruct) -> SMCParamStruct? {
|
||
var output = SMCParamStruct()
|
||
let size = MemoryLayout<SMCParamStruct>.stride // 80
|
||
var outputSize = size
|
||
|
||
let result = IOConnectCallStructMethod(
|
||
conn,
|
||
kSMCHandleYPCEvent,
|
||
&input, size,
|
||
&output, &outputSize
|
||
)
|
||
|
||
guard result == KERN_SUCCESS else { return nil }
|
||
return output
|
||
}
|
||
|
||
// MARK: - 工具
|
||
|
||
private func fourCharCode(_ string: String) -> UInt32 {
|
||
var result: UInt32 = 0
|
||
for char in string.utf8 {
|
||
result = (result << 8) | UInt32(char)
|
||
}
|
||
return result
|
||
}
|
||
|
||
private func charCodeToString(_ code: UInt32) -> String {
|
||
let c1 = UInt8((code >> 24) & 0xFF)
|
||
let c2 = UInt8((code >> 16) & 0xFF)
|
||
let c3 = UInt8((code >> 8) & 0xFF)
|
||
let c4 = UInt8(code & 0xFF)
|
||
return String(UnicodeScalar(c1))
|
||
+ String(UnicodeScalar(c2))
|
||
+ String(UnicodeScalar(c3))
|
||
+ String(UnicodeScalar(c4))
|
||
}
|
||
}
|