NRF52832学习笔记(24)——GATT客户端发现服务和读写特征值

一、背景

1.1 GATT协议

GATT(Generic Attributes Profile)的缩写,中文是通用属性协议,是已连接的低功耗蓝牙设备之间进行通信的协议。

一旦两个设备建立起了连接,GATT 就开始起作用了,这也意味着,你必需完成前面的GAP协议。

GATT使用了 ATT(Attribute Protocol)协议,ATT 协议把 Service,Characteristic 对应的数据保存在一个查找表中,查找表使用 16bit ID 作为每一项的索引。

GATT定义的多层数据结构简要概括起来就是 服务(Service) 可以包含多个 特征(Characteristic),每个特征包含 属性(Properties)值(Value),还可以包含多个 描述(Descriptor)

1.2 属性协议(ATT)

属性协议层 负责数据检索,允许一个设备暴露一些数据块给其他设备,其他设备称之为“属性”。

在ATT环境中,展示属性的设备称之为服务器,与它配对的设备称之为客户端。链路层的主机从机和这里的服务器、客服端是两种概念,主设备既可以是服务器,也可以是客户端。从设备毅然。

1.3 GATT通信中角色

从GATT的角度来看,处于连接状态时的两个设备,它们各自充当两种角色中的一种:
服务端(Server)
包含被GATT客户端读取或写入的特征数据的设备。
客户端(Client)
从GATT服务器中读取数据或向GATT服务器写入数据的设备。

外围设备(从机)作为 GATT 服务端(Server),它维持了 ATT 的查找表以及 service 和 characteristic 的定义;

客户端和服务器的GATT角色独立于外围设备和中央设备的GAP角色。外围设备可以是GATT客户端或GATT服务器,中心可以是GATT客户端或GATT服务器
NRF52832学习笔记(24)——GATT客户端发现服务和读写特征值_第1张图片

二、主机客户端的搭建

2.1 主机客户端声明

在主函数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);
}

2.2 主机客户端事件处理

/**@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;
    }
}

2.3 主机客户端初始化

对于主机客户端初始化,在 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);
}

2.4 数据发现初始化

在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);
}

2.5 注册数据发现事件处理函数

/**@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);

    }
}

2.6 主函数中添加函数

NRF52832学习笔记(24)——GATT客户端发现服务和读写特征值_第2张图片

三、发现服务流程

一旦我们主机发现我们的从机,并成功连接之后,会进入 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)。
NRF52832学习笔记(24)——GATT客户端发现服务和读写特征值_第3张图片
或者我们可以通过判断该特征是否有写权限,来获取句柄。
NRF52832学习笔记(24)——GATT客户端发现服务和读写特征值_第4张图片
当上述的流程都正确跑完,我们就可以进行最后一步的行动,也就是发送数据,在这个例程当中我们是利用按键触发来发送对应的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属性服务实验

你可能感兴趣的:(NRF52832,52832,nrf52832,gatt,读写特征值,发现服务)