feat(gui): Systemanzeige in der Kopfzeile (Speicherring + Turbine)
Zwei Symbole zwischen Bluetooth-Tab und Hell/Dunkel-Schalter: ein Ring mit der Plattenbelegung in Prozent und eine Turbine, deren Drehzahl der CPU-Last folgt. Beide klappen bei Mauskontakt eine Karte mit Zahlen auf (belegt/frei/ gesamt bzw. CPU, RAM, Temperatur). Neues Modul sysstat.js liest alles aus /proc bzw. fs.statfsSync, damit die GUI im Sekundentakt fragen kann, ohne df/top-Prozesse zu starten. CPU-% aus zwei /proc/stat-Messpunkten mit eigenem 2-s-Ticker, RAM ueber MemAvailable, Temperatur als waermste SoC-/Core-Zone. Doku: Kapitel 07 + Changelog.
This commit is contained in:
+105
@@ -0,0 +1,105 @@
|
||||
// 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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user