在上一篇中文章中主要讲了一下AP配网概念、WiFi连接设备流程以及遇到的机型的问题,但是当时的实现思路也存在着缺陷、部分场景没有处理好,导致后续设备升级、小程序版本迭代中出现了问题。
本篇文章主要对出现的问题进行阐述、剖析、解决方法进行总结,希望能给有这方面开发需求的掘友们带来帮助。
好了,话不多说,让我们步入今天的主题。
暴露出的问题
提出的新的需求 : 本次主要是用户提出了个需求:针对配网的设备能否批量配置(这一批设备配置链接相同的WiFi
)、给指定某个进入AP
状态的设备进行配置(指定给单个设备配置单独的WiFi
);
思考针对这种情况下之前的处理逻辑就不适用了,需要进行调整,主要有以下几个问题:
- 怎样区分设备
- 怎样进行批量的设置,批量下发配置信息
其实以上的问题归根到底主要就是:我们如何区分设备,只要我们能够区分出设备,那么我们就可以连接上、然后来一个循环进行配置(理想很美好,后续却遇到了一些问题)
解决方法
怎样区分设备呢?这种问题在上层软件肯定是没法做到的,必须要让硬件部门的同事对设备生成的SSID
进行修改,因为目前设备生成的都是一个名称,对于用户来说肯定没法区分。
那么我们就成功的将压力转移给了硬件人员,他们只需要根据设备的MAC
,生成唯一的SSID
即可。(其实对于他们来说也简单,只要将MAC
进行组合动态生成即可,可以生成:公共标识_mac后五位 ) 拓展-什么是SSID、BSSID:
SSID
我们可以看做是 WiFi名BSSID
可以理解为每个无线接入的MAC地址
更详细的大家可自行查询相关资料。
主体思路
当设备动态生成了SSID
,接下来就是前端工程师的任务了,在这里我先主要描述一下设备配网的流程以及配置WiFi
的主要流程。
设备配置流程 WiFi配置流程 以上就是我们思路的整体流程,后面会按照该流程进行实现。
开发环境
- HBuilder X 3.6.18
- 微信开发者工具 1.06.2303220;调试基础库 3.3.4
WIFI配置
进入具体的WiFi
配置部分,一些基本的在这里我会简单说一下,部分代码与上一篇文章中的是一致的,但是本次讲解的更加细致;对于实际操作中我栽的坑,这里我会详细说明。
全局标识
这里我先将展示一下后续用到的一些公共标识。
js
let udp = null;
let configInfo = {
ssid: '',
pwd: '',
url: ''
};
const deviceInfo = {
SSID: '旧版的SSID',
password: '',
}
let sendTimer = null; // 发送UDP消息
let maxUdp = 10;
let connectUdpNum = 0;
// udp 传输超时显示
let udpOverTimer = null;
// 手动链接wifi的callback 回调,这个主要是显示页面弹框,提醒用户手动连接
// page 中的显示弹框回调
let manualWifiCallback
1、手机版本判断
js
wx.getSystemInfo({
success: function(res) {
phoneType = res.platform;
var system = '';
if (res.platform == 'android') system = parseInt(res.system.substr(8));
if (res.platform == 'ios') system = parseInt(res.system.substr(4));
if (res.platform == 'android' && system < 6) {
wx.showToast({
title: '手机版本不支持',
})
return;
}
if (res.platform == 'ios' && system < 11.2) {
wx.showToast({
title: '手机版本不支持',
})
return;
}
// 初始化WiFi模块
startWifi();
}
})
2、初始化WiFi模块
2-1 注意
- 初始化
WiFi
模块,是指微信小程序内部初始化,并不代表了当前的用户手机WiFi
是否打开
现象 :手机不打开WiFi
在调用uni.startWfif
时也会进入success
回调。
那么我们怎样判断用户是否打开了WiFi
呢,在后边我们可以通过获取WiFi
列表来给用户进行弹窗提醒,大家请耐心继续往后看。
js
function startWifi() {
uni.showLoading({
title: '初始化WIFI模块'
})
uni.startWifi({
success(res) {
console.log('wifi 初始化成功');
/**
* 判断当前用户连接的是不是就是 设备的 WiFi
*/
uni.getConnectedWifi({
success: (e) => {
console.log('获取wifi名', e);
let {
SSID
} = e.wifi;
if (
SSID === '之前的旧SSID' ||
SSID.split('_')[0] === '新SSID公共部分'
) {
uni.showLoading({
title: '设置中...'
})
sendCfg();
} else {
// 获取用户权限
getUserAuthorize();
// 监听WiFi的连接
onWifiConnected();
}
},
fail: (err) => {
// 获取连接的 WiFi 失败
uni.showLoading({
title: '未获取到当前连接的WiFi...'
})
getUserAuthorize();
onWifiConnected();
}
})
},
fail(err) {
uni.hideLoading();
uni.showToast({
title: 'WIFI初始化失败',
icon: 'error'
})
}
})
}
2-2 开启WiFi连接事件监听
js
/**
* 监听当前的 WiFi 连接事件
*/
function onWifiConnected() {
uni.onWifiConnected((res) => {
console.log('监听 WiFi 连接', res);
let {
wifi: {
SSID
}
} = res;
if (
SSID === '旧标识' ||
SSID.split('_')[0] === '新公共标识'
) {
connectSsid = SSID;
// 连接上了设备进行配置
uni.showLoading({
title: '设备wifi连接成功',
})
sendCfg()
}
})
}
3、判断当前用户的权限授权,引导授权定位
3-1 注意:
我们为什么要获取用户的定位授权呢?
- 针对不同的设备,生成的不同的
SSID
,我们需要获取WiFi
列表,
微信官方说明-2019-04-28 :当前我们发现在Android
平台下,部分小程序会通过wx.getWifiList
接口,嗅探周边Wi-Fi
热点来推断用户所在的位置信息。为了更好地保护用户隐私,现策略调整为:Android
平台下,所有小程序在调用wx.getWifiList
接口前,需获取用户位置信息授权(scope.userLocation
)。
3-2 注意:
- 因为我们需要用户授权定位,这个就涉及到了"隐私与安全",我们需要登录小程序更新我们的"用户隐私保护指引",描述我们处于什么目的需要用户进行授权;该部分内容在授权弹框、资料页中会显示给用户。
这些是微信官方要求的没办法避免,否则会影响获取用户的定位授权。
我们进入微信公众平台登录小程序,设置 =》 基本设置 =》 服务内容声明 =》 用户隐私保护指引 然后点击进行更新完善内容即可。
3-3 注意
出现的问题 :在实际测试过程中,发现 IOS
在获取定位授权时无法显示定位授权弹框,并且诡异的是,我们进入 小程序的"设置"中,并没有显示授权定位权限,但是在 Android
中正常 解决方法 :这是由于我们采用的uniapp写的小程序,使用的 HBuilder X
工具,我们完成了前两项的后,还需要调整一下我们工程中manifest.json
=> "微信小程序配置" =》 "微信小程序权限配置" 勾选上 "位置接口"并填写申请原因,这样就可以解决 IOS
中出现的问题了
js
function getUserAuthorize() {
uni.showLoading({
title: '获取定位权限...'
})
uni.getSetting({
success(res) {
if (!res.authSetting['scope.userLocation']) {
uni.authorize({
scope: 'scope.userLocation',
success: (res) => {
// 用户已经同意小程序使用定位功能,
// 后续调用 不会弹窗询问
getWifiList();
},
fail: (err) => {
uni.hideLoading();
uni.showModal({
content: '请授权获取定位权限...',
showCancel: false,
success: function (res) {
// 引导进入授权
uni.openSetting({
success: (res) => {
/**
* 用户从 设置 - 授权页面跳转回小程序
*/
if (!res.authSetting['scope.userLocation']) {
// 未授权
uni.showModal({
content: '请授权获取定位权限后,再次点击配置网络按钮',
showCancel: false,
})
} else {
// 授权完成,进行配置
getWifiList();
}
},
fail: (err) => {
console.log('失败了 err', err);
}
});
}
})
}
})
} else if (res.authSetting['scope.userLocation']) {
// 已经进行了授权
getWifiList();
}
},
fail: (err) => {
uni.hideLoading();
}
})
}
4、获取当前的WiFi列表
4-1 注意:
- IOS 中 # wx.onGetWifiList在ios中不进入回调
官方说明 2019-09-19:需要跳转到系统WiFi
列表,等到列表刷新出 WiFi,在微信前台的小程序才会收到onGetWifiList
的回调;这是苹果系统的限制,暂时无法规避
4-2 注意,批量配置、单个配置:
- 针对我们一开始提到的,怎样批量给进入
AP
模式的设备,设置WiFi
呢,再获取WiFi
列表的逻辑中,我们就可以将属于设备的WiFi
筛选出来,然后再后面进行循环配置。
单个配置,我们可以提供给用户让其输入指定设备的信息,然后在配置的时候进行处理。
js
function getWifiList() {
uni.showLoading({
title: '获取当前WiFi列表...'
})
uni.getWifiList({
success: (res) => {
uni.showLoading({
title: '获取当前WiFi列表成功'
})
/**
* 判断如果是苹果手机
* 无法 进行 uni.onGetWifiList 监听
*/
setTimeout(() => {
if(phoneType == 'ios') {
uni.hideLoading();
uni.showModal({
content: 'IOS手机无法主动获取WiFi列表,请先进入【设置-WiFi】页面,带加载完毕WiFi列表后,返回小程序完成后续配置',
showCancel: false,
})
}
}, 2000)
},
fail: (err) => {
let {
errno
} = err;
uni.hideLoading();
uni.showModal({
content: '获取WiFi列表失败,请确认打开了手机WiFi,并授权获取定位;确认无误后,再次点击【配置网络】按钮,进行配置',
})
},
})
// 监听获取到的 WiFi列表
uni.onGetWifiList((res) => {
uni.showLoading({
title: '匹配查询当前配网设备...'
})
let {
wifiList
} = res;
wifiList.forEach(item => {
if(
item.SSID.indexOf('旧SSID') !== -1 ||
item.SSID.indexOf('新SSID') !== -1
) {
// 存储当前设备产生的热点
configDeviceSSIDList.add(item.SSID);
}
})
if (configDeviceSSIDList.size !== 0) {
connectWifi();
} else {
// 当前没有设备进入配网模式
uni.hideLoading();
uni.showModal({
content: '未搜索到设备生成的WiFi,请确定当前设备已经入配网模式,查看一下当前的WiFi列表,然后返回小程序重新点击【配置网络】按钮',
})
}
})
}
5、进行 WiFi 链接
注意 5-1:
问题 :在测试过程中部分手机是无法直接正常连接到 WiFi
的
解决方法:
- 将设备生成的热点,密码设置为空; 这种方式会提升在华为、小米手机中连接
WiFi
的成功率 - 针对无法正常连接
WiFi
的情况,进行兜底措施,引导用户跳转到系统设置页进行连接。
通过wx.connectWifi
官方文档中提供的maunal
参数,可以做到这一点。官方文档
js
function connectWifi() {
let {
ssid,
pwd
} = configInfo;
uni.showLoading({
title: '连接设备WIFI'
})
let configDeviceSsidArr = Array.from(configDeviceSSIDList);
connectSsid = configDeviceSsidArr[0];
uni.connectWifi({
SSID: configDeviceSsidArr[0],
password: '',
// maunal: true,
success(res) {
uni.showLoading({
title: '设置中...'
})
sendCfg();
},
fail(err) {
wx.hideLoading();
manualWifiCallback();
}
})
}
5-2、当前wifi连接失败,进行重连
js
function manualConnectWifi() {
let configDeviceSsidArr = Array.from(configDeviceSSIDList);
connectSsid = configDeviceSsidArr[0];
uni.connectWifi({
SSID: configDeviceSsidArr[0],
password: '',
maunal: true,
success: (res) => {
console.log('手动链接完毕 res', res);
},
fail(err) {
uni.hideLoading();
if (err.errCode === 12005) {
uni.showModal({
content: '请打开WIFI开关,然后再点击配置网络按钮',
success: (res) => {
if (res.confirm) {
console.log('用户点击确定')
}
}
})
} else if (err.errCode === 12007) {
uni.showModal({
title: '用户拒绝授权连接 Wi-Fi,请手动进行连接',
content: 'WIFI连接失败,请手动连接设备WIFI,成功后返回小程序点击配置网络按钮',
success: (res) => {
if (res.confirm) {
// console.log('用户点击确定')
}
}
})
} else if (err.errCode === 12010) {
uni.showModal({
title: '系统其他错误,请手动进行连接',
content: 'WIFI连接失败,请手动连接设备WIFI,成功后返回小程序点击配置网络按钮',
success: (res) => {
if (res.confirm) {
}
}
})
} else {
uni.showModal({
title: '设备WIFI连接失败,请手动进行连接',
content: 'WIFI连接失败,请手动连接设备WIFI;成功后返回小程序点击配置网络按钮',
success: (res) => {
if (res.confirm) {
console.log('用户点击确定')
}
}
})
}
}
})
}
7、建立UDP连接,发送消息
7-1 注意:
- 为了避免设备无法及时接收到发送的信息,我们进行周期性发送,发送多次消息
- 为了防止
UDP
信息传输过程中,数据丢失,保证设备接收到的数据完整性;通过与设备配合将它接收到的信息返回,小程序监听UDP
信息,比对是否一致
js
function sendCfg() {
if (!configInfo.ssid) {
// 当链接成功、或超时之后 关闭定时器以及重置计数器
initCfgSign();
uni.showModal({
content: '请重新点击【配置网络】按钮,进行配网操作',
showCancel: false,
success: (res) => {
}
})
}
let jo = {
"ssid": configInfo.ssid,
"pwd": configInfo.pwd,
"url": configInfo.url
};
if (configInfo.ip && configInfo.ip != '') {
jo.ip = configInfo.ip;
jo.sub = configInfo.sub;
jo.gw = configInfo.gw;
jo.dns1 = configInfo.dns1;
jo.dns2 = configInfo.dns2;
}
uni.showLoading({
title: 'UDP信息传输中...',
})
let js = JSON.stringify(jo);
udp = uni.createUDPSocket();
udp.bind();
udp.send({
address: '地址',
port: 'udp 端口',
message: '字符串信息'
});
if (!sendTimer) {
sendTimer = setInterval(() => {
console.log('周期发送UDP消息', udp);
if (connectUdpNum > maxUdp) {
initCfgSign(true);
uni.showToast({
title: 'UDP连接超时,请重新配置网络',
icon: 'none',
duration: 3000
})
return;
}
udp.send({
address: '地址',
port: 'udp 端口',
message: '字符串信息'
});
connectUdpNum++
}, 1000);
} else {
// 已存在该定时器
console.log('定时器已存在');
}
// 监听 udp 传送的信息,判断传递的数据信息是否正确,有没有丢失
udp.onMessage(onUdpMessage);
}
7-2 监听UDP 信息传输
js
function onUdpMessage(res) {
if (res.remoteInfo.size > 0) {
// 将 ArrayBuffer类型的res.message取出来
let unit8Arr = new Uint8Array(res.message)
let encodedString = String.fromCharCode.apply(null, unit8Arr);
// 没有这一步中文会乱码
let decodedString = decodeURIComponent(escape((encodedString)))
let jo = JSON.parse(decodedString);
console.log('jo', jo);
/**
* 判断返回的连接信息与设备接收到的是否一致
* 防止 UDP传输过程中 数据丢失
*/
let {
ssid,
pwd,
url
} = configInfo;
if (
ssid === jo.ssid &&
pwd === jo.pwd &&
url === jo.url
) {
/**
* 已收到正确的消息,
* 关闭发送UDP 超时监听
*/
if (sendTimer) clearInterval(sendTimer);
uni.showLoading({
title: 'UDP信息传输成功...',
})
let msg = {
err: 0
}
udp.send({
address: '地址',
port: 端口,
message: JSON.stringify(msg),
});
/**
* 防止长时间过长没有返回 err 0
*/
udpOverTimer = setTimeout(() => {
uni.hideLoading();
initCfgSign();
uni.showModal({
title: 'UDP信息传输完毕',
content: '请确认是否配置成功,如果失败请重新进行配网操作',
})
}, 1000 * 10)
} else if (
jo.err === 0
) {
initCfgSign();
/**
* 设备成功接收到数据
*/
uni.showToast({
title: '网络配置成功',
icon: 'success'
})
} else {
initCfgSign();
uni.showModal({
title: 'UDP信息传输数据丢失',
content: 'UDP信息传输过程中数据丢失,请重新进行配置',
})
}
}
}
8、重置当前的标识,关闭WiFi模块、移除WiFi连接事件监听
8-1 重置当前的标识
js
function initCfgSign(isOverTime = false) {
/**
* 判断当前 WiFi 列表中还有无其他 待配置的设备
* 没有的话,关闭当前WiFi列表
* 有的话,继续进行配置
*/
configDeviceSSIDList.delete(connectSsid);
// console.log('configDeviceSSIDList', configDeviceSSIDList);
if(configDeviceSSIDList.size !== 0 && !isOverTime) {
// 继续调用配置功能进行配置
connectWifi();
return;
}
connectSsid = '';
configInfo = null;
maxUdp = 20;
connectUdpNum = 0;
uni.hideLoading();
udp.close();
udp = null;
closeWifi();
if (sendTimer) clearInterval(sendTimer);
if (udpOverTimer) clearTimeout(udpOverTimer);
}
8-2 关闭WiFi模块
js
function closeWifi() {
offWifiConnected();
uni.stopWifi({
success(res) {
console.log('关闭当前wifi模块', res.errMsg)
}
})
}
8-3 移除当前监听 WiFi 连接事件
js
function offWifiConnected() {
uni.offWifiConnected();
}
其他注意事项
以上就是整体我们在实现微信小程序AP配网的过程中,WiFi
部分的详细讲解。我们将上述的各部分代码串联起来,并针对自己的实际需求,就可以完成在微信小程序开发过程中WiFi
的逻辑处理。
但是,在这个过程中,最开始我还踩了个坑,主要是对实际应用场景考虑的不全面。
针对三方,中间商给设备进行配网
这个场景就有点儿特殊了,当我们的设备卖给中间商,然后他们去给用户进行配置,这个时候有个特殊的情景:
- 配置设备的现场并不是实际使用的地方
举个例子:就是中间商或者一些用户,他们要给设备配置WiFi,但是他在家里配置,但是最终要用到工厂中,这就造成,用户配置的时候有可能当前WiFi
开关并没有开、或者当前WiFi
打开了,但是家里并没有WiFi
。
在这种情况下,我们就不能通过获取用户当前的网络类型uni.getNetworkType
,来决定是否初始化WiFi了。
总结
如果你看到这里,那么恭喜你也成功的梳理了一遍微信小程序WiFi 相关的知识。到这儿,本次主要的配置流程、实际操作中WiFi
的注意事项就讲完了。
针对文中一些不足欢迎大家评论区中指出,创作不易,希望大家能多点赞收藏。
技术的路上记录点点滴滴,大家携手前行,与君共勉!!!