100J蓝牙上传语音待验证
This commit is contained in:
@ -13,6 +13,24 @@ class HBY100JProtocol {
|
||||
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 = '') {
|
||||
@ -54,7 +72,23 @@ class HBY100JProtocol {
|
||||
switch (funcCode) {
|
||||
case 0x01: result.resetType = data[0]; break;
|
||||
case 0x02: break;
|
||||
case 0x03: 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] FF,status: 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) {
|
||||
@ -155,6 +189,108 @@ class HBY100JProtocol {
|
||||
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: '语音文件已通过蓝牙上传' };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ================== 全局单例与状态管理 ==================
|
||||
@ -179,6 +315,13 @@ export function fetchBlePowerStatus() {
|
||||
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);
|
||||
@ -239,13 +382,24 @@ export function deviceDeleteAudioFile(params) {
|
||||
})
|
||||
}
|
||||
|
||||
// 更新语音,使用语音
|
||||
// 更新语音,使用语音(优先蓝牙:有 fileUrl 或 localPath 且蓝牙连接时通过蓝牙上传,否则走 4G)
|
||||
// localPath:无网络时本地文件路径,可直接通过蓝牙发送
|
||||
export function deviceUpdateVoice(data) {
|
||||
return request({
|
||||
const httpExec = () => request({
|
||||
url: `/app/hby100j/device/updateVoice`,
|
||||
method: 'post',
|
||||
data:data
|
||||
})
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user