企业微信自建应用H5开发之实现蓝牙的连接和写数据(2022.8.3)
写在前面:经过无数的踩坑和调试,终于实现了IOS设备和Andriod设备的蓝牙开门。当然,不可避免还有很多奇怪的BUG被我忽略了,但是能够正常走通流程并实现。
所使用的技术为Vue2+Spring Boot,如果是利用微信小程序开发的可以跳忽略本文“
第一到第三节都讲如何创建应用并访问到自己所搭建的H5界面,如果没有需要可以跳过
一、创建应用
首先我们要进入企业微信的管理后台,前提是管理员的身份。然后在应用管理
->自建
中创建应用,如下图:
填写相关的信息之后创建应用,进入详情页。
二、设置网页授权和JS-SDK
向下翻,会看到开发者接口等内容,我们需要进行网页授权和JS-SDK
操作
这里需要填写备案的域名,否则无法调用。比如www.baidu.com 此类,填写完域名后需要校验,否则无法使用JS-SDK相关功能。
我们将下载下来的txt文档存放到指定目录下,一般为项目根目录下。我这边因为使用的是nginx反向代理,所以采取了另一种方法,直接在nginx.conf配置文件中加入如下配置。
完成验证之后就成功了一半了。
三、设置应用主页
其中需要几个参数
参数名 | 说明 |
---|---|
CORPID | 企业的ID |
REDIRECT_URI | 回调链接地址,需要用urlencode对链接进行编码 |
AGENTID | 应用ID |
然后在企业微信中打开自建应用就能跳转到自己的前端页面了。
四、调用JS-SDK
步骤1、引入JS文件
图省事的可以直接放在index.html中
步骤2、通过config接口注入权限验证配置
这一步需要通过后端的配合,企业微信的文档写的还算详细,但是具体细节还需要自己研究。
这里贴上我所使用的代码
-
定义签名相关实体类
import lombok.Data; /** * @author : LeonAmour * @date : 2022/7/22 18:49 * 获取前端配置需要的数据 */ @Data public class ConfigDataVo { /** * 随机字符串 */ private String nonceStr; /** * 时间戳 */ private String timestamp; /** * 签名 */ private String signature; /** * 应用ID */ private String appId; /** * access_token(用于获取当前用户ID) */ private String access_token; }
-
编写接口
@ApiOperation("企业微信配置") @GetMapping("/getAutograph") public ConfigDataVo getAutograph(@RequestParam String postUrl) { ConfigDataVo configDataVo = new ConfigDataVo(); //appId String appId = qywxConstant.CORP_ID; //第一步:获取得到token //获取access_token String urlStr = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + qywxConstant.CORP_ID + "&corpsecret=" + qywxConstant.CORP_SECRET; String response1 = restTemplate.getForObject(urlStr, String.class); JSONObject jsonObject1 = JSON.parseObject(response1); Object accessToken = jsonObject1.get("access_token"); //第二步:获取企业的jsapi_ticket String ticketUrl = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=" + accessToken; String response2 = restTemplate.getForObject(ticketUrl, String.class); JSONObject jsonObject2 = JSON.parseObject(response2); Object jsapi_ticket = jsonObject2.get("ticket"); //第三步:生成签名 //生成随机字符串 String nonceStr = RandomStringUtils.randomAlphanumeric(10); //生成时间戳 String timestamp = String.valueOf(System.currentTimeMillis() / 1000); //微信可以访问到的前端html文件的路径地址 String url = postUrl; String signature = "jsapi_ticket=" + jsapi_ticket + "&noncestr=" + nonceStr + "×tamp=" + timestamp + "&url=" + url; configDataVo.setAccess_token((String) accessToken); configDataVo.setNonceStr(nonceStr); configDataVo.setTimestamp(timestamp); configDataVo.setSignature(signature); configDataVo.setAppId(appId); return configDataVo; }
该接口中需要传递一个url,即当前网页的URL(URL的截取就不细讲了,懂点JS都会的)。上述代码中涉及到的qywxConstant的常量由自己定义。
- 前端请求该接口
后端将wx.config所需的字段全部返回,这边返回的签名还未做sha1的加密,前端需要添加一个sha1的依赖。
npm install sha1 --save
在所需的页面进行引用
import sha1 from "sha1";
最后构造下来的权限验证如下:
async getSignature() {
this.url = window.location.href;
const params = {
postUrl: this.url,
};
await qywxSignature(params).then((response) => {
let verifyStr = response.signature;
let signature = sha1(verifyStr);
wx.config({
beta: true, // 必须这么写,否则wx.invoke调用形式的jsapi会有问题
// 上线的时候记得将 debug 关掉哦!!!!!
debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: "ww1234567890", // 必填,企业微信的corpID
timestamp: response.timestamp, // 必填,生成签名的时间戳
nonceStr: response.nonceStr, // 必填,生成签名的随机串
signature: signature, // 必填,签名,见 附录-JS-SDK使用权限签名算法
jsApiList: [
"openBluetoothAdapter",
"onBluetoothAdapterStateChange",
"onBluetoothDeviceFound",
"createBLEConnection",
"onBLEConnectionStateChange",
"getBLEDeviceServices",
"closeBluetoothAdapter",
"getBLEDeviceCharacteristics",
"getBluetoothAdapterState",
"getBluetoothDevices",
"closeBLEConnection",
"notifyBLECharacteristicValueChange",
"onBLECharacteristicValueChange",
"startBluetoothDevicesDiscovery",
"stopBluetoothDevicesDiscovery",
"writeBLECharacteristicValue",
], // 必填,需要使用的JS接口列表,凡是要调用的接口都需要传进来
});
// //打开蓝牙适配器,初始化蓝牙模块
// this.openBluetoothAdapter();
wx.error(function (res) {
console.log("错误!", res);
});
});
},
其中的appId改成自己的。
调用成功后控制台会返回如下内容
这就说明通过config接口注入权限验证配置成功
五、开始调用蓝牙相关接口
企业微信蓝牙开发文档,一切还是以官方文档为准,我主要以代码为主进行简单描述。按顺序执行即可。你们可能会对row这个变量产生疑问,因为我这边的开门是点击对应的门发送蓝牙指令的,所以相关的MAC地址,蓝牙名称和蓝牙门禁密码都在row中
1.初始化蓝牙模块
openBluetoothAdapter(row) {
//初始化蓝牙模块
wx.openBluetoothAdapter({
success: (res) => {
console.log("初始化蓝牙模块成功!", res);
this.startBluetoothDevicesDiscovery(row);
},
fail: (res) => {
console.log("error", res);
if (res.errCode === 10001) {
//蓝牙未打开,监听蓝牙适配器状态变化
wx.onBluetoothAdapterStateChange(function (res) {
console.log("监听蓝牙适配器状态变化", res);
//如果为可用状态
if (res.available) {
//开始搜寻附近蓝牙外围设备
this.startBluetoothDevicesDiscovery(row);
}
});
}
},
});
},
这边基本不会出问题,除非你蓝牙没打开。
2.开始搜索外围设备
//开始搜索外围设备
startBluetoothDevicesDiscovery(row) {
if (this._discoveryStarted) {
return;
}
this._discoveryStarted = true;
wx.startBluetoothDevicesDiscovery({
service: ["FEE0"],
// allowDuplicatesKey: true,
success: (res) => {
console.log("搜索蓝牙设备成功!", res);
this.onBluetoothDeviceFound(row);
},
});
},
这边只针对service的UUID为FFE0
进行搜索,搜索到了就执行监听新发现的蓝牙设备
3.监听寻找到新设备的事件
onBluetoothDeviceFound(row) {
//获取新发现的蓝牙设备
wx.onBluetoothDeviceFound((res) => {
console.log("设备列表:", res);
// res.devices.forEach((device) => {
res.forEach((device) => {
if (!device.name && !device.localName) {
return;
}
//通过获取MAC地址匹配
if (device.deviceId == row.macAddress) {
var deviceId1 = device.deviceId;
//搜索到对应蓝牙后,停止搜索
this.stopBluetoothDevicesDiscovery();
console.log("android-->DeviceId:" + deviceId1);
//连接设备
this.createBLEConnection(row, deviceId1);
}
//通过name匹配设备
if (device.name == row.bluetooth) {
var deviceId2 = device.deviceId;
console.log("ios or android-->DeviceId:" + deviceId2);
//搜索到对应蓝牙后,停止搜索
this.stopBluetoothDevicesDiscovery();
//连接设备
this.createBLEConnection(row, deviceId2);
}
});
});
},
这边会存在几个问题
第一点:只要监听到新的蓝牙设备,就会打印一个res,也就是说返回的res代表的是一个蓝牙设备的信息,而不是搜索到的所有蓝牙设备,我当时针对res.devices进行forEach查了好久没查出问题。现在其实可以将forEach取消,因为只有一个对象
第二点:安卓设备和IOS设备所识别到的deviceId是不同的,这个在官方文档中也着重提示过,所以这边做了两个判断,一个是针对安卓设备的,判断其deviceId是否与我们所找的蓝牙的MAC地址一致,另一个判断就是直接查找名称是否与我们所找的蓝牙设备名称一致的,然后建立连接
第三点:关于先停止搜索蓝牙再连接还是先连接再停止搜索蓝牙设备,这一点尤其要注意。一定是先停止搜索,再进行连接,否则会导致连接超时,具体原因不太清楚,而且这个问题只出现在IOS中,安卓设备先连接再停止也是可行的,非常奇怪
4.连接低功耗蓝牙设备
//连接蓝牙设备
createBLEConnection(row, deviceId) {
wx.createBLEConnection({
deviceId,
success: (res) => {
console.log("连接成功!");
this.getBLEDeviceServices(row, deviceId);
},
fail: (res) => {
console.log("设备连接失败:" + res);
this.closeBLEConnection();
},
});
},
5.获取蓝牙设备所有service(服务)
getBLEDeviceServices(row, deviceId) {
wx.getBLEDeviceServices({
deviceId,
success: (res) => {
if (res.services.length != 0) {
for (let i = 0; i < res.services.length; i++) {
if (
res.services[i].uuid.toString().toUpperCase()==
"0000FFE0-0000-1000-8000-00805F9B34FB"
) {
this.getBLEDeviceCharacteristics(
row,
deviceId,
res.services[i].uuid
);
return;
}
}
}
},
});
},
这边由于我是需要给蓝牙写数据,所以目标很明确,搜索的service的UUID直接写死
6.获取蓝牙设备某个服务中的所有 characteristic(特征值)
//获取设备特征值
getBLEDeviceCharacteristics(row, deviceId, serviceId) {
wx.getBLEDeviceCharacteristics({
deviceId,
serviceId,
success: (res) => {
console.log("获取蓝牙特征值成功", res.characteristics);
for (let i = 0; i < res.characteristics.length; i++) {
let item = res.characteristics[i];
// //读操作
// if (item.properties.read) {
// wx.readBLECharacteristicValue({
// deviceId: deviceId,
// serviceId: serviceId,
// characteristicId: item.uuid,
// });
// }
//写操作
if (item.properties.write) {
this.writeBLECharacteristicValue(
row,
deviceId,
serviceId,
item.uuid
);
}
if (item.properties.notify || item.properties.indicate) {
wx.notifyBLECharacteristicValueChange({
deviceId: deviceId,
serviceId: serviceId,
characteristicId: item.uuid,
state: true,
});
}
}
},
fail(res) {
console.error("获取蓝牙特征值失败", res);
},
});
对所有特征值进行循环,如果特征值中write为true,则写数据
7.写数据
//写数据
writeBLECharacteristicValue(row, deviceId, serviceId, characteristicId) {
var that = this;
let hexCode = string2Hex(row.doorPwd);
console.log(hexCode);
// 向蓝⽛设备发送⼀个0x00的16进制数据
let buffer = hexStringToArrayBuffer(hexCode);
console.log("发送数据:", buffer);
wx.writeBLECharacteristicValue({
deviceId,
serviceId,
characteristicId,
value: buffer,
success: function (res) {
console.log("写入数据成功!");
that.msgSuccess("开门成功");
//写完数据后关闭蓝牙适配器
that.closeBLEConnection(deviceId);
that.closeBluetoothAdapter();
},
});
},
写数据这里涉及到几个方法,将字符串先转成Hex,然后再转成arraybuffer
//hex转arrayBuffer
function hexStringToArrayBuffer(str) {
if (!str) {
return new ArrayBuffer(0);
}
var buffer = new ArrayBuffer(str.length / 2.0);
let dataView = new DataView(buffer);
let ind = 0;
for (let i = 0; i < str.length; i += 2) {
let code = parseInt(str.substr(i, 2), 16);
dataView.setUint8(ind, code);
ind++;
}
return buffer;
}
// 字符串转16进制字符串
function string2Hex(str) {
let val = "";
for (let i = 0; i < str.length; i++) {
if (val == "") val = str.charCodeAt(i).toString(16);
else val += str.charCodeAt(i).toString(16);
}
return val;
}
到此,可以正常调用蓝牙进行开门操作。
六、前端源码
为了防止有部分代码没贴上,放出该页面的所有代码以供参考。企业微信的应用开发是真的坑,希望为后来者提供更多的参考经验。
<template>
<div>
<el-card>
<el-table :data="this.warehouseList">
<el-table-column prop="index" label="序号" width="50px" align="center">
<template slot-scope="scope">
{{ scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column label="仓库名称" align="center" prop="warehouseName" />
<el-table-column
label="操作"
align="center"
class-name="small-padding fixed-width"
>
<template slot-scope="scope">
<el-button
type="primary"
icon="el-icon-arrow-right"
@click="openBluetoothAdapter(scope.row)"
>开门</el-button
>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script>
import { qywxSignature } from "@/api/qywx/wx";
import sha1 from "sha1";
import { listWarehouse } from "@/api/baseinfo/warehouse";
function inArray(arr, key, val) {
for (let i = 0; i < arr.length; i++) {
if (arr[i][key] === val) {
return i;
}
}
return -1;
}
// ArrayBuffer转16进度字符串⽰例
function ab2hex(buffer) {
var hexArr = Array.prototype.map.call(new Uint8Array(buffer), function (bit) {
return ("00" + bit.toString(16)).slice(-2);
});
return hexArr.join("");
}
//hex转arrayBuffer
function hexStringToArrayBuffer(str) {
if (!str) {
return new ArrayBuffer(0);
}
var buffer = new ArrayBuffer(str.length / 2.0);
let dataView = new DataView(buffer);
let ind = 0;
for (let i = 0; i < str.length; i += 2) {
let code = parseInt(str.substr(i, 2), 16);
dataView.setUint8(ind, code);
ind++;
}
return buffer;
}
// 字符串转16进制字符串
function string2Hex(str) {
let val = "";
for (let i = 0; i < str.length; i++) {
if (val == "") val = str.charCodeAt(i).toString(16);
else val += str.charCodeAt(i).toString(16);
}
return val;
}
export default {
name: "wechat",
data() {
return {
warehouseList: [],
device: [],
url: "",
signature: "",
ticket: "",
nonceStr: "",
timestamp: "",
_discoveryStarted: false,
};
},
mounted() {
this.getWarehouse();
this.getSignature();
},
create() {},
methods: {
getWarehouse() {
listWarehouse().then((response) => {
console.log(response);
this.warehouseList = response.rows;
});
},
async getSignature() {
this.url = window.location.href;
console.log(this.url);
const params = {
postUrl: this.url,
};
await qywxSignature(params).then((response) => {
console.log(response);
let verifyStr = response.signature;
let signature = sha1(verifyStr);
console.log("签名", signature);
wx.config({
beta: true, // 必须这么写,否则wx.invoke调用形式的jsapi会有问题
// 上线的时候记得将 debug 关掉哦!!!!!
debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: "ww0123456789", // 必填,正式企业微信的corpID
timestamp: response.timestamp, // 必填,生成签名的时间戳
nonceStr: response.nonceStr, // 必填,生成签名的随机串
signature: signature, // 必填,签名,见 附录-JS-SDK使用权限签名算法
jsApiList: [
"openBluetoothAdapter",
"onBluetoothAdapterStateChange",
"onBluetoothDeviceFound",
"createBLEConnection",
"onBLEConnectionStateChange",
"getBLEDeviceServices",
"closeBluetoothAdapter",
"getBLEDeviceCharacteristics",
"getBluetoothAdapterState",
"getBluetoothDevices",
"closeBLEConnection",
"notifyBLECharacteristicValueChange",
"onBLECharacteristicValueChange",
"startBluetoothDevicesDiscovery",
"stopBluetoothDevicesDiscovery",
"writeBLECharacteristicValue",
], // 必填,需要使用的JS接口列表,凡是要调用的接口都需要传进来
});
// //打开蓝牙适配器,初始化蓝牙模块
// this.openBluetoothAdapter();
wx.error(function (res) {
console.log("错误!", res);
});
});
},
openBluetoothAdapter(row) {
//初始化蓝牙模块
wx.openBluetoothAdapter({
success: (res) => {
console.log("初始化蓝牙模块成功!", res);
this.startBluetoothDevicesDiscovery(row);
},
fail: (res) => {
console.log("error", res);
if (res.errCode === 10001) {
//蓝牙未打开,监听蓝牙适配器状态变化
wx.onBluetoothAdapterStateChange(function (res) {
console.log("监听蓝牙适配器状态变化", res);
//如果为可用状态
if (res.available) {
//开始搜寻附近蓝牙外围设备
this.startBluetoothDevicesDiscovery(row);
}
});
}
},
});
},
//开始搜索外围设备
startBluetoothDevicesDiscovery(row) {
if (this._discoveryStarted) {
return;
}
this._discoveryStarted = true;
wx.startBluetoothDevicesDiscovery({
service: ["FEE0"],
// allowDuplicatesKey: true,
success: (res) => {
console.log("搜索蓝牙设备成功!", res);
this.onBluetoothDeviceFound(row);
},
});
},
onBluetoothDeviceFound(row) {
//获取新发现的蓝牙设备
wx.onBluetoothDeviceFound((res) => {
console.log("设备列表:", res);
// res.devices.forEach((device) => {
res.forEach((device) => {
if (!device.name && !device.localName) {
return;
}
//通过获取MAC地址匹配
if (device.deviceId == row.macAddress) {
var deviceId1 = device.deviceId;
//搜索到对应蓝牙后,停止搜索
this.stopBluetoothDevicesDiscovery();
console.log("android-->tempDeviceId:" + deviceId1);
//连接设备
this.createBLEConnection(row, deviceId1);
}
//通过name匹配设备
if (device.name == row.bluetooth) {
var deviceId2 = device.deviceId;
console.log("ios or android-->tempDeviceId:" + deviceId2);
//搜索到对应蓝牙后,停止搜索
this.stopBluetoothDevicesDiscovery();
//连接设备
this.createBLEConnection(row, deviceId2);
}
});
});
},
//连接蓝牙设备
createBLEConnection(row, deviceId) {
wx.createBLEConnection({
deviceId,
success: (res) => {
console.log("连接成功!");
this.getBLEDeviceServices(row, deviceId);
},
fail: (res) => {
console.log("设备连接失败:" + res);
this.closeBLEConnection();
},
});
},
getBluetoothAdapterState() {
//获取本机蓝牙适配器状态
wx.getBluetoothAdapterState({
success: (res) => {
console.log("获取本机蓝牙适配器状态", res);
//正在搜索
if (res.discovering) {
//寻找新设备
this.onBluetoothDeviceFound();
//蓝牙可用
} else if (res.available) {
//开始搜索附近蓝牙外围设备
this.startBluetoothDevicesDiscovery();
}
},
});
},
//停止搜索新设备
stopBluetoothDevicesDiscovery() {
wx.stopBluetoothDevicesDiscovery();
console.log("停止搜索蓝牙设备");
},
closeBLEConnection(deviceId) {
wx.closeBLEConnection({
deviceId,
});
},
getBLEDeviceServices(row, deviceId) {
wx.getBLEDeviceServices({
deviceId,
success: (res) => {
if (res.services.length != 0) {
for (let i = 0; i < res.services.length; i++) {
if (
res.services[i].uuid.toString().toUpperCase()==
"0000FFE0-0000-1000-8000-00805F9B34FB"
) {
this.getBLEDeviceCharacteristics(
row,
deviceId,
res.services[i].uuid
);
return;
}
}
}
},
});
},
//获取设备特征值
getBLEDeviceCharacteristics(row, deviceId, serviceId) {
wx.getBLEDeviceCharacteristics({
deviceId,
serviceId,
success: (res) => {
console.log("获取蓝牙特征值成功", res.characteristics);
for (let i = 0; i < res.characteristics.length; i++) {
let item = res.characteristics[i];
// //读操作
// if (item.properties.read) {
// wx.readBLECharacteristicValue({
// deviceId: deviceId,
// serviceId: serviceId,
// characteristicId: item.uuid,
// });
// }
//写操作
if (item.properties.write) {
this.writeBLECharacteristicValue(
row,
deviceId,
serviceId,
item.uuid
);
}
if (item.properties.notify || item.properties.indicate) {
wx.notifyBLECharacteristicValueChange({
deviceId: deviceId,
serviceId: serviceId,
characteristicId: item.uuid,
state: true,
});
}
}
},
fail(res) {
console.error("获取蓝牙特征值失败", res);
},
});
// 操作之前先监听,保证第⼀时间获取数据
wx.onBLECharacteristicValueChange((characteristic) => {
const idx = inArray(
this.data.chs,
"uuid",
characteristic.characteristicId
);
const data = {};
if (idx === -1) {
data[`chs[${this.data.chs.length}]`] = {
uuid: characteristic.characteristicId,
value: ab2hex(characteristic.value),
};
} else {
data[`chs[${idx}]`] = {
uuid: characteristic.characteristicId,
value: ab2hex(characteristic.value),
};
}
// data[`chs[${this.data.chs.length}]`] = {
// uuid: characteristic.characteristicId,
// value: ab2hex(characteristic.value)
// }
// this.setData(data);
});
},
//写数据
writeBLECharacteristicValue(row, deviceId, serviceId, characteristicId) {
var that = this;
let hexCode = string2Hex(row.doorPwd);
console.log(hexCode);
// 向蓝⽛设备发送⼀个0x00的16进制数据
let buffer = hexStringToArrayBuffer(hexCode);
console.log("发送数据:", buffer);
wx.writeBLECharacteristicValue({
deviceId,
serviceId,
characteristicId,
value: buffer,
success: function (res) {
console.log("写入数据成功!");
that.msgSuccess("开门成功");
//写完数据后关闭蓝牙适配器
that.closeBLEConnection(deviceId);
that.closeBluetoothAdapter();
},
});
},
closeBluetoothAdapter() {
console.log("关闭蓝牙适配器");
wx.closeBluetoothAdapter();
},
},
};
</script>