Community project

ESP32 Parking Assistant

Photo of ESP32 Parking Assistant
Generated with AI

tm24qdjryk-design

Published November 15, 2026

This parking assistant uses ultrasonic sensors and an ESP32 to detect obstacles while reversing or parking. The system monitors distances from rear and front bumpers, displaying real-time measurements on an OLED screen and providing audio feedback through a piezo buzzer that increases in urgency as obstacles approach.

The guide includes a complete wiring diagram, parts list, and step-by-step assembly instructions for mounting sensors to the bumpers, connecting the automotive power supply with proper fusing, integrating the reverse-gear detection circuit, and programming the ESP32 firmware. Builders will learn to set up I2C communication, configure ultrasonic sensor timing, and implement distance-based alert logic.

Wiring diagram

Wiring diagram for ESP32 Parking Assistant
Loading diagram…

Gather all the parts

QtyComponent
1

JSN-SR04T Ultrasonic Sensor

JSN-SR04T waterproof ultrasonic distance sensor. Sealed transducer on a cable feeds a small driver board with HC-SR04-compatible trigger/echo timing. 25cm to 4m typical, 2m optimal. Common for water tank level, sump monitoring, and aquaponics reservoirs because the transducer survives splashes and condensation that kill an HC-SR04.

1

JSN-SR04T Ultrasonic Sensor

JSN-SR04T waterproof ultrasonic distance sensor. Sealed transducer on a cable feeds a small driver board with HC-SR04-compatible trigger/echo timing. 25cm to 4m typical, 2m optimal. Common for water tank level, sump monitoring, and aquaponics reservoirs because the transducer survives splashes and condensation that kill an HC-SR04.

1

0.96" OLED Display 128x64 I2C (Blue)

Monochrome 0.96 inch OLED display with 128 x 64 blue pixels, driven by an SSD1306 controller over I2C (address 0x3C). It runs from 3.3 to 5 V and connects through a four-pin VCC, GND, SCL, SDA header.

1

Piezo Buzzer

Small passive piezo buzzer (TDK PS1240) for beeps, tones and alerts. It has no built-in oscillator, so drive one lead with a 3-30 V square wave from a microcontroller pin using tone() or PWM and connect the other lead to ground. It is loudest around 4 kHz and works from about 2 to 10 kHz; driving both leads in antiphase roughly doubles the volume.

1

PC817 Optocoupler

Single-channel optocoupler (opto-isolator) in a 4-pin DIP package. Galvanically isolates a low-voltage logic side from a separate circuit by coupling an internal infrared LED to a phototransistor across an insulating gap (isolation voltage ~5 kV). Used to read a higher-voltage signal safely, switch between circuits with different grounds, or shield a microcontroller from a noisy or mains-referenced domain. The input LED needs a series current-limiting resistor (~1 kOhm from a 5 V signal, ~220-330 Ohm from 3.3 V) sized for ~5-20 mA forward current; the output transistor is typically wired open-collector with a pull-up to the MCU side.

1

2.2 kΩ 0.25 W resistor

2.2 kΩ

This resistor limits the current from the car's reverse-light wire before it reaches the optocoupler.

1

10 kΩ 0.25 W resistor

10 kΩ

This resistor is the upper half of the left sensor's voltage divider, making its 5 V signal safe for the ESP32.

1

20 kΩ 0.25 W resistor

20 kΩ

This resistor is the lower half of the left sensor's voltage divider, protecting the ESP32 input.

1

10 kΩ 0.25 W resistor

10 kΩ

This resistor is the upper half of the right sensor's voltage divider, making its 5 V signal safe for the ESP32.

1

20 kΩ 0.25 W resistor

20 kΩ

This resistor is the lower half of the right sensor's voltage divider, protecting the ESP32 input.

1

Momentary push button

This small push button lets the driver turn on future front sensors while manoeuvring.

1

Inline blade-fuse holder with 1 A fuse

1 A

This fused holder protects the new parking-assist wiring if a cable is damaged or shorted.

1

Sealed automotive 8–32 V to 5 V DC-DC converter

This enclosed vehicle converter changes the car's 12 V supply into regulated 5 V for the ESP32 and sensors.

1

