Ship Windows plug-and-play host exe with USB wait and autostart.

Users no longer need Python: run.bat or install_autostart.ps1; agent reconnects when CYD is plugged in.
This commit is contained in:
2026-10-27 20:23:57 +08:00
parent 8cc030178f
commit f83df3fd39
8 changed files with 257 additions and 66 deletions

34
host/build_exe.ps1 Normal file
View File

@@ -0,0 +1,34 @@
# Build a standalone Windows exe (no Python install needed on target PCs).
# Output: host\dist\send_sysinfo.exe
$ErrorActionPreference = "Stop"
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
Set-Location $here
$pyCandidates = @(
"python",
(Join-Path $env:USERPROFILE "AppData\Local\Programs\Python\Python312\python.exe"),
(Join-Path $env:USERPROFILE "AppData\Local\Programs\Python\Python311\python.exe")
)
$py = $null
foreach ($c in $pyCandidates) {
$cmd = Get-Command $c -ErrorAction SilentlyContinue
if ($cmd) { $py = $cmd.Source; break }
if (Test-Path $c) { $py = $c; break }
}
if (-not $py) { throw "Python required only on the build machine." }
& $py -m pip install -q pyinstaller psutil pyserial nvidia-ml-py
& $py -m PyInstaller --noconfirm --clean --onefile --console `
--name send_sysinfo `
--hidden-import serial.tools.list_ports `
--hidden-import psutil `
--collect-all nvidia_ml_py `
.\send_sysinfo.py
if (-not (Test-Path ".\dist\send_sysinfo.exe")) {
throw "Build failed: dist\send_sysinfo.exe missing"
}
Copy-Item ".\dist\send_sysinfo.exe" ".\send_sysinfo.exe" -Force
Write-Host "OK -> $here\send_sysinfo.exe"
Write-Host "Also -> $here\dist\send_sysinfo.exe"

View File

@@ -0,0 +1,32 @@
# Install "plug CYD USB and it just works" for the current Windows user.
# - Copies/uses host\send_sysinfo.exe
# - Adds a Startup-folder shortcut (runs at login, waits for USB serial)
$ErrorActionPreference = "Stop"
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$exe = Join-Path $here "send_sysinfo.exe"
if (-not (Test-Path $exe)) {
Write-Host "Building send_sysinfo.exe first..."
& (Join-Path $here "build_exe.ps1")
}
if (-not (Test-Path $exe)) {
throw "send_sysinfo.exe missing. Run build_exe.ps1 first."
}
$startup = [Environment]::GetFolderPath("Startup")
$lnkPath = Join-Path $startup "CYD-ServerInfo.lnk"
$w = New-Object -ComObject WScript.Shell
$lnk = $w.CreateShortcut($lnkPath)
$lnk.TargetPath = $exe
$lnk.WorkingDirectory = $here
$lnk.WindowStyle = 7 # minimized
$lnk.Description = "CYD host sysinfo agent (waits for USB)"
$lnk.Save()
Write-Host "Autostart installed: $lnkPath"
Write-Host "Starting agent now..."
Start-Process -FilePath $exe -WorkingDirectory $here -WindowStyle Minimized
Write-Host "Done. Plug in CYD USB anytime — agent waits/reconnects automatically."
Write-Host "Remove later: .\uninstall_autostart.ps1"

20
host/run.bat Normal file
View File

@@ -0,0 +1,20 @@
@echo off
setlocal
cd /d "%~dp0"
rem Prefer frozen exe (no Python). Fallback to python for developers.
if exist "%~dp0send_sysinfo.exe" (
start "CYD Server Info" "%~dp0send_sysinfo.exe" %*
exit /b 0
)
where python >nul 2>nul
if %ERRORLEVEL%==0 (
start "CYD Server Info" python "%~dp0send_sysinfo.py" %*
exit /b 0
)
echo send_sysinfo.exe not found and Python missing.
echo Build on a PC with Python: powershell -File build_exe.ps1
pause
exit /b 1

BIN
host/send_sysinfo.exe Normal file

Binary file not shown.

View File

@@ -354,32 +354,76 @@ def build_line() -> str:
)
def find_ports() -> List[str]:
ports = [p.device for p in list_ports.comports()]
if ports:
return sorted(ports)
# Extra Linux globs
_USB_HINT = re.compile(
r"CH340|CP210|CP2102|CP210x|SILABS|Silicon Labs|USB[- ]?SERIAL|"
r"USB[- ]?UART|FT232|FTDI|wchusb|QinHeng|ESP32|Espressif|1A86|10C4",
re.I,
)
def list_port_infos():
infos = list(list_ports.comports())
if infos:
return infos
# Extra Linux globs when pyserial enumeration is empty
out = []
if sys.platform.startswith("linux"):
ports = sorted(
for path in sorted(
glob.glob("/dev/ttyUSB*")
+ glob.glob("/dev/ttyACM*")
+ glob.glob("/dev/tty.usb*")
)
return ports
):
out.append(type("P", (), {"device": path, "description": path, "hwid": ""})())
return out
def pick_port(explicit: str) -> str:
def find_ports() -> List[str]:
return sorted({p.device for p in list_port_infos()})
def score_port(p) -> int:
blob = f"{getattr(p, 'description', '')} {getattr(p, 'hwid', '')} {p.device}"
score = 0
if _USB_HINT.search(blob):
score += 100
if sys.platform.startswith("linux") and (
"ttyUSB" in p.device or "ttyACM" in p.device
):
score += 50
if sys.platform == "darwin" and "usb" in p.device.lower():
score += 50
return score
def pick_port(explicit: str, wait: bool = True) -> str:
if explicit:
return explicit
ports = find_ports()
if not ports:
raise SystemExit("No serial ports found. Plug in the CYD USB cable.")
if len(ports) == 1:
return ports[0]
print("Available ports:", ", ".join(ports))
pick = ports[-1]
print(f"Using last port: {pick} (override with --port)")
return pick
while True:
if any(p.device == explicit for p in list_port_infos()) or os.path.exists(
explicit
):
return explicit
if not wait:
raise SystemExit(f"Port not found: {explicit}")
print(f"Waiting for {explicit} ...", flush=True)
time.sleep(1.0)
while True:
infos = list_port_infos()
if infos:
ranked = sorted(infos, key=lambda p: (score_port(p), p.device))
best = ranked[-1]
if len(infos) > 1:
print(
"Available ports:",
", ".join(f"{p.device}({getattr(p, 'description', '')})" for p in infos),
flush=True,
)
print(f"Using port: {best.device}", flush=True)
return best.device
if not wait:
raise SystemExit("No serial ports found. Plug in the CYD USB cable.")
print("Waiting for CYD USB serial ...", flush=True)
time.sleep(1.0)
def open_serial(port: str, baud: int):
@@ -395,24 +439,8 @@ def open_serial(port: str, baud: int):
return ser
def main() -> int:
ap = argparse.ArgumentParser(description="Push host sysinfo to CYD over USB serial")
ap.add_argument("--port", "-p", default="", help="Serial port (COMx / /dev/ttyUSB0)")
ap.add_argument("--baud", "-b", type=int, default=460800)
ap.add_argument("--interval-ms", "-i", type=int, default=1000)
ap.add_argument("--once", action="store_true", help="Send one line and exit")
args = ap.parse_args()
# Warm CPU percent baseline
psutil.cpu_percent(interval=None)
port = pick_port(args.port)
print(
f"Sending host info -> {port} @ {args.baud} every {args.interval_ms}ms "
f"({platform.system()} {platform.release()}; Ctrl+C to stop)"
)
ser = open_serial(port, args.baud)
def run_session(port: str, baud: int, interval_ms: int, once: bool) -> None:
ser = open_serial(port, baud)
time.sleep(0.35)
try:
while True:
@@ -420,13 +448,63 @@ def main() -> int:
ser.write((line + "\n").encode("utf-8", errors="ignore"))
ser.flush()
print(line, flush=True)
if args.once:
break
time.sleep(max(0.2, args.interval_ms / 1000.0))
if once:
return
time.sleep(max(0.2, interval_ms / 1000.0))
finally:
try:
ser.close()
except Exception:
pass
def main() -> int:
ap = argparse.ArgumentParser(
description="Push host sysinfo to CYD over USB serial (no Python needed if using .exe)"
)
ap.add_argument("--port", "-p", default="", help="Serial port (COMx / /dev/ttyUSB0)")
ap.add_argument("--baud", "-b", type=int, default=460800)
ap.add_argument("--interval-ms", "-i", type=int, default=1000)
ap.add_argument("--once", action="store_true", help="Send one line and exit")
ap.add_argument(
"--no-wait",
action="store_true",
help="Exit immediately if port missing (default: wait / reconnect)",
)
args = ap.parse_args()
# Warm CPU percent baseline
psutil.cpu_percent(interval=None)
print(
f"CYD host agent ({platform.system()} {platform.release()}) "
f"baud={args.baud} interval={args.interval_ms}ms Ctrl+C to stop",
flush=True,
)
try:
while True:
port = pick_port(args.port, wait=not args.no_wait)
print(
f"Sending host info -> {port}",
flush=True,
)
try:
run_session(port, args.baud, args.interval_ms, args.once)
if args.once:
return 0
except serial.SerialException as e:
print(f"Serial lost ({e}); will retry ...", flush=True)
if args.once or args.no_wait:
return 2
time.sleep(1.0)
except OSError as e:
print(f"Port error ({e}); will retry ...", flush=True)
if args.once or args.no_wait:
return 2
time.sleep(1.0)
except KeyboardInterrupt:
print("\nStopped.")
finally:
ser.close()
return 0

View File

@@ -0,0 +1,15 @@
$ErrorActionPreference = "Stop"
$startup = [Environment]::GetFolderPath("Startup")
$lnkPath = Join-Path $startup "CYD-ServerInfo.lnk"
if (Test-Path $lnkPath) {
Remove-Item $lnkPath -Force
Write-Host "Removed: $lnkPath"
} else {
Write-Host "No Startup shortcut found."
}
Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
Where-Object { $_.Name -match 'send_sysinfo' -or ($_.CommandLine -and $_.CommandLine -match 'send_sysinfo') } |
ForEach-Object {
Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
Write-Host "Stopped PID $($_.ProcessId)"
}