配网方式-热点

更新时间:2024-01-31 05:55:24下载pdf

本文介绍 SDK 中的 热点配网 功能。首先,将阐述其工作原理,随后详细说明如何实施热点配网,以便实现将网关设备接入涂鸦 IoT 开发平台的目标。

概述

功能描述

热点配网是指通过网关设备开启 Wi-Fi 热点,手机连接至该热点后,通过 App 向网关设备发送路由器的 SSID、密码及激活 Token。网关设备接收这些信息后,使用相应的 SSID 和密码连接路由器,并利用激活 Token 绑定到涂鸦 IoT 开发平台。

SDK 已内置配网流程的业务逻辑,通过定义一套 TuyaOS Kernel Layer(简称 TKL)接口来屏蔽硬件与系统的差异,您只需适配 TKL 接口即可。

工作原理

热点配网的工作原理如下:

  1. 网关设备开启无线 AP 后,手机连接至该 AP,从而使手机与网关设备处于同一局域网。
  2. 网关设备定期在局域网发送加密的 UDP 广播数据包,包含设备信息。
  3. 用户在手机 App 上输入路由器的 SSID 和密码后,手机 App 与设备建立 TLS 连接,传输配网授权信息及路由器的 SSID 和密码。
  4. 设备解析这些信息后,切换至 Station 模式来连接路由器,并完成激活流程。

热点配网的流程图如下所示:

SDK开发者手机 App云端设置 Wi-Fi 事件回调Wi-Fi 切换到 AP 模式开启 AP请求激活 token返回激活 token连接设备的 APUDP 广播 <port: 6668>建立 TCP 连接 <port: 6667>发送配网信息关闭 APWi-Fi 切换到 Station 模式连接路由器推送 Wi-Fi 连接事件激活返回设备 schemaSDK开发者手机 App云端

开发指导

使用方法

热点配网过程中,SDK 须完成多项任务,例如切换无线工作模式、开启/关闭 AP、连接路由器、获取无线连接状态及无线接口 IP 地址等。因此,您需适配以下 TKL 接口。

  • 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,以便手机连接并传输配网信息。

  • tkl_wifi_stop_ap

    OPERATE_RET tkl_wifi_stop_ap(VOID_T);
    

    配网结束时,SDK 调用此接口关闭设备的 AP。

  • tkl_wifi_set_work_mode

    OPERATE_RET tkl_wifi_set_work_mode(CONST WF_WK_MD_E mode);
    

    SDK 需切换无线工作模式时调用此接口,例如在 tkl_wifi_start_ap 前设置 AP 模式,在 tkl_wifi_station_connect 前设置 Station 模式。

  • 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 地址时调用此接口。

使用示例

以下示例展示了如何使用 wpa_supplicanthostapdudhcpcudhcpd 来实现热点配网功能。

代码仅提供实现思路,需根据实际无线芯片进行适配和优化。

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

    // ...
    
    int main(int argc, char **argv)
    {
        // ...
    #if defined(GW_SUPPORT_WIRED_WIFI) && (GW_SUPPORT_WIRED_WIFI==1)
        /* < support wireless && wired > */
        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)
        /* < only support wireless > */
        TUYA_CALL_ERR_RETURN(tuya_iot_wf_sdk_init(GWCM_OLD, WF_START_AP_ONLY, PID, USER_SW_VER, NULL, 0));
    #else
        /* < other > */
        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);
        }
    }
    
    /**
     * @brief Set Wi-Fi station work status change callback
     *
     * @param[in]      cb: the Wi-Fi station work status change callback
     * @return OPRT_OK on success. Others on error, please refer to tuya_error_code.h
     */
    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 模式。

    /**
     * @brief Set Wi-Fi work mode
     *
     * @param[in]       mode: Wi-Fi work mode
     * @return OPRT_OK on success. Others on error, please refer to tuya_error_code.h
     */
    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 接口设置的模式一致。

    /**
     * @brief Get Wi-Fi work mode
     *
     * @param[out]      mode: Wi-Fi work mode
     * @return OPRT_OK on success. Others on error, please refer to tuya_error_code.h
     */
    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 参数的值。

    /**
     * @brief Start a soft AP
     *
     * @param[in]       cfg: the soft AP config
     * @return OPRT_OK on success. Others on error, please refer to tuya_error_code.h
     */
    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。

    /**
     * @brief Stop a soft AP
     *
     * @return OPRT_OK on success. Others on error, please refer to tuya_error_code.h
     */
    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 地址。

    /**
     * @brief Connect Wi-Fi with SSID and password
     *
     * @param[in]       SSID
     * @param[in]       password
     * @return OPRT_OK on success. Others on error, please refer to tuya_error_code.h
     */
    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 状态,否则设备处于未联网状态,进入激活流程会导致超时。

    /**
     * @brief Get Wi-Fi station work status
     *
     * @param[out]      stat: the Wi-Fi station work status
     * @return OPRT_OK on success. Others on error, please refer to tuya_error_code.h
     */
    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 地址。

    /**
     * @brief Get Wi-Fi IP info. When Wi-Fi works in
     *        AP + station mode, Wi-Fi has two IP addresses.
     *
     * @param[in]       wf: Wi-Fi function type
     * @param[out]      ip: the IP address info
     * @return OPRT_OK on success. Others on error, please refer to tuya_error_code.h
     */
    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 地址。

    /**
     * @brief Get Wi-Fi MAC info. When Wi-Fi works in
     *        AP + station mode, Wi-Fi has two MAC addresses.
     *
     * @param[in]       wf: Wi-Fi function type
     * @param[out]      mac: the MAC info
     * @return OPRT_OK on success. Others on error, please refer to tuya_error_code.h
     */
    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. 实现 tkl_wifi_scan_ap 接口,扫描附近的 AP。

  12. 实现 tkl_wifi_set_country_code 接口,设置国家码。

  13. 其他非必要接口填充空实现。

    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 接口不允许阻塞,耗时的任务建议做异步处理。
  • App 会根据 IP 地址段选择不同的加密方式。开启设备的 AP 时,需要根据 SDK 提供的配置把无线接口设置成对应的 IP 地址,当前版本里 IP 地址是 192.168.176.1