// W13 BLE 协议公共组包模块 // 导出通用的组包构造与CRC16计算(支持 CCITT-FALSE 与 MODBUS) const DEFAULT_HEAD = [0xCC, 0xC0]; const MAX_LEN = 548; // 文档声明的最大总长度(含包头与CRC) export const COMMANDS = { READ_VERSION: 0x01, SET_CONDITION_1: 0x08, SET_CONDITION_2: 0x09, OTA_START: 0x0B, ENABLE_BLE_LOG: 0x0C, // 设置门磁/卫浴事件触发/释放参数 SET_DOOR_BATH_EVENT: 0x16, RADAR_STATUS: 0x11, TEST_KEYS: 0x13, }; function toUint16LE(v) { const x = v & 0xFFFF; return [x & 0xFF, (x >>> 8) & 0xFF]; } function ensureUint8Array(data) { if (!data) return new Uint8Array(0); if (data instanceof Uint8Array) return data; if (Array.isArray(data)) return Uint8Array.from(data.map(x => x & 0xFF)); if (typeof data === 'string') { // 如果是十六进制字符串(包含空格或连续十六进制),则按hex解析 const hexOnly = data.trim(); if (/^[0-9a-fA-F\s]+$/.test(hexOnly)) { const parts = hexOnly.split(/\s+/).filter(Boolean); let bytes = []; if (parts.length > 1) { bytes = parts.map(p => parseInt(p, 16) & 0xFF); } else { const s = parts[0]; for (let i = 0; i < s.length; i += 2) { const hex = s.substr(i, 2); if (hex.length === 2) bytes.push(parseInt(hex, 16) & 0xFF); } } return Uint8Array.from(bytes); } // 默认按ASCII编码;如需GBK请先外部转换后传入字节数组 const arr = new Uint8Array(data.length); for (let i = 0; i < data.length; i++) arr[i] = data.charCodeAt(i) & 0xFF; return arr; } throw new TypeError('Unsupported payload type'); } // CRC-16/CCITT-FALSE: poly=0x1021, init=0xFFFF, refin=false, refout=false, xorout=0x0000 export function crc16Ccitt(bytes, start = 0, end = bytes.length) { let crc = 0xFFFF; for (let i = start; i < end; i++) { crc ^= (bytes[i] << 8); for (let j = 0; j < 8; j++) { if (crc & 0x8000) crc = (crc << 1) ^ 0x1021; else crc <<= 1; crc &= 0xFFFF; } } return crc & 0xFFFF; } // CRC-16/MODBUS: poly=0xA001, init=0xFFFF, refin=true, refout=true, xorout=0x0000 export function crc16Modbus(bytes, start = 0, end = bytes.length) { let crc = 0xFFFF; for (let i = start; i < end; i++) { crc ^= bytes[i]; for (let j = 0; j < 8; j++) { const lsb = crc & 0x0001; crc >>= 1; if (lsb) crc ^= 0xA001; } } return crc & 0xFFFF; } function computeCrc(bytes, type, range) { const [start, end] = range || [0, bytes.length]; if (type === 'MODBUS') return crc16Modbus(bytes, start, end); return crc16Ccitt(bytes, start, end); } /** * 构造W13协议数据帧 * 字段:Head(2) + Len(2) + CRC(2) + Frame(2) + FramNum(2) + Frame_Type(1) + Parameters(N) * Len为整包总长度(含Head与CRC),小端;CRC16默认CCITT-FALSE,计算范围为整包除CRC字段自身。 * * @param {Object} opts * @param {number} opts.frame 帧号(0~65535) * @param {number} opts.framNum 帧总数(0~65535) * @param {number} opts.type 命令字(0~255) * @param {Uint8Array|number[]|string} [opts.params] 参数字节或ASCII字符串 * @param {('CCITT'|'MODBUS')} [opts.crcType] CRC算法选择,默认CCITT * @param {number[]} [opts.head] 包头,默认[0xCC,0xC0] * @returns {Uint8Array} */ export function buildPacket(opts) { const head = (opts && opts.head) || DEFAULT_HEAD; const frame = (opts && opts.frame) != null ? (opts.frame >>> 0) : 1; const framNum = (opts && opts.framNum) != null ? (opts.framNum >>> 0) : 1; const type = (opts && opts.type) != null ? (opts.type & 0xFF) : 0x00; const params = ensureUint8Array(opts && opts.params); // 默认CRC方式改为 MODBUS(可显式传 'CCITT' 使用CCITT-FALSE) const crcType = (opts && opts.crcType) === 'CCITT' ? 'CCITT' : 'MODBUS'; // 预分配:固定头(2) + 长度(2) + CRC(2) + Frame(2) + FramNum(2) + Type(1) + 参数 const fixedLen = 2 + 2 + 2 + 2 + 2 + 1; const totalLen = fixedLen + params.length; if (totalLen > MAX_LEN) throw new RangeError(`Packet too long: ${totalLen} > ${MAX_LEN}`); const buf = new Uint8Array(totalLen); let off = 0; // Head buf[off++] = head[0] & 0xFF; buf[off++] = head[1] & 0xFF; // Len (占位后写入) const lenLE = toUint16LE(totalLen); buf[off++] = lenLE[0]; buf[off++] = lenLE[1]; // CRC (占位,稍后计算写入) const crcPos = off; buf[off++] = 0x00; buf[off++] = 0x00; // Frame const frameLE = toUint16LE(frame); buf[off++] = frameLE[0]; buf[off++] = frameLE[1]; // FramNum const framNumLE = toUint16LE(framNum); buf[off++] = framNumLE[0]; buf[off++] = framNumLE[1]; // Frame_Type buf[off++] = type; // Parameters buf.set(params, off); // 计算CRC:整包除CRC字段自身 // 为避免实现差异,计算时将CRC位置的两个字节视为0 buf[crcPos] = 0x00; buf[crcPos + 1] = 0x00; const crc = computeCrc(buf, crcType, [0, buf.length]); const crcLE = toUint16LE(crc); buf[crcPos] = crcLE[0]; buf[crcPos + 1] = crcLE[1]; return buf; } /** * 便捷构造:使用命令常量与简易参数数组 * @param {number} cmd COMMANDS中的命令字 * @param {number[]|Uint8Array|string} payload 参数 * @param {{frame?:number, framNum?:number, crcType?:('CCITT'|'MODBUS'), head?:number[]}} [options] */ export function buildCommand(cmd, payload, options = {}) { return buildPacket({ frame: options.frame != null ? options.frame : 1, framNum: options.framNum != null ? options.framNum : 1, type: cmd & 0xFF, params: payload, crcType: options.crcType === 'CCITT' ? 'CCITT' : 'MODBUS', head: options.head || DEFAULT_HEAD, }); } /** * 示例:构造“读版本号”命令帧 (Frame_Type=0x01, P0=0x00) * @returns {Uint8Array} */ export function buildReadVersion() { return buildCommand(COMMANDS.READ_VERSION, [0x00]); } /** * 构造:设置门磁开廊灯事件与卫浴雷达开卫浴灯事件 (Frame_Type=0x16) * 参数布局(P0..P8): * P0: 控制位 bit0=门磁事件启用, bit1=卫浴事件启用 * 门磁事件(当 bit0=1 启用): * P1: 触发延迟数值 * P2: 时间单位(1=秒,2=分,3=时) * P3: 释放延迟数值 * P4: 时间单位(1=秒,2=分,3=时) * 卫浴事件(当 bit1=1 启用): * P5: 触发延迟数值 * P6: 时间单位(1=秒,2=分,3=时) * P7: 释放延迟数值 * P8: 时间单位(1=秒,2=分,3=时) * 注意:当仅设置某一侧事件时,另一侧对应的参数必须填 0。 * * @param {{ * door?: {triggerDelay?:number, triggerUnit?:number, releaseDelay?:number, releaseUnit?:number}, * bath?: {triggerDelay?:number, triggerUnit?:number, releaseDelay?:number, releaseUnit?:number} * }} opts * @param {{frame?:number, framNum?:number, crcType?:('CCITT'|'MODBUS'), head?:number[]}} [options] */ export function buildSetDoorBathEvent(opts = {}, options = {}) { const door = opts.door || null; const bath = opts.bath || null; const doorEnabled = !!door; const bathEnabled = !!bath; const P0 = (doorEnabled ? 0x01 : 0x00) | (bathEnabled ? 0x02 : 0x00); const p1 = doorEnabled ? (door.triggerDelay || 0) & 0xFF : 0x00; const p2 = doorEnabled ? (door.triggerUnit || 0) & 0xFF : 0x00; const p3 = doorEnabled ? (door.releaseDelay || 0) & 0xFF : 0x00; const p4 = doorEnabled ? (door.releaseUnit || 0) & 0xFF : 0x00; const p5 = bathEnabled ? (bath.triggerDelay || 0) & 0xFF : 0x00; const p6 = bathEnabled ? (bath.triggerUnit || 0) & 0xFF : 0x00; const p7 = bathEnabled ? (bath.releaseDelay || 0) & 0xFF : 0x00; const p8 = bathEnabled ? (bath.releaseUnit || 0) & 0xFF : 0x00; const payload = [P0, p1, p2, p3, p4, p5, p6, p7, p8]; return buildCommand(COMMANDS.SET_DOOR_BATH_EVENT, payload, options); } /** * 验证十六进制字符串包并计算/写入 CRC(默认 MODBUS),返回完整包与CRC值 * @param {string} hexStr 如 'CC C0 0C 00 00 00 01 00 02 00 0C 08' 或连续hex * @param {'MODBUS'|'CCITT'} [crcType='MODBUS'] * @returns {{crc:number, low:number, high:number, packetHex:string, packet:Array}} */ export function verifyHexPacket(hexStr, crcType = 'MODBUS') { if (typeof hexStr !== 'string') throw new TypeError('hexStr must be a string'); const params = ensureUint8Array(hexStr); if (params.length < 6) throw new RangeError('Packet too short'); // 将输入视为完整包,先把CRC位置清0再计算 const buf = new Uint8Array(params.length); buf.set(params); const crcPos = 4; buf[crcPos] = 0x00; buf[crcPos + 1] = 0x00; const crc = computeCrc(buf, crcType, [0, buf.length]); const crcLE = toUint16LE(crc); buf[crcPos] = crcLE[0]; buf[crcPos + 1] = crcLE[1]; const packetHex = Array.from(buf).map(b => b.toString(16).toUpperCase().padStart(2, '0')).join(' '); return { crc: crc & 0xFFFF, low: crcLE[0], high: crcLE[1], packetHex, packet: Array.from(buf) }; }