Decoding the Rules: What is the Code for Electrical Panel Height?

When electrical inspectors and accessibility auditors verify the code for electrical panel height, they are cross-referencing two distinct but overlapping standards. If you are building a smart home, wiring a subpanel, or designing an inspection tool, you must satisfy both to ensure safety and legal compliance.

NEC 240.24(A) Location: "Overcurrent devices shall be located so that the center of the grip of the operating handle, when in its highest position, is not more than 2.0 m (6 ft 7 in.) above the floor or working platform." (NFPA 70 National Electrical Code)

Simultaneously, the Americans with Disabilities Act (ADA) mandates that operational controls must be within accessible reach ranges. According to the 2010 ADA Standards for Accessible Design (Section 308.2.1), an unobstructed forward reach cannot exceed 48 inches (1219 mm) above the finished floor.

Measuring this manually with a tape measure while standing near a live 200A main lug is cumbersome and introduces parallax error. To solve this, we are going to build a floor-standing, automated LiDAR compliance checker. You place the device on the floor, aim it at a piece of retroreflective tape stuck to the highest breaker handle, and the firmware instantly calculates pass/fail status against both NEC and ADA thresholds.

⚠️ SAFETY CALLOUT: Even though this tool operates on the floor outside the panel, you are working near exposed busbars if the panel cover is removed. Always wear appropriate PPE (arc-rated clothing, safety glasses) and maintain the limited approach boundary. Never reach into a live panel to apply reflective tape; de-energize the main breaker first.

Sensor Selection: Decision Tree for Distance Measurement

To measure up to 2.0 meters (6.5 feet) with millimeter precision, we need to select the right distance sensor. Here is the decision path that terminates in our final hardware pick.

Sensor Type Model Example Max Range Beam Angle / Precision Verdict
Ultrasonic HC-SR04 4.0 m 15° wide cone, ±3mm REJECT. Wide beam hits adjacent breakers and panel deadfront, causing false short readings.
Time-of-Flight (ToF) VL53L0X 2.0 m Narrow, ±3% REJECT. 2.0m max range leaves zero margin for error or floor irregularities; drops out at max height.
Micro-LiDAR Benewake TF-Luna 8.0 m Narrow, ±1cm SELECT. Plenty of headroom, narrow beam isolates the breaker handle, UART/I2C flexible.

Final Pick: The Benewake TF-Luna (approx. $15). It uses an 850nm VCSEL laser, providing a tight beam that easily isolates a standard 3/4" breaker toggle from the surrounding metal deadfront.

Bill of Materials and Pin Mapping

This build targets the ESP32-WROOM-32 DevKit V1 (30-pin variant). We use HardwareSerial (UART2) for the LiDAR to avoid I2C bus contention with the OLED display.

Parts List

  • MCU: ESP32-WROOM-32 DevKit V1 (30-pin) — Espressif ESP32 Datasheet
  • Sensor: Benewake TF-Luna Micro-LiDAR (with 4-pin JST connector)
  • Display: 0.96" SSD1306 I2C OLED (128x64, 4-pin)
  • Power: 18650 Lithium Battery Shield (with 5V/3.3V outputs)
  • Target: 1" square of retroreflective tape (e.g., 3M Diamond Grade)

Pin Mapping Table

Component Pin Label ESP32 GPIO Notes
TF-Luna VCC (Red) 5V (VIN) Requires 5V for stable VCSEL output
TF-Luna GND (Black) GND Common ground with ESP32
TF-Luna TX (White) GPIO 16 (RX2) UART2 Receive (ESP32 listens)
TF-Luna RX (Green) GPIO 17 (TX2) UART2 Transmit (Optional for config)
SSD1306 OLED VCC 3.3V Logic level power
SSD1306 OLED GND GND Common ground
SSD1306 OLED SCL GPIO 22 Default I2C Clock
SSD1306 OLED SDA GPIO 21 Default I2C Data

