【2024 DigiKey创意大赛】【智能家居控制中心】【EP03】ESP32-S3-LCD-EV + BME680 BLE
<p>本次实现通过ESP32 BLE 来实现链接米家的温湿度计.esp32 存在2个蓝牙协议栈. Bluedroid - Dual-mode和 NimBLE - BLE only二选一. 最早使用了Bluedroid来实现这个功能.集成过程中发现esp-rainmarker 使用的是NimBLE,又不是一波令人窒息的操作...</p><h5>NimBLE: <a href="https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/bluetooth/nimble/index.html" target="_blank">https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/bluetooth/nimble/index.html</a></h5>
<h4>Bluedroid - Dual-mode: <a href="https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/bluetooth/bt_le.html" target="_blank">https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/bluetooth/bt_le.html</a></h4>
<h1>概念</h1>
<h3>Gap</h3>
<p>GAP 的广播工作流程</p>
<p> </p>
<div style="text-align: center;"></div>
<p> </p>
<p> GAP(<strong>Generic Access Profile</strong>,通用访问配置文件)是蓝牙协议栈中的一部分,负责定义蓝牙设备之间如何发现、连接和进行交互。GAP提供了设备连接和通信的基本框架。它主要包含以下几个方面:</p>
<h3> 1. <strong>广播和扫描</strong></h3>
<ul start="1">
<li data-list="bullet">
<p><strong>广播(Advertising)</strong>:设备通过广播信道发送数据,其他设备可以扫描到这些广播数据。广播包含了设备的基本信息,例如名称、服务UUID等。</p>
</li>
<li data-list="bullet">
<p><strong>扫描(Scanning)</strong>:设备监听广播信道以发现周围的蓝牙设备和其广播数据。</p>
</li>
</ul>
<h3> 2. <strong>连接</strong></h3>
<ul start="1">
<li data-list="bullet">
<p><strong>发起连接(Connection Initiation)</strong>:扫描设备可以发起与广播设备的连接,建立一条点对点的连接。</p>
</li>
<li data-list="bullet">
<p><strong>连接参数(Connection Parameters)</strong>:GAP定义了在连接建立时如何设置连接参数,如连接间隔、超时时间等。</p>
</li>
</ul>
<h3> 3. <strong>角色</strong></h3>
<p> GAP规定了蓝牙设备可以扮演的四种角色:</p>
<ul start="1">
<li data-list="bullet">
<p><strong>广播者(Broadcaster)</strong>:只广播数据,不接收数据或发起连接。</p>
</li>
<li data-list="bullet">
<p><strong>观察者(Observer)</strong>:只扫描广播数据,不发起连接。</p>
</li>
<li data-list="bullet">
<p><strong>主设备(Central)</strong>:扫描并发起连接,通常是主控设备(如手机)。</p>
</li>
<li data-list="bullet">
<p><strong>从设备(</strong><strong>Peripheral</strong><strong>)</strong>:广播并接受连接请求,通常是外围设备(如传感器)。</p>
</li>
</ul>
<h3> 4. <strong>配对与安全</strong></h3>
<ul start="1">
<li data-list="bullet">
<p><strong>配对(Pairing)</strong>:GAP定义了蓝牙设备如何通过加密进行配对,确保通信的安全性。</p>
</li>
<li data-list="bullet">
<p><strong>密钥分发(Key Distribution)</strong>:配对后,设备之间可以交换密钥用于加密通信。</p>
</li>
<li data-list="bullet">
<p><strong>安全等级(Security Levels)</strong>:GAP支持不同的安全等级,从没有加密到加密和认证。</p>
</li>
</ul>
<h3> 5. <strong>设备名称和可见性</strong></h3>
<ul start="1">
<li data-list="bullet">
<p><strong>设备名称(Device Name)</strong>:GAP允许设备广播其名称,以便用户识别。</p>
</li>
<li data-list="bullet">
<p><strong>可见性(Visibility)</strong>:设备可以配置为可见或不可见,影响它是否能够被其他设备发现。</p>
</li>
</ul>
<h3> 6. <strong>连接管理</strong></h3>
<ul start="1">
<li data-list="bullet">
<p><strong>连接建立与终止</strong>:GAP定义了设备如何建立和终止蓝牙连接。</p>
</li>
<li data-list="bullet">
<p><strong>连接参数更新(Connection Parameter Update)</strong>:在连接过程中,设备可以协商更新连接参数以优化性能。</p>
</li>
</ul>
<h3> 7. <strong>扩展功能</strong></h3>
<ul start="1">
<li data-list="bullet">
<p><strong>扩展广告(Extended Advertising)</strong>:如你提到的,GAP标准中的一部分允许广播更多的数据,支持长时间的广播周期。</p>
</li>
<li data-list="bullet">
<p><strong>周期性广告(Periodic Advertising)</strong>:用于周期性发送广播数据,适用于广播数据不频繁更新的场景。</p>
</li>
</ul>
<p> GAP是设备发现、连接建立、连接管理、安全配对的基础层,支持BLE应用的各种交互方式。</p>
<h3>GATT</h3>
<p>(GATT服务端)和中心设备(GATT客户端)之间的数据交流流程,</p>
<div style="text-align: center;"></div>
<p> GATT(<strong>Generic Attribute Profile</strong>,通用属性配置文件)是蓝牙低功耗(BLE)协议栈中的一个重要部分,负责设备之间数据的组织、交换和传输。它基于属性(Attribute)的形式进行通信,定义了如何在已连接的设备之间通过服务和特征(Characteristics)进行数据传输。GATT的设计使得BLE设备能够以结构化的方式进行数据交互,主要包括以下内容:</p>
<h3> 1. <strong>属性(Attribute)</strong></h3>
<p> 属性是GATT通信的核心,表示设备内部的某一项数据,通常由一个16位或128位的UUID标识。属性包含三个关键元素:</p>
<ul start="1">
<li data-list="bullet">
<p><strong>句柄(Handle)</strong>:属性的唯一标识符,通常是一个16位的值,表示属性在设备上的位置。</p>
</li>
<li data-list="bullet">
<p><strong>类型(Type)</strong>:属性的类型,通过UUID进行标识。例如,某属性可能是一个服务或特征。</p>
</li>
<li data-list="bullet">
<p><strong>值(Value)</strong>:属性的实际数据,可以是任意数据类型。</p>
</li>
</ul>
<h3> 2. <strong>服务(Service)</strong></h3>
<p> 服务是由一组相关的特征和属性组成的,代表一个逻辑功能。例如,心率服务可以包含多个特征来传输心率数据。</p>
<ul start="1">
<li data-list="bullet">
<p><strong>服务类型</strong>:</p>
<ul>
<li data-list="bullet">
<p><strong>主要服务(Primary Service)</strong>:表示设备的主要功能服务。</p>
</li>
<li data-list="bullet">
<p><strong>次要服务(Secondary Service)</strong>:依附于主要服务的辅助功能。</p>
</li>
</ul>
</li>
</ul>
<h3> 3. <strong>特征(Characteristic)</strong></h3>
<p> 特征是GATT的核心数据单元,每个特征由一个值和可选的描述符组成。特征用于读写或通知数据传输。</p>
<ul start="1">
<li data-list="bullet">
<p><strong>特征值</strong><strong>(Characteristic Value)</strong>:特征的具体数据,可以被读取、写入或订阅。</p>
</li>
<li data-list="bullet">
<p><strong>特征属性(Characteristic Properties)</strong>:定义特征支持的操作类型,如读取、写入、通知等。</p>
</li>
</ul>
<h3> 4. <strong>描述符(Descriptor)</strong></h3>
<p> 描述符是特征的附加信息,描述特征的特定细节。例如,描述符可以定义特征值的单位或数据格式。</p>
<ul start="1">
<li data-list="bullet">
<p>常见的描述符包括:</p>
<ul>
<li data-list="bullet">
<p><strong>客户端特征配置描述符(Client Characteristic Configuration Descriptor, CCCD)</strong>:用于配置通知或指示。</p>
</li>
<li data-list="bullet">
<p><strong>特征用户描述符(Characteristic User Description, CUD)</strong>:对特征值的简要说明。</p>
</li>
</ul>
</li>
</ul>
<h3> 5. <strong>GATT 操作</strong></h3>
<p> GATT定义了设备如何通过服务和特征来交互数据,包括以下操作:</p>
<ul start="1">
<li data-list="bullet">
<p><strong>发现操作(Discovery Operations)</strong>:设备可以发现远程设备支持的服务、特征和描述符。这包括发现所有服务、发现特定特征、发现特征的描述符等。</p>
</li>
<li data-list="bullet">
<p><strong>读取操作(Read Operations)</strong>:读取某个特征或描述符的值,通常主设备(Central)向从设备(Peripheral)发起。</p>
</li>
<li data-list="bullet">
<p><strong>写入操作(Write Operations)</strong>:写入某个特征或描述符的值,可以是有响应的写入(Write with Response)或无响应的写入(Write without Response)。</p>
</li>
<li data-list="bullet">
<p><strong>通知(Notification)和指示(Indication)</strong>:从设备可以主动向主设备发送特征值的变化。通知不需要确认,指示则需要主设备确认接收。</p>
</li>
</ul>
<h3> 6. <strong>GATT 服务协议栈</strong></h3>
<p> GATT作为一个协议栈分层,通常与其他蓝牙层次协同工作:</p>
<ul start="1">
<li data-list="bullet">
<p><strong>ATT</strong><strong>(Attribute Protocol)</strong>:GATT基于ATT协议构建,ATT提供了对设备属性进行读写的机制。GATT扩展了ATT的功能,加入了服务和特征的结构化数据模型。</p>
</li>
<li data-list="bullet">
<p><strong>L2CAP(</strong><strong>Logical Link Control</strong><strong> and Adaptation Protocol)</strong>:ATT和GATT协议通过L2CAP层进行数据封装和传输。</p>
</li>
</ul>
<h3> 7. <strong>GATT 角色</strong></h3>
<p> GATT协议中,设备可以扮演以下两种角色:</p>
<ul start="1">
<li data-list="bullet">
<p><strong>GATT 服务器(Server)</strong>:保存数据并响应客户端的请求,通常是外围设备(如传感器)。</p>
</li>
<li data-list="bullet">
<p><strong>GATT 客户端(Client)</strong>:发起请求来读取或写入数据,通常是主设备(如手机或平板)。</p>
</li>
</ul>
<h3> 8. <strong>标准化服务和特征</strong></h3>
<p> 蓝牙组织定义了一系列标准化的服务和特征,用于特定应用场景。例如:</p>
<ul start="1">
<li data-list="bullet">
<p><strong>心率服务(Heart Rate Service)</strong>:用于传输心率数据。</p>
</li>
<li data-list="bullet">
<p><strong>电池服务(Battery Service)</strong>:报告设备的电池电量。</p>
</li>
<li data-list="bullet">
<p><strong>设备信息服务(Device Information Service, DIS)</strong>:提供设备的制造商信息、硬件版本等。</p>
</li>
</ul>
<h3> 9. <strong>长</strong><strong>特征值</strong></h3>
<p> GATT支持通过分块操作(Read Blob/Write Blob)来传输超出单个ATT帧限制的长特征值(通常大于20字节)。</p>
<h3> 10. <strong>GATT 协议操作示例</strong></h3>
<ul start="1">
<li data-list="bullet">
<p><strong>连接后发现服务</strong>:在连接建立后,客户端通常会首先发现服务器支持的服务。</p>
</li>
<li data-list="bullet">
<p><strong>读取心率数据</strong>:客户端可以读取“心率特征”值,获取实时心率。</p>
</li>
<li data-list="bullet">
<p><strong>写入控制命令</strong>:客户端可以通过写入特定特征来控制设备的行为。</p>
</li>
</ul>
<p><strong>GATT 结构</strong></p>
<p>GATT事务是建立在嵌套的Profiles,Services和Characteristics之上的,如下如所示:</p>
<p></p>
<p> 总结来说,GATT负责组织和传输设备之间的属性数据,通过服务和特征的形式,使BLE设备能够以标准化的方式进行数据交互。</p>
<p> </p>
<h1>实践</h1>
<p>首先我们需要通过GAP协议进行扫描和链接,在ble 5.0以上存在着<strong>Advertising </strong>和<strong> Extended Advertising</strong>两种不同的方式.可以通过 <code class="hljs">idf.py menuconfig</code> 进行修改<code class="hljs">Enable extended advertising</code> esp32芯片的板子就不要尝试了,用不了.</p>
<pre>
<code>(Top) → Component config → Bluetooth → NimBLE Options → Enable BLE 5 feature
Espressif IoT Development Framework Configuration
[*] Enable 2M Phy
[*] Enable coded Phy
[ ] Enable extended advertising
(0) Maximum number of periodic advertising syncs
[ ] Enable GATT caching----</code></pre>
<h3>开启蓝牙</h3>
<pre>
<code class="language-cpp">void app_ble_init(void) {
int rc;
/* Initialize NVS — it is used to store PHY calibration data */
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
ret = nimble_port_init();
if (ret != ESP_OK) {
ESP_LOGE(tag, "Failed to init nimble %d ", ret);
return;
}
ESP_LOGI(tag, "init nimble success");
/* Configure the host. */
ble_hs_cfg.reset_cb = blecent_on_reset;
ble_hs_cfg.sync_cb = blecent_on_sync;
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
/* Initialize data structures to track connected peers. */
rc = peer_init(MYNEWT_VAL(BLE_MAX_CONNECTIONS), 64, 64, 64);
assert(rc == 0);
/* Set the default device name. */
rc = ble_svc_gap_device_name_set("nimble-blecent");
assert(rc == 0);
/* XXX Need to have template for store */
ble_store_config_init();
nimble_port_freertos_init(blecent_host_task);
}
</code></pre>
<h3>实现cb_event和开启扫描</h3>
<pre>
</pre>
<pre>
<code class="language-cpp">
/**
* Initiates the GAP general discovery procedure.
*/
static void blecent_scan(void) {
uint8_t own_addr_type;
struct ble_gap_disc_params disc_params;
int rc;
/* Figure out address to use while advertising (no privacy for now) */
rc = ble_hs_id_infer_auto(0, &own_addr_type);
if (rc != 0) {
MODLOG_DFLT(ERROR, "error determining address type; rc=%d\n", rc);
return;
}
/* Tell the controller to filter duplicates; we don't want to process
* repeated advertisements from the same device.
*/
disc_params.filter_duplicates = 1;
/**
* Perform a passive scan.I.e., don't send follow-up scan requests to
* each advertiser.
*/
disc_params.passive = 0;
/* Use defaults for the rest of the parameters. */
disc_params.itvl = 0;
disc_params.window = 0;
disc_params.filter_policy = 0;
disc_params.limited = 0;
rc = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &disc_params,
blecent_gap_event, NULL);
if (rc != 0) {
MODLOG_DFLT(ERROR, "Error initiating GAP discovery procedure; rc=%d\n", rc);
}
}
static void blecent_on_reset(int reason) {
MODLOG_DFLT(ERROR, "Resetting state; reason=%d\n", reason);
}
static void blecent_on_sync(void) {
int rc;
/* Make sure we have proper identity address set (public preferred) */
rc = ble_hs_util_ensure_addr(0);
assert(rc == 0);
/* Begin scanning for a peripheral to connect to. */
blecent_scan();
}
void blecent_host_task(void *param) {
ESP_LOGI(tag, "BLE Host Task Started");
/* This function will return only when nimble_port_stop() is executed */
nimble_port_run();
nimble_port_freertos_deinit();
}
/* This function showcases stack init and deinit procedure. */
static void stack_init_deinit(void) {
int rc;
while (1) {
vTaskDelay(1000);
ESP_LOGI(tag, "Deinit host");
rc = nimble_port_stop();
if (rc == 0) {
nimble_port_deinit();
} else {
ESP_LOGI(tag, "Nimble port stop failed, rc = %d", rc);
break;
}
vTaskDelay(1000);
ESP_LOGI(tag, "Init host");
rc = nimble_port_init();
if (rc != ESP_OK) {
ESP_LOGI(tag, "Failed to init nimble %d ", rc);
break;
}
nimble_port_freertos_init(blecent_host_task);
ESP_LOGI(tag, "Waiting for 1 second");
}
}</code></pre>
<h3>链接米家温湿度计</h3>
<h4>温度计定义</h4>
<pre>
</pre>
<pre>
<code class="language-cpp">/*** The UUID of the service containing the subscribable characterstic ***/
static const ble_uuid_t *remote_svc_uuid =
BLE_UUID128_DECLARE(0xa6, 0xa3, 0x7d, 0x99, 0xf2, 0x6f, 0x1a, 0x8a, 0x0c,
0x4b, 0x0a, 0x7a, 0xb0, 0xcc, 0xe0, 0xeb);
/*** The UUID of the subscribable chatacteristic ***/
static const ble_uuid_t *remote_chr_uuid =
BLE_UUID128_DECLARE(0xa6, 0xa3, 0x7d, 0x99, 0xf2, 0x6f, 0x1a, 0x8a, 0x0c,
0x4b, 0x0a, 0x7a, 0xc1, 0xcc, 0xe0, 0xeb);
static uint8_t target_addr = {0xE1, 0x0A, 0x04, 0x38, 0xC1, 0xA4};
typedef struct {
float temperature;
float humidity;
float voltage;
float battery;
} Result;
Result processBuffer(const uint8_t *buff) {
Result result = {0, 0, 0, 0};
// Temperature conversion
int16_t temp = (int16_t)(buff | (buff << 8)); // Little-endian
result.temperature = temp / 100.0f;
// Humidity conversion
result.humidity = (float)buff;
// Voltage conversion
int16_t voltageRaw = (int16_t)(buff | (buff << 8)); // Little-endian
result.voltage = voltageRaw / 1000.0f;
// Battery percentage calculation
result.battery = ((result.voltage - 2) / (3.261 - 2)) * 100;
result.battery = (result.battery < 0) ? 0
: (result.battery > 100)
? 100
: result.battery; // Clamp between 0 and 100
return result;
}</code></pre>
<h4>BLE读取信息</h4>
<pre>
</pre>
<pre>
<code class="language-cpp">static int blecent_on_read(uint16_t conn_handle,
const struct ble_gatt_error *error,
struct ble_gatt_attr *attr, void *arg) {
MODLOG_DFLT(INFO, "Read complete; status=%d conn_handle=%d", error->status,
conn_handle);
if (error->status == 0) {
MODLOG_DFLT(INFO, " attr_handle=%d value=", attr->handle);
print_mbuf(attr->om);
Result result = processBuffer(attr->om->om_data);
printf("Temperature: %.2f\n", result.temperature);
printf("Humidity: %.2f\n", result.humidity);
printf("Voltage: %.3f\n", result.voltage);
printf("Battery: %.2f%%\n", result.battery);
}
MODLOG_DFLT(INFO, "\n");
return 0;
}
static void blecent_read_write_subscribe(const struct peer *peer) {
const struct peer_chr *chr;
int rc;
/* Read the supported-new-alert-category characteristic. */
chr = peer_chr_find_uuid(
peer,
remote_svc_uuid, remote_chr_uuid);
if (chr == NULL) {
MODLOG_DFLT(ERROR, "Error \n");
goto err;
}
rc = ble_gattc_read(peer->conn_handle, chr->chr.val_handle, blecent_on_read,
NULL);
if (rc != 0) {
MODLOG_DFLT(ERROR, "Error: Failed to read characteristic; rc=%d\n", rc);
goto err;
}
return;
err:
/* Terminate the connection. */
ble_gap_terminate(peer->conn_handle, BLE_ERR_REM_USER_CONN_TERM);
}
static void blecent_on_disc_complete(const struct peer *peer, int status,
void *arg) {
if (status != 0) {
/* Service discovery failed.Terminate the connection. */
MODLOG_DFLT(ERROR,
"Error: Service discovery failed; status=%d "
"conn_handle=%d\n",
status, peer->conn_handle);
ble_gap_terminate(peer->conn_handle, BLE_ERR_REM_USER_CONN_TERM);
return;
}
/* Service discovery has completed successfully.Now we have a complete
* list of services, characteristics, and descriptors that the peer
* supports.
*/
MODLOG_DFLT(INFO,
"Service discovery complete; status=%d "
"conn_handle=%d\n",
status, peer->conn_handle);
/* Now perform three GATT procedures against the peer: read,
* write, and subscribe to notifications for the ANS service.
*/
blecent_read_write_subscribe(peer);
}
static int blecent_gap_event(struct ble_gap_event *event, void *arg) {
struct ble_gap_conn_desc desc;
struct ble_hs_adv_fields fields;
#if MYNEWT_VAL(BLE_HCI_VS)
#if MYNEWT_VAL(BLE_POWER_CONTROL)
struct ble_gap_set_auto_pcl_params params;
#endif
#endif
int rc;
switch (event->type) {
case BLE_GAP_EVENT_DISC:
rc = ble_hs_adv_parse_fields(&fields, event->disc.data,
event->disc.length_data);
if (rc != 0) {
return 0;
}
/* An advertisment report was received during GAP discovery. */
print_adv_fields(&fields);
/* Try to connect to the advertiser if it looks interesting. */
blecent_connect_if_interesting(&event->disc);
return 0;
case BLE_GAP_EVENT_EXT_DISC:
/* An advertisment report was received during GAP discovery. */
ext_print_adv_report(&event->disc);
blecent_connect_if_interesting(&event->disc);
return 0;
default:
return 0;
}
}
</code></pre>
<p>结果:</p>
<div style="text-align: center;"></div>
<h2>优化</h2>
<p>目前提供的是代码核心部分和简单的测试逻辑.项目中会做成可以手动控制扫码设备.通过触摸屏点击对应的蓝牙设备进行连接.并支持多个设备连接.</p>
<h4> </h4>
<h3>后记</h3>
<p>关于如何发现米家蓝牙设备地址和关于idf中的数据大小端问题做个总结:</p>
<h3>如何发现米家设备:</h3>
<ol start="1">
<li data-list="number">
<p>https://github.com/rytilahti/python-miio</p>
</li>
<li data-list="number">
<p>通过手机ble app 找名字<code>LYWSD03MMC</code>或者自己写一个工具</p>
</li>
<li data-list="number">
<p>https://lywsd02mmc.bilaldurrani.com/类似的一些web端的ble工具</p>
</li>
<li data-list="number">
<p>其他...就不列举了</p>
</li>
</ol>
<h3>大小端转换问题:</h3>
<p>我这边写了一个小工具来实现</p>
<p> </p>
<pre>
<code class="language-cpp">#include <stdint.h>
#include <stdio.h>
#include <string.h>
void hexStringToByteArray(const char *hexString, uint8_t *byteArray) {
size_t len = strlen(hexString);
for (size_t i = 0; i < len; i += 2) {
sscanf(hexString + i, "%2hhx", &byteArray);
}
}
void printByteArray(const uint8_t *byteArray) {
printf("BLE_UUID128_DECLARE(");
for (size_t i = 15; i < 16; i--) {
printf("0x%02x", byteArray);
if (i > 0) {
printf(", ");
}
}
printf(")\n");
}
int main() {
const char *hexStrings[] = {
"ebe0ccb07a0a4b0c8a1a6ff2997da3a6",
"ebe0ccc17a0a4b0c8a1a6ff2997da3a6"
};
uint8_t byteArray; // 32 hex digits = 16 bytes
for (size_t j = 0; j < sizeof(hexStrings) / sizeof(hexStrings); j++) {
hexStringToByteArray(hexStrings, byteArray);
printf("Little-endian Byte array for %s: ", hexStrings);
printByteArray(byteArray);
}
return 0;
}
</code></pre>
<p> </p>
页:
[1]