视频演示了懒人遥控灯的,电脑、手机端Web Bluetooth控制亮度 和 遥控器遥控亮度,多种控制方式。
一、设计简介
1、设计目标
本次活动 选择的是 任务三 蓝牙方向,我设计做一个懒人的多端遥跨平台控灯,可以在电脑、平板、手机上开关LED灯 或 调整LED灯亮度,在移动端可以无极调节亮度(调节范围0% -100%),并且配备一个遥控器实现直接遥控亮度(每次调整20%亮度,调节范围0% -100%)。
2、软硬件简介
涉及到的软、硬件、上位机平台如下:
多端跨平台上位机:Web BLE + HTML 的网页端
遥控灯遥控:ESP32 S3模块
遥控灯本体:ESP32 C6模块(安装到开发板上)
其他硬件:COB封装的LED,可调电流模块
3、软硬件功能简介
使用蓝牙技术,实现多端通讯数据交互,在这个通讯框架下,每一个通讯者的角色如下图:
遥控器作为蓝牙主机,需要调试的是单主机的功能,需要扫描广播、查找服务、发现handle等才能实现数据交互。
遥控灯作为蓝牙从机,需要调试的修改广播名 与 广播内容、新增自定义服务 与 自定义UUID。
二、代码实现
下面主要介绍核心代码 和 主要改动点,遥控灯代码运行框图如下:
1、遥控灯
遥控灯需要调试的是 修改广播名 与内容、新增自定义服务 与 UUID、PWM 。
1.1、修改广播名
广播名修改虽然简单,但是十分重要,用于区分设备(无论是手机 还是遥控器),告诉我们这个是我们要找的设备。
#define TEST_DEVICE_NAME "xxu"
XXU 就是本人在电子森林的用户名。
1.2、修改广播内容
注:此修改是非必要修改,注释部分就是改动,作用是关闭显示信号强度、当前从机能提供的UUID等信息
//adv data
static esp_ble_adv_data_t adv_data = {
.set_scan_rsp = false,
.include_name = true,
.include_txpower = false,
// .min_interval = 0x0006, //slave connection min interval, Time = min_interval * 1.25 msec
// .max_interval = 0x0010, //slave connection max interval, Time = max_interval * 1.25 msec
.appearance = 0x00,
.manufacturer_len = 0, //TEST_MANUFACTURER_DATA_LEN,
.p_manufacturer_data = NULL, //&test_manufacturer[0],
.service_data_len = 0,
.p_service_data = NULL,
// .service_uuid_len = sizeof(adv_service_uuid128),
// .p_service_uuid = adv_service_uuid128,
// .flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT),
};
// scan response data
static esp_ble_adv_data_t scan_rsp_data = {
.set_scan_rsp = true,
.include_name = false,
.include_txpower = false,
//.min_interval = 0x0006,
//.max_interval = 0x0010,
.appearance = 0x00,
.manufacturer_len = 0, //TEST_MANUFACTURER_DATA_LEN,
.p_manufacturer_data = NULL, //&test_manufacturer[0],
.service_data_len = 0,
.p_service_data = NULL,
// .service_uuid_len = sizeof(adv_service_uuid128),
// .p_service_uuid = adv_service_uuid128,
// .flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT),
};
1.3、新增自定义服务
新增服务涉及到很多代码,我是以文件从上到下的顺序展示代码的。
TEST_C 后缀的是新增代码,新增的宏就是要用到的服务。
- 0xFF01 是服务的UUID
- 0xFF02 Write数据UUID(主机发送数据)
- 0xFF03 Notify数据UUID(从机上报数据) 但是此工程不用上报数据所以没有启用
#define GATTS_SERVICE_UUID_TEST_B 0x00EE
#define GATTS_CHAR_UUID_TEST_B 0xEE01
#define GATTS_DESCR_UUID_TEST_B 0x2222
#define GATTS_NUM_HANDLE_TEST_B 4
#define GATTS_SERVICE_UUID_TEST_C 0xFF01
#define GATTS_CHAR_UUID_TEST_C 0xFF02
#define GATTS_CHAR_UUID_TEST_C_2 0xFF03
#define GATTS_DESCR_UUID_TEST_C 0x1111
#define GATTS_NUM_HANDLE_TEST_C 7
修改服务的总数 和 新增服务的编号,编号用于区分不同的服务
#define PROFILE_NUM 3 //服务的总数
#define PROFILE_A_APP_ID 0
#define PROFILE_B_APP_ID 1
#define PROFILE_C_APP_ID 2 //新增服务的编号
PROFILE_C_APP_ID 为新增代码,用于注册新增的服务,在蓝牙初始化时默认调用,这里非常非常重要!!!
static struct gatts_profile_inst gl_profile_tab[PROFILE_NUM] = {
[PROFILE_A_APP_ID] = {
.gatts_cb = gatts_profile_a_event_handler,
.gatts_if = ESP_GATT_IF_NONE,
/* Not get the gatt_if, so initial is ESP_GATT_IF_NONE */
},
[PROFILE_B_APP_ID] = {
.gatts_cb = gatts_profile_b_event_handler,
/* This demo does not implement, similar as profile A */
.gatts_if = ESP_GATT_IF_NONE,
/* Not get the gatt_if, so initial is ESP_GATT_IF_NONE */
},
[PROFILE_C_APP_ID] = {
.gatts_cb = gatts_profile_c_event_handler,
/* This demo does not implement, similar as profile A */
.gatts_if = ESP_GATT_IF_NONE,
/* Not get the gatt_if, so initial is ESP_GATT_IF_NONE */
},
};
c_prepare_write_env 是新增的写事件的结构体
static prepare_type_env_t a_prepare_write_env;
static prepare_type_env_t b_prepare_write_env;
static prepare_type_env_t c_prepare_write_env;
gatts_profile_c_event_handler 是模仿其他服务的事件回调函数写的,主要是在接收数据处添加设置PWM的代码,在接受数据后,直接设置PWM大小。
case ESP_GATTS_WRITE_EVT: {
ESP_LOGI(GATTS_TAG, "GATT_WRITE_EVT, conn_id %d, trans_id %" PRIu32 ", handle %d", param->write.conn_id, param->write.trans_id, param->write.handle);
if (!param->write.is_prep){
ESP_LOGI(GATTS_TAG, "GATT_WRITE_EVT, value len %d, value :", param->write.len);
esp_log_buffer_hex(GATTS_TAG, param->write.value, param->write.len);
update_duty(param->write.value[0]);
gatts_profile_c_event_handler 所有代码
static void gatts_profile_c_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_REG_EVT:
ESP_LOGI(GATTS_TAG, "REGISTER_APP_EVT, status %d, app_id %d", param->reg.status, param->reg.app_id);
gl_profile_tab[PROFILE_C_APP_ID].service_id.is_primary = true;
gl_profile_tab[PROFILE_C_APP_ID].service_id.id.inst_id = 0x00;
gl_profile_tab[PROFILE_C_APP_ID].service_id.id.uuid.len = ESP_UUID_LEN_16;
gl_profile_tab[PROFILE_C_APP_ID].service_id.id.uuid.uuid.uuid16 = GATTS_SERVICE_UUID_TEST_C;
esp_ble_gatts_create_service(gatts_if, &gl_profile_tab[PROFILE_C_APP_ID].service_id, GATTS_NUM_HANDLE_TEST_C);
break;
case ESP_GATTS_READ_EVT: {
// ESP_LOGI(GATTS_TAG, "GATT_READ_EVT, conn_id %d, trans_id %" PRIu32 ", handle %d", param->read.conn_id, param->read.trans_id, param->read.handle);
// esp_gatt_rsp_t rsp;
// memset(&rsp, 0, sizeof(esp_gatt_rsp_t));
// rsp.attr_value.handle = param->read.handle;
// rsp.attr_value.len = 4;
// rsp.attr_value.value[0] = 0xde;
// rsp.attr_value.value[1] = 0xed;
// rsp.attr_value.value[2] = 0xbe;
// rsp.attr_value.value[3] = 0xef;
// 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: {
ESP_LOGI(GATTS_TAG, "GATT_WRITE_EVT, conn_id %d, trans_id %" PRIu32 ", handle %d", param->write.conn_id, param->write.trans_id, param->write.handle);
if (!param->write.is_prep){
ESP_LOGI(GATTS_TAG, "GATT_WRITE_EVT, value len %d, value :", param->write.len);
esp_log_buffer_hex(GATTS_TAG, param->write.value, param->write.len);
update_duty(param->write.value[0]);
if (gl_profile_tab[PROFILE_C_APP_ID].descr_handle == param->write.handle && param->write.len == 2){
uint16_t descr_value= param->write.value[1]<<8 | param->write.value[0];
if (descr_value == 0x0001){
if (b_property & ESP_GATT_CHAR_PROP_BIT_NOTIFY){
ESP_LOGI(GATTS_TAG, "notify enable");
uint8_t notify_data[15];
for (int i = 0; i < sizeof(notify_data); ++i)
{
notify_data[i] = i%0xff;
}
//the size of notify_data[] need less than MTU size
esp_ble_gatts_send_indicate(gatts_if, param->write.conn_id, gl_profile_tab[PROFILE_C_APP_ID].char_handle,
sizeof(notify_data), notify_data, false);
}
}else if (descr_value == 0x0002){
if (b_property & ESP_GATT_CHAR_PROP_BIT_INDICATE){
ESP_LOGI(GATTS_TAG, "indicate enable");
uint8_t indicate_data[15];
for (int i = 0; i < sizeof(indicate_data); ++i)
{
indicate_data[i] = i%0xff;
}
//the size of indicate_data[] need less than MTU size
esp_ble_gatts_send_indicate(gatts_if, param->write.conn_id, gl_profile_tab[PROFILE_C_APP_ID].char_handle,
sizeof(indicate_data), indicate_data, true);
}
}
else if (descr_value == 0x0000){
ESP_LOGI(GATTS_TAG, "notify/indicate disable ");
}else{
ESP_LOGE(GATTS_TAG, "unknown value");
}
}
}
example_write_event_env(gatts_if, &c_prepare_write_env, param);
break;
}
case ESP_GATTS_EXEC_WRITE_EVT:
ESP_LOGI(GATTS_TAG,"ESP_GATTS_EXEC_WRITE_EVT");
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, NULL);
example_exec_write_event_env(&c_prepare_write_env, param);
break;
case ESP_GATTS_MTU_EVT:
ESP_LOGI(GATTS_TAG, "ESP_GATTS_MTU_EVT, MTU %d", param->mtu.mtu);
break;
case ESP_GATTS_UNREG_EVT:
break;
case ESP_GATTS_CREATE_EVT:
ESP_LOGI(GATTS_TAG, "CREATE_SERVICE_EVT, status %d, service_handle %d", param->create.status, param->create.service_handle);
gl_profile_tab[PROFILE_C_APP_ID].service_handle = param->create.service_handle;
gl_profile_tab[PROFILE_C_APP_ID].char_uuid.len = ESP_UUID_LEN_16;
gl_profile_tab[PROFILE_C_APP_ID].char_uuid.uuid.uuid16 = GATTS_CHAR_UUID_TEST_C;
esp_ble_gatts_start_service(gl_profile_tab[PROFILE_C_APP_ID].service_handle);
b_property = ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE | ESP_GATT_CHAR_PROP_BIT_NOTIFY;
esp_err_t add_char_ret =esp_ble_gatts_add_char( gl_profile_tab[PROFILE_C_APP_ID].service_handle, &gl_profile_tab[PROFILE_C_APP_ID].char_uuid,
ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
b_property,
NULL, NULL);
if (add_char_ret){
ESP_LOGE(GATTS_TAG, "add char failed, error code =%x",add_char_ret);
}
break;
case ESP_GATTS_ADD_INCL_SRVC_EVT:
break;
case ESP_GATTS_ADD_CHAR_EVT:
ESP_LOGI(GATTS_TAG, "ADD_CHAR_EVT, status %d, attr_handle %d, service_handle %d",
param->add_char.status, param->add_char.attr_handle, param->add_char.service_handle);
gl_profile_tab[PROFILE_C_APP_ID].char_handle = param->add_char.attr_handle;
gl_profile_tab[PROFILE_C_APP_ID].descr_uuid.len = ESP_UUID_LEN_16;
gl_profile_tab[PROFILE_C_APP_ID].descr_uuid.uuid.uuid16 = ESP_GATT_UUID_CHAR_CLIENT_CONFIG;
esp_ble_gatts_add_char_descr(gl_profile_tab[PROFILE_C_APP_ID].service_handle, &gl_profile_tab[PROFILE_C_APP_ID].descr_uuid,
ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
NULL, NULL);
break;
case ESP_GATTS_ADD_CHAR_DESCR_EVT:
gl_profile_tab[PROFILE_C_APP_ID].descr_handle = param->add_char_descr.attr_handle;
ESP_LOGI(GATTS_TAG, "ADD_DESCR_EVT, status %d, attr_handle %d, service_handle %d",
param->add_char_descr.status, param->add_char_descr.attr_handle, param->add_char_descr.service_handle);
break;
case ESP_GATTS_DELETE_EVT:
break;
case ESP_GATTS_START_EVT:
ESP_LOGI(GATTS_TAG, "SERVICE_START_EVT, status %d, service_handle %d",
param->start.status, param->start.service_handle);
break;
case ESP_GATTS_STOP_EVT:
break;
case ESP_GATTS_CONNECT_EVT:
ESP_LOGI(GATTS_TAG, "CONNECT_EVT, conn_id %d, remote %02x:%02x:%02x:%02x:%02x:%02x:",
param->connect.conn_id,
param->connect.remote_bda[0], param->connect.remote_bda[1], param->connect.remote_bda[2],
param->connect.remote_bda[3], param->connect.remote_bda[4], param->connect.remote_bda[5]);
gl_profile_tab[PROFILE_C_APP_ID].conn_id = param->connect.conn_id;
break;
case ESP_GATTS_CONF_EVT:
ESP_LOGI(GATTS_TAG, "ESP_GATTS_CONF_EVT status %d attr_handle %d", param->conf.status, param->conf.handle);
if (param->conf.status != ESP_GATT_OK){
esp_log_buffer_hex(GATTS_TAG, param->conf.value, param->conf.len);
}
break;
case ESP_GATTS_DISCONNECT_EVT:
case ESP_GATTS_OPEN_EVT:
case ESP_GATTS_CANCEL_OPEN_EVT:
case ESP_GATTS_CLOSE_EVT:
case ESP_GATTS_LISTEN_EVT:
case ESP_GATTS_CONGEST_EVT:
default:
break;
}
}
1.4、PWM初始化 与 设置
PWM用于设置恒流模块的电流大小
初始化代码
#define LEDC_TIMER LEDC_TIMER_0
#define LEDC_MODE LEDC_LOW_SPEED_MODE
#define LEDC_OUTPUT_IO (16) // Define the output GPIO
#define LEDC_CHANNEL LEDC_CHANNEL_0
#define LEDC_DUTY_RES LEDC_TIMER_13_BIT // Set duty resolution to 13 bits
#define LEDC_DUTY (4096) // Set duty to 50%. (2 ** 13) * 50% = 4096
#define LEDC_FREQUENCY (2000) // Frequency in Hertz. Set frequency at 4 kHz
static void example_ledc_init(void)
{
// Prepare and then apply the LEDC PWM timer configuration
ledc_timer_config_t ledc_timer = {
.speed_mode = LEDC_MODE,
.timer_num = LEDC_TIMER,
.duty_resolution = LEDC_DUTY_RES,
.freq_hz = LEDC_FREQUENCY, // Set output frequency at 4 kHz
.clk_cfg = LEDC_AUTO_CLK
};
ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));
// Prepare and then apply the LEDC PWM channel configuration
ledc_channel_config_t ledc_channel = {
.speed_mode = LEDC_MODE,
.channel = LEDC_CHANNEL,
.timer_sel = LEDC_TIMER,
.intr_type = LEDC_INTR_DISABLE,
.gpio_num = LEDC_OUTPUT_IO,
.duty = 0, // Set duty to W0%
.hpoint = 0
};
ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));
}
自行封装的设置PWM API
void update_duty(uint8_t duty){
// 设置占空比为50%
ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, ((8192*duty)/100));
// 更新通道占空比
ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
}
1.5、烧录与实验
添加上述所有代码后,烧录运行的效果如下:
广播
服务 与 特征
2、遥控器
遥控器是蓝牙主机,使用gatt_client示例上修改。遥控器端代码运行框图如下:
2.1、添加目标服务UUID
目标服务的短UUID,用于查找、发现服务的handle,确认后才能正常发送数据。
#define REMOTE_SERVICE_UUID 0xFF01
#define REMOTE_WRITE_CHAR_UUID 0xFF02
2.2、修改目标广播名
目标的广播名,用于筛选、确认是正确的目标设备
static const char remote_device_name[] = "xxu";
2.3、添加写数据的特征结构体
添加写数据的特征结构体 用于查找特征时,输入的目标服务的UUID。
static esp_bt_uuid_t write_char_uuid = {
.len = ESP_UUID_LEN_16,
.uuid = {.uuid16 = REMOTE_WRITE_CHAR_UUID,},
};
2.4、确认写数据的handle
确认写数据的handle,这一段非常非常重要,如果handle错误了,从机永远接收不到数据,即使API回调显示发送成功。
status = esp_ble_gattc_get_char_by_uuid( gattc_if,
p_data->search_cmpl.conn_id,
gl_profile_tab[PROFILE_A_APP_ID].service_start_handle,
gl_profile_tab[PROFILE_A_APP_ID].service_end_handle,
write_char_uuid,
char_elem_result,
&count);
if (status != ESP_GATT_OK){
ESP_LOGE(GATTC_TAG, "esp_ble_gattc_get_char_by_uuid error----");
free(char_elem_result);
char_elem_result = NULL;
break;
}
ESP_LOGE(GATTC_TAG, "write handle start %x ",char_elem_result[0].properties);
/* Every service have only one char in our 'ESP_GATTS_DEMO' demo, so we used first 'char_elem_result' */
/*ESP_GATT_CHAR_PROP_BIT_WRITE_NR*/
if (count > 0 && (char_elem_result[0].properties & ESP_GATT_CHAR_PROP_BIT_WRITE )){
write_handle = char_elem_result[0].char_handle;
ESP_LOGE(GATTC_TAG, "write handle = %x", write_handle);
}
添加在:
static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) {
。。。。略。。。。
case ESP_GATTC_SEARCH_CMPL_EVT:{
添加到这里!
}
break
。。。。略。。。。
}
2.5、自封装数据发送API
在原厂基础的API上,重新封装,简化输出的参数,用起来更方便。
static uint8_t test_data[1] = {0};
void ble_write_data(uint8_t *data,uint8_t len){
ESP_LOGE(GATTC_TAG, "len %u data %x", len, data[0]);
esp_ble_gattc_write_char( gl_profile_tab[PROFILE_A_APP_ID].gattc_if,
gl_profile_tab[PROFILE_A_APP_ID].conn_id,
write_handle,
len,
(uint8_t*)data,
ESP_GATT_WRITE_TYPE_RSP,
ESP_GATT_AUTH_REQ_NONE);
}
三、硬件简介 与 连接
本端介绍使用到的硬件 和 硬件与开发板连接的示意图
1、硬件简介
COB封装的LDE灯条,看似一颗LED其实是多颗LED封装在同一块铝基板上,如下图每一颗小黑点就是一颗LED。
LED可调恒流驱动模块
调整LED亮度的方法主要有两种:
- 调整电压:
调整电压,是通过不断的关断、导通电源,在单位时间内调整平均电压 。
通过改变LED两端的电压来调节亮度。理论上,当LED两端电压增加时,其电流也会增加,从而亮度变亮。但是LED是一种非线性器件,其伏安特性曲线比较陡峭。在正常工作电压范围内,电压的微小变化可能会导致电流的较大变化。例如,对于一个额定电压为3.2V的蓝色LED,当电压从3.2V增加到3.3V时,电流可能会从20mA增加到30mA左右,亮度会有明显变化。
所以会有亮度突变 与 频闪问题
- 调整电流
使用恒流源电路来为LED供电。恒流源可以精确地控制输出电流。常见的恒流源有线性恒流源和开关型恒流源。
线性恒流源:通过调整功率管的导通程度来控制电流。控制流过LED的电流,实现亮度调节。
开关型恒流源:利用MOS高频开关来调节电流,利用储能元件,电感、电容组成的电路,不断的导通与闭合维持LED电流的连续性。通过改变控制信号的占空比,可以调节流过LED的平均电流,从而实现亮度调节。
所以选择线性恒流源可以避免亮度突变 与 频闪问题。
2、接线图
LED可调恒流驱动模块接线示意图
整体接线图 模拟
整体接线图 实物与各模块的功用
四、Web Bluetooth
1、概念
Web Bluetooth API是一种浏览器提供的 JavaScript API,允许网页应用直接与蓝牙低功耗(BLE)设备进行通信。它使得 Web 开发者能够构建出能够与各种蓝牙设备通信的 Web 应用,从而扩展了 Web 的功能范围。
但是目前 Web Bluetooth API 规范尚未最终确定,还处于实验室验证阶段。
2、功能
- 设备扫描:可以扫描附近的蓝牙设备,发现周围可用的 BLE 设备。
- 设备连接:能够连接到指定的蓝牙设备,建立通信链路。
- 服务和特征读取:读取蓝牙设备的服务和特征值,了解设备提供的功能。
- 数据读写:可以读取和写入蓝牙设备的数据,实现对设备的控制和数据交互。
- 事件监听:可以监听蓝牙设备的状态变化和数据更新,及时响应设备的事件。
3、工作原理
- 基于 GATT 协议:Web Bluetooth API 基于通用属性配置文件(GATT)协议工作,该协议定义了设备之间进行数据交换的一种通用方式。在通信过程中,Web 应用作为 GATT 客户端,而蓝牙设备作为 GATT 服务器。
- 设备交互:通过
navigator.bluetooth
对象来发现、连接并与 BLE 设备进行通信。可以使用requestDevice
方法请求匹配特定过滤条件的 BLE 设备,然后使用gatt.connect
方法连接到设备,再通过getPrimaryService
和getCharacteristic
等方法获取所需的服务和特征,最后进行数据读写等操作。
4、优势
- 跨平台:支持多种操作系统和浏览器,如 Chrome、三星 Internet 等,具有良好的兼容性。
- 安全性:通过浏览器的安全机制,如用户授权等,确保数据传输的安全性。
- 易用性:提供简单易用的 API,降低了开发难度,方便开发者快速上手。
- 低功耗:支持 BLE 设备,降低了设备的能耗,适用于需要长时间运行的物联网设备。
5、我的Web Bluetooth
比较简单,但是能用
综上Web Bluetooth 目前可以满足一些简单的应用,仅仅跨平台这一点就非常非常香了,希望尽快推进规范落地!!
五、GIF演示
以下是多端控制灯光亮度的GIF
5.1、电脑WebBle控制
5.2、手机 Web Ble控制
5.3、蓝牙遥控器控制
六、总结
感谢贸泽 与 电子森林联合举办的本次活动,让我有幸参加。
本次的开发遇到了不少难点,虽然晚上可以查到相关的资料,但是都不太详细,所以本次记录的比较详细。