Build a Parking sensor with ESP32 board, LEDs and Ultrasonic distance sensor

 What problem am I trying to solve?

My garage was built for cars from another era: inside it’s about ~4.8 m long × 2.5 m wide, and the entrance narrows to just 2.2 m. My car, on the other hand, is 4.7 m long and almost 2 m wide, so squeezing it through the doorway is just step one. Even if I make it in without scraping the sides, I’m stuck: there’s almost no room between the car and the side walls, so I can’t open the door or shut the garage door behind me. I need to use every last millimeter of length, but no matter how gently I press the pedal, or how much I rely on the factory cameras, getting the exact distance to the back wall is nearly impossible.

The factory parking beepers are no help: they scream “STOOOOP!” but never tell me whether I’m 10 cm or 3 cm from the back wall. Worse, there’s no sensor along the right‑hand side to show how close I am there, information I need so I can climb out on the left without adding racing stripes to the paint.

What am I trying to achieve?

I’m using this garage problem as the perfect excuse to geek out with sensors, an ESP board and LEDs. The goal: build my own setup that tells me every last centimeter and millimeter, so I don’t end up with a four‑figure bill for scratched paint.

My first plan was basic: stick a sensor in place (I’ll compare the different kinds later) and show the distance on a little screen in the car or on my phone. It would work, but it isn’t very handy. Then I found a video from which I took a lot of inspiration: it covered different sensors and a clever way to show the distance. That’s where the better idea came from: hang a bright 1 m LED strip that changes color as I move. I’ve played with distance sensors before but never with LED strips, so this is the perfect time to try something new.

Steps

Quick disclaimer: I’m learning as I go and this is strictly a hobby. Hardware is just a hobby: my day job is software, and even though I studied telecommunication engineering at uni (with a bit of electronics I’ve mostly forgotten), I’ve never taken those circuits from paper to real life. If you spot a smarter approach, especially if I’m heading into no‑no territory or anything unsafe (power wiring etc.), please let me know in the comments (or whatever channel you prefer). I’m here to learn and have fun, and yes, as you’ll notice my soldering still needs plenty of love!

So, the first step is to build a prototype. I’ll grab whatever’s in the office, perf‑boards, ESP32s, a chunky buck converter, whatever wire is handy, and mash it together. I’ll try to pick the right wire gauge and keep the solder joints respectable, but neatness can wait. I just need something that lights up and measures distance so I stay excited.

Tidy up later and come with a nice package. If I see that what I’m doing here is useful, in the next posts (maybe even videos) I’ll clean things up by designing a nice PCB, having it made in China, and solder everything properly. For now, the messy prototype is enough to prove the idea and keep the project rolling.

What’s in the build?

We’ll take a quick look at each part before diving into the details and writing the firmware:

  • an HC-SR04 distance sensor
  • a 1 m LED strip – WS2812B RGB
  • a buck converter
  • a 12 V power supply
  • plenty of patience and curiosity

I usually buy these components from amazon, mouser.com, digikey.com or aliexpress.com.

Distance Sensor and ESP32 Board

Let’s start with the distance sensor, what seems to be the main actor of the project. I’ve played with ToF sensors (like the tiny and cheap VL53L0X) before. They use a laser pulse and measure how long the light takes to bounce back. Cheap, easy to wire up over I²C and plenty of libraries exist. The trouble? Their readings jump around a bit. When you need to know if you’re 3 cm or 5 cm from the wall, that wobble is a deal‑breaker.

VL53L0X

The video I mentioned earlier uses a pricier LiDAR module, great range and a narrow beam, but overkill for my wallet and doesn’t deal with close distances.

Comparison between sensors – table taken from https://www.youtube.com/watch?v=HqqlY4_3kQ8

So I went with the good old HC‑SR04 ultrasonic sensor. It’s accurate in the short‑range sweet spot (2 cm – 4 m), costs only a few euros, and works by sending out a sound pulse and timing the echo.

HC-SR04 ultrasonic sensor

I chose the dual‑voltage HC‑SR04 module (3.3 V–5 V) so I can power it straight from the ESP32’s 3.3 V pin. That way, a single battery on the Feather S3 can run ESP32 + sensor, without any extra an extra power source.

In my final build I ended up feeding everything from a 5 V supply, so the sensor could have been powered directly at 5 V. Either approach works here; using 3.3 V simply keeps more options open.

