Built-in project · Arduino Uno R3

Smart dustbin on an Arduino Uno

Smart dustbin on an Arduino: an ultrasonic sensor sees a hand and a servo opens the lid, and a second sensor lights red and keeps it shut when the bin is full. The whole build, an Arduino Uno and 5 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

Teaching? Assign it to your class in one click.

Beginner About 25 minutes to build and run. Runs in your browser. Free, and no account needed.

The parts retail for about $37. On Mokxi they're free.

The circuit runs here once the page has loaded.

What you'll learn

  • Measuring distance with an ultrasonic sensor and pulseIn
  • Timing tasks with millis instead of delay
  • Moving a servo in small steps for smooth motion
  • Turning a distance into a percent full
  • Using a state variable to decide what the lights show
  • Difficulty: Beginner
  • Time: About 25 minutes
  • Board: Arduino Uno R3
  • Parts: 9

How it works

Two ultrasonic sensors do two jobs. The front one (TRIG pin 3, ECHO pin 2) watches for a hand. If something is closer than 20 cm, the servo on A3 swings the lid open and the piezo beeps. The lid closes 2 seconds after the hand has gone.

The second sensor (TRIG pin 13, ECHO pin 12) sits under the lid and looks down into a bin 30 cm deep. While the lid is shut, it measures once a second. The less space it sees, the fuller the bin.

At 80 percent full, the red light comes on and the lid refuses to open, with a low buzz. Green means ready and yellow means the lid is open. Nothing waits in delay: every timer compares against millis.

Parts list

9 parts, plus the jumper wires. They retail for about $37; on Mokxi they're free.

Kits that can build it

This starter kit has every part in the circuit. Each kit's page lists what else it can build.

Wiring table

Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.

Part and pinConnects to
Ultrasonic ranger (2) pin TRIGArduino Uno R3 pin 13
Ultrasonic ranger (2) pin ECHOArduino Uno R3 pin 12
Piezo speaker pin 1Arduino Uno R3 pin 10
Ultrasonic ranger (1) pin TRIGArduino Uno R3 pin 3
Ultrasonic ranger (1) pin ECHOArduino Uno R3 pin 2
Traffic light module pin GArduino Uno R3 pin A0
Traffic light module pin YArduino Uno R3 pin A1
Traffic light module pin RArduino Uno R3 pin A2
Hobby servo pin PWMArduino Uno R3 pin A3
Ultrasonic ranger (1) pin VCC5 V
Ultrasonic ranger (1) pin GNDGround
Ultrasonic ranger (2) pin VCC5 V
Ultrasonic ranger (2) pin GNDGround
Piezo speaker pin 2Ground
Traffic light module pin GNDGround
Hobby servo pin GNDGround
Hobby servo pin VCC5 V

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · Arduino Uno R3
// Smart dustbin: a lid that opens by itself when a hand comes near, and a
// second sensor that says when the bin is full. On an Arduino Uno.
//
//   hand sensor   HC-SR04 on the front: TRIG -> pin 3, ECHO -> pin 2
//   fill sensor   HC-SR04 under the lid, looking down: TRIG -> 13, ECHO -> 12
//   lid servo     SG90: signal -> A3, VCC -> 5 V, GND -> GND
//   lights        traffic light module: green -> A0, yellow -> A1, red -> A2
//   piezo         pin 10 and GND
//
// The analog pins A0 to A3 work as ordinary digital pins too, which leaves
// the pins along the top for the two sensors.
//
// Hold a hand closer than 20 cm to the front and the lid swings open with a
// beep. It stays open while the hand is there and closes two seconds after
// it has gone. The green light means ready, yellow means the lid is open.
//
// While the lid is shut, the fill sensor looks down into the bin, 30 cm
// deep, once a second. The less space it measures, the fuller the bin. At 80
// percent the red light comes on, the Serial Monitor asks for it to be
// emptied, and the lid stays shut with a low buzz until it is.
//
// # Why the lid moves slowly
//
// servo.write(100) on its own would slam the lid open as fast as the motor
// can turn. Here the angle moves 2 degrees every 15 ms, so the lid takes about
// 0.7 seconds to open and does it smoothly, while loop() keeps reading the
// sensors in between. Every timer in this sketch works the same way: it
// remembers when something last happened with millis() and checks how long
// ago that was. Nothing ever waits in delay().

#include <Servo.h>

const uint8_t HAND_TRIG = 3;
const uint8_t HAND_ECHO = 2;
const uint8_t FILL_TRIG = 13;
const uint8_t FILL_ECHO = 12;
const uint8_t SERVO_PIN = A3;
const uint8_t GREEN = A0;
const uint8_t YELLOW = A1;
const uint8_t RED = A2;
const uint8_t PIEZO = 10;

const int HAND_CM = 20;       // a hand closer than this opens the lid
const int BIN_DEPTH_CM = 30;  // from the fill sensor to the bottom of the bin
const int FULL_PERCENT = 80;  // at this the lid stays shut
const int SHUT = 0;           // servo angles for the lid
const int OPEN = 100;
const unsigned long STAY_OPEN_MS = 2000;

