智能配网方案 Airkiss 技术原理介绍及应用

2023-05-16

写在前面

站在巨人的肩膀上,可以看得更远

一、什么是Air Kiss

可以说AirKiss 是微信硬件平台提供的一种WIFI硬件设备快速配置连接网络的技术,是一种创新性的信息传递技术。通过该技术可以便捷的向一台具有WIFI功能的设备传递配网信息,比如wifi的ssid,密码。

在嵌入式设备或没有交互功能的智能设备,需要连接网络时,使用手机将设备需要配网的ap信息发送到空中,待配网设备捕获所发送的信息,通过解析获取需要的ssid和pwd. 示意图如下所示

 二、应用场景

随着移动互联网与物联网技术的发展,越来越多的设备具有了无线网络的接入能力,促使物联网进入大爆发阶段,这些设备的特点是小型化,低功耗,大多设备没有配置屏幕以及键盘,因此将设备连接网络就变成比较困难的问题,怎样将无线网络的SSID与密码传输到设备上,AirKiss技术正是为解决这种难题产生。通过该技术,不借助任何外设的情况下,通过已经接入ap的设备向没有连接ap的设备传递ssid和密码。类似设备有智能手环,无线监控,智能相册等。

AirKiss配网基本流程:

1,wifi设备能够以混杂模式sniffer运行

2,手机上安装微信客户端或含有该协议的其他应用

3,wifi设备通过抓包获取手机上发送的ssid 和密码,连接无线WiFi

三、技术原理

802.11是IEEE制定的无线局域网协议,802.11以802.2的逻辑链路控制封装来携带IP包因此能够以802.2SNAP格式接收无线网络数据。开启WIFI芯片的混杂模式监听空中的无线信号,并以802.2SNAP格式从数据链路层截取数据,得到SNAP数据包

--DA       目标MAC 地址

--SA        源MAC地址

--Length  数据的长度

--LLC       LLC头

--SNA      SNAP包括3bytes的厂商代码和2bytes的协议类型标识

--DATA    密文

--FCS      帧检验序列

怎样利用这个数据包发送我们想要送出去的数据呢?从无线信号监听角度讲, 各个字段都是暴露在外的,因此,Monitor模式监听可以获取相应的信息。但是从发送方来说,因为操作系统的限制,想拿到DA、SA、LLC、SNAP、FCS五个字段的控制需要很高的权限,一般很难拿到,所以只有Length一个字段给我们使用。发送方可以通过改变其所需要发送数据包的长度进行很方便的控制。所以,只要制定出一套利用长度编码的通信协议,就可利用802.2SNAP数据包中的Length字段进行信息传递。

实现原理:在实际应用中,采用UDP广播包的形式作为信息的载体。信息发送方向空间中发送一连串的UDP广播包,每一包的长度都按照Air Kiss协议进行编码,信息接收方利用混杂模式监听空间中的无线信号,并从数据链路层截取SNAP格式数据包,得到已经编码的Length字段,然后接收方按照约定的AirKiss协议解析出需要的信息。

四、通信协议

在信号载体方面,采用wifi无线信号进行信息传递,1-14信道支持。

在信号编码方面,802.2SNAP数据包中的Length字段为数据发送方唯一可控字段。所以AirKiss通信协议就是利用发送数据包中的长度编码,在本协议中,把Length字段编码位数限制在9bit,udp广播的最大长度为512字节。

在实际应用中,很有可能在一个空间中存在多个AP,AP可能分布在相同或不相同的信道上,配网设备处在混杂模式,接收信息不知道是在哪一个信道上,而且在同一个信道上也有可能很多设备在发送UDP广播包,就造成了监听接收到的信息是海量的,设备在接收到的大量信息中定位出发送方所在的信道和发送方的mac地址。第二个就是UDP广播包经过IP层,数据链路层的封装,加密,导致发送方发送UDP广播包的长度以接收方SNAP包中的Length字段存在差异,而且,由于加密方式的不确定,使得差异值也具有不确定性。

所以,在发送链路层数据之前,需要先发送400ms的前导域,前导域由4个字节组成,固定为1,2,3,4,接收方在一定时间内收到差值为1,2,3,4的连续数值,接收方接收到这些前导域数据包后, 利用SNAP包的Length字段与之相减,得到差异值,例如收到53,54,55,56,差异值就是53-1=52,固定在这个信道,以后收到的数据SNAP报的Length减去52,就获得实际信息数据。

链路层协议 

链路层数据结构分为两类,control字段和data字段,control字段包括magic code、prefix code 、sequence header field,data段只有一个data field,control字段和data字段怎么区别呢?他们以第8bit位区别,该位为1表示data field字段,为0表示control字段。

control字段中, magic code 和 prefix code 字段相同,magic code 字段与sequence header 字段通过第7bit位加以区分,改位为1表示sequence header, 0 表示magic code, prefix code;

(一) magic code 字段

magic code 由4个9bits组成,高5位位magic code, 低4位为information字段,前两个9bits的 information字段装载的是要发送数据长度,后面两个9bits的information装载的是要发送的crc8值。我们知道路由器的SSID是一直被路由以广播的形式发送到四周,使wifi设备能搜到该路由,这样我们在开始时获得搜到的路由的ssid,计算出对应ssid的CRC存储起来,与我们接收到的magic code 的CRC值一样,就可以匹配出ssid,减少传输数据,加快配网速度,所以magic code 可以适当多发送。

