When auditing a branch circuit, guessing isn't enough. The house wiring code—specifically the National Electrical Code (NEC) Article 210.19 for branch circuits and 215.2 for feeders—mandates strict voltage drop and continuous load limits. While a standard clamp meter gives you a snapshot, it doesn't calculate real-time voltage drop or flag an 80% continuous load violation on the fly. This guide walks through building an ESP32-based handheld house wiring code auditor that samples AC voltage and current, calculates real-time voltage drop, and flags NEC violations on an OLED screen.

NEC House Wiring Code Limits vs. Real-World Measurements

Before wiring the microcontroller, you need to know the exact thresholds your code will enforce. The NEC provides informational notes on voltage drop and hard rules on continuous loads. Here is the data-dense reference table your ESP32 logic will use to flag violations.

NEC Rule / Metric Code Limit 120V Nominal System 240V Nominal System ESP32 Flag Trigger
Branch Circuit Voltage Drop (210.19 Info Note) 3% Maximum 3.6V Drop 7.2V Drop V_source - V_load > Limit
Feeder + Branch Combined Drop (215.2 Info Note) 5% Maximum 6.0V Drop 12.0V Drop Calculated total system drop
Continuous Load Sizing (210.20) 125% of Load (or 80% of Breaker) 16A on a 20A Breaker 24A on a 30A Breaker I_rms > (Breaker_Rating * 0.8)
THHN Ampacity Derating (310.16) 90°C Column Baseline (30°C Ambient) N/A (Temp Dependent) N/A (Temp Dependent) Requires external temp sensor add-on

Parts List & Pin Mapping

This build targets the ESP32-WROOM-32 DevKit v1 (30-pin variant). Do not use the ESP32-C3 or S3 for this specific code without adjusting the ADC pin definitions, as the WROOM-32's ADC1 and ADC2 mappings are hardcoded below.

Difficulty Rating: Intermediate (Mains voltage exposure during sensor calibration)
Time to Build: 2.5 Hours

Bill of Materials

  • MCU: ESP32-WROOM-32 DevKit v1 (30-pin)
  • Current Sensor: ACS724LLCTR-20AB (±20A, 5V VCC variant) or ACS724LLCTR-05AB (3.3V variant)
  • Voltage Sensor: ZMPT101B AC Voltage Transformer Module
  • Display: 1.3" I2C OLED (SH1106 driver, 128x64)
  • Power: 5V 2A USB-C power supply (for the ESP32 and ACS724)

Pin Mapping Table

Module Module Pin ESP32-WROOM-32 Pin Notes
SH1106 OLED SDA GPIO 21 Default I2C SDA
SH1106 OLED SCL GPIO 22 Default I2C SCL
ACS724 (20AB) OUT GPIO 34 (ADC1_CH6) Requires voltage divider if using 5V sensor on 3.3V ADC
ZMPT101B OUT GPIO 35 (ADC1_CH7) Analog out, center-biased
⚠️ Mains Safety Warning: The ZMPT101B and ACS724 connect directly to line voltage and series loads. De-energize the circuit, lock out the breaker, and verify dead with a tested CAT III multimeter before making physical connections to the sensors. Never touch the screw terminals while the circuit is live.

Assembly & Compilable Auditor Code

Wire the sensors according to the pin table. If you are using the 5V ACS724LLCTR-20AB, you must use a voltage divider (e.g., 10kΩ and 20kΩ) on the OUT pin to step the 2.5V center-point down to ~1.65V so it doesn't saturate the ESP32's 3.3V ADC. Alternatively, buy the 3.3V LCTR-05AB variant.

Install the Adafruit GFX Library and Adafruit SH110X via the Arduino Library Manager. Select "ESP32 Dev Module" as your board in the Arduino IDE.

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>

// --- PIN DEFINITIONS ---
#define PIN_CURRENT_ADC 34
#define PIN_VOLTAGE_ADC 35
#define I2C_SDA 21
#define I2C_SCL 22

// --- NEC THRESHOLDS ---
#define NOMINAL_VOLTAGE 120.0
#define BREAKER_RATING 20.0
#define BRANCH_VD_PERCENT 0.03
#define CONTINUOUS_LOAD_PERCENT 0.80

// --- DISPLAY SETUP ---
Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire, -1);

// --- CALIBRATION CONSTANTS ---
// Adjust these based on your specific sensor calibration
const float VOLTAGE_CAL_FACTOR = 0.295; // Calibrate ZMPT101B
const float CURRENT_CAL_FACTOR = 0.048; // Calibrate ACS724 (mV/A)
const int ADC_CENTER_BIAS = 1890;       // Roughly 1.65V on 12-bit ADC

