Difficulty: Intermediate | Time: 3 Hours | Cost: ~$18 USD

The Jobsite Problem: Faded Insulation and Mixed Standards

When you are troubleshooting a legacy subpanel or reverse-engineering imported industrial machinery, the physical wire code colour on the insulation is often your only clue to the circuit's original intent. But insulation fades under UV exposure, gets painted over by previous contractors, or worse, follows a completely different regional standard (like IEC 60446 brown/blue instead of NEC black/white). Relying purely on visual inspection in a dimly lit junction box is a fast track to miswiring a 240V load.

To solve this, we are building a handheld Wire Code Colour Identifier using an ESP32-CAM. This tool uses computer vision to sample the RGB values of the wire jacket, maps it to the correct regional wire code colour standard (US NM-B or EU IEC), and displays the expected function (Hot, Neutral, Ground) on an integrated OLED screen. It eliminates the guesswork when verifying if a white wire is being used as a switched leg (re-marked with black tape) or if a green wire is genuinely an equipment ground.

Safety Caveat: This tool identifies insulation colour; it does not verify if the wire is energized. Always de-energize the panel, lock out the breaker, and verify dead with a CAT III multimeter before touching any conductors. Local AHJ codes dictate how wires must be re-identified if used outside their standard colour function.

Decision Tree: Selecting the Right Camera Sensor

The ESP32 ecosystem supports several camera modules, but they are not interchangeable. Picking the wrong sensor will result in poor low-light performance inside electrical panels or fatal memory allocation errors on the ESP32. Use this decision matrix to select your sensor.

Sensor Model Resolution Low-Light Performance Price (2026) Verdict
OV2640 2 MP (1600x1200) Good (with onboard LED flash) $6 - $9 PICK THIS. Best balance of SRAM usage and colour accuracy for macro wire inspection.
OV5640 5 MP (2592x1944) Excellent $14 - $18 Overkill. 5MP frames exhaust the ESP32's 520KB SRAM, causing esp_heap_caps_malloc failures.
OV7725 VGA (640x480) Poor (noisy in shadows) $5 - $7 Avoid. VGA resolution lacks the pixel density to distinguish between dark green and bare copper.

Final Decision: Standardize on the OV2640 paired with the AI-Thinker ESP32-CAM board. It supports the necessary JPEG/RGB565 frame buffers without requiring external PSRAM for our specific region-of-interest (ROI) colour sampling.

Hardware Spec Sheet and Pin Mapping

The AI-Thinker ESP32-CAM is notoriously pin-starved. The camera interface consumes 14 GPIOs, leaving very few for peripherals. We must avoid strapping pins (GPIO 0, 2, 12, 15) that dictate boot modes and flash voltages.

Bill of Materials (BOM)
Component Exact Variant / Part Number Notes
Microcontroller AI-Thinker ESP32-CAM (ESP32-WROVER with 4MB PSRAM) Must have PSRAM for stable frame buffers.
Camera Module OV2640 2MP with 24-pin ribbon Standard 2.1mm focal length lens.
Display SSD1306 0.96" 128x64 I2C OLED 4-pin variant (GND, VCC, SCL, SDA).
Haptic/Audio 5V Active Piezo Buzzer (TMB12A05) Active (built-in oscillator), not passive.
Power 18650 Li-Ion + MT3608 Boost to 5V USB power causes brownouts on long cables.

Pin Mapping Table

Wire the OLED and Buzzer strictly to these safe GPIOs. Do not use GPIO 12 for the buzzer; pulling it high at boot will switch the flash SPI voltage to 1.8V and permanently brick the boot sequence until you desolder it.

Peripheral Pin Function ESP32-CAM GPIO Notes
OLED Display SDA GPIO 14 Internal pull-up enabled in code.
OLED Display SCL GPIO 15 Safe for I2C clock.
Piezo Buzzer Signal (+) GPIO 13 Active high. Avoid GPIO 12!
Camera Flash LED Control GPIO 4 Used to illuminate dark panels.

Firmware: Colour Detection and Continuity Code