(二)prefix code 字段

 表格中描述的比较清楚了,前两个information字段装载要发送密码长度,后面两个information装载密码长度的crc8的值,它的作用是告诉接收端接下来要接收密码的长度,以便接收完数据后,对数据进行分割解密。

(三)sequence header 字段

 我们把待发送的数据以4字节划分,每4个数据组成一个sequence, 以sequence为单位进行数据发送,每个sequence都有sequence header 字段和data字段组成,最后一个sequence如果不够4个数据也不用补全。sequence header字段由两个9bits组成,第一个的低7位装载从本sequence index开始到本sequence结束发送的所有数据的crc8的低7位,在接收完一个sequence的数据后,进行crc8值校验,不同的话就说明接收错误,抛掉。data字段就是4个数据帧,包含要发送的数据。

(四)data 字段

data字段的数据结构可以看出,data字段由4个9bits组成,第8位为控制位,固定值为1,0-7位为需要传输的数据。

五、应用层协议

应用要发送的数据有三部分:密码,随机数,SSID。随机数的作用是当设备连上路由后,发送以随机数为内容的UDP广播包,当应用收到含该随机数广播包后就能确认设备已经准确接收到所有数据了,传输的都已字符串形式发送,以'\0'结尾,AES加密,发送的逻辑顺序是先发送密码,在发送随机数,最后发送ssid. 

 

六、案例

 设备端的主程序main.c,采用先搜索周围所有ap的ssid,保存下来,解决解析ssid中文错误问题,采用多线程处理,提高配网速度

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <pthread.h>
#include <sys/time.h>
#include <signal.h>
#include <pthread.h>
#include <unistd.h>

#include "capture/common.h"
#include "capture/osdep.h"
#include "utils/wifi_scan.h"
#include "airkiss.h"
#include "utils/utils.h"

#define CHANNELS_LENGTH 16
#define CHANNELS_TEMP_LENGTH 13

typedef struct{
    char essid[MAX_ESSID_SIZE];
    char bssid[MAX_BSSID_SIZE];
    unsigned char essid_crc;
    unsigned int freq;
    int power;
    int channel;
} wifi_info;

static airkiss_context_t *akcontex = NULL;
const airkiss_config_t akconfig = {
    (airkiss_memset_fn)&memset,
    (airkiss_memcpy_fn)&memcpy,
    (airkiss_memcmp_fn)&memcmp,
    (airkiss_printf_fn)&printf
};

airkiss_result_t ak_result;
struct wif *wi = NULL;

int g_channels[CHANNELS_LENGTH] = {1,6,11};
int g_channels_index = 0;
char * iface;

int switch_flag = 1;
int thread_run = 1;
struct timeval start;
pthread_mutex_t lock;

wifi_info g_wifi_info[50]; 

int udp_broadcast(unsigned char random, int port);
int wi_reset_manager(const char * iface);
/**
 * 信道排序
 */
void sort_channels(int * channels, int length){
    int i, j, temp, m_length=0;
    for(i=3;i<length; i++){
        if(channels[i] != 0){
            m_length++;
        }
    }
    printf("%s: %d",__func__, m_length);
    for(i=0; i < m_length-1; i++){
        for(j=3; j < m_length-1-i+3; j++){
            if(channels[j] > channels[j+1]){
                temp = channels[j+1];
                channels[j+1] = channels[j];
                channels[j] = temp;
            }
        }
    }
}

void switch_channel_func(){
    int ret = 0;
    pthread_mutex_lock(&lock);
    if(g_channels[g_channels_index] == 0 && g_channels[g_channels_index] < 20){
        g_channels_index = 0;
    }
    printf("%s: switch channel %d\n", __func__, g_channels[g_channels_index]);
    ret = wi->wi_set_channel(wi, g_channels[g_channels_index]);
    if (ret)
    {
        LOG_TRACE("Can't set channel to %d", g_channels[g_channels_index]);
    }
    airkiss_change_channel(akcontex);
    g_channels_index++;
    gettimeofday(&start, NULL); 
    pthread_mutex_unlock(&lock);
}


int process_airkiss(const unsigned char *frame, int size){
    int ret = 0;
    int i=0;

    ret = airkiss_recv(akcontex, (void *)frame, size);
    switch (ret)
    {
    case AIRKISS_STATUS_CONTINUE:

        break;
    case AIRKISS_STATUS_CHANNEL_LOCKED:
        //锁定信道, 停止切换操作
        LOG_TRACE("Lock channel %d", wi->wi_get_channel(wi));
        switch_flag = 0;
        break;
    case AIRKISS_STATUS_COMPLETE:
        //解析完成,获取数据
        airkiss_get_result(akcontex, &ak_result);
        for(i=0;i<50;i++){
            //printf("%s: channel = %d, gssid_crc = %x, crc = %x \n",__func__, g_wifi_info[i].channel, g_wifi_info[i].essid_crc, ak_result.ssid_crc);
            if(g_wifi_info[i].channel != 0 && ak_result.ssid_crc == g_wifi_info[i].essid_crc){
                    printf("%s: gessid = %s, ssid = %s\n", __func__,g_wifi_info[i].essid, ak_result.ssid);
                   if(strcmp(g_wifi_info[i].essid, ak_result.ssid)){
                       printf("%s: err ssid = %s \n", __func__, ak_result.ssid);
                       ak_result.ssid = g_wifi_info[i].essid;
                       break;
                   }else{
                       //一样的
                       break;
                   }
            }
        }
        LOG_TRACE("\nResult:  ssid_crc: %x, random = 0x%02x, pwd:%s, ssid:%s",
            ak_result.ssid_crc,ak_result.random, ak_result.pwd, ak_result.ssid);

		wi_reset_manager(iface);
        //连接网络操作

        //ap 连接网络后发送random
        udp_broadcast(ak_result.random, 24333);
        break;
    case AIRKISS_STATUS_DATAFIELD_TIMEOUT:
        LOG_TRACE(" Airkiss parse data field timeout");
        switch_flag = 1;
        break;
    default:
        break;
    }


    return ret;
}