12 V vehicle battery

This is the car's existing 12 V battery, which supplies the fused parking-assist circuit.

1

JSN-SR04T Ultrasonic Sensor

JSN-SR04T waterproof ultrasonic distance sensor. Sealed transducer on a cable feeds a small driver board with HC-SR04-compatible trigger/echo timing. 25cm to 4m typical, 2m optimal. Common for water tank level, sump monitoring, and aquaponics reservoirs because the transducer survives splashes and condensation that kill an HC-SR04.

1

JSN-SR04T Ultrasonic Sensor

JSN-SR04T waterproof ultrasonic distance sensor. Sealed transducer on a cable feeds a small driver board with HC-SR04-compatible trigger/echo timing. 25cm to 4m typical, 2m optimal. Common for water tank level, sump monitoring, and aquaponics reservoirs because the transducer survives splashes and condensation that kill an HC-SR04.

1

Resistor

10 kΩ

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

20 kΩ

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

10 kΩ

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

20 kΩ

Through-hole resistor (current-limiting in series with an LED)

Assemble it in 8 steps

1. Stromversorgung sicher vorbereiten

Klemme vor Arbeiten an der Fahrzeugverkabelung den Minuspol der 12-V-Batterie ab. Setze den 1-A-Sicherungshalter power_fuse so nah wie möglich an die 12-V-Abnahme und verbinde seine Eingangsseite mit einer fachkundig geprüften 12-V-Zündungsplusleitung, nicht mit dauerhaftem Batterieplus. Prüfe, dass die Versorgung bei ausgeschalteter Zündung aus ist; eine konkrete Toyota-Leitung oder Sicherungsnummer ist hier nicht festgelegt. Verbinde die Ausgangsseite mit VIN+ des Wandlers vehicle_buck; VIN- des Wandlers kommt an Fahrzeugmasse.

  • Montiere Sicherung und Wandler trocken im Kofferraum hinter einer Verkleidung.
  • Verwende gecrimpte oder gelötete, isolierte Verbindungen und befestige die Kabel gegen Scheuern.
  • Arbeite nie an ungesicherter Fahrzeugspannung: ein kurzgeschlossenes Kabel kann heiß werden oder einen Fahrzeugbrand verursachen.
  • Leitungen von Airbags, Gurtstraffern und gelben Steckern nicht berühren; dort darf diese Schaltung nicht angeschlossen werden.

2. Controller und Sensorstrom anschließen

Verbinde den 5-V-Ausgang VOUT+ des vehicle_buck mit VIN beziehungsweise 5V des ESP32 und mit VCC beider Sensoren rear_sensor_left und rear_sensor_right. Verbinde VOUT- mit GND des ESP32 und mit GND beider Sensoren. Die Sensoren erhalten damit 5 V, der ESP32 erzeugt daraus seine sichere 3,3-V-Logik.

  • Lege alle Elektronikteile in ein festes, spritzwassergeschütztes Gehäuse im Kofferraum.
  • Die Sensorleitungen erst nach dem mechanischen Einbau auf die richtige Länge bringen.
  • Die 5-V-Leitung niemals an einen 3,3-V-Pin des ESP32 anschließen; falsche Versorgung kann den Controller beschädigen.

3. Die hinteren Sensoren am Stoßfänger befestigen

Bohre zwei passende Öffnungen links und rechts in den hinteren Stoßfänger und setze die Schallköpfe von rear_sensor_left und rear_sensor_right waagerecht ein. Lasse zwischen den Sensoren und dem Boden genug Abstand, damit sie nicht auf die Straße messen. Führe ihre Kabel durch vorhandene Gummitüllen in den Kofferraum.

  • Vor dem Bohren von innen prüfen, dass hinter dem Stoßfänger keine Verstrebung oder Leitung getroffen wird.
  • Die beiden Sensoren möglichst gleich hoch und gerade nach hinten ausrichten.
  • Ein Sensor, der zum Boden, Auspuff oder Reifen zeigt, meldet falsche Hindernisse und ist keine zuverlässige Einparkhilfe.

4. Die sicheren Sensorsignale verdrahten

