一次偶然的机会,群里某开发者问我为什么他的微信配网出问题了,我跟着回复了些问题,发现并不是那么简单,于是乎帮这位朋友适配了下并成功了,决定在全网首个开源可实现Airkiss配网的微信小程序。
获取源码请见文章底部。
1
揭开微信配网的神秘面纱AirKiss微信配网虽然是2016年开放的技术,但一直是智能家居配网领域值得深究的话题!因为在实际Wi-Fi产品用到太多了,今天大家依然非常关注这个问题,今天我就用安信可 ESP-C3-12F 模组调试,全面给大家解析我是如何在微信小程序上实现这个协议的。
首先我们理清几个问题,也是我移植前所思考的问题:
1. airkiss实现的原理?协议?配网过程?
2. 对于airkiss源码,有什么途径获取?可以从设备端、前端拿到协议对比吗?
3. 对于一键配网的弊端,你是如何优化的?为什么有些路由器怎么都无法配网?
4. 微信小程序这块支持本地组网?如何调用相关函数方法?
了解AirKiss协议:
AirKiss是微信硬件平台为Wi-Fi设备提供的微信配网、局域网发现和局域网通讯的技术。开发者若要实现通过微信客户端对Wi-Fi设备配网、通过微信客户端在局域网发现Wi-Fi设备,或者把微信客户端内的音乐、图片、文件等消息通过局域网发送至Wi-Fi设备,需要在硬件设备中集成相应的AirKiss静态库。
AirKiss配网的步骤:
1. 安信可ESP-C3-12F模组开启station模式,并抓取空中的混杂UDP包;
2. 手机微信客户端通过AirKiss发送家里的路由器ssid和密码;
3. 安信可ESP-C3-12F模组通过抓包获取到ssid和密码,然后连接到家里的路由器;
4. 安信可ESP-C3-12F模组局域网发送UDP包给手机客户端表示配网成功;
AirKiss配网的基本原理:
1. 混杂模式
安信可ESP-C3-12F模组刚开始同样是以Station的模式运行,但是还有一个监听模式,我们也称为嗅探探针。是什么意思?它是指正常的wifi设备都有一个MAC地址,其硬件电路会自动过滤目标MAC地址跟其MAC不同的数据包。开启混杂模式就是我们平常时说的抓包,就是空中符合802.11格式的数据包都接收进来,不管MAC是否一样。很明显,手机智能配置APP并不知道该wifi设备的MAC地址,所以手机wifi发送出的数据包,通过家里的路由器转发出去时,wifi设备必须要在混杂模式下才能接收到这些数据包。
2. 信道切换
802.11有多个信道,某一个时候wifi设备和路由器都处于某一个信道。路由器一般都是默认在第六信道,所以要想家里的网信号好一点,可以尝试将路由器的信道改到一个其他道,这样就不会和邻居家的wifi信道重叠了。wifi信号混在同一个频道就会互相干扰。
同理,我们也不能假定wifi设备是处于哪个信道,但是我们可以在app中确定手机wifi的发送信道,这样要求wifi设备在一定的时刻切换信道,以便与接收到数据包。当wifi设备检测到有效的数据包,要锁定在该信道进行后续的通信。
3. 利用数据帧的长度来承载有效信息
我们先来看一看802.2 SNAP(802.11的物理层协议)的数据帧格式
DA字段表示目标mac地址,SA字段表示源mac地址,Length字段表示后面数据的长度,LLC字段表示LLC头,SNAP字段包括3bytes的厂商代码和2bytes的协议类型标识,DATA字段为负载,对于加密信道来说是密文的,FCS字段表示帧检验序列。
从无线信号监听方的角度来说,不管无线信道有没有加密DA、SA、Length、LLC、SNAP、FCS字段总是暴露的,因此信号监听方便有了从这6个字段获取信息的可能。但从发送方的角度来说,由于操作系统的限制(比如Android或者IOS),DA、SA、LLC、SNAP、FCS五个字段的控制需要很高的控制权限,发送方一般是很难拿到的。因此只剩下Length这一字段,发送方可以通过改变其所需要发送数据包的长度进行很方便的控制。
所以,只要制定出一套利用长度编码的通信协议,那么就可利用802.2 SNAP 数据包中的Length字段进行信息传递。
在实际应用中,我们采用UDP广播包作为信息的载体。信息发送方向空间中发送一系列的UDP广播包,其中每一包的长度(即Length字段)都按照AirKiss通信协议进行编码,信息接收方利用混杂模式监听空间中的无线信号,并从数据链路层截取802.2 SNAP格式数据包,便可得到已编码的Length字段,随后接收方便可根据AirKiss通信协议解析出需要的信息。
2
ESP-C3-12F模组AirKiss源码分析安信可ESP-C3-12F是基于乐鑫ESP32C3芯片做的一个支持WiFi+蓝牙的模组,具体参数规格请移步到安信可官网,下面只给大家简单分析AirKiss配网源码的每个关键步骤。
开启嗅探模式,抓取空中的 802.11 包,并传给微信静态库解码。
static void wifi_promiscuous_rx(void *buf, wifi_promiscuous_pkt_type_t type)
{
wifi_promiscuous_pkt_t *pkt = (wifi_promiscuous_pkt_t *) buf;
uint8_t *payload;
uint16_t len;
int ret;
payload = pkt->payload;
len = pkt->rx_ctrl.sig_len;
//把空中的全部802.11包传给微信静态库来做解析
ret = airkiss_recv(ak_ctx, payload, len);
//符合AirKiss包的规则,开始锁定此信道进行解码
if (ret == AIRKISS_STATUS_CHANNEL_LOCKED) {
esp_timer_stop(channel_change_timer);
esp_timer_delete(channel_change_timer);
ESP_LOGI(TAG, "AIRKISS_STATUS_CHANNEL_LOCKED");
//解析完成,停止嗅探
} else if (ret == AIRKISS_STATUS_COMPLETE) {
esp_wifi_set_promiscuous(false);
airkiss_finish();
ESP_LOGI(TAG, "AIRKISS_STATUS_COMPLETE");
}
}
成功解析出来路由器账号密码,并开始连接路由器。
wifi_config_t wifi_config;
int err;
err = airkiss_get_result(ak_ctx, &result);
if (err == 0) {
//这里的 result 就是一个结构体,里面包含ssid、password等信息
} else {
ESP_LOGI(TAG, "airkiss_get_result() failed !");
}
通过UDP协议发一个ack给手机以通知配网成功,数据是一个长度为7的数组。
struct sockaddr_in server_addr;
uint8_t buf[7];
socklen_t sin_size = sizeof(server_addr);
bzero(&server_addr, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_BROADCAST;
server_addr.sin_port = htons(10000);
//第1个buff是随机数,这个随机数是手机发来的
buf[0] = (uint8_t)ak_random_num;
//第2~6是设备mac地址
esp_wifi_get_mac(WIFI_IF_STA, &buf[1]);
//开始发送
sendlen = sendto(send_socket, buf, 7, 0,(struct sockaddr *) &server_addr, sin_size);
3
小程序AirKiss源码分析先写好对应的组包规则,PrefixCode以及Sequence序列包:
function magicCode(ssid, password) {
let bytes = strToUtf8Bytes(ssid);
let length = bytes.length + password.length + 1;
let magicCode = [];
magicCode[0] = 0x00 | (length >>> 4 & 0xF);
if (magicCode[0] == 0) {
magicCode[0] = 0x08;
}
magicCode[1] = 0x10 | (length & 0xF);
let crc8 = CRC8(bytes);
magicCode[2] = 0x20 | (crc8 >>> 4 & 0xF);
magicCode[3] = 0x30 | (crc8 & 0xF);
for (let i = 0; i < 20; ++i) {
for (let j = 0; j < 4; ++j) {
appendEncodedData(magicCode[j]);
}
}
}
function prefixCode(password) {
let length = password.length;
let prefixCode = [];
prefixCode[0] = 0x40 | (length >>> 4 & 0xF);
prefixCode[1] = 0x50 | (length & 0xF);
let crc8 = CRC8([length]);
prefixCode[2] = 0x60 | (crc8 >>> 4 & 0xF);
prefixCode[3] = 0x70 | (crc8 & 0xF);
// console.log(prefixCode.join());
for (let j = 0; j < 4; ++j) {
appendEncodedData(prefixCode[j]);
}
}
发包前,也务必开启UDP监听,等待设备配网成功的ack信息。
wxUdp = wx.createUDPSocket();
//监听端口是 10000
let port = wxUdp.bind(10000);
var replyByteCounter = 0;
//监听函数
wxUdp.onMessage(function(res) {
if (res.remoteInfo.size > 0) {
//处理下数据,得到设备的IP和bssid
callback({"ip":res.remoteInfo.address,"bssid":bssid,"result": "success", "code":1});
}
});
开始发包,注意地址是 255.255.255.255 表示全网段:
let sendData = new ArrayBuffer(mEncodedData[index]);
wxUdp.send({
address: "255.255.255.255",
port: 10000,
message: sendData
});
4
如何集成到我的项目去考虑到开发者集成方便,我已封装成小程序插件并上架了,大家拿来即可上手集成到自己的项目去。
自行注册一个微信小程序,请下载最新版的微信开发者工具。
新建项目之后,打开 app.js
文件添加下面代码:
"plugins": {
"airkiss": {
"version": "1.1.0",
"provider": "wx610ea582556c983e"
}
}
然后,会有提示是否添加插件,按照下面提示添加插件使用。
下面举例说明了如何使用,更多使用技巧和方法参考本小程序源码,源码在文章底部。
const airkiss = requirePlugin('wx610ea582556c983e');
//获取版本
console.log( airkiss.version)
//这里最好加微信小程序判断账号密码是否为空,以及其长度和是否为5G频段
airkiss.startAirkiss(this.data.ssid, this.data.password, function (res) {
switch (res.code) {
case 0:
wx.showModal({
title: '初始化失败',
content: res.result,
showCancel: false,
confirmText: '收到',
})
break;
case 1:
wx.showModal({
title: '配网成功',
content: '设备IP:' + res.ip + '\r\n 设备Mac:' + res.bssid,
showCancel: false,
confirmText: '好的',
})
break;
case 2:
wx.showModal({
title: '配网失败',
content: '请检查密码是否正确',
showCancel: false,
confirmText: '收到',
})
break;
default:
break;
}
})
//停止配网,建议在页面 unload 等生命周期里面调用,释放线程
airkiss.stopAirkiss()
看到这里,希望你会喜欢。
后台发送“AirKiss”获取微信小程序源代码;
往期推荐