void * switch_channel_thread_func(void *arg){
    struct timeval end;
    long time_value;

    printf("%s: start \n", __func__);
    while (thread_run)
    {
        usleep(200);
        if (switch_flag)
        {
            gettimeofday(&end, NULL);

            time_value = 1000000 * (end.tv_sec - start.tv_sec) + end.tv_usec - start.tv_usec;

            if (time_value > 0.5* 1000 * 1000 && switch_flag)
            {
                switch_channel_func();
            }
        }
    }
    return 0;
}

void * wi_thread_func(void *arg){
    int read_size;
    unsigned char buf[RECV_BUFSIZE] = {0};

    while(true){
        read_size = wi->wi_read(wi, buf, RECV_BUFSIZE, NULL);
        if(read_size < 0){
            LOG_ERROR("recv failed, ret = %d", read_size);
            break;
        }
        if(AIRKISS_STATUS_COMPLETE == process_airkiss(buf, read_size)) break;
    }

    thread_run = 0;
    return 0;
}

int udp_broadcast(unsigned char random, int port){
    int fd;
    int enabled = 1;
    int err;
    struct sockaddr_in addr;

    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_BROADCAST;
    addr.sin_port = htons(port);

    fd = socket(AF_INET, SOCK_DGRAM, 0);
    if(fd < 0){
        LOG_ERROR("Create socket failed");
        return -1;
    }

    err = setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (char *)&enabled, sizeof(enabled));
    if(err == -1){
        close(fd);
        return -1;
    }

    LOG_TRACE("Sending random to broadcast:");
    int i;
    useconds_t usecs = 20 * 1000;
    for(i=0; i<50; i++){
        sendto(fd, (unsigned char *)&random, 1, 0,(struct sockaddr *)&addr, sizeof(addr));
        usleep(usecs);
    }
    close(fd);

    return 0;
}

int wi_reset_manager(const char * iface){
    int fd;
    fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
    if(fd < 0){
        LOG_ERROR(" socket failed");
        return -1;
    }
    if(wi_reset_monitor_osdep(fd, iface) < 0){
        LOG_ERROR(" reset monitor failed");
        close(fd);
        return -1;
    }
    close(fd);

    return 0;
}

/**
 *  从第三个开始加入, 保证前3个始终是初始的 1,6,11 
 */
void add_channel(int *array_channel,int channel, int index, int lenght){
    int i=0, j=0;
    for(i=index;i < lenght; i++){
        if (array_channel[i] == 0) //先找一个未被设置的位置,判断此channel是否在g_channels[3,end]存在,存在舍弃,否则加入
        {
            for(j=index; j < lenght; j++){
                if(array_channel[j] == channel) return;
            }
            array_channel[i] = channel;
        }
    }
}

void add_wifi_info(wifi_info info){
    int i,j,index;

    for(i=0; i< 50; i++){
        index = i;
        if(g_wifi_info[i].channel == 0){break;}        
    }

    for(j=0;j<=index;j++){
        if(!strcmp(g_wifi_info[j].bssid, info.bssid)){
            return;
        }
    }
    if(index >= 50) return;
    g_wifi_info[index] = info;

}