Verbinde TRIG von rear_sensor_left mit GPIO25 und TRIG von rear_sensor_right mit GPIO26. Verbinde ECHO links zuerst mit echo_left_top; dessen anderer Anschluss geht zu GPIO27. Verbinde echo_left_bottom zwischen denselben GPIO27-Anschluss und GND. Wiederhole dies rechts: ECHO zu echo_right_top, dessen anderer Anschluss zu GPIO33 und echo_right_bottom von diesem Punkt zu GND. So machen die 10-kΩ- und 20-kΩ-Widerstände aus dem 5-V-Echo ein ungefährliches 3,3-V-Signal.

  • Halte die Widerstände zusammen mit dem ESP32 im Gehäuse, nicht draußen am Stoßfänger.
  • Markiere links und rechts vor dem Schließen der Verkleidung.
  • ECHO nicht direkt mit dem ESP32 verbinden: die 5 V vom Sensor können den 3,3-V-Eingang beschädigen.

5. Display, Ton und Fronttaste montieren

Montiere parking_oled gut sichtbar am Armaturenbrett oder nahe dem Innenspiegel. Verbinde VCC mit 3V3, GND mit GND, SDA mit GPIO21 und SCL mit GPIO22. Verbinde SIG von warning_buzzer mit GPIO14 und seinen zweiten Anschluss mit GND. Verbinde einen Anschluss von front_enable_button mit GPIO13 und den anderen mit GND.

  • Wähle für das Display einen Ort, der die Sicht auf die Straße nicht verdeckt.
  • Die Taste schaltet die Frontsensoren für maximal 15 Sekunden ein; erneutes Drücken schaltet sie aus. Im Rückwärtsgang wird die Taste ignoriert; hinten hat Vorrang.
  • VCC und GND am Display nicht vertauschen, vertauschte Versorgung kann das Display beschädigen.

6. Rückwärtsgang sicher erkennen

Verbinde die positive Leitung der Rückfahrleuchte über reverse_input_resistor mit Anode des reverse_opto. Verbinde die Kathode des reverse_opto mit Fahrzeugmasse. Auf der sicheren ESP32-Seite verbindest du Collector mit GPIO32 und Emitter mit GND. Der Optokoppler trennt das 12-V-Signal des Autos von der empfindlichen Logik.

  • Miss die Leitung der Rückfahrleuchte mit einem Multimeter: sie darf nur bei eingelegtem Rückwärtsgang 12 V führen.
  • Setze den Abzweig mit einem geeigneten Fahrzeug-Steckverbinder, nicht mit blank verdrilltem Draht.
  • Das Rückfahrlicht niemals direkt an GPIO32 anschließen; 12 V und Spannungsspitzen können den ESP32 sofort beschädigen.

7. Vor dem Schließen testen

Schließe den Batterie-Minuspol wieder an. Prüfe zuerst, dass der ESP32 und das Display mit 5 V vom Wandler laufen. Lege bei gesichertem Fahrzeug und getretener Bremse den Rückwärtsgang ein: das Display muss die zwei hinteren Abstände zeigen und der Summer bei einem nahen Karton schneller piepen. Erst danach Verkleidungen schließen und alle Kabel mit Kabelbindern sichern.

  • Teste zunächst mit einem Karton, nicht mit einer Person oder einem Fahrzeug.
  • Die Einparkhilfe ist eine Zusatzhilfe; beim Rangieren weiterhin Kamera, Spiegel und direkte Sicht benutzen.
  • Die Anzeige ersetzt keine Sichtprüfung: kleine, sehr schmale oder schallabsorbierende Hindernisse können Ultraschallsensoren übersehen.

8. Zwei Frontsensoren ergänzen

