1. 项目背景与硬件准备想象一下这样的场景早上起床时手机自动显示卧室当前的温湿度数据晚上躺在床上用手机就能远程关闭客厅的灯光。这种智能家居体验其实用ESP32开发板就能轻松实现。我们今天要做的就是基于ESP32的蓝牙功能构建一个兼具环境监测和远程控制能力的实用系统。硬件清单很简单ESP32开发板推荐使用ESP32-WROOM-32性价比高且稳定性好DHT11温湿度传感器价格约10元精度±2℃/±5%RHLED灯珠220Ω电阻用于状态指示杜邦线若干建议使用母对母线方便插拔接线方式如下DHT11的VCC接ESP32的3.3VDHT11的GND接ESP32的GNDDHT11的DATA接ESP32的GPIO4LED正极通过220Ω电阻接ESP32的GPIO27LED负极接ESP32的GND这里有个实际调试时的小技巧DHT11对供电电压敏感如果读数不稳定可以在VCC和GND之间并联一个0.1μF的电容。我在最初测试时就遇到过数据跳变的问题加了电容后立即稳定下来。2. 蓝牙协议栈关键概念解析2.1 BLE通信模型低功耗蓝牙(BLE)采用客户端-服务器架构ESP32在这里充当GATT服务器提供数据服务手机APP作为GATT客户端访问数据。就像餐厅里服务员与顾客的关系服务员服务器持有菜单和数据顾客客户端发起请求获取信息。协议栈中与我们开发最相关的是这两层GAP层Generic Access Profile负责设备可见性和连接管理广播参数设置广播间隔、广播数据格式连接参数协商连接间隔、延迟、超时GATT层Generic Attribute Profile定义数据交互规则服务(Service)功能模块如温湿度服务特征值(Characteristic)具体数据点如温度值描述符(Descriptor)附加说明如数据格式2.2 属性表(Attribute Table)这是BLE数据存储的核心结构相当于一个键值数据库。每个属性包含Handle唯一标识符类似内存地址UUID标准或自定义的ID如0x2A6E表示温度Value实际数据如25.5℃Permissions读写权限控制举个例子我们系统的属性表会包含主服务声明UUID: 0x1800温度特征UUID: 0x2A6E湿度特征UUID: 0x2A6FLED控制特征自定义UUID3. 开发环境搭建与初始化3.1 工具链配置推荐使用VS Code PlatformIO组合比Arduino IDE更适合工程化开发安装VS Code后搜索安装PlatformIO插件创建新项目选择ESP32开发板如Espressif ESP32 Dev Module添加依赖库lib_deps espressif/esp-idf-lib ^1.0.3 adafruit/DHT sensor library ^1.4.3初始化蓝牙协议栈的代码要点esp_bt_controller_config_t bt_cfg BT_CONTROLLER_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_bt_controller_init(bt_cfg)); ESP_ERROR_CHECK(esp_bt_controller_enable(ESP_BT_MODE_BLE)); esp_bluedroid_config_t bluedroid_cfg BT_BLUEDROID_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_bluedroid_init_with_cfg(bluedroid_cfg)); ESP_ERROR_CHECK(esp_bluedroid_enable());这里有个坑要注意ESP32默认同时支持经典蓝牙和BLE但内存有限。我们通过esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT)释放经典蓝牙占用的内存确保BLE稳定运行。3.2 广播参数配置广播就像设备在喊话包含两个部分广播数据31字节上限设备名称、服务UUID等扫描响应补充信息典型配置示例esp_ble_adv_params_t adv_params { .adv_int_min 0x20, // 最小广播间隔32*0.625ms20ms .adv_int_max 0x40, // 最大广播间隔64*0.625ms40ms .adv_type ADV_TYPE_IND, .own_addr_type BLE_ADDR_TYPE_PUBLIC, .channel_map ADV_CHNL_ALL, .adv_filter_policy ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, };实测发现广播间隔太短会导致手机端扫描时设备显示不稳定建议设置在20-100ms之间。我曾经设置过5ms的间隔结果手机根本搜不到设备调试了半天才发现是这个原因。4. GATT服务实现详解4.1 温湿度服务构建我们创建一个自定义服务包含三个特征温度读取只读、带通知湿度读取只读、带通知LED控制读写服务UUID建议使用在线生成器创建避免冲突。以下是关键代码// 自定义UUID使用在线生成器生成 #define GATTS_SERVICE_UUID_TEST 0xFDEF #define GATTS_CHAR_UUID_TEMP 0xFE01 #define GATTS_CHAR_UUID_HUMIDITY 0xFE02 #define GATTS_CHAR_UUID_LED 0xFE03 // 属性表定义 static const esp_gatts_attr_db_t gatt_db[] { // 服务声明 [IDX_SVC] {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)primary_service_uuid, ESP_GATT_PERM_READ, sizeof(uint16_t), sizeof(GATTS_SERVICE_UUID_TEST), (uint8_t *)GATTS_SERVICE_UUID_TEST}}, // 温度特征 [IDX_CHAR_TEMP] {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)character_declaration_uuid, ESP_GATT_PERM_READ, CHAR_DECLARATION_SIZE, CHAR_DECLARATION_SIZE, (uint8_t *)char_prop_read_notify}}, [IDX_CHAR_VAL_TEMP] {{ESP_GATT_RSP_BY_APP}, {ESP_UUID_LEN_16, (uint8_t *)GATTS_CHAR_UUID_TEMP, ESP_GATT_PERM_READ, sizeof(temp_value), sizeof(temp_value), (uint8_t *)temp_value}}, // LED控制特征省略其他特征定义... };4.2 数据更新机制对于温湿度数据我们需要定时读取并更新特征值。使用FreeRTOS的定时器是个好选择void update_sensor_data(TimerHandle_t xTimer) { float t dht.readTemperature(); float h dht.readHumidity(); if (!isnan(t)) { int16_t temp (int16_t)(t * 100); // 转换为整数保存 esp_ble_gatts_set_attr_value(handle_table[IDX_CHAR_VAL_TEMP], sizeof(temp), (uint8_t *)temp); } // 同理处理湿度数据... }这里有个细节优化蓝牙传输建议使用整数而非浮点数。我们将温度值放大100倍存储如25.5℃存储为2550既能保证精度又节省带宽。5. 客户端交互实现5.1 读写事件处理当手机APP读取或写入特征值时会触发对应回调。关键处理逻辑如下void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { switch (event) { case ESP_GATTS_READ_EVT: { esp_gatt_rsp_t rsp {0}; rsp.attr_value.handle param-read.handle; if (param-read.handle handle_table[IDX_CHAR_VAL_TEMP]) { rsp.attr_value.len sizeof(temp_value); memcpy(rsp.attr_value.value, temp_value, sizeof(temp_value)); } // 其他特征处理... esp_ble_gatts_send_response(gatts_if, param-read.conn_id, param-read.trans_id, ESP_GATT_OK, rsp); break; } case ESP_GATTS_WRITE_EVT: { if (param-write.handle handle_table[IDX_CHAR_VAL_LED]) { uint8_t led_state param-write.value[0]; gpio_set_level(LED_GPIO, led_state); } break; } } }5.2 通知功能实现要让手机能实时获取温湿度变化而不必主动查询需要启用通知功能在特征描述符中添加客户端特征配置(CCCD)当客户端启用通知后服务端可以主动推送更新代码实现// 在属性表中添加CCCD描述符 [IDX_CHAR_CFG_TEMP] {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)character_client_config_uuid, ESP_GATT_PERM_READ|ESP_GATT_PERM_WRITE, sizeof(uint16_t), sizeof(ccc_value), (uint8_t *)ccc_value}}, // 发送通知 void send_notification(uint16_t conn_id, uint16_t handle, uint8_t *value, uint16_t len) { esp_ble_gatts_send_indicate(gatts_if, conn_id, handle, len, value, false); }实际测试时发现iOS设备对通知间隔有严格要求如果发送太频繁会导致连接断开。建议温度变化超过0.5℃或湿度变化超过2%时才发送通知。6. 功耗优化与稳定性提升6.1 连接参数调优BLE的功耗主要取决于连接参数连接间隔Connection Interval7.5ms~4s从机延迟Slave Latency允许跳过的连接事件数监控超时Supervision Timeout10ms~32s推荐配置平衡功耗和响应速度esp_ble_conn_update_params_t conn_params { .min_int 0x10, // 20ms .max_int 0x20, // 40ms .latency 2, // 最多跳过2个连接事件 .timeout 400 // 4s超时 };6.2 错误处理机制健壮的系统需要处理各种异常情况广播失败尝试重新初始化蓝牙协议栈传感器断连添加看门狗定时器检测内存不足定期检查堆空间// 示例内存监控 void check_memory() { ESP_LOGI(TAG, Free heap: %d, esp_get_free_heap_size()); if (esp_get_free_heap_size() 10240) { ESP_LOGE(TAG, Memory low, restarting...); esp_restart(); } }在长期运行测试中我发现ESP32的蓝牙协议栈偶尔会出现内存泄漏。添加定期重启机制比如每24小时软重启一次可以显著提升系统稳定性。7. 移动端适配建议虽然本文聚焦ESP32开发但要让系统完整运行还需要手机端配合7.1 Android开发要点使用BluetoothGatt类进行设备连接处理onCharacteristicChanged回调接收通知注意需要在主线程外执行蓝牙操作7.2 iOS开发注意事项需要在前台状态下才能建立连接服务UUID必须与设备端完全匹配包括大小写后台模式需要特殊权限声明分享一个真实案例有用户反馈iOS设备无法连接最后发现是因为ESP32端的服务UUID是16位的而iOS默认用128位UUID比较。解决方案是在iOS端将UUID统一补全为128位格式。
ESP32蓝牙实战:从零构建一个温湿度监测与远程灯控系统
发布时间:2026/7/7 7:21:15
1. 项目背景与硬件准备想象一下这样的场景早上起床时手机自动显示卧室当前的温湿度数据晚上躺在床上用手机就能远程关闭客厅的灯光。这种智能家居体验其实用ESP32开发板就能轻松实现。我们今天要做的就是基于ESP32的蓝牙功能构建一个兼具环境监测和远程控制能力的实用系统。硬件清单很简单ESP32开发板推荐使用ESP32-WROOM-32性价比高且稳定性好DHT11温湿度传感器价格约10元精度±2℃/±5%RHLED灯珠220Ω电阻用于状态指示杜邦线若干建议使用母对母线方便插拔接线方式如下DHT11的VCC接ESP32的3.3VDHT11的GND接ESP32的GNDDHT11的DATA接ESP32的GPIO4LED正极通过220Ω电阻接ESP32的GPIO27LED负极接ESP32的GND这里有个实际调试时的小技巧DHT11对供电电压敏感如果读数不稳定可以在VCC和GND之间并联一个0.1μF的电容。我在最初测试时就遇到过数据跳变的问题加了电容后立即稳定下来。2. 蓝牙协议栈关键概念解析2.1 BLE通信模型低功耗蓝牙(BLE)采用客户端-服务器架构ESP32在这里充当GATT服务器提供数据服务手机APP作为GATT客户端访问数据。就像餐厅里服务员与顾客的关系服务员服务器持有菜单和数据顾客客户端发起请求获取信息。协议栈中与我们开发最相关的是这两层GAP层Generic Access Profile负责设备可见性和连接管理广播参数设置广播间隔、广播数据格式连接参数协商连接间隔、延迟、超时GATT层Generic Attribute Profile定义数据交互规则服务(Service)功能模块如温湿度服务特征值(Characteristic)具体数据点如温度值描述符(Descriptor)附加说明如数据格式2.2 属性表(Attribute Table)这是BLE数据存储的核心结构相当于一个键值数据库。每个属性包含Handle唯一标识符类似内存地址UUID标准或自定义的ID如0x2A6E表示温度Value实际数据如25.5℃Permissions读写权限控制举个例子我们系统的属性表会包含主服务声明UUID: 0x1800温度特征UUID: 0x2A6E湿度特征UUID: 0x2A6FLED控制特征自定义UUID3. 开发环境搭建与初始化3.1 工具链配置推荐使用VS Code PlatformIO组合比Arduino IDE更适合工程化开发安装VS Code后搜索安装PlatformIO插件创建新项目选择ESP32开发板如Espressif ESP32 Dev Module添加依赖库lib_deps espressif/esp-idf-lib ^1.0.3 adafruit/DHT sensor library ^1.4.3初始化蓝牙协议栈的代码要点esp_bt_controller_config_t bt_cfg BT_CONTROLLER_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_bt_controller_init(bt_cfg)); ESP_ERROR_CHECK(esp_bt_controller_enable(ESP_BT_MODE_BLE)); esp_bluedroid_config_t bluedroid_cfg BT_BLUEDROID_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_bluedroid_init_with_cfg(bluedroid_cfg)); ESP_ERROR_CHECK(esp_bluedroid_enable());这里有个坑要注意ESP32默认同时支持经典蓝牙和BLE但内存有限。我们通过esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT)释放经典蓝牙占用的内存确保BLE稳定运行。3.2 广播参数配置广播就像设备在喊话包含两个部分广播数据31字节上限设备名称、服务UUID等扫描响应补充信息典型配置示例esp_ble_adv_params_t adv_params { .adv_int_min 0x20, // 最小广播间隔32*0.625ms20ms .adv_int_max 0x40, // 最大广播间隔64*0.625ms40ms .adv_type ADV_TYPE_IND, .own_addr_type BLE_ADDR_TYPE_PUBLIC, .channel_map ADV_CHNL_ALL, .adv_filter_policy ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, };实测发现广播间隔太短会导致手机端扫描时设备显示不稳定建议设置在20-100ms之间。我曾经设置过5ms的间隔结果手机根本搜不到设备调试了半天才发现是这个原因。4. GATT服务实现详解4.1 温湿度服务构建我们创建一个自定义服务包含三个特征温度读取只读、带通知湿度读取只读、带通知LED控制读写服务UUID建议使用在线生成器创建避免冲突。以下是关键代码// 自定义UUID使用在线生成器生成 #define GATTS_SERVICE_UUID_TEST 0xFDEF #define GATTS_CHAR_UUID_TEMP 0xFE01 #define GATTS_CHAR_UUID_HUMIDITY 0xFE02 #define GATTS_CHAR_UUID_LED 0xFE03 // 属性表定义 static const esp_gatts_attr_db_t gatt_db[] { // 服务声明 [IDX_SVC] {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)primary_service_uuid, ESP_GATT_PERM_READ, sizeof(uint16_t), sizeof(GATTS_SERVICE_UUID_TEST), (uint8_t *)GATTS_SERVICE_UUID_TEST}}, // 温度特征 [IDX_CHAR_TEMP] {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)character_declaration_uuid, ESP_GATT_PERM_READ, CHAR_DECLARATION_SIZE, CHAR_DECLARATION_SIZE, (uint8_t *)char_prop_read_notify}}, [IDX_CHAR_VAL_TEMP] {{ESP_GATT_RSP_BY_APP}, {ESP_UUID_LEN_16, (uint8_t *)GATTS_CHAR_UUID_TEMP, ESP_GATT_PERM_READ, sizeof(temp_value), sizeof(temp_value), (uint8_t *)temp_value}}, // LED控制特征省略其他特征定义... };4.2 数据更新机制对于温湿度数据我们需要定时读取并更新特征值。使用FreeRTOS的定时器是个好选择void update_sensor_data(TimerHandle_t xTimer) { float t dht.readTemperature(); float h dht.readHumidity(); if (!isnan(t)) { int16_t temp (int16_t)(t * 100); // 转换为整数保存 esp_ble_gatts_set_attr_value(handle_table[IDX_CHAR_VAL_TEMP], sizeof(temp), (uint8_t *)temp); } // 同理处理湿度数据... }这里有个细节优化蓝牙传输建议使用整数而非浮点数。我们将温度值放大100倍存储如25.5℃存储为2550既能保证精度又节省带宽。5. 客户端交互实现5.1 读写事件处理当手机APP读取或写入特征值时会触发对应回调。关键处理逻辑如下void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { switch (event) { case ESP_GATTS_READ_EVT: { esp_gatt_rsp_t rsp {0}; rsp.attr_value.handle param-read.handle; if (param-read.handle handle_table[IDX_CHAR_VAL_TEMP]) { rsp.attr_value.len sizeof(temp_value); memcpy(rsp.attr_value.value, temp_value, sizeof(temp_value)); } // 其他特征处理... esp_ble_gatts_send_response(gatts_if, param-read.conn_id, param-read.trans_id, ESP_GATT_OK, rsp); break; } case ESP_GATTS_WRITE_EVT: { if (param-write.handle handle_table[IDX_CHAR_VAL_LED]) { uint8_t led_state param-write.value[0]; gpio_set_level(LED_GPIO, led_state); } break; } } }5.2 通知功能实现要让手机能实时获取温湿度变化而不必主动查询需要启用通知功能在特征描述符中添加客户端特征配置(CCCD)当客户端启用通知后服务端可以主动推送更新代码实现// 在属性表中添加CCCD描述符 [IDX_CHAR_CFG_TEMP] {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)character_client_config_uuid, ESP_GATT_PERM_READ|ESP_GATT_PERM_WRITE, sizeof(uint16_t), sizeof(ccc_value), (uint8_t *)ccc_value}}, // 发送通知 void send_notification(uint16_t conn_id, uint16_t handle, uint8_t *value, uint16_t len) { esp_ble_gatts_send_indicate(gatts_if, conn_id, handle, len, value, false); }实际测试时发现iOS设备对通知间隔有严格要求如果发送太频繁会导致连接断开。建议温度变化超过0.5℃或湿度变化超过2%时才发送通知。6. 功耗优化与稳定性提升6.1 连接参数调优BLE的功耗主要取决于连接参数连接间隔Connection Interval7.5ms~4s从机延迟Slave Latency允许跳过的连接事件数监控超时Supervision Timeout10ms~32s推荐配置平衡功耗和响应速度esp_ble_conn_update_params_t conn_params { .min_int 0x10, // 20ms .max_int 0x20, // 40ms .latency 2, // 最多跳过2个连接事件 .timeout 400 // 4s超时 };6.2 错误处理机制健壮的系统需要处理各种异常情况广播失败尝试重新初始化蓝牙协议栈传感器断连添加看门狗定时器检测内存不足定期检查堆空间// 示例内存监控 void check_memory() { ESP_LOGI(TAG, Free heap: %d, esp_get_free_heap_size()); if (esp_get_free_heap_size() 10240) { ESP_LOGE(TAG, Memory low, restarting...); esp_restart(); } }在长期运行测试中我发现ESP32的蓝牙协议栈偶尔会出现内存泄漏。添加定期重启机制比如每24小时软重启一次可以显著提升系统稳定性。7. 移动端适配建议虽然本文聚焦ESP32开发但要让系统完整运行还需要手机端配合7.1 Android开发要点使用BluetoothGatt类进行设备连接处理onCharacteristicChanged回调接收通知注意需要在主线程外执行蓝牙操作7.2 iOS开发注意事项需要在前台状态下才能建立连接服务UUID必须与设备端完全匹配包括大小写后台模式需要特殊权限声明分享一个真实案例有用户反馈iOS设备无法连接最后发现是因为ESP32端的服务UUID是16位的而iOS默认用128位UUID比较。解决方案是在iOS端将UUID统一补全为128位格式。