Servo lid;
int lidAngle = SHUT;
int lidTarget = SHUT;
unsigned long lidStepAt = 0;
unsigned long handSeenAt = 0;
unsigned long handCheckAt = 0;
unsigned long fillCheckAt = 0;
bool lidOpen = false;
bool full = false;
int fillPercent = -100;  // nothing printed yet
unsigned int opens = 0;
bool handWasNear = false;

// One measurement in centimeters, or 0 if no echo came back in 25 ms
// (nothing closer than about 4 meters).
int measureCm(uint8_t trig, uint8_t echo) {
  digitalWrite(trig, LOW);
  delayMicroseconds(2);
  digitalWrite(trig, HIGH);
  delayMicroseconds(10);
  digitalWrite(trig, LOW);
  unsigned long us = pulseIn(echo, HIGH, 25000UL);
  // Sound goes there and back at 343 m/s: 58 microseconds a centimeter.
  return (int)(us / 58);
}

void showLights() {
  digitalWrite(RED, full ? HIGH : LOW);
  digitalWrite(YELLOW, lidOpen ? HIGH : LOW);
  digitalWrite(GREEN, !full && !lidOpen ? HIGH : LOW);
}

void openLid(int cm) {
  if (full) {
    // A low buzz: it is full and will not open.
    tone(PIEZO, 220, 300);
    Serial.println("bin full: please empty it");
    return;
  }
  lidOpen = true;
  lidTarget = OPEN;
  opens++;
  tone(PIEZO, 1320, 80);
  Serial.print("hand at ");
  Serial.print(cm);
  Serial.print(" cm: lid open (");
  Serial.print(opens);
  Serial.println(opens == 1 ? " time)" : " times)");
  showLights();
}

void closeLid() {
  lidOpen = false;
  lidTarget = SHUT;
  Serial.println("lid closed");
  showLights();
}

void checkHand(unsigned long now) {
  if (now - handCheckAt < 100) return;
  handCheckAt = now;
  int cm = measureCm(HAND_TRIG, HAND_ECHO);
  bool near = cm > 0 && cm < HAND_CM;
  if (near) {
    handSeenAt = now;
    // Only a hand that has just arrived opens the lid (or asks to), so a
    // hand held over a full bin beeps once, not ten times a second.
    if (!lidOpen && !handWasNear) openLid(cm);
  } else if (lidOpen && now - handSeenAt > STAY_OPEN_MS) {
    closeLid();
  }
  handWasNear = near;
}

void checkFill(unsigned long now) {
  // The fill sensor hangs under the lid, so it only sees into the bin when
  // the lid is shut and has finished moving.
  if (lidOpen || lidAngle != SHUT || now - fillCheckAt < 1000) return;
  fillCheckAt = now;
  int cm = measureCm(FILL_TRIG, FILL_ECHO);
  if (cm == 0 || cm > BIN_DEPTH_CM) cm = BIN_DEPTH_CM;  // nothing in it
  int percent = (BIN_DEPTH_CM - cm) * 100 / BIN_DEPTH_CM;
  // Print only a change of 10 percent or more, so the monitor stays readable.
  if (abs(percent - fillPercent) >= 10) {
    fillPercent = percent;
    Serial.print("bin ");
    Serial.print(percent);
    Serial.println("% full");
  }
  bool nowFull = percent >= FULL_PERCENT;
  if (nowFull != full) {
    full = nowFull;
    Serial.println(full ? "bin full: please empty it" : "bin emptied: ready");
    showLights();
  }
}

void moveLid(unsigned long now) {
  if (lidAngle == lidTarget || now - lidStepAt < 15) return;
  lidStepAt = now;
  lidAngle += lidTarget > lidAngle ? 2 : -2;
  lid.write(lidAngle);
}

void setup() {
  Serial.begin(115200);
  pinMode(HAND_TRIG, OUTPUT);
  pinMode(HAND_ECHO, INPUT);
  pinMode(FILL_TRIG, OUTPUT);
  pinMode(FILL_ECHO, INPUT);
  pinMode(RED, OUTPUT);
  pinMode(YELLOW, OUTPUT);
  pinMode(GREEN, OUTPUT);
  pinMode(PIEZO, OUTPUT);
  lid.attach(SERVO_PIN);
  lid.write(SHUT);
  showLights();
  Serial.println("Smart dustbin ready: hold a hand near the front sensor.");
}

void loop() {
  unsigned long now = millis();
  checkHand(now);
  checkFill(now);
  moveLid(now);
}

Code walkthrough

The key functions of the sketch, and what each one does.

measureCm sketch.ino

This sends a 10 microsecond pulse on the trigger pin, then times the echo with pulseIn, waiting at most 25 ms. Sound travels there and back at about 58 microseconds per centimeter, so dividing by 58 gives the distance. It returns 0 when no echo came back.

