蓝牙配网指南

更新时间:2022-11-24 09:20:26下载pdf

本文主要介绍蓝牙配网的关键流程,通过本文您将会了解如何实现蓝牙配网功能,完成综合 SDK 蓝牙配网的适配。

背景信息

蓝牙配网,顾名思义,通过蓝牙通道传输配网信息(激活令牌、路由器的 SSID 和密码等),完成设备配网的过程。

蓝牙配网的流程是,网关设备通过蓝牙广播发送设备相关的信息,App 接收并解析蓝牙广播包,如果广播包中包含涂鸦服务,则发起扫描响应请求进一步获取设备的详细信息,获取到设备完整的信息后在 App 显示待配网设备。当用户选择激活设备,App 与网关设备建立蓝牙连接,把配网信息通过蓝牙通道发送给网关设备,网关设备获取到配网信息,完成激活流程。

说明:涂鸦蓝牙配网用的是私有服务和私有数据,所有数据均由 SDK 解析和封装,开发者无需关心数据内容。

实现 - 涂鸦蓝牙模组

如果使用涂鸦网关蓝牙模组,蓝牙配网的适配已经在 SDK 内部实现,应用无需任何代码适配,详见 涂鸦蓝牙业务指南

实现 - 自定义模组

如果使用自定义模组,开发者需要完成接口的适配。SDK 已经实现蓝牙配网的底层逻辑,定义了一套蓝牙配网适配接口,本小节将会详细介绍如何适配。

配网流程

蓝牙配网的交互流程示意图:

蓝牙配网指南

蓝牙配网的流程如下所示:

  1. 初始化蓝牙适配的注册。
  2. SDK 初始化后,通知应用初始化蓝牙协议栈或服务,并且把蓝牙配置成 BLE Master。
  3. SDK 封装广播和扫描响应数据传给应用,应用保存数据,并且开启广播发送。
  4. 应用接收到来自 App 端的扫描响应请求,把步骤 3 中的扫描响应数据作为响应内容回复 App 的扫描响应请求。
  5. App 解析蓝牙广播包,如果符合涂鸦蓝牙数据格式,则显示待配网设备。
  6. 当用户添加待配网设备时,进入配网流程。
  7. App 与网关建立蓝牙连接,网关作为 BLE Master 监听到 BLE Slave 连接事件,应用把连接事件传给 SDK。
  8. App 与网关进行蓝牙数据传输。应用接收到蓝牙数据时,把数据传给 SDK 解析处理。SDK 封装响应数据传给应用,应用通过蓝牙通道回复 App。
  9. 当 SDK 解析出完整的配网信息,把路由器的 SSID 和密码传给应用,应用连接路由器,连接成功后 SDK 向云端发起激活请求,完成激活流程。

数据格式

蓝牙广播和扫描响应的数据格式:

蓝牙配网指南

蓝牙广播和扫描响应在蓝牙规范中有定义,SDK 把广播数据和扫描响应数据封装成 AD Structure 格式,应用无需额外解析处理,直接在广播和扫描响应中使用即可。

蓝牙传输的数据格式:

蓝牙配网指南

App 和网关建立蓝牙连接后,通过蓝牙 The Attribute Protocol(ATT) 协议来传输数据,SDK 封装和解析的数据都属于 ATT Payload。

Service UUID: 0x1910。

Characteristic UUID 属性 说明
Write Characteristic 0x2B11 Write Without Response App 向 BLE 设备发送数据
Notify Characteristic 0x2B10 Notify BLE 设备向 App 发送通知信息

接口适配

