NRF52832学习笔记(25)——Notify属性服务

一、简介

图中,主从数据发送的数据包TX和RX表示方向性的数据通道,也就是蓝牙的空中属性,空中操作事件都是采用蓝牙操作句柄进行的,因为句柄能够唯一表示各个属性。空中特性的性质包括:
主机RX 从机TX 方向:

  • 通知:从机端上传数据给主机,不需要主机回复一个响应
  • 指示:从机端上传数据给主机,需要主机端发一个确认给服务器

通知和指示之间不同之处在于指示有应用层上的确认,而通知没有。
主机TX 从机RX 方向:

  • 没有回应的写

Client Characteristic Configuration Descriptor(CCCD)是客户端特征配置描述符。当主机向CCCD中写入0x0001,此时使能notify;当写入0x0000时,此时禁止notify。
在nordic的协议栈当中,他的这个notify使能是交给用户自己处理的,也是说即便主机没有向cccd中写入0x0001去使能notify,我们同样可以直接利用notify去发送数据,只能这样不符合规范。

二、主机端

2.1 主机设备流程

  1. 扫描符合我们连接过滤要求的从机设备
  2. 成功连接我们的从机设备,并且更新连接参数和 MTU
  3. 发现服务
  4. 成功使用了从机服务的 notify 功能

2.2 主机客户端声明

首先,在 main 主函数里,服务的初始化函数 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.3 主机客户端事件处理

成功发现服务的事件 BLE_LBS_C_EVT_DISCOVERY_COMPLETE,里面首先还是调用 ble_lbs_c_handles_assign 函数将获取的句柄值和我们 m_ble_lbs_c 实例绑定起来。然后就去调用 ble_lbs_c_button_notif_enable 函数去使能从机的 notify 功能。

/**@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.4 主机客户端使能通知

使能 notify 的函数的代码,其实就是一个 write 功能,不过不是向 handle_value 去发送数据,而是向 cccd_handle 去发送了一个 0x01(BLE_GATT_HVX_NOTIFICATION),0x00 的数据。

uint32_t ble_lbs_c_button_notif_enable(ble_lbs_c_t * p_ble_lbs_c)
{
    VERIFY_PARAM_NOT_NULL(p_ble_lbs_c);

    if (p_ble_lbs_c->conn_handle == BLE_CONN_HANDLE_INVALID)
    {
        return NRF_ERROR_INVALID_STATE;
    }

    return cccd_configure(p_ble_lbs_c->conn_handle,
                          p_ble_lbs_c->peer_lbs_db.button_cccd_handle,
                          true);
}
/**@brief Function for configuring the CCCD.
 *
 * @param[in] conn_handle The connection handle on which to configure the CCCD.
 * @param[in] handle_cccd The handle of the CCCD to be configured.
 * @param[in] enable      Whether to enable or disable the CCCD.
 *
 * @return NRF_SUCCESS if the CCCD configure was successfully sent to the peer.
 */
static uint32_t cccd_configure(uint16_t conn_handle, uint16_t handle_cccd, bool enable)
{
    NRF_LOG_DEBUG("Configuring CCCD. CCCD Handle = %d, Connection Handle = %d",
        handle_cccd,conn_handle);

    tx_message_t * p_msg;
    uint16_t       cccd_val = enable ? BLE_GATT_HVX_NOTIFICATION : 0;                           // 是否是写CCCD

    p_msg              = &m_tx_buffer[m_tx_insert_index++];
    m_tx_insert_index &= TX_BUFFER_MASK;

    p_msg->req.write_req.gattc_params.handle   = handle_cccd;
    p_msg->req.write_req.gattc_params.len      = 2;//WRITE_MESSAGE_LENGTH;
    p_msg->req.write_req.gattc_params.p_value  = p_msg->req.write_req.gattc_value;              // 要写的值
    p_msg->req.write_req.gattc_params.offset   = 0;
    p_msg->req.write_req.gattc_params.write_op = BLE_GATT_OP_WRITE_REQ;
    p_msg->req.write_req.gattc_value[0]        = LSB_16(cccd_val);
    p_msg->req.write_req.gattc_value[1]        = MSB_16(cccd_val);
    p_msg->conn_handle                         = conn_handle;
    p_msg->type                                = WRITE_REQ;

    tx_buffer_process();
    return NRF_SUCCESS;
}

2.5 接收从机数据处理