But if you plan to use an HC‑SR04 with an ESP32, get the 3.3 V‑compatible version. ESP32 GPIOs top out at 3.3 V, so running the sensor at 5 V would drive its ECHO pin to 5 V and require a resistor. Powered at 3.3 V, the sensor’s signals stay safely within the ESP32’s limits.

Yeah Yeah I know, terrible soldering 🧑‍🏭 😅 But remember what I said at the beginning? Let’s make it work first, then will move to something cleaner like a PCB with SMD components.

The ESP32 itself powers the ultrasonic sensor, which is fine because the sensor only draws about 70 mW. Wiring is as follows:

  • Green: ESP32 pin 5 → Sensor TRIG
  • White: ESP32 pin 12 → Sensor ECHO
  • Red: 3.3 V
  • Black: GND

By contrast, the LED strip pulls roughly 18 W, far beyond what the ESP32 can supply. The 3.3 V pin is limited to about 700 mA (≈ 2.3 W), so the strip needs its own dedicated power source.

I’m currently using a UM Feather S3 ESP32, larger and more capable than necessary, but it was on hand.

UM FeatherS3

For a streamlined build I’d switch to something smaller, such as the Seeed Studio XIAO ESP32.

Seeed studio XIAO ESP32

At the end, any ESP32 board will work; just choose one that accepts a 5 V input so the same power supply can run both the microcontroller and the LED strip.

When we eventually optimize the design for a truly polished product, we might skip a breakout “board” altogether and solder the ESP32 chip straight onto a custom PCB, routing all the connections right there. I’ve never done this myself, but it would look incredibly clean.

Of course, development boards are great during prototyping: they manage battery hookup and charging via a JST connector, expose plenty of GPIOs, supply 5 V in and out, include Stemma QT headers, offer USB‑C for flashing, and pack a compact 3‑D antenna; all the conveniences you need when you’re still iterating.

Let’s make a first experiment: see the sensor working with its measurements. Let’s see the code we need just by using the Arduino IDE. We don’t need any library!

#define SONIC_TRIG_PIN  5
#define SONIC_ECHO_PIN  12

void setup() {  
  Serial.begin(115200);
  pinMode(SONIC_TRIG_PIN, OUTPUT);  
  pinMode(SONIC_ECHO_PIN, INPUT);  
}

void loop() {
  float distance, duration;
  digitalWrite(SONIC_TRIG_PIN, LOW);  
  delayMicroseconds(2);  
	
  digitalWrite(SONIC_TRIG_PIN, HIGH);  
  delayMicroseconds(10);  
	
  digitalWrite(SONIC_TRIG_PIN, LOW);  
  duration = pulseIn(SONIC_ECHO_PIN, HIGH);

  distance = (duration*.0343)/2;
	
  Serial.println(String(distance) + "cm");
  delay(100);
}

In the setup, the ESP opens a 115200-baud serial connection so you can read measurements on your computer, then configures the HC-SR04’s TRIG pin (pin 5) as an OUTPUT to send the ultrasonic-burst trigger pulse and the ECHO pin (pin 12) as an INPUT to listen for the returning echo.

The sketch then repeatedly does three things:

  1. Triggering the ping:
    1. It drives the TRIG pin LOW for 2 μs, then HIGH for 10 μs, then LOW again.
    2. That 10 μs HIGH pulse tells the HC-SR04 to send out an 8-cycle ultrasonic burst.
  2. Listening for the echo:
    1. After the burst, the sensor raises its ECHO pin HIGH and keeps it HIGH until the reflected sound returns.
    2. The pulseIn(SONIC_ECHO_PIN, HIGH) call measures how many microseconds the ECHO pin stayed HIGH, that’s the round-trip time of the sound pulse.
  3. Calculating and printing distance:
    • It converts that time into centimeters with distance = (duration * 0.0343) / 2 (0.0343 cm/μs is the speed of sound, divided by 2 because you only want the one-way distance).
    • Finally, it prints the result to Serial every 100 ms.

LED Strip – WS2812B RGB

The led strip is a WS2812B RGB, in this case I got a 60 leds for 1m strip, powered at 5V and needs a maximum of 18W.

WS2812B RGB

It has three main wires on a single connector:

  • Red – 5 V
  • White – GND
  • Green – Data (goes to an ESP32 GPIO)

Because the strip can pull 3.6 A, the red and white leads must go to a separate 5 V supply rated for at least 18 W. The green data line connects to an ESP32 GPIO (I’m using pin 7). A second red/white pair is provided if you need to inject power at another point on the strip.

