
1. 项目缘起当桌面需要一个“安静”的性能看板作为一名常年与代码和硬件打交道的开发者我的工作台面上总是堆满了各种设备。主机的性能监控一直是个不大不小的痛点打开任务管理器或第三方软件会占用宝贵的屏幕空间而传统的硬件监控屏要么体积庞大、线材凌乱要么功耗和发热感人在安静的夜晚风扇的嗡嗡声尤其恼人。直到我手头闲置的几块ESP32开发板和一块电子墨水屏E-Ink进入了我的视线。一个想法逐渐成型为什么不利用ESP32的低功耗和无线连接能力配合电子墨水屏近乎零功耗、无闪烁、类纸张的显示特性打造一个专属于桌面的、实时且“安静”的PC性能监控器呢这个“ESP32 PC性能监控器”项目核心目标就是通过Wi-Fi从你的电脑上抓取CPU、内存、GPU等关键性能数据并近乎实时地显示在一块电子墨水屏上。它不依赖主机屏幕独立供电数据刷新时才有极低的功耗完美契合了“信息常显但存在感极低”的桌面美学与实用需求。2. 核心架构与通信原理拆解这个项目的核心逻辑并不复杂可以清晰地分为“数据采集端”、“数据传输桥梁”和“数据显示端”三部分。理解这个架构是后续一切实操的基础。2.1 数据流全景图整个系统的工作流如下数据采集PC端在你的Windows电脑上运行一个轻量级的后台服务或脚本。这个服务的职责是定期例如每秒一次查询操作系统的性能计数器获取我们关心的指标。数据传输网络采集到的数据需要被发送到ESP32。这里我们采用HTTP协议。PC端的服务会启动一个微型的HTTP服务器提供一个特定的API端点例如http://localhost:8080/stats。ESP32则以客户端的身份定期向这个端点发起GET请求。数据处理与显示ESP32端ESP32收到包含JSON格式数据的HTTP响应后对其进行解析提取出各个性能数值。然后它根据预设的布局和图形算法在内存中绘制出新的显示画面最后驱动电子墨水屏更新显示。选择HTTPJSON的方案主要是基于其通用性和易调试性。你几乎可以用任何你熟悉的语言Python、C#、Node.js等来编写PC端的服务而ESP32也有成熟稳定的HTTP客户端库支持。JSON格式的数据结构清晰方便扩展新的监控项。2.2 为什么是ESP32与电子墨水屏ESP32是这个项目的“大脑”它的优势非常明显双核与丰富外设即便在运行Wi-Fi栈和图形处理逻辑时仍有充足的计算能力。内置的SPI、I2C等接口能轻松驱动各类显示屏。极低的待机功耗在深度睡眠模式下电流可低至10μA级别。虽然本项目需要持续联网但其主动工作时的功耗也远低于传统单片机或小型单板电脑。成熟的开发生态无论是官方的ESP-IDF框架还是对开发者更友好的Arduino Core for ESP32都有海量的库和社区支持开发门槛大大降低。电子墨水屏则是“面子”它的特性决定了这个项目的独特体验双稳态显示只有在刷新画面时才需要通电画面保持期间零功耗。这意味着我们的监控器可以7x24小时显示信息而不用担心发热或耗电。无闪烁、不伤眼其原理是反射环境光本身不发光视觉感受接近纸张长时间观看也不会疲劳。强光下可视依靠反射光在阳光或强光直射下反而更清晰与传统LCD屏形成鲜明对比。当然电子墨水屏也有其局限性最主要的就是刷新率低和全屏刷新时有明显的闪烁残影。这对于实时监控来说是一个挑战我们需要在“数据实时性”和“视觉舒适度”之间做出精心的权衡。通常我们会将刷新间隔设置在2-10秒并对显示内容进行局部刷新优化避免频繁的全屏闪烁。3. 硬件选型与电路连接实战硬件是项目的骨架正确的选型和连接是成功的第一步。3.1 硬件清单与选型考量你需要准备以下核心部件ESP32开发板推荐选择带有USB转串口芯片的型号如ESP32 DevKitC V4、NodeMCU-32S等方便烧录和调试。如果追求极致小巧ESP32-S3系列也是不错的选择。电子墨水屏E-Ink Display尺寸2.9英寸、4.2英寸或7.5英寸是桌面摆放的常见选择。尺寸越大能显示的信息越多但价格和功耗刷新时也越高。颜色单色黑/白、三色黑/白/红是最常见的。三色屏可以用红色来高亮显示过载的数值如CPU温度超过阈值视觉效果更直观。驱动IC与接口市场上绝大多数电子墨水屏都通过SPI接口通信。常见的驱动芯片有SSD1680、SSD1675、IL3895等。购买时务必确认其配套的驱动程序库是否完善。推荐型号Waveshare微雪的系列产品文档和库支持非常完善是入门和快速开发的首选。例如一款4.2英寸的三色黑/白/红SPI屏就是很好的起点。连接线与电源杜邦线母对母用于连接ESP32和屏幕。一个5V/1A的USB电源适配器用于给整个系统供电。可选外壳与支架3D打印一个外壳能让项目看起来更完整也能保护电路。3.2 电路连接详解连接的核心是SPI总线。以下是基于一款通用SPI电子墨水屏与ESP32 DevKitC的接法示例电子墨水屏引脚ESP32 GPIO引脚功能说明VCC3.3V电源务必确认屏的电压多数小尺寸屏是3.3V大屏可能是5V。接错会烧毁屏幕GNDGND电源地DIN (MOSI)GPIO 23SPI主设备输出从设备输入数据线CLK (SCK)GPIO 18SPI时钟线CS (片选)GPIO 5片选信号低电平有效DC (数据/命令)GPIO 2用于区分发送的是数据还是命令RST (复位)GPIO 4复位引脚低电平复位BUSY (忙)GPIO 15屏幕忙状态指示高电平表示忙此时不可发送指令重要提示在连接前第一件事就是查阅你的屏幕的具体数据手册确认其工作电压VCC接3.3V还是5V、引脚定义和时序要求。上表是常见接法但并非绝对。连接完成后建议先用厂家提供的示例程序测试屏幕是否能正常显示排除硬件连接问题。4. 软件实现从PC端到ESP32的完整代码解析软件部分分为两端PC端的“数据提供者”和ESP32端的“数据消费者与显示者”。4.1 PC端数据服务以Python为例PC端的任务是提供一个简单的HTTP API。这里用Python的psutil获取系统信息和Flask创建Web服务库来实现非常简洁。# pc_monitor_server.py import psutil import time from flask import Flask, jsonify from flask_cors import CORS # 可选解决跨域问题 app Flask(__name__) CORS(app) # 允许跨域请求 app.route(/stats, methods[GET]) def get_stats(): 获取当前系统性能数据 try: # 1. 获取CPU信息 cpu_percent psutil.cpu_percent(interval0.5) # 阻塞0.5秒获取瞬时利用率 cpu_freq psutil.cpu_freq() cpu_current_ghz round(cpu_freq.current / 1000, 2) if cpu_freq else 0 # 2. 获取内存信息 mem psutil.virtual_memory() mem_percent mem.percent mem_used_gb round(mem.used / (1024**3), 2) mem_total_gb round(mem.total / (1024**3), 2) # 3. 获取磁盘信息默认C盘 disk psutil.disk_usage(C:/) disk_percent disk.percent # 4. 获取网络信息可选发送/接收字节数 net_io psutil.net_io_counters() net_sent_mb round(net_io.bytes_sent / (1024**2), 2) net_recv_mb round(net_io.bytes_recv / (1024**2), 2) # 5. 获取传感器温度需要管理员权限且硬件支持 try: temps psutil.sensors_temperatures() cpu_temp temps.get(coretemp, [{}])[0].current if coretemp in temps else None except: cpu_temp None data { cpu: { percent: cpu_percent, freq_ghz: cpu_current_ghz }, memory: { percent: mem_percent, used_gb: mem_used_gb, total_gb: mem_total_gb }, disk: { percent: disk_percent }, network: { sent_mb: net_sent_mb, recv_mb: net_recv_mb }, temperature: { cpu_c: cpu_temp }, timestamp: int(time.time()) } return jsonify(data) except Exception as e: return jsonify({error: str(e)}), 500 if __name__ __main__: # 在本地所有网络接口的8080端口启动服务 # 注意确保你的防火墙允许此端口的入站连接以便ESP32能访问 app.run(host0.0.0.0, port8080, debugFalse)运行这个脚本后你的PC就成为了一个数据服务器。在浏览器访问http://你的电脑IP:8080/stats就能看到返回的JSON数据。4.2 ESP32端程序基于Arduino框架ESP32端的代码负责定时抓取数据、解析并驱动屏幕显示。这里假设你使用GxEPD2这个强大的电子纸库它支持众多型号的屏幕。// ESP32_PC_Monitor.ino #include WiFi.h #include HTTPClient.h #include ArduinoJson.h #include GxEPD2_BW.h // 根据你的屏幕选择这里是黑白屏 #include GxEPD2_3C.h // 如果是三色屏 #include Fonts/FreeMonoBold9pt7b.h #include Fonts/FreeMonoBold12pt7b.h #include Fonts/FreeMonoBold18pt7b.h // 1. 屏幕定义 - 以4.2英寸黑白屏为例根据你的屏幕型号修改 // GxEPD2_BWGxEPD2_420, GxEPD2_420::HEIGHT display(GxEPD2_420(/*CS*/ 5, /*DC*/ 2, /*RST*/ 4, /*BUSY*/ 15)); // 如果是三色屏如4.2英寸黑/白/红 GxEPD2_3CGxEPD2_420c, GxEPD2_420c::HEIGHT display(GxEPD2_420c(/*CS*/ 5, /*DC*/ 2, /*RST*/ 4, /*BUSY*/ 15)); // 2. 网络配置 const char* ssid 你的Wi-Fi名称; const char* password 你的Wi-Fi密码; const char* serverURL http://192.168.1.100:8080/stats; // 替换为你的PC IP地址 // 3. 全局变量存储数据 float cpuPercent 0; float memPercent 0; float diskPercent 0; float cpuTemp 0; String lastUpdateTime ; // 4. 绘制界面的函数 void drawDisplay() { display.setRotation(1); // 根据你的摆放方向调整旋转角度0,1,2,3 display.setFullWindow(); display.firstPage(); do { display.fillScreen(GxEPD_WHITE); display.setTextColor(GxEPD_BLACK); // 绘制标题 display.setFont(FreeMonoBold12pt7b); display.setCursor(10, 30); display.print(PC Performance); // 绘制分隔线 display.drawFastHLine(10, 40, display.width() - 20, GxEPD_BLACK); // 绘制CPU信息 display.setFont(FreeMonoBold9pt7b); display.setCursor(10, 70); display.print(CPU:); display.setFont(FreeMonoBold18pt7b); display.setCursor(display.width() - 100, 70); display.printf(%2.0f%%, cpuPercent); // 绘制内存信息 display.setFont(FreeMonoBold9pt7b); display.setCursor(10, 110); display.print(MEM:); display.setFont(FreeMonoBold18pt7b); display.setCursor(display.width() - 100, 110); display.printf(%2.0f%%, memPercent); // 绘制磁盘信息 display.setFont(FreeMonoBold9pt7b); display.setCursor(10, 150); display.print(DISK:); display.setFont(FreeMonoBold18pt7b); display.setCursor(display.width() - 100, 150); display.printf(%2.0f%%, diskPercent); // 绘制温度如果有 if (cpuTemp 0) { display.setFont(FreeMonoBold9pt7b); display.setCursor(10, 190); display.print(TEMP:); display.setFont(FreeMonoBold18pt7b); display.setCursor(display.width() - 100, 190); display.printf(%2.0fC, cpuTemp); // 如果温度过高可以用红色显示三色屏 // if(cpuTemp 80) display.setTextColor(GxEPD_RED); } // 绘制更新时间 display.setFont(FreeMonoBold9pt7b); display.setTextColor(GxEPD_BLACK); display.setCursor(10, display.height() - 10); display.print(Updated: ); display.print(lastUpdateTime); } while (display.nextPage()); // 更新完成后屏幕进入深度睡眠以省电如果屏幕支持 // display.hibernate(); } // 5. 从服务器获取数据的函数 bool fetchData() { if (WiFi.status() ! WL_CONNECTED) { Serial.println(WiFi not connected!); return false; } HTTPClient http; http.begin(serverURL); int httpCode http.GET(); if (httpCode HTTP_CODE_OK) { String payload http.getString(); Serial.println(payload); // 解析JSON DynamicJsonDocument doc(1024); DeserializationError error deserializeJson(doc, payload); if (error) { Serial.print(JSON解析失败: ); Serial.println(error.c_str()); http.end(); return false; } // 提取数据 cpuPercent doc[cpu][percent]; memPercent doc[memory][percent]; diskPercent doc[disk][percent]; // 温度可能为null if (!doc[temperature][cpu_c].isNull()) { cpuTemp doc[temperature][cpu_c]; } else { cpuTemp 0; } // 更新时间戳 long ts doc[timestamp]; struct tm *tm_info; time_t rawtime (time_t)ts; tm_info localtime(rawtime); char timeStr[20]; strftime(timeStr, 20, %H:%M:%S, tm_info); lastUpdateTime String(timeStr); http.end(); return true; } else { Serial.printf(HTTP请求失败错误码: %d\n, httpCode); http.end(); return false; } } void setup() { Serial.begin(115200); delay(1000); // 初始化屏幕 display.init(115200, true, 2, false); // 使用SPI速率115200重置2ms延时 display.setFullWindow(); display.fillScreen(GxEPD_WHITE); display.display(true); // 初始全屏刷新清屏 // 连接Wi-Fi WiFi.begin(ssid, password); Serial.print(Connecting to WiFi); while (WiFi.status() ! WL_CONNECTED) { delay(500); Serial.print(.); } Serial.println(\nConnected! IP: WiFi.localIP().toString()); // 首次获取数据并显示 if (fetchData()) { drawDisplay(); } else { // 显示错误信息 display.setCursor(10, 50); display.print(Failed to fetch data); display.display(true); } } void loop() { // 每10秒更新一次可根据需要调整电子墨水屏不宜刷新过频 delay(10000); if (fetchData()) { drawDisplay(); } }这段代码提供了完整的骨架。你需要根据实际屏幕型号修改GxEPD2的初始化并根据你的网络和服务器地址修改配置。5. 关键优化与深度避坑指南把代码跑通只是第一步要让这个监控器稳定、美观、好用还需要处理不少细节。5.1 电子墨水屏的刷新策略优化全屏刷新display.display(true)会有明显的黑闪白闪过程观感很差。我们需要优化局部刷新GxEPD2库支持局部刷新display.display(false)或display.displayPartial(...)。但局部刷新有次数限制通常几十到几百次之后必须进行一次全刷来清除残影。一个稳健的策略是每进行N次局部刷新后强制进行一次全刷。例如可以设置一个计数器每更新20次数据后第21次用全刷。避免频繁刷新实时监控不等于每秒刷新。对于性能数据2-5秒的更新间隔已经足够反映趋势同时能极大延长屏幕寿命和改善观感。在我的实际使用中设置为3秒一次局部刷新每30次后全刷一次体验最佳。深度睡眠一些屏幕驱动芯片支持hibernate()或sleep()命令在刷新完成后调用可以将屏幕功耗降至几乎为零。务必在每次display.display()之后调用。5.2 网络通信的健壮性处理Wi-Fi连接并不总是稳定的代码必须有容错能力。连接超时与重试在setup()和loop()中连接Wi-Fi或请求数据时要设置超时。如果失败不要无限等待而是记录错误进入下一次循环重试。JSON解析容错始终检查DeserializationError。网络传输的JSON字符串可能不完整必须做好解析失败的预案比如保留上一次的有效数据。静态IP或mDNS建议在路由器中为ESP32分配静态IP或者在PC端和ESP32端使用mDNS如http://esp32-pc-monitor.local这样即使路由器DHCP分配的IP变了也能连接。5.3 功耗管理与电源考量虽然ESP32和电子墨水屏本身功耗很低但优化后可以做到用移动电源续航数周。ESP32的睡眠模式在loop()的delay间隔ESP32其实仍在全速运行。可以改用**深度睡眠Deep Sleep**模式。设置一个定时器例如10秒唤醒后连接Wi-Fi、获取数据、刷新屏幕然后立刻再次进入深度睡眠。这需要将GPIO16 (XPD_DCDC)连接到RST引脚来实现定时唤醒。这种方式功耗最低但Wi-Fi重连过程会稍慢。Modem Sleep模式折中方案。CPU保持运行但关闭Wi-Fi射频部分。在需要通信时再激活。这比深度睡眠唤醒快但功耗稍高。电源选择避免使用电脑USB口供电可能会因为电脑睡眠而断电。一个独立的5V/1A手机充电头是最可靠的选择。5.4 显示界面设计与信息密度一块小屏幕如何清晰展示信息是关键。字体与布局选择等宽字体如代码中使用的FreeMono便于对齐。关键数据百分比用大字体标签用小字体。合理利用留白和分隔线。图形化元素除了数字可以绘制简单的进度条或柱状图来直观表示利用率。例如在CPU百分比后面画一个填充的矩形条。多页显示如果信息太多如增加了GPU、每个CPU核心、进程列表等可以设计为多页面通过屏幕触摸或物理按键进行切换。颜色利用如果使用三色屏制定一个颜色规则。例如数值正常时用黑色超过警告阈值如CPU80%用红色网络活动时用红色闪烁等。6. 进阶扩展与创意玩法基础功能实现后这个项目还有巨大的扩展空间。监控更多指标利用psutil可以轻松添加GPU使用率需py3nvml库、单个进程的CPU/内存占用、电池状态笔记本、网络上传下载速度实时曲线等。双向交互为ESP32增加一个物理按钮或电容触摸点。短按切换显示页面长按触发PC端的某个动作比如清理内存、保存当前性能快照等。这需要PC端服务增加接收POST请求的端点。历史趋势图ESP32内存有限但可以存储最近几十个时间点的数据并在屏幕上绘制一个简单的折线图展示CPU利用率在过去几分钟的变化趋势。多主机支持修改PC端服务使其可以配置监控局域网内多台电脑的IP和端口。ESP32轮询这些地址在一个屏幕上分区域显示多个主机的关键状态做成一个简易的“服务器机架监控面板”。美化与外壳使用Fusion 360或Tinkercad设计一个精致的3D打印外壳将屏幕和ESP32板子封装起来背面预留电源线和散热孔瞬间提升项目质感。这个“ESP32 PC性能监控器”项目从想法到实现贯穿了硬件连接、嵌入式编程、网络通信和桌面软件开发的多个环节。它不仅仅是一个桌面小工具更是一个非常好的全栈练手项目。当你看到那块如同便签纸一样贴在显示器旁的小屏幕安静地、实时地反馈着主机的“脉搏”时那种将想法转化为实物的成就感以及它带来的实用性与科技美感正是DIY项目最大的乐趣所在。