完善100J蓝牙
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
import request from '@/utils/request'
|
||||
import Common from '@/utils/Common.js'
|
||||
|
||||
// ================== 蓝牙协议封装类 ==================
|
||||
class HBY100JProtocol {
|
||||
@ -205,45 +206,110 @@ class HBY100JProtocol {
|
||||
return reject(new Error('缺少文件地址或本地路径'));
|
||||
}
|
||||
const isLocalPath = !/^https?:\/\//i.test(fileUrlOrLocalPath);
|
||||
if (onProgress) onProgress(1);
|
||||
const readFromPath = (path) => {
|
||||
const doSend = (bytes) => {
|
||||
this._sendVoiceChunks(bytes, fileType, CHUNK_SIZE, onProgress)
|
||||
.then(resolve).catch(reject);
|
||||
};
|
||||
// App 端 getFileSystemManager 未实现,直接用 plus.io.requestFileSystem+getFile
|
||||
readFromPathPlus(path, doSend, reject);
|
||||
};
|
||||
const readFileEntry = (entry, doSend, reject) => {
|
||||
entry.file((file) => {
|
||||
const reader = new plus.io.FileReader();
|
||||
reader.onloadend = (e) => {
|
||||
try {
|
||||
const buf = e.target.result;
|
||||
const bytes = new Uint8Array(buf);
|
||||
doSend(bytes);
|
||||
} catch (err) {
|
||||
console.error('[100J-蓝牙] 读取ArrayBuffer异常:', err);
|
||||
reject(err);
|
||||
}
|
||||
};
|
||||
reader.onerror = () => reject(new Error('读取文件失败'));
|
||||
reader.readAsArrayBuffer(file);
|
||||
}, (err) => reject(err));
|
||||
};
|
||||
const readFromPathPlus = (path, doSend, reject) => {
|
||||
if (typeof plus === 'undefined' || !plus.io) {
|
||||
console.error('[100J-蓝牙] 当前环境不支持文件读取(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);
|
||||
// _downloads/ 用 requestFileSystem+getFile(避免 resolveLocalFileSystemURL 卡住)
|
||||
if (path && path.startsWith('_downloads/')) {
|
||||
const fileName = path.replace(/^_downloads\//, '');
|
||||
plus.io.requestFileSystem(plus.io.PUBLIC_DOWNLOADS, (fs) => {
|
||||
fs.root.getFile(fileName, {}, (entry) => readFileEntry(entry, doSend, reject), (err) => reject(err));
|
||||
}, (err) => reject(err));
|
||||
}, (err) => reject(err));
|
||||
return;
|
||||
}
|
||||
// _doc/ 用 requestFileSystem(PRIVATE_DOC),逐级 getDirectory 再 getFile(嵌套路径兼容)
|
||||
if (path && path.startsWith('_doc/')) {
|
||||
const relPath = path.replace(/^_doc\//, '');
|
||||
const parts = relPath.split('/');
|
||||
const fileName = parts.pop();
|
||||
const dirs = parts;
|
||||
plus.io.requestFileSystem(plus.io.PRIVATE_DOC, (fs) => {
|
||||
let cur = fs.root;
|
||||
const next = (i) => {
|
||||
if (i >= dirs.length) {
|
||||
cur.getFile(fileName, { create: false }, (entry) => readFileEntry(entry, doSend, reject), (err) => reject(err));
|
||||
return;
|
||||
}
|
||||
cur.getDirectory(dirs[i], { create: false }, (dir) => { cur = dir; next(i + 1); }, (err) => reject(err));
|
||||
};
|
||||
next(0);
|
||||
}, (err) => reject(err));
|
||||
return;
|
||||
}
|
||||
// 其他路径兜底
|
||||
let resolvePath = path;
|
||||
if (path && path.startsWith('/') && !path.startsWith('file://')) resolvePath = 'file://' + path;
|
||||
plus.io.resolveLocalFileSystemURL(resolvePath, (entry) => readFileEntry(entry, doSend, reject), (err) => reject(err));
|
||||
};
|
||||
if (isLocalPath) {
|
||||
// 本地路径:无网络时直接读取
|
||||
readFromPath(fileUrlOrLocalPath);
|
||||
} else {
|
||||
// 网络 URL:需下载后读取
|
||||
uni.downloadFile({
|
||||
url: fileUrlOrLocalPath,
|
||||
// 网络 URL:优先用 uni.request 直接拉取 ArrayBuffer(类似 100 设备,无文件 IO),失败再走 downloadFile
|
||||
let fetchUrl = fileUrlOrLocalPath;
|
||||
if (fetchUrl.startsWith('http://')) fetchUrl = 'https://' + fetchUrl.slice(7);
|
||||
uni.request({
|
||||
url: fetchUrl,
|
||||
method: 'GET',
|
||||
responseType: 'arraybuffer',
|
||||
timeout: 60000,
|
||||
success: (res) => {
|
||||
if (res.statusCode !== 200) {
|
||||
reject(new Error('下载失败: ' + res.statusCode));
|
||||
return;
|
||||
if (res.statusCode === 200 && res.data) {
|
||||
const bytes = res.data instanceof ArrayBuffer ? new Uint8Array(res.data) : new Uint8Array(res.data || []);
|
||||
if (bytes.length > 0) {
|
||||
const doSend = (b) => {
|
||||
this._sendVoiceChunks(b, fileType, CHUNK_SIZE, onProgress).then(resolve).catch(reject);
|
||||
};
|
||||
doSend(bytes);
|
||||
return;
|
||||
}
|
||||
}
|
||||
readFromPath(res.tempFilePath);
|
||||
fallbackDownload();
|
||||
},
|
||||
fail: (err) => reject(err)
|
||||
fail: () => fallbackDownload()
|
||||
});
|
||||
const fallbackDownload = () => {
|
||||
uni.downloadFile({
|
||||
url: fetchUrl,
|
||||
success: (res) => {
|
||||
if (res.statusCode !== 200 || !res.tempFilePath) {
|
||||
reject(new Error('下载失败: ' + (res.statusCode || '无路径')));
|
||||
return;
|
||||
}
|
||||
Common.moveFileToDownloads(res.tempFilePath).then((p) => readFromPath(p)).catch(() => readFromPath(res.tempFilePath));
|
||||
},
|
||||
fail: (err) => reject(err)
|
||||
});
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -251,41 +317,49 @@ class HBY100JProtocol {
|
||||
_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 DELAY_AFTER_START = 200; // 开始包后、等设备响应后再发的缓冲(ms)
|
||||
const DELAY_PACKET = 200; // 数据包间延时(ms),设备收不全时适当加大
|
||||
const toHex = (arr) => Array.from(arr).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' ');
|
||||
console.log('[100J-蓝牙] 语音文件总大小:', total, '字节, fileType=', ft);
|
||||
if (onProgress) onProgress(0);
|
||||
const bleToolPromise = import('@/utils/BleHelper.js').then(m => m.default.getBleTool());
|
||||
const send = (dataBytes) => {
|
||||
const send = (dataBytes, label = '') => {
|
||||
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;
|
||||
const hex = toHex(v);
|
||||
const preview = v.length <= 32 ? hex : hex.slice(0, 96) + '...';
|
||||
console.log(`[100J-蓝牙] 发送${label} 共${v.length}字节:`, preview);
|
||||
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))
|
||||
return send(startData, ' 开始包')
|
||||
.then(() => { if (onProgress) onProgress(1); return waitPromise; })
|
||||
.then(() => { if (onProgress) onProgress(2); return delay(DELAY_AFTER_START); })
|
||||
.then(() => {
|
||||
let seq = 0;
|
||||
const sendNext = (offset) => {
|
||||
if (offset >= total) {
|
||||
return delay(DELAY_PACKET).then(() => send([ft, 2]));
|
||||
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(() => {
|
||||
return send(chunkData, ` #${seq} 数据包`).then(() => {
|
||||
seq++;
|
||||
if (onProgress) onProgress(Math.min(100, Math.floor((offset + chunk.length) / total * 100)));
|
||||
const pct = Math.round((offset + chunk.length) / total * 100);
|
||||
if (onProgress) onProgress(Math.min(99, Math.max(3, pct)));
|
||||
return delay(DELAY_PACKET).then(() => sendNext(offset + chunk.length));
|
||||
});
|
||||
};
|
||||
return sendNext(0);
|
||||
})
|
||||
.then(() => delay(DELAY_PACKET))
|
||||
.then(() => {
|
||||
if (onProgress) onProgress(100);
|
||||
return { code: 200, msg: '语音文件已通过蓝牙上传' };
|
||||
@ -382,8 +456,8 @@ export function deviceDeleteAudioFile(params) {
|
||||
})
|
||||
}
|
||||
|
||||
// 更新语音,使用语音(优先蓝牙:有 fileUrl 或 localPath 且蓝牙连接时通过蓝牙上传,否则走 4G)
|
||||
// localPath:无网络时本地文件路径,可直接通过蓝牙发送
|
||||
// 更新语音/使用语音:蓝牙优先,4G 兜底(不影响原有 4G 音频下发)
|
||||
// 有 fileUrl 或 localPath 且蓝牙可用时走蓝牙;否则或蓝牙失败时走 4G(与原先逻辑一致)
|
||||
export function deviceUpdateVoice(data) {
|
||||
const httpExec = () => request({
|
||||
url: `/app/hby100j/device/updateVoice`,
|
||||
@ -396,9 +470,9 @@ export function deviceUpdateVoice(data) {
|
||||
const hasFileUrl = fileUrl && typeof fileUrl === 'string' && fileUrl.length > 0;
|
||||
const fileSource = hasLocalPath ? localPath : (hasFileUrl ? fileUrl : null);
|
||||
if (!fileSource) {
|
||||
return httpExec(); // 无文件源直接走 4G
|
||||
return httpExec(); // 无文件源:直接 4G(原有逻辑)
|
||||
}
|
||||
const bleExec = () => protocolInstance.uploadVoiceFileBle(fileSource, 1, data.onProgress); // fileType=1 语音
|
||||
const bleExec = () => protocolInstance.uploadVoiceFileBle(fileSource, 1, data.onProgress);
|
||||
return execWithBleFirst(bleExec, httpExec, '语音文件上传');
|
||||
}
|
||||
// 100J信息
|
||||
@ -409,31 +483,15 @@ export function deviceDetail(id) {
|
||||
})
|
||||
}
|
||||
|
||||
// 优先蓝牙:未连接时先尝试重连;蓝牙发送失败时回退4G
|
||||
// 蓝牙优先、4G 兜底:未连接时尝试重连;蓝牙失败时回退 4G(保持原有 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; });
|
||||
};
|
||||
const doBle = () => bleExec().then(res => ({ ...(res || {}), _channel: 'ble' }));
|
||||
const do4G = () => 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 doBle().catch(() => { console.log('[100J] 蓝牙失败,回退4G'); 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();
|
||||
return reconnected ? doBle().catch(() => { console.log('[100J] 蓝牙失败,回退4G'); return do4G(); }) : do4G();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user