Built-in project · Arduino Uno R3

Bottling line on an Arduino Uno

A belt brings each bottle to the filler, a pump fills it until the float switch trips, and the LCD counts them, with a jam alarm and a safety guard that stops everything. The whole build, an Arduino Uno and 9 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.

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

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

The circuit runs here once the page has loaded.

What you'll learn

  • Sensors
  • Motors
  • Reading a button
  • Using the Relay board
  • Difficulty: Intermediate
  • Time: About 35 minutes
  • Board: Arduino Uno R3
  • Parts: 16

How it works

A belt brings each bottle to the filler, a pump fills it until the float switch trips, and the LCD counts them, with a jam alarm and a safety guard that stops everything.

Arduino Uno R3 runs the sketch below. The circuit around it: Relay board, two channels, DC motor, Mini water pump, Obstacle sensor, FC-51, Float switch, ZP4510, Limit switch, lever microswitch. pinMode sets BELT_RELAY as an output. digitalRead reads whether in.pin is HIGH or LOW. Serial.begin opens the serial port at 9600 baud. tone plays a note on ALARM_PIN. millis reads the time since power-up, so the sketch can time things without stopping.

Parts list

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

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
Arduino Uno R3 pin GNDGround
Pushbutton pin 1aArduino Uno R3 pin 12
Limit switch, lever microswitch pin NOArduino Uno R3 pin 11
Float switch, ZP4510 pin 1Arduino Uno R3 pin 10
Obstacle sensor, FC-51 pin OUTArduino Uno R3 pin 9
Relay board, two channels pin IN1Arduino Uno R3 pin 7
Relay board, two channels pin IN2Arduino Uno R3 pin 6
Piezo speaker pin 1Arduino Uno R3 pin 4
Character LCD with I2C backpack pin SDAArduino Uno R3 pin A4
Character LCD with I2C backpack pin SCLArduino Uno R3 pin A5
Relay board, two channels pin GNDGround
Relay board, two channels pin VCC5 V
Relay board, two channels pin NO16 V
DC motor pin 1Relay board, two channels pin COM1
Relay board, two channels pin NO26 V
Mini water pump pin V+Relay board, two channels pin COM2
DC motor pin 2Ground
Mini water pump pin V-Ground
Obstacle sensor, FC-51 pin GNDGround
Obstacle sensor, FC-51 pin VCC5 V
Float switch, ZP4510 pin 2Ground
Limit switch, lever microswitch pin COMGround
Pushbutton pin 2aGround
Piezo speaker pin 2Ground
Character LCD with I2C backpack pin GNDGround
Character LCD with I2C backpack pin VCC5 V

The note on the canvas

Word for word, as it sits beside the circuit in the editor.

Try it
1. Press Run. Click the guard switch to close the safety door (hold it pressed), then press START: the belt runs.
2. Drag the IR sensor's obstacle close (a bottle arrives). The belt stops and the pump fills it.
3. Drag the float switch's water above 30 mm: the pump stops and the LCD counts one more bottle.
4. Drag the obstacle away again so the bottle leaves. Let go of the guard at any time: everything stops.

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
// Bottling line: a conveyor belt carries empty bottles to a filler. An IR
// sensor sees each bottle arrive and stops the belt under the nozzle, a pump
// fills it until a float switch says it is full (or a timer runs out), and
// the belt carries it away. The LCD counts the bottles.
//
// Three things protect the line, the way they do in a real factory:
//   * a guard switch on the safety door: open it and everything stops at
//     once, and closing it again does NOT restart the line. Someone has to
//     press START on purpose.
//   * a jam timeout: a filled bottle that does not leave the sensor in time
//     means it is stuck, so the line stops and beeps.
//   * a no-bottle timeout: a belt running with nothing on it is wasting
//     power, so it stops and beeps until someone loads more bottles.
//
//   guard switch  NO -> 11, COM -> GND  (door shut presses the lever: LOW)
//   START button  -> 12 and GND
//   IR sensor     FC-51 OUT -> 9 (LOW when a bottle is in front), VCC, GND
//   float switch  one wire -> 10, the other -> GND (closed = full: LOW)
//   relay board   IN1 -> 7 (belt), IN2 -> 6 (pump), VCC -> 5 V, GND
//                 belt motor and pump each on their relay's COM, NO -> 6 V
//   piezo         pin 4 -> piezo -> GND (the alarm)
//   LCD 16x2 I2C  SDA -> A4, SCL -> A5 (address 0x27)
//
// The relay board is "active low": writing LOW to IN1 switches the belt ON.
// That is why setup() writes HIGH to the relay pins BEFORE making them
// outputs, so the belt and the pump do not twitch when the Uno starts.
//
// Things to try. Change FILL_MS for bigger bottles. Make the line stop by
// itself after a crate of 12.

