赞
踩
【微信小程序控制硬件第1篇 】 全网首发,借助 emq 消息服务器带你如何搭建微信小程序的mqtt服务器,轻松控制智能硬件!
【微信小程序控制硬件第2篇 】 开始微信小程序之旅,导入小程序Mqtt客户端源码,实现简单的验证和通讯于服务器!
【微信小程序控制硬件第3篇 】 从软件到硬件搭建一个微信小程序控制esp8266的项目,自定义通讯协议,为面试职位和比赛项目加分!
【微信小程序控制硬件第4篇 】 深度剖析微信公众号配网 Airkiss 原理与过程,esp8266如何自定义回调参数给微信,实现绑定设备第一步!
【微信小程序控制硬件第5篇 】理清接下来必须走的架构思想,学习下 JavaScript 的观察者模式,在微信小程序多页面同时接收到设备推送事件!
【微信小程序控制硬件第6篇 】服务器如何集成七牛云存储SDK,把用户自定义设备图片存储在第三方服务器!
【微信小程序控制硬件第7篇 】动起来做一个微信小程序Mqtt协议控制智能硬件的框架,为自己心里全栈工程师梦想浇水!!
【微信小程序控制硬件第8篇 】微信小程序以 websocket 连接阿里云IOT物联网平台mqtt服务器,封装起来使用就是这么简单!
【微信小程序控制硬件第9篇 】巧借阿里云物联网平台的免费连接,从微信小程序颜色采集控制 esp8266 输出七彩灯效果,中秋节来个直播如何?!
【微信公众号控制硬件 第10篇 】如何在微信公众号网页实现连接mqtt服务器教程!!
【微信小程序控制硬件 第11篇 】全网首发,微信小程序ble蓝牙控制esp32,实现无需网络也可以控制亮度开关。
wi-fi
设备芯片!这里强调的是,微信配网是指airKiss
技术,并非蓝牙配网!上图为我总结,如果不够清晰,请点击图片浏览!
准备材料:
SSL
证书!esp8266
模块的最小系统!问:对于个人公众号可以有这个微信配网的权限功能吗?
问:对于企业公众号的微信配网的权限功能如何开启呢?
问:和微信小程序配置后台一样,这个服务器都是需要
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
成功初始化后调起即可,之后就会自动进去跳转到配网界面的!appID
和密钥,以及要把我们的服务器的IP
地址填入,注意是IP
地址!!为何不用域名??因为微信这样防止别人冒充域名来调起 SDK
!设备功能
接口已经获得!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>
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
的!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
数据!onScanWXDeviceResult
方法在苹果手机包括iPhone X、iPhone6
收不到wifi设备的传来的一些信息!本人经过多次查阅,需要在openWXDeviceLib
方法调用时候,也就是初始化硬件设备库,传入加上这个公众号原始ID。具体如下: //初始化硬件设备库,否则部分机型无法初始化成功导致后面的扫描不了本地设备!
wx.invoke('openWXDeviceLib', {'connType': 'lan', 'brandUserName': 'gh_xxxxxx'}, function (res) {
alert("openWXDeviceLib:" + JSON.stringify(res));
});
deviceID
传过来格式不能是 json
格式,个人建议把此字符串base64
加密后传给微信。否则在苹果直接没有回调也没有错误信息!!!js sdk
时候没有传公众号id,仅仅对于苹果手机比如 iPhone X本地扫描局域网设备没有回调,报错信息也没有!gh_xxxx
!否则苹果手机没有回调信息!但安卓手机与此无关!就是下面这个函数的调用传参数特别注意:airkiss_lan_pack(airkiss_lan_cmdid_t ak_lan_cmdid, void* appid, void* deviceid, void* _datain, unsigned short inlength, void* _dataout, unsigned short* outlength, const airkiss_config_t* config);
ak_lan_cmdid
为要打包的类型,appid
为厂商公众号ID,deviceid
为设备ID,_datain
为要发送的数据,inlength
为发送数据的长度,_dataout
为打包后的数据缓冲区,outlength
为缓冲区的空间,函数成功返回后将赋值为数据包的实际长度!php
部署的时候,一定要把文件改为可读可写的权限!另外,不要把我的博客作为学习标准,我的只是笔记,难有疏忽之处,如果有,请指出来,也欢迎留言哈!
esp8266
带你飞、加群个人QQ
群,不喜的朋友勿喷勿加:434878850php
代码:https://github.com/xuhongv/StudyInEsp8266/tree/master/30_ESP8266_RTOS_AirKiss/serverPhpCopyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。