When auditing a residential or commercial panel, verifying compliance with the wiring electrical code (NEC) usually requires manual calculations and clamp meter readings. This project automates that process. By building an ESP32-based smart panel monitor, you can continuously track real-time RMS current and voltage on a branch circuit, automatically flagging violations of NEC Article 210.20 (the 80% continuous load rule) and Article 210.19 (the 3% voltage drop recommendation). The direct answer to what this system does: it samples AC waveforms at 10kHz, calculates true RMS values, and triggers serial and HTTP alerts when your physical wiring breaches code-mandated ampacity and voltage drop thresholds.
Project Spec Sheet & Parts List
This build targets the ESP32-WROOM-32 DevKit V1 (30-pin variant). Do not use the 38-pin variant without adjusting the pin mapping table below, as the ADC1 pins differ. All prices reflect typical 2026 market rates for authentic components.
| Component | Exact Variant / Model | Est. Price | Purpose in Build |
|---|---|---|---|
| Microcontroller | ESP32-WROOM-32 DevKit V1 (30-pin) | $6.50 | Main logic, ADC sampling, WiFi alerting |
| Current Sensor | SCT-013-013B (100A / 1V output) | $12.00 | Non-invasive RMS current measurement (built-in burden resistor) |
| Voltage Sensor | ZMPT101B AC Voltage Module | $4.50 | Isolated AC voltage step-down for RMS calculation |
| DC Bias Components | 2x 10kΩ resistors, 1x 10µF capacitor | $0.50 | Offsets AC signal to 1.65V for ESP32 unipolar ADC |
| Power Supply | Hi-Link HLK-PM01 (5V 600mA) | $3.50 | Steps down 120V AC to 5V DC to power the ESP32 |
Pin Mapping & Sensor Wiring
The ESP32 ADC pins (GPIO 32-39) are strictly input-only and operate on a 0-3.3V scale. Because AC waveforms swing negative, the DC bias circuit is mandatory. Connect the 10kΩ resistors in series between 3.3V and GND to create a 1.65V midpoint, buffer it with the 10µF capacitor to GND, and feed that midpoint into the sensor ground returns.
| ESP32 Pin | Component | Wire Color | Notes / Constraints |
|---|---|---|---|
| GPIO 34 (ADC1_CH6) | SCT-013-013B Signal | Red | Input only. Do not use ADC2 (WiFi conflicts). |
| GPIO 35 (ADC1_CH7) | ZMPT101B Signal | Yellow | Input only. Calibrate trimpot before sealing. |
| 3.3V | DC Bias Divider Top | Orange | Feeds the 10kΩ/10kΩ voltage divider. |
| GND | DC Bias Divider Bottom | Black | Common ground for ESP32, sensors, and bias cap. |
| 5V (VIN) | HLK-PM01 5V Output | Red (Thick) | Powers the ESP32 onboard regulator. |
Complete ESP32 Compliance Monitor Code
The following C++ code is fully compilable in the Arduino IDE (v2.0+) using the ESP32 board package. It uses a fixed-window 20ms sampling technique to calculate true RMS without external library dependencies, avoiding version conflicts. It targets the ESP32-WROOM-32 DevKit V1.
#include <WiFi.h>
#include <HTTPClient.h>
// --- PIN DEFINITIONS ---
#define PIN_CURRENT_SENSOR 34
#define PIN_VOLTAGE_SENSOR 35
// --- CALIBRATION CONSTANTS ---
// Adjust these based on your multimeter readings during setup
#define CURRENT_CALIBRATION 100.0 // Amps per 1V ADC reading (SCT-013-013B is 100A/1V)
#define VOLTAGE_CALIBRATION 170.0 // Adjust ZMPT101B trimpot to match this baseline
#define ADC_RESOLUTION 4095.0
#define V_REF 3.3
#define DC_BIAS 1.65
// --- NEC CODE THRESHOLDS ---
#define BREAKER_RATING_AMPS 20.0
#define CONTINUOUS_LOAD_LIMIT 0.80 // NEC 210.20(A): 80% rule
#define NOMINAL_VOLTAGE 120.0
#define VDROP_MAX_PERCENT 3.0 // NEC 210.19(A) Info Note: 3% max drop
// --- WIFI & ALERTING ---
const char* ssid = "YourNetworkSSID";
const char* password = "YourNetworkPassword";
const char* webhook_url = "https://hooks.slack.com/services/YOUR/WEBHOOK/URL";
unsigned long lastSampleTime = 0;
const int sampleWindow = 20; // 20ms for 50/60Hz approx window
void setup() {
Serial.begin(115200);
analogReadResolution(12); // 12-bit ADC for ESP32
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi Connected. IP: " + WiFi.localIP().toString());
}
void loop() {
if (millis() - lastSampleTime >= sampleWindow) {
lastSampleTime = millis();
double sumI = 0, sumV = 0;
int samples = 0;
unsigned long startTime = micros();
// Sample for exactly 20ms (approx 1 full 50Hz cycle or 1.2 60Hz cycles)
while ((micros() - startTime) < 20000) {
int rawI = analogRead(PIN_CURRENT_SENSOR);
int rawV = analogRead(PIN_VOLTAGE_SENSOR);
// Convert to voltage and remove DC bias
double vI = ((rawI / ADC_RESOLUTION) * V_REF) - DC_BIAS;
double vV = ((rawV / ADC_RESOLUTION) * V_REF) - DC_BIAS;
// Check for ADC saturation (clipping at 0 or 4095)
if (rawI >= 4090 || rawI <= 5) {
Serial.println("ERR: ADC_SATURATION_PIN_34");
return; // Abort this cycle to prevent false RMS calculation
}
sumI += vI * vI;
sumV += vV * vV;
samples++;
}
if (samples > 0) {
double rmsVoltageRaw = sqrt(sumV / samples);
double rmsCurrentRaw = sqrt(sumI / samples);
double realVoltage = rmsVoltageRaw * VOLTAGE_CALIBRATION;
double realCurrent = rmsCurrentRaw * CURRENT_CALIBRATION;
// Filter out noise floor
if (realCurrent < 0.2) realCurrent = 0.0;
Serial.printf("V: %.1fV | I: %.2fA\n", realVoltage, realCurrent);
checkNECCompliance(realVoltage, realCurrent);
}
}
}
void checkNECCompliance(double voltage, double current) {
// 1. Check 80% Continuous Load Rule (NEC 210.20)
double maxContinuousCurrent = BREAKER_RATING_AMPS * CONTINUOUS_LOAD_LIMIT;
if (current > maxContinuousCurrent) {
Serial.printf("ALERT: NEC 210.20 VIOLATION - Load %.2fA exceeds 80%% limit (%.1fA)\n", current, maxContinuousCurrent);
sendWebhook("NEC 210.20 Overload", current);
}
// 2. Check 3% Voltage Drop Rule (NEC 210.19)
double vDropPercent = ((NOMINAL_VOLTAGE - voltage) / NOMINAL_VOLTAGE) * 100.0;
if (vDropPercent > VDROP_MAX_PERCENT && current > 1.0) { // Only check under load
Serial.printf("ERR: VDROP_GT_3PCT - Drop is %.2f%% at %.2fA\n", vDropPercent, current);
sendWebhook("NEC 210.19 Voltage Drop", vDropPercent);
}
}
void sendWebhook(String alertType, double value) {
if (WiFi.status() == WL_CONNECTED) {
HTTPClient http;
http.begin(webhook_url);
http.addHeader("Content-Type", "application/json");
String payload = "{\"text\":\"[ESP32 Panel Monitor] " + alertType + " detected. Value: " + String(value) + "\"}";
int httpCode = http.POST(payload);
if (httpCode > 0) {
Serial.printf("Webhook sent, code: %d\n", httpCode);
} else {
Serial.printf("ERR: WEBHOOK_FAIL_%d\n", httpCode);
}
http.end();
}
}
Debugging: First Three Things to Check When It Fails
When the serial monitor throws errors or outputs garbage data, follow this ranked decision path. These are the most common failure modes on the bench.
1. Error: ERR: ADC_SATURATION_PIN_34
This means the AC waveform is clipping against the 0V or 3.3V rails of the ESP32 ADC.
- Cause A (Most Likely): The SCT-013 clamp is wrapped around both the hot and neutral wires. The magnetic fields cancel out, but minor imbalances cause erratic DC bias shifting. Fix: Ensure the clamp is around only the single hot (black/red) THHN conductor.
- Cause B: You used an SCT-013-000 (which outputs 50mA) instead of the SCT-013-013B (which outputs 1V). The 000 variant requires an external burden resistor and a different bias circuit. Fix: Swap to the -013B variant or add a 33Ω burden resistor.
- Cause C: The 10µF DC bias capacitor is missing or wired incorrectly, allowing the AC signal to swing below 0V. Fix: Verify the capacitor is in parallel with the bottom 10kΩ resistor.
2. Error: ERR: VDROP_GT_3PCT (False Positives)
The code flags a voltage drop violation, but your multimeter reads 120V at the panel.
- Cause A: The ZMPT101B calibration trimpot drifted during installation. Fix: Measure the actual AC voltage at the outlet with a Fluke 117. Turn the ZMPT101B blue trimpot with a ceramic screwdriver until the serial monitor matches the Fluke reading.
- Cause B: The HLK-PM01 power supply is introducing high-frequency switching noise into the 3.3V rail, corrupting the ADC reference. Fix: Add a 100µF electrolytic capacitor across the 5V and GND pins on the ESP32.
3. Error: ERR: WEBHOOK_FAIL_401 or -1
- Cause A: HTTP 401 means your Slack/Discord webhook URL is expired or malformed. Fix: Regenerate the webhook URL and update the
webhook_urlconstant. - Cause B: Code -1 means DNS resolution failed. Fix: Ensure your WiFi SSID is 2.4GHz. The ESP32-WROOM-32 cannot connect to 5GHz networks.
Extending and Simplifying the Build
To Simplify: If you don't need remote alerting, strip out the WiFi.h and HTTPClient.h libraries. Replace the webhook function with a local I2C OLED display (SSD1306) using the Adafruit_SSD1306 library to show real-time Amps and Volts. This reduces power consumption and eliminates network debugging.
To Extend: For full home automation integration, replace the HTTP webhook with MQTT. Use the PubSubClient library to publish homeassistant/sensor/panel_branch1/current and voltage topics. This allows Home Assistant to graph the data over time, helping you identify slow-building continuous load violations that a simple threshold alert might miss. For deeper code compliance analysis, add a second CT clamp to the neutral wire to detect neutral-ground bond faults by comparing the delta between line and neutral current.
FAQ: Common Wiring Electrical Code Questions
Does the wiring electrical code require GFCI protection for all 15A and 20A receptacles?
Not universally, but the scope expands with every NEC cycle. As of the 2023 NEC (and carried into 2026 adoptions), GFCI protection is required for all 125V through 250V receptacles rated 150A or less in specific locations: bathrooms, garages, outdoors, crawl spaces, unfinished basements, kitchens, boathouses, and laundry areas. The 2023 update specifically expanded this to include all receptacles in these areas, not just those within 6 feet of a water source. Always check your local AHJ, as some municipalities amend the NEC to require whole-house GFCI or AFCI protection.
What is the maximum voltage drop allowed by the wiring electrical code?
The NEC recommends a maximum of 3% voltage drop on the furthest branch circuit, and a combined maximum of 5% for the feeder and branch circuit combined (NEC Article 210.19(A) Informational Note No. 4). However, because this is an 'Informational Note' and not a mandatory article in most jurisdictions, it is technically a recommendation for 'reasonable efficiency' rather than a strict pass/fail inspection criterion—unless your local state or municipality has explicitly adopted it as enforceable law, which states like California and Washington often do.
How do I calculate the 80% continuous load rule for breaker sizing?
A 'continuous load' is defined by the NEC as any load where the maximum current is expected to continue for 3 hours or more (e.g., EV chargers, space heaters, commercial lighting). To size the breaker, multiply the continuous load by 1.25 (or divide the breaker rating by 0.80). For example, if your EV charger draws a continuous 16A, you calculate 16A × 1.25 = 20A. Therefore, you must use a minimum 20A breaker and 12 AWG copper wire. You cannot put a 16A continuous load on a 15A breaker, even though 16A is close to 15A, because 15A × 0.80 = 12A maximum continuous capacity.
Can I mix 12 AWG and 14 AWG wire on a 20A breaker?
No. This is a direct violation of NEC Article 240.4(D) and the ampacity tables in 310.16. A 20A breaker requires a minimum of 12 AWG copper wire for the entire circuit. If you use 14 AWG wire anywhere on that branch circuit (even just a 2-foot jumper to a receptacle), the 14 AWG wire becomes a bottleneck. If a fault occurs, the 14 AWG wire could melt and start a fire inside the wall before the 20A breaker trips. The breaker must always be sized to protect the smallest wire gauge in the circuit.






