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:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -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
|
||||
|
||||
56
README.md
56
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/
|
||||
```
|
||||
|
||||
34
host/build_exe.ps1
Normal file
34
host/build_exe.ps1
Normal 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"
|
||||
32
host/install_autostart.ps1
Normal file
32
host/install_autostart.ps1
Normal 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
20
host/run.bat
Normal 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
BIN
host/send_sysinfo.exe
Normal file
Binary file not shown.
@@ -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
|
||||
|
||||
|
||||
|
||||
15
host/uninstall_autostart.ps1
Normal file
15
host/uninstall_autostart.ps1
Normal 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)"
|
||||
}
|
||||
Reference in New Issue
Block a user