接下来,我们一步步来完成蓝牙配网的适配工作。

  1. 在 SDK 初始化之前初始化蓝牙适配的注册。

    // ...
    #if defined(TY_BT_MOD) && (TY_BT_MOD == 1)
    #include "tuya_os_adapt_bt.h"
    #endif
    
    int main(int argc, char **argv)
    {
        /**
         * 必须在 SDK 其他接口之前调用
         */
    #if defined(TY_BT_MOD) && (TY_BT_MOD == 1)
        tuya_os_adapt_reg_bt_intf();
    #endif
    
        tuya_os_intf_init();
    
        // ...
    }
    
  2. 适配蓝牙初始化接口,在接口中实现初始化蓝牙协议栈,把蓝牙配置成 BLE Master 模式,并且把数据处理的函数指针保存起来,通过该函数指针把接收到的蓝牙数据传给 SDK 处理。

    TY_BT_MSG_CB bt_msg_cb;
    
    OPERATE_RET tuya_adapter_bt_port_init(ty_bt_param_t *p)
    {
        /**
         * 函数中需要实现以下功能:
         *   a) 初始化蓝牙协议栈,保证蓝牙能正常通讯。
         *   b) 保存函数指针变量,把接收到的蓝牙数据通过该函数指针传递给 SDK。
         */
        bt_msg_cb = p->cb;
    
        return OPRT_OK;
    }
    
  3. 适配蓝牙广播数据重置接口,在接口中把广播和扫描响应数据内容保存,并且开启广播。

    tuya_ble_data_buf_t adv_data = {0};
    tuya_ble_data_buf_t rsp_data = {0};
    
    OPERATE_RET tuya_adapter_bt_adv_reset(tuya_ble_data_buf_t *adv, tuya_ble_data_buf_t *scan_resp)
    {
        if ((adv == NULL) || (adv->len == 0) || (scan_resp == NULL) || (scan_resp->len == 0)) {
            return OPRT_INVALID_PARM;
        }
    
        if (adv_data.data != NULL) {
            Free(adv_data.data);
            adv_data.data = NULL;
        }
        adv_data.len = adv->len;
        adv_data.data = Malloc(adv_data.len);
        if (adv_data.data != NULL) {
            memcpy(adv_data.data, adv->data, adv->len);
        }
        
        if (rsp_data.data != NULL) {
            Free(rsp_data.data);
            rsp_data.data = NULL;
        }
        rsp_data.len = scan_resp->len;
        rsp_data.data = Malloc(rsp_data.len);
        if (rsp_data.data != NULL) {
            memcpy(rsp_data.data, scan_resp->data, scan_resp->len);
        }
    
        /**
         * 需要实现的逻辑:
         *   a) 开启广播(数据:adv_data.data,数据长度:adv_data.len)。
         *   b) 保存扫描响应数据,在收到扫描响应请求时返回该数据(数据:rsp_data.data,数据长度:rsp_data.len)。
         */
    
        return OPRT_OK;
    }
    
  4. 适配数据发送接口,在接口中把数据发送给 App。

    OPERATE_RET tuya_adapter_bt_send(BYTE_T *data, UINT8_T len)
    {
        /**
         * 需要实现的逻辑:把数据通过 GATT Server Send Characteristic Notification 发给 App。
         */
    
        return OPRT_OK;
    }
    
  5. 把接收到的蓝牙数据传给 SDK 解析。

    蓝牙连接建立时,发送事件给 SDK:

    VOID example_ble_connected(VOID)
    {
        tuya_ble_data_buf_t data = {0};
    
        if (bt_msg_cb == NULL) {
            return;
        }
    
        data.len = 0;
        data.data = NULL;
        bt_msg_cb(0, TY_BT_EVENT_CONNECTED, &data);
    }
    

    蓝牙连接断开时,发送事件给 SDK:

    VOID example_ble_disconnected(VOID)
    {
        tuya_ble_data_buf_t data = {0};
    
        if (bt_msg_cb == NULL) {
            return;
        }
    
        data.len = 0;
        data.data = NULL;
        bt_msg_cb(0, TY_BT_EVENT_DISCONNECTED, &data);
        bt_msg_cb(0, TY_BT_EVENT_ADV_READY, &data);
    }
    

    蓝牙接收数据时,发送事件给 SDK:

    /**
     * 需要实现的逻辑:应用接收到蓝牙数据,把数据中的 GATT Server Attribute Value 传给 SDK 处理。 
     */
    VOID example_ble_rx(BYTE_T *rx_data, UINT8_T rx_len)
    {
        tuya_ble_data_buf_t data = {0};
    
        if (bt_msg_cb == NULL) {
            return;
        }
    
        data.len = rx_len;
        data.data = rx_data;
        bt_msg_cb(0, TY_BT_EVENT_RX_DATA, &data);
    }
    

附录

main.c

#include <unistd.h>

#include "uni_log.h"
#include "base_os_adapter.h"
#include "tuya_iot_base_api.h"
#include "tuya_iot_com_api.h"

#include "tuya_iot_sdk_api.h"
#include "tuya_iot_sdk_defs.h"

#if defined(TY_BT_MOD) && (TY_BT_MOD == 1)
#include "tuya_os_adapt_bt.h"
#endif

#define PID       "fljmamufiym5fktz"                  // 替换成自己的产品 ID
#define UUID      "tuya461dbc63aeeb991f"              // 替换成自己的 UUID
#define AUTHKEY   "c8X4PR4wx1gMFaQlaZu5dfgVvVRwB8Ug"  // 替换成自己的 AUTHKEY

STATIC VOID __gw_reset_cb(GW_RESET_TYPE_E type)
{
    PR_DEBUG("gw reset callback, type: %d", type);

    if (GW_RESET_DATA_FACTORY != type) {
        exit(0);
    }

    return;
}

