SystemStat: add thermal_zone fallback for CPU and GPU temperature

The hwmon-based temperature detection only supports coretemp (Intel),
k10temp and zenpower (AMD). On ARM SoCs using SCMI firmware sensors
(e.g., CIX Sky1 with Mali-G720), temperature data is exposed via
/sys/class/thermal/thermal_zone* rather than hwmon.

Add a fallback that scans thermal zones when no hwmon sensor is found:

- CPU: reads all cpu-*-thermal zones and reports the hottest core
- GPU: uses gpu-avg-thermal (firmware average) when available,
  otherwise takes the max of individual gpu[N]-thermal zones

This enables system monitor temperature display on ARM platforms
without requiring any user configuration.

Tested on CIX Sky1 (Radxa Orion O6) with 14 SCMI thermal zones.
This commit is contained in:
Entrpi
2026-02-02 16:11:19 +11:00
parent 7134da650c
commit 2d8e41beaf
+185 -3
View File
@@ -242,6 +242,15 @@ Singleton {
property int intelTempFilesChecked: 0
property int intelTempMaxFiles: 20 // Will test up to temp20_input
// Thermal zone fallback (for ARM SoCs with SCMI sensors, etc.)
// Matches thermal zone types containing "cpu" and picks the hottest big-core zone.
// Also provides GPU temp via zones like "gpu-avg-thermal" or "gpu0-thermal".
readonly property var thermalZoneCpuPatterns: ["cpu-b", "cpu-m", "cpu"]
readonly property var thermalZoneGpuPatterns: ["gpu-avg", "gpu0", "gpu"]
property string cpuThermalZonePath: ""
property var cpuThermalZonePaths: [] // All matching CPU zones for averaging
property string gpuThermalZonePath: ""
// GPU temperature detection
// On dual-GPU systems, we prioritize discrete GPUs over integrated GPUs
// Priority: NVIDIA (opt-in) > AMD dGPU > Intel Arc > AMD iGPU
@@ -592,8 +601,8 @@ Singleton {
function checkNext() {
if (currentIndex >= 16) {
// Check up to hwmon10
Logger.w("No supported temperature sensor found");
// No hwmon sensor found, try thermal_zone fallback (ARM SoCs, SCMI, etc.)
thermalZoneScanner.startScan();
return;
}
@@ -657,6 +666,159 @@ Singleton {
}
}
// --------------------------------------------
// Thermal zone fallback for CPU and GPU temperature
// Used on ARM SoCs (e.g., SCMI sensors) where hwmon doesn't expose
// coretemp/k10temp/zenpower. Scans /sys/class/thermal/thermal_zoneN/type
// for CPU and GPU zone names, then reads temp from all matching zones.
//
// CPU: reads all cpu-*-thermal zones and reports the hottest core.
// GPU: prefers gpu-avg-thermal (firmware average), falls back to max of gpu[0-9]-thermal.
FileView {
id: thermalZoneScanner
property int currentIndex: 0
property var cpuZones: []
property var gpuZones: []
property string gpuAvgZonePath: ""
printErrors: false
function startScan() {
currentIndex = 0;
cpuZones = [];
gpuZones = [];
gpuAvgZonePath = "";
checkNext();
}
function checkNext() {
if (currentIndex >= 20) {
finishScan();
return;
}
thermalZoneScanner.path = `/sys/class/thermal/thermal_zone${currentIndex}/type`;
thermalZoneScanner.reload();
}
onLoaded: {
const name = text().trim();
const zonePath = `/sys/class/thermal/thermal_zone${currentIndex}`;
if (name.startsWith("cpu") && name.endsWith("thermal")) {
cpuZones.push({"type": name, "path": zonePath + "/temp"});
} else if (name === "gpu-avg-thermal") {
gpuAvgZonePath = zonePath + "/temp";
} else if (/^gpu[0-9]+-?thermal$/.test(name)) {
gpuZones.push({"type": name, "path": zonePath + "/temp"});
}
currentIndex++;
Qt.callLater(() => { checkNext(); });
}
onLoadFailed: function (error) {
currentIndex++;
Qt.callLater(() => { checkNext(); });
}
function finishScan() {
// CPU thermal zones
if (cpuZones.length > 0) {
root.cpuTempSensorName = "thermal_zone";
root.cpuThermalZonePaths = cpuZones.map(z => z.path);
const types = cpuZones.map(z => z.type).join(", ");
Logger.i("SystemStat", `Found ${cpuZones.length} CPU thermal zone(s): ${types}`);
} else {
Logger.w("No supported temperature sensor found");
}
// GPU thermal zones
if (gpuAvgZonePath !== "") {
root.gpuThermalZonePath = gpuAvgZonePath;
root.gpuAvailable = true;
root.gpuType = "thermal_zone";
Logger.i("SystemStat", `Found GPU thermal zone: gpu-avg-thermal`);
} else if (gpuZones.length > 0) {
root.gpuThermalZonePaths = gpuZones.map(z => z.path);
root.gpuThermalZonePath = gpuZones[0].path; // fallback single path
root.gpuAvailable = true;
root.gpuType = "thermal_zone";
const types = gpuZones.map(z => z.type).join(", ");
Logger.i("SystemStat", `Found ${gpuZones.length} GPU thermal zone(s): ${types} (using max)`);
}
}
}
// Thermal zone reader for CPU: reads all zones, reports max (hottest core)
FileView {
id: cpuThermalZoneReader
property int currentZoneIndex: 0
property var collectedTemps: []
printErrors: false
onLoaded: {
const temp = parseInt(text().trim()) / 1000.0;
if (!isNaN(temp) && temp > 0)
collectedTemps.push(temp);
currentZoneIndex++;
Qt.callLater(() => { readNextCpuThermalZone(); });
}
onLoadFailed: function (error) {
currentZoneIndex++;
Qt.callLater(() => { readNextCpuThermalZone(); });
}
}
function readNextCpuThermalZone() {
if (cpuThermalZoneReader.currentZoneIndex >= root.cpuThermalZonePaths.length) {
if (cpuThermalZoneReader.collectedTemps.length > 0) {
root.cpuTemp = Math.round(Math.max(...cpuThermalZoneReader.collectedTemps));
} else {
root.cpuTemp = 0;
}
root.pushCpuTempHistory();
return;
}
cpuThermalZoneReader.path = root.cpuThermalZonePaths[cpuThermalZoneReader.currentZoneIndex];
cpuThermalZoneReader.reload();
}
// Thermal zone reader for GPU: reads single zone (gpu-avg-thermal) or max of gpu[N] zones
FileView {
id: gpuThermalZoneReader
property int currentZoneIndex: 0
property var collectedTemps: []
printErrors: false
onLoaded: {
const temp = parseInt(text().trim()) / 1000.0;
if (!isNaN(temp) && temp > 0)
collectedTemps.push(temp);
// If we have multiple GPU zones (no gpu-avg), iterate and take max
if (root.gpuThermalZonePaths && root.gpuThermalZonePaths.length > 0) {
currentZoneIndex++;
if (currentZoneIndex < root.gpuThermalZonePaths.length) {
Qt.callLater(() => { readNextGpuThermalZone(); });
return;
}
// All zones read, take max
root.gpuTemp = Math.round(Math.max(...collectedTemps));
} else {
// Single gpu-avg-thermal zone
root.gpuTemp = Math.round(temp);
}
root.pushGpuHistory();
}
}
function readNextGpuThermalZone() {
gpuThermalZoneReader.path = root.gpuThermalZonePaths[gpuThermalZoneReader.currentZoneIndex];
gpuThermalZoneReader.reload();
}
// Property to store multiple GPU thermal zone paths (when no gpu-avg is available)
property var gpuThermalZonePaths: []
// --------------------------------------------
// --------------------------------------------
// GPU Temperature
@@ -1104,6 +1266,11 @@ Singleton {
root.intelTempValues = [];
root.intelTempFilesChecked = 0;
checkNextIntelTemp();
} // For thermal_zone fallback (ARM SoCs, SCMI, etc.), read all CPU zones and take max
else if (root.cpuTempSensorName === "thermal_zone") {
cpuThermalZoneReader.currentZoneIndex = 0;
cpuThermalZoneReader.collectedTemps = [];
readNextCpuThermalZone();
}
}
@@ -1160,7 +1327,11 @@ Singleton {
// Priority (when dGPU monitoring disabled): AMD iGPU only (discrete GPUs skipped to preserve D3cold)
function selectBestGpu() {
if (root.foundGpuSensors.length === 0) {
Logger.d("SystemStat", "No GPU temperature sensor found");
// No hwmon GPU sensors found, try thermal_zone fallback
if (root.gpuThermalZonePath === "" && root.gpuThermalZonePaths.length === 0) {
// Thermal zone scanner hasn't found GPU zones yet; start a scan
thermalZoneScanner.startScan();
}
return;
}
@@ -1222,6 +1393,17 @@ Singleton {
} else if (root.gpuType === "amd" || root.gpuType === "intel") {
gpuTempReader.path = `${root.gpuTempHwmonPath}/temp1_input`;
gpuTempReader.reload();
} else if (root.gpuType === "thermal_zone") {
if (root.gpuThermalZonePaths && root.gpuThermalZonePaths.length > 0) {
// Multiple GPU zones (no gpu-avg), read all and take max
gpuThermalZoneReader.currentZoneIndex = 0;
gpuThermalZoneReader.collectedTemps = [];
readNextGpuThermalZone();
} else {
// Single gpu-avg-thermal zone
gpuThermalZoneReader.path = root.gpuThermalZonePath;
gpuThermalZoneReader.reload();
}
}
}
}