Die Zeile zeigt nur noch den Wert in Grad. Damit die neue Beschriftung stimmt, filtert sysstat.js die Zonen enger: nur soc-thermal, bigcore0/1 und littlecore. GPU-, NPU- und Center-Zone konnten vorher den Hoechstwert stellen, obwohl "CPU" daran steht. Doku: Kapitel 07 + Changelog.
154 lines
5.9 KiB
JavaScript
154 lines
5.9 KiB
JavaScript
// Systemwerte für die Kopfzeile: Plattenbelegung, CPU-Last, RAM, Temperatur.
|
|
// Bewusst ohne Shell-Aufrufe (kein df/top) — alles aus /proc bzw. statfs, damit
|
|
// die GUI im Sekundentakt fragen darf, ohne Prozesse zu starten.
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const DISK_PATH = path.join(__dirname, 'recordings');
|
|
|
|
// ---- CPU ----------------------------------------------------------------
|
|
// Auslastung braucht zwei Messpunkte. Ein eigener Ticker hält den Wert bereit,
|
|
// sonst wäre die erste Antwort nach jedem Start immer 0 %.
|
|
let prevCpu = null;
|
|
let cpuPct = 0;
|
|
|
|
function sampleCpu() {
|
|
try {
|
|
const line = fs.readFileSync('/proc/stat', 'utf8').split('\n', 1)[0];
|
|
const v = line.trim().split(/\s+/).slice(1).map(Number).filter(n => Number.isFinite(n));
|
|
if (v.length < 5) return;
|
|
const idle = v[3] + v[4]; // idle + iowait
|
|
const total = v.reduce((a, b) => a + b, 0);
|
|
if (prevCpu) {
|
|
const dTotal = total - prevCpu.total;
|
|
const dIdle = idle - prevCpu.idle;
|
|
if (dTotal > 0) cpuPct = Math.max(0, Math.min(100, (1 - dIdle / dTotal) * 100));
|
|
}
|
|
prevCpu = { total, idle };
|
|
} catch { /* ohne /proc bleibt der letzte Wert stehen */ }
|
|
}
|
|
sampleCpu();
|
|
setInterval(sampleCpu, 2000).unref();
|
|
|
|
// ---- Temperatur ---------------------------------------------------------
|
|
// Gemeldet wird die CPU-Temperatur: wärmste der Kern-Zonen (bigcore0/1,
|
|
// littlecore) plus soc-thermal. GPU, NPU und die WLAN-Karte bleiben draußen —
|
|
// die GUI beschriftet den Wert als „CPU-Temperatur“, also darf auch nur CPU
|
|
// drinstecken.
|
|
const TEMP_ZONES = (() => {
|
|
try {
|
|
return fs.readdirSync('/sys/class/thermal')
|
|
.filter(d => /^thermal_zone\d+$/.test(d))
|
|
.map(d => {
|
|
const dir = `/sys/class/thermal/${d}`;
|
|
let type = d;
|
|
try { type = fs.readFileSync(`${dir}/type`, 'utf8').trim(); } catch { }
|
|
return { type, file: `${dir}/temp` };
|
|
})
|
|
.filter(z => /^(soc|bigcore\d*|littlecore)-thermal$/i.test(z.type));
|
|
} catch { return []; }
|
|
})();
|
|
|
|
function readTemp() {
|
|
let best = null;
|
|
for (const z of TEMP_ZONES) {
|
|
try {
|
|
const c = Number(fs.readFileSync(z.file, 'utf8').trim()) / 1000;
|
|
if (Number.isFinite(c) && (!best || c > best.c)) best = { c, zone: z.type };
|
|
} catch { }
|
|
}
|
|
return best;
|
|
}
|
|
|
|
// ---- RAM ----------------------------------------------------------------
|
|
// MemAvailable statt MemFree: Cache zählt nicht als belegt, sonst stünde das
|
|
// Board dauernd auf 95 %.
|
|
function readMem() {
|
|
try {
|
|
const txt = fs.readFileSync('/proc/meminfo', 'utf8');
|
|
const kb = k => {
|
|
const m = txt.match(new RegExp(`^${k}:\\s+(\\d+) kB`, 'm'));
|
|
return m ? Number(m[1]) * 1024 : null;
|
|
};
|
|
const total = kb('MemTotal');
|
|
const avail = kb('MemAvailable') ?? kb('MemFree');
|
|
if (!total || avail == null) return null;
|
|
return { totalBytes: total, freeBytes: avail, usedBytes: total - avail, usedPct: (1 - avail / total) * 100 };
|
|
} catch { return null; }
|
|
}
|
|
|
|
// ---- Aufnahmen ----------------------------------------------------------
|
|
// Summe aller Audiodateien unter recordings/. Der Scan ist billig (readdir +
|
|
// stat, keine Lesevorgänge), wird aber gepuffert, damit ein Poll im
|
|
// Sekundentakt nicht dauernd über das Verzeichnis läuft.
|
|
const AUDIO_EXT = /\.(mp3|wav|m4a|opus|ogg|flac|aac)$/i;
|
|
const REC_SCAN_TTL_MS = 60_000;
|
|
let recCache = { bytes: 0, files: 0, at: 0 };
|
|
let recScan = null;
|
|
|
|
async function scanRecordings() {
|
|
let bytes = 0, files = 0;
|
|
try {
|
|
const entries = await fs.promises.readdir(DISK_PATH, { withFileTypes: true, recursive: true });
|
|
for (const e of entries) {
|
|
if (!e.isFile() || !AUDIO_EXT.test(e.name)) continue;
|
|
try {
|
|
const st = await fs.promises.stat(path.join(e.parentPath || e.path || DISK_PATH, e.name));
|
|
bytes += st.size; files++;
|
|
} catch { /* zwischenzeitlich gelöscht */ }
|
|
}
|
|
} catch { return recCache; }
|
|
recCache = { bytes, files, at: Date.now() };
|
|
return recCache;
|
|
}
|
|
|
|
function recordingsSize() {
|
|
const fresh = Date.now() - recCache.at < REC_SCAN_TTL_MS;
|
|
if (!fresh && !recScan) recScan = scanRecordings().finally(() => { recScan = null; });
|
|
// Beim allerersten Aufruf gibt es noch keine Zahl — dann auf den Scan warten,
|
|
// sonst zeigt die Anzeige direkt nach dem Start einmal 0 % Belegung.
|
|
return recCache.at === 0 ? recScan : recCache;
|
|
}
|
|
|
|
// ---- Platte -------------------------------------------------------------
|
|
// Zwei Sichtweisen auf denselben Datenträger:
|
|
// partition: was df sagt (inklusive System, Modelle, node_modules)
|
|
// budget: der Platz, um den es dem Nutzer geht — freier Platz PLUS die
|
|
// Aufnahmen, die schon daliegen. Das ist genau das, was ein frisch
|
|
// aufgesetztes System dieser Ausbaustufe für Aufnahmen übrig
|
|
// hätte. Die Prozentzahl im Ring = Aufnahmen / Budget.
|
|
async function readDisk() {
|
|
let st;
|
|
try { st = fs.statfsSync(fs.existsSync(DISK_PATH) ? DISK_PATH : __dirname); } catch { return null; }
|
|
const bs = st.bsize;
|
|
const totalBytes = st.blocks * bs;
|
|
const freeBytes = st.bavail * bs;
|
|
const usedBytes = (st.blocks - st.bfree) * bs;
|
|
const partDenom = usedBytes + freeBytes;
|
|
|
|
const rec = await recordingsSize();
|
|
const recBytes = rec ? rec.bytes : 0;
|
|
const budgetBytes = freeBytes + recBytes;
|
|
|
|
return {
|
|
totalBytes, freeBytes, usedBytes,
|
|
partPct: partDenom > 0 ? (usedBytes / partDenom) * 100 : 0,
|
|
recBytes, recFiles: rec ? rec.files : 0,
|
|
budgetBytes,
|
|
usedPct: budgetBytes > 0 ? (recBytes / budgetBytes) * 100 : 0
|
|
};
|
|
}
|
|
|
|
export async function status() {
|
|
const t = readTemp();
|
|
return {
|
|
cpu: { pct: Math.round(cpuPct * 10) / 10, cores: os.cpus().length, load1: Math.round(os.loadavg()[0] * 100) / 100 },
|
|
ram: readMem(),
|
|
disk: await readDisk(),
|
|
temp: t ? { c: Math.round(t.c * 10) / 10, zone: t.zone } : null
|
|
};
|
|
}
|