new-20250827 加入008A、100Y #41

Merged
liubiao merged 10 commits from liubiao/APP:new-20250827 into main 2026-04-03 10:05:25 +08:00
43 changed files with 2294 additions and 633 deletions
Showing only changes of commit 222c578f2c - Show all commits

View File

@ -210,7 +210,7 @@ class HBY100JProtocol {
}
break;
case 0x05:
// 05: 文件更新响应 FB 05 [fileType] [status] FFstatus: 1=成功 2=失败
// 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 (!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];
// 单包约 507B500 负载),依赖 MTUAndroid 上为整包 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();
};

View File

@ -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 = {};

View File

@ -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 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 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();
},

View File

@ -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) => {

View File

@ -630,20 +630,38 @@ class BleHelper {
}
return new Promise((resolve, reject) => {
if (this.data.isOpenBlue) {
resolve();
return;
}
const runInit = () => {
this.CheckBlue().then((res) => {
// console.log("res=", 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;
}
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=trueOpenBlue 会跳过重开,
// 再开蓝牙后无法 createBLEConnection / 一直报 10001 适配器不可用
this.data.isOpenBlue = false;
console.log("蓝牙模块不可用了,将所有设备标记为断开连接");
this.data.connectingDevices = {}; //清空连接锁
this.data.LinkedList.filter((v) => {