Built-in project · Arduino Uno R3

Packaging line on an Arduino Uno

An E18-D80NK counts items into a box; when the count reaches the box size set with a button, the feed relay stops, a servo flap closes the box with a beep, a pusher moves it on and the next box starts, with the throughput on serial. The whole build, an Arduino Uno and 8 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 30 minutes to build and run. Runs in your browser. Free, and no account needed.

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

The circuit runs here once the page has loaded.

What you'll learn

  • Sensors
  • Motors
  • Displays
  • Controllers
  • Moving a servo to an angle
  • Difficulty: Intermediate
  • Time: About 30 minutes
  • Board: Arduino Uno R3
  • Parts: 15

How it works

An E18-D80NK counts items into a box; when the count reaches the box size set with a button, the feed relay stops, a servo flap closes the box with a beep, a pusher moves it on and the next box starts, with the throughput on serial.

Arduino Uno R3 runs the sketch below. The circuit around it: Hobby servo, Buzzer, Pushbutton, Relay board, two channels, DC motor, Solenoid. pinMode sets SENSOR_PIN as an input with its pull-up on. digitalRead reads whether BUTTON_PIN is HIGH or LOW. delay waits 20 milliseconds. Serial.begin opens the serial port at 9600 baud. millis reads the time since power-up, so the sketch can time things without stopping.

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
Hobby servo pin PWMArduino Uno R3 pin 9
Buzzer pin 1Arduino Uno R3 pin 8
Relay board, two channels pin IN2Arduino Uno R3 pin 5
Relay board, two channels pin IN1Arduino Uno R3 pin 4
Pushbutton pin 1bArduino Uno R3 pin 3
IR proximity switch, E18-D80NK pin OUTArduino Uno R3 pin 2
Character LCD with I2C backpack pin VCCArduino Uno R3 pin 5V
Character LCD with I2C backpack pin GNDArduino Uno R3 pin GND
Character LCD with I2C backpack pin SDAArduino Uno R3 pin A4
Character LCD with I2C backpack pin SCLArduino Uno R3 pin A5
Hobby servo pin GNDGround
Hobby servo pin VCC5 V
Buzzer pin 2Ground
Pushbutton pin 2bGround
Relay board, two channels pin GNDGround
Relay board, two channels pin VCC5 V
DC motor pin 1Relay board, two channels pin NO1
Relay board, two channels pin COM15 V
Solenoid pin 1Relay board, two channels pin NO2
Relay board, two channels pin COM25 V
DC motor pin 2Ground
Solenoid pin 2Ground
IR proximity switch, E18-D80NK pin VCC5 V
IR proximity switch, E18-D80NK pin GNDGround

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
// Packaging line: count items into a box, and when the box holds its batch,
// stop the feed, close the box and start the next one.
//
//   2       E18-D80NK, OUT (brown on 5 V, blue on GND): low while an item
//           drops through the beam into the box
//   3       the box size button, its other side on GND
//   4       relay 1, the feed motor's contactor
//   5       relay 2, the pusher solenoid that moves a full box on
//   8       the active buzzer's plus leg, its other leg on GND
//   9       the box flap, a servo (red on 5 V, brown on GND)
//   LCD     SDA -> A4    SCL -> A5    VCC -> 5 V    GND -> GND
//
// Counting is how a line knows when a job is done. The box size N is a
// setpoint: the button steps it through 2, 4, 6 and 8, and the line compares
// its count with it on every pass of the loop.
//
//   FEEDING   feed on. Each item through the beam adds one. Count = N ->
//             CLOSING.
//   CLOSING   feed off, beep, the flap swings shut. As it swings back the
//             pusher moves the full box on. Done -> one more box, count
//             back to 0, FEEDING.
//
// The sensor is an interlock while the feed is stopped: an item that turns
// up in CLOSING is not counted, and the beam has to be clear again before
// the next count, so nothing slips into the wrong box. The serial monitor
// shows the throughput in items per minute, pauses included.
//
// Press Run, press the button to pick a box size, then drag the sensor's
// target close and away once per item.

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

const int SENSOR_PIN = 2;
const int BUTTON_PIN = 3;
const int FEED_PIN = 4;
const int PUSHER_PIN = 5;
const int BUZZER_PIN = 8;
const int FLAP_PIN = 9;

