Why Monitor Electrical Panel Requirements Code Compliance?
The National Electrical Code (NEC) Article 110.26 mandates strict working space requirements around electrical panels to ensure safe maintenance and emergency shutoffs. The core electrical panel requirements code dictates a minimum clear working space depth of 36 inches (914 mm), a width of 30 inches (762 mm) or the width of the equipment (whichever is greater), and a headroom of 6.5 feet (2.0 m). Additionally, the space must be adequately illuminated—typically interpreted by local Authorities Having Jurisdiction (AHJ) as a minimum of 50 lux (5 foot-candles) at the panel face.
In commercial facilities, large basements, or busy garages, these clearances are frequently violated by stored inventory, and panel lighting often fails unnoticed. Rather than relying on manual annual inspections, we can build an embedded compliance monitor. This project uses an ESP32 to continuously measure the 36-inch clearance via Time-of-Flight (ToF) sensors, verify ambient illumination, and track enclosure temperature, pushing MQTT alerts the moment a code violation occurs.
NEC 110.26 Compliance Thresholds & Sensor Mapping
Before wiring the bench, we must map the legal code requirements to concrete sensor thresholds. The table below defines the exact parameters the ESP32 will evaluate.
| NEC 110.26 Parameter | Code Minimum | Sensor Module | Alert Threshold (Trigger) |
|---|---|---|---|
| Working Space Depth | 36 inches (914 mm) | VL53L0X ToF | < 34 inches (863 mm) |
| Illumination Level | 50 lux (5 fc) nominal | BH1750FVI | < 45 lux for > 5 mins |
| Ambient Temperature | < 40°C (104°F) typical | BME280 | > 45°C (113°F) |
| Headroom Clearance | 6.5 ft (2.0 m) | Secondary ToF (Optional) | < 76 inches (1.93 m) |
Hardware Spec Sheet & Pin Mapping
To ensure reliable I2C communication and accurate readings, we are using specific, high-quality breakout boards. Generic unbranded clones often lack the necessary pull-up resistors or use counterfeit sensor ICs that fail under continuous polling.
Parts List
- Microcontroller: ESP32-WROOM-32 DevKit V1 (30-pin variant, 3.3V logic)
- Distance Sensor: Pololu VL53L0X Time-of-Flight Carrier (Item #2492) with onboard voltage regulator and pull-ups
- Light Sensor: GY-302 BH1750FVI Digital Ambient Light Sensor (I2C address 0x23)
- Environmental Sensor: Adafruit BME280 I2C/SPI Breakout (Product ID: 2652)
- Enclosure: Bud Industries NBF-32014 NEMA 1 Polycarbonate Enclosure (to mount adjacent to panel)
- Wiring: 22 AWG stranded silicone wire, 4.7kΩ through-hole resistors (for I2C pull-ups if using generic sensors)
ESP32 Pin Mapping Table
All three sensors share the same I2C bus. Because the ESP32's internal pull-ups are weak (~45kΩ) and the bus capacitance increases with multiple modules, we rely on the breakouts' onboard 10kΩ pull-ups or add external 4.7kΩ resistors to the 3V3 line.
| ESP32 DevKit V1 Pin | GPIO Number | Target Sensor Pin | Function / Notes |
|---|---|---|---|
| 3V3 | N/A | VIN / VCC (All Sensors) | 3.3V Power Rail |
| GND | N/A | GND (All Sensors) | Common Ground |
| D21 | GPIO 21 | SDA (All Sensors) | I2C Data Line (Add 4.7kΩ pull-up to 3V3) |
| D22 | GPIO 22 | SCL (All Sensors) | I2C Clock Line (Add 4.7kΩ pull-up to 3V3) |
Assembly and I2C Bus Wiring Steps
Follow these numbered steps to assemble the sensor node. Proper wire prep and bus management are critical to prevent the I2C lockups we will address in the debugging section.
- Prep the Enclosure: Drill a 1-inch hole in the bottom of the NEMA enclosure for cable glands. Mount the ESP32 and sensor breakouts on the internal DIN rail or standoffs using M2.5 nylon screws.
- Wire the Power Rail: Connect the 3V3 output of the ESP32 to a common terminal block. Do the same for GND. Never daisy-chain power through the sensor breakout pins; a voltage drop across the first sensor will starve the last one.
- Route the I2C Lines: Cut 22 AWG silicone wire for SDA and SCL. Keep I2C trace lengths under 30 cm (12 inches) to minimize bus capacitance. If you must run longer wires to position the ToF sensor at the edge of the 36-inch zone, use a twisted-pair CAT6 cable and terminate it with an I2C bus extender (like the PCA9615).
- Install Pull-Up Resistors: If your BH1750 breakout lacks onboard pull-ups, solder a 4.7kΩ resistor between SDA and 3V3, and another between SCL and 3V3 at the ESP32 header.
- Mount the ToF Sensor: Position the VL53L0X at the top edge of the panel face, pointing straight out into the room. Ensure the 90-degree field of view is not obstructed by the panel door hinges.
- Verify Before Powering: Use a multimeter in continuity mode to verify there are no shorts between 3V3 and GND, or between SDA and SCL.
Complete ESP32 Compliance Monitor Code
The following C++ code is written for the Arduino IDE 2.x targeting the ESP32 DevKit V1 board definition (Espressif Systems ESP32 Arduino Core v2.0.14 or later). It utilizes non-blocking timing, explicit I2C error handling, and MQTT publishing.
Required Libraries (install via Arduino Library Manager): Adafruit_VL53L0X, BH1750, Adafruit_BME280, PubSubClient.
/*
* NEC 110.26 Panel Compliance Monitor
* Target Board: ESP32 DevKit V1 (ESP32-WROOM-32)
* Core Version: Espressif ESP32 Arduino Core v2.0.14+
*/
#include
#include
#include
#include
#include
#include
// --- Network & MQTT Config ---
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* mqtt_server = "192.168.1.50";
const int mqtt_port = 1883;
// --- I2C Pin Definitions ---
#define I2C_SDA 21
#define I2C_SCL 22
#define I2C_FREQ 100000 // 100kHz for stability with multiple devices
// --- Thresholds based on NEC 110.26 ---
const int CLEARANCE_LIMIT_MM = 863; // 34 inches (Alert if less than this)
const int LUX_LIMIT = 45; // Alert if less than 45 lux
const float TEMP_LIMIT_C = 45.0; // Alert if greater than 45C
// --- Object Instantiation ---
Adafruit_VL53L0X lox = Adafruit_VL53L0X();
BH1750 lightMeter;
Adafruit_BME280 bme;
WiFiClient espClient;
PubSubClient client(espClient);
unsigned long lastRead = 0;
const unsigned long readInterval = 10000; // 10 seconds
void setup_wifi() {
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi Connected");
}
void reconnect_mqtt() {
while (!client.connected()) {
String clientId = "ESP32-PanelMonitor-" + String(random(0xffff), HEX);
if (client.connect(clientId.c_str())) {
client.publish("panel/status", "online");
} else {
delay(5000);
}
}
}
void setup() {
Serial.begin(115200);
// Explicit I2C initialization with timeout protection
Wire.begin(I2C_SDA, I2C_SCL);
Wire.setClock(I2C_FREQ);
Wire.setTimeout(50); // 50ms timeout to prevent bus lockups
// Sensor Initialization with Error Handling
if (!lox.begin()) {
Serial.println("FATAL: Failed to boot VL53L0X. Check I2C address 0x29.");
while(1); // Halt execution
}
if (!lightMeter.begin(BH1750::CONTINUOUS_HIGH_RES_MODE)) {
Serial.println("FATAL: Failed to boot BH1750. Check I2C address 0x23.");
while(1);
}
if (!bme.begin(0x76)) { // Adafruit BME280 defaults to 0x77, some clones use 0x76
Serial.println("FATAL: Failed to boot BME280. Verify address (0x76 or 0x77).");
while(1);
}
setup_wifi();
client.setServer(mqtt_server, mqtt_port);
}
void loop() {
if (!client.connected()) reconnect_mqtt();
client.loop();
if (millis() - lastRead >= readInterval) {
lastRead = millis();
// 1. Read Clearance (ToF)
VL53L0X_RangingMeasurementData_t measure;
lox.rangingTest(&measure, false);
int distance_mm = measure.RangeMilliMeter;
// 2. Read Illumination
float lux = lightMeter.readLightLevel();
// 3. Read Temperature
float temp_c = bme.readTemperature();
// Evaluate NEC Compliance
String violation = "none";
if (distance_mm < CLEARANCE_LIMIT_MM && distance_mm > 20) {
// >20mm check filters out ToF out-of-range errors
violation = "CLEARANCE_BLOCKED";
} else if (lux < LUX_LIMIT) {
violation = "INSUFFICIENT_LIGHT";
} else if (temp_c > TEMP_LIMIT_C) {
violation = "OVERHEATING";
}
// Publish Telemetry
String payload = "{\"dist_mm\":" + String(distance_mm) +
",\"lux\":" + String(lux) +
",\"temp_c\":" + String(temp_c) +
",\"violation\":\"" + violation + "\"}";
client.publish("panel/compliance/telemetry", payload.c_str());
if (violation != "none") {
client.publish("panel/compliance/alert", violation.c_str());
Serial.printf("ALERT: %s triggered!\n", violation.c_str());
}
}
}
Debugging Common I2C and Sensor Errors
When combining three I2C devices on a single ESP32 bus, timing collisions and electrical noise from nearby AC mains can cause the microcontroller to crash. If your serial monitor outputs errors, follow this decision path.
The First Three Things to Check When It Fails
- Run an I2C Scanner: Upload a standard I2C Scanner sketch. You must see exactly three addresses (typically
0x29,0x23, and0x76/0x77). If you see none, your SDA/SCL wires are swapped or you lack a common ground. - Verify Pull-Up Resistor Strength: Measure the resistance between SDA and 3V3 with the power off. It should read between 2.2kΩ and 4.7kΩ. If it reads >10kΩ, the bus rise time is too slow, causing NACK errors.
- Check for 5V Logic Injection: Ensure you are powering the sensors from the ESP32's 3V3 pin, not the VIN/5V pin. Feeding 5V into the ESP32's GPIO 21/22 will permanently damage the silicon.
Ranked Causes for Exact Error Strings
[E][Wire.cpp:498] requestFrom(): i2cWriteReadNonStop error 2 (NACK)
What it means: The ESP32 sent a clock pulse, but the sensor did not acknowledge (pull SDA low). The bus is physically failing to communicate.
Ranked Causes:
- Missing or weak pull-up resistors: The SDA line isn't returning to HIGH fast enough before the next clock edge. Fix: Add 4.7kΩ external pull-ups.
- I2C Address Conflict: Two sensors are hardcoded to the same address. Fix: Check datasheets; BH1750 and BME280 sometimes share 0x76 if specific pads are bridged.
- Capacitive Load Too High: Wires are too long (>30cm) or routed parallel to 120V AC cables, inducing noise. Fix: Shorten wires or use shielded twisted pair.
Guru Meditation Error: Core 1 panic'ed (Interrupt wdt timeout on CPU1)
What it means: The Watchdog Timer reset the ESP32 because a task (usually the WiFi or I2C interrupt) was blocked for too long.
Ranked Causes:
- I2C Bus Lockup in Loop: The
Wire.requestFrom()function hung indefinitely waiting for a sensor that dropped offline. Fix: EnsureWire.setTimeout(50)is in your setup() as shown in the code above. - Blocking Delays in ISRs: You added a
delay()orSerial.print()inside an interrupt service routine. Fix: Move all logic to the main loop.
Extending and Simplifying the Build
Depending on your facility's needs and budget, you can scale this project up or down.
How to Simplify (Cost & Complexity Reduction)
If you only care about the most frequently cited NEC violation—blocked working space—drop the BME280 and BH1750. Run the ESP32 solely with the VL53L0X ToF sensor. This eliminates I2C address management, reduces code complexity, and allows you to power the entire node via a standard 5V USB wall adapter without worrying about total bus current draw. You can also replace the MQTT stack with ESP RainMaker for out-of-the-box cloud dashboards without hosting a local broker.
How to Extend (Advanced Compliance Tracking)
To build a true facility management tool, add a PIR motion sensor (AM312) and a magnetic reed switch on the panel door. By correlating the ToF distance with door state, you can differentiate between "someone is actively working in front of the panel" (door open, motion detected, clearance temporarily blocked) versus "someone stacked pallets in front of the panel" (door closed, no motion, clearance blocked). Push these distinct states to a Node-RED backend to generate automated work orders for facility managers.
For deeper reading on the legal requirements governing these clearances, refer to the NFPA 70 National Electrical Code documentation, specifically Article 110.26. For ESP32 I2C hardware limitations and bus capacitance calculations, consult the Espressif I2C API Reference.






