@@ -11,7 +11,7 @@ | |||
}, | |||
"plugins": { | |||
"sdkPlugin": { | |||
"version": "1.1.1", | |||
"version": "1.1.2", | |||
"provider": "wx17e93aad47cdae1a" | |||
} | |||
}, |
@@ -4,16 +4,23 @@ const plugin = requirePlugin("sdkPlugin").AiLink; | |||
function inArray(arr, key, val) { | |||
if (!arr || !arr.length || typeof arr != 'object' || !Array.isArray(arr)) { | |||
return -1 | |||
} | |||
for (let i = 0; i < arr.length; i++) { | |||
if (arr[i][key] === val) { | |||
return i; | |||
if (!key) { | |||
if (arr[i] == val) { | |||
return i | |||
} | |||
} else if (arr[i][key] === val) { | |||
return i | |||
} | |||
} | |||
return -1; | |||
} | |||
// ArrayBuffer转16进度字符串示例 | |||
function ab2hex(buffer, split = ',') { | |||
function ab2hex(buffer, split) { | |||
var hexArr = Array.prototype.map.call( | |||
new Uint8Array(buffer), | |||
function (bit) { | |||
@@ -41,9 +48,17 @@ Page({ | |||
], | |||
connected: false, | |||
chs: [], | |||
cmd: '', | |||
name: '', | |||
deviceId: null, | |||
historyList: [], | |||
}, | |||
onLoad: function () { | |||
let historyList = [] | |||
historyList = wx.getStorageSync('historyList') | |||
this.setData({ | |||
historyList, | |||
}) | |||
}, | |||
// 初始化蓝牙模块 | |||
@@ -92,7 +107,7 @@ Page({ | |||
allowDuplicatesKey: true, | |||
services: [ | |||
"FFE0", | |||
"F0A0", | |||
// "F0A0", // BM30广播模块需加上,如使用连接模块可忽略 | |||
], | |||
success: (res) => { | |||
console.log('startBluetoothDevicesDiscovery success', res) | |||
@@ -129,6 +144,9 @@ Page({ | |||
} else { | |||
let buff = device.advertisData.slice(-6) | |||
device.mac = new Uint8Array(buff) // 保存广播数据中的mac地址,这是由于iOS不直接返回mac地址 | |||
let tempMac = Array.from(device.mac) | |||
tempMac.reverse() | |||
device.macAddr = ab2hex(tempMac, ':').toUpperCase() | |||
} | |||
if (idx === -1) { | |||
data[`devices[${foundDevices.length}]`] = device | |||
@@ -141,6 +159,16 @@ Page({ | |||
}, | |||
// 连接低功耗蓝牙设备 | |||
createBLEConnection(e) { | |||
this._connLoading = true | |||
wx.showLoading({ | |||
title: '连接中', | |||
}) | |||
setTimeout(() => { | |||
if (this._connLoading) { | |||
this._connLoading = false | |||
wx.hideLoading() | |||
} | |||
}, 6000) | |||
const ds = e.currentTarget.dataset | |||
const index = ds.index | |||
// 保存当前连接的设备,注意不能从wxml的dataset中直接返回该对象,因为ArrarBuffer类型的数据无法保留 | |||
@@ -159,13 +187,40 @@ Page({ | |||
}) | |||
console.log("createBLEConnection:success") | |||
wx.stopBluetoothDevicesDiscovery() | |||
this.onBLEConnectionStateChange() | |||
this.getBLEDeviceServices(deviceId) | |||
}, | |||
fail: res => { | |||
this._connLoading = false | |||
wx.hideLoading() | |||
wx.showToast({ | |||
title: '连接失败', | |||
icon: 'none' | |||
}) | |||
} | |||
}) | |||
// 连接上设备就可以停止蓝牙搜索,减少功耗。 | |||
this.stopBluetoothDevicesDiscovery() | |||
}, | |||
onBLEConnectionStateChange() { | |||
wx.onBLEConnectionStateChange((res) => { | |||
console.log('wx.onBLEConnectionStateChange() ', res.deviceId, res.connected, res.errorCode, res.errorMsg) | |||
// 该方法回调中可以用于处理连接意外断开等异常情况 | |||
// console.log(`%c device ${res.deviceId} state has changed, connected: ${res.connected}`, 'color: #F26363') | |||
if (!res.connected) { | |||
wx.showToast({ | |||
title: '连接已断开', | |||
icon: 'none' | |||
}) | |||
this.setData({ | |||
connected: false, | |||
showWriteInput: false, | |||
}) | |||
} | |||
}) | |||
}, | |||
// 断开与低功耗蓝牙设备的连接 | |||
closeBLEConnection() { | |||
wx.closeBLEConnection({ | |||
@@ -174,6 +229,7 @@ Page({ | |||
this.setData({ | |||
connected: false, | |||
chs: [], | |||
showWriteInput: false, | |||
}) | |||
}, | |||
@@ -182,9 +238,8 @@ Page({ | |||
wx.getBLEDeviceServices({ | |||
deviceId, | |||
success: (res) => { | |||
console.log(res) | |||
for (let i = 0; i < res.services.length; i++) { | |||
if (res.services[i].isPrimary && res.services[i].uuid.indexOf("FFE0")>-1) { | |||
if (res.services[i].isPrimary && res.services[i].uuid.indexOf('FFE0') > -1) { | |||
this.getBLEDeviceCharacteristics(deviceId, res.services[i].uuid) | |||
return | |||
} | |||
@@ -207,44 +262,48 @@ Page({ | |||
// 这部分功能在插件的 initPlugin 中已实现,如需用到其中的 uuid 也可取消注释 | |||
// // let uuid1, uuid2, uuid3; | |||
// for (let i = 0; i < res.characteristics.length; i++) { | |||
// let item = res.characteristics[i] | |||
// if (item.uuid.indexOf('0000FFE1') != -1) { | |||
// this.uuid1 = item.uuid //下发数据 | |||
// } else if (item.uuid.indexOf('0000FFE2') != -1) { | |||
// this.uuid2 = item.uuid //监听数据 | |||
// } else if (item.uuid.indexOf('0000FFE3') != -1) { | |||
// this.uuid3 = item.uuid //写入设置 | |||
// } | |||
// } | |||
// // 打开监听 | |||
// wx.notifyBLECharacteristicValueChange({ | |||
// deviceId, | |||
// serviceId, | |||
// characteristicId: this.uuid2, | |||
// state: true, | |||
// }) | |||
// wx.notifyBLECharacteristicValueChange({ | |||
// deviceId, | |||
// serviceId, | |||
// characteristicId: this.uuid3, | |||
// state: true, | |||
// }) | |||
for (let i = 0; i < res.characteristics.length; i++) { | |||
let item = res.characteristics[i] | |||
if (item.uuid.indexOf('0000FFE1') != -1) { | |||
this.uuid1 = item.uuid //下发数据 | |||
} else if (item.uuid.indexOf('0000FFE2') != -1) { | |||
this.uuid2 = item.uuid //监听数据 | |||
} else if (item.uuid.indexOf('0000FFE3') != -1) { | |||
this.uuid3 = item.uuid //写入设置 | |||
} | |||
} | |||
// 打开监听 | |||
wx.notifyBLECharacteristicValueChange({ | |||
deviceId, | |||
serviceId, | |||
characteristicId: this.uuid2, | |||
state: true, | |||
}) | |||
wx.notifyBLECharacteristicValueChange({ | |||
deviceId, | |||
serviceId, | |||
characteristicId: this.uuid3, | |||
state: true, | |||
}) | |||
// 初始化插件 | |||
plugin.initPlugin(res.characteristics, this._device) | |||
wx.onBLECharacteristicValueChange((characteristic) => { | |||
// 解析特征值,返回解密后的数据 | |||
console.log("===" + plugin.ab2hex(characteristic.value)) | |||
let bleData = plugin.parseBleData(characteristic.value) | |||
console.log(bleData) | |||
if (bleData.status == 0) { | |||
console.log("握手成功") | |||
this._connLoading = false | |||
wx.hideLoading() | |||
wx.showToast({ | |||
title: '连接成功', | |||
}) | |||
} else if (bleData.status == 1) { | |||
console.log(bleData) | |||
let payload = bleData.data //对应协议中的payload数据,可以自行解析该数据 | |||
console.log(ab2hex(payload, ' ')) | |||
console.log(ab2hex(bleData.completeData, ' ')) | |||
// console.log(ab2hex(payload, ' ')) | |||
// console.log(ab2hex(bleData.completeData, ' ')) | |||
// 以体脂秤数据解析为例 | |||
let weight, adc; | |||
@@ -291,17 +350,17 @@ Page({ | |||
if (idx === -1) { | |||
data[`chs[${this.data.chs.length}]`] = { | |||
uuid: characteristic.characteristicId, | |||
value: ab2hex(payload, ' ') | |||
value: ab2hex(bleData.completeData, ' ') | |||
} | |||
} else { | |||
data[`chs[${idx}]`] = { | |||
uuid: characteristic.characteristicId, | |||
value: ab2hex(payload, ' ') | |||
value: ab2hex(bleData.completeData, ' ') | |||
} | |||
} | |||
this.setData(data) | |||
} else { | |||
console.log(bleData) | |||
// console.log(bleData) | |||
} | |||
}) | |||
}, | |||
@@ -319,7 +378,7 @@ Page({ | |||
value: buffer, | |||
success(res) { | |||
console.log('writeBLECharacteristicValue success', res) | |||
console.log(ab2hex(buffer)) | |||
console.log('下发指令==> ' + ab2hex(buffer)) | |||
} | |||
}) | |||
}, | |||
@@ -339,23 +398,25 @@ Page({ | |||
showWriteInput: false | |||
}) | |||
}, | |||
// 指令输入 | |||
writingCmd(e) { | |||
this._cmd = e.detail.value | |||
}, | |||
// 指令下发 | |||
submitCmd() { | |||
if (!this._cmd) { | |||
submitCmd(e, cmd) { | |||
let arr = [] | |||
let temp = [] | |||
if (!cmd) { | |||
cmd = this.data.cmd | |||
} | |||
console.log(cmd) | |||
if(!cmd || !this.data.connected){ | |||
return | |||
} | |||
let arr = [] | |||
if (this._cmd.indexOf(",") == -1) { | |||
arr = this._cmd.split(" ") | |||
if(cmd.indexOf(",") == -1){ | |||
temp = cmd.split(" ") | |||
} else { | |||
arr = this._cmd.split(",") | |||
temp = cmd.split(",") | |||
} | |||
for (let i in arr) { | |||
arr[i] = parseInt(arr[i], 16) | |||
let tempCmd = temp.join(' ') | |||
for(let i = 0; i < temp.length; i++){ | |||
arr[i] = parseInt(temp[i],16) | |||
} | |||
// let arr = [ | |||
// 0xA6, | |||
@@ -365,15 +426,54 @@ Page({ | |||
// 0x6A, | |||
// //A6 01 28 29 6A | |||
// ] | |||
if (arr[0] == 0xA6) { | |||
let payload = arr.slice(2, -2) | |||
if(arr[0] == 0xA6){ | |||
let len = arr[1] | |||
let payload = arr.slice(2, 2 + len) | |||
plugin.sendDataOfA6(payload) | |||
} else if (arr[0] == 0xA7) { | |||
let cid = [arr[1], arr[2]] // 001E | |||
let payload = arr.slice(4, -2) | |||
// let buff = new Uint8Array(arr).buffer | |||
// this.writeBLECharacteristicValue(buff, this.uuid3, this._deviceId, this._serviceId) | |||
} else if(arr[0] == 0xA7) { | |||
let len = arr[3] | |||
let payload = arr.slice(4, 4 + len) | |||
plugin.sendDataOfA7(payload) | |||
// this.writeBLECharacteristicValue(buff, this.uuid1, this._deviceId, this._serviceId) | |||
} | |||
return | |||
let historyList = wx.getStorageSync('historyList') || [] | |||
let idx = historyList.findIndex(item => item.cmd == tempCmd) | |||
if (idx < 0) { | |||
historyList.push({cmd: tempCmd}) | |||
} | |||
this.setData({ | |||
historyList | |||
}) | |||
wx.setStorage({ | |||
data: historyList, | |||
key: 'historyList', | |||
}) | |||
wx.showToast({ | |||
title: '已发送', | |||
icon: 'none' | |||
}) | |||
}, | |||
history_delete(e) { | |||
let index = e.currentTarget.dataset.index | |||
this.data.historyList.splice(index, 1) | |||
this.setData({ | |||
historyList: this.data.historyList | |||
}) | |||
wx.setStorage({ | |||
data: this.data.historyList, | |||
key: 'historyList', | |||
}) | |||
}, | |||
history_copy(e) { | |||
let index = e.currentTarget.dataset.index | |||
wx.setClipboardData({ | |||
data: this.data.historyList[index].cmd | |||
}) | |||
}, | |||
history_send(e) { | |||
let index = e.currentTarget.dataset.index | |||
let cmd = this.data.historyList[index].cmd | |||
this.submitCmd(null, cmd) | |||
}, | |||
}); |
@@ -8,49 +8,65 @@ module.exports.len = function(arr) { | |||
} | |||
</wxs> | |||
<button bindtap="openBluetoothAdapter">开始扫描</button> | |||
<button bindtap="stopBluetoothDevicesDiscovery">停止扫描</button> | |||
<button bindtap="closeBluetoothAdapter">结束流程</button> | |||
<view class="devices_summary">已发现 {{devices.length}} 个外围设备:</view> | |||
<scroll-view class="device_list" scroll-y scroll-with-animation> | |||
<view wx:for="{{devices}}" wx:key="index" | |||
data-device-id="{{item.deviceId}}" | |||
data-name="{{item.name || item.localName}}" | |||
data-mac="{{item.mac}}" | |||
data-index="{{index}}" | |||
bindtap="createBLEConnection" | |||
class="device_item" | |||
hover-class="device_item_hover"> | |||
<view style="font-size: 16px; color: #333;">{{item.name}}</view> | |||
<view style="font-size: 10px">信号强度: {{item.RSSI}}dBm ({{utils.max(0, item.RSSI + 100)}}%)</view> | |||
<view style="font-size: 10px">mac地址: {{item.deviceId}}</view> | |||
<!-- <view style="font-size: 10px">Service数量: {{utils.len(item.advertisServiceUUIDs)}}</view> --> | |||
<view style="font-size: 14px">广播数据:{{item.analyzeDataText}}</view> | |||
<view class="container"> | |||
<view class="header"> | |||
<button bindtap="openBluetoothAdapter" style="font-size: 32rpx;line-height:100rpx;width: 100%;">开始扫描</button> | |||
<button bindtap="stopBluetoothDevicesDiscovery" style="font-size: 32rpx;line-height:100rpx;width: 100%;">停止扫描</button> | |||
<button bindtap="closeBluetoothAdapter" style="font-size: 32rpx;line-height:100rpx;width: 100%;">结束流程</button> | |||
</view> | |||
</scroll-view> | |||
<view class="connected_info" wx:if="{{connected}}"> | |||
<view> | |||
<text>已连接到 {{name}}</text> | |||
<view class="operation"> | |||
<!-- <button wx:if="{{canWrite}}" size="mini" bindtap="writeBLECharacteristicValue">写数据</button> --> | |||
<button size="mini" bindtap="showWriteInputView">写入指令</button> | |||
<button size="mini" bindtap="closeBLEConnection">断开连接</button> | |||
<view class="devices_summary">已发现 {{devices.length}} 个外围设备:</view> | |||
<scroll-view class="device_list" scroll-y scroll-with-animation> | |||
<view wx:for="{{devices}}" wx:key="index" | |||
data-device-id="{{item.deviceId}}" | |||
data-name="{{item.name || item.localName}}" | |||
data-mac="{{item.mac}}" | |||
data-index="{{index}}" | |||
bindtap="createBLEConnection" | |||
class="device_item" | |||
hover-class="device_item_hover"> | |||
<view style="font-size: 32rpx;"> | |||
<text style="color:#000;font-weight:bold">{{item.name}}</text> | |||
<text style="font-size:26rpx">(信号强度: {{item.RSSI}}dBm)</text> | |||
</view> | |||
<view style="font-size: 26rpx">mac地址: {{item.macAddr || item.deviceId}}</view> | |||
<!-- <view style="font-size: 10px">Service数量: {{utils.len(item.advertisServiceUUIDs)}}</view> --> | |||
<view style="font-size: 26rpx">广播数据:{{item.analyzeDataText}}</view> | |||
</view> | |||
</scroll-view> | |||
<view class="connected_info" wx:if="{{connected}}"> | |||
<view> | |||
<text>已连接到 {{name}}</text> | |||
<view class="operation"> | |||
<!-- <button wx:if="{{canWrite}}" size="mini" bindtap="writeBLECharacteristicValue">写数据</button> --> | |||
<button size="mini" bindtap="showWriteInputView" style="line-height:54rpx; font-size:28rpx;padding:0 20rpx;margin-right:10rpx">写入指令</button> | |||
<button size="mini" bindtap="closeBLEConnection" style="line-height:54rpx; font-size:28rpx;padding:0 20rpx;">断开连接</button> | |||
</view> | |||
</view> | |||
<view wx:for="{{chs}}" wx:key="index" style="font-size: 26rpx; margin-top: 20rpx;"> | |||
<view>特性值: {{item.value}}</view> | |||
</view> | |||
</view> | |||
<view wx:for="{{chs}}" wx:key="index" style="font-size: 12px; margin-top: 10px;"> | |||
<view>特性值: {{item.value}}</view> | |||
</view> | |||
</view> | |||
<view class="writeInputViewBg" wx:if="{{showWriteInput}}"></view> | |||
<view class="writeInputView" wx:if="{{showWriteInput}}"> | |||
<view class="writeInput"> | |||
<input bindinput="writingCmd"></input> | |||
</view> | |||
<view class="hint">请输入16进制数,Byte之间用空格或英文逗号分隔</view> | |||
<view class="btns"> | |||
<button size="mini" bindtap="submitCmd">确定</button> | |||
<button size="mini" bindtap="hideWriteInputView">取消</button> | |||
<view class="writeInputView" wx:if="{{showWriteInput}}"> | |||
<view class="writeInput"> | |||
<input type="text" model:value="{{cmd}}"></input> | |||
</view> | |||
<view class="hint">请输入16进制数,Byte之间用空格或英文逗号分隔</view> | |||
<view class="btns"> | |||
<button bindtap="submitCmd" style="line-height:70rpx; font-size:30rpx;padding:0 50rpx;margin-right:30rpx;">发送</button> | |||
<button bindtap="hideWriteInputView" style="line-height:70rpx; font-size:30rpx;padding:0 50rpx;">取消</button> | |||
</view> | |||
<view class="history"> | |||
<view class="history_title">历史记录:</view> | |||
<scroll-view class="history_wrap" scroll-y="true"> | |||
<view class="history_item" wx:for="{{historyList}}" wx:key="index"> | |||
<view class="history_cmd">{{item.cmd}}</view> | |||
<view class="history_btns"> | |||
<button size="mini" bindtap="history_send" data-index="{{index}}" style="line-height:54rpx; font-size:24rpx;padding:0 30rpx;">发送</button> | |||
<button size="mini" bindtap="history_delete" data-index="{{index}}" style="line-height:54rpx; font-size:24rpx;padding:0 30rpx;margin-left:20rpx">删除</button> | |||
</view> | |||
</view> | |||
</scroll-view> | |||
</view> | |||
</view> | |||
</view> |
@@ -1,23 +1,60 @@ | |||
page { | |||
background: #f0f0f0; | |||
color: #333; | |||
--safe-bottom: env(safe-area-inset-bottom); | |||
} | |||
view, | |||
cover-view, | |||
scroll-view, | |||
swiper, | |||
swiper-item, | |||
movable-area, | |||
movable-view, | |||
button, | |||
input, | |||
textarea, | |||
label, | |||
navigator | |||
{ | |||
box-sizing: border-box; | |||
} | |||
.container{ | |||
position: relative; | |||
display: flex; | |||
flex-direction: column; | |||
align-items: center; | |||
width: 100vw; | |||
height: 100vh; | |||
padding-bottom: 200rpx; | |||
} | |||
.header{ | |||
width: 100%; | |||
} | |||
.container button{ | |||
padding-top: 0; | |||
padding-bottom: 0; | |||
border: 1px solid #ddd; | |||
} | |||
.devices_summary { | |||
margin-top: 30px; | |||
padding: 10px; | |||
font-size: 16px; | |||
width: 100%; | |||
line-height: 70rpx; | |||
padding: 0 30rpx; | |||
margin-top: 30rpx; | |||
font-size: 32rpx; | |||
} | |||
.device_list { | |||
height: 300px; | |||
margin: 50px 5px; | |||
flex: 1; | |||
width: 100%; | |||
height: calc(100vh - 600rpx); | |||
margin-top: 0; | |||
border: 1px solid #EEE; | |||
border: 1px solid #fff; | |||
border-radius: 5px; | |||
width: auto; | |||
background: #fff; | |||
} | |||
.device_item { | |||
line-height: 1.5; | |||
padding: 10rpx 30rpx; | |||
border-bottom: 1px solid #EEE; | |||
padding: 10px; | |||
color: #666; | |||
} | |||
.device_item_hover { | |||
background-color: rgba(0, 0, 0, .1); | |||
@@ -26,18 +63,17 @@ page { | |||
position: fixed; | |||
bottom: 0; | |||
width: 100%; | |||
background-color: #F0F0F0; | |||
height: 200rpx; | |||
padding: 10px; | |||
padding-bottom: 20px; | |||
margin-bottom: env(safe-area-inset-bottom); | |||
border-top: 1px solid #ddd; | |||
background-color: #fff; | |||
font-size: 14px; | |||
min-height: 100px; | |||
box-shadow: 0px 0px 3px 0px; | |||
box-shadow: 0px -4rpx 4rpx 0px rgba(0,0,0,.05); | |||
} | |||
.connected_info .operation { | |||
position: absolute; | |||
display: inline-block; | |||
right: 30px; | |||
right: 30rpx; | |||
} | |||
.writeInputViewBg{ | |||
position: fixed; | |||
@@ -50,27 +86,69 @@ page { | |||
} | |||
.writeInputView{ | |||
position: fixed; | |||
top: 30%; | |||
left: 50%; | |||
bottom: 200rpx; | |||
left: 0; | |||
z-index: 1001; | |||
background-color: #fff; | |||
width: 80vw; | |||
height: 300rpx; | |||
display: flex; | |||
flex-direction: column; | |||
align-items: center; | |||
transform: translateX(-50%); | |||
width: 100%; | |||
height: calc(100vh - 500rpx); | |||
background-color: #fff; | |||
border-top: 1px solid #ddd; | |||
} | |||
.writeInput{ | |||
margin-top: 60rpx; | |||
width: 90%; | |||
.history{ | |||
position: relative; | |||
flex: 1; | |||
width: 690rpx; | |||
padding-top: 60rpx; | |||
border: 1px solid #ddd; | |||
margin: 30rpx; | |||
} | |||
.history_title{ | |||
position: absolute; | |||
top: 0; | |||
left: 0; | |||
padding: 0 30rpx; | |||
line-height: 60rpx; | |||
font-size: 30rpx; | |||
} | |||
.history_wrap{ | |||
width: 100%; | |||
height: 100%; | |||
} | |||
.history_item{ | |||
display: flex; | |||
align-items: center; | |||
justify-content: space-between; | |||
width: 100%; | |||
height: 72rpx; | |||
padding: 0 30rpx; | |||
border-top: 1px solid #ddd; | |||
} | |||
.history_cmd{ | |||
flex: 1; | |||
overflow: hidden; | |||
text-overflow: ellipsis; | |||
white-space: nowrap; | |||
font-size: 24rpx; | |||
} | |||
.history_btns{ | |||
height: 60rpx; | |||
overflow: hidden; | |||
} | |||
.writeInput{ | |||
margin-top: 30rpx; | |||
width: 690rpx; | |||
height: 80rpx; | |||
border: 1rpx solid #ccc; | |||
} | |||
.writeInput input{ | |||
width: 100%; | |||
height: 100%; | |||
box-sizing: border-box; | |||
padding: 10rpx; | |||
font-size: 32rpx; | |||
} | |||
.hint{ | |||
padding-top: 10rpx; | |||
@@ -82,5 +160,6 @@ page { | |||
align-items: center; | |||
justify-content: space-between; | |||
width: 60%; | |||
margin-top: 50rpx; | |||
padding-bottom: 30rpx; | |||
margin-top: 30rpx; | |||
} |