Montiere front_sensor_left und front_sensor_right gerade nach vorne. Nur die Sensorköpfe sind wasserfest; die Platinen müssen trocken und geschützt bleiben. Verbinde beide VCC mit geregelten 5 V und beide GND mit ESP32-GND. TRIG vorne links geht an GPIO18, TRIG vorne rechts an GPIO19. ECHO links geht über echo_front_left_top (10 kΩ) an GPIO16. Vom selben Verbindungspunkt geht echo_front_left_bottom (20 kΩ) an GND. ECHO rechts geht über echo_front_right_top (10 kΩ) an GPIO17. Vom selben Verbindungspunkt geht echo_front_right_bottom (20 kΩ) an GND. Die Widerstände nahe am ESP32 montieren. Vorhandene hintere Anschlüsse bleiben unverändert.

  • Vor dem Bohren Leitungen und Verstrebungen prüfen. Kabel fern von Hitze, beweglichen Teilen und Airbags führen und gegen Scheuern sichern.
  • Zuerst am Tisch mit USB testen: Taste drücken, vorne links und rechts mit einem Karton in 50 bis 150 cm Abstand prüfen. Nach 15 Sekunden oder erneutem Tastendruck muss BEREIT erscheinen. Prüfe den Dauerton bei 30 cm und den Unsicherheitshinweis bei fehlendem Echo; ungerundete Werte steuern den Warnton mit 5 cm Hysterese beim Entwarnen.
  • GPIO32 mit GND verbinden simuliert Rückwärtsgang: hinten muss Vorrang haben. Nach Lösen müssen vorne 15 Sekunden lang Entfernungen erscheinen. Den Fahrzeugwandler beim USB-Test abtrennen.
  • Die ECHO-Ausgänge niemals direkt an GPIO16 oder GPIO17 anschließen. Jeder Sensor benötigt seinen eigenen Spannungsteiler.
  • Unter etwa 25 cm sind Messwerte unzuverlässig. -- bedeutet keine gültige Messung, nicht freie Fahrt. Nach drei ungültigen Messungen eines aktiven Sensors erscheint MESSUNG UNSICHER. Das kann auch bei freiem Raum ohne Echo auftreten und ist keine eindeutige Fehlerdiagnose. Kein zertifiziertes Fahrzeug-Sicherheitssystem.
  • Vor Arbeiten an der Fahrzeugelektrik die Versorgung trennen. Einbau und geschützte Fahrzeugversorgung im Zweifel fachkundig prüfen lassen.

Review all connections

1. Connections between "power_fuse" and "ESP32"

Functionpower_fuseESP32
powerOUT → Sealed automotive 8–32 V to 5 V DC-DC converter VIN+EXT
powerIN → professionally verified ignition-switched 12 V supplyEXT

2. Connections between "vehicle_buck" and "ESP32"

Functionvehicle_buckESP32
powerVOUT+ → regulated 5 V system supply5V
groundVOUT- → system groundGND
groundVIN- → 12 V vehicle battery BAT-EXT

3. Connections between "rear_sensor_left" and "ESP32"

Functionrear_sensor_leftESP32
powerVCC → sensor power5V
groundGND → sensor groundGND
dataTRIG → left distance-measurement triggerGPIO 25
dataECHO → 10 kΩ 0.25 W resistor INEXT

4. Connections between "echo_left_bottom" and "ESP32"

Functionecho_left_bottomESP32
digitalIN → 10 kΩ 0.25 W resistor OUTEXT
groundOUT → divider groundGND

5. Connections between "rear_sensor_right" and "ESP32"

Functionrear_sensor_rightESP32
powerVCC → sensor power5V
groundGND → sensor groundGND
dataTRIG → right distance-measurement triggerGPIO 26
dataECHO → 10 kΩ 0.25 W resistor INEXT

6. Connections between "echo_right_bottom" and "ESP32"

Functionecho_right_bottomESP32
digitalIN → 10 kΩ 0.25 W resistor OUTEXT
groundOUT → divider groundGND

7. Connections between "parking_oled" and "ESP32"

Functionparking_oledESP32
powerVCC → display power3V3
groundGND → display groundGND
i2cSDA → display dataGPIO 21
i2cSCL → display clockGPIO 22

8. Connections between "warning_buzzer" and "ESP32"

Functionwarning_buzzerESP32
pwmSIG → audible distance warningGPIO 14
groundGND → buzzer groundGND

9. Connections between "front_enable_button" and "ESP32"

Functionfront_enable_buttonESP32
digitalA → front-sensor activation buttonGPIO 13
groundB → button groundGND

10. Connections between "reverse_input_resistor" and "ESP32"

Functionreverse_input_resistorESP32
digitalIN → Toyota reverse-light positive wireEXT
digitalOUT → PC817 Optocoupler AnodeEXT