STATIC VOID __gw_upgrade_cb(CONST FW_UG_S *fw)
{
    PR_DEBUG("gw upgrade callback");

    if (fw == NULL) {
        PR_ERR("invalid param");
        return;
    }

    PR_DEBUG("        tp: %d", fw->tp);
    PR_DEBUG("    fw_url: %s", fw->fw_url);
    PR_DEBUG("    sw_ver: %s", fw->sw_ver);
    PR_DEBUG("   fw_hmac: %s", fw->fw_hmac);
    PR_DEBUG(" file_size: %u", fw->file_size);

    return;
}

STATIC VOID __gw_active_stat_cb(GW_STATUS_E status)
{
    PR_DEBUG("gw active stat callback, status: %d", status);

    return;
}

STATIC VOID __gw_reboot_cb(VOID)
{
    PR_DEBUG("gw reboot callback");

    exit(0);

    return;
}

STATIC VOID __nw_stat_cb(IN CONST SDK_NW_STAT_T stat)
{
    PR_DEBUG("network stat: %d", stat);

    return;
}

STATIC VOID __wired_stat_cb(IN CONST SDK_WIRED_NW_STAT_T stat)
{
    PR_DEBUG("wired stat: %d", stat);

    return;
}

int main(int argc, char **argv)
{
    OPERATE_RET rt = OPRT_OK;
    GW_PROD_INFO_S prod_info = {0};

    /* gw base callback */
    TY_GW_INFRA_CBS_S gw_cbs = {
        .gw_reset_cb       = __gw_reset_cb,
        .gw_upgrade_cb     = __gw_upgrade_cb,
        .gw_active_stat_cb = __gw_active_stat_cb,
        .gw_reboot_cb      = __gw_reboot_cb,
    };

#if defined(TY_BT_MOD) && (TY_BT_MOD == 1)
    tuya_os_adapt_reg_bt_intf();
#endif

    /* initiate os-layer service*/
    tuya_os_intf_init();

    /* initiate iot-layer service */
    TUYA_CALL_ERR_RETURN(tuya_iot_init("./"));

    /* set the logging level to debug */
    SET_PR_DEBUG_LEVEL(TY_LOG_LEVEL_DEBUG);

    PR_DEBUG("SDK INFO: %s", tuya_iot_get_sdk_info());

    /* set uuid and authkey */
    prod_info.uuid     = UUID;
    prod_info.auth_key = AUTHKEY;
    TUYA_CALL_ERR_RETURN(tuya_iot_set_gw_prod_info(&prod_info));

    /* pre-initiate sdk service */
    TUYA_CALL_ERR_RETURN(tuya_iot_sdk_pre_init(TRUE));  

    /* initiate application service, more service in here */
    TUYA_CALL_ERR_RETURN(tuya_user_svc_init(&gw_cbs));

    /* initiate sdk service */
#if defined(GW_SUPPORT_WIRED_WIFI) && (GW_SUPPORT_WIRED_WIFI==1)
    TUYA_CALL_ERR_RETURN(tuya_iot_wired_wf_sdk_init(IOT_GW_NET_WIRED_WIFI, GWCM_OLD, WF_START_AP_ONLY, PID, USER_SW_VER, NULL, 0));
#elif defined(WIFI_GW) && (WIFI_GW==1)
    TUYA_CALL_ERR_RETURN(tuya_iot_wf_sdk_init(GWCM_OLD, WF_START_AP_ONLY, PID, USER_SW_VER, NULL, 0));
#else
    TUYA_CALL_ERR_RETURN(tuya_iot_sdk_init(PID, USER_SW_VER, NULL, 0));
#endif

    /* register net stat notification callback */
    TUYA_CALL_ERR_RETURN(tuya_iot_sdk_reg_netstat_cb(__nw_stat_cb, __wired_stat_cb, NULL));

    /* start application service, more service in here */
    TUYA_CALL_ERR_RETURN(tuya_user_svc_start(NULL));

    while (1) {
        sleep(10);
    }

    return 0;
}

hal_bt.c

#include "tuya_os_adapt_bt.h"
#include "mem_pool.h"

TY_BT_MSG_CB bt_msg_cb = NULL;

tuya_ble_data_buf_t adv_data = {0};
tuya_ble_data_buf_t rsp_data = {0};

OPERATE_RET tuya_adapter_bt_port_init(ty_bt_param_t *p)
{
    /**
     * 函数中需要实现以下功能:
     *   a) 初始化蓝牙协议栈,保证蓝牙能正常通讯。
     *   b) 保存函数指针变量,把接收到的蓝牙数据通过该函数指针传递给 SDK。
     */
    bt_msg_cb = p->cb;

    return OPRT_OK;
}