This firmware targets the AI-Thinker ESP32-CAM board variant in the Arduino IDE (Select Board: AI Thinker ESP32-CAM). It initializes the camera, captures a frame, samples a 20x20 pixel grid in the center of the image, calculates the average RGB values, and maps them to the US NEC wire code colour standards.

Required Libraries: Adafruit_SSD1306, Adafruit_GFX, and the official esp32-camera library.

#include "esp_camera.h"
#include 
#include 
#include 

// --- AI-Thinker ESP32-CAM Pin Definitions ---
#define PWDN_GPIO_NUM     32
#define RESET_GPIO_NUM    -1
#define XCLK_GPIO_NUM      0
#define SIOD_GPIO_NUM     26
#define SIOC_GPIO_NUM     27
#define Y9_GPIO_NUM       35
#define Y8_GPIO_NUM       34
#define Y7_GPIO_NUM       39
#define Y6_GPIO_NUM       36
#define Y5_GPIO_NUM       21
#define Y4_GPIO_NUM       19
#define Y3_GPIO_NUM       18
#define Y2_GPIO_NUM        5
#define VSYNC_GPIO_NUM    25
#define HREF_GPIO_NUM     23
#define PCLK_GPIO_NUM     22

// --- Peripheral Pins ---
#define FLASH_LED_PIN      4
#define BUZZER_PIN        13
#define OLED_SDA          14
#define OLED_SCL          15

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

void setup() {
  Serial.begin(115200);
  pinMode(FLASH_LED_PIN, OUTPUT);
  pinMode(BUZZER_PIN, OUTPUT);
  digitalWrite(FLASH_LED_PIN, HIGH); // Turn on flash for panel illumination

  // Initialize I2C on custom pins for OLED
  Wire.begin(OLED_SDA, OLED_SCL);
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println(F("SSD1306 allocation failed"));
    for(;;); // Halt
  }
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(0,0);
  display.println("Booting Camera...");
  display.display();

  // Camera Configuration
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sccb_sda = SIOD_GPIO_NUM;
  config.pin_sccb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.frame_size = FRAMESIZE_QVGA; // 320x240 sufficient for colour sampling
  config.pixel_format = PIXFORMAT_RGB565;
  config.grab_mode = CAMERA_GRAB_LATEST;
  config.fb_location = CAMERA_FB_IN_PSRAM;
  config.jpeg_quality = 12;
  config.fb_count = 2;

  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera probe failed with error 0x%x\n", err);
    display.clearDisplay();
    display.setCursor(0,0);
    display.printf("CAM ERR: 0x%x", err);
    display.display();
    return;
  }
  
  display.clearDisplay();
  display.println("Sensor Ready.");
  display.display();
  delay(500);
}

void loop() {
  camera_fb_t * fb = esp_camera_fb_get();
  if (!fb) {
    Serial.println("Camera capture failed");
    return;
  }

  // Sample center 20x20 pixels for average RGB
  uint32_t sumR = 0, sumG = 0, sumB = 0;
  int samples = 0;
  int midX = 160; int midY = 120; // Center of QVGA
  
  uint16_t *pixels = (uint16_t *)fb->buf;
  
  for (int y = midY - 10; y < midY + 10; y++) {
    for (int x = midX - 10; x < midX + 10; x++) {
      uint16_t p = pixels[y * 320 + x];
      // Extract RGB565 to 8-bit
      uint8_t r = (p >> 11) & 0x1F; r = (r << 3) | (r >> 2);
      uint8_t g = (p >> 5) & 0x3F;  g = (g << 2) | (g >> 4);
      uint8_t b = p & 0x1F;         b = (b << 3) | (b >> 2);
      sumR += r; sumG += g; sumB += b;
      samples++;
    }
  }
  esp_camera_fb_return(fb);

  uint8_t avgR = sumR / samples;
  uint8_t avgG = sumG / samples;
  uint8_t avgB = sumB / samples;

  String wireColour = "Unknown";
  String wireFunction = "Check Manual";

  // US NEC NM-B Wire Code Colour Mapping Thresholds
  if (avgR < 60 && avgG < 60 && avgB < 60) {
    wireColour = "BLACK"; wireFunction = "HOT (Line)";
  } else if (avgR > 180 && avgG > 180 && avgB > 180) {
    wireColour = "WHITE"; wireFunction = "NEUTRAL";
  } else if (avgR > 150 && avgG < 80 && avgB < 80) {
    wireColour = "RED"; wireFunction = "HOT (Leg 2)";
  } else if (avgR < 80 && avgG > 120 && avgB < 80) {
    wireColour = "GREEN"; wireFunction = "GROUND";
  } else if (avgR > 150 && avgG > 100 && avgB < 50) {
    wireColour = "BARE/COPPER"; wireFunction = "GROUND";
  }

  // Update OLED
  display.clearDisplay();
  display.setCursor(0,0);
  display.setTextSize(2);
  display.println(wireColour);
  display.setTextSize(1);
  display.setCursor(0,25);
  display.print("Function: ");
  display.println(wireFunction);
  display.setCursor(0,40);
  display.printf("RGB: %d,%d,%d", avgR, avgG, avgB);
  display.display();

  // Audio feedback for Ground/Neutral confirmation
  if (wireFunction == "GROUND" || wireFunction == "NEUTRAL") {
    digitalWrite(BUZZER_PIN, HIGH);
    delay(50);
    digitalWrite(BUZZER_PIN, LOW);
  }

  delay(300); // Debounce / Frame rate limit
}

