- 本博文由热爱分享热爱技术的半颗心脏原创,非官方人员、非组织名义编写,博文如有不对或侵犯您的权益,请及时留言,第一时间纠正!
【微信小程序控制硬件①】 全网首发,借助 emq 消息服务器带你如何搭建微信小程序的mqtt服务器,轻松控制智能硬件!
【微信小程序控制硬件②】 开始微信小程序之旅,导入小程序Mqtt客户端源码,实现简单的验证和通讯于服务器!
【微信小程序控制硬件③】 从软件到硬件搭建一个微信小程序控制esp8266的项目,自定义通讯协议,为面试职位和比赛项目加分!
【微信小程序控制硬件④】 深度剖析微信公众号配网 Airkiss 原理与过程,esp8266如何自定义回调参数给微信,实现绑定设备第一步!
一、前言;
- 说到微信配网,大家并不陌生!前面我已经说到,<font color=red>微信小程序并不支持设备配网!</font>,也许可能大概之后,会拟补这个缺陷吧?所以,我们还得是要回到我们的微信公众号配网,那么问题来了:配网的目的是什么?,<font color=blue>目的就是在于如何把用户要连接的路由器的账号和密码发送给智能的
wi-fi
设备芯片!</font>这里强调的是,微信配网是指airKiss
技术,并非蓝牙配网! - 微信配网的好处就是减少用户再次下载一个app专门用来配网,毕竟用户也不想专门去下载一个应用去配网,我们要做一个微信控制设备,那就要全套做完,<font color=red>从微信公众号配网到微信小程序控制设备!对吧?
二、准备材料以及注意事项;
上图为我总结,如果不够清晰,请点击图片浏览!
-
<font color=red size=5>准备材料:
- 一个具备设备功能接口开启的公众号!一般是企业认证的或者是个人测试号!
- 一个可外网连接的服务器,阿里云购买或者其他平台!
- 一个备案成功的域名,而且具备
SSL
证书! - 一个
esp8266
模块的最小系统!
问:对于个人公众号可以有这个微信配网的权限功能吗?
- 答:很遗憾告诉你,目前2018-12-3为止这个不行的!我的个人公众号想加这个功能,但是受限了,呵呵!但是,为了方便我们个人开发者熟悉微信公众号的开发,微信很有心地提供了个人测试号给我们调试,但是这个测试号只能自己订阅哈!!
问:对于企业公众号的微信配网的权限功能如何开启呢?
- 答:这个是我刚刚了解到的,这个企业公众号必须被认证过,才有资格开启设备权限接口!!花钱认证的那种,多少钱一年自己去看看吧!本篇文章得力于群里一个小老板提供的企业公众号账户密码,万分感谢!
问:和微信小程序配置后台一样,这个服务器都是需要
https
吗?
- 这个我没测试过,如果你开发微信小程序服务器,这个是必须是
https
的,我建议大家还是配https`的,没必要引起相关的问题!
三、如何调起微信公众号的配网界面;
- 为了更好让大家了解清楚流程,我这里介绍大家看看怎么调起微信提供的
JS SDK
的视频,注意是PHP
服务器语言的: - 【如何调起微信的
JS SDK
教学视频传送门】https://ke.qq.com/course/306636
-
<font color=red size=4>上述视频中是调用分享接口的,不是配网接口!我们替换一下即可,为此,我特意总结下:
- ①:
access_token
是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token
。开发者需要进行妥善保存。access_token
的有效期目前为2个小时(也即是 2 * 60 * 60 =7200 秒),需定时刷新,重复获取将导致上次获取的access_token
失效!而且每天都是有次数的请求此access_token
!! - ②:
access_token
的获取涉及到一些算法,这个微信平台也会提供示范代码,当然了!本篇博文我是用php
语言编写,并且放置在自己的服务器运行! - ③:微信公众号后台配置的工作很重要,这步必须要保证服务器与公众号一一对应起来,否则也会报异常!
- ④:微信的
JS SDK
初始化的时候,要填入微信公众号的开发者ID(AppID
)以及密钥,还有您要调用的js
接口,之后在ready
成功初始化后调起即可,之后就会自动进去跳转到配网界面的!
- ①:
3.1 微信公众号后台配置要点。
- 第一步:就是要获取
appID
和密钥,以及要把我们的服务器的IP
地址填入,注意是IP
地址!!为何不用域名??因为微信这样防止别人冒充域名来调起SDK
!
- 第二步:确保我们的
设备功能
接口已经获得!
- 第三步:确保我们的域名关联到公众号!
- 第4步:下载微信提供的字符串文档到服务器根目录!确保该文件可以被访问!
- 自此为止,微信公众号后台的配置已经全部弄好了,是不是觉得很简单?我可是摸索了很久的了,呵呵!!
四、 服务器php
代码编写。
其实这些服务器调起
airkiss
接口在网上多的是,那么我这里整理下代码思路:先获取
AccessToken
,如果access_token.php
文件没有保存或者创建时间和当前时间对比超过2小时,则用上面提到的appID
以及密钥用https
来请求,并且保存在access_token.php
文件中!之后通过
AccessToken
来请求票据jsApiTicket
,如果jsapi_ticket.php
文件没有保存或者创建时间和当前时间对比超过2小时,则用上面提到的AccessToken
来请求,并且保存在jsapi_ticket.php
文件中!最后的调用
JS SDK
调取必须要有签名signature
,这就是我们为何苦心2次请求的最后的参数!具体还要哪些参数,请看代码!
- 核心类
JSSDK
,包含对重复请求微信获取access_token
和ApiTicket
的处理!
<?php
/**
* Created by PhpStorm.
* User: XuHongYss
* Date: 2018/12/1
* Time: 15:57
*/
class JSSDK {
private $appId;
private $appSecret;
//构造方法,传入appid和密钥
public function __construct($appId, $appSecret) {
$this->appId = $appId;
$this->appSecret = $appSecret;
}
//获取签名
public function getSignPackage() {
$jsapiTicket = $this->getJsApiTicket();
// 注意 URL 一定要动态获取,不能 hardcode.
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
$url = "$protocol$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";
$timestamp = time();
$nonceStr = $this->createNonceStr();
// 这里参数的顺序要按照 key 值 ASCII 码升序排序
$string = "jsapi_ticket=$jsapiTicket&noncestr=$nonceStr×tamp=$timestamp&url=$url";
$signature = sha1($string);
//var_dump($signature);exit;
$signPackage = array(
"appId" => $this->appId,
"nonceStr" => $nonceStr,
"timestamp" => $timestamp,
"url" => $url,
"signature" => $signature,
"rawString" => $string
);
return $signPackage;
}
/**
*
* 创建随机数
*
* @param int $length 长度,默认是16
* @return string 返回随机数
*/
private function createNonceStr($length = 16) {
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
$str = "";
for ($i = 0; $i < $length; $i++) {
$str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
}
return $str;
}
/**
* @return mixed 获取 JsApiTicket
*/
private function getJsApiTicket() {
// jsapi_ticket 应该全局存储与更新,以下代码以写入到文件中做示例
$data = json_decode($this->get_php_file("jsapi_ticket.php"));
if ($data->expire_time < time()) {
$accessToken = $this->getAccessToken();
$url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi&access_token=$accessToken";
$res = json_decode($this->httpGet($url));
$ticket = $res->ticket;
if ($ticket) {
$data->expire_time = time() + 7000;
$data->jsapi_ticket = $ticket;
$this->set_php_file("jsapi_ticket.php", json_encode($data));
}
} else {
$ticket = $data->jsapi_ticket;
}
return $ticket;
}
/**
* @return mixed 获取AccessToken
*/
private function getAccessToken() {
// access_token 应该全局存储与更新,以下代码以写入到文件中做示例
$data = json_decode($this->get_php_file("access_token.php"));
if ($data->expire_time < time()) {
$url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=$this->appId&secret=$this->appSecret";
//var_dump($url);exit;
$res = json_decode($this->httpGet($url));
//var_dump($res->expires_in);exit;
$access_token = $res->access_token;
//var_dump($access_token);exit;
if ($access_token) {
$data->expire_time = time() + 7000;
$data->access_token = $access_token;
$this->set_php_file("access_token.php", json_encode($data));
}
} else {
$access_token = $data->access_token;
}
return $access_token;
}
/**
* @param $url https请求的url
* @return mixed
*/
private function httpGet($url) {
$curl = curl_init();
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_TIMEOUT, 500);
// 为保证第三方服务器与微信服务器之间数据传输的安全性,所有微信接口采用https方式调用,必须使用下面2行代码打开ssl安全校验。
// 如果在部署过程中代码在此处验证失败,请到 http://curl.haxx.se/ca/cacert.pem 下载新的证书判别文件。
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($curl, CURLOPT_URL, $url);
$res = curl_exec($curl);
curl_close($curl);
return $res;
}
/**
* @param $filename 文件名
* @return string 内容
*/
private function get_php_file($filename) {
return trim(substr(file_get_contents($filename), 15));
}
/**
* @param $filename 文件名字
* @param $content 内容
*/
private function set_php_file($filename, $content) {
$fp = fopen($filename, "w");
fwrite($fp, "<?php exit();?>" . $content);
fclose($fp);
}
}
- 微信要请求的文件:
airkiss.php
:注意在初始化的,填入的是自己公众号的的参数!这个文件是没有显示任何内容的,当然了,你可以设置一些内容进去,比如告诫用户要怎么样操作设备让他进去配网模式的文字提示!- 需要注意的是很多人都是引入一个接口
configWXDeviceWiFi
也是可以调用这个接口的,但是我下面为何要引用那么多接口呢?这里我先卖个关子! - 所有一切调用接口都是在
checkJsApi
成功回调后才执行的!
- 需要注意的是很多人都是引入一个接口
<?php
require_once "jssdk.php";
$jssdk = new JSSDK("填入微信提供的APPID", "填入微信提供的密钥");
$signPackage = $jssdk->GetSignPackage();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
</body>
<script src="http://res.wx.qq.com/open/js/jweixin-1.0.0.js"></script>
<script>
wx.config({
beta:true,//开启内测接口调用,注入wx.invoke方法
debug:false,//关闭调试模式
appId: '<?php echo $signPackage["appId"];?>',//AppID
timestamp: <?php echo $signPackage["timestamp"];?>,//时间戳
nonceStr: '<?php echo $signPackage["nonceStr"];?>',//随机串
signature: '<?php echo $signPackage["signature"];?>',//签名
jsApiList:['openWXDeviceLib','startScanWXDevice','onScanWXDeviceResult','configWXDeviceWiFi']
});
// echo 'start config';
wx.ready(function () {
// 在这里调用 API
wx.checkJsApi({
jsApiList: ['configWXDeviceWiFi'],
success: function(res) {
wx.invoke('configWXDeviceWiFi', {}, function(res){
var err_msg = res.err_msg;
if(err_msg == 'configWXDeviceWiFi:ok') {
//配置成功
wx.invoke('openWXDeviceLib',{'connType':'lan'},function(res){
// alert(res.err_msg);
});
wx.invoke('startScanWXDevice',{'connType':'lan'}, function(res) {
console.log('startScanWXDevice',res);
alert(JSON.stringify(res));
});
wx.on('onScanWXDeviceResult',function(res){
alert("扫描到1个设备"+JSON.stringify(res));
//自己解析一下res,里面会有deviceid,扫描设备的目的就是为了得到这个
//然后就可以开始绑定了
});
} else {
//配置失败
alert(err_msg);
}
});
}
});
});
wx.error(function(res){
alert("配置出错:"+res);
});
</script>
</html>
4.1 如何调用?
- 总的来说只要在微信公众号调用访问
airkiss.php
这个文件就可以了!下面我用简单的自定义菜单点击访问实现,具体如下:
- 好了,微信就这样大功告成了!下面我们来分析设备端的代码!
五、esp8266
实现airkiss
原理配网;
小徐做过其他领域的
SDK
接入,而且配网代码都是利用他们提供的,非乐鑫的smartConfig
,所以乐鑫的提供的配网SDK
不可用,那么问题来了!既然不要乐鑫的配网代码,esp8266
又是如何成功抓取到第三方的数据包呢?上述问题,其实原理是嗅探技术
sniff
实现的,esp8266
来空中抓802.2 SNAP
数据包,然后根据双方的协议剖析数据包得到要连接的路由器账号和密码:具体的技术实现:https://blog.csdn.net/lb5761311/article/details/77945848如果你搞定了上面的原理,其实是可以自己做
app
配网,避开用乐鑫的app
配网,这样提高产品逼格!呵呵!
- 好,那么
esp8266
在嗅探技术是如何实现的呢?小徐有幸从aliosThings
找到源码,因为这个乐鑫是不开放的,那么我这里贴下代码,我们主要看嗅探的代码,发现他又是调用一层代码,这个代码是微信提供的算法,这个算法我就不带大家看了,主要是怎么处理802.2 SNAP
数据包!
#include <aos/aos.h>
#include <hal/wifi.h>
#include <string.h>
#include "lwip/ip_addr.h"
#include "lwip/pbuf.h"
#include "espressif/c_types.h"
#include "espressif/esp_libc.h"
#include "espressif/esp_wifi.h"
#include "airkiss.h"
// airkiss 状态回调函数
typedef void (*airkiss_cb_fn)(AIR_KISS_STATE state, void *pdata);
void start_airkiss(airkiss_cb_fn airkiss_done);
static void start_scan(void);
static void udp_send_random(uint8_t num);
static void channel_change_action(void *arg);
// 当前监听的无线信道
uint8_t cur_channel = 1;
uint8_t wifi_ssid_crc;
uint8_t airkiss_random_num;
char wifi_ssid[32 + 1]; /* SSID got form airkiss */
char wifi_pwd[64 + 1]; /* password got form airkiss */
// 信道锁定标志
uint8_t airkiss_channel_locked = 0;
// Airkiss 过程中需要的 RAM 资源,完成 Airkiss 后可释放
airkiss_context_t *akcontexprt;
// 定义 Airkiss 库需要用到的一些标准函数,由对应的硬件平台提供,前三个为必要函数
const airkiss_config_t akconf = {
(airkiss_memset_fn)&memset,
(airkiss_memcpy_fn)&memcpy,
(airkiss_memcmp_fn)&memcmp,
(airkiss_printf_fn)&printf
};
airkiss_cb_fn airkiss_cb = NULL;
hal_wifi_init_type_t type;
extern hal_wifi_module_t aos_wifi_esp8266;
uint8_t crc8_chk_value(uint8_t *str)
{
uint8_t crc = 0;
uint8_t i;
while(*str != '\0')
{
crc ^= *str++;
for(i = 0; i < 8; i++)
{
if(crc & 0x01)
crc = (crc >> 1) ^ 0x8c;
else
crc >>= 1;
}
}
return crc;
}
//wifi 事件回调函数
const hal_wifi_event_cb_t wifi_event_cb = {
&wifi_connect_fail,
&wifi_ip_got,
&wifi_stat_chg,
&wifi_scan_compeleted,
&wifi_scan_adv_compeleted,
&wifi_para_chg,
&wifi_fatal_err
};
// 用于切换信道的定时任务
static void channel_change_action(void *arg)
{
if (!airkiss_channel_locked)
{
// 切换信道
if (cur_channel >= 13)
cur_channel = 1;
else
cur_channel++;
hal_wifi_set_channel(&aos_wifi_esp8266, cur_channel);
airkiss_change_channel(akcontexprt);
aos_post_delayed_action(100, channel_change_action, NULL);
}
}
//配网完成
static void airkiss_finish(void)
{
int8_t err;
uint8 buffer[256];
airkiss_result_t result;
err = airkiss_get_result(akcontexprt, &result);
if (err == 0)
{
stpcpy(wifi_pwd, result.pwd);
wifi_ssid_crc = result.reserved;
airkiss_random_num = result.random;
}
else
{
printf("AIRKISS_STATUS_GETTING_PSWD_FAILED\r\n");
}
aos_free(akcontexprt);
start_scan();
}
static void wifi_promiscuous_rx(uint8_t *data, int len, hal_wifi_link_info_t *info)
{
int8_t ret;
ret = airkiss_recv(akcontexprt, data, len);
if (ret == AIRKISS_STATUS_CHANNEL_LOCKED)
{
airkiss_channel_locked = 1;
airkiss_cb(AIRKISS_STATE_FIND_CHANNEL, NULL);
printf("T|LOCK CHANNEL : %d\r\n", cur_channel);
}
else if (ret == AIRKISS_STATUS_COMPLETE)
{
hal_wifi_stop_wifi_monitor(&aos_wifi_esp8266);
airkiss_finish();
}
}
//开始扫描
static void start_scan(void)
{
wifi_set_opmode(STATION_MODE);
hal_wifi_install_event(&aos_wifi_esp8266, &wifi_event_cb);
hal_wifi_start_scan(&aos_wifi_esp8266);
}
//调用函数
void start_airkiss(airkiss_cb_fn airkiss_done)
{
int8_t ret;
airkiss_cb = airkiss_done;
akcontexprt = (airkiss_context_t*)aos_malloc(sizeof(airkiss_context_t));
// 初始化 Airkiss 流程,每次调用该接口,流程重新开始
ret = airkiss_init(akcontexprt, &akconf);
if (ret < 0)
{
printf("Airkiss init failed!\r\n");
return;
}
// 开始抓包
cur_channel = 1;
airkiss_channel_locked = 0;
hal_wifi_set_channel(&aos_wifi_esp8266, cur_channel);
hal_wifi_register_monitor_cb(&aos_wifi_esp8266, wifi_promiscuous_rx);
hal_wifi_start_wifi_monitor(&aos_wifi_esp8266);
aos_post_delayed_action(100, channel_change_action, NULL);
airkiss_cb(AIRKISS_STATE_WAIT, NULL);
}
- 上面已经分析了
esp8266
是如何配网成功的。当配网成功之后,微信还有一个扫描本的设备的接口!!上面服务器代码已经卖了关子,为何要调用那么多接口!原因就是当我们配网成功之后,可以通过UDP
广播包发送消息给微信,让微信拿到我们设备自定义发来的消息之后,可以为所欲为做自己的事情,比如设备入库! - 这个
UDP
如何实现发送微信呢?这个其实在乐鑫的代码实现了,我也贴贴吧!默认端口号是12476
,从代码分析得到,微信在扫描本地设备时候,是作为一个服务器监听这个端口12476
的! - 那么我们的自定义发送给微信的消息在哪呢?看见下面有2个宏定义么?
DEVICE_TYPE
和DEVICE_ID
,我们修改下其即可!因为代码中看到了airkiss_lan_pack()
方法传入这2个参数!下面我们把其内容修改如下:
#define DEVICE_TYPE "https://blog.csdn.net/xh870189248"
#define DEVICE_ID "https://github.com/xuhongv"
#define DEFAULT_LAN_PORT 12476 //服务器的UDP端口
LOCAL esp_udp ssdp_udp;
LOCAL struct espconn pssdpudpconn;
LOCAL os_timer_t ssdp_time_serv;
uint8 lan_buf[200];
uint16 lan_buf_len;
uint8 udp_sent_cnt = 0;
const airkiss_config_t akconf =
{
(airkiss_memset_fn)&memset,
(airkiss_memcpy_fn)&memcpy,
(airkiss_memcmp_fn)&memcmp,
0,
};
LOCAL void ICACHE_FLASH_ATTR
airkiss_wifilan_time_callback(void)
{
uint16 i;
airkiss_lan_ret_t ret;
if ((udp_sent_cnt++) >30) {
udp_sent_cnt = 0;
os_timer_disarm(&ssdp_time_serv);//s
//return;
}
ssdp_udp.remote_port = DEFAULT_LAN_PORT;
ssdp_udp.remote_ip[0] = 255;
ssdp_udp.remote_ip[1] = 255;
ssdp_udp.remote_ip[2] = 255;
ssdp_udp.remote_ip[3] = 255;
lan_buf_len = sizeof(lan_buf);
ret = airkiss_lan_pack(AIRKISS_LAN_SSDP_NOTIFY_CMD,
DEVICE_TYPE, DEVICE_ID, 0, 0, lan_buf, &lan_buf_len, &akconf);
if (ret != AIRKISS_LAN_PAKE_READY) {
os_printf("Pack lan packet error!");
return;
}
ret = espconn_sendto(&pssdpudpconn, lan_buf, lan_buf_len);
if (ret != 0) {
os_printf("UDP send error!");
}
os_printf("Finish send notify!\n");
}
void ICACHE_FLASH_ATTR
airkiss_start_discover(void)
{
ssdp_udp.local_port = DEFAULT_LAN_PORT;
pssdpudpconn.type = ESPCONN_UDP;
pssdpudpconn.proto.udp = &(ssdp_udp);
espconn_regist_recvcb(&pssdpudpconn, airkiss_wifilan_recv_callbk);
espconn_create(&pssdpudpconn);
os_timer_disarm(&ssdp_time_serv);
os_timer_setfn(&ssdp_time_serv, (os_timer_func_t *)airkiss_wifilan_time_callback, NULL);
os_timer_arm(&ssdp_time_serv, 1000, 1);//1s
}
- 下面我们验证下,发现,他传这个
DEVICE_ID
给微信的,而且还好,是个json
数据!
六、后记;
-
php
部署的时候,一定要把文件改为可读可写的权限!
另外,不要把我的博客作为学习标准,我的只是笔记,难有疏忽之处,如果有,请指出来,也欢迎留言哈!
- 玩转
esp8266
带你飞、加群付费QQ
群,不喜的朋友勿喷勿加:434878850 - 本博文对应硬件代码:https://github.com/xuhongv/StudyInEsp8266/tree/master/30_ESP8266_RTOS_AirKiss
- 本博文对应
php
代码:https://github.com/xuhongv/StudyInEsp8266/tree/master/30_ESP8266_RTOS_AirKiss/serverPhp - esp8266源代码学习汇总(持续更新,欢迎star):https://github.com/xuhongv/StudyInEsp8266
- esp32源代码学习汇总(持续更新,欢迎star):https://github.com/xuhongv/StudyInEsp32