
1. 为什么我要给桌面塞一块状态屏先说结论我给自己攒了一个叫 Status Deck 的小硬件巴掌大放在显示器下面用 BLE 跟电脑上的桌面端 Agent 通信实时显示构建状态、CI 结果、服务器负载、待办数量这些我写代码时最常瞄一眼的信息。整套东西从固件到桌面端都是自己写的没有用任何现成的成品方案。做这个东西的起因很朴素。我日常写代码的时候注意力经常被各种通知切碎CI 挂了弹一个系统通知服务器告警弹一个IM 消息再弹一个。这些通知混在一起我根本分不清哪个是真正需要立刻处理的。更烦的是每次想看构建状态都得切到浏览器、点开流水线页面、等它加载一来一回两分钟没了。我需要的是一个余光可见的东西——不用切窗口扫一眼就知道现在有没有事。市面上不是没有类似产品但要么是纯软件方案比如在副屏上开个网页要么是成品智能屏前者占屏幕资源后者封闭、改不动、还得联网。我想要的是一个完全可控、离线可用、能随便改显示逻辑的小硬件。ESP32 正好合适自带 BLE、有 WiFi、GPIO 够用、价格便宜、社区资料多到看不完。于是就有了 Status Deck 这个项目。这篇是第一篇主要讲整体设计思路、硬件选型、通信协议怎么定、固件怎么搭骨架。后面会单独写桌面端 Agent 的实现、BLE 抓包调试、以及显示层的优化。如果你也是那种想给自己做个桌面小玩意、又不想被成品方案绑死的开发者这套思路应该能直接抄。2. Status Deck 到底要解决什么问题2.1 从通知轰炸到状态常驻传统的通知模型是事件驱动的有事发生就弹一下弹完就消失。问题是弹的那一下我可能在专注写代码根本没看到等我回过神来通知已经堆了一屏还得一条条翻。而状态类信息比如当前分支 CI 是否通过生产环境是否健康本质上是持续存在的状态不是一次性事件。用通知去表达状态本身就是错配。Status Deck 的核心思路是把状态从事件里剥离出来。桌面端 Agent 负责采集各种状态源Git、CI、监控、待办归一化成几个简单的状态值通过 BLE 推给硬件硬件只负责显示不做任何逻辑判断。这样职责非常清晰Agent 挂了不影响显示显示的是最后一次收到的状态硬件重启也不影响 Agent。2.2 为什么不用 WiFi 而选 BLE这里有个反直觉的选择。ESP32 明明支持 WiFi为什么我最终选了 BLE 作为主通道第一配网成本。WiFi 方案意味着每次换环境都要重新配网SSID、密码、IP 这些事在开发阶段极其烦人。BLE 不需要配网配对一次就行换 WiFi 环境完全无感。第二功耗和发热。桌面设备虽然插着电但 WiFi 持续连接会让 ESP32 明显发热尤其是信号不好的时候。BLE 的连接维持功耗低得多摸上去是凉的。第三安全性。BLE 是点对点短距通信物理上就限定了范围不像 WiFi 那样暴露在局域网里。对于显示构建状态、服务器负载这类信息我不想让它出现在局域网任何一台设备都能扫到的地方。第四延迟。BLE 的连接间隔可以调到很低我用的 15ms状态更新几乎无感。WiFi 方案走 MQTT 或者 WebSocket中间多一层 broker 或者握手反而更重。当然 BLE 也有代价带宽低、传输大数据慢、不同平台的 BLE 栈行为差异大。但 Status Deck 传的就是几个状态值几十字节的事BLE 完全够用。2.3 桌面端 Agent 的角色定位Agent 是整个系统的大脑跑在电脑上用 Python 写后面会讲为什么不用 Node。它做三件事采集定时轮询或者订阅各种状态源。比如每 30 秒查一次 CI 的 API每 5 秒读一次本地 Git 状态订阅监控系统的 webhook。归一化把五花八门的状态统一成有限的几种。我定义的状态类型只有几种ok、warn、error、busy、idle每种对应一个颜色和图标。推送通过 BLE 把归一化后的状态打包发给硬件。只在状态变化时推避免无谓的通信。Agent 和硬件之间是单向为主、双向为辅的关系。状态推送是单向的但硬件可以发心跳和按键事件回来Agent 收到按键可以触发一些动作比如按一下刷新 CI 状态。3. 硬件选型ESP32 型号怎么挑3.1 常见 ESP32 型号对比ESP32 家族型号很多选错了后面会很难受。我把几个主流型号拉出来对比一下型号核心BLEWiFi价格区间适合场景ESP32双核 Xtensa4.2支持低通用资料最多ESP32-S3双核 Xtensa5.0支持中需要 USB、AI 加速ESP32-C3单核 RISC-V5.0支持低成本敏感、单核够用ESP32-C6单核 RISC-V5.0支持中需要新协议支持Status Deck 我最终用的是ESP32-S3。理由有几个BLE 5.0 的连接参数更灵活USB 原生支持方便调试和烧录不用外接 USB-TTL双核可以一个核跑 BLE 协议栈、一个核跑显示刷新互不干扰。如果你只是想先跑通普通 ESP32 也完全够资料还更多。3.2 显示方案的选择显示是 Status Deck 的脸面选型要考虑几个维度刷新率、可视角度、驱动复杂度、功耗。我试过三种方案0.96 寸 OLEDSSD1306I2C 接口驱动简单但太小放桌面上看不清而且单色显示状态不够直观。1.3 寸 IPSST7789SPI 接口彩色可视角度好尺寸合适。这是我最终选的方案。电子墨水屏省电、护眼但刷新慢几百毫秒做状态显示会有明显拖影不适合。ST7789 用 SPI 驱动ESP32-S3 的 SPI 时钟可以跑到 80MHz刷一屏 240x240 的全屏内容也就几毫秒。我用的是 TFT_eSPI 库配置起来比较直接。3.3 供电和外壳供电我一开始想用电池后来放弃了。桌面设备插着 USB 就行电池反而增加充电管理、电量显示这些不必要的复杂度。直接用 USB-C 供电顺便还能烧录和串口调试。外壳我用 3D 打印设计的时候注意两点一是屏幕开孔要留公差二是背面要留散热孔。ESP32-S3 跑 BLE 加 SPI 刷屏的时候会有一定发热密闭外壳里温度会累积。提示如果你也打算 3D 打印外壳屏幕开孔建议比实际尺寸大 0.3-0.5mm不同打印机的收缩率不一样留点余量好装。4. 通信协议BLE 上跑什么4.1 用 GATT 还是自定义协议BLE 通信有两种思路一种是走标准的 GATT 服务定义 Characteristic 来读写另一种是在 BLE 之上再套一层自定义协议。我选的是GATT 自定义 payload。GATT 负责连接管理和通道定义payload 用紧凑的二进制格式。这样既利用了 BLE 成熟的连接机制又能把数据压到最小。具体定义了一个主服务下面挂几个 Characteristic状态推送 CharacteristicAgent 往这里写状态数据硬件 notify 订阅。心跳 Characteristic硬件定期写Agent 读用来判断硬件是否在线。按键事件 Characteristic硬件按键触发时写Agent 订阅。4.2 状态数据的二进制格式状态数据我用固定长度的二进制不用 JSON。原因很简单BLE 的 MTU 有限默认 23 字节协商后可以到 247JSON 的键名太占地方。一个状态包我压到 8 字节[类型 1B][状态码 1B][数值 2B][时间戳 4B]类型表示这是哪个状态源CI、Git、监控等状态码是 ok/warn/error 这些数值可以放百分比或者计数时间戳用来判断数据新鲜度。8 字节的包一个 BLE 包就能装下延迟极低。如果你觉得二进制调试麻烦开发阶段可以先用 JSON跑通之后再换成二进制。我实际就是这么干的先用 JSON 把链路打通确认没问题再优化格式。4.3 连接参数怎么调BLE 的连接参数直接影响延迟和功耗这几个参数要重点调Connection Interval连接间隔。我设的 15ms最小值 7.5ms 的倍数。间隔越小延迟越低但功耗越高。桌面设备插电可以往低了调。Slave Latency从设备延迟。设 0表示每个连接事件从设备都要响应延迟最低。Supervision Timeout监督超时。设 4 秒左右太短容易误判断连太长断连发现慢。这些参数在 ESP32 端通过esp_ble_gap_set_prefer_conn_params设置桌面端也要配合。不同操作系统的 BLE 栈对这些参数的支持程度不一样Windows 上尤其要注意它有时候会强制覆盖你设的参数。5. 固件骨架从零搭一个能跑的框架5.1 开发环境的选择ESP32 的开发环境有好几种Arduino IDE、ESP-IDF、PlatformIO。我最终用的是PlatformIO ESP-IDF 框架。Arduino IDE 上手快但工程管理弱依赖一多就乱而且编译速度慢Windows 上尤其明显。ESP-IDF 是官方框架功能全、性能好但纯命令行配置对新手不友好。PlatformIO 是折中方案用platformio.ini管理依赖和编译配置底层可以选 Arduino 或 ESP-IDF 框架编译速度也比 Arduino IDE 快。我的platformio.ini大致长这样[env:esp32-s3-devkitc-1] platform espressif32 board esp32-s3-devkitc-1 framework espidf monitor_speed 115200 build_flags -DCONFIG_BT_ENABLED1 -DCONFIG_BT_BLE_ENABLED1如果你完全没接触过 ESP32建议先用 Arduino IDE 跑个点灯确认工具链没问题再切到 PlatformIO。离线包的问题在热词里出现频率很高我的建议是第一次装的时候联网装好之后 PlatformIO 会缓存离线也能编译。5.2 任务划分双核怎么用ESP32-S3 是双核合理划分任务能避免 BLE 和显示互相卡。我的划分是Core 0跑 BLE 协议栈和通信任务。BLE 协议栈对实时性要求高不能被显示刷新阻塞。Core 1跑显示刷新和按键扫描。显示刷新是周期性的偶尔慢一点无所谓。任务之间用 FreeRTOS 的队列通信。BLE 收到状态数据后丢进队列显示任务从队列取出来渲染。这样解耦之后即使显示任务卡住BLE 也不会丢包。// 简化的队列通信示意 QueueHandle_t status_queue xQueueCreate(10, sizeof(status_packet_t)); // BLE 回调里 xQueueSend(status_queue, packet, 0); // 显示任务里 status_packet_t pkt; if (xQueueReceive(status_queue, pkt, portMAX_DELAY)) { render_status(pkt); }5.3 BLE 初始化的关键步骤BLE 初始化顺序不能乱乱了会各种报错。我的顺序是初始化 NVS非易失存储BLE 协议栈需要用它存配对信息。释放经典蓝牙内存如果不用经典蓝牙给 BLE 腾空间。初始化控制器和协议栈。注册 GATT 服务。配置广播参数开始广播。每一步的返回值都要检查ESP-IDF 的 API 出错会返回错误码不检查的话后面会莫名其妙崩溃。我踩过的坑是 NVS 没初始化就调 BLE 初始化结果直接重启查了半天才发现是 NVS 的问题。5.4 广播数据怎么设计广播数据是设备被发现时展示的信息包括设备名、服务 UUID 等。这里有个细节广播包只有 31 字节装不下太多东西。设备名我设成StatusDeck服务 UUID 用自定义的 128 位 UUID。如果你想让设备名动态变化比如显示当前状态要注意广播包长度限制。我试过把状态塞进广播包结果名字一长就超了后来还是老老实实走连接后的 GATT 通信。6. 实测踩过的坑和调试技巧6.1 BLE 抓包怎么只看指定设备调试 BLE 最有效的工具是抓包。Wireshark 配合专用的 BLE 抓包硬件比如 Nordic 的 sniffer能看到空口的所有数据。但空口数据量巨大怎么只看自己设备的我的做法是先用手机 App比如 nRF Connect扫一下拿到设备的 MAC 地址然后在 Wireshark 里加过滤条件btle.advertising_address xx:xx:xx:xx:xx:xx连接之后过滤条件换成btle.address xx:xx:xx:xx:xx:xx这样就能只看自己设备的数据流清爽很多。抓包的时候要注意BLE 的加密连接抓到的数据是密文得在 Wireshark 里配置密钥才能解密这个后面单独写。6.2 连接不稳定的排查链路我遇到过连接一会儿就断的问题排查过程记录一下供参考第一步看串口日志。ESP32 端会打印断开原因码比如reason 0x08是超时0x13是对方主动断开。原因码能直接定位方向。第二步看是不是连接参数的问题。如果原因码是超时多半是 supervision timeout 设太短或者连接间隔和从设备延迟不匹配。第三步看是不是电源问题。ESP32 在 BLE 发射瞬间电流会飙到 100mA 以上如果 USB 供电不稳或者线材太差会瞬间掉压导致重启。我换了一根粗一点的 USB 线就好了。第四步看是不是干扰。2.4GHz 频段很拥挤WiFi 路由器、微波炉都会干扰。把设备挪个位置试试。这个排查顺序的逻辑是从软件到硬件从确定到不确定。串口日志是最确定的先看电源和干扰是最不确定的最后试。6.3 烧录方式的坑ESP32 烧录有几种方式USB 直接烧、USB-TTL 烧、OTA 烧。开发阶段用 USB 直接烧最方便但要注意自动下载电路。ESP32-S3 的 USB 是原生的直接插上就能烧不用手动按 BOOT 键。但普通 ESP32 需要自动下载电路两个三极管控制 EN 和 IO0如果电路没做对就得手动按 BOOT 再按 RESET很烦。我第一版板子就是忘了做自动下载电路每次烧录都要按两个键后来重新打板才解决。OTA 烧录适合设备装进外壳之后不用拆开就能更新固件。但 OTA 需要设备先连上 WiFi而且要有 OTA 分区表。Status Deck 主通道是 BLEOTA 我暂时没做更新固件还是拆开插 USB。6.4 显示刷新的性能问题ST7789 用 SPI 驱动如果每次状态变化都全屏刷新会有明显的闪烁。我的优化是局部刷新只重绘变化的区域。具体做法是把屏幕分成几个区域每个区域对应一个状态源。状态变化时只重绘对应的区域。TFT_eSPI 支持设置绘制窗口setAddrWindow只往指定区域写数据。另一个优化是双缓冲。在内存里维护一份完整的帧缓冲所有绘制操作先画到缓冲里画完一次性推给屏幕。这样避免了绘制过程中的中间状态被显示出来。ESP32-S3 的 PSRAM 够大开个 240x240x2 字节的缓冲约 115KB完全没问题。7. 这套架构还能怎么扩展Status Deck 目前只做了状态显示但架构留了不少扩展空间。按键交互硬件上留了两个按键短按刷新、长按切换显示模式。按键事件通过 BLE 回传给 AgentAgent 可以触发任意动作比如按一下重新跑 CI。多设备联动BLE Mesh 可以让多个 Status Deck 组网一个显示 CI、一个显示监控、一个显示待办。ESP32 支持 BLE Mesh但 Mesh 和普通 BLE 连接不能同时跑需要切换模式这个后面研究。边缘计算ESP32-S3 带 AI 加速指令理论上可以在设备端做一些简单的判断比如根据历史数据预测状态趋势。不过目前状态判断都在 Agent 端做设备端保持哑终端更简单可靠。固件安全如果 Status Deck 要显示敏感信息比如生产环境状态固件加密和 BLE 配对加密就很重要。ESP32 支持 Secure Boot 和 Flash 加密BLE 也支持配对绑定。这些我打算在后续版本加上。我个人在实际操作中的体会是做这类小硬件项目最容易犯的错是想太多、做太少。一开始就想着要支持十种状态源、要 Mesh 组网、要 OTA结果一个月过去连点灯都没跑通。正确的做法是先跑通最小闭环——一个状态源、一块屏、一条 BLE 链路——然后再往上加。Status Deck 的第一版就只显示 CI 状态跑通之后加别的状态源就是复制粘贴的事。最后分享一个小技巧调试 BLE 的时候手机上的 nRF Connect 是神器。它能扫描、连接、读写 Characteristic、看日志比在电脑上折腾半天快得多。我基本是先在手机上把 GATT 服务调通确认读写正常再去写桌面端 Agent。这样能把问题范围缩小到是硬件问题还是桌面端问题排查效率高很多。