Files
Wx_BLWConfigTools_V02_Prod/utils/w13Packet.js

253 lines
8.7 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.
// 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<number>}}
*/
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) };
}