# 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 }