int main(int argc, char *argv[])
{
    int i = 0, j, ret;
    int init;
    wireless_scan_head head;
    wireless_scan *presult = NULL;
    int temp_channels[CHANNELS_TEMP_LENGTH] = {0};
    pthread_t ak_pthread, switch_pthread;

    if (argc != 2)
    {
        LOG_ERROR("Please input wifi module, ex: %s wlan0", argv[0]);
        return -1;
    }

    iface = argv[1];

    //设置wifi mode 为managed
    wi_reset_manager(iface);
    switch_flag = 1;
    thread_run = 1;

    //扫描环境中现有的路由信道,做两次操作,比较合并
    for (i = 0; i < 2; i++)
    {
        ret = wifi_scan(iface, &head);
        if (ret < 0)
            continue;

        presult = head.result;
        wifi_info info;
        while (presult != NULL)
        {
            get_essid(presult, info.essid, MAX_ESSID_SIZE);
            get_bssid(presult, info.bssid, MAX_BSSID_SIZE);
            info.freq = get_freq_mhz(presult);
            info.power = get_strength_dbm(presult);
            info.channel = getChannelFromFrequency(info.freq);
            info.essid_crc = airkiss_crc8((unsigned char *)info.essid, strlen(info.essid));

            LOG_TRACE("bssid:[%s], channel:[%2d], power:[%d dBm], essid_crc:[%02x], essid:[%s]",
                      info.bssid, info.channel, info.power, info.essid_crc, info.essid);
            add_channel(temp_channels, info.channel, 0, CHANNELS_TEMP_LENGTH);
            add_wifi_info(info);
            presult = presult->next;
        }
        for (j = 0; j < CHANNELS_TEMP_LENGTH; j++)
        {
            add_channel(g_channels, temp_channels[j], 3, CHANNELS_LENGTH);
        }
        usleep(1000);
    }

    sort_channels(g_channels, CHANNELS_LENGTH);
    printf("g_channels: ");
    for (i = 0; i < CHANNELS_LENGTH; i++)
    {
        printf("%d ", g_channels[i]);
    }
    printf("\n");

    wi = wi_open(iface);
    if (wi == NULL)
    {
        LOG_ERROR("Can't init interface %s", iface);
        return -1;
    }

    akcontex = (airkiss_context_t *)malloc(sizeof(airkiss_context_t));
    init = airkiss_init(akcontex, &akconfig);
    if (init != 0)
    {
        LOG_ERROR("Airkiss init failed");
        return -1;
    }

    LOG_TRACE("Airkiss version:%s", airkiss_version());

    pthread_mutex_init(&lock, NULL);
    gettimeofday(&start, NULL); 

    ret = pthread_create(&ak_pthread, NULL, wi_thread_func, NULL);
    if(ret == 0){
        LOG_TRACE("phtread create success");
    }else{
        LOG_TRACE("phtread create failed");
    }

    ret = pthread_create(&switch_pthread, NULL, switch_channel_thread_func, NULL);
    if(ret == 0){
        LOG_TRACE("phtread create success");
    }else{
        LOG_TRACE("phtread create failed");
    }

    pthread_join(ak_pthread,NULL);
    pthread_join(switch_pthread, NULL);
    free(akcontex);

    return 0;
}

协议端代码可以去github上找,有大神贡献的,在此不方便贴出来。

应用端app示例

MainActivity.java

package com.android.airkisswechat;

import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import rx.Observable;
import rx.Observer;
import rx.Subscriber;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;

public class MainActivity extends AppCompatActivity {

    private static String TAG="Airkiss";

    @BindView(R.id.bt_confirm)
    Button mSendButton;

    @BindView(R.id.tv_ssid)
    TextView mSSIDTV;

    @BindView(R.id.et_password)
    EditText mPasswordEt;


    private boolean isSendPackage=false;

    private Subscription sendSubscribe;
    private Subscription receiveSubscribe;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