const unsigned long CLEAR_MS = 50;     // the beam clear this long between items
const unsigned long BEEP_MS = 200;     // the box-done beep
const unsigned long SHUT_MS = 800;     // flap closed
const unsigned long OPEN_MS = 500;     // flap swinging back, pusher out, before the feed restarts
const unsigned long DEBOUNCE_MS = 30;  // the button steady this long
const int FLAP_OPEN = 0;
const int FLAP_SHUT = 90;
const int SIZES[] = {2, 4, 6, 8};
const int SIZE_COUNT = 4;

// The relay board switches on a low input.
const int ON = LOW;
const int OFF = HIGH;

enum State { FEEDING, CLOSING };
const char* const NAMES[] = {"FEEDING", "CLOSING"};

LiquidCrystal_I2C lcd(0x27, 16, 2);
Servo flap(FLAP_PIN);

State state = FEEDING;
unsigned long enteredAt = 0;
int sizeIndex = 0;
int count = 0;
unsigned long boxes = 0;
unsigned long items = 0;
unsigned long firstItemAt = 0;

// The beam: an item counts on the change from clear to blocked, and only
// once the beam has been clear for CLEAR_MS. `armed` is false while the
// feed is stopped and until the beam is clear again after it restarts.
bool armed = true;
unsigned long lastBlocked = 0;

// The button: a press counts once its reading has held for DEBOUNCE_MS.
bool buttonDown = false;
bool buttonRaw = false;
unsigned long buttonChangedAt = 0;

int boxSize() {
  return SIZES[sizeIndex];
}

unsigned long perMinute(unsigned long now) {
  if (items < 2 || now == firstItemAt) return 0;
  return (items - 1) * 60000UL / (now - firstItemAt);
}

// The LCD takes a few milliseconds a character, so it is written only when
// something changes, and always after the outputs have been set.
void showCount() {
  lcd.setCursor(0, 0);
  if (state == CLOSING) {
    lcd.print("Closing box     ");
  } else {
    lcd.print("Items ");
    lcd.print(count);
    lcd.print(" of ");
    lcd.print(boxSize());
    lcd.print("   ");
  }
}

void show() {
  showCount();
  lcd.setCursor(0, 1);
  lcd.print("Boxes ");
  lcd.print(boxes);
  lcd.print("  ");
  lcd.print(perMinute(millis()));
  lcd.print("/min    ");
}

void enter(State next) {
  state = next;
  // The feed runs only while the line is filling a box, and the buzzer
  // beeps as a box starts closing. Outputs first, the display after.
  digitalWrite(FEED_PIN, next == FEEDING ? ON : OFF);
  digitalWrite(BUZZER_PIN, next == CLOSING);
  // Whatever is in the beam now is not part of the next count.
  armed = false;
  Serial.print("state ");
  Serial.println(NAMES[next]);
  show();
  enteredAt = millis();
}

// True once for each press of the box size button.
bool buttonPressed(unsigned long now) {
  bool raw = digitalRead(BUTTON_PIN) == LOW;
  if (raw != buttonRaw) {
    buttonRaw = raw;
    buttonChangedAt = now;
  }
  if (raw != buttonDown && now - buttonChangedAt >= DEBOUNCE_MS) {
    buttonDown = raw;
    return raw;
  }
  return false;
}

void setup() {
  pinMode(SENSOR_PIN, INPUT_PULLUP);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(FEED_PIN, OUTPUT);
  pinMode(PUSHER_PIN, OUTPUT);
  pinMode(BUZZER_PIN, OUTPUT);
  digitalWrite(FEED_PIN, OFF);
  digitalWrite(PUSHER_PIN, OFF);
  Serial.begin(9600);
  Serial.println("Packaging line");
  flap.begin();
  flap.write(FLAP_OPEN);
  lcd.init();
  lcd.backlight();
  Serial.print("box size ");
  Serial.println(boxSize());
  enter(FEEDING);
}

