Smart Distance Monitoring System

by s225577153 in Circuits > Arduino

53 Views, 1 Favorites, 0 Comments

Smart Distance Monitoring System

Untitled 2.png

This project presents a Smart Distance Monitoring System designed to detect nearby obstacles in real time and provide clear, accurate feedback to the user. The system uses an HC-SR04 ultrasonic sensor to measure distance continuously, and responds through three output channels, an LCD display showing the exact distance, three LEDs indicating the current proximity zone, and a passive buzzer that produces different tones depending on the severity of the situation.

The idea behind this project is a practical one. In many everyday scenarios, such as reversing a vehicle, operating machinery near walls, or navigating a wheelchair in a confined space, users have limited ability to judge exact distances to nearby objects. Existing low-cost solutions typically offer only a single threshold alarm, which provides no sense of scale or urgency. This system addresses that limitation by implementing multiple response zones that scale with proximity, giving users a much accurate and more useful signal.

This guide provides a complete walkthrough of the hardware setup, wiring, code structure, and design decisions involved in building this system. It is written in a way that someone with a basic understanding of Arduino can follow these steps and reproduce the project from scratch.


Supplies

Screenshot 2026-05-15 at 11.31.36 am.png
Screenshot 2026-05-15 at 11.37.06 am.png
Screenshot 2026-05-15 at 11.37.19 am.png
Screenshot 2026-05-15 at 11.37.38 am.png

The following table lists all components used in this project. All parts are widely available from electronics suppliers and online stores.

Component Model / Specification Purpose


| Arduino | Arduino Nano 33 IoT | Main microcontroller

| Ultrasonic Sensor | HC-SR04 | Measures distance to nearby objects

| LCD Display | 16x2 with I2C adapter module | Displays live distance and system status

| Green LED | 5mm standard LED | Safe and scanning zone indicator

| Yellow LED | 5mm standard LED | Warning zone indicator

| Red LED | 5mm standard LED | Danger zone indicator

| Passive Buzzer | Passive buzzer module | Audible alert with variable tones

| Resistors | 220 ohm x3 | Current limiting for LEDs

| Jumper Wires Male-to-male and Male-to-female | All circuit connections

| Breadboard | Full-size 830-point breadboard | Prototyping platform

| USB Cable | Micro USB | Power supply and code upload

Note: This project requires a passive buzzer, not an active one. An active buzzer produces only a single fixed tone and cannot be controlled using the Arduino tone() function. A passive buzzer allows the frequency and pattern to be programmed, which is essential for this project.

Project Purpose and Design Goals

The primary goal of this project was to build a distance monitoring system that behaves like a real embedded system rather than a simple sensor demonstration. This meant meeting several specific design requirements beyond just reading a distance value and displaying it.

Multi-zone response: The system must distinguish between different levels of proximity and respond differently to each, rather than triggering a single alarm at one threshold e.g No beep for green led(indicating safe zone), a low pitch tone for yellow led(indicating that the object is a little bit closer), a high pitch volume tone for red led(indicating that the object is very close).

Non-blocking operation: All components, the sensor, LCD, and buzzer must update independently without any one of them pausing the others. This is achieved using millis() based timing throughout the code.

Fault tolerance: The system must detect and handle sensor failures gracefully, displaying meaningful error information rather than showing incorrect data as if it were valid.

• Noise reduction: Individual sensor readings can be affected by reflections and interference. The system must average multiple readings and discard invalid ones to produce reliable distance values.

These requirements guided every design decision in the project, from pin configuration choices to the structure of the main loop.


Why Build This Project

The idea behind this project came from a very common problem. When reversing a car or moving a wheelchair near furniture, most people have no way of knowing precisely how many centimetres of space they have left. A single beep alarm is not very helpful because it does not tell you whether you are 40 cm away or 5 cm away. The response should change depending on the situation, and that is what this system does.

Beyond the everyday use case, this project also demonstrates core concepts of embedded systems development including sensor interfacing, decision-making logic, multi-output control, error handling, and signal averaging. These are exactly the skills that real engineers apply when designing safety-critical systems.

The system is also low cost, easy to reproduce, and does not require any special tools beyond a basic Arduino setup. Anyone reading this guide should be able to build and run it themselves with minimal effort and expense.