        ActionBar bar = getSupportActionBar();
        bar.setLogo(getResources().getDrawable(R.mipmap.ic_launcher));
        bar.setDisplayUseLogoEnabled(true);
        bar.setDisplayShowHomeEnabled(true);
        bar.setTitle("  Scics-SmartConfig");

    }

    @Override
    protected void onResume() {
        super.onResume();
        if (checkPermission(this)) {
            Context context = getApplicationContext();
            ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo networkInfo = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
            if (networkInfo.isConnected()) {
                final WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
                final WifiInfo connectionInfo = wifiManager.getConnectionInfo();
                if (connectionInfo != null) {
                    String ssid = connectionInfo.getSSID();
                    if (Build.VERSION.SDK_INT >= 17 && ssid.startsWith("\"") && ssid.endsWith("\""))
                        ssid = ssid.replaceAll("^\"|\"$", "");
                    mSSIDTV.setText(ssid);
                    //mSSIDTV.setEnabled(false);
                }
            }
        }
    }

    //检查位置权限
    public boolean checkPermission(Activity context) {
        //9.0以前版本获取wifi ssid不用申请此权限
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            return true;
        }
        int result = context.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION);
        if (result == PackageManager.PERMISSION_GRANTED) {
            return true;
        }
        String[] permission = {Manifest.permission.ACCESS_COARSE_LOCATION};
        context.requestPermissions(permission, 1001);
        return false;
    }


    @Override
    public void onRequestPermissionsResult(int requestCode,  String[] permissions, int[] grantResults) {
        if (requestCode == 1001) {
            if (grantResults.length > 0 && PackageManager.PERMISSION_GRANTED == grantResults[0]) {

            } else {
                AlertDialog.Builder alertDialog = new AlertDialog.Builder(this);
                alertDialog.setTitle("权限拒绝");
                alertDialog.setMessage("请在设置中打开此应用的位置权限后重试");
                alertDialog.setCancelable(false);
                alertDialog.setPositiveButton("确定", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        finish();
                    }
                });
                alertDialog.show();
            }
        }
    }



    @OnClick(R.id.bt_confirm)
    public void onViewClicked(View view) {
        switch (view.getId()){
            case R.id.bt_confirm:
                if (!isSendPackage) {
                    isSendPackage = true;
                    onConnectBtnClick(view);
                    mSendButton.setText(R.string.tx_stop);
                }else{
                    isSendPackage = false;
                    mSendButton.setText(R.string.tx_confirm);
                }
                break;
        }
    }




    public void onConnectBtnClick(View view) {
        if (sendSubscribe != null && sendSubscribe.isUnsubscribed()) {
            sendSubscribe.unsubscribe();
        }
        if (receiveSubscribe != null && receiveSubscribe.isUnsubscribed()) {
            receiveSubscribe.unsubscribe();
        }
        final String ssid = mSSIDTV.getText().toString();
        final String password = mPasswordEt.getText().toString();
        if (ssid.isEmpty() || password.isEmpty()) {
            Context context = getApplicationContext();
            CharSequence text = "请输入wifi密码";
            int duration = Toast.LENGTH_SHORT;
            Toast toast = Toast.makeText(context, text, duration);
            toast.show();
            return;
        }

        //发送AirKiss
        sendSubscribe = Observable.create(new Observable.OnSubscribe<String>() {
            @Override
            public void call(Subscriber<? super String> subscriber) {
                subscriber.onNext("start");
                byte DUMMY_DATA[] = new byte[1500];
                Log.i(TAG," send subscribe start  ");
                AirKissEncoder airKissEncoder = new AirKissEncoder(ssid, password);
                DatagramSocket sendSocket = null;
                try {
                    sendSocket = new DatagramSocket();
                    sendSocket.setBroadcast(true);
                    int encoded_data[] = airKissEncoder.getEncodedData();
                    for (int i = 0; i < encoded_data.length; ++i) {
                        DatagramPacket pkg = new DatagramPacket(DUMMY_DATA,
                                encoded_data[i],
                                InetAddress.getByName("255.255.255.255"),
                                10000);
                        sendSocket.send(pkg);
                        Thread.sleep(4);
                    }
                    Log.i(TAG," send subscribe oncmpleted  ");
                    subscriber.onCompleted();
                } catch (Exception e) {
                    subscriber.onError(e);
                    e.printStackTrace();
                } finally {
                    sendSocket.close();
                    sendSocket.disconnect();
                }
            }
        })
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<String>() {
                    ProgressDialog mDialog = new ProgressDialog(MainActivity.this);
                    @Override
                    public void onCompleted() {
                        Log.i(TAG,"send onCompleted");

                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.i(TAG,"send onError ");
                        Toast.makeText(MainActivity.this, "连接失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
                    }

                    @Override
                    public void onNext(String string) {
                        Log.i(TAG,"send onNext ="+string);

                        mDialog.setTitle("配网");
                        mDialog.setMessage("发送配网信息...");
                        mDialog.setCancelable(false);
                        mDialog.show();
                        new Handler().postDelayed(new Runnable() {
                            @Override
                            public void run() {
                                //发送配网ssid pwd,因为字节发送需要时间比较久,此处只是设置比较短的提示,但实际上后台线程还在发送中。
                                mDialog.dismiss();
                                receiveUDPPackage();
                            }
                        }, 5000);

                    }



                });




    }



    public void receiveUDPPackage(){
        //接收udp包
        receiveSubscribe = Observable.create(new Observable.OnSubscribe<String>() {
            @Override
            public void call(Subscriber<? super String> subscriber) {
                byte[] buffer = new byte[15000];
                DatagramSocket udpServerSocket = null;
                try {
                    udpServerSocket = new DatagramSocket(24333);
                    udpServerSocket.setSoTimeout(1000 * 60); //设置超时时间, 超过时间没有收到停止接收
                    DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
                    while (true) {
                        Log.d(TAG, "running");
                        udpServerSocket.receive(packet);
                        buffer = packet.getData();
                        String hexString = Str_Hex.byte2hex(buffer);
                        Log.d("received:", hexString);
                        //对收到的UDP包进行解码
                        //各个设备返回的UDP包格式不一样  将解码的UDP包通过RxJava发送到主线程 进行UI处理
                        if (!TextUtils.isEmpty(hexString)) {
                            Log.d("received:", hexString);
                            subscriber.onNext(hexString);
                            break;
                        }
                    }

                    subscriber.onCompleted();
                } catch (SocketException e) {
                    Log.i(TAG,"receive socket exception");
                    subscriber.onError(e);
                    e.printStackTrace();
                } catch (IOException e) {
                    Log.i(TAG,"receive io exception");
                    subscriber.onError(e);
                    e.printStackTrace();
                } finally {
                    Log.i(TAG,"receive finally");
                    udpServerSocket.close();
                    udpServerSocket.disconnect();
                }
            }
        }).subscribeOn(Schedulers.newThread())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Subscriber<String>() {
                    ProgressDialog mDialog = new ProgressDialog(MainActivity.this);

                    @Override
                    public void onStart() {
                        super.onStart();
                        mDialog.setTitle("配网");
                        mDialog.setMessage("正在连接...");
                        mDialog.setCancelable(false);
                        mDialog.show();
                    }

                    @Override
                    public void onCompleted() {
                        mDialog.dismiss();
                        mSendButton.performClick();
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.i(TAG,"receive onError");
                        Toast.makeText(MainActivity.this, "连接失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
                        mDialog.dismiss();
                        mSendButton.performClick();
                    }

                    @Override
                    public void onNext(String s) {
                        Log.i(TAG,"receive onNext");
                        Toast.makeText(MainActivity.this, "收到的UDP包:" + s, Toast.LENGTH_SHORT).show();
                    }

                });
    }


    @Override
    protected void onStop() {
        Log.i(TAG, "onStop ");
        super.onStop();

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.i(TAG, "onDestroy");
        if (sendSubscribe != null && sendSubscribe.isUnsubscribed()) {
            sendSubscribe.unsubscribe();
        }
        if (receiveSubscribe != null && receiveSubscribe.isUnsubscribed()) {
            receiveSubscribe.unsubscribe();
        }
        mSendButton.performClick();
    }

}

