import ble_store from "../store/bluetooth"; import game_store from "../store/game"; import Toast from "../../static/vant/toast/toast"; import Notify from "../../static/vant/notify/notify"; import {LOG_DEBUG, LOG_WECHAT, LOG_WECHAT_ERROR} from "./log"; function getDeviceWriteInfo() { let deviceId = ble_store.getters.getDeviceId(); let serviceId = ble_store.getters.getServiceId(); let characteristicWriteId = ble_store.getters.getCharacteristicWriteId(); let characteristicNotifyId = ble_store.getters.getCharacteristicNotifyId(); return { deviceId, serviceId, characteristicWriteId, characteristicNotifyId}; } /** * 判断一代脑机 */ function isJELLYFISH() { let $code = ble_store.getters.getDeviceSn(); return $code.indexOf("JELLYFISH") >= 0; } /** * ArrayBuffer转16进度字符串示例 * @param buffer * @returns {string} */ function ab2hex(buffer) { const hexArr = Array.prototype.map.call( new Uint8Array(buffer), function (bit) { return ("00" + bit.toString(16)).slice(-2); } ); return hexArr.join(""); } /** * todo 解析hex * @param hexStr * @param count * @param start * @returns {string} */ function doAnalysis(hexStr, count, start = 6) { let $result = ""; //从下标6开始往后截取 let $str = hexStr.substring(start); let $data = $str.substring(0, count * 2); for (let $i = 0; $i < $data.length; $i += 2) { let $code = parseInt($data.substring($i, $i+2), 16) $result += String.fromCharCode($code) } return $result; } export default { /** * 获取蓝牙设备服务 * @param deviceId 脑机mac */ getBLEDeviceServices(deviceId) { const that = this; wx.getBLEDeviceServices({ deviceId, success: (res) => { LOG_DEBUG("获取蓝牙设备服务service:\n", JSON.stringify(res.services)); for (let i = 0; i < res.services.length; i++) { LOG_DEBUG("第" + (i + 1) + "个UUID:" + res.services[i].uuid + "\n"); if (res.services[i].uuid.indexOf('6E') !== -1 || res.services[i].uuid.indexOf('0000FFF0') !== -1) { // 获取蓝牙设备某个服务中所有特征值 that.getBLEDeviceCharacteristics(deviceId, res.services[i].uuid); ble_store.setters.setServiceId(res.services[i].uuid); LOG_DEBUG("脑机deviceId(mac)", deviceId, "notifyServicesId:" + res.services[i].uuid); return; } } }, fail() { let deviceId = wx.getStorageSync('deviceId'); //断开蓝牙连接 wx.closeBLEConnection({ deviceId: deviceId }); }, }); }, /** * 获取蓝牙设备某个服务中所有特征值 */ getBLEDeviceCharacteristics(deviceId, serviceId) { const that = this; wx.getBLEDeviceCharacteristics({ deviceId, serviceId, success: (res) => { LOG_DEBUG("获取服务", serviceId, "的特征值:\n", JSON.stringify(res)); for (let i = 0; i < res.characteristics.length; i++) { let item = res.characteristics[i] if (item.properties.read) { LOG_DEBUG("第" + (i + 1) + ",该特征值可读:" + item.uuid); } if (item.properties.write) { LOG_DEBUG("第" + (i + 1) + ",该特征值可写:" + item.uuid); if(item.uuid.indexOf("0002") !== -1){ ble_store.setters.setCharacteristicWriteId(item.uuid); //打开数据帧 (打开大包数据) that.SendOrder("ff", "打开数据帧"); } } if (item.properties.notify || item.properties.indicate) { LOG_DEBUG("第" + (i + 1) + ",该特征值可监听:" + item.uuid); if(item.uuid.indexOf("0003") !== -1){ ble_store.setters.setCharacteristicNotifyId(item.uuid); } // 启用蓝牙低功耗设备特征值变化时的 notify 功能,订阅特征。 wx.notifyBLECharacteristicValueChange({ deviceId: deviceId, serviceId: serviceId, characteristicId: item.uuid, state: true, success() { that.notifyDatas(null); LOG_DEBUG("init正在监听特征值:", item.uuid); } }); } } }, fail() { let deviceId = wx.getStorageSync('deviceId'); //断开蓝牙连接 wx.closeBLEConnection({ deviceId: deviceId }); }, }); }, /** * 脑机连接教具 */ sendToyConnection(toyItem) { let that = this; if(toyItem && toyItem["hex"]) { let $hex = toyItem["hex"].substr(toyItem["hex"].length - 2, 2); if ($hex === "80") { wx.setStorageSync("report_mode", 2) } else { wx.setStorageSync("report_mode", 1) } LOG_DEBUG("连接教具(获取连接ID):", `03 00 ${$hex} 00 0A`, JSON.stringify(toyItem)); // 连接教具: 03 00 ${$hex} 00 0a that.sendConnectOneToMore($hex); } }, /** * 发送一对多连接 * 连接教具(获取连接ID) */ sendConnectOneToMore(id) { this.WriteBufferInBle(`03 00 ${id} 00 0A`, "一对多教具连接"); }, /** * 发送一对一连接 * 连接教具(使用获取的ID) */ sendConnectOneToOne(id) { ble_store.setters.setCurrentToyId(id); this.WriteBufferInBle(`03 00 ${id} 01 0A`, "连接教具(使用获取的ID)") }, /** * 连接教具(使用下发的ID) */ sendConnectOneToToy(id) { ble_store.setters.setCurrentToyId(id); this.WriteBufferInBle(`03 00 ${id} 02 0A`, "连接教具(使用下发的ID)") }, /** * 写入8位指令 * @param id 末尾id * @param $comments * @constructor */ SendOrder(id, $comments = "") { let $hexStr = `03 00 00 00 ${id}`; this.WriteBufferInBle($hexStr, $comments) }, /** * 打开或关闭LED灯 AA CC 03 00 00 ctrl EC CKS * 00/01 */ SendLedOrder(id) { let $hexStr = `03 00 00 ${id} ec`; this.WriteBufferInBle($hexStr, "控制脑机LED灯") }, /** * 设置教具为无运动状态 AA CC 03 00 00 00 34 CKS * 00/01 */ SendMotionOrder(id) { let $hexStr = `03 00 00 ${id} 34`; this.WriteBufferInBle($hexStr, "设置教具为无运动状态") }, /** * 自动发送RF重连教具 * @param {Boolean} isOn 是否打开重连功能 * @param timeOut 有效时间 * AA CC 03 00 01 0a d0 21 */ sendAutoConnectRf(isOn, timeOut) { let onVal = isOn ? '01' : '00'; let mTimeOut = timeOut.toString(16); if (mTimeOut.length === 1) { mTimeOut = `0${mTimeOut}`; } let $hexStr = `03 00 ${onVal} ${mTimeOut} d0`; this.WriteBufferInBle($hexStr, "RF重连"); }, /** * 开启脑控 */ sendControl() { let that = this; wx.showLoading({ title: "正在启动" }) setTimeout(()=>{ that.SendOrder("07", "开启脑控"); },500) }, /** * 关闭脑控 */ sendControlClose() { let that = this setTimeout(()=>{ // 打开LED that.SendLedOrder("01"); },500); setTimeout(()=>{ // 关闭脑控 that.SendOrder("09", "关闭脑控"); ble_store.setters.setCurrentToyId("00"); },1000); //如果是一代脑机则发送 00 教具 if (isJELLYFISH()) { setTimeout(()=>{ that.sendConnectOneToMore('00'); },1500); } }, /** * todo 写入buffer * @param $hex * @param $buffer_len * @param $comments * @constructor */ WriteBufferInBle($hex, $comments = "", $buffer_len = 8) { let { deviceId, serviceId, characteristicWriteId } = getDeviceWriteInfo(); let $code = ble_store.getters.getDeviceSn(); if (deviceId && serviceId && characteristicWriteId) { let $hex_header = "aa cc "; let $hex_sum = 0; let $hex_ary = $hex.split(" "); $hex_ary.forEach(($val) => { $hex_sum += parseInt($val, 16); }) let $checksum = ($hex_sum ^ parseInt("ffffffff", 16)) & parseInt("ff", 16); $hex = $hex_header + $hex + " " + ("00" + $checksum.toString(16)).substr(-2, 2); let buffer = new ArrayBuffer($buffer_len); let dataView = new DataView(buffer); $hex_ary = $hex.split(" "); $hex_ary.forEach(($val, $index) => { dataView.setUint8($index, parseInt($val, 16)) }) wx.writeBLECharacteristicValue({ deviceId: deviceId, serviceId: serviceId, characteristicId: characteristicWriteId, value: buffer, success: function () { LOG_WECHAT($code, "写入指令:", $hex, $comments); }, fail: function (err) { LOG_WECHAT_ERROR($code, "写入指令失败:", $hex, $comments, JSON.stringify(err)); }, }); } }, /** * 监听脑机数据 * @param $this */ notifyDatas($this) { const that = this; let deviceId = ble_store.getters.getDeviceId(); console.log("%c监听脑机数据", "color:red;", deviceId); // 监听蓝牙低功耗设备的特征值变化事件 wx.onBLECharacteristicValueChange((characteristic) => { let hexStr = ab2hex(characteristic.value); let $code = ble_store.getters.getDeviceSn(); let $comments = ""; // 处理打开脑控的应答 if (hexStr.toUpperCase().indexOf("AADD07") >= 0) { ble_store.setters.setBluetoothLinkStatus(true); $comments = "打开脑控的应答"; } // 收到发送UUID的应答立马发送连接教具的指令 if (hexStr.toUpperCase().indexOf("AADD8E") >= 0) { let $currentToyId = ble_store.getters.getCurrentToyId(); //发送教具连接(连接教具(使用下发的ID)) that.sendConnectOneToToy($currentToyId); $comments = "连接教具(使用下发的ID)的应答"; } if (hexStr.toUpperCase().indexOf("AAEE87") >= 0) { let $currentToyId = ble_store.getters.getCurrentToyId(); //获取教具设备编号 if ($currentToyId !== '80') { that.SendOrder('87', "获取教具编号") } $comments = "获取教具编号异常"; } //let $game_status = game_store.getters.getGameStatus(); //let $currentToyId = ble_store.getters.getCurrentToyId(); // 连接页面 if($this && $this.$options.name){ LOG_DEBUG("当前页面名称:", $this?$this.$options.name:""); // 监听脑机电量 if (hexStr.substring(0, 8) === "55550203") { $comments = "监听脑机电量"; let $power = parseInt(hexStr.substring(8, 10), 16); // let $voltage = parseInt(hexStr.substring(10, 12), 16); // // 监听是否插入USB // $this.hasUsb = $voltage.toString().substring(0, 1) === "1"; if ($power) { $this.device_power = $power; } if ($power < 10 && $power > 0) { wx.showToast({ title: "脑机电量不足", icon: "none", duration: 2000}); } } // 判断教具连接 if (hexStr.toUpperCase().indexOf("AADD0A") >= 0) { $comments = "连接教具的应答"; //没连接上教具 if (hexStr.toUpperCase().indexOf("AADD0A0000") >= 0) { if(!isJELLYFISH()){ $this.connect_toy = 3; } return false; } let $baseIndex = hexStr.toUpperCase().indexOf("AADD0A"); let $hex_index = hexStr.substring($baseIndex + 28, 30) let $toy_id = hexStr.substring($baseIndex + 8, 10) LOG_DEBUG("连接HEX:", $hex_index, "教具ID:", $toy_id) // 连接上教具 if (new RegExp("00").test($hex_index) === true) { if (isJELLYFISH()){ // 一代脑机 $this.connect_toy = 2; if($this.toy_item && $this.toy_item.name){ wx.showToast({title: "已连接到" + $this.toy_item.name }); } } else { LOG_DEBUG("一对多") // 发送一对一连接 03 00 ${$toy_id} 01 0A that.sendConnectOneToOne($toy_id); ble_store.setters.setCurrentToyId($toy_id); // current_toy_id = $toy_id; } } if (new RegExp("01").test($hex_index) === true) { LOG_DEBUG("一对一") if($this.toy_item && $this.toy_item.name){ wx.showToast({title: "已连接到" + $this.toy_item.name }); } //连接成功 $this.connect_toy = 2; // 判断冥想模式不发送获取教具名字 if ($toy_id !== "80") { // 获取一次教具名称 setTimeout(() => { that.SendOrder('87', "获取教具编号") }, 3000) // 不断获取教具电量 let toy_interval = setInterval(() => { let $game_status = game_store.getters.getGameStatus(); if($game_status === 0 || $game_status === 3){ clearInterval(toy_interval); } else{ that.SendOrder('8a', "获取教具电量"); } }, 10000); } } } // 获取教具名称 if (hexStr.toUpperCase().indexOf("AADD87") >= 0) { let $currentToyId = ble_store.getters.getCurrentToyId(); let $mHexStr = hexStr.substring(hexStr.toUpperCase().indexOf("AADD87")) let $datas = doAnalysis($mHexStr, 10); let $number = $datas.match(/\d+/); $number = $number ? $number : "00000000"; let toy_list_pre = {'00': "", '01': "SW", '02': "KL", '04': "SC", '05': "PP", '06': "SU", '09': "UF", '12': "JM", '13': "QM"} let $sn = toy_list_pre[$currentToyId] + $number; $this.toy_sn = $sn; //保存教具sn ble_store.setters.setToySn($sn); LOG_DEBUG("获取教具名称hexStr:", hexStr, ",获取教具名称$sn", $sn); $comments = "获取教具名称"; } // 获取教具电量 if (hexStr.toUpperCase().indexOf("AADD8A") >= 0) {//接收教具电量状态 let $_hexStr = hexStr.substring(hexStr.toUpperCase().indexOf("AADD8A") + 6); let $power = parseInt($_hexStr.substring(0, 2), 16) if ($power > 0) { $this.toy_power = $power } $comments = "获取教具电量的应答"; } // 教具断链(连续多次到教具的命令没有响应) if (hexStr.toUpperCase().indexOf("AAEE70") >= 0) { //connect_toy = false wx.showModal({ content: "教具已断开", success(res) { if (res.confirm) { let $game_status = game_store.getters.getGameStatus(); if ($game_status === 1 || $game_status === 2) { $this.endTheGame(); } $this.connect_toy = 3; } } }) $comments = "连续多次到教具的命令没有响应"; } // 监听数据 if (hexStr.substring(0, 6) === "555520") { $comments = "监听数据"; // 监听佩戴正确, 当s1为 00时 数据有效 $this.device_bg = (hexStr.substring(8, 10) === "00"); LOG_DEBUG("监听佩戴正确:", hexStr.substring(0, 10), $this.device_bg); //游戏中模块 let $game_status = game_store.getters.getGameStatus(); if ($game_status === 1 || $game_status === 2) { // 获取蓝牙低功耗设备(脑机)的信号强度 wx.getBLEDeviceRSSI({ deviceId: deviceId, success(res) {$this.RSSI = res.RSSI;} }); // 分析实时数据 if($this.$options.name === "StartGames"){ $this.analysisGameData(hexStr); } } } //todo 接收脑机关机指令 if (hexStr.toUpperCase().indexOf("AADD5A00000000A5") >= 0) { let $game_status = game_store.getters.getGameStatus(); if ($game_status === 1 || $game_status === 2) { Notify({ type: 'danger', duration: 0, message: '智脑机已关机,训练结束', onOpened() { $this.endTheGame(); } }); } that.clearStatus($this); $comments = "脑环关机的应答"; } } // 日志DEBUG: 脑机电量 || 教具电量 || 数据 let logFlag = (hexStr.substring(0, 8) === "55550203") || (hexStr.toUpperCase().indexOf("AADD8A") >= 0) || (hexStr.substring(0, 6) === "555520"); if (logFlag) { LOG_DEBUG($code, "电量及数据应答:", hexStr, $comments); } else { // 日志-推送到微信 LOG_WECHAT($code, "指令应答:", hexStr, $comments); } }); }, /** * 监听蓝牙连接状态 * 监听蓝牙低功耗连接状态改变事件。包括开发者主动连接或断开连接,设备丢失,连接异常断开等 */ watchBLEstatus($this) { let that = this; if($this && $this.$options.name){ LOG_DEBUG("微信自身监听低功耗蓝牙连接状态:", $this.$options.name); } // 微信自身监听低功耗蓝牙连接状态的改变事件 wx.onBLEConnectionStateChange((res) => { // 该方法回调中可以用于处理连接意外断开等异常情况 ble_store.setters.setBluetoothLinkStatus(res.connected); LOG_DEBUG("监听脑机连接状态:", res.connected); if (!res.connected) { //判断游戏是否游戏中 let $game_status = game_store.getters.getGameStatus(); LOG_DEBUG("智脑机已断开连接,游戏状态:", $game_status); if ($game_status === 1 || $game_status === 2) { // if($this && $this.$options.name && $this.$options.name === "StartGames"){ // $this.endTheGame(); // } if($this && $this.$options.name && $this.$options.name === "StartGames"){ //如果是一代脑则结束游戏 if (isJELLYFISH()) { $this.endTheGame(); } else { Notify({ type: 'danger', duration: 0, message: '智脑机已断开连接,正在尝试重新连接', onOpened() { that.reconnectDevice(res.deviceId, $this); console.log("智脑机已断开连接deviceId", res.deviceId); } }); } } } else { that.clearStatus($this); } } }); }, /** * 重新连接蓝牙 */ reconnectDevice($deviceId, $this){ let that = this; let $code = ble_store.getters.getDeviceSn(); //重连的次数 let $connect_count = 0; let $rec = setInterval(() => { let $game_status = game_store.getters.getGameStatus(); LOG_DEBUG("正在尝试重新连接,游戏状态:", $game_status); // 开始搜寻附近的蓝牙外围设备。此操作比较耗费系统资源,请在搜索到需要的设备后及时调用 wx.stopBluetoothDevicesDiscovery 停止搜索。 wx.startBluetoothDevicesDiscovery({ allowDuplicatesKey: true, success: function(res) { try { // 监听搜索到新设备的事件 wx.onBluetoothDeviceFound((res) => { res.devices.forEach((device) => { if (!device.name && !device.localName) { return; } if (device.localName && device.localName !== "") { device.name = device.localName; } if (device["name"].toUpperCase() === $code) { // 停止搜寻附近的蓝牙外围设备。若已经找到需要的蓝牙设备并不需要继续搜索时,建议调用该接口停止蓝牙搜索。 wx.stopBluetoothDevicesDiscovery(); // 连接低功耗蓝牙设备 wx.createBLEConnection({ deviceId: $deviceId, success() { clearInterval($rec) Notify({type: 'success', message: `第${$connect_count}次重新连接成功`}); LOG_WECHAT($code, `第${$connect_count}次重新连接成功`); // 获取蓝牙设备服务 that.getBLEDeviceServices($deviceId); // 继续游戏 setTimeout(function(){ $this.startTheGame(); }, 3000); }, fail(err) { Notify({type: 'danger', message: `第${$connect_count}次重新连接失败`}); LOG_WECHAT($code, `第${$connect_count}次重新连接失败`, err.errMsg); } }) } }); }); } catch (e) { Notify({type: 'danger', message: `第${$connect_count}次重新连接失败`}); LOG_WECHAT($code, `第${$connect_count}次重新连接失败`, e.errMsg); } }, fail(err) { Notify({type: 'danger', message: `第${$connect_count}次重新连接失败`}); LOG_WECHAT($code, `第${$connect_count}次重新连接失败`, err.errMsg); }, }); if ($connect_count >= 3) { let $game_status = game_store.getters.getGameStatus(); if ($game_status === 1 || $game_status === 2) { $this.endTheGame(); } clearInterval($rec); // 移除搜索到新设备的事件的全部监听函数 wx.offBluetoothDeviceFound(); // 停止搜寻附近的蓝牙外围设备。若已经找到需要的蓝牙设备并不需要继续搜索时,建议调用该接口停止蓝牙搜索。 wx.stopBluetoothDevicesDiscovery(); that.clearStatus($this); } $connect_count += 1; }, 7000); }, /** * 关闭脑机蓝牙连接 */ closeConnection($this) { const that = this; let $code = ble_store.getters.getDeviceSn(); // 关闭脑控 that.SendOrder("09", "关闭脑控"); ble_store.setters.setBluetoothLinkStatus(false); game_store.setters.setGameStatus(0); // todo 清空链接的设备 $this.connect_toy = 0; $this.device_bg = false; // 断开教具及蓝牙连接 setTimeout(()=>{ that.SendOrder("31", "断开教具及蓝牙连接"); // 移除搜索到新设备的事件的全部监听函数 wx.offBluetoothDeviceFound(); // 停止搜寻附近的蓝牙外围设备。若已经找到需要的蓝牙设备并不需要继续搜索时,建议调用该接口停止蓝牙搜索。 wx.stopBluetoothDevicesDiscovery(); let deviceId = ble_store.getters.getDeviceId(); //断开蓝牙连接 wx.closeBLEConnection({ deviceId: deviceId, success() { Toast.success({ message: "断开蓝牙连接成功", }); LOG_WECHAT($code, "断开蓝牙连接成功", deviceId); }, fail(err) { LOG_WECHAT_ERROR($code, "断开蓝牙连接"+deviceId+"失败error:", JSON.stringify(err)); }, complete() { // 清除状态 that.clearStatus($this); }, }); },500); }, /** * 清除状态 */ clearStatus($this){ wx.closeBluetoothAdapter(); game_store.setters.setGameStatus(0); ble_store.setters.clearDeviceToy(); $this.device_bg = false; $this.device_status = 0; $this.connect_toy = 0; $this.$forceUpdate(); }, /** * 错误信息 */ connectionError(errCode) { if (errCode === 10000) { return "未初始化蓝牙适配器"; } if (errCode === 10001) { return "当前蓝牙适配器不可用"; } if (errCode === 10002) { return "没有找到指定设备"; } if (errCode === 10003) { return "连接失败"; } if (errCode === 10006) { return "当前连接已断开"; } return "未知连接错误:"+errCode; }, };