To monitor a branch circuit for code compliance without guessing, use an ESP32 DevKit V1 (ESP32-WROOM-32, 30-pin variant) paired with a SCT-013-030 current transformer and a ZMPT101B voltage sensor. This embedded setup calculates real-time RMS current and voltage, alerting you immediately if a load violates the NEC Article 210.20(A) 80% continuous load rule (e.g., drawing >16A on a 20A breaker for 3+ hours) or exceeds the 3% voltage drop recommendation outlined in NEC Article 215.2.

While the NFPA 70 (National Electrical Code) is adopted and enforced by local Authorities Having Jurisdiction (AHJs), the math behind ampacity derating and continuous load limits is universal. This project bridges the gap between low-voltage embedded systems and high-voltage electrical wiring code, giving makers and apprentice electricians a bench tool to verify circuit behavior before closing up a panel.

Project Specs & Bill of Materials

Difficulty: Intermediate (Mains voltage exposure required) | Time: 2.5 Hours | Cost: ~$28 USD

Do not substitute the SCT-013-000 for the -030 variant listed below. The -000 requires an external burden resistor and biasing network, whereas the -030 has a built-in burden resistor and outputs a 0-1V analog signal directly, saving you an hour of bench math and reducing noise.

Component Exact Variant / Part Number Role in Circuit Approx. Cost
Microcontroller ESP32 DevKit V1 (30-pin, ESP32-WROOM-32) ADC sampling, logic, WiFi telemetry $7.50
Current Sensor SCT-013-030 (30A max, 1V output) Non-invasive AC current measurement $6.00
Voltage Sensor ZMPT101B Module (AC 250V max) Isolated AC voltage step-down & rectification $4.50
Display 128x64 I2C OLED (SSD1306, 0x3C address) Real-time Vrms, Irms, and Code Status $5.00
Power Supply 5V 2A USB-C Wall Adapter Powers ESP32 and OLED $5.00
⚠️ MAINS VOLTAGE SAFETY WARNING: This project requires interacting with 120V/240V AC branch circuits. Before clamping the SCT-013 or wiring the ZMPT101B, you must de-energize the circuit at the breaker panel, apply a Lock-Out/Tag-Out (LOTO) device, and verify the circuit is dead using a known-working CAT III or CAT IV multimeter. Never work on live panel busbars. Local electrical wiring code may require a licensed electrician for permanent in-panel installations.

Hardware Wiring & Pin Mapping

A critical mistake when wiring analog sensors to the ESP32 is using ADC2 pins (GPIO 4, 12-15, 25-27). ADC2 is shared with the WiFi radio; if WiFi is active, ADC2 reads will fail or return garbage. We strictly use ADC1 pins for this build. Furthermore, the ESP32-WROOM-32 datasheet notes that GPIO 34, 35, 36, and 39 are input-only and lack internal pull-ups, making them perfect for raw analog sensor inputs.

Sensor / Module Sensor Pin ESP32 GPIO Notes & Constraints
SCT-013-030 White (Signal) GPIO 34 (ADC1_CH6) 3.5mm jack sleeve to GND, tip to GPIO 34
SCT-013-030 Red/Black (GND) GND Shared ground with ESP32
ZMPT101B Vout GPIO 35 (ADC1_CH7) Requires trimpot calibration on bench
ZMPT101B VCC 3V3 Do NOT feed 5V to this module's VCC
ZMPT101B GND GND Shared ground
SSD1306 OLED SDA GPIO 21 Default I2C SDA for ESP32
SSD1306 OLED SCL GPIO 22 Default I2C SCL for ESP32

Firmware: Calculating Ampacity & Voltage Drop

The firmware below uses the OpenEnergyMonitor EmonLib to handle the complex RMS math and phase-shift calibration. The code targets the Arduino IDE (ESP32 Core v2.0.14 or newer). It continuously samples the waveform, calculates the RMS values, and checks the load against a hardcoded 20A breaker limit (16A continuous threshold).

