System Management Controller (SMC) Interface and Background Timer

A project log for macOS Temperature Monitor

A temperature monitor for macOS 10.15+ using SwiftUI

Rob GrizzardRob Grizzard 02/22/2020 at 21:470 Comments

The SMC can be used to retrieve various sensor readings [1] and set fan speeds [2].  The fuzzer at [1] is very interesting, and I may use it in the future to dynamically populate a list of available sensors, but for now I am content to use the average of two SMC keys (TC0E and TC0F) that report the CPU die temperature as reported by a superuser forum post.  I am following Sébastien Lavoie's example and applying the GNU General Public License v2.0 to this project because it uses devnull's smc library [3].

A background timer was used to request the CPU temperature at regular intervals.  The steps followed for its implementation were:

  1. Create a new Swift file within the macos-temperature-monitor group and call it "Background_Timer.swift".  The class, "Background_Timer", should conform to the ObservableObject protocol for interaction with the UI. 
  2. Daniel Galasko's excellent post on Medium about background timers in Swift [4] was followed to include the class "RepeatingTimer".
  3. An instance of RepeatingTimer was added as a property of instances of Background_Timer, and its event handler was modified during initialization to update its counter property:
    init() { myRT = RepeatingTimer(timeInterval: 10) myRT.eventHandler = { print("Timer Fired") self.incrementCounter() print("Counter value: \(self.counter)") } myRT.resume() }
  4. The UI was updated to display the counter from the timer [5].

Next, the SMC logic was added.

  1. The file "XPC_Tester.swift" was renamed to "CPU_Temp_Handler.swift" and its contents were replaced with an empty class called "CPU_Temp_Handler" that conforms to the ObservedObject protocol.
  2. A new group was added to CPU_Temp_XPC target, and it was called "SMC"
  3. A C file was added to the SMC group, and it was named "smc.c".  A header was also created automatically by selecting "Also create a header file" from the dialog. 
    smc.c was added to the SMC group and applied to the CPU_Temp_XPC target.
    An Objective-C bridging header was created by selecting that option from the dialog.
  4. The contents for these files came from the examples at [3], with the addition of of the function void getCpuTemp(char* to_write, size_t size) that can be used to get the average of the two CPU temperature sensors and write their average in Fahrenheit to an input buffer.
    void getCpuTemp(char* to_write, size_t size) { double temperature_A = SMCGetTemperature(SMC_KEY_CPU_0_DIE_TEMP_A); double temperature_B = SMCGetTemperature(SMC_KEY_CPU_0_DIE_TEMP_B); double avg_temp = temperature_A; if (temperature_B != 0.0) { avg_temp = (temperature_A + temperature_B) / 2.0; } avg_temp = convertToFahrenheit(avg_temp); //printf("size: %lu", size); snprintf(to_write, size, "%fF", avg_temp); }
  5. The XPC interface was updated to interact with this library:
    1. The function prototypefunc getCPUTemp(withReply reply: @escaping (String) -> Void) was added to the protocol
    2. Its definition was added to the CPU_Temp_XPC class:
      func getCPUTemp(withReply reply: @escaping (String) -> Void){ _ = SMCOpen() var toReturn = "" let sizeToReturn: CUnsignedLong = 10 var addressBuffer = [Int8](repeating:0, count:Int(sizeToReturn)) getCpuTemp(&addressBuffer, Int(sizeToReturn)) let data = Data(bytes: addressBuffer as [Int8], count: Int(CUnsignedLong(sizeToReturn))); toReturn = String(data: data, encoding: .utf8) ?? "" SMCClose() reply(toReturn) <br>
  6. There likely exists a better way to update the UI with the value retrieved from the XPC Service passing data from the SMC than what follows, and if you have improvements then please let me know:
    1. The class that communicates with the XPC Service, CPU_Temp_Handler, has an instance property to store the String returned from the XPC Service, and a function to assign the returned value to this property
      import Foundation import CPU_Temp_XPC class CPU_Temp_Handler { var CPU_Temp = "" func setCPUTemp() { let connection = NSXPCConnection(serviceName: "com.grizz.CPU-Temp-XPC") connection.remoteObjectInterface = NSXPCInterface(with: CPU_Temp_XPC_Protocol.self) connection.resume() let service = connection.remoteObjectProxyWithErrorHandler { error in print("Received error:", error) } as? CPU_Temp_XPC_Protocol service?.getCPUTemp() { response in self.CPU_Temp = response } } }
    2. The Background_Timer class has an instance of CPU_Temp_Handler and copies its value of the CPU temperature returned from the XPC Service.
      1. The Background_Timer class then rounds and publishes the CPU temperature value to the UI.
        class Background_Timer: ObservableObject { var my_CPU_Temp_Handler = CPU_Temp_Handler() @Published var cpu_Temp_copy = "" let myRT: RepeatingTimer init() { myRT = RepeatingTimer(timeInterval: 5) myRT.eventHandler = { print("Timer Fired") self.updateCPUTemp() print("CPU temp: \(self.my_CPU_Temp_Handler.CPU_Temp)") } myRT.resume() } func updateCPUTemp() { DispatchQueue.main.sync { self.my_CPU_Temp_Handler.setCPUTemp() let temp = self.my_CPU_Temp_Handler.CPU_Temp self.cpu_Temp_copy = String(format: "%3.2f", (temp as NSString).doubleValue) } } }<br>
  7. There is an issue where the first few timer firings do not retrieve a value from the XPC Service.
  8. The ContentView then consumes the temperature value:
  9. This is all shown in the commit at [6].

Please reach out if you see areas for improvement.