#include <LiquidCrystal_I2C.h>

// ---- pins ----
const int GUARD_PIN = 11;
const int START_PIN = 12;
const int BOTTLE_PIN = 9;
const int LEVEL_PIN = 10;
const int BELT_RELAY = 7;
const int PUMP_RELAY = 6;
const int ALARM_PIN = 4;

const int RELAY_ON = LOW;     // active-low relay board
const int RELAY_OFF = HIGH;

// ---- timing, in milliseconds ----
const unsigned long SETTLE_MS = 500;       // let the bottle stop wobbling
const unsigned long FILL_MS = 4000;        // the longest a fill may take
const unsigned long JAM_MS = 3000;         // a bottle must leave by then
const unsigned long NO_BOTTLE_MS = 8000;   // an empty belt stops after this
const unsigned long DEBOUNCE_MS = 30;

LiquidCrystal_I2C lcd(0x27, 16, 2);

// ---- the line's states ----
enum State { STOPPED, RUNNING, SETTLING, FILLING, RUN_OUT, FAULT };
State state = STOPPED;
unsigned long stateSince = 0;   // when the current state began
unsigned long bottles = 0;      // bottles filled since power on
bool bottleFilled = false;      // the bottle at the filler is already full
bool guardWas = false;          // the guard last loop, to report changes
bool alarmOn = false;           // the piezo is beeping right now

// A debounced input: it only changes after it has read the same for a while.
struct Input {
  int pin;
  bool stable;          // the debounced reading
  bool last;            // the raw reading last time
  unsigned long since;  // when the raw reading last changed
};
Input startBtn = {START_PIN, HIGH, HIGH, 0};
Input bottleEye = {BOTTLE_PIN, HIGH, HIGH, 0};
Input levelFloat = {LEVEL_PIN, HIGH, HIGH, 0};

// Read an input; returns true on the loop the debounced value changes.
bool update(Input &in, unsigned long now) {
  bool raw = digitalRead(in.pin);
  if (raw != in.last) { in.last = raw; in.since = now; }
  if (now - in.since >= DEBOUNCE_MS && raw != in.stable) {
    in.stable = raw;
    return true;
  }
  return false;
}

// what the sensors say, in words
bool guardClosed() { return digitalRead(GUARD_PIN) == LOW; }
bool bottleHere() { return bottleEye.stable == LOW; }
bool levelFull() { return levelFloat.stable == LOW; }

// ---- outputs ----
void belt(bool on) { digitalWrite(BELT_RELAY, on ? RELAY_ON : RELAY_OFF); }
void pump(bool on) { digitalWrite(PUMP_RELAY, on ? RELAY_ON : RELAY_OFF); }

void allOff() {
  belt(false);
  pump(false);
}

void showCount(const char *status) {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Bottles: ");
  lcd.print(bottles);
  lcd.setCursor(0, 1);
  lcd.print(status);
}

void enter(State next, unsigned long now) {
  state = next;
  stateSince = now;
}

// Stop the line for a problem: everything off, the alarm on, START to clear.
void fault(const char *why, unsigned long now) {
  allOff();
  enter(FAULT, now);
  Serial.print("FAULT: ");
  Serial.println(why);
  showCount(why);
}