Step-by-Step Assembly and Calibration

  1. Prepare the Target: Stick a 1-inch square of retroreflective tape directly onto the center of the grip of the highest breaker handle in the panel. This ensures the LiDAR's 850nm light bounces straight back, even if the breaker is black plastic.
  2. Wire the UART: Connect the TF-Luna TX to ESP32 GPIO 16. Do not swap TX/RX; the TF-Luna streams data out of its TX pin, which must go into the ESP32's RX pin.
  3. Wire the I2C: Connect the OLED SDA/SCL to GPIO 21/22. If your OLED module lacks built-in pull-up resistors, add 4.7kΩ resistors from SDA and SCL to 3.3V.
  4. Mount the Sensor: 3D print or mount the TF-Luna in a small puck enclosure with a flat bottom. The laser aperture must be perfectly parallel to the floor. Add a small bubble level to the top of the enclosure to verify it isn't tilted, which introduces cosine error at 2-meter distances.
  5. Power Up: Insert a charged 18650 cell into the battery shield. The ESP32 will boot, initialize the I2C bus, and begin parsing the UART stream.

The ESP32 Firmware: Compilable Code with Error Handling

The following C++ code is designed for the Arduino IDE (ESP32 board package v2.0.x or v3.0.x). It parses the 9-byte TF-Luna UART frame, validates the checksum, and evaluates the distance against the 2000mm NEC limit and 1219mm ADA limit.

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <HardwareSerial.h>

// --- Hardware Definitions ---
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C // Use 0x3D if your OLED has a different jumper

// TF-Luna connected to UART2 (GPIO 16 = RX, GPIO 17 = TX)
HardwareSerial tfSerial(2);

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// --- Compliance Thresholds (in millimeters) ---
const int NEC_MAX_HEIGHT = 2000; // 2.0 meters (6 ft 7 in)
const int ADA_MAX_REACH = 1219;  // 48 inches

// --- TF-Luna Frame Variables ---
uint8_t frame[9];
int frameIndex = 0;
bool frameValid = false;
int distance_mm = 0;

void setup() {
  Serial.begin(115200); // Debug serial
  
  // Initialize I2C OLED
  if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println(F("OLED: SSD1306 allocation failed"));
    while(true) { delay(100); } // Halt execution
  }
  
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(0,0);
  display.println("Booting Inspector...");
  display.display();

  // Initialize UART for TF-Luna (Default baud is 115200)
  tfSerial.begin(115200, SERIAL_8N1, 16, 17);
  delay(500); // Allow sensor to stabilize
}

void loop() {
  // Read incoming UART bytes from TF-Luna
  while (tfSerial.available() > 0) {
    uint8_t currentByte = tfSerial.read();
    
    // Look for the header bytes (0x59 0x59)
    if (frameIndex == 0 && currentByte != 0x59) continue;
    if (frameIndex == 1 && currentByte != 0x59) {
      frameIndex = 0; // Reset if second header byte is wrong
      continue;
    }
    
    frame[frameIndex++] = currentByte;
    
    // When we have 9 bytes, validate checksum
    if (frameIndex == 9) {
      uint8_t checksum = 0;
      for (int i = 0; i < 8; i++) {
        checksum += frame[i];
      }
      
      if (checksum == frame[8]) {
        distance_mm = frame[2] + (frame[3] << 8);
        frameValid = true;
      } else {
        Serial.println("TF-Luna: Checksum mismatch");
      }
      frameIndex = 0; // Reset for next frame
    }
  }

  // Update Display if we have a valid reading
  if (frameValid) {
    display.clearDisplay();
    
    // Display Distance
    display.setTextSize(2);
    display.setCursor(0, 0);
    display.print(distance_mm);
    display.setTextSize(1);
    display.print(" mm");
    
    // Evaluate NEC Code for Electrical Panel Height
    display.setCursor(0, 25);
    if (distance_mm > 0 && distance_mm <= NEC_MAX_HEIGHT) {
      display.println("NEC 240.24: PASS");
    } else if (distance_mm > NEC_MAX_HEIGHT) {
      display.println("NEC 240.24: FAIL");
    } else {
      display.println("NEC: Out of Range");
    }
    
    // Evaluate ADA Reach
    display.setCursor(0, 40);
    if (distance_mm > 0 && distance_mm <= ADA_MAX_REACH) {
      display.println("ADA Reach:  PASS");
    } else if (distance_mm > ADA_MAX_REACH) {
      display.println("ADA Reach:  FAIL");
    } else {
      display.println("ADA: Out of Range");
    }
    
    display.display();
    frameValid = false; // Wait for next valid frame to update
  }
  
  delay(50); // TF-Luna outputs at 100Hz (10ms/frame), 50ms loop is safe
}
💡 Pro-Tip: The TF-Luna outputs data at 100Hz by default. The 50ms delay in the loop prevents the ESP32 from starving the watchdog timer while still updating the screen fast enough to feel instantaneous when you move the sensor.