11. Connections between "reverse_opto" and "ESP32"

Functionreverse_optoESP32
digitalCollector → reverse-active signal; ESP32 internal pull-up usedGPIO 32
groundEmitter → logic-side optocoupler groundGND
groundCathode → 12 V vehicle battery BAT-EXT

12. Connections between "echo_left_top" and "ESP32"

Functionecho_left_topESP32
digitalOUT → safe 3.3 V left echo signalGPIO 27

13. Connections between "echo_right_top" and "ESP32"

Functionecho_right_topESP32
digitalOUT → safe 3.3 V right echo signalGPIO 33

14. Connections between "vehicle_battery" and "ESP32"

Functionvehicle_batteryESP32
groundBAT- → vehicle chassis groundEXT
powerBAT+ → existing vehicle electrical system upstream of ignition switching; not a direct parking-aid connectionEXT

15. Connections between "front_sensor_left" and "ESP32"

Functionfront_sensor_leftESP32
powerVCC5V
groundGNDGND
dataTRIGGPIO 18
dataECHO → Resistor P1EXT

16. Connections between "echo_front_left_top" and "ESP32"

Functionecho_front_left_topESP32
digitalP2GPIO 16

17. Connections between "echo_front_left_bottom" and "ESP32"

Functionecho_front_left_bottomESP32
digitalP1 → Resistor P2EXT
groundP2GND

18. Connections between "front_sensor_right" and "ESP32"

Functionfront_sensor_rightESP32
powerVCC5V
groundGNDGND
dataTRIGGPIO 19
dataECHO → Resistor P1EXT

19. Connections between "echo_front_right_top" and "ESP32"

Functionecho_front_right_topESP32
digitalP2GPIO 17

20. Connections between "echo_front_right_bottom" and "ESP32"

Functionecho_front_right_bottomESP32
digitalP1 → Resistor P2EXT
groundP2GND

Deploy the firmware

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

constexpr int LEFT_TRIG_PIN = 25;
constexpr int RIGHT_TRIG_PIN = 26;
constexpr int LEFT_ECHO_PIN = 27;
constexpr int RIGHT_ECHO_PIN = 33;
constexpr int FRONT_LEFT_TRIG_PIN = 18;
constexpr int FRONT_RIGHT_TRIG_PIN = 19;
constexpr int FRONT_LEFT_ECHO_PIN = 16;
constexpr int FRONT_RIGHT_ECHO_PIN = 17;
constexpr int REVERSE_PIN = 32;
constexpr int BUZZER_PIN = 14;
constexpr int FRONT_BUTTON_PIN = 13;
constexpr int OLED_SDA_PIN = 21;
constexpr int OLED_SCL_PIN = 22;

constexpr uint8_t SCREEN_WIDTH = 128;
constexpr uint8_t SCREEN_HEIGHT = 64;
constexpr unsigned long SAMPLE_INTERVAL_MS = 65;
constexpr unsigned long FRONT_HOLD_MS = 15000;
constexpr unsigned long ECHO_TIMEOUT_US = 25000;

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

int lastLeftCm = -2;
int lastRightCm = -2;
bool lastUncertain = false;
bool buzzerRunning = false;
int warningLevel = 0;
unsigned long lastSampleMs = 0;
unsigned long buzzerOffAtMs = 0;
unsigned long nextBuzzerAtMs = 0;

int readDistanceCm(int trigPin, int echoPin) {
  digitalWrite(trigPin, LOW);
  delayMicroseconds(3);
  digitalWrite(trigPin, HIGH);
  delayMicroseconds(10);
  digitalWrite(trigPin, LOW);

  const unsigned long pulseLength = pulseIn(echoPin, HIGH, ECHO_TIMEOUT_US);
  if (pulseLength == 0) {
    return -1;
  }

  const int distance = static_cast<int>(pulseLength / 58UL);
  if (distance < 25 || distance > 400) {
    return -1;
  }
  return distance;
}

int displayDistance(int cm) {
  if (cm < 0) {
    return -1;
  }
  return ((cm + 2) / 5) * 5;
}

