Add CYD host PC info panel with serial SI protocol and LVGL bars.

ESP32 shows CPU/GPU/RAM/disk from host/send_sysinfo.ps1 over USB; firmware packages for 2.8 and 3.5 included.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-27 20:13:09 +08:00
commit f724796359
32 changed files with 2664 additions and 0 deletions

12
.gitignore vendored Normal file
View File

@@ -0,0 +1,12 @@
.pio/
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
*.pyc
__pycache__/
.DS_Store
Thumbs.db
*.elf
# Build dir bins stay out; firmware/ prebuilt packages stay in
!firmware/**/*.bin

62
README.md Normal file
View File

@@ -0,0 +1,62 @@
# CYD 本机 / 服务器信息面板
在 **2.8" / 3.5" CYD** 上显示当前电脑的 **CPU、GPU、内存、磁盘** 等主要信息。显示/触控骨架对齐 [tftest](../tftest) / [wifitest](../wifitest);板型差异按 [CYD_PROJECT_GUIDE.md](../CYD_PROJECT_GUIDE.md)。
| 板型 | env | 屏 / 触控 | 背光 |
|------|-----|-----------|------|
| **2.8" · 2432S028R** | `cyd`(默认) | ILI9341 + XPT2046 SoftSPI | GPIO**21** |
| **3.5" · 3248S035C** | `cyd_35r` | ST7796 + GT911 I2C | GPIO**27** |
本工程**无外设接线**(只吃 USB 供电 + 串口)。
## 原理
ESP32 无法直接读 PC 硬件。本机跑 [`host/send_sysinfo.ps1`](host/send_sysinfo.ps1),经 **USB 串口** 按行推送:
```
SI|host=DESKTOP|cpu=i7-13700K|cpu%=12.3|gpu=RTX 4070|gpu%=5|ram=18.2/31.8|disk=512/931
```
屏上 LVGL 面板解析后画进度条;超过约 5 秒无新包显示 **stale**。
GPU 占用:优先 `nvidia-smi`,否则尝试 Windows GPU Engine 计数器;都失败则显示 `n/a`(名称仍可读)。
## 刷写
| 板型 | 目录 |
|------|------|
| 2.8" | [`firmware/cyd/`](firmware/cyd/) |
| 3.5" | [`firmware/cyd_35r/`](firmware/cyd_35r/) |
```powershell
cd firmware\cyd # 或 firmware\cyd_35r
.\flash.ps1
# 或
.\flash.ps1 -Port COM3
```
源码烧录:`pio run -e cyd -t upload`(3.5:`-e cyd_35r`)
重新打包:`.\firmware\export_from_build.ps1`(3.5 加 `-Env cyd_35r`)
串口监视:`pio device monitor -b 460800`
## 推送本机信息
板子刷好并插上 USB 后,在 **PC** 上:
```powershell
cd host
.\send_sysinfo.ps1
# 或
.\send_sysinfo.ps1 -Port COM5 -IntervalMs 1000
```
注意:串口同时只能被一个程序占用;跑推送脚本时不要开 `pio device monitor`。
## 文件
```
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.ps1
firmware/
```

13
copy_lv_conf.py Normal file
View File

@@ -0,0 +1,13 @@
Import("env") # noqa: F821
from pathlib import Path
src = Path(env["PROJECT_DIR"]) / "include" / "lv_conf.h" # noqa: F821
dst_dir = Path(env["PROJECT_LIBDEPS_DIR"]) / env["PIOENV"] # noqa: F821
dst_dir.mkdir(parents=True, exist_ok=True)
dst = dst_dir / "lv_conf.h"
if src.exists():
dst.write_text(src.read_text(encoding="utf-8"), encoding="utf-8")
lvgl = dst_dir / "lvgl" / "lv_conf.h"
if (dst_dir / "lvgl").exists():
lvgl.write_text(src.read_text(encoding="utf-8"), encoding="utf-8")
print(f"copy_lv_conf: synced -> {dst}")

53
firmware/README.md Normal file
View File

@@ -0,0 +1,53 @@
# 预编译固件(可随时刷写)
| 目录 | 板子 |
|------|------|
| [`cyd/`](cyd/) | **2.8"** CYD ESP32-2432S028R(ILI9341) |
| [`cyd_35r/`](cyd_35r/) | **3.5"** CYD ESP32-3248S035C(ST7796 + GT911) |
功能:**本机/服务器信息面板**(USB 串口收 `SI|...`,屏上显示 CPU/GPU/RAM/DISK)
版本信息见各目录下 `VERSION.txt`。
也可在 [`../../cydweb`](../../cydweb)(CYD Flasher)里通过浏览器串口刷写本目录固件。
## 直接刷写(无需重新编译)
1. USB 连接板子
2. 执行:
```powershell
cd firmware\cyd # 3.5": firmware\cyd_35r
.\flash.ps1
# 或指定端口
.\flash.ps1 -Port COM3
```
也可运行 `flash.bat`。
依赖其一即可:
- 已安装 PlatformIO(`pio`,自带 esptool)
- 或 `pip install esptool`
## 手动 esptool
```bash
esptool.py --chip esp32 --port COM3 --baud 921600 write_flash -z ^
--flash_mode dio --flash_freq 40m --flash_size 4MB ^
0x1000 bootloader.bin ^
0x8000 partitions.bin ^
0xe000 boot_app0.bin ^
0x10000 firmware.bin
```
## 从源码重新打包进本目录
在仓库根目录:
```powershell
.\firmware\export_from_build.ps1 # 2.8 -> firmware/cyd
.\firmware\export_from_build.ps1 -Env cyd_35r # 3.5 -> firmware/cyd_35r
.\firmware\export_from_build.ps1 -SkipBuild
```
刷完后在 PC 上运行 `..\..\host\send_sysinfo.ps1` 推送本机信息。

15
firmware/cyd/VERSION.txt Normal file
View File

@@ -0,0 +1,15 @@
name: CYD serverinfotest (cyd)
git: unknown
date: 2026-10-27
board: ESP32-2432S028R (ILI9341)
flash_size: 4MB
offsets:
0x1000 bootloader.bin
0x8000 partitions.bin
0xe000 boot_app0.bin
0x10000 firmware.bin
sha256:
bootloader.bin 3D234A7471F67B013686DABD4DEE7C1FA915C9928463616A94BC9297ACF1ABF8
boot_app0.bin F94C5D786A7A8FAB06AC5D10E33BF37711A6697636DC037559EA19CC410A17F0
firmware.bin 85838702F4062A3679CAE17D0365B1E14DDBB4C3BA235A0A6500FDDB84A56B6A
partitions.bin 148B959CBFF1C38AA8E1D5C0BA9D612C54997B945E56A63F41223EEF650653A1

