←News detail

Tencent Cloud Developer CommunityUpdated Oct 26 00:11

eul, unmaintained for 5 years, is fixed with Workbuddy and restores temperature and GPU monitoring on Apple M6

AI summary

The open-source menu bar monitoring tool eul, unmaintained for 5 years, completed a low-level fix with Workbuddy and once again reads CPU temperature and GPU information on the Mac Mini 6 with the Apple M6 chip. The root cause is that SMCParamStruct changed from 80 bytes to 88 bytes in arm64 user mode, and all 135 temperature keys on the M6 are of the flt type rather than the sp78 type from the Intel era. After the fix, it dynamically enumerates 2,066 SMC keys, retains 126 valid sensors, and remains compatible with the logic for Intel models.

Body · AI translation

I. Opening: The Scene of Death

I recently got my hands on a Mac Mini 6 (Apple M6 chip), planning to use it to evaluate the performance of small models on small machines. So I needed a tool that could visually and conveniently observe system performance. After comparing options, the open-source project eul was a perfect fit—it's compact enough (the app is only about 5MB), and it natively lives in the menu bar.

But after installing it with brew, the result was dumbfounding: CPU temperature and all GPU-related information showed N/A. For scenarios involving large-model testing, this information is extremely critical.

::: center Figure 1.1 CPU temperature and all GPU-related information showing N/A ::: So I found eul's source code. On GitHub, the latest commit on the master branch is b5c3bd0, with the commit message reading Create Localizable.strings (#238). The date column prominently displays: 5 years ago.

::: center Figure 1.2 Update status of the eul project source code ::: The 10k stars in the upper right corner seem to showcase past glory, just lying quietly on GitHub. In the Issues section, someone asked two years ago: "Is there any hope for M1?" No one answered.

Is there any hope for it on M6? I don't have much macOS app development experience, but in today's AI era, perhaps it's worth a try.

--- II. The Problem: The Investigation Scene After getting the 5-year-old source code, I found that eul's temperature-reading implementation comes from SMCKit, which dates back to the Intel era. SMCKit's README explicitly states it is "for Intel based Macs," and its built-in sensor list consists entirely of keys for Intel models (TC0P/TG0P/TM0S, etc.), which obviously can do nothing for Apple Silicon.

Using a Debug build, the assertion triggers directly, and the app crashes on launch: eul/SMC.swift:390: Assertion failed: SMCParamStruct size is != 80

Clearly, some of the original data structures no longer apply to today's M6 runtime environment.

After looking up some materials, I discovered something even more disheartening: Apple has never publicly documented the AppleSMC API or its key naming conventions. Am I supposed to blindly guess based on my limited macOS development experience?

It's time to use the tool of this era: Workbuddy

--- III. Revive, eul: Write the Probe First, Then Operate

Since there is no official documentation support, Workbuddy first wrote a Swift probe (smcprobe.swift) that talks directly to the underlying driver. The probe does two things: read #KEY to get the total number of keys on the machine, enumerate them one by one, filter out all temperature keys with the T prefix, and decode their current values. Traverse the IOAccelerator services and print all statistics from PerformanceStatistics.

The most dramatic moment happened: the probe, meant to troubleshoot the fault, crashed first.

While reading data, the probe's underlying IOKit call directly threw an error: IOKit call error: 0x-1ffffd39. To find out why, I had the probe print the size of the data structure it sends to the driver. It turned out the size had become 88 bytes.

This exactly explains the assertion error when eul crashed on launch earlier: SMCParamStruct size is != 80. Clearly, faced with the 88-byte driver data packet, the program's underlying layer simply refused to communicate.

This means the probe used to troubleshoot the problem perfectly reproduced eul's fault on its own. This directly turned root-cause localization from "blind men feeling an elephant" into "precise measurement."

Once the probe ran successfully, it obtained first-hand data from the M6: Total number of SMC keys: 2066 Number of Tx temperature keys: 135 Temperature type: all 135 / 135 are of flt type, with no sp78

