working temps and freq

This commit is contained in:
Hariharan Mudaliar 2025-10-19 15:43:44 +05:30
commit 0f64d69680
9 changed files with 1222 additions and 9 deletions

View file

@ -14350,6 +14350,10 @@
},
"Sensitivity" : {
},
"Sensors" : {
"comment" : "A label displayed above the group of CPU sensor gauges.",
"isCommentAutoGenerated" : true
},
"Session · ↓ %@ · ↑ %@" : {
"comment" : "A row displaying the total downloaded and uploaded bytes for the network session.",

View file

@ -29,7 +29,9 @@ struct CPUStatsDetailView: View {
coreCount: statsManager.cpuCoreUsage.count,
systemColor: systemColor,
userColor: userColor,
idleColor: idleColor
idleColor: idleColor,
temperature: statsManager.cpuTemperature,
frequency: statsManager.cpuFrequency
)
}

View file

@ -60,6 +60,8 @@ struct CPUUsageDashboard: View {
let systemColor: Color
let userColor: Color
let idleColor: Color
let temperature: CPUTemperatureMetrics
let frequency: CPUFrequencyMetrics?
var body: some View {
let usageSection = VStack(alignment: .leading, spacing: 12) {
@ -85,6 +87,14 @@ struct CPUUsageDashboard: View {
DetailRow(color: nil, label: "Load 15m", value: String(format: "%.2f", loadAverage.fifteenMinutes))
}
.frame(maxWidth: .infinity, alignment: .leading)
let sensorsSection = CPUSensorGaugeGroup(
temperature: temperature,
frequency: frequency,
usageTint: userColor,
temperatureTint: Color(red: 0.98, green: 0.53, blue: 0.18)
)
.frame(maxWidth: .infinity, alignment: .leading)
return ViewThatFits(in: .horizontal) {
VStack(alignment: .leading, spacing: 20) {
@ -97,19 +107,23 @@ struct CPUUsageDashboard: View {
.frame(width: 132, height: 132)
.frame(maxWidth: .infinity, alignment: .leading)
sensorsSection
usageSection
Divider().padding(.vertical, 4)
systemSection
}
HStack(alignment: .top, spacing: 28) {
CPUSegmentDonut(
breakdown: breakdown,
systemColor: systemColor,
userColor: userColor,
idleColor: idleColor
)
.frame(width: 132, height: 132)
VStack(alignment: .leading, spacing: 18) {
CPUSegmentDonut(
breakdown: breakdown,
systemColor: systemColor,
userColor: userColor,
idleColor: idleColor
)
.frame(width: 132, height: 132)
sensorsSection
}
usageSection
systemSection
}
@ -117,6 +131,95 @@ struct CPUUsageDashboard: View {
}
}
private struct CPUSensorGaugeGroup: View {
let temperature: CPUTemperatureMetrics
let frequency: CPUFrequencyMetrics?
let usageTint: Color
let temperatureTint: Color
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Sensors")
.font(.caption)
.foregroundColor(.secondary)
HStack(spacing: 18) {
CPUTemperatureGauge(temperature: temperature.celsius, tint: temperatureTint)
CPUFrequencyGauge(frequency: frequency, tint: usageTint)
}
}
}
}
private struct CPUTemperatureGauge: View {
let temperature: Double?
let tint: Color
private let maxTemperature: Double = 110
var body: some View {
CircularGaugeView(
title: "Temperature",
value: normalizedValue,
tint: tint,
centerPrimaryText: centerPrimary,
subtitle: nil,
size: 84,
lineWidth: 9
)
}
private var normalizedValue: Double {
guard let temperature, maxTemperature > 0 else { return 0 }
return min(max(temperature / maxTemperature, 0), 1)
}
private var centerPrimary: String {
guard let temperature else { return "—" }
return String(format: "%.0f°", temperature)
}
}
private struct CPUFrequencyGauge: View {
let frequency: CPUFrequencyMetrics?
let tint: Color
var body: some View {
CircularGaugeView(
title: "Frequency",
value: normalizedValue,
tint: tint,
centerPrimaryText: centerPrimary,
centerSecondaryText: frequency != nil ? "GHz" : nil,
subtitle: subtitle,
size: 84,
lineWidth: 9
)
}
private var normalizedValue: Double {
guard let frequency else { return 0 }
let candidateMax = frequency.maxOverallGHz > 0 ? frequency.maxOverallGHz : frequency.overallGHz
let denominator = max(candidateMax, 1.0)
return min(max(frequency.overallGHz / denominator, 0), 1)
}
private var centerPrimary: String {
guard let frequency else { return "—" }
return String(format: "%.1f", frequency.overallGHz)
}
private var subtitle: String? {
guard let frequency else { return nil }
var components: [String] = []
if let eValue = frequency.eCoreGHz {
components.append("E \(String(format: "%.1f", eValue))")
}
if let pValue = frequency.pCoreGHz {
components.append("P \(String(format: "%.1f", pValue))")
}
return components.isEmpty ? nil : components.joined(separator: " • ")
}
}
private struct CPUSegmentDonut: View {
let breakdown: CPULoadBreakdown
let systemColor: Color

View file

@ -379,6 +379,8 @@ class StatsManager: ObservableObject {
@Published private(set) var cpuCoreUsage: [CPUCoreUsage] = []
@Published private(set) var cpuUptime: TimeInterval = 0
@Published private(set) var topCPUProcesses: [ProcessStats] = []
@Published private(set) var cpuTemperature: CPUTemperatureMetrics = CPUTemperatureMetrics(celsius: nil)
@Published private(set) var cpuFrequency: CPUFrequencyMetrics?
@Published private(set) var memoryBreakdown: MemoryBreakdown = .zero
@Published private(set) var gpuBreakdown: GPUBreakdown = .zero
@Published private(set) var gpuDevices: [GPUDeviceMetrics] = []
@ -439,6 +441,7 @@ class StatsManager: ObservableObject {
private let maxProcessEntries: Int = 20
private var isProcessRefreshInFlight = false
private let gpuCollector = GPUInfoCollector()
private let cpuSensorCollector = CPUSensorCollector()
// MARK: - Initialization
private init() {
@ -562,6 +565,8 @@ class StatsManager: ObservableObject {
diskDevices = []
previousInterfaceCounters.removeAll()
interfaceTotals.removeAll()
cpuTemperature = CPUTemperatureMetrics(celsius: nil)
cpuFrequency = nil
}
// MARK: - Private Methods
@ -656,6 +661,10 @@ class StatsManager: ObservableObject {
cpuCoreUsage = coreUsage
}
cpuUptime = ProcessInfo.processInfo.systemUptime
cpuTemperature = cpuSensorCollector.readTemperature()
if let frequencyMetrics = cpuSensorCollector.readFrequency() {
cpuFrequency = frequencyMetrics
}
// Update history arrays (sliding window)
updateHistory(value: newCpuUsage, history: &cpuHistory)

View file

@ -0,0 +1,162 @@
//
// AppleHardwareInfo.swift
// DynamicIsland
//
// Minimal system introspection helpers derived from the Stats project.
//
import Foundation
import IOKit
import Darwin
enum ApplePlatform: String {
case intel
case m1
case m1Pro
case m1Max
case m1Ultra
case m2
case m2Pro
case m2Max
case m2Ultra
case m3
case m3Pro
case m3Max
case m3Ultra
case m4
case m4Pro
case m4Max
case m4Ultra
}
struct CPUClusterFrequencies {
let eCoreFrequencies: [Int32]
let pCoreFrequencies: [Int32]
}
struct CPUClusterCounts {
let eCores: Int
let pCores: Int
}
final class AppleHardwareInfo {
static let shared = AppleHardwareInfo()
let cpuName: String?
let platform: ApplePlatform?
let logicalCoreCount: Int
let clusterCounts: CPUClusterCounts
let clusterFrequencies: CPUClusterFrequencies
private init() {
let name = AppleHardwareInfo.fetchCPUName()
cpuName = name
platform = AppleHardwareInfo.detectPlatform(from: name)
logicalCoreCount = AppleHardwareInfo.fetchLogicalCoreCount()
clusterCounts = AppleHardwareInfo.fetchClusterCounts()
clusterFrequencies = AppleHardwareInfo.fetchClusterFrequencies(cpuName: name ?? "")
}
private static func fetchCPUName() -> String? {
var size = 0
sysctlbyname("machdep.cpu.brand_string", nil, &size, nil, 0)
guard size > 0 else { return nil }
var buffer = [CChar](repeating: 0, count: size)
let result = sysctlbyname("machdep.cpu.brand_string", &buffer, &size, nil, 0)
guard result == 0 else { return nil }
return String(cString: buffer)
}
private static func fetchLogicalCoreCount() -> Int {
var cores: Int32 = 0
var size = MemoryLayout<Int32>.size
let result = sysctlbyname("hw.logicalcpu", &cores, &size, nil, 0)
return result == 0 ? Int(cores) : ProcessInfo.processInfo.processorCount
}
private static func detectPlatform(from cpuName: String?) -> ApplePlatform? {
guard let name = cpuName?.lowercased() else { return nil }
if name.contains("intel") { return .intel }
if name.contains("m1") {
if name.contains("ultra") { return .m1Ultra }
if name.contains("max") { return .m1Max }
if name.contains("pro") { return .m1Pro }
return .m1
}
if name.contains("m2") {
if name.contains("ultra") { return .m2Ultra }
if name.contains("max") { return .m2Max }
if name.contains("pro") { return .m2Pro }
return .m2
}
if name.contains("m3") {
if name.contains("ultra") { return .m3Ultra }
if name.contains("max") { return .m3Max }
if name.contains("pro") { return .m3Pro }
return .m3
}
if name.contains("m4") {
if name.contains("ultra") { return .m4Ultra }
if name.contains("max") { return .m4Max }
if name.contains("pro") { return .m4Pro }
return .m4
}
return nil
}
private static func fetchClusterCounts() -> CPUClusterCounts {
var iterator = io_iterator_t()
guard IOServiceGetMatchingServices(kIOMasterPortDefault, IOServiceMatching("AppleARMPE"), &iterator) == KERN_SUCCESS else {
return CPUClusterCounts(eCores: 0, pCores: 0)
}
defer { IOObjectRelease(iterator) }
var eCoreCount: Int32 = 0
var pCoreCount: Int32 = 0
while case let service = IOIteratorNext(iterator), service != 0 {
defer { IOObjectRelease(service) }
var children = io_iterator_t()
guard IORegistryEntryGetChildIterator(service, kIOServicePlane, &children) == KERN_SUCCESS else { continue }
defer { IOObjectRelease(children) }
while case let child = IOIteratorNext(children), child != 0 {
defer { IOObjectRelease(child) }
guard let name = di_getIOName(child), name == "cpus", let props = di_getIOProperties(child) else { continue }
if let data = props.object(forKey: "e-core-count") as? Data {
eCoreCount = data.withUnsafeBytes { $0.load(as: Int32.self) }
}
if let data = props.object(forKey: "p-core-count") as? Data {
pCoreCount = data.withUnsafeBytes { $0.load(as: Int32.self) }
}
}
}
return CPUClusterCounts(eCores: Int(eCoreCount), pCores: Int(pCoreCount))
}
private static func fetchClusterFrequencies(cpuName: String) -> CPUClusterFrequencies {
var iterator = io_iterator_t()
guard IOServiceGetMatchingServices(kIOMasterPortDefault, IOServiceMatching("AppleARMIODevice"), &iterator) == KERN_SUCCESS else {
return CPUClusterFrequencies(eCoreFrequencies: [], pCoreFrequencies: [])
}
defer { IOObjectRelease(iterator) }
let lowerName = cpuName.lowercased()
let isM4 = lowerName.contains("m4")
var eFreq: [Int32] = []
var pFreq: [Int32] = []
while case let service = IOIteratorNext(iterator), service != 0 {
defer { IOObjectRelease(service) }
guard let name = di_getIOName(service), name == "pmgr", let props = di_getIOProperties(service) else { continue }
if let data = props["voltage-states1-sram"] as? Data {
eFreq = di_convertCFDataToFrequencies(data, isM4: isM4)
}
if let data = props["voltage-states5-sram"] as? Data {
pFreq = di_convertCFDataToFrequencies(data, isM4: isM4)
}
}
return CPUClusterFrequencies(eCoreFrequencies: eFreq, pCoreFrequencies: pFreq)
}
}

View file

@ -0,0 +1,237 @@
//
// CPUSensorCollector.swift
// DynamicIsland
//
// Ports the Stats project's CPU temperature and frequency readers.
//
import Foundation
import IOKit
struct CPUFrequencyMetrics: Equatable {
let overallGHz: Double
let eCoreGHz: Double?
let pCoreGHz: Double?
let maxOverallGHz: Double
let maxECoreGHz: Double?
let maxPCoreGHz: Double?
}
struct CPUTemperatureMetrics: Equatable {
let celsius: Double?
}
final class CPUSensorCollector {
private let hardware = AppleHardwareInfo.shared
private var channels: CFMutableDictionary?
private var subscription: IOReportSubscriptionRef?
private var previousSample: (samples: CFDictionary, time: TimeInterval)?
init() {
setupFrequencyChannel()
}
deinit {
if let subscription {
IOReportCreateSamples(subscription, nil, nil) // best-effort flush
}
}
func readTemperature() -> CPUTemperatureMetrics {
let platform = hardware.platform
if let value = primaryTemperatureKeyCandidates.compactMap({ SMC.shared.getValue($0) }).first(where: { $0 < 110 }) {
return CPUTemperatureMetrics(celsius: value)
}
let list = temperatureFallbackKeys(for: platform)
var total: Double = 0
var count: Double = 0
for key in list {
if let value = SMC.shared.getValue(key), value < 110 {
total += value
count += 1
}
}
guard total > 0, count > 0 else {
return CPUTemperatureMetrics(celsius: nil)
}
return CPUTemperatureMetrics(celsius: total / count)
}
func readFrequency() -> CPUFrequencyMetrics? {
guard let channels, let subscription else { return nil }
let timestamp = Date().timeIntervalSince1970
guard let currentSample = IOReportCreateSamples(subscription, channels, nil)?.takeRetainedValue() else {
return nil
}
defer {
previousSample = (currentSample, timestamp)
}
guard let previous = previousSample,
let diff = IOReportCreateSamplesDelta(previous.samples, currentSample, nil)?.takeRetainedValue() else {
return nil
}
let samples = collectIOSamples(from: diff)
let eFrequencies = hardware.clusterFrequencies.eCoreFrequencies
let pFrequencies = hardware.clusterFrequencies.pCoreFrequencies
let eCount = max(hardware.clusterCounts.eCores, 0)
let pCount = max(hardware.clusterCounts.pCores, 0)
var eValues: [Double] = []
var pValues: [Double] = []
for sample in samples where sample.group == "CPU Stats" {
if sample.channel.hasPrefix("ECPU") {
let value = calculateFrequency(from: sample.delta, freqs: eFrequencies)
if value > 0 { eValues.append(value) }
} else if sample.channel.hasPrefix("PCPU") {
let value = calculateFrequency(from: sample.delta, freqs: pFrequencies)
if value > 0 { pValues.append(value) }
}
}
let eAverage = eValues.isEmpty ? nil : (eValues.reduce(0, +) / Double(eValues.count))
let pAverage = pValues.isEmpty ? nil : (pValues.reduce(0, +) / Double(pValues.count))
guard eAverage != nil || pAverage != nil else {
return nil
}
let overall: Double
if let eAverage, let pAverage, (eCount + pCount) > 0 {
overall = ((eAverage * Double(eCount)) + (pAverage * Double(pCount))) / Double(eCount + pCount)
} else if let eAverage {
overall = eAverage
} else if let pAverage {
overall = pAverage
} else {
return nil
}
let maxE = eFrequencies.max().map { Double($0) / 1000 }
let maxP = pFrequencies.max().map { Double($0) / 1000 }
let maxOverall: Double
if let maxE, let maxP, (eCount + pCount) > 0 {
maxOverall = ((maxE * Double(eCount)) + (maxP * Double(pCount))) / Double(max(eCount + pCount, 1))
} else if let maxE {
maxOverall = maxE
} else if let maxP {
maxOverall = maxP
} else {
maxOverall = (overall / 1000)
}
return CPUFrequencyMetrics(
overallGHz: overall / 1000,
eCoreGHz: eAverage.map { $0 / 1000 },
pCoreGHz: pAverage.map { $0 / 1000 },
maxOverallGHz: maxOverall,
maxECoreGHz: maxE,
maxPCoreGHz: maxP
)
}
// MARK: - Private Helpers
private func setupFrequencyChannel() {
let names = [
("CPU Stats" as CFString, "CPU Complex Performance States" as CFString),
("CPU Stats" as CFString, "CPU Core Performance States" as CFString)
]
var mergedChannels: CFMutableDictionary?
var list: [CFDictionary] = []
for (group, subgroup) in names {
if let channel = IOReportCopyChannelsInGroup(group, subgroup, 0, 0, 0)?.takeRetainedValue() {
list.append(channel)
}
}
guard let first = list.first else { return }
for channel in list.dropFirst() {
IOReportMergeChannels(first, channel, nil)
}
guard let copy = CFDictionaryCreateMutableCopy(kCFAllocatorDefault, CFDictionaryGetCount(first), first) else {
return
}
mergedChannels = copy
channels = mergedChannels
var dictionary: Unmanaged<CFMutableDictionary>?
subscription = IOReportCreateSubscription(nil, mergedChannels, &dictionary, 0, nil)
dictionary?.release()
}
private struct IOSample {
let group: String
let subGroup: String
let channel: String
let unit: String
let delta: CFDictionary
}
private func collectIOSamples(from data: CFDictionary) -> [IOSample] {
let key = "IOReportChannels" as CFString
var rawValue: UnsafeRawPointer?
let found = withUnsafeMutablePointer(to: &rawValue) { pointer -> Bool in
CFDictionaryGetValueIfPresent(data, Unmanaged.passUnretained(key).toOpaque(), pointer)
}
guard found, let rawValue else { return [] }
let array = unsafeBitCast(rawValue, to: CFArray.self)
var result: [IOSample] = []
for index in 0..<CFArrayGetCount(array) {
let element = CFArrayGetValueAtIndex(array, index)
let entry = unsafeBitCast(element, to: CFDictionary.self)
let group = IOReportChannelGetGroup(entry)?.takeUnretainedValue() as String? ?? ""
let subGroup = IOReportChannelGetSubGroup(entry)?.takeUnretainedValue() as String? ?? ""
let channel = IOReportChannelGetChannelName(entry)?.takeUnretainedValue() as String? ?? ""
let unit = IOReportChannelGetUnitLabel(entry)?.takeUnretainedValue() as String? ?? ""
result.append(IOSample(group: group, subGroup: subGroup, channel: channel, unit: unit, delta: entry))
}
return result
}
private func getResidencies(dict: CFDictionary) -> [(label: String, value: Int64)] {
let count = IOReportStateGetCount(dict)
var result: [(String, Int64)] = []
for index in 0..<count {
let name = IOReportStateGetNameForIndex(dict, index)?.takeUnretainedValue() as String? ?? ""
let value = IOReportStateGetResidency(dict, index)
result.append((name, value))
}
return result
}
private func calculateFrequency(from dict: CFDictionary, freqs: [Int32]) -> Double {
guard !freqs.isEmpty else { return 0 }
let states = getResidencies(dict: dict)
guard let offset = states.firstIndex(where: { !$0.label.elementsEqual("IDLE") && !$0.label.elementsEqual("DOWN") && !$0.label.elementsEqual("OFF") }) else {
return 0
}
let usage = states.dropFirst(offset).reduce(0.0) { $0 + Double($1.value) }
guard usage > 0 else { return 0 }
var total: Double = 0
for (index, frequency) in freqs.enumerated() {
let stateIndex = offset + index
guard states.indices.contains(stateIndex) else { continue }
let fraction = Double(states[stateIndex].value) / usage
total += fraction * Double(frequency)
}
return total
}
private var primaryTemperatureKeyCandidates: [String] {
["TC0D", "TC0E", "TC0F", "TC0P", "TC0H"]
}
private func temperatureFallbackKeys(for platform: ApplePlatform?) -> [String] {
switch platform {
case .m1, .m1Pro, .m1Max, .m1Ultra:
return ["Tp09", "Tp0T", "Tp01", "Tp05", "Tp0D", "Tp0H", "Tp0L", "Tp0P", "Tp0X", "Tp0b"]
case .m2, .m2Pro, .m2Max, .m2Ultra:
return ["Tp1h", "Tp1t", "Tp1p", "Tp1l", "Tp01", "Tp05", "Tp09", "Tp0D", "Tp0X", "Tp0b", "Tp0f", "Tp0j"]
case .m3, .m3Pro, .m3Max, .m3Ultra:
return ["Te05", "Te0L", "Te0P", "Te0S", "Tf04", "Tf09", "Tf0A", "Tf0B", "Tf0D", "Tf0E", "Tf44", "Tf49", "Tf4A", "Tf4B", "Tf4D", "Tf4E"]
case .m4, .m4Pro, .m4Max, .m4Ultra:
return ["Te05", "Te09", "Te0H", "Te0S", "Tp01", "Tp05", "Tp09", "Tp0D", "Tp0V", "Tp0Y", "Tp0b", "Tp0e"]
default:
return ["TC0P", "TC0E", "TC0F", "TC0H", "TC0D"]
}
}
}

View file

@ -0,0 +1,52 @@
//
// IOKitHelpers.swift
// DynamicIsland
//
// Helper utilities borrowed from the Stats project for working with IORegistry and CFData blobs.
//
import Foundation
import IOKit
import Darwin
func di_getIOProperties(_ entry: io_registry_entry_t) -> NSDictionary? {
var properties: Unmanaged<CFMutableDictionary>? = nil
guard IORegistryEntryCreateCFProperties(entry, &properties, kCFAllocatorDefault, 0) == kIOReturnSuccess else {
return nil
}
defer { properties?.release() }
return properties?.takeUnretainedValue()
}
func di_getIOName(_ entry: io_registry_entry_t) -> String? {
let pointer = UnsafeMutablePointer<io_name_t>.allocate(capacity: 1)
defer { pointer.deallocate() }
let result = IORegistryEntryGetName(entry, pointer)
guard result == kIOReturnSuccess else {
if let message = String(validatingUTF8: mach_error_string(result)) {
print("IORegistryEntryGetName error: \(message)")
}
return nil
}
return String(cString: UnsafeRawPointer(pointer).assumingMemoryBound(to: CChar.self))
}
func di_convertCFDataToFrequencies(_ data: Data, isM4: Bool = false) -> [Int32] {
guard !data.isEmpty else { return [] }
let bytes = [UInt8](data)
let multiplier: UInt32 = isM4 ? 1_000 : 1_000_000
var frequencies: [Int32] = []
for chunkStart in stride(from: 0, to: bytes.count, by: 8) {
let upperBound = min(chunkStart + 8, bytes.count)
let chunk = bytes[chunkStart..<upperBound]
if chunk.count < 4 { continue }
let value = chunk.prefix(4).enumerated().reduce(UInt32(0)) { partial, element in
partial | (UInt32(element.element) << (UInt32(element.offset) * 8))
}
frequencies.append(Int32(value / multiplier))
}
return frequencies
}

View file

@ -0,0 +1,112 @@
//
// IOReportBridging.swift
// DynamicIsland
//
// Lightweight runtime loader for private IOReport symbols used by the GPU/CPU collectors.
// Avoids static linking against the private framework by resolving symbols via dlopen/dlsym.
//
import Foundation
import Darwin
public typealias IOReportSubscriptionRef = OpaquePointer
private enum IOReportLoader {
private static let libraryHandle: UnsafeMutableRawPointer? = {
let paths = [
"/System/Library/PrivateFrameworks/IOReport.framework/IOReport",
"/usr/lib/libIOReport.dylib"
]
for path in paths {
if let handle = dlopen(path, RTLD_LAZY | RTLD_LOCAL) {
return handle
}
}
return nil
}()
static func symbol<T>(_ name: String, as type: T.Type) -> T? {
guard let handle = libraryHandle, let pointer = dlsym(handle, name) else {
return nil
}
return unsafeBitCast(pointer, to: T.self)
}
}
func IOReportCopyChannelsInGroup(_ group: CFString?, _ subGroup: CFString?, _ a: UInt64, _ b: UInt64, _ c: UInt64) -> Unmanaged<CFDictionary>? {
typealias Fn = @convention(c) (CFString?, CFString?, UInt64, UInt64, UInt64) -> Unmanaged<CFDictionary>?
guard let fn: Fn = IOReportLoader.symbol("IOReportCopyChannelsInGroup", as: Fn.self) else { return nil }
return fn(group, subGroup, a, b, c)
}
func IOReportMergeChannels(_ a: CFDictionary, _ b: CFDictionary, _ c: CFTypeRef?) {
typealias Fn = @convention(c) (CFDictionary, CFDictionary, CFTypeRef?) -> Void
guard let fn: Fn = IOReportLoader.symbol("IOReportMergeChannels", as: Fn.self) else { return }
fn(a, b, c)
}
func IOReportCreateSubscription(_ a: UnsafeMutableRawPointer?, _ b: CFMutableDictionary?, _ c: UnsafeMutablePointer<Unmanaged<CFMutableDictionary>?>?, _ d: UInt64, _ e: CFTypeRef?) -> IOReportSubscriptionRef? {
typealias Fn = @convention(c) (UnsafeMutableRawPointer?, CFMutableDictionary?, UnsafeMutablePointer<Unmanaged<CFMutableDictionary>?>?, UInt64, CFTypeRef?) -> IOReportSubscriptionRef?
guard let fn: Fn = IOReportLoader.symbol("IOReportCreateSubscription", as: Fn.self) else { return nil }
return fn(a, b, c, d, e)
}
func IOReportCreateSamples(_ subscription: IOReportSubscriptionRef?, _ channels: CFMutableDictionary?, _ client: CFTypeRef?) -> Unmanaged<CFDictionary>? {
typealias Fn = @convention(c) (IOReportSubscriptionRef?, CFMutableDictionary?, CFTypeRef?) -> Unmanaged<CFDictionary>?
guard let fn: Fn = IOReportLoader.symbol("IOReportCreateSamples", as: Fn.self) else { return nil }
return fn(subscription, channels, client)
}
func IOReportCreateSamplesDelta(_ previous: CFDictionary, _ current: CFDictionary, _ zone: CFTypeRef?) -> Unmanaged<CFDictionary>? {
typealias Fn = @convention(c) (CFDictionary, CFDictionary, CFTypeRef?) -> Unmanaged<CFDictionary>?
guard let fn: Fn = IOReportLoader.symbol("IOReportCreateSamplesDelta", as: Fn.self) else { return nil }
return fn(previous, current, zone)
}
func IOReportChannelGetGroup(_ dictionary: CFDictionary) -> Unmanaged<CFString>? {
typealias Fn = @convention(c) (CFDictionary) -> Unmanaged<CFString>?
guard let fn: Fn = IOReportLoader.symbol("IOReportChannelGetGroup", as: Fn.self) else { return nil }
return fn(dictionary)
}
func IOReportChannelGetSubGroup(_ dictionary: CFDictionary) -> Unmanaged<CFString>? {
typealias Fn = @convention(c) (CFDictionary) -> Unmanaged<CFString>?
guard let fn: Fn = IOReportLoader.symbol("IOReportChannelGetSubGroup", as: Fn.self) else { return nil }
return fn(dictionary)
}
func IOReportChannelGetChannelName(_ dictionary: CFDictionary) -> Unmanaged<CFString>? {
typealias Fn = @convention(c) (CFDictionary) -> Unmanaged<CFString>?
guard let fn: Fn = IOReportLoader.symbol("IOReportChannelGetChannelName", as: Fn.self) else { return nil }
return fn(dictionary)
}
func IOReportChannelGetUnitLabel(_ dictionary: CFDictionary) -> Unmanaged<CFString>? {
typealias Fn = @convention(c) (CFDictionary) -> Unmanaged<CFString>?
guard let fn: Fn = IOReportLoader.symbol("IOReportChannelGetUnitLabel", as: Fn.self) else { return nil }
return fn(dictionary)
}
func IOReportStateGetCount(_ dictionary: CFDictionary) -> Int32 {
typealias Fn = @convention(c) (CFDictionary) -> Int32
guard let fn: Fn = IOReportLoader.symbol("IOReportStateGetCount", as: Fn.self) else { return 0 }
return fn(dictionary)
}
func IOReportStateGetNameForIndex(_ dictionary: CFDictionary, _ index: Int32) -> Unmanaged<CFString>? {
typealias Fn = @convention(c) (CFDictionary, Int32) -> Unmanaged<CFString>?
guard let fn: Fn = IOReportLoader.symbol("IOReportStateGetNameForIndex", as: Fn.self) else { return nil }
return fn(dictionary, index)
}
func IOReportStateGetResidency(_ dictionary: CFDictionary, _ index: Int32) -> Int64 {
typealias Fn = @convention(c) (CFDictionary, Int32) -> Int64
guard let fn: Fn = IOReportLoader.symbol("IOReportStateGetResidency", as: Fn.self) else { return 0 }
return fn(dictionary, index)
}
func IOReportSimpleGetIntegerValue(_ dictionary: CFDictionary, _ index: Int32) -> Int64 {
typealias Fn = @convention(c) (CFDictionary, Int32) -> Int64
guard let fn: Fn = IOReportLoader.symbol("IOReportSimpleGetIntegerValue", as: Fn.self) else { return 0 }
return fn(dictionary, index)
}

View file

@ -0,0 +1,532 @@
//
// smc.swift
// SMC
//
// Created by Serhiy Mytrovtsiy on 25/05/2021.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved.
//
import Foundation
import IOKit
internal enum SMCDataType: String {
case UI8 = "ui8 "
case UI16 = "ui16"
case UI32 = "ui32"
case SP1E = "sp1e"
case SP3C = "sp3c"
case SP4B = "sp5b"
case SP5A = "sp5a"
case SPA5 = "spa5"
case SP69 = "sp669"
case SP78 = "sp78"
case SP87 = "sp87"
case SP96 = "sp96"
case SPB4 = "spb4"
case SPF0 = "spf0"
case FLT = "flt "
case FPE2 = "fpe2"
case FP2E = "fp2e"
case FDS = "{fds"
}
internal enum SMCKeys: UInt8 {
case kernelIndex = 2
case readBytes = 5
case writeBytes = 6
case readIndex = 8
case readKeyInfo = 9
case readPLimit = 11
case readVers = 12
}
public enum FanMode: Int, Codable {
case automatic = 0
case forced = 1
}
internal struct SMCKeyData_t {
typealias SMCBytes_t = (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 vers_t {
var major: CUnsignedChar = 0
var minor: CUnsignedChar = 0
var build: CUnsignedChar = 0
var reserved: CUnsignedChar = 0
var release: CUnsignedShort = 0
}
struct LimitData_t {
var version: UInt16 = 0
var length: UInt16 = 0
var cpuPLimit: UInt32 = 0
var gpuPLimit: UInt32 = 0
var memPLimit: UInt32 = 0
}
struct keyInfo_t {
var dataSize: IOByteCount32 = 0
var dataType: UInt32 = 0
var dataAttributes: UInt8 = 0
}
var key: UInt32 = 0
var vers = vers_t()
var pLimitData = LimitData_t()
var keyInfo = keyInfo_t()
var padding: UInt16 = 0
var result: UInt8 = 0
var status: UInt8 = 0
var data8: UInt8 = 0
var data32: UInt32 = 0
var bytes: SMCBytes_t = (UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0))
}
internal struct SMCVal_t {
var key: String
var dataSize: UInt32 = 0
var dataType: String = ""
var bytes: [UInt8] = Array(repeating: 0, count: 32)
init(_ key: String) {
self.key = key
}
}
extension FourCharCode {
init(fromString str: String) {
precondition(str.count == 4)
self = str.utf8.reduce(0) { sum, character in
return sum << 8 | UInt32(character)
}
}
func toString() -> String {
return String(describing: UnicodeScalar(self >> 24 & 0xff)!) +
String(describing: UnicodeScalar(self >> 16 & 0xff)!) +
String(describing: UnicodeScalar(self >> 8 & 0xff)!) +
String(describing: UnicodeScalar(self & 0xff)!)
}
}
extension UInt16 {
init(bytes: (UInt8, UInt8)) {
self = UInt16(bytes.0) << 8 | UInt16(bytes.1)
}
}
extension UInt32 {
init(bytes: (UInt8, UInt8, UInt8, UInt8)) {
self = UInt32(bytes.0) << 24 | UInt32(bytes.1) << 16 | UInt32(bytes.2) << 8 | UInt32(bytes.3)
}
}
extension Int {
init(fromFPE2 bytes: (UInt8, UInt8)) {
self = (Int(bytes.0) << 6) + (Int(bytes.1) >> 2)
}
}
extension Float {
init?(_ bytes: [UInt8]) {
self = bytes.withUnsafeBytes {
return $0.load(fromByteOffset: 0, as: Self.self)
}
}
var bytes: [UInt8] {
withUnsafeBytes(of: self, Array.init)
}
}
public class SMC {
public static let shared = SMC()
private var conn: io_connect_t = 0
public init() {
var result: kern_return_t
var iterator: io_iterator_t = 0
let device: io_object_t
let matchingDictionary: CFMutableDictionary = IOServiceMatching("AppleSMC")
result = IOServiceGetMatchingServices(kIOMasterPortDefault, matchingDictionary, &iterator)
if result != kIOReturnSuccess {
print("Error IOServiceGetMatchingServices(): " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return
}
device = IOIteratorNext(iterator)
IOObjectRelease(iterator)
if device == 0 {
print("Error IOIteratorNext(): " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return
}
result = IOServiceOpen(device, mach_task_self_, 0, &conn)
IOObjectRelease(device)
if result != kIOReturnSuccess {
print("Error IOServiceOpen(): " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return
}
}
deinit {
let result = self.close()
if result != kIOReturnSuccess {
print("error close smc connection: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
}
}
public func close() -> kern_return_t {
return IOServiceClose(conn)
}
public func getValue(_ key: String) -> Double? {
var result: kern_return_t = 0
var val: SMCVal_t = SMCVal_t(key)
result = read(&val)
if result != kIOReturnSuccess {
print("Error read(\(key)): " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return nil
}
if val.dataSize > 0 {
if val.bytes.first(where: { $0 != 0 }) == nil && val.key != "FS! " && val.key != "F0Md" && val.key != "F1Md" {
return nil
}
switch val.dataType {
case SMCDataType.UI8.rawValue:
return Double(val.bytes[0])
case SMCDataType.UI16.rawValue:
return Double(UInt16(bytes: (val.bytes[0], val.bytes[1])))
case SMCDataType.UI32.rawValue:
return Double(UInt32(bytes: (val.bytes[0], val.bytes[1], val.bytes[2], val.bytes[3])))
case SMCDataType.SP1E.rawValue:
let result: Double = Double(UInt16(val.bytes[0]) * 256 + UInt16(val.bytes[1]))
return Double(result / 16384)
case SMCDataType.SP3C.rawValue:
let result: Double = Double(UInt16(val.bytes[0]) * 256 + UInt16(val.bytes[1]))
return Double(result / 4096)
case SMCDataType.SP4B.rawValue:
let result: Double = Double(UInt16(val.bytes[0]) * 256 + UInt16(val.bytes[1]))
return Double(result / 2048)
case SMCDataType.SP5A.rawValue:
let result: Double = Double(UInt16(val.bytes[0]) * 256 + UInt16(val.bytes[1]))
return Double(result / 1024)
case SMCDataType.SP69.rawValue:
let result: Double = Double(UInt16(val.bytes[0]) * 256 + UInt16(val.bytes[1]))
return Double(result / 512)
case SMCDataType.SP78.rawValue:
let intValue: Double = Double(Int(val.bytes[0]) * 256 + Int(val.bytes[1]))
return Double(intValue / 256)
case SMCDataType.SP87.rawValue:
let intValue: Double = Double(Int(val.bytes[0]) * 256 + Int(val.bytes[1]))
return Double(intValue / 128)
case SMCDataType.SP96.rawValue:
let intValue: Double = Double(Int(val.bytes[0]) * 256 + Int(val.bytes[1]))
return Double(intValue / 64)
case SMCDataType.SPA5.rawValue:
let result: Double = Double(UInt16(val.bytes[0]) * 256 + UInt16(val.bytes[1]))
return Double(result / 32)
case SMCDataType.SPB4.rawValue:
let intValue: Double = Double(Int(val.bytes[0]) * 256 + Int(val.bytes[1]))
return Double(intValue / 16)
case SMCDataType.SPF0.rawValue:
let intValue: Double = Double(Int(val.bytes[0]) * 256 + Int(val.bytes[1]))
return intValue
case SMCDataType.FLT.rawValue:
let value: Float? = Float(val.bytes)
if value != nil {
return Double(value!)
}
return nil
case SMCDataType.FPE2.rawValue:
return Double(Int(fromFPE2: (val.bytes[0], val.bytes[1])))
default:
return nil
}
}
return nil
}
public func getStringValue(_ key: String) -> String? {
var result: kern_return_t = 0
var val: SMCVal_t = SMCVal_t(key)
result = read(&val)
if result != kIOReturnSuccess {
print("Error read(): " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return nil
}
if val.dataSize > 0 {
if val.bytes.first(where: { $0 != 0}) == nil {
return nil
}
switch val.dataType {
case SMCDataType.FDS.rawValue:
let c1 = String(UnicodeScalar(val.bytes[4]))
let c2 = String(UnicodeScalar(val.bytes[5]))
let c3 = String(UnicodeScalar(val.bytes[6]))
let c4 = String(UnicodeScalar(val.bytes[7]))
let c5 = String(UnicodeScalar(val.bytes[8]))
let c6 = String(UnicodeScalar(val.bytes[9]))
let c7 = String(UnicodeScalar(val.bytes[10]))
let c8 = String(UnicodeScalar(val.bytes[11]))
let c9 = String(UnicodeScalar(val.bytes[12]))
let c10 = String(UnicodeScalar(val.bytes[13]))
let c11 = String(UnicodeScalar(val.bytes[14]))
let c12 = String(UnicodeScalar(val.bytes[15]))
return (c1 + c2 + c3 + c4 + c5 + c6 + c7 + c8 + c9 + c10 + c11 + c12).trimmingCharacters(in: .whitespaces)
default:
print("unsupported data type \(val.dataType) for key: \(key)")
return nil
}
}
return nil
}
public func getAllKeys() -> [String] {
var list: [String] = []
let keysNum: Double? = self.getValue("#KEY")
if keysNum == nil {
print("ERROR no keys count found")
return list
}
var result: kern_return_t = 0
var input: SMCKeyData_t = SMCKeyData_t()
var output: SMCKeyData_t = SMCKeyData_t()
for i in 0...Int(keysNum!) {
input = SMCKeyData_t()
output = SMCKeyData_t()
input.data8 = SMCKeys.readIndex.rawValue
input.data32 = UInt32(i)
result = call(SMCKeys.kernelIndex.rawValue, input: &input, output: &output)
if result != kIOReturnSuccess {
continue
}
list.append(output.key.toString())
}
return list
}
public func write(_ key: String, _ newValue: Int) -> kern_return_t {
var value = SMCVal_t(key)
value.dataSize = 2
value.bytes = [UInt8(newValue >> 6), UInt8((newValue << 2) ^ ((newValue >> 6) << 8)), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0)]
return write(value)
}
// MARK: - fans
public func setFanMode(_ id: Int, mode: FanMode) {
if self.getValue("F\(id)Md") != nil {
var result: kern_return_t = 0
var value = SMCVal_t("F\(id)Md")
result = read(&value)
if result != kIOReturnSuccess {
print("Error read fan mode: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return
}
value.bytes = [UInt8(mode.rawValue), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0)]
result = write(value)
if result != kIOReturnSuccess {
print("Error write: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return
}
}
let fansMode = Int(self.getValue("FS! ") ?? 0)
var newMode: UInt8 = 0
if fansMode == 0 && id == 0 && mode == .forced {
newMode = 1
} else if fansMode == 0 && id == 1 && mode == .forced {
newMode = 2
} else if fansMode == 1 && id == 0 && mode == .automatic {
newMode = 0
} else if fansMode == 1 && id == 1 && mode == .forced {
newMode = 3
} else if fansMode == 2 && id == 1 && mode == .automatic {
newMode = 0
} else if fansMode == 2 && id == 0 && mode == .forced {
newMode = 3
} else if fansMode == 3 && id == 0 && mode == .automatic {
newMode = 2
} else if fansMode == 3 && id == 1 && mode == .automatic {
newMode = 1
}
if fansMode == newMode {
return
}
var result: kern_return_t = 0
var value = SMCVal_t("FS! ")
result = read(&value)
if result != kIOReturnSuccess {
print("Error read fan mode: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return
}
value.bytes = [0, newMode, UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0)]
result = write(value)
if result != kIOReturnSuccess {
print("Error write: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return
}
}
public func setFanSpeed(_ id: Int, speed: Int) {
let maxSpeed = Int(self.getValue("F\(id)Mx") ?? 4000)
if speed > maxSpeed {
print("new fan speed (\(speed)) is more than maximum speed (\(maxSpeed))")
return
}
var result: kern_return_t = 0
var value = SMCVal_t("F\(id)Tg")
result = read(&value)
if result != kIOReturnSuccess {
print("Error read fan value: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return
}
if value.dataType == "flt " {
let bytes = Float(speed).bytes
value.bytes[0] = bytes[0]
value.bytes[1] = bytes[1]
value.bytes[2] = bytes[2]
value.bytes[3] = bytes[3]
} else if value.dataType == "fpe2" {
value.bytes[0] = UInt8(speed >> 6)
value.bytes[1] = UInt8((speed << 2) ^ ((speed >> 6) << 8))
value.bytes[2] = UInt8(0)
value.bytes[3] = UInt8(0)
}
result = write(value)
if result != kIOReturnSuccess {
print("Error write: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return
}
}
public func resetFans() {
var value = SMCVal_t("FS! ")
value.dataSize = 2
let result = write(value)
if result != kIOReturnSuccess {
print("Error write: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
}
}
// MARK: - internal functions
private func read(_ value: UnsafeMutablePointer<SMCVal_t>) -> kern_return_t {
var result: kern_return_t = 0
var input = SMCKeyData_t()
var output = SMCKeyData_t()
input.key = FourCharCode(fromString: value.pointee.key)
input.data8 = SMCKeys.readKeyInfo.rawValue
result = call(SMCKeys.kernelIndex.rawValue, input: &input, output: &output)
if result != kIOReturnSuccess {
return result
}
value.pointee.dataSize = UInt32(output.keyInfo.dataSize)
value.pointee.dataType = output.keyInfo.dataType.toString()
input.keyInfo.dataSize = output.keyInfo.dataSize
input.data8 = SMCKeys.readBytes.rawValue
result = call(SMCKeys.kernelIndex.rawValue, input: &input, output: &output)
if result != kIOReturnSuccess {
return result
}
memcpy(&value.pointee.bytes, &output.bytes, Int(value.pointee.dataSize))
return kIOReturnSuccess
}
private func write(_ value: SMCVal_t) -> kern_return_t {
var input = SMCKeyData_t()
var output = SMCKeyData_t()
input.key = FourCharCode(fromString: value.key)
input.data8 = SMCKeys.writeBytes.rawValue
input.keyInfo.dataSize = IOByteCount32(value.dataSize)
input.bytes = (value.bytes[0], value.bytes[1], value.bytes[2], value.bytes[3], value.bytes[4], value.bytes[5],
value.bytes[6], value.bytes[7], value.bytes[8], value.bytes[9], value.bytes[10], value.bytes[11],
value.bytes[12], value.bytes[13], value.bytes[14], value.bytes[15], value.bytes[16], value.bytes[17],
value.bytes[18], value.bytes[19], value.bytes[20], value.bytes[21], value.bytes[22], value.bytes[23],
value.bytes[24], value.bytes[25], value.bytes[26], value.bytes[27], value.bytes[28], value.bytes[29],
value.bytes[30], value.bytes[31])
let result = self.call(SMCKeys.kernelIndex.rawValue, input: &input, output: &output)
if result != kIOReturnSuccess {
return result
}
return kIOReturnSuccess
}
private func call(_ index: UInt8, input: inout SMCKeyData_t, output: inout SMCKeyData_t) -> kern_return_t {
let inputSize = MemoryLayout<SMCKeyData_t>.stride
var outputSize = MemoryLayout<SMCKeyData_t>.stride
return IOConnectCallStructMethod(conn, UInt32(index), &input, inputSize, &output, &outputSize)
}
}