For a quick test I’ll drop everything onto a breadboard: green to GPIO 7, red/white to my bench supply (set to 5 V). On the software side I’m using the FastLED library, which supports WS2812B with just a few lines of code.

ESP32 <-> LED strip

Eventually I’ll mount the strip inside a slim aluminum LED profile; those extruded channels that give the whole assembly a sleek, pro finish.

WS2812B LEDs

And now let’s try it out, just by blinking it.

#include <FastLED.h>

#define LED_PIN     7      // change to your data pin
#define NUM_LEDS    60     // number of LEDs in your strip
#define BRIGHTNESS  255    // max brightness (0–255)
#define LED_TYPE    WS2812B
#define COLOR_ORDER GRB

CRGB leds[NUM_LEDS];

void setup() {
  FastLED.addLeds<LED_TYPE,LED_PIN,COLOR_ORDER>(leds, NUM_LEDS);
  FastLED.setBrightness(BRIGHTNESS);
}

void loop() {
  // Turn all LEDs on (white)
  fill_solid(leds, NUM_LEDS, CRGB::White);
  FastLED.show();
  delay(1000);

  // Turn all LEDs off
  fill_solid(leds, NUM_LEDS, CRGB::Black);
  FastLED.show();
  delay(1000);
}

First up in setup(), we register our 60-pixel WS2812B strip on pin 7 with GRB order by calling

FastLED.addLeds<WS2812B, 7, GRB>(leds, 60);

Right after, we dial our brightness to 255, which is the maximum, giving us full-output white, using

FastLED.setBrightness(255);

Then we jump into loop() where we switch on and off the LEDs with the colors we want:

fill_solid(leds, NUM_LEDS, CRGB::White) paints each single LED full white, and FastLED.show() immediately pushes that frame out so you see the bar glow at its brightest.

After a one-second delay(1000), we call fill_solid(leds, NUM_LEDS, CRGB::Black) to set every pixel to off, run FastLED.show() again to update the strip, wait another second, and repeat; giving you that steady on-one-second, off-one-second blink.

Power everything up

Ok, now that we are able to get the distance from the sensor and drive the led strip, we can focus on what is not a small detail: how do we power all this?

We must power two elements: the ESP board and the LED strip. Fortunately, both require 5 V, so we can connect them in parallel, sharing the 5 V supply. But the LED strip demands far more power!

At the moment I’m feeding both the ESP and LED strip from the bench power supply set to 5 V, but we want to power the entire setup with a transformer plugged into the wall AC source that converts the current to DC, and it needs to deliver at least 20 W: 18 W for the LED strip + I’d say another 2 W for the ESP max.

Transformer >20W -> ESP + HC-SR04 and LED strip

A stable 5 V DC supply capable of at least 20 W is essential

I’m starting with a power supply that transforms AC (here in EU is 230V) to a 12 V DC, then stepping it down with a buck converter to get a steady 5 V DC.

In theory, we could tap 5 V from 12 V with a simple resistor voltage divider, but that’s both wasteful (those resistors burn off power as heat ) and risky: the output tracks any fluctuation in the input. Unless the 12 V rail is rock‑solid, your 5 V line won’t be stable. A buck converter avoids both issues, delivering an efficient, regulated 5 V.

A voltage regulator? Maybe not

Before I learnt about buck converters, I experimented with voltage regulators. They’re handy little components that provide a stable lower voltage, but they aren’t very efficient. Like voltage dividers, they regulate voltage by dissipating excess energy as heat.

Voltage regulator dissipating energy

This is a picture taken with a Topdon tc001 thermal camera and after just a few seconds the voltage regulator was at 64°C.

Buck Converter

Before I knew much about DC-DC conversion, I kept thinking: <<Come on, it can’t be that we have to waste most of the energy as heat… there must be a more efficient way>>

I discovered buck converters while experimenting with 12V batteries. I quickly learned (though it wasn’t obvious to me at first) that these batteries don’t actually provide a stable 12V – they fluctuate between 11V and 15V depending on whether the engine is running, the battery’s charge level, and other factors.

This buck converter takes an input voltage above 5V (ideally around 9V or more) and steps it down to a stable 5V output. Its efficiency ranges from 70% to over 90%, which is a big improvement over linear regulators. It’s a ready-to-use board with a fixed 5V output, built-in connectors, and even an input jack for a transformer. It can deliver up to 5A of current, which means 25 watts of power. Perfect, this looks like exactly what we need!