OPERATE_RET tuya_adapter_bt_port_deinit(VOID)
{
    return OPRT_OK; 
}

OPERATE_RET tuya_adapter_bt_gap_disconnect(VOID)
{
    return OPRT_OK;
}

OPERATE_RET tuya_adapter_bt_send(BYTE_T *data, UINT8_T len)
{
    /**
     * 需要实现的逻辑:把数据通过 GATT Server Send Characteristic Notification 发给 App。
     */

    return OPRT_OK;
}

OPERATE_RET tuya_adapter_bt_adv_reset(tuya_ble_data_buf_t *adv, tuya_ble_data_buf_t *scan_resp)
{
    if ((adv == NULL) || (adv->len == 0) || (scan_resp == NULL) || (scan_resp->len == 0)) {
        return OPRT_INVALID_PARM;
    }

    if (adv_data.data != NULL) {
        Free(adv_data.data);
        adv_data.data = NULL;
    }
    adv_data.len = adv->len;
    adv_data.data = Malloc(adv_data.len);
    if (adv_data.data != NULL) {
        memcpy(adv_data.data, adv->data, adv->len);
    }
    
    if (rsp_data.data != NULL) {
        Free(rsp_data.data);
        rsp_data.data = NULL;
    }
    rsp_data.len = scan_resp->len;
    rsp_data.data = Malloc(rsp_data.len);
    if (rsp_data.data != NULL) {
        memcpy(rsp_data.data, scan_resp->data, scan_resp->len);
    }

    /**
     * 需要实现的逻辑:
     *   a) 开启广播(数据:adv_data.data,数据长度:adv_data.len)。
     *   b) 保存扫描响应数据,在收到扫描响应请求时返回该数据(数据:rsp_data.data,数据长度:rsp_data.len)。
     */

    return OPRT_OK;
}

OPERATE_RET tuya_adapter_bt_adv_start(VOID)
{
    return OPRT_OK;
}

OPERATE_RET tuya_adapter_bt_adv_stop(VOID)
{
    return OPRT_OK;
}

OPERATE_RET tuya_adapter_bt_get_rssi(SCHAR_T *rssi)
{
    return OPRT_OK;
}

OPERATE_RET tuya_adapter_bt_scan_assign(INOUT ty_bt_scan_info_t *info)
{
    return OPRT_OK;
}

OPERATE_RET tuya_adapter_bt_scan_init(IN TY_BT_SCAN_ADV_CB scan_adv_cb)
{
    return OPRT_OK;
}

OPERATE_RET tuya_adapter_bt_scan_start(VOID_T)
{
    return OPRT_OK;
}

OPERATE_RET tuya_adapter_bt_scan_stop(VOID_T)
{
    return OPRT_OK;
}

VOID example_ble_connected(VOID)
{
    tuya_ble_data_buf_t data = {0};

    if (bt_msg_cb == NULL) {
        return;
    }

    data.len = 0;
    data.data = NULL;
    bt_msg_cb(0, TY_BT_EVENT_CONNECTED, &data);
}

VOID example_ble_disconnected(VOID)
{
    tuya_ble_data_buf_t data = {0};

    if (bt_msg_cb == NULL) {
        return;
    }

    data.len = 0;
    data.data = NULL;
    bt_msg_cb(0, TY_BT_EVENT_DISCONNECTED, &data);
    bt_msg_cb(0, TY_BT_EVENT_ADV_READY, &data);
}

/**
 * 需要实现的逻辑:应用接收到蓝牙数据,把数据中的 GATT Server Attribute Value 传给 SDK 处理。 
 */
VOID example_ble_rx(BYTE_T *rx_data, UINT8_T rx_len)
{
    tuya_ble_data_buf_t data = {0};

    if (bt_msg_cb == NULL) {
        return;
    }

    data.len = rx_len;
    data.data = rx_data;
    bt_msg_cb(0, TY_BT_EVENT_RX_DATA, &data);
}

/**
 * 需要实现的逻辑:应用接收到蓝牙数据,把数据中的 GATT Server Attribute Value 传给 SDK 处理。 
 */
VOID example_ble_rx(BYTE_T *rx_data, UINT8_T rx_len)
{
    tuya_ble_data_buf_t data = {0};

    if (bt_msg_cb == NULL) {
        return;
    }

    data.len = rx_len;
    data.data = rx_data;
    bt_msg_cb(0, TY_BT_EVENT_RX_DATA, &data);
}