尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
ESP32-S3全栈实战:BLE GATT与JSON打造桌面状态显示设备
1. 从一块裸板到能用的桌面摆件Status Deck 到底要做什么Status Deck 这个概念最早是我在折腾桌面信息展示时冒出来的。手头有一块 ESP32-S3 开发板带 1.9 寸 LCD 屏本来只是拿来跑跑点灯和 WiFi 扫描后来觉得太浪费就想把它做成一个能实时显示状态的小摆件——比如当前时间、天气、待办事项、服务器负载甚至电脑上正在播放的音乐。这个系列的第一篇聊了硬件选型和外壳设计这一篇专门讲技术栈怎么定、项目怎么落地。关键词里出现了 ESP32-S3、BLE、GATT、JSON、全栈这几个词基本勾勒出了整个项目的技术轮廓。ESP32-S3 是主控负责驱动屏幕、处理逻辑、联网BLE 和 GATT 用来做近场配置和手机端通信JSON 是数据交换格式不管是设备跟服务端通信还是手机 App 跟设备通信都靠它来承载结构化数据全栈则意味着这个项目不只是写个固件就完事还涉及后端接口、前端配置页面、甚至手机端的调试工具。适合谁来参考这篇内容如果你手上有 ESP32 系列开发板想做一个带联网和蓝牙功能的桌面小设备或者你正在学全栈开发但不想只做 Web 项目想找一个能摸得着的硬件载体那这篇会比较对路。我会把选型理由、踩过的坑、实际跑通的配置都摊开讲代码和配置尽量给到能直接抄的程度。先说清楚这个项目的最终形态一块 ESP32-S3 板子插在桌面上屏幕轮播显示几组信息手机通过 BLE 连上设备后可以发送 JSON 格式的配置指令比如切换显示模式、修改 WiFi 凭据、调整刷新频率设备本身也能主动连 WiFi从自建的后端接口拉取数据。整个链路里JSON 贯穿始终BLE 负责近场配置WiFi 负责远程数据同步。这就是 Status Deck 的核心骨架。2. 主控为什么锁定 ESP32-S3 而不是其他方案2.1 算力、内存和外设的平衡点选主控这件事我前后对比过好几轮。ESP32-S3 的双核 Xtensa LX7 跑在 240MHz自带 512KB SRAM 和 384KB ROM通常模组还会外挂 8MB PSRAM 和 16MB Flash。这个配置放在桌面状态显示这个场景里属于明显过剩但过剩有过剩的好处——你可以同时跑 WiFi 协议栈、BLE 协议栈、LVGL 图形库、JSON 解析还能留出余量做 OTA 升级。对比几个常见选项ESP8266 便宜但只有单核 80MHz跑个 WiFi 加屏幕就喘BLE 更是别想树莓派 Pico W 的 RP2040 没有原生 BLE得外挂模块而且 WiFi 性能一般STM32 系列要自己配 WiFi 和 BLE 模组开发周期拉长。ESP32-S3 把 WiFi 和 BLE 5.0 都集成在片内省掉了外挂模组的麻烦这对个人项目来说太重要了。还有一个容易被忽略的点ESP32-S3 支持 USB OTG 和原生 USB Serial/JTAG烧录和调试不用额外买 USB-TTL 转换器一根 Type-C 线搞定。我在早期用 ESP32 老版本时每次烧录都要手动按 BOOT 和 EN 键S3 上这个问题基本消失了。2.2 BLE 5.0 和 WiFi 共存的实际表现ESP32-S3 支持 BLE 5.0包括 2M PHY 和 Coded PHY理论速率比 BLE 4.2 翻倍。实际用下来在 GATT 通信场景里2M PHY 对吞吐的提升感知不算特别强因为 GATT 本身有 MTU 限制默认 23 字节协商后一般能到 247 字节甚至 512 字节。真正让我满意的是 BLE 和 WiFi 共存时的稳定性。早期 ESP32 在 WiFi 和 BLE 同时工作时容易出现 BLE 连接断流或者 WiFi 延迟飙升。S3 上这个问题改善明显我实测在 WiFi 持续拉取数据的同时BLE 保持连接并接收 JSON 配置指令连续跑 48 小时没有出现断连。当然前提是你在软件层面做好任务优先级分配别把 BLE 回调塞在 WiFi 任务里。注意BLE 和 WiFi 共用 2.4GHz 射频理论上存在干扰。实际项目中建议把 BLE 连接间隔设大一点比如 30ms 到 50ms减少射频切换频率对稳定性有帮助。2.3 开发框架的选择ESP-IDF 还是 Arduino这个问题我被问过很多次。Arduino 框架上手快库多适合快速验证ESP-IDF 更底层配置灵活适合做产品化。Status Deck 这个项目我最终选了 ESP-IDF原因有三个。第一BLE GATT 服务在 ESP-IDF 里的 API 更完整能精细控制广播参数、连接参数、MTU 协商Arduino 的 BLE 库封装太厚遇到问题不好排查。第二JSON 解析我用的是 cJSONESP-IDF 直接集成内存管理更可控。第三项目后期要做 OTA 和分区表定制ESP-IDF 的构建系统更顺手。如果你只是想快速跑个 DemoArduino 完全够用。但如果你打算把这个设备长期放在桌面上用建议直接上 ESP-IDF省得后期迁移。3. BLE GATT 服务设计让手机和设备说同一种语言3.1 服务与特征的划分逻辑BLE 通信的核心是 GATT 层设备对外暴露服务Service服务里包含特征Characteristic手机端通过读写特征来交换数据。Status Deck 里我设计了两个服务一个是配置服务用来接收 JSON 格式的设置指令另一个是状态服务用来上报设备当前状态。配置服务里放了三个特征一个可写特征用来接收配置 JSON一个可读特征用来返回当前配置一个通知特征用来在配置变更后主动推送确认。状态服务里放了一个通知特征定期上报设备运行状态比如内存占用、WiFi 信号强度、当前显示模式。为什么这么分因为配置和状态是两类不同的数据流。配置是低频、小数据量、需要确认的状态是高频、可能较大、允许丢包的。分开之后手机端可以只订阅状态通知不碰配置特征逻辑更清晰。3.2 UUID 的规划与避坑每个服务和特征都需要一个 UUID。标准 UUID 是 128 位但 BLE 允许使用 16 位短 UUID 用于标准服务。自定义服务必须用 128 位 UUID我一般用在线生成器随机生成然后固定下来写进代码。这里有个坑UUID 的字节序。BLE 协议里 UUID 是小端序传输的但很多生成器给你的是大端序字符串。如果你在代码里直接按字符串填手机端可能扫不到服务。我的做法是生成后手动确认字节序或者在代码里用宏定义转换。另一个坑是特征权限。可写特征要设成 WRITE 或 WRITE_NR可读设成 READ通知设成 NOTIFY。如果你设了 NOTIFY 但没设 READ有些手机端库会报错。我一般把通知特征同时设成 READ 和 NOTIFY兼容性更好。3.3 JSON 在 BLE 上的分片传输BLE 的 MTU 默认 23 字节协商后通常能到 247 字节。JSON 配置如果超过 MTU就得手动分片。我的做法是在 JSON 外面包一层简单的协议第一个字节表示总包数第二个字节表示当前包序号后面跟数据。手机端按序号拼接设备端收齐后解析。实测下来一个典型的配置 JSON 大概 200 到 400 字节协商 MTU 到 247 后两包就能传完。如果你不想自己做分片也可以把 JSON 压缩一下去掉空格和换行能省不少字节。// 简化的分片接收逻辑示意 typedef struct { uint8_t total; uint8_t current; uint8_t data[240]; } ble_packet_t; static ble_packet_t rx_buffer[10]; static uint8_t rx_count 0; void on_ble_write(uint8_t *data, uint16_t len) { ble_packet_t *pkt (ble_packet_t *)data; rx_buffer[pkt-current] *pkt; rx_count; if (rx_count pkt-total) { // 拼接并解析 JSON char json_buf[1024]; uint16_t offset 0; for (int i 0; i pkt-total; i) { memcpy(json_buf offset, rx_buffer[i].data, 240); offset 240; } parse_config_json(json_buf); rx_count 0; } }提示分片传输时一定要加超时重置。如果手机端发到一半断开设备端的 rx_count 会卡住下次连接就乱了。我一般设 5 秒超时超时后清空缓冲区。4. JSON 作为数据交换格式的实战细节4.1 为什么选 JSON 而不是二进制或 MessagePackJSON 的可读性是最大优势。调试的时候你直接看串口打印的 JSON 字符串就能知道数据对不对不用对着十六进制猜。而且手机端、后端、设备端都能轻松解析 JSON跨语言兼容性好。二进制协议省空间但调试痛苦MessagePack 比 JSON 紧凑但生态不如 JSON 广。Status Deck 的数据量不大JSON 的体积劣势可以接受。真正需要注意的是解析时的内存分配cJSON 在解析大 JSON 时会频繁 mallocESP32-S3 虽然有 PSRAM但也要小心碎片。4.2 cJSON 在 ESP32 上的内存管理cJSON 的 API 很简单cJSON_Parse 解析cJSON_GetObjectItem 取字段用完 cJSON_Delete 释放。但这里有个常见错误解析失败时返回 NULL如果你不检查就直接取字段程序会崩。我的习惯是封装一层安全解析函数cJSON *safe_parse(const char *json_str) { cJSON *root cJSON_Parse(json_str); if (root NULL) { ESP_LOGE(TAG, JSON parse failed: %s, cJSON_GetErrorPtr()); return NULL; } return root; } void handle_config(const char *json_str) { cJSON *root safe_parse(json_str); if (root NULL) return; cJSON *mode cJSON_GetObjectItem(root, mode); if (cJSON_IsString(mode)) { set_display_mode(mode-valuestring); } cJSON_Delete(root); }另外cJSON 解析后的对象树是动态分配的如果你在 BLE 回调里解析注意回调栈大小。ESP-IDF 默认 BLE 回调栈可能不够我遇到过解析稍大 JSON 就栈溢出重启的情况。解决办法是在 menuconfig 里把 BLE 任务栈调到 4096 以上。4.3 JSON 字段设计的一些经验字段名尽量短但别短到看不懂。比如用 mode 而不是 display_mode用 wifi_ssid 而不是 w。嵌套层级别太深两层足够太深了解析和调试都麻烦。数值类型要注意JSON 里数字都是 doubleESP32 上解析后取整要小心精度。布尔值用 true/false别用 0/1可读性差。数组用来传列表比如待办事项列表但数组长度要限制别让设备端解析时内存爆掉。5. 全栈视角下的项目分层与数据流5.1 设备端、后端、手机端各自负责什么Status Deck 虽然是个硬件项目但它的数据流是典型的前后端架构。设备端负责采集和显示后端负责聚合和转发手机端负责配置和调试。设备端ESP32-S3 跑 FreeRTOS一个任务驱动屏幕刷新一个任务处理 BLE 通信一个任务定时拉取 WiFi 数据。三个任务通过队列通信避免直接共享全局变量。后端我用 Python Flask 写了一个极简接口提供天气、时间、自定义消息三个端点返回 JSON。部署在本地树莓派上设备通过 HTTP GET 拉取。为什么不用云服务因为延迟低而且数据不出局域网心里踏实。手机端初期用 BLE 调试助手手动发 JSON后期用 MIT App Inventor 拼了一个简单 App能扫描设备、连接、发送预设配置。如果你不想做 App用 nRF Connect 这类通用工具也够用。5.2 数据流的完整链路一条典型的配置指令从手机到设备再到后端链路是这样的手机端构造 JSON通过 BLE 写入配置特征设备端收到后解析如果是 WiFi 凭据就保存到 NVS 并尝试连接连接成功后设备端定时向后端发 HTTP 请求后端返回 JSON 数据设备端解析后更新屏幕显示同时通过 BLE 通知特征把状态推给手机。这个链路里JSON 出现了三次BLE 配置、HTTP 响应、BLE 状态通知。格式统一用 JSON解析逻辑可以复用代码量少很多。5.3 错误处理与重试机制全栈项目最怕的就是某一环断了整个系统卡死。我的做法是每一层都加超时和重试。BLE 写入超时 3 秒HTTP 请求超时 5 秒WiFi 连接失败重试 3 次后进入配网模式。配网模式是个兜底方案如果设备连不上 WiFi就自动开启 BLE 广播等待手机发送新的 WiFi 凭据。这样即使你换了路由器设备也不会变砖。6. 项目实施中的踩坑记录与排查思路6.1 BLE 连接后立刻断开的问题这个问题折腾了我一个下午。现象是手机能扫描到设备也能连接但连上后一两秒就断开。排查思路先看串口日志发现 GATT 服务注册成功但连接参数协商失败。查资料发现ESP32-S3 默认的连接参数可能跟某些手机不兼容尤其是 Android 端。解决办法是在 GAP 事件回调里连接建立后主动发起连接参数更新请求把最小连接间隔设成 2430ms最大设成 4050ms从机延迟设成 0超时设成 4004 秒。改完之后连接稳定了。case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT: ESP_LOGI(TAG, Connection params updated, status%d, param-update_conn_params.status); break; // 连接建立后主动请求更新 esp_ble_conn_update_params_t conn_params { .min_int 0x18, // 30ms .max_int 0x28, // 50ms .latency 0, .timeout 400 // 4s }; esp_ble_gap_update_conn_params(conn_params);6.2 JSON 解析导致设备重启前面提过栈溢出的问题这里展开说。现象是手机发送一个稍大的 JSON 配置设备直接重启串口打印 Stack canary watchpoint triggered。这是典型的栈溢出。排查方法在 menuconfig 里把 BLE 任务栈从默认的 3584 调到 4096如果还不够就调到 5120。另外cJSON 解析时如果 JSON 嵌套太深递归调用也会吃栈。我的建议是限制 JSON 嵌套不超过 3 层数组长度不超过 20。还有一个隐蔽的坑cJSON_Parse 在解析失败时会分配内存然后返回 NULL如果你不调用 cJSON_Delete 就泄漏了。虽然单次泄漏不大但 BLE 回调频繁触发累积起来会耗尽堆内存。所以解析失败也要记得释放。6.3 WiFi 和 BLE 同时工作时的射频冲突前面提过共存问题这里给一个实际案例。我最初把 WiFi 的省电模式设成 WIFI_PS_MIN_MODEM结果 BLE 通知延迟明显增大有时候几秒才收到。后来改成 WIFI_PS_NONEBLE 延迟恢复正常但功耗上去了。对于桌面设备来说功耗不是首要考虑所以我最终用了 WIFI_PS_NONE。如果你做电池供电的版本就得在延迟和功耗之间权衡可以试试 WIFI_PS_MAX_MODEM但要做好 BLE 延迟增大的心理准备。6.4 屏幕刷新与 BLE 回调的优先级冲突LVGL 刷新屏幕是耗时操作如果 BLE 回调里直接调用屏幕更新函数会导致 BLE 响应变慢甚至断连。我的做法是 BLE 回调只负责解析 JSON 和更新数据模型屏幕刷新交给独立的 LVGL 任务通过队列传递更新事件。这样解耦之后BLE 回调执行时间控制在毫秒级不会阻塞协议栈。LVGL 任务按自己的节奏刷新即使慢一点用户也感知不到。7. 从能跑到好用几个提升体验的细节7.1 配置持久化与恢复出厂设置设备配置存在 NVS非易失性存储里断电不丢。但有时候配置错了导致设备连不上就需要恢复出厂设置。我的做法是长按板载 BOOT 键 5 秒清除 NVS 并重启设备进入配网模式。NVS 的读写要注意键名长度限制最大 15 个字符。我一般用 wifi_ssid、wifi_pass、disp_mode 这种短键名。写入前先 nvs_open写完 nvs_commit别忘了 nvs_close。7.2 OTA 升级的预留设计虽然第一版没做 OTA但分区表我预留了 OTA 分区。ESP32-S3 的 Flash 是 16MB我分了两个 app 分区各 4MB剩下给文件系统和 NVS。这样后期加 OTA 不用重新分区直接改代码就行。OTA 的触发方式我打算用 BLE 发送一个特殊 JSON 指令设备收到后从指定 URL 下载固件。这样不用拆机接线升级方便。7.3 屏幕显示内容的布局思路1.9 寸 LCD 分辨率 170x320竖屏。我分了三行第一行显示时间和日期第二行显示天气和温度第三行显示自定义消息或待办事项。字体用 LVGL 内置的 montserrat 16 和 24中文显示需要额外加载字库比较占 Flash我暂时只显示英文和数字。刷新频率设成 1 秒一次时间每秒更新天气每 10 分钟拉一次。屏幕背光用 PWM 控制晚上自动调暗避免刺眼。8. 关于技术栈选型的一些个人体会这个项目做下来最大的感受是选型没有绝对的对错只有适不适合。ESP32-S3 不是性能最强的但它在 WiFi、BLE、算力、生态、价格之间找到了一个很好的平衡点。JSON 不是最紧凑的格式但它的可读性和跨平台兼容性让调试效率高了很多。ESP-IDF 不是最易用的框架但它给的掌控感是 Arduino 给不了的。如果你也在做类似的项目我的建议是先把最小闭环跑通一块板子、一个 BLE 服务、一个 JSON 解析、一个屏幕刷新。这四个跑通了剩下的就是堆功能。别一上来就想着做完整产品那样容易卡在细节里出不来。另外串口日志是你的好朋友。ESP32-S3 的 USB Serial/JTAG 可以直接打印日志配合 ESP_LOGI 分级输出排查问题效率翻倍。我习惯在关键路径上都加日志比如 BLE 连接、JSON 解析、WiFi 状态变化出问题时一眼就能定位。最后说一个实际使用中的小技巧BLE 调试时手机端用 nRF Connect 比用自己写的 App 方便得多能直接看服务、特征、读写数据。等协议稳定了再去做 App省得来回改。
RELATED

