GATT(Generic Attributes Profile)的缩写,中文是通用属性协议,是已连接的低功耗蓝牙设备之间进行通信的协议。
一旦两个设备建立起了连接,GATT 就开始起作用了,这也意味着,你必需完成前面的GAP协议。
GATT使用了 ATT(Attribute Protocol)协议,ATT 协议把 Service,Characteristic 对应的数据保存在一个查找表中,查找表使用 16bit ID 作为每一项的索引。
GATT定义的多层数据结构简要概括起来就是 服务(Service) 可以包含多个 特征(Characteristic),每个特征包含 属性(Properties) 和 值(Value),还可以包含多个 描述(Descriptor)。
属性协议层 负责数据检索,允许一个设备暴露一些数据块给其他设备,其他设备称之为“属性”。
在ATT环境中,展示属性的设备称之为服务器,与它配对的设备称之为客户端。链路层的主机从机和这里的服务器、客服端是两种概念,主设备既可以是服务器,也可以是客户端。从设备毅然。
从GATT的角度来看,处于连接状态时的两个设备,它们各自充当两种角色中的一种:
服务端(Server)
包含被GATT客户端读取或写入的特征数据的设备。
客户端(Client)
从GATT服务器中读取数据或向GATT服务器写入数据的设备。
外围设备(从机)作为 GATT 服务端(Server),它维持了 ATT 的查找表以及 service 和 characteristic 的定义;
客户端和服务器的GATT角色独立于外围设备和中央设备的GAP角色。外围设备可以是GATT客户端或GATT服务器,中心可以是GATT客户端或GATT服务器。
在主函数main.c文件中,编写LED服务客户端初始化函数 lbs_c_init(),它的主要工作就是对客户端进行初始化,并声明一个LED服务客户端事件回调函数 lbs_c_evt_handler。
/**@brief LED Button client initialization.
*/
static void lbs_c_init(void)
{
ret_code_t err_code;
ble_lbs_c_init_t lbs_c_init_obj;
lbs_c_init_obj.evt_handler = lbs_c_evt_handler;
err_code = ble_lbs_c_init(&m_ble_lbs_c, &lbs_c_init_obj);
APP_ERROR_CHECK(err_code);
}
/**@brief Handles events coming from the LED Button central module.
*/
static void lbs_c_evt_handler(ble_lbs_c_t * p_lbs_c, ble_lbs_c_evt_t * p_lbs_c_evt)
{
switch (p_lbs_c_evt->evt_type)
{
case BLE_LBS_C_EVT_DISCOVERY_COMPLETE:
{
ret_code_t err_code;
err_code = ble_lbs_c_handles_assign(&m_ble_lbs_c,
p_lbs_c_evt->conn_handle,
&p_lbs_c_evt->params.peer_db);
NRF_LOG_INFO("LED Button service discovered on conn_handle 0x%x.", p_lbs_c_evt->conn_handle);
err_code = app_button_enable();
APP_ERROR_CHECK(err_code);
// LED Button service discovered. Enable notification of Button.
err_code = ble_lbs_c_button_notif_enable(p_lbs_c);
APP_ERROR_CHECK(err_code);
} break; // BLE_LBS_C_EVT_DISCOVERY_COMPLETE
case BLE_LBS_C_EVT_BUTTON_NOTIFICATION:
{
NRF_LOG_INFO("Button state changed on peer to 0x%x.", p_lbs_c_evt->params.button.button_state);
if (p_lbs_c_evt->params.button.button_state)
{
bsp_board_led_on(LEDBUTTON_LED);
}
else
{
bsp_board_led_off(LEDBUTTON_LED);
}
} break; // BLE_LBS_C_EVT_BUTTON_NOTIFICATION
default:
// No implementation needed.
break;
}
}
对于主机客户端初始化,在 ble_lbs_c.c 文件中编写。这个函数专门声明主机客户端的相关参数。服务发现只能发现 16bit 的UUID,包含主服务UUID,特征UUID。但是发现库函数并不能发现 128bit 的UUID,因此要正确进行主从设备的服务交换,对于私有服务,主机必须在初始化客户端时声明基础UUID。并清空一系列的句柄,注册UUID类型、主服务UUID的发现模块,用于对比发现主服务UUID。
uint32_t ble_lbs_c_init(ble_lbs_c_t * p_ble_lbs_c, ble_lbs_c_init_t * p_ble_lbs_c_init)
{
uint32_t err_code;
ble_uuid_t lbs_uuid;
ble_uuid128_t lbs_base_uuid = {LBS_UUID_BASE};// 基础UUID
VERIFY_PARAM_NOT_NULL(p_ble_lbs_c);
VERIFY_PARAM_NOT_NULL(p_ble_lbs_c_init);
VERIFY_PARAM_NOT_NULL(p_ble_lbs_c_init->evt_handler);
p_ble_lbs_c->peer_lbs_db.button_cccd_handle = BLE_GATT_HANDLE_INVALID;
p_ble_lbs_c->peer_lbs_db.button_handle = BLE_GATT_HANDLE_INVALID;
p_ble_lbs_c->peer_lbs_db.led_handle = BLE_GATT_HANDLE_INVALID;
p_ble_lbs_c->conn_handle = BLE_CONN_HANDLE_INVALID;// 清空连接句柄
p_ble_lbs_c->evt_handler = p_ble_lbs_c_init->evt_handler;// 分配事件
// 添加基础UUID
err_code = sd_ble_uuid_vs_add(&lbs_base_uuid, &p_ble_lbs_c->uuid_type);
if (err_code != NRF_SUCCESS)
{
return err_code;
}
VERIFY_SUCCESS(err_code);
lbs_uuid.type = p_ble_lbs_c->uuid_type;// 主服务UUID类型
lbs_uuid.uuid = LBS_UUID_SERVICE;// 主服务UUID
// 用于注册DB发现模块的函数
return ble_db_discovery_evt_register(&lbs_uuid);
}
在main.c中,在数据发现初始化函数中,设置了数据发现中断函数db_disc_handler()。
/**@brief Database discovery initialization.
*/
static void db_discovery_init(void)
{
ret_code_t err_code = ble_db_discovery_init(db_disc_handler);
APP_ERROR_CHECK(err_code);
}
/**@brief Function for handling database discovery events.
*
* @details This function is callback function to handle events from the database discovery module.
* Depending on the UUIDs that are discovered, this function should forward the events
* to their respective services.
*
* @param[in] p_event Pointer to the database discovery event.
*/
static void db_disc_handler(ble_db_discovery_evt_t * p_evt)
{
ble_lbs_on_db_disc_evt(&m_ble_lbs_c, p_evt);
}
数据发现中断处理函数ble_lbs_on_db_disc_evt的主要功能就是当数据发现标志 BLE_DB_DISCOERY_COMPLETE 完成后,会触发 BLE_LBS_C_EVT_DISCOVERY_COMPLETE LED服务客户端发现完成事件。
void ble_lbs_on_db_disc_evt(ble_lbs_c_t * p_ble_lbs_c, ble_db_discovery_evt_t const * p_evt)
{
// Check if the Led Button Service was discovered.
if (p_evt->evt_type == BLE_DB_DISCOVERY_COMPLETE &&
p_evt->params.discovered_db.srv_uuid.uuid == LBS_UUID_SERVICE &&
p_evt->params.discovered_db.srv_uuid.type == p_ble_lbs_c->uuid_type)
{
ble_lbs_c_evt_t evt;
evt.evt_type = BLE_LBS_C_EVT_DISCOVERY_COMPLETE;
evt.conn_handle = p_evt->conn_handle;
for (uint32_t i = 0; i < p_evt->params.discovered_db.char_count; i++)
{
const ble_gatt_db_char_t * p_char = &(p_evt->params.discovered_db.charateristics[i]);
switch (p_char->characteristic.uuid.uuid)
{
case LBS_UUID_LED_CHAR:
evt.params.peer_db.led_handle = p_char->characteristic.handle_value;
break;
case LBS_UUID_BUTTON_CHAR:
evt.params.peer_db.button_handle = p_char->characteristic.handle_value;
evt.params.peer_db.button_cccd_handle = p_char->cccd_handle;
break;
default:
break;
}
}
NRF_LOG_DEBUG("Led Button Service discovered at peer.");
//If the instance has been assigned prior to db_discovery, assign the db_handles
if (p_ble_lbs_c->conn_handle != BLE_CONN_HANDLE_INVALID)
{
if ((p_ble_lbs_c->peer_lbs_db.led_handle == BLE_GATT_HANDLE_INVALID)&&
(p_ble_lbs_c->peer_lbs_db.button_handle == BLE_GATT_HANDLE_INVALID)&&
(p_ble_lbs_c->peer_lbs_db.button_cccd_handle == BLE_GATT_HANDLE_INVALID))
{
p_ble_lbs_c->peer_lbs_db = evt.params.peer_db;
}
}
p_ble_lbs_c->evt_handler(p_ble_lbs_c, &evt);
}
}
一旦我们主机发现我们的从机,并成功连接之后,会进入 BLE_GAP_EVT_CONNECTED 状态。 在这个状态下,我们就需要开始我们的服务发现了,调用ble_db_discovery_start()函数开始发现服务。
//******************************************************************
// fn : ble_evt_handler
//
// brief : BLE事件回调
// details : 包含以下几种事件类型:COMMON、GAP、GATT Client、GATT Server、L2CAP
//
// param : ble_evt_t 事件类型
// p_context 未使用
//
// return : none
static void ble_evt_handler(ble_evt_t const * p_ble_evt, void * p_context)
{
ret_code_t err_code;
ble_gap_evt_t const * p_gap_evt = &p_ble_evt->evt.gap_evt;
ble_gap_evt_connected_t const * p_connected_evt = &p_gap_evt->params.connected;
switch (p_ble_evt->header.evt_id)
{
// 连接
case BLE_GAP_EVT_CONNECTED:
NRF_LOG_INFO("Connected. conn_DevAddr: %s\nConnected. conn_handle: 0x%04x\nConnected. conn_Param: %d,%d,%d,%d",
Util_convertBdAddr2Str((uint8_t*)p_connected_evt->peer_addr.addr),
p_gap_evt->conn_handle,
p_connected_evt->conn_params.min_conn_interval,
p_connected_evt->conn_params.max_conn_interval,
p_connected_evt->conn_params.slave_latency,
p_connected_evt->conn_params.conn_sup_timeout
);
m_conn_handle = p_gap_evt->conn_handle;
err_code = ble_led_c_handles_assign(&m_ble_led_c, m_conn_handle, NULL);
APP_ERROR_CHECK(err_code);
// 开始发现服务,NUS客户端等待发现结果
err_code = ble_db_discovery_start(&m_db_disc, p_ble_evt->evt.gap_evt.conn_handle);
APP_ERROR_CHECK(err_code);
break;
当成功发现服务之后,会进入 db_disc_handler 回调函数,在这个回调函数之中,因为我们这个工程仅需要处理led的服务,所以我们调用 ble_led_c_on_db_disc_evt 去发现led相关的特征值内容,其中会携带我们的ble_db_discovery_evt_t参数(底层返回的所有和服务数据库相关的信息都在这个参数里面)。
//******************************************************************
// fn : db_disc_handler
//
// brief : 用于处理数据库发现事件的函数
// details : 此函数是一个回调函数,用于处理来自数据库发现模块的事件。
// 根据发现的UUID,此功能将事件转发到各自的服务。
//
// param : p_event -> 指向数据库发现事件的指针
//
// return : none
static void db_disc_handler(ble_db_discovery_evt_t * p_evt)
{
ble_led_c_on_db_disc_evt(&m_ble_led_c, p_evt);
}
所以接下来,我们需要先判断一下,底层返回的ble_db_discovery_evt_t中携带的类型是否是BLE_DB_DISCOVERY_COMPLETE,也就是数据库成功的完成发现,且发现的UUID是LED_UUID_SERVICE。
当我们一切都是按照正确的流程跑完,可以看到在这个函数的最后,它会给我们返回一个p_ble_led_c->evt_handler(p_ble_led_c, &evt);,也就是向mian.c文件中给我们一个回调(ble_led_c_init初始化函数时注册的回调),其中携带的任务参数类型是BLE_LED_C_EVT_DISCOVERY_COMPLETE。
//******************************************************************************
// fn :ble_led_c_on_db_disc_evt
//
// brief : 处理led服务发现的函数
//
// param : p_ble_led_c -> 指向LED客户端结构的指针
// p_evt -> 指向从数据库发现模块接收到的事件的指针
//
// return : none
void ble_led_c_on_db_disc_evt(ble_led_c_t * p_ble_led_c, ble_db_discovery_evt_t const * p_evt)
{
// 判断LED服务是否发现完成
if (p_evt->evt_type == BLE_DB_DISCOVERY_COMPLETE &&
p_evt->params.discovered_db.srv_uuid.uuid == LED_UUID_SERVICE &&
p_evt->params.discovered_db.srv_uuid.type == p_ble_led_c->uuid_type)
{
ble_led_c_evt_t evt;
evt.evt_type = BLE_LED_C_EVT_DISCOVERY_COMPLETE;
evt.conn_handle = p_evt->conn_handle;
for (uint32_t i = 0; i < p_evt->params.discovered_db.char_count; i++)
{
const ble_gatt_db_char_t * p_char = &(p_evt->params.discovered_db.charateristics[i]);
switch (p_char->characteristic.uuid.uuid)
{
// 根据LED特征值的UUID,获取我们句柄handle_value
case LED_UUID_CHAR:
evt.params.peer_db.led_handle = p_char->characteristic.handle_value;
break;
default:
break;
}
}
NRF_LOG_DEBUG("Led Button Service discovered at peer.");
// 如果实例是在db_discovery之前分配的,则分配db_handles
if (p_ble_led_c->conn_handle != BLE_CONN_HANDLE_INVALID)
{
if (p_ble_led_c->peer_led_db.led_handle == BLE_GATT_HANDLE_INVALID)
{
p_ble_led_c->peer_led_db = evt.params.peer_db;
}
}
p_ble_led_c->evt_handler(p_ble_led_c, &evt);
}
}
那么接下来,我们再去看一下mian.c中此回调函数下的处理。
在ble_led_c_evt_handler回调函数下,我们判断传入的事件类型,可以看到正是刚刚的 BLE_LED_C_EVT_DISCOVERY_COMPLETE 事件,也就是代表我们已经成功的获取了我们指定服务(LED_UUID_SERVICE)下的指定特征值(LED_UUID_CHAR)的句柄(handle_value)。
然后我们调用ble_led_c_handles_assign函数,去将我们的连接句柄connHandle以及特征值句柄handle_value,绑定给p_ble_led_c实例。
//******************************************************************
// fn : ble_led_c_evt_handler
//
// brief : LED服务事件
//
// param : none
//
// return : none
static void ble_led_c_evt_handler(ble_led_c_t * p_ble_led_c, ble_led_c_evt_t * p_evt)
{
ret_code_t err_code;
switch (p_evt->evt_type)
{
case BLE_LED_C_EVT_DISCOVERY_COMPLETE:
NRF_LOG_INFO("Discovery complete.");
err_code = ble_led_c_handles_assign(&m_ble_led_c, p_evt->conn_handle, &p_evt->params.peer_db);
APP_ERROR_CHECK(err_code);
break;
default:
break;
}
}
在上面LED服务发现函数 ble_led_c_on_db_disc_evt() 中,如果确实成功的发现我们的LED服务,接下来我们就需要从服务中取出我们需要的特征值,也就是 LED_UUID_CHAR。我们需要从这个特征值当中获取我们用于通信的句柄(handle_value)。
或者我们可以通过判断该特征是否有写权限,来获取句柄。
当上述的流程都正确跑完,我们就可以进行最后一步的行动,也就是发送数据,在这个例程当中我们是利用按键触发来发送对应的LED的状态变化。 我们到mian.c中,查看按键触发会调用的btn_evt_handler_t回调函数,在这个函数中,我们最后会调用LED服务数据发送的功能函数ble_led_led_status_send。当上述的流程都正确跑完,我们就可以进行最后一步的行动,也就是发送数据,在这个例程当中我们是利用按键触发来发送对应的LED的状态变化。 我们到mian.c中,查看按键触发会调用的btn_evt_handler_t回调函数,在这个函数中,我们最后会调用LED服务数据发送的功能函数ble_led_led_status_send。
//******************************************************************
// fn : btn_evt_handler_t
//
// brief : 按键触发回调函数
//
// param : butState -> 当前的按键值
//
// return : none
void btn_evt_handler_t (uint8_t butState)
{
uint8_t buf[LED_UUID_CHAR_LEN] = {0x01,0x01,0x01,0x01};
switch(butState)
{
case BUTTON_1:
buf[0] = 0x00;
break;
case BUTTON_2:
buf[1] = 0x00;
break;
case BUTTON_3:
buf[2] = 0x00;
break;
case BUTTON_4:
buf[3] = 0x00;
break;
default:
break;
}
ble_led_status_send(&m_ble_led_c,buf,LED_UUID_CHAR_LEN); // 发送Wirte属性数据包
ble_led_status_read(&m_ble_led_c); // 发送Read属性的读取消息
}
最后我们来分析一下这个发送函数,是如何使用我们刚刚一大圈代码处理,最终得到的connhandle以及handle_value的。
首先先判断下数据的长度,是不是符合我们的特征值的长度限制(不能超过我们定义的特征值的大小,否则返回参数错误),这个判断是很有必要的!
接下来我们判断一下connhandle是否为0xffff(BLE_CONN_HANDLE_INVALID),也就是尚未连接任何设备,如果没有连接,则返回状态无效。
最后我们定义了ble_gattc_write_params_t结构体用于赋值我们需要发送的数据,其中值得注意的是.handle = p_ble_led_c->peer_led_db.led_handle,这个就是我们刚刚获得的handle_value(特征值句柄),其他参数大家依葫芦画瓢,比较好理解,就不给大家介绍了。最终我们调用 sd_ble_gattc_write 函数将数据发送出去。
//******************************************************************************
// fn :ble_led_led_status_send
//
// brief : LED状态控制函数
//
// param : p_ble_led_c -> 指向要关联的LED结构实例的指针
// p_string -> 发送的LED相关的数据
// length -> 发送的LED相关的数据长度
//
// return : none
uint32_t ble_led_led_status_send(ble_led_c_t * p_ble_led_c, uint8_t * p_string, uint16_t length)
{
VERIFY_PARAM_NOT_NULL(p_ble_led_c);
if (length > LED_UUID_CHAR_LEN)
{
NRF_LOG_WARNING("Content too long.");
return NRF_ERROR_INVALID_PARAM;
}
if (p_ble_led_c->conn_handle == BLE_CONN_HANDLE_INVALID)
{
return NRF_ERROR_INVALID_STATE;
}
ble_gattc_write_params_t const write_params =
{
.write_op = BLE_GATT_OP_WRITE_CMD,
.flags = BLE_GATT_EXEC_WRITE_FLAG_PREPARED_WRITE,
.handle = p_ble_led_c->peer_led_db.led_handle,
.offset = 0,
.len = length,
.p_value = p_string
};
return sd_ble_gattc_write(p_ble_led_c->conn_handle, &write_params);
}
首先是我们还是在main文件的按键回调函数中调用的 ble_led_status_read(&m_ble_led_c); 函数,去读取从机特征值中的数据的,这里我们直接分析一下这个函数。
可以看到函数内容很简单,只调用了一个 sd_ble_gattc_read 函数去读取,包含的参数内容分别是我们的connhandle以及handle_value。
//******************************************************************************
// fn :ble_led_status_read
//
// brief : 读取LED特征值
//
// param : p_ble_led_c -> 指向要关联的LED结构实例的指针
//
// return : none
uint32_t ble_led_status_read(ble_led_c_t * p_ble_led_c)
{
VERIFY_PARAM_NOT_NULL(p_ble_led_c);
return sd_ble_gattc_read(p_ble_led_c->conn_handle,p_ble_led_c->peer_led_db.led_handle,0);
}
当我们成功Read之后,底层的sotfdevice会通过 ble_led_c_on_ble_evt 函数给我们返回 BLE_GATTC_EVT_READ_RSP 事件。
//******************************************************************************
// fn :ble_led_c_on_ble_evt
//
// brief : BLE事件处理函数
//
// param : p_ble_evt -> ble事件
// p_context -> ble事件处理程序的参数(暂时理解应该是不同的功能,注册时所携带的结构体参数)
//
// return : none
void ble_led_c_on_ble_evt(ble_evt_t const * p_ble_evt, void * p_context)
{
if ((p_context == NULL) || (p_ble_evt == NULL))
{
return;
}
ble_led_c_t * p_ble_led_c = (ble_led_c_t *)p_context;
switch (p_ble_evt->header.evt_id)
{
case BLE_GAP_EVT_DISCONNECTED:
on_disconnected(p_ble_led_c, p_ble_evt);
break;
case BLE_GATTC_EVT_READ_RSP:
on_read(p_ble_led_c, p_ble_evt);
break;
default:
break;
}
}
在 BLE_GATTC_EVT_READ_RSP 事件中,我们调用 on_read 函数去处理我们读取的值,我们将读取到的值,通过RTT LOG打印出来。
//******************************************************************************
// fn :on_read
//
// brief : 处理read事件的函数。
//
// param : p_ble_led_c -> led服务结构体
// p_ble_evt -> ble事件
//
// return : none
static void on_read(ble_led_c_t * p_ble_led_c, ble_evt_t const * p_ble_evt)
{
if (p_ble_led_c->conn_handle == p_ble_evt->evt.gap_evt.conn_handle)
{
NRF_LOG_INFO("Recive State:%02X,%02X,%02X,%02X",
p_ble_evt->evt.gattc_evt.params.read_rsp.data[0],
p_ble_evt->evt.gattc_evt.params.read_rsp.data[1],
p_ble_evt->evt.gattc_evt.params.read_rsp.data[2],
p_ble_evt->evt.gattc_evt.params.read_rsp.data[3]);
}
}
• 由 Leung 写于 2020 年 8 月 11 日
• 参考:青风电子社区
NRF52832DK协议栈实验——21 Write/Read属性服务实验