5V to ESP in parallel with LED strip

I’m planning to use a power distribution board I bought a few months ago, knew it would come in handy eventually. It lets me split a single 5V input (+ and -) into multiple parallel 5V outputs (+ and -). While the total input power is limited to 25W, each individual connector on the board is rated to handle up to 10A.

Powering from AC to 5V DC

Power and Wiring

Let’s talk about wiring. The LED strip uses 22 AWG wires, so I’ll stick with the same gauge for the connection from the buck converter to the power distribution board, and from there to both the ESP and the LED strip. The ESP won’t draw more than 400mA, while the LED strip could pull up to 3.6A at full load (18W @ 5V). Fortunately, 22 AWG should still handle that, but it’s something to keep in mind.

EXIF_HDL_ID_1

Now, there’s another challenge: the transformer socket is 4 meters away from the back of the garage. That means I need to run ~4 meters of DC wire (+ and -) to carry 12V to the buck converter. Here’s where things get interesting: wire length, voltage, current, and even temperature all start to matter a lot. The longer the wire, the higher the voltage drop. And if we’re drawing the same power at a lower voltage, the current increases, which makes the drop worse. Plus, thinner wires (higher AWG) have more resistance and less current capacity.

4m 18AWG wire – 12V, max 20W

For this 4-meter run, I’m using an 18 AWG automotive wire, which should be fine for 12V and ~20W. I couldn’t find a good table that combines AWG, length, and current ratings, so I asked ChatGPT o3 for a recommendation, fingers crossed the wires don’t overheat 🤞 (and if you have a good reference table, please share it in the comments!).

The plan is to connect the 12V transformer to the 4-meter wire, and place the buck converter at the end, right near the devices. The idea is that sending power at a higher voltage keeps the current lower, minimizing losses, and then I step it down to 5V only at the end. Let me know if that’s the right approach!

Connecting all together

I tried to manually draw a full schema with pen and paper (actually an e-Ink tablet), but I think it’s far better this time to draw it using something like draw.io.

Firmware and Logic

What’s still missing? The complete firmware that reads the distance from the HC-SR04 sensor and controls the LEDs accordingly, turning on red, white, or green based on how far the object is.

The project’s code is already on this GitHub poeticoding/parking-sensor repo, and if development continues I’ll also add the PCB layout and 3D‑printable enclosure there.

#include <FastLED.h>

// ——— LED CONFIGURATION ———
// This is tuned for 60 leds
#define LED_PIN           7      // GPIO pin for LED strip data line
#define NUM_LEDS          60     // Total number of LEDs on the strip
#define BRIGHTNESS        255    // LED brightness (0-255)
#define LEDS_PER_TRIPLET  3      // Number of LEDs grouped together
#define MAX_TRIPLETS      10     // Maximum number of triplets (30 LEDs per side)


// ——— DISTANCE SENSOR CONFIGURATION ———
#define ULTRASONIC_TRIG_PIN    5
#define ULTRASONIC_ECHO_PIN    12
#define SOUND_SPEED_CM_US      0.0343  // Speed of sound in cm/microsecond

// ——— PROXIMITY THRESHOLDS ———
#define DANGER_DISTANCE_CM     20      // Very close - all red
#define MAX_DETECTION_CM       200     // Maximum useful detection range
#define DISTANCE_PER_LEVEL     20      // Distance range per LED level
#define COLOR_CHANGE_LEVEL     3       // Level where color changes from red to white

// ——— TIMING ———
#define LOOP_DELAY_MS         100
#define STARTUP_BLINK_MS      500


CRGB ledStrip[NUM_LEDS];

/**
* Lights up a triplet of LEDs symmetrically from both ends of the strip
* @param tripletIndex: Which triplet to control (0-9)
* @param color: Color to set the triplet to
*/
void setSymmetricalTriplet(int tripletIndex, CRGB color) {
 int startIndex = tripletIndex * LEDS_PER_TRIPLET;
 
 // Light up triplet from the beginning of strip
 ledStrip[startIndex] = color;
 ledStrip[startIndex + 1] = color;
 ledStrip[startIndex + 2] = color;
 
 // Light up corresponding triplet from the end of strip (mirrored)
 ledStrip[NUM_LEDS - (startIndex + 1)] = color;
 ledStrip[NUM_LEDS - (startIndex + 2)] = color;
 ledStrip[NUM_LEDS - (startIndex + 3)] = color;
}

