1010//
1111
1212import Foundation
13+ import IOKit
1314
1415let helper = Helper ( )
1516helper. run ( )
@@ -21,12 +22,27 @@ class Helper: NSObject, NSXPCListenerDelegate, HelperProtocol {
2122 private var shouldQuit = false
2223 private var shouldQuitCheckInterval = 1.0
2324
24- private var smc : String ? = nil
25+ private var smcConnection : io_connect_t = 0
2526
2627 override init ( ) {
2728 self . listener = NSXPCListener ( machServiceName: " eu.exelban.Stats.SMC.Helper " )
2829 super. init ( )
2930 self . listener. delegate = self
31+
32+ // Open SMC connection on initialization
33+ let ( conn, result) = smcOpenConnection ( )
34+ if result == kIOReturnSuccess {
35+ self . smcConnection = conn
36+ NSLog ( " SMC connection opened successfully " )
37+ } else {
38+ NSLog ( " Failed to open SMC connection: \( result) " )
39+ }
40+ }
41+
42+ deinit {
43+ if self . smcConnection != 0 {
44+ IOServiceClose ( self . smcConnection)
45+ }
3046 }
3147
3248 public func run( ) {
@@ -111,40 +127,101 @@ extension Helper {
111127 completion ( Bundle . main. object ( forInfoDictionaryKey: " CFBundleShortVersionString " ) as? String ?? " 0 " )
112128 }
113129 func setSMCPath( _ path: String ) {
114- self . smc = path
130+ // Legacy method - no longer needed with direct SMC access
131+ // Kept for protocol compatibility
115132 }
116133
117134 func setFanMode( id: Int , mode: Int , completion: ( String ? ) -> Void ) {
118- guard let smc = self . smc else {
119- completion ( " missing smc tool " )
135+ guard self . smcConnection != 0 else {
136+ completion ( " SMC connection not available " )
120137 return
121138 }
122- let result = syncShell ( " \( smc) fan \( id) -m \( mode) " )
123139
124- if let error = result. error, !error. isEmpty {
125- NSLog ( " error set fan mode: \( error) " )
126- completion ( nil )
127- return
140+ if mode == 1 {
141+ // Switching TO manual mode - unlock fan control for this specific fan
142+ // The unlock function also sets the fan to manual mode
143+ let unlockResult = smcUnlockFanControl ( self . smcConnection, fanIndex: id)
144+ if unlockResult != kIOReturnSuccess {
145+ NSLog ( " Failed to unlock fan control: \( unlockResult) " )
146+ completion ( " Failed to unlock fan control " )
147+ return
148+ }
149+ } else if mode == 0 {
150+ // Switching TO automatic mode
151+ // Count how many OTHER fans are currently in manual mode (mode byte == 1)
152+ let ( numResult, numBytes, _) = smcRead ( self . smcConnection, key: SMC_KEY_FNUM)
153+ var otherFansManual = 0
154+
155+ if numResult == kIOReturnSuccess && !numBytes. isEmpty {
156+ let fanCount = Int ( numBytes [ 0 ] )
157+ for i in 0 ..< fanCount {
158+ if i == id { continue } // Skip the fan we're setting to auto
159+ let checkKey = String ( format: SMC_KEY_FAN_MODE, i)
160+ let ( _, checkBytes, _) = smcRead ( self . smcConnection, key: checkKey)
161+ // Mode byte: 0=auto, 1=manual, 3=system
162+ if !checkBytes. isEmpty && checkBytes [ 0 ] == 1 {
163+ otherFansManual += 1
164+ }
165+ }
166+ }
167+
168+ if otherFansManual > 0 {
169+ // Other fans are still manual, keep Ftst=1 but set this fan to auto
170+ let modeKey = String ( format: SMC_KEY_FAN_MODE, id)
171+ let modeResult = smcWrite ( self . smcConnection, key: modeKey, value: [ 0 ] , size: 1 )
172+ if modeResult != kIOReturnSuccess {
173+ NSLog ( " Warning: Failed to set auto mode for fan \( id) : \( modeResult) " )
174+ }
175+ NSLog ( " Set fan \( id) to auto, other fans still manual " )
176+ } else {
177+ // No other fans in manual mode, reset Ftst to restore automatic control
178+ // This allows fans to enter system mode (mode 3) and idle at 0 RPM
179+ let resetResult = smcWrite (
180+ self . smcConnection, key: SMC_KEY_FAN_TEST, value: [ 0 ] , size: 1
181+ )
182+ if resetResult != kIOReturnSuccess {
183+ NSLog ( " Warning: Failed to reset Ftst: \( resetResult) " )
184+ }
185+ NSLog ( " Reset Ftst, restoring automatic fan control " )
186+ }
128187 }
129188
130- completion ( result. output)
189+ NSLog ( " Fan \( id) mode set to \( mode) " )
190+ completion ( " Success " )
131191 }
132192
133193 func setFanSpeed( id: Int , value: Int , completion: ( String ? ) -> Void ) {
134- guard let smc = self . smc else {
135- completion ( " missing smc tool " )
194+ guard self . smcConnection != 0 else {
195+ completion ( " SMC connection not available " )
136196 return
137197 }
138198
139- let result = syncShell ( " \( smc) fan \( id) -v \( value) " )
199+ // Read current target to determine data format (also verifies key exists)
200+ let targetKey = String ( format: SMC_KEY_FAN_TARGET, id)
201+ let ( readResult, _, size) = smcRead ( self . smcConnection, key: targetKey)
140202
141- if let error = result . error , !error . isEmpty {
142- NSLog ( " error set fan speed : \( error ) " )
143- completion ( nil )
203+ if readResult != kIOReturnSuccess {
204+ NSLog ( " Error reading fan target : \( readResult ) " )
205+ completion ( " Failed to read fan target " )
144206 return
145207 }
146208
147- completion ( result. output)
209+ // Convert speed to appropriate format based on data size
210+ let speedBytes = floatToBytes ( Float ( value) , size: size)
211+
212+ // Write new target speed
213+ // Note: Fan should already be in manual mode from a prior setFanMode call.
214+ // If not, this write may not take effect until mode is set.
215+ let writeResult = smcWrite ( self . smcConnection, key: targetKey, value: speedBytes, size: size)
216+
217+ if writeResult != kIOReturnSuccess {
218+ NSLog ( " Error setting fan speed: \( writeResult) " )
219+ completion ( " Failed to set fan speed " )
220+ return
221+ }
222+
223+ NSLog ( " Fan \( id) speed set to \( value) " )
224+ completion ( " Success " )
148225 }
149226
150227 func powermetrics( _ samplers: [ String ] , completion: @escaping ( String ? ) -> Void ) {
0 commit comments