{
// ===========================================================================
// POWER PROFILE ANALYZER
// ===========================================================================
// Features:
// - Device profile configuration (MCU, Radio, Sensors, Voltage)
// - State machine with power states and transitions
// - Time-series power visualization
// - Duty cycle calculator with battery life estimation
// - Comparison mode for up to 3 profiles
// - Preset scenarios and export functionality
//
// 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,
height: 2400,
colors: {
navy: "#2C3E50",
teal: "#16A085",
orange: "#E67E22",
gray: "#7F8C8D",
lightGray: "#ECF0F1",
white: "#FFFFFF",
green: "#27AE60",
red: "#E74C3C",
yellow: "#F1C40F",
purple: "#9B59B6",
blue: "#3498DB",
pink: "#E91E63"
},
// MCU definitions with power profiles (mA at 3.3V)
mcus: [
{
id: "esp32",
name: "ESP32",
deepSleep: 0.01,
lightSleep: 0.8,
idle: 20,
active: 80,
transmit: 240,
voltageRange: [2.3, 3.6],
color: "#3498DB"
},
{
id: "stm32l4",
name: "STM32L4",
deepSleep: 0.00012,
lightSleep: 0.4,
idle: 1.5,
active: 8,
transmit: 8,
voltageRange: [1.71, 3.6],
color: "#9B59B6"
},
{
id: "nrf52",
name: "nRF52840",
deepSleep: 0.0003,
lightSleep: 0.3,
idle: 0.5,
active: 5,
transmit: 15,
voltageRange: [1.7, 5.5],
color: "#27AE60"
},
{
id: "atmega328p",
name: "ATmega328P",
deepSleep: 0.0001,
lightSleep: 0.8,
idle: 1.5,
active: 6,
transmit: 6,
voltageRange: [1.8, 5.5],
color: "#E67E22"
}
],
// Radio definitions
radios: [
{
id: "wifi",
name: "Wi-Fi",
sleep: 0.01,
idle: 15,
rx: 100,
tx: 240,
color: "#E74C3C"
},
{
id: "ble",
name: "BLE",
sleep: 0.001,
idle: 0.005,
rx: 8,
tx: 12,
color: "#16A085"
},
{
id: "lora",
name: "LoRa",
sleep: 0.0002,
idle: 1.6,
rx: 12,
tx: 120,
color: "#F39C12"
},
{
id: "zigbee",
name: "Zigbee",
sleep: 0.001,
idle: 0.5,
rx: 20,
tx: 35,
color: "#8E44AD"
},
{
id: "ltem",
name: "LTE-M",
sleep: 0.005,
idle: 6,
rx: 80,
tx: 250,
color: "#2980B9"
}
],
// Sensor definitions
sensors: [
{
id: "temperature",
name: "Temperature",
sleep: 0.0001,
active: 0.35,
sampleTime: 10, // ms
color: "#1ABC9C"
},
{
id: "accelerometer",
name: "Accelerometer",
sleep: 0.003,
active: 0.5,
sampleTime: 5,
color: "#3498DB"
},
{
id: "gps",
name: "GPS",
sleep: 0.01,
active: 45,
sampleTime: 30000, // 30s for first fix
color: "#E74C3C"
},
{
id: "camera",
name: "Camera",
sleep: 0.1,
active: 200,
sampleTime: 100,
color: "#9B59B6"
}
],
// Power states
states: [
{ id: "deepSleep", name: "Deep Sleep", order: 0 },
{ id: "lightSleep", name: "Light Sleep", order: 1 },
{ id: "idle", name: "Idle", order: 2 },
{ id: "active", name: "Active", order: 3 },
{ id: "transmit", name: "Transmit", order: 4 }
],
// Transition triggers
triggers: [
{ id: "timer", name: "Timer", description: "Periodic timer wakeup" },
{ id: "interrupt", name: "Interrupt", description: "External pin interrupt" },
{ id: "sensor", name: "Sensor Event", description: "Sensor threshold crossed" }
],
// Preset scenarios
presets: [
{
id: "beacon",
name: "Sensor Beacon",
description: "Wake, measure, transmit, sleep",
mcu: "nrf52",
radio: "ble",
sensors: ["temperature"],
voltage: 3.3,
wakeInterval: 60,
stateTimings: {
deepSleep: 59.5,
lightSleep: 0,
idle: 0.05,
active: 0.35,
transmit: 0.1
}
},
{
id: "always_on",
name: "Always-On Monitoring",
description: "Continuous sensor monitoring with periodic reports",
mcu: "stm32l4",
radio: "wifi",
sensors: ["temperature", "accelerometer"],
voltage: 3.3,
wakeInterval: 1,
stateTimings: {
deepSleep: 0,
lightSleep: 0.5,
idle: 0.3,
active: 0.15,
transmit: 0.05
}
},
{
id: "event_driven",
name: "Event-Driven Wakeup",
description: "Sleep until interrupt, then process and report",
mcu: "atmega328p",
radio: "zigbee",
sensors: ["accelerometer"],
voltage: 3.3,
wakeInterval: 300,
stateTimings: {
deepSleep: 299,
lightSleep: 0,
idle: 0.1,
active: 0.7,
transmit: 0.2
}
},
{
id: "gps_tracker",
name: "GPS Tracker with Cellular",
description: "Periodic GPS fix with cellular upload",
mcu: "esp32",
radio: "ltem",
sensors: ["gps"],
voltage: 3.7,
wakeInterval: 300,
stateTimings: {
deepSleep: 260,
lightSleep: 5,
idle: 2,
active: 30,
transmit: 3
}
}
],
// Battery options for estimation
batteries: [
{ id: "cr2032", name: "CR2032 Coin Cell", capacity: 225, voltage: 3.0 },
{ id: "aa_lithium", name: "AA Lithium (2x)", capacity: 3000, voltage: 3.0 },
{ id: "lipo_1000", name: "LiPo 1000mAh", capacity: 1000, voltage: 3.7 },
{ id: "lipo_3000", name: "LiPo 3000mAh", capacity: 3000, voltage: 3.7 },
{ id: "18650", name: "18650 Li-Ion", capacity: 3400, voltage: 3.7 }
]
};
// ---------------------------------------------------------------------------
// STATE MANAGEMENT
// ---------------------------------------------------------------------------
let state = {
// Current profile (profile 0)
activeProfile: 0,
profiles: [
{
name: "Profile 1",
mcu: "esp32",
radio: "ble",
sensors: ["temperature"],
voltage: 3.3,
wakeInterval: 60,
activeTimePerWake: 0.5,
stateTimings: {
deepSleep: 59.0,
lightSleep: 0.2,
idle: 0.1,
active: 0.6,
transmit: 0.1
},
stateTriggers: {
deepSleep: "timer",
lightSleep: "timer",
idle: "timer",
active: "sensor",
transmit: "timer"
},
battery: "lipo_1000",
enabled: true
},
{
name: "Profile 2",
mcu: "nrf52",
radio: "ble",
sensors: ["temperature"],
voltage: 3.3,
wakeInterval: 60,
activeTimePerWake: 0.3,
stateTimings: {
deepSleep: 59.3,
lightSleep: 0.1,
idle: 0.05,
active: 0.45,
transmit: 0.1
},
stateTriggers: {
deepSleep: "timer",
lightSleep: "timer",
idle: "timer",
active: "sensor",
transmit: "timer"
},
battery: "lipo_1000",
enabled: false
},
{
name: "Profile 3",
mcu: "stm32l4",
radio: "lora",
sensors: ["temperature"],
voltage: 3.3,
wakeInterval: 300,
activeTimePerWake: 0.5,
stateTimings: {
deepSleep: 299.0,
lightSleep: 0.2,
idle: 0.1,
active: 0.5,
transmit: 0.2
},
stateTriggers: {
deepSleep: "timer",
lightSleep: "timer",
idle: "timer",
active: "sensor",
transmit: "timer"
},
battery: "aa_lithium",
enabled: false
}
],
showComparison: false,
selectedPreset: null,
timelineZoom: 1,
showExportPanel: false
};
// ---------------------------------------------------------------------------
// CALCULATION FUNCTIONS
// ---------------------------------------------------------------------------
function calculateProfilePower(profile) {
const mcu = config.mcus.find(m => m.id === profile.mcu);
const radio = config.radios.find(r => r.id === profile.radio);
const selectedSensors = profile.sensors.map(id => config.sensors.find(s => s.id === id));
const battery = config.batteries.find(b => b.id === profile.battery);
// Voltage scaling factor (approximate)
const voltageRatio = profile.voltage / 3.3;
// Calculate current for each state (mA)
const statePower = {};
// Deep sleep: MCU + radio + sensors all in sleep
statePower.deepSleep = mcu.deepSleep * voltageRatio +
radio.sleep +
selectedSensors.reduce((sum, s) => sum + s.sleep, 0);
// Light sleep: MCU light sleep + radio sleep + sensors sleep
statePower.lightSleep = mcu.lightSleep * voltageRatio +
radio.sleep +
selectedSensors.reduce((sum, s) => sum + s.sleep, 0);
// Idle: MCU idle + radio idle + sensors sleep
statePower.idle = mcu.idle * voltageRatio +
radio.idle +
selectedSensors.reduce((sum, s) => sum + s.sleep, 0);
// Active: MCU active + radio rx + sensors active
statePower.active = mcu.active * voltageRatio +
radio.rx +
selectedSensors.reduce((sum, s) => sum + s.active, 0);
// Transmit: MCU active + radio tx + sensors sleep
statePower.transmit = mcu.active * voltageRatio +
radio.tx +
selectedSensors.reduce((sum, s) => sum + s.sleep, 0);
// Calculate weighted average current
const totalTime = Object.values(profile.stateTimings).reduce((a, b) => a + b, 0);
let avgCurrent = 0;
let peakCurrent = 0;
Object.keys(statePower).forEach(stateId => {
const timeInState = profile.stateTimings[stateId] || 0;
avgCurrent += (statePower[stateId] * timeInState) / totalTime;
peakCurrent = Math.max(peakCurrent, statePower[stateId]);
});
// Energy per cycle (mWh)
const energyPerCycle = Object.keys(statePower).reduce((total, stateId) => {
const timeInState = (profile.stateTimings[stateId] || 0) / 3600; // hours
return total + (statePower[stateId] * profile.voltage * timeInState);
}, 0);
// Cycles per day
const cyclesPerDay = 86400 / profile.wakeInterval;
// Daily energy (mWh)
const dailyEnergy = energyPerCycle * cyclesPerDay;
// Battery life (days)
const batteryEnergyMWh = battery.capacity * battery.voltage; // mWh
const batteryLifeDays = batteryEnergyMWh / dailyEnergy * 0.8; // 80% usable
// Duty cycle
const sleepTime = (profile.stateTimings.deepSleep || 0) + (profile.stateTimings.lightSleep || 0);
const dutyCycle = ((totalTime - sleepTime) / totalTime) * 100;
return {
statePower,
avgCurrent,
peakCurrent,
energyPerCycle,
dailyEnergy,
batteryLifeDays,
dutyCycle,
cyclesPerDay,
totalCycleTime: totalTime,
battery
};
}
function generateTimelineData(profile, duration = 120) {
// Generate power timeline data for visualization
const metrics = calculateProfilePower(profile);
const data = [];
const cycleTime = profile.wakeInterval;
// Order of states in a typical wake cycle
const stateOrder = ["deepSleep", "lightSleep", "idle", "active", "transmit"];
let currentTime = 0;
while (currentTime < duration) {
let cycleOffset = 0;
stateOrder.forEach(stateId => {
const stateTime = profile.stateTimings[stateId] || 0;
if (stateTime > 0) {
// Add points for this state
const stateStart = currentTime + cycleOffset;
const stateEnd = stateStart + stateTime;
// Entry point
data.push({
time: stateStart,
current: metrics.statePower[stateId],
state: stateId,
transition: true
});
// Middle points (for visualization)
if (stateTime > 0.1) {
data.push({
time: stateStart + stateTime * 0.5,
current: metrics.statePower[stateId],
state: stateId,
transition: false
});
}
cycleOffset += stateTime;
}
});
currentTime += cycleTime;
}
return data;
}
// ---------------------------------------------------------------------------
// CREATE MAIN 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");
// ---------------------------------------------------------------------------
// SECTION 1: DEVICE PROFILE CONFIGURATION
// ---------------------------------------------------------------------------
const devicePanel = container.append("div")
.style("background", config.colors.lightGray)
.style("border-radius", "12px")
.style("padding", "20px")
.style("margin-bottom", "20px")
.style("border", `2px solid ${config.colors.navy}`);
devicePanel.append("h3")
.style("margin", "0 0 15px 0")
.style("color", config.colors.navy)
.style("font-size", "18px")
.style("border-bottom", `2px solid ${config.colors.teal}`)
.style("padding-bottom", "10px")
.text("Device Profile Configuration");
// Profile tabs
const profileTabs = devicePanel.append("div")
.style("display", "flex")
.style("gap", "10px")
.style("margin-bottom", "15px");
state.profiles.forEach((profile, idx) => {
const tab = profileTabs.append("button")
.attr("class", `profile-tab-${idx}`)
.style("padding", "10px 20px")
.style("border", "none")
.style("border-radius", "8px 8px 0 0")
.style("cursor", "pointer")
.style("font-weight", "bold")
.style("font-size", "14px")
.style("background", idx === state.activeProfile ? config.colors.navy : config.colors.white)
.style("color", idx === state.activeProfile ? config.colors.white : config.colors.navy)
.text(profile.name)
.on("click", function() {
state.activeProfile = idx;
updateProfileTabs();
updateAllVisualizations();
});
});
function updateProfileTabs() {
state.profiles.forEach((profile, idx) => {
devicePanel.select(`.profile-tab-${idx}`)
.style("background", idx === state.activeProfile ? config.colors.navy : config.colors.white)
.style("color", idx === state.activeProfile ? config.colors.white : config.colors.navy);
});
}
// Configuration grid
const configGrid = devicePanel.append("div")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fit, minmax(200px, 1fr))")
.style("gap", "20px");
// MCU Selection
const mcuGroup = configGrid.append("div")
.style("background", config.colors.white)
.style("padding", "15px")
.style("border-radius", "8px")
.style("border", `1px solid ${config.colors.gray}`);
mcuGroup.append("label")
.style("display", "block")
.style("font-weight", "600")
.style("color", config.colors.navy)
.style("margin-bottom", "10px")
.style("font-size", "13px")
.text("Microcontroller");
const mcuSelect = mcuGroup.append("select")
.style("width", "100%")
.style("padding", "10px")
.style("border-radius", "6px")
.style("border", `1px solid ${config.colors.gray}`)
.style("font-size", "13px")
.style("cursor", "pointer")
.on("change", function() {
state.profiles[state.activeProfile].mcu = this.value;
updateAllVisualizations();
});
config.mcus.forEach(mcu => {
mcuSelect.append("option")
.attr("value", mcu.id)
.text(`${mcu.name} (${mcu.active}mA active)`);
});
// MCU power stats
const mcuStats = mcuGroup.append("div")
.attr("class", "mcu-stats")
.style("margin-top", "10px")
.style("font-size", "11px")
.style("color", config.colors.gray);
// Radio Selection
const radioGroup = configGrid.append("div")
.style("background", config.colors.white)
.style("padding", "15px")
.style("border-radius", "8px")
.style("border", `1px solid ${config.colors.gray}`);
radioGroup.append("label")
.style("display", "block")
.style("font-weight", "600")
.style("color", config.colors.navy)
.style("margin-bottom", "10px")
.style("font-size", "13px")
.text("Radio Module");
const radioSelect = radioGroup.append("select")
.style("width", "100%")
.style("padding", "10px")
.style("border-radius", "6px")
.style("border", `1px solid ${config.colors.gray}`)
.style("font-size", "13px")
.style("cursor", "pointer")
.on("change", function() {
state.profiles[state.activeProfile].radio = this.value;
updateAllVisualizations();
});
config.radios.forEach(radio => {
radioSelect.append("option")
.attr("value", radio.id)
.text(`${radio.name} (TX: ${radio.tx}mA)`);
});
// Radio power stats
const radioStats = radioGroup.append("div")
.attr("class", "radio-stats")
.style("margin-top", "10px")
.style("font-size", "11px")
.style("color", config.colors.gray);
// Sensor Selection
const sensorGroup = configGrid.append("div")
.style("background", config.colors.white)
.style("padding", "15px")
.style("border-radius", "8px")
.style("border", `1px solid ${config.colors.gray}`);
sensorGroup.append("label")
.style("display", "block")
.style("font-weight", "600")
.style("color", config.colors.navy)
.style("margin-bottom", "10px")
.style("font-size", "13px")
.text("Sensors (select multiple)");
config.sensors.forEach(sensor => {
const sensorLabel = sensorGroup.append("label")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "8px")
.style("margin-bottom", "8px")
.style("cursor", "pointer")
.style("font-size", "12px");
sensorLabel.append("input")
.attr("type", "checkbox")
.attr("value", sensor.id)
.attr("class", `sensor-check-${sensor.id}`)
.style("accent-color", sensor.color)
.on("change", function() {
const profile = state.profiles[state.activeProfile];
if (this.checked) {
if (!profile.sensors.includes(sensor.id)) {
profile.sensors.push(sensor.id);
}
} else {
profile.sensors = profile.sensors.filter(s => s !== sensor.id);
}
updateAllVisualizations();
});
sensorLabel.append("span")
.style("color", sensor.color)
.html(`${sensor.name} <span style="color: ${config.colors.gray}">(${sensor.active}mA)</span>`);
});
// Voltage Selection
const voltageGroup = configGrid.append("div")
.style("background", config.colors.white)
.style("padding", "15px")
.style("border-radius", "8px")
.style("border", `1px solid ${config.colors.gray}`);
voltageGroup.append("label")
.style("display", "block")
.style("font-weight", "600")
.style("color", config.colors.navy)
.style("margin-bottom", "10px")
.style("font-size", "13px")
.text("Operating Voltage");
const voltageSlider = voltageGroup.append("input")
.attr("type", "range")
.attr("min", 1.8)
.attr("max", 5.0)
.attr("step", 0.1)
.attr("value", state.profiles[0].voltage)
.style("width", "100%")
.style("cursor", "pointer")
.on("input", function() {
state.profiles[state.activeProfile].voltage = +this.value;
voltageDisplay.text(`${(+this.value).toFixed(1)}V`);
updateAllVisualizations();
});
const voltageDisplay = voltageGroup.append("div")
.style("text-align", "center")
.style("font-size", "20px")
.style("font-weight", "bold")
.style("color", config.colors.teal)
.style("margin-top", "10px")
.text(`${state.profiles[0].voltage.toFixed(1)}V`);
// ---------------------------------------------------------------------------
// SECTION 2: STATE MACHINE CONFIGURATION
// ---------------------------------------------------------------------------
const statePanel = container.append("div")
.style("background", config.colors.lightGray)
.style("border-radius", "12px")
.style("padding", "20px")
.style("margin-bottom", "20px")
.style("border", `2px solid ${config.colors.navy}`);
statePanel.append("h3")
.style("margin", "0 0 15px 0")
.style("color", config.colors.navy)
.style("font-size", "18px")
.style("border-bottom", `2px solid ${config.colors.teal}`)
.style("padding-bottom", "10px")
.text("State Machine Configuration");
const stateGrid = statePanel.append("div")
.style("display", "grid")
.style("grid-template-columns", "repeat(5, 1fr)")
.style("gap", "15px");
// State color mapping
const stateColors = {
deepSleep: config.colors.teal,
lightSleep: config.colors.green,
idle: config.colors.yellow,
active: config.colors.orange,
transmit: config.colors.red
};
// Create state cards
config.states.forEach(s => {
const stateCard = stateGrid.append("div")
.style("background", config.colors.white)
.style("padding", "12px")
.style("border-radius", "8px")
.style("border", `2px solid ${stateColors[s.id]}`)
.style("text-align", "center");
stateCard.append("div")
.style("font-weight", "bold")
.style("color", stateColors[s.id])
.style("margin-bottom", "8px")
.style("font-size", "12px")
.text(s.name);
stateCard.append("label")
.style("display", "block")
.style("font-size", "10px")
.style("color", config.colors.gray)
.style("margin-bottom", "3px")
.text("Time (s)");
stateCard.append("input")
.attr("type", "number")
.attr("class", `state-time-${s.id}`)
.attr("min", 0)
.attr("step", 0.01)
.attr("value", state.profiles[0].stateTimings[s.id] || 0)
.style("width", "100%")
.style("padding", "6px")
.style("border-radius", "4px")
.style("border", `1px solid ${config.colors.gray}`)
.style("font-size", "12px")
.style("text-align", "center")
.style("box-sizing", "border-box")
.on("input", function() {
state.profiles[state.activeProfile].stateTimings[s.id] = +this.value || 0;
updateAllVisualizations();
});
stateCard.append("label")
.style("display", "block")
.style("font-size", "10px")
.style("color", config.colors.gray)
.style("margin-top", "8px")
.style("margin-bottom", "3px")
.text("Trigger");
const triggerSelect = stateCard.append("select")
.attr("class", `state-trigger-${s.id}`)
.style("width", "100%")
.style("padding", "4px")
.style("border-radius", "4px")
.style("border", `1px solid ${config.colors.gray}`)
.style("font-size", "10px")
.on("change", function() {
state.profiles[state.activeProfile].stateTriggers[s.id] = this.value;
});
config.triggers.forEach(t => {
triggerSelect.append("option")
.attr("value", t.id)
.text(t.name);
});
});
// State machine diagram
const stateDiagram = statePanel.append("div")
.style("margin-top", "20px");
stateDiagram.append("h4")
.style("margin", "0 0 10px 0")
.style("color", config.colors.navy)
.style("font-size", "14px")
.text("State Transition Diagram");
const diagramSvg = stateDiagram.append("svg")
.attr("viewBox", "0 0 900 120")
.attr("width", "100%")
.style("background", config.colors.white)
.style("border-radius", "8px");
// Draw state nodes
const nodePositions = {
deepSleep: 90,
lightSleep: 270,
idle: 450,
active: 630,
transmit: 810
};
Object.keys(nodePositions).forEach(stateId => {
const x = nodePositions[stateId];
const stateDef = config.states.find(s => s.id === stateId);
// Node circle
diagramSvg.append("circle")
.attr("cx", x)
.attr("cy", 60)
.attr("r", 35)
.attr("fill", stateColors[stateId])
.attr("stroke", config.colors.navy)
.attr("stroke-width", 2);
// Node label
diagramSvg.append("text")
.attr("x", x)
.attr("y", 60)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("fill", config.colors.white)
.attr("font-size", "10px")
.attr("font-weight", "bold")
.text(stateDef.name.split(" ")[0]);
// Arrow to next state
if (stateId !== "transmit") {
diagramSvg.append("path")
.attr("d", `M ${x + 40} 60 L ${x + 130} 60`)
.attr("stroke", config.colors.gray)
.attr("stroke-width", 2)
.attr("marker-end", "url(#arrowhead)");
}
});
// Arrow back from transmit to deep sleep
diagramSvg.append("path")
.attr("d", "M 810 100 Q 450 150 90 100")
.attr("fill", "none")
.attr("stroke", config.colors.gray)
.attr("stroke-width", 2)
.attr("stroke-dasharray", "5,5")
.attr("marker-end", "url(#arrowhead)");
// Define arrowhead marker
diagramSvg.append("defs")
.append("marker")
.attr("id", "arrowhead")
.attr("markerWidth", 10)
.attr("markerHeight", 7)
.attr("refX", 9)
.attr("refY", 3.5)
.attr("orient", "auto")
.append("polygon")
.attr("points", "0 0, 10 3.5, 0 7")
.attr("fill", config.colors.gray);
// ---------------------------------------------------------------------------
// SECTION 3: PRESET SCENARIOS
// ---------------------------------------------------------------------------
const presetPanel = container.append("div")
.style("background", config.colors.white)
.style("border-radius", "12px")
.style("padding", "15px")
.style("margin-bottom", "20px")
.style("border", `2px solid ${config.colors.gray}`);
presetPanel.append("h4")
.style("margin", "0 0 10px 0")
.style("color", config.colors.navy)
.style("font-size", "14px")
.text("Preset Scenarios");
const presetGrid = presetPanel.append("div")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fit, minmax(200px, 1fr))")
.style("gap", "10px");
config.presets.forEach(preset => {
const presetCard = presetGrid.append("button")
.style("padding", "12px")
.style("background", config.colors.lightGray)
.style("border", `2px solid ${config.colors.gray}`)
.style("border-radius", "8px")
.style("cursor", "pointer")
.style("text-align", "left")
.on("click", () => applyPreset(preset))
.on("mouseenter", function() {
d3.select(this).style("border-color", config.colors.teal);
})
.on("mouseleave", function() {
d3.select(this).style("border-color", config.colors.gray);
});
presetCard.append("div")
.style("font-weight", "bold")
.style("color", config.colors.navy)
.style("font-size", "13px")
.style("margin-bottom", "5px")
.text(preset.name);
presetCard.append("div")
.style("font-size", "11px")
.style("color", config.colors.gray)
.text(preset.description);
});
function applyPreset(preset) {
const profile = state.profiles[state.activeProfile];
profile.mcu = preset.mcu;
profile.radio = preset.radio;
profile.sensors = [...preset.sensors];
profile.voltage = preset.voltage;
profile.wakeInterval = preset.wakeInterval;
profile.stateTimings = { ...preset.stateTimings };
// Update UI controls
mcuSelect.property("value", preset.mcu);
radioSelect.property("value", preset.radio);
voltageSlider.property("value", preset.voltage);
voltageDisplay.text(`${preset.voltage.toFixed(1)}V`);
// Update sensor checkboxes
config.sensors.forEach(s => {
sensorGroup.select(`.sensor-check-${s.id}`)
.property("checked", preset.sensors.includes(s.id));
});
// Update state timing inputs
Object.keys(preset.stateTimings).forEach(stateId => {
statePanel.select(`.state-time-${stateId}`)
.property("value", preset.stateTimings[stateId]);
});
// Update wake interval
wakeIntervalInput.property("value", preset.wakeInterval);
updateAllVisualizations();
}
// ---------------------------------------------------------------------------
// SECTION 4: POWER PROFILE VISUALIZATION
// ---------------------------------------------------------------------------
const chartPanel = container.append("div")
.style("background", config.colors.white)
.style("border-radius", "12px")
.style("padding", "20px")
.style("margin-bottom", "20px")
.style("border", `2px solid ${config.colors.navy}`);
chartPanel.append("h3")
.style("margin", "0 0 15px 0")
.style("color", config.colors.navy)
.style("font-size", "18px")
.style("border-bottom", `2px solid ${config.colors.teal}`)
.style("padding-bottom", "10px")
.text("Power Profile Visualization");
// Chart controls
const chartControls = chartPanel.append("div")
.style("display", "flex")
.style("gap", "15px")
.style("margin-bottom", "15px")
.style("align-items", "center");
chartControls.append("label")
.style("font-size", "13px")
.style("color", config.colors.navy)
.text("Timeline Duration (s): ");
const durationInput = chartControls.append("input")
.attr("type", "range")
.attr("min", 10)
.attr("max", 600)
.attr("value", 120)
.style("width", "200px")
.on("input", function() {
durationDisplay.text(`${this.value}s`);
updatePowerChart();
});
const durationDisplay = chartControls.append("span")
.style("font-weight", "bold")
.style("color", config.colors.teal)
.text("120s");
chartControls.append("div")
.style("margin-left", "auto")
.style("display", "flex")
.style("gap", "10px");
// Power chart SVG
const chartHeight = 300;
const chartMargin = { top: 30, right: 50, bottom: 50, left: 70 };
const chartSvg = chartPanel.append("svg")
.attr("viewBox", `0 0 900 ${chartHeight}`)
.attr("width", "100%")
.style("background", config.colors.lightGray)
.style("border-radius", "8px");
const chartG = chartSvg.append("g")
.attr("transform", `translate(${chartMargin.left}, ${chartMargin.top})`);
const chartWidth = 900 - chartMargin.left - chartMargin.right;
const chartInnerHeight = chartHeight - chartMargin.top - chartMargin.bottom;
// Scales
const xScale = d3.scaleLinear().range([0, chartWidth]);
const yScale = d3.scaleLog().range([chartInnerHeight, 0]);
// Axes
const xAxis = chartG.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0, ${chartInnerHeight})`);
const yAxis = chartG.append("g")
.attr("class", "y-axis");
// Axis labels
chartSvg.append("text")
.attr("x", chartMargin.left + chartWidth / 2)
.attr("y", chartHeight - 10)
.attr("text-anchor", "middle")
.attr("fill", config.colors.gray)
.attr("font-size", "12px")
.text("Time (seconds)");
chartSvg.append("text")
.attr("transform", "rotate(-90)")
.attr("x", -(chartMargin.top + chartInnerHeight / 2))
.attr("y", 15)
.attr("text-anchor", "middle")
.attr("fill", config.colors.gray)
.attr("font-size", "12px")
.text("Current (mA) - Log Scale");
// Power line
const powerLine = chartG.append("path")
.attr("class", "power-line")
.attr("fill", "none")
.attr("stroke", config.colors.navy)
.attr("stroke-width", 2);
// State regions
const stateRegions = chartG.append("g")
.attr("class", "state-regions");
// Average line
const avgLine = chartG.append("line")
.attr("class", "avg-line")
.attr("stroke", config.colors.orange)
.attr("stroke-width", 2)
.attr("stroke-dasharray", "5,5");
// Peak line
const peakLine = chartG.append("line")
.attr("class", "peak-line")
.attr("stroke", config.colors.red)
.attr("stroke-width", 2)
.attr("stroke-dasharray", "3,3");
// Legend
const legend = chartSvg.append("g")
.attr("transform", `translate(${chartMargin.left + 10}, 10)`);
Object.keys(stateColors).forEach((stateId, i) => {
const stateDef = config.states.find(s => s.id === stateId);
const lg = legend.append("g")
.attr("transform", `translate(${i * 150}, 0)`);
lg.append("rect")
.attr("width", 12)
.attr("height", 12)
.attr("rx", 2)
.attr("fill", stateColors[stateId])
.attr("opacity", 0.5);
lg.append("text")
.attr("x", 16)
.attr("y", 10)
.attr("font-size", "10px")
.attr("fill", config.colors.navy)
.text(stateDef.name);
});
function updatePowerChart() {
const profile = state.profiles[state.activeProfile];
const duration = +durationInput.property("value");
const data = generateTimelineData(profile, duration);
const metrics = calculateProfilePower(profile);
// Update scales
xScale.domain([0, duration]);
const minCurrent = Math.max(0.0001, Math.min(...Object.values(metrics.statePower)) * 0.5);
const maxCurrent = Math.max(...Object.values(metrics.statePower)) * 1.5;
yScale.domain([minCurrent, maxCurrent]);
// Update axes
xAxis.transition().duration(300)
.call(d3.axisBottom(xScale).ticks(10));
yAxis.transition().duration(300)
.call(d3.axisLeft(yScale).ticks(5).tickFormat(d => d >= 1 ? d.toFixed(1) : d.toFixed(3)));
// Draw state regions
stateRegions.selectAll("*").remove();
const cycleTime = profile.wakeInterval;
let currentTime = 0;
while (currentTime < duration) {
let cycleOffset = 0;
const stateOrder = ["deepSleep", "lightSleep", "idle", "active", "transmit"];
stateOrder.forEach(stateId => {
const stateTime = profile.stateTimings[stateId] || 0;
if (stateTime > 0) {
const stateStart = currentTime + cycleOffset;
const stateEnd = Math.min(stateStart + stateTime, duration);
if (stateStart < duration) {
stateRegions.append("rect")
.attr("x", xScale(stateStart))
.attr("y", 0)
.attr("width", xScale(stateEnd) - xScale(stateStart))
.attr("height", chartInnerHeight)
.attr("fill", stateColors[stateId])
.attr("opacity", 0.15);
}
cycleOffset += stateTime;
}
});
currentTime += cycleTime;
}
// Draw power line
const line = d3.line()
.x(d => xScale(d.time))
.y(d => yScale(d.current))
.curve(d3.curveStepAfter);
powerLine.datum(data)
.transition()
.duration(300)
.attr("d", line);
// Update average line
avgLine
.attr("x1", 0)
.attr("x2", chartWidth)
.attr("y1", yScale(metrics.avgCurrent))
.attr("y2", yScale(metrics.avgCurrent));
// Update peak line
peakLine
.attr("x1", 0)
.attr("x2", chartWidth)
.attr("y1", yScale(metrics.peakCurrent))
.attr("y2", yScale(metrics.peakCurrent));
}
// ---------------------------------------------------------------------------
// SECTION 5: METRICS DISPLAY
// ---------------------------------------------------------------------------
const metricsPanel = container.append("div")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fit, minmax(150px, 1fr))")
.style("gap", "15px")
.style("margin-bottom", "20px");
const metricCards = [
{ id: "avgCurrent", label: "Average Current", unit: "mA", color: config.colors.teal },
{ id: "peakCurrent", label: "Peak Current", unit: "mA", color: config.colors.red },
{ id: "energyPerCycle", label: "Energy/Cycle", unit: "mWh", color: config.colors.orange },
{ id: "dutyCycle", label: "Duty Cycle", unit: "%", color: config.colors.purple },
{ id: "batteryLife", label: "Battery Life", unit: "days", color: config.colors.green },
{ id: "cyclesPerDay", label: "Cycles/Day", unit: "", color: config.colors.blue }
];
metricCards.forEach(mc => {
const card = metricsPanel.append("div")
.style("background", `linear-gradient(135deg, ${mc.color} 0%, ${d3.color(mc.color).darker(0.5)} 100%)`)
.style("padding", "15px")
.style("border-radius", "10px")
.style("color", config.colors.white)
.style("text-align", "center");
card.append("div")
.style("font-size", "11px")
.style("opacity", "0.9")
.style("margin-bottom", "5px")
.text(mc.label);
card.append("div")
.attr("class", `metric-${mc.id}`)
.style("font-size", "22px")
.style("font-weight", "bold")
.text("--");
card.append("div")
.style("font-size", "12px")
.style("opacity", "0.8")
.text(mc.unit);
});
function updateMetrics() {
const profile = state.profiles[state.activeProfile];
const metrics = calculateProfilePower(profile);
metricsPanel.select(".metric-avgCurrent")
.text(metrics.avgCurrent < 1 ? metrics.avgCurrent.toFixed(4) : metrics.avgCurrent.toFixed(2));
metricsPanel.select(".metric-peakCurrent")
.text(metrics.peakCurrent.toFixed(1));
metricsPanel.select(".metric-energyPerCycle")
.text(metrics.energyPerCycle < 0.001 ? (metrics.energyPerCycle * 1000).toFixed(3) + " uWh" : metrics.energyPerCycle.toFixed(4));
metricsPanel.select(".metric-dutyCycle")
.text(metrics.dutyCycle.toFixed(2));
metricsPanel.select(".metric-batteryLife")
.text(metrics.batteryLifeDays > 365 ? (metrics.batteryLifeDays / 365).toFixed(1) + " yrs" : metrics.batteryLifeDays.toFixed(0));
metricsPanel.select(".metric-cyclesPerDay")
.text(metrics.cyclesPerDay.toFixed(0));
}
// ---------------------------------------------------------------------------
// SECTION 6: DUTY CYCLE CALCULATOR
// ---------------------------------------------------------------------------
const dutyPanel = container.append("div")
.style("background", config.colors.lightGray)
.style("border-radius", "12px")
.style("padding", "20px")
.style("margin-bottom", "20px")
.style("border", `2px solid ${config.colors.navy}`);
dutyPanel.append("h3")
.style("margin", "0 0 15px 0")
.style("color", config.colors.navy)
.style("font-size", "18px")
.style("border-bottom", `2px solid ${config.colors.teal}`)
.style("padding-bottom", "10px")
.text("Duty Cycle Calculator");
const dutyGrid = dutyPanel.append("div")
.style("display", "grid")
.style("grid-template-columns", "1fr 1fr 1fr")
.style("gap", "20px");
// Wake Interval
const wakeGroup = dutyGrid.append("div")
.style("background", config.colors.white)
.style("padding", "15px")
.style("border-radius", "8px");
wakeGroup.append("label")
.style("display", "block")
.style("font-weight", "600")
.style("color", config.colors.navy)
.style("margin-bottom", "8px")
.style("font-size", "13px")
.text("Wake Interval (seconds)");
const wakeIntervalInput = wakeGroup.append("input")
.attr("type", "number")
.attr("min", 1)
.attr("max", 86400)
.attr("value", state.profiles[0].wakeInterval)
.style("width", "100%")
.style("padding", "10px")
.style("border-radius", "6px")
.style("border", `1px solid ${config.colors.gray}`)
.style("font-size", "14px")
.style("box-sizing", "border-box")
.on("input", function() {
state.profiles[state.activeProfile].wakeInterval = +this.value || 60;
updateAllVisualizations();
});
const wakeFormatted = wakeGroup.append("div")
.attr("class", "wake-formatted")
.style("margin-top", "8px")
.style("font-size", "12px")
.style("color", config.colors.gray);
// Battery Selection
const batteryGroup = dutyGrid.append("div")
.style("background", config.colors.white)
.style("padding", "15px")
.style("border-radius", "8px");
batteryGroup.append("label")
.style("display", "block")
.style("font-weight", "600")
.style("color", config.colors.navy)
.style("margin-bottom", "8px")
.style("font-size", "13px")
.text("Battery Type");
const batterySelect = batteryGroup.append("select")
.style("width", "100%")
.style("padding", "10px")
.style("border-radius", "6px")
.style("border", `1px solid ${config.colors.gray}`)
.style("font-size", "13px")
.style("cursor", "pointer")
.on("change", function() {
state.profiles[state.activeProfile].battery = this.value;
updateAllVisualizations();
});
config.batteries.forEach(b => {
batterySelect.append("option")
.attr("value", b.id)
.text(`${b.name} (${b.capacity}mAh)`);
});
// Battery Life Result
const batteryResult = dutyGrid.append("div")
.style("background", config.colors.navy)
.style("padding", "15px")
.style("border-radius", "8px")
.style("color", config.colors.white)
.style("text-align", "center");
batteryResult.append("div")
.style("font-size", "12px")
.style("opacity", "0.8")
.style("margin-bottom", "5px")
.text("Estimated Battery Life");
batteryResult.append("div")
.attr("class", "battery-life-result")
.style("font-size", "28px")
.style("font-weight", "bold")
.style("color", config.colors.teal)
.text("--");
batteryResult.append("div")
.attr("class", "battery-life-unit")
.style("font-size", "14px")
.style("opacity", "0.8")
.text("days");
// Duty cycle visualization bar
const dutyBar = dutyPanel.append("div")
.style("margin-top", "20px");
dutyBar.append("h4")
.style("margin", "0 0 10px 0")
.style("color", config.colors.navy)
.style("font-size", "14px")
.text("Time Distribution per Cycle");
const dutyBarContainer = dutyBar.append("div")
.style("display", "flex")
.style("height", "40px")
.style("border-radius", "8px")
.style("overflow", "hidden")
.style("border", `2px solid ${config.colors.gray}`);
function updateDutyBar() {
const profile = state.profiles[state.activeProfile];
const totalTime = Object.values(profile.stateTimings).reduce((a, b) => a + b, 0);
dutyBarContainer.selectAll("*").remove();
Object.keys(stateColors).forEach(stateId => {
const stateTime = profile.stateTimings[stateId] || 0;
const percentage = (stateTime / totalTime) * 100;
if (percentage > 0) {
const segment = dutyBarContainer.append("div")
.style("width", `${percentage}%`)
.style("background", stateColors[stateId])
.style("display", "flex")
.style("align-items", "center")
.style("justify-content", "center")
.style("color", config.colors.white)
.style("font-size", percentage > 5 ? "10px" : "8px")
.style("font-weight", "bold")
.style("min-width", percentage > 2 ? "auto" : "0");
if (percentage > 3) {
segment.text(`${percentage.toFixed(1)}%`);
}
}
});
}
// ---------------------------------------------------------------------------
// SECTION 7: COMPARISON MODE
// ---------------------------------------------------------------------------
const comparePanel = container.append("div")
.style("background", config.colors.lightGray)
.style("border-radius", "12px")
.style("padding", "20px")
.style("margin-bottom", "20px")
.style("border", `2px solid ${config.colors.purple}`);
comparePanel.append("h3")
.style("margin", "0 0 15px 0")
.style("color", config.colors.navy)
.style("font-size", "18px")
.style("border-bottom", `2px solid ${config.colors.teal}`)
.style("padding-bottom", "10px")
.text("Comparison Mode");
// Enable comparison toggles
const compareToggles = comparePanel.append("div")
.style("display", "flex")
.style("gap", "20px")
.style("margin-bottom", "20px");
state.profiles.forEach((profile, idx) => {
const toggle = compareToggles.append("label")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "8px")
.style("cursor", "pointer");
toggle.append("input")
.attr("type", "checkbox")
.attr("checked", idx === 0 ? true : null)
.attr("class", `compare-toggle-${idx}`)
.style("accent-color", [config.colors.navy, config.colors.teal, config.colors.orange][idx])
.on("change", function() {
state.profiles[idx].enabled = this.checked;
updateComparisonChart();
updateComparisonTable();
});
toggle.append("span")
.style("font-weight", "600")
.style("color", [config.colors.navy, config.colors.teal, config.colors.orange][idx])
.text(profile.name);
});
// Comparison chart
const compareChartHeight = 250;
const compareSvg = comparePanel.append("svg")
.attr("viewBox", `0 0 900 ${compareChartHeight}`)
.attr("width", "100%")
.style("background", config.colors.white)
.style("border-radius", "8px")
.style("margin-bottom", "20px");
const compareG = compareSvg.append("g")
.attr("transform", `translate(${chartMargin.left}, ${chartMargin.top})`);
const compareXScale = d3.scaleLinear().range([0, chartWidth]);
const compareYScale = d3.scaleLog().range([compareChartHeight - chartMargin.top - chartMargin.bottom, 0]);
const compareXAxis = compareG.append("g")
.attr("class", "compare-x-axis")
.attr("transform", `translate(0, ${compareChartHeight - chartMargin.top - chartMargin.bottom})`);
const compareYAxis = compareG.append("g")
.attr("class", "compare-y-axis");
const profileColors = [config.colors.navy, config.colors.teal, config.colors.orange];
function updateComparisonChart() {
const duration = 120;
let allData = [];
let minCurrent = 1;
let maxCurrent = 1;
state.profiles.forEach((profile, idx) => {
if (profile.enabled) {
const data = generateTimelineData(profile, duration);
const metrics = calculateProfilePower(profile);
data.forEach(d => d.profileIdx = idx);
allData = allData.concat(data);
minCurrent = Math.min(minCurrent, ...Object.values(metrics.statePower));
maxCurrent = Math.max(maxCurrent, ...Object.values(metrics.statePower));
}
});
compareXScale.domain([0, duration]);
compareYScale.domain([Math.max(0.0001, minCurrent * 0.5), maxCurrent * 1.5]);
compareXAxis.call(d3.axisBottom(compareXScale).ticks(10));
compareYAxis.call(d3.axisLeft(compareYScale).ticks(5).tickFormat(d => d >= 1 ? d.toFixed(1) : d.toFixed(3)));
// Clear existing lines
compareG.selectAll(".profile-line").remove();
// Draw line for each enabled profile
state.profiles.forEach((profile, idx) => {
if (profile.enabled) {
const data = generateTimelineData(profile, duration);
const line = d3.line()
.x(d => compareXScale(d.time))
.y(d => compareYScale(d.current))
.curve(d3.curveStepAfter);
compareG.append("path")
.attr("class", "profile-line")
.attr("d", line(data))
.attr("fill", "none")
.attr("stroke", profileColors[idx])
.attr("stroke-width", 2)
.attr("opacity", 0.8);
}
});
}
// Comparison table
const compareTable = comparePanel.append("div")
.attr("class", "compare-table")
.style("overflow-x", "auto");
function updateComparisonTable() {
compareTable.selectAll("*").remove();
const table = compareTable.append("table")
.style("width", "100%")
.style("border-collapse", "collapse")
.style("font-size", "12px");
const headers = ["Metric", ...state.profiles.filter(p => p.enabled).map((p, i) => p.name)];
const thead = table.append("thead");
const headerRow = thead.append("tr");
headers.forEach((h, i) => {
headerRow.append("th")
.style("padding", "10px")
.style("background", config.colors.navy)
.style("color", config.colors.white)
.style("text-align", i === 0 ? "left" : "center")
.text(h);
});
const tbody = table.append("tbody");
const metrics = [
{ key: "avgCurrent", label: "Average Current (mA)" },
{ key: "peakCurrent", label: "Peak Current (mA)" },
{ key: "dutyCycle", label: "Duty Cycle (%)" },
{ key: "batteryLifeDays", label: "Battery Life (days)" },
{ key: "energyPerCycle", label: "Energy/Cycle (mWh)" },
{ key: "cyclesPerDay", label: "Cycles per Day" }
];
metrics.forEach((m, rowIdx) => {
const row = tbody.append("tr")
.style("background", rowIdx % 2 === 0 ? config.colors.white : config.colors.lightGray);
row.append("td")
.style("padding", "10px")
.style("font-weight", "600")
.style("color", config.colors.navy)
.text(m.label);
state.profiles.filter(p => p.enabled).forEach(profile => {
const metrics = calculateProfilePower(profile);
let value = metrics[m.key];
if (m.key === "avgCurrent") {
value = value < 1 ? value.toFixed(4) : value.toFixed(2);
} else if (m.key === "peakCurrent") {
value = value.toFixed(1);
} else if (m.key === "dutyCycle") {
value = value.toFixed(2);
} else if (m.key === "batteryLifeDays") {
value = value > 365 ? (value / 365).toFixed(1) + " yrs" : value.toFixed(0);
} else if (m.key === "energyPerCycle") {
value = value.toFixed(4);
} else {
value = value.toFixed(0);
}
row.append("td")
.style("padding", "10px")
.style("text-align", "center")
.text(value);
});
});
}
// ---------------------------------------------------------------------------
// SECTION 8: EXPORT PANEL
// ---------------------------------------------------------------------------
const exportPanel = container.append("div")
.style("background", config.colors.lightGray)
.style("border-radius", "12px")
.style("padding", "20px")
.style("margin-bottom", "20px")
.style("border", `2px solid ${config.colors.green}`);
exportPanel.append("h3")
.style("margin", "0 0 15px 0")
.style("color", config.colors.navy)
.style("font-size", "18px")
.style("border-bottom", `2px solid ${config.colors.teal}`)
.style("padding-bottom", "10px")
.text("Export Power Profile");
const exportGrid = exportPanel.append("div")
.style("display", "grid")
.style("grid-template-columns", "1fr 1fr")
.style("gap", "20px");
// JSON Export
const jsonExport = exportGrid.append("div")
.style("background", config.colors.white)
.style("padding", "15px")
.style("border-radius", "8px");
jsonExport.append("h4")
.style("margin", "0 0 10px 0")
.style("color", config.colors.navy)
.style("font-size", "14px")
.text("JSON Power Profile");
const jsonTextarea = jsonExport.append("textarea")
.attr("class", "json-export")
.attr("readonly", true)
.style("width", "100%")
.style("height", "200px")
.style("font-family", "monospace")
.style("font-size", "10px")
.style("padding", "10px")
.style("border", `1px solid ${config.colors.gray}`)
.style("border-radius", "6px")
.style("resize", "vertical")
.style("box-sizing", "border-box");
jsonExport.append("button")
.style("margin-top", "10px")
.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-weight", "bold")
.text("Copy JSON")
.on("click", function() {
const text = jsonTextarea.property("value");
navigator.clipboard.writeText(text);
d3.select(this).text("Copied!").style("background", config.colors.green);
setTimeout(() => {
d3.select(this).text("Copy JSON").style("background", config.colors.teal);
}, 2000);
});
// C Code Export
const cExport = exportGrid.append("div")
.style("background", config.colors.white)
.style("padding", "15px")
.style("border-radius", "8px");
cExport.append("h4")
.style("margin", "0 0 10px 0")
.style("color", config.colors.navy)
.style("font-size", "14px")
.text("C Power Management Code");
const cTextarea = cExport.append("textarea")
.attr("class", "c-export")
.attr("readonly", true)
.style("width", "100%")
.style("height", "200px")
.style("font-family", "monospace")
.style("font-size", "10px")
.style("padding", "10px")
.style("border", `1px solid ${config.colors.gray}`)
.style("border-radius", "6px")
.style("resize", "vertical")
.style("box-sizing", "border-box");
cExport.append("button")
.style("margin-top", "10px")
.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-weight", "bold")
.text("Copy C Code")
.on("click", function() {
const text = cTextarea.property("value");
navigator.clipboard.writeText(text);
d3.select(this).text("Copied!").style("background", config.colors.green);
setTimeout(() => {
d3.select(this).text("Copy C Code").style("background", config.colors.orange);
}, 2000);
});
function generateJSONExport() {
const profile = state.profiles[state.activeProfile];
const metrics = calculateProfilePower(profile);
const exportData = {
profileName: profile.name,
configuration: {
mcu: profile.mcu,
radio: profile.radio,
sensors: profile.sensors,
operatingVoltage: profile.voltage
},
stateMachine: {
states: config.states.map(s => ({
id: s.id,
name: s.name,
durationSeconds: profile.stateTimings[s.id] || 0,
trigger: profile.stateTriggers[s.id] || "timer",
currentMA: metrics.statePower[s.id]
})),
wakeIntervalSeconds: profile.wakeInterval
},
powerMetrics: {
averageCurrentMA: metrics.avgCurrent,
peakCurrentMA: metrics.peakCurrent,
dutyCyclePercent: metrics.dutyCycle,
energyPerCycleWh: metrics.energyPerCycle / 1000,
cyclesPerDay: metrics.cyclesPerDay
},
batteryEstimate: {
batteryType: profile.battery,
capacityMAh: metrics.battery.capacity,
estimatedLifeDays: metrics.batteryLifeDays
},
generatedAt: new Date().toISOString()
};
jsonTextarea.property("value", JSON.stringify(exportData, null, 2));
}
function generateCCodeExport() {
const profile = state.profiles[state.activeProfile];
const metrics = calculateProfilePower(profile);
const mcuDef = config.mcus.find(m => m.id === profile.mcu);
const code = `/*
* Power Management Configuration
* Generated by IoT Power Profile Analyzer
* ${new Date().toISOString()}
*/
#ifndef POWER_MANAGER_H
#define POWER_MANAGER_H
#include <stdint.h>
/* Power States */
typedef enum {
PWR_STATE_DEEP_SLEEP = 0,
PWR_STATE_LIGHT_SLEEP,
PWR_STATE_IDLE,
PWR_STATE_ACTIVE,
PWR_STATE_TRANSMIT,
PWR_STATE_COUNT
} PowerState_t;
/* Transition Triggers */
typedef enum {
PWR_TRIGGER_TIMER = 0,
PWR_TRIGGER_INTERRUPT,
PWR_TRIGGER_SENSOR_EVENT
} PowerTrigger_t;
/* State Configuration */
typedef struct {
PowerState_t state;
uint32_t duration_ms;
PowerTrigger_t trigger;
float current_mA;
} StateConfig_t;
/* Power Profile Configuration */
#define MCU_TYPE "${mcuDef.name}"
#define RADIO_TYPE "${profile.radio.toUpperCase()}"
#define OPERATING_VOLTAGE_MV ${(profile.voltage * 1000).toFixed(0)}
#define WAKE_INTERVAL_MS ${profile.wakeInterval * 1000}
/* State Durations (milliseconds) */
#define DEEP_SLEEP_DURATION_MS ${((profile.stateTimings.deepSleep || 0) * 1000).toFixed(0)}
#define LIGHT_SLEEP_DURATION_MS ${((profile.stateTimings.lightSleep || 0) * 1000).toFixed(0)}
#define IDLE_DURATION_MS ${((profile.stateTimings.idle || 0) * 1000).toFixed(0)}
#define ACTIVE_DURATION_MS ${((profile.stateTimings.active || 0) * 1000).toFixed(0)}
#define TRANSMIT_DURATION_MS ${((profile.stateTimings.transmit || 0) * 1000).toFixed(0)}
/* Expected Current Draw (mA) */
#define DEEP_SLEEP_CURRENT_MA ${metrics.statePower.deepSleep.toFixed(4)}f
#define LIGHT_SLEEP_CURRENT_MA ${metrics.statePower.lightSleep.toFixed(4)}f
#define IDLE_CURRENT_MA ${metrics.statePower.idle.toFixed(2)}f
#define ACTIVE_CURRENT_MA ${metrics.statePower.active.toFixed(1)}f
#define TRANSMIT_CURRENT_MA ${metrics.statePower.transmit.toFixed(1)}f
/* Calculated Metrics */
#define AVG_CURRENT_MA ${metrics.avgCurrent.toFixed(4)}f
#define PEAK_CURRENT_MA ${metrics.peakCurrent.toFixed(1)}f
#define DUTY_CYCLE_PERCENT ${metrics.dutyCycle.toFixed(2)}f
#define EST_BATTERY_LIFE_DAYS ${metrics.batteryLifeDays.toFixed(0)}
/* State Configuration Array */
static const StateConfig_t power_states[PWR_STATE_COUNT] = {
{ PWR_STATE_DEEP_SLEEP, DEEP_SLEEP_DURATION_MS, PWR_TRIGGER_TIMER, DEEP_SLEEP_CURRENT_MA },
{ PWR_STATE_LIGHT_SLEEP, LIGHT_SLEEP_DURATION_MS, PWR_TRIGGER_TIMER, LIGHT_SLEEP_CURRENT_MA },
{ PWR_STATE_IDLE, IDLE_DURATION_MS, PWR_TRIGGER_TIMER, IDLE_CURRENT_MA },
{ PWR_STATE_ACTIVE, ACTIVE_DURATION_MS, PWR_TRIGGER_SENSOR_EVENT, ACTIVE_CURRENT_MA },
{ PWR_STATE_TRANSMIT, TRANSMIT_DURATION_MS, PWR_TRIGGER_TIMER, TRANSMIT_CURRENT_MA }
};
/* Function Prototypes */
void power_init(void);
void power_enter_state(PowerState_t state);
void power_transition_to_next(void);
PowerState_t power_get_current_state(void);
float power_get_avg_current(void);
/* Implementation Stubs */
static PowerState_t current_state = PWR_STATE_DEEP_SLEEP;
void power_init(void) {
/* Initialize power management */
current_state = PWR_STATE_DEEP_SLEEP;
}
void power_enter_state(PowerState_t state) {
if (state < PWR_STATE_COUNT) {
current_state = state;
/* Platform-specific power state entry */
switch (state) {
case PWR_STATE_DEEP_SLEEP:
/* Enter deep sleep mode */
break;
case PWR_STATE_LIGHT_SLEEP:
/* Enter light sleep mode */
break;
case PWR_STATE_IDLE:
/* Enter idle mode */
break;
case PWR_STATE_ACTIVE:
/* Enter active mode */
break;
case PWR_STATE_TRANSMIT:
/* Enable radio transmit */
break;
}
}
}
void power_transition_to_next(void) {
PowerState_t next = (current_state + 1) % PWR_STATE_COUNT;
power_enter_state(next);
}
PowerState_t power_get_current_state(void) {
return current_state;
}
float power_get_avg_current(void) {
return AVG_CURRENT_MA;
}
#endif /* POWER_MANAGER_H */
`;
cTextarea.property("value", code);
}
// ---------------------------------------------------------------------------
// UPDATE ALL VISUALIZATIONS
// ---------------------------------------------------------------------------
function updateAllVisualizations() {
const profile = state.profiles[state.activeProfile];
const metrics = calculateProfilePower(profile);
// Update MCU select and stats
mcuSelect.property("value", profile.mcu);
const mcu = config.mcus.find(m => m.id === profile.mcu);
mcuStats.html(`Deep: ${mcu.deepSleep}mA | Light: ${mcu.lightSleep}mA | Active: ${mcu.active}mA`);
// Update radio select and stats
radioSelect.property("value", profile.radio);
const radio = config.radios.find(r => r.id === profile.radio);
radioStats.html(`Sleep: ${radio.sleep}mA | RX: ${radio.rx}mA | TX: ${radio.tx}mA`);
// Update sensor checkboxes
config.sensors.forEach(s => {
sensorGroup.select(`.sensor-check-${s.id}`)
.property("checked", profile.sensors.includes(s.id));
});
// Update voltage
voltageSlider.property("value", profile.voltage);
voltageDisplay.text(`${profile.voltage.toFixed(1)}V`);
// Update state timings
Object.keys(profile.stateTimings).forEach(stateId => {
statePanel.select(`.state-time-${stateId}`)
.property("value", profile.stateTimings[stateId]);
});
// Update triggers
Object.keys(profile.stateTriggers).forEach(stateId => {
statePanel.select(`.state-trigger-${stateId}`)
.property("value", profile.stateTriggers[stateId]);
});
// Update wake interval
wakeIntervalInput.property("value", profile.wakeInterval);
const interval = profile.wakeInterval;
let formatted = "";
if (interval < 60) formatted = `${interval} seconds`;
else if (interval < 3600) formatted = `${(interval / 60).toFixed(1)} minutes`;
else if (interval < 86400) formatted = `${(interval / 3600).toFixed(1)} hours`;
else formatted = `${(interval / 86400).toFixed(1)} days`;
wakeFormatted.text(formatted);
// Update battery select
batterySelect.property("value", profile.battery);
// Update battery life display
const batteryLife = metrics.batteryLifeDays;
if (batteryLife > 365) {
dutyPanel.select(".battery-life-result").text((batteryLife / 365).toFixed(1));
dutyPanel.select(".battery-life-unit").text("years");
} else if (batteryLife > 30) {
dutyPanel.select(".battery-life-result").text((batteryLife / 30).toFixed(1));
dutyPanel.select(".battery-life-unit").text("months");
} else {
dutyPanel.select(".battery-life-result").text(batteryLife.toFixed(0));
dutyPanel.select(".battery-life-unit").text("days");
}
// Update all charts and displays
updatePowerChart();
updateMetrics();
updateDutyBar();
updateComparisonChart();
updateComparisonTable();
generateJSONExport();
generateCCodeExport();
}
// ---------------------------------------------------------------------------
// INITIALIZE
// ---------------------------------------------------------------------------
state.profiles[0].enabled = true;
updateAllVisualizations();
return container.node();
}