withTimeout方法可以在搜寻设备时等待指定的秒数,如果30秒内未搜索到则取消搜索
/**
* 超时控制函数
* @param {Promise} promise 回调函数
* @param {number} timeout 超时时间, 默认10s
*/
export function withTimeout(promise, timeout = 10000) {
let timeoutEvent = null
const 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 - itotal
return 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()
}
// #endif
this.getLocation()
var that=this
setTimeout(function() {
console.info('onload-----')
that.initPage()
}, 500);
// 每秒钟输出一次当前时间
var that = this
setInterval(function() {
that.refreshDataInterval()
}, 500);
//this.hideLoading()
//uni.hideLoading();
},
// onShow() {
// uni.openBluetoothAdapter() //初始化蓝牙
// },
async destroyed() {
console.log("销毁页面")
this.isDestory=true
await 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 = true
var config = this.fragrance.HexToConfig(tempStr)
if (config) {
this.fragranceInfo = config
this.refreshDataTP = timeNowTP
this.hexDataStr = tempStr
console.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 = false
this.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 = true
if (tryCount < 1) {
this.hideLoading()
this.isRefresh = false
return false
}
try {
var that = this
if (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 = tempStr
that.fragranceInfo = config
console.log("刷新数据成功")
//uni.hideLoading()
that.hideLoading()
that.isRefresh = false
return 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 = true
var that = this
setTimeout(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) * 2
tempFragranceInfo.LightFragranceModeWorkTime = (tempFragranceInfo.LightFragranceModeWorkTime + 1) * 2
tempFragranceInfo.MiddleFragranceModeStopTime = (tempFragranceInfo.MiddleFragranceModeStopTime + 1) * 2
tempFragranceInfo.MiddleFragranceModeWorkTime = (tempFragranceInfo.MiddleFragranceModeWorkTime + 1) * 2
tempFragranceInfo.StrongFragranceModeStopTime = (tempFragranceInfo.StrongFragranceModeStopTime + 1) * 2
tempFragranceInfo.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 = this
try {
// 连接蓝牙
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 = this
return 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.deviceId
resolve(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)) { //校验ADDC
return 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) + 1
var 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 - 1
var LightFragranceModeWorkTime = parseInt(HexStr.substring(26, 28), 16) / 2 - 1
var MiddleFragranceModeStopTime = parseInt(HexStr.substring(28, 30), 16) / 2 - 1
var MiddleFragranceModeWorkTime = parseInt(HexStr.substring(30, 32), 16) / 2 - 1
var StrongFragranceModeStopTime = parseInt(HexStr.substring(32, 34), 16) / 2 - 1
var StrongFragranceModeWorkTime = parseInt(HexStr.substring(34, 36), 16) / 2 - 1
return {
//香氛机型(单香机型=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.0
console.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 = this
var buffer = that.string2buffer(hexStr); //9.0
return 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 - itotal
return 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.0
console.log("this.#deviceId", this.#deviceId)
console.log("this.#serviceId", this.#serviceId)
console.log("this.#writeCharacteristicId", this.#writeCharacteristicId)
var that = this
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()
},
fail(res) {
console.log('writeBLECharacteristicValue fail', res.errMsg)
},
complete(res) {
console.log('writeBLECharacteristicValue ', JSON.stringify(res))
}
})
}
getReturnDataStr() {
let returnData = this.#deviceReturnData
return 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 - 1
return 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 - 1
return 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 - 1
return await this.BLEConnect(deviceId, timeout, mtu, retry, false)
}
isok = await this.getBLEDeviceCharacteristics()
console.log("isok-222", isok)
if (!isok) {
retry = retry - 1
return 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 = this
return new Promise((resolve, reject) => {
uni.getBLEDeviceServices({
deviceId: that.#deviceId,
success(res) {
// 5.启用notify
//console.log('getBLEDeviceServices', res)
// this.#connected = true
resolve(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 = this
console.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].uuid
that.#writeCharacteristicId = res.characteristics[0].uuid
that.#connected = true
console.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, // 蓝牙设备 id
serviceId: this.#serviceId, // 蓝牙特征值对应服务的 uuid
characteristicId: this.#writeCharacteristicId, // 蓝牙特征值的 uuid
value: 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] & 0xFFFF
for (let j = 0; j < 8; j++) {
if (crcValue & 0x0001) {
crcValue >>= 1
crcValue ^= 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] : '', // 设备ID
MAC: d.deviceId, // MAC地址
advertisData: ab2hex(d.advertisData).trim(), // 广播数据
RSSI: d.RSSI, // RSSI
connectable: 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 = null
const 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]);
}