The National Electrical Code (NEC) is uncompromising about working space around service equipment. Homeowners frequently violate this by stacking cardboard boxes, shelving, or storage bins directly in front of their breaker panels. Electrical panel code clearance, governed by NEC Article 110.26, mandates a strict 36-inch depth, 30-inch width, and 78-inch height clear zone. Violations can result in failed inspections, insurance claim denials, or severe arc flash hazards if a panel cover is removed during a fault.
Rather than relying on visual memory or painted floor tape that fades, we can engineer a permanent, automated compliance monitor. This guide details how to build an ESP32-based Time-of-Flight (ToF) distance monitor that continuously measures the physical clearance in front of your panel and triggers local and MQTT alerts if the 36-inch boundary is breached.
Mounting sensors near an electrical panel involves working inches from exposed mains busbars. De-energize the main breaker, lock/tag it, and verify dead with a tested CAT III/IV multimeter before mounting any hardware. Use non-conductive 3D-printed or wood mounts. Never drill into the panel enclosure itself. NEC-style guidance applies here; your local AHJ has final authority on panel proximity modifications.
NEC 110.26 Working Space Requirements
Before wiring a single sensor, you must understand the exact dimensional thresholds your embedded system needs to enforce. The required depth of the working space depends on the nominal voltage to ground and the conditions of the surrounding environment. For a standard 120/240V residential split-phase panel, you fall into the 0-150V to ground category, requiring a minimum 3-foot (36-inch) depth.
| Nominal Voltage to Ground | Condition 1 (Insulated/Exposed on one side) | Condition 2 (Exposed live parts on both sides) | Condition 3 (Exposed, insulated barriers) |
|---|---|---|---|
| 0 - 150V (Residential 120V) | 3 ft (36 in) | 3 ft (36 in) | 3 ft (36 in) |
| 151 - 600V (Residential 240V/Commercial) | 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) |
| Width Requirement | 30 inches or the width of the equipment, whichever is greater. | ||
| Headroom Requirement | 6.5 ft (78 inches) from the floor to the top of the clearance space. | ||
Source: NFPA 70 National Electrical Code (NEC), Article 110.26(A).
Hardware BOM and Pin Mapping
We are using the ESP32 DevKit V1 (30-pin variant) as the brain. For distance measurement, ultrasonic sensors (like the HC-SR04) are inadequate here; they struggle with soft cardboard boxes and narrow panel gaps. Instead, we use the VL53L1X Time-of-Flight (ToF) laser sensor, which provides millimeter-accurate readings up to 4 meters via I2C. A local SSD1306 OLED provides at-a-glance status, and an active piezo buzzer handles local audio alerts.
Parts List
- MCU: ESP32 DevKit V1 (30-pin, ESP32-WROOM-32 module)
- Sensor: Adafruit VL53L1X Time-of-Flight Breakout (Product ID: 3967)
- Display: 0.96" SSD1306 128x64 I2C OLED
- Audio: 5V Active Piezo Buzzer (continuous tone)
- Wiring: 24 AWG silicone stranded wire (4-conductor for I2C runs)
- Mounting: Nylon M3 standoffs and wood screws (no metal near panel)
Pin Mapping Table
| ESP32 Pin | Component | Function / Notes |
|---|---|---|
| GPIO 21 | VL53L1X / OLED | I2C SDA (Requires 4.7kΩ pull-up to 3.3V) |
| GPIO 22 | VL53L1X / OLED | I2C SCL (Requires 4.7kΩ pull-up to 3.3V) |
| GPIO 25 | VL53L1X XSHUT | Sensor Hardware Shutdown (Active LOW) |
| GPIO 26 | Piezo Buzzer (+) | Alert Tone PWM Output |
| 3V3 | VL53L1X VIN / OLED VCC | Logic and Sensor Power |
| GND | All Components | Common Ground Reference |
Assembly and Calibration Steps
- Mount the Sensor Bracket: Secure a 3D-printed PLA/PETG bracket or a wooden block to the wall or adjacent stud exactly opposite the center of the electrical panel. Ensure the mounting surface is rigid; drywall flex will cause false readings.
- Wire the I2C Bus: Run 24 AWG silicone wire from the ESP32 (mounted safely away from the panel in a standard low-voltage enclosure) to the sensor. Critical: Solder 4.7kΩ pull-up resistors between SDA/VCC and SCL/VCC at the sensor breakout. The VL53L1X internal pull-ups are too weak for runs over 6 inches.
- Configure XSHUT: Wire GPIO 25 to the XSHUT pin. This pin must be driven HIGH to wake the sensor. If left floating, the sensor will remain in hardware standby.
- Establish the Baseline: Power the ESP32 via USB. Open the Serial Monitor. With the 36-inch zone completely clear, note the baseline millimeter reading. For a standard 36-inch clearance, the sensor should read approximately 914mm. Set your alert threshold to 850mm to account for minor wall irregularities and baseboards.
- Verify the Buzzer: Place a piece of cardboard at the 30-inch mark. The ESP32 should trigger GPIO 26 HIGH, sounding the active piezo buzzer, and update the OLED to read "CLEARANCE VIOLATION".
ESP32 Firmware: MQTT Alerts and Local Display
The following firmware targets the ESP32 DevKit V1 (30-pin). It initializes the VL53L1X, reads the distance every 500ms, updates the local OLED, and publishes an MQTT payload if the 850mm threshold is breached. Ensure you install the Adafruit_VL53L1X, Adafruit_SSD1306, and PubSubClient libraries via the Arduino Library Manager before compiling.
#include <Wire.h>
#include <Adafruit_VL53L1X.h>
#include <Adafruit_SSD1306.h>
#include <WiFi.h>
#include <PubSubClient.h>
// --- Pin Definitions for ESP32 DevKit V1 (30-pin) ---
#define SDA_PIN 21
#define SCL_PIN 22
#define XSHUT_PIN 25
#define BUZZER_PIN 26
// --- Clearance Thresholds (in millimeters) ---
#define CLEAR_ZONE_MM 914 // 36 inches
#define ALERT_THRESHOLD_MM 850 // Trigger slightly early for baseboards
// --- Network & MQTT Config ---
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* mqtt_server = "192.168.1.100";
const char* mqtt_topic = "homeassistant/sensor/panel_clearance/state";
WiFiClient espClient;
PubSubClient mqtt(espClient);
Adafruit_VL53L1X vl53 = Adafruit_VL53L1X(XSHUT_PIN, -1);
Adafruit_SSD1306 display(128, 64, &Wire, -1);
bool violationActive = false;
void setup_wifi() {
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) { delay(500); }
mqtt.setServer(mqtt_server, 1883);
}
void setup() {
Serial.begin(115200);
pinMode(BUZZER_PIN, OUTPUT);
digitalWrite(BUZZER_PIN, LOW);
Wire.begin(SDA_PIN, SCL_PIN);
// Initialize OLED
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;);
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
// Initialize ToF Sensor
if (!vl53.begin(0x29, &Wire)) {
Serial.print(F("Error: Failed to find VL53L1X sensor"));
display.clearDisplay();
display.setCursor(0,0);
display.println("SENSOR OFFLINE");
display.display();
while(1) { delay(10); } // Halt execution
}
vl53.startRanging();
setup_wifi();
}
void loop() {
if (!mqtt.connected()) {
if (mqtt.connect("ESP32_PanelMonitor")) {
mqtt.publish(mqtt_topic, "online");
}
}
mqtt.loop();
if (vl53.isRangeComplete()) {
uint16_t distance_mm = vl53.readRange();
display.clearDisplay();
display.setCursor(0, 0);
display.print("Dist: "); display.print(distance_mm); display.println(" mm");
if (distance_mm < ALERT_THRESHOLD_MM && distance_mm > 20) { // >20mm filters sensor noise
if (!violationActive) {
violationActive = true;
digitalWrite(BUZZER_PIN, HIGH);
mqtt.publish(mqtt_topic, "VIOLATION");
}
display.println("STATUS: BLOCKED!");
} else {
if (violationActive) {
violationActive = false;
digitalWrite(BUZZER_PIN, LOW);
mqtt.publish(mqtt_topic, "CLEAR");
}
display.println("STATUS: COMPLIANT");
}
display.display();
}
delay(500);
}
Debugging Sensor Failures and I2C Faults
When working with high-precision I2C sensors over extended wire runs, bus lockups and initialization failures are common. If your serial monitor outputs the exact error string "Error: Failed to find VL53L1X sensor" or hangs on "Timeout waiting for VL53L1X", follow this diagnostic path.
The First Three Things to Check
- I2C Pull-up Resistors: The VL53L1X breakout relies on weak internal pull-ups. For reliable 400kHz communication over 24 AWG wire runs longer than 6 inches, you must add external 4.7kΩ pull-ups to 3.3V on both SDA and SCL. Without them, the signal edges degrade, causing the ESP32 to miss ACK bits.
- XSHUT Pin State: The XSHUT pin is active LOW. If your ESP32 GPIO 25 is not explicitly driven HIGH during
setup()before callingvl53.begin(), the sensor remains in hardware standby and will not acknowledge I2C requests. - I2C Address Collision: The VL53L1X defaults to I2C address
0x29. Ensure no other device on your custom bus (like a secondary sensor or an incorrectly strapped OLED) is attempting to use this address.
Ranked Causes for "Timeout waiting for VL53L1X"
If the sensor initializes but throws a timeout error during the readRange() loop, the issue is usually environmental or timing-related:
- Cause 1: Ambient IR Interference. The VL53L1X uses a 940nm VCSEL laser. Direct sunlight or incandescent bulbs hitting the sensor aperture will saturate the SPAD array, causing infinite integration times. Fix: Mount the sensor in a shrouded 3D-printed hood or apply a 940nm bandpass filter.
- Cause 2: Target Reflectivity. If a homeowner places a matte black storage bin in the clearance zone, the photon return rate drops drastically. Fix: Adjust the sensor's timing budget in the library configuration from the default 50ms to 100ms to allow more photon collection.
- Cause 3: I2C Bus Capacitance. Running unshielded I2C wires parallel to 120V AC Romex cables induces noise and increases bus capacitance. Fix: Route low-voltage sensor wires at least 2 inches away from any mains wiring, or use shielded twisted pair (CAT5/6) for the I2C run, grounding the shield at the ESP32 end only.
For deeper sensor configuration, refer to the Adafruit VL53L1X Time-of-Flight Sensor Guide.
Extending or Simplifying the Build
This build is designed to be a robust, set-and-forget compliance monitor, but you can scale it based on your specific environment and skill level.
How to Simplify
If you do not have an MQTT broker (like Mosquitto) or Home Assistant running, strip out the WiFi.h and PubSubClient.h dependencies. Rely entirely on the local SSD1306 OLED and the piezo buzzer. This reduces the code footprint, eliminates WiFi reconnection edge cases, and allows the system to run indefinitely on a standard 5V/2A USB wall adapter without network security concerns.
How to Extend
For commercial panels or subpanels in tight mechanical rooms, a single ToF sensor might not capture the full 30-inch width requirement. You can extend this build by adding a second VL53L1X sensor. Because the default I2C address is hardcoded, you must use the XSHUT pin sequence to boot them individually: hold Sensor 2 in standby (XSHUT LOW), boot Sensor 1, change Sensor 1's address via software to 0x30, then release Sensor 2's XSHUT pin and boot it at 0x29. This dual-sensor array allows you to monitor both the left and right boundaries of the 30-inch width clearance, ensuring no shelving units encroach on the sides of the panel.






