node-red-contrib-symi
Version:
Node-RED nodes for smart home device communication with Home Assistant integration, supporting HassKit, Clowire, Alive, LF and complete Symi protocols
225 lines (194 loc) • 6.29 kB
JavaScript
/**
* 亖米(Symi)协议处理器
* 支持灯光、空调、窗帘、音乐、地暖、新风、场景等设备
* 数据帧格式:15字节基础长度,帧头0x7e,帧尾0x7d,CRC8校验
*
* 协议结构:
* [帧头][本地地址][数据类型][数据长度][设备类型][品牌ID][设备地址][设备通道]
* [房间号][房间类型][房间ID][操作码][操作信息...][CRC8][帧尾]
*/
const MIN_DATA_LENGTH = 15
// 设备类型定义
const DEVICE_TYPES = {
LIGHT: 0x01, // 灯光
HVAC: 0x02, // 空调
CURTAIN: 0x03, // 窗帘
MUSIC: 0x04, // 音乐
HEATING: 0x05, // 地暖
VENTILATION: 0x06, // 新风
SCENE: 0x07 // 场景
};
// 数据类型定义
const DATA_TYPES = {
RESPONSE: 0x01, // 应答
QUERY: 0x02, // 查询
SET: 0x03, // 设置
REPORT: 0x04 // 上报
};
const fns = {
light: require('./symi/Light'),
hvac: require('./symi/HVAC'),
curtain: require('./symi/Curtain'),
heating: require('./symi/Heating'),
ventilation: require('./symi/Ventilation'),
scene: require('./symi/Scene')
}
/**
* CRC8校验算法 - 按照协议文档标准实现
* 校验从第1位帧头到操作信息最后一字节
* @param {Buffer} data 待校验数据
*/
function CRC8_Check(data) {
let crc = 0;
let len = data.length;
if (len > 2) {
let j = len - 2;
let ptr = 0;
while (j--) {
crc ^= data[ptr++];
for (let i = 0; i < 8; i++) {
if (crc & 0x80) {
crc = (crc << 1) ^ 0x07;
} else {
crc <<= 1;
}
}
}
}
return data[len - 2] === (crc & 0xff);
}
/**
* 计算CRC8校验值 - 按照协议文档标准实现
* @param {Buffer} data 数据(不包含CRC和帧尾)
*/
function CRC8_Calculate(data) {
let crc = 0;
for (let k = 0; k < data.length; k++) {
crc ^= data[k];
for (let i = 0; i < 8; i++) {
if (crc & 0x80) {
crc = (crc << 1) ^ 0x07;
} else {
crc <<= 1;
}
}
}
return crc & 0xff;
}
/**
* 解析协议数据帧
* @param {Buffer} data 数据帧
*/
function parseFrame(data) {
if (!data || data.length < MIN_DATA_LENGTH) return null;
return {
frameHeader: data[0], // 帧头
localAddress: data[1], // 本地地址
dataType: data[2], // 数据类型
dataLength: data[3], // 数据长度
deviceType: data[4], // 设备类型
brandId: data[5], // 品牌ID
deviceAddress: data[6], // 设备地址
deviceChannel: data[7], // 设备通道
roomNumber: data[8], // 房间号
roomType: data[9], // 房间类型
roomId: data[10], // 房间ID
operationCode: data[11], // 操作码
operationData: data.slice(12, data.length - 2), // 操作信息
crc: data[data.length - 2], // CRC8校验
frameTail: data[data.length - 1] // 帧尾
};
}
/**
* 构建协议数据帧
* @param {object} frame 帧数据对象
*/
function buildFrame(frame) {
const operationDataLength = frame.operationData ? frame.operationData.length : 1;
const totalLength = 13 + operationDataLength; // 基础12字节 + 操作信息 + CRC + 帧尾
let buffer = Buffer.alloc(totalLength);
buffer[0] = 0x7E; // 帧头
buffer[1] = frame.localAddress || 0x01; // 本地地址
buffer[2] = frame.dataType || DATA_TYPES.REPORT; // 数据类型
buffer[3] = totalLength; // 数据长度
buffer[4] = frame.deviceType || 0x01; // 设备类型
buffer[5] = frame.brandId || 0x00; // 品牌ID
buffer[6] = frame.deviceAddress || 0x01; // 设备地址
buffer[7] = frame.deviceChannel || 0x00; // 设备通道
buffer[8] = frame.roomNumber || 0x00; // 房间号
buffer[9] = frame.roomType || 0x00; // 房间类型
buffer[10] = frame.roomId || 0x00; // 房间ID
buffer[11] = frame.operationCode || 0x00; // 操作码
// 复制操作信息
if (frame.operationData && frame.operationData.length > 0) {
frame.operationData.copy(buffer, 12);
} else {
buffer[12] = 0x00; // 默认操作信息
}
// 计算并设置CRC8校验
const crcData = buffer.slice(0, totalLength - 2);
buffer[totalLength - 2] = CRC8_Calculate(crcData);
// 设置帧尾
buffer[totalLength - 1] = 0x7D;
return buffer;
}
module.exports = {
DEVICE_TYPES,
DATA_TYPES,
/**
* 收到tcp数据预处理
* @param {Buffer} data
*/
handleRece: function (data) {
// 校验数据长度
if (data.length < MIN_DATA_LENGTH) return null;
// 校验帧头帧尾
if (data[0] !== 0x7E || data[data.length - 1] !== 0x7D) return null;
// CRC8校验
if (CRC8_Check(data)) {
return data;
}
return null;
},
/**
* 处理写入数据
* @param {Buffer|Array|Object} buf
*/
handle_write: function (buf) {
if (Buffer.isBuffer(buf) || Array.isArray(buf)) {
// 传统方式:直接处理缓冲区数据
let data = Buffer.from(buf);
return buildFrame({
operationData: data
});
} else if (typeof buf === 'object') {
// 新方式:使用帧对象
return buildFrame(buf);
}
return null;
},
/**
* 解析数据帧
* @param {Buffer} data
*/
parseFrame: parseFrame,
/**
* 构建数据帧
* @param {object} frame
*/
buildFrame: buildFrame,
/**
* 获取对象转缓冲区函数
* @param {string} domain
*/
getObjToBuf: function (domain) {
return fns[domain] ? fns[domain].obj2buf : null;
},
/**
* 获取缓冲区转对象函数
* @param {string} domain
*/
getBufToObj: function (domain) {
return fns[domain] ? fns[domain].buf2obj : null;
}
};