BIN
firmware/cyd/boot_app0.bin Normal file

Binary file not shown.

BIN
firmware/cyd/bootloader.bin Normal file

Binary file not shown.

BIN
firmware/cyd/firmware.bin Normal file

Binary file not shown.

5
firmware/cyd/flash.bat Normal file
View File

@@ -0,0 +1,5 @@
@echo off
setlocal
cd /d "%~dp0"
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0flash.ps1" %*
exit /b %ERRORLEVEL%

98
firmware/cyd/flash.ps1 Normal file
View File

@@ -0,0 +1,98 @@
# Flash archived CYD Server Info panel firmware (no rebuild needed)
# Usage:
# .\flash.ps1
# .\flash.ps1 -Port COM5
# .\flash.ps1 -Port COM5 -Baud 460800
param(
[string]$Port = "",
[int]$Baud = 921600
)
$ErrorActionPreference = "Stop"
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
Set-Location $here
function Find-Esptool {
$pio = Get-Command pio -ErrorAction SilentlyContinue
if (-not $pio) {
$pioPath = Join-Path $env:USERPROFILE "AppData\Local\Programs\Python\Python312\Scripts\pio.exe"
if (Test-Path $pioPath) {
$env:Path = "$(Split-Path $pioPath);$env:Path"
$pio = Get-Command pio -ErrorAction SilentlyContinue
}
}
if ($pio) {
$pkg = Join-Path $env:USERPROFILE ".platformio\packages\tool-esptoolpy\esptool.py"
if (Test-Path $pkg) { return @{ Kind = "py"; Path = $pkg } }
}
$py = Get-Command python -ErrorAction SilentlyContinue
if ($py) {
try {
& python -c "import esptool" 2>$null
if ($LASTEXITCODE -eq 0) { return @{ Kind = "mod"; Path = "esptool" } }
} catch {}
}
$esp = Get-Command esptool.py -ErrorAction SilentlyContinue
if ($esp) { return @{ Kind = "py"; Path = $esp.Source } }
return $null
}
if (-not $Port) {
$ports = @()
try {
Add-Type -AssemblyName System.IO.Ports -ErrorAction SilentlyContinue | Out-Null
$ports = [System.IO.Ports.SerialPort]::GetPortNames() | Sort-Object
} catch {}
if (-not $ports -or $ports.Count -eq 0) {
$ports = Get-CimInstance Win32_SerialPort -ErrorAction SilentlyContinue |
Select-Object -ExpandProperty DeviceID | Sort-Object
}
if (-not $ports -or $ports.Count -eq 0) {
Write-Error "No serial ports found. Plug in the CYD USB cable, then retry with -Port COMx"
}
if ($ports.Count -eq 1) {
$Port = $ports[0]
} else {
Write-Host "Available ports: $($ports -join ', ')"
$Port = $ports[-1]
Write-Host "Using last port: $Port (override with -Port)"
}
}
$tool = Find-Esptool
if (-not $tool) {
Write-Error "esptool not found. Install PlatformIO (`pio`) or: pip install esptool"
}
$needed = @("bootloader.bin", "partitions.bin", "boot_app0.bin", "firmware.bin")
foreach ($f in $needed) {
if (-not (Test-Path (Join-Path $here $f))) {
Write-Error "Missing $f — run ..\export_from_build.ps1 from repo root first"
}
}
$argsCommon = @(
"--chip", "esp32",
"--port", $Port,
"--baud", "$Baud",
"--before", "default_reset",
"--after", "hard_reset",
"write_flash", "-z",
"--flash_mode", "dio",
"--flash_freq", "40m",
"--flash_size", "4MB",
"0x1000", "bootloader.bin",
"0x8000", "partitions.bin",
"0xe000", "boot_app0.bin",
"0x10000", "firmware.bin"
)
Write-Host "Flashing $here -> $Port @ $Baud"
if ($tool.Kind -eq "mod") {
& python -m esptool @argsCommon
} else {
& python $tool.Path @argsCommon
}
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Write-Host "Done. Open serial monitor at 460800 if needed."

13
firmware/cyd/meta.json Normal file
View File

@@ -0,0 +1,13 @@
{
"boardId": "esp32-2432s028",
"name": "CYD Server Info",
"summary": "LVGL panel for host CPU / GPU / RAM / disk via USB serial",
"description": "Receives SI|... lines from host/send_sysinfo.ps1 and shows live bars for CPU, GPU, memory and system drive. No external GPIO peripherals.",
"tags": [
"monitor",
"host-info",
"cpu",
"gpu",
"serial"
]
}

BIN
firmware/cyd/partitions.bin Normal file

Binary file not shown.

View File

@@ -0,0 +1,15 @@
name: CYD serverinfotest (cyd_35r)
git: unknown
date: 2026-10-27
board: ESP32-3248S035 (ST7796)
flash_size: 4MB
offsets:
0x1000 bootloader.bin
0x8000 partitions.bin
0xe000 boot_app0.bin
0x10000 firmware.bin
sha256:
bootloader.bin 3D234A7471F67B013686DABD4DEE7C1FA915C9928463616A94BC9297ACF1ABF8
boot_app0.bin F94C5D786A7A8FAB06AC5D10E33BF37711A6697636DC037559EA19CC410A17F0
firmware.bin 93C0D8E2A7C31848960C2DBA2894DE9D18BF21D3D94B47DDD5406CF18A1FD1B3
partitions.bin 148B959CBFF1C38AA8E1D5C0BA9D612C54997B945E56A63F41223EEF650653A1

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,5 @@
@echo off
setlocal
cd /d "%~dp0"
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0flash.ps1" %*
exit /b %ERRORLEVEL%

View File