void drawDistanceCell(int x, int y, int cm, bool active) {
  display.setCursor(x, y);
  if (!active) display.print("AUS");
  else if (cm < 0) display.print("-- cm");
  else { display.print(cm); display.print(" cm"); }
  const int barY = y + 9;
  display.drawRect(x, barY, 43, 6, SSD1306_WHITE);
  if (active && cm >= 0) {
    const int fill = map(200 - constrain(cm, 25, 200), 0, 175, 0, 41);
    if (fill > 0) display.fillRect(x + 1, barY + 1, fill, 4, SSD1306_WHITE);
  }
}

void drawOverview(int mode, int leftCm, int rightCm, bool uncertain) {
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.print(uncertain ? "MESSUNG UNSICHER" :
                mode == 2 ? "HINTEN AKTIV" :
                mode == 1 ? "VORNE AKTIV" : "BEREIT");
  display.setCursor(37, 10);
  display.print("LINKS   RECHTS");
  display.setCursor(0, 22);
  display.print("VORN");
  drawDistanceCell(37, 20, leftCm, mode == 1);
  drawDistanceCell(85, 20, rightCm, mode == 1);
  display.drawLine(0, 38, 127, 38, SSD1306_WHITE);
  display.setCursor(0, 46);
  display.print("HINT");
  drawDistanceCell(37, 44, leftCm, mode == 2);
  drawDistanceCell(85, 44, rightCm, mode == 2);
  display.display();
}

void drawSensorScreen(int leftCm, int rightCm, bool front, bool uncertain = false) {
  drawOverview(front ? 1 : 2, leftCm, rightCm, uncertain);
}

void drawIdleScreen(bool frontNotice) {
  drawOverview(0, -1, -1, false);
}

void stopBuzzer() {
  if (buzzerRunning) noTone(BUZZER_PIN);
  buzzerRunning = false;
  buzzerOffAtMs = 0;
}

void startBuzzer() {
  if (!buzzerRunning) tone(BUZZER_PIN, 4000);
  buzzerRunning = true;
}

void updateBuzzer(int nearestCm, unsigned long now) {
  int target = 0;
  if (nearestCm >= 0) {
    target = nearestCm <= 30 ? 4 : nearestCm <= 50 ? 3 :
             nearestCm <= 80 ? 2 : nearestCm <= 120 ? 1 : 0;
    // More urgent warnings immediately; reduce urgency only 5 cm past
    // the old threshold. No averaging that could hide a close obstacle.
    const int limits[] = {0, 120, 80, 50, 30};
    if (target < warningLevel && nearestCm <= limits[warningLevel] + 5)
      target = warningLevel;
  }
  if (target != warningLevel) {
    stopBuzzer();
    nextBuzzerAtMs = now;
    warningLevel = target;
  }
  if (warningLevel == 0) {
    stopBuzzer();
    return;
  }
  if (warningLevel == 4) {
    startBuzzer();
    buzzerOffAtMs = 0;
    return;
  }
  if (buzzerRunning && static_cast<long>(now - buzzerOffAtMs) >= 0)
    stopBuzzer();
  const unsigned long gapMs = warningLevel == 3 ? 110 : warningLevel == 2 ? 260 : 550;
  if (static_cast<long>(now - nextBuzzerAtMs) >= 0) {
    startBuzzer();
    buzzerOffAtMs = now + 70;
    nextBuzzerAtMs = now + gapMs;
  }
}

void setup() {
  pinMode(LEFT_TRIG_PIN, OUTPUT);
  pinMode(RIGHT_TRIG_PIN, OUTPUT);
  pinMode(LEFT_ECHO_PIN, INPUT);
  pinMode(RIGHT_ECHO_PIN, INPUT);
  pinMode(FRONT_LEFT_TRIG_PIN, OUTPUT);
  pinMode(FRONT_RIGHT_TRIG_PIN, OUTPUT);
  digitalWrite(FRONT_LEFT_TRIG_PIN, LOW);
  digitalWrite(FRONT_RIGHT_TRIG_PIN, LOW);
  pinMode(FRONT_LEFT_ECHO_PIN, INPUT);
  pinMode(FRONT_RIGHT_ECHO_PIN, INPUT);
  pinMode(REVERSE_PIN, INPUT_PULLUP);
  pinMode(FRONT_BUTTON_PIN, INPUT_PULLUP);
  pinMode(BUZZER_PIN, OUTPUT);
  digitalWrite(LEFT_TRIG_PIN, LOW);
  digitalWrite(RIGHT_TRIG_PIN, LOW);

  Wire.begin(OLED_SDA_PIN, OLED_SCL_PIN);
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  drawIdleScreen(false);
}

