Build Your Own Smart School Bus Robot! 🚌
A fun DIY smart bus project using Arduino — with a passenger counter, auto door, ticket dispenser, footboard safety, driver fatigue alarm, panic button, GPS display, and talking voice announcements. Perfect for school science fairs and robotics club!
Ever wondered how a real smart bus counts passengers, opens its own door, or announces the next stop all by itself? In this DIY robotics project, you'll build a mini model bus that does all of that using an Arduino Mega, some simple sensors, and a bit of code. No previous robotics experience needed — just follow each "bus stop" below in order!
🗺️ Our Route (What We're Building)
Full Parts List (Shopping List)
Here's everything you need for the whole bus. Don't worry — most parts cost just a few dollars and are reusable in future projects too!
| Part | Qty | Used For |
|---|---|---|
| Arduino Mega 2560 | 1 | The "brain" — controls everything |
| IR obstacle sensor module | 4 | Passenger counter (2), footboard (1), fatigue alarm (1) |
| HC-SR04 ultrasonic sensor | 1 | Auto door |
| SG90 micro servo motor | 2 | Door motor, ticket dispenser |
| 16x2 LCD with I2C backpack | 1 | GPS display / passenger count |
| NEO-6M GPS module | 1 | Location tracking |
| DFPlayer Mini MP3 module + micro SD card | 1 | Voice announcements |
| Small 8-ohm speaker | 1 | Playing announcements |
| Push button (big red arcade button ideal) | 2 | Panic button, ticket request button |
| Active buzzer | 2 | Fatigue alarm, panic alarm |
| LED (red, green) | 2 | Status indicators |
| 220Ω resistor | 2 | Protecting the LEDs |
| Breadboard + jumper wires | 1 set | All wiring |
| 9V battery / power bank + on-off switch | 1 | Powering the whole bus |
| Cardboard or foam board bus model | 1 | The bus body itself! |
The Brain: Arduino Mega 2560
We use an Arduino Mega instead of a regular Uno because our smart bus has 8 features running at once — the Mega gives us enough pins and enough memory to handle all of them together.
| Arduino Mega Pin | Connected To |
|---|---|
| 2, 3 | Passenger counter IR sensors (entry, exit) |
| 4 | Footboard safety IR sensor |
| 5 | Driver fatigue IR sensor |
| 6, 7 | Ultrasonic sensor (Trig, Echo) — auto door |
| 8 | Door servo motor (signal) |
| 9 | Ticket dispenser servo motor (signal) |
| 10 | Ticket request button |
| 11 | Panic button |
| 12 | Panic buzzer |
| 13 | Fatigue alarm buzzer |
| A0 | Red status LED (panic) |
| A1 | Green status LED (running normally) |
| SDA (20), SCL (21) | 16x2 I2C LCD screen |
| Serial1 (TX1/RX1, pins 18/19) | NEO-6M GPS module |
| Serial2 (TX2/RX2, pins 16/17) | DFPlayer Mini MP3 module |
🚏 Passenger Counter
How it works: Two IR (infrared) sensors are placed at the door — one slightly in front of the other. When someone walks in, the sensors are triggered in one order (entry → count goes up). When someone walks out, they're triggered in the opposite order (exit → count goes down).
- Mount the two IR sensors near the door frame, about 10cm apart — one closer to inside, one closer to outside.
- Connect each sensor's VCC to Arduino 5V, GND to GND.
- Connect the entry sensor's OUT pin to Arduino digital pin 2, and the exit sensor's OUT pin to pin 3.
- Connect the LCD's SDA and SCL pins to the Mega's SDA (20) and SCL (21) pins.
- Upload the code below and test by walking your hand past the sensors in each direction.
// Passenger Counter
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 16, 2);
const int entryPin = 2;
const int exitPin = 3;
int passengerCount = 0;
void setup() {
pinMode(entryPin, INPUT);
pinMode(exitPin, INPUT);
lcd.init();
lcd.backlight();
}
void loop() {
if (digitalRead(entryPin) == LOW) {
passengerCount++;
delay(500); // avoid double counting
}
if (digitalRead(exitPin) == LOW && passengerCount > 0) {
passengerCount--;
delay(500);
}
lcd.setCursor(0, 0);
lcd.print("Passengers: ");
lcd.print(passengerCount);
}
🚪 Auto Door
How it works: An ultrasonic sensor measures the distance to anything in front of the door. If something is closer than 20cm, a servo motor rotates to swing the door open. After a short delay with nothing detected, it closes again.
- Mount the ultrasonic sensor facing outward at the door, at about waist height.
- Connect Trig to pin 6 and Echo to pin 7, VCC to 5V, GND to GND.
- Attach the servo's horn to your cardboard door hinge with a small stick or wire arm.
- Connect the servo's signal wire to pin 8, power wire to 5V, ground wire to GND.
- Upload the code and test by moving your hand toward the door.
// Auto Door
#include <Servo.h>
const int trigPin = 6;
const int echoPin = 7;
Servo doorServo;
void setup() {
pinMode(trigPin, OUTPUT);
pinMode(echoPin, INPUT);
doorServo.attach(8);
doorServo.write(0); // door closed
}
long getDistanceCM() {
digitalWrite(trigPin, LOW);
delayMicroseconds(2);
digitalWrite(trigPin, HIGH);
delayMicroseconds(10);
digitalWrite(trigPin, LOW);
long duration = pulseIn(echoPin, HIGH);
return duration * 0.034 / 2;
}
void loop() {
long distance = getDistanceCM();
if (distance > 0 && distance < 20) {
doorServo.write(90); // open door
delay(3000);
} else {
doorServo.write(0); // close door
}
delay(200);
}
🦶 Footboard Safety
How it works: An IR sensor under the footboard detects if someone is standing there. If detected, a buzzer sounds and a warning shows on the LCD, reminding the "driver" to wait until everyone is fully inside.
- Place the IR sensor just under the footboard/step edge, facing upward.
- Wire VCC to 5V, GND to GND, OUT to pin 4.
- Connect a buzzer's positive leg to pin 13, and negative leg to GND.
// Footboard Safety
const int footPin = 4;
const int fatigueBuzzer = 13;
void setup() {
pinMode(footPin, INPUT);
pinMode(fatigueBuzzer, OUTPUT);
}
void loop() {
if (digitalRead(footPin) == LOW) { // someone on footboard
digitalWrite(fatigueBuzzer, HIGH);
} else {
digitalWrite(fatigueBuzzer, LOW);
}
}
🎫 Ticket Dispenser
How it works: A push button tells the Arduino "a passenger wants a ticket." The Arduino rotates a servo motor which nudges a small stack of paper strips forward, dispensing one ticket, then returns to its starting position.
- Wire the push button between pin 10 and GND (we'll use the internal pull-up resistor in code).
- Roll a small strip of paper (the "ticket roll") and rest it against the servo horn.
- Connect the servo's signal wire to pin 9.
- Press the button and watch the servo nudge a ticket forward!
// Ticket Dispenser
#include <Servo.h>
const int ticketButton = 10;
Servo ticketServo;
void setup() {
pinMode(ticketButton, INPUT_PULLUP);
ticketServo.attach(9);
ticketServo.write(0);
}
void loop() {
if (digitalRead(ticketButton) == LOW) {
ticketServo.write(90); // push ticket out
delay(700);
ticketServo.write(0); // reset
delay(500);
}
}
😴 Driver Fatigue Alarm
How it works: A small IR sensor is aimed at the driver's eye area (in our model, a toy figure). If it detects the eye is "closed" (blocked) continuously for more than a couple of seconds, it sounds an alarm and flashes a warning on the LCD.
- Position the IR sensor about 5-8cm from the driver figure's "eyes."
- Connect OUT to pin 5, VCC to 5V, GND to GND.
- Reuse the buzzer from the footboard circuit — the code below manages timing so both alarms don't conflict.
// Driver Fatigue Alarm
const int eyePin = 5;
const int alarmBuzzer = 13;
unsigned long eyesClosedStart = 0;
bool eyesClosed = false;
void setup() {
pinMode(eyePin, INPUT);
pinMode(alarmBuzzer, OUTPUT);
}
void loop() {
if (digitalRead(eyePin) == LOW) { // eyes "closed" / blocked
if (!eyesClosed) {
eyesClosed = true;
eyesClosedStart = millis();
}
if (millis() - eyesClosedStart > 2000) {
digitalWrite(alarmBuzzer, HIGH); // sound alarm after 2 sec
}
} else {
eyesClosed = false;
digitalWrite(alarmBuzzer, LOW);
}
}
🆘 Emergency Panic Button
How it works: This is the simplest but most important circuit. When the button is pressed, the Arduino instantly turns on a buzzer and flashes an LED, and shows "EMERGENCY!" on the LCD until it's manually reset.
- Mount a big, obvious button (arcade-style if possible) somewhere passengers can reach.
- Wire it between pin 11 and GND, using the internal pull-up resistor in code.
- Connect a separate buzzer to pin 12 and a red LED (with a 220Ω resistor) to pin A0.
// Emergency Panic Button
const int panicButton = 11;
const int panicBuzzer = 12;
const int redLED = A0;
bool panicActive = false;
void setup() {
pinMode(panicButton, INPUT_PULLUP);
pinMode(panicBuzzer, OUTPUT);
pinMode(redLED, OUTPUT);
}
void loop() {
if (digitalRead(panicButton) == LOW) {
panicActive = true;
}
if (panicActive) {
digitalWrite(panicBuzzer, HIGH);
digitalWrite(redLED, (millis() / 300) % 2); // flash the LED
} else {
digitalWrite(panicBuzzer, LOW);
digitalWrite(redLED, LOW);
}
}
📍 GPS Display
How it works: A GPS module listens to satellite signals and sends location data over a serial connection. The Arduino reads this data using the TinyGPS++ library and prints the coordinates to the LCD.
- Connect the GPS module's TX pin to the Mega's RX1 (pin 19), and RX pin to the Mega's TX1 (pin 18).
- Power the module with 5V and GND.
- Place the GPS antenna where it can "see" the sky — indoors it may take a while to get a signal (this is normal!).
// GPS Display
#include <TinyGPS++.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
TinyGPSPlus gps;
LiquidCrystal_I2C lcd(0x27, 16, 2);
void setup() {
Serial1.begin(9600); // GPS module
lcd.init();
lcd.backlight();
}
void loop() {
while (Serial1.available() > 0) {
gps.encode(Serial1.read());
}
if (gps.location.isUpdated()) {
lcd.setCursor(0, 0);
lcd.print("Lat:");
lcd.print(gps.location.lat(), 4);
lcd.setCursor(0, 1);
lcd.print("Lng:");
lcd.print(gps.location.lng(), 4);
}
}
🔊 Voice Announcements
How it works: Pre-recorded MP3 clips (like "Next stop: Main Street") are stored on a micro SD card plugged into a DFPlayer Mini module. The Arduino tells the module which track to play and when.
- Copy your recorded MP3 clips onto a micro SD card, named 0001.mp3, 0002.mp3, and so on.
- Insert the card into the DFPlayer Mini and wire the speaker to its SPK pins.
- Connect DFPlayer TX to Mega RX2 (pin 17) and RX to Mega TX2 (pin 16).
- Trigger announcements whenever the auto door opens (a natural "next stop" moment)!
// Voice Announcements
#include <DFRobotDFPlayerMini.h>
DFRobotDFPlayerMini mp3;
void setup() {
Serial2.begin(9600);
mp3.begin(Serial2);
mp3.volume(20); // 0 to 30
}
void announceStop(int trackNumber) {
mp3.play(trackNumber); // plays 000X.mp3
}
🧩 See the Full Combined Arduino Code (all 8 features together)
Once every feature works on its own, combine them into one sketch like this. Adjust pin numbers if you changed anything above.
// ===== SMART BUS — FULL COMBINED CODE =====
#include <Servo.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <TinyGPS++.h>
#include <DFRobotDFPlayerMini.h>
// --- Pin setup ---
const int entryPin = 2, exitPin = 3, footPin = 4, eyePin = 5;
const int trigPin = 6, echoPin = 7, doorServoPin = 8, ticketServoPin = 9;
const int ticketButton = 10, panicButton = 11, panicBuzzer = 12, sharedBuzzer = 13;
const int redLED = A0, greenLED = A1;
Servo doorServo, ticketServo;
LiquidCrystal_I2C lcd(0x27, 16, 2);
TinyGPSPlus gps;
DFRobotDFPlayerMini mp3;
int passengerCount = 0;
bool panicActive = false;
unsigned long eyesClosedStart = 0;
bool eyesClosed = false;
void setup() {
pinMode(entryPin, INPUT); pinMode(exitPin, INPUT);
pinMode(footPin, INPUT); pinMode(eyePin, INPUT);
pinMode(trigPin, OUTPUT); pinMode(echoPin, INPUT);
pinMode(ticketButton, INPUT_PULLUP);
pinMode(panicButton, INPUT_PULLUP);
pinMode(panicBuzzer, OUTPUT); pinMode(sharedBuzzer, OUTPUT);
pinMode(redLED, OUTPUT); pinMode(greenLED, OUTPUT);
doorServo.attach(doorServoPin); doorServo.write(0);
ticketServo.attach(ticketServoPin); ticketServo.write(0);
lcd.init(); lcd.backlight();
Serial1.begin(9600); // GPS
Serial2.begin(9600); // DFPlayer
mp3.begin(Serial2);
mp3.volume(20);
digitalWrite(greenLED, HIGH);
}
long getDistanceCM() {
digitalWrite(trigPin, LOW); delayMicroseconds(2);
digitalWrite(trigPin, HIGH); delayMicroseconds(10);
digitalWrite(trigPin, LOW);
long duration = pulseIn(echoPin, HIGH);
return duration * 0.034 / 2;
}
void loop() {
// 1. Passenger counter
if (digitalRead(entryPin) == LOW) { passengerCount++; delay(400); }
if (digitalRead(exitPin) == LOW && passengerCount > 0) { passengerCount--; delay(400); }
// 2. Auto door
long dist = getDistanceCM();
if (dist > 0 && dist < 20) {
doorServo.write(90);
mp3.play(1); // "next stop" clip
delay(3000);
} else {
doorServo.write(0);
}
// 3. Footboard safety + 5. Fatigue alarm share one buzzer, panic overrides both
bool footWarning = (digitalRead(footPin) == LOW);
if (digitalRead(eyePin) == LOW) {
if (!eyesClosed) { eyesClosed = true; eyesClosedStart = millis(); }
} else { eyesClosed = false; }
bool fatigueWarning = eyesClosed && (millis() - eyesClosedStart > 2000);
// 4. Ticket dispenser
if (digitalRead(ticketButton) == LOW) {
ticketServo.write(90); delay(700); ticketServo.write(0); delay(300);
}
// 6. Panic button (highest priority)
if (digitalRead(panicButton) == LOW) { panicActive = true; }
if (panicActive) {
digitalWrite(panicBuzzer, HIGH);
digitalWrite(redLED, (millis() / 300) % 2);
digitalWrite(greenLED, LOW);
lcd.setCursor(0, 0); lcd.print("** EMERGENCY! **");
} else {
digitalWrite(panicBuzzer, LOW);
digitalWrite(sharedBuzzer, footWarning || fatigueWarning ? HIGH : LOW);
digitalWrite(greenLED, HIGH);
// 7. GPS display (only shown when no alerts)
while (Serial1.available() > 0) { gps.encode(Serial1.read()); }
lcd.setCursor(0, 0);
lcd.print("Pax:"); lcd.print(passengerCount);
lcd.print(" ");
if (gps.location.isValid()) {
lcd.setCursor(8, 0);
lcd.print(gps.location.lat(), 2);
}
lcd.setCursor(0, 1);
if (footWarning) lcd.print("Wait! Footboard!");
else if (fatigueWarning) lcd.print("Driver: Wake up!");
else lcd.print("Bus running OK ");
}
delay(150);
}
Testing Checklist
Before you show off your smart bus, go through this checklist with a parent, teacher, or robotics mentor:
- ☐ Passenger counter goes up when "entering" and down when "exiting"
- ☐ Door opens when something is closer than 20cm, and closes after a few seconds
- ☐ Footboard buzzer sounds when the footboard sensor is blocked
- ☐ Ticket dispenser servo moves forward and back when the button is pressed
- ☐ Fatigue alarm triggers only after eyes stay "closed" for 2+ seconds (not instantly)
- ☐ Panic button instantly triggers buzzer + flashing LED, and stays on until reset
- ☐ GPS shows changing coordinates once it gets a satellite signal (best done outdoors)
- ☐ Voice announcement plays when the door opens
Frequently Asked Questions
Do I need to know how to code to build this?
Not much! Each code snippet above is ready to use — you just need to install the Arduino IDE, install the listed libraries, and upload the code. It's a great first robotics project for learning the basics.
Can I build this with an Arduino Uno instead of a Mega?
You can, but you'll need to build fewer features at once or use a multiplexer, since the Uno has far fewer pins. We recommend starting with the passenger counter, auto door, and panic button on an Uno, then upgrading to a Mega for the full 8-feature build.
Why isn't my GPS module getting a signal?
GPS modules need a clear view of the sky to talk to satellites. If you're indoors, place the antenna near a window, or test outside — it can take a few minutes for the first "fix."
What age group is this project best for?
This project works well for students aged 10 and up, especially with adult help for wiring and soldering. It's a popular choice for school science fairs and robotics clubs.
How much does this project cost to build?
Most component kits for this build cost a modest amount online, since IR sensors, servos, and buttons are inexpensive. The GPS module and MP3 player are usually the priciest parts.

Comments
Post a Comment