├── README.md ├── app.js ├── app.json ├── app.wxss ├── pages ├── index │ ├── index.js │ ├── index.wxml │ └── index.wxss ├── logs │ ├── logs.js │ ├── logs.json │ ├── logs.wxml │ └── logs.wxss └── scan │ ├── scan.js │ ├── scan.json │ ├── scan.wxml │ └── scan.wxss ├── project.config.json └── utils └── util.js /README.md: -------------------------------------------------------------------------------- 1 | # wechat-ble-demo 2 | 微信小程序蓝牙测试 3 | 4 | 相关说明文章:http://blog.csdn.net/lablenet/article/details/78492824 5 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | //app.js 2 | App({ 3 | onLaunch: function () { 4 | // 展示本地存储能力 5 | var logs = wx.getStorageSync('logs') || [] 6 | logs.unshift(Date.now()) 7 | wx.setStorageSync('logs', logs) 8 | 9 | // 登录 10 | wx.login({ 11 | success: res => { 12 | // 发送 res.code 到后台换取 openId, sessionKey, unionId 13 | } 14 | }) 15 | // 获取用户信息 16 | // wx.getSetting({ 17 | // success: res => { 18 | // if (res.authSetting['scope.userInfo']) { 19 | // // 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框 20 | // wx.getUserInfo({ 21 | // success: res => { 22 | // // 可以将 res 发送给后台解码出 unionId 23 | // this.globalData.userInfo = res.userInfo 24 | 25 | // // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回 26 | // // 所以此处加入 callback 以防止这种情况 27 | // if (this.userInfoReadyCallback) { 28 | // this.userInfoReadyCallback(res) 29 | // } 30 | // } 31 | // }) 32 | // } 33 | // } 34 | // }) 35 | }, 36 | globalData: { 37 | userInfo: null 38 | } 39 | }) -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | "pages/scan/scan", 4 | "pages/index/index", 5 | "pages/logs/logs" 6 | ], 7 | "window": { 8 | "backgroundTextStyle": "light", 9 | "navigationBarBackgroundColor": "#fff", 10 | "navigationBarTitleText": "WeChat", 11 | "navigationBarTextStyle": "black" 12 | } 13 | } -------------------------------------------------------------------------------- /app.wxss: -------------------------------------------------------------------------------- 1 | /**app.wxss**/ 2 | .container { 3 | height: 100%; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: space-between; 8 | padding: 200rpx 0; 9 | box-sizing: border-box; 10 | } 11 | -------------------------------------------------------------------------------- /pages/index/index.js: -------------------------------------------------------------------------------- 1 | //index.js 2 | //获取应用实例 3 | const app = getApp() 4 | 5 | Page({ 6 | data: { 7 | motto: 'Hello World', 8 | userInfo: {}, 9 | hasUserInfo: false, 10 | canIUse: wx.canIUse('button.open-type.getUserInfo') 11 | }, 12 | //事件处理函数 13 | bindViewTap: function() { 14 | wx.navigateTo({ 15 | url: '../logs/logs' 16 | }) 17 | }, 18 | onLoad: function () { 19 | if (app.globalData.userInfo) { 20 | this.setData({ 21 | userInfo: app.globalData.userInfo, 22 | hasUserInfo: true 23 | }) 24 | } else if (this.data.canIUse){ 25 | // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回 26 | // 所以此处加入 callback 以防止这种情况 27 | app.userInfoReadyCallback = res => { 28 | this.setData({ 29 | userInfo: res.userInfo, 30 | hasUserInfo: true 31 | }) 32 | } 33 | } else { 34 | // 在没有 open-type=getUserInfo 版本的兼容处理 35 | wx.getUserInfo({ 36 | success: res => { 37 | app.globalData.userInfo = res.userInfo 38 | this.setData({ 39 | userInfo: res.userInfo, 40 | hasUserInfo: true 41 | }) 42 | } 43 | }) 44 | } 45 | }, 46 | getUserInfo: function(e) { 47 | console.log(e) 48 | app.globalData.userInfo = e.detail.userInfo 49 | this.setData({ 50 | userInfo: e.detail.userInfo, 51 | hasUserInfo: true 52 | }) 53 | } 54 | }) 55 | -------------------------------------------------------------------------------- /pages/index/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{userInfo.nickName}} 8 | 9 | 10 | 11 | 12 | {{motto}} 13 | 14 | 15 | -------------------------------------------------------------------------------- /pages/index/index.wxss: -------------------------------------------------------------------------------- 1 | /**index.wxss**/ 2 | .userinfo { 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | } 7 | 8 | .userinfo-avatar { 9 | width: 128rpx; 10 | height: 128rpx; 11 | margin: 20rpx; 12 | border-radius: 50%; 13 | } 14 | 15 | .userinfo-nickname { 16 | color: #aaa; 17 | } 18 | 19 | .usermotto { 20 | margin-top: 200px; 21 | } -------------------------------------------------------------------------------- /pages/logs/logs.js: -------------------------------------------------------------------------------- 1 | //logs.js 2 | const util = require('../../utils/util.js') 3 | 4 | Page({ 5 | data: { 6 | logs: [] 7 | }, 8 | onLoad: function () { 9 | this.setData({ 10 | logs: (wx.getStorageSync('logs') || []).map(log => { 11 | return util.formatTime(new Date(log)) 12 | }) 13 | }) 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /pages/logs/logs.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "查看启动日志" 3 | } -------------------------------------------------------------------------------- /pages/logs/logs.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{index + 1}}. {{log}} 5 | 6 | 7 | -------------------------------------------------------------------------------- /pages/logs/logs.wxss: -------------------------------------------------------------------------------- 1 | .log-list { 2 | display: flex; 3 | flex-direction: column; 4 | padding: 40rpx; 5 | } 6 | .log-item { 7 | margin: 10rpx; 8 | } 9 | -------------------------------------------------------------------------------- /pages/scan/scan.js: -------------------------------------------------------------------------------- 1 | // pages/scan/scan.js 2 | Page({ 3 | 4 | /** 5 | * 页面的初始数据 6 | */ 7 | data: { 8 | bleStatus:"蓝牙未打开", 9 | bleAdapterStatus:"未初始化", 10 | bleChipInfo:{}, 11 | bleChips:[], 12 | bleConnSuccess:false, 13 | bleNotifyData:"未读取数据" 14 | }, 15 | 16 | /** 17 | * 开始扫描 18 | */ 19 | onScanClick:function(event){ 20 | console.log('扫描开始') 21 | let self = this 22 | wx.openBluetoothAdapter({ 23 | success: function(res) { 24 | // 扫描蓝牙 25 | self.bleDisCovery() 26 | self.setData({ 27 | bleAdapterStatus:"初始化成功" 28 | }) 29 | }, 30 | fail:function(error){ 31 | self.setData({ 32 | bleAdapterStatus: "初始化失败" 33 | }) 34 | wx.showModal({ 35 | showCancel: false, 36 | title: '提示', 37 | content: '设备蓝牙未打开,请打开蓝牙功能', 38 | success: function (res) { 39 | if (res.confirm) { 40 | //console.log('用户点击确定') 41 | } 42 | } 43 | }); 44 | }, 45 | complete:function(){ 46 | //console.log('complete') 47 | } 48 | }); 49 | }, 50 | /** 51 | * 解析数据信息 52 | */ 53 | bleFound:function(){ 54 | console.log("发现设备信息") 55 | let self =this 56 | wx.onBluetoothDeviceFound(function (res) { 57 | let devices = res.devices 58 | console.log(devices) 59 | let length = self.data.bleChips.length 60 | let devicesLength = devices.length 61 | if (devicesLength > length){ 62 | self.data.bleChips = devices 63 | self.setData({ 64 | bleChips: devices 65 | }); 66 | } 67 | console.log(self.data.bleChips) 68 | }); 69 | 70 | }, 71 | /** 72 | * 扫描设备 73 | */ 74 | bleDisCovery:function(){ 75 | console.log("扫描蓝牙") 76 | let self = this 77 | wx.startBluetoothDevicesDiscovery({ 78 | interval:1000, 79 | success: function(res){ 80 | self.bleFound(); 81 | } 82 | }); 83 | }, 84 | /** 85 | * 初始化蓝牙 86 | */ 87 | bleInit:function(){ 88 | console.log('初始化蓝牙') 89 | let self = this 90 | wx.openBluetoothAdapter({ 91 | success: function(res) { 92 | self.setData({ 93 | bleAdapterStatus: "初始化成功" 94 | }) 95 | }, 96 | fail:function(msg){ 97 | self.setData({ 98 | bleAdapterStatus: "初始化失败" 99 | }) 100 | wx.showModal({ 101 | showCancel:false, 102 | title: '提示', 103 | content: '设备蓝牙未打开,请打开蓝牙功能', 104 | success:function(res){ 105 | if (res.confirm) { 106 | //console.log('用户点击确定') 107 | // 退出小程序 108 | } 109 | } 110 | }); 111 | } 112 | }); 113 | }, 114 | /** 115 | * 蓝牙设备监听 116 | */ 117 | bleStatusListener:function(){ 118 | // 监听蓝牙状态 119 | let slef =this 120 | wx.onBluetoothAdapterStateChange(function (res) { 121 | console.log(`adapterState changed, now is`, res) 122 | if (res.available){ 123 | // 是否可用 124 | console.log("蓝牙状态以改变!") 125 | slef.setData({ 126 | bleStatus: "蓝牙已打开" 127 | }); 128 | }else{ 129 | slef.setData({ 130 | bleStatus: "蓝牙已关闭" 131 | }); 132 | // 不可用时 133 | wx.showModal({ 134 | showCancel: false, 135 | title: '提示', 136 | content: '设备蓝牙未打开,请打开蓝牙功能', 137 | success: function (res) { 138 | if (res.confirm) { 139 | // console.log('用户点击确定') 140 | // 退出小程序 141 | } 142 | } 143 | }); 144 | } 145 | }); 146 | }, 147 | onConnBle:function(e){ 148 | // 停止扫描 149 | wx.stopBluetoothDevicesDiscovery({ 150 | success: function(res) { 151 | }, 152 | }); 153 | // 接收点击事件的参数 154 | let device = e.currentTarget.dataset.item 155 | console.log(`conn ble >> ${device}`) 156 | this.setData({ 157 | bleChipInfo: device 158 | }) 159 | let deviceId = device.deviceId 160 | let self = this 161 | // 连接设备 162 | console.log("连接设备中...") 163 | wx.createBLEConnection({ 164 | deviceId: deviceId, 165 | success: function(res) { 166 | wx.showToast({ 167 | title: '连接成功', 168 | }); 169 | // 连接成功,打开 notify 170 | setTimeout(function(){ 171 | self.bleServices(deviceId) 172 | },1500) 173 | 174 | }, 175 | fail:function(errMsg){ 176 | wx.showToast({ 177 | title: `连接失败:${errMsg}`, 178 | }) 179 | } 180 | }); 181 | }, 182 | bleServices: function (deviceId){ 183 | let self = this 184 | wx.getBLEDeviceServices({ 185 | deviceId: deviceId, 186 | success: function (res) { 187 | wx.showToast({ 188 | title: 'service success', 189 | }) 190 | let services = res.services 191 | for(let index in services){ 192 | let service= services[index] 193 | console.log(service) 194 | if (service.uuid === '49535343-FE7D-4AE5-8FA9-9FAFD205E455'){ 195 | console.log("have service: 49535343-FE7D-4AE5-8FA9-9FAFD205E455") 196 | self.bleServiceChart(deviceId, service.uuid) 197 | } 198 | } 199 | console.log('device services:', res.services) 200 | } 201 | }) 202 | }, 203 | bleServiceChart: function (deviceId,serviceId){ 204 | let self = this; 205 | wx.getBLEDeviceCharacteristics({ 206 | // 这里的 deviceId 需要在上面的 getBluetoothDevices 或 onBluetoothDeviceFound 接口中获取 207 | deviceId: deviceId, 208 | // 这里的 serviceId 需要在上面的 getBLEDeviceServices 接口中获取 209 | serviceId: serviceId, 210 | success: function (res) { 211 | console.log('device getBLEDeviceCharacteristics:', res.characteristics) 212 | let characteristics = res.characteristics 213 | for (let index in characteristics){ 214 | let characteristic = characteristics[index] 215 | if (characteristic.uuid === '49535343-1E4D-4BD9-BA61-23C647249616'){ 216 | console.log("have characteristic: 49535343-1E4D-4BD9-BA61-23C647249616") 217 | } 218 | console.log(characteristic) 219 | } 220 | self.openNotify(deviceId) 221 | } 222 | }) 223 | }, 224 | openNotify: function (deviceId) { 225 | this.setData({ 226 | bleConnSuccess: true 227 | }); 228 | let self = this 229 | wx.notifyBLECharacteristicValueChange({ 230 | deviceId: deviceId, 231 | serviceId: '49535343-FE7D-4AE5-8FA9-9FAFD205E455', 232 | characteristicId: '49535343-1E4D-4BD9-BA61-23C647249616', 233 | state: true, 234 | success: function (res) { 235 | console.log('notify success') 236 | self.onNotifyChange() 237 | wx.showToast({ 238 | title: 'notify success', 239 | }); 240 | }, 241 | fail: function (err) { 242 | console.log(err) 243 | wx.showToast({ 244 | title: 'notify fail', 245 | }); 246 | } 247 | }); 248 | }, 249 | onNotifyChange:function(){ 250 | // 接收数据 251 | let self = this 252 | wx.onBLECharacteristicValueChange(function (res) { 253 | console.log(res.characteristicId) 254 | let byteDatas = Array.from(new Int8Array(res.value)) 255 | console.log(byteDatas) 256 | const data = byteDatas.join(',') 257 | self.setData({ 258 | bleNotifyData:data 259 | }); 260 | console.log(data) 261 | }); 262 | }, 263 | 264 | /** 265 | * 生命周期函数--监听页面加载 266 | */ 267 | onLoad: function (options) { 268 | if (wx.openBluetoothAdapter) { 269 | wx.openBluetoothAdapter() 270 | } else { 271 | wx.showModal({ 272 | title: '提示', 273 | content: '当前微信版本过低,无法使用该功能,请升级到最新微信版本后重试。' 274 | }) 275 | } 276 | }, 277 | 278 | 279 | /** 280 | * 生命周期函数--监听页面初次渲染完成 281 | */ 282 | onReady: function () { 283 | // 监听蓝牙 284 | this.bleStatusListener() 285 | }, 286 | 287 | /** 288 | * 生命周期函数--监听页面显示 289 | */ 290 | onShow: function () { 291 | // 初始化蓝牙 292 | this.bleInit() 293 | }, 294 | 295 | /** 296 | * 生命周期函数--监听页面隐藏 297 | */ 298 | onHide: function () { 299 | 300 | }, 301 | 302 | /** 303 | * 生命周期函数--监听页面卸载 304 | */ 305 | onUnload: function () { 306 | wx.closeBluetoothAdapter({ 307 | success: function(res) { 308 | 309 | }, 310 | }); 311 | }, 312 | 313 | /** 314 | * 页面相关事件处理函数--监听用户下拉动作 315 | */ 316 | onPullDownRefresh: function () { 317 | 318 | }, 319 | 320 | /** 321 | * 页面上拉触底事件的处理函数 322 | */ 323 | onReachBottom: function () { 324 | 325 | }, 326 | 327 | /** 328 | * 用户点击右上角分享 329 | */ 330 | onShareAppMessage: function () { 331 | 332 | } 333 | }) -------------------------------------------------------------------------------- /pages/scan/scan.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "蓝牙测试" 3 | } -------------------------------------------------------------------------------- /pages/scan/scan.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 蓝牙状态监听:{{bleStatus}} 6 | 蓝牙初始化:{{bleAdapterStatus}} 7 | 当前连接的设备:{{bleChipInfo.name}}-{{bleChipInfo.deviceId}} 8 | 9 | 10 | 11 | 扫描结果 12 | 13 | 14 | {{index}}-{{item.name}}-{{item.deviceId}} 15 | 16 | 17 | 18 | 19 | notify data: 20 | {{bleNotifyData}} 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /pages/scan/scan.wxss: -------------------------------------------------------------------------------- 1 | /* pages/scan/scan.wxss */ 2 | .scan_view{ 3 | margin: 16px; 4 | } 5 | .result{ 6 | padding: 16px; 7 | direction: flex; 8 | flex-direction: column; 9 | background-color: gainsboro; 10 | box-shadow: 0 3px 3px 0 gainsboro; 11 | height:100px; 12 | width: 90%; 13 | flex-wrap:nowrap; 14 | } 15 | 16 | .result_text{ 17 | width: 100%; 18 | height: 20px; 19 | color: black; 20 | font-size: 14px; 21 | display: block; 22 | } 23 | 24 | .btn_scan{ 25 | margin-top: 16px 26 | } 27 | 28 | .scan_result_title{ 29 | color: black; 30 | font-size: 14px; 31 | display: block; 32 | margin-top: 16px; 33 | } 34 | .scan_result{ 35 | display: flex; 36 | flex-direction: row; 37 | padding: 16px; 38 | background-color: gainsboro; 39 | height: 50px; 40 | margin-top: 8px; 41 | } 42 | 43 | .notify_result{ 44 | display: flex; 45 | flex-direction: column; 46 | background-color: gainsboro; 47 | box-shadow: 0 3px 3px 0 gainsboro; 48 | margin: 16px; 49 | padding: 16px; 50 | } 51 | 52 | .notify_result_data{ 53 | font-size: 12px; 54 | display: block; 55 | } -------------------------------------------------------------------------------- /project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "项目配置文件。", 3 | "setting": { 4 | "urlCheck": true, 5 | "es6": true, 6 | "postcss": true, 7 | "minified": true, 8 | "newFeature": true 9 | }, 10 | "compileType": "miniprogram", 11 | "libVersion": "1.6.4", 12 | "appid": "wx3c5fc86bd5706314", 13 | "projectname": "%E7%9D%BF%E7%95%9C%E4%BF%9DDev", 14 | "condition": { 15 | "search": { 16 | "current": -1, 17 | "list": [] 18 | }, 19 | "conversation": { 20 | "current": -1, 21 | "list": [] 22 | }, 23 | "miniprogram": { 24 | "current": -1, 25 | "list": [] 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /utils/util.js: -------------------------------------------------------------------------------- 1 | const formatTime = date => { 2 | const year = date.getFullYear() 3 | const month = date.getMonth() + 1 4 | const day = date.getDate() 5 | const hour = date.getHours() 6 | const minute = date.getMinutes() 7 | const second = date.getSeconds() 8 | 9 | return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':') 10 | } 11 | 12 | const formatNumber = n => { 13 | n = n.toString() 14 | return n[1] ? n : '0' + n 15 | } 16 | 17 | module.exports = { 18 | formatTime: formatTime 19 | } 20 | --------------------------------------------------------------------------------