%% fig-alt: Bar chart showing maximum payload sizes for common IoT protocols, ranging from Sigfox at 12 bytes to NB-IoT at 1500 bytes.
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#2C3E50', 'primaryTextColor': '#FFFFFF', 'primaryBorderColor': '#16A085', 'lineColor': '#7F8C8D', 'secondaryColor': '#ECF0F1', 'tertiaryColor': '#FFFFFF'}}}%%
flowchart LR
subgraph Limits["Protocol Maximum Payload Sizes"]
SIG["Sigfox<br/>12 bytes"]
SF12["LoRaWAN SF12<br/>51 bytes"]
SF10["LoRaWAN SF10<br/>115 bytes"]
SF7["LoRaWAN SF7<br/>242 bytes"]
BLE["BLE<br/>244 bytes"]
NB["NB-IoT<br/>1,500 bytes"]
end
SIG --> SF12 --> SF10 --> SF7 --> BLE --> NB
style SIG fill:#E74C3C,stroke:#2C3E50,stroke-width:2px,color:#fff
style SF12 fill:#E67E22,stroke:#2C3E50,stroke-width:2px,color:#fff
style SF10 fill:#F1C40F,stroke:#2C3E50,stroke-width:2px,color:#000
style SF7 fill:#27AE60,stroke:#2C3E50,stroke-width:2px,color:#fff
style BLE fill:#9B59B6,stroke:#2C3E50,stroke-width:2px,color:#fff
style NB fill:#3498DB,stroke:#2C3E50,stroke-width:2px,color:#fff
55 Payload Builder & Validator
Interactive IoT payload design and format comparison tool
55.1 IoT Payload Builder
Design efficient payloads for your IoT application and understand the tradeoffs between different data formats.
This interactive tool helps you design IoT payloads by adding fields, comparing encoding formats (JSON, Compact JSON, CBOR estimate, Binary struct), validating against protocol limits, and calculating data volume at scale.
- Add Fields using the field builder - select type, name, and example value
- Compare Formats to see size differences between JSON, CBOR, and binary
- Check Protocol Compatibility against LoRaWAN, Sigfox, NB-IoT limits
- Calculate Scale to estimate daily/monthly data volumes
- Try Templates for common IoT patterns (temperature sensor, GPS tracker, etc.)
Show code
{
// ===========================================================================
// PAYLOAD BUILDER & VALIDATOR
// ===========================================================================
// Features:
// - Field builder with type selectors
// - Format comparison (JSON, Compact, CBOR, Binary)
// - Protocol limit validation
// - Scale calculator
// - Common pattern templates
//
// IEEE Color Palette:
// Navy: #2C3E50 (primary)
// Teal: #16A085 (secondary)
// Orange: #E67E22 (highlights)
// Gray: #7F8C8D (neutral)
// LtGray: #ECF0F1 (backgrounds)
// Green: #27AE60 (good)
// Red: #E74C3C (warnings)
// ===========================================================================
// ---------------------------------------------------------------------------
// CONFIGURATION
// ---------------------------------------------------------------------------
const config = {
width: 950,
colors: {
navy: "#2C3E50",
teal: "#16A085",
orange: "#E67E22",
gray: "#7F8C8D",
lightGray: "#ECF0F1",
white: "#FFFFFF",
green: "#27AE60",
red: "#E74C3C",
yellow: "#F1C40F",
purple: "#9B59B6",
blue: "#3498DB"
},
// Data type definitions with byte sizes
dataTypes: [
{ id: "string", name: "String", jsonOverhead: 2, binarySize: (v) => v.length, description: "Text values" },
{ id: "int8", name: "Int8 (-128 to 127)", jsonOverhead: 0, binarySize: () => 1, description: "Small integers" },
{ id: "uint8", name: "UInt8 (0 to 255)", jsonOverhead: 0, binarySize: () => 1, description: "Small positive integers" },
{ id: "int16", name: "Int16 (-32768 to 32767)", jsonOverhead: 0, binarySize: () => 2, description: "Medium integers" },
{ id: "uint16", name: "UInt16 (0 to 65535)", jsonOverhead: 0, binarySize: () => 2, description: "Medium positive integers" },
{ id: "int32", name: "Int32", jsonOverhead: 0, binarySize: () => 4, description: "Large integers" },
{ id: "uint32", name: "UInt32 (timestamps)", jsonOverhead: 0, binarySize: () => 4, description: "Timestamps, large values" },
{ id: "float32", name: "Float32 (single precision)", jsonOverhead: 0, binarySize: () => 4, description: "Decimal numbers" },
{ id: "float16", name: "Float16 (half precision)", jsonOverhead: 0, binarySize: () => 2, description: "Low-precision decimals" },
{ id: "bool", name: "Boolean", jsonOverhead: 0, binarySize: () => 1, description: "True/false values" },
{ id: "array_int8", name: "Array of Int8", jsonOverhead: 2, binarySize: (v) => v.length, description: "Byte arrays" }
],
// Protocol limits
protocolLimits: {
lorawan_sf12: { name: "LoRaWAN SF12", maxBytes: 51, color: "#27AE60" },
lorawan_sf10: { name: "LoRaWAN SF10", maxBytes: 115, color: "#2ECC71" },
lorawan_sf7: { name: "LoRaWAN SF7", maxBytes: 242, color: "#16A085" },
sigfox: { name: "Sigfox", maxBytes: 12, color: "#E74C3C" },
nbiot: { name: "NB-IoT", maxBytes: 1500, color: "#3498DB" },
ble: { name: "BLE (ATT MTU)", maxBytes: 244, color: "#9B59B6" }
},
// Common templates
templates: {
temperature: {
name: "Temperature Sensor",
description: "Basic environmental monitoring",
fields: [
{ name: "device_id", type: "string", value: "sens001", description: "Device identifier" },
{ name: "temp", type: "float16", value: "23.5", description: "Temperature in Celsius" },
{ name: "humidity", type: "uint8", value: "65", description: "Humidity percentage" },
{ name: "ts", type: "uint32", value: "1702732800", description: "Unix timestamp" }
]
},
gps_tracker: {
name: "GPS Tracker",
description: "Asset tracking with location",
fields: [
{ name: "id", type: "string", value: "TRK01", description: "Tracker ID" },
{ name: "lat", type: "float32", value: "37.7749", description: "Latitude" },
{ name: "lng", type: "float32", value: "-122.4194", description: "Longitude" },
{ name: "speed", type: "uint8", value: "45", description: "Speed in km/h" },
{ name: "hdg", type: "uint16", value: "180", description: "Heading in degrees" },
{ name: "ts", type: "uint32", value: "1702732800", description: "Timestamp" }
]
},
smart_meter: {
name: "Smart Meter",
description: "Energy monitoring payload",
fields: [
{ name: "meter_id", type: "string", value: "MTR001", description: "Meter identifier" },
{ name: "kwh", type: "float32", value: "1523.45", description: "Total kWh consumed" },
{ name: "power", type: "uint16", value: "2340", description: "Current power in watts" },
{ name: "voltage", type: "float16", value: "230.5", description: "Voltage" },
{ name: "ts", type: "uint32", value: "1702732800", description: "Timestamp" }
]
},
soil_sensor: {
name: "Agricultural Sensor",
description: "Soil monitoring for precision agriculture",
fields: [
{ name: "id", type: "string", value: "SOIL01", description: "Sensor ID" },
{ name: "moisture", type: "uint8", value: "45", description: "Soil moisture %" },
{ name: "temp", type: "int8", value: "18", description: "Soil temperature" },
{ name: "ph", type: "uint8", value: "72", description: "pH x 10 (7.2)" },
{ name: "battery", type: "uint8", value: "85", description: "Battery %" }
]
},
parking: {
name: "Parking Sensor (Sigfox)",
description: "Ultra-compact for 12-byte limit",
fields: [
{ name: "id", type: "uint16", value: "1234", description: "Spot ID" },
{ name: "occupied", type: "bool", value: "true", description: "Is occupied" },
{ name: "duration", type: "uint16", value: "3600", description: "Duration in seconds" },
{ name: "battery", type: "uint8", value: "95", description: "Battery %" }
]
}
}
};
// ---------------------------------------------------------------------------
// STATE MANAGEMENT
// ---------------------------------------------------------------------------
let state = {
fields: [
{ id: 1, name: "device_id", type: "string", value: "sensor001", enabled: true },
{ id: 2, name: "temp", type: "float16", value: "23.5", enabled: true },
{ id: 3, name: "humidity", type: "uint8", value: "65", enabled: true },
{ id: 4, name: "ts", type: "uint32", value: "1702732800", enabled: true }
],
nextId: 5,
// Scale calculator
deviceCount: 100,
messagesPerDay: 96 // Every 15 minutes
};
// ---------------------------------------------------------------------------
// PAYLOAD CALCULATIONS
// ---------------------------------------------------------------------------
function buildPayloadObject() {
const obj = {};
state.fields.filter(f => f.enabled).forEach(field => {
const typeInfo = config.dataTypes.find(t => t.id === field.type);
let value;
if (field.type === "string") {
value = field.value;
} else if (field.type === "bool") {
value = field.value.toLowerCase() === "true";
} else if (field.type.includes("float")) {
value = parseFloat(field.value) || 0;
} else if (field.type === "array_int8") {
value = field.value.split(",").map(v => parseInt(v.trim()) || 0);
} else {
value = parseInt(field.value) || 0;
}
obj[field.name] = value;
});
return obj;
}
function calculateSizes() {
const payload = buildPayloadObject();
const enabledFields = state.fields.filter(f => f.enabled);
// JSON (pretty)
const jsonPretty = JSON.stringify(payload, null, 2);
// JSON (compact)
const jsonCompact = JSON.stringify(payload);
// Estimate CBOR size (roughly 50-60% of JSON)
// CBOR keeps field names but uses efficient binary encoding for values
let cborEstimate = 1; // Map header
enabledFields.forEach(field => {
cborEstimate += 1; // String header for key
cborEstimate += field.name.length; // Key name
const typeInfo = config.dataTypes.find(t => t.id === field.type);
if (field.type === "string") {
cborEstimate += 1 + field.value.length; // String header + value
} else if (field.type.includes("float16")) {
cborEstimate += 3; // Float16 header + 2 bytes
} else if (field.type.includes("float32")) {
cborEstimate += 5; // Float32 header + 4 bytes
} else if (field.type === "bool") {
cborEstimate += 1; // Single byte
} else if (field.type.includes("8")) {
cborEstimate += 1; // Small int can be single byte
} else if (field.type.includes("16")) {
cborEstimate += 3; // Header + 2 bytes
} else if (field.type.includes("32")) {
cborEstimate += 5; // Header + 4 bytes
} else if (field.type === "array_int8") {
const arr = field.value.split(",");
cborEstimate += 1 + arr.length; // Array header + bytes
}
});
// Binary struct size (no field names, pure data)
let binarySize = 0;
enabledFields.forEach(field => {
const typeInfo = config.dataTypes.find(t => t.id === field.type);
if (field.type === "string") {
binarySize += field.value.length;
} else if (field.type === "array_int8") {
binarySize += field.value.split(",").length;
} else {
binarySize += typeInfo.binarySize(field.value);
}
});
return {
jsonPretty: { content: jsonPretty, size: new Blob([jsonPretty]).size },
jsonCompact: { content: jsonCompact, size: new Blob([jsonCompact]).size },
cbor: { content: "(binary)", size: cborEstimate },
binary: { content: "(struct)", size: binarySize }
};
}
function checkProtocolCompatibility(sizes) {
const results = {};
Object.entries(config.protocolLimits).forEach(([key, protocol]) => {
results[key] = {
name: protocol.name,
maxBytes: protocol.maxBytes,
color: protocol.color,
jsonFits: sizes.jsonCompact.size <= protocol.maxBytes,
cborFits: sizes.cbor.size <= protocol.maxBytes,
binaryFits: sizes.binary.size <= protocol.maxBytes,
jsonPercent: (sizes.jsonCompact.size / protocol.maxBytes) * 100,
binaryPercent: (sizes.binary.size / protocol.maxBytes) * 100
};
});
return results;
}
function calculateScale(sizes) {
const dailyMessages = state.deviceCount * state.messagesPerDay;
const monthlyMessages = dailyMessages * 30;
return {
json: {
daily: dailyMessages * sizes.jsonCompact.size,
monthly: monthlyMessages * sizes.jsonCompact.size
},
cbor: {
daily: dailyMessages * sizes.cbor.size,
monthly: monthlyMessages * sizes.cbor.size
},
binary: {
daily: dailyMessages * sizes.binary.size,
monthly: monthlyMessages * sizes.binary.size
}
};
}
function formatBytes(bytes) {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}
// ---------------------------------------------------------------------------
// CREATE CONTAINER
// ---------------------------------------------------------------------------
const container = d3.create("div")
.style("font-family", "system-ui, -apple-system, sans-serif")
.style("max-width", `${config.width}px`)
.style("margin", "0 auto");
// ---------------------------------------------------------------------------
// TEMPLATE SELECTOR
// ---------------------------------------------------------------------------
const templatePanel = container.append("div")
.style("background", config.colors.lightGray)
.style("border-radius", "12px")
.style("padding", "20px")
.style("margin-bottom", "15px")
.style("border", `2px solid ${config.colors.gray}`);
templatePanel.append("h3")
.style("margin", "0 0 15px 0")
.style("color", config.colors.navy)
.style("font-size", "16px")
.text("Common Payload Patterns");
const templateGrid = templatePanel.append("div")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fit, minmax(170px, 1fr))")
.style("gap", "10px");
Object.entries(config.templates).forEach(([key, template]) => {
const btn = templateGrid.append("button")
.style("padding", "12px 15px")
.style("border", `2px solid ${config.colors.teal}`)
.style("border-radius", "8px")
.style("background", config.colors.white)
.style("cursor", "pointer")
.style("text-align", "left")
.style("transition", "all 0.2s")
.on("mouseover", function() {
d3.select(this).style("background", config.colors.teal).style("color", config.colors.white);
})
.on("mouseout", function() {
d3.select(this).style("background", config.colors.white).style("color", config.colors.navy);
})
.on("click", () => {
loadTemplate(key);
});
btn.append("div")
.style("font-weight", "bold")
.style("font-size", "13px")
.style("color", config.colors.navy)
.text(template.name);
btn.append("div")
.style("font-size", "11px")
.style("color", config.colors.gray)
.style("margin-top", "4px")
.text(template.description);
});
// ---------------------------------------------------------------------------
// FIELD BUILDER
// ---------------------------------------------------------------------------
const fieldPanel = container.append("div")
.style("background", config.colors.white)
.style("border-radius", "12px")
.style("padding", "20px")
.style("margin-bottom", "15px")
.style("border", `2px solid ${config.colors.navy}`);
fieldPanel.append("h3")
.style("margin", "0 0 15px 0")
.style("color", config.colors.navy)
.style("font-size", "16px")
.text("Field Builder");
// Add field button
const addFieldRow = fieldPanel.append("div")
.style("display", "flex")
.style("gap", "10px")
.style("margin-bottom", "15px")
.style("flex-wrap", "wrap")
.style("align-items", "center");
const fieldNameInput = addFieldRow.append("input")
.attr("type", "text")
.attr("placeholder", "Field name")
.style("padding", "8px 12px")
.style("border", `1px solid ${config.colors.gray}`)
.style("border-radius", "6px")
.style("font-size", "13px")
.style("width", "140px");
const fieldTypeSelect = addFieldRow.append("select")
.style("padding", "8px 12px")
.style("border", `1px solid ${config.colors.gray}`)
.style("border-radius", "6px")
.style("font-size", "13px")
.style("background", config.colors.white)
.style("cursor", "pointer");
config.dataTypes.forEach(dt => {
fieldTypeSelect.append("option")
.attr("value", dt.id)
.text(dt.name);
});
const fieldValueInput = addFieldRow.append("input")
.attr("type", "text")
.attr("placeholder", "Example value")
.style("padding", "8px 12px")
.style("border", `1px solid ${config.colors.gray}`)
.style("border-radius", "6px")
.style("font-size", "13px")
.style("width", "140px");
const addBtn = addFieldRow.append("button")
.style("padding", "8px 20px")
.style("background", config.colors.teal)
.style("color", config.colors.white)
.style("border", "none")
.style("border-radius", "6px")
.style("font-size", "13px")
.style("font-weight", "bold")
.style("cursor", "pointer")
.text("Add Field")
.on("click", () => {
const name = fieldNameInput.property("value").trim();
const type = fieldTypeSelect.property("value");
const value = fieldValueInput.property("value").trim() || getDefaultValue(type);
if (name) {
state.fields.push({
id: state.nextId++,
name: name,
type: type,
value: value,
enabled: true
});
fieldNameInput.property("value", "");
fieldValueInput.property("value", "");
updateFieldList();
updateResults();
}
});
// Field list container
const fieldListContainer = fieldPanel.append("div")
.attr("class", "field-list")
.style("max-height", "300px")
.style("overflow-y", "auto");
// ---------------------------------------------------------------------------
// FORMAT COMPARISON
// ---------------------------------------------------------------------------
const formatPanel = container.append("div")
.style("background", config.colors.navy)
.style("border-radius", "12px")
.style("padding", "20px")
.style("margin-bottom", "15px")
.style("color", config.colors.white);
formatPanel.append("h3")
.style("margin", "0 0 15px 0")
.style("font-size", "16px")
.text("Format Comparison");
const formatGrid = formatPanel.append("div")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fit, minmax(200px, 1fr))")
.style("gap", "15px");
// Format cards
const formats = [
{ id: "jsonCompact", name: "JSON (Compact)", color: config.colors.orange, description: "Human-readable, universal" },
{ id: "cbor", name: "CBOR (Estimated)", color: config.colors.teal, description: "Binary JSON, ~50% smaller" },
{ id: "binary", name: "Binary Struct", color: config.colors.green, description: "Pure data, smallest size" }
];
formats.forEach(fmt => {
const card = formatGrid.append("div")
.style("background", "rgba(255,255,255,0.1)")
.style("border-radius", "8px")
.style("padding", "15px")
.style("border-left", `4px solid ${fmt.color}`);
card.append("div")
.style("font-weight", "bold")
.style("font-size", "14px")
.style("margin-bottom", "5px")
.text(fmt.name);
card.append("div")
.attr("class", `format-size-${fmt.id}`)
.style("font-size", "28px")
.style("font-weight", "bold")
.style("color", fmt.color)
.text("--");
card.append("div")
.style("font-size", "11px")
.style("opacity", "0.7")
.text("bytes");
card.append("div")
.attr("class", `format-savings-${fmt.id}`)
.style("font-size", "11px")
.style("margin-top", "8px")
.style("padding", "4px 8px")
.style("background", "rgba(255,255,255,0.1)")
.style("border-radius", "4px")
.style("display", "inline-block");
card.append("div")
.style("font-size", "10px")
.style("opacity", "0.6")
.style("margin-top", "8px")
.text(fmt.description);
});
// Size comparison bar
const barContainer = formatPanel.append("div")
.style("margin-top", "20px");
barContainer.append("div")
.style("font-size", "12px")
.style("margin-bottom", "8px")
.style("opacity", "0.8")
.text("Relative Size Comparison");
const barSvg = barContainer.append("svg")
.attr("viewBox", "0 0 600 80")
.attr("width", "100%")
.style("max-width", "600px");
// ---------------------------------------------------------------------------
// PROTOCOL COMPATIBILITY
// ---------------------------------------------------------------------------
const protocolPanel = container.append("div")
.style("background", config.colors.lightGray)
.style("border-radius", "12px")
.style("padding", "20px")
.style("margin-bottom", "15px")
.style("border", `2px solid ${config.colors.gray}`);
protocolPanel.append("h3")
.style("margin", "0 0 15px 0")
.style("color", config.colors.navy)
.style("font-size", "16px")
.text("Protocol Compatibility");
const protocolGrid = protocolPanel.append("div")
.attr("class", "protocol-grid")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fit, minmax(140px, 1fr))")
.style("gap", "10px");
// ---------------------------------------------------------------------------
// SCALE CALCULATOR
// ---------------------------------------------------------------------------
const scalePanel = container.append("div")
.style("background", config.colors.white)
.style("border-radius", "12px")
.style("padding", "20px")
.style("margin-bottom", "15px")
.style("border", `2px solid ${config.colors.teal}`);
scalePanel.append("h3")
.style("margin", "0 0 15px 0")
.style("color", config.colors.navy)
.style("font-size", "16px")
.text("Scale Calculator");
const scaleInputRow = scalePanel.append("div")
.style("display", "flex")
.style("gap", "20px")
.style("margin-bottom", "20px")
.style("flex-wrap", "wrap");
// Device count
const deviceGroup = scaleInputRow.append("div");
deviceGroup.append("label")
.style("display", "block")
.style("font-size", "12px")
.style("color", config.colors.gray)
.style("margin-bottom", "5px")
.text("Number of Devices");
const deviceSlider = deviceGroup.append("input")
.attr("type", "range")
.attr("min", 1)
.attr("max", 10000)
.attr("value", state.deviceCount)
.style("width", "150px")
.style("accent-color", config.colors.teal);
const deviceValue = deviceGroup.append("span")
.style("margin-left", "10px")
.style("font-weight", "bold")
.style("color", config.colors.navy)
.text(state.deviceCount);
deviceSlider.on("input", function() {
state.deviceCount = +this.value;
deviceValue.text(state.deviceCount);
updateResults();
});
// Messages per day
const msgGroup = scaleInputRow.append("div");
msgGroup.append("label")
.style("display", "block")
.style("font-size", "12px")
.style("color", config.colors.gray)
.style("margin-bottom", "5px")
.text("Messages per Device per Day");
const msgSlider = msgGroup.append("input")
.attr("type", "range")
.attr("min", 1)
.attr("max", 1440)
.attr("value", state.messagesPerDay)
.style("width", "150px")
.style("accent-color", config.colors.teal);
const msgValue = msgGroup.append("span")
.style("margin-left", "10px")
.style("font-weight", "bold")
.style("color", config.colors.navy)
.text(state.messagesPerDay);
msgSlider.on("input", function() {
state.messagesPerDay = +this.value;
msgValue.text(state.messagesPerDay);
updateResults();
});
// Scale results
const scaleResults = scalePanel.append("div")
.attr("class", "scale-results")
.style("display", "grid")
.style("grid-template-columns", "repeat(3, 1fr)")
.style("gap", "15px");
// ---------------------------------------------------------------------------
// PAYLOAD PREVIEW
// ---------------------------------------------------------------------------
const previewPanel = container.append("div")
.style("background", config.colors.lightGray)
.style("border-radius", "12px")
.style("padding", "20px")
.style("border", `2px solid ${config.colors.gray}`);
previewPanel.append("h3")
.style("margin", "0 0 15px 0")
.style("color", config.colors.navy)
.style("font-size", "16px")
.text("JSON Preview");
const previewCode = previewPanel.append("pre")
.attr("class", "json-preview")
.style("background", config.colors.navy)
.style("color", config.colors.lightGray)
.style("padding", "15px")
.style("border-radius", "8px")
.style("font-family", "monospace")
.style("font-size", "12px")
.style("overflow-x", "auto")
.style("white-space", "pre-wrap")
.style("word-break", "break-all");
// ---------------------------------------------------------------------------
// HELPER FUNCTIONS
// ---------------------------------------------------------------------------
function getDefaultValue(type) {
const defaults = {
string: "value",
int8: "0",
uint8: "0",
int16: "0",
uint16: "0",
int32: "0",
uint32: "0",
float32: "0.0",
float16: "0.0",
bool: "false",
array_int8: "1,2,3"
};
return defaults[type] || "0";
}
function loadTemplate(templateKey) {
const template = config.templates[templateKey];
state.fields = template.fields.map((f, i) => ({
id: state.nextId + i,
name: f.name,
type: f.type,
value: f.value,
enabled: true
}));
state.nextId += template.fields.length;
updateFieldList();
updateResults();
}
function updateFieldList() {
fieldListContainer.selectAll("*").remove();
if (state.fields.length === 0) {
fieldListContainer.append("div")
.style("padding", "20px")
.style("text-align", "center")
.style("color", config.colors.gray)
.text("No fields added. Add a field or select a template above.");
return;
}
state.fields.forEach((field, index) => {
const row = fieldListContainer.append("div")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "10px")
.style("padding", "10px")
.style("background", index % 2 === 0 ? config.colors.lightGray : config.colors.white)
.style("border-radius", "6px")
.style("margin-bottom", "5px");
// Enable checkbox
row.append("input")
.attr("type", "checkbox")
.attr("checked", field.enabled ? true : null)
.style("accent-color", config.colors.teal)
.style("cursor", "pointer")
.on("change", function() {
field.enabled = this.checked;
updateResults();
});
// Field name
row.append("input")
.attr("type", "text")
.attr("value", field.name)
.style("padding", "6px 10px")
.style("border", `1px solid ${config.colors.gray}`)
.style("border-radius", "4px")
.style("font-size", "12px")
.style("width", "100px")
.on("input", function() {
field.name = this.value;
updateResults();
});
// Type selector
const typeSelect = row.append("select")
.style("padding", "6px 10px")
.style("border", `1px solid ${config.colors.gray}`)
.style("border-radius", "4px")
.style("font-size", "11px")
.style("background", config.colors.white)
.style("cursor", "pointer")
.on("change", function() {
field.type = this.value;
updateResults();
});
config.dataTypes.forEach(dt => {
typeSelect.append("option")
.attr("value", dt.id)
.attr("selected", dt.id === field.type ? true : null)
.text(dt.name);
});
// Value input
row.append("input")
.attr("type", "text")
.attr("value", field.value)
.style("padding", "6px 10px")
.style("border", `1px solid ${config.colors.gray}`)
.style("border-radius", "4px")
.style("font-size", "12px")
.style("width", "100px")
.on("input", function() {
field.value = this.value;
updateResults();
});
// Type info
const typeInfo = config.dataTypes.find(t => t.id === field.type);
row.append("span")
.style("font-size", "10px")
.style("color", config.colors.gray)
.style("flex", "1")
.text(typeInfo ? `(${typeInfo.binarySize(field.value)}B)` : "");
// Delete button
row.append("button")
.style("padding", "4px 10px")
.style("background", config.colors.red)
.style("color", config.colors.white)
.style("border", "none")
.style("border-radius", "4px")
.style("font-size", "11px")
.style("cursor", "pointer")
.text("X")
.on("click", () => {
state.fields = state.fields.filter(f => f.id !== field.id);
updateFieldList();
updateResults();
});
});
}
function updateResults() {
const sizes = calculateSizes();
const compatibility = checkProtocolCompatibility(sizes);
const scale = calculateScale(sizes);
// Update format sizes
container.select(".format-size-jsonCompact").text(sizes.jsonCompact.size);
container.select(".format-size-cbor").text(sizes.cbor.size);
container.select(".format-size-binary").text(sizes.binary.size);
// Update savings percentages
const jsonSize = sizes.jsonCompact.size;
container.select(".format-savings-jsonCompact").text("Baseline (100%)");
container.select(".format-savings-cbor")
.text(`${Math.round((1 - sizes.cbor.size / jsonSize) * 100)}% smaller`);
container.select(".format-savings-binary")
.text(`${Math.round((1 - sizes.binary.size / jsonSize) * 100)}% smaller`);
// Update size bars
updateSizeBars(sizes);
// Update protocol compatibility
updateProtocolCompatibility(compatibility);
// Update scale results
updateScaleResults(scale);
// Update JSON preview
const payload = buildPayloadObject();
previewCode.text(JSON.stringify(payload, null, 2));
}
function updateSizeBars(sizes) {
barSvg.selectAll("*").remove();
const maxSize = sizes.jsonCompact.size;
const barData = [
{ name: "JSON", size: sizes.jsonCompact.size, color: config.colors.orange },
{ name: "CBOR", size: sizes.cbor.size, color: config.colors.teal },
{ name: "Binary", size: sizes.binary.size, color: config.colors.green }
];
const barHeight = 20;
const barGap = 8;
const maxWidth = 450;
barData.forEach((d, i) => {
const y = i * (barHeight + barGap);
const width = maxSize > 0 ? (d.size / maxSize) * maxWidth : 0;
// Label
barSvg.append("text")
.attr("x", 0)
.attr("y", y + 14)
.attr("font-size", "11px")
.attr("fill", "#ffffff")
.text(d.name);
// Bar background
barSvg.append("rect")
.attr("x", 60)
.attr("y", y)
.attr("width", maxWidth)
.attr("height", barHeight)
.attr("fill", "rgba(255,255,255,0.1)")
.attr("rx", 3);
// Bar fill
barSvg.append("rect")
.attr("x", 60)
.attr("y", y)
.attr("width", width)
.attr("height", barHeight)
.attr("fill", d.color)
.attr("rx", 3);
// Size label
barSvg.append("text")
.attr("x", 520)
.attr("y", y + 14)
.attr("font-size", "11px")
.attr("fill", "#ffffff")
.attr("text-anchor", "end")
.text(`${d.size} bytes`);
});
}
function updateProtocolCompatibility(compatibility) {
const grid = container.select(".protocol-grid");
grid.selectAll("*").remove();
Object.entries(compatibility).forEach(([key, proto]) => {
const card = grid.append("div")
.style("background", config.colors.white)
.style("border-radius", "8px")
.style("padding", "12px")
.style("border", `2px solid ${proto.binaryFits ? config.colors.green : config.colors.red}`);
card.append("div")
.style("font-weight", "bold")
.style("font-size", "12px")
.style("color", config.colors.navy)
.style("margin-bottom", "5px")
.text(proto.name);
card.append("div")
.style("font-size", "11px")
.style("color", config.colors.gray)
.style("margin-bottom", "8px")
.text(`Max: ${proto.maxBytes} bytes`);
// Status indicators
const statuses = [
{ label: "JSON", fits: proto.jsonFits },
{ label: "CBOR", fits: proto.cborFits },
{ label: "Binary", fits: proto.binaryFits }
];
statuses.forEach(s => {
const statusRow = card.append("div")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "5px")
.style("font-size", "10px")
.style("margin-bottom", "3px");
statusRow.append("span")
.style("width", "8px")
.style("height", "8px")
.style("border-radius", "50%")
.style("background", s.fits ? config.colors.green : config.colors.red);
statusRow.append("span")
.style("color", s.fits ? config.colors.green : config.colors.red)
.text(`${s.label}: ${s.fits ? "OK" : "Too large"}`);
});
});
}
function updateScaleResults(scale) {
const results = container.select(".scale-results");
results.selectAll("*").remove();
const formats = [
{ name: "JSON", data: scale.json, color: config.colors.orange },
{ name: "CBOR", data: scale.cbor, color: config.colors.teal },
{ name: "Binary", data: scale.binary, color: config.colors.green }
];
formats.forEach(fmt => {
const card = results.append("div")
.style("background", config.colors.lightGray)
.style("border-radius", "8px")
.style("padding", "15px")
.style("border-left", `4px solid ${fmt.color}`);
card.append("div")
.style("font-weight", "bold")
.style("color", config.colors.navy)
.style("font-size", "13px")
.style("margin-bottom", "10px")
.text(fmt.name);
card.append("div")
.style("font-size", "11px")
.style("color", config.colors.gray)
.text("Daily:");
card.append("div")
.style("font-size", "16px")
.style("font-weight", "bold")
.style("color", fmt.color)
.style("margin-bottom", "8px")
.text(formatBytes(fmt.data.daily));
card.append("div")
.style("font-size", "11px")
.style("color", config.colors.gray)
.text("Monthly:");
card.append("div")
.style("font-size", "16px")
.style("font-weight", "bold")
.style("color", fmt.color)
.text(formatBytes(fmt.data.monthly));
// Cost estimate (at $0.01/KB)
const monthlyCostCents = (fmt.data.monthly / 1024) * 1;
card.append("div")
.style("font-size", "10px")
.style("color", config.colors.gray)
.style("margin-top", "8px")
.style("padding-top", "8px")
.style("border-top", `1px solid ${config.colors.gray}`)
.text(`~$${(monthlyCostCents / 100).toFixed(2)}/mo @ $0.01/KB`);
});
}
// ---------------------------------------------------------------------------
// EXPORT PANEL
// ---------------------------------------------------------------------------
const exportPanel = container.append("div")
.style("background", config.colors.navy)
.style("padding", "15px 20px")
.style("border-radius", "12px")
.style("margin-top", "15px")
.style("display", "flex")
.style("gap", "15px")
.style("align-items", "center")
.style("justify-content", "flex-end");
exportPanel.append("span")
.style("font-size", "13px")
.style("color", config.colors.white)
.style("font-weight", "bold")
.style("margin-right", "auto")
.text("Export Payload:");
// Export JSON button
exportPanel.append("button")
.text("Download JSON")
.style("padding", "10px 20px")
.style("background", config.colors.teal)
.style("color", config.colors.white)
.style("border", "none")
.style("border-radius", "6px")
.style("cursor", "pointer")
.style("font-size", "13px")
.style("font-weight", "bold")
.style("transition", "all 0.2s")
.on("mouseover", function() {
d3.select(this).style("background", "#0d7d6a");
})
.on("mouseout", function() {
d3.select(this).style("background", config.colors.teal);
})
.on("click", () => {
const sizes = calculateSizes();
const compatibility = checkProtocolCompatibility(sizes);
const scale = calculateScale(sizes);
const payload = buildPayloadObject();
const exportConfig = {
exportedAt: new Date().toISOString(),
tool: "Payload Builder & Validator",
payloadStructure: {
fields: state.fields.filter(f => f.enabled).map(f => ({
name: f.name,
type: f.type,
value: f.value,
binarySize: config.dataTypes.find(t => t.id === f.type)?.binarySize(f.value) || 0
})),
payload: payload
},
encoding: {
json: {
content: JSON.stringify(payload),
sizeBytes: sizes.jsonCompact.size
},
cborEstimate: {
sizeBytes: sizes.cbor.size
},
binary: {
sizeBytes: sizes.binary.size
}
},
sizeAnalysis: {
jsonTooBigFor: Object.entries(compatibility)
.filter(([k, v]) => !v.jsonFits)
.map(([k, v]) => v.name),
cborTooBigFor: Object.entries(compatibility)
.filter(([k, v]) => !v.cborFits)
.map(([k, v]) => v.name),
binaryTooBigFor: Object.entries(compatibility)
.filter(([k, v]) => !v.binaryFits)
.map(([k, v]) => v.name)
},
protocolCompatibility: compatibility,
scaleProjection: {
deviceCount: state.deviceCount,
messagesPerDay: state.messagesPerDay,
dailyVolume: {
json: formatBytes(scale.json.daily),
cbor: formatBytes(scale.cbor.daily),
binary: formatBytes(scale.binary.daily)
},
monthlyVolume: {
json: formatBytes(scale.json.monthly),
cbor: formatBytes(scale.cbor.monthly),
binary: formatBytes(scale.binary.monthly)
}
}
};
const blob = new Blob([JSON.stringify(exportConfig, null, 2)], {type: "application/json"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "payload-design.json";
a.click();
URL.revokeObjectURL(url);
});
// Copy to clipboard button
exportPanel.append("button")
.text("Copy to Clipboard")
.style("padding", "10px 20px")
.style("background", config.colors.orange)
.style("color", config.colors.white)
.style("border", "none")
.style("border-radius", "6px")
.style("cursor", "pointer")
.style("font-size", "13px")
.style("font-weight", "bold")
.style("transition", "all 0.2s")
.on("mouseover", function() {
d3.select(this).style("background", "#d35400");
})
.on("mouseout", function() {
d3.select(this).style("background", config.colors.orange);
})
.on("click", function() {
const sizes = calculateSizes();
const compatibility = checkProtocolCompatibility(sizes);
const scale = calculateScale(sizes);
const payload = buildPayloadObject();
const exportConfig = {
exportedAt: new Date().toISOString(),
tool: "Payload Builder & Validator",
payloadStructure: {
fields: state.fields.filter(f => f.enabled).map(f => ({
name: f.name,
type: f.type,
value: f.value,
binarySize: config.dataTypes.find(t => t.id === f.type)?.binarySize(f.value) || 0
})),
payload: payload
},
encoding: {
json: {
content: JSON.stringify(payload),
sizeBytes: sizes.jsonCompact.size
},
cborEstimate: {
sizeBytes: sizes.cbor.size
},
binary: {
sizeBytes: sizes.binary.size
}
},
sizeAnalysis: {
jsonTooBigFor: Object.entries(compatibility)
.filter(([k, v]) => !v.jsonFits)
.map(([k, v]) => v.name),
cborTooBigFor: Object.entries(compatibility)
.filter(([k, v]) => !v.cborFits)
.map(([k, v]) => v.name),
binaryTooBigFor: Object.entries(compatibility)
.filter(([k, v]) => !v.binaryFits)
.map(([k, v]) => v.name)
},
protocolCompatibility: compatibility,
scaleProjection: {
deviceCount: state.deviceCount,
messagesPerDay: state.messagesPerDay,
dailyVolume: {
json: formatBytes(scale.json.daily),
cbor: formatBytes(scale.cbor.daily),
binary: formatBytes(scale.binary.daily)
},
monthlyVolume: {
json: formatBytes(scale.json.monthly),
cbor: formatBytes(scale.cbor.monthly),
binary: formatBytes(scale.binary.monthly)
}
}
};
navigator.clipboard.writeText(JSON.stringify(exportConfig, null, 2)).then(() => {
const btn = d3.select(this);
btn.text("Copied!").style("background", config.colors.green);
setTimeout(() => {
btn.text("Copy to Clipboard").style("background", config.colors.orange);
}, 2000);
});
});
// ---------------------------------------------------------------------------
// INITIALIZE
// ---------------------------------------------------------------------------
updateFieldList();
updateResults();
return container.node();
}55.2 Understanding Payload Design
55.2.1 Why Payload Size Matters
In IoT systems, every byte counts. Payload size directly impacts:
- Bandwidth costs - Cellular data is expensive at scale
- Battery life - Larger payloads = longer transmit time = more energy
- Protocol compatibility - Many protocols have strict size limits
- Latency - Smaller payloads transmit faster
55.2.2 Format Trade-offs
| Format | Pros | Cons | Best For |
|---|---|---|---|
| JSON | Human-readable, universal | Large, parsing overhead | Wi-Fi, debugging |
| CBOR | 40-50% smaller, self-describing | Binary, less tooling | LoRaWAN, CoAP |
| Binary Struct | Smallest possible | Rigid, no field names | Sigfox, ultra-constrained |
55.2.3 Protocol Payload Limits
%% fig-alt: Decision tree for choosing appropriate data format based on protocol and payload requirements.
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#2C3E50', 'primaryTextColor': '#FFFFFF', 'primaryBorderColor': '#16A085', 'lineColor': '#7F8C8D', 'secondaryColor': '#ECF0F1', 'tertiaryColor': '#FFFFFF'}}}%%
flowchart TD
START[What protocol<br/>are you using?] --> Q1{Sigfox?}
Q1 -->|Yes| BIN[Custom Binary Only<br/>12 byte limit]
Q1 -->|No| Q2{LoRaWAN?}
Q2 -->|Yes| Q3{SF12?<br/>Long range}
Q3 -->|Yes| CBOR1[CBOR or Binary<br/>51 byte limit]
Q3 -->|No| CBOR2[CBOR preferred<br/>115-242 bytes]
Q2 -->|No| Q4{Wi-Fi/LTE/Ethernet?}
Q4 -->|Yes| JSON[JSON OK<br/>Bandwidth plentiful]
Q4 -->|No| Q5{BLE?}
Q5 -->|Yes| CBOR3[CBOR recommended<br/>244 byte ATT MTU]
Q5 -->|No| ASSESS[Assess your limits<br/>Choose accordingly]
style START fill:#2C3E50,stroke:#16A085,stroke-width:2px,color:#fff
style BIN fill:#E74C3C,stroke:#2C3E50,stroke-width:2px,color:#fff
style CBOR1 fill:#E67E22,stroke:#2C3E50,stroke-width:2px,color:#fff
style CBOR2 fill:#16A085,stroke:#2C3E50,stroke-width:2px,color:#fff
style CBOR3 fill:#16A085,stroke:#2C3E50,stroke-width:2px,color:#fff
style JSON fill:#27AE60,stroke:#2C3E50,stroke-width:2px,color:#fff
This decision tree helps you quickly identify which data format is appropriate based on your communication protocol.
55.2.4 Field Type Optimization
When designing payloads, choose the smallest data type that fits your data:
| Data Range | Recommended Type | Size |
|---|---|---|
| 0 to 255 | uint8 | 1 byte |
| -128 to 127 | int8 | 1 byte |
| 0 to 65,535 | uint16 | 2 bytes |
| Temperature (-40 to 125) | int8 | 1 byte |
| Humidity (0-100%) | uint8 | 1 byte |
| GPS coordinates | float32 | 4 bytes |
| Timestamps | uint32 | 4 bytes |
- Use short field names in JSON (e.g., “t” instead of “temperature”)
- Scale values to use smaller integers (e.g., temp * 10 as uint16 instead of float)
- Use enums for categorical data (0, 1, 2 instead of “low”, “medium”, “high”)
- Batch readings when possible to reduce per-message overhead
- Include version field for schema evolution
- Omit null/default values to reduce size
- Over-engineering: Using float64 for temperature (int8 or int16 usually sufficient)
- Verbose names: “device_identifier” vs “id” adds 14 bytes in JSON
- Redundant data: Sending device ID in every message when it’s in the topic/address
- No compression: Consider gzip for JSON over high-bandwidth links
55.2.5 Real-World Example: Temperature Sensor
| Format | Payload | Size |
|---|---|---|
| JSON | {"id":"S001","t":23.5,"h":65,"ts":1702732800} |
47 bytes |
| Compact | {"i":"S001","t":23.5,"h":65,"s":1702732800} |
44 bytes |
| CBOR | Binary encoding | ~28 bytes |
| Binary | 4B id + 2B temp + 1B hum + 4B ts | 11 bytes |
The binary format achieves 77% reduction compared to JSON!
55.3 What’s Next
Explore related data format topics:
- Data Formats for IoT - Comprehensive format guide
- Packet Structure and Framing - How payloads are wrapped
- Protocol Selection Framework - Choose the right protocol
- LoRaWAN Overview - LPWAN constraints
Interactive tool created for the IoT Class Textbook - PAYLOAD-001