@@ -105,8 +105,8 @@ enum FanControlMode: String, CaseIterable {
105105 )
106106 case . manual:
107107 return FanModeGuidance (
108- summary: " Writes one fixed RPM target across every controllable fan until you reset or quit. " ,
109- detail: " Best for short debugging sessions when you know the exact airflow target you want . " ,
108+ summary: " Writes fixed RPM targets per controllable fan until you reset or quit. " ,
109+ detail: " Best for short debugging sessions when you know the exact airflow target each fan should hold . " ,
110110 ownership: . coreMonitor,
111111 helperRequirement: . managedControl,
112112 restoresSystemAutomaticOnExit: true ,
@@ -381,6 +381,7 @@ final class FanController: ObservableObject {
381381
382382 @Published var mode : FanControlMode = FanController . defaultMode
383383 @Published var manualSpeed : Int = 2200
384+ @Published var manualFanSpeeds : [ Int ] = [ ]
384385 @Published var autoAggressiveness : Double = 1.5
385386 @Published var autoMaxSpeed : Int = 6500
386387 @Published var statusMessage : String = " Idle "
@@ -397,6 +398,7 @@ final class FanController: ObservableObject {
397398 private weak var systemMonitor : SystemMonitor ?
398399 private var controlTimer : Timer ?
399400 private var lastAppliedSpeed : Int = 0
401+ private var lastAppliedManualSpeeds : [ Int ] = [ ]
400402 private let helperManager = SMCHelperManager . shared
401403 private var workspaceObservers : [ NSObjectProtocol ] = [ ]
402404 private var customPreset : CustomFanPreset ?
@@ -428,6 +430,20 @@ final class FanController: ObservableObject {
428430 return String ( decoding: data, as: UTF8 . self)
429431 }
430432
433+ static func normalizedManualFanSpeeds(
434+ _ speeds: [ Int ] ,
435+ fanCount: Int ,
436+ fallback: Int ,
437+ minSpeed: Int ,
438+ maxSpeed: Int
439+ ) -> [ Int ] {
440+ guard fanCount > 0 else { return [ ] }
441+ return ( 0 ..< fanCount) . map { index in
442+ let rawSpeed = index < speeds. count ? speeds [ index] : fallback
443+ return max ( minSpeed, min ( maxSpeed, rawSpeed) )
444+ }
445+ }
446+
431447 var customPresetFilePath : String {
432448 customPresetFileURL ( ) . path
433449 }
@@ -457,15 +473,42 @@ final class FanController: ObservableObject {
457473 }
458474 self . mode = resolvedMode
459475 lastAppliedSpeed = 0
476+ lastAppliedManualSpeeds = [ ]
460477 saveSettings ( )
461478 applyCurrentMode ( force: true )
462479 }
463480
464481 func setManualSpeed( _ speed: Int ) {
465482 manualSpeed = max ( minSpeed, min ( maxSpeed, speed) )
483+ let fanCount = max ( resolvedFanCount ( ) , manualFanSpeeds. count, 1 )
484+ manualFanSpeeds = Array ( repeating: manualSpeed, count: fanCount)
485+ lastAppliedManualSpeeds = [ ]
466486 saveSettings ( )
467487 guard mode == . manual else { return }
468- applyFanSpeed ( manualSpeed)
488+ applyManualFanSpeeds ( force: true )
489+ }
490+
491+ func manualSpeed( for fanID: Int ) -> Int {
492+ guard fanID >= 0 else { return manualSpeed }
493+ return manualFanSpeeds. indices. contains ( fanID) ? manualFanSpeeds [ fanID] : manualSpeed
494+ }
495+
496+ func setManualFanSpeed( _ speed: Int , for fanID: Int ) {
497+ guard fanID >= 0 else { return }
498+ let fanCount = max ( resolvedFanCount ( ) , fanID + 1 )
499+ var speeds = Self . normalizedManualFanSpeeds (
500+ manualFanSpeeds,
501+ fanCount: fanCount,
502+ fallback: manualSpeed,
503+ minSpeed: minSpeed,
504+ maxSpeed: maxSpeed
505+ )
506+ speeds [ fanID] = max ( minSpeed, min ( maxSpeed, speed) )
507+ manualFanSpeeds = speeds
508+ manualSpeed = speeds. first ?? manualSpeed
509+ saveSettings ( )
510+ guard mode == . manual else { return }
511+ applyManualFanSpeeds ( force: true )
469512 }
470513
471514 func setAutoAggressiveness( _ value: Double ) {
@@ -717,9 +760,7 @@ final class FanController: ObservableObject {
717760 }
718761 lastAppliedSpeed = - 1
719762 case . manual:
720- applyFanSpeed ( manualSpeed)
721- lastAppliedSpeed = manualSpeed
722- statusMessage = " Manual: \( manualSpeed) RPM "
763+ applyManualFanSpeeds ( force: true )
723764 startControlLoop ( )
724765 case . smart, . balanced, . performance, . max, . custom:
725766 startControlLoop ( )
@@ -731,11 +772,7 @@ final class FanController: ObservableObject {
731772
732773 switch mode {
733774 case . manual:
734- if abs ( manualSpeed - lastAppliedSpeed) >= 50 || lastAppliedSpeed == 0 {
735- applyFanSpeed ( manualSpeed)
736- lastAppliedSpeed = manualSpeed
737- }
738- statusMessage = " Manual: \( manualSpeed) RPM "
775+ applyManualFanSpeeds ( force: false )
739776
740777 case . automatic, . silent:
741778 if Self . shouldRequestSystemAutomaticHandoff ( lastAppliedSpeed: lastAppliedSpeed) {
@@ -916,6 +953,42 @@ final class FanController: ObservableObject {
916953 _ = applyPerFanSpeeds ( speeds, successMessage: " Applied \( speed) RPM " )
917954 }
918955
956+ private func applyManualFanSpeeds( force: Bool ) {
957+ let fanCount = max ( resolvedFanCount ( ) , 1 )
958+ let speeds = Self . normalizedManualFanSpeeds (
959+ manualFanSpeeds,
960+ fanCount: fanCount,
961+ fallback: manualSpeed,
962+ minSpeed: minSpeed,
963+ maxSpeed: maxSpeed
964+ )
965+
966+ manualFanSpeeds = speeds
967+ manualSpeed = speeds. first ?? manualSpeed
968+
969+ let shouldApply = force
970+ || lastAppliedManualSpeeds. count != speeds. count
971+ || zip ( speeds, lastAppliedManualSpeeds) . contains { current, previous in
972+ abs ( current - previous) >= 50
973+ }
974+ || lastAppliedSpeed == 0
975+
976+ if shouldApply {
977+ _ = applyPerFanSpeeds ( speeds, successMessage: manualStatusMessage ( for: speeds) )
978+ lastAppliedManualSpeeds = speeds
979+ lastAppliedSpeed = speeds. max ( ) ?? 0
980+ } else {
981+ statusMessage = manualStatusMessage ( for: speeds)
982+ }
983+ }
984+
985+ private func manualStatusMessage( for speeds: [ Int ] ) -> String {
986+ guard speeds. count > 1 else {
987+ return " Manual: \( speeds. first ?? manualSpeed) RPM "
988+ }
989+ return " Manual: " + speeds. map { " \( $0) " } . joined ( separator: " / " ) + " RPM "
990+ }
991+
919992 @discardableResult
920993 private func applyPerFanSpeeds( _ requestedSpeeds: [ Int ] , successMessage: String ? ) -> Bool {
921994 guard let monitor = systemMonitor else { return false }
@@ -1150,6 +1223,15 @@ final class FanController: ObservableObject {
11501223 manualSpeed = savedSpeed
11511224 }
11521225
1226+ let savedPerFanSpeeds = defaults. array ( forKey: " manualFanSpeeds " ) as? [ Int ] ?? [ ]
1227+ manualFanSpeeds = Self . normalizedManualFanSpeeds (
1228+ savedPerFanSpeeds,
1229+ fanCount: savedPerFanSpeeds. count,
1230+ fallback: manualSpeed,
1231+ minSpeed: minSpeed,
1232+ maxSpeed: maxSpeed
1233+ )
1234+
11531235 let savedAggr = defaults. double ( forKey: " autoAggressiveness " )
11541236 if savedAggr >= 0.0 && savedAggr <= 3.0 {
11551237 autoAggressiveness = savedAggr
@@ -1165,6 +1247,7 @@ final class FanController: ObservableObject {
11651247 let defaults = UserDefaults . standard
11661248 defaults. set ( mode. canonicalMode. rawValue, forKey: " fanControlMode " )
11671249 defaults. set ( manualSpeed, forKey: " manualFanSpeed " )
1250+ defaults. set ( manualFanSpeeds, forKey: " manualFanSpeeds " )
11681251 defaults. set ( autoAggressiveness, forKey: " autoAggressiveness " )
11691252 defaults. set ( autoMaxSpeed, forKey: " autoMaxSpeed " )
11701253 }
0 commit comments