parsedFunction = {
const match = selectedFunction.match(/FC([0-9A-F]{2})/);
if (match) {
const code = parseInt(match[1], 16);
return functionCodes.find(f => f.code === code) || functionCodes[0];
}
return functionCodes[0];
}
// Parse write value
parsedWriteValue = {
const val = writeValue.trim();
if (val.startsWith("0x") || val.startsWith("0X")) {
return parseInt(val, 16) & 0xFFFF;
}
return parseInt(val, 10) & 0xFFFF;
}
// CRC-16 calculation for Modbus RTU
calculateCRC = (bytes) => {
let crc = 0xFFFF;
for (let i = 0; i < bytes.length; i++) {
crc ^= bytes[i];
for (let j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc = (crc >> 1) ^ 0xA001;
} else {
crc >>= 1;
}
}
}
return crc;
}
// LRC calculation for Modbus ASCII
calculateLRC = (bytes) => {
let lrc = 0;
for (let i = 0; i < bytes.length; i++) {
lrc = (lrc + bytes[i]) & 0xFF;
}
return ((lrc ^ 0xFF) + 1) & 0xFF;
}
// Build request frame
requestFrame = {
const fc = parsedFunction;
const bytes = [];
// Slave address
bytes.push(slaveAddress);
// Function code
bytes.push(fc.code);
// Data based on function code
if (fc.code === 0x01 || fc.code === 0x02 || fc.code === 0x03 || fc.code === 0x04) {
// Read functions: start address (2 bytes) + quantity (2 bytes)
bytes.push((startAddress >> 8) & 0xFF);
bytes.push(startAddress & 0xFF);
bytes.push((quantity >> 8) & 0xFF);
bytes.push(quantity & 0xFF);
} else if (fc.code === 0x05) {
// Write single coil: address (2 bytes) + value (0xFF00 or 0x0000)
bytes.push((startAddress >> 8) & 0xFF);
bytes.push(startAddress & 0xFF);
const coilValue = parsedWriteValue ? 0xFF00 : 0x0000;
bytes.push((coilValue >> 8) & 0xFF);
bytes.push(coilValue & 0xFF);
} else if (fc.code === 0x06) {
// Write single register: address (2 bytes) + value (2 bytes)
bytes.push((startAddress >> 8) & 0xFF);
bytes.push(startAddress & 0xFF);
bytes.push((parsedWriteValue >> 8) & 0xFF);
bytes.push(parsedWriteValue & 0xFF);
} else if (fc.code === 0x0F) {
// Write multiple coils
bytes.push((startAddress >> 8) & 0xFF);
bytes.push(startAddress & 0xFF);
bytes.push((quantity >> 8) & 0xFF);
bytes.push(quantity & 0xFF);
const byteCount = Math.ceil(quantity / 8);
bytes.push(byteCount);
for (let i = 0; i < byteCount; i++) {
bytes.push(0xFF); // All coils ON for demo
}
} else if (fc.code === 0x10) {
// Write multiple registers
bytes.push((startAddress >> 8) & 0xFF);
bytes.push(startAddress & 0xFF);
bytes.push((quantity >> 8) & 0xFF);
bytes.push(quantity & 0xFF);
bytes.push(quantity * 2); // Byte count
for (let i = 0; i < quantity; i++) {
bytes.push(((parsedWriteValue + i) >> 8) & 0xFF);
bytes.push((parsedWriteValue + i) & 0xFF);
}
}
return bytes;
}
// Build response frame (simulated)
responseFrame = {
const fc = parsedFunction;
const bytes = [];
// Check for exception simulation
if (simulateError.startsWith("Exception:")) {
bytes.push(slaveAddress);
bytes.push(fc.code | 0x80); // Exception flag
if (simulateError.includes("Illegal Function")) {
bytes.push(0x01);
} else if (simulateError.includes("Illegal Address")) {
bytes.push(0x02);
} else if (simulateError.includes("Illegal Value")) {
bytes.push(0x03);
} else if (simulateError.includes("Device Failure")) {
bytes.push(0x04);
}
return bytes;
}
// Normal response
bytes.push(slaveAddress);
bytes.push(fc.code);
if (fc.code === 0x01 || fc.code === 0x02) {
// Read coils/discrete: byte count + data bytes
const byteCount = Math.ceil(quantity / 8);
bytes.push(byteCount);
for (let i = 0; i < byteCount; i++) {
bytes.push(Math.floor(Math.random() * 256));
}
} else if (fc.code === 0x03 || fc.code === 0x04) {
// Read registers: byte count + register values
bytes.push(quantity * 2);
for (let i = 0; i < quantity; i++) {
const val = Math.floor(Math.random() * 65536);
bytes.push((val >> 8) & 0xFF);
bytes.push(val & 0xFF);
}
} else if (fc.code === 0x05 || fc.code === 0x06) {
// Write single: echo request
bytes.push((startAddress >> 8) & 0xFF);
bytes.push(startAddress & 0xFF);
if (fc.code === 0x05) {
const coilValue = parsedWriteValue ? 0xFF00 : 0x0000;
bytes.push((coilValue >> 8) & 0xFF);
bytes.push(coilValue & 0xFF);
} else {
bytes.push((parsedWriteValue >> 8) & 0xFF);
bytes.push(parsedWriteValue & 0xFF);
}
} else if (fc.code === 0x0F || fc.code === 0x10) {
// Write multiple: address + quantity
bytes.push((startAddress >> 8) & 0xFF);
bytes.push(startAddress & 0xFF);
bytes.push((quantity >> 8) & 0xFF);
bytes.push(quantity & 0xFF);
}
return bytes;
}
// Frame visualization
frameVisualization = {
const width = 900;
const height = 500;
const margin = { top: 60, right: 30, bottom: 30, left: 30 };
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", "100%")
.attr("height", height)
.style("font-family", "system-ui, -apple-system, sans-serif")
.style("background", "linear-gradient(135deg, #f5f7fa 0%, #e4e9f0 100%)")
.style("border-radius", "12px")
.style("box-shadow", "0 4px 20px rgba(0,0,0,0.1)");
// Title
svg.append("text")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.attr("font-size", "18px")
.attr("font-weight", "bold")
.attr("fill", colors.navy)
.text(`Modbus ${modbusMode} Frame Structure`);
// Mode indicator
const modeColors = { RTU: colors.teal, TCP: colors.blue, ASCII: colors.purple };
svg.append("rect")
.attr("x", width / 2 - 40)
.attr("y", 40)
.attr("width", 80)
.attr("height", 20)
.attr("rx", 10)
.attr("fill", modeColors[modbusMode]);
svg.append("text")
.attr("x", width / 2)
.attr("y", 54)
.attr("text-anchor", "middle")
.attr("font-size", "11px")
.attr("font-weight", "bold")
.attr("fill", colors.white)
.text(modbusMode === "RTU" ? "Serial 9600-115200" : modbusMode === "TCP" ? "Port 502" : "Serial ASCII");
// Request frame section
const reqY = 100;
svg.append("text")
.attr("x", margin.left)
.attr("y", reqY)
.attr("font-size", "14px")
.attr("font-weight", "bold")
.attr("fill", colors.navy)
.text("REQUEST (Master to Slave)");
// Build complete request with framing
let reqBytes = [...requestFrame];
let reqLabels = [];
let reqColors = [];
if (modbusMode === "RTU") {
// RTU: Data + CRC-16
const crc = calculateCRC(reqBytes);
reqBytes.push(crc & 0xFF); // CRC Low
reqBytes.push((crc >> 8) & 0xFF); // CRC High
reqLabels = ["Slave", "FC", ...Array(requestFrame.length - 2).fill("Data"), "CRC-L", "CRC-H"];
reqColors = [colors.orange, colors.teal, ...Array(requestFrame.length - 2).fill(colors.blue), colors.red, colors.red];
} else if (modbusMode === "TCP") {
// TCP: MBAP Header (7 bytes) + PDU
const pduLength = reqBytes.length;
const mbap = [
(transactionId >> 8) & 0xFF, transactionId & 0xFF, // Transaction ID
0x00, 0x00, // Protocol ID (0 for Modbus)
((pduLength + 1) >> 8) & 0xFF, (pduLength + 1) & 0xFF, // Length (includes unit ID)
slaveAddress // Unit ID
];
reqBytes = [...mbap, ...requestFrame.slice(1)]; // Exclude slave address (in MBAP)
reqLabels = ["TxID-H", "TxID-L", "Proto-H", "Proto-L", "Len-H", "Len-L", "Unit", "FC", ...Array(requestFrame.length - 2).fill("Data")];
reqColors = [colors.purple, colors.purple, colors.gray, colors.gray, colors.yellow, colors.yellow, colors.orange, colors.teal, ...Array(requestFrame.length - 2).fill(colors.blue)];
} else if (modbusMode === "ASCII") {
// ASCII: ':' + hex chars + LRC + CR LF (shown as bytes for simplicity)
const lrc = calculateLRC(reqBytes);
reqLabels = ["Slave", "FC", ...Array(requestFrame.length - 2).fill("Data"), "LRC"];
reqColors = [colors.orange, colors.teal, ...Array(requestFrame.length - 2).fill(colors.blue), colors.red];
reqBytes.push(lrc);
}
// Draw request bytes
const byteWidth = Math.min(50, (width - margin.left - margin.right) / Math.max(reqBytes.length, 12));
const byteHeight = 35;
reqBytes.forEach((byte, i) => {
const x = margin.left + i * byteWidth;
const y = reqY + 15;
// Byte box
svg.append("rect")
.attr("x", x)
.attr("y", y)
.attr("width", byteWidth - 2)
.attr("height", byteHeight)
.attr("rx", 4)
.attr("fill", reqColors[i] || colors.gray)
.attr("stroke", colors.navy)
.attr("stroke-width", 1);
// Byte value
svg.append("text")
.attr("x", x + (byteWidth - 2) / 2)
.attr("y", y + byteHeight / 2 + 4)
.attr("text-anchor", "middle")
.attr("font-size", "11px")
.attr("font-weight", "bold")
.attr("fill", colors.white)
.attr("font-family", "monospace")
.text(byte.toString(16).toUpperCase().padStart(2, "0"));
// Label
svg.append("text")
.attr("x", x + (byteWidth - 2) / 2)
.attr("y", y + byteHeight + 12)
.attr("text-anchor", "middle")
.attr("font-size", "8px")
.attr("fill", colors.darkGray)
.text(reqLabels[i] || "");
});
// Response frame section
const respY = 220;
const isException = simulateError.startsWith("Exception:");
const isTimeout = simulateError === "Timeout";
const isCRCError = simulateError === "CRC Error";
svg.append("text")
.attr("x", margin.left)
.attr("y", respY)
.attr("font-size", "14px")
.attr("font-weight", "bold")
.attr("fill", isException || isTimeout || isCRCError ? colors.red : colors.navy)
.text(isTimeout ? "RESPONSE (Timeout - No Response)" :
isException ? "RESPONSE (Exception)" :
isCRCError ? "RESPONSE (CRC Error Detected)" :
"RESPONSE (Slave to Master)");
if (!isTimeout) {
let respBytes = [...responseFrame];
let respLabels = [];
let respColors = [];
if (modbusMode === "RTU") {
let crc = calculateCRC(respBytes);
if (isCRCError) {
crc ^= 0xFFFF; // Corrupt CRC
}
respBytes.push(crc & 0xFF);
respBytes.push((crc >> 8) & 0xFF);
if (isException) {
respLabels = ["Slave", "FC+0x80", "ExCode", "CRC-L", "CRC-H"];
respColors = [colors.orange, colors.red, colors.red, colors.red, colors.red];
} else {
respLabels = ["Slave", "FC", ...Array(responseFrame.length - 2).fill("Data"), "CRC-L", "CRC-H"];
respColors = [colors.orange, colors.teal, ...Array(responseFrame.length - 2).fill(colors.green), isCRCError ? colors.red : colors.purple, isCRCError ? colors.red : colors.purple];
}
} else if (modbusMode === "TCP") {
const pduLength = respBytes.length;
const mbap = [
(transactionId >> 8) & 0xFF, transactionId & 0xFF,
0x00, 0x00,
((pduLength + 1) >> 8) & 0xFF, (pduLength + 1) & 0xFF,
slaveAddress
];
respBytes = [...mbap, ...responseFrame.slice(1)];
if (isException) {
respLabels = ["TxID-H", "TxID-L", "Proto-H", "Proto-L", "Len-H", "Len-L", "Unit", "FC+0x80", "ExCode"];
respColors = [colors.purple, colors.purple, colors.gray, colors.gray, colors.yellow, colors.yellow, colors.orange, colors.red, colors.red];
} else {
respLabels = ["TxID-H", "TxID-L", "Proto-H", "Proto-L", "Len-H", "Len-L", "Unit", "FC", ...Array(responseFrame.length - 2).fill("Data")];
respColors = [colors.purple, colors.purple, colors.gray, colors.gray, colors.yellow, colors.yellow, colors.orange, colors.teal, ...Array(responseFrame.length - 2).fill(colors.green)];
}
} else if (modbusMode === "ASCII") {
let lrc = calculateLRC(respBytes);
if (isCRCError) lrc ^= 0xFF;
respBytes.push(lrc);
if (isException) {
respLabels = ["Slave", "FC+0x80", "ExCode", "LRC"];
respColors = [colors.orange, colors.red, colors.red, colors.red];
} else {
respLabels = ["Slave", "FC", ...Array(responseFrame.length - 2).fill("Data"), "LRC"];
respColors = [colors.orange, colors.teal, ...Array(responseFrame.length - 2).fill(colors.green), isCRCError ? colors.red : colors.purple];
}
}
// Draw response bytes (truncate if too many)
const maxDisplay = Math.floor((width - margin.left - margin.right) / byteWidth);
const displayBytes = respBytes.slice(0, maxDisplay);
displayBytes.forEach((byte, i) => {
const x = margin.left + i * byteWidth;
const y = respY + 15;
svg.append("rect")
.attr("x", x)
.attr("y", y)
.attr("width", byteWidth - 2)
.attr("height", byteHeight)
.attr("rx", 4)
.attr("fill", respColors[i] || colors.gray)
.attr("stroke", colors.navy)
.attr("stroke-width", 1);
svg.append("text")
.attr("x", x + (byteWidth - 2) / 2)
.attr("y", y + byteHeight / 2 + 4)
.attr("text-anchor", "middle")
.attr("font-size", "11px")
.attr("font-weight", "bold")
.attr("fill", colors.white)
.attr("font-family", "monospace")
.text(byte.toString(16).toUpperCase().padStart(2, "0"));
svg.append("text")
.attr("x", x + (byteWidth - 2) / 2)
.attr("y", y + byteHeight + 12)
.attr("text-anchor", "middle")
.attr("font-size", "8px")
.attr("fill", colors.darkGray)
.text(respLabels[i] || "");
});
if (respBytes.length > maxDisplay) {
svg.append("text")
.attr("x", margin.left + maxDisplay * byteWidth + 10)
.attr("y", respY + 15 + byteHeight / 2 + 4)
.attr("font-size", "12px")
.attr("fill", colors.gray)
.text(`... +${respBytes.length - maxDisplay} more bytes`);
}
} else {
// Timeout indicator
svg.append("rect")
.attr("x", margin.left)
.attr("y", respY + 15)
.attr("width", 200)
.attr("height", byteHeight)
.attr("rx", 4)
.attr("fill", colors.red)
.attr("opacity", 0.3);
svg.append("text")
.attr("x", margin.left + 100)
.attr("y", respY + 15 + byteHeight / 2 + 4)
.attr("text-anchor", "middle")
.attr("font-size", "14px")
.attr("font-weight", "bold")
.attr("fill", colors.red)
.text("TIMEOUT - No Response Received");
}
// Frame format legend
const legendY = 330;
svg.append("text")
.attr("x", margin.left)
.attr("y", legendY)
.attr("font-size", "13px")
.attr("font-weight", "bold")
.attr("fill", colors.navy)
.text("Frame Format Legend");
const legendItems = [
{ color: colors.orange, label: "Slave/Unit ID", desc: "Device address (1-247)" },
{ color: colors.teal, label: "Function Code", desc: "Operation type (01-10)" },
{ color: colors.blue, label: "Request Data", desc: "Address, quantity, values" },
{ color: colors.green, label: "Response Data", desc: "Returned values" },
{ color: colors.red, label: "CRC/LRC/Error", desc: "Checksum or exception" },
{ color: colors.purple, label: "TCP Header", desc: "MBAP transaction info" }
];
legendItems.forEach((item, i) => {
const col = i % 3;
const row = Math.floor(i / 3);
const x = margin.left + col * 280;
const y = legendY + 20 + row * 25;
svg.append("rect")
.attr("x", x)
.attr("y", y)
.attr("width", 16)
.attr("height", 16)
.attr("rx", 3)
.attr("fill", item.color);
svg.append("text")
.attr("x", x + 22)
.attr("y", y + 12)
.attr("font-size", "10px")
.attr("font-weight", "bold")
.attr("fill", colors.navy)
.text(item.label);
svg.append("text")
.attr("x", x + 22)
.attr("y", y + 24)
.attr("font-size", "9px")
.attr("fill", colors.gray)
.text(item.desc);
});
// CRC/LRC calculation display
const crcY = 420;
svg.append("text")
.attr("x", margin.left)
.attr("y", crcY)
.attr("font-size", "13px")
.attr("font-weight", "bold")
.attr("fill", colors.navy)
.text(modbusMode === "ASCII" ? "LRC Checksum Calculation" : modbusMode === "RTU" ? "CRC-16 Calculation" : "No Checksum (TCP uses TCP/IP)");
if (modbusMode === "RTU") {
const crc = calculateCRC(requestFrame);
svg.append("text")
.attr("x", margin.left)
.attr("y", crcY + 20)
.attr("font-size", "10px")
.attr("font-family", "monospace")
.attr("fill", colors.darkGray)
.text(`CRC-16/Modbus: Polynomial 0xA001, Init 0xFFFF`);
svg.append("text")
.attr("x", margin.left)
.attr("y", crcY + 35)
.attr("font-size", "11px")
.attr("font-family", "monospace")
.attr("fill", colors.teal)
.text(`Result: 0x${crc.toString(16).toUpperCase().padStart(4, "0")} = [${(crc & 0xFF).toString(16).toUpperCase().padStart(2, "0")} ${((crc >> 8) & 0xFF).toString(16).toUpperCase().padStart(2, "0")}] (Low-High)`);
} else if (modbusMode === "ASCII") {
const lrc = calculateLRC(requestFrame);
svg.append("text")
.attr("x", margin.left)
.attr("y", crcY + 20)
.attr("font-size", "10px")
.attr("font-family", "monospace")
.attr("fill", colors.darkGray)
.text(`LRC: Sum of bytes, two's complement`);
svg.append("text")
.attr("x", margin.left)
.attr("y", crcY + 35)
.attr("font-size", "11px")
.attr("font-family", "monospace")
.attr("fill", colors.purple)
.text(`Result: 0x${lrc.toString(16).toUpperCase().padStart(2, "0")}`);
}
// Function code description
svg.append("text")
.attr("x", width - margin.right)
.attr("y", crcY)
.attr("text-anchor", "end")
.attr("font-size", "12px")
.attr("font-weight", "bold")
.attr("fill", colors.navy)
.text(`${parsedFunction.name} (FC${parsedFunction.code.toString(16).toUpperCase().padStart(2, "0")})`);
svg.append("text")
.attr("x", width - margin.right)
.attr("y", crcY + 18)
.attr("text-anchor", "end")
.attr("font-size", "10px")
.attr("fill", colors.gray)
.text(parsedFunction.description);
return svg.node();
}