// One measurement in centimeters, or 0 if no echo came back in 25 ms
// (nothing closer than about 4 meters).
int measureCm(uint8_t trig, uint8_t echo) {
  digitalWrite(trig, LOW);
  delayMicroseconds(2);
  digitalWrite(trig, HIGH);
  delayMicroseconds(10);
  digitalWrite(trig, LOW);
  unsigned long us = pulseIn(echo, HIGH, 25000UL);
  // Sound goes there and back at 343 m/s: 58 microseconds a centimeter.
  return (int)(us / 58);
}

checkHand sketch.ino

Every 100 ms this measures the front sensor. A hand closer than HAND_CM counts as near. Only a hand that has just arrived calls openLid, so holding a hand over a full bin does not beep ten times a second. The lid closes once the hand has been gone for STAY_OPEN_MS.

void checkHand(unsigned long now) {
  if (now - handCheckAt < 100) return;
  handCheckAt = now;
  int cm = measureCm(HAND_TRIG, HAND_ECHO);
  bool near = cm > 0 && cm < HAND_CM;
  if (near) {
    handSeenAt = now;
    // Only a hand that has just arrived opens the lid (or asks to), so a
    // hand held over a full bin beeps once, not ten times a second.
    if (!lidOpen && !handWasNear) openLid(cm);
  } else if (lidOpen && now - handSeenAt > STAY_OPEN_MS) {
    closeLid();
  }
  handWasNear = near;
}

checkFill sketch.ino

This runs only when the lid is shut and still. Once a second it measures down into the bin and turns the distance into a percent of BIN_DEPTH_CM. It prints a change of 10 percent or more. When the percent reaches FULL_PERCENT, it marks the bin full and updates the lights.

void checkFill(unsigned long now) {
  // The fill sensor hangs under the lid, so it only sees into the bin when
  // the lid is shut and has finished moving.
  if (lidOpen || lidAngle != SHUT || now - fillCheckAt < 1000) return;
  fillCheckAt = now;
  int cm = measureCm(FILL_TRIG, FILL_ECHO);
  if (cm == 0 || cm > BIN_DEPTH_CM) cm = BIN_DEPTH_CM;  // nothing in it
  int percent = (BIN_DEPTH_CM - cm) * 100 / BIN_DEPTH_CM;
  // Print only a change of 10 percent or more, so the monitor stays readable.
  if (abs(percent - fillPercent) >= 10) {
    fillPercent = percent;
    Serial.print("bin ");
    Serial.print(percent);
    Serial.println("% full");
  }
  bool nowFull = percent >= FULL_PERCENT;
  if (nowFull != full) {
    full = nowFull;
    Serial.println(full ? "bin full: please empty it" : "bin emptied: ready");
    showLights();
  }
}

moveLid sketch.ino

This eases the servo toward lidTarget. Every 15 ms it moves the angle by 2 degrees, so the lid takes about 0.7 seconds to open instead of slamming. It never blocks, so loop keeps reading the sensors.

void moveLid(unsigned long now) {
  if (lidAngle == lidTarget || now - lidStepAt < 15) return;
  lidStepAt = now;
  lidAngle += lidTarget > lidAngle ? 2 : -2;
  lid.write(lidAngle);
}

Troubleshooting

The usual mistakes with this circuit, and the fix for each.

  • The lid opens when nobody is near, or never opens This project
    Check that the front sensor is the one on pins 3 and 2, and the fill sensor is on pins 13 and 12. Swapped sensors make the bin think it is full.
  • The lid stays shut and the red light is on This project
    The fill sensor sees something closer than 6 cm, which is 80 percent of BIN_DEPTH_CM. Move the object away to empty the bin.
  • The lights do not match the colors This project
    Green goes to A0, yellow to A1 and red to A2 on the traffic light module.
  • Distance reads zero, or never changes. Ultrasonic ranger
    Power it from 5 V and share ground. Send a 10 microsecond HIGH pulse on TRIG, then time ECHO with pulseIn. Check TRIG and ECHO are not swapped.
  • Readings jump around or are wrong on a 3.3 V board. Ultrasonic ranger
    ECHO outputs 5 V, so divide it down for 3.3 V boards. Aim at flat, hard surfaces. The range is about 2 cm to 4 m, and soft or angled targets bounce the sound away.
  • A passive piezo stays silent when you just set the pin HIGH. Piezo speaker
    A passive piezo needs a changing signal. Use tone(pin, frequency) or a PWM wave. Around 2 to 4 kHz is loudest. A buzzer with a built in oscillator makes sound from plain DC instead.
  • Sound is very quiet or the pin seems overloaded. Piezo speaker
    Drive the piezo from a pin directly, or through a 100 ohm resistor. For a bigger buzzer, switch it with a transistor. Check that you wired the right two legs and joined grounds.
  • One color never lights, or all three light together. Traffic light module
    Check each wire goes to its own pin labeled R, Y, G, and that GND is connected. A pin driven high lights its LED. Resistors are already on the board, so do not add more.

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.