diff --git a/api/100J/HBY100-J.js b/api/100J/HBY100-J.js
index 76f4448..4f8fd44 100644
--- a/api/100J/HBY100-J.js
+++ b/api/100J/HBY100-J.js
@@ -210,7 +210,7 @@ class HBY100JProtocol {
}
break;
case 0x05:
- // 05: 文件更新响应 FB 05 [fileType] [status] FF,status: 1=成功 2=失败
+ // 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 (!skipSideEffects && this._fileResponseResolve) this._fileResponseResolve(result);
@@ -704,6 +704,16 @@ class HBY100JProtocol {
return bleToolPromise.then(ble => ble.sendData(this.bleDeviceId, buf, this.SERVICE_UUID, this.WRITE_UUID));
};
const delay = (ms) => new Promise(r => setTimeout(r, ms));
+ const showTailWriteLoading = () => {
+ try {
+ uni.showLoading({ title: '设备写入中…', mask: true });
+ } catch (e) {}
+ };
+ const hideTailWriteLoading = () => {
+ try {
+ uni.hideLoading();
+ } catch (e) {}
+ };
// 开始包: FA 05 [fileType] [phase=0] [size 4B LE] FF
const startData = [ft, 0, total & 0xFF, (total >> 8) & 0xFF, (total >> 16) & 0xFF, (total >> 24) & 0xFF];
// 单包约 507B(500 负载),依赖 MTU;Android 上为整包 write
@@ -720,9 +730,30 @@ class HBY100JProtocol {
let seq = 0;
const sendNext = (offset) => {
if (offset >= total) {
+ // 结束包 FA 05 01 02 FF:协议(10) 设备应答 FB 05;尾包后设备需落盘,按约 7KB/s 估算等待,避免未写完就超时误走 4G
+ const WRITE_BPS = 7 * 1024;
+ const END_ACK_MS = Math.min(600000, Math.max(25000, Math.ceil(total / WRITE_BPS) * 1000 + 25000));
+ console.log('[100J-蓝牙] 尾包后等待设备写入应答,超时', END_ACK_MS, 'ms(按约 7KB/s 估算落盘)');
return delay(DELAY_PACKET)
.then(() => send([ft, 2], ' 结束包'))
- .then(() => { emitProgress(99); });
+ .then(() => {
+ showTailWriteLoading();
+ return this.waitForFileResponse(END_ACK_MS);
+ })
+ .then((ack) => {
+ if (ack && ack.fileStatus === 2) {
+ return Promise.reject(new Error('设备写入失败,请重试'));
+ }
+ if (ack && ack.fileStatus === 1) {
+ console.log('[100J-蓝牙] 结束包后设备确认写入成功 FB 05');
+ emitProgress(99);
+ return;
+ }
+ return Promise.reject(new Error('设备写入确认超时,请稍后重试或靠近设备'));
+ })
+ .finally(() => {
+ hideTailWriteLoading();
+ });
}
const chunk = bytes.slice(offset, Math.min(offset + chunkSize, total));
const chunkData = [ft, 1, seq & 0xFF, (seq >> 8) & 0xFF, ...chunk];
@@ -1058,6 +1089,9 @@ export function deviceUpdateVoice(data) {
const hasLocalPath = effectiveLocal.length > 0 || hasBleCache;
const remoteUrl = isHttpUrlString(fu) ? fu : (isHttpUrlString(lp) ? lp : '');
const fileSource = hasLocalPath ? (effectiveLocal || BLE_CACHE_SENTINEL) : (remoteUrl || null);
+ // 仅「没有任何云端 URL、只能靠本机路径/缓存发二进制」时禁止 4G 兜底。
+ // 若列表项带 https,即使用户曾下发过而残留 put100JVoiceBleCache,仍应允许走 4G(设备按 id 拉 OSS),否则会误报「本地语音需通过蓝牙下发」。
+ const no4GFallback = !remoteUrl && hasLocalPath;
if (!fileSource) {
console.log('[100J] 语音上传:无 fileUrl/localPath,仅 HTTP updateVoice(不会走蓝牙传文件)');
return httpExec(0).then((res) => { if (res && typeof res === 'object') res._channel = '4g'; return res; });
@@ -1083,8 +1117,8 @@ export function deviceUpdateVoice(data) {
})
);
const http4g = () => httpExec(0);
- // 本地文件:禁止一切 4G 兜底(含蓝牙未开时),避免仅传 id 假成功
- return execWithBleFirst(bleExec, http4g, '语音文件上传', data.onWaiting, { no4GFallback: hasLocalPath });
+ // 无云端 URL 的纯本地文件:禁止 4G 兜底,避免仅传 id 假成功;有 https 时蓝牙失败可 4G
+ return execWithBleFirst(bleExec, http4g, '语音文件上传', data.onWaiting, { no4GFallback });
}
// 100J信息
export function deviceDetail(id) {
@@ -1116,6 +1150,12 @@ function execWithBleFirst(bleExec, httpExec, logName, onWaiting, opts = {}) {
if (no4G) {
return Promise.reject(e instanceof Error && e.message ? e : new Error(String((e && e.message) || '蓝牙发送语音失败,请靠近设备后重试')));
}
+ const msg = (e && e.message) ? String(e.message) : String(e || '');
+ // 分片已发完、仅尾包后等设备落盘应答失败:不应再 4G updateVoice(文件已走蓝牙,重复下发会误导)
+ if (msg.indexOf('设备写入确认超时') !== -1 || msg.indexOf('设备写入失败') !== -1) {
+ console.log('[100J]', logName || '指令', '蓝牙语音尾包阶段失败,不回退4G:', msg);
+ return Promise.reject(e instanceof Error && e.message ? e : new Error(msg));
+ }
console.log('[100J]', logName || '指令', '蓝牙失败,回退4G');
return do4G();
};
diff --git a/pages/100J/HBY100-J.vue b/pages/100J/HBY100-J.vue
index 15576c7..0496367 100644
--- a/pages/100J/HBY100-J.vue
+++ b/pages/100J/HBY100-J.vue
@@ -64,7 +64,7 @@
{{ deviceInfo && deviceInfo.longitude ? Number(deviceInfo.longitude).toFixed(4) : '' }}
{{ deviceInfo && deviceInfo.latitude ? Number(deviceInfo.latitude).toFixed(4) : '' }}
-
+
@@ -1097,6 +1097,7 @@
});
});
} else if (prevVoiceType === '7' && val !== '7' && val !== '-1') {
+ console.log('走到这里了没有');
// 从「播放语音」切到其它内置音色:先关播报;报警未开启时不走 forceAlarm,仅 UI 预选音色
const data = {
deviceId: this.deviceInfo.deviceId,
@@ -1105,14 +1106,16 @@
voiceStrobeAlarm: this.deviceInfo.voiceStrobeAlarm
};
deviceVoiceBroadcast(data).then((res) => {
+ this.formData.sta_VoiceType = val
if (res.code == 200) {
uni.showToast({
- title: res.msg || '已切换',
+ title: res.msg,
icon: 'none'
});
+
} else {
uni.showToast({
- title: res.msg || '操作失败',
+ title: res.msg,
icon: 'none'
});
}
@@ -1132,11 +1135,17 @@
const isClose = item === 0;
// 与「已解除不再重复关报警」对称:已在报警中不再弹窗重复下发「开启」,未报警时不再重复「解除」
if (!isClose && this.deviceInfo.voiceStrobeAlarm === 1) {
- uni.showToast({ title: '当前已在报警中', icon: 'none' });
+ uni.showToast({
+ title: '当前已在报警中',
+ icon: 'none'
+ });
return;
}
if (isClose && this.deviceInfo.voiceStrobeAlarm !== 1) {
- uni.showToast({ title: '当前未在报警中', icon: 'none' });
+ uni.showToast({
+ title: '当前未在报警中',
+ icon: 'none'
+ });
return;
}
if (!this.Status) this.Status = {};
diff --git a/pages/100J/audioManager/AudioList.vue b/pages/100J/audioManager/AudioList.vue
index 6ec61c5..83bc0e0 100644
--- a/pages/100J/audioManager/AudioList.vue
+++ b/pages/100J/audioManager/AudioList.vue
@@ -264,30 +264,14 @@
uni.navigateBack();
}, delayMs);
},
- //语音管理列表(合并云端 + 本地无网络保存的语音)
+ // 语音列表仅展示接口数据;上传落库后以服务端 URL 为准,不再写/读本地 path 与 BLE 字节缓存。
getinitData(val, isLoadMore = false) {
const deviceId = this.device.deviceId;
if (!deviceId) return;
- const mergeLocal = (serverList) => {
- const key = `100J_local_audio_${deviceId}`;
- const cacheKey = `100J_local_path_cache_${deviceId}`;
- const localList = uni.getStorageSync(key) || [];
- const pathCache = uni.getStorageSync(cacheKey) || {};
- const localMapped = localList.map(item => ({
- ...item,
- fileNameExt: item.name || '本地语音',
- createTime: item._createTime || item.createTime || Common.DateFormat(new Date(), "yyyy年MM月dd日"),
- fileUrl: item.fileUrl || item.localPath,
- useStatus: 0,
- _isLocal: true
- }));
- const enriched = (serverList || []).map(item => {
- const urlKey = item.fileUrl || item.url || item.filePath || item.audioUrl || item.ossUrl;
- const localPath = pathCache[urlKey] || pathCache[item.id];
- return localPath ? { ...item, localPath } : item;
- });
- return [...localMapped, ...enriched];
- };
+ try {
+ uni.removeStorageSync(`100J_local_path_cache_${deviceId}`);
+ uni.removeStorageSync(`100J_local_audio_${deviceId}`);
+ } catch (e) {}
deviceVoliceList({ deviceId }).then((res) => {
if (res.code == 200) {
this.total = res.total;
@@ -296,14 +280,13 @@
createTime: item.createTime || Common.DateFormat(new Date(), "yyyy年MM月dd日"),
useStatus: Number(item.useStatus) === 1 ? 1 : 0
}));
- this.dataListA = mergeLocal(list);
- if (this.mescroll) this.mescroll.endBySize(this.dataListA.length, this.total + (this.dataListA.length - list.length));
+ this.dataListA = list;
+ if (this.mescroll) this.mescroll.endBySize(this.dataListA.length, this.total);
}
}).catch(() => {
- // 无网络时仅显示本地保存的语音
- this.dataListA = mergeLocal([]);
- this.total = this.dataListA.length;
- if (this.mescroll) this.mescroll.endBySize(this.dataListA.length, this.total);
+ this.dataListA = [];
+ this.total = 0;
+ if (this.mescroll) this.mescroll.endBySize(0, 0);
});
},
createAudioPlayer(localPath) {
@@ -409,7 +392,7 @@
let data = {
fileName: this.cEdit.fileNameExt,
deviceId: this.device.deviceId,
- fileId: item.fileId
+ id: item.id
}
videRenameAudioFile(data).then((res) => {
console.log('res');
@@ -456,22 +439,11 @@
return;
}
let task = () => {
- if (item._isLocal) {
- // 本地项:从本地存储移除
- 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);
- uni.showToast({ title: '已删除', icon: 'none', duration: 1000 });
- this.getinitData();
- this.$refs.swipeAction.closeAll();
- return;
- }
- deviceDeleteAudioFile({ fileId: item.fileId, deviceId: this.device.deviceId }).then((res) => {
+ const devId = this.device.deviceId;
+ const vid = (item.id != null && item.id !== '') ? item.id : item.id;
+ deviceDeleteAudioFile({ id: item.id, deviceId: devId }).then((res) => {
if (res.code == 200) {
+ if (devId && vid != null && vid !== '') remove100JVoiceBleCache(devId, vid);
uni.showToast({ title: res.msg, icon: 'none', duration: 1000 });
this.getinitData();
this.$refs.swipeAction.closeAll();
@@ -551,25 +523,14 @@
Apply(item, index) {
this.updateProgress = 0;
this.clearVoiceApplyTimers();
- // 本地项在无网时禁止下发,仅弹窗(isUpdating 在确认可执行后再置 true)
- // 本地项优先用 localPath;云端项用 fileUrl(兼容多种字段名),相对路径补全 baseURL
- let fileUrl = '';
- let localPath = (item.localPath && typeof item.localPath === 'string') ? item.localPath : '';
- if (!item._isLocal) {
- const raw = item.fileUrl || item.url || item.filePath || item.audioUrl || item.ossUrl || '';
- fileUrl = (typeof raw === 'string' && raw) ? (raw.startsWith('/') ? (baseURL + raw) : raw) : '';
- } else {
- // 本地项:localPath 优先;mergeLocal 可能把路径放在 fileUrl,但勿把 http 当成本地路径
- if (!localPath && item.fileUrl) {
- const cand = String(item.fileUrl).trim();
- if (cand && !/^https?:\/\//i.test(cand)) localPath = cand;
- }
- }
+ const raw = item.fileUrl || item.url || item.filePath || item.audioUrl || item.ossUrl || '';
+ const fileUrl = (typeof raw === 'string' && raw)
+ ? (raw.startsWith('/') ? (baseURL + raw) : raw)
+ : '';
const data = {
- id: (item.id != null && item.id !== '') ? item.id : item.fileId,
- // 本地合并项 mergeLocal 会把路径写在 fileUrl,需带给接口层做 effectiveLocal 兜底
- fileUrl: item._isLocal ? (typeof item.fileUrl === 'string' ? item.fileUrl : '') : fileUrl,
- localPath,
+ id: item.id,
+ fileUrl,
+ localPath: '',
onProgress: (p) => {
const n = Math.min(100, Math.max(0, Math.round(Number(p) || 0)));
const cur = Number(this.updateProgress) || 0;
@@ -606,9 +567,10 @@
clearTimeout(this.upgradeTimer);
this.upgradeTimer = null;
}
+ // 蓝牙链:尾包 FA 05 01 02 FF 后已收到 FB 05 成功应答,再调 updateVoice,此处再 toast 并返回上一页
const title = RES._updateVoiceAfterBleFailed
? '蓝牙已下发,云端同步失败可稍后重试'
- : '音频上传成功';
+ : '设备已确认写入';
this.syncVoiceListUseStatus(item);
uni.showToast({ title, icon: RES._updateVoiceAfterBleFailed ? 'none' : 'success', duration: 2000 });
this.isUpdating = false;
@@ -673,28 +635,6 @@
uni.showToast({ title: err.message || '操作失败', icon: 'none', duration: 2500 });
});
};
- if (item._isLocal) {
- uni.getNetworkType({
- success: (net) => {
- if (net.networkType === 'none') {
- uni.showModal({
- title: '无法使用',
- content: '无网保存的本地语音无法通过蓝牙下发。请先连接 WiFi 或移动网络后,重新录制并保存(上传云端),再点「使用」。',
- showCancel: false,
- confirmText: '知道了'
- });
- return;
- }
- this.isUpdating = true;
- runDeviceUpdate();
- },
- fail: () => {
- this.isUpdating = true;
- runDeviceUpdate();
- }
- });
- return;
- }
this.isUpdating = true;
runDeviceUpdate();
},
diff --git a/pages/100J/audioManager/Recording.vue b/pages/100J/audioManager/Recording.vue
index 2ff2e10..3f59151 100644
--- a/pages/100J/audioManager/Recording.vue
+++ b/pages/100J/audioManager/Recording.vue
@@ -137,9 +137,6 @@
updateLoading
} from '@/utils/loading.js';
import Common from '@/utils/Common.js';
- import {
- cache100JVoiceFileForBle
- } from '@/api/100J/HBY100-J.js';
export default {
data() {
@@ -523,22 +520,6 @@
}
const resData = JSON.parse(res.data);
if (resData.code === 200) {
- // 缓存本地路径,Apply 时优先用本地文件走蓝牙,避免下载失败
- const deviceId = these.Status.ID;
- if (deviceId) {
- const cacheKey = `100J_local_path_cache_${deviceId}`;
- const d = resData.data;
- const fileUrl = (d && typeof d === 'object' && d.fileUrl) || (typeof d === 'string' ? d : '');
- if (filePath) {
- let cache = uni.getStorageSync(cacheKey) || {};
- 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);
- }
- }
- }
// 合并两个存储操作
Promise.all([
new Promise((resolve, reject) => {
diff --git a/utils/BleHelper.js b/utils/BleHelper.js
index 87c5e64..08be2b4 100644
--- a/utils/BleHelper.js
+++ b/utils/BleHelper.js
@@ -630,20 +630,38 @@ class BleHelper {
}
return new Promise((resolve, reject) => {
- if (this.data.isOpenBlue) {
+ const runInit = () => {
+ this.CheckBlue().then((res) => {
+ return init();
+ }).then(resolve).catch((ex) => {
+ console.error("异常:", ex);
+ reject(ex);
+ });
+ };
+ if (!this.data.isOpenBlue) {
+ runInit();
+ return;
+ }
+ // 已打开过仍须向系统确认可用:关蓝牙再开后 isOpenBlue 可能未及时被置 false,或状态回调未送达
+ if (typeof uni.getBluetoothAdapterState !== 'function') {
resolve();
return;
}
-
- this.CheckBlue().then((res) => {
- // console.log("res=", res)
- return init();
- }).then(resolve).catch((ex) => {
- console.error("异常:", ex);
- reject(ex);
+ uni.getBluetoothAdapterState({
+ success: (info) => {
+ this.data.available = !!info.available;
+ if (info.available) {
+ resolve();
+ return;
+ }
+ this.data.isOpenBlue = false;
+ runInit();
+ },
+ fail: () => {
+ this.data.isOpenBlue = false;
+ runInit();
+ }
});
-
-
});
}
@@ -865,6 +883,9 @@ class BleHelper {
if (!state.available) { //蓝牙状态不可用了,将所有设备标记为断开连接
+ // 系统关蓝牙后原生 BLE 适配器已销毁;若仍认为 isOpenBlue=true,OpenBlue 会跳过重开,
+ // 再开蓝牙后无法 createBLEConnection / 一直报 10001 适配器不可用
+ this.data.isOpenBlue = false;
console.log("蓝牙模块不可用了,将所有设备标记为断开连接");
this.data.connectingDevices = {}; //清空连接锁
this.data.LinkedList.filter((v) => {