深度剖析微信公眾號配網 Airkiss 原理與過程,esp8266如何自定義回調參數給微信,實現綁定設備第一步?。?/h1>

  • 本博文由熱愛分享熱愛技術的半顆心臟原創,非官方人員、非組織名義編寫,博文如有不對或侵犯您的權益,請及時留言,第一時間糾正!

【微信小程序控制硬件①】 全網首發,借助 emq 消息服務器帶你如何搭建微信小程序的mqtt服務器,輕松控制智能硬件!
【微信小程序控制硬件②】 開始微信小程序之旅,導入小程序Mqtt客戶端源碼,實現簡單的驗證和通訊于服務器!
【微信小程序控制硬件③】 從軟件到硬件搭建一個微信小程序控制esp8266的項目,自定義通訊協議,為面試職位和比賽項目加分!
【微信小程序控制硬件④】 深度剖析微信公眾號配網 Airkiss 原理與過程,esp8266如何自定義回調參數給微信,實現綁定設備第一步!


一、前言;


  • 說到微信配網,大家并不陌生!前面我已經說到,<font color=red>微信小程序并不支持設備配網!</font>,也許可能大概之后,會擬補這個缺陷吧?所以,我們還得是要回到我們的微信公眾號配網,那么問題來了:配網的目的是什么?,<font color=blue>目的就是在于如何把用戶要連接的路由器的賬號和密碼發送給智能的wi-fi設備芯片!</font>這里強調的是,微信配網是指airKiss技術,并非藍牙配網!
  • 微信配網的好處就是減少用戶再次下載一個app專門用來配網,畢竟用戶也不想專門去下載一個應用去配網,我們要做一個微信控制設備,那就要全套做完,<font color=red>從微信公眾號配網到微信小程序控制設備!對吧?

二、準備材料以及注意事項;


1.png
  • 上圖為我總結,如果不夠清晰,請點擊圖片瀏覽!

  • <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!
2.png

  • 第二步:確保我們的設備功能接口已經獲得!
3.png

  • 第三步:確保我們的域名關聯到公眾號!
在這里插入圖片描述

  • 第4步:下載微信提供的字符串文檔到服務器根目錄!確保該文件可以被訪問!
5.png

  • 自此為止,微信公眾號后臺的配置已經全部弄好了,是不是覺得很簡單?我可是摸索了很久的了,呵呵??!

四、 服務器php代碼編寫。


6.png
  • 其實這些服務器調起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這個文件就可以了!下面我用簡單的自定義菜單點擊訪問實現,具體如下:
7.png

  • 好了,微信就這樣大功告成了!下面我們來分析設備端的代碼!

五、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數據!
8.png

六、后記;


  • php部署的時候,一定要把文件改為可讀可寫的權限!

另外,不要把我的博客作為學習標準,我的只是筆記,難有疏忽之處,如果有,請指出來,也歡迎留言哈!


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容