How the System Works

Screenshot 2026-05-17 at 3.39.00 pm.png

The system operates as a continuous loop with two independent time-based processes. The first process reads the ultrasonic sensor every 80 milliseconds. The second process updates the LCD and all output devices every 300 milliseconds. These two processes run independently using millis() timing, meaning neither process blocks or pauses the other.


During each sensor read cycle, the system fires five ultrasonic pulses and collects the results. Readings outside the valid range of 2 cm to 400 cm are discarded. The remaining valid readings are averaged to produce a stable, noise-reduced distance value. The system also tracks how many readings timed out versus how many returned invalid data, as this distinction is used for fault detection.


During each output update cycle, the system evaluates the current distance and fault flags, determines the active zone, and updates the LCD, LEDs, and buzzer accordingly.



| Zone | Distance Range | LED Active | Buzzer | LCD Display |


| Scanning | No object or above 150 cm | Green | Silent | Scanning... |

| Safe | 30 cm to 150 cm | Green | Silent | Distance in cm (Smoothed) |

| Warning | 10 cm to 30 cm | Yellow | 1000 Hz short tone | Distance in cm (Smoothed) |

| Danger | Below 10 cm | Red | 1600 Hz continuous tone | Distance in cm (Smoothed) |

| Sensor Error | Invalid or fault signal | All off | Silent | SENSOR ERROR! / Check Pins D2/D3 |

The buzzer uses two different frequencies intentionally. The 1000 Hz tone in the warning zone produces a moderate alert, while the 1600 Hz continuous tone in the danger zone is noticeably higher pitched and more urgent. This frequency difference gives users an immediate sense of severity even without looking at the display.

Wiring the Circuit

Screenshot 2026-05-17 at 3.43.01 pm.png

It is recommended to build the circuit in stages, checking each group of connections before moving on to the next. The most common cause of problems in breadboard projects is a loose or incorrectly placed wire, so taking a methodical approach saves time overall.

| Component | Component Pin | Arduino Pin | Notes |


| HC-SR04 | VCC | 5V | Must use 5V, not 3.3V |

| HC-SR04 | GND | GND | Ground |

| HC-SR04 | TRIG | D2 | Trigger pulse output |

| HC-SR04 | ECHO | D3 | Echo return — INPUT_PULLDOWN |

| LCD (I2C) | VCC | 5V | Power |

| LCD (I2C) | GND | GND | Ground |

| LCD (I2C) | SDA | A4 (SDA) | I2C data line |

| LCD (I2C) | SCL | A5 (SCL) | I2C clock line |

| Green LED | Anode (+) | D4 via 220 ohm resistor | Safe and scanning zone |

| Yellow LED | Anode (+) | D5 via 220 ohm resistor | Warning zone |

| Red LED | Anode (+) | D6 via 220 ohm resistor | Danger zone |

| All LEDs | Cathode (-) | GND | Common ground |

| Buzzer | Positive | D7 | Alert signal output |

| Buzzer | Negative | GND | Ground |

Setting Up Your Software Environment

Screenshot 2026-05-15 at 11.31.28 am.png

Before you can write and upload code to the Arduino i33, make sure your development environment is configured correctly with the right libraries installed.

Install the Arduino IDE

1. Download and install the Arduino IDE from https://www.arduino.cc/en/software

2. Open the IDE and go to Tools > Board > Boards Manager.

3. Search for Arduino SAMD Boards and install it. This adds support for the Arduino Nano 33 IoT.

4. Connect your Arduino Nano 33 IoT. via Micro USB cable.

5. Go to Tools > Board and select Arduino Nano 33 IoT.

6. Go to Tools > Port and select the correct COM port for your board.

Install Required Libraries

Install both libraries through the Library Manager (Sketch > Include Library > Manage Libraries).

• LiquidCrystal_I2C — by Frank de Brabander, version 1.1.2 or later. Allows I2C communication with the 16x2 LCD.

• Wire — built-in Arduino library for I2C. Included automatically, no separate install needed

Writing and Understanding the Code



The code is organised into three clear layers that reflect standard embedded systems design practice: a sensing layer that handles data acquisition, a decision layer that determines the current system state, and an output layer that controls all physical responses. This separation makes each part of the code independently readable, testable, and modifiable.

