配网方式-无线

更新时间:2023-09-06 10:41:15下载pdf

概述

基本概念

本文详细介绍 SDK 上无线配网功能,实现网关设备接入到涂鸦生态。无线配网仅支持 AP 配网,快速配网(EZ 配网)功能已经下架,后续不再支持。

配网流程的业务逻辑已经在 SDK 内部实现,SDK 定义一套 TuyaOS Kernel Layer(简称 TKL)接口来屏蔽硬件和系统差异,TKL 接口由开发者实现,在 TKL 接口完成底层硬件的操作。本文档将提供 TKL 接口开发指导。

功能描述

无线配网是把网关类设备连接到接入以太网的 Wi-Fi 路由上,从而连接到涂鸦 IoT 云的行为。需要输入路由器热点名称与密码。

适用场景

工作原理/实现方案

AP 配网也称 热点配网 。其工作原理是,网关设备开启无线 AP,手机连接网关设备的 AP,使得手机和网关设备处于同一局域网下,设备把它的设备信息经过加密处理后,定期在局域网上发送 UDP 广播数据包。

用户在手机 App 上输入路由器的 SSID 和密码后,手机 App 与设备建立 TLS 连接,把配网授权信息以及路由器的 SSID 和密码发送给设备。设备解析出路由器 SSID 和密码后,切换到 Station 模式连接路由器,连接成功后走激活流程,直到配网完成。

AP 配网的流程图如下图所示:

配网方式-无线

开发指导

使用方法

无线配网过程中,SDK 需要获取和切换无线工作模式,开启和关闭 AP,连接路由器,获取无线连接状态,获取无线接口 IP 地址等,需要开发者适配以下 TKL(Tuya kernel layer)接口。

  • tkl_wifi_init

    OPERATE_RET tkl_wifi_init(WIFI_EVENT_CB cb);
    

    SDK 初始化时会调用该接口,该接口的参数是无线连接状态变化的通知回调函数指针,需要应用实时监控无线连接的状态变化。当无线连接状态发生变化时,使用通知回调函数指针把其状态传递给 SDK。

    举例,当成功连接到路由器并分配到 IP 地址时,执行 cb(WFE_CONNECTED, NULL)。当连接路由器失败时,执行 cb(WFE_CONNECT_FAILED, NULL)。当断开路由器连接时,执行 cb(WFE_DISCONNECTED, NULL)

  • tkl_wifi_start_ap

    OPERATE_RET tkl_wifi_start_ap(CONST WF_AP_CFG_IF_S *cfg);
    

    SDK 初始化时,若设备处于未配网状态,则会调用该接口,用于开启设备的 AP,允许手机连接,建立局域网通讯来传输配网信息。应用需要在接口中实现启动 AP 的功能。

  • tkl_wifi_stop_ap

    OPERATE_RET tkl_wifi_stop_ap(VOID_T);
    

    当 SDK 配网结束时,会调用该接口,用于关闭设备的 AP。应用需要在接口中实现关闭 AP 的功能。

  • tkl_wifi_set_work_mode

    OPERATE_RET tkl_wifi_set_work_mode(CONST WF_WK_MD_E mode);
    

    当 SDK 需要切换无线的工作模式时,会调用该接口,通常是在 tkl_wifi_stop_ap 接口之前调用该接口设置 AP 模式,在 tkl_wifi_station_connect 之前调用该接口设置 Station 模式。应用需要在接口中根据 mode 参数把无线切换到对应的工作模式。

  • tkl_wifi_get_work_mode

    OPERATE_RET tkl_wifi_get_work_mode(WF_WK_MD_E *mode);
    

    当 SDK 需要获取无线的工作模式时,会调用该接口。返回的工作模式要求与 tkl_wifi_set_work_mode 接口设置的一致,否则可能会影响配网流程。

  • tkl_wifi_station_connect

    OPERATE_RET tkl_wifi_station_connect(CONST SCHAR_T *ssid, CONST SCHAR_T *passwd);
    

    当 SDK 接收到来自手机 App 的配网信息后,会调用该接口。应用需要在接口中实现连接路由器的功能。

  • tkl_wifi_station_disconnect

    OPERATE_RET tkl_wifi_station_disconnect(VOID_T);
    

    当 SDK 需要断开路由器连接时,会调用该接口。应用需要在接口中实现断开路由器的功能。

  • tkl_wifi_station_get_status

    OPERATE_RET tkl_wifi_station_get_status(WF_STATION_STAT_E *stat);
    

    当 SDK 需要获取当前无线连接状态时,会调用该接口。SDK 需要获取到 WSS_GOT_IP 状态才会走配网流程,所以当设备连接到路由器并且分配到 IP 地址,需要返回 WSS_GOT_IP

  • tkl_wifi_get_ip

    OPERATE_RET tkl_wifi_get_ip(CONST WF_IF_E wf, NW_IP_S *ip);
    

    当 SDK 需要获取当前无线接口的 IP 地址时,会调用该接口。应用需要在接口中根据 wf 参数来获取 AP 或 Station 接口的 IP 地址。

