1
0
forked from dyf/APP
Files
APP/api/100J/HBY100-J.js
2026-03-18 18:09:31 +08:00

492 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import request from '@/utils/request'
// ================== 蓝牙协议封装类 ==================
class HBY100JProtocol {
constructor() {
this.deviceId = ''; // 4G 接口所需的 deviceId
this.isBleConnected = false;
this.bleDeviceId = ''; // 小程序/APP中连接蓝牙的 deviceId
// 蓝牙服务与特征值 UUID
this.SERVICE_UUID = '0000AE30-0000-1000-8000-00805F9B34FB'; // 0xAE30
this.WRITE_UUID = '0000AE03-0000-1000-8000-00805F9B34FB'; // 0xAE03
this.NOTIFY_UUID = '0000AE02-0000-1000-8000-00805F9B34FB'; // 0xAE02
this.onNotifyCallback = null;
this._fileResponseResolve = null; // 文件上传时等待设备 FB 05 响应
}
// 等待设备 FB 05 响应,超时后仍 resolve设备可能不响应每包
waitForFileResponse(timeoutMs = 2000) {
return new Promise((resolve) => {
const timer = setTimeout(() => {
if (this._fileResponseResolve) {
this._fileResponseResolve = null;
resolve(null);
}
}, timeoutMs);
this._fileResponseResolve = (result) => {
clearTimeout(timer);
this._fileResponseResolve = null;
resolve(result);
};
});
}
setBleConnectionStatus(status, bleDeviceId = '') {
this.isBleConnected = status;
if (bleDeviceId) {
this.bleDeviceId = bleDeviceId;
}
}
onNotify(callback) {
this.onNotifyCallback = callback;
}
parseBleData(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 };
switch (funcCode) {
case 0x01: result.resetType = data[0]; break;
case 0x02: break;
case 0x03:
// 5.4 获取设备位置经度8B+纬度8B 均为 float64设备主动上报(1分钟)与主动查询响应格式相同
if (data.length >= 16) {
const lonBuf = new ArrayBuffer(8);
const latBuf = new ArrayBuffer(8);
new Uint8Array(lonBuf).set(data.slice(0, 8));
new Uint8Array(latBuf).set(data.slice(8, 16));
result.longitude = new DataView(lonBuf).getFloat64(0, true);
result.latitude = new DataView(latBuf).getFloat64(0, true);
}
break;
case 0x05:
// 05: 文件更新响应 FB 05 [fileType] [status] FFstatus: 1=成功 2=失败
if (data.length >= 1) result.fileType = data[0];
if (data.length >= 2) result.fileStatus = data[1]; // 1=Success, 2=Failure
if (this._fileResponseResolve) this._fileResponseResolve(result);
break;
case 0x04:
// 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:
// 06: 语音播报响应
result.voiceBroadcast = data[0];
break;
case 0x09:
// 09: 修改音量响应
result.volume = data[0];
break;
case 0x0A:
// 0A: 爆闪模式响应
result.strobeEnable = data[0];
result.strobeMode = data[1];
break;
case 0x0B:
// 0B: 修改警示灯爆闪频率响应
result.strobeFrequency = data[0];
break;
case 0x0C:
// 0C: 强制声光报警响应
result.alarmEnable = data[0];
result.alarmMode = data[1];
break;
case 0x0D:
// 0D: 警示灯 LED 亮度调节响应
result.redBrightness = data[0];
result.blueBrightness = data[1];
result.yellowBrightness = data[2];
break;
case 0x0E:
// 0E: 获取当前工作方式响应
result.voiceBroadcast = data[0];
result.alarmEnable = data[1];
result.alarmMode = data[2];
result.strobeEnable = data[3];
result.strobeMode = data[4];
result.strobeFrequency = data[5];
result.volume = data[6];
result.redBrightness = data[7];
result.blueBrightness = data[8];
result.yellowBrightness = data[9];
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);
}
return result;
}
sendBleData(funcCode, dataBytes = []) {
return new Promise((resolve, reject) => {
if (!this.isBleConnected || !this.bleDeviceId) {
return reject(new Error('蓝牙未连接'));
}
const buffer = new ArrayBuffer(dataBytes.length + 3);
const view = new Uint8Array(buffer);
view[0] = 0xFA; // 数据头
view[1] = funcCode; // 功能码
for (let i = 0; i < dataBytes.length; i++) {
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 => {
const bleTool = module.default.getBleTool();
bleTool.sendData(this.bleDeviceId, buffer, this.SERVICE_UUID, this.WRITE_UUID)
.then(res => resolve(res))
.catch(err => reject(err));
});
});
}
// 纯蓝牙指令发送方法
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]); }
setStrobeFrequency(frequency) { return this.sendBleData(0x0B, [frequency]); }
setForceAlarm(enable, mode) { return this.sendBleData(0x0C, [enable, mode]); }
setLightBrightness(red, blue = 0, yellow = 0) { return this.sendBleData(0x0D, [red, blue, yellow]); }
getCurrentWorkMode() { return this.sendBleData(0x0E, []); }
// 0x05 文件上传:分片传输,协议 FA 05 [fileType] [phase] [data...] FF
// fileType: 1=语音 2=图片 3=动图 4=OTA
// phase: 0=开始 1=数据 2=结束
// 每包最大字节 蓝牙CHUNK_SIZE=500
// 支持 fileUrl(需网络下载) 或 localPath(无网络时本地文件)
uploadVoiceFileBle(fileUrlOrLocalPath, fileType = 1, onProgress) {
const CHUNK_SIZE = 500; // 每包有效数据,参考 6155 deviceDetail.vue
return new Promise((resolve, reject) => {
if (!this.isBleConnected || !this.bleDeviceId) {
return reject(new Error('蓝牙未连接'));
}
if (!fileUrlOrLocalPath) {
return reject(new Error('缺少文件地址或本地路径'));
}
const isLocalPath = !/^https?:\/\//i.test(fileUrlOrLocalPath);
const readFromPath = (path) => {
if (typeof plus === 'undefined' || !plus.io) {
reject(new Error('当前环境不支持文件读取'));
return;
}
plus.io.resolveLocalFileSystemURL(path, (entry) => {
entry.file((file) => {
const reader = new plus.io.FileReader();
reader.onloadend = (e) => {
try {
const buf = e.target.result;
const bytes = new Uint8Array(buf);
this._sendVoiceChunks(bytes, fileType, CHUNK_SIZE, onProgress)
.then(resolve).catch(reject);
} catch (err) {
reject(err);
}
};
reader.onerror = () => reject(new Error('读取文件失败'));
reader.readAsArrayBuffer(file);
}, (err) => reject(err));
}, (err) => reject(err));
};
if (isLocalPath) {
// 本地路径:无网络时直接读取
readFromPath(fileUrlOrLocalPath);
} else {
// 网络 URL需下载后读取
uni.downloadFile({
url: fileUrlOrLocalPath,
success: (res) => {
if (res.statusCode !== 200) {
reject(new Error('下载失败: ' + res.statusCode));
return;
}
readFromPath(res.tempFilePath);
},
fail: (err) => reject(err)
});
}
});
}
_sendVoiceChunks(bytes, fileType, chunkSize, onProgress) {
const total = bytes.length;
const ft = (fileType & 0xFF) || 1;
const DELAY_AFTER_START = 80; // 开始包后、等设备响应后再发的缓冲(ms)
const DELAY_PACKET = 80; // 数据包间延时(ms)参考6155
const bleToolPromise = import('@/utils/BleHelper.js').then(m => m.default.getBleTool());
const send = (dataBytes) => {
const buf = new ArrayBuffer(dataBytes.length + 3);
const v = new Uint8Array(buf);
v[0] = 0xFA;
v[1] = 0x05;
for (let i = 0; i < dataBytes.length; i++) v[2 + i] = dataBytes[i];
v[v.length - 1] = 0xFF;
return bleToolPromise.then(ble => ble.sendData(this.bleDeviceId, buf, this.SERVICE_UUID, this.WRITE_UUID));
};
const delay = (ms) => new Promise(r => setTimeout(r, ms));
// 开始包: FA 05 [fileType] [phase=0] [size 4B LE] FF
const startData = [ft, 0, total & 0xFF, (total >> 8) & 0xFF, (total >> 16) & 0xFF, (total >> 24) & 0xFF];
const waitPromise = this.waitForFileResponse(1000);
return send(startData)
.then(() => waitPromise)
.then(() => delay(DELAY_AFTER_START))
.then(() => {
let seq = 0;
const sendNext = (offset) => {
if (offset >= total) {
return delay(DELAY_PACKET).then(() => send([ft, 2]));
}
const chunk = bytes.slice(offset, Math.min(offset + chunkSize, total));
const chunkData = [ft, 1, seq & 0xFF, (seq >> 8) & 0xFF, ...chunk];
return send(chunkData).then(() => {
seq++;
if (onProgress) onProgress(Math.min(100, Math.floor((offset + chunk.length) / total * 100)));
return delay(DELAY_PACKET).then(() => sendNext(offset + chunk.length));
});
};
return sendNext(0);
})
.then(() => {
if (onProgress) onProgress(100);
return { code: 200, msg: '语音文件已通过蓝牙上传' };
});
}
}
// ================== 全局单例与状态管理 ==================
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 });
}
// 暴露给页面:解析蓝牙接收到的数据
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();
}
// 暴露给页面:蓝牙连接后主动拉取定位(优先蓝牙设备也会每1分钟主动上报)
export function fetchBleLocation() {
if (!protocolInstance.isBleConnected) return Promise.reject(new Error('蓝牙未连接'));
console.log('[100J-蓝牙] 拉取定位 已通过蓝牙发送 FA 03 FF');
return protocolInstance.getLocation();
}
// 暴露给页面:尝试重连蓝牙(优先策略:断线后发指令前先尝试重连)
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 接口 (拦截层) ==================
// 获取语音管理列表
export function deviceVoliceList(params) {
return request({
url: `/app/video/queryAudioFileList`,
method: 'get',
data:params
})
}
// 重命名
export function videRenameAudioFile(data) {
return request({
url: `/app/video/renameAudioFile`,
method: 'post',
data:data
})
}
// 删除语音文件列表
export function deviceDeleteAudioFile(params) {
return request({
url: `/app/video/deleteAudioFile`,
method: 'get',
data:params
})
}
// 更新语音,使用语音(优先蓝牙:有 fileUrl 或 localPath 且蓝牙连接时通过蓝牙上传,否则走 4G
// localPath无网络时本地文件路径可直接通过蓝牙发送
export function deviceUpdateVoice(data) {
const httpExec = () => request({
url: `/app/hby100j/device/updateVoice`,
method: 'post',
data: { id: data.id }
});
const localPath = data.localPath;
const fileUrl = data.fileUrl;
const hasLocalPath = localPath && typeof localPath === 'string' && localPath.length > 0;
const hasFileUrl = fileUrl && typeof fileUrl === 'string' && fileUrl.length > 0;
const fileSource = hasLocalPath ? localPath : (hasFileUrl ? fileUrl : null);
if (!fileSource) {
return httpExec(); // 无文件源直接走 4G
}
const bleExec = () => protocolInstance.uploadVoiceFileBle(fileSource, 1, data.onProgress); // fileType=1 语音
return execWithBleFirst(bleExec, httpExec, '语音文件上传');
}
// 100J信息
export function deviceDetail(id) {
return request({
url: `/app/hby100j/device/${id}`,
method: 'get',
})
}
// 优先蓝牙未连接时先尝试重连蓝牙发送失败时回退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) {
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) {
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) {
return execWithBleFirst(
() => protocolInstance.setStrobeFrequency(data.frequency).then(() => ({ code: 200, msg: '操作成功(蓝牙)' })),
() => request({ url: `/app/hby100j/device/strobeFrequency`, method: 'post', data }),
'爆闪频率'
);
}
// 灯光调节亮度
export function deviceLightAdjustment(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) {
return execWithBleFirst(
() => protocolInstance.setVolume(data.volume).then(() => ({ code: 200, msg: '操作成功(蓝牙)' })),
() => request({ url: `/app/hby100j/device/updateVolume`, method: 'post', data }),
'调节音量'
);
}
// 语音播放
export function deviceVoiceBroadcast(data) {
return execWithBleFirst(
() => protocolInstance.setVoiceBroadcast(data.voiceBroadcast).then(() => ({ code: 200, msg: '操作成功(蓝牙)' })),
() => request({ url: `/app/hby100j/device/voiceBroadcast`, method: 'post', data }),
'语音播报'
);
}