Pin Definitions and Global Variables

All hardware pin assignments and timing constants are defined at the top of the file. Using named definitions rather than hardcoded numbers throughout the code makes it straightforward to adjust settings or reassign pins without needing to search through the entire codebase.

#include <Wire.h>
#include <LiquidCrystal_I2C.h>

#define TRIG_PIN 2
#define ECHO_PIN 3
#define GREEN_LED 4
#define YELLOW_LED 5
#define RED_LED 6
#define BUZZER_PIN 7

LiquidCrystal_I2C lcd(0x27, 16, 2);

// Non-blocking timing
unsigned long lastSensorRead = 0;
const long sensorInterval = 80;
unsigned long lastLcdUpdate = 0;
const long lcdInterval = 300;

// State variables
int averageDistance = 0;
bool sensorFaultTriggered = false;
bool noTargetInView = false;


// Logging variables
String currentZone = "STARTUP";
String previousZone = "";
unsigned long zoneStartTime = 0;
const int NUM_READINGS = 5;


The sensorInterval and lcdInterval constants define how frequently each process runs. The sensor reads every 80ms and the LCD updates every 300ms. Because these are managed with millis() rather than delay(), both operate simultaneously without interference.Using named constants means adjusting any threshold or pin requires one change in one place. The two interval constants control how frequently each process runs independently.

Sensing Layer

The sensing layer runs every 80 milliseconds. It fires five ultrasonic pulses, validates each result, and produces an averaged distance value from the valid readings. The system tracks valid sample count and timeout count separately, as both values are needed for accurate fault classification.

void runSensingLayer() {
long totalDuration = 0;
int validSamplesCount = 0;
int timeoutSamplesCount = 0;

for (int i = 0; i < NUM_READINGS; i++) {
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);

long singleDuration = pulseIn(ECHO_PIN, HIGH, 15000);

if (singleDuration > 0) {
int checkDist = singleDuration * 0.034 / 2;
if (checkDist >= 2 && checkDist <= 400) {
totalDuration += singleDuration;
validSamplesCount++;
}
} else {
timeoutSamplesCount++;
}
delayMicroseconds(500);
}

// Fault classification
if (validSamplesCount < 2 && timeoutSamplesCount < NUM_READINGS) {
sensorFaultTriggered = true;
noTargetInView = false;
} else if (timeoutSamplesCount == NUM_READINGS) {
noTargetInView = true;
sensorFaultTriggered = false;
averageDistance = 999;
} else {
sensorFaultTriggered = false;
noTargetInView = false;
long avgDuration = totalDuration / validSamplesCount;
averageDistance = avgDuration * 0.034 / 2;
}
}


The 15000 microsecond timeout passed to pulseIn prevents the program from freezing indefinitely when no echo is received. Only readings within the sensor's reliable range of 2 to 400 centimetres are accepted. The 500 microsecond delay between individual pulses prevents one pulse from interfering with the echo detection of the previous one.This function fires five pulses and tracks two separate counters. validSamplesCount counts readings that came back within time and fell in the valid range. timeoutSamplesCount counts readings where no echo returned within 15ms. These two counters are what allow the system to tell the difference between a broken sensor and an empty room."

Function-3 Log Data

void logData(String zone, int distance, String event) {
unsigned long ts = millis();

Serial.print("[LOG] ");
Serial.print(ts);
Serial.print("ms | ");
String paddedZone = zone;
while (paddedZone.length() < 10) paddedZone += " ";
Serial.print(paddedZone);
Serial.print(" | ");
if (distance == 999) Serial.print("--- ");
else { Serial.print(distance); Serial.print(" cm "); }
Serial.print(" | ");
Serial.println(event);

if (zone != previousZone) {
if (previousZone != "") {
unsigned long duration = ts - zoneStartTime;
Serial.print("[TRANSITION] ");
Serial.print(previousZone);
Serial.print(" to ");
Serial.print(zone);
Serial.print(" (spent ");
Serial.print(duration);
Serial.println("ms in previous zone)");
}
previousZone = zone;
zoneStartTime = ts;
}
}