void loop() {
  unsigned long now = millis();
  bool blocked = digitalRead(SENSOR_PIN) == LOW;
  int flapAngle = FLAP_OPEN;

  if (buttonPressed(now)) {
    sizeIndex = (sizeIndex + 1) % SIZE_COUNT;
    Serial.print("box size ");
    Serial.println(boxSize());
    showCount();
  }

  if (blocked) {
    if (state == FEEDING && armed && now - lastBlocked >= CLEAR_MS) {
      count++;
      items++;
      if (items == 1) firstItemAt = now;
      Serial.print("item ");
      Serial.print(count);
      Serial.print(" of ");
      Serial.println(boxSize());
      // A full box stops the feed on this very pass.
      if (count >= boxSize()) {
        enter(CLOSING);
        now = millis();
      } else {
        showCount();
      }
    } else if (state == CLOSING && armed) {
      // The interlock: the feed is stopped, so this item does not count.
      Serial.println("item ignored, feed stopped");
    }
    armed = false;
    lastBlocked = now;
  } else if (!armed && now - lastBlocked >= CLEAR_MS) {
    armed = true;
  }

  switch (state) {
    case FEEDING:
      if (count >= boxSize()) enter(CLOSING);
      break;
    case CLOSING:
      if (now - enteredAt < SHUT_MS) flapAngle = FLAP_SHUT;
      if (now - enteredAt >= SHUT_MS + OPEN_MS) {
        boxes++;
        count = 0;
        Serial.print("box ");
        Serial.print(boxes);
        Serial.println(" done");
        Serial.print("throughput ");
        Serial.print(perMinute(now));
        Serial.println(" items/min");
        enter(FEEDING);
      }
      break;
  }

  digitalWrite(BUZZER_PIN, state == CLOSING && millis() - enteredAt < BEEP_MS);
  // The pusher only moves once the flap has closed the box.
  bool pushing = state == CLOSING && millis() - enteredAt >= SHUT_MS;
  digitalWrite(PUSHER_PIN, pushing ? ON : OFF);
  flap.write(flapAngle);
  delay(20);
}

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() {
  pinMode(SENSOR_PIN, INPUT_PULLUP);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(FEED_PIN, OUTPUT);
  pinMode(PUSHER_PIN, OUTPUT);
  pinMode(BUZZER_PIN, OUTPUT);
  digitalWrite(FEED_PIN, OFF);
  digitalWrite(PUSHER_PIN, OFF);
  Serial.begin(9600);
  Serial.println("Packaging line");
  flap.begin();
  flap.write(FLAP_OPEN);
  lcd.init();
  lcd.backlight();
  Serial.print("box size ");
  Serial.println(boxSize());
  enter(FEEDING);
}

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 blocked = digitalRead(SENSOR_PIN) == LOW;
  int flapAngle = FLAP_OPEN;

  if (buttonPressed(now)) {
    sizeIndex = (sizeIndex + 1) % SIZE_COUNT;
    Serial.print("box size ");
    Serial.println(boxSize());
    showCount();
  }

  if (blocked) {
    if (state == FEEDING && armed && now - lastBlocked >= CLEAR_MS) {
      count++;
      items++;
      if (items == 1) firstItemAt = now;
      Serial.print("item ");
      Serial.print(count);
      Serial.print(" of ");
      Serial.println(boxSize());
      // A full box stops the feed on this very pass.
      if (count >= boxSize()) {
        enter(CLOSING);
        now = millis();
      } else {
        showCount();
      }
  // ... 37 more lines in the editor

Troubleshooting

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

  • The servo jitters, twitches or resets the board when it moves. Hobby servo
    Power it from its own 5 V supply, not the Arduino 5 V pin, and join the grounds. A hobby servo can pull several hundred milliamps. Add a 100 uF to 470 uF capacitor across its supply.
  • The servo hums at the end of its travel or will not reach full angle. Hobby servo
    Stay inside the pulse range, about 1000 to 2000 us for most hobby servos. Pushing past the end stop makes it strain. Calibrate the min and max pulse for your servo.
  • The buzzer makes a constant tone, or you cannot change its pitch. Buzzer
    An active buzzer has its own oscillator, so it only beeps on and off. A passive buzzer needs tone() to set pitch. Check which type you have.
  • The buzzer is silent or very quiet. Buzzer
    Check polarity: the plus mark goes to the pin or supply. For loud buzzers, drive them through a transistor instead of straight from a pin.
  • The input reads random HIGH and LOW when the button is not pressed. Pushbutton
    A floating pin picks up noise. Add a 10 k pull-down or pull-up resistor, or use INPUT_PULLUP and wire the button between the pin and GND. Then pressed reads LOW.
  • The button seems always pressed, or never changes. Pushbutton
    A 4-leg tactile switch has legs joined in pairs. Place it across the breadboard center gap so the pairs sit on different rows. Wired along one side, the legs are already connected.
  • 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.

Change it and keep it

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