相关推荐

嵌入式RTOS内存管理实战:malloc/free风险与监控方案

嵌入式RTOS内存管理实战:malloc/free风险与监控方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/11/4 1:06:40
24位ADC与8位MCU打造高精度温度采集方案:PT100/热电偶实测

24位ADC与8位MCU打造高精度温度采集方案:PT100/热电偶实测

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/11/4 1:06:40
STM32 OLED调试面板:从点亮到实时诊断中枢的工程实践

STM32 OLED调试面板:从点亮到实时诊断中枢的工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/11/4 1:06:40
MORE NEWS

更多资讯

📰

深度揭秘:Hermes Agent如何构建企业级多智能体通信架构

深度揭秘:Hermes Agent如何构建企业级多智能体通信架构 在当今分布式AI系统日益复杂的背景下,多智能体协作的效率直接决定了系统的整体性能。Hermes Agent作为一款面向企业级应用的开源多智能体框架,通过创新的消息传递机制和智能协调协议&a…

📰

3个高级配置技巧:解锁Hermes Agent的隐藏潜能

3个高级配置技巧:解锁Hermes Agent的隐藏潜能 你是否曾想过,为什么同样的Hermes Agent在不同开发者手中表现差异如此之大?当其他人轻松处理复杂任务时,你的Agent却频繁遇到上下文限制或响应迟缓的问题。这并非模型能力不足&#…