This function prints structured log entries to the Serial Monitor every time the output layer runs. It also detects zone changes and logs how much time was spent in the previous zone. This makes the system's internal state directly readable during testing rather than having to infer it from the LEDs.



Function 4- Loop

The main loop contains two independent millis() gated blocks. The sensing block runs every 80ms and the output block runs every 300ms. Because neither uses delay(), both run concurrently. The loop executes hundreds of times per second and on each pass checks whether each interval has elapsed.

void loop() {
unsigned long currentMillis = millis();

if (currentMillis - lastSensorRead >= sensorInterval) {
lastSensorRead = currentMillis;
runSensingLayer();
}

if (currentMillis - lastLcdUpdate >= lcdInterval) {
lastLcdUpdate = currentMillis;
runOutputLayer();
}
}


Output Layer - LCD, LEDs and Buzzer Control

The output layer runs every 300 milliseconds. It reads the current state flags and distance value, then updates all output devices accordingly. The LCD refresh rate is deliberately limited to 300ms to prevent visible flickering on the display.

void runOutputLayer() {
lcd.clear();

if (sensorFaultTriggered) {
lcd.setCursor(0, 0); lcd.print("SENSOR ERROR! ");
lcd.setCursor(0, 1); lcd.print("Check Pins D2/D3");
digitalWrite(GREEN_LED, LOW);
digitalWrite(YELLOW_LED, LOW);
digitalWrite(RED_LED, LOW);
noTone(BUZZER_PIN);
logData("FAULT", 0, "Sensor fault detected");

} else if (noTargetInView || averageDistance > 150) {
lcd.setCursor(0, 0); lcd.print("Target Status: ");
lcd.setCursor(0, 1); lcd.print("Scanning... ");
digitalWrite(GREEN_LED, HIGH);
digitalWrite(YELLOW_LED, LOW);
digitalWrite(RED_LED, LOW);
noTone(BUZZER_PIN);
logData("SCANNING", averageDistance, "No object in range");

} else {
lcd.setCursor(0, 0); lcd.print("Target Status: ");
lcd.setCursor(0, 1);
lcd.print(averageDistance);
lcd.print(" cm (Smoothed) ");

if (averageDistance > 30) {
digitalWrite(GREEN_LED, HIGH);
digitalWrite(YELLOW_LED, LOW);
digitalWrite(RED_LED, LOW);
noTone(BUZZER_PIN);
logData("SAFE", averageDistance, "Object in safe zone");

} else if (averageDistance > 10) {
digitalWrite(GREEN_LED, LOW);
digitalWrite(YELLOW_LED, HIGH);
digitalWrite(RED_LED, LOW);
tone(BUZZER_PIN, 1000); delay(50); noTone(BUZZER_PIN);
logData("WARNING", averageDistance, "Object in warning zone");

} else {
digitalWrite(GREEN_LED, LOW);
digitalWrite(YELLOW_LED, LOW);
digitalWrite(RED_LED, HIGH);
tone(BUZZER_PIN, 1600);
logData("DANGER", averageDistance, "OBJECT TOO CLOSE");
}
}
}

Uploading and Testing

Once you have wired the circuit and written the code, you are ready to upload and test the system.

1. Open the Arduino IDE and paste the full code into a new sketch.

2. Go to Sketch > Verify/Compile and fix any errors shown before continuing.

3. Connect your Arduino i33 via USB and select the correct board and port under Tools.

4. Click Upload. The IDE will compile and send the code to the board.

5. The LCD should display the welcome message for two seconds then show live distance readings.

6. Hold your hand in front of the sensor and move it closer. Above 40 cm the green LED is on. Between 20 and 40 cm the yellow LED comes on with slow beeping. Below 20 cm the red LED lights up with fast urgent beeping.

7. Cover the sensor completely to test error handling. The LCD should show the error message and all LEDs should turn off.


Serial log output during normal operation

Input: System running across all zones. Expected: Serial Monitor shows correct zone name, smoothed distance, and timestamp on every output cycle. Result: Pass.


Serial log zone transition

Input: Object moved between zones. Expected: Transition entry logged with previous zone name and time spent in that zone. Result: Pass.


Serial log fault event

