@@ -43,6 +43,7 @@ internal enum SMCKeys: UInt8 {
4343 case readVers = 12
4444}
4545
46+
4647public enum FanMode : Int , Codable {
4748 case automatic = 0
4849 case forced = 1
@@ -352,6 +353,65 @@ public class SMC {
352353 // MARK: - fans
353354
354355 public func setFanMode( _ id: Int , mode: FanMode ) {
356+ #if arch(arm64)
357+ // Apple Silicon: unlock before setting manual mode
358+ if mode == . forced {
359+ if !unlockFanControl( fanId: id) {
360+ print ( " Error: failed to unlock fan control for fan \( id) " )
361+ return
362+ }
363+ // unlockFanControl already set the mode to 1, so we're done with F%dMd
364+ } else {
365+ // Setting to automatic - check if OTHER fans are still in manual mode
366+ let otherFansManual = countManualFans ( excluding: id)
367+
368+ var result : kern_return_t = 0
369+ // Set mode and target to 0
370+ if self . getValue ( " F \( id) Md " ) != nil {
371+ var value = SMCVal_t ( " F \( id) Md " )
372+
373+ result = read ( & value)
374+ guard result == kIOReturnSuccess else {
375+ print ( " Error read fan mode: " + ( String ( cString: mach_error_string ( result) , encoding: String . Encoding. ascii) ?? " unknown error " ) )
376+ return
377+ }
378+
379+ value. bytes [ 0 ] = 0
380+ result = write ( value)
381+ guard result == kIOReturnSuccess else {
382+ print ( " Error write fan mode: " + ( String ( cString: mach_error_string ( result) , encoding: String . Encoding. ascii) ?? " unknown error " ) )
383+ return
384+ }
385+ }
386+
387+ // Apple Silicon uses flt (IEEE 754 float) format
388+ var targetValue = SMCVal_t ( " F \( id) Tg " )
389+
390+ result = read ( & targetValue)
391+ guard result == kIOReturnSuccess else {
392+ print ( " Error read fan target: " + ( String ( cString: mach_error_string ( result) , encoding: String . Encoding. ascii) ?? " unknown error " ) )
393+ return
394+ }
395+
396+ let bytes = Float ( 0 ) . bytes
397+ targetValue. bytes [ 0 ] = bytes [ 0 ]
398+ targetValue. bytes [ 1 ] = bytes [ 1 ]
399+ targetValue. bytes [ 2 ] = bytes [ 2 ]
400+ targetValue. bytes [ 3 ] = bytes [ 3 ]
401+
402+ result = write ( targetValue)
403+ guard result == kIOReturnSuccess else {
404+ print ( " Error write fan target: " + ( String ( cString: mach_error_string ( result) , encoding: String . Encoding. ascii) ?? " unknown error " ) )
405+ return
406+ }
407+
408+ if otherFansManual == 0 {
409+ resetFanControl ( )
410+ print ( " Last manual fan - Ftst reset, thermalmonitord has control " )
411+ }
412+ }
413+ #else
414+ // Intel
355415 if self . getValue ( " F \( id) Md " ) != nil {
356416 var result : kern_return_t = 0
357417 var value = SMCVal_t ( " F \( id) Md " )
@@ -422,15 +482,31 @@ public class SMC {
422482 print ( " Error write: " + ( String ( cString: mach_error_string ( result) , encoding: String . Encoding. ascii) ?? " unknown error " ) )
423483 return
424484 }
485+ #endif
425486 }
426487
427488 public func setFanSpeed( _ id: Int , speed: Int ) {
428- let maxSpeed = Int ( self . getValue ( " F \( id) Mx " ) ?? 4000 )
489+ // Enforce recommended max as safety limit
490+ if let maxSpeed = self . getValue ( " F \( id) Mx " ) , speed > Int ( maxSpeed) {
491+ print ( " Fan speed ( \( speed) ) exceeds maximum ( \( Int ( maxSpeed) ) ), clamping " )
492+ return setFanSpeed ( id, speed: Int ( maxSpeed) )
493+ }
429494
430- if speed > maxSpeed {
431- print ( " new fan speed ( \( speed) ) is more than maximum speed ( \( maxSpeed) ) " )
495+ #if arch(arm64)
496+ // Apple Silicon: ensure fan is in manual mode before setting speed
497+ var modeVal = SMCVal_t ( " F \( id) Md " )
498+ let modeResult = read ( & modeVal)
499+ guard modeResult == kIOReturnSuccess else {
500+ print ( " Error read fan mode: " + ( String ( cString: mach_error_string ( modeResult) , encoding: String . Encoding. ascii) ?? " unknown error " ) )
432501 return
433502 }
503+ if modeVal. bytes [ 0 ] != 1 {
504+ if !unlockFanControl( fanId: id) {
505+ print ( " Error: Failed to unlock fan \( id) for speed change " )
506+ return
507+ }
508+ }
509+ #endif
434510
435511 var result : kern_return_t = 0
436512 var value = SMCVal_t ( " F \( id) Tg " )
@@ -471,6 +547,92 @@ public class SMC {
471547 }
472548 }
473549
550+ // MARK: - Apple Silicon Fan Control
551+
552+ #if arch(arm64)
553+ /// Unlock fan control for Apple Silicon by writing Ftst=1
554+ /// This prevents thermalmonitord from overriding manual fan settings
555+ private func unlockFanControl( fanId: Int ) -> Bool {
556+ // Check if already unlocked (Ftst=1 from another fan)
557+ var ftstCheck = SMCVal_t ( " Ftst " )
558+ var result = read ( & ftstCheck)
559+ guard result == kIOReturnSuccess else {
560+ print ( " Error read Ftst: " + ( String ( cString: mach_error_string ( result) , encoding: String . Encoding. ascii) ?? " unknown error " ) )
561+ return false
562+ }
563+ let alreadyUnlocked = ftstCheck. bytes [ 0 ] == 1
564+
565+ if !alreadyUnlocked {
566+ var ftstVal = SMCVal_t ( " Ftst " )
567+ result = read ( & ftstVal)
568+ guard result == kIOReturnSuccess else {
569+ print ( " Error read Ftst: " + ( String ( cString: mach_error_string ( result) , encoding: String . Encoding. ascii) ?? " unknown error " ) )
570+ return false
571+ }
572+ ftstVal. bytes [ 0 ] = 1
573+ let ftstResult = write ( ftstVal)
574+ guard ftstResult == kIOReturnSuccess else {
575+ print ( " Error unlocking fan control: " + ( String ( cString: mach_error_string ( ftstResult) , encoding: String . Encoding. ascii) ?? " unknown error " ) )
576+ return false
577+ }
578+ }
579+
580+ // Retry writing mode=1 until thermalmonitord releases control
581+ let modeKey = " F \( fanId) Md "
582+ let maxAttempts = alreadyUnlocked ? 10 : 100
583+ for attempt in 0 ..< maxAttempts {
584+ var modeVal = SMCVal_t ( modeKey)
585+ guard read ( & modeVal) == kIOReturnSuccess else {
586+ usleep ( alreadyUnlocked ? 10_000 : 50_000 )
587+ continue
588+ }
589+ modeVal. bytes [ 0 ] = 1
590+ let writeResult = write ( modeVal)
591+ if writeResult == kIOReturnSuccess {
592+ print ( " Fan \( fanId) set to manual mode after \( attempt + 1 ) attempts " )
593+ return true
594+ }
595+ usleep ( alreadyUnlocked ? 10_000 : 50_000 )
596+ }
597+ print ( " Timeout setting fan \( fanId) to manual mode " )
598+ return false
599+ }
600+
601+ /// Reset Ftst to 0, returning control to thermalmonitord
602+ private func resetFanControl( ) {
603+ var value = SMCVal_t ( " Ftst " )
604+ var result = read ( & value)
605+ guard result == kIOReturnSuccess else {
606+ print ( " Error read Ftst: " + ( String ( cString: mach_error_string ( result) , encoding: String . Encoding. ascii) ?? " unknown error " ) )
607+ return
608+ }
609+ value. bytes [ 0 ] = 0
610+ result = write ( value)
611+ if result == kIOReturnSuccess {
612+ print ( " Ftst=0 written successfully " )
613+ } else {
614+ print ( " Error writing Ftst=0: " + ( String ( cString: mach_error_string ( result) , encoding: String . Encoding. ascii) ?? " unknown error " ) )
615+ }
616+ }
617+
618+ /// Check how many fans are currently in manual mode
619+ /// - Parameter excluding: Optional fan ID to exclude from count (for checking OTHER fans)
620+ private func countManualFans( excluding fanId: Int ? = nil ) -> Int {
621+ guard let fanCount = getValue ( " FNum " ) else { return 0 }
622+ var count = 0
623+ for i in 0 ..< Int ( fanCount) {
624+ if let exclude = fanId, i == exclude { continue }
625+ var modeVal = SMCVal_t ( " F \( i) Md " )
626+ if read ( & modeVal) == kIOReturnSuccess {
627+ if modeVal. bytes [ 0 ] == 1 {
628+ count += 1
629+ }
630+ }
631+ }
632+ return count
633+ }
634+ #endif
635+
474636 // MARK: - internal functions
475637
476638 private func read( _ value: UnsafeMutablePointer < SMCVal_t > ) -> kern_return_t {
@@ -520,6 +682,12 @@ public class SMC {
520682 return result
521683 }
522684
685+ // IOKit can return kIOReturnSuccess but SMC firmware may still reject the write.
686+ // Check SMC-level result code (0x00 = success, non-zero = error)
687+ if output. result != 0x00 {
688+ return kIOReturnError
689+ }
690+
523691 return kIOReturnSuccess
524692 }
525693
0 commit comments