// ========== 功能码映射表 ==========
const fcMap = {
1: "读线圈状态 (Read Coils)",
2: "读离散输入 (Read Discrete Inputs)",
3: "读保持寄存器 (Read Holding Registers)",
4: "读输入寄存器 (Read Input Registers)",
5: "写单个线圈 (Write Single Coil)",
6: "写单个寄存器 (Write Single Register)",
15: "写多个线圈 (Write Multiple Coils)",
16: "写多个寄存器 (Write Multiple Registers)"
};
// ========== 工具函数 ==========
const readAddrQty = (buf) => ({
address: buf.readUInt16BE(0),
quantity: buf.readUInt16BE(2)
});
function buildParsedFrame(buf) {
if (!Buffer.isBuffer(buf) || buf.length < 8) {
return { type: "Invalid Frame", raw: buf?.toString("hex") };
}
const transactionId = buf.readUInt16BE(0);
const protocolId = buf.readUInt16BE(2);
const length = buf.readUInt16BE(4); // number of bytes following this field (unitId + pdu)
const unitId = buf.readUInt8(6);
const functionCode = buf.readUInt8(7);
const data = buf.slice(8);
const desc = fcMap[functionCode & 0x7F] || "未知功能码";
const isException = (functionCode & 0x80) !== 0;
let direction = "Request"; // 默认为请求
let parsed = {};
try {
// ======== 判断方向更准确的方式:基于 FC 和数据格式 =========
if (isException) {
direction = "Response";
} else {
// 对于读命令,响应的数据第一个字节是 byte count
if ([1, 2, 3, 4].includes(functionCode & 0x7F)) {
if (data.length >= 1 && data[0] === data.length - 1) {
direction = "Response";
}
}
// 写命令的响应通常是 echo 地址+值,长度固定
else if ([5, 6].includes(functionCode)) {
if (data.length === 4) direction = "Response";
}
// 批量写响应只返回地址和数量(4字节)
else if ([15, 16].includes(functionCode)) {
if (data.length === 4) direction = "Response";
}
}
// ======== 异常响应解析 ========
if (isException) {
const exceptionCode = data.length > 0 ? data.readUInt8(0) : null;
parsed = {
direction: "Response",
type: "异常响应 (Exception)",
exceptionCode,
exceptionMsg: getExceptionMessage(exceptionCode)
};
}
// ======== 请求帧解析 ========
else if (direction === "Request") {
switch (functionCode) {
case 1:
case 2:
case 3:
case 4:
if (data.length >= 4) {
const { address, quantity } = readAddrQty(data);
// 标准规定 quantity 不能超过 0x7D0 (2000) 等限制可加校验
parsed = { type: desc, address, quantity };
} else {
parsed = { type: "Malformed Read Request", raw: data.toString("hex") };
}
break;
case 5:
if (data.length >= 4) {
const addr = data.readUInt16BE(0);
const val = data.readUInt16BE(2);
parsed = {
type: desc,
address: addr,
value: val === 0xFF00 // Modbus 规范要求写线圈时 ON=0xFF00, OFF=0x0000
};
} else {
parsed = { type: "Malformed Write Single Coil", raw: data.toString("hex") };
}
break;
case 6:
if (data.length >= 4) {
parsed = {
type: desc,
address: data.readUInt16BE(0),
value: data.readUInt16BE(2)
};
} else {
parsed = { type: "Malformed Write Single Register", raw: data.toString("hex") };
}
break;
case 15:
if (data.length >= 5) {
const addr = data.readUInt16BE(0);
const qty = data.readUInt16BE(2);
const byteCount = data.readUInt8(4);
// 数量合法性检查
if (qty === 0 || qty > 1968) {
parsed = { type: "Invalid Quantity", quantity: qty, raw: data.toString("hex") };
break;
}
const expectedBytes = Math.ceil(qty / 8);
if (byteCount !== expectedBytes) {
parsed = {
type: "Byte count mismatch",
expected: expectedBytes,
actual: byteCount,
raw: data.toString("hex")
};
break;
}
if (data.length < 5 + byteCount) {
parsed = { type: "Truncated Data", raw: data.toString("hex") };
break;
}
const values = [];
for (let i = 0; i < byteCount; i++) {
const b = data[5 + i];
for (let j = 0; j < 8; j++) {
if (values.length < qty) {
// 注意:低位在前(LSB),即 bit0 是第一个线圈
values.push(Boolean((b >> j) & 1));
}
}
}
parsed = { type: desc, address: addr, quantity: qty, values };
} else {
parsed = { type: "Malformed Write Multiple Coils", raw: data.toString("hex") };
}
break;
case 16:
if (data.length >= 5) {
const addr = data.readUInt16BE(0);
const qty = data.readUInt16BE(2);
const byteCount = data.readUInt8(4);
if (qty === 0 || qty > 123) {
parsed = { type: "Invalid Quantity", quantity: qty, raw: data.toString("hex") };
break;
}
if (byteCount !== qty * 2) {
parsed = {
type: "Byte count mismatch",
expected: qty * 2,
actual: byteCount,
raw: data.toString("hex")
};
break;
}
if (data.length < 5 + byteCount) {
parsed = { type: "Truncated Data", raw: data.toString("hex") };
break;
}
const values = [];
for (let i = 0; i < qty; i++) {
values.push(data.readUInt16BE(5 + i * 2));
}
parsed = { type: desc, address: addr, quantity: qty, values };
} else {
parsed = { type: "Malformed Write Multiple Registers", raw: data.toString("hex") };
}
break;
default:
parsed = { type: "Unsupported Function Code", functionCode, raw: data.toString("hex") };
}
}
// ======== 响应帧解析 ========
else if (direction === "Response") {
switch (functionCode) {
case 1:
case 2:
if (data.length >= 1) {
const byteCount = data.readUInt8(0);
if (data.length !== 1 + byteCount) {
parsed = { type: "Data length mismatch", raw: data.toString("hex") };
break;
}
const values = [];
for (let i = 0; i < byteCount; i++) {
const b = data[1 + i];
for (let j = 0; j < 8; j++) {
if (values.length < 2000) { // 安全上限
values.push(Boolean((b >> j) & 1)); // LSB first
}
}
}
parsed = { type: `${desc} 响应`, byteCount, values };
} else {
parsed = { type: "Malformed Response", raw: data.toString("hex") };
}
break;
case 3:
case 4:
if (data.length >= 1) {
const byteCount = data.readUInt8(0);
if (byteCount % 2 !== 0) {
parsed = { type: "Odd byte count in register response", byteCount };
break;
}
const regCount = byteCount / 2;
const values = [];
for (let i = 0; i < regCount; i++) {
if (1 + i * 2 + 1 < data.length) {
values.push(data.readUInt16BE(1 + i * 2));
}
}
parsed = { type: `${desc} 响应`, byteCount, values };
} else {
parsed = { type: "Malformed Response", raw: data.toString("hex") };
}
break;
case 5:
case 6:
case 15:
case 16:
if (data.length === 4) {
parsed = {
type: `${desc} 响应`,
address: data.readUInt16BE(0),
quantity: data.readUInt16BE(2)
};
} else {
parsed = { type: "Malformed Write Response", raw: data.toString("hex") };
}
break;
default:
parsed = { type: "Unknown Response", raw: data.toString("hex") };
}
}
} catch (err) {
parsed = { type: "Parse Error", error: err.message, raw: buf.toString("hex") };
}
// ======== MBAP 长度一致性检查 ========
const pduLength = buf.length - 6; // unitId + PDU
if (Math.abs(pduLength - length) > 1) {
parsed.mbapWarning = `MBAP长度不符 (expected=${length}, actual=${pduLength})`;
}
// ======== 地址标签映射支持 ========
const map = global.get("modbusMap") || {};
if (parsed.address !== undefined && map[parsed.address]) {
parsed.tagName = map[parsed.address];
}
return {
transactionId,
protocolId,
length,
unitId,
functionCode,
description: desc,
direction,
...parsed
};
}
// 辅助函数:异常码解释
function getExceptionMessage(code) {
const messages = {
1: "Illegal Function",
2: "Illegal Data Address",
3: "Illegal Data Value",
4: "Slave Device Failure",
5: "Acknowledge",
6: "Slave Device Busy",
7: "Negative Acknowledge",
8: "Memory Parity Error",
10: "Gateway Path Unavailable",
11: "Gateway Target Device Failed to Respond"
};
return messages[code] || "Unknown Exception";
}
// ========== 主逻辑 ==========
const buf = msg.payload;
if (!Buffer.isBuffer(buf)) {
node.warn("Payload is not a Buffer");
return null;
}
const frames = [];
let offset = 0;
while (offset < buf.length) {
if (offset + 8 > buf.length) {
node.warn("Incomplete frame header — waiting for more data");
break;
}
const len = buf.readUInt16BE(offset + 4);
const totalFrameLength = 6 + len; // MBAP (6字节) + 后续内容
const end = offset + totalFrameLength;
if (end > buf.length) {
node.warn(`Incomplete Modbus frame detected (need ${end - buf.length} more bytes)`);
break;
}
const frame = buf.slice(offset, end);
frames.push(buildParsedFrame(frame));
offset = end;
}
if (frames.length === 0) {
node.status({ fill: "red", shape: "dot", text: "No complete frame" });
return null;
}
node.status({
fill: "green",
shape: "dot",
text: `${frames.length} frame(s), FC${frames[frames.length - 1]?.functionCode}`
});
msg.payload = frames.length === 1 ? frames[0] : frames;
return msg;
优化代码并修正错误
最新发布