一、简介
图中,主从数据发送的数据包TX和RX表示方向性的数据通道,也就是蓝牙的空中属性,空中操作事件都是采用蓝牙操作句柄进行的,因为句柄能够唯一表示各个属性。空中特性的性质包括:
主机RX 从机TX 方向:
- 通知:从机端上传数据给主机,不需要主机回复一个响应
- 指示:从机端上传数据给主机,需要主机端发一个确认给服务器
通知和指示之间不同之处在于指示有应用层上的确认,而通知没有。
主机TX 从机RX 方向:
- 写
- 没有回应的写
- 读
Client Characteristic Configuration Descriptor(CCCD)是客户端特征配置描述符。当主机向CCCD中写入0x0001,此时使能notify;当写入0x0000时,此时禁止notify。
在nordic的协议栈当中,他的这个notify使能是交给用户自己处理的,也是说即便主机没有向cccd中写入0x0001去使能notify,我们同样可以直接利用notify去发送数据,只能这样不符合规范。
二、主机端
2.1 主机设备流程
- 扫描符合我们连接过滤要求的从机设备
- 成功连接我们的从机设备,并且更新连接参数和 MTU
- 发现服务
- 成功使用了从机服务的 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 从机设备流程
- 开启广播
- 被主机成功连接,并交互连接参数
- 等待主机获取服务(一般主机成功获取服务的时间在0.5s~1s之间,这个时间仅供大家参考)
- 等待主机成功使能notify功能
- 从机给主机发送相应的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 日