Bench Tip: The ZMPT101B module has a blue trimpot. Before uploading this code, connect the ZMPT101B to your mains via a plug, open the Serial Monitor, and turn the trimpot until the raw analog read (`analogRead(35)`) centers exactly around 1800-2000 (midpoint of the 12-bit ADC at 3.3V). If it's pegged at 0 or 4095, your calibration constant will fail.
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <EmonLib.h>

// --- PIN DEFINITIONS ---
#define PIN_CURRENT 34  // ADC1_CH6 (Input only)
#define PIN_VOLTAGE 35  // ADC1_CH7 (Input only)
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C

// --- NEC COMPLIANCE THRESHOLDS ---
const float BREAKER_RATING = 20.0;    // 20A Breaker
const float CONTINUOUS_LIMIT = BREAKER_RATING * 0.80; // NEC 210.20(A) 80% Rule = 16A
const float NOMINAL_VOLTAGE = 120.0;
const float MAX_VOLTAGE_DROP = NOMINAL_VOLTAGE * 0.03; // NEC 215.2 3% Drop = 3.6V

// --- CALIBRATION CONSTANTS ---
// Adjust these based on your multimeter readings
const float CURRENT_CAL = 29.5; 
const float VOLTAGE_CAL = 145.2; 

EnergyMonitor emon1;
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

bool codeViolation = false;

void setup() {
  Serial.begin(115200);
  delay(500);

  // I2C OLED Error Handling
  if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println(F("[ERROR] SSD1306 allocation failed. Check I2C wiring."));
    pinMode(2, OUTPUT);
    while(true) { digitalWrite(2, !digitalRead(2)); delay(100); } // Blink onboard LED
  }
  
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.println("Initializing Sensors...");
  display.display();

  // Initialize EmonLib
  emon1.current(PIN_CURRENT, CURRENT_CAL);
  emon1.voltage(PIN_VOLTAGE, VOLTAGE_CAL, 1.7); // 1.7 = phase shift calibration
  
  Serial.println("System Ready. Monitoring for NEC violations.");
}

void loop() {
  // Calculate RMS over 1480 milliseconds (approx 90 cycles at 60Hz)
  emon1.calcVI(20, 2000); 
  
  float realPower   = emon1.realPower;
  float apparentPower = emon1.apparentPower;
  float Vrms        = emon1.Vrms;
  float Irms        = emon1.Irms;
  float powerFactor = emon1.powerFactor;
  
  // --- NEC CODE COMPLIANCE CHECKS ---
  codeViolation = false;
  String violationMsg = "CODE COMPLIANT";
  
  // Check 1: Continuous Load (80% Rule)
  if (Irms > CONTINUOUS_LIMIT) {
    codeViolation = true;
    violationMsg = "VIOLATION: >80% LOAD";
    Serial.printf("[ALERT] Continuous load violation: %.2fA exceeds 16A limit.\n", Irms);
  }
  
  // Check 2: Voltage Drop (3% Rule)
  if ((NOMINAL_VOLTAGE - Vrms) > MAX_VOLTAGE_DROP && Irms > 1.0) {
    codeViolation = true;
    violationMsg = "VIOLATION: V-DROP >3%";
    Serial.printf("[ALERT] Voltage drop violation: %.2fV drop at %.2fA.\n", (NOMINAL_VOLTAGE - Vrms), Irms);
  }

  // --- SERIAL OUTPUT ---
  Serial.printf("Vrms: %.1fV | Irms: %.2fA | PF: %.2f | Status: %s\n", Vrms, Irms, powerFactor, violationMsg.c_str());

  // --- OLED RENDERING ---
  display.clearDisplay();
  display.setCursor(0,0);
  display.setTextSize(1);
  display.printf("V: %.1f  A: %.2f", Vrms, Irms);
  display.setCursor(0, 12);
  display.printf("W: %.0f  PF: %.2f", realPower, powerFactor);
  
  display.setCursor(0, 30);
  if (codeViolation) {
    display.setTextSize(2);
    display.println("WARNING");
    display.setTextSize(1);
    display.println(violationMsg);
  } else {
    display.setTextSize(1);
    display.println("STATUS: COMPLIANT");
    display.printf("Limit: %.1fA / %.1fV", CONTINUOUS_LIMIT, NOMINAL_VOLTAGE - MAX_VOLTAGE_DROP);
  }
  
  display.display();
  delay(1000);
}

