搞定蓝牙-第六篇(HID)

搞定蓝牙-第六篇(HID)如果只有左 ctrl 键按下 则返回 00000 十六进制 如果只有数字键 1 按下 则返回 00000 如果数字键 1 和 2 同时按下 则返回 0000595A000

大家好,欢迎来到IT知识分享网。

ble与HID

HOGP

我们发现,电脑连接了蓝牙键盘就可以直接使用了,不需要配置任何东西,那么,这两者是怎么通讯的呢。我们使用的电脑windows系统内置一段程序来自动识别鼠标、键盘的数据, windows系统的这个程序和鼠标、键盘内部的程序的需要使用同一套规则,两者才能正常通讯。除了鼠标、键盘还有游戏手柄、多媒体控制器等等都可以使用这套规则,这类设备叫做HID(Human Interface Device)。在ble中,我们称这套规则为HOGP(HID Over GATT Profile)。

GAPP与HID

报告地图,这个东西在USB HID那边叫做报告描述符。我们使用的键盘可能是87键、104键、108键,还有一些宏键盘是几个按键的,那么HOGP是怎么支持这些所有这些不同类型的键盘的呢?所以,键盘需要告诉系统,我有多少个键、我有多少个LED、我的键值从哪些到哪些等等。这些内容通过报告地图固定在键盘的芯片里面了。举个栗子。

//表示用途页为通用桌面设备  0x05, 0x01, // USAGE_PAGE (Generic Desktop)  //表示用途为键盘  0x09, 0x06, // USAGE (Keyboard)  //表示应用集合,必须要以END_COLLECTION来结束它,见最后的END_COLLECTION  0xa1, 0x01, // COLLECTION (Application)  //表示用途页为按键  0x05, 0x07, // USAGE_PAGE (Keyboard)  //用途最小值,这里为左ctrl键  0x19, 0xe0, // USAGE_MINIMUM (Keyboard LeftControl)  //用途最大值,这里为右GUI键,即window键  0x29, 0xe7, // USAGE_MAXIMUM (Keyboard Right GUI)  //逻辑最小值为0  0x15, 0x00, // LOGICAL_MINIMUM (0)  //逻辑最大值为1  0x25, 0x01, // LOGICAL_MAXIMUM (1)  //报告大小(即这个字段的宽度)为1bit,所以前面的逻辑最小值为0,逻辑最大值为1  0x75, 0x01, // REPORT_SIZE (1)  //报告的个数为8,即总共有8个bits  0x95, 0x08, // REPORT_COUNT (8)  //输入用,变量,值,绝对值。像键盘这类一般报告绝对值,  //而鼠标移动这样的则报告相对值,表示鼠标移动多少  0x81, 0x02, // INPUT (Data,Var,Abs)  //上面这这几项描述了一个输入用的字段,总共为8个bits,每个bit表示一个按键  //分别从左ctrl键到右GUI键。这8个bits刚好构成一个字节,它位于报告的第一个字节。  //它的最低位,即bit-0对应着左ctrl键,如果返回的数据该位为1,则表示左ctrl键被按下,  //否则,左ctrl键没有按下。最高位,即bit-7表示右GUI键的按下情况。中间的几个位,  //需要根据HID协议中规定的用途页表(HID Usage Tables)来确定。这里通常用来表示  //特殊键,例如ctrl,shift,del键等  //这样的数据段个数为1  0x95, 0x01, // REPORT_COUNT (1)  //每个段长度为8bits  0x75, 0x08, // REPORT_SIZE (8)  //输入用,常量,值,绝对值  0x81, 0x03, // INPUT (Cnst,Var,Abs)  //上面这8个bit是常量,设备必须返回0  //这样的数据段个数为5  0x95, 0x05, // REPORT_COUNT (5)  //每个段大小为1bit  0x75, 0x01, // REPORT_SIZE (1)  //用途是LED,即用来控制键盘上的LED用的,因此下面会说明它是输出用  0x05, 0x08, // USAGE_PAGE (LEDs)  //用途最小值是Num Lock,即数字键锁定灯  0x19, 0x01, // USAGE_MINIMUM (Num Lock)  //用途最大值是Kana,这个是什么灯我也不清楚^_^  0x29, 0x05, // USAGE_MAXIMUM (Kana)  //如前面所说,这个字段是输出用的,用来控制LED。变量,值,绝对值。  //1表示灯亮,0表示灯灭  0x91, 0x02, // OUTPUT (Data,Var,Abs)  //这样的数据段个数为1  0x95, 0x01, // REPORT_COUNT (1)  //每个段大小为3bits  0x75, 0x03, // REPORT_SIZE (3)  //输出用,常量,值,绝对  0x91, 0x03, // OUTPUT (Cnst,Var,Abs)  //由于要按字节对齐,而前面控制LED的只用了5个bit,  //所以后面需要附加3个不用bit,设置为常量。  //报告个数为6  0x95, 0x06, // REPORT_COUNT (6)  //每个段大小为8bits  0x75, 0x08, // REPORT_SIZE (8)  //逻辑最小值0  0x15, 0x00, // LOGICAL_MINIMUM (0)  //逻辑最大值255  0x25, 0xFF, // LOGICAL_MAXIMUM (255)  //用途页为按键  0x05, 0x07, // USAGE_PAGE (Keyboard)  //使用最小值为0  0x19, 0x00, // USAGE_MINIMUM (Reserved (no event indicated))  //使用最大值为0x65  0x29, 0x65, // USAGE_MAXIMUM (Keyboard Application)  //输入用,变量,数组,绝对值  0x81, 0x00, // INPUT (Data,Ary,Abs)  //以上定义了6个8bit宽的数组,每个8bit(即一个字节)用来表示一个按键,所以可以同时  //有6个按键按下。没有按键按下时,全部返回0。如果按下的键太多,导致键盘扫描系统  //无法区分按键时,则全部返回0x01,即6个0x01。如果有一个键按下,则这6个字节中的第一  //个字节为相应的键值(具体的值参看HID Usage Tables),如果两个键按下,则第1、2两个  //字节分别为相应的键值,以次类推。  //关集合,跟上面的对应  0xc0 // END_COLLECTION 

