mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
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:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user