313 lines
13 KiB
Swift
313 lines
13 KiB
Swift
import Foundation
|
|
import SwiftUI
|
|
import Observation
|
|
|
|
// MARK: - Typed Telemetry Models
|
|
|
|
public struct CPULoadMetrics: Sendable {
|
|
public var systemLoad: Double = 0
|
|
public var userLoad: Double = 0
|
|
public var idleLoad: Double = 100
|
|
public var totalLoad: Double = 0
|
|
public var perCoreLoad: [Double] = []
|
|
public var cpuFrequencyMHz: Double = 0
|
|
public var isThrottled: Bool = false
|
|
}
|
|
|
|
public struct MemoryMetrics: Sendable {
|
|
public var totalBytes: UInt64 = 0
|
|
public var usedBytes: UInt64 = 0
|
|
public var freeBytes: UInt64 = 0
|
|
public var activeBytes: UInt64 = 0
|
|
public var inactiveBytes: UInt64 = 0
|
|
public var wiredBytes: UInt64 = 0
|
|
public var compressedBytes: UInt64 = 0
|
|
public var swapTotalBytes: UInt64 = 0
|
|
public var swapUsedBytes: UInt64 = 0
|
|
public var utilizationPercentage: Double = 0
|
|
public var pressureLevel: String = "Normal"
|
|
}
|
|
|
|
public struct StorageVolumeItem: Identifiable, Sendable {
|
|
public var id: String { mountPoint }
|
|
public let mountPoint: String
|
|
public let volumeName: String
|
|
public let fileSystem: String
|
|
public let totalBytes: UInt64
|
|
public let freeBytes: UInt64
|
|
public let usedBytes: UInt64
|
|
public let usedPercentage: Double
|
|
public let isReadOnly: Bool
|
|
}
|
|
|
|
public struct CPUThermalMetrics: Sendable {
|
|
public var packageTemperature: Double = 0
|
|
public var averageCoreTemperature: Double = 0
|
|
public var peakCoreTemperature: Double = 0
|
|
public var coreTemperatures: [(index: Int, key: String, temperature: Double)] = []
|
|
public var thermalPressureState: String = "Nominal"
|
|
public var isThrottling: Bool = false
|
|
}
|
|
|
|
public struct FanTelemetryItem: Identifiable, Sendable {
|
|
public var id: Int { index }
|
|
public let index: Int
|
|
public let name: String
|
|
public let currentRPM: Double
|
|
public let minRPM: Double
|
|
public let maxRPM: Double
|
|
public let targetRPM: Double
|
|
public let utilization: Double
|
|
}
|
|
|
|
public struct ComponentThermalItem: Identifiable, Sendable {
|
|
public var id: String { key }
|
|
public let key: String
|
|
public let name: String
|
|
public let category: String
|
|
public let temperature: Double
|
|
}
|
|
|
|
public struct PowerMetrics: Sendable {
|
|
public var systemTotalWatts: Double = 0
|
|
public var cpuWatts: Double = 0
|
|
public var gpuWatts: Double = 0
|
|
public var memoryWatts: Double = 0
|
|
public var cpuVoltage: Double = 0
|
|
public var cpuCurrent: Double = 0
|
|
public var powerSource: String = "AC Power"
|
|
public var isCharging: Bool = false
|
|
public var batteryLevel: Double = 100.0
|
|
public var sensorReadings: [(key: String, name: String, value: Double, unit: String)] = []
|
|
}
|
|
|
|
// MARK: - SystemTelemetryStore
|
|
|
|
@Observable
|
|
@MainActor
|
|
public final class SystemTelemetryStore {
|
|
public static let shared = SystemTelemetryStore()
|
|
|
|
public private(set) var isRunning: Bool = false
|
|
public private(set) var lastUpdateTimestamp: Date = .now
|
|
public private(set) var sampleInterval: TimeInterval = 1.0
|
|
|
|
/// Raw snapshots organized by provider identifier
|
|
public private(set) var latestSnapshot: [String: [String: Any]] = [:]
|
|
|
|
/// Structured Telemetry Metrics
|
|
public private(set) var cpuLoad = CPULoadMetrics()
|
|
public private(set) var memory = MemoryMetrics()
|
|
public private(set) var storageVolumes: [StorageVolumeItem] = []
|
|
public private(set) var cpuThermal = CPUThermalMetrics()
|
|
public private(set) var fans: [FanTelemetryItem] = []
|
|
public private(set) var componentTemps: [ComponentThermalItem] = []
|
|
public private(set) var power = PowerMetrics()
|
|
|
|
/// System identification
|
|
public let hostModel: String
|
|
public let osVersion: String
|
|
public let kernelVersion: String
|
|
public let physicalCpuCount: Int
|
|
public let logicalCpuCount: Int
|
|
|
|
private let coordinator = MMTelemetryCoordinator.shared
|
|
|
|
private init() {
|
|
self.hostModel = SystemTelemetryStore.readSysctlString("hw.model") ?? "Intel Mac"
|
|
self.osVersion = ProcessInfo.processInfo.operatingSystemVersionString
|
|
self.kernelVersion = SystemTelemetryStore.readSysctlString("kern.osrelease") ?? "Unknown Kernel"
|
|
self.physicalCpuCount = SystemTelemetryStore.readSysctlInt("hw.physicalcpu") ?? 1
|
|
self.logicalCpuCount = SystemTelemetryStore.readSysctlInt("hw.logicalcpu") ?? 1
|
|
|
|
registerDefaultProviders()
|
|
setupCoordinator()
|
|
}
|
|
|
|
private func registerDefaultProviders() {
|
|
// Register all Primary System & Thermal Telemetry providers (M2)
|
|
coordinator.register(MMCPULoadProvider())
|
|
coordinator.register(MMMemoryTelemetryProvider())
|
|
coordinator.register(MMStorageTelemetryProvider())
|
|
coordinator.register(MMCPUThermalProvider())
|
|
coordinator.register(MMFanTelemetryProvider())
|
|
coordinator.register(MMComponentThermalProvider())
|
|
coordinator.register(MMPowerTelemetryProvider())
|
|
}
|
|
|
|
private func setupCoordinator() {
|
|
coordinator.sampleInterval = self.sampleInterval
|
|
coordinator.snapshotHandler = { [weak self] snapshot in
|
|
guard let self else { return }
|
|
self.latestSnapshot = snapshot
|
|
self.lastUpdateTimestamp = Date()
|
|
self.decodeSnapshot(snapshot)
|
|
}
|
|
}
|
|
|
|
private func decodeSnapshot(_ snapshot: [String: [String: Any]]) {
|
|
// 1. CPU Load
|
|
if let cpuDict = snapshot["com.i3omb.macmonitor.telemetry.cpu"] {
|
|
var m = CPULoadMetrics()
|
|
m.systemLoad = (cpuDict["systemLoad"] as? NSNumber)?.doubleValue ?? 0
|
|
m.userLoad = (cpuDict["userLoad"] as? NSNumber)?.doubleValue ?? 0
|
|
m.idleLoad = (cpuDict["idleLoad"] as? NSNumber)?.doubleValue ?? 100
|
|
m.totalLoad = (cpuDict["totalLoad"] as? NSNumber)?.doubleValue ?? 0
|
|
if let perCore = cpuDict["perCoreLoad"] as? [NSNumber] {
|
|
m.perCoreLoad = perCore.map { $0.doubleValue }
|
|
}
|
|
m.cpuFrequencyMHz = (cpuDict["cpuFrequencyMHz"] as? NSNumber)?.doubleValue ?? 0
|
|
m.isThrottled = (cpuDict["isThrottled"] as? NSNumber)?.boolValue ?? false
|
|
self.cpuLoad = m
|
|
}
|
|
|
|
// 2. Memory
|
|
if let memDict = snapshot["com.i3omb.macmonitor.telemetry.memory"] {
|
|
var m = MemoryMetrics()
|
|
m.totalBytes = (memDict["totalBytes"] as? NSNumber)?.uint64Value ?? 0
|
|
m.usedBytes = (memDict["usedBytes"] as? NSNumber)?.uint64Value ?? 0
|
|
m.freeBytes = (memDict["freeBytes"] as? NSNumber)?.uint64Value ?? 0
|
|
m.activeBytes = (memDict["activeBytes"] as? NSNumber)?.uint64Value ?? 0
|
|
m.inactiveBytes = (memDict["inactiveBytes"] as? NSNumber)?.uint64Value ?? 0
|
|
m.wiredBytes = (memDict["wiredBytes"] as? NSNumber)?.uint64Value ?? 0
|
|
m.compressedBytes = (memDict["compressedBytes"] as? NSNumber)?.uint64Value ?? 0
|
|
m.swapTotalBytes = (memDict["swapTotalBytes"] as? NSNumber)?.uint64Value ?? 0
|
|
m.swapUsedBytes = (memDict["swapUsedBytes"] as? NSNumber)?.uint64Value ?? 0
|
|
m.utilizationPercentage = (memDict["utilizationPercentage"] as? NSNumber)?.doubleValue ?? 0
|
|
m.pressureLevel = memDict["pressureLevel"] as? String ?? "Normal"
|
|
self.memory = m
|
|
}
|
|
|
|
// 3. Storage
|
|
if let stgDict = snapshot["com.i3omb.macmonitor.telemetry.storage"],
|
|
let volArray = stgDict["volumes"] as? [[String: Any]] {
|
|
self.storageVolumes = volArray.compactMap { dict in
|
|
guard let mount = dict["mountPoint"] as? String else { return nil }
|
|
return StorageVolumeItem(
|
|
mountPoint: mount,
|
|
volumeName: dict["volumeName"] as? String ?? mount,
|
|
fileSystem: dict["fileSystem"] as? String ?? "APFS",
|
|
totalBytes: (dict["totalBytes"] as? NSNumber)?.uint64Value ?? 0,
|
|
freeBytes: (dict["freeBytes"] as? NSNumber)?.uint64Value ?? 0,
|
|
usedBytes: (dict["usedBytes"] as? NSNumber)?.uint64Value ?? 0,
|
|
usedPercentage: (dict["usedPercentage"] as? NSNumber)?.doubleValue ?? 0,
|
|
isReadOnly: (dict["isReadOnly"] as? NSNumber)?.boolValue ?? false
|
|
)
|
|
}
|
|
}
|
|
|
|
// 4. CPU Thermal
|
|
if let thDict = snapshot["com.i3omb.macmonitor.telemetry.thermal.cpu"] {
|
|
var m = CPUThermalMetrics()
|
|
m.packageTemperature = (thDict["packageTemperature"] as? NSNumber)?.doubleValue ?? 0
|
|
m.averageCoreTemperature = (thDict["averageCoreTemperature"] as? NSNumber)?.doubleValue ?? 0
|
|
m.peakCoreTemperature = (thDict["peakCoreTemperature"] as? NSNumber)?.doubleValue ?? 0
|
|
m.thermalPressureState = thDict["thermalPressureState"] as? String ?? "Nominal"
|
|
m.isThrottling = (thDict["isThrottling"] as? NSNumber)?.boolValue ?? false
|
|
if let coresList = thDict["coreTemperatures"] as? [[String: Any]] {
|
|
m.coreTemperatures = coresList.compactMap { d in
|
|
let idx = (d["coreIndex"] as? NSNumber)?.intValue ?? 0
|
|
let key = d["key"] as? String ?? "TC\(idx)C"
|
|
let temp = (d["temperature"] as? NSNumber)?.doubleValue ?? 0
|
|
return (index: idx, key: key, temperature: temp)
|
|
}
|
|
}
|
|
self.cpuThermal = m
|
|
}
|
|
|
|
// 5. Fans
|
|
if let fanDict = snapshot["com.i3omb.macmonitor.telemetry.fan"],
|
|
let fanArray = fanDict["fans"] as? [[String: Any]] {
|
|
self.fans = fanArray.compactMap { d in
|
|
let idx = (d["index"] as? NSNumber)?.intValue ?? 0
|
|
return FanTelemetryItem(
|
|
index: idx,
|
|
name: d["name"] as? String ?? "Fan \(idx + 1)",
|
|
currentRPM: (d["currentRPM"] as? NSNumber)?.doubleValue ?? 0,
|
|
minRPM: (d["minRPM"] as? NSNumber)?.doubleValue ?? 0,
|
|
maxRPM: (d["maxRPM"] as? NSNumber)?.doubleValue ?? 0,
|
|
targetRPM: (d["targetRPM"] as? NSNumber)?.doubleValue ?? 0,
|
|
utilization: (d["utilization"] as? NSNumber)?.doubleValue ?? 0
|
|
)
|
|
}
|
|
}
|
|
|
|
// 6. Component Thermals
|
|
if let compDict = snapshot["com.i3omb.macmonitor.telemetry.thermal.components"],
|
|
let compArray = compDict["components"] as? [[String: Any]] {
|
|
self.componentTemps = compArray.compactMap { d in
|
|
guard let key = d["key"] as? String else { return nil }
|
|
return ComponentThermalItem(
|
|
key: key,
|
|
name: d["name"] as? String ?? key,
|
|
category: d["category"] as? String ?? "Other",
|
|
temperature: (d["temperature"] as? NSNumber)?.doubleValue ?? 0
|
|
)
|
|
}
|
|
}
|
|
|
|
// 7. Power
|
|
if let pwrDict = snapshot["com.i3omb.macmonitor.telemetry.power"] {
|
|
var m = PowerMetrics()
|
|
m.systemTotalWatts = (pwrDict["systemTotalWatts"] as? NSNumber)?.doubleValue ?? 0
|
|
m.cpuWatts = (pwrDict["cpuWatts"] as? NSNumber)?.doubleValue ?? 0
|
|
m.gpuWatts = (pwrDict["gpuWatts"] as? NSNumber)?.doubleValue ?? 0
|
|
m.memoryWatts = (pwrDict["memoryWatts"] as? NSNumber)?.doubleValue ?? 0
|
|
m.cpuVoltage = (pwrDict["cpuVoltage"] as? NSNumber)?.doubleValue ?? 0
|
|
m.cpuCurrent = (pwrDict["cpuCurrent"] as? NSNumber)?.doubleValue ?? 0
|
|
m.powerSource = pwrDict["powerSource"] as? String ?? "AC Power"
|
|
m.isCharging = (pwrDict["isCharging"] as? NSNumber)?.boolValue ?? false
|
|
m.batteryLevel = (pwrDict["batteryLevel"] as? NSNumber)?.doubleValue ?? 100.0
|
|
if let sensorList = pwrDict["sensors"] as? [[String: Any]] {
|
|
m.sensorReadings = sensorList.compactMap { d in
|
|
guard let k = d["key"] as? String else { return nil }
|
|
let n = d["name"] as? String ?? k
|
|
let v = (d["value"] as? NSNumber)?.doubleValue ?? 0
|
|
let u = d["unit"] as? String ?? ""
|
|
return (key: k, name: n, value: v, unit: u)
|
|
}
|
|
}
|
|
self.power = m
|
|
}
|
|
}
|
|
|
|
public func start() {
|
|
coordinator.start()
|
|
isRunning = coordinator.isRunning
|
|
}
|
|
|
|
public func stop() {
|
|
coordinator.stop()
|
|
isRunning = coordinator.isRunning
|
|
}
|
|
|
|
public func setSampleInterval(_ interval: TimeInterval) {
|
|
self.sampleInterval = interval
|
|
coordinator.sampleInterval = interval
|
|
}
|
|
|
|
public func triggerManualSample() {
|
|
coordinator.sampleImmediately()
|
|
}
|
|
|
|
public var registeredProviderCount: Int {
|
|
coordinator.registeredProviders().count
|
|
}
|
|
|
|
private static func readSysctlString(_ name: String) -> String? {
|
|
var size: size_t = 0
|
|
sysctlbyname(name, nil, &size, nil, 0)
|
|
guard size > 0 else { return nil }
|
|
var buffer = [CChar](repeating: 0, count: size)
|
|
sysctlbyname(name, &buffer, &size, nil, 0)
|
|
return String(cString: buffer)
|
|
}
|
|
|
|
private static func readSysctlInt(_ name: String) -> Int? {
|
|
var value: Int32 = 0
|
|
var size = MemoryLayout<Int32>.size
|
|
let result = sysctlbyname(name, &value, &size, nil, 0)
|
|
return result == 0 ? Int(value) : nil
|
|
}
|
|
}
|