viewof slaveAddress = Inputs.range([1, 247], {
value: 1,
step: 1,
label: "Slave Address"
})
viewof modbusMode = Inputs.radio(["RTU", "TCP", "ASCII"], {
value: "RTU",
label: "Modbus Mode"
})
viewof simulateError = Inputs.select(["None", "CRC Error", "Timeout", "Exception: Illegal Function", "Exception: Illegal Address"], {
value: "None",
label: "Simulate Error"
})
// Function code for display
functionCodes = [
{ code: 0x01, name: "Read Coils" },
{ code: 0x02, name: "Read Discrete Inputs" },
{ code: 0x03, name: "Read Holding Registers" },
{ code: 0x04, name: "Read Input Registers" },
{ code: 0x05, name: "Write Single Coil" },
{ code: 0x06, name: "Write Single Register" }
]
viewof selectedFunction = Inputs.select(functionCodes.map(f => `FC${f.code.toString(16).toUpperCase().padStart(2, '0')} - ${f.name}`), {
value: "FC03 - Read Holding Registers",
label: "Function Code"
})
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];
}
communicationViz = {
const width = 900;
const height = 400;
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(180deg, #f8f9fa 0%, #e9ecef 100%)")
.style("border-radius", "12px");
const masterX = 120;
const slaveX = width - 120;
const lineY = 180;
// 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 Master-Slave Communication");
// Master device
const masterGroup = svg.append("g").attr("class", "master");
masterGroup.append("rect")
.attr("x", masterX - 70)
.attr("y", 60)
.attr("width", 140)
.attr("height", 80)
.attr("rx", 10)
.attr("fill", colors.navy)
.attr("stroke", colors.teal)
.attr("stroke-width", 3);
masterGroup.append("text")
.attr("x", masterX)
.attr("y", 90)
.attr("text-anchor", "middle")
.attr("font-size", "14px")
.attr("font-weight", "bold")
.attr("fill", colors.white)
.text("MASTER");
masterGroup.append("text")
.attr("x", masterX)
.attr("y", 110)
.attr("text-anchor", "middle")
.attr("font-size", "10px")
.attr("fill", colors.lightGray)
.text("PLC / SCADA / HMI");
masterGroup.append("text")
.attr("x", masterX)
.attr("y", 128)
.attr("text-anchor", "middle")
.attr("font-size", "9px")
.attr("fill", colors.teal)
.text("(Initiates requests)");
// Slave device
const slaveGroup = svg.append("g").attr("class", "slave");
slaveGroup.append("rect")
.attr("x", slaveX - 70)
.attr("y", 60)
.attr("width", 140)
.attr("height", 80)
.attr("rx", 10)
.attr("fill", colors.orange)
.attr("stroke", colors.navy)
.attr("stroke-width", 3);
slaveGroup.append("text")
.attr("x", slaveX)
.attr("y", 90)
.attr("text-anchor", "middle")
.attr("font-size", "14px")
.attr("font-weight", "bold")
.attr("fill", colors.white)
.text(`SLAVE #${slaveAddress}`);
slaveGroup.append("text")
.attr("x", slaveX)
.attr("y", 110)
.attr("text-anchor", "middle")
.attr("font-size", "10px")
.attr("fill", colors.white)
.text("Sensor / Actuator / VFD");
slaveGroup.append("text")
.attr("x", slaveX)
.attr("y", 128)
.attr("text-anchor", "middle")
.attr("font-size", "9px")
.attr("fill", colors.lightGray)
.text("(Responds to requests)");
// Communication bus
svg.append("line")
.attr("x1", masterX)
.attr("y1", lineY)
.attr("x2", slaveX)
.attr("y2", lineY)
.attr("stroke", colors.gray)
.attr("stroke-width", 4)
.attr("stroke-dasharray", modbusMode === "RTU" ? "none" : "10,5");
// Bus type label
svg.append("text")
.attr("x", width / 2)
.attr("y", lineY - 10)
.attr("text-anchor", "middle")
.attr("font-size", "11px")
.attr("fill", colors.gray)
.text(modbusMode === "RTU" ? "RS-485 Serial Bus" : modbusMode === "TCP" ? "TCP/IP Network (Port 502)" : "RS-232/RS-485 ASCII");
// Timing diagram
const timingY = 230;
svg.append("line")
.attr("x1", masterX - 50)
.attr("y1", timingY)
.attr("x2", slaveX + 50)
.attr("y2", timingY)
.attr("stroke", colors.lightGray)
.attr("stroke-width", 2);
svg.append("text")
.attr("x", masterX - 50)
.attr("y", timingY - 10)
.attr("font-size", "10px")
.attr("fill", colors.gray)
.text("Time");
// Timing steps
const steps = [
{ x: masterX, label: "Request", time: "t0", color: colors.teal, y: -15 },
{ x: width / 2 - 50, label: "Transmit", time: "t1", color: colors.blue, y: 15 },
{ x: slaveX, label: "Process", time: "t2", color: colors.orange, y: -15 },
{ x: width / 2 + 50, label: "Response", time: "t3", color: colors.green, y: 15 },
{ x: masterX + 100, label: "Receive", time: "t4", color: colors.purple, y: -15 }
];
steps.forEach(step => {
svg.append("circle")
.attr("cx", step.x)
.attr("cy", timingY)
.attr("r", 6)
.attr("fill", step.color);
svg.append("text")
.attr("x", step.x)
.attr("y", timingY + step.y)
.attr("text-anchor", "middle")
.attr("font-size", "9px")
.attr("fill", step.color)
.text(step.label);
});
// Request arrow
svg.append("line")
.attr("x1", masterX + 70)
.attr("y1", lineY + 25)
.attr("x2", slaveX - 70)
.attr("y2", lineY + 25)
.attr("stroke", colors.teal)
.attr("stroke-width", 3)
.attr("marker-end", "url(#arrowhead)");
svg.append("text")
.attr("x", width / 2)
.attr("y", lineY + 20)
.attr("text-anchor", "middle")
.attr("font-size", "10px")
.attr("font-weight", "bold")
.attr("fill", colors.teal)
.text(`${parsedFunction.name}`);
// Response arrow
svg.append("line")
.attr("x1", slaveX - 70)
.attr("y1", lineY + 50)
.attr("x2", masterX + 70)
.attr("y2", lineY + 50)
.attr("stroke", simulateError !== "None" ? colors.red : colors.green)
.attr("stroke-width", 3)
.attr("marker-end", "url(#arrowhead-response)");
svg.append("text")
.attr("x", width / 2)
.attr("y", lineY + 45)
.attr("text-anchor", "middle")
.attr("font-size", "10px")
.attr("font-weight", "bold")
.attr("fill", simulateError !== "None" ? colors.red : colors.green)
.text(simulateError !== "None" ? (simulateError.includes("Timeout") ? "TIMEOUT" : "Exception") : "Response Data");
// Arrow markers
svg.append("defs").html(`
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="${colors.teal}" />
</marker>
<marker id="arrowhead-response" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="${simulateError !== "None" ? colors.red : colors.green}" />
</marker>
`);
// Frame summary boxes
const boxY = 300;
// Request summary
svg.append("rect")
.attr("x", 30)
.attr("y", boxY)
.attr("width", 250)
.attr("height", 85)
.attr("rx", 8)
.attr("fill", colors.teal)
.attr("opacity", 0.1)
.attr("stroke", colors.teal)
.attr("stroke-width", 2);
svg.append("text")
.attr("x", 40)
.attr("y", boxY + 20)
.attr("font-size", "12px")
.attr("font-weight", "bold")
.attr("fill", colors.navy)
.text("Request Frame");
const reqInfo = [
`Slave: ${slaveAddress}`,
`FC: 0x${parsedFunction.code.toString(16).toUpperCase().padStart(2, "0")} (${parsedFunction.name})`,
`Address: ${startAddress} | Qty: ${quantity}`,
`Mode: ${modbusMode}`
];
reqInfo.forEach((info, i) => {
svg.append("text")
.attr("x", 45)
.attr("y", boxY + 38 + i * 14)
.attr("font-size", "10px")
.attr("font-family", "monospace")
.attr("fill", colors.darkGray)
.text(info);
});
// Response summary
svg.append("rect")
.attr("x", width - 280)
.attr("y", boxY)
.attr("width", 250)
.attr("height", 85)
.attr("rx", 8)
.attr("fill", simulateError !== "None" ? colors.red : colors.green)
.attr("opacity", 0.1)
.attr("stroke", simulateError !== "None" ? colors.red : colors.green)
.attr("stroke-width", 2);
svg.append("text")
.attr("x", width - 270)
.attr("y", boxY + 20)
.attr("font-size", "12px")
.attr("font-weight", "bold")
.attr("fill", colors.navy)
.text(simulateError !== "None" ? "Error Response" : "Success Response");
let respInfo;
if (simulateError === "Timeout") {
respInfo = ["No response received", "Check connection", "Verify slave address", "Retry recommended"];
} else if (simulateError.startsWith("Exception:")) {
const exName = simulateError.replace("Exception: ", "");
respInfo = [`Exception: ${exName}`, "FC: 0x" + (parsedFunction.code | 0x80).toString(16).toUpperCase(), "Check parameters", "Verify addressing"];
} else if (simulateError === "CRC Error") {
respInfo = ["CRC mismatch detected", "Data corruption likely", "Check wiring/EMI", "Retry transmission"];
} else {
const dataBytes = parsedFunction.code <= 0x04 ? quantity * (parsedFunction.code <= 0x02 ? 0.125 : 2) : 4;
respInfo = [`Data bytes: ${Math.ceil(dataBytes)}`, `Registers: ${quantity}`, "Status: OK", "CRC: Valid"];
}
respInfo.forEach((info, i) => {
svg.append("text")
.attr("x", width - 265)
.attr("y", boxY + 38 + i * 14)
.attr("font-size", "10px")
.attr("font-family", "monospace")
.attr("fill", colors.darkGray)
.text(info);
});
// Turnaround time
svg.append("text")
.attr("x", width / 2)
.attr("y", height - 15)
.attr("text-anchor", "middle")
.attr("font-size", "10px")
.attr("fill", colors.gray)
.text(modbusMode === "RTU" ? "Turnaround: 3.5 character times minimum" :
modbusMode === "TCP" ? "Response timeout: typically 1000ms" :
"Frame delimiters: ':' start, CR-LF end");
return svg.node();
}