Files
APP/api/100J/HBY100-J.js
2026-04-01 10:17:40 +08:00

1251 lines
58 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}
/** 从 readFile success 的 res.data 解析为 Uint8Array失败返回 null */
function _bytesFromReadFileResult(res) {
try {
const raw = res && res.data;
if (raw instanceof ArrayBuffer) return new Uint8Array(raw);
if (raw instanceof Uint8Array) return raw;
if (raw && ArrayBuffer.isView(raw) && raw.buffer instanceof ArrayBuffer) {
return new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength);
}
} catch (e) {}
return null;
}
/** 为 readFile 准备多路径App 上 FSM 常需绝对路径_downloads/_doc 相对路径需 convert */
function getLocalPathReadCandidates(path) {
const s = String(path || '').trim();
if (!s) return [];
const out = [s];
try {
if (typeof plus !== 'undefined' && plus.io && typeof plus.io.convertLocalFileSystemURL === 'function') {
if (/^_(?:downloads|doc|www)\//i.test(s)) {
const c = plus.io.convertLocalFileSystemURL(s);
if (c && typeof c === 'string' && c !== s) out.push(c);
}
}
} catch (e) {}
return [...new Set(out)];
}
/**
* App-Plus 上仍尝试 uni.getFileSystemManager().readFile部分机型对 _downloads/_doc 路径可用),
* 带超时避免 success/fail 永不回调;与 tryGetUniFileSystemManager 不同:此处不因 plus.io 存在而跳过。
*/
function tryUniReadFileSinglePath(filePath, timeoutMs) {
return new Promise((resolve) => {
try {
if (!filePath || typeof uni === 'undefined' || typeof uni.getFileSystemManager !== 'function') {
resolve(null);
return;
}
const fsm = uni.getFileSystemManager();
if (!fsm || typeof fsm.readFile !== 'function') {
resolve(null);
return;
}
let finished = false;
const t = setTimeout(() => {
if (finished) return;
finished = true;
resolve(null);
}, timeoutMs);
fsm.readFile({
filePath: filePath,
success: (res) => {
if (finished) return;
finished = true;
clearTimeout(t);
const bytes = _bytesFromReadFileResult(res);
resolve(bytes && bytes.length > 0 ? bytes : null);
},
fail: () => {
if (finished) return;
finished = true;
clearTimeout(t);
resolve(null);
}
});
} catch (e) {
resolve(null);
}
});
}
function tryUniReadFileOnAppWithTimeout(path, timeoutMs) {
const candidates = getLocalPathReadCandidates(path);
if (!candidates.length) return Promise.resolve(null);
const n = Math.min(candidates.length, 3);
const per = Math.max(2800, Math.ceil(timeoutMs / n));
const tryIdx = (i) => {
if (i >= candidates.length) return Promise.resolve(null);
return tryUniReadFileSinglePath(candidates[i], per).then((bytes) => {
if (bytes && bytes.length > 0) {
if (i > 0) console.log('[100J-蓝牙] readFile 备用路径成功, idx=', i, 'pathHead=', String(candidates[i]).slice(0, 64));
return bytes;
}
return tryIdx(i + 1);
});
};
return tryIdx(0);
}
// ================== 蓝牙协议封装类 ==================
class HBY100JProtocol {
constructor() {
this.deviceId = ''; // 4G 接口所需的 deviceId
this.isBleConnected = false;
this.bleDeviceId = ''; // 小程序/APP中连接蓝牙的 deviceId
// 蓝牙服务与特征值 UUID
this.SERVICE_UUID = '0000AE30-0000-1000-8000-00805F9B34FB'; // 0xAE30
this.WRITE_UUID = '0000AE03-0000-1000-8000-00805F9B34FB'; // 0xAE03
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 = '') {
this.isBleConnected = !!status;
if (bleDeviceId) {
this.bleDeviceId = bleDeviceId;
} else if (!status) {
// 断开时必须清空,否则 execWithBleFirst 误判「有 id 未连」且 uploadVoiceFileBle 仍可能带旧 id
this.bleDeviceId = '';
}
}
/** 协议单字节:界面常传字符串或 -1必须转成 0~255 */
_u8(val, fallback = 0) {
const n = Number(val);
if (!Number.isFinite(n) || n < 0) return fallback & 0xFF;
return n & 0xFF;
}
onNotify(callback) {
this.onNotifyCallback = callback;
}
/**
* @param {Uint8Array|ArrayBuffer} buffer
* @param {{ skipSideEffects?: boolean }} [options] skipSideEffects=true仅解析字段不打日志、不触发 onNotify/文件回调(供 BleReceive 与设备页 bleValueNotify 双订阅时避免重复)
*/
parseBleData(buffer, options = {}) {
const skipSideEffects = !!options.skipSideEffects;
const view = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
if (view.length < 3) return null;
const header = view[0];
const tail = view[view.length - 1];
// 5.1 连接蓝牙设备主动上报 MAC 地址: FC + 6字节MAC + FF
if (header === 0xFC && tail === 0xFF && view.length >= 8) {
const macBytes = view.slice(1, 7);
const macAddress = Array.from(macBytes).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(':');
const result = { type: 'mac', macAddress };
if (!skipSideEffects) {
console.log('[100J-蓝牙] 设备上报MAC:', macAddress, '原始:', Array.from(view).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' '));
if (this.onNotifyCallback) this.onNotifyCallback(result);
}
return result;
}
if (header !== 0xFB || tail !== 0xFF) return null; // 校验头尾
const funcCode = view[1];
const data = view.slice(2, view.length - 1);
const hexStr = Array.from(view).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' ');
let result = { funcCode, rawData: data };
switch (funcCode) {
case 0x01: result.resetType = data[0]; break;
case 0x02: 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] 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);
break;
case 0x04:
// 5.5 获取设备电源状态: 容量8B + 电压8B + 百分比1B + 车载1B + 续航2B(分钟) + [充电状态1B] + FF
// 充电状态为固件新增,在续航之后;旧固件仅 20 字节 payload不影响 [16..19] 字段
if (data.length >= 20) {
result.batteryPercentage = data[16];
result.vehiclePower = data[17];
result.batteryRemainingTime = data[18] | (data[19] << 8); // 小端序,单位分钟
}
if (data.length >= 21) {
result.chargingStatus = data[20]; // 0未充电 1充电中 2已充满
}
break;
case 0x06:
// 06: 语音播报响应
result.voiceBroadcast = data[0];
break;
case 0x09:
// 09: 修改音量响应
result.volume = data[0];
break;
case 0x0A:
// 0A: 爆闪模式响应
result.strobeEnable = data[0];
result.strobeMode = data[1];
break;
case 0x0B:
// 0B: 修改警示灯爆闪频率响应
result.strobeFrequency = data[0];
break;
case 0x0C:
// 0C: 强制声光报警响应
result.alarmEnable = data[0];
result.alarmMode = data[1];
break;
case 0x0D:
// 0D: 警示灯 LED 亮度调节响应
result.redBrightness = data[0];
result.blueBrightness = data[1];
result.yellowBrightness = data[2];
break;
case 0x0E:
// 0E: 获取当前工作方式响应
result.voiceBroadcast = data[0];
result.alarmEnable = data[1];
result.alarmMode = data[2];
result.strobeEnable = data[3];
result.strobeMode = data[4];
result.strobeFrequency = data[5];
result.volume = data[6];
result.redBrightness = data[7];
result.blueBrightness = data[8];
result.yellowBrightness = data[9];
break;
}
const funcNames = { 0x01: '复位', 0x02: '基础信息', 0x03: '位置', 0x04: '电源状态', 0x05: '文件更新', 0x06: '语音播报', 0x09: '音量', 0x0A: '爆闪模式', 0x0B: '爆闪频率', 0x0C: '强制报警', 0x0D: 'LED亮度', 0x0E: '工作方式' };
const name = funcNames[funcCode] || ('0x' + funcCode.toString(16));
if (!skipSideEffects) {
console.log('[100J-蓝牙] 设备响应 FB:', name, '解析:', JSON.stringify(result), '原始:', hexStr);
if (this.onNotifyCallback) {
this.onNotifyCallback(result);
}
}
return result;
}
sendBleData(funcCode, dataBytes = []) {
return new Promise((resolve, reject) => {
if (!this.isBleConnected || !this.bleDeviceId) {
return reject(new Error('蓝牙未连接'));
}
const buffer = new ArrayBuffer(dataBytes.length + 3);
const view = new Uint8Array(buffer);
view[0] = 0xFA; // 数据头
view[1] = funcCode; // 功能码
for (let i = 0; i < dataBytes.length; i++) {
view[2 + i] = dataBytes[i];
}
view[view.length - 1] = 0xFF; // 结尾
const sendHex = Array.from(view).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' ');
console.log('[100J-蓝牙] 下发指令 FA:', '0x' + funcCode.toString(16).toUpperCase(), sendHex);
// 使用项目中统一的 BleHelper 发送数据
import('@/utils/BleHelper.js').then(module => {
const bleTool = module.default.getBleTool();
bleTool.sendData(this.bleDeviceId, buffer, this.SERVICE_UUID, this.WRITE_UUID)
.then(res => resolve(res))
.catch(err => reject(err));
});
});
}
// 纯蓝牙指令发送方法
deviceReset(type = 0) { return this.sendBleData(0x01, [type]); }
getBasicInfo() { return this.sendBleData(0x02, []); }
getLocation() { return this.sendBleData(0x03, []); }
getPowerStatus() { return this.sendBleData(0x04, []); }
setVoiceBroadcast(enable) { return this.sendBleData(0x06, [this._u8(enable)]); }
setVolume(volume) { return this.sendBleData(0x09, [this._u8(volume)]); }
setStrobeMode(enable, mode) { return this.sendBleData(0x0A, [this._u8(enable), this._u8(mode)]); }
setStrobeFrequency(frequency) { return this.sendBleData(0x0B, [this._u8(frequency)]); }
setForceAlarm(enable, mode) { return this.sendBleData(0x0C, [this._u8(enable), this._u8(mode)]); }
setLightBrightness(red, blue = 0, yellow = 0) {
return this.sendBleData(0x0D, [this._u8(red), this._u8(blue), this._u8(yellow)]);
}
getCurrentWorkMode() { return this.sendBleData(0x0E, []); }
// 0x05 文件上传:分片传输,协议 FA 05 [fileType] [phase] [data...] FF
// fileType: 1=语音 2=图片 3=动图 4=OTA
// phase: 0=开始 1=数据 2=结束
// 每包最大负载见 uploadVoiceFileBle 内 CHUNK_SIZE需与 MTU 匹配)
// 支持 fileUrl(需网络下载) 或 localPath(无网络时本地文件)
// 说明:下发的是已录制/已落盘的 MP3二进制分片经 GATT 写特征;非 A2DP/HFP 等「蓝牙录音实时流」
// meta.voiceListId若存在 uni 中 100J_voice_b64_* 缓存(与 HBY100 存 Storage 同理),优先用缓存字节下发
uploadVoiceFileBle(fileUrlOrLocalPath, fileType = 1, onProgress, meta = null) {
// 协议 5.6:单包负载最大 500B加 FA/05/FF 等约 507B真机需已协商足够 MTU见 BleHelper setBLEMTU 512
const CHUNK_SIZE = 500;
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;
}
console.warn('[100J-蓝牙] 未命中 uni 缓存将尝试读本地文件。key=', voiceBleCacheStorageKey(did, voiceListId), '若保存后立刻使用仍失败,请稍等再试或重新保存');
}
if (fileUrlOrLocalPath === BLE_CACHE_SENTINEL) {
return reject(new Error('本地语音缓存不存在或已失效,请重新保存录音'));
}
if (!fileUrlOrLocalPath) {
return reject(new Error('缺少文件地址或本地路径'));
}
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);
};
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) => {
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) => {
const tryUniReadFile = (onFail) => {
// 先走 uni readFile含 App带超时旧逻辑在 plus 存在时完全跳过 FSM导致只能卡 plus.io。
tryUniReadFileOnAppWithTimeout(path, 5000).then((bytes) => {
if (bytes && bytes.length > 0) {
console.log('[100J-蓝牙] readFile 已读出本地语音,字节:', bytes.length);
doSend(bytes);
return;
}
try {
const fsm = tryGetUniFileSystemManager();
if (!fsm || typeof fsm.readFile !== 'function') {
onFail();
return;
}
fsm.readFile({
filePath: path,
success: (res) => {
const b = _bytesFromReadFileResult(res);
if (b && b.length > 0) {
console.log('[100J-蓝牙] readFile(非App) 已读出本地语音,字节:', b.length);
doSend(b);
return;
}
onFail();
},
fail: () => onFail()
});
} catch (e) {
onFail();
}
});
};
// _downloads/resolve 与 requestFileSystem 并行竞速,避免单一路径在部分机型上长期无回调
if (path && path.startsWith('_downloads/')) {
const fileName = path.replace(/^_downloads\//, '');
tryUniReadFile(() => {
if (typeof plus === 'undefined' || !plus.io) {
reject(new Error('当前环境不支持文件读取'));
return;
}
console.log('[100J-蓝牙] _downloads 并行 resolve+PUBLIC_DOWNLOADS, fileName=', fileName);
let finished = false;
const outerMs = 20000;
const outerT = setTimeout(() => {
if (finished) return;
finished = true;
reject(new Error('读取下载目录语音超时,请重新保存录音后重试'));
}, outerMs);
const win = (bytes) => {
if (finished) return;
if (!bytes || !(bytes.length > 0)) return;
finished = true;
clearTimeout(outerT);
doSend(bytes);
};
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 (finished) return;
readFileEntry(entry, win, () => {});
}, () => {});
plus.io.requestFileSystem(plus.io.PUBLIC_DOWNLOADS, (fs) => {
if (finished) return;
fs.root.getFile(fileName, {}, (entry) => {
if (finished) return;
readFileEntry(entry, win, () => {});
}, () => {});
}, () => {});
});
return;
}
// _doc/PRIVATE_DOC 逐级与 resolve 并行,谁先读到有效字节谁胜出
if (path && path.startsWith('_doc/')) {
const relPath = path.replace(/^_doc\//, '');
const parts = relPath.split('/');
const fileName = parts.pop();
const dirs = parts;
tryUniReadFile(() => {
if (typeof plus === 'undefined' || !plus.io) {
reject(new Error('当前环境不支持文件读取'));
return;
}
console.log('[100J-蓝牙] _doc 并行 resolve+PRIVATE_DOC, path=', path.slice(0, 96));
let finished = false;
const outerMs = 20000;
const outerT = setTimeout(() => {
if (finished) return;
finished = true;
reject(new Error('读取本地语音超时(文档目录),请重新保存录音后重试'));
}, outerMs);
const win = (bytes) => {
if (finished) return;
if (!bytes || !(bytes.length > 0)) return;
finished = true;
clearTimeout(outerT);
doSend(bytes);
};
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 (finished) return;
readFileEntry(entry, win, () => {});
}, () => {});
plus.io.requestFileSystem(plus.io.PRIVATE_DOC, (fs) => {
if (finished) return;
let cur = fs.root;
const next = (i) => {
if (finished) return;
if (i >= dirs.length) {
cur.getFile(fileName, { create: false }, (entry) => {
if (finished) return;
readFileEntry(entry, win, () => {});
}, () => {});
return;
}
cur.getDirectory(dirs[i], { create: false }, (dir) => {
cur = dir;
next(i + 1);
}, () => {});
};
next(0);
}, () => {});
});
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;
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);
});
};
const startUrlFetch = () => {
let fetchUrl = fileUrlOrLocalPath;
if (fetchUrl.startsWith('http://')) fetchUrl = 'https://' + fetchUrl.slice(7);
if (onProgress) onProgress(2);
const fallbackDownload = () => {
uni.downloadFile({
url: fetchUrl,
success: (res) => {
if (res.statusCode !== 200 || !res.tempFilePath) {
if (onProgress) onProgress(0);
reject(new Error('下载失败: ' + (res.statusCode || '无路径')));
return;
}
Common.moveFileToDownloads(res.tempFilePath).then((p) => readFromPath(p)).catch(() => readFromPath(res.tempFilePath));
},
fail: (err) => {
if (onProgress) onProgress(0);
reject(err || new Error('下载失败'));
}
});
};
const reqTimeoutMs = 20000;
const reqPromise = new Promise((resolveReq, rejectReq) => {
uni.request({
url: fetchUrl,
method: 'GET',
responseType: 'arraybuffer',
timeout: reqTimeoutMs,
success: (res) => resolveReq(res),
fail: (e) => rejectReq(e || new Error('请求失败'))
});
});
const timeoutPromise = new Promise((_, rejectT) => {
setTimeout(() => rejectT(new Error('拉取语音超时')), reqTimeoutMs + 2000);
});
Promise.race([reqPromise, timeoutPromise])
.then((res) => {
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) => {
if (voiceListId !== '' && did && b && b.length) {
put100JVoiceBleCache(did, voiceListId, b);
}
this._sendVoiceChunks(b, fileType, CHUNK_SIZE, onProgress).then(resolve).catch(reject);
};
doSend(bytes);
return;
}
}
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('无法检测网络,请连接网络后重试'));
}
});
}
});
}
_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 toHex = (arr) => Array.from(arr).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' ');
console.log('[100J-蓝牙] 语音下发总大小:', total, '字节, fileType=', ft);
// 进度单调递增:前段固定 2→8数据段占 8~95结束包 99→100避免先 5% 再掉回 1% 的错觉
let progressPeak = 0;
const emitProgress = (raw) => {
const n = Math.round(Number(raw));
if (!Number.isFinite(n)) return;
const v = Math.min(100, Math.max(progressPeak, n));
progressPeak = v;
if (onProgress) onProgress(v);
};
if (total <= 0) {
emitProgress(100);
return Promise.resolve({ code: 200, msg: '语音文件已通过蓝牙上传' });
}
emitProgress(2);
const bleToolPromise = import('@/utils/BleHelper.js').then(m => m.default.getBleTool());
let bleRef = null;
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];
// 单包约 507B500 负载),依赖 MTUAndroid 上为整包 write
const waitPromise = this.waitForFileResponse(2500);
const prepMtuThenSend = (ble) => {
const run = () => {
bleRef = ble;
ble.setVoiceUploading(true);
return send(startData, ' 开始包')
.then(() => waitPromise)
.then(() => delay(DELAY_AFTER_START))
.then(() => {
emitProgress(8);
let seq = 0;
const sendNext = (offset) => {
if (offset >= total) {
return delay(DELAY_PACKET)
.then(() => send([ft, 2], ' 结束包'))
.then(() => { emitProgress(99); });
}
const chunk = bytes.slice(offset, Math.min(offset + chunkSize, total));
const chunkData = [ft, 1, seq & 0xFF, (seq >> 8) & 0xFF, ...chunk];
return send(chunkData, ` #${seq} 数据包`).then(() => {
seq++;
const doneRatio = (offset + chunk.length) / total;
emitProgress(8 + Math.round(doneRatio * 87));
return delay(DELAY_PACKET).then(() => sendNext(offset + chunk.length));
});
};
return sendNext(0);
})
.then(() => {
emitProgress(100);
return { code: 200, msg: '语音文件已通过蓝牙上传' };
});
};
try {
if (typeof plus !== 'undefined' && plus.os && plus.os.name === 'Android' && ble.setMtu) {
return ble.setMtu(this.bleDeviceId)
.catch((e) => {
console.warn('[100J-蓝牙] setBLEMTU 失败,大数据包可能无法一次写入,已用较小分片缓解:', e && (e.message || e));
})
.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);
});
}
}
// ================== 全局单例与状态管理 ==================
const protocolInstance = new HBY100JProtocol();
// 暴露给页面:更新蓝牙连接状态
export function updateBleStatus(isConnected, bleDeviceId, deviceId) {
protocolInstance.setBleConnectionStatus(isConnected, bleDeviceId);
protocolInstance.deviceId = deviceId;
console.log('[100J] 蓝牙状态:', isConnected ? '已连接(后续指令走蓝牙)' : '已断开(后续指令走4G)', { bleDeviceId: bleDeviceId || '-', deviceId });
}
// 暴露给页面:获取当前蓝牙连接状态(用于跨页面传递,确保语音管理等子页走蓝牙优先)
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) => {
if (!deviceId || voiceListId == null || !filePath) {
resolve();
return;
}
// 仅用 uni readFile + 超时App 上不再走 tryGetUniFileSystemManager 跳过逻辑。
// 旧版在此用 plus.requestFileSystem 且无总超时,部分机型永不回调 → Promise 挂死。
tryUniReadFileOnAppWithTimeout(filePath, 4000).then((bytes) => {
if (bytes && bytes.length) {
put100JVoiceBleCache(deviceId, voiceListId, bytes);
console.log('[100J] cache100JVoiceFileForBle 已写入 uni 缓存(readFile),字节:', bytes.length);
}
resolve();
});
});
}
// 暴露给页面解析蓝牙接收到的数据options 见类上 parseBleData 注释)
export function parseBleData(buffer, options) {
return protocolInstance.parseBleData(buffer, options);
}
// 暴露给页面:蓝牙连接后主动拉取电源状态(电量、续航)
export function fetchBlePowerStatus() {
if (!protocolInstance.isBleConnected) return Promise.reject(new Error('蓝牙未连接'));
console.log('[100J-蓝牙] 拉取电源状态 已通过蓝牙发送 FA 04 FF');
return protocolInstance.getPowerStatus();
}
// 暴露给页面:蓝牙连接后主动拉取定位(优先蓝牙设备也会每1分钟主动上报)
export function fetchBleLocation() {
if (!protocolInstance.isBleConnected) return Promise.reject(new Error('蓝牙未连接'));
console.log('[100J-蓝牙] 拉取定位 已通过蓝牙发送 FA 03 FF');
return protocolInstance.getLocation();
}
// 100J 设备页扫描最长约 15s指令侧不宜空等过久适配器关闭时不应再轮询
const WAIT_BLE_CONNECTED_MS = 5000;
const BLE_POLL_INTERVAL_MS = 400;
const BLE_RECONNECT_MS = 2000;
/**
* 系统蓝牙是否可用。fail/available=false 时立即走 4G。
* 若系统报开启,再与 BleHelper.data.available 交叉校验(关蓝牙后助手往往先变为 false避免仍弹「连蓝牙」类误导
*/
function getBleAdapterAvailable() {
return new Promise((resolve) => {
if (typeof uni.getBluetoothAdapterState !== 'function') {
resolve(true);
return;
}
uni.getBluetoothAdapterState({
success: (res) => {
if (!res.available) {
resolve(false);
return;
}
import('@/utils/BleHelper.js').then((m) => {
try {
const ble = m.default.getBleTool();
if (ble && ble.data && ble.data.available === false) {
resolve(false);
return;
}
} catch (e) {}
resolve(true);
}).catch(() => resolve(true));
},
fail: () => resolve(false)
});
});
}
// 等待协议层出现 bleDeviceId页面后台扫描/连接中),超时则走 4G
function waitForBleConnection(maxWaitMs = WAIT_BLE_CONNECTED_MS, intervalMs = BLE_POLL_INTERVAL_MS) {
if (protocolInstance.isBleConnected && protocolInstance.bleDeviceId) return Promise.resolve(true);
return new Promise((resolve) => {
const start = Date.now();
const tick = () => {
if (protocolInstance.isBleConnected && protocolInstance.bleDeviceId) {
console.log('[100J] 等待蓝牙连接成功');
resolve(true);
return;
}
if (Date.now() - start >= maxWaitMs) {
console.log('[100J] 等待蓝牙连接超时将走4G');
resolve(false);
return;
}
setTimeout(tick, intervalMs);
};
console.log('[100J] 蓝牙未连接,短时等待扫描/连接…', maxWaitMs, 'ms');
tick();
});
}
// 暴露给页面:尝试重连蓝牙(优先策略:断线后发指令前先尝试重连)
export function tryReconnectBle(timeoutMs = BLE_RECONNECT_MS) {
if (protocolInstance.isBleConnected) return Promise.resolve(true);
if (!protocolInstance.bleDeviceId) return Promise.resolve(false);
return new Promise((resolve) => {
import('@/utils/BleHelper.js').then(module => {
const bleTool = module.default.getBleTool();
const deviceId = protocolInstance.bleDeviceId;
const f = bleTool.data.LinkedList.find(v => v.deviceId === deviceId);
if (!f) {
resolve(false);
return;
}
const svc = f.writeServiceId || '0000AE30-0000-1000-8000-00805F9B34FB';
const write = f.wirteCharactId || '0000AE03-0000-1000-8000-00805F9B34FB';
const notify = f.notifyCharactId || '0000AE02-0000-1000-8000-00805F9B34FB';
const timer = setTimeout(() => {
resolve(protocolInstance.isBleConnected);
}, timeoutMs);
console.log('[100J] 蓝牙优先:尝试重连', deviceId);
bleTool.LinkBlue(deviceId, svc, write, notify, 1).then(() => {
clearTimeout(timer);
protocolInstance.setBleConnectionStatus(true, deviceId);
console.log('[100J] 蓝牙重连成功');
resolve(true);
}).catch(() => {
clearTimeout(timer);
resolve(false);
});
});
});
}
// ================== API 接口 (拦截层) ==================
// 获取语音管理列表
export function deviceVoliceList(params) {
return request({
url: `/app/video/queryAudioFileList`,
method: 'get',
data:params
})
}
// 重命名
export function videRenameAudioFile(data) {
return request({
url: `/app/video/renameAudioFile`,
method: 'post',
data:data
})
}
// 删除语音文件列表
export function deviceDeleteAudioFile(params) {
return request({
url: `/app/video/deleteAudioFile`,
method: 'get',
data:params
})
}
// 更新语音/使用语音:蓝牙优先;云端 fileUrl 蓝牙失败时可 4G 仅传 id 触发设备拉流
// 本地 localPath 仅能通过蓝牙下发,禁止回退 4G否则服务端成功但设备无文件
function isHttpUrlString(s) {
return !!(s && /^https?:\/\//i.test(String(s).trim()));
}
/** 与后端约定communicationMode 0=4G1=蓝牙(云端记录本次「使用」语音的通讯方式) */
export function deviceUpdateVoice(data) {
const httpExec = (communicationMode) => request({
url: `/app/hby100j/device/updateVoice`,
method: 'post',
data: {
id: data.id,
communicationMode: communicationMode === 1 ? 1 : 0
}
});
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);
// 仅「没有任何云端 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; });
}
console.log('[100J] 语音上传:有文件源,尝试蓝牙下发文件', { isBleConnected: protocolInstance.isBleConnected, hasBleDeviceId: !!protocolInstance.bleDeviceId, local: !!hasLocalPath });
// 蓝牙传完文件后再调 updateVoice(communicationMode=1)。若仅 HTTP 失败而文件已下发,仍返回 _channel=ble
// 切勿让整链 reject 触发 execWithBleFirst 走 4G+MQTT否则界面等不到进度会误报「音频进度同步超时」。
const bleExec = () => protocolInstance.uploadVoiceFileBle(fileSource, 1, data.onProgress, { voiceListId: data.id })
.then(() =>
httpExec(1)
.then((res) => {
if (res && typeof res === 'object') res._channel = 'ble';
return res;
})
.catch((e) => {
console.warn('[100J] 蓝牙已传完语音文件updateVoice(communicationMode=1) 失败:', e && (e.message || e));
return {
code: 200,
msg: '语音已通过蓝牙下发',
_channel: 'ble',
_updateVoiceAfterBleFailed: true
};
})
);
const http4g = () => httpExec(0);
// 无云端 URL 的纯本地文件:禁止 4G 兜底,避免仅传 id 假成功;有 https 时蓝牙失败可 4G
return execWithBleFirst(bleExec, http4g, '语音文件上传', data.onWaiting, { no4GFallback });
}
// 100J信息
export function deviceDetail(id) {
return request({
url: `/app/hby100j/device/${id}`,
method: 'get',
})
}
// 蓝牙优先、4G 兜底:未连接时先等待扫描/连接,再尝试重连;蓝牙失败时回退 4G
// 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) => {
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 {
uni.hideLoading();
} catch (e) {}
};
const showWaitUi = (title) => {
if (typeof onWaiting === 'function') return;
try {
uni.hideLoading();
uni.showLoading({ title, mask: false });
} catch (e) {}
};
// 协议层认为已连:仍可能被系统蓝牙关闭/底层已断而滞后,先校验适配器,避免先发蓝牙卡超时再回退
if (protocolInstance.isBleConnected && protocolInstance.bleDeviceId) {
return getBleAdapterAvailable().then((adapterOk) => {
if (!adapterOk) {
protocolInstance.setBleConnectionStatus(false, '');
return go4GOrReject('系统蓝牙已关闭走4G');
}
console.log('[100J]', logName || '指令', '协议层已连接,走蓝牙');
return doBle().catch(onBleSendFail);
});
}
console.log('[100J]', logName || '指令', '协议层未就绪', { isBleConnected: protocolInstance.isBleConnected, hasBleDeviceId: !!protocolInstance.bleDeviceId });
// 无 bleDeviceId本地仅蓝牙场景仍弹「请稍候」并等扫描可走 4G 时不弹 loading、不白等 5s直接 4G
if (!protocolInstance.bleDeviceId) {
return getBleAdapterAvailable().then((adapterOk) => {
if (!adapterOk) {
protocolInstance.setBleConnectionStatus(false, '');
return go4GOrReject('系统蓝牙未开启走4G');
}
if (no4G) {
if (typeof onWaiting === 'function') onWaiting();
else showWaitUi('请稍候…');
return waitForBleConnection()
.then((connected) => {
return connected ? doBle().catch(onBleSendFail) : go4GOrReject('蓝牙未连接');
})
.finally(hideWaitUi);
}
return waitForBleConnection(0, BLE_POLL_INTERVAL_MS).then((connected) =>
connected ? doBle().catch(onBleSendFail) : go4GOrReject('无蓝牙连接走4G')
);
});
}
// 有 bleDeviceId 但未连:用户刚关蓝牙/超出范围时,不再弹「请稍候」等重连 ~2s双通道在线时直接 4G
return getBleAdapterAvailable().then((adapterOk) => {
if (!adapterOk) {
protocolInstance.setBleConnectionStatus(false, '');
return go4GOrReject('系统蓝牙未开启走4G');
}
console.log('[100J]', logName || '指令', '蓝牙已断开直接走4G不阻塞重连');
return go4GOrReject(null);
});
}
// 爆闪模式
export function deviceStrobeMode(data) {
return execWithBleFirst(
() => protocolInstance.setStrobeMode(data.enable, data.mode).then(() => ({ code: 200, msg: '操作成功(蓝牙)' })),
() => request({ url: `/app/hby100j/device/strobeMode`, method: 'post', data }),
'爆闪模式'
);
}
// 强制报警
export function deviceForceAlarmActivation(data) {
return execWithBleFirst(
() => protocolInstance.setForceAlarm(data.voiceStrobeAlarm, data.mode).then(() => ({ code: 200, msg: '操作成功(蓝牙)' })),
() => request({ url: `/app/hby100j/device/forceAlarmActivation`, method: 'post', data }),
'强制报警'
);
}
// 爆闪频率
export function deviceStrobeFrequency(data) {
return execWithBleFirst(
() => protocolInstance.setStrobeFrequency(data.frequency).then(() => ({ code: 200, msg: '操作成功(蓝牙)' })),
() => request({ url: `/app/hby100j/device/strobeFrequency`, method: 'post', data }),
'爆闪频率'
);
}
// 灯光调节亮度
export function deviceLightAdjustment(data) {
return execWithBleFirst(
() => protocolInstance.setLightBrightness(data.brightness, data.brightness, data.brightness).then(() => ({ code: 200, msg: '操作成功(蓝牙)' })),
() => request({ url: `/app/hby100j/device/lightAdjustment`, method: 'post', data }),
'灯光亮度'
);
}
// 调节音量
export function deviceUpdateVolume(data) {
return execWithBleFirst(
() => protocolInstance.setVolume(data.volume).then(() => ({ code: 200, msg: '操作成功(蓝牙)' })),
() => request({ url: `/app/hby100j/device/updateVolume`, method: 'post', data }),
'调节音量'
);
}
/** 蓝牙侧语音播报:自定义音(mode=7)且强制报警仍开时,需先 0x0C 关报警+模式7 再 0x06已解除则不必重复下发 0x0C */
function bleVoiceBroadcastChain(data) {
const on = Number(data.voiceBroadcast) === 1;
const mode = data.mode != null ? String(data.mode) : '';
if (on && mode === '7') {
const alarmOn = Number(data.voiceStrobeAlarm) === 1;
if (alarmOn) {
return protocolInstance.setForceAlarm(0, 7).then(() => protocolInstance.setVoiceBroadcast(1));
}
return protocolInstance.setVoiceBroadcast(1);
}
return protocolInstance.setVoiceBroadcast(on ? 1 : 0);
}
// 语音播放HTTP 透传 data便于后端识别 mode
export function deviceVoiceBroadcast(data) {
const httpData = data && typeof data === 'object' ? { ...data } : data;
if (httpData && typeof httpData === 'object') delete httpData.voiceStrobeAlarm;
return execWithBleFirst(
() => bleVoiceBroadcastChain(data).then(() => ({ code: 200, msg: '操作成功(蓝牙)' })),
() => request({ url: `/app/hby100j/device/voiceBroadcast`, method: 'post', data: httpData }),
'语音播报'
);
}