【IoT】物联网之 WIFI 一键配网 smartConfig 浅析(ESP32)

593 阅读12分钟

一、背景

物联网时代技术开始规模化服务于民众,方便快捷显得尤为重要,WIFI 直连便是一个典型案例。

目前主流的 WIFI 配置模式有以下 2 种:

1、智能硬件处于 AP 模式(类似路由器,组成局域网),手机用于 STA 模式

手机连接到处于 AP 模式的智能硬件后组成局域网,手机发送需要连接路由的 SSID 及密码至智能硬件,智能硬件主动去连接指定路由后,完成配网

2、一键配网(smartConfig)模式

智能硬件处于混杂模式下,监听网络中的所有报文;手机 APP 将 SSID 和密码编码到 UDP 报文中,通过广播包或组播报发送,智能硬件接收到 UDP 报文后解码,得到正确的 SSID 和密码,然后主动连接指定 SSID 的路由完成连接。

AP 模式:

AP 是 (Wireless) Access Point 的缩写,即 (无线) 访问接入点。简单来讲就像是无线路由器一样,设备打开后进入 AP 模式,在手机的网络列表里面,可以搜索到类似 TPLINK_XXX 的名字(SSID)。

连接步骤:

1、智能硬件设备初始化并进入 AP 模式 2、手机扫描 WIFI 列表:扫描到智能硬件设备后(SSID)连接该智能硬件设备,通过 UDP 发送 经过 AES 加密过的 ssid/password/token 3、智能硬件设备通过 UDP 包获取配置信息,切换网络模式连接 WIFI 后配网完成

smartConfig 模式:

这种快速连接方式,相对于 AP 模式连接简化操作,更加贴近于市场

1、手机连上 WiFi,打开智能硬件指定 APP 软件,进入配置界面,输入手机所在 WiFi 密码,请求配网 TOKEN 2、智能硬件开启混杂模式监听所有网络数据包 3、手机通过广播、组播循环发送 ssid/password/token 4、硬件设备通过 UDP 包(长度)获取配置信息捕捉到 ssid/password/token,连接路由器(广播根据 UDP 包长度,组播根据 IP 地址信息)

从原理上讲只要芯片驱动支持开启混杂模式(WiFi Promiscuous),就可以支持一键配网功能

手机编码发送采用有 UDP 组播或广播,不同的发送方式和编码,对应的解码过程也不一样

1、广播:

发送方可通过改变其所需要发送数据包的长度进行控制,因此只要指定出一套利用长度编码的通讯协议,就可利用数据包的 Length 字段进行数据传递

2、 组播:

组播地址是保留的 D 类地址从224.0.0.0-239.255.255.255

IP 地址与 MAC 地址映射关系为:将 MAC 地址的前 25 位设定为 01.00.5e,而 MAC 地址的后 23 位对应 IP 地址位

故发送端可以将数据编码在组播 IP 的后 23bit 中,通过组播包发送接收端进行解码即可

二、smartConfig 原理浅析

在没有和其他设备(支持 smartConfig 技术)建立任何性质的通讯链路的情况下, 配置该设备接入 WIFI 网络 

普通权限的应用程序是没有能力完全控制和定义传输层及下层所有协议数据的, 唯一可以完全控制的就是应用层数据

本质上就是将 UDP 包头的数据长度作为 smartConfig 的数据,APP 端和设备端共用一套编码表即可解析数据 

TCP/IP 协议栈中的网络层和传输层的数据结构 

常用的网络层协议是 IPv4, IPv4 的头部绝大多数情况下都是定长的20字节

传输层协议是 UDP, 因为 UDP 协议头部为定长的 8 字节

明文长度 = 20 + 8 + dataLen

密文长度 = 20 + 8 + dataLen + 算法常量

例子:

如果需要发出一个密文长度为 500 字节的 802.11 帧,只需要在 UDP 中填充任意(500 – 20 – 8 – 算法常亮)个字节数据即可

因此,只需要利用可控的密文长度(dataLen)定义一张编码表即可将数据告诉任何知道这张编码表的设备(IoT硬件设备)

自定义一张编码表,流程如下:

