基于ESP32与WebSocket打造实时PC硬件性能监视器 1. 项目概述为什么需要一块硬件性能监视器在折腾电脑硬件的圈子里有一类玩家被称为“副屏党”。他们不满足于Windows任务管理器那简陋的界面或者AIDA64、HWMonitor等软件在游戏全屏时无法查看的局限。他们追求的是将CPU温度、占用率、内存使用量、显卡负载等关键系统状态实时、直观地显示在一块独立的屏幕上仿佛给自己的电脑装上了一块“仪表盘”。传统的解决方案要么是购买昂贵的专用副屏要么是使用闲置的旧手机或平板但前者成本高后者又涉及复杂的软件配置和供电问题。这时ESP32这款芯片的优势就凸显出来了。它内置Wi-Fi和蓝牙功耗极低价格亲民一块开发板通常只需二三十元并且拥有强大的社区生态和丰富的库支持。用ESP32打造一个PC性能监视器本质上就是让它通过Wi-Fi连接到你的电脑接收电脑端软件发送的系统状态数据然后驱动一块小巧的OLED或LCD屏幕将这些数据以酷炫的界面显示出来。这不仅仅是一个极客玩具它解决了几个真实痛点游戏玩家可以随时瞥一眼硬件状态避免过热降频超频爱好者可以实时监控电压和频率的细微变化即便是普通用户桌面上多一个会动的、显示着系统信息的“小摆件”也极具科技感和实用性。这个项目的核心就是打通“软件采集”与“硬件显示”之间的桥梁。下面我将以一个资深硬件爱好者的视角带你从零开始手把手实现一个功能完整、界面美观的ESP32 PC性能监视器。我们会涵盖从硬件选型、软件环境搭建、通信协议设计、到UI界面绘制和最终调试优化的全流程并分享我踩过的坑和积累的实战技巧。2. 核心硬件选型与电路设计解析工欲善其事必先利其器。硬件是项目的基石选对组件能让后续开发事半功倍。2.1 ESP32开发板选型S3还是C3市面上ESP32型号繁多对于本项目我们主要考虑两个主流型号ESP32-S3和ESP32-C3。ESP32-S3双核处理器主频高达240MHz拥有更充足的PSRAM和Flash支持USB OTG性能强劲。如果你的UI界面非常复杂比如要显示动态图表、多页信息或者未来想扩展更多传感器如环境温湿度S3是更稳妥的选择。ESP32-C3单核RISC-V处理器主频160MHz成本更低功耗更优。对于显示静态或简单刷新的系统状态信息文本、数字、进度条完全够用是性价比之选。我的建议对于大多数初次尝试者一块ESP32-S3 DevKitC或类似开发板是最佳起点。它性能冗余足调试接口USB转串口完备板上常有用户按键和LED方便测试。价格比C3贵不了多少但能让你在开发UI时更从容。2.2 显示模块OLED与LCD的抉择显示模块是项目的“脸面”直接决定观感和用户体验。OLED有机发光二极管代表型号是SSD1306驱动的0.96或1.3英寸屏幕。优点是对比度极高黑色纯粹显示效果非常锐利功耗低且通常支持I2C通信接线简单仅需4根线。缺点是尺寸小分辨率有限128x64或128x32不适合显示大量信息且有潜在的烧屏风险长期显示静态内容。LCD液晶显示器常见的有ST7789、ILI9341驱动的1.3寸、1.54寸或2寸TFT屏幕。优点是色彩丰富通常是262K色尺寸和分辨率选择多240x240, 240x320等可以显示更复杂的图形和图标无烧屏顾虑。缺点是通常需要SPI接口接线稍多6-7根线功耗比OLED高且在显示深色背景时可能有背光漏光。我的选择与心得经过多次尝试我最终推荐使用一块1.54英寸的IPS TFT LCDST7789驱动240x240分辨率。原因如下可视面积大能同时舒适地显示8-10行信息布局更灵活。色彩表现力可以用不同颜色区分不同类型的数值如CPU用蓝色温度用红/黄内存用绿色视觉层次更清晰。SPI速度优势虽然接线多但SPI的刷新率远高于I2C在更新大量数据时比如整屏刷新图表更流畅。成熟的驱动库TFT_eSPI库对ST7789支持非常好功能强大。注意购买屏幕时务必确认其驱动芯片型号并选择带IPS技术的型号它的可视角度远优于普通TN屏。另外很多屏幕集成了SD卡槽虽然本项目用不到但可以作为未来扩展。2.3 电路连接与供电方案连接非常简单。我们以ESP32-S3 DevKitC和ST7789 SPI LCD为例ESP32-S3 引脚ST7789 LCD 引脚功能说明3.3VVCC电源正极GNDGND电源地GPIO 13SCLKSPI时钟线GPIO 11MOSI (SDA)SPI数据线主出从入GPIO 10CS片选低电平有效GPIO 9DC数据/命令选择GPIO 8RST复位可接也可由软件控制GPIO 7BLK背光控制接3.3V常亮或接GPIO可控供电的坑ESP32在启动和Wi-Fi通信时会有瞬时电流峰值。如果通过电脑USB口供电同时驱动LCD可能会因电流不足导致开发板不断重启。强烈建议使用一个5V/1A以上的手机充电头或充电宝单独为ESP32供电。如果非要使用电脑USB请确保连接的是主板后置的USB口供电更足而非机箱前置面板。3. 软件生态搭建与核心通信协议设计硬件连好后大脑软件才是灵魂。这部分我们将搭建开发环境并设计电脑与ESP32之间高效、稳定的通信桥梁。3.1 开发环境选择Arduino IDE vs. PlatformIOArduino IDE入门简单库管理方便适合新手快速验证想法。但对于多文件项目管理和高级调试支持较弱。PlatformIO基于VSCode专业开发者的首选。它是一个嵌入式开发平台集成了库管理、构建、上传、调试等一系列工具对项目结构管理非常友好支持代码自动补全和跳转。我的强力推荐直接使用VSCode PlatformIO扩展。虽然初期配置比Arduino IDE稍复杂但它能极大提升开发效率和代码质量。你可以在一个项目内清晰地区分src源代码、lib自定义库、include头文件并且PlatformIO能自动处理复杂的库依赖关系。3.2 核心通信协议为什么是WebSocket电脑上的监控软件需要将数据推送给ESP32。常见的方案有HTTP轮询、MQTT和WebSocket。HTTP轮询ESP32定时如每秒向电脑发送HTTP GET请求获取数据。简单但效率低下延迟高且无谓地消耗网络和计算资源。MQTT轻量级的发布/订阅消息协议。需要电脑和ESP32都连接到一个MQTT代理服务器Broker数据通过主题Topic传递。适合分布式、多设备场景但对于本项目的单对单直连略显重量级。WebSocket它在单个TCP连接上提供全双工通信通道。一旦握手建立服务器电脑端可以随时主动向客户端ESP32推送数据延迟极低毫秒级且开销小。设计决策我们采用WebSocket作为通信协议。理由很充分实时性是性能监控的第一要务WebSocket的主动推送机制完美契合它允许我们以极高的频率如100ms更新数据而不会造成连接风暴协议本身成熟ESP32和PC端都有非常稳定的库支持。数据格式选择JSON我们通过WebSocket传输的数据包将采用JSON格式。JSON结构清晰易于人类阅读和调试并且几乎所有的编程语言都有完善的解析库。一个典型的数据包可能长这样{ cpu: { usage: 45.2, temp: 65, freq: 4200 }, gpu: { usage: 78.5, temp: 72, vram_used: 5124 }, ram: { used: 8192, total: 16384 }, timestamp: 1697012345 }3.3 PC端数据采集选择你的“探针”ESP32需要数据那么数据从何而来我们需要在电脑上运行一个后台服务我们称之为“探针”或“Agent”来采集系统信息并通过WebSocket服务器发送出去。这里有几个主流选择Open Hardware Monitor (OHM) 自定义桥接程序OHM是一个开源、免费的硬件监控库能获取CPU、GPU、主板、硬盘等几乎所有传感器的数据。我们可以写一个C#或Python的小程序调用OHM的库获取数据封装成JSON再通过WebSocket服务器如Fleckfor C#websocketsfor Python发送出去。这是最灵活、数据最全的方案。HWiNFO 共享内存接口HWiNFO是功能极其强大的专业监控软件它提供了“共享内存”或“SDK”接口允许外部程序读取其采集的实时数据。稳定性极高数据极其准确。你需要根据其文档编写读取共享内存的客户端程序。使用现成的数据源有些游戏内覆盖显示软件如MSI Afterburner的RTSS也提供网络或共享内存接口可以尝试对接。我的实战路径我选择了方案一OHM Python。原因如下完全开源可控从数据采集到网络发送整个链条自己掌握便于调试和定制。Python开发快捷利用pythonnet调用OHM的.NET库再用websockets库建立服务器几十行代码就能跑通原型。跨平台潜力核心逻辑是Python理论上稍加修改也可用于Linux需换用psutil等库采集数据。重要提示无论选择哪种方案务必确保你的“探针”程序以系统服务或开机自启动的方式运行并且要处理好异常如传感器断开、WebSocket连接中断保证其长时间稳定运行在后台。4. ESP32端固件开发全流程详解这是项目的核心编码部分。我们将使用PlatformIO环境基于Arduino框架进行开发。4.1 项目初始化与库依赖管理首先在PlatformIO中创建一个新项目选择正确的开发板如Espressif ESP32-S3-DevKitC-1。然后打开项目根目录下的platformio.ini文件这是项目的配置文件我们需要在这里声明依赖的库。[env:esp32-s3-devkitc-1] platform espressif32 board esp32-s3-devkitc-1 framework arduino monitor_speed 115200 ; 库依赖 lib_deps bblanchon/ArduinoJson^6.21.3 links2004/WebSockets^2.4.1 bodmer/TFT_eSPI^2.5.0 ; TFT_eSPI需要额外配置关键库说明ArduinoJson必选项。用于解析从PC端收到的JSON格式数据包高效且易用。WebSockets由Links2004维护的ESP32 WebSocket客户端库稳定且功能完整。TFT_eSPI强大的TFT驱动库。注意它需要手动配置。你需要在项目目录下找到或创建lib/TFT_eSPI/User_Setups相关的头文件来指定你的屏幕型号、驱动芯片和引脚连接。这是第一个容易踩坑的地方。4.2 TFT_eSPI库的配置与初始化在项目目录中找到.pio/libdeps/你的环境名/TFT_eSPI文件夹。复制User_Setup_Select.h文件到你的项目src目录下或直接修改库内的该文件。编辑这个文件注释掉其他所有的#include只留下与你屏幕驱动芯片对应的一行例如//#include User_Setup.h // Default setup is root library folder //#include User_Setups/Setup1_ILI9341.h #include User_Setups/Setup25_ST7789.h // 假设你的屏幕是ST7789 // ... 其他Setup然后找到对应的Setup文件如Setup25_ST7789.h根据你的实际接线修改里面的引脚定义#define TFT_CS 10 // Chip select control pin #define TFT_DC 9 // Data Command control pin #define TFT_RST 8 // Reset pin (could connect to Arduino RESET pin) #define TFT_MOSI 11 // Data out #define TFT_SCLK 13 // Clock out // ... 确保SPI频率、屏幕尺寸、旋转方向等参数正确初始化代码示例#include TFT_eSPI.h TFT_eSPI tft TFT_eSPI(); void setup() { Serial.begin(115200); tft.init(); tft.setRotation(1); // 根据你的屏幕安装方向调整0-3 tft.fillScreen(TFT_BLACK); // 清屏为黑色 tft.setTextColor(TFT_WHITE, TFT_BLACK); // 设置字体颜色前景背景 tft.setTextSize(2); // 设置字体大小 tft.setCursor(0, 0); // 设置起始光标位置 tft.println(ESP32 Monitor); }4.3 WebSocket客户端连接与数据解析接下来实现WebSocket客户端逻辑连接PC端的服务器并处理消息。#include ArduinoJson.h #include WebSocketsClient.h WebSocketsClient webSocket; const char* ws_server_host 192.168.1.100; // 你的电脑内网IP const uint16_t ws_server_port 8080; // WebSocket服务器端口 const char* ws_server_path /; // 路径 void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) { switch(type) { case WStype_DISCONNECTED: Serial.printf([WSc] Disconnected!\n); break; case WStype_CONNECTED: Serial.printf([WSc] Connected to url: %s\n, payload); // 连接成功后可以发送一个认证或请求数据的信息如果需要 // webSocket.sendTXT(Hello from ESP32); break; case WStype_TEXT: { Serial.printf([WSc] Got text: %s\n, payload); // 收到文本数据JSON开始解析 parseSystemStats((char*)payload); break; } case WStype_ERROR: case WStype_PING: case WStype_PONG: break; } } void parseSystemStats(char* jsonString) { StaticJsonDocument1024 doc; // 根据你的JSON大小调整宁大勿小 DeserializationError error deserializeJson(doc, jsonString); if (error) { Serial.print(F(deserializeJson() failed: )); Serial.println(error.f_str()); return; } // 提取数据 float cpuUsage doc[cpu][usage]; // 45.2 int cpuTemp doc[cpu][temp]; // 65 int ramUsed doc[ram][used]; // 8192 int ramTotal doc[ram][total]; // 16384 // ... 提取其他数据 // 调用UI更新函数传入这些数据 updateDisplay(cpuUsage, cpuTemp, ramUsed, ramTotal); } void setup() { // ... 其他初始化 webSocket.begin(ws_server_host, ws_server_port, ws_server_path); webSocket.onEvent(webSocketEvent); webSocket.setReconnectInterval(5000); // 断开后5秒重连 } void loop() { webSocket.loop(); // 必须持续调用以处理WebSocket事件 // ... 其他循环任务 }4.4 用户界面(UI)绘制与优化策略UI绘制是让数据“活”起来的关键。我们需要考虑布局、刷新效率和视觉效果。1. 布局设计 对于240x240的屏幕一个实用的布局是分区域显示顶部区域显示标题、时间或系统概要如“PC Dashboard”。左侧主区域用大字体和进度条显示核心指标CPU使用率、CPU温度、GPU使用率、GPU温度。右侧副区域显示内存使用量已用/总量、网络上行/下行速度、硬盘活动指示灯等。底部区域可以显示一些滚动信息或自定义标签。2. 局部刷新与双缓冲TFT_eSPI库支持局部刷新setAddrWindow和pushColors但更高级的做法是使用帧缓冲Frame Buffer。库提供了TFT_eSprite类它允许你在内存PSRAM中创建一个离屏的“精灵”Sprite先在这个精灵上完成所有绘制操作最后一次性将整个精灵快速推送到屏幕上。这能有效消除屏幕闪烁提升视觉流畅度。TFT_eSprite spr TFT_eSprite(tft); // 创建一个关联到tft的精灵 void setup() { // ... // 在PSRAM中创建帧缓冲如果ESP32有PSRAM的话 if(psramFound()){ spr.setColorDepth(8); // 8位色深256色足以满足需求节省内存 spr.createSprite(240, 240); // 创建和屏幕一样大的精灵 } else { // 没有PSRAM只能在主内存创建小一些的缓冲或不用 spr.createSprite(120, 120); } } void updateDisplay(float cpuUsage, int cpuTemp, ...) { spr.fillSprite(TFT_BLACK); // 用黑色清空精灵 // 1. 绘制标题 spr.setTextColor(TFT_CYAN, TFT_BLACK); spr.drawString(PC Stats, 5, 5, 4); // 2. 绘制CPU使用率进度条 drawProgressBar(10, 40, 100, 15, cpuUsage, TFT_BLUE, TFT_DARKGREY, CPU); // 3. 绘制CPU温度用颜色表示 uint16_t tempColor (cpuTemp 60) ? TFT_GREEN : (cpuTemp 80) ? TFT_YELLOW : TFT_RED; spr.setTextColor(tempColor, TFT_BLACK); spr.drawNumber(cpuTemp, 120, 42, 2); spr.drawString(C, 145, 42, 2); // ... 绘制其他元素 // 最后将精灵推送到屏幕的指定位置这里是全屏 spr.pushSprite(0, 0); } void drawProgressBar(int x, int y, int w, int h, float percent, uint16_t color, uint16_t bgColor, const char* label) { // 绘制背景 spr.fillRoundRect(x, y, w, h, 3, bgColor); // 计算填充宽度 int fillW (percent / 100.0) * w; if(fillW w) fillW w; // 绘制前景 spr.fillRoundRect(x, y, fillW, h, 3, color); // 绘制标签和百分比文本 spr.setTextColor(TFT_WHITE, bgColor); spr.drawString(label, x, y - 12, 2); spr.drawFloat(percent, 1, x w 5, y, 2); spr.drawString(%, x w 30, y, 2); }3. 刷新率控制 无需以WebSocket接收数据的最高频率刷新屏幕。人类视觉对超过15-20FPS的变化已感觉流畅。我们可以设置一个定时器比如每100ms10FPS刷新一次UI。这能降低ESP32的CPU负载减少屏幕刷新带来的微小闪烁。unsigned long lastUpdateTime 0; const unsigned long updateInterval 100; // 毫秒 void loop() { webSocket.loop(); unsigned long now millis(); if (now - lastUpdateTime updateInterval) { lastUpdateTime now; // 调用一个函数使用最新的数据更新精灵并推送 refreshDisplay(); } }5. 系统集成、调试与性能优化实录当硬件、PC端探针、ESP32固件都准备就绪后真正的挑战在于让它们稳定、优雅地协同工作。5.1 PC端探针Python示例实现细节这里给出一个基于Pythonpsutil(用于基础信息) 和pythonnet(用于调用OpenHardwareMonitor) 的简化示例。实际应用中你可能需要结合两者。import asyncio import websockets import json import psutil import time # 假设通过某种方式从OHM获取了GPU等更详细的数据 # 例如通过一个单独的线程读取OHM的WMI接口或共享内存 class SystemMonitor: def __init__(self): self.data {} async def gather_data(self): 收集系统数据 # CPU使用率 self.data[cpu] { usage: psutil.cpu_percent(intervalNone), # 非阻塞式获取 # temp: get_cpu_temp_from_ohm(), # 需要实现 # freq: psutil.cpu_freq().current } # 内存 mem psutil.virtual_memory() self.data[ram] { used: mem.used // (1024**2), # 转换为MB total: mem.total // (1024**2), percent: mem.percent } # 网络每秒速率 # net_io psutil.net_io_counters() # 计算逻辑略... self.data[timestamp] time.time() return json.dumps(self.data) async def serve(self, websocket, path): WebSocket服务器处理函数 print(fClient connected from {websocket.remote_address}) try: while True: message await self.gather_data() await websocket.send(message) await asyncio.sleep(0.1) # 发送间隔100ms except websockets.exceptions.ConnectionClosed: print(fClient disconnected) async def main(): monitor SystemMonitor() server await websockets.serve(monitor.serve, 0.0.0.0, 8080) # 监听所有IP print(WebSocket server started on ws://0.0.0.0:8080) await server.wait_closed() if __name__ __main__: asyncio.run(main())关键点这个Python脚本需要以管理员或系统权限运行才能访问所有硬件传感器信息。你需要处理异常确保在ESP32断开连接时不会崩溃并且要处理好psutil可能无法获取某些特定硬件如GPU数据的问题。对于GPU、主板温度等集成OpenHardwareMonitor的Lib是更专业的方案。5.2 连接稳定性与错误处理实战在实际使用中Wi-Fi连接可能不稳定PC可能休眠程序可能崩溃。我们必须让ESP32固件足够健壮。Wi-Fi重连机制ESP32的Wi-Fi库自带重连但我们可以做得更好。#include WiFi.h #include WiFiMulti.h WiFiMulti wifiMulti; void setup() { wifiMulti.addAP(你的SSID, 你的密码); // 可以添加多个AP备用 // wifiMulti.addAP(SSID2, PASS2); Serial.print(Connecting to WiFi); while(wifiMulti.run() ! WL_CONNECTED) { Serial.print(.); delay(500); } Serial.println(\nConnected!); } // 在loop中可以定期检查连接状态如果断开则尝试重连WebSocket心跳与超时为了防止连接因空闲被中间路由器断开可以定期发送Ping心跳包。WebSockets库通常会自动处理Ping/Pong。此外可以设置一个“最后接收数据时间戳”如果超过一定时间如10秒没收到数据就主动断开并重连。unsigned long lastDataTime 0; const unsigned long dataTimeout 10000; // 10秒 void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) { switch(type) { case WStype_TEXT: lastDataTime millis(); // 收到数据更新时间戳 parseSystemStats((char*)payload); break; // ... 其他case } } void loop() { webSocket.loop(); // 检查数据超时 if(millis() - lastDataTime dataTimeout webSocket.isConnected()) { Serial.println(Data timeout, disconnecting...); webSocket.disconnect(); lastDataTime millis(); } // 定期UI刷新逻辑... }JSON解析容错PC端发送的数据包格式可能意外错误。务必检查DeserializationError并做好默认值处理避免解析失败导致程序重启。float cpuUsage doc[cpu][usage] | 0.0; // 使用“|”操作符提供默认值5.3 性能优化与功耗考量ESP32侧优化使用PSRAM如果ESP32-S3模块有PSRAM务必利用起来创建帧缓冲这将极大提升图形绘制速度。降低刷新率如前所述将UI刷新率控制在10-15FPS完全足够。Wi-Fi功耗模式如果设备是USB常供电可以不考虑。如果是电池供电可以设置WiFi.setSleep(true)来启用Wi-Fi节能模式但这可能略微增加延迟。CPU频率默认是240MHz如果UI不复杂可以尝试降低到160MHz以节省些许功耗setCpuFrequencyMhz(160);。PC端优化数据采样间隔不需要以极限频率采集数据。对于CPU使用率等指标500ms-1s的采样间隔对于显示来说已经足够细腻也能降低PC端进程的负载。差分更新可以设计更复杂的协议仅当数据变化超过一定阈值时才发送进一步减少网络流量和ESP32的解析负担。5.4 常见问题与排查技巧速查表以下是我在多次实践中遇到的一些典型问题及解决方法问题现象可能原因排查步骤与解决方案ESP32无法连接Wi-Fi密码错误、信号弱、路由器设置1. 检查串口日志。2. 尝试用手机热点测试。3. 检查路由器是否开启了MAC过滤或AP隔离。WebSocket连接失败PC端IP/端口错误、防火墙阻止、服务器未运行1. 在PC上用netstat -an屏幕白屏或花屏引脚接错、电源不足、初始化顺序问题1. 逐一核对屏幕与ESP32的连接。2. 尝试外接独立电源为ESP32供电。3. 检查TFT_eSPI的引脚配置头文件是否正确。4. 在setup()中增加delay(500)再初始化屏幕。数据更新延迟大Wi-Fi信号差、PC端发送间隔长、ESP32处理阻塞1. 拉近ESP32与路由器的距离。2. 检查PC端探针的发送循环间隔是否设置合理如0.1秒。3. 检查ESP32的loop()中是否有delay()函数阻塞。ESP32频繁重启内存不足、堆栈溢出、看门狗超时1. 检查JSON文档大小是否超出分配的StaticJsonDocument。2. 优化UI绘制减少局部变量。3. 在长时间操作的循环中调用yield()或delay(0)喂看门狗。显示内容错位或乱码屏幕旋转设置错误、字体设置问题、坐标计算错误1. 调整tft.setRotation()的值。2. 确认使用的字体文件是否支持显示的字符。3. 仔细检查绘制函数中的坐标和尺寸参数。PC端探针获取不到GPU数据权限不足、监控库不支持该硬件1. 以管理员身份运行探针程序。2. 尝试使用更专业的库如LibreHardwareMonitor或HWiNFO的SDK。3. 查看库的文档或社区确认你的显卡型号是否被支持。最后的个人体会这个项目最迷人的地方在于它完美结合了软件和硬件将虚拟的系统数据具象化为一个实实在在的桌面物件。从最初的屏幕点亮到第一个数字成功显示再到最终所有数据流畅稳定地更新每一步都充满成就感。我建议你在实现基础功能后不妨尝试一些个性化改造比如为不同的温度区间设置颜色动画添加一个简单的频谱显示模拟或者用3D打印一个酷炫的外壳。这不仅仅是一个监控工具更是你桌面上的一个声明宣告着你是一个热爱技术、乐于创造的玩家。