接收从机数据的处理部分,首先是看到 ble_lbs_c_on_ble_evt 函数,这个函数在我们调用 BLE_LBS_C_DEF(m_lbs_c); 注册实例的时候,就已经创建好了,用于接收底层的 softdevice 的消息返回。 我们看下其中的 BLE_GATTC_EVT_HVX(Handle Value Notification or Indication event) 事件,在这个事件下我们接收到从机发送给我们的数据。

void ble_lbs_c_on_ble_evt(ble_evt_t const * p_ble_evt, void * p_context)
{
    if ((p_context == NULL) || (p_ble_evt == NULL))
    {
        return;
    }

    ble_lbs_c_t * p_ble_lbs_c = (ble_lbs_c_t *)p_context;

    switch (p_ble_evt->header.evt_id)                                                           // 解析发过来的事件ID
    {
        case BLE_GATTC_EVT_HVX:                                                                 // 接收从机通知
            on_hvx(p_ble_lbs_c, p_ble_evt);                                                     // 设置触发RX操作事件,接收蓝牙数据
            break;

        case BLE_GATTC_EVT_WRITE_RSP:                                                           // 写从机
            on_write_rsp(p_ble_lbs_c, p_ble_evt);
            break;

        case BLE_GAP_EVT_DISCONNECTED:                                                          // 断开连接
            on_disconnected(p_ble_lbs_c, p_ble_evt);
            break;

        default:
            break;
    }
}

对于接收到的从机数据的处理,首先还是一样的,我们需要判断一下数据的来源是不是我们 button_handle。当确认都是正确的,然后我们将接收的数据复制给 ble_lbs_c_evt_t,然后通过它的回调上传到我们的 main 文件中,携带的事件ID为 BLE_LBS_C_EVT_BUTTON_NOTIFICATION

/**@brief Function for handling Handle Value Notification received from the SoftDevice.
 *
 * @details This function will uses the Handle Value Notification received from the SoftDevice
 *          and checks if it is a notification of Button state from the peer. If
 *          it is, this function will decode the state of the button and send it to the
 *          application.
 *
 * @param[in] p_ble_lbs_c Pointer to the Led Button Client structure.
 * @param[in] p_ble_evt   Pointer to the BLE event received.
 */
static void on_hvx(ble_lbs_c_t * p_ble_lbs_c, ble_evt_t const * p_ble_evt)
{
    // Check if the event is on the link for this instance
    if (p_ble_lbs_c->conn_handle != p_ble_evt->evt.gattc_evt.conn_handle)
    {
        return;
    }
    // Check if this is a Button notification.
    if ( p_ble_evt->evt.gattc_evt.params.hvx.handle == p_ble_lbs_c->peer_lbs_db.button_handle)
    {   
        if (p_ble_evt->evt.gattc_evt.params.hvx.len > 0)
        {
            ble_lbs_c_evt_t ble_lbs_c_evt;

            ble_lbs_c_evt.evt_type                   = BLE_LBS_C_EVT_BUTTON_NOTIFICATION;       // 触发TX操作,接收从机上传数据
            ble_lbs_c_evt.conn_handle                = p_ble_lbs_c->conn_handle;
            ble_lbs_c_evt.params.button.button_state = p_ble_evt->evt.gattc_evt.params.hvx.data[0];
            ble_lbs_c_evt.data.size                  = p_ble_evt->evt.gattc_evt.params.hvx.len; // 数据长度
            uint8_t temp[p_ble_evt->evt.gattc_evt.params.hvx.len];
            memcpy(temp, p_ble_evt->evt.gattc_evt.params.hvx.data, p_ble_evt->evt.gattc_evt.params.hvx.len);                  
            ble_lbs_c_evt.data.p_data = temp;                                                   // 数据
                    
            p_ble_lbs_c->evt_handler(p_ble_lbs_c, &ble_lbs_c_evt);       
        }        
    }
}

接下来返回到我们的 main 文件中,我们在 lbs_c_evt_handler 回调中可以看到BLE_LBS_C_EVT_BUTTON_NOTIFICATION 事件的处理,我们将接收到的从机数据用于控制相应的LED灯点亮。

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;

三、从机端

3.1 从机设备流程

  1. 开启广播
  2. 被主机成功连接,并交互连接参数
  3. 等待主机获取服务(一般主机成功获取服务的时间在0.5s~1s之间,这个时间仅供大家参考)
  4. 等待主机成功使能notify功能
  5. 从机给主机发送相应的notify数据包

3.2 初始化服务

先看一下服务配置文件,首先还是注册一下服务,注册的服务句柄是 p_nus->service_handle。服务注册完成之后,我们注册按键的特征值,可以看到我们分别使能了按键的notify通知属性(add_char_params.char_props.notify = 1;)