📰

双态持久化记忆系统:重新定义AI助手的长时记忆范式

双态持久化记忆系统:重新定义AI助手的长时记忆范式 在传统AI助手领域,"健忘症"一直是制约用户体验的核心瓶颈。每个会话都是孤立的,AI无法记住用户的偏好、工作习惯或历史约定,导致重复的配置和低效的交互。Hermes Age…

📰

Hermes Agent:你的AI协作伙伴,如何让复杂任务变得简单?

Hermes Agent:你的AI协作伙伴,如何让复杂任务变得简单? 你是否曾为管理多个AI助手而烦恼?想象一下,你需要同时处理Telegram上的客户咨询、Discord的技术讨论,还要协调团队看板上的开发任务。每个平台都需要…

📰

FastAPI API文档定制指南:4 步做出带品牌色的 Swagger 与 ReDoc 界面

FastAPI API文档定制指南:4 步做出带品牌色的 Swagger 与 ReDoc 界面 【免费下载链接】fastapi FastAPI framework, high performance, easy to learn, fast to code, ready for production 项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi 做 Fa…

📰

如何快速构建智能交通流量预测系统:Hermes Agent机器学习模型应用指南

如何快速构建智能交通流量预测系统:Hermes Agent机器学习模型应用指南 智能交通流量预测系统是现代城市管理的重要组成部分,而Hermes Agent作为一款强大的AI代理框架,提供了丰富的机器学习模型应用能力,帮助开发者快速实现这一目…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