fix: 蓝牙协议接口编码实现。
This commit is contained in:
BIN
mixerControlE-.png
Normal file
BIN
mixerControlE-.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 MiB |
BIN
mixerControlE.jpg
Normal file
BIN
mixerControlE.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
BIN
mixerControlE.png
Normal file
BIN
mixerControlE.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
@@ -902,9 +902,18 @@ const getBLEDeviceServices = async (deviceId) => {
|
|||||||
let notifyCharacteristicId = '' // 可监听特征值
|
let notifyCharacteristicId = '' // 可监听特征值
|
||||||
let mainServiceId = '' // 主服务ID
|
let mainServiceId = '' // 主服务ID
|
||||||
|
|
||||||
// 自动获取第一个服务的特征值(用于展示)
|
// 查找协议规定的服务ID (0x00FF)
|
||||||
|
// 注意:不同设备可能返回不同格式的UUID,需要兼容匹配
|
||||||
if (res.services.length > 0) {
|
if (res.services.length > 0) {
|
||||||
mainServiceId = res.services[0].uuid
|
// 优先查找包含 "00FF" 或 "0xFF" 的服务UUID
|
||||||
|
const targetService = res.services.find(s => {
|
||||||
|
const uuid = s.uuid.toUpperCase()
|
||||||
|
return uuid.includes('00FF') || uuid.includes('0XFF') || uuid === '0X00FF'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 如果找到目标服务,使用它;否则使用第一个服务
|
||||||
|
mainServiceId = targetService ? targetService.uuid : res.services[0].uuid
|
||||||
|
console.log('使用的主服务UUID:', mainServiceId, targetService ? '(匹配协议0x00FF)' : '(默认第一个服务)')
|
||||||
const chars = await getBLEDeviceCharacteristicsSync(deviceId, mainServiceId)
|
const chars = await getBLEDeviceCharacteristicsSync(deviceId, mainServiceId)
|
||||||
characteristics.value = chars
|
characteristics.value = chars
|
||||||
|
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ const channels = ref([
|
|||||||
timerMode: 'Timer',
|
timerMode: 'Timer',
|
||||||
timerValue: 'Not Set',
|
timerValue: 'Not Set',
|
||||||
remainingTime: 0, // 剩余秒数
|
remainingTime: 0, // 剩余秒数
|
||||||
timerHours: 0 // 设置的小时数
|
timerMinutes: 0 // 设置的分钟数
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'CH2',
|
id: 'CH2',
|
||||||
@@ -299,12 +299,12 @@ const loadChannelData = (channelId) => {
|
|||||||
if (channel) {
|
if (channel) {
|
||||||
// 恢复保存的数据,但状态默认为停止
|
// 恢复保存的数据,但状态默认为停止
|
||||||
channel.runValue = data.runValue || channel.runValue
|
channel.runValue = data.runValue || channel.runValue
|
||||||
channel.timerHours = data.timerHours || 0
|
channel.timerMinutes = data.timerMinutes || 0
|
||||||
channel.remainingTime = 0
|
channel.remainingTime = 0
|
||||||
channel.status = 'stopped'
|
channel.status = 'stopped'
|
||||||
|
|
||||||
if (channel.timerHours > 0) {
|
if (channel.timerMinutes > 0) {
|
||||||
channel.timerValue = `Set ${channel.timerHours} hours`
|
channel.timerValue = `Set ${channel.timerMinutes} min`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -320,7 +320,7 @@ const saveChannelData = (channelId) => {
|
|||||||
if (channel) {
|
if (channel) {
|
||||||
const data = {
|
const data = {
|
||||||
runValue: channel.runValue,
|
runValue: channel.runValue,
|
||||||
timerHours: channel.timerHours
|
timerMinutes: channel.timerMinutes
|
||||||
}
|
}
|
||||||
uni.setStorageSync(STORAGE_KEY_PREFIX + channelId, JSON.stringify(data))
|
uni.setStorageSync(STORAGE_KEY_PREFIX + channelId, JSON.stringify(data))
|
||||||
}
|
}
|
||||||
@@ -412,32 +412,32 @@ const setTimer = (channelId) => {
|
|||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: 'Timer Setting',
|
title: 'Timer Setting',
|
||||||
editable: true,
|
editable: true,
|
||||||
placeholderText: 'Enter hours (0-99)',
|
placeholderText: 'Enter minutes (0-9999)',
|
||||||
content: channel.timerHours > 0 ? String(channel.timerHours) : '',
|
content: channel.timerMinutes > 0 ? String(channel.timerMinutes) : '',
|
||||||
cancelText: 'Cancel',
|
cancelText: 'Cancel',
|
||||||
confirmText: 'OK',
|
confirmText: 'OK',
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res.confirm && res.content) {
|
if (res.confirm && res.content) {
|
||||||
const hours = parseInt(res.content)
|
const minutes = parseInt(res.content)
|
||||||
if (isNaN(hours) || hours < 0 || hours > 99) {
|
if (isNaN(minutes) || minutes < 0 || minutes > 9999) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: 'Please enter 0-99',
|
title: 'Please enter 0-9999',
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
channel.timerHours = hours
|
channel.timerMinutes = minutes
|
||||||
channel.remainingTime = hours * 3600 // Convert to seconds (hours*3600)
|
channel.remainingTime = minutes * 60 // Convert to seconds (minutes*60)
|
||||||
|
|
||||||
if (hours > 0) {
|
if (minutes > 0) {
|
||||||
channel.timerValue = `Set ${hours} hours`
|
channel.timerValue = `Set ${minutes} min`
|
||||||
} else {
|
} else {
|
||||||
channel.timerValue = 'Not Set'
|
channel.timerValue = 'Not Set'
|
||||||
}
|
}
|
||||||
|
|
||||||
saveChannelData(channelId)
|
saveChannelData(channelId)
|
||||||
sendBluetoothCommand('setTimer', channelId, hours)
|
sendBluetoothCommand('setTimer', channelId, minutes)
|
||||||
|
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: `Timer Set Successfully`,
|
title: `Timer Set Successfully`,
|
||||||
@@ -463,10 +463,10 @@ const startChannel = (channelId) => {
|
|||||||
const channel = channels.value.find(c => c.id === channelId)
|
const channel = channels.value.find(c => c.id === channelId)
|
||||||
if (channel) {
|
if (channel) {
|
||||||
// 如果设置了定时,进入定时模式
|
// 如果设置了定时,进入定时模式
|
||||||
if (channel.timerHours > 0) {
|
if (channel.timerMinutes > 0) {
|
||||||
channel.status = 'timing'
|
channel.status = 'timing'
|
||||||
// 重置剩余时间
|
// 重置剩余时间
|
||||||
channel.remainingTime = channel.timerHours * 3600
|
channel.remainingTime = channel.timerMinutes * 60
|
||||||
startTimer(channelId)
|
startTimer(channelId)
|
||||||
} else {
|
} else {
|
||||||
channel.status = 'running'
|
channel.status = 'running'
|
||||||
@@ -511,9 +511,24 @@ const batchStart = () => {
|
|||||||
cancelText: 'Cancel',
|
cancelText: 'Cancel',
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
|
// 更新本地状态
|
||||||
selectedChannels.value.forEach(channelId => {
|
selectedChannels.value.forEach(channelId => {
|
||||||
startChannel(channelId)
|
const channel = channels.value.find(c => c.id === channelId)
|
||||||
|
if (channel) {
|
||||||
|
// 如果设置了定时,进入定时模式
|
||||||
|
if (channel.timerMinutes > 0) {
|
||||||
|
channel.status = 'timing'
|
||||||
|
channel.remainingTime = channel.timerMinutes * 60
|
||||||
|
startTimer(channelId)
|
||||||
|
} else {
|
||||||
|
channel.status = 'running'
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 批量发送蓝牙命令(一次性发送所有选中通道的状态)
|
||||||
|
sendBluetoothCommand('batchStart', null, null)
|
||||||
|
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: `Started ${selectedChannels.value.length} channels`,
|
title: `Started ${selectedChannels.value.length} channels`,
|
||||||
icon: 'success'
|
icon: 'success'
|
||||||
@@ -540,9 +555,18 @@ const batchStop = () => {
|
|||||||
cancelText: 'Cancel',
|
cancelText: 'Cancel',
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
|
// 更新本地状态
|
||||||
selectedChannels.value.forEach(channelId => {
|
selectedChannels.value.forEach(channelId => {
|
||||||
stopChannel(channelId)
|
const channel = channels.value.find(c => c.id === channelId)
|
||||||
|
if (channel) {
|
||||||
|
channel.status = 'stopped'
|
||||||
|
stopTimer(channelId)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 批量发送蓝牙命令(一次性发送所有选中通道的状态)
|
||||||
|
sendBluetoothCommand('batchStop', null, null)
|
||||||
|
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: `Stopped ${selectedChannels.value.length} channels`,
|
title: `Stopped ${selectedChannels.value.length} channels`,
|
||||||
icon: 'success'
|
icon: 'success'
|
||||||
@@ -591,7 +615,7 @@ const stopTimer = (channelId) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送蓝牙命令
|
// 发送蓝牙命令(根据协议规范)
|
||||||
const sendBluetoothCommand = (command, channelId, value) => {
|
const sendBluetoothCommand = (command, channelId, value) => {
|
||||||
if (!connected.value) {
|
if (!connected.value) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
@@ -607,43 +631,144 @@ const sendBluetoothCommand = (command, channelId, value) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 构造简短的 JSON 命令(减少字节数)
|
const channelIndex = channelId ? parseInt(channelId.replace('CH', '')) - 1 : 0
|
||||||
const commandData = {
|
|
||||||
c: command, // cmd 缩写为 c
|
|
||||||
n: channelId, // channel 缩写为 n
|
|
||||||
v: value || 0 // value 缩写为 v
|
|
||||||
}
|
|
||||||
|
|
||||||
const jsonStr = JSON.stringify(commandData)
|
switch(command) {
|
||||||
console.log('Sending Bluetooth command:', jsonStr, `(${jsonStr.length} bytes)`)
|
case 'start':
|
||||||
|
// 写入电机状态(0xFF01)
|
||||||
|
writeMotorStatus(channelIndex, 3) // 3=运行
|
||||||
|
break
|
||||||
|
|
||||||
// 检查是否需要分包
|
case 'stop':
|
||||||
if (jsonStr.length <= 20) {
|
// 写入电机状态(0xFF01)
|
||||||
// 单包发送
|
writeMotorStatus(channelIndex, 1) // 1=停止
|
||||||
sendSinglePacket(jsonStr)
|
break
|
||||||
} else {
|
|
||||||
// 分包发送
|
case 'batchStart':
|
||||||
sendMultiplePackets(jsonStr)
|
// 批量启动 - 一次性发送所有选中通道的状态
|
||||||
|
writeMotorStatus(0, 3, true, selectedChannels.value)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'batchStop':
|
||||||
|
// 批量停止 - 一次性发送所有选中通道的状态
|
||||||
|
writeMotorStatus(0, 1, true, selectedChannels.value)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'setRunValue':
|
||||||
|
// 写入电机速度(0xFF02)
|
||||||
|
// 注意:速度范围是 0-100,不需要转换
|
||||||
|
writeMotorSpeed(channelIndex, value)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'setTimer':
|
||||||
|
// 写入电机定时设置(0xFF04),单位:分钟
|
||||||
|
writeMotorTimer(channelIndex, value) // 直接使用分钟数
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'query':
|
||||||
|
// 查询电机所有属性(0xFF06)- 通过读取实现
|
||||||
|
readMotorAllProperties()
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'queryRemainingTime':
|
||||||
|
// 查询电机定时剩余时间(0xFF05)
|
||||||
|
readMotorRemainingTime()
|
||||||
|
break
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to construct Bluetooth command:', error)
|
console.error('Failed to send Bluetooth command:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送单个数据包
|
// 写入电机状态(0xFF01) - 格式:电机序号 + 状态值
|
||||||
const sendSinglePacket = (data) => {
|
// 支持单个或批量操作
|
||||||
const buffer = stringToArrayBuffer(data)
|
const writeMotorStatus = (channelIndex, status, isBatch = false, batchChannels = []) => {
|
||||||
|
// 写格式:长度可变,每两个字节表示一组,采用电机序号+电机状态的方式
|
||||||
|
// 单个:[1, 2] 表示设置第1个电机为状态2(定时)
|
||||||
|
// 批量:[1, 2, 2, 3, 3, 2] 表示设置第1个电机状态2,第2个电机状态3,第3个电机状态2
|
||||||
|
|
||||||
|
let channels = []
|
||||||
|
if (isBatch && batchChannels.length > 0) {
|
||||||
|
// 批量操作
|
||||||
|
channels = batchChannels.map(chId => {
|
||||||
|
const idx = parseInt(chId.replace('CH', '')) - 1
|
||||||
|
return { index: idx, status: status }
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 单个操作
|
||||||
|
channels = [{ index: channelIndex, status: status }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = new ArrayBuffer(channels.length * 2)
|
||||||
|
const dataView = new Uint8Array(buffer)
|
||||||
|
|
||||||
|
channels.forEach((ch, i) => {
|
||||||
|
dataView[i * 2] = ch.index + 1 // 电机序号(1-6)
|
||||||
|
dataView[i * 2 + 1] = ch.status // 状态值(0=初始化, 1=停止, 2=定时, 3=运行)
|
||||||
|
})
|
||||||
|
|
||||||
|
writeBLEData('0xFF01', buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入电机速度(0xFF02) - 格式:电机序号 + 速度值
|
||||||
|
// 支持单个或批量操作
|
||||||
|
const writeMotorSpeed = (channelIndex, speed, isBatch = false, batchChannels = []) => {
|
||||||
|
// 写格式:长度可变,每两个字节表示一组,采用电机序号+电机速度的方式
|
||||||
|
// 单个:[1, 50] 表示设置第1个电机速度50
|
||||||
|
// 批量:[1, 50, 2, 60] 表示设置第1个电机速度50,第2个电机速度60
|
||||||
|
|
||||||
|
let channels = []
|
||||||
|
if (isBatch && batchChannels.length > 0) {
|
||||||
|
// 批量操作 - 使用各通道自己的速度
|
||||||
|
channels = batchChannels.map(chId => {
|
||||||
|
const idx = parseInt(chId.replace('CH', '')) - 1
|
||||||
|
const ch = channels.value[idx]
|
||||||
|
return { index: idx, speed: ch.runValue }
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 单个操作
|
||||||
|
channels = [{ index: channelIndex, speed: speed }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = new ArrayBuffer(channels.length * 2)
|
||||||
|
const dataView = new Uint8Array(buffer)
|
||||||
|
|
||||||
|
channels.forEach((ch, i) => {
|
||||||
|
dataView[i * 2] = ch.index + 1 // 电机序号(1-6)
|
||||||
|
dataView[i * 2 + 1] = Math.min(100, Math.max(0, ch.speed)) // 速度值(0-100)
|
||||||
|
})
|
||||||
|
|
||||||
|
writeBLEData('0xFF02', buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入电机定时设置(0xFF04) - 格式:电机序号 + 定时时间(小端序)
|
||||||
|
const writeMotorTimer = (channelIndex, minutes) => {
|
||||||
|
// 写格式:长度可变,每3个字节表示一组,采用电机序号+电机定时时间的方式
|
||||||
|
// 例如:[1, 2, 5] 表示第1个电机定时1282分钟(2 + 5*256 = 1282)
|
||||||
|
const buffer = new ArrayBuffer(3)
|
||||||
|
const dataView = new Uint8Array(buffer)
|
||||||
|
|
||||||
|
dataView[0] = channelIndex + 1 // 电机序号(1-6)
|
||||||
|
dataView[1] = minutes & 0xFF // 定时时间低位
|
||||||
|
dataView[2] = (minutes >> 8) & 0xFF // 定时时间高位
|
||||||
|
|
||||||
|
writeBLEData('0xFF04', buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用 BLE 写入函数
|
||||||
|
const writeBLEData = (characteristicUUID, buffer) => {
|
||||||
|
console.log(`Writing to ${characteristicUUID}, ${buffer.byteLength} bytes`)
|
||||||
|
|
||||||
uni.writeBLECharacteristicValue({
|
uni.writeBLECharacteristicValue({
|
||||||
deviceId: deviceId.value,
|
deviceId: deviceId.value,
|
||||||
serviceId: serviceId.value,
|
serviceId: serviceId.value,
|
||||||
characteristicId: characteristicId.value,
|
characteristicId: characteristicUUID,
|
||||||
value: buffer,
|
value: buffer,
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
console.log('✅ Bluetooth data sent successfully')
|
console.log(`✅ Successfully wrote to ${characteristicUUID}`)
|
||||||
},
|
},
|
||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
console.error('❌ Bluetooth data send failed:', err)
|
console.error(`❌ Failed to write to ${characteristicUUID}:`, err)
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: 'Send Failed',
|
title: 'Send Failed',
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
@@ -652,39 +777,38 @@ const sendSinglePacket = (data) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分包发送(超过 20 字节)
|
// 读取电机所有属性(0xFF06)
|
||||||
const sendMultiplePackets = (data) => {
|
const readMotorAllProperties = () => {
|
||||||
const maxPacketSize = 20
|
uni.readBLECharacteristicValue({
|
||||||
const packets = []
|
|
||||||
|
|
||||||
// 将数据分割成多个包
|
|
||||||
for (let i = 0; i < data.length; i += maxPacketSize) {
|
|
||||||
packets.push(data.slice(i, i + maxPacketSize))
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`📦 Data needs to be sent in ${packets.length} packets`)
|
|
||||||
|
|
||||||
// 依次发送每个包(间隔 100ms)
|
|
||||||
packets.forEach((packet, index) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
const buffer = stringToArrayBuffer(packet)
|
|
||||||
|
|
||||||
uni.writeBLECharacteristicValue({
|
|
||||||
deviceId: deviceId.value,
|
deviceId: deviceId.value,
|
||||||
serviceId: serviceId.value,
|
serviceId: serviceId.value,
|
||||||
characteristicId: characteristicId.value,
|
characteristicId: '0xFF06',
|
||||||
value: buffer,
|
success: (res) => {
|
||||||
success: () => {
|
console.log('✅ Successfully read all properties')
|
||||||
console.log(`✅ Packet ${index + 1}/${packets.length} sent successfully`)
|
|
||||||
},
|
},
|
||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
console.error(`❌ Packet ${index + 1}/${packets.length} send failed:`, err)
|
console.error('❌ Failed to read all properties:', err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, index * 100)
|
}
|
||||||
|
|
||||||
|
// 读取电机定时剩余时间(0xFF05)
|
||||||
|
const readMotorRemainingTime = () => {
|
||||||
|
uni.readBLECharacteristicValue({
|
||||||
|
deviceId: deviceId.value,
|
||||||
|
serviceId: serviceId.value,
|
||||||
|
characteristicId: '0xFF05',
|
||||||
|
success: (res) => {
|
||||||
|
console.log('✅ Successfully read remaining time')
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('❌ Failed to read remaining time:', err)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 注:sendSinglePacket 和 sendMultiplePackets 已被新的协议方式替代,保留作为备用
|
||||||
|
|
||||||
// 字符串转 ArrayBuffer(辅助函数)
|
// 字符串转 ArrayBuffer(辅助函数)
|
||||||
const stringToArrayBuffer = (str) => {
|
const stringToArrayBuffer = (str) => {
|
||||||
const buffer = new ArrayBuffer(str.length)
|
const buffer = new ArrayBuffer(str.length)
|
||||||
@@ -732,102 +856,220 @@ const initBluetoothCommunication = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听特征值变化(接收数据)
|
// 监听特征值变化(接收数据)- 启用所有相关特征值的通知
|
||||||
if (notifyCharacteristicId.value) {
|
const characteristicsToNotify = [
|
||||||
|
{ id: '0xFF01', name: '电机状态' },
|
||||||
|
{ id: '0xFF02', name: '电机速度' },
|
||||||
|
{ id: '0xFF03', name: '运行方向' },
|
||||||
|
{ id: '0xFF04', name: '定时设置' },
|
||||||
|
{ id: '0xFF05', name: '剩余时间' },
|
||||||
|
{ id: '0xFF06', name: '所有属性' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 注册数据监听回调
|
||||||
uni.onBLECharacteristicValueChange((res) => {
|
uni.onBLECharacteristicValueChange((res) => {
|
||||||
console.log('收到蓝牙数据:', res)
|
console.log('收到蓝牙数据:', res)
|
||||||
|
|
||||||
|
// 根据特征值ID处理数据
|
||||||
|
const dataView = new Uint8Array(res.value)
|
||||||
|
const dataLength = dataView.length
|
||||||
|
const charId = res.characteristicId
|
||||||
|
|
||||||
|
if (dataLength === 6) {
|
||||||
|
// 6字节数据,根据特征值ID区分
|
||||||
|
if (charId === '0xFF01') {
|
||||||
|
// 电机状态
|
||||||
|
channels.value.forEach((channel, index) => {
|
||||||
|
const status = dataView[index]
|
||||||
|
const statusMap = { 0: 'stopped', 1: 'stopped', 2: 'timing', 3: 'running' }
|
||||||
|
channel.status = statusMap[status] || 'stopped'
|
||||||
|
console.log(`${channel.name} 状态:`, channel.status)
|
||||||
|
})
|
||||||
|
} else if (charId === '0xFF02') {
|
||||||
|
// 电机速度
|
||||||
|
channels.value.forEach((channel, index) => {
|
||||||
|
const speed = dataView[index]
|
||||||
|
channel.runValue = speed
|
||||||
|
console.log(`${channel.name} 速度:`, speed)
|
||||||
|
})
|
||||||
|
} else if (charId === '0xFF03') {
|
||||||
|
// 运行方向
|
||||||
|
channels.value.forEach((channel, index) => {
|
||||||
|
const direction = dataView[index]
|
||||||
|
console.log(`${channel.name} 方向:`, direction === 0 ? '正转' : '反转')
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 未知特征值,使用智能判断
|
||||||
|
parseMotorStatusOrSpeed(dataView)
|
||||||
|
}
|
||||||
|
} else if (dataLength === 12) {
|
||||||
|
// 12字节数据,根据特征值ID区分
|
||||||
|
if (charId === '0xFF04') {
|
||||||
|
parseMotorTimer(dataView, 'setup') // 定时设置
|
||||||
|
} else if (charId === '0xFF05') {
|
||||||
|
parseMotorTimer(dataView, 'remaining') // 剩余时间
|
||||||
|
} else {
|
||||||
|
parseMotorTimer(dataView, 'unknown') // 未知,智能判断
|
||||||
|
}
|
||||||
|
} else if (dataLength === 42) {
|
||||||
|
// 42字节数据 - 所有属性
|
||||||
|
parseMotorAllProperties(dataView)
|
||||||
|
} else {
|
||||||
|
// 其他长度数据,使用通用处理
|
||||||
handleBluetoothData(res.value)
|
handleBluetoothData(res.value)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 启用通知
|
// 为每个特征值启用通知
|
||||||
|
characteristicsToNotify.forEach((char, index) => {
|
||||||
|
setTimeout(() => {
|
||||||
uni.notifyBLECharacteristicValueChange({
|
uni.notifyBLECharacteristicValueChange({
|
||||||
deviceId: deviceId.value,
|
deviceId: deviceId.value,
|
||||||
serviceId: serviceId.value,
|
serviceId: serviceId.value,
|
||||||
characteristicId: notifyCharacteristicId.value,
|
characteristicId: char.id,
|
||||||
state: true,
|
state: true,
|
||||||
success: () => {
|
success: () => {
|
||||||
console.log('启用蓝牙数据监听成功')
|
console.log(`启用 ${char.name}(${char.id}) 数据监听成功`)
|
||||||
|
if (index === characteristicsToNotify.length - 1) {
|
||||||
hasRegisteredListener.value = true
|
hasRegisteredListener.value = true
|
||||||
// 启动定时查询
|
// 启动定时查询
|
||||||
startDataQuery()
|
startDataQuery()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
console.error('启用蓝牙数据监听失败:', err)
|
console.warn(`启用 ${char.name}(${char.id}) 数据监听失败:`, err)
|
||||||
hasRegisteredListener.value = false
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
}, index * 100) // 间隔 100ms 避免冲突
|
||||||
hasRegisteredListener.value = true
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理接收到的蓝牙数据
|
// 处理接收到的蓝牙数据(根据协议规范解析)
|
||||||
const handleBluetoothData = (buffer) => {
|
const handleBluetoothData = (buffer) => {
|
||||||
try {
|
try {
|
||||||
// 将 ArrayBuffer 转换为字符串
|
const dataView = new Uint8Array(buffer)
|
||||||
const dataStr = arrayBufferToString(buffer)
|
const dataLength = dataView.length
|
||||||
console.log('收到数据片段:', dataStr)
|
|
||||||
|
|
||||||
// 处理分包数据:将数据追加到缓冲区
|
console.log('收到蓝牙数据:', dataLength, '字节')
|
||||||
receiveBuffer.value += dataStr
|
|
||||||
|
|
||||||
// 尝试解析完整的 JSON 数据
|
// 根据数据长度判断是哪个特征值的数据
|
||||||
try {
|
if (dataLength === 6) {
|
||||||
// 查找完整的 JSON 对象(以 { 开始,} 结束)
|
// 可能是电机状态(0xFF01)或电机速度(0xFF02)或运行方向(0xFF03)
|
||||||
const jsonMatch = receiveBuffer.value.match(/\{[^}]*\}/)
|
// 需要根据实际情况判断,这里先处理状态和速度
|
||||||
if (jsonMatch) {
|
parseMotorStatusOrSpeed(dataView)
|
||||||
const jsonStr = jsonMatch[0]
|
} else if (dataLength === 12) {
|
||||||
const data = JSON.parse(jsonStr)
|
// 电机定时设置(0xFF04)或剩余时间(0xFF05)
|
||||||
|
// 注意:这里无法直接区分,需要通过上下文(最近的读取操作)来判断
|
||||||
// 解析成功,更新通道状态
|
// 建议:在监听回调中记录特征值UUID
|
||||||
updateChannelFromDevice(data)
|
parseMotorTimer(dataView, 'unknown') // 传入类型标识
|
||||||
|
} else if (dataLength === 42) {
|
||||||
// 清空已处理的数据(保留剩余未处理的数据)
|
// 电机所有属性(0xFF06)
|
||||||
receiveBuffer.value = receiveBuffer.value.substring(jsonMatch.index + jsonStr.length)
|
parseMotorAllProperties(dataView)
|
||||||
} else {
|
} else {
|
||||||
// 如果缓冲区太大但没有完整 JSON,可能是垃圾数据,清空缓冲区
|
console.warn('未知的数据长度:', dataLength)
|
||||||
if (receiveBuffer.value.length > 200) {
|
|
||||||
console.warn('缓冲区数据过大且无法解析,清空缓冲区:', receiveBuffer.value)
|
|
||||||
receiveBuffer.value = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// JSON 解析失败,可能是数据还不完整,等待更多数据
|
|
||||||
console.log('等待更多数据片段..., 当前缓冲区:', receiveBuffer.value)
|
|
||||||
|
|
||||||
// 如果缓冲区太大,清空它
|
|
||||||
if (receiveBuffer.value.length > 200) {
|
|
||||||
console.warn('缓冲区溢出,清空:', receiveBuffer.value)
|
|
||||||
receiveBuffer.value = ''
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('解析蓝牙数据失败:', error)
|
console.error('解析蓝牙数据失败:', error)
|
||||||
receiveBuffer.value = '' // 出错时清空缓冲区
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据设备返回的数据更新通道状态
|
// 解析电机状态或速度(6字节)
|
||||||
const updateChannelFromDevice = (data) => {
|
// 注意:无法通过数值范围准确区分,建议通过特征值ID区分
|
||||||
if (!data.channel) return
|
const parseMotorStatusOrSpeed = (dataView) => {
|
||||||
|
// 暂时策略:如果所有值都 <= 3,认为是状态;否则认为是速度
|
||||||
const channel = channels.value.find(c => c.id === data.channel)
|
let allSmall = true
|
||||||
if (channel) {
|
for (let i = 0; i < 6; i++) {
|
||||||
// 更新通道状态(根据实际数据格式调整)
|
if (dataView[i] > 3) {
|
||||||
if (data.status !== undefined) {
|
allSmall = false
|
||||||
channel.status = data.status // running/stopped/timing
|
break
|
||||||
}
|
}
|
||||||
if (data.speed !== undefined) {
|
|
||||||
channel.runValue = Math.round(data.speed / 4) // 实际转速转回 0-100
|
|
||||||
}
|
|
||||||
if (data.remainingTime !== undefined) {
|
|
||||||
channel.remainingTime = data.remainingTime
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`通道 ${data.channel} 状态已更新:`, channel)
|
channels.value.forEach((channel, index) => {
|
||||||
|
const value = dataView[index]
|
||||||
|
|
||||||
|
if (allSmall) {
|
||||||
|
// 认为是电机状态
|
||||||
|
const statusMap = { 0: 'stopped', 1: 'stopped', 2: 'timing', 3: 'running' }
|
||||||
|
channel.status = statusMap[value] || 'stopped'
|
||||||
|
console.log(`${channel.name} 状态:`, channel.status)
|
||||||
|
} else {
|
||||||
|
// 认为是电机速度(0-100)
|
||||||
|
channel.runValue = value
|
||||||
|
console.log(`${channel.name} 速度:`, value)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 解析电机定时(12字节)
|
||||||
|
// type: 'setup' = 定时设置(0xFF04), 'remaining' = 剩余时间(0xFF05), 'unknown' = 未知
|
||||||
|
const parseMotorTimer = (dataView, type = 'unknown') => {
|
||||||
|
channels.value.forEach((channel, index) => {
|
||||||
|
// 低位在前,高位在后(小端序)
|
||||||
|
const minutes = dataView[index * 2] | (dataView[index * 2 + 1] << 8)
|
||||||
|
|
||||||
|
if (minutes > 0) {
|
||||||
|
if (type === 'remaining') {
|
||||||
|
// 明确是剩余时间(0xFF05)
|
||||||
|
channel.remainingTime = minutes * 60 // 转为秒
|
||||||
|
console.log(`${channel.name} 剩余时间:`, minutes, '分钟')
|
||||||
|
} else if (type === 'setup') {
|
||||||
|
// 明确是定时设置(0xFF04)
|
||||||
|
channel.timerMinutes = minutes
|
||||||
|
channel.timerValue = `Set ${minutes} min`
|
||||||
|
console.log(`${channel.name} 定时设置:`, minutes, '分钟')
|
||||||
|
} else {
|
||||||
|
// 未知类型,根据当前状态智能判断(兼容旧逻辑)
|
||||||
|
if (channel.status === 'timing' && channel.remainingTime > 0) {
|
||||||
|
// 如果正在定时中且已有剩余时间,优先更新剩余时间
|
||||||
|
channel.remainingTime = minutes * 60
|
||||||
|
console.log(`${channel.name} 剩余时间(推测):`, minutes, '分钟')
|
||||||
|
} else {
|
||||||
|
// 否则当作定时设置
|
||||||
|
channel.timerMinutes = minutes
|
||||||
|
channel.timerValue = `Set ${minutes} min`
|
||||||
|
console.log(`${channel.name} 定时设置(推测):`, minutes, '分钟')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析电机所有属性(42字节)
|
||||||
|
const parseMotorAllProperties = (dataView) => {
|
||||||
|
// 格式:6字节状态 + 6字节速度 + 6字节方向 + 12字节定时设置 + 12字节剩余时间
|
||||||
|
|
||||||
|
channels.value.forEach((channel, index) => {
|
||||||
|
// 电机状态(0-5字节)
|
||||||
|
const status = dataView[index]
|
||||||
|
const statusMap = { 0: 'stopped', 1: 'stopped', 2: 'timing', 3: 'running' }
|
||||||
|
channel.status = statusMap[status] || 'stopped'
|
||||||
|
|
||||||
|
// 电机速度(6-11字节)
|
||||||
|
const speed = dataView[6 + index]
|
||||||
|
channel.runValue = speed
|
||||||
|
|
||||||
|
// 电机方向(12-17字节)
|
||||||
|
const direction = dataView[12 + index]
|
||||||
|
// 可以根据需要存储方向信息
|
||||||
|
|
||||||
|
// 定时设置(18-29字节)
|
||||||
|
const timerMinutes = dataView[18 + index * 2] | (dataView[18 + index * 2 + 1] << 8)
|
||||||
|
if (timerMinutes > 0) {
|
||||||
|
channel.timerMinutes = timerMinutes
|
||||||
|
channel.timerValue = `Set ${timerMinutes} min`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 剩余时间(30-41字节)
|
||||||
|
const remainingMinutes = dataView[30 + index * 2] | (dataView[30 + index * 2 + 1] << 8)
|
||||||
|
channel.remainingTime = remainingMinutes * 60 // 转为秒
|
||||||
|
|
||||||
|
console.log(`${channel.name} 完整数据 - 状态:${channel.status}, 速度:${speed}, 方向:${direction}, 定时:${timerMinutes}分钟, 剩余:${remainingMinutes}分钟`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注:updateChannelFromDevice 已被新的协议解析方式替代
|
||||||
|
|
||||||
// 启动定时查询所有通道数据
|
// 启动定时查询所有通道数据
|
||||||
const startDataQuery = () => {
|
const startDataQuery = () => {
|
||||||
// 如果已经在查询,先停止
|
// 如果已经在查询,先停止
|
||||||
@@ -855,7 +1097,7 @@ const stopDataQuery = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询所有通道数据
|
// 查询所有通道数据(包括状态、速度、剩余时间等)
|
||||||
const queryAllChannelsData = () => {
|
const queryAllChannelsData = () => {
|
||||||
if (!connected.value || isQuerying.value) {
|
if (!connected.value || isQuerying.value) {
|
||||||
return
|
return
|
||||||
@@ -863,17 +1105,20 @@ const queryAllChannelsData = () => {
|
|||||||
|
|
||||||
isQuerying.value = true
|
isQuerying.value = true
|
||||||
|
|
||||||
// 依次查询每个通道(根据实际协议调整)
|
// 优先查询完整属性(0xFF06),包含所有信息
|
||||||
channels.value.forEach((channel, index) => {
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
sendBluetoothCommand('query', channel.id)
|
sendBluetoothCommand('query', null)
|
||||||
}, index * 200) // 每个通道间隔 200ms,避免数据冲突
|
}, 0)
|
||||||
})
|
|
||||||
|
// 备选:单独查询剩余时间(0xFF05)
|
||||||
|
setTimeout(() => {
|
||||||
|
sendBluetoothCommand('queryRemainingTime', null)
|
||||||
|
}, 500)
|
||||||
|
|
||||||
// 查询完成后重置标志
|
// 查询完成后重置标志
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
isQuerying.value = false
|
isQuerying.value = false
|
||||||
}, channels.value.length * 200 + 500)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 页面卸载
|
// 页面卸载
|
||||||
|
|||||||
Reference in New Issue
Block a user