Input: Echo wire disconnected while system is running. Expected: Fault event logged immediately with timestamp and message identifying D3. Result: Pass.

Common Problems and Solutions

  1. LCD remains blank after upload: Verify the I2C address (try 0x3F if 0x27 does not work). Confirm that Wire.begin() and lcd.init() are both present in the setup function, if the lcd does not show text but glows, indirectly it means you have adjust the brightness using a screw driver in the potentiometer(Generally Blue colour box) of I2C Backpack.
  2. • Distance readings are unstable or jumping: Check that all breadboard connections are firmly seated. Confirm TRIG is connected to D2 and ECHO to D3 and that they are not swapped.
  3. • Buzzer produces no sound: Verify that the component is a passive buzzer. Active buzzers do not respond to the Arduino tone() function in the same way.
  4. • LEDs do not illuminate: Confirm that 220 ohm resistors are connected in series on the anode (long leg) of each LED, and that the cathode (short leg) connects to the GND rail.


Non-Blocking Timing With Millis()

One of the core design requirements of this project was to keep the system fully responsive at all times. This meant avoiding the use of delay() anywhere in the main operation of the code.

When delay() is used, the entire Arduino program pauses for that duration. During a 500ms delay, for example, the sensor cannot read, the LCD cannot update, and the buzzer cannot change state. In a real-time safety system, this is a significant problem - an object could move from the safe zone into the danger zone during a delay period and the system would completely miss the transition.

The millis() function provides a better approach. It returns the number of milliseconds that have elapsed since the Arduino powered on. By recording the timestamp of the last time each process ran and comparing it to the current time, the code can check whether enough time has passed to run that process again -without pausing anything else.

Implementation Example

unsigned long currentMillis = millis();


// Sensor process — runs every 80ms
if (currentMillis - lastSensorRead >= sensorInterval) {
lastSensorRead = currentMillis;
// Sensor reading code here
}

// Output process — runs every 300ms
if (currentMillis - lastLcdUpdate >= lcdInterval) {
lastLcdUpdate = currentMillis;
// LCD and output update code here
}


Both of these blocks exist in the same loop() function. Because neither uses delay(), both run independently at their own intervals without blocking each other. The main loop executes hundreds of times per second, and on each pass it checks whether each timer interval has elapsed and acts accordingly. This is the standard approach for managing multiple concurrent tasks in an embedded system without a real-time operating system.The HD version of this project also adds structured Serial Monitor logging as a fourth evaluation criterion called observability. Every 300ms the logData() function writes the current zone, smoothed distance, timestamp in milliseconds, and a plain-language event description to the Serial Monitor. Zone transitions are logged with the time spent in the previous zone. This makes the internal state of the system directly readable during testing rather than having to be inferred from the physical outputs alone. To view the log, open Tools, Serial Monitor in the Arduino IDE and set the baud rate to 9600.

Fault Tolerance and Error Handling

Fault tolerance is implemented at three distinct levels in this project, each addressing a different type of failure scenario.

Level 1 — Hardware Protection: INPUT_PULLDOWN on Echo Pin

The echo pin is configured with INPUT_PULLDOWN rather than the standard INPUT mode. This is a hardware-level fault prevention measure. When a pin is configured as a standard INPUT, disconnecting the wire attached to it causes the pin to float, meaning it picks up random electrical noise from the environment and produces unpredictable readings. These random values can fall within the valid range and cause the system to report phantom distances that do not correspond to any real object.

With INPUT_PULLDOWN, the pin is internally connected to ground through a resistor whenever no external signal is driving it. If the echo wire becomes disconnected, the pin immediately reads zero volts rather than floating noise. This is detected cleanly by the fault classification logic, allowing the system to enter the error state reliably rather than displaying incorrect distance data.

Level 2 — Software Fault Classification

The fault classification logic distinguishes between two situations that could both result in no valid distance reading, but which have very different causes and meanings.

The first situation is a genuinely empty room or an object beyond sensor range. In this case, all five ultrasonic pulses complete their full timeout period without receiving an echo. This is an expected and normal operating condition, the sensor is functioning correctly and there is simply nothing to detect. The system enters the Scanning state and continues normal operation.