Debugging: Sensor Failures & Calibration Errors

When working with AC waveforms on microcontrollers, the ADC noise floor and phase-shift miscalculations are the primary culprits for bad data. If your build fails, follow this diagnostic tree.

Symptom: Serial prints [EmonLib] Vrms read: 0.00V or Irms reads 0.15A with no load

This exact error string (or a zeroed-out reading) indicates the ADC is either clipping, floating, or reading the noise floor. Here are the first three things to check:

  1. ZMPT101B Trimpot Saturation: Measure the DC voltage between the ZMPT101B Vout and GND pins with a multimeter. It must read exactly 1.65V DC (half of 3.3V). If it reads 0.1V or 3.2V, adjust the blue trimpot. If the op-amp on the module is saturated, EmonLib cannot detect the AC sine wave crossing.
  2. ADC Pin Conflict: Verify you are using GPIO 34 and 35. If you accidentally wired to GPIO 25 or 26 (ADC2), the ESP32's WiFi stack will periodically hijack the ADC, resulting in intermittent zero-reads or Guru Meditation Error: Core 1 panic'ed (Interrupt wdt timeout) crashes.
  3. SCT-013 Ground Loop: The SCT-013-030 outputs a 0-1V AC signal centered around 0V, but the ESP32 ADC cannot read negative voltages. The internal biasing in EmonLib expects a DC offset. If your Irms reads 0.15A with the clamp open, you have a ground loop or USB noise issue. Add a 10µF electrolytic capacitor between the ESP32 3V3 and GND pins to smooth the reference rail.

Extending the Build for Multi-Wire Branch Circuits (MWBC)

To simplify this build for a basic smart plug, strip out the OLED and ZMPT101B, assume a fixed 120V, and use ESP-NOW to transmit the Irms payload to a central gateway every 5 seconds. This drops the BOM cost to under $14 and reduces power consumption.

To extend the build for advanced panel diagnostics, add a second SCT-013-030 to monitor a Multi-Wire Branch Circuit (MWBC). In an MWBC, two 120V hot legs share a single neutral. According to electrical wiring code principles, the neutral only carries the unbalanced current. By clamping CT1 on Hot A, CT2 on Hot B, and CT3 on the Shared Neutral, you can use the ESP32 to calculate the vector sum. If the neutral current exceeds the difference between Hot A and Hot B, you have a code violation (likely a crossed neutral or a shared neutral on non-simultaneous breakers, violating NEC 210.4).

FAQ: Electrical Wiring Code Questions for Smart Panels

Does the NEC electrical wiring code allow smart relays on 15A breakers?

Yes, but the smart relay itself must be rated for the load and the breaker. If you install a 10A-rated smart relay (like a Sonoff POW Elite 16A) on a 15A breaker, the electrical wiring code requires the breaker to be sized to protect the weakest component in the circuit. Therefore, a 15A breaker protecting a 10A relay is a violation; you must down-breaker to 10A (if available) or use a relay rated for at least 15A (or 20A to satisfy continuous load rules).

How does electrical wiring code define a continuous load for IoT monitors?

NEC Article 100 defines a continuous load as one where the maximum current is expected to continue for 3 hours or more. For an IoT monitor, this means your firmware shouldn't trigger an 80% alarm for a 17A spike that lasts 4 minutes (like a microwave or table saw). You must implement a rolling time-average in your code. Only flag a code violation if the rolling 180-minute average exceeds 80% of the breaker rating.

What is the electrical wiring code limit for voltage drop on branch circuits?

NEC Article 215.2 contains an Informational Note (which is technically a recommendation, not a strictly enforceable rule in all jurisdictions, but considered best practice) stating that branch circuit voltage drop should not exceed 3%, and the combined feeder + branch circuit drop should not exceed 5%. On a nominal 120V circuit, a 3% drop is 3.6V. If your ESP32 monitor reads 115V at the outlet while drawing 15A, your voltage drop is 5V (4.1%), indicating undersized wire (e.g., 14 AWG run too far) or loose terminations causing high resistance.