// 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 --------------------------------------------------------- // Interessant sind die SoC-/Kern-Zonen; WLAN-Karte & Co. bleiben draußen, sonst // zeigt die Anzeige eine Temperatur, die nichts mit der Rechenlast zu tun hat. 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|core|center|npu|gpu/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; } } // ---- Platte ------------------------------------------------------------- // Gemessen wird die Partition, auf der die Aufnahmen liegen — das ist der // Platz, der der App tatsächlich ausgeht. Die Prozentzahl folgt df: der für // root reservierte Anteil zählt als belegt. function readDisk() { try { const st = fs.statfsSync(fs.existsSync(DISK_PATH) ? DISK_PATH : __dirname); const bs = st.bsize; const totalBytes = st.blocks * bs; const freeBytes = st.bavail * bs; const usedBytes = (st.blocks - st.bfree) * bs; const denom = usedBytes + freeBytes; return { totalBytes, freeBytes, usedBytes, usedPct: denom > 0 ? (usedBytes / denom) * 100 : 0 }; } catch { return null; } } export 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: readDisk(), temp: t ? { c: Math.round(t.c * 10) / 10, zone: t.zone } : null }; }