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.
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.
| 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:
- 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.
- 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.
- 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."






