@@ -352,6 +352,56 @@ public class SMC {
352352 // MARK: - fans
353353
354354 public func setFanMode( _ id: Int , mode: FanMode ) {
355+ #if arch(arm64)
356+ // Apple Silicon: unlock before setting manual mode
357+ if mode == . forced {
358+ if !unlockFanControl( fanId: id) {
359+ print ( " Error: failed to unlock fan control for fan \( id) " )
360+ return
361+ }
362+ // unlockFanControl already set the mode to 1, so we're done with F%dMd
363+ } else {
364+ // Setting to automatic - count OTHER fans in manual mode (skip current fan)
365+ guard let fanCount = getValue ( " FNum " ) else {
366+ print ( " Error: Failed to read fan count " )
367+ return
368+ }
369+ var otherFansManual = 0
370+ for i in 0 ..< Int ( fanCount) {
371+ if i == id { continue }
372+ var modeVal = SMCVal_t ( " F \( i) Md " )
373+ if read ( & modeVal) == kIOReturnSuccess {
374+ if modeVal. bytes [ 0 ] == 1 {
375+ otherFansManual += 1
376+ }
377+ }
378+ }
379+
380+ // Set mode and target to 0
381+ if self . getValue ( " F \( id) Md " ) != nil {
382+ var value = SMCVal_t ( " F \( id) Md " )
383+ _ = read ( & value)
384+ value. bytes [ 0 ] = 0
385+ _ = write ( value)
386+ }
387+
388+ // Apple Silicon uses flt (IEEE 754 float) format
389+ var targetValue = SMCVal_t ( " F \( id) Tg " )
390+ _ = read ( & targetValue)
391+ let bytes = Float ( 0 ) . bytes
392+ targetValue. bytes [ 0 ] = bytes [ 0 ]
393+ targetValue. bytes [ 1 ] = bytes [ 1 ]
394+ targetValue. bytes [ 2 ] = bytes [ 2 ]
395+ targetValue. bytes [ 3 ] = bytes [ 3 ]
396+ _ = write ( targetValue)
397+
398+ if otherFansManual == 0 {
399+ resetFanControl ( )
400+ print ( " Last manual fan - Ftst reset, thermalmonitord has control " )
401+ }
402+ }
403+ #else
404+ // Intel
355405 if self . getValue ( " F \( id) Md " ) != nil {
356406 var result : kern_return_t = 0
357407 var value = SMCVal_t ( " F \( id) Md " )
@@ -422,6 +472,7 @@ public class SMC {
422472 print ( " Error write: " + ( String ( cString: mach_error_string ( result) , encoding: String . Encoding. ascii) ?? " unknown error " ) )
423473 return
424474 }
475+ #endif
425476 }
426477
427478 public func setFanSpeed( _ id: Int , speed: Int ) {
@@ -432,6 +483,18 @@ public class SMC {
432483 return
433484 }
434485
486+ #if arch(arm64)
487+ // Apple Silicon: ensure fan is in manual mode before setting speed
488+ var modeVal = SMCVal_t ( " F \( id) Md " )
489+ _ = read ( & modeVal)
490+ if modeVal. bytes [ 0 ] != 1 {
491+ if !unlockFanControl( fanId: id) {
492+ print ( " Error: Failed to unlock fan \( id) for speed change " )
493+ return
494+ }
495+ }
496+ #endif
497+
435498 var result : kern_return_t = 0
436499 var value = SMCVal_t ( " F \( id) Tg " )
437500
@@ -441,18 +504,20 @@ public class SMC {
441504 return
442505 }
443506
444- if value. dataType == " flt " {
445- let bytes = Float ( speed) . bytes
446- value. bytes [ 0 ] = bytes [ 0 ]
447- value. bytes [ 1 ] = bytes [ 1 ]
448- value. bytes [ 2 ] = bytes [ 2 ]
449- value. bytes [ 3 ] = bytes [ 3 ]
450- } else if value. dataType == " fpe2 " {
451- value. bytes [ 0 ] = UInt8 ( speed >> 6 )
452- value. bytes [ 1 ] = UInt8 ( ( speed << 2 ) ^ ( ( speed >> 6 ) << 8 ) )
453- value. bytes [ 2 ] = UInt8 ( 0 )
454- value. bytes [ 3 ] = UInt8 ( 0 )
455- }
507+ #if arch(arm64)
508+ // Apple Silicon: flt (IEEE 754 float) format
509+ let bytes = Float ( speed) . bytes
510+ value. bytes [ 0 ] = bytes [ 0 ]
511+ value. bytes [ 1 ] = bytes [ 1 ]
512+ value. bytes [ 2 ] = bytes [ 2 ]
513+ value. bytes [ 3 ] = bytes [ 3 ]
514+ #else
515+ // Intel: fpe2 format
516+ value. bytes [ 0 ] = UInt8 ( speed >> 6 )
517+ value. bytes [ 1 ] = UInt8 ( ( speed << 2 ) ^ ( ( speed >> 6 ) << 8 ) )
518+ value. bytes [ 2 ] = UInt8 ( 0 )
519+ value. bytes [ 3 ] = UInt8 ( 0 )
520+ #endif
456521
457522 result = write ( value)
458523 if result != kIOReturnSuccess {
@@ -469,7 +534,81 @@ public class SMC {
469534 if result != kIOReturnSuccess {
470535 print ( " Error write: " + ( String ( cString: mach_error_string ( result) , encoding: String . Encoding. ascii) ?? " unknown error " ) )
471536 }
537+
538+ #if arch(arm64)
539+ // Also reset Ftst on Apple Silicon
540+ resetFanControl ( )
541+ #endif
542+ }
543+
544+ // MARK: - Apple Silicon Fan Control
545+
546+ #if arch(arm64)
547+ /// Unlock fan control for Apple Silicon by writing Ftst=1
548+ /// This prevents thermalmonitord from overriding manual fan settings
549+ private func unlockFanControl( fanId: Int ) -> Bool {
550+ // Check if already unlocked (Ftst=1 from another fan)
551+ var ftstCheck = SMCVal_t ( " Ftst " )
552+ _ = read ( & ftstCheck)
553+ let alreadyUnlocked = ftstCheck. bytes [ 0 ] == 1
554+
555+ if !alreadyUnlocked {
556+ var ftstVal = SMCVal_t ( " Ftst " )
557+ _ = read ( & ftstVal)
558+ ftstVal. bytes [ 0 ] = 1
559+ let ftstResult = write ( ftstVal)
560+ if ftstResult != kIOReturnSuccess {
561+ print ( " Error unlocking fan control: " + ( String ( cString: mach_error_string ( ftstResult) , encoding: String . Encoding. ascii) ?? " unknown error " ) )
562+ return false
563+ }
564+ }
565+
566+ // Retry writing mode=1 until thermalmonitord releases control
567+ let modeKey = " F \( fanId) Md "
568+ let maxAttempts = alreadyUnlocked ? 10 : 100
569+ for attempt in 0 ..< maxAttempts {
570+ var modeVal = SMCVal_t ( modeKey)
571+ _ = read ( & modeVal)
572+ modeVal. bytes [ 0 ] = 1
573+ let result = write ( modeVal)
574+ if result == kIOReturnSuccess {
575+ print ( " Fan \( fanId) set to manual mode after \( attempt + 1 ) attempts " )
576+ return true
577+ }
578+ usleep ( alreadyUnlocked ? 10_000 : 50_000 )
579+ }
580+ print ( " Timeout setting fan \( fanId) to manual mode " )
581+ return false
582+ }
583+
584+ /// Reset Ftst to 0, returning control to thermalmonitord
585+ private func resetFanControl( ) {
586+ var value = SMCVal_t ( " Ftst " )
587+ _ = read ( & value)
588+ value. bytes [ 0 ] = 0
589+ let result = write ( value)
590+ if result == kIOReturnSuccess {
591+ print ( " Ftst=0 written successfully " )
592+ } else {
593+ print ( " Error writing Ftst=0: \( result) " )
594+ }
595+ }
596+
597+ /// Check how many fans are currently in manual mode
598+ private func countManualFans( ) -> Int {
599+ guard let fanCount = getValue ( " FNum " ) else { return 0 }
600+ var count = 0
601+ for i in 0 ..< Int ( fanCount) {
602+ var modeVal = SMCVal_t ( " F \( i) Md " )
603+ if read ( & modeVal) == kIOReturnSuccess {
604+ if modeVal. bytes [ 0 ] == 1 {
605+ count += 1
606+ }
607+ }
608+ }
609+ return count
472610 }
611+ #endif
473612
474613 // MARK: - internal functions
475614
0 commit comments