这里我们需要注意的是下面的 cccd_write_access 参数被使能 SEC_OPEN

uint32_t ble_nus_init(ble_nus_t * p_nus, ble_nus_init_t const * p_nus_init)
{
    ret_code_t            err_code;
    ble_uuid_t            ble_uuid;
    ble_uuid128_t         nus_base_uuid = NUS_BASE_UUID;
    ble_add_char_params_t add_char_params;

    VERIFY_PARAM_NOT_NULL(p_nus);
    VERIFY_PARAM_NOT_NULL(p_nus_init);

    // Initialize the service structure.
    p_nus->data_handler = p_nus_init->data_handler;

    /**@snippet [Adding proprietary Service to the SoftDevice] */
    // Add a custom base UUID.
    err_code = sd_ble_uuid_vs_add(&nus_base_uuid, &p_nus->uuid_type);
    VERIFY_SUCCESS(err_code);

    ble_uuid.type = p_nus->uuid_type;
    ble_uuid.uuid = BLE_UUID_NUS_SERVICE;

    // Add the service.
    err_code = sd_ble_gatts_service_add(BLE_GATTS_SRVC_TYPE_PRIMARY,
                                        &ble_uuid,
                                        &p_nus->service_handle);
    /**@snippet [Adding proprietary Service to the SoftDevice] */
    VERIFY_SUCCESS(err_code);

    // Add the RX Characteristic.
    memset(&add_char_params, 0, sizeof(add_char_params));
    add_char_params.uuid                     = BLE_UUID_NUS_RX_CHARACTERISTIC;
    add_char_params.uuid_type                = p_nus->uuid_type;
    add_char_params.max_len                  = BLE_NUS_MAX_RX_CHAR_LEN;
    add_char_params.init_len                 = sizeof(uint8_t);
    add_char_params.is_var_len               = true;
    add_char_params.char_props.write         = 1;
    add_char_params.char_props.write_wo_resp = 1;

    add_char_params.read_access  = SEC_OPEN;
    add_char_params.write_access = SEC_OPEN;

    err_code = characteristic_add(p_nus->service_handle, &add_char_params, &p_nus->rx_handles);
    if (err_code != NRF_SUCCESS)
    {
        return err_code;
    }

    // Add the TX Characteristic.
    /**@snippet [Adding proprietary characteristic to the SoftDevice] */
    memset(&add_char_params, 0, sizeof(add_char_params));
    add_char_params.uuid              = BLE_UUID_NUS_TX_CHARACTERISTIC;
    add_char_params.uuid_type         = p_nus->uuid_type;
    add_char_params.max_len           = BLE_NUS_MAX_TX_CHAR_LEN;
    add_char_params.init_len          = sizeof(uint8_t);
    add_char_params.is_var_len        = true;
    add_char_params.char_props.notify = 1;

    add_char_params.read_access       = SEC_OPEN;
    add_char_params.write_access      = SEC_OPEN;
    add_char_params.cccd_write_access = SEC_OPEN;

    return characteristic_add(p_nus->service_handle, &add_char_params, &p_nus->tx_handles);
    /**@snippet [Adding proprietary characteristic to the SoftDevice] */
}

3.3 接收通知使能

在 BLE 事件处理的函数中,我们应该要处理 CCCD_Write 的数据的,所以在由 softdevice 返回消息的 ble_nus_on_ble_evt 函数中,我们需要处理一下BLE_GATTS_EVT_WRITE 事件。

void ble_nus_on_ble_evt(ble_evt_t const * p_ble_evt, void * p_context)
{
    if ((p_context == NULL) || (p_ble_evt == NULL))
    {
        return;
    }

    ble_nus_t * p_nus = (ble_nus_t *)p_context;

    switch (p_ble_evt->header.evt_id)
    {
        case BLE_GAP_EVT_CONNECTED:
            on_connect(p_nus, p_ble_evt);
            break;

        case BLE_GATTS_EVT_WRITE:
            on_write(p_nus, p_ble_evt);
            break;

        case BLE_GATTS_EVT_HVN_TX_COMPLETE:
            on_hvx_tx_complete(p_nus, p_ble_evt);
            break;

        default:
            // No implementation needed.
            break;
    }
}

在这个 on_write 函数中,我们接收到了主机发送过来的使能从机 notify 的数据,我们需要判断一下接收的数据的句柄是不是 cccd_handle,以及接收的数据长度是不是2字节(使能数据:01 00)。

/**@brief Function for handling a GATT write event from the SoftDevice.
 *
 * @details To provide the start_on_notify_cccd_handle functionality.
 *
 * @param[in]  p_ble_evt  Event from the SoftDevice.
 */
