Files
serverinfotest/host/send_sysinfo.ps1
DESKTOP-PUHFO60\dsx2016 6c81bcf26a Fix host sender for PowerShell 7 via pyserial fallback.
System.IO.Ports is often missing on PS7; keep WMI collection in PowerShell and write USB serial through Python when needed.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-27 20:14:30 +08:00

271 lines
7.9 KiB
PowerShell

# Push this PC's CPU / GPU / RAM / disk stats to CYD over USB serial.
# Usage:
# .\send_sysinfo.ps1
# .\send_sysinfo.ps1 -Port COM5
# .\send_sysinfo.ps1 -Port COM5 -IntervalMs 1000
#
# Serial I/O: Windows PowerShell 5 (System.IO.Ports) or Python + pyserial.
param(
[string]$Port = "",
[int]$IntervalMs = 1000,
[int]$Baud = 460800
)
$ErrorActionPreference = "Stop"
function Find-CydPort {
$ports = @()
try {
$ports = [System.IO.Ports.SerialPort]::GetPortNames() | Sort-Object
} catch {}
if (-not $ports -or $ports.Count -eq 0) {
try {
$ports = Get-CimInstance Win32_SerialPort -ErrorAction SilentlyContinue |
Select-Object -ExpandProperty DeviceID | Sort-Object
} catch {}
}
if (-not $ports -or $ports.Count -eq 0) {
$key = 'HKLM:\HARDWARE\DEVICEMAP\SERIALCOMM'
if (Test-Path $key) {
$ports = (Get-ItemProperty $key).PSObject.Properties |
Where-Object { $_.Name -notmatch '^PS' } |
ForEach-Object { [string]$_.Value } |
Sort-Object
}
}
if (-not $ports -or $ports.Count -eq 0) {
throw "No serial ports found. Plug in the CYD USB cable."
}
if ($ports.Count -eq 1) { return $ports[0] }
Write-Host "Available ports: $($ports -join ', ')"
$pick = $ports[-1]
Write-Host "Using last port: $pick (override with -Port)"
return $pick
}
function Sanitize([string]$s, [int]$max = 40) {
if (-not $s) { return "" }
$t = ($s -replace '[|=\r\n]', ' ').Trim()
if ($t.Length -gt $max) { $t = $t.Substring(0, $max).Trim() }
return $t
}
function Get-CpuName {
$p = Get-CimInstance Win32_Processor -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $p) { return "CPU" }
$n = [string]$p.Name
$n = $n -replace '\(R\)|\(TM\)|CPU @.*|with Radeon.*', ''
$n = ($n -replace '\s+', ' ').Trim()
return Sanitize $n 44
}
function Get-CpuPct {
try {
$c = Get-CimInstance Win32_Processor -ErrorAction Stop
$vals = @($c | ForEach-Object { [double]$_.LoadPercentage })
if ($vals.Count -eq 0) { return 0.0 }
return [math]::Round(($vals | Measure-Object -Average).Average, 1)
} catch {
return 0.0
}
}
function Get-GpuName {
$gpus = Get-CimInstance Win32_VideoController -ErrorAction SilentlyContinue |
Where-Object { $_.Name -and $_.Name -notmatch 'Microsoft Basic|Remote Desktop' }
if (-not $gpus) { return "GPU" }
$pick = $gpus | Where-Object { $_.Name -match 'NVIDIA|GeForce|RTX|GTX|Radeon|AMD' } |
Select-Object -First 1
if (-not $pick) { $pick = $gpus | Select-Object -First 1 }
$n = [string]$pick.Name
$n = $n -replace 'NVIDIA GeForce ', ''
$n = $n -replace 'AMD ', ''
return Sanitize $n 44
}
function Get-GpuPct {
$smi = Get-Command nvidia-smi -ErrorAction SilentlyContinue
if ($smi) {
try {
$out = & nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits 2>$null
if ($LASTEXITCODE -eq 0 -and $out) {
$first = ([string]($out | Select-Object -First 1)).Trim()
if ($first -match '^\d+') { return [double]$first }
}
} catch {}
}
try {
$counters = Get-Counter '\GPU Engine(*)\Utilization Percentage' -ErrorAction Stop
$samples = $counters.CounterSamples |
Where-Object { $_.CookedValue -ge 0 -and $_.InstanceName -match 'engtype_3D|engtype_Graphics' }
if ($samples) {
$sum = ($samples | Measure-Object -Property CookedValue -Sum).Sum
if ($sum -gt 100) { $sum = 100 }
return [math]::Round($sum, 1)
}
} catch {}
return -1.0
}
function Get-RamGb {
$os = Get-CimInstance Win32_OperatingSystem -ErrorAction Stop
$total = [math]::Round($os.TotalVisibleMemorySize / 1MB, 1)
$free = [math]::Round($os.FreePhysicalMemory / 1MB, 1)
$used = [math]::Round($total - $free, 1)
return @{ Used = $used; Total = $total }
}
function Get-DiskGb {
$d = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='C:'" -ErrorAction SilentlyContinue
if (-not $d) {
$d = Get-CimInstance Win32_LogicalDisk -ErrorAction SilentlyContinue |
Where-Object { $_.DriveType -eq 3 -and $_.Size -gt 0 } |
Select-Object -First 1
}
if (-not $d -or -not $d.Size) {
return @{ Used = 0.0; Total = 0.0 }
}
$total = [math]::Round($d.Size / 1GB, 1)
$free = [math]::Round($d.FreeSpace / 1GB, 1)
$used = [math]::Round($total - $free, 1)
return @{ Used = $used; Total = $total }
}
function Build-Line {
$hostName = Sanitize $env:COMPUTERNAME 36
$cpu = Get-CpuName
$cpuPct = Get-CpuPct
$gpu = Get-GpuName
$gpuPct = Get-GpuPct
$ram = Get-RamGb
$disk = Get-DiskGb
$gpuField = if ($gpuPct -lt 0) { "gpu%=-1" } else { "gpu%=$gpuPct" }
return "SI|host=$hostName|cpu=$cpu|cpu%=$cpuPct|gpu=$gpu|$gpuField|ram=$($ram.Used)/$($ram.Total)|disk=$($disk.Used)/$($disk.Total)"
}
function Find-Python {
foreach ($c in @(
"python",
(Join-Path $env:USERPROFILE "AppData\Local\Programs\Python\Python312\python.exe"),
(Join-Path $env:USERPROFILE "AppData\Local\Programs\Python\Python311\python.exe")
)) {
if (-not $c) { continue }
$cmd = Get-Command $c -ErrorAction SilentlyContinue
if ($cmd) { return $cmd.Source }
if (Test-Path $c) { return $c }
}
return $null
}
function Test-DotNetSerial {
try {
[void][System.IO.Ports.SerialPort]
return $true
} catch {
try {
Add-Type -AssemblyName System.IO.Ports -ErrorAction Stop | Out-Null
return $true
} catch {
return $false
}
}
}
if (-not $Port) { $Port = Find-CydPort }
Write-Host "Sending host info -> $Port @ $Baud every ${IntervalMs}ms (Ctrl+C to stop)"
if (Test-DotNetSerial) {
$sp = [System.IO.Ports.SerialPort]::new($Port, $Baud)
$sp.ReadTimeout = 200
$sp.WriteTimeout = 2000
$sp.NewLine = "`n"
$sp.DtrEnable = $true
$sp.RtsEnable = $true
try {
$sp.Open()
Start-Sleep -Milliseconds 400
while ($true) {
$line = Build-Line
$sp.WriteLine($line)
Write-Host $line
Start-Sleep -Milliseconds $IntervalMs
}
} finally {
if ($sp.IsOpen) { $sp.Close() }
$sp.Dispose()
}
exit 0
}
$py = Find-Python
if (-not $py) {
throw "No System.IO.Ports (PS7) and no python. Use Windows PowerShell 5, or: pip install pyserial"
}
$pyCheck = & $py -c "import serial; print('ok')" 2>$null
if ($LASTEXITCODE -ne 0 -or "$pyCheck" -notmatch 'ok') {
throw "Python found but pyserial missing. Run: `"$py`" -m pip install pyserial"
}
$writer = Join-Path $PSScriptRoot "_serial_writer.py"
@'
import sys, serial, time
port, baud = sys.argv[1], int(sys.argv[2])
# Keep DTR/RTS stable so ESP32 is not reset each write
ser = serial.Serial()
ser.port = port
ser.baudrate = baud
ser.timeout = 0.2
ser.dtr = False
ser.rts = False
ser.open()
time.sleep(0.3)
try:
for line in sys.stdin:
line = line.rstrip("\r\n")
if not line:
continue
ser.write((line + "\n").encode("utf-8", errors="ignore"))
ser.flush()
print(line, flush=True)
finally:
ser.close()
'@ | Set-Content -Encoding ascii $writer
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $py
$psi.Arguments = "`"$writer`" $Port $Baud"
$psi.UseShellExecute = $false
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
$psi.CreateNoWindow = $true
$proc = [System.Diagnostics.Process]::Start($psi)
try {
while (-not $proc.HasExited) {
$line = Build-Line
$proc.StandardInput.WriteLine($line)
$proc.StandardInput.Flush()
# Non-blocking-ish read of echo
if (-not $proc.StandardOutput.EndOfStream) {
$echo = $proc.StandardOutput.ReadLine()
if ($echo) { Write-Host $echo }
} else {
Write-Host $line
}
Start-Sleep -Milliseconds $IntervalMs
}
$err = $proc.StandardError.ReadToEnd()
if ($err) { Write-Host $err }
} finally {
try { $proc.StandardInput.Close() } catch {}
if (-not $proc.HasExited) {
Start-Sleep -Milliseconds 200
if (-not $proc.HasExited) { $proc.Kill() }
}
$proc.Dispose()
Remove-Item $writer -ErrorAction SilentlyContinue
}