Line OEE dashboard on an Arduino Uno
A 20x4 LCD shows how well a machine is doing as one number: availability, performance and quality from a part counter, a reject button and a run/stop switch, multiplied into OEE. 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.
Intermediate About 25 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.
What you'll learn
- Sensors
- Displays
- Reading a button
- Reading a knob with an analog pin
- Difficulty: Intermediate
- Time: About 25 minutes
- Board: Arduino Uno R3
- Parts: 10
How it works
A 20x4 LCD shows how well a machine is doing as one number: availability, performance and quality from a part counter, a reject button and a run/stop switch, multiplied into OEE.
Arduino Uno R3 runs the sketch below. The circuit around it: Slide switch, Pushbutton, IR proximity switch, E18-D80NK, Potentiometer, Character LCD, 20x4, with I2C backpack. pinMode sets SENSOR_PIN as an input with its pull-up on. digitalRead reads whether in.pin is HIGH or LOW. analogRead measures the voltage on SPEED_PIN as a number. 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
10 parts, plus the jumper wires. They retail for about $43; on Mokxi they're free.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Half breadboard
- 1 × Slide switchIn Logic Lab Kit, Spaceship Panel Kit.
- 1 × PushbuttonIn Starter Kit, Christmas Lights Kit, Logic Lab Kit.
- 1 × IR proximity switch, E18-D80NK
- 3 × Power, 5 V
- 1 × PotentiometerIn Halloween Prop Kit, Starter Projects Kit.
- 1 × Character LCD, 20x4, with I2C backpackIn Spaceship Panel Kit, Escape Box 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 |
|---|---|
| IR proximity switch, E18-D80NK pin OUT | Arduino Uno R3 pin 4 |
| Pushbutton pin 1a | Arduino Uno R3 pin 3 |
| Slide switch pin 2 | Arduino Uno R3 pin 2 |
| Potentiometer pin 2 | Arduino Uno R3 pin A0 |
| Character LCD, 20x4, with I2C backpack pin SDA | Arduino Uno R3 pin A4 |
| Character LCD, 20x4, with I2C backpack pin SCL | Arduino Uno R3 pin A5 |
| Slide switch pin 1 | Ground |
| Pushbutton pin 2a | Ground |
| IR proximity switch, E18-D80NK pin VCC | 5 V |
| IR proximity switch, E18-D80NK pin GND | Ground |
| Potentiometer pin 1 | Ground |
| Potentiometer pin 3 | 5 V |
| Character LCD, 20x4, with I2C backpack pin GND | Ground |
| Character LCD, 20x4, with I2C backpack pin VCC | 5 V |
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Line OEE dashboard: how well is this machine doing, as one number?
//
// 4 E18-D80NK, OUT (brown on 5 V, blue on GND): low while a part
// passes; each part adds one to the count
// 3 REJECT button to GND: the last part was bad
// 2 RUN/STOP slide switch: middle to pin 2, one end to GND. To GND
// means the machine runs; open means it is stopped (downtime)
// A0 potentiometer: the ideal cycle time, 1.0 to 5.0 seconds a part
// LCD 20x4, SDA -> A4 SCL -> A5 VCC -> 5 V GND -> GND
//
// OEE (overall equipment effectiveness) multiplies three factors, and each
// one counts a different kind of loss:
//
// Availability run time / shift time so far lost to stops
// Performance parts x ideal cycle / run time lost to running slow
// Quality good parts / all parts lost to rejects
// OEE A x P x Q
//
// A machine that runs 90 percent of the time, at 90 percent speed, with 90
// percent good parts is only at 73 percent OEE: small losses multiply. Stop
// the line and A falls while P and Q stay put. Feed parts slowly and only P
// falls. Press REJECT and only Q falls.
//
// A shift here is 60 seconds so you can watch a whole one. The numbers go to
// the serial monitor once a second. The loop never waits: not with delay(),
// and not on the LCD either. Writing all 80 characters at once takes long
// enough for a part to slip past the sensor unseen, so the screen is drawn
// one character per pass of the loop, between looks at the inputs.
//
// Press Run, then wave a target past the sensor every couple of seconds.
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
const int SENSOR_PIN = 4;
const int REJECT_PIN = 3;
const int RUN_PIN = 2;
const int SPEED_PIN = A0;
const unsigned long SHIFT_MS = 60000; // one shift
const unsigned long REPORT_MS = 1000; // serial and LCD once a second
const unsigned long DEBOUNCE_MS = 30;
LiquidCrystal_I2C lcd(0x27, 20, 4);
unsigned long shiftStart = 0;
unsigned long runMs = 0; // time the machine was running this shift
unsigned long lastTick = 0;
unsigned long lastReport = 0;
unsigned long total = 0;
unsigned long rejects = 0;
bool lastRejected = true; // nothing to reject before the first part
bool running = false;
// What the LCD should show, and the next character still to be written.
char screen[4][21];
int drawAt = 80;
// Debounced inputs: the reading, when it last changed, and the settled value.
struct Input {
int pin;
bool raw;
unsigned long changed;
bool stable;
};
Input part = {SENSOR_PIN, false, 0, false};
Input reject = {REJECT_PIN, false, 0, false};
// True once, when an input settles low (a part arrives, the button goes down).
bool fell(Input& in, unsigned long now) {
bool low = digitalRead(in.pin) == LOW;
if (low != in.raw) {
in.raw = low;
in.changed = now;
}
if (low != in.stable && now - in.changed >= DEBOUNCE_MS) {
in.stable = low;
return low;
}
return false;
}
// The knob, as an ideal cycle time in milliseconds, in tenths of a second.
unsigned long idealMs() {
unsigned long ms = 1000 + (unsigned long)analogRead(SPEED_PIN) * 4000UL / 1023;
return (ms + 50) / 100 * 100;
}
int percent(unsigned long part, unsigned long whole) {
if (whole == 0) return 0;
unsigned long p = (part * 100 + whole / 2) / whole;
return p > 100 ? 100 : (int)p;
}
// Text into one row of `screen`, from column `col`; returns the column after.
int put(int row, int col, const char* text) {
while (*text && col < 20) screen[row][col++] = *text++;
return col;
}
// A number, right-aligned in `width` columns.
int putNumber(int row, int col, unsigned long value, int width) {
char digits[11];
int n = 0;
do {
digits[n++] = '0' + value % 10;
value /= 10;
} while (value > 0);
while (width-- > n && col < 20) screen[row][col++] = ' ';
while (n > 0 && col < 20) screen[row][col++] = digits[--n];
return col;
}
// Write one character of `screen` to the LCD: a few hundred microseconds.
void drawOne() {
if (drawAt >= 80) return;
int row = drawAt / 20;
int col = drawAt % 20;
if (col == 0) lcd.setCursor(0, row);
lcd.print(screen[row][col]);
drawAt++;
}
void report(unsigned long now) {
unsigned long elapsed = now - shiftStart;
unsigned long ideal = idealMs();
unsigned long good = total - rejects;
int a = percent(runMs, elapsed);
// Performance in whole seconds of work, so the sum fits in a long.
int p = percent(total * (ideal / 100), runMs / 100);
int q = total == 0 ? 100 : percent(good, total);
int oee = (int)(((long)a * p * q + 5000) / 10000);
Serial.print("t ");
Serial.print(elapsed / 1000);
Serial.print(running ? " RUN" : " STOP");
Serial.print(" total ");
Serial.print(total);
Serial.print(" good ");
Serial.print(good);
Serial.print(" reject ");
Serial.print(rejects);
Serial.print(" ideal ");
Serial.print(ideal);
Serial.print(" A ");
Serial.print(a);
Serial.print(" P ");
Serial.print(p);
Serial.print(" Q ");
Serial.print(q);
Serial.print(" OEE ");
Serial.println(oee);
for (int row = 0; row < 4; row++) {
for (int col = 0; col < 20; col++) screen[row][col] = ' ';
screen[row][20] = 0;
}
int col = put(0, 0, running ? "RUN " : "STOP");
col = put(0, col, " Count ");
putNumber(0, col, total, 1);
col = put(1, 0, "Good ");
col = putNumber(1, col, good, 1);
col = put(1, col, " Reject ");
putNumber(1, col, rejects, 1);
col = put(2, 0, "A");
col = putNumber(2, col, a, 4);
col = put(2, col, "% P");
col = putNumber(2, col, p, 4);
col = put(2, col, "% Q");
col = putNumber(2, col, q, 4);
put(2, col, "%");
col = put(3, 0, "OEE");
col = putNumber(3, col, oee, 4);
col = put(3, col, "% Ideal ");
col = putNumber(3, col, ideal / 1000, 1);
col = put(3, col, ".");
col = putNumber(3, col, ideal / 100 % 10, 1);
put(3, col, "s");
drawAt = 0;
}
void newShift(unsigned long now) {
shiftStart = now;
lastTick = now;
lastReport = now;
runMs = 0;
total = 0;
rejects = 0;
lastRejected = true;
Serial.println("shift start");
}
void setup() {
pinMode(SENSOR_PIN, INPUT_PULLUP);
pinMode(REJECT_PIN, INPUT_PULLUP);
pinMode(RUN_PIN, INPUT_PULLUP);
Serial.begin(9600);
Serial.println("Line OEE dashboard");
lcd.init();
lcd.backlight();
newShift(millis());
}
void loop() {
unsigned long now = millis();
// Run time grows only while the switch says the machine runs.
bool runNow = digitalRead(RUN_PIN) == LOW;
if (running) runMs += now - lastTick;
lastTick = now;
if (runNow != running) {
running = runNow;
Serial.println(running ? "machine running" : "machine stopped");
}
if (fell(part, now)) {
total++;
lastRejected = false;
Serial.print("part ");
Serial.println(total);
}
// One reject per part: the button marks the last part bad, once.
if (fell(reject, now) && !lastRejected) {
rejects++;
lastRejected = true;
Serial.print("reject ");
Serial.println(rejects);
}
if (now - lastReport >= REPORT_MS) {
lastReport += REPORT_MS;
report(now);
}
drawOne();
if (now - shiftStart >= SHIFT_MS) {
report(now);
Serial.println("shift over");
newShift(now);
}
}
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(REJECT_PIN, INPUT_PULLUP);
pinMode(RUN_PIN, INPUT_PULLUP);
Serial.begin(9600);
Serial.println("Line OEE dashboard");
lcd.init();
lcd.backlight();
newShift(millis());
}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();
// Run time grows only while the switch says the machine runs.
bool runNow = digitalRead(RUN_PIN) == LOW;
if (running) runMs += now - lastTick;
lastTick = now;
if (runNow != running) {
running = runNow;
Serial.println(running ? "machine running" : "machine stopped");
}
if (fell(part, now)) {
total++;
lastRejected = false;
Serial.print("part ");
Serial.println(total);
}
// One reject per part: the button marks the last part bad, once.
if (fell(reject, now) && !lastRejected) {
rejects++;
lastRejected = true;
Serial.print("reject ");
Serial.println(rejects);
}
if (now - lastReport >= REPORT_MS) {
lastReport += REPORT_MS;
// ... 11 more lines in the editorTroubleshooting
The usual mistakes with this circuit, and the fix for each.
- The input reads random values in one position. Slide switch
A switch only connects or disconnects. Add a pull-up or pull-down resistor, or use INPUT_PULLUP, so the pin has a defined level when the switch is open. - The load never turns on when the switch is flipped. Slide switch
Use the center (common) leg and one outer leg. The unused outer leg goes nowhere. Check that the switch is in series with the load. - 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. - OUT floats or reads high even with a target in range. IR proximity switch
The output is NPN open collector. It needs a pull-up resistor (a few kohm) to the logic supply, or INPUT_PULLUP. Detection pulls OUT low. - It does not detect, or the range is wrong. IR proximity switch
Power it from 5 V (supply is 5 V on the E18-D80NK). Adjust the range screw. Shiny or dark objects reflect differently than light matte ones. - The reading jumps around or only covers part of the range. Potentiometer
Wire the two outer legs to 5 V and ground and the middle leg (the wiper) to an analog pin. Using the wrong two legs gives a dead or partial range. - Turning the knob does nothing, or the value is stuck at 0 or 1023. Potentiometer
Check that the wiper goes to an analog input (A0 to A5 on an Uno), not a digital pin. Make sure both ends are connected, since a floating end gives noise.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.