Homeowners and facility managers frequently violate National Electrical Code (NEC) rules regarding breaker panels without realizing it. Stacking cardboard boxes in front of a subpanel, using the interior of a commercial enclosure as a storage shelf, or allowing ambient temperatures to exceed derating thresholds are common infractions. When the Authority Having Jurisdiction (AHJ) or an insurance inspector walks through, these violations result in failed inspections or voided coverage.

Rather than relying on manual walkthroughs, we can build an embedded sensor node that continuously audits electrical panel code requirements. By combining an ESP32 microcontroller with a Time-of-Flight (ToF) distance sensor and an environmental sensor, you can monitor NEC 110.26 working space clearances, detect internal storage violations, and log temperature data to ensure your wire ampacity derating columns remain valid.

MAINS VOLTAGE WARNING: This project involves mounting sensors near or on an electrical panel. Do NOT open a live panel, remove the dead-front, or drill into an energized enclosure unless you are a qualified electrician using appropriate PPE and lockout/tagout procedures. For this build, all sensors are designed to be mounted on the exterior of the panel enclosure or the dead-front cover to avoid arc flash hazards and maintain the panel's UL listing.

Mapping NEC Rules to Sensor Thresholds

To build a useful compliance monitor, we must translate abstract legal code into concrete, measurable engineering thresholds. The table below maps the most frequently cited NFPA 70 (NEC) panel requirements to the specific metrics our ESP32 node will track.