Workbuddy followed the trail and formulated a "four-cut" repair plan. First cut: Stitch up the ABI crack (88 bytes vs 80 bytes) The SMCParamStruct struct comes from Apple's early open-source projects, and the driver side always parses it as 80 bytes. But one field in the struct, IOByteCount, becomes 64-bit (8 bytes) in arm64 user space. These extra 4 bytes plus alignment padding make the struct 88 bytes. Debug builds crash directly, and Release builds fail silently on calls. Fix: Without relying on platform definitions, Workbuddy suggested fixing the type of dataSize directly as UInt32. This ensures a stable binary layout and removes three redundant type conversions. Second cut: Adaptive data types (sp78 vs flt) The original code hard-coded the reading logic as the Intel-era sp78 (2-byte fixed point). But the M6 is entirely flt (4-byte IEEE 754 floating point). Reading it as sp78 yields only garbage values. Fix: Change from "assuming the type" to "querying the type." First query keyInformation to get the real type, then choose the decoder based on the actual type. Workbuddy also conveniently fixed the bug in the original fromSP78 that lost decimal places, and added a fromFLT decoder, covering both T2 and Apple Silicon chips in one go. Third cut: Abandon the static list, embrace dynamic discovery The original code only iterated over 28 hard-coded Intel keys. On the M6, none of these keys exist, resulting in an empty sensor list. Fix: Add an allTemperatureSensors() function that directly enumerates 2066 keys, filters out 135 candidates by "T prefix + temperature type," then removes uninitialized thermal zone placeholders (such as Tz11, which is always 0.0 °C), and finally keeps 126 valid sensors. Correction interlude: When aggregating temperatures, the first version of the code matched prefixes by "display name." But the display names had long been rewritten (for example, to "CPU P-Core"), so the matching failed. Workbuddy reminded us: identifiers (key codes) and display names must be strictly separated. After switching to matching by code prefixes such as Tp0 and Tg0, the temperatures finally appeared. Fourth cut: GPU identification from virtual to real The original code required the GPU to have a PCI identifier (device-id / IOPCIMatch). But on Apple Silicon, the integrated GPU has no such thing at all, causing the GPU list to be discarded outright. Fix: Make device-id go from "required" to "optional," falling back to the model name (such as Apple M6) when missing. GPU temperature also changed from "must have a registry field" to "registry first, SMC Tg0x as fallback."

--- IV. It Runs, But This Is Not the End

After some fine-tuning of compilation and build parameters (adjusting SDKROOT, etc.), the app finally ran successfully.

::: center Figure 4.1 CPU temperature and all GPU-related information displaying normally :::

But this is not just a victory of a few numbers. The code comparison before and after the fix is essentially pulling eul's underlying collection architecture back from "a dead end for Intel models" onto the right track of "facing the underlying protocol." Overall architecture evolution after the fix

::: center Figure 4.2 Overall architecture evolution ::: As can be seen from the figure above, the system's data flow has not changed; it is still bottom-up (hardware -> interface layer -> collection layer -> state layer -> presentation layer). The real change happens in those few arrows at the very bottom.

Previously, the underlying interface layer (SMCKit / IOHelper) was "hard-wired" to the hardware, with key names, types, and identifiers all hard-coded; now they have become a general-purpose pipeline with a "translator" and a "filter." Temperature reading chain: From "dead end" to "adaptive"

::: center Figure 4.3 Temperature reading chain ::: On the left is the original path: look up by 28 hard-coded Intel key names -> not found -> empty list -> display N/A. On the right is the fixed path: enumerate all keys -> filter by flt/sp78 type -> sanity filtering (removing invalid thermal zones like Tz11 that are always 0) -> cluster by Key Code prefix and take the maximum value -> output the real temperature.