dataLen --> 映射 1234    --> 起始符; 连续的3个起始符, 用于表示数据传输开始  1324    --> 结束符; 连续的3个结束符, 用于表示数据传输结束  110     --> 间隔符; 连续的2个间隔符, 用于表示数据符之间的间隔  1000    --> 数据符; 表示 ASCII 0x00  1001    --> 数据符; 表示 ASCII 0x01  …  1127    --> 数据符; 表示 ASCII 0x7F

假设我们要把字符串"Jay"告诉摄像头, 整个流程大致如下: (假设算法常亮为 16)

APP 端:   打开手机 APP, 在输入框中填入要发送的字符串”Jay”, 点击发送:

1.1、APP 连续发送 3 个 UDP 广播包, 填充数据为 1190 个字节 0x00 数据 ( 1234 – 16 – 20 – 8 = 1190 ), 表示传输开始  1.2、APP 发送     1 个 UDP 广播包, 填充数据为 1030 个字节 0x00 数据 ( 1074 – 16 -20 – 8 = 1030  ), 传输字符 J  1.3、APP 连续发送 2 个 UDP 广播包, 填充数据为 66   个字节 0x00 数据 ( 110 – 16 – 20 – 8 = 66    ), 表示数据间隔  1.4、APP 发送     1 个 UDP 广播包, 填充数据为 1053 个字节 0x00 数据 ( 1097 – 16 -20 – 8 = 1053  ), 传输字符 1097 对应 a  1.5、APP 连续发送 2 个 UDP 广播包, 填充数据为 66   个字节 0x00 数据 ( 110 – 16 – 20 – 8 = 66    ), 表示数据间隔  1.6、APP 发送     1 个 UDP 广播包, 填充数据为 1077 个字节 0x00 数据 ( 1121 – 16 -20 – 8 = 1077  ), 传输字符 1121 对应 y  1.7、APP 连续发送 3 个 UDP 广播包, 填充数据为 1280 个字节 0x00 数据 ( 1324- 16 – 20 – 8 = 1280  ), 表示传输结束

从步骤 1.1 开始循环多次, 直到超时或 IoT 设备成功接入 WIFI

IoT 设备端:

设备上电进入混杂模式,开始监听信号覆盖范围内的所有 WIFI 数据帧

捕获数据帧, 如果连续收到 3 个密文,其数据长度 dataLen 为 1234 字节, 且来自于同一个发射源 channel-A 的数据帧, 则进入下一步, 否则该步骤

捕获发射源 channel-A 的数据帧, 持续捕获密文数据长度为 110 或 1000-1127 之间的数据帧, 直到捕获到连续 3 个密文数据长度为 1324 的数据帧

将上述数据帧按照编码表进行映射, 由于手机 APP 并非是独占网络, 所以捕获到的数据可能有噪音, 比如解码出来的结果可能是(/表示分隔符): mnJ/o@a/ymmm

如果没有噪音, 记为候选数据RC, 重复捕获X, 进行二次验证, 通过则表示接收完成, 没通过也重复捕获 channel-A,

将这次所得结果同上一次做交集, 循环如此直到得出唯一结果, 即 RC, 之后再重复 5

由于捕获的数据帧头部信息中已经包含了 WIFI 的 BSSID 信息, 使用 "Jay" 作为密码去尝试连接相应的 WIFI

三、ESP32 配网实例:

手机 APP:

ESP-TOUCH

烧写设备端:

#include "WiFi.h"

void setup() { Serial.begin(115200);

//Init WiFi as Station, start SmartConfig WiFi.mode(WIFI_AP_STA); WiFi.beginSmartConfig();

//Wait for SmartConfig packet from mobile Serial.println("Waiting for SmartConfig."); while (!WiFi.smartConfigDone()) { delay(500); Serial.print("."); }

Serial.println(""); Serial.println("SmartConfig received.");

//Wait for WiFi to connect to AP Serial.println("Waiting for WiFi"); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }

Serial.println("WiFi Connected.");

Serial.print("IP Address: "); Serial.println(WiFi.localIP()); }

void loop() { // put your main code here, to run repeatedly:

} 四、知识扩展

在当前网络通信中有三种通信模式:

