热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

蓝牙:GATT,属性,特性,服务

接着上一篇。通用属性配置文件(GenericAttributeProfile)1.GATT简介通用属性配置文件GenericAttributePr

接着上一篇。


通用属性配置文件(Generic Attribute Profile)


1.GATT简介

通用属性配置文件Generic Attribute Profile简称GATT。
GATT定义了属性类型并规定了如何使用,包括了一个数据传输和存储的框架和一些基本操作。中间包含了一些概念如特性characteristics,服务services等,在后面介绍。同时还定义了发现服务,特性和服务间的连接的处理过程,也包括读写特性值。


1.1.GATT的角色

GATT中定义了2种角色:服务器server和客户端client.
GATT的服务器是指提供数据的设备,而GATT的客户端是指通过GATT的服务器获取数据的设备。


1.2.GATT的层次

GATT服务器通过一个属性表来组织发送的数据。这里的关系大概是这样:
这里写图片描述
一个Profile中可包含一个或者多个服务;一个服务可包含一个或者多个特性(逻辑上的集合);一个特性至少包含两个属性:一个用于声明,其他用于存储属性值。


2.属性(Attribute)

一个属性包括句柄,UUID,值。
句柄是属性在GATT表中的索引,在一个设备中每个属性的句柄是唯一的。UUID包括属性表中的数据类型信息,它是理解属性表中值的每一个字节的关键信息。GATT表中可能有多个属性拥有相同的UUID值。
属性相关的数据结构:

/**@brief 属性元数据. */
typedef struct
{ble_gap_conn_sec_mode_t read_perm; /**<读权限 */ble_gap_conn_sec_mode_t write_perm; /**<写权限 */uint8_t vlen :1; /**<属性长度. */uint8_t vloc :2; /**<值存放的位置, 详情参照BLE_GATTS_VLOCS.*/uint8_t rd_auth :1; /**<读许可,该值会在每次读操作后向应用重新发起请求*/ uint8_t wr_auth :1; /**<写许可会在每次读操作后向应用重新发起请求(但不写命令). */
} ble_gatts_attr_md_t;/**@brief GATT 属性. */
typedef struct
{ble_uuid_t* p_uuid; /**<指向属性的UUID */ble_gatts_attr_md_t* p_attr_md; /**<指向属性元数据的数据结构*/uint16_t init_len; /**<初始化属性长度 */uint16_t init_offs; /**<初始化属性偏移 */uint16_t max_len; /**<最大属性长度,参照BLE_GATTS_ATTR_LENS_MAX*/uint8_t* p_value; /**<指向属性数据。请注意如果 BLE_GATTS_VLOC_USER值的位置被选在attribute metadata结构体中,则需指向有效生命周期缓冲区。协议栈可能在没有得到应用程序许可的情况下直接操作 */
} ble_gatts_attr_t;/**@brief GATT 属性内容. */
typedef struct
{ble_uuid_t srvc_uuid; /**<服务UUID */ble_uuid_t char_uuid; /**<特性的UUID(BLE_UUID_TYPE_UNKNOWN无效). */ble_uuid_t desc_uuid; /**<描述UUID(BLE_UUID_TYPE_UNKNOWN无效). */uint16_t srvc_handle; /**<服务句柄 */uint16_t value_handle; /**<特性的处理句柄(BLE_GATT_HANDLE_INVALID 无效). */uint8_t type; /**<属性类型, 参照BLE_GATTS_ATTR_TYPES. */
} ble_gatts_attr_context_t;

描述符:
在任何特性中的属性不是定义为属性值就是描述符。描述符是一个额外的属性,以提供更多特性的信息。这里有个特殊的描述符:客户端特性配置描述符cccd。这个描述符是给任意支持通知或指示功能的特性额外添加的。在cccd中写入1为使能通知功能,写入2使能指示功能,写0禁止通知和指示功能。

UUID:
在GATT规范中定义所有的属性都必须要有一个UUID值,UUID是全球唯一的128位的数据,用来识别不同的特性。
蓝牙核心规范制定了两种不同的UUID,一种是基本的UUID,一种是代替基本UUID的16为UUID。
我们协议栈的后面那里使用的就是代替基本UUID的16为UUID:先增加一个特定的基本UUID,在定义一个十六位的UUID。加载到基本UUID之上。源码中UUID如下:

#define LBS_UUID_BASE {0x23, 0xD1, 0xBC, 0xEA, 0x5F, 0x78, 0x23, 0x15, 0xDE, 0xEF, 0x12, 0x12, 0x00, 0x00, 0x00, 0x00}
#define LBS_UUID_SERVICE 0x1523
#define LBS_UUID_LED_CHAR 0x1525
#define LBS_UUID_BUTTON_CHAR 0x1524
//添加一个特定的基本UUID
ble_uuid128_t base_uuid = {LBS_UUID_BASE};
err_code = sd_ble_uuid_vs_add(&base_uuid, &p_lbs->uuid_type);