这个报告总共有8字节输入(一个字节ctrl~win键、一个字节led、六个字节普通按键,使用普通键可以一次发送六个),1字节输出。如果只有左ctrl键按下,则返回01 00 00 00 00 00 00 00(十六进制),如果只有数字键1 按下,则返回00 00 59 00 00 00 00 00,如果数字键1 和2 同时按下,则返回00 00 59 5A 00 00 00 00,如果再按下左shift 键,则返回02 00 59 5A 00 00 00 00,然后再释放1 键,则返回02 00 5A 00 00 00 00 00,然后全部按键释放,则返回00 00 00 00 00 00 00 00。这些数据(即报告)都是通过中断端点返回的。当按下Num Lock键时,PC会发送输出报告,从报告描述符中我们知道,Num Lock的LED对应着输出报告的最低位,当数字小键盘打开时,输出xxxxxxx1(二进制,打x的由其它的LED状态决定);当数字小键盘关闭时,输出xxxxxxx0(同前)。取出最低位就可以控制数字键锁定LED了。

ESP32程序分析

初始化

void app_main() { 
    esp_err_t ret; // Initialize NVS. 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 ); ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT)); esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); ret = esp_bt_controller_init(&bt_cfg); if (ret) { 
    ESP_LOGE(HID_DEMO_TAG, "%s initialize controller failed\n", __func__); return; } ret = esp_bt_controller_enable(ESP_BT_MODE_BLE); if (ret) { 
    ESP_LOGE(HID_DEMO_TAG, "%s enable controller failed\n", __func__); return; } ret = esp_bluedroid_init(); if (ret) { 
    ESP_LOGE(HID_DEMO_TAG, "%s init bluedroid failed\n", __func__); return; } ret = esp_bluedroid_enable(); if (ret) { 
    ESP_LOGE(HID_DEMO_TAG, "%s init bluedroid failed\n", __func__); return; } if((ret = esp_hidd_profile_init()) != ESP_OK) { 
    ESP_LOGE(HID_DEMO_TAG, "%s init bluedroid failed\n", __func__); } ///register the callback function to the gap module esp_ble_gap_register_callback(gap_event_handler); esp_hidd_register_callbacks(hidd_event_callback); /* set the security iocap & auth_req & key size & init key response key parameters to the stack*/ esp_ble_auth_req_t auth_req = ESP_LE_AUTH_BOND; //bonding with peer device after authentication esp_ble_io_cap_t iocap = ESP_IO_CAP_NONE; //set the IO capability to No output No input uint8_t key_size = 16; //the key size should be 7~16 bytes uint8_t init_key = ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK; uint8_t rsp_key = ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK; esp_ble_gap_set_security_param(ESP_BLE_SM_AUTHEN_REQ_MODE, &auth_req, sizeof(uint8_t)); esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &iocap, sizeof(uint8_t)); esp_ble_gap_set_security_param(ESP_BLE_SM_MAX_KEY_SIZE, &key_size, sizeof(uint8_t)); /* If your BLE device act as a Slave, the init_key means you hope which types of key of the master should distribute to you, and the response key means which key you can distribute to the Master; If your BLE device act as a master, the response key means you hope which types of key of the slave should distribute to you, and the init key means which key you can distribute to the slave. */ esp_ble_gap_set_security_param(ESP_BLE_SM_SET_INIT_KEY, &init_key, sizeof(uint8_t)); esp_ble_gap_set_security_param(ESP_BLE_SM_SET_RSP_KEY, &rsp_key, sizeof(uint8_t)); xTaskCreate(&hid_demo_task, "hid_task", 2048, NULL, 5, NULL); } 

从main函数可以看到,除了esp_hidd_register_callbacks(hidd_event_callback);和之前不同,其他都一样。我们主要分析这个函数。