AirKissEncoder.java

package com.android.airkisswechat;

import java.util.Arrays;
import java.util.Random;

public class AirKissEncoder {
    private int mEncodedData[] = new int[2 << 15];
    private int mLength = 0;

    // Random char should be in range [0, 127).
    private char mRandomChar = (char)(new Random().nextInt(0x7F));

    public AirKissEncoder(String ssid, String password) {
        int times = 10;     //设置发送的数据组数,此处设为10,说明要发送10个这个的一串数据
        while (times-- > 0) {
            leadingPart();
            magicCode(ssid, password);

            for (int i = 0; i < 15; ++i) {
                prefixCode(password);
                String data = password + mRandomChar + ssid;
                int index;
                byte content[] = new byte[4];
                for (index = 0; index < data.length() / 4; ++index) {
                    System.arraycopy(data.getBytes(), index * 4, content, 0, content.length);
                    sequence(index, content);
                }

                if (data.length() % 4 != 0) {
                    content = new byte[data.length() % 4];
                    System.arraycopy(data.getBytes(), index * 4, content, 0, content.length);
                    sequence(index, content);
                }
            }
        }
    }

    public int[] getEncodedData() {
        return Arrays.copyOf(mEncodedData, mLength);
    }

    public char getRandomChar() {
        return mRandomChar;
    }

    private void appendEncodedData(int length) {
        mEncodedData[mLength++] = length;
    }

    private int CRC8(byte data[]) {
        int len = data.length;
        int i = 0;
        byte crc = 0x00;
        while (len-- > 0) {
            byte extract = data[i++];
            for (byte tempI = 8; tempI != 0; tempI--) {
                byte sum = (byte) ((crc & 0xFF) ^ (extract & 0xFF));
                sum = (byte) ((sum & 0xFF) & 0x01);
                crc = (byte) ((crc & 0xFF) >>> 1);
                if (sum != 0) {
                    crc = (byte)((crc & 0xFF) ^ 0x8C);
                }
                extract = (byte) ((extract & 0xFF) >>> 1);
            }
        }
        return (crc & 0xFF);
    }

    private int CRC8(String stringData) {
        return CRC8(stringData.getBytes());
    }

    private void leadingPart() {
        for (int i = 0; i < 50; ++i) {
            for (int j = 1; j <= 4; ++j)
                appendEncodedData(j);
        }
    }

    private void magicCode(String ssid, String password) {
        int length = ssid.length() + password.length() + 1;
        int magicCode[] = new int[4];
        magicCode[0] = 0x00 | (length >>> 4 & 0xF);
        if (magicCode[0] == 0)
            magicCode[0] = 0x08;
        magicCode[1] = 0x10 | (length & 0xF);
        int crc8 = CRC8(ssid);
        magicCode[2] = 0x20 | (crc8 >>> 4 & 0xF);
        magicCode[3] = 0x30 | (crc8 & 0xF);
        for (int i = 0; i < 20; ++i) {
            for (int j = 0; j < 4; ++j)
                appendEncodedData(magicCode[j]);
        }
    }

    private void prefixCode(String password) {
        int length = password.length();
        int prefixCode[] = new int[4];
        prefixCode[0] = 0x40 | (length >>> 4 & 0xF);
        prefixCode[1] = 0x50 | (length & 0xF);
        int crc8 = CRC8(new byte[] {(byte)length});
        prefixCode[2] = 0x60 | (crc8 >>> 4 & 0xF);
        prefixCode[3] = 0x70 | (crc8 & 0xF);
        for (int j = 0; j < 4; ++j)
            appendEncodedData(prefixCode[j]);
    }

    private void sequence(int index, byte data[]) {
        byte content[] = new byte[data.length + 1];
        content[0] = (byte)(index & 0xFF);
        System.arraycopy(data, 0, content, 1, data.length);
        int crc8 = CRC8(content);
        appendEncodedData(0x80 | crc8);
        appendEncodedData(0x80 | index);
        for (byte aData : data)
            appendEncodedData(aData | 0x100);
    }
}

测试时需要先启动设备端程序,然后在手机上发送配网信息,根据周围环境条件,配网速度可能或快或慢,本文章旨在记录工作中学习,长时间不用忘记,方便回忆。

 

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

