withTimeout方法可以在搜寻设备时等待指定的秒数,如果30秒内未搜索到则取消搜索
/*** 超时控制函数* @param {Promise} promise 回调函数* @param {number} timeout 超时时间, 默认10s*/
export function withTimeout(promise, timeout = 10000) {let timeoutEvent = nullconst logicPromise = new Promise((resolve, reject) => {promise.then((data) => {if (timeoutEvent) {// 清理超时clearTimeout(timeoutEvent)timeoutEvent = null}resolve(data)})})// 创建一个新的 Promise 对象,用于处理超时情况const timeoutPromise = new Promise((resolve, reject) => {timeoutEvent = setTimeout(() => {reject(`执行超时`);}, timeout);});// 将两个 Promise 对象(原始的 promise 和超时的 promise)合并为一个新的 Promise 对象return Promise.race([logicPromise, timeoutPromise]);
}
计算数据校验和:
校验字节等于命令字节与所有数据字节之和的反码。求和按带进位加 (ADDC)方式计算,每个进位都被加到本次结果的最低位(LSB)。
举例:如命令字节=0x01;数据=0xF0,0xF1,0xF2,0xF3,0xF4,0xF5,0xF6,0xF7,0xF8,0xF9,0xFA,0xFB,0xFC,0xFD,0xFE,0xFF,0x00,0x01;则校验字节0x01+0xF0+0xF1+0xF2+0xF3+0xF4+0xF5+0xF6+0xF7+0xF8+0xF9+0xFA+0xFB+0xFC+0xFD+0xFE+0xFF+0x00+0x01 = 0x0F79;0x0F+0x79 = 0x88;校验字节 = 0xFF – 0x88 = 0x77。getAddc(str) {let itotal = 0,len = str.length,num = 0;var tempTotal = "";while (num < len) {let s = str.substring(num, num + 2);itotal += parseInt(s, 16);num = num + 2;if (itotal >= 256) {itotal = parseInt((itotal - itotal % 256) / 256) + itotal % 256}}itotal = 255 - itotalreturn itotal.toString(16).padStart(2, '0')}
vue页面代码
<template><view class="content"><view class="titlebox"><view class="title">空气净化香氛系统</view><image src="http://f.zstjj.com/f/uniapp/280035/images/sz-icon.png" @click="changeMode"></image></view><view class="lowerpart"><view><view class="subhead">香氛浓度</view><view class="elliptic"><view class="strip"></view><view class="strip-text"><view class="dot" @click="selectFragranceMode(1)">自动</view><image v-if="fragranceInfo.FragranceMode == 1"src="http://f.zstjj.com/f/uniapp/280035/images/sliderbtn.png" class="dotImg zd"></image><view class="dot" @click="selectFragranceMode(2)">淡</view><image v-if="fragranceInfo.FragranceMode == 2"src="http://f.zstjj.com/f/uniapp/280035/images/sliderbtn.png" class="dotImg dan"></image><view class="dot" @click="selectFragranceMode(3)">中</view><image v-if="fragranceInfo.FragranceMode == 3"src="http://f.zstjj.com/f/uniapp/280035/images/sliderbtn.png" class="dotImg zhong"></image><view class="dot" @click="selectFragranceMode(4)">浓</view><image v-if="fragranceInfo.FragranceMode == 4"src="http://f.zstjj.com/f/uniapp/280035/images/sliderbtn.png" class="dotImg nong"></image></view></view></view><view><view class="subhead">香氛选择</view><view class="first-floor"><view class="fragrant-box" @click="selectCurrentFragranceWorkChannel(1)":class="fragranceInfo.CurrentFragranceWorkChannel ==1 ? 'selected' : ''"><image:src="'http://f.zstjj.com/f/uniapp/280035/images/xf_'+fragranceInfo.FirstFragranceType+'.png'"></image><view style="margin-right:20rpx;"><view>{{fragranceMap[fragranceInfo.FirstFragranceType]}}</view><view style="color: #989393;margin-top: 10rpx;letter-spacing: 0px;font-weight: normal;">{{fragranceInfo.FirstFragranceResidue}}%</view></view></view><view class="fragrant-box" @click="selectCurrentFragranceWorkChannel(2)":class="fragranceInfo.CurrentFragranceWorkChannel == 2 ? 'selected' : ''"><image:src="'http://f.zstjj.com/f/uniapp/280035/images/xf_'+fragranceInfo.SecondFragranceType+'.png'"></image><view style="margin-right:20rpx;"><view>{{fragranceMap[fragranceInfo.SecondFragranceType]}}</view><view style="color: #989393;margin-top: 10rpx;letter-spacing: 0px;font-weight: normal;">{{fragranceInfo.SecondFragranceResidue}}%</view></view></view></view><view class="second-floor"><view class="switch-box"><text>香氛开关</text><view class="switch-rotate"><switch :checked="fragranceInfo.FragranceWorkState==1" color="#ffffff00"class="switch-style s-size" /><view class="switch_shade" @tap='changeFragranceWorkState()'></view></view></view><view class="switch-box"><text>等离子开关</text><view class="switch-rotate"><switch :checked="fragranceInfo.IPCWorkState==1" color="#ffffff00"class="switch-style s-size" /><view class="switch_shade" @tap='changeIPCWorkState()'></view></view></view><view class="switch-box"><text>蓝牙</text><view class="switch-rotate"><switch :checked="isBluetoohConnect" color="#ffffff00" class="switch-style s-size" /><view class="switch_shade" @tap='changeBluetooh()'></view></view><!-- <view "switch-rotate" ><switch :checked='item.is_Checked' Color='#ffffff00' style="transform: scale(0.4);" /><view class="switch_shade" @tap='changeChecked()'></view></view> --></view></view></view></view><!-- 设置弹窗 --><uni-popup ref="modeDialog" type="center"><view class="popInfo"><view class="pop-title">设置</view><view class="froms-strip"><view class="f-title">语音开关</view><switch :checked="fragranceInfo.VoiceControlState==1" style="width: 15%;transform:scale(0.7);background: linear-gradient(92deg, #a4edf9, #2999ff);border-radius: 40rpx;" color="#ffffff00" /><view class="switch_shade" @tap='changeVoiceControlState()'></view></view><!-- <view class="froms-strip"><view class="f-title">香氛模式</view><view class="f-select"><uni-data-select v-model="value" :localdata="range" @change="change" :clear="false"></uni-data-select></view></view> --><view class="degree"><view class="d-title">淡香</view><view class="degree-info"><view class="hang"><view>每隔</view><picker @change="changeLightFragranceModeStopTime":value="fragranceInfo.LightFragranceModeStopTime" :range="array" range-key="name"><view class="pickerValue">{{array[fragranceInfo.LightFragranceModeStopTime].name}}</view></picker><view>秒</view></view><view class="hang"><view>工作</view><picker @change="changeLightFragranceModeWorkTime":value="fragranceInfo.LightFragranceModeWorkTime" :range="array" range-key="name"><view class="pickerValue">{{array[fragranceInfo.LightFragranceModeWorkTime].name}}</view></picker><view>秒</view></view></view></view><view class="degree"><view class="d-title">中香</view><view class="degree-info"><view class="hang"><view>每隔</view><picker @change="changeMiddleFragranceModeStopTime":value="fragranceInfo.MiddleFragranceModeStopTime" :range="array" range-key="name"><view class="pickerValue">{{array[fragranceInfo.MiddleFragranceModeStopTime].name}}</view></picker><view>秒</view></view><view class="hang"><view>工作</view><picker @change="changeMiddleFragranceModeWorkTime":value="fragranceInfo.MiddleFragranceModeWorkTime" :range="array" range-key="name"><view class="pickerValue">{{array[fragranceInfo.MiddleFragranceModeWorkTime].name}}</view></picker><view>秒</view></view></view></view><view class="degree"><view class="d-title">浓香</view><view class="degree-info"><view class="hang"><view>每隔</view><picker @change="changeStrongFragranceModeStopTime":value="fragranceInfo.StrongFragranceModeStopTime" :range="array" range-key="name"><view class="pickerValue">{{array[fragranceInfo.StrongFragranceModeStopTime].name}}</view></picker><view>秒</view></view><view class="hang"><view>工作</view><picker @change="changeStrongFragranceModeWorkTime":value="fragranceInfo.StrongFragranceModeWorkTime" :range="array" range-key="name"><view class="pickerValue">{{array[fragranceInfo.StrongFragranceModeWorkTime].name}}</view></picker><view>秒</view></view></view></view><view class="btnBox"><view class="cancelBtn" @click="dialogClose">取消</view><view class="confirmBtn" @click="changeFragranceModeTime()">确定</view></view></view></uni-popup><!-- #ifdef MP-WEIXIN --><ws-wx-privacy id="privacy-popup"></ws-wx-privacy><!-- #endif --></view>
</template><script>import {Fragrance} from '@/utils/adjust_fragrance.js'export default {options: {styleIsolation: "shared"},data() {return {selected: 'A',value: 0,array: Array.from({length: 125}, (_, i) => i * 2 + 2).map(num => ({//key: num,name: num//name: num})),range: [{value: 0,text: "手动模式"},// {// value: 1,// text: "自动模式"// },],fragranceMap: {0: "未配置",1: "青茶",2: "鸢尾",3: "茉莉",4: "森林",5: "蓝风铃",6: "水蜜桃",7: "海洋",8: "浪漫",9: "魅力",},SecondFragranceMap: {},selectedImage: 1,fragranceInfo: {//香氛机型(单香机型=0x01, 两香机型=0x02)FragranceType: 0,//当前香氛模式(自动模式=0x00,淡香模式=0x01,中香模式=0x02,浓香模式=0x03);FragranceMode: 1,//香氛工作状态(关闭状态=0x00, 打开状态=0x01)FragranceWorkState: 2,//等离子工作状态(关闭状态=0x00, 打开状态=0x01)。IPCWorkState: 2,//当前香氛工作通道(1号香氛=0x00, 2号香氛=0x01)CurrentFragranceWorkChannel: 1,//Byte6= 1号香氛类型(未配置=0x00,青茶香氛=0x01,鸢尾香氛=0x02,茉莉香氛=0x03,//森林香氛=0x04,蓝风铃香氛=0x05,水蜜桃香氛=0x06,海洋香氛=0x07,浪漫香氛=0x08,魅力香氛=0x09)FirstFragranceType: 0,//2号香氛类型(未配置=0x00,青茶香氛=0x01,鸢尾香氛=0x02,茉莉香氛=0x03,//森林香氛=0x04,蓝风铃香氛=0x05,水蜜桃香氛=0x06,海洋香氛=0x07,浪漫香氛=0x08,魅力香氛=0x09)SecondFragranceType: 0,//1号香氛余量(0~100)FirstFragranceResidue: 0,//2号香氛余量(0~100)SecondFragranceResidue: 0,//空气质量等级(未配置=0x00,优=0x01, 良=0x02, 中=0x03, 差=0x04)AirQualityLevel: 0,//语音控制使能状态(关闭状态=0x00, 打开状态=0x01)VoiceControlState: 2,//淡香模式停止时间反馈(秒)LightFragranceModeStopTime: 0,//淡香模式工作时间反馈(秒)LightFragranceModeWorkTime: 0,//中香模式停止时间反馈(秒)MiddleFragranceModeStopTime: 0,//中香模式工作时间反馈(秒)MiddleFragranceModeWorkTime: 0,//浓香模式停止时间反馈(秒)StrongFragranceModeStopTime: 0,//浓香模式工作时间反馈(秒)StrongFragranceModeWorkTime: 0,},isBluetoohConnect: false,refreshDataTP: 0, //刷新数据时间戳hexDataStr: "", //数据isClickBtn: false, //是否点击了按钮isRefresh: false,isFirstRefresh:false,isDestory:false,}},async onLoad(options) {// uni.showLoading({// title: '准备连接中...',// mask: true// });uni.hideHomeButton()// #ifdef MP-WEIXIN // 如果是微信小程序,首页默认弹窗指引授权 (2.23.3以下基础库不支持,所以判断方法是否存在)if (wx.requirePrivacyAuthorize instanceof Function) {await this.mpWeixinPrivacyAuthorization()}// #endifthis.getLocation()var that=thissetTimeout(function() {console.info('onload-----')that.initPage()}, 500);// 每秒钟输出一次当前时间var that = thissetInterval(function() {that.refreshDataInterval()}, 500);//this.hideLoading()//uni.hideLoading();},// onShow() {// uni.openBluetoothAdapter() //初始化蓝牙// },async destroyed() {console.log("销毁页面")this.isDestory=trueawait this.destroyedBluetooh()},methods: {async initPage(){try {this.fragrance = new Fragrance()// 连接蓝牙await this.toConnect(true)// if (this.isFirstRefresh){// await this.toConnect()// }} catch (e) {console.error('蓝牙连接错误', e)//uni.hideLoading()this.hideLoading()this.showToastError("蓝牙连接错误", () => {this.setBluetoothBreak()})return}},mpWeixinPrivacyAuthorization() {return new Promise((resolve, reject) => {if (uni.getStorageSync('mpweixin_disagree_authorization') == 1) {resolve()return} // 用户已拒绝,首页不再自动弹出询问,在用户完成微信登录后清除wx.requirePrivacyAuthorize({success: async () => {console.log('mpweixin_agree_authorization')// 用户同意授权// 微信小程序自动登录代码resolve()},fail: () => {console.log("拒绝授权")// this.common.toast('用户拒绝授权')uni.setStorageSync('mpweixin_disagree_authorization', 1)resolve()}, // 用户拒绝授权complete: () => {console.log("结束授权")}})})console.log("结束代码")},getLocation() {// 获取位置权限uni.getLocation({type: 'wgs84',success: function(res) {},fail: function(err) {}})},//获取授权getAuthorize() {uni.showModal({content: '授权蓝牙和位置权限才可以扫描蓝牙,是否去设置打开?',confirmText: "确认",cancelText: '取消',success: (res) => {if (res.confirm) {uni.openSetting({// success: (res) => {// console.log("授权后请重新打开此页面,授权成功")// },// fail: (err) => {// console.log(err)// }})} else {}}})},//刷新当前数据refreshDataInterval() {var timeNowTP = new Date().getTime()if (timeNowTP - this.refreshDataTP < 1500) {return}var tempStr = this.fragrance.getReturnDataStr()if (this.hexDataStr != tempStr && (!this.isRefresh)) {this.isRefresh = truevar config = this.fragrance.HexToConfig(tempStr)if (config) {this.fragranceInfo = configthis.refreshDataTP = timeNowTPthis.hexDataStr = tempStrconsole.log("refreshDataInterval-刷新数据")this.isRefresh = false}}},//修改淡香模式停止时间changeLightFragranceModeStopTime: function(e) {this.fragranceInfo.LightFragranceModeStopTime = parseInt(e.detail.value);},//修改淡香模式工作时间changeLightFragranceModeWorkTime: function(e) {this.fragranceInfo.LightFragranceModeWorkTime = parseInt(e.detail.value);},//修改中香模式停止时间changeMiddleFragranceModeStopTime: function(e) {this.fragranceInfo.MiddleFragranceModeStopTime = parseInt(e.detail.value);},//修改中香模式工作时间changeMiddleFragranceModeWorkTime: function(e) {this.fragranceInfo.MiddleFragranceModeWorkTime = parseInt(e.detail.value);},//修改浓香模式停止时间changeStrongFragranceModeStopTime: function(e) {this.fragranceInfo.StrongFragranceModeStopTime = parseInt(e.detail.value);},//修改浓香模式工作时间changeStrongFragranceModeWorkTime: function(e) {this.fragranceInfo.StrongFragranceModeWorkTime = parseInt(e.detail.value);},async changeBluetooh(e) {if (this.isBluetoohConnect) { //如果蓝牙已连接,点击按钮,断开蓝牙uni.showLoading({title: '断开蓝牙...',mask: true});await this.destroyedBluetooh()this.isBluetoohConnect = falsethis.hideLoading()//uni.hideLoading()} else { //如果未连接,点击连接蓝牙await this.toConnect()}},// 蓝牙断开连接lostBluetoothConnect(res) {if (!res.connected) {console.log("提示蓝牙已断开")if(!this.isDestory){this.showToastError("蓝牙已断开", () => {this.setBluetoothBreak()})}}},// 标记蓝牙连接已断开setBluetoothBreak() {this.isBluetoohConnect = false},async destroyedBluetooh() {if (this.fragrance) {// 销毁蓝牙连接await this.fragrance.close()}this.isBluetoohConnect = false},//刷新页面数据async refreshData(tryCount = 5) {//console.log("refreshData-刷新数据", tryCount)// console.log("this.isRefresh-刷新数据", this.isRefresh)if (this.isRefresh && tryCount == 5) { //如果正在刷新,那么不重新开启刷新线程return false}this.isRefresh = trueif (tryCount < 1) {this.hideLoading()this.isRefresh = falsereturn false}try {var that = thisif (tryCount == 3 && that.fragrance.getReturnData() == null) {that.fragrance.startNotice()}var tempStr = that.fragrance.getReturnDataStr()if (tempStr != that.hexDataStr) {var config = that.fragrance.HexToConfig(tempStr)if (config) {that.hexDataStr = tempStrthat.fragranceInfo = configconsole.log("刷新数据成功")//uni.hideLoading()that.hideLoading()that.isRefresh = falsereturn true}}setTimeout(function() {return that.refreshData(tryCount - 1)}, 100)} catch (e) {//TODO handle the exception}},hideLoading() {setTimeout(function() {uni.hideLoading()}, 500)},//操作香氛工作状态async changeFragranceWorkState() {if (!this.checkBtnState()) {console.log("changeFragranceWorkState-两秒内重复点击")return}this.refreshDataTP = new Date().getTime()if (!this.isBluetoohConnect) {this.showToastError("请先连接蓝牙", () => {//this.setBluetoothBreak()})return}uni.showLoading({title: '设置香氛中...',mask: true});var tempFragranceInfo = JSON.parse(JSON.stringify(this.fragranceInfo))if (tempFragranceInfo.FragranceWorkState == 1) { //打开香氛tempFragranceInfo.FragranceWorkState = 2} else { //关闭香氛tempFragranceInfo.FragranceWorkState = 1}const state = await this.fragrance.WriteSwitchControlData(tempFragranceInfo)if (state) { //写入成功,刷新页面await this.refreshData()} else {this.showToastError("调整香氛失败", () => {this.setBluetoothBreak()})}},checkBtnState() {if (this.isClickBtn) {return false}this.isClickBtn = truevar that = thissetTimeout(function() {that.isClickBtn = false}, 1500)return true},//操作等离子状态async changeIPCWorkState() {if (!this.checkBtnState()) {console.log("changeFragranceWorkState-两秒内重复点击")return}this.refreshDataTP = new Date().getTime()//console.log("!this.isBluetoohConnect", !this.isBluetoohConnect)if (!this.isBluetoohConnect) {this.showToastError("请先连接蓝牙", () => {//this.setBluetoothBreak()})return}uni.showLoading({title: '设置等离子中...',mask: true});var tempFragranceInfo = JSON.parse(JSON.stringify(this.fragranceInfo))if (tempFragranceInfo.IPCWorkState == 1) { //打开等离子tempFragranceInfo.IPCWorkState = 2} else { //关闭等离子tempFragranceInfo.IPCWorkState = 1}const state = await this.fragrance.WriteSwitchControlData(tempFragranceInfo)if (state) { //写入成功,刷新页面await this.refreshData()//uni.hideLoading()this.hideLoading()} else {this.showToastError("设置等离子失败", () => {this.setBluetoothBreak()})}},//语音控制使能状态async changeVoiceControlState() {if (!this.checkBtnState()) {console.log("changeFragranceWorkState-两秒内重复点击")return}this.refreshDataTP = new Date().getTime()if (!this.isBluetoohConnect) {this.showToastError("请先连接蓝牙", () => {//this.setBluetoothBreak()})return}uni.showLoading({title: '设置语音状态中...',mask: true});var tempFragranceInfo = JSON.parse(JSON.stringify(this.fragranceInfo))if (tempFragranceInfo.VoiceControlState == 1) { //打开语音控制tempFragranceInfo.VoiceControlState = 2} else { //关闭语音控制tempFragranceInfo.VoiceControlState = 1}//console.log("tempFragranceInfo.VoiceControlState", tempFragranceInfo.VoiceControlState)const state = await this.fragrance.WriteSwitchControlData(tempFragranceInfo)if (state) { //写入成功,刷新页面await this.refreshData()//uni.hideLoading()this.hideLoading()} else {this.showToastError("设置等离子失败", () => {this.setBluetoothBreak()})}},//修改香氛模式时间async changeFragranceModeTime() {if (!this.checkBtnState()) {console.log("changeFragranceWorkState-两秒内重复点击")return}this.refreshDataTP = new Date().getTime()if (!this.isBluetoohConnect) {this.showToastError("请先连接蓝牙", () => {//this.setBluetoothBreak()})return}this.dialogClose()uni.showLoading({title: '设置香氛模式时间...',mask: true});var tempFragranceInfo = JSON.parse(JSON.stringify(this.fragranceInfo))tempFragranceInfo.LightFragranceModeStopTime = (tempFragranceInfo.LightFragranceModeStopTime + 1) * 2tempFragranceInfo.LightFragranceModeWorkTime = (tempFragranceInfo.LightFragranceModeWorkTime + 1) * 2tempFragranceInfo.MiddleFragranceModeStopTime = (tempFragranceInfo.MiddleFragranceModeStopTime + 1) * 2tempFragranceInfo.MiddleFragranceModeWorkTime = (tempFragranceInfo.MiddleFragranceModeWorkTime + 1) * 2tempFragranceInfo.StrongFragranceModeStopTime = (tempFragranceInfo.StrongFragranceModeStopTime + 1) * 2tempFragranceInfo.StrongFragranceModeWorkTime = (tempFragranceInfo.StrongFragranceModeWorkTime + 1) * 2//console.log("tempFragranceInfo.LightFragranceModeStopTime",tempFragranceInfo.LightFragranceModeStopTime)const state = await this.fragrance.WriteFragranceSetData(tempFragranceInfo)if (state) { //写入成功,刷新页面//console.log("selectFragranceMode-写入成功")//this.showToastSuccess('调整香氛浓度成功')this.refreshDataTP = new Date().getTime()await this.refreshData()//uni.hideLoading()this.hideLoading()} else {this.showToastError("调整香氛模式时间失败", () => {this.setBluetoothBreak()})console.log("changeFragranceModeTime-写入失败")}},// 连接按钮async toConnect() {uni.showLoading({title: '蓝牙搜索连接中...',mask: true});if (this.isBluetoohConnect) {// 已连接this.showToastSuccess('蓝牙已连接')return}var that = thistry {// 连接蓝牙const [isok, errCode] = await this.fragrance.BLEConnect(this.deviceId)if (!isok) {if (errCode == 100 ){this.showToastError("没有找到指定设备", () => {this.setBluetoothBreak()})} else if (errCode == 103) {this.getAuthorize()console.log("关掉loading")this.hideLoading()} else if (errCode == 10001) {this.showToastError("请打开本机蓝牙", () => {this.setBluetoothBreak()})}else if ( errCode == 10002) {this.showToastError("蓝牙连接超时", () => {this.setBluetoothBreak()})}else {this.showToastError("蓝牙连接错误", () => {this.setBluetoothBreak()})}return}this.showToastSuccess('蓝牙连接成功')await this.fragrance.ReadFragranceInfo()this.refreshDataTP = new Date().getTime()await this.refreshData()this.isBluetoohConnect = true //设置为连接状态// 监听连接状态this.fragrance.onBLEConnectionStateChange((res) => {// console.log(`连接状态变化`, res)this.lostBluetoothConnect(res)})} catch (e) {console.log('蓝牙连接错误', e)//uni.hideLoading()//this.hideLoading()this.showToastError("蓝牙连接错误", () => {this.setBluetoothBreak()})return}//this.hideLoading()//uni.hideLoading()},//开关switch1Change: function(e) {console.log('switch1 发生 change 事件,携带值为', e.detail.value)},//香氛浓度 async selectFragranceMode(index) {if (!this.checkBtnState()) {console.log("changeFragranceWorkState-两秒内重复点击")return}this.refreshDataTP = new Date().getTime()//console.log("!this.isBluetoohConnect", !this.isBluetoohConnect)if (!this.isBluetoohConnect) {this.showToastError("请先连接蓝牙", () => {//this.setBluetoothBreak()})return}uni.showLoading({title: '设置香氛浓度中...',mask: true});var tempFragranceInfo = JSON.parse(JSON.stringify(this.fragranceInfo))tempFragranceInfo.FragranceMode = index//console.log("tempFragranceInfo.FragranceMode", tempFragranceInfo.FragranceMode)const state = await this.fragrance.WriteSwitchControlData(tempFragranceInfo)if (state) { //写入成功,刷新页面//console.log("selectFragranceMode-写入成功")//this.showToastSuccess('调整香氛浓度成功')await this.refreshData()//uni.hideLoading()this.hideLoading()} else {this.showToastError("调整香氛浓度失败", () => {this.setBluetoothBreak()})console.log("selectFragranceMode-写入失败")}},//香氛选择handleClick(item) {this.selected = item;},//香氛选择async selectCurrentFragranceWorkChannel(index) {if (!this.checkBtnState()) {console.log("changeFragranceWorkState-两秒内重复点击")return}this.refreshDataTP = new Date().getTime()if (!this.isBluetoohConnect) {this.showToastError("请先连接蓝牙", () => {//this.setBluetoothBreak()})return}if (this.fragranceInfo.FragranceWorkState != 1) {this.showToastError("请先打开香氛控制开关", () => {//this.setBluetoothBreak()})return}uni.showLoading({title: '设置香氛中...',mask: true});var tempFragranceInfo = JSON.parse(JSON.stringify(this.fragranceInfo))tempFragranceInfo.CurrentFragranceWorkChannel = index//console.log("tempFragranceInfo.CurrentFragranceWorkChannel", tempFragranceInfo//.CurrentFragranceWorkChannel)const state = await this.fragrance.WriteSwitchControlData(tempFragranceInfo)if (state) { //写入成功,刷新页面await this.refreshData()this.hideLoading()//uni.hideLoading()} else {this.showToastError("调整香氛失败", () => {this.setBluetoothBreak()})console.log("selectCurrentFragranceWorkChannel写入失败")}},//设置弹窗changeMode() {this.$refs.modeDialog.open()},//关闭弹窗dialogClose() {this.$refs.modeDialog.close()this.refreshDataTP = new Date().getTime()this.hexDataStr = ""},change(e) {console.log("e:", e);},/*** 操作成功提示* @param {string} msg 消息内容*/showToastSuccess(msg) {uni.showToast({title: msg,icon: 'success',duration: 2000})},/*** 操作错误提示* @param {string} msg 消息内容*/showToastError(msg, cb = null) {setTimeout(() => {uni.showToast({title: msg,icon: 'none',duration: 2000,complete: () => {if (cb) {cb()}}})}, 100)},}}
</script><style lang="scss">page {width: 100%;height: 100%;background: url('http://f.zstjj.com/f/uniapp/280035/images/xxbg.png') no-repeat;background-size: cover;box-sizing: border-box;}.content {height: 100%;position: relative;overflow: hidden;display: flex;flex-direction: column;justify-content: space-between;}.titlebox {margin: 20rpx 35rpx;position: relative;display: flex;justify-content: center;align-items: center;}.titlebox image {position: absolute;right: 0rpx;top: 0rpx;width: 56rpx;height: 56rpx;}.title {font-size: 46rpx;color: white;letter-spacing: 2rpx;margin-left: 40rpx;margin-top: 12rpx;}.lowerpart {position: absolute;width: 90%;bottom: 0px;margin: 20rpx 35rpx;}.subhead {color: white;font-weight: bold;letter-spacing: 1px;margin: 35rpx 44rpx 25rpx;}.elliptic {width: 100%;height: 99rpx;padding-top: 20rpx;border-radius: 100rpx;background: #2b2f3a;border: 2rpx solid #738ca187;display: flex;align-items: center;justify-content: center;flex-direction: column;}.strip {width: 85%;height: 22rpx;border-radius: 25rpx;margin-bottom: 20rpx;background: linear-gradient(93deg, #a4edf9, #70bbff, #ba8cff);}.strip-text {width: 85%;color: white;font-size: 28rpx;font-weight: lighter;position: relative;display: flex;justify-content: space-between;}.dot {margin: 0px 5rpx;position: relative;}.dot::after {content: "";display: block;width: 14rpx;height: 14rpx;background-color: #ffffff;position: absolute;left: 8rpx;top: -37rpx;border-radius: 50%;}.dot::before {content: "";display: block;width: 40rpx;height: 40rpx;background-color: #ffffff00;position: absolute;left: -5rpx;top: -50rpx;}.dotImg {width: 76rpx;height: 76rpx;position: absolute;bottom: 32rpx;}.zd {left: -18rpx;}.dan {left: 178rpx;}.zhong {right: 150rpx;}.nong {right: -24rpx;}.first-floor {display: flex;justify-content: space-between;}.fragrant-box {width: 47%;height: 190rpx;background: #2b2f3ad4;border: 2rpx solid #738ca187;border-radius: 40rpx;color: #ffffff;font-weight: lighter;letter-spacing: 1px;display: flex;align-items: center;justify-content: center;}.selected {color: #333333;font-weight: bold;// background: linear-gradient(177deg, #5fa6ff, #74ecff);background: linear-gradient(178deg, #3c92ff, #82eeff);}.fragrant-box image {width: 80rpx;height: 80rpx;margin-right: 25rpx;}.second-floor {margin: 40rpx 0 45rpx;display: flex;justify-content: space-between;}.switch-box {width: 30%;height: 180rpx;padding: 34rpx 0px 80rpx;background: #2b2f3af5;border: 2rpx solid #55697987;border-radius: 40rpx;position: relative;display: flex;align-items: center;justify-content: space-between;flex-direction: column;}.switch-box text {color: white;font-weight: bold;letter-spacing: 2rpx;}.switch-rotate {transform: rotate(270deg);}.switch-style {width: 92%;background: linear-gradient(92deg, #82eeff, #3c92ff);border-radius: 40rpx;}.s-size {transform: scale(1.3);}.popInfo {width: 600rpx;padding: 23rpx 40rpx 35rpx;border-radius: 38rpx;background: white;display: flex;flex-direction: column;align-items: flex-start;}.pop-title {width: 100%;color: #000000;margin: 0 0 25rpx;text-align: center;letter-spacing: 4rpx;font-weight: bolder;font-size: 34rpx;}.froms-strip {width: 100%;margin: 23rpx 0;padding-bottom: 20rpx;border-bottom: 1rpx solid #dddddd;position: relative;display: flex;justify-content: space-between;align-items: center;}.f-select /deep/ .uni-select {width: 28%;border: 2rpx solid #ffffff;position: absolute;right: 2rpx;top: -16rpx;font-size: 32rpx;appearance: none;}.f-select /deep/ .uni-select__selector-item {padding: 5rpx 10rpx;}.f-select /deep/ .uni-select__input-text {color: #787878;}.f-title {color: black;}.degree {margin: 0px 0px 40rpx 0px;}.d-title {color: black;margin: 0px 0px 16rpx 10rpx;}.degree-info {width: 540rpx;padding: 16rpx 30rpx;border-radius: 16rpx;background: #e4e4e6;display: flex;justify-content: space-around;}.degree-info view {color: black;letter-spacing: 2rpx;display: flex;align-items: center;}.uni-input {width: 100rpx;color: #028bfd;text-align: center;font-weight: bold;}.btnBox {width: 100%;margin-top: 30rpx;display: flex;justify-content: space-around;}.cancelBtn {width: 40%;background: #e5e5e5;font-size: 34rpx;color: black;letter-spacing: 4rpx;text-align: center;border-radius: 50rpx;padding: 20rpx;margin-right: 20rpx;}.confirmBtn {width: 40%;background: #028bfd;font-size: 34rpx;color: white;letter-spacing: 4rpx;text-align: center;border-radius: 50rpx;padding: 20rpx 10rpx;}.switch-box-2 {position: relative;display: inline-block;}.switch_shade {position: absolute;top: 0;right: 0;bottom: 0;left: 0;z-index: 10;}
</style>
js函数代码
export class Fragrance {// 读UUID:0000FEC1-0000-1000-8000-XXXXX#appointNotifyCharacteristicId = '0000FEC1-0000-1000-8000-XXXXX'// 写UUID:0000FEC1-0000-1000-8000-XXXXX#appointWriteCharacteristicId = '0000FEC1-0000-1000-8000-XXXXX'// 指定服务ID#appointServiceId = '0000FEE7-0000-1000-8000-XXXXX'// 指定设备ID#appointDeviceId = '60E962AD-434B-F6EF-D82C-XXXXX'// 读UUID:0000fec1-0000-1000-8000-XXXXX#notifyCharacteristicId = '0000FEC1-0000-1000-8000-XXXXX'// 写UUID:0000fec1-0000-1000-8000-XXXXX#writeCharacteristicId = '0000FEC1-0000-1000-8000-XXXXX'// #serviceId = '0000FEE7-0000-1000-8000-XXXXX'// 设备ID#deviceId = ''#deviceReturnData = ''#connected = false // 连接状态标志位isConnected() {return this.#connected}/*** 关闭蓝牙连接, 关闭蓝牙模块*/async close() {try {this.#connected = false// 关闭蓝牙连接await uni.closeBLEConnection({deviceId: this.#deviceId})// 关闭蓝牙模块await uni.closeBluetoothAdapter()} catch (e) {// 这里错误 不处理console.error('关闭蓝牙错误', e)}}// 开始搜索蓝牙uni.startBluetoothDevicesDiscovery(),使用uni.onBluetoothDeviceFound 注册搜索到的设备,async ScanDevice() {var that = thisreturn new Promise((resolve, reject) => {console.log("开始搜索")try {uni.startBluetoothDevicesDiscovery({interval: 0, // 上报设备的间隔。0 表示找到新设备立即上报,其他数值根据传入的间隔上报allowDuplicatesKey: true,// services: ['0000FEE7'], // 只搜索主服务 UUID 为 0000FEE7 的设备// 扫描模式,越高扫描越快,也越耗电。仅安卓微信客户端 7.0.12 及以上支持。// low:低,medium:中,high:高powerLevel: 'high',success(res) {uni.onBluetoothDeviceFound(function(devices) {var d = {}if (typeof devices == Array) {d = devices[0]} else {d = devices.devices[0]}// console.log('onBluetoothDeviceFound-', d.name)// 找到名称 KLOVEF-AirFragranceSystem 的设备是代表已搜索到if (d.name == 'KLOVEF-AirFragranceSystem') {// console.log('onBluetoothDeviceFound-', devices)that.#deviceId = d.deviceIdresolve(d.deviceId)///console.log("this.#deviceId-aaa", that.#deviceId)// uni.stopBluetoothDevicesDiscovery({// success(res) {// console.log('关闭成功')// },// fail(res) {// console.log('关闭失败' + res.errMsg)// }// })}})},fail(res) {console.log("搜索失败", res.errMsg)}})} catch (e) {console.log("结束搜索-111")console.error('ScanDevice' + e)}console.log("结束搜索-222")})}HexToConfig(HexStr) {if (HexStr.length != 40) { //校验数据长度return null}if (!this.checkAddc(HexStr)) { //校验ADDCreturn null}var FragranceType = parseInt(HexStr.substring(2, 4), 16)var FragranceMode = parseInt(HexStr.substring(4, 6), 16) + 1//返回和写入时一致的数据,旧返回数据 (关闭状态=0x00, 打开状态=0x01)//目标返回数据 (未控制=0x00, 打开香氛=0x01, 关闭香氛=0x02)var FragranceWorkState = parseInt(HexStr.substring(6, 8), 16)if (FragranceWorkState != 1) {FragranceWorkState = 2}//返回和写入时一致的数据,旧返回数据 等离子工作状态(关闭状态=0x00, 打开状态=0x01)//目标返回数据 (未控制=0x00, 打开等离子=0x01, 关闭等离子=0x02)var IPCWorkState = parseInt(HexStr.substring(8, 10), 16)if (IPCWorkState != 1) {IPCWorkState = 2}var CurrentFragranceWorkChannel = parseInt(HexStr.substring(10, 12), 16) + 1var FirstFragranceType = parseInt(HexStr.substring(12, 14), 16)var SecondFragranceType = parseInt(HexStr.substring(14, 16), 16)var FirstFragranceResidue = parseInt(HexStr.substring(16, 18), 16)//console.log("FirstFragranceResidue",HexStr.substring(16, 18), 16)var SecondFragranceResidue = parseInt(HexStr.substring(18, 20), 16)var AirQualityLevel = parseInt(HexStr.substring(20, 22), 16)//返回和写入时一致的数据,旧返回数据 等离子工作状态((关闭状态=0x00, 打开状态=0x01)//目标返回数据 (未控制=0x00, 打开语音控制=0x01, 关闭语音控制=0x02)var VoiceControlState = parseInt(HexStr.substring(22, 24), 16)if (VoiceControlState != 1) {VoiceControlState = 2}//这里为了方便前端显示,转换成索引var LightFragranceModeStopTime = parseInt(HexStr.substring(24, 26), 16) / 2 - 1var LightFragranceModeWorkTime = parseInt(HexStr.substring(26, 28), 16) / 2 - 1var MiddleFragranceModeStopTime = parseInt(HexStr.substring(28, 30), 16) / 2 - 1var MiddleFragranceModeWorkTime = parseInt(HexStr.substring(30, 32), 16) / 2 - 1var StrongFragranceModeStopTime = parseInt(HexStr.substring(32, 34), 16) / 2 - 1var StrongFragranceModeWorkTime = parseInt(HexStr.substring(34, 36), 16) / 2 - 1return {//香氛机型(单香机型=0x01, 两香机型=0x02)FragranceType: FragranceType,//当前香氛模式(自动模式=0x00,淡香模式=0x01,中香模式=0x02,浓香模式=0x03);FragranceMode: FragranceMode,//香氛工作状态(关闭状态=0x00, 打开状态=0x01)FragranceWorkState: FragranceWorkState,//等离子工作状态(关闭状态=0x00, 打开状态=0x01)。IPCWorkState: IPCWorkState,//当前香氛工作通道(1号香氛=0x00, 2号香氛=0x01)CurrentFragranceWorkChannel: CurrentFragranceWorkChannel,//Byte6= 1号香氛类型(未配置=0x00,青茶香氛=0x01,鸢尾香氛=0x02,茉莉香氛=0x03,//森林香氛=0x04,蓝风铃香氛=0x05,水蜜桃香氛=0x06,海洋香氛=0x07,浪漫香氛=0x08,魅力香氛=0x09)FirstFragranceType: FirstFragranceType,//2号香氛类型(未配置=0x00,青茶香氛=0x01,鸢尾香氛=0x02,茉莉香氛=0x03,//森林香氛=0x04,蓝风铃香氛=0x05,水蜜桃香氛=0x06,海洋香氛=0x07,浪漫香氛=0x08,魅力香氛=0x09)SecondFragranceType: SecondFragranceType,//1号香氛余量(0~100)FirstFragranceResidue: FirstFragranceResidue,//2号香氛余量(0~100)SecondFragranceResidue: SecondFragranceResidue,//空气质量等级(未配置=0x00,优=0x01, 良=0x02, 中=0x03, 差=0x04)AirQualityLevel: AirQualityLevel,//语音控制使能状态(关闭状态=0x00, 打开状态=0x01)VoiceControlState: VoiceControlState,//淡香模式停止时间反馈(秒)LightFragranceModeStopTime: LightFragranceModeStopTime,//淡香模式工作时间反馈(秒)LightFragranceModeWorkTime: LightFragranceModeWorkTime,//中香模式停止时间反馈(秒)MiddleFragranceModeStopTime: MiddleFragranceModeStopTime,//中香模式工作时间反馈(秒)MiddleFragranceModeWorkTime: MiddleFragranceModeWorkTime,//浓香模式停止时间反馈(秒)StrongFragranceModeStopTime: StrongFragranceModeStopTime,//浓香模式工作时间反馈(秒)StrongFragranceModeWorkTime: StrongFragranceModeWorkTime,}}/*** 开始通知*/startNotice() {console.log("startNotice-监听数据")var that = this;uni.notifyBLECharacteristicValueChange({state: true, // 启用 notify 功能// 这里的 deviceId 需要已经通过 createBLEConnection 与对应设备建立链接 deviceId: that.#deviceId,// 这里的 serviceId 需要在 getBLEDeviceServices 接口中获取serviceId: that.#serviceId,// 这里的 characteristicId 需要在 getBLEDeviceCharacteristics 接口中获取characteristicId: that.#writeCharacteristicId,success(res) {//接收蓝牙返回消息uni.onBLECharacteristicValueChange((sjRes) => {// 此时可以拿到蓝牙设备返回来的数据是一个ArrayBuffer类型数据,//所以需要通过一个方法转换成字符串that.#deviceReturnData = that.ab2hex(sjRes.value) //10.0console.log("startNotice-that.#deviceReturnData", that.#deviceReturnData)})},fail(err) {// uni.showToast({// title: '通知失败',// icon: 'error',// duration: 2000// })console.log("startNotice", err)}})}//手机写入开关控制数据async WriteSwitchControlData(f) {var hexStr = "02" + f.FragranceMode.toString().padStart(2, '0') + f.CurrentFragranceWorkChannel.toString().padStart(2, '0') + f.FragranceWorkState.toString().padStart(2, '0') + f.IPCWorkState.toString().padStart(2, '0') + f.VoiceControlState.toString().padStart(2, '0')//console.log("hexStr", hexStr)hexStr = hexStr + "00000000000000"hexStr = this.addAddc(hexStr)var isOK = await this.SetFragranceInfo(hexStr)//console.log("手机写入香氛设置数据", isOK)return isOK}//手机写入香氛设置数据async WriteFragranceSetData(f) {var hexStr = "03" + f.LightFragranceModeStopTime.toString(16).padStart(2, '0') + f.LightFragranceModeWorkTime.toString(16).padStart(2, '0') + f.MiddleFragranceModeStopTime.toString(16).padStart(2, '0') + f.MiddleFragranceModeWorkTime.toString(16).padStart(2, '0') + f.StrongFragranceModeStopTime.toString(16).padStart(2, '0') + f.StrongFragranceModeWorkTime.toString(16).padStart(2, '0')hexStr = hexStr + "000000000000"hexStr = this.addAddc(hexStr)var isOK = await this.SetFragranceInfo(hexStr)//console.log("手机写入香氛设置数据", isOK)return await this.SetFragranceInfo(hexStr)}//设置香氛数据async SetFragranceInfo(hexStr) {//console.log("增加addc 后 SetFragranceInfo-hexStr", hexStr)var that = thisvar buffer = that.string2buffer(hexStr); //9.0return new Promise((resolve, reject) => {try {uni.writeBLECharacteristicValue({// 这里的 deviceId 需要在 getBluetoothDevices 或 onBluetoothDeviceFound 接口中获取deviceId: that.#deviceId,// 这里的 serviceId 需要在 getBLEDeviceServices 接口中获取serviceId: that.#serviceId,// 这里的 characteristicId 需要在 getBLEDeviceCharacteristics 接口中获取characteristicId: that.#writeCharacteristicId,// 这里的 value 是ArrayBuffer类型value: buffer,success(res) {//that.startNotice()//console.log('SetFragranceInfo success', res)if (res.errCode == 0) {//console.log("返回写入成功")resolve(true)} else {resolve(false)}},fail(res) {console.log('writeBLECharacteristicValue fail', res.errMsg)console.log("返回写入失败")//return false resolve(false)},complete(res) {console.log('writeBLECharacteristicValue ', JSON.stringify(res))}})} catch (e) {console.error('uni.onBLECharacteristicValueChange' + e)}})}//设置香氛数据async SetFragranceInfoByCode(code) {var that = this//向蓝牙设备发送一个0x00的16进制数据//打开香氛var hexStr = "02" + code + "0101010300000000000000"hexStr = this.addAddc(hexStr)//console.log(code + "写入开关控制数据-hexStr", hexStr)var buffer = that.string2buffer(hexStr); //9.0//console.log("开始发送数据-写入开关控制数据")uni.writeBLECharacteristicValue({// 这里的 deviceId 需要在 getBluetoothDevices 或 onBluetoothDeviceFound 接口中获取deviceId: that.#deviceId,// 这里的 serviceId 需要在 getBLEDeviceServices 接口中获取serviceId: that.#serviceId,// 这里的 characteristicId 需要在 getBLEDeviceCharacteristics 接口中获取characteristicId: that.#writeCharacteristicId,// 这里的 value 是ArrayBuffer类型value: buffer,success(res) {//that.startNotice()console.log('SetFragranceInfo success', res.errMsg)},fail(res) {console.log('writeBLECharacteristicValue fail', res.errMsg)},complete(res) {console.log('writeBLECharacteristicValue ', JSON.stringify(res))}})}//增加ADDC校验addAddc(str) {return str + this.getAddc(str)}getAddc(str) {let itotal = 0,len = str.length,num = 0;var tempTotal = "";while (num < len) {let s = str.substring(num, num + 2);itotal += parseInt(s, 16);num = num + 2;if (itotal >= 256) {itotal = parseInt((itotal - itotal % 256) / 256) + itotal % 256}}itotal = 255 - itotalreturn itotal.toString(16).padStart(2, '0')}checkAddc(str) {if (str.length != 40) {return false}var data = str.substring(0, 38)var allData = this.addAddc(data)return str == allData}//读取香氛设备信息async ReadFragranceInfo() {var that = this// 向蓝牙设备发送一个0x00的16进制数据var buffer = this.string2buffer('01000000000000000000000000FE'); //9.0console.log("this.#deviceId", this.#deviceId)console.log("this.#serviceId", this.#serviceId)console.log("this.#writeCharacteristicId", this.#writeCharacteristicId)var that = thisuni.writeBLECharacteristicValue({// 这里的 deviceId 需要在 getBluetoothDevices 或 onBluetoothDeviceFound 接口中获取deviceId: that.#deviceId,// 这里的 serviceId 需要在 getBLEDeviceServices 接口中获取serviceId: that.#serviceId,// 这里的 characteristicId 需要在 getBLEDeviceCharacteristics 接口中获取characteristicId: that.#writeCharacteristicId,// 这里的 value 是ArrayBuffer类型value: buffer,success(res) {that.startNotice()},fail(res) {console.log('writeBLECharacteristicValue fail', res.errMsg)},complete(res) {console.log('writeBLECharacteristicValue ', JSON.stringify(res))}})}getReturnDataStr() {let returnData = this.#deviceReturnDatareturn returnData}getReturnData() {//console.log("that.#deviceReturnData-999", this.#deviceReturnData)var config = this.HexToConfig(this.#deviceReturnData)//console.log("config", config)return config}// ArrayBuffer转16进度字符串示例ab2hex(buffer) {if (!buffer) return ""const hexArr = Array.prototype.map.call(new Uint8Array(buffer),function(bit) {return ('00' + bit.toString(16)).slice(-2)})return hexArr.join('')}/*** 将字符串转换成ArrayBufer */string2buffer(str) {let val = ""if (!str) return;let length = str.length;let index = 0;let array = []while (index < length) {array.push(str.substring(index, index + 2));index = index + 2;}val = array.join(",");// 将16进制转化为ArrayBuffer return new Uint8Array(val.match(/[\da-f]{2}/gi).map(function(h) {return parseInt(h, 16)})).buffer}/*** 连接低功耗蓝牙设备* @param {string} deviceId 设备ID* @param {number} timeout 蓝牙连接超时时间, 单位ms, 默认3000ms(3秒)* @param {number} mut 每次传输的数据大小, 默认244bytes(C385协议上就是默认每次传一帧数据, 总大小为244byte)* @param {number} retry 错误重试, 默认2次* @param {number} first 是否首次进入,如果是则重置 #deviceId*/async BLEConnect(deviceId, timeout = 3000, mtu = 244, retry = 3, first = true) {console.log("开始连接蓝牙" + retry)if (retry < 0) {// 重试次数已达到, 执行回调反馈给调用端return [false, 0]}try {//1. 检查适配器是否启动 uni.getBluetoothAdapterState(),没有启动则启动蓝牙适配器 uni.openBluetoothAdapter// this.openBluetoothAdapter()if (first) {this.#deviceId = ''}const [bluetooth, state] = await uni.openBluetoothAdapter()console.log("bluetooth", bluetooth)console.log("state", state)if (bluetooth) {//console.log("typeof(bluetooth)",bluetooth)if (bluetooth.errno == 103 || bluetooth.errMsg.indexOf("fail auth deny") != -1) {return [false, 103] //103需要打开蓝牙权限}return [false, bluetooth.errCode] //10001需要打开蓝牙}const [adapter, adapterState] = await uni.getBluetoothAdapterState()console.log("adapter", adapter)console.log("adapterState", adapterState)if (typeof(adapterState) == "undefined") {//return showModal()}if (!adapterState.discovering && retry == 0) {return [false, 10002] //未开启定位权限}if (!adapterState.available) {return [false, 10001] //蓝牙未打开}if (first || this.#deviceId == '') { // 如果没有搜到过设备则重新进入搜索,如果已经搜到过就无需搜索了try {const deviceId = await withTimeout(this.ScanDevice(), 3000)console.log('deviceId', deviceId)} catch (e) {console.log('ScanDevice', err)return [false, 100]}uni.stopBluetoothDevicesDiscovery({success(res) {console.log('关闭成功')},fail(res) {console.log('关闭失败' + res.errMsg)}})// await this.ScanDevice()//console.log("this.#deviceId",this.#deviceId)if (this.#deviceId == "" && retry < 0) { //需要打开设备return [false, 100]}if (this.#deviceId == "") {console.log('deviceId not value')retry = retry - 1return that.BLEConnect(this.#deviceId, timeout, mtu, retry)}}const [createErr, createSccess] = await uni.createBLEConnection({deviceId: this.#deviceId,timeout})//console.log('createBLEConnection', createErr, createSccess)if (createErr) {try {// 需要成对出现,如果连接错误也需要退出await uni.closeBLEConnection({deviceId: this.#deviceId})} catch {}// 连接错误console.log("连接错误", createErr)if (createErr.errCode == 10012 && retry == 0) {return [false, createErr.errCode] //10012,需要打开设备} else if (createErr.errCode != -1) {retry = retry - 1return await this.BLEConnect(this.#deviceId, timeout + 2000, mtu, retry, false)}}var that = this//setTimeout(() => {var isok = await this.getBLEDeviceServices()console.log("isok-111", isok)if (!isok) {retry = retry - 1return await this.BLEConnect(deviceId, timeout, mtu, retry, false)}isok = await this.getBLEDeviceCharacteristics()console.log("isok-222", isok)if (!isok) {retry = retry - 1return await this.BLEConnect(deviceId, timeout, mtu, retry, false)}return [true, 0]} catch (e) {console.log("重试", e)// 重试retry = retry - 1//console.log("连接蓝牙失败-catch-333")return await this.BLEConnect(deviceId, timeout, mtu, retry, false)}return [false, 0]}async getBLEDeviceServices() {console.log("开始执行-getBLEDeviceServices")var that = thisreturn new Promise((resolve, reject) => {uni.getBLEDeviceServices({deviceId: that.#deviceId,success(res) {// 5.启用notify//console.log('getBLEDeviceServices', res)// this.#connected = trueresolve(true)console.log("开始执行-getBLEDeviceServices-执行成功")// console.log("getBLEDeviceServices-res", res)// if (res.errno == 0 && res.services.length > 0) {// //console.log('getBLEDeviceServices-成功')// if (res.services[0].uuid != that.#appointServiceId) {// resolve(false)// }// that.#serviceId = res.services[0].uuid// // that.#connected = true// // that.startNotice()// // console.log("蓝牙连接成功")// resolve(true)// console.log("开始执行-getBLEDeviceServices-执行成功")// //})// } else {// resolve(false)// }},fail(err) {resolve(false)}})})}async getBLEDeviceCharacteristics() {var that = thisconsole.log("开始执行-getBLEDeviceCharacteristics")return new Promise((resolve, reject) => {uni.getBLEDeviceCharacteristics({deviceId: that.#deviceId,serviceId: that.#serviceId,success(res) {//console.log("getBLEDeviceCharacteristics-res", res)if (res.characteristics.length > 0) {// console.log("连接蓝牙成功-222")// console.log("res.characteristics", res.characteristics)if (res.characteristics[0].uuid != that.#appointNotifyCharacteristicId) {resolve(false)}that.#notifyCharacteristicId = res.characteristics[0].uuidthat.#writeCharacteristicId = res.characteristics[0].uuidthat.#connected = trueconsole.log("开始执行-成功-getBLEDeviceCharacteristics")that.startNotice()// console.log("this.#deviceId-999", that.#deviceId)// console.log("this.#writeCharacteristicId ", that// .#writeCharacteristicId)console.log("蓝牙连接成功")resolve(true)}},fail() {resolve(false)}})})}/*** 发送数据* @param {ArrayBuffer} bytes*/async #sendData(bytes) {await uni.writeBLECharacteristicValue({deviceId: this.#deviceId, // 蓝牙设备 idserviceId: this.#serviceId, // 蓝牙特征值对应服务的 uuidcharacteristicId: this.#writeCharacteristicId, // 蓝牙特征值的 uuidvalue: bytes, // 这里的value是ArrayBuffer类型fail: (err) => {console.log('writeBLECharacteristicValue', err)},})}// notify 接收消息回调函数#sendDataCallback = null// notify 接收消息回调函数消息队列(防止漏接收消息)#sendDataCallbackMsgQueue = null/*** 监听低功耗蓝牙设备的特征值变化事件。必须先启用 notifyBLECharacteristicValueChange 接口才能接收到设备推送的 notification。* @param {Function} callback 回调函数*/onBLEConnectionStateChange(callback) {uni.onBLEConnectionStateChange((res) => {// 该方法回调中可以用于处理连接意外断开等异常情况// console.log(`设备 ${res.deviceId} 状态已经改变, connected: ${res.connected}`)this.#connected = res.connected // 更新连接状态callback(res)})}}/*** 16进制字符串转16进制 ArrayBuffer* @param {string} hexString 16进制字符串*/
function string2HexArray(hexString) {if (!hexString) return;let length = hexString.length;let index = 0;let array = []while (index < length) {array.push(hexString.substring(index, index + 2));index = index + 2;}const val = array.join(",");return new Uint8Array(val.match(/[\da-f]{2}/gi).map((h) => parseInt(h, 16))).buffer
}/*** ArrayBuffer转16进度字符串示例* @param {Array} buffer 字节数组* @return {string} 转换后的字符串*/
export function ab2hex(buffer) {const hexArr = Array.prototype.map.call(new Uint8Array(buffer),(bit) => {return ('00' + bit.toString(16)).slice(-2)})return hexArr.join('')
}/*** 判断是不是要连接的蓝牙设备 是不是以 OTA- 开头* @param {string} deviceName 设备名称*/
export function isOTADevice(deviceName) {const prefix = "OTA-"return deviceName.toUpperCase().startsWith(prefix)
}function crc16(str) {const data = Buffer.from(str, "hex")let crcValue = 0xFFFF;for (let i = 0; i < data.length; i++) {crcValue ^= data[i] & 0xFFFFfor (let j = 0; j < 8; j++) {if (crcValue & 0x0001) {crcValue >>= 1crcValue ^= 0xA001} else {crcValue >>= 1}}}crcValue = crcValue.toString(16).toUpperCase().padStart(4, '0')return crcValue.substring(0, 2) + crcValue.substring(2, 4)
}/*** 字符串转ASCII码函数* @param {string} str*/
function toASCII(str) {return str.split('').map(char => char.charCodeAt(0)).join('');
}/*** 十六进制字符串转ASCII码* @param {string} hexStr*/
function hexToASCII(hexStr) {let result = '';for (let i = 0; i < hexStr.length; i += 2) {let hex = hexStr.substr(i, 2);result += String.fromCharCode(parseInt(hex, 16));}return result;
}/*** 格式化长度* @param {number} fileLen*/
function formatLen(fileLen) {return fileLen.toLocaleString('en-US', {minimumIntegerDigits: 6,useGrouping: false}).padStart(6, '0');
}/*** 搜索监听C385升级设备* @param {Function} notifier 回调函数* @param {Function} callback 回调函数* @param {Function} cancelCallback 取消回调函数*/
export async function listenUpdateDevice(notifier, callback, cancelCallback = null) {const showModal = () => {uni.showModal({title: '提示!',content: '初始化蓝牙失败,请打开本机蓝牙!',showCancel: true,cancelText: "取消",confirmText: "确定",success: (res) => {if (res.confirm) {// 用户点击了确定return listenUpdateDevice(notifier, callback, cancelCallback)} else if (res.cancel) {// 用户点击了取消if (cancelCallback) {cancelCallback()}return}}});}try {// 1.初始化蓝牙模块const res = await uni.openBluetoothAdapter()// 2.获取本机蓝牙适配器状态(正常状态下是一定能获取的)let [_, state] = await uni.getBluetoothAdapterState()if (!state) {return listenUpdateDevice(notifier, callback, cancelCallback)}// console.log(state)if (!state.available) {return showModal()}// 3.开启搜寻附近的蓝牙外围设备await uni.startBluetoothDevicesDiscovery()// 通知已经开始搜索设备了notifier()// 4.开始监听搜索设备uni.onBluetoothDeviceFound(async found => {const d = found.devices[0]if (isOTADevice(d.name) && d.connectable) {const device = {name: d.name, // 名称id: d.advertisServiceUUIDs.length > 0 ? d.advertisServiceUUIDs[0] : '', // 设备IDMAC: d.deviceId, // MAC地址advertisData: ab2hex(d.advertisData).trim(), // 广播数据RSSI: d.RSSI, // RSSIconnectable: d.connectable // 是否能连接}callback(device)// 搜索到了要升级的设备旧关闭(startBluetoothDevicesDiscovery会很耗电, 所以搜索到蓝牙设备后要及时关毕)// await uni.stopBluetoothDevicesDiscovery()}})} catch (e) {console.error('listenUpdateDevice错误', e)return showModal()}
}/*** 关闭搜索监听C385升级设备* @param {Function} callback 回调函数*/
export async function closeListenUpdateDevice() {// 搜索到了要升级的设备旧关闭(startBluetoothDevicesDiscovery会很耗电, 所以搜索到蓝牙设备后要及时关毕)await uni.stopBluetoothDevicesDiscovery()
}/*** ASCII 转 ArrayBuffer* @param {string} str ASCII 字符串*/
function textEncode(str) {const bytes = [];for (let i = 0; i < str.length; i++) {let charcode = str.charCodeAt(i);if (charcode < 0x80) {bytes.push(charcode);} else if (charcode < 0x800) {bytes.push(0xC0 | (charcode >> 6),0x80 | (charcode & 0x3F));} else {bytes.push(0xE0 | (charcode >> 12),0x80 | ((charcode >> 6) & 0x3F),0x80 | (charcode & 0x3F));}}return new Uint8Array(bytes).buffer;
}/*** 超时控制函数* @param {Promise} promise 回调函数* @param {number} timeout 超时时间, 默认10s*/
export function withTimeout(promise, timeout = 10000) {let timeoutEvent = nullconst logicPromise = new Promise((resolve, reject) => {promise.then((data) => {if (timeoutEvent) {// 清理超时clearTimeout(timeoutEvent)timeoutEvent = null}resolve(data)})})// 创建一个新的 Promise 对象,用于处理超时情况const timeoutPromise = new Promise((resolve, reject) => {timeoutEvent = setTimeout(() => {reject(`执行超时`);}, timeout);});// 将两个 Promise 对象(原始的 promise 和超时的 promise)合并为一个新的 Promise 对象return Promise.race([logicPromise, timeoutPromise]);
}