Storing boxes, tools, or shop equipment in front of an electrical panel is a common workshop habit—and a direct violation of the National Electrical Code. NEC 110.26(A) mandates a strict working space clearance to ensure electricians have room to safely operate and escape in the event of an arc flash. For standard residential and light commercial panels (0-150V to ground), this means a minimum 36-inch deep and 30-inch wide clear zone.
In this build, we will design an embedded IoT monitoring system that enforces these NEC code electrical panel clearance rules. Using an ESP32 and Time-of-Flight (ToF) sensors, the system continuously measures the depth and width of the space in front of the panel, triggering local alarms and logging violations when the working space is obstructed.
NEC 110.26 Working Space Rules: The Sensor Thresholds
Before wiring the microcontroller, we must define the exact physical thresholds our code will enforce. The required depth of the working space depends on the nominal voltage to ground and the conditions of the surrounding surfaces. For a standard 120/240V split-phase residential panel, we fall under the 0-150V category.
| Nominal Voltage to Ground | Condition 1 (Exposed/Insulated) |
Condition 2 (Exposed/Insulated) |
Condition 3 (Insulated/Insulated) |
|---|---|---|---|
| 0 - 150V | 3 ft (36 in) | 3 ft (36 in) | 3 ft (36 in) |
| 151 - 600V | 3 ft (36 in) | 3.5 ft (42 in) | 4 ft (48 in) |
| 601 - 2500V | 3 ft (36 in) | 4 ft (48 in) | 5 ft (60 in) |
| 2501 - 9000V | 4 ft (48 in) | 5 ft (60 in) | 6 ft (72 in) |
Condition Definitions: Condition 1 means exposed live parts on one side and no live/grounded parts on the other. Condition 3 (most restrictive) means insulated or grounded surfaces on both sides. For our ESP32 firmware, we will hardcode the 36-inch (914 mm) depth threshold and the 30-inch (762 mm) width threshold, which covers all Conditions for standard 120/240V residential panels.
Hardware BOM and Pin Mapping
To measure both depth (distance from panel to obstruction) and width (lateral clearance), we need two independent distance sensors. Standard ultrasonic sensors (HC-SR04) suffer from wide beam angles (15°+), which causes false triggers off the panel door hinges. Instead, we use VL53L0X Time-of-Flight (ToF) lasers, which have a tight 25° cone and millimeter accuracy up to 2 meters.
0x29. To use two on the same bus without an I2C multiplexer, we use the sensor's XSHUT (shutdown) pins to boot them sequentially and assign custom addresses in firmware.
Parts List
- MCU: ESP32-WROOM-32 DevKit V1 (30-pin variant)
- Sensors: 2x Adafruit VL53L0X Time-of-Flight Breakout Boards
- Display: SSD1306 128x64 I2C OLED (0.96 inch)
- Alert: 3.3V/5V Active Piezo Buzzer
- Power: 5V 2A USB-C power supply
Pin Mapping Table
| Component | Sensor/Display Pin | ESP32 GPIO | Notes |
|---|---|---|---|
| Sensor 1 (Depth) | VIN / GND / SDA / SCL | 3V3 / GND / 21 / 22 | I2C Address assigned to 0x30 |
| Sensor 1 (Depth) | XSHUT | GPIO 25 | Boot control pin |
| Sensor 2 (Width) | VIN / GND / SDA / SCL | 3V3 / GND / 21 / 22 | I2C Address assigned to 0x31 |
| Sensor 2 (Width) | XSHUT | GPIO 26 | Boot control pin |
| OLED Display | SDA / SCL | GPIO 21 / 22 | Default I2C Address 0x3C |
| Piezo Buzzer | I/O (+) | GPIO 27 | Use active buzzer, not passive |
Firmware: Compilable ESP32 Code with I2C Error Handling
The following code targets the ESP32-WROOM-32 DevKit V1. It requires the Adafruit_VL53L0X and Adafruit_SSD1306 libraries installed via the Arduino Library Manager. The firmware handles the XSHUT pin toggling to assign unique I2C addresses, polls the sensors every 500ms, and triggers the buzzer if the 36-inch depth or 30-inch width thresholds are violated.
#include <Wire.h>
#include <Adafruit_VL53L0X.h>
#include <Adafruit_SSD1306.h>
// --- PIN DEFINITIONS ---
#define XSHUT_DEPTH 25
#define XSHUT_WIDTH 26
#define BUZZER_PIN 27
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C
// --- NEC THRESHOLDS (in millimeters) ---
// 36 inches = 914.4mm, 30 inches = 762mm
const int DEPTH_THRESHOLD_MM = 914;
const int WIDTH_THRESHOLD_MM = 762;
Adafruit_VL53L0X lox_depth = Adafruit_VL53L0X();
Adafruit_VL53L0X lox_width = Adafruit_VL53L0X();
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
void setup() {
Serial.begin(115200);
Wire.begin(21, 22);
pinMode(XSHUT_DEPTH, OUTPUT);
pinMode(XSHUT_WIDTH, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
digitalWrite(BUZZER_PIN, LOW);
// Initialize OLED
if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;);
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0);
display.println("Booting ToF Sensors...");
display.display();
// --- I2C ADDRESS ASSIGNMENT VIA XSHUT ---
// 1. Hold both sensors in reset
digitalWrite(XSHUT_DEPTH, LOW);
digitalWrite(XSHUT_WIDTH, LOW);
delay(10);
// 2. Boot Depth sensor and assign address 0x30
digitalWrite(XSHUT_DEPTH, HIGH);
delay(10);
if (!lox_depth.begin(0x30)) {
Serial.println("Failed to boot VL53L0X (Depth)");
display.println("ERR: Depth Sensor");
display.display();
while(1);
}
// 3. Boot Width sensor and assign address 0x31
digitalWrite(XSHUT_WIDTH, HIGH);
delay(10);
if (!lox_width.begin(0x31)) {
Serial.println("Failed to boot VL53L0X (Width)");
display.println("ERR: Width Sensor");
display.display();
while(1);
}
Serial.println("Both sensors initialized successfully.");
}
void loop() {
VL53L0X_RangingMeasurementData_t measure_depth;
VL53L0X_RangingMeasurementData_t measure_width;
lox_depth.rangingTest(&measure_depth, false);
lox_width.rangingTest(&measure_width, false);
int depth_mm = measure_depth.RangeMilliMeter;
int width_mm = measure_width.RangeMilliMeter;
bool depth_violation = (depth_mm < DEPTH_THRESHOLD_MM && depth_mm > 20);
bool width_violation = (width_mm < WIDTH_THRESHOLD_MM && width_mm > 20);
bool is_violation = depth_violation || width_violation;
// Trigger Buzzer
digitalWrite(BUZZER_PIN, is_violation ? HIGH : LOW);
// Update OLED
display.clearDisplay();
display.setCursor(0,0);
display.println("NEC 110.26 MONITOR");
display.println("------------------");
display.print("Depth: "); display.print(depth_mm); display.println(" mm");
display.print("Width: "); display.print(width_mm); display.println(" mm");
display.println("------------------");
if (is_violation) {
display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); // Inverted
display.println("!! CLEARANCE BLOCKED !!");
display.setTextColor(SSD1306_WHITE);
} else {
display.println("Status: COMPLIANT");
}
display.display();
// Debug output
Serial.printf("Depth: %dmm | Width: %dmm | Violation: %s\n",
depth_mm, width_mm, is_violation ? "YES" : "NO");
delay(500);
}
Debugging: "Failed to boot VL53L0X" and Sensor Blind Spots
When working with multiple I2C devices on the ESP32, bus contention and initialization sequencing are the primary failure points. If your serial monitor halts and prints the exact error string "Failed to boot VL53L0X", the Adafruit_VL53L0X library failed to receive an acknowledgment (ACK) during the begin() handshake.
First Three Things to Check When It Fails
- XSHUT Pin Logic Levels and Timing: The XSHUT pin is active-low. If your GPIO is floating or the delay between pulling XSHUT HIGH and calling
begin()is under 5ms, the sensor's internal state machine won't have time to boot and lock onto the new I2C address. Ensure yourdelay(10)is present. - I2C Bus Capacitance and Pull-ups: The VL53L0X breakout includes 10k pull-up resistors. However, if your sensor wires running to the panel door exceed 12 inches, parasitic capacitance will degrade the I2C clock edges. Add external 4.7kΩ pull-up resistors to both SDA and SCL lines at the ESP32 end.
- Sensor Line-of-Sight and Crosstalk: If the sensors boot but return erratic
8190mm(out of range) or20mm(false close) readings, check for IR crosstalk. Mounting two 940nm ToF sensors less than 4 inches apart in a confined metal box causes multipath interference. Angle the width sensor slightly outward (5°) to prevent the depth sensor from reading the width sensor's aperture.
Ranked Causes for I2C Timeouts
| Rank | Root Cause | Diagnostic Measurement | Fix |
|---|---|---|---|
| 1 | Address Collision (Both at 0x29) | Run I2C Scanner sketch | Verify XSHUT wiring; ensure Sensor 2 is held LOW while Sensor 1 boots. |
| 2 | Insufficient 3.3V Current | Measure 3V3 pin under load (should be >3.2V) | Power sensors from ESP32 5V pin (if breakout has onboard LDO) or use external 3.3V supply. |
| 3 | SDA/SCL Swapped | Visual trace of GPIO 21/22 | Swap SDA and SCL wires. GPIO 21 is SDA, GPIO 22 is SCL on DevKit V1. |
Extending the Build: MQTT Alerts and Multi-Panel Arrays
The standalone OLED and buzzer build is ideal for a single residential garage panel, but commercial facilities with dozens of subpanels require centralized logging.
How to Extend for Facility Management
To integrate this into a smart building dashboard, add the PubSubClient library to the ESP32 firmware. Connect the ESP32 to local WiFi and publish the depth_mm and width_mm variables to an MQTT broker (like Mosquitto or HiveMQ) every 5 seconds. In Home Assistant or Node-RED, create an automation that sends an email or Slack alert to the facility manager if a violation persists for more than 15 minutes (filtering out temporary obstructions like a person standing there to flip a breaker).
How to Simplify for Basic Compliance
If you only need to prevent large objects (like shelving units or pallets) from blocking the panel, drop the width sensor and the OLED display entirely. Use a single VL53L0X pointed straight down the center of the 36-inch depth zone, and swap the ESP32 for a cheaper ESP8266 NodeMCU. This cuts the BOM cost by 60% and eliminates the I2C address-toggling logic, reducing the firmware to a simple lox.rangingTest() loop.
For further reading on working space requirements, refer to the NFPA National Electrical Code (NEC) guidelines. For sensor wiring specifics, consult the Adafruit VL53L0X wiring guide.






