2020-06-23 00:03:00 +02:00
//
// r e a d e r s . s w i f t
2020-07-06 15:52:57 +02:00
// S e n s o r s
2020-06-23 00:03:00 +02:00
//
// C r e a t e d b y S e r h i y M y t r o v t s i y o n 1 7 / 0 6 / 2 0 2 0 .
// U s i n g S w i f t 5 . 0 .
// R u n n i n g o n m a c O S 1 0 . 1 5 .
//
// C o p y r i g h t © 2 0 2 0 S e r h i y M y t r o v t s i y . A l l r i g h t s r e s e r v e d .
//
import Cocoa
2021-06-04 19:37:29 +02:00
import Kit
2020-06-23 00:03:00 +02:00
2023-06-27 17:09:38 +02:00
internal class SensorsReader : Reader < Sensors_List > {
2022-01-11 17:51:11 +01:00
static let HIDtypes : [ SensorType ] = [ . temperature , . voltage ]
2022-08-27 15:35:26 +02:00
2023-06-27 17:09:38 +02:00
internal var list : Sensors_List = Sensors_List ( )
2022-08-27 15:35:26 +02:00
2022-08-21 06:53:42 -04:00
private var lastRead : Date = Date ( )
private let firstRead : Date = Date ( )
2022-01-11 17:51:11 +01:00
2022-01-14 20:34:39 +01:00
private var HIDState : Bool {
2023-04-28 21:03:32 +02:00
Store . shared . bool ( key : " Sensors_hid " , defaultValue : false )
2022-11-11 14:27:31 +01:00
}
2022-12-22 18:13:34 +01:00
private var unknownSensorsState : Bool
2022-01-14 20:34:39 +01:00
2025-02-08 10:55:48 +01:00
private var channels : CFMutableDictionary ? = nil
private var subscription : IOReportSubscriptionRef ? = nil
private var powers : ( CPU : Double , GPU : Double , ANE : Double , RAM : Double , PCI : Double ) = ( 0.0 , 0.0 , 0.0 , 0.0 , 0.0 )
2024-02-10 17:03:47 +01:00
init ( callback : @ escaping ( T ? ) -> Void = { _ in } ) {
2022-12-22 18:13:34 +01:00
self . unknownSensorsState = Store . shared . bool ( key : " Sensors_unknown " , defaultValue : false )
2024-02-10 17:03:47 +01:00
super . init ( . sensors , callback : callback )
2025-02-08 10:55:48 +01:00
self . channels = self . getChannels ( )
var dict : Unmanaged < CFMutableDictionary > ?
self . subscription = IOReportCreateSubscription ( nil , self . channels , & dict , 0 , nil )
dict ? . release ( )
2023-06-27 17:09:38 +02:00
self . list . sensors = self . sensors ( )
2022-11-11 14:27:31 +01:00
}
private func sensors ( ) -> [ Sensor_p ] {
2021-03-25 22:14:20 +01:00
var available : [ String ] = SMC . shared . getAllKeys ( )
2022-11-13 15:51:39 +01:00
var list : [ Sensor_p ] = [ ]
2022-01-15 13:57:09 +01:00
var sensorsList = SensorsList
2022-08-27 15:35:26 +02:00
if let platform = SystemKit . shared . device . platform {
sensorsList = sensorsList . filter ( { $0 . platforms . contains ( platform ) } )
}
2021-08-07 12:33:23 +03:00
2021-08-09 14:01:07 +03:00
if let count = SMC . shared . getValue ( " FNum " ) {
2022-11-13 15:51:39 +01:00
list += self . loadFans ( Int ( count ) )
2021-08-07 12:33:23 +03:00
}
2020-06-23 00:03:00 +02:00
available = available . filter ( { ( key : String ) -> Bool in
switch key . prefix ( 1 ) {
2022-01-03 17:26:50 +01:00
case " T " , " V " , " P " , " I " : return true
2020-06-23 00:03:00 +02:00
default : return false
}
} )
2022-01-14 20:34:39 +01:00
sensorsList . forEach { ( s : Sensor ) in
2020-07-16 19:43:43 +02:00
if let idx = available . firstIndex ( where : { $0 = = s . key } ) {
list . append ( s )
available . remove ( at : idx )
2020-06-23 00:03:00 +02:00
}
}
2022-01-14 20:34:39 +01:00
sensorsList . filter { $0 . key . contains ( " % " ) } . forEach { ( s : Sensor ) in
2020-12-10 19:55:46 +01:00
var index = 1
for i in 0. . < 10 {
let key = s . key . replacingOccurrences ( of : " % " , with : " \( i ) " )
2022-11-11 14:27:31 +01:00
if let idx = available . firstIndex ( where : { $0 = = key } ) {
2020-12-10 19:55:46 +01:00
var sensor = s . copy ( )
sensor . key = key
sensor . name = s . name . replacingOccurrences ( of : " % " , with : " \( index ) " )
list . append ( sensor )
2022-11-11 14:27:31 +01:00
available . remove ( at : idx )
2020-12-10 19:55:46 +01:00
index += 1
}
}
}
2022-12-22 18:13:34 +01:00
available . forEach { ( key : String ) in
var type : SensorType ? = nil
switch key . prefix ( 1 ) {
case " T " : type = . temperature
case " V " : type = . voltage
case " P " : type = . power
case " I " : type = . current
default : type = nil
}
if let t = type {
list . append ( Sensor ( key : key , name : key , group : . unknown , type : t , platforms : [ ] ) )
2022-11-11 14:27:31 +01:00
}
}
2020-12-10 19:55:46 +01:00
2021-04-10 15:50:02 +02:00
for sensor in list {
2021-03-25 22:14:20 +01:00
if let newValue = SMC . shared . getValue ( sensor . key ) {
2020-09-05 00:05:04 +02:00
if let idx = list . firstIndex ( where : { $0 . key = = sensor . key } ) {
list [ idx ] . value = newValue
}
}
}
2022-11-11 14:27:31 +01:00
var results : [ Sensor_p ] = [ ]
2022-11-13 15:51:39 +01:00
results += list . filter ( { ( s : Sensor_p ) -> Bool in
2022-01-14 20:34:39 +01:00
if s . type = = . temperature && ( s . value = = 0 || s . value > 110 ) {
2021-04-10 15:50:02 +02:00
return false
2022-05-22 19:03:36 +02:00
} else if s . type = = . current && s . value > 100 {
return false
2021-04-10 15:50:02 +02:00
}
return true
} )
2022-01-11 17:51:11 +01:00
#if arch ( arm64 )
2022-01-14 20:34:39 +01:00
if self . HIDState {
2022-11-11 14:27:31 +01:00
results += self . initHIDSensors ( )
2022-01-14 20:34:39 +01:00
}
2025-02-08 10:55:48 +01:00
results += self . initIOSensors ( )
2022-01-11 17:51:11 +01:00
#endif
2022-11-14 17:53:55 +01:00
results += self . initCalculatedSensors ( results )
2022-02-10 17:23:18 +01:00
2022-11-11 14:27:31 +01:00
return results
2020-06-23 00:03:00 +02:00
}
public override func read ( ) {
2023-06-27 17:09:38 +02:00
for i in self . list . sensors . indices {
guard self . list . sensors [ i ] . group != . hid && ! self . list . sensors [ i ] . isComputed else { continue }
if ! self . unknownSensorsState && self . list . sensors [ i ] . group = = . unknown { continue }
2023-08-05 23:48:45 +02:00
var newValue = SMC . shared . getValue ( self . list . sensors [ i ] . key ) ? ? 0
if self . list . sensors [ i ] . type = = . temperature && self . list . sensors [ i ] . group = = . CPU &&
( newValue < 10 || newValue > 120 ) { // f i x f o r m 2 b r o k e n s e n s o r s
newValue = self . list . sensors [ i ] . value
}
self . list . sensors [ i ] . value = newValue
2022-02-10 17:23:18 +01:00
}
2023-06-27 17:09:38 +02:00
var cpuSensors = self . list . sensors . filter ( { $0 . group = = . CPU && $0 . type = = . temperature && $0 . average } ) . map { $0 . value }
var gpuSensors = self . list . sensors . filter ( { $0 . group = = . GPU && $0 . type = = . temperature && $0 . average } ) . map { $0 . value }
let fanSensors = self . list . sensors . filter ( { $0 . type = = . fan && ! $0 . isComputed } )
2022-02-10 17:23:18 +01:00
2022-01-11 17:51:11 +01:00
#if arch ( arm64 )
2022-01-14 20:34:39 +01:00
if self . HIDState {
for typ in SensorsReader . HIDtypes {
let ( page , usage , type ) = self . m1Preset ( type : typ )
AppleSiliconSensors ( page , usage , type ) . forEach { ( key , value ) in
guard let key = key as ? String , let value = value as ? Double , value < 300 && value >= 0 else {
return
}
2023-06-27 17:09:38 +02:00
if let idx = self . list . sensors . firstIndex ( where : { $0 . group = = . hid && $0 . key = = key } ) {
self . list . sensors [ idx ] . value = value
2022-01-14 20:34:39 +01:00
}
2022-01-11 17:51:11 +01:00
}
}
2022-01-14 20:34:39 +01:00
2023-06-27 17:09:38 +02:00
cpuSensors += self . list . sensors . filter ( { $0 . key . hasPrefix ( " pACC MTR Temp " ) || $0 . key . hasPrefix ( " eACC MTR Temp " ) } ) . map { $0 . value }
gpuSensors += self . list . sensors . filter ( { $0 . key . hasPrefix ( " GPU MTR Temp " ) } ) . map { $0 . value }
2022-04-07 20:06:23 +02:00
2023-06-27 17:09:38 +02:00
let socSensors = self . list . sensors . filter ( { $0 . key . hasPrefix ( " SOC MTR Temp " ) } ) . map { $0 . value }
2022-04-07 20:06:23 +02:00
if ! socSensors . isEmpty {
2023-06-27 17:09:38 +02:00
if let idx = self . list . sensors . firstIndex ( where : { $0 . key = = " Average SOC " } ) {
self . list . sensors [ idx ] . value = socSensors . reduce ( 0 , + ) / Double ( socSensors . count )
2022-04-07 20:06:23 +02:00
}
if let max = socSensors . max ( ) {
2023-06-27 17:09:38 +02:00
if let idx = self . list . sensors . firstIndex ( where : { $0 . key = = " Hottest SOC " } ) {
self . list . sensors [ idx ] . value = max
2022-04-07 20:06:23 +02:00
}
}
}
2022-01-11 17:51:11 +01:00
}
2025-02-08 10:55:48 +01:00
if let ( cpu , gpu , ane , ram , pci ) = self . IOSensors ( ) {
if let idx = self . list . sensors . firstIndex ( where : { $0 . key = = " CPU Power " } ) {
self . list . sensors [ idx ] . value = cpu
}
if let idx = self . list . sensors . firstIndex ( where : { $0 . key = = " GPU Power " } ) {
self . list . sensors [ idx ] . value = gpu
}
if let idx = self . list . sensors . firstIndex ( where : { $0 . key = = " ANE Power " } ) {
self . list . sensors [ idx ] . value = ane
}
if let idx = self . list . sensors . firstIndex ( where : { $0 . key = = " RAM Power " } ) {
self . list . sensors [ idx ] . value = ram
}
if let idx = self . list . sensors . firstIndex ( where : { $0 . key = = " PCI Power " } ) {
self . list . sensors [ idx ] . value = pci
}
}
2022-01-11 17:51:11 +01:00
#endif
2022-02-10 17:23:18 +01:00
if ! cpuSensors . isEmpty {
2023-06-27 17:09:38 +02:00
if let idx = self . list . sensors . firstIndex ( where : { $0 . key = = " Average CPU " } ) {
self . list . sensors [ idx ] . value = cpuSensors . reduce ( 0 , + ) / Double ( cpuSensors . count )
2022-02-10 17:23:18 +01:00
}
if let max = cpuSensors . max ( ) {
2023-06-27 17:09:38 +02:00
if let idx = self . list . sensors . firstIndex ( where : { $0 . key = = " Hottest CPU " } ) {
self . list . sensors [ idx ] . value = max
2022-02-10 17:23:18 +01:00
}
}
}
if ! gpuSensors . isEmpty {
2023-06-27 17:09:38 +02:00
if let idx = self . list . sensors . firstIndex ( where : { $0 . key = = " Average GPU " } ) {
self . list . sensors [ idx ] . value = gpuSensors . reduce ( 0 , + ) / Double ( gpuSensors . count )
2022-02-10 17:23:18 +01:00
}
if let max = gpuSensors . max ( ) {
2023-06-27 17:09:38 +02:00
if let idx = self . list . sensors . firstIndex ( where : { $0 . key = = " Hottest GPU " } ) {
self . list . sensors [ idx ] . value = max
2022-02-10 17:23:18 +01:00
}
2022-01-11 17:51:11 +01:00
}
2020-06-23 00:03:00 +02:00
}
2022-03-06 19:26:19 +08:00
if ! fanSensors . isEmpty && fanSensors . count > 1 {
2023-03-07 18:22:39 +01:00
if let f = fanSensors . max ( by : { $0 . value < $1 . value } ) as ? Fan {
2023-12-30 19:49:07 +01:00
if let idx = self . list . sensors . firstIndex ( where : { $0 . key = = " Fastest fan " } ) {
2023-06-27 17:09:38 +02:00
if var fan = self . list . sensors [ idx ] as ? Fan {
2023-03-07 20:54:35 +01:00
fan . value = f . value
fan . minSpeed = f . minSpeed
fan . maxSpeed = f . maxSpeed
2023-06-27 17:09:38 +02:00
self . list . sensors [ idx ] = fan
2023-03-07 20:54:35 +01:00
}
2022-03-06 19:26:19 +08:00
}
}
}
2022-01-11 17:51:11 +01:00
2023-06-27 17:09:38 +02:00
if let PSTRSensor = self . list . sensors . first ( where : { $0 . key = = " PSTR " } ) , PSTRSensor . value > 0 {
2022-09-21 17:33:53 +02:00
let sinceLastRead = Date ( ) . timeIntervalSince ( self . lastRead )
let sinceFirstRead = Date ( ) . timeIntervalSince ( self . firstRead )
2023-06-27 17:09:38 +02:00
if let totalIdx = self . list . sensors . firstIndex ( where : { $0 . key = = " Total System Consumption " } ) , sinceLastRead > 0 {
self . list . sensors [ totalIdx ] . value += PSTRSensor . value * sinceLastRead / 3600
if let avgIdx = self . list . sensors . firstIndex ( where : { $0 . key = = " Average System Total " } ) , sinceFirstRead > 0 {
self . list . sensors [ avgIdx ] . value = self . list . sensors [ totalIdx ] . value * 3600 / sinceFirstRead
2022-08-21 06:53:42 -04:00
}
}
2022-09-21 17:33:53 +02:00
self . lastRead = Date ( )
2022-08-21 06:53:42 -04:00
}
2023-03-10 20:25:38 +01:00
// c u t o f f l o w d c i n v o l t a g e
2023-06-27 17:09:38 +02:00
if let idx = self . list . sensors . firstIndex ( where : { $0 . key = = " VD0R " } ) , self . list . sensors [ idx ] . value < 0.4 {
self . list . sensors [ idx ] . value = 0
2023-03-10 20:25:38 +01:00
}
// c u t o f f l o w d c i n c u r r e n t
2023-06-27 17:09:38 +02:00
if let idx = self . list . sensors . firstIndex ( where : { $0 . key = = " ID0R " } ) , self . list . sensors [ idx ] . value < 0.05 {
self . list . sensors [ idx ] . value = 0
2023-03-10 20:25:38 +01:00
}
2020-06-23 00:03:00 +02:00
self . callback ( self . list )
}
2021-08-07 12:33:23 +03:00
2023-03-07 18:22:39 +01:00
private func initCalculatedSensors ( _ sensors : [ Sensor_p ] ) -> [ Sensor_p ] {
var list : [ Sensor_p ] = [ ]
2022-08-27 15:35:26 +02:00
2022-11-14 17:53:55 +01:00
var cpuSensors = sensors . filter ( { $0 . group = = . CPU && $0 . type = = . temperature && $0 . average } ) . map { $0 . value }
var gpuSensors = sensors . filter ( { $0 . group = = . GPU && $0 . type = = . temperature && $0 . average } ) . map { $0 . value }
2022-08-27 15:35:26 +02:00
#if arch ( arm64 )
if self . HIDState {
2022-11-14 17:53:55 +01:00
cpuSensors += sensors . filter ( { $0 . key . hasPrefix ( " pACC MTR Temp " ) || $0 . key . hasPrefix ( " eACC MTR Temp " ) } ) . map { $0 . value }
gpuSensors += sensors . filter ( { $0 . key . hasPrefix ( " GPU MTR Temp " ) } ) . map { $0 . value }
2022-08-27 15:35:26 +02:00
}
#endif
2023-03-07 18:22:39 +01:00
let fanSensors = sensors . filter ( { $0 . type = = . fan && ! $0 . isComputed } )
2022-08-27 15:35:26 +02:00
if ! cpuSensors . isEmpty {
let value = cpuSensors . reduce ( 0 , + ) / Double ( cpuSensors . count )
list . append ( Sensor ( key : " Average CPU " , name : " Average CPU " , value : value , group : . CPU , type : . temperature , platforms : Platform . all , isComputed : true ) )
if let max = cpuSensors . max ( ) {
list . append ( Sensor ( key : " Hottest CPU " , name : " Hottest CPU " , value : max , group : . CPU , type : . temperature , platforms : Platform . all , isComputed : true ) )
}
}
if ! gpuSensors . isEmpty {
let value = gpuSensors . reduce ( 0 , + ) / Double ( gpuSensors . count )
list . append ( Sensor ( key : " Average GPU " , name : " Average GPU " , value : value , group : . GPU , type : . temperature , platforms : Platform . all , isComputed : true ) )
if let max = gpuSensors . max ( ) {
list . append ( Sensor ( key : " Hottest GPU " , name : " Hottest GPU " , value : max , group : . GPU , type : . temperature , platforms : Platform . all , isComputed : true ) )
}
}
if ! fanSensors . isEmpty && fanSensors . count > 1 {
2023-03-07 18:22:39 +01:00
if let f = fanSensors . max ( by : { $0 . value < $1 . value } ) as ? Fan {
2023-12-30 19:49:07 +01:00
list . append ( Fan ( id : - 1 , key : " Fastest fan " , name : " Fastest fan " , minSpeed : f . minSpeed , maxSpeed : f . maxSpeed , value : f . value , mode : . automatic , isComputed : true ) )
2022-08-27 15:35:26 +02:00
}
}
// I n i t t o t a l p o w e r s i n c e l a u n c h e d , o n l y i f T o t a l P o w e r s e n s o r i s a v a i l a b l e
2022-11-14 17:53:55 +01:00
if sensors . contains ( where : { $0 . key = = " PSTR " } ) {
2022-08-27 15:35:26 +02:00
list . append ( Sensor ( key : " Total System Consumption " , name : " Total System Consumption " , value : 0 , group : . sensor , type : . energy , platforms : Platform . all , isComputed : true ) )
list . append ( Sensor ( key : " Average System Total " , name : " Average System Total " , value : 0 , group : . sensor , type : . power , platforms : Platform . all , isComputed : true ) )
}
return list . filter ( { ( s : Sensor_p ) -> Bool in
switch s . type {
case . temperature :
return s . value < 110 && s . value >= 0
case . voltage :
return s . value < 300 && s . value >= 0
case . current :
return s . value < 100 && s . value >= 0
default : return true
}
} ) . sorted { $0 . key . lowercased ( ) < $1 . key . lowercased ( ) }
}
2022-11-11 14:27:31 +01:00
public func unknownCallback ( ) {
2022-12-22 18:13:34 +01:00
self . unknownSensorsState = Store . shared . bool ( key : " Sensors_unknown " , defaultValue : false )
2022-11-11 14:27:31 +01:00
}
2022-08-27 15:35:26 +02:00
}
// MARK: - F a n s
extension SensorsReader {
2022-11-13 15:51:39 +01:00
private func loadFans ( _ count : Int ) -> [ Sensor_p ] {
2022-02-04 18:36:39 +01:00
debug ( " Found \( Int ( count ) ) fans " , log : self . log )
2022-11-13 15:51:39 +01:00
var list : [ Fan ] = [ ]
2022-02-04 18:36:39 +01:00
for i in 0. . < Int ( count ) {
var name = SMC . shared . getStringValue ( " F \( i ) ID " )
var mode : FanMode
if name = = nil && count = = 2 {
switch i {
case 0 :
name = localizedString ( " Left fan " )
case 1 :
name = localizedString ( " Right fan " )
default : break
}
}
if let md = SMC . shared . getValue ( " F \( i ) Md " ) {
mode = FanMode ( rawValue : Int ( md ) ) ? ? . automatic
} else {
mode = self . getFanMode ( i )
}
2022-11-13 15:51:39 +01:00
list . append ( Fan (
2022-02-04 18:36:39 +01:00
id : i ,
key : " F \( i ) Ac " ,
name : name ? ? " \( localizedString ( " Fan " ) ) # \( i ) " ,
minSpeed : SMC . shared . getValue ( " F \( i ) Mn " ) ? ? 1 ,
maxSpeed : SMC . shared . getValue ( " F \( i ) Mx " ) ? ? 1 ,
value : SMC . shared . getValue ( " F \( i ) Ac " ) ? ? 0 ,
mode : mode
) )
}
2022-11-13 15:51:39 +01:00
return list
2022-02-04 18:36:39 +01:00
}
2021-08-07 12:33:23 +03:00
private func getFanMode ( _ id : Int ) -> FanMode {
feat: added better fan control for M3/M4 Apple Silicon (#2924)
* Fix Ftst key handling for Apple Silicon fan control
* Update CFBundleVersion to 747 in Info.plist
Signed-off-by: Alex Goodkind <alex@goodkind.io>
* Update TeamId and SMC.Helper certificate identifier in Info.plist
Signed-off-by: Alex Goodkind <alex@goodkind.io>
* Add debug logging to SMC fan control functions
* Use writeWithRetry for Apple Silicon fan control writes and bump helper version to 1.0.3
* SMC fan control: serialize ops, Ftst timing, verification, logging
- Helper: serial queue for setFanMode/setFanSpeed/resetFanControl
- smc.swift: 3s wait after Ftst=1, longer mode retry (100ms), SMC result logging
- helpers: per-fan verification with cancel-on-supersede, clearer logs
- smc.swift: neutral write logs (no 'succeeded'), FAILED on error
* - Updated error handling in SMCHelper to suppress expected XPC errors (codes 4097 and 4099) during helper updates/restarts.
- Removed unnecessary debug print statement in ModeButtons for improved log clarity.
* Update version numbers in Info.plist files to 752 and change TeamId for SMC.Helper
* Add FanMode.auto3 and isAutomatic, re-add F%dMd write in automatic path, use isAutomatic in countManualFans; bump SMC Helper to 1.0.24
* Apple Silicon fan control: direct-first writes, strip diagnostic bloat
Try direct F%dMd=1 write before Ftst unlock (instant on M1, fallback
on M3/M4). Remove verification system, diagnostic prints, dead code.
* Apple Silicon fan control: direct-first writes
Try direct F%dMd=1 write before Ftst unlock (instant on M1,
fallback on M3/M4).
Bump helper to 1.0.2/3, Stats/Widgets to 751.
---------
Signed-off-by: Alex Goodkind <alex@goodkind.io>
2026-02-22 06:17:23 -08:00
#if arch ( arm64 )
// A p p l e S i l i c o n : R e a d F % d M d d i r e c t l y
// M o d e v a l u e s : 0 = a u t o , 1 = m a n u a l , 3 = s y s t e m ( t r e a t e d a s a u t o f o r U I )
let modeValue = Int ( SMC . shared . getValue ( " F \( id ) Md " ) ? ? 0 )
return modeValue = = 1 ? . forced : . automatic
#else
// L e g a c y I n t e l : U s e F S ! b i t m a s k
// B i t m a s k : 0 = a l l a u t o , 1 = f a n 0 f o r c e d , 2 = f a n 1 f o r c e d , 3 = b o t h f o r c e d
2021-08-07 12:33:23 +03:00
let fansMode : Int = Int ( SMC . shared . getValue ( " FS! " ) ? ? 0 )
var mode : FanMode = . automatic
if fansMode = = 0 {
mode = . automatic
} else if fansMode = = 3 {
mode = . forced
} else if fansMode = = 1 && id = = 0 {
mode = . forced
} else if fansMode = = 2 && id = = 1 {
mode = . forced
}
return mode
feat: added better fan control for M3/M4 Apple Silicon (#2924)
* Fix Ftst key handling for Apple Silicon fan control
* Update CFBundleVersion to 747 in Info.plist
Signed-off-by: Alex Goodkind <alex@goodkind.io>
* Update TeamId and SMC.Helper certificate identifier in Info.plist
Signed-off-by: Alex Goodkind <alex@goodkind.io>
* Add debug logging to SMC fan control functions
* Use writeWithRetry for Apple Silicon fan control writes and bump helper version to 1.0.3
* SMC fan control: serialize ops, Ftst timing, verification, logging
- Helper: serial queue for setFanMode/setFanSpeed/resetFanControl
- smc.swift: 3s wait after Ftst=1, longer mode retry (100ms), SMC result logging
- helpers: per-fan verification with cancel-on-supersede, clearer logs
- smc.swift: neutral write logs (no 'succeeded'), FAILED on error
* - Updated error handling in SMCHelper to suppress expected XPC errors (codes 4097 and 4099) during helper updates/restarts.
- Removed unnecessary debug print statement in ModeButtons for improved log clarity.
* Update version numbers in Info.plist files to 752 and change TeamId for SMC.Helper
* Add FanMode.auto3 and isAutomatic, re-add F%dMd write in automatic path, use isAutomatic in countManualFans; bump SMC Helper to 1.0.24
* Apple Silicon fan control: direct-first writes, strip diagnostic bloat
Try direct F%dMd=1 write before Ftst unlock (instant on M1, fallback
on M3/M4). Remove verification system, diagnostic prints, dead code.
* Apple Silicon fan control: direct-first writes
Try direct F%dMd=1 write before Ftst unlock (instant on M1,
fallback on M3/M4).
Bump helper to 1.0.2/3, Stats/Widgets to 751.
---------
Signed-off-by: Alex Goodkind <alex@goodkind.io>
2026-02-22 06:17:23 -08:00
#endif
2021-08-07 12:33:23 +03:00
}
2022-08-27 15:35:26 +02:00
}
// MARK: - H I D s e n s o r s
extension SensorsReader {
2022-01-11 17:51:11 +01:00
private func m1Preset ( type : SensorType ) -> ( Int32 , Int32 , Int32 ) {
2021-05-08 20:38:26 +02:00
var page : Int32 = 0
var usage : Int32 = 0
2021-04-10 15:50:02 +02:00
var eventType : Int32 = kIOHIDEventTypeTemperature
// u s a g e P a g e :
// k H I D P a g e _ A p p l e V e n d o r = 0 x f f 0 0 ,
// k H I D P a g e _ A p p l e V e n d o r T e m p e r a t u r e S e n s o r = 0 x f f 0 5 ,
// k H I D P a g e _ A p p l e V e n d o r P o w e r S e n s o r = 0 x f f 0 8 ,
// k H I D P a g e _ G e n e r i c D e s k t o p
//
// u s a g e :
// k H I D U s a g e _ A p p l e V e n d o r _ T e m p e r a t u r e S e n s o r = 0 x 0 0 0 5 ,
// k H I D U s a g e _ A p p l e V e n d o r P o w e r S e n s o r _ C u r r e n t = 0 x 0 0 0 2 ,
// k H I D U s a g e _ A p p l e V e n d o r P o w e r S e n s o r _ V o l t a g e = 0 x 0 0 0 3 ,
// k H I D U s a g e _ G D _ K e y b o a r d
//
switch type {
case . temperature :
page = 0xff00
usage = 0x0005
eventType = kIOHIDEventTypeTemperature
case . current :
page = 0xff08
2021-12-09 18:01:54 +01:00
usage = 0x0002
2021-04-10 15:50:02 +02:00
eventType = kIOHIDEventTypePower
case . voltage :
page = 0xff08
2021-12-09 18:01:54 +01:00
usage = 0x0003
2021-04-10 15:50:02 +02:00
eventType = kIOHIDEventTypePower
2022-08-21 06:53:42 -04:00
case . power , . energy , . fan : break
2021-04-10 15:50:02 +02:00
}
2022-01-11 17:51:11 +01:00
return ( page , usage , eventType )
2021-04-10 15:50:02 +02:00
}
2022-01-11 17:51:11 +01:00
private func initHIDSensors ( ) -> [ Sensor ] {
var list : [ Sensor ] = [ ]
2021-04-10 15:50:02 +02:00
2022-01-11 17:51:11 +01:00
for typ in SensorsReader . HIDtypes {
let ( page , usage , type ) = self . m1Preset ( type : typ )
2022-01-17 22:56:47 +01:00
if let sensors = AppleSiliconSensors ( page , usage , type ) {
sensors . forEach { ( key , value ) in
guard let key = key as ? String , let value = value as ? Double else {
return
}
var name : String = key
HIDSensorsList . forEach { ( s : Sensor ) in
if s . key . contains ( " % " ) {
var index = 1
for i in 0. . < 64 {
if s . key . replacingOccurrences ( of : " % " , with : " \( i ) " ) = = key {
name = s . name . replacingOccurrences ( of : " % " , with : " \( index ) " )
}
index += 1
2022-01-11 17:51:11 +01:00
}
2022-01-17 22:56:47 +01:00
} else if s . key = = key {
name = s . name
2021-12-22 17:52:41 +01:00
}
2021-09-04 05:35:42 +02:00
}
2022-01-17 22:56:47 +01:00
list . append ( Sensor (
key : key ,
name : name ,
value : value ,
group : . hid ,
2022-08-27 15:35:26 +02:00
type : typ ,
platforms : Platform . all
2022-01-17 22:56:47 +01:00
) )
2021-09-04 05:35:42 +02:00
}
2021-09-03 20:02:00 +02:00
}
}
2022-01-11 17:51:11 +01:00
let socSensors = list . filter ( { $0 . key . hasPrefix ( " SOC MTR Temp " ) } ) . map { $0 . value }
if ! socSensors . isEmpty {
let value = socSensors . reduce ( 0 , + ) / Double ( socSensors . count )
2022-08-27 15:35:26 +02:00
list . append ( Sensor ( key : " Average SOC " , name : " Average SOC " , value : value , group : . hid , type : . temperature , platforms : Platform . all ) )
2022-01-11 17:51:11 +01:00
if let max = socSensors . max ( ) {
2022-08-27 15:35:26 +02:00
list . append ( Sensor ( key : " Hottest SOC " , name : " Hottest SOC " , value : max , group : . hid , type : . temperature , platforms : Platform . all ) )
2022-02-10 17:23:18 +01:00
}
}
2022-08-21 06:53:42 -04:00
2022-02-10 17:23:18 +01:00
return list . filter ( { ( s : Sensor_p ) -> Bool in
switch s . type {
case . temperature :
return s . value < 110 && s . value >= 0
case . voltage :
return s . value < 300 && s . value >= 0
case . current :
return s . value < 100 && s . value >= 0
default : return true
}
} ) . sorted { $0 . key . lowercased ( ) < $1 . key . lowercased ( ) }
}
2022-01-14 20:34:39 +01:00
public func HIDCallback ( ) {
if self . HIDState {
2023-06-27 17:09:38 +02:00
self . list . sensors += self . initHIDSensors ( )
2022-01-14 20:34:39 +01:00
} else {
2023-06-27 17:09:38 +02:00
self . list . sensors = self . list . sensors . filter ( { $0 . group != . hid } )
2022-01-14 20:34:39 +01:00
}
}
2021-04-10 15:50:02 +02:00
}
2025-02-08 10:55:48 +01:00
// MARK: - A p p l e S i l i c o n p o w e r s e n s o r s
extension SensorsReader {
private func getChannels ( ) -> CFMutableDictionary ? {
let channelNames : [ ( String , String ? ) ] = [ ( " Energy Model " , nil ) ]
var channels : [ CFDictionary ] = [ ]
for ( gname , sname ) in channelNames {
let channel = IOReportCopyChannelsInGroup ( gname as CFString ? , sname as CFString ? , 0 , 0 , 0 )
guard let channel = channel ? . takeRetainedValue ( ) else { continue }
channels . append ( channel )
}
let chan = channels [ 0 ]
for i in 1. . < channels . count {
IOReportMergeChannels ( chan , channels [ i ] , nil )
}
let size = CFDictionaryGetCount ( chan )
guard let channel = CFDictionaryCreateMutableCopy ( kCFAllocatorDefault , size , chan ) ,
let chan = channel as ? [ String : Any ] , chan [ " IOReportChannels " ] != nil else {
return nil
}
return channel
}
private func initIOSensors ( ) -> [ Sensor ] {
guard let ( cpu , gpu , ane , ram , pci ) = self . IOSensors ( ) else { return [ ] }
return [
Sensor ( key : " CPU Power " , name : " CPU Power " , value : cpu , group : . CPU , type : . power , platforms : Platform . apple , isComputed : true ) ,
Sensor ( key : " GPU Power " , name : " GPU Power " , value : gpu , group : . GPU , type : . power , platforms : Platform . apple , isComputed : true ) ,
Sensor ( key : " ANE Power " , name : " ANE Power " , value : ane , group : . system , type : . power , platforms : Platform . apple , isComputed : true ) ,
Sensor ( key : " RAM Power " , name : " RAM Power " , value : ram , group : . system , type : . power , platforms : Platform . apple , isComputed : true ) ,
Sensor ( key : " PCI Power " , name : " PCI Power " , value : pci , group : . system , type : . power , platforms : Platform . apple , isComputed : true )
]
}
private func IOSensors ( ) -> ( Double , Double , Double , Double , Double ) ? {
guard let sample = IOReportCreateSamples ( self . subscription , self . channels , nil ) ? . takeRetainedValue ( ) ,
let dict = sample as ? [ String : Any ] else {
return nil
}
let items = dict [ " IOReportChannels " ] as ! CFArray
let prevCPU = self . powers . CPU
let prevGPU = self . powers . GPU
let prevANE = self . powers . ANE
let prevRAM = self . powers . RAM
let prevPCI = self . powers . PCI
for i in 0. . < CFArrayGetCount ( items ) {
let dict = CFArrayGetValueAtIndex ( items , i )
let item = unsafeBitCast ( dict , to : CFDictionary . self )
guard let group = IOReportChannelGetGroup ( item ) ? . takeUnretainedValue ( ) as ? String ,
group = = " Energy Model " ,
let channel = IOReportChannelGetChannelName ( item ) ? . takeUnretainedValue ( ) as ? String ,
let unit = IOReportChannelGetUnitLabel ( item ) ? . takeUnretainedValue ( ) as ? String else { continue }
let value = Double ( IOReportSimpleGetIntegerValue ( item , 0 ) )
if channel . hasSuffix ( " CPU Energy " ) {
self . powers . CPU = value . power ( unit )
} else if channel . hasSuffix ( " GPU Energy " ) {
self . powers . GPU = value . power ( unit )
} else if channel . starts ( with : " ANE " ) {
self . powers . ANE = value . power ( unit )
} else if channel . starts ( with : " DRAM " ) {
self . powers . RAM = value . power ( unit )
} else if channel . starts ( with : " PCI " ) && channel . hasSuffix ( " Energy " ) {
self . powers . PCI = value . power ( unit )
}
}
guard prevCPU != 0 else { return ( 0 , 0 , 0 , 0 , 0 ) } // o m i t f i r s t r e a d
return (
self . powers . CPU - prevCPU ,
self . powers . GPU - prevGPU ,
self . powers . ANE - prevANE ,
self . powers . RAM - prevRAM ,
self . powers . PCI - prevPCI
)
}
}