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.json b/pages.json index fade1c1..a44b2fb 100644 --- a/pages.json +++ b/pages.json @@ -347,6 +347,14 @@ "navigationBarTitleText" : "HBY102" } }, + { + "path" : "pages/102J/HBY102J", + "style" : + { + "navigationStyle": "custom", + "navigationBarTitleText" : "HBY102" + } + }, { "path" : "pages/common/user/logOff", "style" : diff --git a/pages/102J/HBY102J.vue b/pages/102J/HBY102J.vue new file mode 100644 index 0000000..d0f4b75 --- /dev/null +++ b/pages/102J/HBY102J.vue @@ -0,0 +1,2302 @@ + + + + + \ No newline at end of file diff --git a/pages/6075J/BJQ6075J.vue b/pages/6075J/BJQ6075J.vue index 3f65de2..c530287 100644 --- a/pages/6075J/BJQ6075J.vue +++ b/pages/6075J/BJQ6075J.vue @@ -260,7 +260,7 @@ baseURL } from '@/utils/request.js'; import { - showLoading, + showLoading, hideLoading, updateLoading } from '@/utils/loading.js'; @@ -295,7 +295,7 @@ }, data() { return { - Status: { + Status: { pageHide: false, diff --git a/pages/common/addBLE/addEquip.vue b/pages/common/addBLE/addEquip.vue index 5ebecfd..3575018 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); } @@ -402,9 +406,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 75be4ed..255128b 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, @@ -30,6 +33,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), @@ -70,15 +74,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; } @@ -107,8 +120,8 @@ class BleReceive { } } else { - // 100J AE30 二进制帧在 f 不完整时已在上方 parseBleData,此处不再误报「无法处理」 - if (!is100JAe30) { + // AE30:100J 在 f 不完整时已在上方 parseBleData;102J 控制帧被有意跳过 parseBleData + if (!isAe30) { console.error("已收到该消息,但无法处理", receive, "f:", f); } } @@ -1013,6 +1026,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 = {};