@@ -0,0 +1,98 @@
# Flash archived CYD Server Info panel firmware (no rebuild needed)
# Usage:
# .\flash.ps1
# .\flash.ps1 -Port COM5
# .\flash.ps1 -Port COM5 -Baud 460800
param(
[string]$Port = "",
[int]$Baud = 921600
)
$ErrorActionPreference = "Stop"
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
Set-Location $here
function Find-Esptool {
$pio = Get-Command pio -ErrorAction SilentlyContinue
if (-not $pio) {
$pioPath = Join-Path $env:USERPROFILE "AppData\Local\Programs\Python\Python312\Scripts\pio.exe"
if (Test-Path $pioPath) {
$env:Path = "$(Split-Path $pioPath);$env:Path"
$pio = Get-Command pio -ErrorAction SilentlyContinue
}
}
if ($pio) {
$pkg = Join-Path $env:USERPROFILE ".platformio\packages\tool-esptoolpy\esptool.py"
if (Test-Path $pkg) { return @{ Kind = "py"; Path = $pkg } }
}
$py = Get-Command python -ErrorAction SilentlyContinue
if ($py) {
try {
& python -c "import esptool" 2>$null
if ($LASTEXITCODE -eq 0) { return @{ Kind = "mod"; Path = "esptool" } }
} catch {}
}
$esp = Get-Command esptool.py -ErrorAction SilentlyContinue
if ($esp) { return @{ Kind = "py"; Path = $esp.Source } }
return $null
}
if (-not $Port) {
$ports = @()
try {
Add-Type -AssemblyName System.IO.Ports -ErrorAction SilentlyContinue | Out-Null
$ports = [System.IO.Ports.SerialPort]::GetPortNames() | Sort-Object
} catch {}
if (-not $ports -or $ports.Count -eq 0) {
$ports = Get-CimInstance Win32_SerialPort -ErrorAction SilentlyContinue |
Select-Object -ExpandProperty DeviceID | Sort-Object
}
if (-not $ports -or $ports.Count -eq 0) {
Write-Error "No serial ports found. Plug in the CYD USB cable, then retry with -Port COMx"
}
if ($ports.Count -eq 1) {
$Port = $ports[0]
} else {
Write-Host "Available ports: $($ports -join ', ')"
$Port = $ports[-1]
Write-Host "Using last port: $Port (override with -Port)"
}
}
$tool = Find-Esptool
if (-not $tool) {
Write-Error "esptool not found. Install PlatformIO (`pio`) or: pip install esptool"
}
$needed = @("bootloader.bin", "partitions.bin", "boot_app0.bin", "firmware.bin")
foreach ($f in $needed) {
if (-not (Test-Path (Join-Path $here $f))) {
Write-Error "Missing $f — run ..\export_from_build.ps1 from repo root first"
}
}
$argsCommon = @(
"--chip", "esp32",
"--port", $Port,
"--baud", "$Baud",
"--before", "default_reset",
"--after", "hard_reset",
"write_flash", "-z",
"--flash_mode", "dio",
"--flash_freq", "40m",
"--flash_size", "4MB",
"0x1000", "bootloader.bin",
"0x8000", "partitions.bin",
"0xe000", "boot_app0.bin",
"0x10000", "firmware.bin"
)
Write-Host "Flashing $here -> $Port @ $Baud"
if ($tool.Kind -eq "mod") {
& python -m esptool @argsCommon
} else {
& python $tool.Path @argsCommon
}
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Write-Host "Done. Open serial monitor at 460800 if needed."

View File

@@ -0,0 +1,13 @@
{
"boardId": "esp32-3248s035",
"name": "CYD Server Info",
"summary": "LVGL panel for host CPU / GPU / RAM / disk via USB serial",
"description": "Receives SI|... lines from host/send_sysinfo.ps1 and shows live bars for CPU, GPU, memory and system drive. No external GPIO peripherals. 3.5C: ST7796 + GT911, BL=27.",
"tags": [
"monitor",
"host-info",
"cpu",
"gpu",
"serial"
]
}

Binary file not shown.

View File

@@ -0,0 +1,71 @@
# Export firmware bins from PlatformIO build.
# Default: 2.8" (cyd -> firmware/cyd)
# 3.5": .\firmware\export_from_build.ps1 -Env cyd_35r
# (only after: pio run -e cyd_35r)
param(
[switch]$SkipBuild,
[ValidateSet("cyd", "cyd_35r")]
[string]$Env = "cyd"
)
$ErrorActionPreference = "Stop"
$root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
Set-Location $root
$env:Path = "C:\Users\dsx2016\AppData\Local\Programs\Python\Python312\Scripts;" + $env:Path
if (-not $SkipBuild) {
pio run -e $Env
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}
$src = Join-Path $root (".pio\build\" + $Env)
if ($Env -eq "cyd_35r") {
$dstName = "cyd_35r"
$boardLine = "board: ESP32-3248S035 (ST7796)"
$boardIdHint = "esp32-3248s035"
} else {
$dstName = "cyd"
$boardLine = "board: ESP32-2432S028R (ILI9341)"
$boardIdHint = "esp32-2432s028"
}
$dst = Join-Path $root ("firmware\" + $dstName)
New-Item -ItemType Directory -Force -Path $dst | Out-Null
foreach ($f in @("bootloader.bin", "partitions.bin", "firmware.bin")) {
$p = Join-Path $src $f
if (-not (Test-Path $p)) { Write-Error "Missing build artifact: $p — build with: pio run -e $Env" }
Copy-Item $p $dst -Force
}
$bootApp = Join-Path $env:USERPROFILE ".platformio\packages\framework-arduinoespressif32\tools\partitions\boot_app0.bin"
if (-not (Test-Path $bootApp)) {
Write-Error "boot_app0.bin not found under PlatformIO packages"
}
Copy-Item $bootApp (Join-Path $dst "boot_app0.bin") -Force
$git = ""
try { $git = (git -C $root rev-parse --short HEAD).Trim() } catch { $git = "unknown" }
$date = Get-Date -Format "yyyy-MM-dd"
$hashes = Get-ChildItem (Join-Path $dst "*.bin") | Get-FileHash -Algorithm SHA256
$lines = @(
"name: CYD serverinfotest ($dstName)",
"git: $git",
"date: $date",
$boardLine,
"flash_size: 4MB",
"offsets:",
" 0x1000 bootloader.bin",
" 0x8000 partitions.bin",
" 0xe000 boot_app0.bin",
" 0x10000 firmware.bin",
"sha256:"
)
foreach ($h in $hashes) {
$lines += (" {0,-15} {1}" -f (Split-Path $h.Path -Leaf), $h.Hash)
}
$lines | Set-Content -Encoding utf8 (Join-Path $dst "VERSION.txt")
Write-Host "Exported -> $dst (env=$Env git=$git)"

162
host/send_sysinfo.ps1 Normal file
View File

@@ -0,0 +1,162 @@
# 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
param(
[string]$Port = "",
[int]$IntervalMs = 1000,
[int]$Baud = 460800
)
$ErrorActionPreference = "Stop"
function Find-CydPort {
$ports = @()
try {
Add-Type -AssemblyName System.IO.Ports -ErrorAction SilentlyContinue | Out-Null
$ports = [System.IO.Ports.SerialPort]::GetPortNames() | Sort-Object
} catch {}
if (-not $ports -or $ports.Count -eq 0) {
$ports = Get-CimInstance Win32_SerialPort -ErrorAction SilentlyContinue |
Select-Object -ExpandProperty DeviceID | 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
# Shorten common Intel/AMD noise
$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" }
# Prefer NVIDIA / AMD discrete if present
$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 ', 'RTX/GTX '
$n = $n -replace 'AMD ', ''
return Sanitize $n 44
}
function Get-GpuPct {
# 1) nvidia-smi
$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 {}
}
# 2) Windows GPU Engine counter (may need a few samples)
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)"
}
if (-not $Port) { $Port = Find-CydPort }
Add-Type -AssemblyName System.IO.Ports
$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()
Write-Host "Sending host info -> $Port @ $Baud every ${IntervalMs}ms (Ctrl+C to stop)"
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()
}