/**
* Sets the progress bar display from both ends toward center
* @param progressLevel: How many triplets to light up (1-10)
* @param color: Color for the lit triplets
*/
void setProximityDisplay(uint8_t progressLevel, CRGB color) {
 for(int triplet = 0; triplet < MAX_TRIPLETS; triplet++) {
   if(triplet < progressLevel) {
     setSymmetricalTriplet(triplet, color);
   } else {
     setSymmetricalTriplet(triplet, CRGB::Black);  // Turn off unused triplets
   }
 }
}

/**
* Measures distance using HC-SR04 ultrasonic sensor
* @return: Distance in centimeters
*/
float measureDistanceCM() {
 // Send ultrasonic pulse
 digitalWrite(ULTRASONIC_TRIG_PIN, LOW);  
 delayMicroseconds(2);  
 digitalWrite(ULTRASONIC_TRIG_PIN, HIGH);  
 delayMicroseconds(10);  
 digitalWrite(ULTRASONIC_TRIG_PIN, LOW);  

 // Measure echo duration
 float pulseDuration = pulseIn(ULTRASONIC_ECHO_PIN, HIGH);

 // Convert duration to distance: (time * speed_of_sound) / 2
 return (pulseDuration * SOUND_SPEED_CM_US) / 2;
}

/**
* Startup indicator - blinks all LEDs red
*/
void startupBlink() {
 setProximityDisplay(MAX_TRIPLETS, CRGB::Red);
 FastLED.show();
 delay(STARTUP_BLINK_MS);
 setProximityDisplay(0, CRGB::Black);
 FastLED.show();
}

void setup() {
//  Serial.begin(115200);
 
 // Initialize LED strip
 FastLED.addLeds<WS2812B, LED_PIN, GRB>(ledStrip, NUM_LEDS);
 FastLED.setBrightness(BRIGHTNESS);
 
 delay(500);
 
 // Initialize ultrasonic sensor pins
 pinMode(ULTRASONIC_TRIG_PIN, OUTPUT);  
 pinMode(ULTRASONIC_ECHO_PIN, INPUT);  

 // Signal successful startup
 startupBlink();
}

void loop() {
 float currentDistance = measureDistanceCM();
 
 // DANGER ZONE: Object very close (≤20cm) - All LEDs red
 if(currentDistance <= DANGER_DISTANCE_CM) {
   setProximityDisplay(MAX_TRIPLETS, CRGB::Red);
 } 
 // PROXIMITY ZONE: Object in detection range (20-200cm)
 else if(currentDistance > DANGER_DISTANCE_CM && currentDistance < MAX_DETECTION_CM) {
   // Calculate how many LED levels to show based on distance
   // Closer objects = more LEDs lit
   uint8_t distanceLevel = (uint8_t)(currentDistance / DISTANCE_PER_LEVEL);
   uint8_t ledsToLight = MAX_TRIPLETS - distanceLevel;
   
   // Choose color: Red for close objects, White for moderate distance
   CRGB proximityColor = (distanceLevel >= COLOR_CHANGE_LEVEL) ? CRGB::White : CRGB::Red;
   
   setProximityDisplay(ledsToLight, proximityColor);
 } 
 // SAFE ZONE: Object far away or out of range (>200cm) - Single green indicator
 else {
   setProximityDisplay(1, CRGB::Green);
 }
 
 // Update the LED display
 FastLED.show();
 delay(LOOP_DELAY_MS);
}

The setup uses a 60-LED strip arranged so that LEDs light up from both ends toward the center, creating a mirrored effect. Based on the distance readings from the ultrasonic sensor, the system behaves as follows:

When objects are very far away (over 200cm or out of range), only one LED triplet on each end glows green, indicating “all clear.” As an object approaches and gets within 200cm, the system enters a progressive warning mode where more LED triplets illuminate as the object gets closer. The color transitions from white for moderate distances to red for closer proximity. When an object comes within 20cm, all LEDs turn red, providing a clear warning signal.

Final Result

Here’s the prototype up and running! It’s functional but still pretty rough. I’m not brave enough to leave it plugged in because I don’t fully trust my soldering 😅. I’d love a cleaner build with no dangling wires.

The next step might be a compact PCB: feed 12 V into the board, integrate the buck converter, ESP, and ultrasonic sensor, and let the PCB power the LEDs. Or should I keep the modules separate? An all‑in‑one board would definitely look tidier…