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.
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.
Parts list
15 parts, plus the jumper wires. They retail for about $49; on Mokxi they're free.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Half breadboard
- 1 × Hobby servoIn Halloween Prop Kit, Christmas Lights Kit, Starter Projects Kit.
- 5 × Power, 5 V
- 1 × BuzzerIn Starter Kit, Rocket Flight Computer Kit.
- 1 × PushbuttonIn Starter Kit, Christmas Lights Kit, Logic Lab Kit.
- 1 × Relay board, two channels
- 1 × DC motor
- 1 × Solenoid
- 1 × IR proximity switch, E18-D80NK
- 1 × Character LCD with I2C backpackIn Claw Machine Kit, Vending Machine Kit, Rhythm Game Kit.
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 pin | Connects to |
|---|---|
| Hobby servo pin PWM | Arduino Uno R3 pin 9 |
| Buzzer pin 1 | Arduino Uno R3 pin 8 |
| Relay board, two channels pin IN2 | Arduino Uno R3 pin 5 |
| Relay board, two channels pin IN1 | Arduino Uno R3 pin 4 |
| Pushbutton pin 1b | Arduino Uno R3 pin 3 |
| IR proximity switch, E18-D80NK pin OUT | Arduino Uno R3 pin 2 |
| Character LCD with I2C backpack pin VCC | Arduino Uno R3 pin 5V |
| Character LCD with I2C backpack pin GND | Arduino Uno R3 pin GND |
| Character LCD with I2C backpack pin SDA | Arduino Uno R3 pin A4 |
| Character LCD with I2C backpack pin SCL | Arduino Uno R3 pin A5 |
| Hobby servo pin GND | Ground |
| Hobby servo pin VCC | 5 V |
| Buzzer pin 2 | Ground |
| Pushbutton pin 2b | Ground |
| Relay board, two channels pin GND | Ground |
| Relay board, two channels pin VCC | 5 V |
| DC motor pin 1 | Relay board, two channels pin NO1 |
| Relay board, two channels pin COM1 | 5 V |
| Solenoid pin 1 | Relay board, two channels pin NO2 |
| Relay board, two channels pin COM2 | 5 V |
| DC motor pin 2 | Ground |
| Solenoid pin 2 | Ground |
| IR proximity switch, E18-D80NK pin VCC | 5 V |
| IR proximity switch, E18-D80NK pin GND | Ground |
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// 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 editorTroubleshooting
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.