NEC Article Code Requirement Sensor Used Violation Trigger Threshold
110.26(A)(1) Working space depth (min. 36" for 0-150V to ground) VL53L0X ToF (Exterior) Distance < 36.0 inches for > 5 continuous minutes
110.14(B) / 310.15 Termination & conductor temperature limits (derating) BME280 (Exterior/Interior) Ambient temp > 104°F (40°C) baseline
312.1 Enclosures not to be used for storage Reed Switch + Internal ToF Door open AND internal object detected < 6.0 inches
110.12 Mechanical execution (unused openings sealed) Ambient Light Sensor (ALS) Light leak detected when panel door is closed

By polling these sensors every 60 seconds, the ESP32 can publish an MQTT payload to your home automation server (like Home Assistant), triggering an alert if a physical object breaches the 36-inch working space or if the panel interior exceeds safe thermal limits.

Hardware BOM and Pin Mapping

This build targets the ESP32-WROOM-32 (30-pin DevKit V1). We use the 30-pin variant because it exposes both GPIO21 and GPIO22 cleanly for the default I2C bus without requiring custom pin remapping in the Wire library.

Parts List

  • Microcontroller: ESP32-WROOM-32 DevKit V1 (30-pin) — Ensure it's the 30-pin, not the 38-pin variant, as pinouts differ.
  • Distance Sensor: Adafruit VL53L0X Time-of-Flight Breakout (Product ID: 3317)
  • Environmental Sensor: Adafruit BME280 I2C/SPI Breakout (Product ID: 2652)
  • Door Sensor: 5V/24V Magnetic Reed Switch (Normally Open)
  • Power: 5V 2A USB-C power supply
  • Enclosure: 3D-printed PETG housing (PETG is required over PLA for thermal stability near electrical equipment)

Pin Mapping Table

Component Pin Label ESP32 DevKit V1 GPIO Notes
VL53L0X VIN 3V3 Do NOT use 5V; the sensor logic is strictly 3.3V.
VL53L0X GND GND Common ground plane.
VL53L0X SDA / SCL GPIO 21 / GPIO 22 Default I2C bus. Breakout includes 10k pull-ups.
BME280 VIN / GND 3V3 / GND Shares power rail with VL53L0X.
BME280 SDA / SCL GPIO 21 / GPIO 22 Daisy-chained on the same I2C bus.
Reed Switch Signal GPIO 34 Input-only pin. Uses internal pull-up.
Reed Switch Ground GND Switch closes to ground when magnet is near.

Wiring and Installation Steps

  1. Prep the I2C Bus: Solder the included header pins to both the VL53L0X and BME280 breakouts. Connect the SDA and SCL pins of both sensors in parallel to GPIO 21 and GPIO 22 on the ESP32. Tip: The Adafruit breakouts have onboard 10k pull-up resistors. If you were using raw modules, you would need to add external pull-ups to the 3.3V rail.
  2. Wire the Reed Switch: Connect one lead of the magnetic reed switch to GPIO 34, and the other to GND. We will enable the ESP32's internal pull-up resistor in software, so no external resistor is needed.
  3. Mount the Exterior ToF Sensor: Using double-sided VHB tape, mount the VL53L0X sensor on the outside of the panel door, facing outward into the room. Ensure the field of view (25 degrees) is unobstructed by the panel handle or hinges. This measures the 36-inch NEC 110.26 clearance.
  4. Mount the Environmental Sensor: If you are qualified to open the panel, mount the BME280 inside the wiring gutter (the empty space on the sides of the breakers) using a 3D-printed clip. If not, mount it on the exterior dead-front; ambient room temperature is still a valid proxy for general derating audits.
  5. Align the Magnet: Mount the reed switch on the panel frame and the corresponding rare-earth magnet on the swinging door. Test with a multimeter in continuity mode to ensure the circuit closes when the door is shut.

Complete ESP32 Firmware and Error Handling

The following C++ code is written for the Arduino IDE. It requires the Adafruit_VL53L0X and Adafruit_BME280 libraries, which you can install via the Library Manager. The code includes explicit I2C initialization checks to prevent silent failures and uses a non-blocking timer to avoid triggering the ESP32's watchdog timer.

#include <Wire.h>
#include <Adafruit_VL53L0X.h>
#include <Adafruit_BME280.h>

// --- Pin Definitions ---
#define I2C_SDA 21
#define I2C_SCL 22
#define REED_SWITCH_PIN 34

// --- Sensor Objects ---
Adafruit_VL53L0X lox = Adafruit_VL53L0X();
Adafruit_BME280 bme;

// --- State Variables ---
unsigned long lastReadTime = 0;
const unsigned long READ_INTERVAL = 5000; // 5 seconds
bool doorIsClosed = true;

void setup() {
  Serial.begin(115200);
  while (!Serial) { delay(10); }
  Serial.println(F("Electrical Panel Code Monitor Booting..."));

  // Initialize I2C with explicit pins
  Wire.begin(I2C_SDA, I2C_SCL);

  // Initialize Reed Switch (Input with internal pull-up)
  pinMode(REED_SWITCH_PIN, INPUT_PULLUP);

  // Initialize VL53L0X ToF Sensor
  if (!lox.begin()) {
    Serial.println(F("ERROR: Failed to boot VL53L0X! Check wiring or I2C address."));
    while (1) { delay(1000); } // Halt execution on critical sensor failure
  }
  Serial.println(F("VL53L0X ToF Sensor OK."));

  // Initialize BME280 Environmental Sensor
  if (!bme.begin(0x77, &Wire)) { // Default I2C addr is 0x77 for Adafruit, 0x76 for generic
    Serial.println(F("ERROR: Could not find a valid BME280 sensor, check wiring!"));
    while (1) { delay(1000); }
  }
  Serial.println(F("BME280 Sensor OK."));
}

void loop() {
  // Read door state (LOW means magnet is near, circuit closed)
  doorIsClosed = (digitalRead(REED_SWITCH_PIN) == LOW);

  // Non-blocking sensor read
  if (millis() - lastReadTime >= READ_INTERVAL) {
    lastReadTime = millis();

    // 1. Read Working Space Clearance (ToF)
    VL53L0X_RangingMeasurementData_t measure;
    lox.rangingTest(&measure, false);
    
    float clearance_inches = 0.0;
    bool space_violated = false;
    
    if (measure.RangeStatus != 4) {  // 4 indicates out of range or error
      float clearance_mm = measure.RangeMilliMeter;
      clearance_inches = clearance_mm / 25.4;
      if (clearance_inches < 36.0) {
        space_violated = true;
      }
    }

    // 2. Read Ambient Temperature
    float temp_c = bme.readTemperature();
    float temp_f = (temp_c * 9.0 / 5.0) + 32.0;
    bool thermal_violated = (temp_f > 104.0); // NEC 310.15 baseline limit

    // 3. Output Telemetry
    Serial.print(F("Door: ")); Serial.print(doorIsClosed ? F("Closed") : F("Open"));
    Serial.print(F(" | Clearance: ")); Serial.print(clearance_inches, 1); Serial.print(F(" in"));
    Serial.print(F(" [")); Serial.print(space_violated ? F("FAIL 110.26") : F("PASS")); Serial.print(F("]"));
    Serial.print(F(" | Temp: ")); Serial.print(temp_f, 1); Serial.print(F(" F"));
    Serial.print(F(" [")); Serial.print(thermal_violated ? F("FAIL Derating") : F("PASS")); Serial.println(F("]"));
  }
}

Debugging: I2C Boot Failures and Sensor Errors

When working with I2C sensors in industrial or garage environments, electrical noise and wiring mistakes are common. If your serial monitor outputs the exact error string: ERROR: Failed to boot VL53L0X! Check wiring or I2C address., do not immediately assume the sensor is dead.

The First 3 Things to Check When I2C Fails:
  1. Voltage Mismatch: Verify you wired the VL53L0X VIN pin to the ESP32's 3V3 pin, not the 5V pin. Feeding 5V into the Adafruit breakout's onboard regulator is usually fine, but feeding 5V directly into a raw module's VCC will instantly fry the 2.8V logic core.
  2. Missing Pull-Up Resistors: I2C requires pull-up resistors on both SDA and SCL lines. If you are using generic, unbranded VL53L0X boards from bulk marketplaces, they often omit these resistors to save $0.02. Measure the resistance between SDA and 3.3V with a multimeter; it should read around 4.7kΩ to 10kΩ. If it reads infinite (OL), add external 10kΩ resistors.
  3. Swapped SDA/SCL Lines: It is remarkably easy to cross GPIO 21 and GPIO 22. Run an I2C scanner sketch (available in the Arduino IDE examples under Wire > I2CScanner) to verify the ESP32 actually sees devices at addresses 0x29 (VL53L0X) and 0x77 (BME280).

Another common issue is RangeStatus 4 in the serial output. This is not a hardware failure; it is a datasheet-specific status code from STMicroelectronics indicating the sensor's phase limit was exceeded or the target is out of its maximum 2-meter range. If your panel faces a wall further than 6 feet away, the sensor will naturally return Status 4, which the code correctly interprets as "no obstruction" (a passing grade for NEC 110.26).

Extending and Simplifying the Build

Depending on your specific AHJ requirements or home automation setup, you may want to scale this project up or strip it down.

How to Extend (Add MQTT and Home Assistant)

To integrate this into a smart home dashboard, add the PubSubClient library. In the loop(), format the telemetry into a JSON string and publish it to an MQTT topic like homeassistant/sensor/panel_compliance/state. You can then create Home Assistant automations that send a push notification to your phone if space_violated remains true for more than 5 minutes, effectively giving you a "panel blocked" alert.

How to Simplify (The Budget Reed-Switch Only Build)

If you only care about NEC 312.1 (preventing the panel from being left open or used as a shelf) and don't need working-space clearance monitoring, drop the VL53L0X and BME280 entirely. Wire a simple magnetic reed switch to GPIO 34. Modify the code to trigger a buzzer or WiFi alert whenever doorIsClosed == false for more than 60 seconds. This reduces the BOM cost from ~$25 to under $4 and eliminates I2C debugging entirely, making it an ideal project for beginners learning basic ESP32 GPIO interrupts.