【微信小程序控制硬件第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
的处理!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, "" . $content);
fclose($fp);
}
}
airkiss.php
:注意在初始化的,填入的是自己公众号的的参数!这个文件是没有显示任何内容的,当然了,你可以设置一些内容进去,比如告诫用户要怎么样操作设备让他进去配网模式的文字提示!
configWXDeviceWiFi
也是可以调用这个接口的,但是我下面为何要引用那么多接口呢?这里我先卖个关子!checkJsApi
成功回调后才执行的!GetSignPackage();
?>
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
#include
#include
#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/serverPhp