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.
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 |
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:
- 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
0x3Dinstead of0x3C. - ACS724 VCC Levels: If your current readings are pegged at 0A or maxing out randomly, check the sensor's VCC. The
-20ABvariant 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 theVINpin (assuming 5V USB input). - 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.