The second situation is a hardware fault, typically a disconnected or damaged echo wire. In this case, the INPUT_PULLDOWN configuration causes the pin to read zero immediately rather than completing the full timeout. The readings return very quickly with invalid or out-of-range values, producing a pattern of fewer than two valid samples combined with fewer than five clean timeouts. This signature is distinct from the clean all-timeout pattern of an empty room, and the system correctly identifies it as a fault condition, triggering the SENSOR ERROR display.

This distinction is what makes the error handling genuinely useful. A system that only checks whether a reading is valid or not cannot tell these two situations apart. By tracking both validSamplesCount and timeoutSamplesCount independently, the system can make the correct decision in both cases.A further check reads the echo pin state before any pulse is fired. A connected sensor will drive the echo pin during operation even in an empty room, whereas a disconnected wire stays stuck LOW the entire time. This pre-trigger check makes the disconnected wire case reliable even when all five readings time out.

Level 3 — Averaging for Noise Reduction

Even during normal operation, individual ultrasonic sensor readings can be affected by surface reflections, measurement angle, or brief interference. Taking five readings per cycle and averaging only the valid ones smooths out these variations and significantly reduces the chance of a single anomalous reading causing a false zone transition. The LCD displays the word Smoothed alongside the distance value to make clear to the user that the displayed figure is a processed average rather than a single raw measurement.

Summary of Fault Responses


| Fault Scenario | Detection Method | System Response |

| Echo wire disconnected | INPUT_PULLDOWN + low valid sample count | SENSOR ERROR displayed, all LEDs off, buzzer silent |
| Object out of range / empty room| All five readings timeout cleanly | Scanning state, green LED on, buzzer silent |
| Single noisy reading | Out-of-range filter discards reading | No visible change, average unaffected |
| Multiple noisy readings | Valid sample count drops below 2 | Sensor fault triggered, error state activated |
| System recovers | Valid readings resume | Automatic recovery, normal operation resumes |

How the System Handles Real Situations

A key part of this project is that it responds intelligently to different real-world conditions, not just a single alarm scenario.

Normal Operation — Object Moving Closer

As an object moves toward the sensor, the system smoothly transitions from safe to warning to danger. Each transition updates the LCD, changes the LED, and adjusts the buzzer pattern without any flickering or delay.

Sensor Error or Disconnected Wire

If the sensor fails to return a valid reading, the averaging function returns minus one. The main loop catches this immediately, shows an error message on the LCD, and turns off all LEDs and the buzzer. You do not want the system to silently show the last known reading when the sensor is no longer working.

Rapid Movement

Because each reading is an average of five measurements, a single momentary bad reflection does not trigger a false alarm. The object would need to be genuinely close for several consecutive readings before the danger zone activates.

Object at Maximum Range

Any echo suggesting a distance beyond 400 cm is discarded by the validation check inside the averaging function and does not contribute to the average.



Potential Improvements and Extensions


• Add data logging using a micro SD card module to record distance readings over time.

• Replace the passive buzzer with a small speaker and use different musical pitches for each zone.

• Add a second ultrasonic sensor facing the opposite direction to monitor both sides simultaneously.

• Integrate Wi-Fi to send distance alerts to a phone or web dashboard. The Arduino Nano 33 IoT already has built-in Wi-Fi.

• Implement a calibration mode triggered by a button press to let the user set their own thresholds.

Reflection

Working through this project involved solving problems that were not obvious until the system was actually running. The most significant early issue was using delay() in the buzzer control code. The program was freezing during every beep, which meant the sensor stopped reading and the LCD stopped updating for the duration of each tone. Fixing that required restructuring the entire loop around millis() based timing, which ended up improving every part of the system, not just the buzzer. The fault detection design also took longer to get right than expected. An early version treated any invalid reading as an error, which caused SENSOR ERROR to appear in an empty room. Understanding the difference between how a disconnected wire and an empty room each produce different patterns in the sensor data led to the dual-counter approach that correctly handles both cases. Adding structured Serial Monitor logging to the HD version made debugging significantly faster because the exact system state at every moment was visible in text. Looking back, the logging would have been added from the very beginning of development if the project were undertaken again.

Academic Submission Notice

This article was submitted as part of an assignment to Deakin University, School of IT, Unit SIT210/730 — Embedded Systems Development.