Files

240 lines
8.6 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.
//
// SMCConnection.swift
// OpenStats
//
// 与 AppleSMC 用户客户端通信。结构体布局与读写流程移植自 exelban/stats (MIT):
// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved.
//
import Foundation
import IOKit
import Localization
public struct SMCKey: Hashable, Sendable, CustomStringConvertible {
public let code: UInt32
public init(_ string: String) {
precondition(string.utf8.count == 4, "SMC key must be 4 chars")
code = string.utf8.reduce(0) { $0 << 8 | UInt32($1) }
}
init(code: UInt32) { self.code = code }
public var description: String {
let bytes = [24, 16, 8, 0].map { UInt8(truncatingIfNeeded: code >> $0) }
return String(decoding: bytes, as: UTF8.self)
}
}
public struct SMCKeyInfo: Sendable, Equatable {
public let size: UInt32
public let type: String
}
public enum SMCError: Error, Sendable, CustomStringConvertible {
case serviceNotFound
case openFailed(kern_return_t)
case callFailed(SMCKey, kern_return_t)
case firmwareRejected(SMCKey, UInt8)
case unsupportedType(SMCKey, String)
public var description: String {
switch self {
case .serviceNotFound: tr("未找到 AppleSMC 服务")
case .openFailed(let kr): tr("打开 SMC 失败 (\(Self.message(kr)))")
case .callFailed(let key, let kr): tr("SMC [\(key)] 调用失败 (\(Self.message(kr)))")
case .firmwareRejected(let key, let code): tr("SMC [\(key)] 被固件拒绝 (0x\(String(code, radix: 16)))")
case .unsupportedType(let key, let type): tr("SMC [\(key)] 不支持的数据类型 \(type)")
}
}
private static func message(_ kr: kern_return_t) -> String {
String(cString: mach_error_string(kr))
}
}
/// 与内核 `SMCParamStruct` 等长(80 字节),字段顺序不可调整。
struct SMCParam {
typealias Bytes = (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)
struct Version { var major: UInt8 = 0, minor: UInt8 = 0, build: UInt8 = 0, reserved: UInt8 = 0, release: UInt16 = 0 }
struct PLimit { var version: UInt16 = 0, length: UInt16 = 0, cpu: UInt32 = 0, gpu: UInt32 = 0, mem: UInt32 = 0 }
struct KeyInfo { var dataSize: UInt32 = 0, dataType: UInt32 = 0, dataAttributes: UInt8 = 0 }
var key: UInt32 = 0
var vers = Version()
var pLimit = PLimit()
var keyInfo = KeyInfo()
var padding: UInt16 = 0
var result: UInt8 = 0
var status: UInt8 = 0
var data8: UInt8 = 0
var data32: UInt32 = 0
var bytes: Bytes = (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)
}
private enum SMCSelector: UInt8 {
case kernelIndex = 2
case readBytes = 5
case writeBytes = 6
case readIndex = 8
case readKeyInfo = 9
}
/// 非线程安全:请在单一 actor / 串行队列内使用。
public final class SMCConnection {
private var connection: io_connect_t = 0
private var infoCache: [SMCKey: SMCKeyInfo] = [:]
public init() throws {
let service = IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching("AppleSMC"))
guard service != 0 else { throw SMCError.serviceNotFound }
defer { IOObjectRelease(service) }
let kr = IOServiceOpen(service, mach_task_self_, 0, &connection)
guard kr == KERN_SUCCESS else { throw SMCError.openFailed(kr) }
}
deinit {
IOServiceClose(connection)
}
// MARK: 读取
public func info(_ key: SMCKey) throws -> SMCKeyInfo {
if let cached = infoCache[key] { return cached }
var input = SMCParam()
input.key = key.code
input.data8 = SMCSelector.readKeyInfo.rawValue
let output = try call(key, &input)
let info = SMCKeyInfo(size: output.keyInfo.dataSize, type: SMCKey(code: output.keyInfo.dataType).description)
infoCache[key] = info
return info
}
public func bytes(_ key: SMCKey) throws -> (SMCKeyInfo, [UInt8]) {
let info = try info(key)
var input = SMCParam()
input.key = key.code
input.keyInfo.dataSize = info.size
input.data8 = SMCSelector.readBytes.rawValue
var output = try call(key, &input)
let count = min(Int(info.size), 32)
let bytes = withUnsafeBytes(of: &output.bytes) { Array($0.prefix(count)) }
return (info, bytes)
}
/// 读取数值型键,失败或类型不支持时返回 nil。
public func double(_ key: SMCKey) -> Double? {
guard let (info, bytes) = try? bytes(key) else { return nil }
return SMCDecoder.decode(type: info.type, bytes: bytes)
}
public func keyCount() -> Int {
Int(double(SMCKey("#KEY")) ?? 0)
}
public func key(at index: Int) -> SMCKey? {
var input = SMCParam()
input.data8 = SMCSelector.readIndex.rawValue
input.data32 = UInt32(index)
guard let output = try? call(SMCKey(code: 0), &input) else { return nil }
return SMCKey(code: output.key)
}
public func allKeys() -> [SMCKey] {
(0..<keyCount()).compactMap { key(at: $0) }
}
// MARK: 写入(需要 root)
public func write(_ key: SMCKey, bytes newBytes: [UInt8]) throws {
let info = try info(key)
var input = SMCParam()
input.key = key.code
input.data8 = SMCSelector.writeBytes.rawValue
input.keyInfo.dataSize = info.size
withUnsafeMutableBytes(of: &input.bytes) { buffer in
for (i, byte) in newBytes.prefix(min(Int(info.size), 32)).enumerated() { buffer[i] = byte }
}
let output = try call(key, &input)
// IOKit 返回成功时,SMC 固件仍可能拒绝写入
guard output.result == 0 else { throw SMCError.firmwareRejected(key, output.result) }
}
public func write(_ key: SMCKey, value: Double) throws {
let info = try info(key)
guard let encoded = SMCDecoder.encode(type: info.type, value: value) else {
throw SMCError.unsupportedType(key, info.type)
}
try write(key, bytes: encoded)
}
public func exists(_ key: SMCKey) -> Bool {
guard let info = try? info(key) else { return false }
return info.size > 0
}
private func call(_ key: SMCKey, _ input: inout SMCParam) throws -> SMCParam {
var output = SMCParam()
var outputSize = MemoryLayout<SMCParam>.stride
let kr = IOConnectCallStructMethod(connection, UInt32(SMCSelector.kernelIndex.rawValue),
&input, MemoryLayout<SMCParam>.stride, &output, &outputSize)
guard kr == KERN_SUCCESS else { throw SMCError.callFailed(key, kr) }
// result 0x84 表示键不存在
if output.result == 0x84 { throw SMCError.callFailed(key, kIOReturnNotFound) }
return output
}
}
public enum SMCDecoder {
public static func decode(type: String, bytes: [UInt8]) -> Double? {
func be16() -> Double? { bytes.count >= 2 ? Double(UInt16(bytes[0]) << 8 | UInt16(bytes[1])) : nil }
func signed16() -> Double? { bytes.count >= 2 ? Double(Int16(bitPattern: UInt16(bytes[0]) << 8 | UInt16(bytes[1]))) : nil }
switch type {
case "flt ":
guard bytes.count >= 4 else { return nil }
return Double(bytes.withUnsafeBytes { $0.loadUnaligned(as: Float32.self) })
case "ui8 ", "flag":
return bytes.first.map(Double.init)
case "ui16":
return be16()
case "ui32":
guard bytes.count >= 4 else { return nil }
return Double(bytes.prefix(4).reduce(UInt32(0)) { $0 << 8 | UInt32($1) })
case "fpe2":
return be16().map { $0 / 4 }
case "sp78":
return signed16().map { $0 / 256 }
case "sp87":
return signed16().map { $0 / 128 }
case "sp96":
return signed16().map { $0 / 64 }
case "spb4":
return signed16().map { $0 / 16 }
case "spf0":
return signed16()
default:
return nil
}
}
public static func encode(type: String, value: Double) -> [UInt8]? {
switch type {
case "flt ":
return withUnsafeBytes(of: Float32(value)) { Array($0) }
case "ui8 ", "flag":
return [UInt8(clamping: Int(value))]
case "fpe2":
let raw = UInt16(clamping: Int(value * 4))
return [UInt8(raw >> 8), UInt8(raw & 0xff)]
default:
return nil
}
}
}