void setup() {
  Serial.begin(115200);
  
  // Initialize I2C with explicit pins and 400kHz clock
  Wire.begin(I2C_SDA, I2C_SCL, 400000);
  
  // Error Handling: Check OLED allocation
  if(!display.begin(0x3C, true)) {
    Serial.println(F("SH1106 allocation failed"));
    // Blink onboard LED to indicate fatal I2C error
    pinMode(2, OUTPUT);
    while(1) { digitalWrite(2, !digitalRead(2)); delay(250); }
  }
  
  display.clearDisplay();
  display.setTextColor(SH110X_WHITE);
  display.setTextSize(1);
  display.setCursor(0,0);
  display.println("NEC Auditor Ready");
  display.display();
  delay(1000);
  
  analogReadResolution(12); // Set ESP32 ADC to 12-bit
  analogSetAttenuation(ADC_11db); // Full scale ~3.3V
}

void loop() {
  float sumV = 0, sumI = 0;
  const int samples = 500; // Sample over several AC cycles
  
  // Sample AC Waveform
  for(int i=0; i<samples; i++) {
    int rawV = analogRead(PIN_VOLTAGE_ADC);
    int rawI = analogRead(PIN_CURRENT_ADC);
    
    float vDiff = (rawV - ADC_CENTER_BIAS) * VOLTAGE_CAL_FACTOR;
    float iDiff = (rawI - ADC_CENTER_BIAS) * CURRENT_CAL_FACTOR;
    
    sumV += vDiff * vDiff;
    sumI += iDiff * iDiff;
    delayMicroseconds(200); // Space samples across the sine wave
  }
  
  float rmsVoltage = sqrt(sumV / samples);
  float rmsCurrent = sqrt(sumI / samples);
  
  // Calculate NEC Violations
  float voltageDrop = NOMINAL_VOLTAGE - rmsVoltage;
  float vdPercent = voltageDrop / NOMINAL_VOLTAGE;
  float maxContinuousCurrent = BREAKER_RATING * CONTINUOUS_LOAD_PERCENT;
  
  bool vdViolation = (vdPercent > BRANCH_VD_PERCENT);
  bool loadViolation = (rmsCurrent > maxContinuousCurrent);
  
  // Update OLED
  display.clearDisplay();
  display.setCursor(0,0);
  display.printf("V: %5.1fV  I: %4.1fA\n", rmsVoltage, rmsCurrent);
  display.printf("VD: %3.1f%% / 3.0%%\n", vdPercent * 100.0);
  display.printf("Load: %3.1fA / %3.1fA\n", rmsCurrent, maxContinuousCurrent);
  
  display.setCursor(0, 48);
  if(vdViolation || loadViolation) {
    display.setTextColor(SH110X_WHITE, SH110X_BLACK);
    display.print("!!! NEC VIOLATION !!!");
  } else {
    display.print("Status: COMPLIANT");
  }
  
  display.display();
  delay(500);
}

Debugging: I2C Timeouts and Sensor Saturation

When working with the ESP32's ADC and I2C bus on a noisy workbench, you will likely encounter bus lockups. The most common fatal error string in the Serial Monitor is:

[E][Wire.cpp:498] requestFrom(): i2cWriteReadNonStop returned Error 261

This indicates the I2C bus is stuck or the OLED failed to initialize. Here are the first three things to check when it fails:

  1. I2C Pull-ups and Address: The ESP32's internal pull-ups are often too weak for long jumper wires. Add external 4.7kΩ pull-up resistors to SDA and SCL. Also, verify your OLED address; some SH1106 boards ship at 0x3D instead of 0x3C.
  2. ACS724 VCC Levels: If your current readings are pegged at 0A or maxing out randomly, check the sensor's VCC. The -20AB variant requires exactly 5.0V. If you are powering it from the ESP32's 3V3 pin, it will brown out and output garbage. Power it from the VIN pin (assuming 5V USB input).
  3. ZMPT101B Trimpot Calibration: If voltage reads 0.0V, the module's onboard operational amplifier is likely saturated. Turn the blue trimpot on the ZMPT101B while monitoring the raw ADC value in Serial. Adjust it until the raw ADC reads ~1890 when the AC input is disconnected (0V).

Extending and Simplifying the Build

This base auditor is highly effective for spot-checking receptacles, but you can adapt it to your specific workflow.

How to Extend the Build

To turn this into a permanent panel monitor, add an MQTT publish loop. By including the PubSubClient library, you can push the rmsCurrent and voltageDrop variables to a Home Assistant broker every 60 seconds. This allows you to graph continuous load trends over 24 hours, proving whether a circuit truly violates the 80% house wiring code rule over a 3-hour continuous period (the NEC definition of a continuous load).

How to Simplify the Build

If you only care about thermal breaker tripping and don't need precise voltage drop calculations, drop the ZMPT101B voltage sensor entirely. Hardcode NOMINAL_VOLTAGE to 120.0V or 240.0V using a simple SPDT toggle switch wired to GPIO 25. This frees up an ADC pin, eliminates the AC isolation risk on the bench, and reduces the code footprint by removing the voltage RMS calculation loop.

For deeper reading on the standards enforced by this tool, refer to the NFPA National Electrical Code documentation and the Espressif ESP32 Technical Reference Manual for ADC non-linearity workarounds.