单播、广播、组播(又叫多播),其中多播出现的时间最晚,但同时具备单播和广播的优点,最具有发展前景.

通信方式分类:

    1.单播:单台主机与单台主机之间的通信;

    2.广播:单台主机与网络中所有主机的通信;

    3.组播:单台主机与选定的一组主机的通信;

单播:

     单播是网络通信中最常见的,网络节点之间的通信 就好像是人们之间的对话一样,如果一个人对另外一个人说话,那么用网络技术的术语来描述就是“单播”,此时信息的接收和传递只在两个节点之间进行。

     1. 单播的优点:

         (1)服务器以及响应客户端的请求;

         (2)服务器能针对每个客户端的不同请求发送不同的响应,容易显示个性化服务;

     2. 单播的缺点:

         服务器针对每个客户机发送数据流,服务器流量=客户机数量×客户机流量;在客户数量大、每个客户机流量大的流媒体应用中服务器不堪重负;

     3. 应用场景:

        单播在网络中得到了广泛的应用,网络上绝大部分的数据都是以单播的形式传输的,例如:收发电子邮件、游览网页时,必须与邮件服务器、服务器建立连接,此时使用的就是单播通信方式;

UDP 单播:

import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress;

// 客户端 public class ClientTest { private static final int MAXRECEIVED = 255;

public static void main(String[] args) throws IOException
{
    byte[] msg = new String("connect test successfully!!!").getBytes();

    DatagramSocket client = new DatagramSocket();

    InetAddress inetAddr = InetAddress.getLocalHost();
    SocketAddress socketAddr = new InetSocketAddress(inetAddr, 8888);

    DatagramPacket sendPacket = new DatagramPacket(msg, msg.length,
            socketAddr);

    client.send(sendPacket);

    client.close();
}

}

import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.util.Arrays;

//服务端 public class ServerTest { private static final int MAXREV = 255;

public static void main(String[] args) throws IOException
{
    DatagramSocket server = new DatagramSocket(8888);
    DatagramPacket recvPacket = new DatagramPacket(new byte[MAXREV], MAXREV);

    while (true)
    {
        server.receive(recvPacket);

        byte[] receiveMsg = Arrays.copyOfRange(recvPacket.getData(),
                recvPacket.getOffset(),
                recvPacket.getOffset() + recvPacket.getLength());

        System.out.println("Handing at client "
                + recvPacket.getAddress().getHostName() + " ip "
                + recvPacket.getAddress().getHostAddress());

        System.out.println("Server Receive Data:" + new String(receiveMsg));

        server.send(recvPacket);

    }

}

} 广播:

    广播可以比作为一个人通过广播喇叭对在场的全体说话,换句话说: 广播是一台主机对某一个网络上的所有主机发送数据报包。

    这个网络可能是网络,也可能时子网,还有可能是所有子网。

    广播有两类:本地广播和定向广播:

            定向广播:将数据报包发送到本网络之外的特定网络的所有主机,互联网上的大部分路由器都不转发定向广播消息;

            本地广播:将数据报包发送到本地网络的所有主机,IPv4的本地广播地址为“255.255.255.255”,路由器不会转发此广播;

    1.广播的优点:

       (1)通信的效率高,信息一下子就可以传递到某一个网络上的所有主机。

       (2)由于服务器不用向每个客户端单独发送数据,所以服务器流量比较负载低;

    2.广播的缺点:

       (1)非常占用网络的带宽;

       (2)缺乏针对性,也不管主机是否真的需要接收该数据, 就强制的接收数据;

    3.应用场景:

       有线电视就是典型的广播型网络

UDP 广播:

import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress;

//客户端 public class BroadcastSender { public static void main(String[] args) throws IOException { byte[] msg = new String("connection successfully!!!").getBytes(); /* * 在Java UDP中单播与广播的代码是相同的,要实现具有广播功能的程序只需要使用广播地址即可, 例如:这里使用了本地的广播地址 */ InetAddress inetAddr = InetAddress.getByName("255.255.255.255"); DatagramSocket client = new DatagramSocket();

    DatagramPacket sendPack = new DatagramPacket(msg, msg.length, inetAddr,
            8888);

    client.send(sendPack);
    System.out.println("Client send msg complete");
    client.close();
}

}

