colors = ({
navy: "#2C3E50",
teal: "#16A085",
orange: "#E67E22",
gray: "#7F8C8D",
lightGray: "#ECF0F1",
white: "#FFFFFF",
red: "#E74C3C",
green: "#27AE60",
purple: "#9B59B6",
darkTeal: "#1ABC9C",
lightNavy: "#34495E",
yellow: "#F39C12",
blue: "#3498DB",
darkGray: "#34495E"
})
viewof modbusMode = Inputs.radio(["RTU", "TCP", "ASCII"], {
value: "RTU",
label: "Active Mode"
})
modeComparisonViz = {
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(135deg, #f8f9fa 0%, #e9ecef 100%)")
.style("border-radius", "12px");
// 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 Mode Comparison");
const modes = [
{
name: "RTU",
color: colors.teal,
transport: "RS-232/RS-485",
framing: "Binary + CRC-16",
speed: "9600-115200 bps",
maxDevices: "32 (RS-485)",
maxDistance: "1200m (RS-485)",
pros: ["Compact frames", "Fast transmission", "CRC error detection"],
cons: ["Character timing critical", "Binary not human-readable"],
useCase: "Most common industrial"
},
{
name: "TCP",
color: colors.blue,
transport: "Ethernet/TCP/IP",
framing: "MBAP Header (7 bytes)",
speed: "10/100/1000 Mbps",
maxDevices: "Unlimited",
maxDistance: "Unlimited (routable)",
pros: ["High speed", "Scalable", "Routable", "Standard IT infrastructure"],
cons: ["Higher overhead", "Network complexity"],
useCase: "Enterprise/SCADA systems"
},
{
name: "ASCII",
color: colors.purple,
transport: "RS-232/RS-485",
framing: "':' + Hex ASCII + LRC",
speed: "1200-19200 bps",
maxDevices: "32 (RS-485)",
maxDistance: "1200m (RS-485)",
pros: ["Human readable", "Easy debugging", "Wider timing tolerance"],
cons: ["Larger frames (2x RTU)", "Slower"],
useCase: "Legacy/debugging"
}
];
const cardWidth = 280;
const cardHeight = 320;
const cardSpacing = 20;
const startX = (width - (cardWidth * 3 + cardSpacing * 2)) / 2;
modes.forEach((mode, i) => {
const x = startX + i * (cardWidth + cardSpacing);
const y = 55;
const isActive = modbusMode === mode.name;
// Card background
svg.append("rect")
.attr("x", x)
.attr("y", y)
.attr("width", cardWidth)
.attr("height", cardHeight)
.attr("rx", 12)
.attr("fill", colors.white)
.attr("stroke", isActive ? mode.color : colors.lightGray)
.attr("stroke-width", isActive ? 4 : 2)
.attr("filter", isActive ? "drop-shadow(0 4px 8px rgba(0,0,0,0.15))" : "none");
// Header
svg.append("rect")
.attr("x", x)
.attr("y", y)
.attr("width", cardWidth)
.attr("height", 45)
.attr("rx", 12)
.attr("fill", mode.color);
// Fix rounded corners at bottom of header
svg.append("rect")
.attr("x", x)
.attr("y", y + 33)
.attr("width", cardWidth)
.attr("height", 12)
.attr("fill", mode.color);
svg.append("text")
.attr("x", x + cardWidth / 2)
.attr("y", y + 28)
.attr("text-anchor", "middle")
.attr("font-size", "16px")
.attr("font-weight", "bold")
.attr("fill", colors.white)
.text(`Modbus ${mode.name}`);
// Content
let contentY = y + 60;
const lineHeight = 16;
const specs = [
{ label: "Transport", value: mode.transport },
{ label: "Framing", value: mode.framing },
{ label: "Speed", value: mode.speed },
{ label: "Max Devices", value: mode.maxDevices },
{ label: "Distance", value: mode.maxDistance }
];
specs.forEach(spec => {
svg.append("text")
.attr("x", x + 10)
.attr("y", contentY)
.attr("font-size", "9px")
.attr("font-weight", "bold")
.attr("fill", colors.darkGray)
.text(spec.label + ":");
svg.append("text")
.attr("x", x + 80)
.attr("y", contentY)
.attr("font-size", "9px")
.attr("fill", colors.navy)
.text(spec.value);
contentY += lineHeight;
});
contentY += 8;
// Pros
svg.append("text")
.attr("x", x + 10)
.attr("y", contentY)
.attr("font-size", "9px")
.attr("font-weight", "bold")
.attr("fill", colors.green)
.text("Advantages:");
contentY += 12;
mode.pros.forEach(pro => {
svg.append("text")
.attr("x", x + 15)
.attr("y", contentY)
.attr("font-size", "8px")
.attr("fill", colors.darkGray)
.text("+ " + pro);
contentY += 11;
});
contentY += 5;
// Cons
svg.append("text")
.attr("x", x + 10)
.attr("y", contentY)
.attr("font-size", "9px")
.attr("font-weight", "bold")
.attr("fill", colors.red)
.text("Limitations:");
contentY += 12;
mode.cons.forEach(con => {
svg.append("text")
.attr("x", x + 15)
.attr("y", contentY)
.attr("font-size", "8px")
.attr("fill", colors.darkGray)
.text("- " + con);
contentY += 11;
});
contentY += 8;
// Use case
svg.append("rect")
.attr("x", x + 10)
.attr("y", contentY - 3)
.attr("width", cardWidth - 20)
.attr("height", 20)
.attr("rx", 4)
.attr("fill", mode.color)
.attr("opacity", 0.15);
svg.append("text")
.attr("x", x + cardWidth / 2)
.attr("y", contentY + 11)
.attr("text-anchor", "middle")
.attr("font-size", "9px")
.attr("font-weight", "bold")
.attr("fill", mode.color)
.text(mode.useCase);
// Active indicator
if (isActive) {
svg.append("circle")
.attr("cx", x + cardWidth - 15)
.attr("cy", y + 22)
.attr("r", 8)
.attr("fill", colors.white);
svg.append("text")
.attr("x", x + cardWidth - 15)
.attr("y", y + 26)
.attr("text-anchor", "middle")
.attr("font-size", "10px")
.attr("fill", mode.color)
.text("*");
}
});
return svg.node();
}