恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
ESP32-S3蓝牙LE门磁监测:GATT Server实现与低功耗设计
首页
资讯中心
/
ESP32-S3蓝牙LE门磁监测:GATT Server实现与低功耗设计
ESP32-S3蓝牙LE门磁监测:GATT Server实现与低功耗设计
发布时间:2026/10/7 2:39:05
1. 项目缘起与整体设计思路1.1 为什么选择ESP32-S3来做门磁监测门磁这东西说白了就是一个簧片开关加磁铁装门窗上开合状态就是通断两态。传统做法是拉一根线接到报警主机或者用433MHz无线模块发个简单编码。但这类方案有个通病只能告诉你“开了”还是“关了”没有上下文没有历史更没法远程查状态。我住的地方是个老小区楼道门有时候被风一吹就虚掩着人走了门没关严家里有猫有狗的就特别操心。所以我想做一个能实时上报门磁状态、还能通过手机蓝牙直接查看的小节点。选ESP32-S3不是因为它性能多强而是几个很实际的原因。第一它原生支持Bluetooth LE而且协议栈成熟Arduino核心和ESP-IDF两条路都能走不用外挂模块。第二S3的GPIO资源够用我后面还想加个温湿度传感器做环境联动引脚不能太紧张。第三它支持外部中断唤醒门磁这种事件驱动的场景用中断比轮询省电得多。第四S3的USB-OTG可以直接做串口调试不用额外买FTDI。这几个点凑在一起ESP32-S3就成了最顺手的选择。注意ESP32-S3的GPIO编号和经典ESP32不一样尤其是Strapping引脚GPIO0、GPIO3、GPIO45、GPIO46在启动时有特殊电平要求接门磁的时候尽量避开这些脚否则上电可能进不了正常启动模式。1.2 蓝牙LE方案的核心取舍蓝牙LE在这个项目里扮演的是“本地直连通道”的角色。我没有走WiFiMQTT那条路原因很直接门磁节点装在门框附近那里WiFi信号往往很差而且WiFi功耗高电池撑不了多久。蓝牙LE的广播和连接功耗低得多手机靠近就能读状态不需要经过路由器、云平台、App服务器这一长串链路。具体到蓝牙LE的实现方式有两种主流选择一种是让ESP32-S3做GATT Server手机作为Client去连接并读取特征值另一种是让ESP32-S3做Broadcaster把门磁状态放在广播数据里手机被动扫描就能看到。我最终选了GATT Server方案理由是广播数据包长度有限传统广播最多31字节而且广播里放状态变化不够灵活手机端还得持续扫描反而费电。GATT Server虽然需要建立连接但连接后可以读、可以订阅通知状态变化能主动推给手机体验更接近“实时”。这里有个细节值得展开门磁状态变化是异步事件如果手机没连着状态就丢了。所以我在固件里加了一个“状态保持”逻辑——门磁当前状态存在一个变量里手机连上来先读一次然后订阅通知。这样即使手机来晚了也能知道当前是开还是关。至于历史记录那需要额外存储这个项目里我先不做保持简单。1.3 系统架构与数据流整个系统的数据流很清晰门磁开关接在GPIO上配置为输入上拉或下拉取决于接线方式。门磁闭合时GPIO被拉到某个电平断开时被拉到另一个电平。我用外部中断捕获边沿变化中断服务程序里只做一件事设置一个标志位。主循环检测到标志位后读取当前电平更新状态变量然后通过蓝牙LE的GATT特征值通知已连接的客户端。这里有个设计决策中断里不做蓝牙操作。因为蓝牙协议栈的API不是中断安全的在ISR里调用可能导致崩溃或不可预期行为。所以中断只负责“记下有事发生”实际处理放在主循环里。这个模式在嵌入式开发里很常见叫“中断打标、主循环处理”能避免很多竞态问题。手机端我用的是通用的蓝牙调试App比如nRF Connect或者LightBlue不需要自己写App。这些App能扫描、连接、读特征值、订阅通知足够验证功能。如果你要自己做AppAndroid用BluetoothGattiOS用CoreBluetooth那是另一个话题了。2. 硬件准备与接线细节2.1 物料清单与选型建议这个项目需要的硬件不多但每一样都有讲究。ESP32-S3开发板我手头用的是带USB-C接口的版本Flash至少4MBPSRAM有没有都行这个项目用不到。门磁传感器就是最常见的常闭型簧片开关几块钱一个注意买带引线的方便接杜邦线。磁铁部分一般和开关一起卖安装时磁铁和开关的距离要小于触发距离通常1到2厘米。物料规格数量备注ESP32-S3开发板任意带USB-C的型号1确认GPIO引出门磁开关常闭型带引线1也可用常开型代码需调整杜邦线母对母2连接门磁和开发板面包板半尺寸1可选直接插线也行USB数据线USB-C1要能传数据不是纯充电线门磁开关有常闭和常开两种。常闭的意思是门关上时开关闭合门打开时开关断开。常开则相反。我选常闭是因为它在门关着的时候形成回路如果线被剪断或者接触不良读到的就是“开”相当于故障安全。当然你也可以用常开代码里把逻辑反过来就行。提示买门磁的时候注意看触发距离有些便宜的只有几毫米安装时稍微偏一点就触发不了。我踩过这个坑后来换了触发距离1厘米以上的型号安装容错率高很多。2.2 接线方式与GPIO选择接线本身很简单门磁开关一端接GND另一端接GPIO。GPIO配置为输入启用内部上拉。这样门磁闭合时GPIO被拉到GND读到低电平门磁断开时内部上拉把GPIO拉到高电平读到高电平。这种接法叫“低电平有效”好处是抗干扰能力比高电平有效强一些因为GND是低阻抗的。GPIO我选的是GPIO4。为什么选这个首先它不是Strapping引脚上电时没有特殊要求。其次它支持外部中断ESP32-S3几乎所有GPIO都支持中断但有些引脚在深度睡眠唤醒时有特殊功能GPIO4不在其列。第三它离GND引脚比较近接线方便。如果你用别的引脚只要避开GPIO0、GPIO3、GPIO45、GPIO46这几个Strapping脚以及GPIO19、GPIO20USB D/D-基本都没问题。接线示意门磁开关一端 → ESP32-S3的GND门磁开关另一端 → ESP32-S3的GPIO4就这么两根线。如果你要加外部上拉电阻可以在GPIO4和3.3V之间接一个10kΩ电阻但ESP32-S3内部上拉已经够用一般不需要外接。2.3 供电与功耗的初步考虑这个项目我暂时用USB供电因为还在调试阶段。但如果要电池供电有几个点要注意。ESP32-S3在蓝牙LE连接状态下的平均电流大概在几十毫安级别具体取决于连接间隔和发射功率。如果门磁大部分时间处于断开状态门关着可以让ESP32-S3进入Light Sleep靠GPIO中断唤醒。Light Sleep下电流可以降到几百微安但蓝牙连接会断开需要重新广播。我的建议是如果只是验证功能USB供电完全够用。如果要电池供电先测一下实际电流再决定用多大电池。一块1000mAh的锂电池如果平均电流10mA理论上能撑100小时但实际要考虑电压转换损耗和电池自放电打个七折比较稳妥。3. 软件开发环境搭建3.1 Arduino IDE还是ESP-IDF这个问题我被问过很多次。Arduino IDE上手快库多社区大适合快速原型。ESP-IDF更底层配置灵活功耗控制精细但学习曲线陡。这个项目我选Arduino IDE原因很简单蓝牙LE的GATT Server在Arduino核心里有现成的库几行代码就能跑起来。ESP-IDF当然也能做但配置GATT服务需要写更多的初始化代码对于门磁这种简单应用没必要。Arduino IDE的版本我用的2.3.xESP32核心用的是2.0.11。为什么不用最新的3.x因为3.x的蓝牙库API有变动而且有些第三方库还没跟上。2.0.11是经过大量项目验证的稳定版本蓝牙LE的API和示例都很成熟。如果你已经装了3.x也可以但要注意BLEDevice、BLEServer这些类的用法可能有细微差别。注意Arduino IDE 2.x在Windows上编译ESP32项目时第一次编译会比较慢因为要编译整个核心库。之后增量编译就快了。如果遇到“一直等待”的情况检查一下开发板管理器里的ESP32包是否下载完整有时候网络问题会导致包损坏。3.2 ESP32核心安装与版本选择安装ESP32核心的步骤打开Arduino IDE文件 → 首选项 → 附加开发板管理器网址填入ESP32的包地址。然后工具 → 开发板 → 开发板管理器搜索“esp32”选择版本2.0.11安装。安装完成后在开发板列表里选“ESP32S3 Dev Module”。这里有个坑ESP32-S3的Flash模式和分区方案要根据你的开发板来选。大多数S3开发板用的是QIO 80MHz Flash分区方案选“Default 4MB with spiffs”就行。如果你的板子有8MB Flash可以选更大的分区。USB CDC On Boot要选“Enabled”这样串口监视器才能通过USB看到输出。如果选“Disabled”串口就没输出你会以为程序没跑起来。配置项推荐值说明BoardESP32S3 Dev Module通用S3开发板Flash ModeQIO 80MHz大多数S3板子适用Flash Size4MB按实际板子选Partition SchemeDefault 4MB with spiffs够用USB CDC On BootEnabled串口输出必需Upload Speed921600稳定且快3.3 蓝牙LE库的选择与初始化Arduino核心自带的蓝牙LE库叫BLEDevice基于NimBLE或Bluedroid。ESP32-S3默认用NimBLE因为NimBLE更省资源。初始化代码很简单#include BLEDevice.h #include BLEServer.h #include BLEUtils.h #include BLE2902.h BLEServer* pServer NULL; BLECharacteristic* pCharacteristic NULL; bool deviceConnected false; bool oldDeviceConnected false; #define SERVICE_UUID 4fafc201-1fb5-459e-8fcc-c5c9c331914b #define CHARACTERISTIC_UUID beb5483e-36e1-4688-b7f5-ea07361b26a8UUID我随便生成的你可以用在线UUID生成器只要保证唯一就行。服务UUID和特征值UUID要配对手机端扫描时才能正确识别。特征值的属性要设成READ | NOTIFYREAD让手机能主动读NOTIFY让ESP32能主动推。4. 核心代码实现与逻辑拆解4.1 中断配置与去抖动处理门磁开关是机械触点闭合和断开瞬间会有抖动可能产生多个边沿。如果不处理一次开关动作会触发多次中断蓝牙通知也会发多次。去抖动有两种做法硬件加RC滤波软件加延时确认。我用软件方式在中断里记录时间戳主循环里判断距离上次有效变化是否超过50毫秒。volatile bool doorChanged false; volatile unsigned long lastInterruptTime 0; const unsigned long DEBOUNCE_MS 50; void IRAM_ATTR doorISR() { unsigned long now millis(); if (now - lastInterruptTime DEBOUNCE_MS) { doorChanged true; lastInterruptTime now; } }IRAM_ATTR这个属性很重要它把中断服务程序放到IRAM里确保在Flash操作期间中断也能响应。ESP32-S3的中断默认可能在Flash里如果蓝牙协议栈正在操作Flash中断响应会延迟甚至丢失。加上IRAM_ATTR能避免这个问题。提示millis()在中断里能用但它在某些低功耗模式下会停止计数。如果你用Light Sleep要用esp_timer_get_time()代替。4.2 GATT服务与特征值定义GATT服务的结构是一个Server下面挂多个Service每个Service下面挂多个Characteristic。这个项目只需要一个Service和一个Characteristic。Characteristic的值我用一个字节表示0x00表示门关0x01表示门开。简单明了手机端解析也容易。class MyServerCallbacks: public BLEServerCallbacks { void onConnect(BLEServer* pServer) { deviceConnected true; }; void onDisconnect(BLEServer* pServer) { deviceConnected false; } }; void setupBLE() { BLEDevice::init(DoorSensor); pServer BLEDevice::createServer(); pServer-setCallbacks(new MyServerCallbacks()); BLEService *pService pServer-createService(SERVICE_UUID); pCharacteristic pService-createCharacteristic( CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY ); pCharacteristic-addDescriptor(new BLE2902()); pService-start(); BLEAdvertising *pAdvertising BLEDevice::getAdvertising(); pAdvertising-addServiceUUID(SERVICE_UUID); pAdvertising-setScanResponse(true); pAdvertising-setMinPreferred(0x06); pAdvertising-setMinPreferred(0x12); BLEDevice::startAdvertising(); }BLE2902是客户端特征配置描述符手机端订阅通知时需要写这个描述符。没有它NOTIFY功能用不了。setMinPreferred那两个参数是连接间隔建议值0x06对应7.5ms0x12对应22.5ms。实际连接间隔由手机端决定这里只是建议。4.3 主循环状态机与通知推送主循环的逻辑是检测doorChanged标志如果为真读取GPIO电平更新状态如果设备已连接就发通知然后重新广播如果之前断开了。void loop() { if (doorChanged) { doorChanged false; int doorState digitalRead(DOOR_PIN); uint8_t value (doorState HIGH) ? 0x01 : 0x00; pCharacteristic-setValue(value, 1); if (deviceConnected) { pCharacteristic-notify(); } } if (!deviceConnected oldDeviceConnected) { delay(500); pServer-startAdvertising(); oldDeviceConnected deviceConnected; } if (deviceConnected !oldDeviceConnected) { oldDeviceConnected deviceConnected; } }这里有个细节断开连接后要重新广播否则手机再也搜不到。但重新广播前加个500ms延时给蓝牙协议栈一点时间清理连接资源。这个延时是经验值太短可能导致广播启动失败。5. 手机端调试与验证方法5.1 用nRF Connect快速验证nRF Connect是Nordic出的蓝牙调试AppAndroid和iOS都有。打开后扫描能看到名为“DoorSensor”的设备。连接后展开服务列表找到我们定义的服务UUID点开特征值点三个箭头那个图标订阅通知。然后手动开关门磁App里应该能看到特征值从00变成01或者反过来。如果订阅后没反应检查几个点特征值属性有没有NOTIFYBLE2902描述符有没有加手机端有没有真的写入描述符开启通知。nRF Connect里订阅通知是点那个带箭头的图标不是点读图标。读图标只能读一次不会持续更新。5.2 串口监视器辅助排查串口监视器是调试蓝牙LE的利器。在关键位置加Serial.println比如连接建立、断开、通知发送、GPIO电平变化。波特率设115200和代码里Serial.begin(115200)一致。如果串口没输出检查USB CDC On Boot是否Enabled以及开发板是否真的在运行。我习惯在setup()里打印一行“Boot OK”在onConnect里打印“Client connected”在onDisconnect里打印“Client disconnected”。这样一眼就能看出蓝牙状态。门磁变化时也打印当前电平方便对照。5.3 常见连接问题与解决手机搜不到设备最常见的原因是广播没启动或者广播数据里没有服务UUID。有些手机尤其是Android扫描时只显示有服务UUID的设备。另外如果ESP32-S3之前连接过某个手机重新上电后可能还在广播旧连接参数导致新手机搜不到。解决办法是上电时先BLEDevice::deinit()再init()强制重置蓝牙栈。连接后立刻断开通常是GATT服务定义有问题。检查Service和Characteristic的UUID是否合法属性是否匹配。如果特征值属性只有READ没有NOTIFY手机端订阅通知会失败有些手机会直接断开。另外MTU大小也可能影响默认MTU是23字节够用但如果手机请求更大MTU而ESP32没响应也可能出问题。现象可能原因解决办法搜不到设备广播未启动检查startAdvertising调用连接后立刻断开GATT定义错误检查UUID和属性订阅通知无反应缺少BLE2902添加描述符状态更新延迟连接间隔太长调整setMinPreferred串口无输出USB CDC未启用设为Enabled6. 实操心得与避坑经验6.1 中断去抖动的参数选择去抖动时间我设的50ms这个值不是拍脑袋来的。机械开关的抖动时间通常在1到10ms之间50ms留了足够余量。但如果你设得太大比如200ms快速开关门时可能漏掉一次变化。我实测下来50ms对于家用门磁足够既不会误触发也不会漏事件。如果你用的是霍尔传感器无机械触点可以设到10ms甚至更小。提示去抖动时间要小于你期望的最小事件间隔。如果门可能在一秒内开关多次去抖动时间就不能超过500ms。一般家用场景50ms是安全值。6.2 蓝牙连接间隔与响应速度蓝牙LE的连接间隔决定了手机端能多快收到通知。连接间隔越短响应越快但功耗越高。手机端通常会在连接时请求一个它认为合适的间隔ESP32可以接受或拒绝。我在setMinPreferred里设了7.5ms到22.5ms的范围实际协商下来通常在30ms到50ms之间。这个延迟对于门磁监测完全够用人眼几乎感觉不到。如果你发现通知有明显延迟比如开关门后一两秒手机才更新可能是连接间隔被协商得太长。可以在手机端App里看看实际连接间隔nRF Connect能显示。如果超过100ms可以尝试在ESP32端强制要求更短的间隔但有些手机会拒绝。6.3 电源噪声对门磁读数的干扰门磁开关的线如果太长比如超过一米可能引入电源噪声导致GPIO读数不稳定。我遇到过这种情况门磁线走了两米结果门关着的时候GPIO偶尔读到高电平误报开门。解决办法是在GPIO和GND之间加一个0.1uF电容滤掉高频噪声。另外门磁线尽量远离电源线不要平行走线。如果条件允许用屏蔽线更好。但家用场景普通杜邦线加个电容基本就够了。我后来把线缩短到50厘米以内问题就消失了。所以接线长度也是个要考虑的因素。6.4 固件更新与OTA的取舍这个项目我没做OTA因为门磁节点装好后不太需要频繁更新。但如果要加OTAESP32-S3支持蓝牙OTA和WiFi OTA。蓝牙OTA速度慢但不需要WiFi。WiFi OTA快但需要网络配置。对于门磁这种简单设备我建议先不做OTA减少复杂度。真要更新拆下来用USB烧录也就几分钟的事。如果你确实需要OTAArduinoOTA库支持WiFi方式蓝牙OTA可以用ESP32-BLE-OTA库。但要注意OTA期间蓝牙连接会断开门磁状态监测会中断。所以OTA最好在门关着的时候做避免漏报。7. 功能扩展与后续优化方向7.1 加装温湿度传感器做环境联动ESP32-S3的GPIO还剩很多加一个DHT22或者SHT30温湿度传感器很容易。SHT30走I2C接两根线SDA、SCL就行。加进去之后GATT特征值可以扩展成多个一个报门磁状态一个报温度一个报湿度。手机端可以同时订阅多个特征值实现环境监测。这个扩展的实际价值在于你可以知道门开着的时候室内温度有没有快速变化比如冬天开门冷空气进来。或者结合湿度判断是否下雨天门窗没关。这些联动逻辑可以在手机端做也可以在ESP32端做简单判断后上报。7.2 低功耗优化与电池供电如果要电池供电第一步是把USB供电去掉改用锂电池加LDO或者DC-DC。第二步是优化蓝牙参数增大连接间隔降低发射功率。第三步是让ESP32-S3在空闲时进入Light Sleep靠GPIO中断唤醒。Light Sleep下蓝牙会断开所以需要权衡要么保持连接但功耗高要么断开连接但省电。我的建议是如果门磁状态变化不频繁比如一天开关几次可以用“广播深度睡眠”模式。门磁变化时唤醒广播几秒然后继续睡。手机想查状态时主动扫描广播。这种方式功耗极低但实时性差。如果要求实时通知那就保持连接接受几十毫安的功耗用大容量电池。7.3 多节点组网与蓝牙Mesh单个门磁节点功能有限如果家里有多个门窗可以组一个蓝牙Mesh网络。ESP32-S3支持蓝牙MeshArduino核心也有Mesh库。每个节点作为一个Mesh元素手机通过代理节点读取所有门磁状态。这样只需要连接一个节点就能看到全部门窗状态。蓝牙Mesh的配置比GATT Server复杂一些需要配网、绑定App Key、设置发布订阅地址。但一旦跑通扩展性很好。我目前还没做这个因为家里就一个门需要监测。如果你有多节点需求蓝牙Mesh是个值得投入的方向。7.4 数据记录与本地存储ESP32-S3的Flash可以存一些历史记录。比如每次门磁变化时把时间戳和状态写到SPIFFS或者LittleFS里。手机连接后可以读取历史记录了解过去一段时间门开关的频率和时长。这对于分析门窗使用习惯很有用。存储格式可以用简单的CSVtimestamp,state。时间戳可以用millis()但重启后会归零。如果要绝对时间需要加RTC模块或者蓝牙连接时从手机同步时间。我目前没做这个因为Flash写入寿命有限频繁写会缩短寿命。如果要做建议用环形缓冲区限制写入频率。8. 实测效果与稳定性观察8.1 连续运行72小时的状态记录我把节点装在家里的入户门上连续跑了72小时。期间正常开关门大概40多次蓝牙连接用手机测试了十几次。没有出现死机、断连不恢复、误报的情况。门磁状态变化到手机收到通知的延迟体感在100毫秒以内基本是即时的。唯一一次异常是手机蓝牙关闭再打开后ESP32没有自动重新广播手机搜不到设备。后来发现是onDisconnect回调里没有及时重启广播。加了500ms延时后重启广播问题解决。这个延时很关键太短了广播启动会失败太长了手机端等待时间长。8.2 不同手机平台的兼容性我测试了Android和iOS两种手机。Android用nRF ConnectiOS用LightBlue。两者都能正常连接、读值、订阅通知。差异在于Android的连接间隔协商更积极通常能到30ms左右iOS倾向于更长的间隔大概50ms到100ms。所以iOS上通知延迟稍微大一点但完全可接受。另外iOS对广播数据的要求更严格。如果广播里没有服务UUIDiOS可能根本不显示设备。所以pAdvertising-addServiceUUID(SERVICE_UUID)这行不能省。Android相对宽松但加上更规范。8.3 距离与穿墙能力实测蓝牙LE的传输距离在空旷环境下大概10米左右穿一堵墙后降到5米左右穿两堵墙基本就断了。我家门到卧室隔了一堵承重墙手机在卧室能连上但信号强度在-80dBm左右偶尔会断。在客厅无墙遮挡信号强度-50dBm非常稳定。如果你需要更远的距离可以调高发射功率。ESP32-S3的蓝牙发射功率可以设到9dBm但功耗会增加。默认是0dBm够用。如果穿墙需求强可以考虑加一个蓝牙中继节点或者改用WiFi方案。9. 代码完整清单与关键注释9.1 完整Arduino Sketch#include BLEDevice.h #include BLEServer.h #include BLEUtils.h #include BLE2902.h #define DOOR_PIN 4 #define SERVICE_UUID 4fafc201-1fb5-459e-8fcc-c5c9c331914b #define CHARACTERISTIC_UUID beb5483e-36e1-4688-b7f5-ea07361b26a8 BLEServer* pServer NULL; BLECharacteristic* pCharacteristic NULL; bool deviceConnected false; bool oldDeviceConnected false; volatile bool doorChanged false; volatile unsigned long lastInterruptTime 0; const unsigned long DEBOUNCE_MS 50; void IRAM_ATTR doorISR() { unsigned long now millis(); if (now - lastInterruptTime DEBOUNCE_MS) { doorChanged true; lastInterruptTime now; } } class MyServerCallbacks: public BLEServerCallbacks { void onConnect(BLEServer* pServer) { deviceConnected true; Serial.println(Client connected); }; void onDisconnect(BLEServer* pServer) { deviceConnected false; Serial.println(Client disconnected); } }; void setup() { Serial.begin(115200); pinMode(DOOR_PIN, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(DOOR_PIN), doorISR, CHANGE); BLEDevice::init(DoorSensor); pServer BLEDevice::createServer(); pServer-setCallbacks(new MyServerCallbacks()); BLEService *pService pServer-createService(SERVICE_UUID); pCharacteristic pService-createCharacteristic( CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY ); pCharacteristic-addDescriptor(new BLE2902()); uint8_t initialValue (digitalRead(DOOR_PIN) HIGH) ? 0x01 : 0x00; pCharacteristic-setValue(initialValue, 1); pService-start(); BLEAdvertising *pAdvertising BLEDevice::getAdvertising(); pAdvertising-addServiceUUID(SERVICE_UUID); pAdvertising-setScanResponse(true); pAdvertising-setMinPreferred(0x06); pAdvertising-setMinPreferred(0x12); BLEDevice::startAdvertising(); Serial.println(Boot OK, advertising...); } void loop() { if (doorChanged) { doorChanged false; int doorState digitalRead(DOOR_PIN); uint8_t value (doorState HIGH) ? 0x01 : 0x00; pCharacteristic-setValue(value, 1); Serial.print(Door state: ); Serial.println(value ? OPEN : CLOSED); if (deviceConnected) { pCharacteristic-notify(); } } if (!deviceConnected oldDeviceConnected) { delay(500); pServer-startAdvertising(); Serial.println(Restart advertising); oldDeviceConnected deviceConnected; } if (deviceConnected !oldDeviceConnected) { oldDeviceConnected deviceConnected; } }9.2 关键参数速查表参数值说明DOOR_PINGPIO4门磁输入引脚DEBOUNCE_MS50去抖动时间SERVICE_UUID自定义服务标识CHARACTERISTIC_UUID自定义特征值标识连接间隔建议7.5-22.5mssetMinPreferred广播重启延时500ms断开后重启广播9.3 烧录与验证步骤用USB线连接ESP32-S3到电脑Arduino IDE里选对开发板和端口点击上传等待编译和烧录完成打开串口监视器波特率115200看到“Boot OK, advertising...”说明启动成功手机打开nRF Connect扫描“DoorSensor”连接后订阅特征值通知手动开关门磁观察串口和手机端状态变化如果串口没输出检查USB CDC On Boot是否Enabled。如果手机搜不到检查广播是否启动可以按一下开发板复位键重启。10. 最后分享几个实操小技巧门磁安装的时候磁铁和开关的间距很关键。我一开始装得太近门稍微震动就触发。后来调到刚好在门关上时闭合、门打开时断开的位置用万用表蜂鸣档确认通断再固定螺丝。这个步骤不能省装歪了后面调试很麻烦。蓝牙UUID不要用示例里的自己生成一套。虽然示例UUID能用但多个设备同时用相同UUID会混淆。在线UUID生成器很多生成两个版本4的UUID一个给服务一个给特征值。如果你用PlatformIO而不是Arduino IDEplatformio.ini里要指定board esp32-s3-devkitc-1framework arduinomonitor_speed 115200。PlatformIO的库管理比Arduino IDE方便但第一次编译也要下载工具链耐心等。串口打印不要太多尤其是在中断里。Serial.print在中断里调用可能导致崩溃因为串口操作不是中断安全的。我的做法是中断里只设标志所有打印都在主循环里做。这个原则在嵌入式开发里通用记住就行。这个门磁监测节点我用了几个月稳定性很好。后来我又做了一个装在阳台门上两个节点用不同的设备名区分。手机端可以分别连接互不干扰。如果你也想做多个记得改设备名和UUID避免冲突。