From 2c9197833b69a658b5bb68b21773e631cce58907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E5=BE=AE=E4=B8=80=E7=AC=91?= <709648985@qq.com> Date: Wed, 18 Mar 2026 15:04:49 +0800 Subject: [PATCH] =?UTF-8?q?100J=E8=BF=98=E5=B7=AE=E8=AF=AD=E9=9F=B3?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E8=93=9D=E7=89=99=E4=B8=8A=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/100J/HBY100-J.js | 192 ++++++++++++++++++++++++++-------------- pages/100J/HBY100-J.vue | 135 +++++++++++++++++++++++----- utils/BleHelper.js | 15 ++++ 3 files changed, 256 insertions(+), 86 deletions(-) diff --git a/api/100J/HBY100-J.js b/api/100J/HBY100-J.js index 9966781..63f2d03 100644 --- a/api/100J/HBY100-J.js +++ b/api/100J/HBY100-J.js @@ -27,15 +27,27 @@ class HBY100JProtocol { } parseBleData(buffer) { - const view = new Uint8Array(buffer); + const view = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer); if (view.length < 3) return null; const header = view[0]; const tail = view[view.length - 1]; + + // 5.1 连接蓝牙设备主动上报 MAC 地址: FC + 6字节MAC + FF + if (header === 0xFC && tail === 0xFF && view.length >= 8) { + const macBytes = view.slice(1, 7); + const macAddress = Array.from(macBytes).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(':'); + const result = { type: 'mac', macAddress }; + console.log('[100J-蓝牙] 设备上报MAC:', macAddress, '原始:', Array.from(view).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' ')); + if (this.onNotifyCallback) this.onNotifyCallback(result); + return result; + } + if (header !== 0xFB || tail !== 0xFF) return null; // 校验头尾 const funcCode = view[1]; const data = view.slice(2, view.length - 1); + const hexStr = Array.from(view).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' '); let result = { funcCode, rawData: data }; @@ -44,11 +56,11 @@ class HBY100JProtocol { case 0x02: break; case 0x03: break; case 0x04: - // 04: 获取电源状态 (根据协议图解析) - // 假设电量百分比在第16字节,具体需要根据你提供的协议图来定 - // 这里我按照协议图的结构,如果电量百分比在第16字节: - if (data.length >= 17) { - result.batteryPercentage = data[16]; + // 5.5 获取设备电源状态: 电池容量8B + 电压8B + 百分比1B + 车载电源1B + 续航时间2B(分钟) + if (data.length >= 20) { + result.batteryPercentage = data[16]; + result.vehiclePower = data[17]; + result.batteryRemainingTime = data[18] | (data[19] << 8); // 小端序,单位分钟 } break; case 0x06: @@ -94,6 +106,10 @@ class HBY100JProtocol { break; } + const funcNames = { 0x01: '复位', 0x02: '基础信息', 0x03: '位置', 0x04: '电源状态', 0x05: '文件更新', 0x06: '语音播报', 0x09: '音量', 0x0A: '爆闪模式', 0x0B: '爆闪频率', 0x0C: '强制报警', 0x0D: 'LED亮度', 0x0E: '工作方式' }; + const name = funcNames[funcCode] || ('0x' + funcCode.toString(16)); + console.log('[100J-蓝牙] 设备响应 FB:', name, '解析:', JSON.stringify(result), '原始:', hexStr); + if (this.onNotifyCallback) { this.onNotifyCallback(result); } @@ -114,6 +130,8 @@ class HBY100JProtocol { view[2 + i] = dataBytes[i]; } view[view.length - 1] = 0xFF; // 结尾 + const sendHex = Array.from(view).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' '); + console.log('[100J-蓝牙] 下发指令 FA:', '0x' + funcCode.toString(16).toUpperCase(), sendHex); // 使用项目中统一的 BleHelper 发送数据 import('@/utils/BleHelper.js').then(module => { @@ -127,6 +145,9 @@ class HBY100JProtocol { // 纯蓝牙指令发送方法 deviceReset(type = 0) { return this.sendBleData(0x01, [type]); } + getBasicInfo() { return this.sendBleData(0x02, []); } + getLocation() { return this.sendBleData(0x03, []); } + getPowerStatus() { return this.sendBleData(0x04, []); } setVoiceBroadcast(enable) { return this.sendBleData(0x06, [enable]); } setVolume(volume) { return this.sendBleData(0x09, [volume]); } setStrobeMode(enable, mode) { return this.sendBleData(0x0A, [enable, mode]); } @@ -143,6 +164,7 @@ const protocolInstance = new HBY100JProtocol(); export function updateBleStatus(isConnected, bleDeviceId, deviceId) { protocolInstance.setBleConnectionStatus(isConnected, bleDeviceId); protocolInstance.deviceId = deviceId; + console.log('[100J] 蓝牙状态:', isConnected ? '已连接(后续指令走蓝牙)' : '已断开(后续指令走4G)', { bleDeviceId: bleDeviceId || '-', deviceId }); } // 暴露给页面:解析蓝牙接收到的数据 @@ -150,6 +172,46 @@ export function parseBleData(buffer) { return protocolInstance.parseBleData(buffer); } +// 暴露给页面:蓝牙连接后主动拉取电源状态(电量、续航) +export function fetchBlePowerStatus() { + if (!protocolInstance.isBleConnected) return Promise.reject(new Error('蓝牙未连接')); + console.log('[100J-蓝牙] 拉取电源状态 已通过蓝牙发送 FA 04 FF'); + return protocolInstance.getPowerStatus(); +} + +// 暴露给页面:尝试重连蓝牙(优先策略:断线后发指令前先尝试重连) +export function tryReconnectBle(timeoutMs = 2500) { + if (protocolInstance.isBleConnected) return Promise.resolve(true); + if (!protocolInstance.bleDeviceId) return Promise.resolve(false); + return new Promise((resolve) => { + import('@/utils/BleHelper.js').then(module => { + const bleTool = module.default.getBleTool(); + const deviceId = protocolInstance.bleDeviceId; + const f = bleTool.data.LinkedList.find(v => v.deviceId === deviceId); + if (!f) { + resolve(false); + return; + } + const svc = f.writeServiceId || '0000AE30-0000-1000-8000-00805F9B34FB'; + const write = f.wirteCharactId || '0000AE03-0000-1000-8000-00805F9B34FB'; + const notify = f.notifyCharactId || '0000AE02-0000-1000-8000-00805F9B34FB'; + const timer = setTimeout(() => { + resolve(protocolInstance.isBleConnected); + }, timeoutMs); + console.log('[100J] 蓝牙优先:尝试重连', deviceId); + bleTool.LinkBlue(deviceId, svc, write, notify, 1).then(() => { + clearTimeout(timer); + protocolInstance.setBleConnectionStatus(true, deviceId); + console.log('[100J] 蓝牙重连成功'); + resolve(true); + }).catch(() => { + clearTimeout(timer); + resolve(false); + }); + }); + }); +} + // ================== API 接口 (拦截层) ================== // 获取语音管理列表 @@ -193,86 +255,84 @@ export function deviceDetail(id) { }) } +// 优先蓝牙:未连接时先尝试重连;蓝牙发送失败时回退4G +function execWithBleFirst(bleExec, httpExec, logName) { + const doBle = () => { + return bleExec().then(res => ({ ...(res || {}), _channel: 'ble' })); + }; + const do4G = () => { + console.log('[100J-4G]', logName, '已通过HTTP发送', '(蓝牙不可用)'); + return httpExec().then(res => { res._channel = '4g'; return res; }); + }; + if (protocolInstance.isBleConnected) { + console.log('[100J-蓝牙]', logName, '(连接正常)'); + return doBle().catch(err => { + console.log('[100J] 蓝牙发送失败,回退4G', err); + return do4G(); + }); + } + return tryReconnectBle(2500).then(reconnected => { + if (reconnected) { + console.log('[100J-蓝牙]', logName, '(重连成功)'); + return doBle().catch(err => { + console.log('[100J] 蓝牙发送失败,回退4G', err); + return do4G(); + }); + } + return do4G(); + }); +} + // 爆闪模式 export function deviceStrobeMode(data) { - if (protocolInstance.isBleConnected) { - return protocolInstance.setStrobeMode(data.enable, data.mode).then(res => { - return { code: 200, msg: '操作成功(蓝牙)' }; - }); - } - return request({ - url: `/app/hby100j/device/strobeMode`, - method: 'post', - data:data - }) + return execWithBleFirst( + () => protocolInstance.setStrobeMode(data.enable, data.mode).then(() => ({ code: 200, msg: '操作成功(蓝牙)' })), + () => request({ url: `/app/hby100j/device/strobeMode`, method: 'post', data }), + '爆闪模式' + ); } // 强制报警 export function deviceForceAlarmActivation(data) { - if (protocolInstance.isBleConnected) { - return protocolInstance.setForceAlarm(data.voiceStrobeAlarm, data.mode).then(res => { - return { code: 200, msg: '操作成功(蓝牙)' }; - }); - } - return request({ - url: `/app/hby100j/device/forceAlarmActivation`, - method: 'post', - data:data - }) + return execWithBleFirst( + () => protocolInstance.setForceAlarm(data.voiceStrobeAlarm, data.mode).then(() => ({ code: 200, msg: '操作成功(蓝牙)' })), + () => request({ url: `/app/hby100j/device/forceAlarmActivation`, method: 'post', data }), + '强制报警' + ); } // 爆闪频率 export function deviceStrobeFrequency(data) { - if (protocolInstance.isBleConnected) { - return protocolInstance.setStrobeFrequency(data.frequency).then(res => { - return { code: 200, msg: '操作成功(蓝牙)' }; - }); - } - return request({ - url: `/app/hby100j/device/strobeFrequency`, - method: 'post', - data:data - }) + return execWithBleFirst( + () => protocolInstance.setStrobeFrequency(data.frequency).then(() => ({ code: 200, msg: '操作成功(蓝牙)' })), + () => request({ url: `/app/hby100j/device/strobeFrequency`, method: 'post', data }), + '爆闪频率' + ); } // 灯光调节亮度 export function deviceLightAdjustment(data) { - if (protocolInstance.isBleConnected) { - return protocolInstance.setLightBrightness(data.brightness, data.brightness, data.brightness).then(res => { - return { code: 200, msg: '操作成功(蓝牙)' }; - }); - } - return request({ - url: `/app/hby100j/device/lightAdjustment`, - method: 'post', - data:data - }) + return execWithBleFirst( + () => protocolInstance.setLightBrightness(data.brightness, data.brightness, data.brightness).then(() => ({ code: 200, msg: '操作成功(蓝牙)' })), + () => request({ url: `/app/hby100j/device/lightAdjustment`, method: 'post', data }), + '灯光亮度' + ); } // 调节音量 export function deviceUpdateVolume(data) { - if (protocolInstance.isBleConnected) { - return protocolInstance.setVolume(data.volume).then(res => { - return { code: 200, msg: '操作成功(蓝牙)' }; - }); - } - return request({ - url: `/app/hby100j/device/updateVolume`, - method: 'post', - data:data - }) + return execWithBleFirst( + () => protocolInstance.setVolume(data.volume).then(() => ({ code: 200, msg: '操作成功(蓝牙)' })), + () => request({ url: `/app/hby100j/device/updateVolume`, method: 'post', data }), + '调节音量' + ); } // 语音播放 export function deviceVoiceBroadcast(data) { - if (protocolInstance.isBleConnected) { - return protocolInstance.setVoiceBroadcast(data.voiceBroadcast).then(res => { - return { code: 200, msg: '操作成功(蓝牙)' }; - }); - } - return request({ - url: `/app/hby100j/device/voiceBroadcast`, - method: 'post', - data:data - }) + return execWithBleFirst( + () => protocolInstance.setVoiceBroadcast(data.voiceBroadcast).then(() => ({ code: 200, msg: '操作成功(蓝牙)' })), + () => request({ url: `/app/hby100j/device/voiceBroadcast`, method: 'post', data }), + '语音播报' + ); } \ No newline at end of file diff --git a/pages/100J/HBY100-J.vue b/pages/100J/HBY100-J.vue index 30beb8c..929253c 100644 --- a/pages/100J/HBY100-J.vue +++ b/pages/100J/HBY100-J.vue @@ -243,7 +243,8 @@ deviceUpdateVolume, deviceVoiceBroadcast, updateBleStatus, - parseBleData + parseBleData, + fetchBlePowerStatus } from '@/api/100J/HBY100-J.js' import BleHelper from '@/utils/BleHelper.js'; var bleTool = BleHelper.getBleTool(); @@ -604,17 +605,9 @@ console.log(this.activePermissions, 'this.activePermissions'); these.fetchDeviceDetail(data.data.deviceId) } - // 尝试连接蓝牙 + // 尝试连接蓝牙:需先扫描获取 BLE deviceId,不能直接用 MAC if (data.data.deviceMac) { - // 假设 deviceMac 是蓝牙的 deviceId - bleTool.LinkBlue(data.data.deviceMac).then(() => { - console.log("100J 蓝牙连接成功"); - this.bleStateRecovry({ - deviceId: data.data.deviceMac - }); - }).catch(err => { - console.log("100J 蓝牙连接失败,将使用4G", err); - }); + these.tryConnect100JBle(data.data.deviceMac); } }); this.createThrottledFunctions(); @@ -639,6 +632,8 @@ bleTool.removeRecoveryCallback("HBY100J"); bleTool.removeStateBreakCallback("HBY100J"); bleTool.removeStateRecoveryCallback("HBY100J"); + bleTool.removeDeviceFound("HBY100J_SCAN"); + bleTool.StopSearch(); }, onShow() { this.Status.pageHide = false; @@ -993,24 +988,124 @@ deviceRecovry(res) {}, deviceDispose(res) {}, + // 100J 蓝牙连接:先查缓存/尝试直连,失败则扫描(createBLEConnection 需要扫描返回的 deviceId) + tryConnect100JBle(deviceMac) { + const that = this; + const macNorm = (m) => (m || '').replace(/:/g, '').toUpperCase(); + const targetMacNorm = macNorm(deviceMac); + const last6 = targetMacNorm.slice(-6); + + // 1. 查缓存:之前连过且 mac 匹配 + const cached = bleTool.data.LinkedList.find(v => { + const m = macNorm(v.macAddress); + return m === targetMacNorm || m.slice(-6) === last6; + }); + if (cached && cached.deviceId) { + console.log('[100J] 使用缓存设备连接', cached.deviceId); + bleTool.LinkBlue(cached.deviceId).then(() => { + console.log('100J 蓝牙连接成功(缓存)'); + that.bleStateRecovry({ deviceId: cached.deviceId }); + }).catch(err => { + console.log('100J 蓝牙连接失败(缓存),尝试扫描', err); + that.connect100JByScan(deviceMac, last6); + }); + return; + } + + // 2. 无缓存:先尝试直连(Android 上 deviceId 可能为 MAC) + console.log('[100J] 尝试直连', deviceMac); + bleTool.LinkBlue(deviceMac).then(() => { + console.log('100J 蓝牙连接成功(直连)'); + that.bleStateRecovry({ deviceId: deviceMac }); + }).catch(err => { + console.log('100J 蓝牙直连失败,开始扫描', err); + that.connect100JByScan(deviceMac, last6); + }); + }, + connect100JByScan(deviceMac, last6) { + const that = this; + let resolved = false; + const timeout = 15000; + const timer = setTimeout(() => { + if (resolved) return; + resolved = true; + bleTool.StopSearch(); + bleTool.removeDeviceFound('HBY100J_SCAN'); + console.log('100J 蓝牙扫描超时,将使用4G'); + }, timeout); + + bleTool.addDeviceFound((res) => { + if (resolved) return; + const devices = res.devices || []; + const match = devices.find(d => { + const name = (d.name || '').replace(/\r\n/g, '').trim(); + return name === 'HBY100J' || name.startsWith('LED-') && name.slice(-6) === last6; + }); + if (match) { + resolved = true; + clearTimeout(timer); + bleTool.StopSearch(); + bleTool.removeDeviceFound('HBY100J_SCAN'); + console.log('[100J] 扫描到目标设备', match.name, match.deviceId); + bleTool.LinkBlue(match.deviceId).then(() => { + console.log('100J 蓝牙连接成功(扫描)'); + that.bleStateRecovry({ deviceId: match.deviceId }); + }).catch(err => { + console.log('100J 蓝牙连接失败,将使用4G', err); + }); + } + }, 'HBY100J_SCAN'); + + bleTool.StartSearch().then(() => { + console.log('[100J] 开始扫描蓝牙,设备名 HBY100J 或 LED-' + last6); + }).catch(err => { + if (!resolved) { + resolved = true; + clearTimeout(timer); + bleTool.removeDeviceFound('HBY100J_SCAN'); + console.log('100J 蓝牙扫描启动失败,将使用4G', err); + } + }); + }, bleStateBreak() { updateBleStatus(false, '', this.deviceInfo.deviceId); }, bleStateRecovry(res) { - let bleDeviceId = res ? res.deviceId : ''; + // 蓝牙适配器恢复可用(关闭蓝牙后重新开启):无 deviceId,需主动重连 + if (!res || !res.deviceId) { + const mac = (this.device && this.device.deviceMac) || (this.deviceInfo && this.deviceInfo.deviceMac); + if (mac) { + console.log('[100J] 蓝牙适配器已恢复,尝试重连', mac); + this.tryConnect100JBle(mac); + } + return; + } + let bleDeviceId = res.deviceId; updateBleStatus(true, bleDeviceId, this.deviceInfo.deviceId); + // 蓝牙连接成功后主动拉取电源状态(电量、续航时间) + fetchBlePowerStatus().catch(() => {}); }, previewImg(img) {}, bleValueNotify: function(receive, device, path, recArr) { //订阅消息 - // 注意:这里 receive.deviceId 是蓝牙的 MAC 地址,而 this.formData.deviceId 是 4G 的 ID - // 所以这里需要修改判断逻辑,或者不判断直接解析 + // 解析蓝牙上报数据 (协议: FC=MAC主动上报, FB=指令响应) + if (!receive.bytes || receive.bytes.length < 3) return; + const parsedData = parseBleData(receive.bytes); + if (!parsedData) return; - // 尝试解析蓝牙上报的数据 - if (receive.bytes) { - const parsedData = parseBleData(receive.bytes); - if (parsedData && parsedData.batteryPercentage !== undefined) { - this.deviceInfo.batteryPercentage = parsedData.batteryPercentage; - } + // 5.1 连接后设备主动上报 MAC 地址 (FC + 6字节 + FF) + if (parsedData.type === 'mac' && parsedData.macAddress) { + this.formData.macAddress = parsedData.macAddress; + this.device.deviceMac = parsedData.macAddress; + this.deviceInfo.deviceMac = parsedData.macAddress; + return; + } + + // 5.5 获取设备电源状态 (0x04) + if (parsedData.batteryPercentage !== undefined) { + this.deviceInfo.batteryPercentage = parsedData.batteryPercentage; + } + if (parsedData.batteryRemainingTime !== undefined) { + this.deviceInfo.batteryRemainingTime = parsedData.batteryRemainingTime; } if (this.deviceInfo.batteryPercentage <= 20) { diff --git a/utils/BleHelper.js b/utils/BleHelper.js index 2578eb2..ba468be 100644 --- a/utils/BleHelper.js +++ b/utils/BleHelper.js @@ -954,6 +954,21 @@ class BleHelper { } else { console.log("蓝牙连接已恢复", res); + // 系统级连接恢复:更新 LinkedList 并通知业务层,避免 sendData 误判未连接而重复 createBLEConnection + let f = this.data.LinkedList.find(v => v.deviceId == res.deviceId); + if (f) { + f.Linked = true; + this.updateCache(); + } + if (this.cfg.recoveryCallback.length > 0) { + this.cfg.recoveryCallback.forEach(c => { + try { + c.callback({ deviceId: res.deviceId, connected: true }); + } catch (err) { + console.error("执行蓝牙恢复连接的回调异常", err); + } + }); + } } }, 50);