行空板 K10 打造桌面级性能仪表盘
一行命令,把电脑的 CPU、内存、显卡温度、网速全部搬到一块 240×320 的小屏幕上实时跳动。
1. 计算机信息呈现桌面级性能仪表盘
我用python psutil库提取pc运行参数,结合行空板 K10板(ESP32-S3 + ILI9341 屏幕),把它变成一个摆在桌上的性能监视器。三天写代码加两天跟 Windows 温度 API 死磕之后,呈现的效果如下:

2. 整体架构
┌──────────────────────┐ WiFi HTTP POST ┌────────────────────┐
│ PC (Python/psutil) │ ──────────────────────▶ │ K10 (ESP32-S3) │
│ │ /stats 端点 │ │
│ CPU 使用率/频率 │ │ WebServer :80 │
│ CPU 温度 (WMI) │ │ ILI9341 240×320 │
│ GPU 温度 (nvidia) │ │ LovyanGFX 渲染 │
│ RAM / Disk / Net │ │ efontCN_16 中文 │
│ 电池 / 运行时长 │ │ NVS 存 WiFi 配置 │
└──────────────────────┘ └────────────────────┘
PC 端每秒用 psutil 采一次数据,打包成 JSON,HTTP POST 到 K10 的 /stats。K10 收到马上刷新屏幕。没什么花哨的。
3. Python 端:psutil 怎么采数据
3.1 psutil 能拿到什么
psutil 是 Python 下最老的系统监控库,跨平台,一行 pip install psutil 就能用。这个项目用到的 API:
| API | 拿什么 | 实际输出 |
|---|---|---|
psutil.cpu_percent(interval=0.5) |
CPU 使用率 | 12.3 (%) |
psutil.cpu_freq() |
CPU 频率 | 3200 (MHz) |
psutil.virtual_memory() |
内存总量/已用 | 63.4GB / 14.7GB |
psutil.disk_usage('C:\\') |
磁盘用量 | 48.3% |
psutil.net_io_counters() |
网络累计字节 | 两次采样做差 |
psutil.sensors_battery() |
电池百分比/充电状态 | 71%, plugged |
psutil.boot_time() |
开机时间戳 | 算出运行时长 |
注意 cpu_percent 需要 interval=0.5 才会有准确读数,否则第一次返回 0。net_io_counters 返回的是累计值,算实时速率需要两次采样相减除以时间间隔,这个搞嵌入式计时的应该秒懂。
完整采集函数大概长这样:
def get_stats():
cpu_percent = psutil.cpu_percent(interval=0.5)
cpu_freq = int(psutil.cpu_freq().current)
cpu_temp = _get_cpu_temp() # 下面细说
gpu_temp = _get_gpu_temp() # 下面细说
mem = psutil.virtual_memory()
ram_pct = round(mem.percent, 1)
ram_used = round(mem.used / 1024**3, 1)
ram_total = round(mem.total / 1024**3, 1)
disk = psutil.disk_usage('C:\\')
battery = psutil.sensors_battery()
bat_pct = int(battery.percent) if battery else -1
return {"cpu": cpu_percent, "freq": cpu_freq, ...}
3.2 温度采集,这才是最坑的
CPU 温度
跑第一版的时候,控制台安静地打印 cpu_temp=-1。看了一眼代码,psutil.sensors_temperatures() 返回空。查了才知道,psutil 7.x 把这个函数给移除了。
然后我就开始了漫长的 Windows WMI 探险:
第一站:MSAcpi_ThermalZoneTemperature,在 root/wmi 命名空间下。API 很标准,返回值是 0.1K。但一跑就报 拒绝访问。需要管理员权限。
第二站:翻微软文档翻到了 Win32_PerfFormattedData_Counters_ThermalZoneInformation,在 root/cimv2 下面。不用管理员。返回值是 0.1°C(注意单位,不是 0.1K),直接 /10.0 就是摄氏度。凌晨两点终于看到 temp=32 的时候,那种感觉懂的都懂。
于是温度采集变成了三级回退:
def _get_cpu_temp():
# 1. psutil 老版本
try: return int(psutil.sensors_temperatures()['coretemp'][0].current)
except: pass
# 2. MSAcpi — 要管理员
out = subprocess.run(['powershell', '-Command',
'(Get-CimInstance -Namespace root/wmi '
'-ClassName MSAcpi_ThermalZoneTemperature).CurrentTemperature'],
capture_output=True, text=True)
if out.stdout.strip():
return int(float(out.stdout.strip()) / 10.0 - 273.15)
# 3. ThermalZoneInformation — 不!用!管!理!员!
out = subprocess.run(['powershell', '-Command',
'(Get-CimInstance -Namespace root/cimv2 -ClassName '
'Win32_PerfFormattedData_Counters_ThermalZoneInformation).Temperature'],
capture_output=True, text=True)
if out.stdout.strip():
return int(float(out.stdout.strip()) / 10.0) # 0.1°C
return -1
GPU 温度
这个简单到离谱。只要你装了 NVIDIA 驱动,一行 nvidia-smi 搞定:
def _get_gpu_temp():
out = subprocess.run(
['nvidia-smi', '--query-gpu=temperature.gpu',
'--format=csv,noheader'],
capture_output=True, text=True
)
return int(out.stdout.strip()) if out.stdout.strip() else -1
没有 NVIDIA 显卡就返回 -1,K10 那端看到 -1 就不画 GPU 温度。不会崩。
4. K10 端:固件做了什么
硬件就一块行空板 K10(ESP32-S3, 16MB Flash, 8MB PSRAM),屏幕 ILI9341 240×320,背光走 XL9535。K10 固件主要干三件事:
- WebServer 在 80 端口跑着,收 PC 的
/statsPOST 请求,收到就刷屏 - LovyanGFX 驱动屏幕,
efontCN_16内置中文字体画标签,英文数字画进度条和数值 - 配网系统 独立跑一套 WiFi 管理逻辑,下面细说
K10 代码已经不短了(编译出来 1.28MB),好在合并固件直接烧,不用从头搭。如果感兴趣细节,项目里有完整 .ino 源码。
5. 配网:扫码太麻烦,那就扫 WiFi
传统 ESP32 配网就两种路子:SmartConfig(用手机 App 广播 WiFi 密码,时灵时不灵)、手动敲 SSID(手机键盘打中文 WiFi 名,谁试谁知道)。PC Pulse 用的是第三种:AP 回退 + 网页扫描选网。
5.1 整体流程
K10 上电后的 WiFi 决策树:
上电 → 读 NVS 里的 WiFi 配置
├─ 有配置 → WiFi.begin() 连路由器
│ ├─ 连上了 → 正常模式,屏幕显示 IP
│ └─ 连不上(超时 15 秒)→ 开 AP
└─ 没配置 → 开 AP(首次使用)
AP 热点固定 PC-Pulse,密码 12345678。手机连上后打开 192.168.4.1 就能配网。配完点保存,K10 把 SSID 和密码写入 NVS 的 k10mon 命名空间,然后 ESP.restart() 自动重启,下次上电走第一条分支直连路由器。
5.2 K10 端两个 API
配网页不是写死的 HTML,是前后端分离的:
GET /scan — 调 WiFi.scanNetworks() 同步扫描周围 WiFi,返回 JSON 数组:
[
{"s": "MyWiFi_5G", "r": -45, "e": 1},
{"s": "TP-LINK_2.4G", "r": -72, "e": 1},
{"s": "Starbucks", "r": -88, "e": 0}
]
三个字段的意思:s = SSID,r = 信号强度 RSSI(越接近 0 越强),e = 是否需要密码(1 加密 / 0 开放)。最多返回 30 个,WiFi.scanDelete() 及时释放内存。
GET /saved — 从 NVS 里读出上一次保存的 SSID:
{"ssid": "MyWiFi_5G", "has": true}
页面加载时先调这个,如果有已存的 SSID 就直接预填到输入框里,省得再扫一遍。
5.3 配网页的前端
整个 HTML 页面塞在 .ino 的 PROGMEM 区,不占 ESP32 的运行内存。打开以后做的事:
- 页面加载完,先
fetch('/saved')查有没有存过的 SSID,有就填上 - 然后
fetch('/scan')拿到 WiFi 列表,遍历渲染 - 每条 WiFi 用三个指标展示:SSID 名称、信号强度(用 CSS 画绿/黄/红点的简版信号条)、🔒 图标(加密的还是开放的)
- 点一下某个 WiFi 行,CSS 高亮选中,同时自动填入 SSID 输入框
- 密码框单独输,输完点「保存并连接」
- JS 用
fetch('/save', {method: 'POST', body: new URLSearchParams({ssid, pass})})发给 K10 的/save端点 - K10 收到后写 NVS,返回一个"已保存,3 秒后重启"的页面,然后
ESP.restart()
说白了就是用手机浏览器的能力替代了配网 App。整个页面不到 2KB,没有框架、没有 CDN,纯原生 JS,在 K10 的 AP 模式下访问秒开。
5.4 NVS 存储
K10 用 ESP32 的 Preferences 库做持久化,键值存在 k10mon 命名空间下:
k10mon / ssid = "MyWiFi_5G"
k10mon / pass = "my_password"
k10mon / done = true ← 标记"已经配过网了"
done 这个布尔值很关键。Read 端用它判断"是首次使用还是配过网了",决定走哪条分支。如果没有这个标记,即使 NVS 里残留了上回的 SSID 也不会试着连,直接开 AP,避免拿着旧密码反复超时的尴尬。
6. 上手流程
三步走:烧固件 → 配网拿 IP → 启动 PC 端。
6.1 下载固件
合并好的固件已上传到天翼云盘,直接下载烧录即可:
链接:https://cloud.189.cn/t/J3aIbmniEFfa
访问码:uax0
文件是 PC_Pulse_K10_merged.bin,4MB,含 bootloader + 分区表 + 完整固件。
6.2 烧录
用 esptool 写 0x0 地址就行。Windows 用户打开 ESP32 烧录工具(如 ESP Flash Download Tool),选 ESP32-S3、地址填 0x0,加载合并固件,点 Start。
命令行的话:
esptool.py --chip esp32s3 --port COM7 write_flash 0x0 PC_Pulse_K10_merged.bin
烧完 K10 自动重启,屏幕亮起。
6.3 配网
首次上电 K10 自动开 AP 热点(PC-Pulse / 12345678)。手机连上去,浏览器打开 192.168.4.1,扫一圈选你的 WiFi,输密码,保存。K10 自动重启连上路由器,屏幕上会显示 IP 地址(比如 192.168.3.53)。
配网细节见第 5 节,全程不用敲一行代码。
6.4 启动 PC 端
拿到 K10 的 IP 之后,在 PC 上:
cd PC_Pulse
uv sync
uv run python main.py 192.168.3.53 # 换成屏幕上的 IP
环境变量也可以:K10_IP, K10_PORT, UPDATE_INTERVAL。跑起来后 PC 端每秒推一次数据,K10 屏幕实时刷新。
7. 花了两天踩出来的经验
- psutil 版本:升级到 7.x 之后
sensors_temperatures()就没了。官方没在任何 changelog 里提,我是对着空返回值发了一下午呆才发现的。 - Windows WMI 有两个坑:
root/wmi下面的类大多要管理员权限,而且单位是 0.1K 不是 0.1°C。root/cimv2下的ThermalZoneInformation两个问题都没有,一开始不知道,走了一大圈弯路。 - LovyanGFX 的
efontCN_16是白送的,不用另外搞字库。16px 在 240px 宽的屏幕上刚好,再大就挤了。 - 配网:扫列表比手工输入好一万倍。而且把 HTML 放 PROGMEM 里完全不占运行内存,WiFi 扫描 API 返回 JSON 而不是 HTML 片段,前后端分得清清楚楚。
更多推荐
所有评论(0)