From b99ac04c8813418453af41d13801568572d386d8 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: Thu, 26 Mar 2026 19:20:52 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96100j?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/100J/HBY100-J.js | 639 +++++++++++++++++++++++--- pages/100J/audioManager/AudioList.vue | 29 +- pages/100J/audioManager/Recording.vue | 144 +++++- pages/common/addBLE/addEquip.vue | 14 +- pages/common/index/index.vue | 9 +- utils/BleHelper.js | 9 +- utils/BleReceive.js | 18 +- utils/Common.js | 3 +- 8 files changed, 745 insertions(+), 120 deletions(-) diff --git a/api/100J/HBY100-J.js b/api/100J/HBY100-J.js index 06dec71..7b09bb7 100644 --- a/api/100J/HBY100-J.js +++ b/api/100J/HBY100-J.js @@ -1,6 +1,22 @@ import request from '@/utils/request' import Common from '@/utils/Common.js' +/** + * App-Plus 下 uni.getFileSystemManager 往往仅占位,readFile 可能永不回调。 + * 用 plus.io 判定 App(勿单依赖 plus.runtime,未就绪时会误走 FSM → 卡在 uploadVoiceFileBle 开始)。 + * 仅非 App(如微信小程序)再尝试 FSM。 + */ +function tryGetUniFileSystemManager() { + try { + if (typeof uni === 'undefined') return null; + if (typeof plus !== 'undefined' && plus.io) return null; + if (typeof uni.getFileSystemManager !== 'function') return null; + return uni.getFileSystemManager(); + } catch (e) { + return null; + } +} + // ================== 蓝牙协议封装类 ================== class HBY100JProtocol { constructor() { @@ -35,9 +51,12 @@ class HBY100JProtocol { } setBleConnectionStatus(status, bleDeviceId = '') { - this.isBleConnected = status; + this.isBleConnected = !!status; if (bleDeviceId) { this.bleDeviceId = bleDeviceId; + } else if (!status) { + // 断开时必须清空,否则 execWithBleFirst 误判「有 id 未连」且 uploadVoiceFileBle 仍可能带旧 id + this.bleDeviceId = ''; } } @@ -205,24 +224,66 @@ class HBY100JProtocol { // phase: 0=开始 1=数据 2=结束 // 每包最大字节 蓝牙:CHUNK_SIZE=500 // 支持 fileUrl(需网络下载) 或 localPath(无网络时本地文件) - uploadVoiceFileBle(fileUrlOrLocalPath, fileType = 1, onProgress) { + // 说明:下发的是已录制/已落盘的 MP3(等)二进制分片,经 GATT 写特征;非 A2DP/HFP 等「蓝牙录音实时流」 + // meta.voiceListId:若存在 uni 中 100J_voice_b64_* 缓存(与 HBY100 存 Storage 同理),优先用缓存字节下发 + uploadVoiceFileBle(fileUrlOrLocalPath, fileType = 1, onProgress, meta = null) { const CHUNK_SIZE = 500; // 每包有效数据,参考 6155 deviceDetail.vue + const BLE_CACHE_SENTINEL = '__100J_BLE_CACHE__'; return new Promise((resolve, reject) => { + const srcStr = String(fileUrlOrLocalPath || ''); + const isLocalPath = !/^https?:\/\//i.test(srcStr); + const voiceListId = meta && meta.voiceListId != null ? meta.voiceListId : ''; + const did = this.deviceId || ''; + console.log('[100J-蓝牙] uploadVoiceFileBle 开始', { + isBleConnected: this.isBleConnected, + hasBleDeviceId: !!this.bleDeviceId, + isLocalPath, + sourceHead: srcStr.slice(0, 96), + voiceListId: voiceListId !== '' ? String(voiceListId) : '' + }); if (!this.isBleConnected || !this.bleDeviceId) { return reject(new Error('蓝牙未连接')); } + if (voiceListId !== '' && did) { + const cached = get100JVoiceBleCacheBytes(did, voiceListId); + if (cached && cached.length > 0) { + if (onProgress) onProgress(1); + console.log('[100J-蓝牙] 使用 uni 存储缓存下发(类比 HBY100 getStorage),字节:', cached.length); + this._sendVoiceChunks(cached, fileType, CHUNK_SIZE, onProgress).then(resolve).catch(reject); + return; + } + } + if (fileUrlOrLocalPath === BLE_CACHE_SENTINEL) { + return reject(new Error('本地语音缓存不存在或已失效,请重新保存录音')); + } if (!fileUrlOrLocalPath) { return reject(new Error('缺少文件地址或本地路径')); } - const isLocalPath = !/^https?:\/\//i.test(fileUrlOrLocalPath); if (onProgress) onProgress(1); + let localReadWatchdog = null; + const clearLocalReadWatchdog = () => { + if (localReadWatchdog) { + clearTimeout(localReadWatchdog); + localReadWatchdog = null; + } + }; const readFromPath = (path) => { const doSend = (bytes) => { + clearLocalReadWatchdog(); + if (voiceListId !== '' && did && bytes && bytes.length) { + put100JVoiceBleCache(did, voiceListId, bytes); + } this._sendVoiceChunks(bytes, fileType, CHUNK_SIZE, onProgress) .then(resolve).catch(reject); }; - // App 端 getFileSystemManager 未实现,直接用 plus.io.requestFileSystem+getFile - readFromPathPlus(path, doSend, reject); + const onReadErr = (err) => { + clearLocalReadWatchdog(); + if (onProgress) onProgress(0); + reject(err); + }; + // HBY100 下发语音:不读 MP3 文件,而是 uni.getStorageSync(pcmStorageKey_id) 取服务端返回的 PCM 十六进制串再分包发蓝牙(见 pages/100/HBY100.vue audioApply)。 + // 100J 协议为 FA 05 传原始 MP3 二进制,必须从本地路径读文件;优先 readFile,失败再走 plus.io(部分机型 plus 回调不归位会触发外层看门狗)。 + readFromPathPlus(path, doSend, onReadErr); }; const readFileEntry = (entry, doSend, reject) => { entry.file((file) => { @@ -242,48 +303,238 @@ class HBY100JProtocol { }, (err) => reject(err)); }; const readFromPathPlus = (path, doSend, reject) => { - if (typeof plus === 'undefined' || !plus.io) { - console.error('[100J-蓝牙] 当前环境不支持文件读取(plus.io)'); - reject(new Error('当前环境不支持文件读取')); - return; - } - // _downloads/ 用 requestFileSystem+getFile(避免 resolveLocalFileSystemURL 卡住) + const tryUniReadFile = (onFail) => { + try { + // 仅非 App(如微信小程序)走 uni FSM。App-Plus 上禁止此处调用 getFileSystemManager: + // 否则会刷「not yet implemented」,且 readFile 的 success/fail 可能永不回调 → 永远进不了 plus.io 兜底,表现为「读不了本地文件」。 + const fsm = tryGetUniFileSystemManager(); + if (!fsm || typeof fsm.readFile !== 'function') { + onFail(); + return; + } + fsm.readFile({ + filePath: path, + success: (res) => { + try { + const raw = res && res.data; + let bytes = null; + if (raw instanceof ArrayBuffer) bytes = new Uint8Array(raw); + else if (raw instanceof Uint8Array) bytes = raw; + else if (raw && ArrayBuffer.isView(raw) && raw.buffer instanceof ArrayBuffer) { + bytes = new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength); + } + if (bytes && bytes.length > 0) { + console.log('[100J-蓝牙] readFile 已读出本地语音,字节:', bytes.length); + doSend(bytes); + return; + } + } catch (e) {} + onFail(); + }, + fail: () => onFail() + }); + } catch (e) { + onFail(); + } + }; + // _downloads/:与 Common.moveFileToDownloads 一致,文件在 PUBLIC_DOWNLOADS 根目录。 + // 优先 requestFileSystem+getFile(老版本注释:resolveLocalFileSystemURL 易卡住);resolve 仅作兜底。 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)); + tryUniReadFile(() => { + if (typeof plus === 'undefined' || !plus.io) { + reject(new Error('当前环境不支持文件读取')); + return; + } + console.log('[100J-蓝牙] _downloads 读取开始, fileName=', fileName); + let finished = false; + const outerMs = 20000; + const outerT = setTimeout(() => { + if (finished) return; + finished = true; + reject(new Error('读取下载目录语音超时,请重新保存录音后重试')); + }, outerMs); + const finishOk = (bytes) => { + if (finished) return; + finished = true; + clearTimeout(outerT); + clearTimeout(reqFsGuardT); + doSend(bytes); + }; + const finishErr = (err) => { + if (finished) return; + finished = true; + clearTimeout(outerT); + clearTimeout(reqFsGuardT); + reject(err || new Error('读取文件失败')); + }; + let fallbackStarted = false; + const tryResolveUrl = () => { + if (finished || fallbackStarted) return; + fallbackStarted = true; + clearTimeout(reqFsGuardT); + console.log('[100J-蓝牙] _downloads 改用 resolveLocalFileSystemURL 兜底'); + let url = path; + try { + if (plus.io.convertLocalFileSystemURL) { + const c = plus.io.convertLocalFileSystemURL(path); + if (c) url = c; + } + } catch (e) {} + if (typeof url === 'string' && url.startsWith('/') && !url.startsWith('file://')) { + url = 'file://' + url; + } + plus.io.resolveLocalFileSystemURL(url, (entry) => { + readFileEntry(entry, finishOk, () => { + finishErr(new Error('_downloads 文件不存在或无法读取')); + }); + }, () => finishErr(new Error('_downloads 路径解析失败'))); + }; + const reqFsGuardMs = 6000; + const reqFsGuardT = setTimeout(() => { + if (finished || fallbackStarted) return; + console.log('[100J-蓝牙] _downloads requestFileSystem 超时,尝试 resolve 兜底'); + tryResolveUrl(); + }, reqFsGuardMs); + plus.io.requestFileSystem(plus.io.PUBLIC_DOWNLOADS, (fs) => { + if (finished || fallbackStarted) return; + fs.root.getFile(fileName, {}, (entry) => { + if (finished || fallbackStarted) return; + readFileEntry(entry, finishOk, (err) => { + if (finished) return; + clearTimeout(reqFsGuardT); + console.log('[100J-蓝牙] getFile 后读失败,尝试 resolve', err && err.message); + tryResolveUrl(); + }); + }, (err) => { + if (finished) return; + clearTimeout(reqFsGuardT); + console.log('[100J-蓝牙] getFile 失败,尝试 resolve', err && err.message); + tryResolveUrl(); + }); + }, (err) => { + if (finished) return; + clearTimeout(reqFsGuardT); + console.log('[100J-蓝牙] requestFileSystem 失败,尝试 resolve', err && err.message); + tryResolveUrl(); + }); + }); return; } - // _doc/ 用 requestFileSystem(PRIVATE_DOC),逐级 getDirectory 再 getFile(嵌套路径兼容) + // _doc/:与 _downloads 同理,App 只走 plus;并加 resolve 优先 + 超时,避免 requestFileSystem 链卡死 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)); + tryUniReadFile(() => { + if (typeof plus === 'undefined' || !plus.io) { + reject(new Error('当前环境不支持文件读取')); + return; + } + let finished = false; + const outerMs = 20000; + const outerT = setTimeout(() => { + if (finished) return; + finished = true; + reject(new Error('读取本地语音超时(文档目录),请重新保存录音后重试')); + }, outerMs); + const finishOk = (bytes) => { + if (finished) return; + finished = true; + clearTimeout(outerT); + doSend(bytes); }; - next(0); - }, (err) => reject(err)); + const finishErr = (err) => { + if (finished) return; + finished = true; + clearTimeout(outerT); + reject(err || new Error('读取文件失败')); + }; + const tryWalkFs = () => { + 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, finishOk, finishErr), (err) => finishErr(err)); + return; + } + cur.getDirectory(dirs[i], { create: false }, (dir) => { cur = dir; next(i + 1); }, (err) => finishErr(err)); + }; + next(0); + }, (err) => finishErr(err)); + }; + let resolvePhaseDone = false; + const resolveT = setTimeout(() => { + if (resolvePhaseDone) return; + resolvePhaseDone = true; + console.log('[100J-蓝牙] _doc resolve 超时,改用 PRIVATE_DOC 逐级目录'); + tryWalkFs(); + }, 6000); + let url = path; + try { + if (plus.io.convertLocalFileSystemURL) { + const c = plus.io.convertLocalFileSystemURL(path); + if (c) url = c; + } + } catch (e) {} + if (typeof url === 'string' && url.startsWith('/') && !url.startsWith('file://')) { + url = 'file://' + url; + } + plus.io.resolveLocalFileSystemURL(url, (entry) => { + if (resolvePhaseDone) return; + resolvePhaseDone = true; + clearTimeout(resolveT); + readFileEntry(entry, finishOk, (err) => { + console.log('[100J-蓝牙] _doc resolve 后读失败,改用逐级目录', err && err.message); + tryWalkFs(); + }); + }, () => { + if (resolvePhaseDone) return; + resolvePhaseDone = true; + clearTimeout(resolveT); + tryWalkFs(); + }); + }); return; } - // 其他路径兜底 + if (typeof plus === 'undefined' || !plus.io) { + console.error('[100J-蓝牙] 当前环境不支持 plus.io,且非 _doc/_downloads 相对路径'); + reject(new Error('当前环境不支持文件读取')); + return; + } + // 其他路径兜底:部分机型 resolve 长期无回调,限时避免与外层读流程叠死 + // 与 Common.moveFileToDownloads 一致:uni 临时路径先 convert 再 resolve 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)); + try { + if (resolvePath && typeof plus !== 'undefined' && plus.io && plus.io.convertLocalFileSystemURL) { + const isRel = /^(?:_doc\/|_downloads\/|https?:)/i.test(resolvePath); + if (!isRel) { + const conv = plus.io.convertLocalFileSystemURL(resolvePath); + if (conv && typeof conv === 'string') resolvePath = conv; + } + } + } catch (e) {} + if (resolvePath && resolvePath.startsWith('/') && !resolvePath.startsWith('file://')) resolvePath = 'file://' + resolvePath; + let resolveDone = false; + const t = setTimeout(() => { + if (resolveDone) return; + resolveDone = true; + reject(new Error('解析本地文件路径超时,请重新保存录音')); + }, 12000); + plus.io.resolveLocalFileSystemURL(resolvePath, (entry) => { + if (resolveDone) return; + resolveDone = true; + clearTimeout(t); + readFileEntry(entry, doSend, reject); + }, (err) => { + if (resolveDone) return; + resolveDone = true; + clearTimeout(t); + reject(err); + }); }; - if (isLocalPath) { - // 本地路径:无网络时直接读取 - readFromPath(fileUrlOrLocalPath); - } else { - // 网络 URL:优先用 uni.request 拉取;加超时避免断网时进度长期卡在 1%~2% + const startUrlFetch = () => { let fetchUrl = fileUrlOrLocalPath; if (fetchUrl.startsWith('http://')) fetchUrl = 'https://' + fetchUrl.slice(7); if (onProgress) onProgress(2); @@ -324,6 +575,9 @@ class HBY100JProtocol { const bytes = res.data instanceof ArrayBuffer ? new Uint8Array(res.data) : new Uint8Array(res.data || []); if (bytes.length > 0) { const doSend = (b) => { + if (voiceListId !== '' && did && b && b.length) { + put100JVoiceBleCache(did, voiceListId, b); + } this._sendVoiceChunks(b, fileType, CHUNK_SIZE, onProgress).then(resolve).catch(reject); }; doSend(bytes); @@ -333,6 +587,31 @@ class HBY100JProtocol { fallbackDownload(); }) .catch(() => fallbackDownload()); + }; + if (isLocalPath) { + // 本地路径:无网络时直接读取;plus 回调偶发不返回时避免进度永远停在 1% + localReadWatchdog = setTimeout(() => { + localReadWatchdog = null; + if (onProgress) onProgress(0); + reject(new Error('读取本地语音超时,请重新录制')); + }, 60000); + readFromPath(fileUrlOrLocalPath); + } else { + // 网络 URL:无网时立即失败,避免卡在 1%~2% + uni.getNetworkType({ + success: (net) => { + if (net.networkType === 'none') { + if (onProgress) onProgress(0); + reject(new Error('无网络,无法下载云端语音,请连接网络后重试')); + return; + } + startUrlFetch(); + }, + fail: () => { + if (onProgress) onProgress(0); + reject(new Error('无法检测网络,请连接网络后重试')); + } + }); } }); } @@ -362,11 +641,13 @@ class HBY100JProtocol { 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 bleToolPromise.then(ble => { - bleRef = ble; - ble.setVoiceUploading(true); - return send(startData, ' 开始包') + // 单包最大约 500+ 协议头尾,需 MTU>~520;BleHelper 里 setMtu 为异步且未与首写串联,易在未协商完就 write 导致长时间无回调→界面超时 + const waitPromise = this.waitForFileResponse(2500); + const prepMtuThenSend = (ble) => { + const run = () => { + bleRef = ble; + ble.setVoiceUploading(true); + return send(startData, ' 开始包') .then(() => { if (onProgress) onProgress(3); return waitPromise; }) .then(() => { if (onProgress) onProgress(5); return delay(DELAY_AFTER_START); }) .then(() => { @@ -389,7 +670,18 @@ class HBY100JProtocol { if (onProgress) onProgress(100); return { code: 200, msg: '语音文件已通过蓝牙上传' }; }); - }).finally(() => { + }; + try { + if (typeof plus !== 'undefined' && plus.os && plus.os.name === 'Android' && ble.setMtu) { + return ble.setMtu(this.bleDeviceId) + .catch(() => {}) + .then(() => new Promise((r) => setTimeout(r, 350))) + .then(run); + } + } catch (e) {} + return run(); + }; + return bleToolPromise.then(ble => prepMtuThenSend(ble)).finally(() => { if (bleRef) bleRef.setVoiceUploading(false); }); } @@ -410,6 +702,191 @@ export function getBleStatus() { return { isConnected: protocolInstance.isBleConnected, bleDeviceId: protocolInstance.bleDeviceId, deviceId: protocolInstance.deviceId }; } +/** + * 按当前设备 MAC 与 BleHelper.LinkedList 对齐协议层连接状态。 + * 语音列表页进入时 eventChannel 携带的 getBleStatus() 可能过期;点「使用」前调用,确保真走 uploadVoiceFileBle。 + */ +export function sync100JBleProtocolFromHelper(deviceRow) { + if (!deviceRow || !deviceRow.deviceId) return Promise.resolve(); + const did = deviceRow.deviceId; + protocolInstance.deviceId = did; + return import('@/utils/BleHelper.js').then((m) => { + const bleTool = m.default.getBleTool(); + const mac = deviceRow.deviceMac || deviceRow.device_mac || ''; + if (!bleTool.data.available) { + updateBleStatus(false, '', did); + return; + } + const macNorm = (s) => String(s || '').replace(/:/g, '').toUpperCase(); + const target = macNorm(mac); + const last6 = target.length >= 6 ? target.slice(-6) : ''; + const list = bleTool.data.LinkedList || []; + const item = list.find((v) => { + if (!v) return false; + const mm = macNorm(v.macAddress || ''); + if (!mm) return false; + if (target && mm === target) return true; + return !!(last6 && mm.length >= 6 && mm.slice(-6) === last6); + }); + if (item && item.Linked && item.deviceId) { + updateBleStatus(true, item.deviceId, did); + } else { + updateBleStatus(false, '', did); + } + }).catch(() => {}); +} + +/** 与 HBY100 一致:语音内容进 uni 存储,下发时取出再发蓝牙。100 存 PCM 十六进制串;100J 存整文件 base64(比 hex 省空间),取出后转 Uint8Array,仍走 FA 05 二进制分片(设备协议未变)。 */ +export function voiceBleCacheStorageKey(deviceId, voiceListId) { + return '100J_voice_b64_' + String(deviceId) + '_' + String(voiceListId); +} + +function _uint8ToBase64Chunk(u8) { + if (!u8 || !u8.length) return ''; + try { + const buf = u8.buffer instanceof ArrayBuffer + ? u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength) + : u8; + if (typeof uni !== 'undefined' && typeof uni.arrayBufferToBase64 === 'function') { + return uni.arrayBufferToBase64(buf); + } + } catch (e) {} + let binary = ''; + for (let i = 0; i < u8.length; i++) binary += String.fromCharCode(u8[i]); + return btoa(binary); +} + +function _base64ToUint8Array(b64) { + if (!b64 || typeof b64 !== 'string') return null; + const s = b64.replace(/\s/g, ''); + try { + if (typeof uni !== 'undefined' && typeof uni.base64ToArrayBuffer === 'function') { + const buf = uni.base64ToArrayBuffer(s); + return new Uint8Array(buf); + } + } catch (e) {} + try { + const binary = atob(s); + const len = binary.length; + const u8 = new Uint8Array(len); + for (let i = 0; i < len; i++) u8[i] = binary.charCodeAt(i); + return u8; + } catch (e) { + return null; + } +} + +export function put100JVoiceBleCache(deviceId, voiceListId, uint8Array) { + if (!deviceId || voiceListId == null || !uint8Array || !uint8Array.length) return; + try { + const b64 = _uint8ToBase64Chunk(uint8Array); + if (b64) uni.setStorageSync(voiceBleCacheStorageKey(deviceId, voiceListId), b64); + } catch (e) {} +} + +export function get100JVoiceBleCacheBytes(deviceId, voiceListId) { + if (!deviceId || voiceListId == null) return null; + try { + const b64 = uni.getStorageSync(voiceBleCacheStorageKey(deviceId, voiceListId)); + return _base64ToUint8Array(b64); + } catch (e) { + return null; + } +} + +export function remove100JVoiceBleCache(deviceId, voiceListId) { + if (!deviceId || voiceListId == null) return; + try { + uni.removeStorageSync(voiceBleCacheStorageKey(deviceId, voiceListId)); + } catch (e) {} +} + +/** 保存录音/上传成功后:读落盘路径并写入缓存,点「使用」时可与 HBY100 一样不再依赖 plus 读文件 */ +export function cache100JVoiceFileForBle(deviceId, voiceListId, filePath) { + return new Promise((resolve) => { + const done = () => resolve(); + if (!deviceId || voiceListId == null || !filePath) { + done(); + return; + } + const tryFs = () => { + const fsm = tryGetUniFileSystemManager(); + if (!fsm || typeof fsm.readFile !== 'function') return false; + fsm.readFile({ + filePath, + success: (res) => { + try { + const raw = res && res.data; + let bytes = null; + if (raw instanceof ArrayBuffer) bytes = new Uint8Array(raw); + else if (raw instanceof Uint8Array) bytes = raw; + else if (raw && ArrayBuffer.isView(raw) && raw.buffer instanceof ArrayBuffer) { + bytes = new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength); + } + if (bytes && bytes.length) put100JVoiceBleCache(deviceId, voiceListId, bytes); + } catch (e) {} + done(); + }, + fail: () => done() + }); + return true; + }; + if (tryFs()) return; + if (typeof plus === 'undefined' || !plus.io) { + done(); + return; + } + const readEntry = (entry, cb) => { + entry.file((file) => { + const reader = new plus.io.FileReader(); + reader.onloadend = (e) => { + try { + const buf = e.target.result; + if (buf && buf.byteLength) put100JVoiceBleCache(deviceId, voiceListId, new Uint8Array(buf)); + } catch (err) {} + cb(); + }; + reader.onerror = () => cb(); + reader.readAsArrayBuffer(file); + }, () => cb()); + }; + if (filePath.startsWith('_downloads/')) { + const name = filePath.replace(/^_downloads\//, ''); + plus.io.requestFileSystem(plus.io.PUBLIC_DOWNLOADS, (fs) => { + fs.root.getFile(name, {}, (entry) => readEntry(entry, done), () => done()); + }, () => done()); + return; + } + if (filePath.startsWith('_doc/')) { + const rel = filePath.replace(/^_doc\//, ''); + const parts = rel.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) => readEntry(entry, done), () => done()); + return; + } + cur.getDirectory(dirs[i], { create: false }, (dir) => { cur = dir; next(i + 1); }, () => done()); + }; + next(0); + }, () => done()); + return; + } + let resolvePath = filePath; + try { + if (plus.io.convertLocalFileSystemURL && !/^(?:_doc\/|_downloads\/|https?:)/i.test(filePath)) { + const c = plus.io.convertLocalFileSystemURL(filePath); + if (c) resolvePath = c; + } + } catch (e) {} + if (resolvePath.startsWith('/') && !resolvePath.startsWith('file://')) resolvePath = 'file://' + resolvePath; + plus.io.resolveLocalFileSystemURL(resolvePath, (entry) => readEntry(entry, done), () => done()); + }); +} + // 暴露给页面:解析蓝牙接收到的数据 export function parseBleData(buffer) { return protocolInstance.parseBleData(buffer); @@ -549,26 +1026,38 @@ export function deviceDeleteAudioFile(params) { }) } -// 更新语音/使用语音:蓝牙优先,4G 兜底(不影响原有 4G 音频下发) -// 有 fileUrl 或 localPath 且蓝牙可用时走蓝牙;否则或蓝牙失败时走 4G(与原先逻辑一致) +// 更新语音/使用语音:蓝牙优先;云端 fileUrl 蓝牙失败时可 4G 仅传 id 触发设备拉流 +// 本地 localPath 仅能通过蓝牙下发,禁止回退 4G(否则服务端成功但设备无文件) +function isHttpUrlString(s) { + return !!(s && /^https?:\/\//i.test(String(s).trim())); +} + 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); + const lp = (data.localPath && String(data.localPath).trim()) || ''; + const fu = (data.fileUrl && String(data.fileUrl).trim()) || ''; + // mergeLocal 会把本地路径塞进 fileUrl;列表项若丢 localPath 仍可能只剩「非 http」的 fileUrl + const effectiveLocal = (lp && !isHttpUrlString(lp)) ? lp : ((fu && !isHttpUrlString(fu)) ? fu : ''); + const BLE_CACHE_SENTINEL = '__100J_BLE_CACHE__'; + const hasBleCache = protocolInstance.deviceId && data.id != null && (() => { + const u8 = get100JVoiceBleCacheBytes(protocolInstance.deviceId, data.id); + return !!(u8 && u8.length); + })(); + const hasLocalPath = effectiveLocal.length > 0 || hasBleCache; + const remoteUrl = isHttpUrlString(fu) ? fu : (isHttpUrlString(lp) ? lp : ''); + const fileSource = hasLocalPath ? (effectiveLocal || BLE_CACHE_SENTINEL) : (remoteUrl || null); if (!fileSource) { - console.log('[100J] 语音上传:无 fileUrl/localPath,走 4G'); - return httpExec(); // 无文件源:直接 4G(原有逻辑) + console.log('[100J] 语音上传:无 fileUrl/localPath,仅 HTTP updateVoice(不会走蓝牙传文件)'); + return httpExec().then((res) => { if (res && typeof res === 'object') res._channel = '4g'; return res; }); } - console.log('[100J] 语音上传:有文件源,蓝牙优先', { isBleConnected: protocolInstance.isBleConnected, bleDeviceId: protocolInstance.bleDeviceId || '-' }); - const bleExec = () => protocolInstance.uploadVoiceFileBle(fileSource, 1, data.onProgress); - return execWithBleFirst(bleExec, httpExec, '语音文件上传', data.onWaiting); + console.log('[100J] 语音上传:有文件源,尝试蓝牙下发文件', { isBleConnected: protocolInstance.isBleConnected, hasBleDeviceId: !!protocolInstance.bleDeviceId, local: !!hasLocalPath }); + const bleExec = () => protocolInstance.uploadVoiceFileBle(fileSource, 1, data.onProgress, { voiceListId: data.id }); + // 本地文件:禁止一切 4G 兜底(含蓝牙未开时),避免仅传 id 假成功 + return execWithBleFirst(bleExec, httpExec, '语音文件上传', data.onWaiting, { no4GFallback: hasLocalPath }); } // 100J信息 export function deviceDetail(id) { @@ -579,9 +1068,30 @@ export function deviceDetail(id) { } // 蓝牙优先、4G 兜底:未连接时先等待扫描/连接,再尝试重连;蓝牙失败时回退 4G -function execWithBleFirst(bleExec, httpExec, logName, onWaiting) { +// opts.no4GFallback:本地语音等场景禁止走 HTTP,避免无网/假成功 +function execWithBleFirst(bleExec, httpExec, logName, onWaiting, opts = {}) { + const no4G = !!opts.no4GFallback; + const localBleOnlyMsg = '本地语音需通过蓝牙下发,请连接设备蓝牙后重试'; const doBle = () => bleExec().then(res => ({ ...(res || {}), _channel: 'ble' })); - const do4G = () => httpExec().then(res => { res._channel = '4g'; return res; }); + const do4G = () => httpExec().then((res) => { + console.log('[100J] 语音上传:已改走 HTTP(4G) updateVoice,未执行蓝牙文件分片'); + if (res && typeof res === 'object') res._channel = '4g'; + return res; + }); + const go4GOrReject = (reasonLog) => { + if (no4G) { + return Promise.reject(new Error(localBleOnlyMsg)); + } + if (reasonLog) console.log('[100J]', logName || '指令', reasonLog); + return do4G(); + }; + const onBleSendFail = (e) => { + if (no4G) { + return Promise.reject(e instanceof Error && e.message ? e : new Error(String((e && e.message) || '蓝牙发送语音失败,请靠近设备后重试'))); + } + console.log('[100J]', logName || '指令', '蓝牙失败,回退4G'); + return do4G(); + }; const hideWaitUi = () => { if (typeof onWaiting === 'function') return; try { @@ -597,26 +1107,21 @@ function execWithBleFirst(bleExec, httpExec, logName, onWaiting) { }; if (protocolInstance.isBleConnected && protocolInstance.bleDeviceId) { - return doBle().catch(() => { - console.log('[100J]', logName || '指令', '蓝牙失败,回退4G'); - return do4G(); - }); + console.log('[100J] 语音上传:协议层已连接,执行蓝牙传文件'); + return doBle().catch(onBleSendFail); } + console.log('[100J] 语音上传:协议层未就绪,将等待重连或走 4G', { isBleConnected: protocolInstance.isBleConnected, hasBleDeviceId: !!protocolInstance.bleDeviceId }); // 无 bleDeviceId:系统蓝牙关闭则立即 4G;开启则短时等页面扫描连上(不再白等 12s) if (!protocolInstance.bleDeviceId) { return getBleAdapterAvailable().then((adapterOk) => { if (!adapterOk) { - console.log('[100J]', logName || '指令', '系统蓝牙未开启,走4G'); - return do4G(); + return go4GOrReject('系统蓝牙未开启,走4G'); } if (typeof onWaiting === 'function') onWaiting(); else showWaitUi('请稍候…'); return waitForBleConnection() .then((connected) => { - return connected ? doBle().catch(() => { - console.log('[100J]', logName || '指令', '蓝牙失败,回退4G'); - return do4G(); - }) : do4G(); + return connected ? doBle().catch(onBleSendFail) : go4GOrReject('蓝牙未连接'); }) .finally(hideWaitUi); }); @@ -624,16 +1129,12 @@ function execWithBleFirst(bleExec, httpExec, logName, onWaiting) { // 有 bleDeviceId 但未连:系统蓝牙关则直接 4G,否则短时重连 return getBleAdapterAvailable().then((adapterOk) => { if (!adapterOk) { - console.log('[100J]', logName || '指令', '系统蓝牙未开启,走4G'); - return do4G(); + return go4GOrReject('系统蓝牙未开启,走4G'); } if (typeof onWaiting !== 'function') showWaitUi('请稍候…'); return tryReconnectBle() .then((reconnected) => { - return reconnected ? doBle().catch(() => { - console.log('[100J]', logName || '指令', '蓝牙失败,回退4G'); - return do4G(); - }) : do4G(); + return reconnected ? doBle().catch(onBleSendFail) : go4GOrReject('蓝牙未连接'); }) .finally(hideWaitUi); }); diff --git a/pages/100J/audioManager/AudioList.vue b/pages/100J/audioManager/AudioList.vue index 4a5dd4e..3ee7e84 100644 --- a/pages/100J/audioManager/AudioList.vue +++ b/pages/100J/audioManager/AudioList.vue @@ -105,7 +105,9 @@ videRenameAudioFile, deviceDeleteAudioFile, deviceUpdateVoice, - updateBleStatus + updateBleStatus, + sync100JBleProtocolFromHelper, + remove100JVoiceBleCache } from '@/api/100J/HBY100-J.js' import { baseURL } from '@/utils/request.js' import { @@ -431,7 +433,10 @@ let task = () => { if (item._isLocal) { // 本地项:从本地存储移除 - const key = `100J_local_audio_${this.device.deviceId}`; + const devId = this.device.deviceId; + const vid = (item.id != null && item.id !== '') ? item.id : item.fileId; + remove100JVoiceBleCache(devId, vid); + const key = `100J_local_audio_${devId}`; let list = uni.getStorageSync(key) || []; list = list.filter(l => l.id !== item.id && l.Id !== item.Id); uni.setStorageSync(key, list); @@ -528,12 +533,16 @@ const raw = item.fileUrl || item.url || item.filePath || item.audioUrl || item.ossUrl || ''; fileUrl = (typeof raw === 'string' && raw) ? (raw.startsWith('/') ? (baseURL + raw) : raw) : ''; } else { - // 本地项:localPath 优先,无则用 fileUrl(mergeLocal 中可能只有 fileUrl 存路径) - if (!localPath && item.fileUrl) localPath = item.fileUrl; + // 本地项:localPath 优先;mergeLocal 可能把路径放在 fileUrl,但勿把 http 当成本地路径 + if (!localPath && item.fileUrl) { + const cand = String(item.fileUrl).trim(); + if (cand && !/^https?:\/\//i.test(cand)) localPath = cand; + } } const data = { - id: item.id, - fileUrl, + id: (item.id != null && item.id !== '') ? item.id : item.fileId, + // 本地合并项 mergeLocal 会把路径写在 fileUrl,需带给接口层做 effectiveLocal 兜底 + fileUrl: item._isLocal ? (typeof item.fileUrl === 'string' ? item.fileUrl : '') : fileUrl, localPath, onProgress: (p) => { this.updateProgress = p; }, // 不传「蓝牙连接中」类提示:关蓝牙走 4G 时易误导;进度条 + 必要时全局请稍候即可 @@ -546,8 +555,9 @@ this.isUpdating = false; this.updateProgress = 0; } - }, 60000); - deviceUpdateVoice(data).then((RES) => { + }, 120000); // 蓝牙分片+MTU 协商+大包写入较慢,60s 易误报「操作超时」 + // 进入列表时的蓝牙快照可能过期;与 HBY100 详情页一致,从 BleHelper 按 MAC 再对齐一次,否则常静默走 4G、看不到 [100J-蓝牙] 分片日志 + sync100JBleProtocolFromHelper(this.device).then(() => deviceUpdateVoice(data)).then((RES) => { clearTimeout(overallTimer); if (RES.code == 200) { // 蓝牙上传:进度已由 onProgress 更新,直接完成 @@ -596,7 +606,8 @@ }).catch((err) => { clearTimeout(overallTimer); this.isUpdating = false; - uni.showToast({ title: err.message || '操作失败', icon: 'none', duration: 2000 }); + this.updateProgress = 0; + uni.showToast({ title: err.message || '操作失败', icon: 'none', duration: 2500 }); }); }, closePop: function() { diff --git a/pages/100J/audioManager/Recording.vue b/pages/100J/audioManager/Recording.vue index 764d71b..3850f39 100644 --- a/pages/100J/audioManager/Recording.vue +++ b/pages/100J/audioManager/Recording.vue @@ -137,8 +137,9 @@ updateLoading } from '@/utils/loading.js'; import Common from '@/utils/Common.js'; - - + import { + cache100JVoiceFileForBle + } from '@/api/100J/HBY100-J.js'; export default { data() { @@ -422,10 +423,19 @@ }, 1200); }, // 无网络时保存到本地,供蓝牙直接发送(不依赖 OSS) - // 将临时文件复制到持久化目录 _doc/100J_audio/,避免被系统清理 + // 当前部分机型 PUBLIC_DOWNLOADS 在读取阶段会出现 requestFileSystem/resolve 长时间无回调, + // 因此本地语音优先落到 _doc/100J_audio,后续蓝牙下发走 PRIVATE_DOC 分支更稳定。 saveLocalForBle(filePath) { const deviceId = these.Status.ID; - if (!deviceId) return; + if (!deviceId) { + uni.showToast({ title: '缺少设备信息,请从语音列表进入录音页', icon: 'none', duration: 2500 }); + return; + } + const warmCacheNow = (srcPath) => { + if (!srcPath) return; + // 像 HBY100 一样优先写 uni 存储:后续「使用」先读缓存,尽量不再依赖当场读文件 + cache100JVoiceFileForBle(deviceId, 'local_' + these.cEdit.Id, srcPath); + }; const doSave = (persistentPath) => { const item = { ...these.cEdit, @@ -442,22 +452,86 @@ uni.setStorageSync(key, list); these.AudioData.tempFilePath = ""; these.Status.isRecord = false; + // 再用持久路径补写一遍缓存(若前面已成功则覆盖同 key) + cache100JVoiceFileForBle(deviceId, item.id, persistentPath); uni.navigateBack(); }; - if (typeof plus !== 'undefined' && plus.io) { + const toPlusUrl = (p) => { + if (!p) return p; + try { + if (plus.io.convertLocalFileSystemURL) { + const c = plus.io.convertLocalFileSystemURL(p); + if (c) return c; + } + } catch (e) {} + return p; + }; + const copyToDocByReadWrite = (srcPath, fileName, onOk, onFail) => { + plus.io.resolveLocalFileSystemURL(toPlusUrl(srcPath), (srcEntry) => { + srcEntry.file((file) => { + const reader = new plus.io.FileReader(); + reader.onloadend = (e) => { + const buf = e.target.result; + if (!buf || !(buf.byteLength > 0)) { + onFail(new Error('读取录音文件为空')); + return; + } + plus.io.requestFileSystem(plus.io.PRIVATE_DOC, (fs) => { + fs.root.getDirectory('100J_audio', { create: true }, (dirEntry) => { + dirEntry.getFile(fileName, { create: true }, (newEntry) => { + newEntry.createWriter((writer) => { + writer.onwrite = () => onOk(); + writer.onerror = (ex) => onFail(ex || new Error('写入失败')); + try { + const blob = new Blob([buf], { type: 'application/octet-stream' }); + writer.write(blob); + } catch (wex) { + writer.write(buf); + } + }, onFail); + }, onFail); + }, onFail); + }, onFail); + }; + reader.onerror = () => onFail(new Error('读取录音失败')); + reader.readAsArrayBuffer(file); + }, onFail); + }, onFail); + }; + const fallbackDocSubdir = () => { + if (typeof plus === 'undefined' || !plus.io) { + doSave(filePath); + return; + } const fileName = 'audio_' + (these.cEdit.Id || Date.now()) + '.mp3'; - plus.io.resolveLocalFileSystemURL(filePath, (entry) => { + const docRel = '_doc/100J_audio/' + fileName; + const tryRwFallback = (reason) => { + console.warn('[100J] saveLocalForBle 复制失败,尝试读写落盘:', reason || ''); + copyToDocByReadWrite(filePath, fileName, () => doSave(docRel), () => doSave(filePath)); + }; + plus.io.resolveLocalFileSystemURL(toPlusUrl(filePath), (entry) => { plus.io.resolveLocalFileSystemURL('_doc/', (docEntry) => { docEntry.getDirectory('100J_audio', { create: true }, (dirEntry) => { - entry.copyTo(dirEntry, fileName, (newEntry) => { - doSave(newEntry.fullPath); - }, () => { doSave(filePath); }); - }, () => { doSave(filePath); }); - }, () => { doSave(filePath); }); - }, () => { doSave(filePath); }); - } else { + entry.copyTo(dirEntry, fileName, () => { + doSave(docRel); + }, () => { tryRwFallback('copyTo'); }); + }, () => { tryRwFallback('getDirectory'); }); + }, () => { tryRwFallback('resolve _doc'); }); + }, () => { tryRwFallback('resolve src'); }); + }; + if (typeof plus === 'undefined' || !plus.io) { doSave(filePath); + return; } + const rawPath = (filePath && String(filePath).trim()) || ''; + // 保存流程开始时即尝试缓存一次(通常是 _doc/uniapp_temp 源路径) + warmCacheNow(rawPath); + // 已是 _doc 路径时直接保存;其余路径统一转存到 _doc/100J_audio + if (rawPath.indexOf('_doc/') === 0) { + doSave(rawPath); + return; + } + fallbackDocSubdir(); }, // 保存录音并上传(已修复文件格式问题) uploadLuYin() { @@ -478,19 +552,40 @@ console.log("自动添加.mp3扩展名,新路径:", uploadFilePath); } - console.log("上传文件路径:", uploadFilePath); - plus.io.resolveLocalFileSystemURL(uploadFilePath, (entry) => { - entry.getMetadata((metadata) => { - console.log("文件大小:", metadata.size, "字节"); - console.log("文件类型验证通过"); - this.doUpload(uploadFilePath); + const startOssUpload = () => { + console.log("上传文件路径:", uploadFilePath); + plus.io.resolveLocalFileSystemURL(uploadFilePath, (entry) => { + entry.getMetadata((metadata) => { + console.log("文件大小:", metadata.size, "字节"); + console.log("文件类型验证通过"); + this.doUpload(uploadFilePath); + }, (err) => { + console.error("获取文件元数据失败:", err); + this.doUpload(this.AudioData.tempFilePath); + }); }, (err) => { - console.error("获取文件元数据失败:", err); + console.error("文件不存在或路径错误:", err); this.doUpload(this.AudioData.tempFilePath); }); - }, (err) => { - console.error("文件不存在或路径错误:", err); - this.doUpload(this.AudioData.tempFilePath); + }; + + // 无网络时不调 OSS,直接落本地列表,避免 uploadFile 白失败;列表里点「使用」走蓝牙下发 + uni.getNetworkType({ + success: (res) => { + if (res.networkType === 'none') { + this.saveLocalForBle(uploadFilePath); + uni.showToast({ + title: '无网络,已保存到本地。请在列表连接蓝牙后点「使用」下发;有网后可再保存上传云端', + icon: 'none', + duration: 4000 + }); + return; + } + startOssUpload(); + }, + fail: () => { + startOssUpload(); + } }); }, @@ -535,6 +630,9 @@ if (fileUrl) cache[fileUrl] = filePath; if (d && typeof d === 'object' && d.id) cache[d.id] = filePath; uni.setStorageSync(cacheKey, cache); + if (d && typeof d === 'object' && d.id) { + cache100JVoiceFileForBle(deviceId, d.id, filePath); + } } } // 合并两个存储操作 diff --git a/pages/common/addBLE/addEquip.vue b/pages/common/addBLE/addEquip.vue index ac06bba..3635f4c 100644 --- a/pages/common/addBLE/addEquip.vue +++ b/pages/common/addBLE/addEquip.vue @@ -185,7 +185,7 @@ }, onHide: function() { this.Status.isPageHidden = true; - ble.StopSearch(); + if (ble) ble.StopSearch(); }, onUnload() { @@ -205,7 +205,6 @@ } }, onLoad(option) { - debugger; eventChannel = this.getOpenerEventChannel(); eventChannel.on('detailData', function(rec) { @@ -351,11 +350,10 @@ // console.log("+++ 发现新设备,准备添加到列表:", JSON.stringify(device)); if (these.device && these.device.bluetoothName && device.name) { - if (these.device.bluetoothName === device.name || (device.name && device.name - .indexOf(these - .device.bluetoothName) > -1) || (device.name && this.device - .bluetoothName.indexOf( - device.name) > -1)) { + const bn = these.device.bluetoothName; + if (these.device.bluetoothName === device.name || + (device.name.indexOf(bn) > -1) || + (bn.indexOf(device.name) > -1)) { device.isTarget = true; } } @@ -629,7 +627,7 @@ if (f && f.macAddress) { - if (!this.device.deviceMac) { //走服务端验证 + if (!these.device || !these.device.deviceMac) { //走服务端验证 console.error("走服务端验证") request({ url: '/app/device/getDeviceInfoByDeviceMac', diff --git a/pages/common/index/index.vue b/pages/common/index/index.vue index a734c0d..8b06b19 100644 --- a/pages/common/index/index.vue +++ b/pages/common/index/index.vue @@ -300,7 +300,7 @@ }, bleStateRecovery() { console.log("蓝牙适配器恢复可用,重连断开的设备"); - ble.linkAllDevices(); + if (ble && ble.linkAllDevices) ble.linkAllDevices(); }, bleBreak(res) { @@ -327,7 +327,7 @@ let f = null; if (ble.data && ble.data.LinkedList) { f = ble.data.LinkedList.find(v => { - + if (!v) return false; if (v.macAddress && v.device && v.device.id) { return v.device.id == this.deviceList[i].id; } @@ -749,6 +749,9 @@ onLoad() { // console.error("首页加载"); + // 必须先初始化 ble:getTab/downCallback 会触发 updateBleStatu,否则会访问 null + ble = bleTool.getBleTool(); + recei = BleReceive.getBleReceive(); this.getTab() this.downCallback(); @@ -763,8 +766,6 @@ console.log('列表收到消息了么'); this.downCallback(); }); - ble = bleTool.getBleTool(); - recei = BleReceive.getBleReceive(); //蓝牙连接成功的回调 ble.addRecoveryCallback((res) => { // console.log("蓝牙连接成功的回调"); diff --git a/utils/BleHelper.js b/utils/BleHelper.js index f700c1c..f8ed745 100644 --- a/utils/BleHelper.js +++ b/utils/BleHelper.js @@ -38,11 +38,10 @@ class BleHelper { if (linkedDevices && linkedDevices.length && linkedDevices.length > 0) { // console.log("111111", linkedDevices); linkedDevices = linkedDevices.filter((v) => { - if (v) { - v.Linked = false; - v.notifyState = false; - } - return v.device ? true : false; + if (!v) return false; + v.Linked = false; + v.notifyState = false; + return !!v.device; }); } diff --git a/utils/BleReceive.js b/utils/BleReceive.js index 4793bb0..4a62373 100644 --- a/utils/BleReceive.js +++ b/utils/BleReceive.js @@ -49,6 +49,19 @@ class BleReceive { ReceiveData(receive, f, path, recArr) { + // 100J:首页等场景 LinkedList 项可能未带齐 mac/device,但语音分片上传依赖 parseBleData 消费 FB 05 + const sid = receive && receive.serviceId ? String(receive.serviceId) : ''; + const is100JAe30 = /ae30/i.test(sid); + const fReady = f && f.macAddress && f.device && f.device.id; + if (is100JAe30 && receive && receive.bytes && receive.bytes.length >= 3 && !fReady) { + try { + parseBleData(new Uint8Array(receive.bytes)); + } catch (e) { + console.warn('[100J] ReceiveData 兜底解析失败', e); + } + return receive; + } + if (f && f.macAddress && f.device && f.device.id) { let handler = null; let keys = Object.keys(this.HandlerMap); @@ -73,7 +86,10 @@ class BleReceive { } } else { - console.log("已收到该消息,但无法处理", receive, "f:", f); + // 100J AE30 二进制帧在 f 不完整时已在上方 parseBleData,此处不再误报「无法处理」 + if (!is100JAe30) { + console.log("已收到该消息,但无法处理", receive, "f:", f); + } } return receive; diff --git a/utils/Common.js b/utils/Common.js index 58f0695..acb048d 100644 --- a/utils/Common.js +++ b/utils/Common.js @@ -445,7 +445,8 @@ export default { }); }, (error) => { - console.log('文件不存在/路径错误:', error.message); // 核心问题! + console.log('文件不存在/路径错误:', error.message); + reject(error || new Error('resolveLocalFileSystemURL 失败')); } ); });