29
include/host_info.h Normal file
View File

@@ -0,0 +1,29 @@
#pragma once
#include <stdint.h>
/** Max chars for hostname / CPU / GPU name shown on UI. */
static constexpr int HOST_NAME_MAX = 40;
static constexpr int HOST_CPU_MAX = 48;
static constexpr int HOST_GPU_MAX = 48;
struct HostInfo {
char host[HOST_NAME_MAX];
char cpu[HOST_CPU_MAX];
char gpu[HOST_GPU_MAX];
float cpu_pct; // 0..100
float gpu_pct; // 0..100, -1 if unknown
float ram_used_gb;
float ram_total_gb;
float disk_used_gb;
float disk_total_gb;
bool valid;
uint32_t updated_ms;
};
void host_info_begin();
/** Poll USB Serial for SI|... lines; return true if a packet was applied. */
bool host_info_poll();
const HostInfo &host_info_get();
/** True if last packet is fresher than stale_ms. */
bool host_info_is_live(uint32_t stale_ms = 5000);

1127
include/lv_conf.h Normal file

File diff suppressed because it is too large Load Diff

42
include/pins.h Normal file
View File

@@ -0,0 +1,42 @@
#pragma once
// Board pins — 2.8" default; 3.5" when CYD_BOARD_3248S035 is set (env:cyd_35r)
#if defined(CYD_BOARD_3248S035)
#define TFT_BL_PIN 27
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 480
#define UI_HOR_RES 480
#define UI_VER_RES 320
/* 3.5C capacitive GT911 (ESP32-3248S035C). GPIO33 is I2C SDA, not TOUCH_CS.
* Backlight is GPIO27; INT is GPIO21 — they are NOT the same pin. */
#define CYD_GT911_SDA 33
#define CYD_GT911_SCL 32
#define CYD_GT911_INT 21
#define CYD_GT911_RST 25
#else
#define TFT_BL_PIN 21
#define SCREEN_WIDTH 240
#define SCREEN_HEIGHT 320
#define UI_HOR_RES 320
#define UI_VER_RES 240
/* 2.8" CYD: dedicated SoftSPI touch bus. */
#define CYD_TOUCH_IRQ 36
#define CYD_TOUCH_MOSI 32
#define CYD_TOUCH_MISO 39
#define CYD_TOUCH_CLK 25
#define CYD_TOUCH_CS 33
#endif
/* Map 320x240 design coordinates onto the active UI resolution (identity on 2.8"). */
#define UX(x) ((int)(((long)(x) * (UI_HOR_RES)) / 320))
#define UY(y) ((int)(((long)(y) * (UI_VER_RES)) / 240))
#define UW(w) UX(w)
#define UH(h) UY(h)
/* Partial frame buffer: 3.5 needs smaller ratio to fit DRAM. */
#if defined(CYD_BOARD_3248S035)
#define DRAW_BUF_DIV 20
#else
#define DRAW_BUF_DIV 10
#endif

7
include/touch.h Normal file
View File

@@ -0,0 +1,7 @@
#pragma once
#include <lvgl.h>
#include <stdint.h>
void touch_init();
void touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data);

4
include/ui.h Normal file
View File

@@ -0,0 +1,4 @@
#pragma once
void ui_create();
void ui_tick();

109
platformio.ini Normal file
View File

@@ -0,0 +1,109 @@
; CYD — host PC / server info panel (USB serial feed + LVGL)
[env:cyd]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 460800
upload_speed = 460800
board_build.partitions = default.csv
lib_deps =
bodmer/TFT_eSPI@^2.5.43
lvgl/lvgl@9.2.2
build_flags =
-DCORE_DEBUG_LEVEL=0
-DUSER_SETUP_LOADED=1
-DILI9341_2_DRIVER=1
-DTFT_WIDTH=240
-DTFT_HEIGHT=320
-DTFT_MISO=12
-DTFT_MOSI=13
-DTFT_SCLK=14
-DTFT_CS=15
-DTFT_DC=2
-DTFT_RST=-1
-DTFT_BL=21
-DTFT_BACKLIGHT_ON=HIGH
-DTOUCH_CS=-1
-DLOAD_GLCD=1
-DLOAD_FONT2=1
-DLOAD_FONT4=1
-DLOAD_FONT6=1
-DLOAD_FONT7=1
-DLOAD_FONT8=1
-DLOAD_GFXFF=1
-DSMOOTH_FONT=1
-DSPI_FREQUENCY=40000000
-DSPI_READ_FREQUENCY=20000000
-DLV_CONF_SKIP
-DLV_COLOR_DEPTH=16
-DLV_MEM_SIZE=49152
-DLV_FONT_MONTSERRAT_14=1
-DLV_FONT_MONTSERRAT_16=1
-DLV_FONT_MONTSERRAT_18=1
-DLV_FONT_MONTSERRAT_20=1
-DLV_USE_TFT_ESPI=1
-DLV_USE_FLEX=1
-DLV_USE_LABEL=1
-DLV_USE_BAR=1
-I${PROJECT_DIR}/include
extra_scripts = pre:copy_lv_conf.py
; Optional 3.5" CYD ESP32-3248S035C (ST7796 + GT911 capacitive). Default [env:cyd].
; Build: pio run -e cyd_35r
; Keep TOUCH_CS=-1 — GPIO33 is GT911 SDA, not resistive touch CS.
; TFT_BL=GPIO27 (not 21 — 21 is GT911 INT). See ../CYD_PROJECT_GUIDE.md
[env:cyd_35r]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 460800
upload_speed = 460800
board_build.partitions = default.csv
lib_deps =
bodmer/TFT_eSPI@^2.5.43
lvgl/lvgl@9.2.2
extra_scripts = pre:copy_lv_conf.py
build_flags =
-DCORE_DEBUG_LEVEL=0
-DUSER_SETUP_LOADED=1
-DST7796_DRIVER=1
-DTFT_WIDTH=320
-DTFT_HEIGHT=480
-DTFT_MISO=12
-DTFT_MOSI=13
-DTFT_SCLK=14
-DTFT_CS=15
-DTFT_DC=2
-DTFT_RST=-1
-DTFT_BL=27
-DTFT_BACKLIGHT_ON=HIGH
-DTOUCH_CS=-1
-DLOAD_GLCD=1
-DLOAD_FONT2=1
-DLOAD_FONT4=1
-DLOAD_FONT6=1
-DLOAD_FONT7=1
-DLOAD_FONT8=1
-DLOAD_GFXFF=1
-DSMOOTH_FONT=1
-DSPI_FREQUENCY=40000000
-DSPI_READ_FREQUENCY=20000000
-DUSE_HSPI_PORT
-DSUPPORT_TRANSACTIONS
-DTFT_RGB_ORDER=TFT_BGR
-DCYD_BOARD_3248S035=1
-DLV_CONF_SKIP
-DLV_COLOR_DEPTH=16
-DLV_MEM_SIZE=32768
-DLV_FONT_MONTSERRAT_14=1
-DLV_FONT_MONTSERRAT_16=1
-DLV_FONT_MONTSERRAT_18=1
-DLV_FONT_MONTSERRAT_20=1
-DLV_USE_TFT_ESPI=1
-DLV_USE_FLEX=1
-DLV_USE_LABEL=1
-DLV_USE_BAR=1
-I${PROJECT_DIR}/include

152
src/host_info.cpp Normal file
View File

@@ -0,0 +1,152 @@
#include "host_info.h"
#include <Arduino.h>
#include <cstdio>
#include <cstring>
static HostInfo g_info = {};
static char g_line[384];
static size_t g_len = 0;
static void copy_trunc(char *dst, size_t dst_sz, const char *src) {
if (!dst || dst_sz == 0) {
return;
}
if (!src) {
dst[0] = '\0';
return;
}
strncpy(dst, src, dst_sz - 1);
dst[dst_sz - 1] = '\0';
}
static const char *find_field(const char *line, const char *key) {
// Look for |key=value| or start-of-string after "SI|"
const size_t klen = strlen(key);
const char *p = line;
while (p && *p) {
if (strncmp(p, key, klen) == 0 && p[klen] == '=') {
return p + klen + 1;
}
p = strchr(p, '|');
if (!p) {
break;
}
++p;
}
return nullptr;
}
static void copy_field(char *dst, size_t dst_sz, const char *line, const char *key) {
const char *v = find_field(line, key);
if (!v) {
dst[0] = '\0';
return;
}
size_t n = 0;
while (v[n] && v[n] != '|' && n + 1 < dst_sz) {
dst[n] = v[n];
++n;
}
dst[n] = '\0';
}
static float field_float(const char *line, const char *key, float fallback) {
const char *v = find_field(line, key);
if (!v || !*v) {
return fallback;
}
return (float)atof(v);
}
static void apply_line(const char *line) {
// SI|host=...|cpu=...|cpu%=..|gpu=...|gpu%=..|ram=used/total|disk=used/total
if (strncmp(line, "SI|", 3) != 0) {
return;
}
HostInfo next = g_info;
next.valid = true;
next.updated_ms = millis();
copy_field(next.host, sizeof(next.host), line, "host");
copy_field(next.cpu, sizeof(next.cpu), line, "cpu");
copy_field(next.gpu, sizeof(next.gpu), line, "gpu");
next.cpu_pct = field_float(line, "cpu%", next.cpu_pct);
next.gpu_pct = field_float(line, "gpu%", next.gpu_pct);
const char *ram = find_field(line, "ram");
if (ram) {
float u = 0, t = 0;
if (sscanf(ram, "%f/%f", &u, &t) == 2) {
next.ram_used_gb = u;
next.ram_total_gb = t;
}
}
const char *disk = find_field(line, "disk");
if (disk) {
float u = 0, t = 0;
if (sscanf(disk, "%f/%f", &u, &t) == 2) {
next.disk_used_gb = u;
next.disk_total_gb = t;
}
}
if (next.cpu_pct < 0) {
next.cpu_pct = 0;
}
if (next.cpu_pct > 100) {
next.cpu_pct = 100;
}
if (next.gpu_pct > 100) {
next.gpu_pct = 100;
}
g_info = next;
}
void host_info_begin() {
memset(&g_info, 0, sizeof(g_info));
g_info.gpu_pct = -1.0f;
g_info.valid = false;
g_len = 0;
g_line[0] = '\0';
}
bool host_info_poll() {
bool got = false;
while (Serial.available() > 0) {
const int c = Serial.read();
if (c < 0) {
break;
}
if (c == '\r') {
continue;
}
if (c == '\n') {
g_line[g_len] = '\0';
if (g_len > 0) {
apply_line(g_line);
got = g_info.valid;
}
g_len = 0;
continue;
}
if (g_len + 1 < sizeof(g_line)) {
g_line[g_len++] = (char)c;
} else {
// Overflow — drop line
g_len = 0;
}
}
return got;
}
const HostInfo &host_info_get() { return g_info; }
bool host_info_is_live(uint32_t stale_ms) {
if (!g_info.valid) {
return false;
}
return (millis() - g_info.updated_ms) <= stale_ms;
}

52
src/main.cpp Normal file
View File

@@ -0,0 +1,52 @@
#include <Arduino.h>
#include <lvgl.h>
#include "host_info.h"
#include "pins.h"
#include "touch.h"
#include "ui.h"
static constexpr uint32_t DRAW_BUF_SIZE =
(SCREEN_WIDTH * SCREEN_HEIGHT / DRAW_BUF_DIV * (LV_COLOR_DEPTH / 8));
static uint32_t draw_buf[DRAW_BUF_SIZE / 4];
void setup() {
Serial.setRxBufferSize(2048);
Serial.begin(460800);
delay(200);
Serial.println();
Serial.printf("LVGL %d.%d.%d | CYD Server Info\n", lv_version_major(),
lv_version_minor(), lv_version_patch());
Serial.println("BOOT_OK");
Serial.println("PROTO SI|host=..|cpu=..|cpu%=..|gpu=..|gpu%=..|ram=u/t|disk=u/t");
Serial.printf("heap=%u BL=%d panel=%dx%d ui=%dx%d\n",
(unsigned)ESP.getFreeHeap(), TFT_BL_PIN, SCREEN_WIDTH,
SCREEN_HEIGHT, UI_HOR_RES, UI_VER_RES);
pinMode(TFT_BL_PIN, OUTPUT);
digitalWrite(TFT_BL_PIN, HIGH);
touch_init();
host_info_begin();
lv_init();
lv_display_t *disp =
lv_tft_espi_create(SCREEN_WIDTH, SCREEN_HEIGHT, draw_buf, sizeof(draw_buf));
lv_display_set_rotation(disp, LV_DISPLAY_ROTATION_270);
lv_indev_t *indev = lv_indev_create();
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(indev, touch_read_cb);
lv_indev_set_display(indev, disp);
ui_create();
Serial.printf("ui_ok heap=%u\n", (unsigned)ESP.getFreeHeap());
}
void loop() {
host_info_poll();
lv_timer_handler();
lv_tick_inc(5);
ui_tick();
delay(5);
}

284
src/touch.cpp Normal file
View File