void loop() {
  const unsigned long now = millis();
  const bool reverseActive = digitalRead(REVERSE_PIN) == LOW;
  static bool previousReverse = false;
  static bool frontTimerActive = false;
  static unsigned long frontStartedMs = 0;
  static int previousMode = -1;
  static bool measureLeft = true;
  static int leftCm = -1;
  static int rightCm = -1;
  static int nearestCm = -1;
  static uint8_t leftInvalid = 0;
  static uint8_t rightInvalid = 0;
  static bool buttonRaw = false;
  static bool buttonStable = false;
  static unsigned long buttonChangedMs = 0;

  const bool pressed = digitalRead(FRONT_BUTTON_PIN) == LOW;
  if (pressed != buttonRaw) {
    buttonRaw = pressed;
    buttonChangedMs = now;
  }
  if (now - buttonChangedMs >= 30 && buttonStable != buttonRaw) {
    buttonStable = buttonRaw;
    if (buttonStable && !reverseActive) {
      frontStartedMs = now;
      frontTimerActive = !frontTimerActive;
    }
  }
  if (previousReverse && !reverseActive) {
    frontStartedMs = now;
    frontTimerActive = true;
  }
  previousReverse = reverseActive;
  if (frontTimerActive && now - frontStartedMs >= FRONT_HOLD_MS) {
    frontTimerActive = false;
  }
  const int mode = reverseActive ? 2 : (frontTimerActive ? 1 : 0);
  if (mode != previousMode) {
    stopBuzzer();
    nearestCm = -1;
    warningLevel = 0;
    leftInvalid = rightInvalid = 0;
    lastUncertain = false;
    leftCm = rightCm = -1;
    lastLeftCm = lastRightCm = -2;
    measureLeft = true;
    nextBuzzerAtMs = now;
    if (mode == 0) drawIdleScreen(false);
    else drawSensorScreen(-1, -1, mode == 1);
    previousMode = mode;
  }

  // Only one sensor transmits per slot, avoiding mutual echoes.
  if (mode != 0 && now - lastSampleMs >= SAMPLE_INTERVAL_MS) {
    lastSampleMs = now;
    const bool front = mode == 1;
    if (measureLeft) {
      leftCm = readDistanceCm(
          front ? FRONT_LEFT_TRIG_PIN : LEFT_TRIG_PIN,
          front ? FRONT_LEFT_ECHO_PIN : LEFT_ECHO_PIN);
      leftInvalid = leftCm < 0 ? min(3, int(leftInvalid) + 1) : 0;
    } else {
      rightCm = readDistanceCm(
          front ? FRONT_RIGHT_TRIG_PIN : RIGHT_TRIG_PIN,
          front ? FRONT_RIGHT_ECHO_PIN : RIGHT_ECHO_PIN);
      rightInvalid = rightCm < 0 ? min(3, int(rightInvalid) + 1) : 0;
      const int shownLeft = displayDistance(leftCm);
      const int shownRight = displayDistance(rightCm);
      const bool uncertain = leftInvalid >= 3 || rightInvalid >= 3;
      if (shownLeft != lastLeftCm || shownRight != lastRightCm || uncertain != lastUncertain) {
        drawSensorScreen(shownLeft, shownRight, front, uncertain);
        lastLeftCm = shownLeft;
        lastRightCm = shownRight;
        lastUncertain = uncertain;
      }
      nearestCm = -1;
      if (leftCm >= 0) nearestCm = leftCm;
      if (rightCm >= 0 && (nearestCm < 0 || rightCm < nearestCm)) nearestCm = rightCm;
    }
    measureLeft = !measureLeft;
  }
  if (mode != 0) updateBuzzer(nearestCm, millis());
  delay(5);
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace, diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik