美文网首页物联网loT从业者物联网相关技术研究
NRF52832学习笔记(24)——GATT客户端发现服务和读写

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

作者: Leung_ManWah | 来源:发表于2020-08-11 09:31 被阅读0次

    一、背景

    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服务器

    二、主机客户端的搭建

    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 主函数中添加函数

    三、发现服务流程

    一旦我们主机发现我们的从机,并成功连接之后,会进入 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属性服务实验

    相关文章

      网友评论

        本文标题:NRF52832学习笔记(24)——GATT客户端发现服务和读写

        本文链接:https://www.haomeiwen.com/subject/ifejdktx.html