@@ -0,0 +1,284 @@
#include "touch.h"
#include "pins.h"
#include <Arduino.h>
#if defined(CYD_BOARD_3248S035)
#include <Wire.h>
#endif
/**
* 2.8": dedicated SoftSPI + BL/TR diagonal fix.
* 3.5C (3248S035C): capacitive GT911 on I2C SDA33/SCL32/INT21/RST25.
*/
int16_t g_dbg_phys_x = -1;
int16_t g_dbg_phys_y = -1;
int16_t g_dbg_logic_x = -1;
int16_t g_dbg_logic_y = -1;
bool g_dbg_touch_down = false;
static int16_t clampi(int32_t v, int32_t lo, int32_t hi) {
if (v < lo) {
return (int16_t)lo;
}
if (v > hi) {
return (int16_t)hi;
}
return (int16_t)v;
}
#if defined(CYD_BOARD_3248S035)
/* Minimal GT911 — no config rewrite (TAMC setResolution can hang Wire). */
static uint8_t s_gt_addr = 0x5D;
static constexpr uint16_t GT_POINT_INFO = 0x814E;
static constexpr uint16_t GT_POINT_1 = 0x814F;
static bool gt_write8(uint16_t reg, uint8_t val) {
Wire.beginTransmission(s_gt_addr);
Wire.write((uint8_t)(reg >> 8));
Wire.write((uint8_t)(reg & 0xFF));
Wire.write(val);
return Wire.endTransmission() == 0;
}
static bool gt_read(uint16_t reg, uint8_t *buf, uint8_t len) {
Wire.beginTransmission(s_gt_addr);
Wire.write((uint8_t)(reg >> 8));
Wire.write((uint8_t)(reg & 0xFF));
if (Wire.endTransmission(false) != 0) {
return false;
}
const uint8_t n = Wire.requestFrom(s_gt_addr, len);
if (n != len) {
return false;
}
for (uint8_t i = 0; i < len; i++) {
buf[i] = Wire.read();
}
return true;
}
static bool gt_probe(uint8_t addr) {
Wire.beginTransmission(addr);
return Wire.endTransmission() == 0;
}
/* TAMC-compatible address select: INT low => 0x5D, INT high => 0x14 */
static void gt_hw_reset(bool addr_0x14) {
pinMode(CYD_GT911_INT, OUTPUT);
pinMode(CYD_GT911_RST, OUTPUT);
digitalWrite(CYD_GT911_INT, LOW);
digitalWrite(CYD_GT911_RST, LOW);
delay(10);
digitalWrite(CYD_GT911_INT, addr_0x14 ? HIGH : LOW);
delay(1);
digitalWrite(CYD_GT911_RST, HIGH);
delay(5);
digitalWrite(CYD_GT911_INT, LOW);
delay(50);
/* Backlight is GPIO27 — leave INT as input for polling / optional IRQ. */
pinMode(CYD_GT911_INT, INPUT);
delay(50);
}
static bool gt_find_addr() {
for (uint8_t a : {uint8_t(0x5D), uint8_t(0x14)}) {
if (gt_probe(a)) {
s_gt_addr = a;
return true;
}
}
return false;
}
void touch_init() {
Serial.println("Touch: GT911 I2C capacitive (3.5C) minimal");
Wire.begin(CYD_GT911_SDA, CYD_GT911_SCL);
Wire.setClock(100000);
#if defined(WIRE_HAS_TIMEOUT) || defined(ESP32)
Wire.setTimeOut(50);
#endif
if (!gt_find_addr()) {
Serial.println("GT911 not found — hw reset 0x5D");
gt_hw_reset(false);
if (!gt_find_addr()) {
Serial.println("GT911 not found — hw reset 0x14");
gt_hw_reset(true);
gt_find_addr();
}
}
Serial.printf("GT911 probe 0x5D=%s 0x14=%s using=0x%02X\n",
gt_probe(0x5D) ? "OK" : "no", gt_probe(0x14) ? "OK" : "no",
s_gt_addr);
Serial.printf("GT911 SDA=%d SCL=%d INT=%d RST=%d\n", CYD_GT911_SDA,
CYD_GT911_SCL, CYD_GT911_INT, CYD_GT911_RST);
}
void touch_read_cb(lv_indev_t * /*indev*/, lv_indev_data_t *data) {
uint8_t info = 0;
static uint32_t last_log = 0;
const bool do_log = (millis() - last_log > 200);
if (!gt_read(GT_POINT_INFO, &info, 1)) {
data->state = LV_INDEV_STATE_RELEASED;
g_dbg_touch_down = false;
if (do_log) {
last_log = millis();
Serial.println("TouchProbe GT911 I2C err");
}
return;
}
const uint8_t touches = info & 0x0F;
const bool buffered = (info & 0x80) != 0;
if (!buffered || touches == 0) {
if (buffered) {
gt_write8(GT_POINT_INFO, 0);
}
data->state = LV_INDEV_STATE_RELEASED;
g_dbg_touch_down = false;
return;
}
uint8_t pt[7];
if (!gt_read(GT_POINT_1, pt, 7)) {
gt_write8(GT_POINT_INFO, 0);
data->state = LV_INDEV_STATE_RELEASED;
g_dbg_touch_down = false;
return;
}
gt_write8(GT_POINT_INFO, 0);
const int16_t raw_x = (int16_t)(pt[1] | (pt[2] << 8));
const int16_t raw_y = (int16_t)(pt[3] | (pt[4] << 8));
/* Native 320x480 → LVGL 270. Mirror long axis so L/R matches UI
* (else preset 3 hits 10). Resulting UI ≈ (H-1-raw_y, raw_x). */
const int16_t nx = clampi(raw_x, 0, SCREEN_WIDTH - 1);
const int16_t ny = clampi(raw_y, 0, SCREEN_HEIGHT - 1);
const int16_t feed_x = (int16_t)((SCREEN_WIDTH - 1) - nx);
const int16_t feed_y = (int16_t)((SCREEN_HEIGHT - 1) - ny);
const int16_t ui_x = (int16_t)((SCREEN_HEIGHT - 1) - ny);
const int16_t ui_y = nx;
g_dbg_phys_x = raw_x;
g_dbg_phys_y = raw_y;
g_dbg_logic_x = ui_x;
g_dbg_logic_y = ui_y;
g_dbg_touch_down = true;
if (do_log) {
last_log = millis();
Serial.printf("Touch HIT GT911 raw(%d,%d) ~ui(%d,%d)\n", raw_x, raw_y,
ui_x, ui_y);
}
data->point.x = feed_x;
data->point.y = feed_y;
data->state = LV_INDEV_STATE_PRESSED;
}
#else
static void soft_write(uint8_t data) {
for (int i = 7; i >= 0; --i) {
digitalWrite(CYD_TOUCH_MOSI, (data >> i) & 1);
digitalWrite(CYD_TOUCH_CLK, HIGH);
delayMicroseconds(2);
digitalWrite(CYD_TOUCH_CLK, LOW);
delayMicroseconds(2);
}
}
static uint16_t soft_read12() {
uint16_t v = 0;
digitalWrite(CYD_TOUCH_CLK, HIGH);
delayMicroseconds(2);
digitalWrite(CYD_TOUCH_CLK, LOW);
delayMicroseconds(2);
for (int i = 0; i < 12; ++i) {
digitalWrite(CYD_TOUCH_CLK, HIGH);
delayMicroseconds(2);
v = (uint16_t)((v << 1) | (digitalRead(CYD_TOUCH_MISO) ? 1u : 0u));
digitalWrite(CYD_TOUCH_CLK, LOW);
delayMicroseconds(2);
}
return v;
}
static uint16_t xpt_read(uint8_t cmd) {
digitalWrite(CYD_TOUCH_CS, LOW);
soft_write(cmd);
uint16_t v = soft_read12();
digitalWrite(CYD_TOUCH_CS, HIGH);
return v;
}
void touch_init() {
pinMode(CYD_TOUCH_CS, OUTPUT);
pinMode(CYD_TOUCH_CLK, OUTPUT);
pinMode(CYD_TOUCH_MOSI, OUTPUT);
pinMode(CYD_TOUCH_MISO, INPUT);
pinMode(CYD_TOUCH_IRQ, INPUT);
digitalWrite(CYD_TOUCH_CS, HIGH);
digitalWrite(CYD_TOUCH_CLK, LOW);
Serial.println("Touch: SoftSPI + diagonal fix (BL/TR)");
}
void touch_read_cb(lv_indev_t * /*indev*/, lv_indev_data_t *data) {
uint16_t adc_x = xpt_read(0x90);
uint16_t adc_y = xpt_read(0xD0);
adc_x = (uint16_t)((adc_x + xpt_read(0x90)) / 2);
adc_y = (uint16_t)((adc_y + xpt_read(0xD0)) / 2);
const bool irq = (digitalRead(CYD_TOUCH_IRQ) == LOW);
const bool in_range =
(adc_x > 80 && adc_x < 4000 && adc_y > 80 && adc_y < 4000);
if (!in_range || (!irq && (adc_x < 200 || adc_y < 200))) {
data->state = LV_INDEV_STATE_RELEASED;
g_dbg_touch_down = false;
return;
}
const int16_t p_x = (int16_t)adc_x;
const int16_t p_y = (int16_t)(4095 - adc_y);
int16_t x = map(p_x, 200, 3700, 1, SCREEN_WIDTH);
int16_t y = map(p_y, 240, 3800, 1, SCREEN_HEIGHT);
x = clampi(x, 0, SCREEN_WIDTH - 1);
y = clampi(y, 0, SCREEN_HEIGHT - 1);
const int32_t old_ux = y;
const int32_t old_uy = (SCREEN_WIDTH - 1) - x;
int16_t ui_x = clampi((old_uy * (UI_HOR_RES - 1)) / (UI_VER_RES - 1), 0,
UI_HOR_RES - 1);
int16_t ui_y = clampi((old_ux * (UI_VER_RES - 1)) / (UI_HOR_RES - 1), 0,
UI_VER_RES - 1);
const int16_t feed_x = (int16_t)((SCREEN_WIDTH - 1) - ui_y);
const int16_t feed_y = ui_x;
g_dbg_phys_x = x;
g_dbg_phys_y = y;
g_dbg_logic_x = ui_x;
g_dbg_logic_y = ui_y;
g_dbg_touch_down = true;
static uint32_t last_log = 0;
if (millis() - last_log > 80) {
last_log = millis();
Serial.printf("Touch UI(%d,%d)\n", ui_x, ui_y);
}
data->point.x = feed_x;
data->point.y = feed_y;
data->state = LV_INDEV_STATE_PRESSED;
}
#endif