import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.util.Arrays;

//服务端 public class BroadcastReceive { public static void main(String[] args) throws IOException {

    DatagramPacket receive = new DatagramPacket(new byte[1024], 1024);
    DatagramSocket server = new DatagramSocket(8888);

    System.out.println("---------------------------------");
    System.out.println("Server current start......");
    System.out.println("---------------------------------");

    while (true)
    {
        server.receive(receive);

        byte[] recvByte = Arrays.copyOfRange(receive.getData(), 0,
                receive.getLength());

        System.out.println("Server receive msg:" + new String(recvByte));
    }

}

} 组播:

     组播可以比作为你对着大街喊:女士免费领优惠券,那么女士就会过来,男士就不会过来(组播:其中所有的女士就是一个组)

     换句话说:             组播是一台主机向指定的一组主机发送数据报包,因为如果采用单播方式,逐个节点传输,有多少个目标节点就会有多少次传送过程,这种方式显然效率极低,是不可取的;            如果采用不区分目标、全部发送的广播方式,虽然一次可以传送完数据,但是显然达不到区分特定数据接收对象的目的,又会占用网络带宽。            采用组播方式,既可以实现一次传送所有目标节点的数据,也可以达到只对特定对象传送数据的目的;

     IP 网络的组播一般通过组播 IP 地址来实现,组播 IP 地址就是 D 类 IP 地址,即 224.0.0.0 至 239.255.255.255 之间的IP地址。

     1.组播的优点:

        (1)具备广播所具备的所有优点;

        (2)与单播相比,提供了发送数据报包的效率,与广播相比,减少了网络流量;

     2.组播的缺点:

        与单播协议相比没有纠错机制,发生丢包错包后难以弥补,但可以通过一定的容错机制和QOS加以弥补;

UDP 组播:

import java.io.IOException; import java.net.DatagramPacket; import java.net.InetAddress; import java.net.MulticastSocket;

//客户端 public class MulticastSender { public static void main(String[] args) throws IOException { int port = 8888; byte[] msg = "Connection successfully!!!".getBytes();

    InetAddress inetRemoteAddr = InetAddress.getByName("224.0.0.5");

    /*
     * Java UDP组播应用程序主要通过MulticastSocket实例进行通信,它是DatagramSocket的是一个子类,
     * 其中包含了一些额外的可以控制多播的属性.
     * 
     * 注意:
     * 
     * 多播数据报包实际上可以通过DatagramSocket发送,只需要简单地指定一个多播地址。
     * 我们这里使用MulticastSocket,是因为它具有DatagramSocket没有的能力
     */
    MulticastSocket client = new MulticastSocket();

    DatagramPacket sendPack = new DatagramPacket(msg, msg.length,
            inetRemoteAddr, port);

    client.send(sendPack);

    System.out.println("Client send msg complete");

    client.close();

}

}

import java.io.IOException; import java.net.DatagramPacket; import java.net.InetAddress; import java.net.MulticastSocket; import java.util.Arrays;

//服务端 public class MulticastReceive { public static void main(String[] args) throws IOException { InetAddress inetRemoteAddr = InetAddress.getByName("224.0.0.5");

    DatagramPacket recvPack = new DatagramPacket(new byte[1024], 1024);

    MulticastSocket server = new MulticastSocket(8888);

    /*
     * 如果是发送数据报包,可以不加入多播组; 如果是接收数据报包,必须加入多播组; 这里是接收数据报包,所以必须加入多播组;
     */
    server.joinGroup(inetRemoteAddr);

    System.out.println("---------------------------------");
    System.out.println("Server current start......");
    System.out.println("---------------------------------");

    while (true)
    {
        server.receive(recvPack);

        byte[] recvByte = Arrays.copyOfRange(recvPack.getData(), 0,
                recvPack.getLength());

        System.out.println("Server receive msg:" + new String(recvByte));
    }

}

} refer:

wenku.baidu.com/view/ab6bc0… blog.csdn.net/sadshen/art…

my.oschina.net/u/2396236/b… blog.csdn.net/li_yangyang… blog.csdn.net/flyingcys/a…

blog.csdn.net/dabing69221…