void silence() {
  noTone(ALARM_PIN);
  alarmOn = false;
}

void startLine(unsigned long now) {
  silence();
  belt(true);
  // a full bottle still at the filler (after a jam) just moves on;
  // anything else waits for the next bottle to fill
  enter(bottleHere() && bottleFilled ? RUN_OUT : RUNNING, now);
  Serial.println("Belt running");
  showCount("Running");
}

void setup() {
  // relays off FIRST, then outputs: no twitch at power up
  digitalWrite(BELT_RELAY, RELAY_OFF);
  digitalWrite(PUMP_RELAY, RELAY_OFF);
  pinMode(BELT_RELAY, OUTPUT);
  pinMode(PUMP_RELAY, OUTPUT);

  pinMode(GUARD_PIN, INPUT_PULLUP);
  pinMode(START_PIN, INPUT_PULLUP);
  pinMode(BOTTLE_PIN, INPUT);        // the FC-51 drives its own output
  pinMode(LEVEL_PIN, INPUT_PULLUP);
  pinMode(ALARM_PIN, OUTPUT);

  Serial.begin(9600);
  lcd.init();
  lcd.backlight();
  Serial.println("Bottling line. Close the guard and press START");
  showCount("Close the guard");
}

void loop() {
  unsigned long now = millis();
  bool startPressed = update(startBtn, now) && startBtn.stable == LOW;
  bool bottleChanged = update(bottleEye, now);
  update(levelFloat, now);
  unsigned long inState = now - stateSince;

  // ---- the safety interlock comes first, every loop, no debounce ----
  bool guard = guardClosed();
  if (!guard) {
    if (guardWas || (state != STOPPED && state != FAULT)) {
      allOff();
      silence();
      enter(STOPPED, now);
      Serial.println("GUARD OPEN: everything stopped");
      showCount("GUARD OPEN");
    }
    guardWas = false;
    return;   // nothing else runs while the door is open
  }
  if (!guardWas) {
    guardWas = true;
    Serial.println("Guard closed. Press START");
    showCount("Press START");
  }

  switch (state) {
    case STOPPED:
      if (startPressed) startLine(now);
      break;

    case RUNNING:
      // waiting for a bottle to arrive at the filler
      if (bottleHere()) {
        belt(false);
        enter(SETTLING, now);
        Serial.println("Bottle at the filler");
        showCount("Bottle in place");
      } else if (inState >= NO_BOTTLE_MS) {
        fault("No bottles", now);
      }
      break;

    case SETTLING:
      if (inState >= SETTLE_MS) {
        pump(true);
        enter(FILLING, now);
        Serial.println("Filling");
        showCount("Filling...");
      }
      break;

    case FILLING:
      // stop at the float switch, or when the time is up, whichever first
      if (levelFull() || inState >= FILL_MS) {
        pump(false);
        bottles++;
        bottleFilled = true;
        Serial.print("Bottle ");
        Serial.print(bottles);
        Serial.print(levelFull() ? " full by level in " : " full by timer in ");
        Serial.print(inState);
        Serial.println(" ms");
        belt(true);
        enter(RUN_OUT, now);
        showCount("Moving on");
      }
      break;

    case RUN_OUT:
      // the full bottle has to leave the sensor before the next can come
      if (bottleChanged && !bottleHere()) {
        Serial.println("Bottle away");
        bottleFilled = false;
        enter(RUNNING, now);
        showCount("Running");
      } else if (inState >= JAM_MS) {
        fault("JAM at filler", now);
      }
      break;

    case FAULT:
      // beep half a second on, half a second off until START is pressed.
      // Only call tone() when the beep changes: calling it every loop
      // would restart the note over and over.
      {
        bool beep = (inState / 500) % 2 == 0;
        if (beep != alarmOn) {
          alarmOn = beep;
          if (beep) tone(ALARM_PIN, 2000); else noTone(ALARM_PIN);
        }
      }
      if (startPressed) startLine(now);
      break;
  }
}