智能配网方案 Airkiss 技术原理介绍及应用 的相关文章

  • Git合并指定文件到其他分支

    1 合并某个分支上的指定commit span class token comment 在dev分支合并bugfix分支上的 ac0ca63 commit span git branch span class token operator
  • shell命令—find

    find命令 span class token comment 删除 home fengshuiyue目录下一周前的目录 span fengshuiyue 64 ralc span class token operator gt span
  • shell命令—date

    date命令 span class token comment 获取当前时间 日期格式是 YYYY mm dd HH MM SS span span class token function date span span class tok
  • 大华硬盘录像机、网络摄像机、 网络硬盘录像机外网远程设置DDNS方法

    1 为了便于解释在下文介绍中 xff0c 硬盘录像机 网络摄像机 网络硬盘录像机统一称为 大华设备 2 外网最好是电信 如果不是 xff0c 那必须要确认客户的外网 IP 是唯一的 xff0c 不是与其他用户共用的 3 首先确保 大华设备
  • LibCurl HTTP部分详细介绍

    目录索引 xff1a 一 LibCurl基本编程框架 二 一些基本的函数 三 curl easy setopt函数部分选项介绍 四 curl easy perform 函数说明 xff08 error 状态码 xff09 五 libcurl
  • navicat连接oracle报错:ORA-12737 Instant Client Light:unsupported server character set ZHS16GBK

    今天使用Navicat连接Oracle数据库 xff0c 报了下面的这个错误 xff1a ORA 12737 Instant Client Light unsupported server character set ZHS16GBK 从这
  • RabbitMQ 与 Kafka深度解析(二)

    介绍 作为一名处理大量基于微服务的系统的软件架构师 xff0c 我经常会遇到一个反复出现的问题 xff1a 我应该使用RabbitMQ还是Kafka xff1f 出于某种原因 xff0c 许多开发人员认为这些技术是可以互换的 虽然在某些情况
  • VMWare报错"指定的文件不是虚拟磁盘"或“The file specified is not a virtual disk”

    今天打开原来创建的虚拟机 xff0c 突然报错 指定的文件不是虚拟磁盘 xff0c 如下图 xff1a 由于之前这个虚拟机创建了快照 xff0c 因此下面的解决方法是基于快照的 1 打开虚拟机的 vmx文件 xff0c 我的虚拟机名字为 U
  • Javascript模块加载框架——seajs

    最近看了一些开源web的项目 xff0c 发现其前台采用的框架seajs在编写JavaScript代码上很是方便 xff0c 现将学习的记录记于此 1 什么是JavaScript模块加载 为了解决不同javascript库里操作对象的命名冲
  • JS实现浏览器打印、打印预览

    目前正在做浏览器端采用JS方式实现打印这么一个功能 xff0c JS打印实现的方法很多 xff0c 但是兼容各个浏览器实现打印预览的功能有些棘手 xff0c 现将实现的内容及遇到的问题记录下来 xff0c 希望有大牛看到所提的问题后可以给予
  • 日期与天干地支算法

    天干地支五行对照表 天干 地支与五行的对应表 甲 乙 丙 丁 戊 己 庚 辛 壬 癸 阳 阴 阳 阴 阳 阴 阳 阴 阳 阴 木 木 火 火 土 土 金 金 水 水 子 丑 寅 卯 辰 巳 午 未 申 酉 戌 亥 鼠 牛 虎 兔 龙 蛇 马
  • TR技术评审节点

    产品开发中 xff0c TR是技术评审节点 下面是某产品的技术评审点 xff0c 供参考 xff1a TR1 概念阶段技术评审点 xff1a 产品需求和概念技术评审 xff08 业务需求评审 xff09 是ISO IEC TR 19768
  • linux虚拟机重新启动后不能联网的问题

    这里解决的是linux中 xff0c 曾经能连上网的情况下 xff0c 重启后或一段时间没上后 xff0c 不能联网的问题 xff08 安装后就没连上网 xff0c 就乖乖安装各种教程来一下啊 xff0c 这里可能不一定适合你 当然 xff
  • Spring MVC执行流程

    Spring MVC 框架是高度可配置的 xff0c 包含多种视图技术 xff0c 例如 JSP FreeMarker Tiles iText 和 POI Spring MVC 框架并不关心使用的视图技术 xff0c 也不会强迫开发者只使用
  • 数据分析必备——SQL入门基础知识

    数据说 梦想季 一直很喜欢一句话 xff1a 山鸟与鱼不同路 xff0c 从此山水不相逢 意思就是如果你现在不够优秀 xff0c 即使遇见了 xff0c 也不配拥有 努力是唯一的方向 xff01 导读 xff1a 科学技术的快速发展正在改变
  • CentOS6.5安装nginx

    转载 xff1a http blog csdn net yinwenjie article details 46620711 2 Nginx的安装 2 1 准备工作 操作系统 xff1a centOS 6 5 Nginx的下载地址 xff1
  • Spring源码剖析之AbstractApplicationContext抽象类的refresh()方法

    Spring源码剖析之AbstractApplicationContext抽象类的refresh 方法 简书 Spring中AbstractApplicationContext抽象类的refresh 方法是用来刷新Spring的应用上下文的
  • Python多进程任务如何识别谁是谁

    在Python多进程 多线程编程中 xff0c 如果使用future和add done callback 经常会遇到如何区分异步任务的问题 这里推荐两种方式 方式一 xff1a 给future新增属性 def call back futur

