merge upstream

This commit is contained in:
2026-03-18 15:24:52 +08:00
3 changed files with 279 additions and 95 deletions

View File

@ -27,15 +27,27 @@ class HBY100JProtocol {
} }
parseBleData(buffer) { parseBleData(buffer) {
const view = new Uint8Array(buffer); const view = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
if (view.length < 3) return null; if (view.length < 3) return null;
const header = view[0]; const header = view[0];
const tail = view[view.length - 1]; 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; // 校验头尾 if (header !== 0xFB || tail !== 0xFF) return null; // 校验头尾
const funcCode = view[1]; const funcCode = view[1];
const data = view.slice(2, view.length - 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 }; let result = { funcCode, rawData: data };
@ -44,11 +56,11 @@ class HBY100JProtocol {
case 0x02: break; case 0x02: break;
case 0x03: break; case 0x03: break;
case 0x04: case 0x04:
// 04: 获取电源状态 (根据协议图解析) // 5.5 获取设备电源状态: 电池容量8B + 电压8B + 百分比1B + 车载电源1B + 续航时间2B(分钟)
// 假设电量百分比在第16字节具体需要根据你提供的协议图来定 if (data.length >= 20) {
// 这里我按照协议图的结构如果电量百分比在第16字节 result.batteryPercentage = data[16];
if (data.length >= 17) { result.vehiclePower = data[17];
result.batteryPercentage = data[16]; result.batteryRemainingTime = data[18] | (data[19] << 8); // 小端序,单位分钟
} }
break; break;
case 0x06: case 0x06:
@ -94,6 +106,10 @@ class HBY100JProtocol {
break; 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) { if (this.onNotifyCallback) {
this.onNotifyCallback(result); this.onNotifyCallback(result);
} }
@ -114,6 +130,8 @@ class HBY100JProtocol {
view[2 + i] = dataBytes[i]; view[2 + i] = dataBytes[i];
} }
view[view.length - 1] = 0xFF; // 结尾 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 发送数据 // 使用项目中统一的 BleHelper 发送数据
import('@/utils/BleHelper.js').then(module => { import('@/utils/BleHelper.js').then(module => {
@ -127,6 +145,9 @@ class HBY100JProtocol {
// 纯蓝牙指令发送方法 // 纯蓝牙指令发送方法
deviceReset(type = 0) { return this.sendBleData(0x01, [type]); } 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]); } setVoiceBroadcast(enable) { return this.sendBleData(0x06, [enable]); }
setVolume(volume) { return this.sendBleData(0x09, [volume]); } setVolume(volume) { return this.sendBleData(0x09, [volume]); }
setStrobeMode(enable, mode) { return this.sendBleData(0x0A, [enable, mode]); } setStrobeMode(enable, mode) { return this.sendBleData(0x0A, [enable, mode]); }
@ -143,6 +164,7 @@ const protocolInstance = new HBY100JProtocol();
export function updateBleStatus(isConnected, bleDeviceId, deviceId) { export function updateBleStatus(isConnected, bleDeviceId, deviceId) {
protocolInstance.setBleConnectionStatus(isConnected, bleDeviceId); protocolInstance.setBleConnectionStatus(isConnected, bleDeviceId);
protocolInstance.deviceId = deviceId; protocolInstance.deviceId = deviceId;
console.log('[100J] 蓝牙状态:', isConnected ? '已连接(后续指令走蓝牙)' : '已断开(后续指令走4G)', { bleDeviceId: bleDeviceId || '-', deviceId });
} }
// 暴露给页面:解析蓝牙接收到的数据 // 暴露给页面:解析蓝牙接收到的数据
@ -150,6 +172,46 @@ export function parseBleData(buffer) {
return protocolInstance.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 接口 (拦截层) ================== // ================== 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) { export function deviceStrobeMode(data) {
if (protocolInstance.isBleConnected) { return execWithBleFirst(
return protocolInstance.setStrobeMode(data.enable, data.mode).then(res => { () => protocolInstance.setStrobeMode(data.enable, data.mode).then(() => ({ code: 200, msg: '操作成功(蓝牙)' })),
return { code: 200, msg: '操作成功(蓝牙)' }; () => request({ url: `/app/hby100j/device/strobeMode`, method: 'post', data }),
}); '爆闪模式'
} );
return request({
url: `/app/hby100j/device/strobeMode`,
method: 'post',
data:data
})
} }
// 强制报警 // 强制报警
export function deviceForceAlarmActivation(data) { export function deviceForceAlarmActivation(data) {
if (protocolInstance.isBleConnected) { return execWithBleFirst(
return protocolInstance.setForceAlarm(data.voiceStrobeAlarm, data.mode).then(res => { () => protocolInstance.setForceAlarm(data.voiceStrobeAlarm, data.mode).then(() => ({ code: 200, msg: '操作成功(蓝牙)' })),
return { code: 200, msg: '操作成功(蓝牙)' }; () => request({ url: `/app/hby100j/device/forceAlarmActivation`, method: 'post', data }),
}); '强制报警'
} );
return request({
url: `/app/hby100j/device/forceAlarmActivation`,
method: 'post',
data:data
})
} }
// 爆闪频率 // 爆闪频率
export function deviceStrobeFrequency(data) { export function deviceStrobeFrequency(data) {
if (protocolInstance.isBleConnected) { return execWithBleFirst(
return protocolInstance.setStrobeFrequency(data.frequency).then(res => { () => protocolInstance.setStrobeFrequency(data.frequency).then(() => ({ code: 200, msg: '操作成功(蓝牙)' })),
return { code: 200, msg: '操作成功(蓝牙)' }; () => request({ url: `/app/hby100j/device/strobeFrequency`, method: 'post', data }),
}); '爆闪频率'
} );
return request({
url: `/app/hby100j/device/strobeFrequency`,
method: 'post',
data:data
})
} }
// 灯光调节亮度 // 灯光调节亮度
export function deviceLightAdjustment(data) { export function deviceLightAdjustment(data) {
if (protocolInstance.isBleConnected) { return execWithBleFirst(
return protocolInstance.setLightBrightness(data.brightness, data.brightness, data.brightness).then(res => { () => protocolInstance.setLightBrightness(data.brightness, data.brightness, data.brightness).then(() => ({ code: 200, msg: '操作成功(蓝牙)' })),
return { code: 200, msg: '操作成功(蓝牙)' }; () => request({ url: `/app/hby100j/device/lightAdjustment`, method: 'post', data }),
}); '灯光亮度'
} );
return request({
url: `/app/hby100j/device/lightAdjustment`,
method: 'post',
data:data
})
} }
// 调节音量 // 调节音量
export function deviceUpdateVolume(data) { export function deviceUpdateVolume(data) {
if (protocolInstance.isBleConnected) { return execWithBleFirst(
return protocolInstance.setVolume(data.volume).then(res => { () => protocolInstance.setVolume(data.volume).then(() => ({ code: 200, msg: '操作成功(蓝牙)' })),
return { code: 200, msg: '操作成功(蓝牙)' }; () => request({ url: `/app/hby100j/device/updateVolume`, method: 'post', data }),
}); '调节音量'
} );
return request({
url: `/app/hby100j/device/updateVolume`,
method: 'post',
data:data
})
} }
// 语音播放 // 语音播放
export function deviceVoiceBroadcast(data) { export function deviceVoiceBroadcast(data) {
if (protocolInstance.isBleConnected) { return execWithBleFirst(
return protocolInstance.setVoiceBroadcast(data.voiceBroadcast).then(res => { () => protocolInstance.setVoiceBroadcast(data.voiceBroadcast).then(() => ({ code: 200, msg: '操作成功(蓝牙)' })),
return { code: 200, msg: '操作成功(蓝牙)' }; () => request({ url: `/app/hby100j/device/voiceBroadcast`, method: 'post', data }),
}); '语音播报'
} );
return request({
url: `/app/hby100j/device/voiceBroadcast`,
method: 'post',
data:data
})
} }

View File

@ -37,13 +37,25 @@
<text class="value">{{ deviceInfo.deviceName }}</text> <text class="value">{{ deviceInfo.deviceName }}</text>
</view> </view>
<view class="item"> <view class="item">
<text class="lbl">IMEI</text> <text class="lbl">设备IMEI</text>
<text class="value">{{ deviceInfo.deviceImei }}</text> <text class="value">{{ deviceInfo.deviceImei }}</text>
</view> </view>
<view class="item">
<text class="lbl">Mac地址</text>
<text class="value">{{device.deviceMac}}</text>
</view>
<view class="item">
<text class="lbl">蓝牙名称</text>
<text class="value valueFont">{{device.bluetoothName}}</text>
</view>
<view class="item" @click.top="bleStatuToggle">
<text class="lbl">蓝牙状态</text>
<text class="value" :class="formData.bleStatu?'green':'red'">{{device.getbleStatu}}</text>
</view>
<view class="item"> <view class="item">
<text class="lbl">设备状态</text> <text class="lbl">设备状态</text>
<text class="value">{{ deviceInfo.onlineStatus === 0 ? '离线' : deviceInfo.onlineStatus <text class="value"
=== 2 ? '故障' : '在线' }}</text> :class="deviceInfo.onlineStatus===0?'red':'green'">{{ deviceInfo.onlineStatus === 0 ? '离线': '在线' }}</text>
</view> </view>
<view class="info-row"> <view class="info-row">
<text class="info-label" style="display: flex; align-items: center;">定位信息</text> <text class="info-label" style="display: flex; align-items: center;">定位信息</text>
@ -231,7 +243,8 @@
deviceUpdateVolume, deviceUpdateVolume,
deviceVoiceBroadcast, deviceVoiceBroadcast,
updateBleStatus, updateBleStatus,
parseBleData parseBleData,
fetchBlePowerStatus
} from '@/api/100J/HBY100-J.js' } from '@/api/100J/HBY100-J.js'
import BleHelper from '@/utils/BleHelper.js'; import BleHelper from '@/utils/BleHelper.js';
var bleTool = BleHelper.getBleTool(); var bleTool = BleHelper.getBleTool();
@ -440,7 +453,7 @@
alarmStatus: null, alarmStatus: null,
detailPageUrl: "/pages/650/HBY650", detailPageUrl: "/pages/650/HBY650",
showConfirm: false, showConfirm: false,
deviceId:'' deviceId: ''
}, },
permissions: [], permissions: [],
audioData: { audioData: {
@ -589,30 +602,24 @@
these.fetchDeviceDetail(data.data.id) these.fetchDeviceDetail(data.data.id)
} else { } else {
this.activePermissions = data.data.permission ? data.data.permission.split(',') : []; this.activePermissions = data.data.permission ? data.data.permission.split(',') : [];
console.log(this.activePermissions,'this.activePermissions'); console.log(this.activePermissions, 'this.activePermissions');
these.fetchDeviceDetail(data.data.deviceId) these.fetchDeviceDetail(data.data.deviceId)
} }
// 尝试连接蓝牙:需先扫描获取 BLE deviceId不能直接用 MAC
if (data.data.deviceMac) {
these.tryConnect100JBle(data.data.deviceMac);
}
}); });
this.createThrottledFunctions(); this.createThrottledFunctions();
// 注册蓝牙相关事件 // 注册蓝牙相关事件
bleTool.addReceiveCallback(this.bleValueNotify, "HBY100J"); bleTool.addReceiveCallback(this.bleValueNotify, "HBY100J");
bleTool.addDisposeCallback(this.bleStateBreak, "HBY100J"); bleTool.addDisposeCallback(this.bleStateBreak, "HBY100J");
bleTool.addRecoveryCallback(this.bleStateRecovry, "HBY100J"); bleTool.addRecoveryCallback(this.bleStateRecovry, "HBY100J");
bleTool.addStateBreakCallback(this.bleStateBreak, "HBY100J"); bleTool.addStateBreakCallback(this.bleStateBreak, "HBY100J");
bleTool.addStateRecoveryCallback(this.bleStateRecovry, "HBY100J"); bleTool.addStateRecoveryCallback(this.bleStateRecovry, "HBY100J");
// 尝试连接蓝牙
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);
});
}
}, },
onHide: function() { onHide: function() {
@ -625,6 +632,8 @@
bleTool.removeRecoveryCallback("HBY100J"); bleTool.removeRecoveryCallback("HBY100J");
bleTool.removeStateBreakCallback("HBY100J"); bleTool.removeStateBreakCallback("HBY100J");
bleTool.removeStateRecoveryCallback("HBY100J"); bleTool.removeStateRecoveryCallback("HBY100J");
bleTool.removeDeviceFound("HBY100J_SCAN");
bleTool.StopSearch();
}, },
onShow() { onShow() {
this.Status.pageHide = false; this.Status.pageHide = false;
@ -979,24 +988,124 @@
deviceRecovry(res) {}, deviceRecovry(res) {},
deviceDispose(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() { bleStateBreak() {
updateBleStatus(false, '', this.deviceInfo.deviceId); updateBleStatus(false, '', this.deviceInfo.deviceId);
}, },
bleStateRecovry(res) { 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); updateBleStatus(true, bleDeviceId, this.deviceInfo.deviceId);
// 蓝牙连接成功后主动拉取电源状态(电量、续航时间)
fetchBlePowerStatus().catch(() => {});
}, },
previewImg(img) {}, previewImg(img) {},
bleValueNotify: function(receive, device, path, recArr) { //订阅消息 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); // 5.1 连接后设备主动上报 MAC 地址 (FC + 6字节 + FF)
if (parsedData && parsedData.batteryPercentage !== undefined) { if (parsedData.type === 'mac' && parsedData.macAddress) {
this.deviceInfo.batteryPercentage = parsedData.batteryPercentage; 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) { if (this.deviceInfo.batteryPercentage <= 20) {

View File

@ -963,6 +963,21 @@ class BleHelper {
} else { } else {
console.log("蓝牙连接已恢复", res); 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); }, 50);