esp_err_t esp_hidd_register_callbacks(esp_hidd_event_cb_t callbacks) { 
    esp_err_t hidd_status; if(callbacks != NULL) { 
    hidd_le_env.hidd_cb = callbacks; } else { 
    return ESP_FAIL; } if((hidd_status = hidd_register_cb()) != ESP_OK) { 
    return hidd_status; } esp_ble_gatts_app_register(BATTRAY_APP_ID); if((hidd_status = esp_ble_gatts_app_register(HIDD_APP_ID)) != ESP_OK) { 
    return hidd_status; } return hidd_status; } 

传入的参数是一个回调函数指针,传给了hidd_le_env.hidd_cb = callbacks,记住了哈,以后使用hidd_le_env.hidd_cb就是调用hidd_event_callback()这个函数;这个回调函数会在GATTS回调函数中调用(回调函数中调用回调函数,搁着套娃呢)。然后使用hidd_register_cb,如下图,这个函数注册GATTS回调函数,和之前GATTS的分析一样。

esp_err_t hidd_register_cb(void) { 
    esp_err_t status; status = esp_ble_gatts_register_callback(gatts_event_handler); return status; } 

APP注册

然后注册两个app,在GATTS的demo中只注册了一个,上面也提到,HOGP内部有两个服务,一个是电池服务、一个是HID服务。

esp_ble_gatts_app_register(BATTRAY_APP_ID); if((hidd_status = esp_ble_gatts_app_register(HIDD_APP_ID)) != ESP_OK) { 
    return hidd_status; } 

注册了APP就会进入GATTS回调函数,我们看GATTS回调函数的处理,这个和GATTS那一篇的demo一样。因为先注册的是电池APP,所以gatts_if = heart_rate_profile_tab[idx].gatts_if,所以会先进入GATTS回调函数的电池回调函数。

static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { 
    /* If event is register event, store the gatts_if for each profile */ if (event == ESP_GATTS_REG_EVT) { 
    if (param->reg.status == ESP_GATT_OK) { 
    heart_rate_profile_tab[PROFILE_APP_IDX].gatts_if = gatts_if; } else { 
    ESP_LOGI(HID_LE_PRF_TAG, "Reg app failed, app_id %04x, status %d\n", param->reg.app_id, param->reg.status); return; } } do { 
    int idx; for (idx = 0; idx < PROFILE_NUM; idx++) { 
    if (gatts_if == ESP_GATT_IF_NONE || /* ESP_GATT_IF_NONE, not specify a certain gatt_if, need to call every profile cb function */ gatts_if == heart_rate_profile_tab[idx].gatts_if) { 
    if (heart_rate_profile_tab[idx].gatts_cb) { 
    heart_rate_profile_tab[idx].gatts_cb(event, gatts_if, param); } } } } while (0); } 
case ESP_GATTS_REG_EVT: { 
    esp_ble_gap_config_local_icon (ESP_BLE_APPEARANCE_GENERIC_HID); esp_hidd_cb_param_t hidd_param; hidd_param.init_finish.state = param->reg.status; if(param->reg.app_id == HIDD_APP_ID) { 
    hidd_le_env.gatt_if = gatts_if; if(hidd_le_env.hidd_cb != NULL) { 
    (hidd_le_env.hidd_cb)(ESP_HIDD_EVENT_REG_FINISH, &hidd_param); hidd_le_create_service(hidd_le_env.gatt_if); } } if(param->reg.app_id == BATTRAY_APP_ID) { 
    hidd_param.init_finish.gatts_if = gatts_if; if(hidd_le_env.hidd_cb != NULL) { 
    (hidd_le_env.hidd_cb)(ESP_BAT_EVENT_REG, &hidd_param); } } break; 

创建属性表并开启服务

首先创建的是电池属性表。属性表是这个bas_att_db。创建完成后会进入esp_hidd_prf_cb_hdl的ESP_GATTS_CREAT_ATTR_TAB_EVT事件,因为创建的是电池属性表,会调用esp_ble_gatts_create_attr_tab(hidd_le_gatt_db, gatts_if, HIDD_LE_IDX_NB, 0);创建HID属性表。然后由进入这个函数的这个事件执行两行代码

hid_add_id_tbl(); esp_ble_gatts_start_service(hidd_le_env.hidd_inst.att_tbl[HIDD_LE_IDX_SVC]); 

发送HID消息

void hid_dev_send_report(esp_gatt_if_t gatts_if, uint16_t conn_id, uint8_t id, uint8_t type, uint8_t length, uint8_t *data) { 
    hid_report_map_t *p_rpt; // get att handle for report if ((p_rpt = hid_dev_rpt_by_id(id, type)) != NULL) { 
    // if notifications are enabled ESP_LOGD(HID_LE_PRF_TAG, "%s(), send the report, handle = %d", __func__, p_rpt->handle); esp_ble_gatts_send_indicate(gatts_if, conn_id, p_rpt->handle, length, data, false); } return; } 

从上面的程序可以看到,先找到发送报告的ID(依靠上面创建的ID表来查),然后使用GATTS发送一个通知。所以,HID的数据交互最终也是通过GATTS来传输的,也就是这个协议名为什么叫HID Over GATTS Profile。

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/115457.html

(0)
上一篇 2025-12-03 08:20
下一篇 2025-12-03 08:33

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信