L
ling-sys
↓ 0 downloadsSystem activity and performance monitor in the Ling Design System — dials, meshes, drums and crosshatch for CPU, GPU, memory, disk, network, board, processes and audio, with every subsystem given a voice. A LingOS app for rpi5: live board/CPU/memory/process stats from the kernel (stats-lingos.ling).
lingfu install ling-sysCode / Files (13)
collector.ps1
# LINGSYS collector — samples system activity once a second and writes
# key=value lines to stats.txt (atomically, via a tmp file). Static facts
# go to info.txt once. Ling reads both files; this script is the only
# part of the app that is not .ling, because Ling has no sensor builtins.
param([string]$OutDir = $PSScriptRoot)
$ErrorActionPreference = 'SilentlyContinue'
$statsPath = Join-Path $OutDir 'stats.txt'
$tmpPath = Join-Path $OutDir 'stats.tmp'
$infoPath = Join-Path $OutDir 'info.txt'
$infoTmp = Join-Path $OutDir 'info.tmp'
$enc = New-Object System.Text.UTF8Encoding $false
# CoreAudio peak meters and master volume for the default endpoints
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
[Guid("C02216F6-8C67-4B5B-9D00-D008E73E0064"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IAudioMeterInformation { int GetPeakValue(out float pfPeak); }
[Guid("5CDF2C82-841E-4546-9722-0CF74078229A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IAudioEndpointVolume {
int RegisterControlChangeNotify(IntPtr n);
int UnregisterControlChangeNotify(IntPtr n);
int GetChannelCount(out uint c);
int SetMasterVolumeLevel(float f, Guid g);
int SetMasterVolumeLevelScalar(float f, Guid g);
int GetMasterVolumeLevel(out float f);
int GetMasterVolumeLevelScalar(out float f);
}
[Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMMDevice { int Activate(ref Guid iid, int clsCtx, IntPtr p, [MarshalAs(UnmanagedType.IUnknown)] out object o); }
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMMDeviceEnumerator {
int EnumAudioEndpoints(int df, int mask, out IntPtr devices);
int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice endpoint);
}
[ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")] class MMDeviceEnumeratorComObject { }
public class LingAudio {
public static float Peak(int flow) {
var en = (IMMDeviceEnumerator)new MMDeviceEnumeratorComObject();
IMMDevice dev; en.GetDefaultAudioEndpoint(flow, 1, out dev);
var iid = typeof(IAudioMeterInformation).GUID; object o;
dev.Activate(ref iid, 1, IntPtr.Zero, out o);
float p; ((IAudioMeterInformation)o).GetPeakValue(out p);
return p;
}
public static float Volume() {
var en = (IMMDeviceEnumerator)new MMDeviceEnumeratorComObject();
IMMDevice dev; en.GetDefaultAudioEndpoint(0, 1, out dev);
var iid = typeof(IAudioEndpointVolume).GUID; object o;
dev.Activate(ref iid, 23, IntPtr.Zero, out o);
float v; ((IAudioEndpointVolume)o).GetMasterVolumeLevelScalar(out v);
return v;
}
}
'@
$hasAudio = $true
try { [void][LingAudio]::Peak(0) } catch { $hasAudio = $false }
# ---------- static facts, once ----------
$os = Get-CimInstance Win32_OperatingSystem
$cpu = Get-CimInstance Win32_Processor | Select-Object -First 1
$bb = Get-CimInstance Win32_BaseBoard
$bios = Get-CimInstance Win32_BIOS
$gpu0 = Get-CimInstance Win32_VideoController | Where-Object { $_.CurrentHorizontalResolution } | Select-Object -First 1
if ($null -eq $gpu0) { $gpu0 = Get-CimInstance Win32_VideoController | Select-Object -First 1 }
$nv = $null -ne (Get-Command nvidia-smi -ErrorAction SilentlyContinue)
$totalMB = [math]::Round($os.TotalVisibleMemorySize / 1024)
$info = @(
"host=$($env:COMPUTERNAME)"
"os=$($os.Caption) $($os.Version)"
"cpu_name=$($cpu.Name.Trim())"
"cpu_cores=$($cpu.NumberOfCores)"
"cpu_threads=$($cpu.NumberOfLogicalProcessors)"
"cpu_mhz=$($cpu.MaxClockSpeed)"
"mem_total=$totalMB"
"board=$($bb.Manufacturer) $($bb.Product)"
"bios=$($bios.SMBIOSBIOSVersion)"
"gpu_name=$($gpu0.Name)"
"nv=$([int]$nv)"
)
$sndIdx = 0
foreach ($sd in (Get-CimInstance Win32_SoundDevice | Select-Object -First 8)) {
$info += "snd$sndIdx=$($sd.Name)"
$sndIdx++
}
$info += "snd_n=$sndIdx"
# written to a tmp file then moved, so a half-written file is never read
[System.IO.File]::WriteAllLines($infoTmp, $info, $enc)
Move-Item -Force $infoTmp $infoPath
$boot = $os.LastBootUpTime
$prev = @{}
$prevT = Get-Date
$logical = [Environment]::ProcessorCount
# Probe the ACPI thermal zone once; it stalls each tick on machines without it.
$hasTz = $null -ne (Get-CimInstance -Namespace root/wmi -ClassName MSAcpi_ThermalZoneTemperature -ErrorAction SilentlyContinue | Select-Object -First 1)
# The app touches app.beat while it runs; quit when it goes stale so no
# orphan collector keeps spinning after a crash. A beat left over from a
# previous run must not count, so clear it first.
$beatPath = Join-Path $OutDir 'app.beat'
$beatSeen = $false
if (Test-Path $beatPath) { Remove-Item -Force $beatPath }
$startedAt = Get-Date
while ($true) {
if (Test-Path $beatPath) {
$beatSeen = $true
if (((Get-Date) - (Get-Item $beatPath).LastWriteTime).TotalSeconds -gt 25) { exit }
} elseif ($beatSeen) {
exit
} elseif (((Get-Date) - $startedAt).TotalSeconds -gt 90) {
exit
}
$lines = New-Object System.Collections.Generic.List[string]
$now = Get-Date
$lines.Add("ts=" + [int][double]((Get-Date -UFormat %s)))
$lines.Add("uptime=" + [int]($now - $boot).TotalSeconds)
# CPU per core + total (locale-independent formatted counters)
$pc = Get-CimInstance Win32_PerfFormattedData_PerfOS_Processor
$ncore = 0
foreach ($p in $pc) {
if ($p.Name -eq '_Total') { $lines.Add("cpu_total=" + $p.PercentProcessorTime) }
elseif ($p.Name -match '^\d+$') { $lines.Add("core$($p.Name)=" + $p.PercentProcessorTime); $ncore++ }
}
$lines.Add("cpu_count=$ncore")
# Memory
$pm = Get-CimInstance Win32_PerfFormattedData_PerfOS_Memory
$availMB = $pm.AvailableMBytes
$lines.Add("mem_total=$totalMB")
$lines.Add("mem_used=" + ($totalMB - $availMB))
$lines.Add("mem_faults=" + $pm.PageFaultsPersec)
$lines.Add("mem_commit=" + [math]::Round($pm.CommittedBytes / 1MB))
$lines.Add("mem_cache=" + [math]::Round($pm.CacheBytes / 1MB))
# Actual clock (turbo) as percent of base
$pi = Get-CimInstance Win32_PerfFormattedData_Counters_ProcessorInformation | Where-Object { $_.Name -eq '_Total' }
if ($pi) { $lines.Add("cpu_perf=" + $pi.PercentProcessorPerformance) }
# Disk: the total plus each physical disk, plus logical capacities
$pdAll = Get-CimInstance Win32_PerfFormattedData_PerfDisk_PhysicalDisk
$pd = $pdAll | Where-Object { $_.Name -eq '_Total' }
$lines.Add("disk_read=" + $pd.DiskReadBytesPersec)
$lines.Add("disk_write=" + $pd.DiskWriteBytesPersec)
$lines.Add("disk_busy=" + [math]::Min(100, $pd.PercentDiskTime))
$di = 0
foreach ($d in ($pdAll | Where-Object { $_.Name -ne '_Total' } | Sort-Object Name | Select-Object -First 8)) {
$dn = ($d.Name -replace '\|', ' ').Trim()
$lines.Add("dsk$di=" + $dn + "|" + $d.DiskReadBytesPersec + "|" + $d.DiskWriteBytesPersec + "|" + [math]::Min(100, $d.PercentDiskTime))
$di++
}
$lines.Add("dsk_n=$di")
$vi = 0
foreach ($v in (Get-CimInstance Win32_LogicalDisk -Filter 'DriveType=3' | Select-Object -First 8)) {
$lines.Add("drv$vi=" + ($v.DeviceID -replace ':','') + "|" + [math]::Round($v.Size / 1GB) + "|" + [math]::Round($v.FreeSpace / 1GB))
$vi++
}
$lines.Add("drv_n=$vi")
# Network (sum of real interfaces)
$rx = 0; $tx = 0
foreach ($ni in (Get-CimInstance Win32_PerfFormattedData_Tcpip_NetworkInterface)) {
$rx += $ni.BytesReceivedPersec; $tx += $ni.BytesSentPersec
}
$lines.Add("net_rx=$rx")
$lines.Add("net_tx=$tx")
# System totals
$ps = Get-CimInstance Win32_PerfFormattedData_PerfOS_System
$lines.Add("procs=" + $ps.Processes)
$lines.Add("threads=" + $ps.Threads)
$lines.Add("cswitch=" + $ps.ContextSwitchesPersec)
# GPU
if ($nv) {
$q = nvidia-smi --query-gpu=utilization.gpu,memory.used,memory.total,temperature.gpu,fan.speed,power.draw --format=csv,noheader,nounits 2>$null
if ($q) {
$f = ($q -split ',').ForEach({ $_.Trim() })
$lines.Add("gpu_load=" + $f[0]); $lines.Add("gpu_mem_used=" + $f[1]); $lines.Add("gpu_mem_total=" + $f[2])
$lines.Add("gpu_temp=" + $f[3]); $lines.Add("gpu_fan=" + $f[4]); $lines.Add("gpu_power=" + $f[5])
}
} else {
$g3d = 0.0
foreach ($ge in (Get-CimInstance Win32_PerfFormattedData_GPUPerformanceCounters_GPUEngine)) {
if ($ge.Name -match 'engtype_3D|engtype_Graphics|engtype_Compute') { $g3d += $ge.UtilizationPercentage }
}
$lines.Add("gpu_load=" + [math]::Min(100, [math]::Round($g3d)))
$vram = 0
foreach ($gm in (Get-CimInstance Win32_PerfFormattedData_GPUPerformanceCounters_GPUAdapterMemory)) {
$vram += $gm.DedicatedUsage
}
$lines.Add("gpu_mem_used=" + [math]::Round($vram / 1MB))
if ($gpu0.AdapterRAM -gt 0) { $lines.Add("gpu_mem_total=" + [math]::Round($gpu0.AdapterRAM / 1MB)) }
}
# Audio: default endpoint peaks and master volume
if ($hasAudio) {
try {
$lines.Add("aud_out=" + [math]::Round([LingAudio]::Peak(0) * 100, 1))
$lines.Add("aud_in=" + [math]::Round([LingAudio]::Peak(1) * 100, 1))
$lines.Add("aud_vol=" + [math]::Round([LingAudio]::Volume() * 100))
} catch {}
}
# Board temperature (works only on some machines / as admin)
if ($hasTz) {
$tz = Get-CimInstance -Namespace root/wmi -ClassName MSAcpi_ThermalZoneTemperature | Select-Object -First 1
if ($tz) { $lines.Add("board_temp=" + [math]::Round($tz.CurrentTemperature / 10 - 273.15, 1)) }
}
# Top processes by CPU since the last tick
$dt = ($now - $prevT).TotalSeconds
if ($dt -lt 0.2) { $dt = 1 }
$cur = @{}
$tops = New-Object System.Collections.Generic.List[object]
foreach ($pr in (Get-Process)) {
$cpuS = $pr.TotalProcessorTime.TotalSeconds
$cur[$pr.Id] = $cpuS
if ($prev.ContainsKey($pr.Id)) {
$pct = ($cpuS - $prev[$pr.Id]) / $dt / $logical * 100
if ($pct -gt 0.05) {
$tops.Add([pscustomobject]@{ N = $pr.ProcessName; C = $pct; M = [math]::Round($pr.WorkingSet64 / 1MB) })
}
}
}
$prev = $cur; $prevT = $now
$i = 0
foreach ($tp in ($tops | Sort-Object C -Descending | Select-Object -First 8)) {
$lines.Add("proc$i=" + $tp.N.ToLower() + "|" + [math]::Round($tp.C, 1) + "|" + $tp.M)
$i++
}
$lines.Add("proc_n=$i")
[System.IO.File]::WriteAllLines($tmpPath, $lines, $enc)
Move-Item -Force $tmpPath $statsPath
Start-Sleep -Milliseconds 650
}