I lost the ability to use iStat Pro [1] to monitor the temperatures of my macs when Apple removed the dashboard, and I became interested in writing a replacement. Those interested in a very full-featured replacement will likely be satisfied by iStat Menus [2,3] and its add-on for sensor readings [4]. I am doing this as a learning exercise and welcome all feedback.
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:
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.
Daniel Galasko's excellent post on Medium about background timers in Swift [4] was followed to include the class "RepeatingTimer".
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()
}
The UI was updated to display the counter from the timer [5].
Next, the SMC logic was added.
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.
A new group was added to CPU_Temp_XPC target, and it was called "SMC"
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.
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);
}
The XPC interface was updated to interact with this library:
The function prototypefunc getCPUTemp(withReply reply: @escaping (String) -> Void) was added to the protocol
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>
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:
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...
I started adding an XPC Service [1] by using XCode's built-in functionality.
At this point, I consulted the excellent blog post [2] by Matthew Miner, in which the process of converting the boilerplate [3] Objective-C code into Swift is described. Interested readers should consult the original source, but the steps are repeated here.
NOTE - the NSXPCConnection used for testing must use the target's bundle identifier
This commit adds a class, XPC_Tester, and some front-end logic to test that the XPC Service is configured properly. The test class conforms to the ObservableObject protocol and publishes a variable for tracking by the front-end. The front-end in turn calls the tester to convert an input String to uppercase using the XPC Service.