Code walkthrough

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

setup sketch.ino

Runs once when the board starts. It gets the pins and libraries ready.

void setup() {
  // relays off FIRST, then outputs: no twitch at power up
  digitalWrite(BELT_RELAY, RELAY_OFF);
  digitalWrite(PUMP_RELAY, RELAY_OFF);
  pinMode(BELT_RELAY, OUTPUT);
  pinMode(PUMP_RELAY, OUTPUT);

  pinMode(GUARD_PIN, INPUT_PULLUP);
  pinMode(START_PIN, INPUT_PULLUP);
  pinMode(BOTTLE_PIN, INPUT);        // the FC-51 drives its own output
  pinMode(LEVEL_PIN, INPUT_PULLUP);
  pinMode(ALARM_PIN, OUTPUT);

  Serial.begin(9600);
  lcd.init();
  lcd.backlight();
  Serial.println("Bottling line. Close the guard and press START");
  showCount("Close the guard");
}

loop sketch.ino

Runs over and over for as long as the board has power. This is where the project does its work.

void loop() {
  unsigned long now = millis();
  bool startPressed = update(startBtn, now) && startBtn.stable == LOW;
  bool bottleChanged = update(bottleEye, now);
  update(levelFloat, now);
  unsigned long inState = now - stateSince;

  // ---- the safety interlock comes first, every loop, no debounce ----
  bool guard = guardClosed();
  if (!guard) {
    if (guardWas || (state != STOPPED && state != FAULT)) {
      allOff();
      silence();
      enter(STOPPED, now);
      Serial.println("GUARD OPEN: everything stopped");
      showCount("GUARD OPEN");
    }
    guardWas = false;
    return;   // nothing else runs while the door is open
  }
  if (!guardWas) {
    guardWas = true;
    Serial.println("Guard closed. Press START");
    showCount("Press START");
  }

  switch (state) {
    case STOPPED:
  // ... 68 more lines in the editor

Troubleshooting

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

  • A relay is on at boot or the logic is inverted. Relay board
    These boards are active low, so LOW on IN turns the relay on. Set the pin HIGH before setting it as an output so it does not click at start.
  • The relay clicks but the load does nothing, or the board resets. Relay board
    Use the correct contact: COM with NO for normally off. Power the relay coil with enough current, ideally from a separate 5 V supply, with grounds joined. Mains wiring is dangerous, so be careful.
  • The motor does not spin, or the pin gets hot or the board resets when it is wired straight to a GPIO pin. DC motor
    A pin gives about 20 mA and a motor needs far more. Drive it through a transistor or motor driver, with its own supply and a shared ground.
  • The transistor or pin is damaged by spikes when the motor stops. DC motor
    Put a flyback diode across the motor, cathode to the positive side. A motor is a coil and kicks back voltage when switched off. Add a 0.1 uF capacitor across the motor terminals to cut noise.
  • A pump wired straight to a microcontroller pin does not run or resets the board. It draws far more than a pin can give. Mini water pump
    Switch it with a logic level MOSFET or a transistor with a base resistor, from its own supply of 3 to 6 V. Join the grounds.
  • The motor spikes damage the driver, or the pump runs dry and burns out. A coil load makes voltage kicks when it stops. Mini water pump
    Add a flyback diode across the pump, stripe to the positive side. Keep the pump under water, since a dry run overheats it and wears the seal.
  • It detects nothing or always detects. Range depends on the pot and the surface, and black or shiny things reflect poorly. Obstacle sensor
    Turn the pot until the OUT LED switches at your distance. Test with a pale, matte surface. OUT is LOW when something is seen, so read it as active low.
  • Bright sunlight or an incandescent lamp makes it fire at random, because they give off infrared light. Obstacle sensor
    Shade the sensor or use it indoors. Power it from 3.3 V to 5 V, join the grounds, and keep it from facing other IR parts.

Change it and keep it

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