UUID数据结构:

/** @brief 128 bit UUID values. */
typedef struct
{ unsigned char uuid128[16];
} ble_uuid128_t;/** @brief Bluetooth Low Energy UUID type, encapsulates both 16-bit and 128-bit UUIDs. */
typedef struct
{uint16_t uuid; /**<16-bit UUID value or octets 12-13 of 128-bit UUID. */uint8_t type; /** @ref BLE_UUID_TYPES. If type is BLE_UUID_TYPE_UNKNOWN, the value of uuid is undefined. */
} ble_uuid_t;

3.特性(Characteristics)

一个特性至少包含2个属性:一个用于声明,一个用于存储属性值。通过GATT服务传输的数据必须映射成一系列的特性。可以把特性中的数据看成是一个个捆绑起来的数据,每个特性就是一个数据点。源码中特性的数据结构如下:

/**@brief GATT 特性表示格式 */
typedef struct
{uint8_t format; /**<值的格式,参照BLE_GATT_CPF_FORMATS. */int8_t exponent; /**<整数类型的指数 */uint16_t unit; /**<蓝牙中分配的UUID*/uint8_t name_space; /**<从蓝牙分配的命名空间,参见BLE_GATT_CPF_NAMESPACES.*/uint16_t desc; /**<从蓝牙分配的命名空间描述,参见BLE_GATT_CPF_NAMESPACES.*/
} ble_gatts_char_pf_t;/**@brief GATT 特性性质 */
typedef struct
{/* 标准性质 */uint8_t broadcast :1; /**<是否允许广播 */uint8_t read :1; /**<是否允许读操作 */uint8_t write_wo_resp :1; /**<是否允许命令写修改操作 */uint8_t write :1; /**<是否允许请求写修改操作*/uint8_t notify :1; /**<是否允许通知修改 */uint8_t indicate :1; /**<是否允许指示修改 */uint8_t auth_signed_wr :1; /**<是否允许签名写命令写入*/
} ble_gatt_char_props_t;/**@brief GATT 特性元数据. */
typedef struct
{ble_gatt_char_props_t char_props; /**<特性性质 */ble_gatt_char_ext_props_t char_ext_props; /**<特性拓展性质 */uint8_t* p_char_user_desc; /**<指向UTF-8, NULL则无要求 */uint16_t char_user_desc_max_size; /**<用户描述符的最大字节长 */uint16_t char_user_desc_size; /**<用户描述字节, 必须小于等于char_user_desc_max_size */ ble_gatts_char_pf_t* p_char_pf; /**<指向现存的结构体格式,NULL则对描述符不做要求 */ble_gatts_attr_md_t* p_user_desc_md; /**<用户描述符的Attribute元数据,NULL为默认值 */ble_gatts_attr_md_t* p_cccd_md; /**ble_gatts_attr_md_t* p_sccd_md; /**
} ble_gatts_char_md_t;/**@brief GATT 特性定义句柄 */
typedef struct
{uint16_t value_handle; /**<处理特征值句柄 */uint16_t user_desc_handle; /**<处理用户描述符的句柄,BLE_GATT_HANDLE_INVALID为不存在 */uint16_t cccd_handle; /**<处理CCCD的句柄,BLE_GATT_HANDLE_INVALID 为不存在*/uint16_t sccd_handle; /**<处理SCCD的句柄,BLE_GATT_HANDLE_INVALID 为不存在. */
} ble_gatts_char_handles_t;

ble_gatts_char_md_t结构体中的ble_gatt_char_props_t和ble_gatt_char_ext_props_t类型中记录了GATT的标准和拓展特性。典型的特性包括:
读:GATT客户端可以从GATT服务器中读取特性值
写和没有回应的写:允许GATT客户端写入一个值到GATT服务器的特性中。没有回应的写没有任何应用层上的确认或回应。
通知:允许GATT服务器在某个特性该改变的时候对GATT客户端进行提醒。
指示:允许GATT服务器在某个特性该改变的时候对GATT客户端进行提醒,并在应用层上确认。
广播。


4.服务(serive)

一个服务包括一个或者多个特性,这些特性是逻辑上的集合体。


5.源码部分分析

主程序中这些部分主要是对GATT相关内容的操作。这里的蓝牙在GATT中是作为服务器的角色。


5.1.连接参数

conn_params_init(); //初始化连接参数static void conn_params_init(void)
{uint32_t err_code;ble_conn_params_init_t cp_init;memset(&cp_init, 0, sizeof(cp_init));cp_init.p_conn_params = NULL;cp_init.first_conn_params_update_delay = FIRST_CONN_PARAMS_UPDATE_DELAY;cp_init.next_conn_params_update_delay = NEXT_CONN_PARAMS_UPDATE_DELAY;cp_init.max_conn_params_update_count = MAX_CONN_PARAMS_UPDATE_COUNT;cp_init.start_on_notify_cccd_handle = BLE_GATT_HANDLE_INVALID;cp_init.disconnect_on_fail = false;cp_init.evt_handler = on_conn_params_evt;cp_init.error_handler = conn_params_error_handler;err_code = ble_conn_params_init(&cp_init); //初始化APP_ERROR_CHECK(err_code);
}uint32_t ble_conn_params_init(const ble_conn_params_init_t * p_init)
{uint32_t err_code;m_conn_params_cOnfig= *p_init;m_change_param = false;if (p_init->p_conn_params != NULL){m_preferred_conn_params = *p_init->p_conn_params; // 设置栈的连接参数err_code = sd_ble_gap_ppcp_set(&m_preferred_conn_params);if (err_code != NRF_SUCCESS){return err_code;}}else{// 从栈上获取连接参数err_code = sd_ble_gap_ppcp_get(&m_preferred_conn_params);if (err_code != NRF_SUCCESS){return err_code;}}m_conn_handle = BLE_CONN_HANDLE_INVALID;m_update_count = 0;return app_timer_create(&m_conn_params_timer_id,APP_TIMER_MODE_SINGLE_SHOT,update_timeout_handler);
}

该函数中定义了一个连接参数结构体数据cp_init,用来保存连接参数。再调用ble_conn_params_init函数进行初始化,同时创建一个定时器。


5.2.自定义服务

该程序实现一个按键通知LED点亮的功能。
该部分程序是在services_init函数中添加的

typedef struct
{ble_lbs_led_write_handler_t led_write_handler; /* 当LED的特性被写时调用的回调函数 */
} ble_lbs_init_t;static void services_init(void)
{uint32_t err_code;ble_lbs_init_t init;init.led_write_handler = led_write_handler;err_code = ble_lbs_init(&m_lbs, &init);APP_ERROR_CHECK(err_code);
}

在函数职工定义了一个init变量,然后绑定到led_write_handler这个回调函数上,再调用ble_lbs_init进行初始化。在该函数中必须要添加服务,得到服务句柄。

uint32_t ble_lbs_init(ble_lbs_t * p_lbs, const ble_lbs_init_t * p_lbs_init)
{uint32_t err_code;ble_uuid_t ble_uuid;// 初始化服务结构体p_lbs->conn_handle = BLE_CONN_HANDLE_INVALID;p_lbs->led_write_handler = p_lbs_init->led_write_handler;// 添加服务ble_uuid128_t base_uuid = {LBS_UUID_BASE};err_code = sd_ble_uuid_vs_add(&base_uuid, &p_lbs->uuid_type); //绑定baseUUIDif (err_code != NRF_SUCCESS){return err_code;}ble_uuid.type = p_lbs->uuid_type;ble_uuid.uuid = LBS_UUID_SERVICE;//这里是添加server服务UUID为0x1523,并得到一个唯一的句柄err_code = sd_ble_gatts_service_add(BLE_GATTS_SRVC_TYPE_PRIMARY, &ble_uuid, &p_lbs->service_handle);if (err_code != NRF_SUCCESS){return err_code;}//这里是添加button服务 UUID为0x1524err_code = button_char_add(p_lbs, p_lbs_init);if (err_code != NRF_SUCCESS){return err_code;}//这里是添加led服务 UUID为0x1525err_code = led_char_add(p_lbs, p_lbs_init);if (err_code != NRF_SUCCESS){return err_code;}return NRF_SUCCESS;
}

下面举例button_char_add()。led_char_add函数的过程也与此类似。

static uint32_t button_char_add(ble_lbs_t * p_lbs, const ble_lbs_init_t * p_lbs_init)
{ble_gatts_char_md_t char_md;ble_gatts_attr_md_t cccd_md;ble_gatts_attr_t attr_char_value;ble_uuid_t ble_uuid;ble_gatts_attr_md_t attr_md;memset(&cccd_md, 0, sizeof(cccd_md));//设置声明属性值BLE_GAP_CONN_SEC_MODE_SET_OPEN(&cccd_md.read_perm);BLE_GAP_CONN_SEC_MODE_SET_OPEN(&cccd_md.write_perm);cccd_md.vloc = BLE_GATTS_VLOC_STACK; //属性值分配在栈内存上memset(&char_md, 0, sizeof(char_md));//设置特性值char_md.char_props.read = 1;char_md.char_props.notify = 1;char_md.p_char_user_desc = NULL;char_md.p_char_pf = NULL;char_md.p_user_desc_md = NULL;char_md.p_cccd_md = &cccd_md;char_md.p_sccd_md = NULL;//设置按键UUIDble_uuid.type = p_lbs->uuid_type;ble_uuid.uuid = LBS_UUID_BUTTON_CHAR;memset(&attr_md, 0, sizeof(attr_md));//设置自定义属性值BLE_GAP_CONN_SEC_MODE_SET_OPEN(&attr_md.read_perm);BLE_GAP_CONN_SEC_MODE_SET_NO_ACCESS(&attr_md.write_perm);attr_md.vloc = BLE_GATTS_VLOC_STACK;attr_md.rd_auth = 0;attr_md.wr_auth = 0;attr_md.vlen = 0;memset(&attr_char_value, 0, sizeof(attr_char_value));//设置属性值attr_char_value.p_uuid = &ble_uuid;attr_char_value.p_attr_md = &attr_md;attr_char_value.init_len = sizeof(uint8_t);attr_char_value.init_offs = 0;attr_char_value.max_len = sizeof(uint8_t);attr_char_value.p_value = NULL;//添加按键服务return sd_ble_gatts_characteristic_add(p_lbs->service_handle, &char_md,&attr_char_value,&p_lbs->button_char_handles);
}

sd_ble_gatts_characteristic_add函数用来添加特性。第一个参数是要添加到的服务的句柄,第二个参数是特性值,在它上面绑定一个cccd的属性元数据(描述符),第三个参数值属性的描述,中间绑定了ble按键的UUID值和按键属性的元数据,第四个参数是返回的按键的特性和描述符的唯一句柄。

前面大概讲过,当协议栈启动之后,BLE的事件响应在初始化的时候被绑定到ble_evt_dispatch函数中了。而在这个函数中有一个ble_lbs_on_ble_evt的事件监测函数,代码如下:

void ble_lbs_on_ble_evt(ble_lbs_t * p_lbs, ble_evt_t * p_ble_evt)
{switch (p_ble_evt->header.evt_id){case BLE_GAP_EVT_CONNECTED:on_connect(p_lbs, p_ble_evt);break;case BLE_GAP_EVT_DISCONNECTED:on_disconnect(p_lbs, p_ble_evt);break;case BLE_GATTS_EVT_WRITE:on_write(p_lbs, p_ble_evt);break;default:break;}
}
static void on_disconnect(ble_lbs_t * p_lbs, ble_evt_t * p_ble_evt)
{UNUSED_PARAMETER(p_ble_evt);p_lbs->conn_handle = BLE_CONN_HANDLE_INVALID;
}
static void on_connect(ble_lbs_t * p_lbs, ble_evt_t * p_ble_evt)
{p_lbs->conn_handle = p_ble_evt->evt.gap_evt.conn_handle;
}
//写入回调函数,在初始化的时候别绑定。当有写入时回调
static void led_write_handler(ble_lbs_t * p_lbs, uint8_t led_state)
{if (led_state){nrf_gpio_pin_set(LEDBUTTON_LED_PIN_NO);}else{nrf_gpio_pin_clear(LEDBUTTON_LED_PIN_NO);}
}

连接事件和断开事件调用函数on_connect和on_disconnect,和官方提供的on_connect和on_disconnect处理差不多。

typedef struct
{ble_evt_hdr_t header; /**union{ble_common_evt_t common_evt; /**ble_gap_evt_t gap_evt; /**ble_l2cap_evt_t l2cap_evt; /**ble_gattc_evt_t gattc_evt; /**ble_gatts_evt_t gatts_evt; /**} evt;
} ble_evt_t;static void on_write(ble_lbs_t * p_lbs, ble_evt_t * p_ble_evt)
{ble_gatts_evt_write_t * p_evt_write = &p_ble_evt->evt.gatts_evt.params.write;if ((p_evt_write->handle == p_lbs->led_char_handles.value_handle) &&(p_evt_write->len == 1) &&(p_lbs->led_write_handler != NULL)){p_lbs->led_write_handler(p_lbs, p_evt_write->data[0]);}
}

这里会从事件结构体ble_evt_t中获取(强制转换)GATTS的事件信息(ble_gatts_evt_write_t结构体),然后根据写入的信息完成回调函数的调用。至此为一个服务调用的流程。


推荐阅读
author-avatar
捕鱼达人2602884285
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有