static void on_write(ble_evt_t const * p_ble_evt)
{
    ble_gatts_evt_write_t const * p_evt_write = &p_ble_evt->evt.gatts_evt.params.write;

    // Check if this is the correct CCCD
    if ((p_evt_write->handle == m_conn_params_config.start_on_notify_cccd_handle) &&
        (p_evt_write->len == 2))
    {
        uint16_t                     conn_handle = p_ble_evt->evt.gap_evt.conn_handle;
        ble_conn_params_instance_t * p_instance  = instance_get(conn_handle);

        if (p_instance != NULL)
        {
            // Check if this is a 'start notification'
            if (ble_srv_is_notification_enabled(p_evt_write->data))
            {
                // Do connection parameter negotiation if necessary
                conn_params_negotiation(conn_handle, p_instance);
            }
            else
            {
                ret_code_t err_code;

                // Stop timer if running
                err_code = app_timer_stop(p_instance->timer_id);
                if (err_code != NRF_SUCCESS)
                {
                    send_error_evt(err_code);
                }
            }
        }
    }
}

3.4 从机发送notify数据

首先我们一定要先判断一下是否已经 notify 使能,并且判断数据长度是否符合要求。
下面这个函数,就是我们 notify 发送数据的函数,他的参数我们只需要配置4个。

  • type 配置为 BLE_GATT_HVX_NOTIFICATION,代表是 notify 属性的数据;
  • handle 我们需要配置为我们按键特征值的 value.handle,代表的是按键特征值的 Value这个列表的句柄;
  • p_data 就是我们需要发送的数据;
  • p_len 数据的长度。
uint32_t ble_nus_data_send(ble_nus_t * p_nus,
                           uint8_t   * p_data,
                           uint16_t  * p_length,
                           uint16_t    conn_handle)
{
    ret_code_t                 err_code;
    ble_gatts_hvx_params_t     hvx_params;
    ble_nus_client_context_t * p_client;

    VERIFY_PARAM_NOT_NULL(p_nus);

    err_code = blcm_link_ctx_get(p_nus->p_link_ctx_storage, conn_handle, (void *) &p_client);
    VERIFY_SUCCESS(err_code);

    if ((conn_handle == BLE_CONN_HANDLE_INVALID) || (p_client == NULL))
    {
        return NRF_ERROR_NOT_FOUND;
    }

    if (!p_client->is_notification_enabled)
    {
        return NRF_ERROR_INVALID_STATE;
    }

    if (*p_length > BLE_NUS_MAX_DATA_LEN)
    {
        return NRF_ERROR_INVALID_PARAM;
    }

    memset(&hvx_params, 0, sizeof(hvx_params));

    hvx_params.handle = p_nus->tx_handles.value_handle;
    hvx_params.p_data = p_data;
    hvx_params.p_len  = p_length;
    hvx_params.type   = BLE_GATT_HVX_NOTIFICATION;

    return sd_ble_gatts_hvx(conn_handle, &hvx_params);
}

3.5 main.c

首先我们还是需要添加一下服务初始化函数。

/**@brief Function for initializing services that will be used by the application.
 */
void services_init(void)
{
    uint32_t           err_code;
    ble_nus_init_t     nus_init;
    nrf_ble_qwr_init_t qwr_init = {0};

    // Initialize Queued Write Module.
    qwr_init.error_handler = nrf_qwr_error_handler;
    err_code = nrf_ble_qwr_init(&m_qwr, &qwr_init);
    APP_ERROR_CHECK(err_code);

    // Initialize NUS.
    memset(&nus_init, 0, sizeof(nus_init));
    nus_init.data_handler = nus_data_handler;
    err_code = ble_nus_init(&m_nus, &nus_init);
    APP_ERROR_CHECK(err_code);
}

当有按键按下时,最终会将按键消息传递到这个回调中进行处理,我们根据按键触发的消息,对相应的 buf 值进行修改,最后调用 ble_btn_data_send 函数将数据发送给主机。

//******************************************************************
// fn : btn_evt_handler_t
//
// brief : 按键触发回调函数
// 
// param : butState -> 当前的按键值
//
// return : none
void btn_evt_handler_t (uint8_t butState)
{
  uint8_t buf[BTN_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_nus_data_send(&m_nus, buf, BTN_UUID_CHAR_LEN, m_conn_handle);
}

• 由 Leung 写于 2020 年 9 月 7 日

• 参考:NRF52832DK协议栈实验——22 Notify属性服务实验

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342