使用示例

本文档使用 wpa_supplicanthostapdudhcpc 以及 udhcpd 来实现 AP 配网功能。代码仅提供一种实现的思路,开发者需要根据自己的无线芯片进行适配和优化

  1. 使用无线配网接口初始化 SDK,并把无线工作模式设置为 WF_START_AP_ONLY

    // ...
    
    int main(int argc, char **argv)
    {
        // ...
    #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
        // 有线 SDK,不支持无线配网
        return OPRT_COM_ERROR;
    #endif
        // ...
    }
    
  2. 实现 tkl_wifi_init 接口,在接口中实现开启线程监控无线连接状态,并把通知回调函数指针保存到全局变量。无线连接状态发生变化时(主要关心连接和断开状态),使用通知回调函数指针把状态传递给 SDK。

    STATIC WIFI_EVENT_CB  __wifi_event_cb = NULL;
    
    STATIC BOOL_T __wifi_status(VOID)
    {
        FILE *fp = NULL;
        CHAR_T buf[512] = {0};
        WF_STATION_STAT_E stat = 0;
    
        fp = popen("wpa_cli -i " WLAN_DEV " status", "r");
        if (fp == NULL) {
            return FALSE;
        }
    
        while (fgets(buf, SIZEOF(buf), fp)) {
            if (!strstr(buf, "wpa_state"))
                continue;
    
            char *k = strtok(buf, "=");
            char *v = strtok(NULL, "=");
            if (v && !strncmp(v, "COMPLETED", strlen("COMPLETED"))) {
                tkl_wifi_station_get_status(&stat);
                if (stat == WSS_GOT_IP) {
                    return TRUE;
                }
            }
        }
    
        pclose(fp);
    
        return FALSE;
    }
    
    STATIC VOID *__wifi_status_thread(VOID *arg)
    {
        BOOL_T cur_status = FALSE, lst_status = FALSE;
    
        while (1) {
            if (g_wifi_mode != WWM_STATION) {
                tkl_system_sleep(500);
                continue;
            }
    
            cur_status = __wifi_status();
    
            if (cur_status != lst_status) {
                PR_DEBUG("wifi connection status changed, %d -> %d", lst_status, cur_status);
                if (__wifi_event_cb) {
                    __wifi_event_cb(cur_status ? WFE_CONNECTED : WFE_DISCONNECTED, NULL);
                }
                lst_status = cur_status;
            }
            tkl_system_sleep(1000);
        }
    }
    
    OPERATE_RET tkl_wifi_init(WIFI_EVENT_CB cb)
    {
        pthread_t tid;
    
        __wifi_event_cb = cb;
    
        pthread_create(&tid, NULL, __wifi_status_thread, NULL);
    
        return OPRT_OK;
    }
    
  3. 实现 tkl_wifi_set_work_mode 接口,在接口中实现根据参数设置无线的工作模式,主要是 AP 模式和 Station 模式。

    OPERATE_RET tkl_wifi_set_work_mode(CONST WF_WK_MD_E mode)
    {
        PR_DEBUG("WiFi set mode: %d", mode);
    
        g_wifi_mode = mode;
    
        switch (mode) {
            case WWM_STATION:
                exec_command("iwconfig " WLAN_DEV " mode managed", NULL, 0);
                break;
            case WWM_SOFTAP:
                // exec_command("iwconfig " WLAN_DEV " mode master", NULL, 0);
                break;
            default:
                break;
        }
    
        return OPRT_OK;
    }
    
  4. 实现 tkl_wifi_get_work_mode 接口,在接口中实现获取无线当前的工作模式,要求与 tkl_wifi_set_work_mode 接口设置的一致。为了避免出现状态不一致,直接从全局变量中取值。

    OPERATE_RET tkl_wifi_get_work_mode(WF_WK_MD_E *mode)
    {
        *mode = g_wifi_mode;
    
        PR_DEBUG("WiFi got mode: %d", *mode);
    
        return OPRT_OK;
    }
    
  5. 实现 tkl_wifi_start_ap 接口,在接口中根据参数配置 AP 和开启 AP 的功能,必须使用 cfg 参数中的 IP 地址。udhcpd 作为 DHCP 服务器,给手机分配 IP 地址,IP 地址池以及网关 IP 使用 cfg 参数的值。

    OPERATE_RET tkl_wifi_start_ap(CONST WF_AP_CFG_IF_S *cfg)
    {
        OPERATE_RET op_ret = OPRT_OK;
        INT_T len = 0;
        CHAR_T buf[512] = {0};
        CHAR_T cmd[128] = {0};
        CHAR_T *ap_conf_fmt = 
    "interface=%s\n"
    "ssid=%s\n"
    "country_code=CN\n"
    "channel=%d\n"
    "beacon_int=100\n"
    "max_num_sta=%d\n"
    "auth_algs=3\n"
    "wpa=%d\n"
    "wpa_key_mgmt=WPA-PSK\n"
    "wpa_pairwise=TKIP CCMP\n"
    "rsn_pairwise=CCMP\n";
    
        CHAR_T *udhcpd_conf_fmt = 
    "interface %s\n"
    "start %s.100\n"
    "end %s.200\n"
    "opt subnet %s\n"
    "opt lease 28800\n"
    "opt router %s\n"
    "opt dns %s\n"
    "opt domain SmartLift\n";
    
        INT_T seg1 = 0, seg2 = 0, seg3 = 0, seg4 = 0;
        CHAR_T ip_prefix[12] = {0};
    
        tkl_wifi_station_disconnect();
    
        PR_DEBUG("start ap, ssid: %s, ip: %s", cfg->ssid, cfg->ip.ip);
    
        memcpy(&g_ap_ip, &(cfg->ip), SIZEOF(NW_IP_S));
    
        sscanf(cfg->ip.ip, "%d.%d.%d.%d", &seg1, &seg2, &seg3, &seg4);
        snprintf(ip_prefix, SIZEOF(ip_prefix), "%d.%d.%d", seg1, seg2, seg3);
    
        if (cfg->p_len > 0) {
            len = snprintf(buf, SIZEOF(buf), ap_conf_fmt, WLAN_DEV, cfg->ssid, cfg->chan, cfg->max_conn, 2);
            len += snprintf(buf + len, SIZEOF(buf) - len, "wpa_passphrase=%s\n", cfg->passwd);
        } else {
            len = snprintf(buf, SIZEOF(buf), ap_conf_fmt, WLAN_DEV, cfg->ssid, cfg->chan, cfg->max_conn, 0);
        }
    
        op_ret = save_conf(HOSTAPD_CONF, buf, len);
        if (op_ret != OPRT_OK) {
            PR_ERR("fail to write %s", HOSTAPD_CONF);
        }
    
        len = snprintf(buf, SIZEOF(buf), udhcpd_conf_fmt, WLAN_DEV, ip_prefix, ip_prefix, cfg->ip.mask, cfg->ip.gw, cfg->ip.gw);
        op_ret = save_conf(UDHCPD_CONF, buf, len);
        if (op_ret != OPRT_OK) {
            PR_ERR("fail to write %s", UDHCPD_CONF);
        }
    
        snprintf(cmd, SIZEOF(cmd), "ifconfig %s %s netmask %s", WLAN_DEV, cfg->ip.ip, cfg->ip.mask);
        exec_command(cmd, NULL, 0);
        exec_command("ifconfig " WLAN_DEV " up", NULL, 0);
        tkl_system_sleep(1000);
        exec_command("hostapd -B -P /run/hostapd.pid " HOSTAPD_CONF, NULL, 0);
        exec_command("killall udhcpd", NULL, 0);
        exec_command("udhcpd " UDHCPD_CONF, NULL, 0);
    
        return OPRT_OK;
    }
    
  6. 实现 tkl_wifi_stop_ap 接口,在接口中实现关闭 AP 的功能。SDK 接收到来自手机 App 发送的配网信息后,使用该接口关闭 AP。

    OPERATE_RET tkl_wifi_stop_ap(VOID_T)
    {
        exec_command("killall udhcpd", NULL, 0);
        exec_command("killall hostapd", NULL, 0);
        exec_command("ifconfig " WLAN_DEV " down", NULL, 0);
    
        return OPRT_OK;
    }
    
  7. 实现 tkl_wifi_station_connect 接口,在接口中实现连接路由器的功能。udhcpc 作为 DHCP 客户端从路由器获取 IP 地址。

    OPERATE_RET tkl_wifi_station_connect(CONST SCHAR_T *ssid, CONST SCHAR_T *passwd)
    {
        OPERATE_RET op_ret = OPRT_OK;
        INT_T len = 0;
        CHAR_T buf[512] = {0};
        CHAR_T *wpa_conf_fmt = 
    "ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev\n"
    "update_config=1\n"
    "country=CN\n"
    "\n"
    "network={\n"
            "\tssid=\"%s\"\n"
            "\tpairwise=CCMP TKIP\n"
            "\tkey_mgmt=WPA-PSK\n"
            "\tgroup=CCMP TKIP\n"
            "\tpsk=\"%s\"\n"
    "}\n";
    
        if (!ssid || !passwd) {
            PR_WARN("ssid or passwd is null");
            return OPRT_INVALID_PARM;
        }
    
        tkl_wifi_stop_ap();
    
        PR_DEBUG("ssid: %s, passwd: %s", ssid, passwd);
    
        len = snprintf(buf, SIZEOF(buf), wpa_conf_fmt, ssid, passwd);
        op_ret = save_conf(WPA_SUPPLICANT_CONF, buf, len);
        if (op_ret != OPRT_OK) {
            PR_ERR("fail to write %s", UDHCPD_CONF);
        }
    
        exec_command("wpa_supplicant -B -Dnl80211 -i" WLAN_DEV " -c" WPA_SUPPLICANT_CONF, NULL, 0);
        exec_command("udhcpc -i " WLAN_DEV " -s /etc/udhcpc/default.script -p /run/udhcpc_wlan0.pid -b", NULL, 0);
    
        return OPRT_OK;
    }
    
  8. 实现 tkl_wifi_station_get_status 接口,在接口中实现获取无线连接状态的功能,有可能获取到的是 AP 的 IP 地址,所以需要对 IP 地址进行过滤。只有真正分配到 IP 地址才返回 WSS_GOT_IP 状态。

    OPERATE_RET tkl_wifi_station_get_status(WF_STATION_STAT_E *stat)
    {
        OPERATE_RET op_ret = OPRT_OK;
        NW_IP_S ip = {0};
    
        *stat = WSS_IDLE;
    
        op_ret = tkl_wifi_get_ip(WF_STATION, &ip);
        if (op_ret != OPRT_OK) {
            return op_ret;
        }
    
        if ((strlen(ip.ip) > 0) && (strncmp(g_ap_ip.ip, ip.ip, strlen(ip.ip)) != 0)) {
            *stat = WSS_GOT_IP;
        }
    
        return OPRT_OK;
    }
    
  9. 实现 tkl_wifi_get_ip 接口,在接口中实现获取无线接口的 IP 地址功能。

    OPERATE_RET tkl_wifi_get_ip(CONST WF_IF_E wf, NW_IP_S *ip)
    {
        struct ifreq ifr;
        int sock = 0;
    
        sock = socket(AF_INET, SOCK_DGRAM, 0);
        if (sock < 0) {
            PR_ERR("create socket failed");
            return OPRT_COM_ERROR;
        }
    
        strncpy(ifr.ifr_name, WLAN_DEV, strlen(WLAN_DEV) + 1);
    
        if (ioctl(sock, SIOCGIFADDR, &ifr) == 0)
            strncpy(ip->ip, inet_ntoa(((struct sockaddr_in *)&ifr.ifr_addr)->sin_addr), sizeof(ip->ip));
    
        if (ioctl(sock, SIOCGIFBRDADDR, &ifr) == 0)
            strncpy(ip->gw, inet_ntoa(((struct sockaddr_in *)&ifr.ifr_broadaddr)->sin_addr), sizeof(ip->gw));
    
        if (ioctl(sock, SIOCGIFNETMASK, &ifr) == 0)
            strncpy(ip->mask, inet_ntoa(((struct sockaddr_in *)&ifr.ifr_addr)->sin_addr), sizeof(ip->mask));
    
        close(sock);
    
        PR_DEBUG("WiFi ip->ip: %s", ip->ip);
    
        return OPRT_OK;
    }
    
  10. 实现 tkl_wifi_get_mac 接口,在接口中实现获取无线接口的 MAC 地址。

    OPERATE_RET tkl_wifi_get_mac(CONST WF_IF_E wf, NW_MAC_S *mac)
    {
        int i;
        int fd = -1;
        struct ifreq ifr;
        struct sockaddr *addr;
    
        fd = socket(AF_INET, SOCK_STREAM, 0);
        if (fd < 0) {
             PR_ERR("socket failed");
             return OPRT_SOCK_ERR;
        }
    
        memset(&ifr, 0, SIZEOF(ifr));
        strncpy(ifr.ifr_name, WLAN_DEV, SIZEOF(ifr.ifr_name) - 1);
        addr = (struct sockaddr *)&ifr.ifr_hwaddr;
        addr->sa_family = 1;
    
        if (ioctl(fd, SIOCGIFHWADDR, &ifr) < 0) {
            PR_ERR("ioctl failed");
            close(fd);
            return OPRT_COM_ERROR;
        }
    
        memcpy(mac->mac, addr->sa_data, MAC_ADDR_LEN);
        PR_DEBUG("WiFi mac->mac: %02X-%02X-%02X-%02X-%02X-%02X", mac->mac[0], mac->mac[1], mac->mac[2], \
                                                                 mac->mac[3],mac->mac[4],mac->mac[5]);
    
        close(fd);
    
        return OPRT_OK;
    }
    
  11. 其他非必要接口填充空实现。

    OPERATE_RET tkl_wifi_scan_ap(CONST SCHAR_T *ssid, AP_IF_S **ap_ary, UINT_T *num) 
    { return OPRT_OK; }
    
    OPERATE_RET tkl_wifi_release_ap(AP_IF_S *ap) 
    { return OPRT_OK; }
    
    OPERATE_RET tkl_wifi_set_cur_channel(CONST UCHAR_T chan) 
    { return OPRT_OK; }
    
    OPERATE_RET tkl_wifi_get_cur_channel(UCHAR_T *chan) { return OPRT_OK; }
    
    OPERATE_RET tkl_wifi_set_sniffer(CONST BOOL_T en, CONST SNIFFER_CALLBACK cb) { return OPRT_OK; }
    
    OPERATE_RET tkl_wifi_set_mac(CONST WF_IF_E wf, CONST NW_MAC_S *mac) { return OPRT_OK; }
    
    OPERATE_RET tkl_wifi_get_connected_ap_info(FAST_WF_CONNECTED_AP_INFO_T **fast_ap_info) { return OPRT_OK; }
    
    OPERATE_RET tkl_wifi_get_bssid(UCHAR_T *mac) { return OPRT_OK; }
    
    OPERATE_RET tkl_wifi_set_country_code(CONST COUNTRY_CODE_E ccode) { return OPRT_OK; }
    
    OPERATE_RET tkl_wifi_set_rf_calibrated(VOID_T) { return OPRT_OK; }
    
    OPERATE_RET tkl_wifi_set_lp_mode(CONST BOOL_T enable, CONST UCHAR_T dtim);
    
    OPERATE_RET tkl_wifi_station_fast_connect(CONST FAST_WF_CONNECTED_AP_INFO_T *fast_ap_info) { return OPRT_OK; }
    
    OPERATE_RET tkl_wifi_station_get_conn_ap_rssi(SCHAR_T *rssi) { return OPRT_OK; }
    
    OPERATE_RET tkl_wifi_send_mgnt(CONST UCHAR_T *buf, CONST UINT_T len) { return OPRT_OK; }
    
    OPERATE_RET tkl_wifi_register_recv_mgnt_callback(CONST BOOL_T enable, CONST WIFI_REV_MGNT_CB recv_cb) { return OPRT_OK; }
    
    OPERATE_RET tkl_wifi_ioctl(WF_IOCTL_CMD_E cmd,  VOID *args) { return OPRT_OK; }
    

注意事项

TKL 接口不允许阻塞,耗时的任务建议做异步处理。