Architectural change: The "display name" and "identifier" are now completely decoupled. The original code looked up sensors by "CPU0DIE", so once the hardware changed its name, the code broke. Now the program only recognizes the underlying code prefixes such as Tp0 and TPD, while the display layer can wrap them into a nice-looking "CPU P-Core" at any time using friendlyName. This is the resilience that comes from being "protocol-oriented." GPU reading chain: After moving from the abstract to the concrete

::: center Figure 4.4 GPU reading chain ::: The fix for the GPU chain is much the same. The original code stubbornly searched for PCI identifiers (device-id / IOPCIMatch) in systemprofiler and IOAccelerator, and discarded the entire record if it could not find them.

After the fix, identifiers became a "dual-track system": if there is PCI, use PCI; if there is no PCI (such as the M6 integrated GPU), fall back to matching by model name (such as Apple M6). At the same time, GPU temperature is no longer required to come from the registry, but falls back to the SMC's Tg0x sensor cluster. Scale of changes and compatibility regression

This "surgery" looks like it touched the low level, but in fact the incision is tiny and nothing vital was damaged:

|File|Changes|Core mission| |:-:|:-:|:-:| |SMC.swift|+81 / -10|Fix the ABI crack, adaptive decoding, dynamic sensor discovery| |SmcControl.swift|+51 / -12|Sensor filtering, aggregate maximum temperature by prefix| |GPU.swift|+23 / -8|Remove the hard PCI dependency, fall back to model name compatibility| |GpuStore.swift|+11 / -3|Refactor GPU statistics matching logic| |project.pbxproj|+1 / -1|Fix the SwiftPM SDKROOT script error| More important is compatibility. Through regression verification, the logic for Intel models was not only not broken, but actually became better because the decimal precision of sp78 was fixed. Named sensors (such as CPU0DIE) are still the first priority on Intel machines, and the fallback branch is never triggered at all. This is what a qualified open-source refactor looks like: don't break other people's rice bowls, just mend your own broken leg.

Looking back at this fix, the role Workbuddy played was far more than a "code completion tool." In this process, it profoundly demonstrated a new paradigm for solving low-level system problems in the AI era.

1. AI bridges the obscure "black box gap"

In the traditional development model, faced with "undocumented APIs" and "error 0x-1ffffd39", an engineer lacking OS development experience would most likely get stuck consulting countless outdated StackOverflow posts. But Workbuddy can quickly understand the underlying logic of the crash log and, by generating probe code, "reverse-engineer" the struct size, thereby deriving the 88-byte ABI crack. AI turns the black box into a gray box, and turns trial and error from "a blind man feeling an elephant" into "following the map to find the horse."

2. From "how do I write it" to "how do I design it"

In this project, the core difficulty is not writing Swift syntax, but architectural decisions such as "how should sensors be aggregated more reasonably" and "separating identifiers from display names." Workbuddy is responsible for generating 80% of the boilerplate code, adaptations, and fix drafts; while humans are responsible for verifying, testing, and rewriting the 20% of critical logic. AI is responsible for charging forward, and humans are responsible for pulling the reins. This collaboration model greatly raises the ceiling of individual capability.

3. AI cannot bear the "final step" for you

AI can write the fromFLT decoder, but it cannot handle Xcode signing certificates for you; it can help you change pciMatch to optional, but it cannot bear the open-source maintenance responsibility after the code is merged into the main branch for you. The threshold for "writing code" is getting lower, but the threshold for "being responsible for the result" is always there.

--- Six, Conclusion: Code that stopped 5 years ago also has the right to run on the new M6

10k Stars, 380 Forks, 35 Contributors, 48 Releases. These numbers once represented an active project, and later became a digital monument.

In the next 5 years, perhaps it will stop updating again. But at least for now, it is running on this Mac Mini 6, reading a CPU temperature of 32 °C, along with continued tinkering with large-model evaluation.

---

Friends who like this article are welcome to follow, like, and bookmark — one-click triple support; everyone's support is the greatest motivation for writing articles

Source

Tencent Cloud Developer Community