From 736f24839fddd2aa1687114a6930fc79bcfc9c70 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: Fri, 17 Apr 2026 09:45:02 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0102J=E8=93=9D=E7=89=99?= =?UTF-8?q?=E5=8D=8F=E8=AE=AE=E5=BE=85=E8=AE=BE=E5=A4=87=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/102J/hby102jBleProtocol.js | 159 ++++++++++++++++++++++++++++ pages/102J/HBY102J.vue | 88 ++++++++++------ pages/common/addBLE/addEquip.vue | 12 ++- utils/BleReceive.js | 172 +++++++++++++++++++++++++++++-- 4 files changed, 384 insertions(+), 47 deletions(-) create mode 100644 api/102J/hby102jBleProtocol.js diff --git a/api/102J/hby102jBleProtocol.js b/api/102J/hby102jBleProtocol.js new file mode 100644 index 0000000..32a0c96 --- /dev/null +++ b/api/102J/hby102jBleProtocol.js @@ -0,0 +1,159 @@ +/** + * HBY102J 晶全应用层协议:下行 FA … FF,上行 FB/FC … FF。 + * 与 HBY100-J 使用同一颗蓝牙模组 / 同一套 GATT(AE30 + AE03 写 + AE02 通知), + * 见 `api/100J/HBY100-J.js` 中 SERVICE_UUID / WRITE / NOTIFY;仅帧语义与 100J 不同。 + */ + +export const HBY102J_SERVICE = '0000AE30-0000-1000-8000-00805F9B34FB' +export const HBY102J_WRITE = '0000AE03-0000-1000-8000-00805F9B34FB' +export const HBY102J_NOTIFY = '0000AE02-0000-1000-8000-00805F9B34FB' + +function buildFaFrame(func, dataBytes = []) { + return [0xfa, func & 0xff, ...dataBytes.map((b) => b & 0xff), 0xff] +} + +const BYTE_TO_RADAR = { + 0: 'status_off', + 1: 'status_2M', + 2: 'status_4M', + 3: 'status_7M', + 4: 'status_10M' +} + +const BYTE_TO_LED = { + 0: 'led_off', + 1: 'led_flash', + 2: 'led_low_flash', + 3: 'led_steady', + 4: 'led_alarm' +} + +const RADAR_KEY_TO_BYTE = { + status_off: 0, + status_2M: 1, + status_4M: 2, + status_7M: 3, + status_10M: 4, + status_on: 1 +} + +const LED_KEY_TO_BYTE = { + led_off: 0, + led_flash: 1, + led_low_flash: 2, + led_steady: 3, + led_alarm: 4 +} + +export function encodeChannelSet(channelDecimal1to80) { + const ch = Math.max(1, Math.min(80, Number(channelDecimal1to80) || 1)) + return buildFaFrame(0x21, [ch]) +} + +export function encodeOnline(on) { + return buildFaFrame(0x22, [on ? 1 : 0]) +} + +export function encodeRadarFromUiKey(sta_RadarType) { + const b = RADAR_KEY_TO_BYTE[sta_RadarType] !== undefined ? RADAR_KEY_TO_BYTE[sta_RadarType] : 0 + return buildFaFrame(0x23, [b]) +} + +export function encodeWarningLightFromLedKey(ledKey) { + const m = LED_KEY_TO_BYTE[ledKey] !== undefined ? LED_KEY_TO_BYTE[ledKey] : 0 + return buildFaFrame(0x24, [m]) +} + +export function encodeQueryOnlineCount() { + return buildFaFrame(0x25, [0]) +} + +export function encodeQueryPower() { + return buildFaFrame(0x26, []) +} + +function formatMacFromBytes(u8, start, len) { + const hex = [] + for (let i = 0; i < len; i++) { + hex.push(u8[start + i].toString(16).padStart(2, '0')) + } + return hex.join(':').toUpperCase() +} + +/** + * 解析设备上行一帧(Notify),输出与 HBY102 页 formData 对齐的字段 + */ +export function parseHby102jUplink(u8) { + const out = {} + if (!u8 || u8.length < 3) return out + + const h = u8[0] + const func = u8[1] + const tail = u8[u8.length - 1] + + if (tail !== 0xff && u8.length >= 4) { + // 非标准结尾时仍尽量解析 + } + + if (h === 0xfc && u8.length >= 8 && u8[7] === 0xff) { + out.sta_address = formatMacFromBytes(u8, 1, 6) + return out + } + + if (h !== 0xfb) return out + + switch (func) { + case 0x21: + if (u8.length >= 4) { + out.sta_Channel = u8[2] + } + break + case 0x22: + out.sta_Online = u8[2] === 1 ? 'E49_on' : 'E49_off' + break + case 0x23: + out.sta_RadarType = BYTE_TO_RADAR[u8[2]] !== undefined ? BYTE_TO_RADAR[u8[2]] : 'status_off' + break + case 0x24: + out.sta_LedType = BYTE_TO_LED[u8[2]] !== undefined ? BYTE_TO_LED[u8[2]] : 'led_off' + break + case 0x25: + if (u8.length >= 4) { + out.sta_onlineQuantity = u8[2] + } + break + case 0x26: + if (u8.length >= 5) { + out.sta_PowerPercent = u8[2] + out.sta_charge = u8[3] + } + break + case 0x27: + if (u8.length >= 9) { + out.sta_PowerPercent = u8[2] + out.sta_charge = u8[3] + out.sta_Channel = u8[4] + out.sta_LedType = BYTE_TO_LED[u8[5]] !== undefined ? BYTE_TO_LED[u8[5]] : 'led_off' + out.sta_RadarType = BYTE_TO_RADAR[u8[6]] !== undefined ? BYTE_TO_RADAR[u8[6]] : 'status_off' + out.sta_Online = u8[7] === 1 ? 'E49_on' : 'E49_off' + } + break + case 0x28: + if (u8.length >= 10) { + const peerMac = formatMacFromBytes(u8, 2, 6) + const ev = u8[8] + if (ev === 0x01) { + out.sta_sosadd = peerMac + out.sta_Intrusion = 1 + } else if (ev === 0x02) { + out.sta_sosadd_off = peerMac + } else if (ev === 0x03) { + out.sta_tomac = peerMac + } + } + break + default: + break + } + return out +} diff --git a/pages/102J/HBY102J.vue b/pages/102J/HBY102J.vue index 294b6ca..d0f4b75 100644 --- a/pages/102J/HBY102J.vue +++ b/pages/102J/HBY102J.vue @@ -159,7 +159,7 @@ :showCancel="Status.Pop.showCancel" @cancelPop="closePop" :showSlot="Status.Pop.showSlot"> 修改信道 - @@ -199,7 +199,15 @@ showPop, MsgInfo } from '@/utils/MsgPops.js' - const pagePath = "/pages/102/HBY102"; + import { + encodeChannelSet, + encodeOnline, + encodeRadarFromUiKey, + encodeWarningLightFromLedKey, + encodeQueryOnlineCount, + encodeQueryPower + } from '@/api/102J/hby102jBleProtocol.js' + const pagePath = "/pages/102J/HBY102J"; var ble = null; var these = null; @@ -298,7 +306,7 @@ callback: this.gotoShare } ], - title: 'HBY102' + title: 'HBY102J' }, apiType: '' @@ -411,7 +419,7 @@ latitude: null, longitude: null, alarmStatus: null, - detailPageUrl: "/pages/650/HBY650", + detailPageUrl: "/pages/102J/HBY102J", showConfirm: false }, groupDevices: [], @@ -488,6 +496,9 @@ these.formData.bleStatu = 'connecting'; ble.LinkBlue(f.deviceId, f.writeServiceId, f.wirteCharactId, f.notifyCharactId).then(res => { these.formData.bleStatu = true; + setTimeout(() => { + these.send102jCmd(encodeQueryPower()); + }, 300); }).catch(ex => { these.formData.bleStatu = 'err'; MsgError("连接错误:" + ex.msg, "确定", these); @@ -513,6 +524,9 @@ ble.LinkBlue(f.deviceId, f.writeServiceId, f.wirteCharactId, f.notifyCharactId).then(res => { console.log("连接成功") these.formData.bleStatu = true; + setTimeout(() => { + these.send102jCmd(encodeQueryPower()); + }, 300); }).catch(ex => { these.formData.bleStatu = 'err'; MsgError("连接错误:" + ex.msg, "确定", these); @@ -561,6 +575,15 @@ } }, methods: { + send102jCmd(hexArr) { + let f = this.getDevice(); + if (!f) { + return Promise.reject({ + msg: '未连接蓝牙' + }); + } + return ble.sendHexs(f.deviceId, hexArr, f.writeServiceId, f.wirteCharactId, 30); + }, onChannelChanging() { let f = this.getDevice(); @@ -572,17 +595,20 @@ // #endif let regex = /^([1-9]|[1-7][0-9]|80)$/; - if (!regex.test(this.formData.ins_Channel)) { + if (!regex.test(String(this.formData.ins_Channel))) { uni.showModal({ title: '提示', content: '只能输入1-80整数' }); return; } - var buffer = { - ins_Channel: this.formData.ins_Channel - } - ble.sendString(f.deviceId, buffer); + const ch = Number(this.formData.ins_Channel); + this.send102jCmd(encodeChannelSet(ch)).catch((ex) => { + uni.showModal({ + title: '错误', + content: ex.msg || '发送失败' + }); + }); }, ShowChannelEdit() { @@ -644,10 +670,7 @@ if (ble) { - let buffer = { - ins_Quantity: "query" - }; - ble.sendString(f.deviceId, buffer, f.writeServiceId, f.wirteCharactId, 30).then(res => { + this.send102jCmd(encodeQueryOnlineCount()).then(() => { setTimeout(() => { this.getWarns(); }, 1500); @@ -738,7 +761,7 @@ }, success: (res) => { let json = { - persissonType: '102' + persissonType: '102J' }; Object.assign(json, this.device); res.eventChannel.emit('share', { @@ -813,12 +836,9 @@ val = 'E49_off'; } let task = () => { - let json = { - ins_Online: val - } - - ble.sendString(f.deviceId, json, f.writeServiceId, f.wirteCharactId, 30) - .then(res => { + const on = val === 'E49_on'; + this.send102jCmd(encodeOnline(on)) + .then(() => { this.formData.sta_Online = val; these.setBleFormData(); }) @@ -889,13 +909,8 @@ } let task = () => { let promise = new Promise((resolve, reject) => { - let json = { - ins_RadarType: val - } - - ble.sendString(f.deviceId, json, f.writeServiceId, f.wirteCharactId, 30) - .then(res => { - debugger; + this.send102jCmd(encodeRadarFromUiKey(val)) + .then(() => { this.formData.sta_RadarType = val; this.Status.BottomMenu.activeIndex = index; these.setBleFormData(); @@ -972,14 +987,9 @@ deviceId: '12345' } // #endif - debugger; let task = (val) => { let promise = new Promise((resolve, reject) => { - let json = { - ins_LedType: val - } - json = JSON.stringify(json); - ble.sendString(f.deviceId, json, f.writeServiceId, f.wirteCharactId, 30).then(res => { + this.send102jCmd(encodeWarningLightFromLedKey(val)).then(() => { this.formData.sta_LedType = val; these.setBleFormData(); resolve(); @@ -1094,11 +1104,18 @@ text: "蓝牙恢复可用,正在连接设备" }); this.formData.bleStatu = 'connecting'; - ble.LinkBlue(these.formData.deviceId).then(() => { + const lf = these.getDevice(); + const linkP = lf + ? ble.LinkBlue(these.formData.deviceId, lf.writeServiceId, lf.wirteCharactId, lf.notifyCharactId) + : ble.LinkBlue(these.formData.deviceId); + linkP.then(() => { these.formData.bleStatu = true; updateLoading(these, { text: '连接成功' }); + setTimeout(() => { + these.send102jCmd(encodeQueryPower()); + }, 300); }).catch(ex => { these.formData.bleStatu = 'err'; updateLoading(these, { @@ -1136,6 +1153,9 @@ this.getWarns(); }, 500); + if (json && json.bytes) { + return; + } let active = -1; let f = this.Status.BottomMenu.menuItems.find((item, index) => { diff --git a/pages/common/addBLE/addEquip.vue b/pages/common/addBLE/addEquip.vue index 18165c9..6e22c8a 100644 --- a/pages/common/addBLE/addEquip.vue +++ b/pages/common/addBLE/addEquip.vue @@ -357,8 +357,12 @@ device.isTarget = true; } } + // 102J:与 100J 同 AE30 芯片;协议不同。广播名一般为 HBY102J-xxxxxx(见协议) + if (device.name && /^HBY102J/i.test(device.name)) { + device.isTarget = true; + } if (device.name) { - device.name = device.name.replace('JQZM-', ''); + device.name = device.name.replace(/^JQZM-/i, '').replace(/^HBY102J-/i, ''); } these.EquipMents.push(device); } @@ -399,9 +403,9 @@ return; } - if (receivData.str.indexOf('mac address:') > -1 || receivData.str.indexOf( - 'sta_address') > -1 || - (receivData.bytes[0] === 0xFC && receivData.bytes.length >= 7)) { + if ((receivData.str && (receivData.str.indexOf('mac address:') > -1 || receivData.str.indexOf( + 'sta_address') > -1)) || + (receivData.bytes && receivData.bytes[0] === 0xFC && receivData.bytes.length >= 7)) { if (f.macAddress && these.device) { diff --git a/utils/BleReceive.js b/utils/BleReceive.js index 12206c1..f263bb9 100644 --- a/utils/BleReceive.js +++ b/utils/BleReceive.js @@ -2,6 +2,9 @@ import Common from '@/utils/Common.js' import { parseBleData } from '@/api/100J/HBY100-J.js' +import { + parseHby102jUplink +} from '@/api/102J/hby102jBleProtocol.js' import { MsgSuccess, MsgError, @@ -27,6 +30,7 @@ class BleReceive { '/pages/4877/BJQ4877': this.Receive_4877.bind(this), '/pages/100/HBY100': this.Receive_100.bind(this), '/pages/102/HBY102': this.Receive_102.bind(this), + '/pages/102J/HBY102J': this.Receive_102J.bind(this), '/pages/6170/deviceControl/index': this.Receive_6170.bind(this), '/pages/100J/HBY100-J': this.Receive_100J.bind(this), '/pages/6075J/BJQ6075J': this.Receive_6075.bind(this), @@ -66,15 +70,24 @@ class BleReceive { ReceiveData(receive, f, path, recArr) { - // 100J:首页等场景 LinkedList 项可能未带齐 mac/device,但语音分片上传依赖 parseBleData 消费 FB 05 + // AE30 服务:100J 与 102J 为同一套蓝牙芯片(GATT 相同),应用层协议不同。 + // 100J:f 未就绪时仍需 parseBleData 消费 FB 05 等语音/文件应答。 + // 102J:上行 FB 21–28 为晶全协议,不得走 100J parseBleData,避免误触发语音/文件回调。 const sid = receive && receive.serviceId ? String(receive.serviceId) : ''; - const is100JAe30 = /ae30/i.test(sid); + const isAe30 = /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); + const u8Early = receive && receive.bytes && receive.bytes.length >= 4 + ? new Uint8Array(receive.bytes) + : null; + const is102JFbControl = u8Early && u8Early[0] === 0xfb && u8Early[u8Early.length - 1] === 0xff + && u8Early[1] >= 0x21 && u8Early[1] <= 0x28; + if (isAe30 && receive && receive.bytes && receive.bytes.length >= 3 && !fReady) { + if (!is102JFbControl) { + try { + parseBleData(new Uint8Array(receive.bytes)); + } catch (e) { + console.warn('[100J/AE30] ReceiveData 兜底 parseBleData 失败', e); + } } return receive; } @@ -103,8 +116,8 @@ class BleReceive { } } else { - // 100J AE30 二进制帧在 f 不完整时已在上方 parseBleData,此处不再误报「无法处理」 - if (!is100JAe30) { + // AE30:100J 在 f 不完整时已在上方 parseBleData;102J 控制帧被有意跳过 parseBleData + if (!isAe30) { console.error("已收到该消息,但无法处理", receive, "f:", f); } } @@ -944,6 +957,147 @@ class BleReceive { } + Receive_102J(receive, f, path, recArr) { + let receiveData = {} + try { + if (receive && receive.bytes && receive.bytes.length >= 3) { + receiveData = parseHby102jUplink(new Uint8Array(receive.bytes)) + } + + let recCnt = recArr.find((v) => { + return v.key.replace(/\//g, '').toLowerCase() == f.device.detailPageUrl + .replace(/\//g, '').toLowerCase() + }) + if (!recCnt) { + let msgs = [] + if (receiveData.sta_PowerPercent <= 20 && receiveData.sta_charge == 0) { + msgs.push("设备'" + f.device.deviceName + "'电量低") + } + if (receiveData.sta_Intrusion === 1) { + msgs.push("设备'" + f.device.deviceName + "'闯入报警中") + } + if (this.ref && msgs.length > 0) { + const text = msgs.join(',') + MsgError(text, '', this.ref, () => { + MsgClear(this.ref) + }) + } + } + + if (f.device && path === 'pages/common/index') { + let linkKey = '102J_' + f.device.id + '_linked' + let warnKey = '102J_' + f.device.id + '_warning' + let time = new Date() + + if (receiveData.sta_tomac) { + if (receiveData.sta_tomac.indexOf(':') === -1) { + receiveData.sta_tomac = receiveData.sta_tomac.replace(/(.{2})/g, '$1:').slice(0, -1) + } + uni.getStorageInfo({ + success: function(res) { + let arr = [] + let linked = { + linkId: f.linkId, + read: false, + linkEqs: [{ + linkTime: time, + linkMac: f.macAddress + }, { + linkTime: time, + linkMac: receiveData.sta_tomac + }] + } + if (res.keys.includes(linkKey)) { + arr = uni.getStorageSync(linkKey) + } + if (arr.length === 0) { + arr.unshift(linked) + } else { + let dev = arr.find((v) => { + if (v.linkId === f.linkId) { + let vl = v.linkEqs.find((cvl) => { + if (cvl.linkMac === receiveData.sta_tomac) { + v.read = false + cvl.linkTime = time + return true + } + return false + }) + if (!vl) { + v.linkEqs.push({ + linkTime: time, + linkMac: receiveData.sta_tomac + }) + } + return vl + } + return false + }) + if (!dev) { + arr.unshift(linked) + } + } + uni.setStorage({ + key: linkKey, + data: arr + }) + } + }) + } + + let warnArrs = [] + let guid = Common.guid() + if (receiveData.sta_sosadd) { + if (receiveData.sta_sosadd.indexOf(':') === -1) { + receiveData.sta_sosadd = receiveData.sta_sosadd.replace(/(.{2})/g, '$1:').slice(0, -1) + } + warnArrs.push({ + linkId: f.linkId, + read: false, + key: guid, + warnType: '闯入报警', + warnMac: receiveData.sta_sosadd, + warnTime: time, + warnName: '' + }) + } + + if (receiveData.sta_LedType === 'led_alarm' || receiveData.ins_LedType === 'led_alarm') { + warnArrs.push({ + linkId: f.linkId, + read: false, + key: guid, + warnType: '强制报警', + warnMac: f.macAddress, + warnTime: time, + warnName: '' + }) + } + + if (warnArrs.length > 0) { + uni.getStorageInfo({ + success: function(res) { + let arr = [] + if (res.keys.includes(warnKey)) { + arr = uni.getStorageSync(warnKey) + arr = warnArrs.concat(arr) + } else { + arr = warnArrs + } + uni.setStorage({ + key: warnKey, + data: arr + }) + } + }) + } + } + } catch (error) { + receiveData = {} + console.log('Receive_102J 解析失败', error) + } + return receiveData + } Receive_6075(receive, f, path, recArr) { let receiveData = {};