Debugging: Fixing the 0x20001 Camera Probe Error

The most common failure point when building ESP32-CAM projects is the camera initialization sequence. If your serial monitor outputs the exact string: Camera probe failed with error 0x20001, the ESP32's I2C master failed to handshake with the OV2640's SCCB (Serial Camera Control Bus) interface.

Do not immediately assume the camera module is dead. Run through these first three things to check in order of probability:

  1. Power Rail Brownout (80% of cases): The ESP32-CAM draws upwards of 180mA during the initial sensor handshake. If you are powering it via a long, thin USB cable from a standard 500mA PC port, the voltage at the board's 5V pin will sag below 4.2V. The ESP32 will brownout, resetting the I2C peripheral mid-transaction. Fix: Power the 5V rail with a dedicated 5V/2A buck converter or a high-discharge 18650 Li-Ion cell.
  2. Ribbon Cable Seating (15% of cases): The 24-pin FPC connector on the AI-Thinker board is fragile. If the ribbon cable is inserted even 0.5mm skewed, the SIOC (Clock) or SIOD (Data) pins will miss contact. Fix: Flip the black plastic retaining flap UP, pull the ribbon out, ensure the blue stiffener is perfectly flush with the connector edge, and press the flap down firmly.
  3. GPIO 12 Strapping Conflict (5% of cases): If you wired your OLED SDA or Buzzer to GPIO 12, you have violated the ESP32 boot strapping rules. GPIO 12 must be LOW at boot to select 3.3V flash SPI. If pulled high, the ESP32 attempts to read flash at 1.8V, fails, and throws erratic peripheral errors. Fix: Move any peripherals off GPIO 12. (Our pin mapping above safely uses GPIO 13 and 14).

Extending and Simplifying the Build

Depending on your bench needs, you can scale this project up or down without rewriting the core logic.

How to Simplify (The 'Dumb' Probe)

If you don't need the OLED display and just want a pocket tool that beeps differently for Hot vs Neutral wires, strip out the Wire.h and Adafruit_SSD1306 libraries. Replace the display update block with a simple PWM tone generator on GPIO 13. A 1000Hz beep for Black/Red (Hot) and a 400Hz beep for White (Neutral) saves $4 on the BOM and reduces boot time to under 800ms.

How to Extend (Adding True Voltage Verification)

Colour identification is only half the battle; verifying the wire's actual state is the other. To extend this into a true smart-probe:

  • Add an ADS1115 16-bit ADC on the I2C bus (address 0x48).
  • Wire a high-impedance voltage divider (1MΩ / 10kΩ) to a stainless steel probe tip connected to the ADS1115 A0 pin.
  • Update the code logic: If the camera identifies the wire code colour as White (Neutral), but the ADC reads >2V RMS, trigger a critical warning on the OLED: "WHITE WIRE IS HOT! Switched Leg Detected."
This extension bridges the gap between visual standards and electrical reality, turning a simple camera toy into a legitimate jobsite diagnostic instrument.