随机推荐

  • spring事务配置的五种方式

    https blog csdn net m8396017 article details 51615806 spring事务配置的五种方式 第一种方式 xff1a 每个Bean都有一个代理 第二种方式 xff1a 所有Bean共享一个代理基
  • dependency 和dependencyManagement 的区别

    https blog csdn net m0 37664223 article details 105836943 dependencyManagement 和dependency 的区别 dependencyManagement 我们项目
  • Ubuntu系统使用root远程登录

    https blog csdn net weixin 45239621 article details 115523031 Ubuntu系统使用root远程登录的操作 一般这种情况是某某云服务器或者新的Ubuntu系统 xff08 仅Ubu
  • npm 安装详细教程

    https blog csdn net Cleve baby article details 125632341 转载 xff1a http xiaoyaojones blog 163 com blog static 28370125201
  • Linux新建用户,切换后只显示$问题

    问题 xff1a linux新建用户 xff0c 切换后只显示 的问题 xff0c 而且有些命令也使用不了 xff0c 解决方法如下 1 新建用户命令 root登录 useradd d usr sam m jiang 此命令创建了一个用户j
  • DTI(dwi)使用FSL做预处理及做TBSS处理流程(fsleyes查看结果)

    预处理 刚开始我用的数据初始文件是多个dcm格式的文件 xff0c 当时我从格式转换 提取b0 波脑 涡流 计算张量下来是没有问题的 后来我用dwi的文件 xff08 包含四维的 nii gz bvec bval文件 xff09 xff0c
  • 引用的问题

    1 二者的区别 xff08 1 xff09 引用 访问一个变量是直接访问 xff0c 而指针是间接访问 xff08 2 xff09 引用 是一个变量的别名 xff0c 本身不单独分配自己的内存空间 xff0c 而指针有自己的内存空间 xff
  • Android抓取log文件的方法

    很简单 xff0c 使用命令行来就可以 xff0c 步骤如下 xff1a 1 adb devices 检查调试设备是否连接好 xff0c 没有调试设备抓不了 2 adb logcat c 3 cd Desktop 进入你要存放日志文件的目录
  • ESLint 配置入门

    大家好 xff0c 我是前端西瓜哥 xff0c 今天带大家了解 ESLint 的配置项 ESLint 是一款检查 JavaScript 程序是否符合特定的规则的工具 比如字符串用单引号还是双引号 xff0c tab 缩进用 2 个空格还是
  • linux开机自动登陆

    在超级用户的身份下 编辑vim etc gdm custom conf span class hljs number 1 span span class hljs comment GDM configuration storage span
  • Python(Web服务)全链路日志个跟踪

    1 背景 在我们的实际项目中 xff0c 尤其以Web服务为例 xff0c 经常遇到要做日志跟踪的场景 我们经常采用的方式是 xff0c 生成一个trace id 在全链路的调用中都输出这个id进行跟踪 这里需要处理的几个问题是 xff1a
  • BCLinux用yum安装报Your license is invalid.

    1 介绍 BCLinux基于 CentOS 进行定制化 xff0c 满足企业对于通用 Linux 操作系统的需求 xff0c 提供标准化 平台化的产品发布及运行环境 xff0c 该版本与红帽商业版本及社区版本完全兼容 2 步骤 安装好操作系
  • Excel表格数据如何批量乘以一个数字

    今天跟大家分享一下Excel表格数据如何批量乘以一个数字 1 打开Excel文件 xff0c 我们想要批量将数字乘以10 2 首先我们选择所有数据单元格区域 3 点击下图选项 xff08 Excel工具箱 xff0c 百度即可了解详细下载安
  • Win2003系统部署SSL证书(部署https教程)

    在windows 2003操作系统下 xff0c IIS 6 环境的服务器SSL证书安装教程 安装前 xff0c 请准备好SSL证书 部署前请退出服务器内安装的杀毒软件 xff08 360 金山 安全狗等安全软件有可能导致SSL证书部署出错
  • Android启动模式之singleinstance的坑

    Android启动模式之singleinstance的坑 前言 在实际应用中 xff0c 使用singleinstance启动模式时 xff0c 会遇到一些奇奇怪怪的问题 Android有四种启动模式 xff0c 分别是standard x
  • 关于使用iconfont图标总生成小长方形框的解决办法

    起因是在联系仿写网易的静态页面的时候 xff0c 使用iconfont小图标的时候 xff0c 引入正确 xff0c 但是在页面上一直显示的是一个长方形小框的页面 各种搜索终于找到了解决办法 主要原因是由于iconfont css中路径不对
  • Qt和MFC的效率对比

    Qt和MFC的效率对比 之前一直做mfc xff0c 昨天看了一晚上的Qt xff0c 瞬间就喜欢上它了 xff0c Qt在windows下应该没有mfc的运行效率高 xff0c 但是我想知道差多少 xff0c 不知有没有大牛做过这方面的对
  • android 移植

    from http wiki kldp org wiki php AndroidPortingOnRealTarget s 6 1 Contents 1 Introduction 2 Copyright and Acknowledgemen
  • LD3320语音识别模块+JQ8900-TF语音模块实现简单的语音交互控制

    玩这个真的要感谢头条的强大推荐 xff0c 清明节回家的大巴车上 xff0c 无聊的刷着头条 xff0c 很智能的给我推荐了一款语音识别模块 xff0c 一直很想自己试着玩一把的我 xff0c 按奈不住 xff0c 点开视频看到了介绍 xf
  • 智能配网方案 Airkiss 技术原理介绍及应用

    写在前面 站在巨人的肩膀上 xff0c 可以看得更远 一 什么是Air Kiss 可以说AirKiss 是微信硬件平台提供的一种WIFI硬件设备快速配置连接网络的技术 xff0c 是一种创新性的信息传递技术 通过该技术可以便捷的向一台具有W