
1. 项目缘起为什么需要一块独立的硬件性能监视器作为一名常年与电脑打交道的开发者我发现自己经常陷入一个尴尬的境地为了监控CPU温度、内存占用或者风扇转速我需要频繁地切换窗口或者让任务管理器、HWInfo这类软件常驻在屏幕一角。这不仅占用了宝贵的屏幕空间在运行全屏游戏或进行视频渲染等需要专注的任务时更是完全无法查看。我需要的是一个物理的、独立的、可以随时瞥一眼就能获取关键信息的“仪表盘”。这就像开车时看仪表盘而不是在中控屏上点开一个应用。于是我盯上了手边闲置的ESP32开发板。这块集成了Wi-Fi和蓝牙的廉价微控制器拥有足够的处理能力和丰富的接口正是制作一个网络化硬件监视器的绝佳平台。通过编写一个运行在PC上的轻量级服务端程序采集系统性能数据再经由局域网发送给ESP32ESP32解析数据并驱动一块显示屏进行实时显示——一个“ESP32 PC性能监视器”的构想便清晰起来。它的核心价值在于物理外设的专注显示和低成本的硬件实现让你无需昂贵的副屏就能获得沉浸式工作或娱乐时最关键的系统状态反馈。2. 核心架构设计数据如何从PC“流”到ESP32屏幕整个项目的架构可以清晰地分为三个部分PC端的数据采集与发送、网络通信协议、以及ESP32端的数据接收与显示。理解这个数据流是成功构建项目的关键。2.1 PC端轻量级数据采集服务在PC端我们需要一个常驻后台的“探针”。它的任务是以固定的频率例如每秒1次采集我们关心的系统指标并将其打包发送出去。选择哪种技术来实现这个服务是第一个需要权衡的点。方案选型Python脚本 vs. 编译型后台服务我最初尝试了Python利用psutil库可以非常方便地获取CPU、内存、磁盘、网络等信息几行代码就能搞定。它的优势是开发速度快原型验证极其方便。但作为长期运行的后台服务Python脚本需要依赖解释器环境并且通常以一个控制台窗口的形式运行不够“优雅”资源占用也相对较高。因此对于追求稳定和低开销的最终方案我更倾向于使用Go或C#等编译型语言编写一个真正的Windows服务或后台进程。以Go为例它可以编译成单个可执行文件无需额外运行时并且能很好地以服务形式安装和运行。这里我选择Go作为实现语言主要基于其出色的并发模型、简洁的语法和高效的网络库。关键数据采集点CPU使用率全局使用率以及每个逻辑核心的使用率。内存使用情况已用内存、可用内存、使用百分比。GPU状态对于游戏玩家和创作者GPU的温度、使用率、显存占用至关重要。这通常需要调用厂商SDK如NVIDIA NVAPI或使用像OpenHardwareMonitorLib这样的开源库。温度与风扇CPU封装温度、核心温度、以及主要风扇的转速。这依赖于LibreHardwareMonitor或OpenHardwareMonitor的WMI接口。网络吞吐量当前上行和下行的速度KB/s或MB/s。磁盘活动系统盘或其他指定磁盘的读写速度、使用率。注意采集GPU、温度等硬件传感器数据在Windows上权限要求较高。以管理员身份运行服务是常见做法但在代码中应妥善处理权限不足的异常并向用户给出清晰提示。2.2 通信协议JSON over TCP/UDP or WebSocket数据采集好后需要通过网络发送给ESP32。这里有几个协议选项HTTP JSON最通用、易调试。ESP32可以使用HTTP Client库定期如每秒向PC服务发起GET请求PC端返回JSON格式的数据。优点是实现简单利用现成的库即可。缺点是每次请求都有HTTP头开销且是“拉”模式实时性略差。WebSocket全双工通信协议。建立连接后PC端可以主动向ESP32“推”送数据延迟极低非常适合实时监控。ESP32的Arduino框架有WebSocket客户端库支持。这是实时性要求高时的首选。原始TCP/UDP 自定义格式最轻量级。可以定义非常紧凑的二进制数据包格式传输效率最高。但需要自行处理数据包的组包、拆包和校验复杂度较高。考虑到易用性和足够的实时性我选择了WebSocket作为通信协议。PC端的Go服务内置了一个轻量的WebSocket服务器ESP32作为客户端连接上来。一旦连接建立PC端会以固定的时间间隔如500ms将采集到的数据序列化为JSON字符串通过WebSocket连接主动推送给ESP32。JSON数据包示例{ timestamp: 1697012345678, cpu: { total: 45.3, cores: [12.1, 88.5, 30.2, 10.7, 55.5, 33.3, 20.1, 90.0] }, memory: { used_mb: 8192, total_mb: 16384, percent: 50.0 }, gpu: { name: NVIDIA GeForce RTX 4070, load: 78.5, temp: 72, mem_used_mb: 5120, mem_total_mb: 12288 }, network: { up_kbps: 120, down_kbps: 4500 } }2.3 ESP32端数据接收、解析与显示ESP32端的任务是连接WebSocket服务器接收JSON数据解析后驱动显示屏进行渲染。开发框架选择Arduino vs. ESP-IDF对于此类嵌入式GUI项目Arduino框架因其丰富的库生态和相对简单的开发流程而更具优势。特别是对于显示屏驱动和GUI库Arduino社区的支持非常活跃。因此我选择了Arduino作为ESP32的开发环境。核心库依赖Wi-FiWiFi.h- 用于连接家庭局域网。WebSocketWebSocketClient.h(例如arduinoWebSockets库) - 用于与PC服务通信。JSON解析ArduinoJson.h- 解析来自PC的JSON数据包这是必备库能极大简化数据处理。显示驱动与GUI这部分选择最多也最关键。取决于你使用的屏幕类型SPI TFT屏如ILI9341常用库是TFT_eSPI。它功能强大但需要仔细配置引脚。I2C OLED屏如SSD1306常用库是Adafruit_SSD1306和Adafruit_GFX。适合显示简约信息。LVGL一个强大的开源嵌入式图形库。如果屏幕性能足够如带RAM的彩色屏并且希望UI效果更炫酷、可交互LVGL是终极选择。但学习曲线较陡。考虑到性能监视器需要同时显示多项数据且希望有进度条、图表等元素我选择了一块240x320的SPI接口IPS彩屏并搭配TFT_eSPI库。它在ESP32上性能不错且能较好地绘制图形元素。3. 硬件选型与电路连接搭建你的硬件仪表盘硬件是项目的基石。正确的选型和连接能避免很多后续的调试麻烦。3.1 核心部件清单ESP32开发板任何一款ESP32如ESP32-WROOM-32或ESP32-S3均可。注意选择引脚引出较多的型号方便连接屏幕和其他传感器。显示屏推荐使用SPI接口的TFT彩屏尺寸在2.4寸到3.5寸之间。我使用的是240x320分辨率的ILI9341驱动芯片的屏幕。购买时务必确认其接口SPI或并行8080以及是否需要触摸功能。连接线杜邦线母对母、公对母若干。如果屏幕不带排针可能需要焊接。电源ESP32和屏幕可以通过USB线供电5V也可以使用一个5V/1A以上的USB电源适配器单独供电。如果屏幕背光功耗较大建议使用外部电源避免USB口供电不足。3.2 电路连接详解以ESP32 DevKit V1与ILI9341 SPI屏为例SPI屏通常有多个引脚需要正确连接到ESP32的SPI硬件引脚上以获得最佳性能。以下是典型的连接方式ESP32 引脚 (GPIO)ILI9341 屏幕引脚功能说明GPIO 23 (MOSI)SDI (MOSI)SPI主设备输出从设备输入。传输屏幕数据。GPIO 18 (SCK)SCKSPI时钟信号。GPIO 5CS (Chip Select)片选信号低电平有效。告诉屏幕开始接收数据。GPIO 17DC (Data/Command)数据/命令选择引脚。高电平为数据低电平为命令。GPIO 16RESET复位引脚用于硬件复位屏幕。3.3VVCC电源正极3.3V。切勿接5VGNDGND电源地。重要提示电源电压绝大多数ESP32兼容的SPI TFT屏工作电压是3.3V请务必接到ESP32的3.3V输出引脚接到5V可能会烧毁屏幕。背光控制屏幕的LED或BL引脚是背光控制。可以接一个GPIO通过PWM调节亮度也可以直接接3.3V常亮。MISO引脚本例中未使用MISO因为我们是单向向屏幕写数据。如果你的屏幕支持读操作如读取显存则需要连接。引脚灵活性除了MOSI(23)和SCK(18)这两个硬件SPI引脚不建议更改外CS、DC、RESET引脚可以定义为其他任何空闲的GPIO只需在代码中相应修改即可。连接完成后建议先用厂家或库提供的示例程序测试屏幕是否能正常点亮和显示图形确保硬件连接无误。4. 软件实现全流程从零编写代码硬件就绪后我们来深入软件部分的实现。我将分PC端Go服务和ESP32端Arduino固件详细说明。4.1 PC端服务程序Go实现要点首先我们需要创建一个Go项目并引入必要的依赖。使用go mod init初始化项目后主要依赖库包括github.com/gorilla/websocket用于创建WebSocket服务器。github.com/shirou/gopsutil/v3跨平台的系统信息采集库能获取CPU、内存、磁盘、网络等基础信息。可选github.com/lxn/win或调用OpenHardwareMonitor的COM接口用于获取GPU、温度等高级传感器数据。这里我们以gopsutil为基础高级传感器部分需要额外处理。主程序结构package main import ( // ... 导入包 ) // 定义全局数据结构用于存放采集到的性能数据 type SystemStats struct { Timestamp int64 json:timestamp CPU CPUInfo json:cpu Memory MemInfo json:memory // ... 其他字段 } func main() { // 1. 初始化WebSocket服务器 http.HandleFunc(/ws, wsHandler) go func() { log.Fatal(http.ListenAndServe(:8080, nil)) // 监听8080端口 }() // 2. 主循环定时采集数据并广播给所有连接的客户端 ticker : time.NewTicker(500 * time.Millisecond) // 500ms采集一次 defer ticker.Stop() for range ticker.C { stats : collectSystemStats() // 采集数据 data, _ : json.Marshal(stats) broadcastToClients(data) // 发送给所有已连接的ESP32 } } // collectSystemStats 使用gopsutil采集数据 func collectSystemStats() SystemStats { var stats SystemStats stats.Timestamp time.Now().UnixMilli() // 采集CPU percents, _ : cpu.Percent(0, true) // 获取每个核心的百分比 stats.CPU.Cores percents // ... 采集内存、网络等 // 难点GPU和温度数据 // 方案AWindows调用OpenHardwareMonitor的WMI接口。需要先确保OHM在运行并开启WMI。 // 方案B使用NVAPI仅NVIDIA或ADL仅AMD的Go绑定库但复杂度高。 // 简易方案在代码中注释说明并提供一个模拟数据模式用于测试。 stats.GPU.Load getGPULoadFromOHM() // 需要自己实现这个函数 return stats } // wsHandler 处理WebSocket连接 func wsHandler(w http.ResponseWriter, r *http.Request) { conn, _ : websocket.Upgrade(w, r, nil, 1024, 1024) // 将conn加入全局客户端列表 defer // 连接断开时从列表中移除 for { // 可在此处处理ESP32发来的任何控制消息如请求改变刷新率 _, _, err : conn.ReadMessage() if err ! nil { break } } }实操心得获取GPU和硬件传感器数据是PC端最大的难点。一个稳定可靠的方法是将OpenHardwareMonitor作为Windows服务运行并启用其WMI接口。然后我们的Go程序通过github.com/StackExchange/wmi包来查询WMI获取Sensor类的数据。这需要你对WMI查询语法有一定了解。在开发初期可以先用模拟数据替代确保通信链路和ESP32显示功能正常。4.2 ESP32固件Arduino实现详解ESP32端的代码逻辑相对线性。我们使用PlatformIO或Arduino IDE进行开发。第一步库管理与配置在platformio.ini或Arduino库管理中安装以下库TFT_eSPIby BodmerArduinoJsonby Benoit BlanchonWebSocketClient(如arduinoWebSocketsby Links2004)对于TFT_eSPI库最关键的一步是配置。你需要找到Arduino库安装目录下的TFT_eSPI文件夹编辑其中的User_Setup.h或User_Setup_Select.h文件。注释掉所有默认的驱动定义。找到针对你的屏幕驱动如ILI9341_DRIVER的代码行并取消注释。精确配置引脚根据你实际的硬件连接修改下面这些宏定义#define TFT_CS 5 // Chip select control pin #define TFT_DC 17 // Data Command control pin #define TFT_RST 16 // Reset pin #define TFT_MOSI 23 // Data out #define TFT_SCLK 18 // Clock out // 如果你的屏幕有MISO和LED引脚也一并配置 // #define TFT_MISO 19 // 未使用可省略 // #define TFT_LED 4 // 背光控制如果接在GPIO4上设置正确的屏幕尺寸#define TFT_WIDTH 240#define TFT_HEIGHT 320。第二步主程序逻辑框架#include WiFi.h #include WebSocketClient.h #include ArduinoJson.h #include TFT_eSPI.h TFT_eSPI tft TFT_eSPI(); WebSocketClient webSocketClient; WiFiClient client; const char* ssid 你的Wi-Fi名称; const char* password 你的Wi-Fi密码; const char* ws_host 你的PC的IP地址; // 如 192.168.1.100 const int ws_port 8080; const char* ws_path /ws; // 定义用于存储性能数据的结构体 SystemStats currentStats; void setup() { Serial.begin(115200); tft.init(); tft.setRotation(1); // 根据屏幕方向调整0-3 tft.fillScreen(TFT_BLACK); tft.setTextColor(TFT_WHITE, TFT_BLACK); connectToWiFi(); connectToWebSocket(); } void loop() { if (!client.connected()) { reconnectWebSocket(); } // 接收WebSocket消息 String message; if (webSocketClient.getMessage(message)) { if (message.length() 0) { parseAndUpdateStats(message); // 解析JSON更新currentStats updateDisplay(); // 刷新屏幕显示 } } delay(10); // 短暂延迟避免忙等 } void parseAndUpdateStats(String jsonStr) { StaticJsonDocument1024 doc; // 根据JSON大小调整缓冲区 DeserializationError error deserializeJson(doc, jsonStr); if (error) { Serial.print(JSON解析失败: ); Serial.println(error.c_str()); return; } currentStats.cpuTotal doc[cpu][total]; // ... 解析其他字段存入currentStats结构体 } void updateDisplay() { tft.fillScreen(TFT_BLACK); // 清屏或局部刷新以优化性能 // 1. 绘制CPU使用率进度条形式 drawProgressBar(10, 10, 200, 15, currentStats.cpuTotal, TFT_CYAN, TFT_DARKGREY, CPU); // 2. 绘制内存使用率 drawProgressBar(10, 40, 200, 15, currentStats.memPercent, TFT_GREEN, TFT_DARKGREY, MEM); // 3. 绘制GPU温度数字颜色提示 int gpuTemp currentStats.gpuTemp; tft.setCursor(10, 70); tft.print(GPU Temp: ); tft.setTextColor(getTempColor(gpuTemp), TFT_BLACK); tft.print(gpuTemp); tft.println( C); // 4. 绘制网络速度动态变化的数字 tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.setCursor(10, 100); tft.printf(Net: ↑%d ↓%d KB/s, currentStats.netUp, currentStats.netDown); // 5. 进阶可以绘制每个CPU核心的小进度条 int yPos 130; for (int i 0; i min(8, currentStats.cpuCoresCount); i) { // 假设只显示前8个核心 drawProgressBar(10, yPos, 100, 8, currentStats.cpuCores[i], TFT_YELLOW, TFT_DARKGREY, ); yPos 12; } } // 辅助函数绘制一个带标签的进度条 void drawProgressBar(int x, int y, int w, int h, float percentage, uint16_t color, uint16_t bgColor, const char* label) { // ... 实现进度条绘制逻辑包括背景框、填充矩形和文本标签 } // 辅助函数根据温度返回颜色 uint16_t getTempColor(int temp) { if (temp 60) return TFT_GREEN; else if (temp 80) return TFT_YELLOW; else return TFT_RED; }避坑指南ArduinoJson库的缓冲区大小StaticJsonDocument1024需要仔细估算。如果JSON数据很大例如包含所有CPU核心数据缓冲区太小会导致解析失败。一个技巧是先在串口监视器打印出收到的原始JSON字符串测量其长度然后设置一个稍大的缓冲区如2048或4096。解析失败是ESP32端最常见的故障点。5. 优化、调试与进阶玩法项目基本跑通后我们可以从稳定性、美观度和功能上进行优化。5.1 性能与稳定性优化ESP32端显示优化避免全屏刷新tft.fillScreen(TFT_BLACK)会清空整个屏幕导致闪烁。可以改为只更新数字和进度条变化的区域局部刷新但这需要更精细的绘图控制。双缓冲如果库支持一些高级的库或LVGL支持双缓冲可以在后台缓冲区绘制完整帧然后一次性交换到前台消除撕裂和闪烁。降低刷新率对于性能监控1-2Hz的更新率已经足够流畅。可以将PC端的推送间隔改为1000ms减轻ESP32和网络的负担。通信可靠性心跳机制在WebSocket连接上实现一个简单的心跳包Ping/Pong用于检测连接是否存活。如果断开ESP32应尝试重连。数据校验在JSON解析后检查关键字段是否存在或合理如CPU使用率是否在0-100之间避免解析错误数据导致显示异常。Wi-Fi重连在ESP32的loop()中定期检查Wi-Fi连接状态如果断开则尝试重新连接。5.2 UI设计与用户体验布局与配色设计一个信息层级清晰的布局。最重要的信息如CPU/GPU温度、使用率放在显眼位置用大字体或醒目颜色。使用一致的配色方案例如用绿色表示安全黄色表示警告红色表示危险高温、高负载。图形化元素进度条比单纯的数字更直观。迷你折线图可以绘制最近一段时间如30秒内CPU使用率或温度的变化趋势。这需要ESP32端维护一个小型的历史数据队列。图标为CPU、内存、GPU、网络等元素绘制简单的图标提升美观度。TFT_eSPI库支持绘制BMP位图可以先将图标转换成C语言数组嵌入代码中。交互功能进阶按钮切换页面增加一个物理按钮连接到ESP32的某个GPIO通过中断检测按键切换显示不同的信息页面如概览页、详细监控页、网络统计页。屏幕旋转/亮度调节通过长按、双击等手势如果屏幕带触摸或额外的按钮来实现。5.3 常见问题排查踩坑记录屏幕一片空白检查电源和背光确认VCC接3.3VGND已接背光引脚LED已上电。检查引脚定义这是最常见的问题。反复核对User_Setup.h中的引脚定义是否与实物连接完全一致。特别是TFT_DC和TFT_CS引脚。检查库和驱动确认TFT_eSPI库已正确安装并选择了正确的屏幕驱动宏。WebSocket连接失败检查IP和端口确保ESP32代码中的ws_host是PC的正确局域网IP且PC防火墙允许了8080端口的入站连接开发期间可暂时关闭防火墙测试。检查PC端服务是否运行在PC浏览器访问http://[你的PC IP]:8080如果WebSocket服务器运行正常可能不会返回网页但连接应被处理。更可靠的方法是在PC端用netstat -an | findstr 8080命令查看端口是否在监听。查看串口日志ESP32连接Wi-Fi和WebSocket的每一步都通过Serial.print()输出到串口监视器这是最重要的调试手段。数据不更新或显示异常检查JSON解析在parseAndUpdateStats函数中将解析前的jsonStr和解析错误信息打印到串口确认数据格式正确。检查数据采集在PC端服务中将准备发送的JSON字符串打印到控制台确认数据本身是正确且更新的。缓冲区溢出增大ArduinoJson的文档缓冲区大小。这个项目从构思到实现是一个典型的软硬件结合实践。它不仅仅是一个酷炫的桌面小配件更是一个深入了解系统监控、网络通信、嵌入式GUI和跨平台编程的绝佳机会。你可以根据自己的需求不断添加新的监控项如硬盘剩余空间、进程列表或者尝试更复杂的UI框架如LVGL甚至将多个ESP32屏幕组成一个分布式监控网络。动手去实现它过程中遇到的每一个问题和解法都会让你对技术的理解更深一层。