223
src/ui.cpp Normal file
View File

@@ -0,0 +1,223 @@
#include "ui.h"
#include "host_info.h"
#include "pins.h"
#include <Arduino.h>
#include <cstdio>
#include <lvgl.h>
static constexpr uint32_t COL_BG = 0x0B121A;
static constexpr uint32_t COL_INK = 0xE8EEF4;
static constexpr uint32_t COL_MUTED = 0x7A8B9C;
static constexpr uint32_t COL_LINE = 0x1A2734;
static constexpr uint32_t COL_OK = 0x3DDC97;
static constexpr uint32_t COL_WARN = 0xE8B84A;
static constexpr uint32_t COL_BAD = 0xE85D4C;
static constexpr uint32_t COL_ACCENT = 0x4FC3F7;
static constexpr uint32_t COL_BAR_BG = 0x162230;
struct MetricRow {
lv_obj_t *lbl_tag;
lv_obj_t *lbl_name;
lv_obj_t *lbl_pct;
lv_obj_t *bar;
};
static lv_obj_t *scr = nullptr;
static lv_obj_t *lbl_title = nullptr;
static lv_obj_t *lbl_host = nullptr;
static lv_obj_t *lbl_status = nullptr;
static MetricRow row_cpu = {};
static MetricRow row_gpu = {};
static MetricRow row_ram = {};
static MetricRow row_disk = {};
static uint32_t last_paint_ms = 0;
static int pct_of(float used, float total) {
if (total <= 0.01f) {
return 0;
}
int p = (int)((used * 100.0f) / total + 0.5f);
if (p < 0) {
p = 0;
}
if (p > 100) {
p = 100;
}
return p;
}
static uint32_t color_for_pct(int pct) {
if (pct >= 85) {
return COL_BAD;
}
if (pct >= 65) {
return COL_WARN;
}
return COL_OK;
}
static void style_bar(lv_obj_t *bar) {
lv_obj_set_style_bg_color(bar, lv_color_hex(COL_BAR_BG), LV_PART_MAIN);
lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_set_style_radius(bar, 4, LV_PART_MAIN);
lv_obj_set_style_pad_all(bar, 0, LV_PART_MAIN);
lv_obj_set_style_bg_color(bar, lv_color_hex(COL_OK), LV_PART_INDICATOR);
lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, LV_PART_INDICATOR);
lv_obj_set_style_radius(bar, 4, LV_PART_INDICATOR);
lv_bar_set_range(bar, 0, 100);
lv_bar_set_value(bar, 0, LV_ANIM_OFF);
}
static MetricRow make_row(lv_obj_t *parent, const char *tag, int y) {
MetricRow r = {};
r.lbl_tag = lv_label_create(parent);
lv_label_set_text(r.lbl_tag, tag);
lv_obj_set_style_text_color(r.lbl_tag, lv_color_hex(COL_ACCENT), 0);
lv_obj_set_style_text_font(r.lbl_tag, &lv_font_montserrat_14, 0);
lv_obj_set_pos(r.lbl_tag, UX(8), UY(y));
r.lbl_name = lv_label_create(parent);
lv_label_set_text(r.lbl_name, "—");
lv_label_set_long_mode(r.lbl_name, LV_LABEL_LONG_DOT);
lv_obj_set_width(r.lbl_name, UW(190));
lv_obj_set_style_text_color(r.lbl_name, lv_color_hex(COL_INK), 0);
lv_obj_set_style_text_font(r.lbl_name, &lv_font_montserrat_14, 0);
lv_obj_set_pos(r.lbl_name, UX(52), UY(y));
r.lbl_pct = lv_label_create(parent);
lv_label_set_text(r.lbl_pct, "--%");
lv_obj_set_style_text_color(r.lbl_pct, lv_color_hex(COL_MUTED), 0);
lv_obj_set_style_text_font(r.lbl_pct, &lv_font_montserrat_14, 0);
lv_obj_align(r.lbl_pct, LV_ALIGN_TOP_RIGHT, UX(-8), UY(y));
r.bar = lv_bar_create(parent);
lv_obj_set_size(r.bar, UI_HOR_RES - UW(16), UH(10));
lv_obj_set_pos(r.bar, UX(8), UY(y + 18));
style_bar(r.bar);
return r;
}
static void set_row_pct(MetricRow &r, int pct, const char *name, const char *right) {
if (r.lbl_name && name) {
lv_label_set_text(r.lbl_name, name);
}
if (r.lbl_pct && right) {
lv_label_set_text(r.lbl_pct, right);
lv_obj_set_style_text_color(r.lbl_pct, lv_color_hex(color_for_pct(pct)), 0);
}
if (r.bar) {
lv_bar_set_value(r.bar, pct, LV_ANIM_OFF);
lv_obj_set_style_bg_color(r.bar, lv_color_hex(color_for_pct(pct)),
LV_PART_INDICATOR);
}
}
void ui_create() {
scr = lv_obj_create(nullptr);
lv_obj_set_style_bg_color(scr, lv_color_hex(COL_BG), 0);
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
lv_obj_clear_flag(scr, LV_OBJ_FLAG_SCROLLABLE);
lv_screen_load(scr);
lbl_title = lv_label_create(scr);
lv_label_set_text(lbl_title, "PC Monitor");
lv_obj_set_style_text_color(lbl_title, lv_color_hex(COL_INK), 0);
lv_obj_set_style_text_font(lbl_title, &lv_font_montserrat_18, 0);
lv_obj_set_pos(lbl_title, UX(8), UY(6));
lbl_status = lv_label_create(scr);
lv_label_set_text(lbl_status, "waiting");
lv_obj_set_style_text_color(lbl_status, lv_color_hex(COL_WARN), 0);
lv_obj_set_style_text_font(lbl_status, &lv_font_montserrat_14, 0);
lv_obj_align(lbl_status, LV_ALIGN_TOP_RIGHT, UX(-8), UY(8));
lbl_host = lv_label_create(scr);
lv_label_set_text(lbl_host, "Connect USB + run host script");
lv_label_set_long_mode(lbl_host, LV_LABEL_LONG_DOT);
lv_obj_set_width(lbl_host, UI_HOR_RES - UW(16));
lv_obj_set_style_text_color(lbl_host, lv_color_hex(COL_MUTED), 0);
lv_obj_set_style_text_font(lbl_host, &lv_font_montserrat_14, 0);
lv_obj_set_pos(lbl_host, UX(8), UY(28));
lv_obj_t *line = lv_obj_create(scr);
lv_obj_set_size(line, UI_HOR_RES - UW(16), 1);
lv_obj_set_pos(line, UX(8), UY(48));
lv_obj_set_style_bg_color(line, lv_color_hex(COL_LINE), 0);
lv_obj_set_style_bg_opa(line, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(line, 0, 0);
lv_obj_set_style_pad_all(line, 0, 0);
lv_obj_clear_flag(line, LV_OBJ_FLAG_SCROLLABLE);
row_cpu = make_row(scr, "CPU", 56);
row_gpu = make_row(scr, "GPU", 96);
row_ram = make_row(scr, "RAM", 136);
row_disk = make_row(scr, "DISK", 176);
last_paint_ms = 0;
}
static void paint() {
const HostInfo &h = host_info_get();
const bool live = host_info_is_live(5000);
if (lbl_status) {
if (!h.valid) {
lv_label_set_text(lbl_status, "waiting");
lv_obj_set_style_text_color(lbl_status, lv_color_hex(COL_WARN), 0);
} else if (live) {
lv_label_set_text(lbl_status, "live");
lv_obj_set_style_text_color(lbl_status, lv_color_hex(COL_OK), 0);
} else {
lv_label_set_text(lbl_status, "stale");
lv_obj_set_style_text_color(lbl_status, lv_color_hex(COL_BAD), 0);
}
}
if (!h.valid) {
return;
}
if (lbl_host) {
lv_label_set_text(lbl_host, h.host[0] ? h.host : "(unknown host)");
}
char buf[48];
int cpu_pct = (int)(h.cpu_pct + 0.5f);
snprintf(buf, sizeof(buf), "%d%%", cpu_pct);
set_row_pct(row_cpu, cpu_pct, h.cpu[0] ? h.cpu : "CPU", buf);
if (h.gpu_pct < 0) {
set_row_pct(row_gpu, 0, h.gpu[0] ? h.gpu : "GPU", "n/a");
if (row_gpu.lbl_pct) {
lv_obj_set_style_text_color(row_gpu.lbl_pct, lv_color_hex(COL_MUTED), 0);
}
} else {
int gpu_pct = (int)(h.gpu_pct + 0.5f);
snprintf(buf, sizeof(buf), "%d%%", gpu_pct);
set_row_pct(row_gpu, gpu_pct, h.gpu[0] ? h.gpu : "GPU", buf);
}
int ram_pct = pct_of(h.ram_used_gb, h.ram_total_gb);
snprintf(buf, sizeof(buf), "%.1f/%.0fG", (double)h.ram_used_gb,
(double)h.ram_total_gb);
set_row_pct(row_ram, ram_pct, "Memory", buf);
int disk_pct = pct_of(h.disk_used_gb, h.disk_total_gb);
snprintf(buf, sizeof(buf), "%.0f/%.0fG", (double)h.disk_used_gb,
(double)h.disk_total_gb);
set_row_pct(row_disk, disk_pct, "System drive", buf);
}
void ui_tick() {
const uint32_t now = millis();
if (now - last_paint_ms < 200) {
return;
}
last_paint_ms = now;
paint();
}