Debugging: First Three Things to Check When It Fails

When bringing up UART sensors and I2C displays simultaneously, things often go wrong on the first boot. If your tool doesn't work, follow this ranked decision path based on the serial monitor output.

1. Error: OLED: SSD1306 allocation failed

Cause: The ESP32 cannot find the OLED on the I2C bus. This is the most common failure point.
Fix:

  • Check the I2C address. Many cheap SSD1306 clones use 0x3D instead of 0x3C. Change the SCREEN_ADDRESS macro and re-flash.
  • Run an I2C scanner sketch to verify the display is actually responding on GPIO 21/22.
  • Verify you are powering the OLED from 3.3V, not 5V. Overvolting the logic pin can permanently brick the display controller.

2. Error: TF-Luna: Checksum mismatch

Cause: The ESP32 is receiving data, but the bytes are corrupted or out of sync, meaning the baud rate is wrong or the wiring is picking up noise.
Fix:

  • Confirm the TF-Luna is set to 115200 baud. If you previously connected it to a PC and changed it to 9600 via the Benewake GUI, you must change tfSerial.begin(115200...) to match, or factory reset the sensor.
  • Ensure the TF-Luna TX pin is connected to ESP32 GPIO 16 (RX2), not GPIO 17. Swapping them causes the ESP32 to read its own transmitted echoes.

3. Symptom: Distance reading stuck at 0 mm or Out of Range

Cause: The LiDAR is functioning, but it cannot resolve the target distance.
Fix:

  • The TF-Luna struggles with highly absorptive matte black surfaces (like standard breaker toggles). Ensure you applied the retroreflective tape.
  • Check for ambient IR interference. If the panel is in direct, blazing sunlight, the 850nm VCSEL can be washed out. Shade the sensor aperture with your hand to test.
  • Verify the sensor isn't in "single trigger" mode. The code expects continuous 100Hz streaming.

Extending and Simplifying the Build

Depending on your field needs, you might want to strip this build down or scale it up for commercial inspection workflows.

How to Simplify (The "No-Screen" Variant)

If you want to reduce the BOM cost and eliminate I2C debugging entirely, drop the OLED. Replace it with a common-cathode RGB LED connected to GPIO 25 (Red), 26 (Green), and 27 (Blue) via 220Ω current-limiting resistors.
Logic: If distance_mm <= 1219, light Green (Passes both). If 1219 < distance_mm <= 2000, light Yellow (Passes NEC, fails ADA). If distance_mm > 2000, light Red (Fails both). This turns the tool into a simple "traffic light" inspector.

How to Extend (The "Audit Log" Variant)

For professional electricians who need to generate inspection reports, add an SD card module (SPI on GPIO 5, 18, 19, 23) or utilize the ESP32's native BLE. By adding the BleKeyboard or ESP-NOW libraries, you can press a physical pushbutton on the device to log the timestamp, GPS coordinate (if you add a NEO-6M module), and exact millimeter height to a CSV file or directly to a smartphone app. This creates an immutable digital paper trail proving your installation meets the exact code for electrical panel height requirements on the day of inspection.