diff --git a/.gitignore b/.gitignore index 609b09c..57ab564 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,7 @@ Thumbs.db *.elf # Build dir bins stay out; firmware/ prebuilt packages stay in !firmware/**/*.bin +# PyInstaller intermediates; keep host/send_sysinfo.exe for plug-and-play +host/build/ +host/dist/ +host/*.spec diff --git a/README.md b/README.md index 839b6e9..24bbcdd 100644 --- a/README.md +++ b/README.md @@ -21,23 +21,35 @@ SI|host=DESKTOP|cpu=i7-13700K|cpu%=12.3|gpu=RTX 4070|gpu%=5|ram=18.2/31.8|disk=5 ### 通用主机方案(Win10/11 · Linux · macOS) -业界常规做法(本仓库已采用): +ESP32 **读不到** PC 的 CPU/GPU,主机上必须有个小代理——但**不必安装 Python**。 + +| 用法 | 谁适合 | +|------|--------| +| **Windows 插上即用** | 双击 [`host/run.bat`](host/run.bat) 或跑一次 [`host/install_autostart.ps1`](host/install_autostart.ps1)(开机自启 + 等 USB) | +| **免安装单文件** | [`host/send_sysinfo.exe`](host/send_sysinfo.exe)(PyInstaller 打包,目标机无需 Python) | +| **开发 / Linux / macOS** | [`host/send_sysinfo.py`](host/send_sysinfo.py) + `pip install -r requirements.txt` | + +采集栈(业界常规): | 层级 | 库 / 接口 | 覆盖 | |------|-----------|------| -| **CPU / 内存 / 磁盘** | [`psutil`](https://psutil.readthedocs.io/) | Windows / Linux / macOS / BSD… | -| **串口** | [`pyserial`](https://pyserial.readthedocs.io/) | 同上 | -| **GPU 占用(可选)** | [`nvidia-ml-py`](https://pypi.org/project/nvidia-ml-py/)(NVML)→ `nvidia-smi` → `amd-smi`/`rocm-smi` | NVIDIA 最稳;AMD 有驱动/CLI 时可用;没有则 `gpu%=-1`(名称仍尽量读) | - -入口:**[`host/send_sysinfo.py`](host/send_sysinfo.py)**(推荐)。Windows 也可 `.\host\send_sysinfo.ps1`(内部仍调 Python)。 +| **CPU / 内存 / 磁盘** | [`psutil`](https://psutil.readthedocs.io/) | Windows / Linux / macOS… | +| **串口** | [`pyserial`](https://pyserial.readthedocs.io/) | 同上;断线自动重连、无口时等待插入 | +| **GPU 占用(可选)** | `nvidia-ml-py` → `nvidia-smi` → `amd-smi`/`rocm-smi` | 没有则 `gpu%=-1` | ```powershell +# Windows 最终用户(推荐) cd host +.\install_autostart.ps1 # 装一次:以后插 USB 就推 +# 或临时: +.\run.bat + +# 开发者重打包 exe: +.\build_exe.ps1 + +# Linux / macOS / 有 Python 时: pip install -r requirements.txt -pip install nvidia-ml-py # 有 NVIDIA 时建议装 -python send_sysinfo.py # 自动选串口 -python send_sysinfo.py -p COM5 # Windows -python send_sysinfo.py -p /dev/ttyUSB0 # Linux +python send_sysinfo.py -p /dev/ttyUSB0 ``` ## 刷写 @@ -71,25 +83,21 @@ cd firmware\cyd # 或 firmware\cyd_35r ## 推送本机信息 -板子刷好并插上 USB 后,在主机上: +板子刷好后: -```powershell -cd host -python send_sysinfo.py -# 或 -python send_sysinfo.py -p COM5 -i 1000 -.\send_sysinfo.ps1 -Port COM5 # Windows 快捷入口 -``` +1. Windows:`cd host` → `.\install_autostart.ps1`(装一次)或 `.\run.bat` +2. 插上 USB;代理会等串口并自动推送(状态变 **live**) -注意:串口同时只能被一个程序占用;跑推送脚本时不要开 `pio device monitor`。 +注意:串口同时只能被一个程序占用;跑代理时不要开 `pio device monitor`。卸载自启:`.\uninstall_autostart.ps1`。 ## 文件 ``` -include/pins.h, touch.h, host_info.h, ui.h, lv_conf.h -src/main.cpp, touch.cpp, host_info.cpp, ui.cpp -host/send_sysinfo.py # 跨平台主入口 +include/… src/… firmware/ +host/send_sysinfo.exe # Windows 免 Python(入库) +host/run.bat # 双击启动 +host/install_autostart.ps1 / uninstall_autostart.ps1 +host/build_exe.ps1 # 重新打包 exe +host/send_sysinfo.py # 源码(Linux/macOS/开发) host/requirements.txt -host/send_sysinfo.ps1 # Windows 启动器 -firmware/ ``` diff --git a/host/build_exe.ps1 b/host/build_exe.ps1 new file mode 100644 index 0000000..89aa799 --- /dev/null +++ b/host/build_exe.ps1 @@ -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" diff --git a/host/install_autostart.ps1 b/host/install_autostart.ps1 new file mode 100644 index 0000000..4c04adf --- /dev/null +++ b/host/install_autostart.ps1 @@ -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" diff --git a/host/run.bat b/host/run.bat new file mode 100644 index 0000000..60523a9 --- /dev/null +++ b/host/run.bat @@ -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 diff --git a/host/send_sysinfo.exe b/host/send_sysinfo.exe new file mode 100644 index 0000000..7761241 Binary files /dev/null and b/host/send_sysinfo.exe differ diff --git a/host/send_sysinfo.py b/host/send_sysinfo.py index 794b5cd..e4cf376 100644 --- a/host/send_sysinfo.py +++ b/host/send_sysinfo.py @@ -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 diff --git a/host/uninstall_autostart.ps1 b/host/uninstall_autostart.ps1 new file mode 100644 index 0000000..1d37126 --- /dev/null +++ b/host/uninstall_autostart.ps1 @@ -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)" + }