【微信小程序控制硬件④】 深度剖析微信公众号配网 Airkiss 原理与过程,esp8266如何自定义回调参数给微信,实现绑定设备第一步!(附带源码)

版权声明:本文为博主半颗心脏一心一血敲出来的原创作品,未经博主允许不得转载,多谢支持。 https://blog.csdn.net/xh870189248/article/details/84679910

  • 本博文由热爱分享热爱技术的半颗心脏原创,非官方人员、非组织名义编写,博文如有不对或侵犯您的权益,请及时留言,第一时间纠正!

【微信小程序控制硬件①】 全网首发,借助 emq 消息服务器带你如何搭建微信小程序的mqtt服务器,轻松控制智能硬件!
【微信小程序控制硬件②】 开始微信小程序之旅,导入小程序Mqtt客户端源码,实现简单的验证和通讯于服务器!
【微信小程序控制硬件③】 从软件到硬件搭建一个微信小程序控制esp8266的项目,自定义通讯协议,为面试职位和比赛项目加分!
【微信小程序控制硬件④】 深度剖析微信公众号配网 Airkiss 原理与过程,esp8266如何自定义回调参数给微信,实现绑定设备第一步!


一、前言;


  • 说到微信配网,大家并不陌生!前面我已经说到,微信小程序并不支持设备配网!,也许可能大概之后,会拟补这个缺陷吧?所以,我们还得是要回到我们的微信公众号配网,那么问题来了:配网的目的是什么?,目的就是在于如何把用户要连接的路由器的账号和密码发送给智能的wi-fi设备芯片!这里强调的是,微信配网是指airKiss技术,并非蓝牙配网!
  • 微信配网的好处就是减少用户再次下载一个app专门用来配网,毕竟用户也不想专门去下载一个应用去配网,我们要做一个微信控制设备,那就要全套做完,从微信公众号配网到微信小程序控制设备!对吧?

二、准备材料以及注意事项;


在这里插入图片描述

  • 上图为我总结,如果不够清晰,请点击图片浏览!

  • 准备材料:

    • 一个具备设备功能接口开启的公众号!一般是企业认证的或者是个人测试号!
    • 一个可外网连接的服务器,阿里云购买或者其他平台!
    • 一个备案成功的域名,而且具备SSL证书!
    • 一个esp8266模块的最小系统!

问:对于个人公众号可以有这个微信配网的权限功能吗?

  • 答:很遗憾告诉你,目前2018-12-3为止这个不行的!我的个人公众号想加这个功能,但是受限了,呵呵!但是,为了方便我们个人开发者熟悉微信公众号的开发,微信很有心地提供了个人测试号给我们调试,但是这个测试号只能自己订阅哈!!

问:对于企业公众号的微信配网的权限功能如何开启呢?

  • 答:这个是我刚刚了解到的,这个企业公众号必须被认证过,才有资格开启设备权限接口!!花钱认证的那种,多少钱一年自己去看看吧!本篇文章得力于群里一个小老板提供的企业公众号账户密码,万分感谢!

问:和微信小程序配置后台一样,这个服务器都是需要https吗?

  • 这个我没测试过,如果你开发微信小程序服务器,这个是必须是https的,我建议大家还是配https`的,没必要引起相关的问题!

三、如何调起微信公众号的配网界面;


  • 为了更好让大家了解清楚流程,我这里介绍大家看看怎么调起微信提供的JS SDK的视频,注意是PHP服务器语言的:
  • 【如何调起微信的JS SDK教学视频传送门】https://ke.qq.com/course/306636

  • 上述视频中是调用分享接口的,不是配网接口!我们替换一下即可,为此,我特意总结下:

    • ①: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_tokenApiTicket的处理!
<?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&timestamp=$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_TYPEDEVICE_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数据!

在这里插入图片描述


六、后记;


  • 2019年2月18日笔记:onScanWXDeviceResult方法在苹果手机包括iPhone X、iPhone6收不到wifi设备的传来的一些信息!本人经过多次查阅,需要在openWXDeviceLib方法调用时候,也就是初始化硬件设备库,传入加上这个公众号原始ID。具体如下:
       //初始化硬件设备库,否则部分机型无法初始化成功导致后面的扫描不了本地设备!
        wx.invoke('openWXDeviceLib', {'connType': 'lan', 'brandUserName': 'gh_xxxxxx'}, function (res) {
            alert("openWXDeviceLib:" + JSON.stringify(res));
        });

  • ①:【设备端睬坑】微信公众号配网airkiss开发流程,设备端的deviceID传过来格式如果不是base64格式(貌似字符串也行)比如json,直接没有回调没有错误信息!
  • ②:【前端js睬坑】如果初始化js sdk时候没有传公众号id,仅仅对于苹果手机比如 iPhone X本地扫描局域网设备没有回调,报错信息也没有!
  • ③:【设备端睬坑】最后一点:设备端的宏定义DEVICE_TYPE为微信公众号的原始ID,格式:gh_xxxx!否则苹果设备没有回调信息!

  • php部署的时候,一定要把文件改为可读可写的权限!

另外,不要把我的博客作为学习标准,我的只是笔记,难有疏忽之处,如果有,请指出来,也欢迎留言哈!


猜你喜欢

转载自blog.csdn.net/xh870189248/article/details/84679910