Factory 9: Production dashboard, on an Arduino Uno
A three-stage line measured live: items per minute and how busy each machine is over a rolling twenty seconds, with a bottleneck finder that lights the slowest stage. The whole build, an Arduino Uno and 12 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.
Advanced About 45 minutes to build and run. Runs in your browser. Free, and no account needed.
The parts retail for about $38. On Mokxi they're free.
What you'll learn
- Displays
- Sensors
- Lights
- Reading a button
- Choosing and reading a resistor
- Difficulty: Advanced
- Time: About 45 minutes
- Board: Arduino Uno R3
- Parts: 19
How it works
A three-stage line measured live: items per minute and how busy each machine is over a rolling twenty seconds, with a bottleneck finder that lights the slowest stage.
Arduino Uno R3 runs the sketch below. The circuit around it: TFT display, 240x320, Pushbutton, Resistor, 220 Ω, LED, green, LED, yellow, LED, red. pinMode sets BUTTON_PIN as an input with its pull-up on. digitalRead reads whether b.pin is HIGH or LOW. analogRead measures the voltage on KNOB_PINS[s] as a number. Serial.begin opens the serial port at 9600 baud. tone plays a note on PIEZO_PIN.
Parts list
19 parts, plus the jumper wires. They retail for about $38; on Mokxi they're free.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × TFT display, 240x320In Handheld Console Kit.
- 1 × PushbuttonIn Starter Kit, Christmas Lights Kit, Logic Lab Kit.
- 3 × Resistor, 220 ΩIn Starter Kit, Claw Machine Kit, Halloween Prop Kit.
- 1 × LED, greenIn Starter Kit, Halloween Prop Kit, Logic Lab Kit.
- 1 × LED, yellowIn Starter Kit, Halloween Prop Kit, Logic Lab Kit.
- 1 × LED, redIn Starter Kit, Halloween Prop Kit, Logic Lab Kit.
- 1 × Piezo speakerIn Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 3 × PotentiometerIn Halloween Prop Kit, Starter Projects Kit.
- 6 × Power, 5 V
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 |
|---|---|
| TFT display, 240x320 pin SCK | Arduino Uno R3 pin 13 |
| TFT display, 240x320 pin MOSI | Arduino Uno R3 pin 11 |
| TFT display, 240x320 pin CS | Arduino Uno R3 pin 10 |
| TFT display, 240x320 pin DC | Arduino Uno R3 pin 9 |
| Resistor, 220 Ω (3) pin 1 | Arduino Uno R3 pin 8 |
| Piezo speaker pin 1 | Arduino Uno R3 pin 6 |
| Resistor, 220 Ω (2) pin 1 | Arduino Uno R3 pin 5 |
| Resistor, 220 Ω (1) pin 1 | Arduino Uno R3 pin 3 |
| Pushbutton pin 1a | Arduino Uno R3 pin 2 |
| Arduino Uno R3 pin GND | Ground |
| Potentiometer (1) pin 2 | Arduino Uno R3 pin A0 |
| Potentiometer (2) pin 2 | Arduino Uno R3 pin A1 |
| Potentiometer (3) pin 2 | Arduino Uno R3 pin A2 |
| TFT display, 240x320 pin VCC | 5 V |
| TFT display, 240x320 pin GND | Ground |
| TFT display, 240x320 pin RST | 5 V |
| TFT display, 240x320 pin LED | 5 V |
| Pushbutton pin 2a | Ground |
| LED, green pin A | Resistor, 220 Ω (1) pin 2 |
| LED, green pin C | Ground |
| LED, yellow pin A | Resistor, 220 Ω (2) pin 2 |
| LED, yellow pin C | Ground |
| LED, red pin A | Resistor, 220 Ω (3) pin 2 |
| LED, red pin C | Ground |
| Piezo speaker pin 2 | Ground |
| Potentiometer (1) pin 1 | Ground |
| Potentiometer (1) pin 3 | 5 V |
| Potentiometer (2) pin 1 | Ground |
| Potentiometer (2) pin 3 | 5 V |
| Potentiometer (3) pin 1 | Ground |
| Potentiometer (3) pin 3 | 5 V |
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Factory 9: Production dashboard. Measure the line, find the bottleneck.
//
// A small production line runs by itself: a MINE digs ore, a SMELTER turns each
// ore into a plate, an ASSEMBLER turns each plate into a widget. Three knobs
// set how fast each stage works. Between the stages sit queues that hold eight
// items. The dashboard measures what really happens over the last twenty
// seconds: how many items per minute each stage made and how busy each one
// was, and it points at the bottleneck with a lamp.
//
// Wiring, board to parts:
// TFT CS -> pin 10 DC -> pin 9 SCK -> pin 13 MOSI -> pin 11
// RST and LED -> 5 V, VCC -> 5 V, GND -> GND
// Knobs (10k pots, middle leg to the analog pin): MINE -> A0, SMELTER -> A1,
// ASSEMBLER -> A2
// RESET button: pin 2 to GND
// Bottleneck lamps through 220 ohm to GND: mine pin 3, smelter pin 5,
// assembler pin 8 Piezo: pin 6 to GND
//
// # What this teaches
//
// 1. Telemetry. You cannot improve what you cannot see. Instead of guessing,
// the sketch counts finished items and the time each stage spends working,
// and turns them into numbers: items per minute and utilization.
// 2. A rolling window. Numbers over "all time" hide what is happening now. The
// sketch keeps ten buckets of two seconds each in a ring; the oldest bucket
// is cleared and reused as time moves on, so every number describes the
// last twenty seconds without storing a long history.
// 3. Starved and blocked. A stage with nothing to work on is STARVED (its
// supplier is too slow). A stage with nowhere to put its result is BLOCKED
// (its customer is too slow). The stage that is busiest and has others
// waiting on it is the bottleneck, and the line can never go faster than
// that stage.
// 4. Improving a line means speeding up the bottleneck and nothing else.
// Turning up a stage that is not the bottleneck only fills a queue.
#include "mokxi_ili9341.h"
const uint8_t TFT_CS = 10;
const uint8_t TFT_DC = 9;
const uint8_t TFT_SCK = 13;
const uint8_t TFT_MOSI = 11;
const uint8_t BUTTON_PIN = 2;
const uint8_t LAMP_PINS[3] = {3, 5, 8};
const uint8_t PIEZO_PIN = 6;
const uint8_t KNOB_PINS[3] = {A0, A1, A2};
SoftSpi spi(TFT_SCK, TFT_MOSI, TFT_CS);
Ili9341 tft(spi, TFT_DC);
const int16_t TILE = 16;
const int16_t WORLD_X = 8;
const int16_t WORLD_Y = 8;
const uint16_t STEEL = 0x8c71;
const uint16_t DARK = 0x10a2;
const uint16_t WORKING = 0x2c0a;
const uint16_t STARVED = 0x8400;
const uint16_t BLOCKED = 0x8000;
const uint8_t STAGES = 3;
const char *const NAMES[STAGES] = {"MINE", "SMELTER", "ASSEMBLER"};
const char *const PADDED[STAGES] = {"MINE ", "SMELTER ", "ASSEMBLER "};
const uint8_t QUEUE_CAP = 8;
const uint8_t BUCKETS = 10; // ten buckets ...
const unsigned long BUCKET_MS = 2000; // ... of two seconds: a 20 s window
// A button that reports a press once, after the contacts have stopped bouncing.
struct Button {
uint8_t pin;
bool raw;
bool stable;
unsigned long since;
};
static bool pressedOnce(Button &b) {
bool down = digitalRead(b.pin) == LOW;
unsigned long now = millis();
if (down != b.raw) {
b.raw = down;
b.since = now;
}
if (now - b.since >= 20 && b.stable != b.raw) {
b.stable = b.raw;
return b.stable;
}
return false;
}
Button resetButton = {BUTTON_PIN, false, false, 0};
enum State { IDLE_STARVED, RUNNING, HELD_BLOCKED };
State state[STAGES];
State shownState[STAGES];
bool working[STAGES];
unsigned long endAt[STAGES];
unsigned long cycleMs[STAGES] = {1500, 1500, 1500};
uint8_t queue[2] = {0, 0}; // ore waiting, plates waiting
uint8_t shownQueue[2] = {255, 255};
unsigned long widgets = 0;
// The rolling window: busy milliseconds and finished items per bucket.
unsigned int busy[STAGES][BUCKETS];
unsigned int finished[STAGES][BUCKETS];
unsigned long bucketNumber = 0; // which two-second slice we are in
unsigned long startedAt = 0;
unsigned long lastFrame = 0, lastStats = 0, lastReport = 0, lastBeep = 0;
int ipm[STAGES]; // items per minute over the window
int util[STAGES]; // percent busy over the window
int bottleneck = -1; // -1 means none
int shownIpm[STAGES] = {-1, -1, -1};
int shownUtil[STAGES] = {-1, -1, -1};
int shownBottleneck = -2;
bool shownHot[STAGES] = {false, false, false}; // is each bar drawn as the bottleneck?
long shownWidgets = -1;
static int16_t tileX(uint8_t c) { return (int16_t)(WORLD_X + c * TILE); }
static int16_t tileY(uint8_t r) { return (int16_t)(WORLD_Y + r * TILE); }
static uint8_t bucketIndex() { return (uint8_t)(bucketNumber % BUCKETS); }
// ---- the line --------------------------------------------------------------
// Try to start the next item at a stage. Returns true if work began.
static bool tryStart(uint8_t s, unsigned long now) {
if (s > 0) {
if (queue[s - 1] == 0) {
return false; // nothing to work on: starved
}
queue[s - 1]--;
}
working[s] = true;
endAt[s] = now + cycleMs[s];
return true;
}
static void stepStage(uint8_t s, unsigned long now, unsigned long dt) {
if (working[s]) {
if ((long)(now - endAt[s]) < 0) {
// Still working: this is the only time that counts as busy.
busy[s][bucketIndex()] += (unsigned int)dt;
state[s] = RUNNING;
return;
}
// Finished: hand the item on, unless the next queue is full. A stage that
// cannot hand on is BLOCKED, and blocked is not busy.
if (s < STAGES - 1) {
if (queue[s] >= QUEUE_CAP) {
state[s] = HELD_BLOCKED;
return;
}
queue[s]++;
} else {
widgets++;
}
finished[s][bucketIndex()]++;
working[s] = false;
}
// Idle: start the next item if there is one to work on, else wait starved.
state[s] = tryStart(s, now) ? RUNNING : IDLE_STARVED;
}
// Move to a new bucket when two seconds are up, clearing the one we reuse.
static void rollWindow(unsigned long now) {
unsigned long number = (now - startedAt) / BUCKET_MS;
while (bucketNumber < number) {
bucketNumber++;
uint8_t b = bucketIndex();
for (uint8_t s = 0; s < STAGES; s++) {
busy[s][b] = 0;
finished[s][b] = 0;
}
}
}
// Turn the buckets into numbers, and decide which stage is the bottleneck.
static void measure(unsigned long now) {
unsigned long elapsed = now - startedAt;
unsigned long window = elapsed < BUCKETS * BUCKET_MS ? elapsed : BUCKETS * BUCKET_MS;
if (window < BUCKET_MS) {
window = BUCKET_MS;
}
for (uint8_t s = 0; s < STAGES; s++) {
unsigned long busySum = 0, doneSum = 0;
for (uint8_t b = 0; b < BUCKETS; b++) {
busySum += busy[s][b];
doneSum += finished[s][b];
}
util[s] = (int)((busySum * 100UL) / window);
if (util[s] > 100) {
util[s] = 100;
}
ipm[s] = (int)((doneSum * 60000UL) / window);
}
// The bottleneck is the busiest stage, once the line has run long enough to
// mean anything. If two are within 5 points, the one with more waiting in
// front of it is the real limit.
int best = -1;
if (elapsed >= 6000) {
int bestUtil = 69; // below 70 percent nothing is limiting
for (uint8_t s = 0; s < STAGES; s++) {
int waiting = s == 0 ? 0 : queue[s - 1];
int bestWaiting = best < 0 ? 0 : (best == 0 ? 0 : queue[best - 1]);
if (util[s] > bestUtil || (best >= 0 && util[s] >= bestUtil - 5 && waiting > bestWaiting)) {
best = s;
bestUtil = util[s];
}
}
}
if (best != bottleneck) {
bottleneck = best;
for (uint8_t s = 0; s < STAGES; s++) {
digitalWrite(LAMP_PINS[s], s == bottleneck ? HIGH : LOW);
}
if (bottleneck >= 0 && now - lastBeep > 1000) {
lastBeep = now;
tone(PIEZO_PIN, 800 + 200 * bottleneck, 80);
}
}
}
static void report(unsigned long now) {
Serial.print("t ");
Serial.print((now - startedAt) / 1000);
Serial.print(" ipm ");
Serial.print(ipm[0]);
Serial.print(" ");
Serial.print(ipm[1]);
Serial.print(" ");
Serial.print(ipm[2]);
Serial.print(" util ");
Serial.print(util[0]);
Serial.print(" ");
Serial.print(util[1]);
Serial.print(" ");
Serial.print(util[2]);
Serial.print(" q ");
Serial.print(queue[0]);
Serial.print(" ");
Serial.print(queue[1]);
Serial.print(" bn ");
Serial.println(bottleneck < 0 ? "NONE" : NAMES[bottleneck]);
}
// ---- drawing ---------------------------------------------------------------
static const uint8_t MACHINE_COL[STAGES] = {1, 6, 11};
static void drawMachine(uint8_t s) {
uint16_t colour = state[s] == RUNNING ? WORKING : (state[s] == IDLE_STARVED ? STARVED : BLOCKED);
int16_t x = tileX(MACHINE_COL[s]), y = tileY(0);
tft.fillRect(x, y, 2 * TILE, 2 * TILE, STEEL);
tft.fillRect((int16_t)(x + 3), (int16_t)(y + 3), 2 * TILE - 6, 2 * TILE - 6, colour);
shownState[s] = state[s];
}
// Eight little squares between two machines: filled for each waiting item.
static void drawQueue(uint8_t q) {
int16_t x = tileX(q == 0 ? 3 : 8), y = (int16_t)(tileY(0) + 13);
for (uint8_t i = 0; i < QUEUE_CAP; i++) {
tft.fillRect((int16_t)(x + i * 6), y, 5, 6, i < queue[q] ? 0xfc60 : DARK);
}
shownQueue[q] = queue[q];
}
static void drawBar(uint8_t s) {
int16_t y = (int16_t)(tileY(3) + 4 + s * 26);
tft.fillRect(64, y, 104, 14, DARK);
if (util[s] > 0) {
uint16_t colour = (int)s == bottleneck ? ILI9341_RED : ILI9341_GREEN;
tft.fillRect(64, y, (int16_t)util[s], 14, colour);
}
shownUtil[s] = util[s];
shownHot[s] = (int)s == bottleneck;
}
static void drawLabels() {
for (uint8_t s = 0; s < STAGES; s++) {
tft.text(8, (int16_t)(tileY(3) + 4 + s * 26), s == 0 ? "MINE " : (s == 1 ? "SMELT" : "ASSEM"), ILI9341_WHITE,
ILI9341_BLACK, 2);
}
tft.text(64, 50, "BUSY PERCENT", ILI9341_CYAN, ILI9341_BLACK, 1);
tft.text(176, 50, "PER MIN", ILI9341_CYAN, ILI9341_BLACK, 1);
tft.text(8, 176, "BOTTLENECK", ILI9341_WHITE, ILI9341_BLACK, 2);
tft.text(8, 198, "WIDGETS", ILI9341_WHITE, ILI9341_BLACK, 2);
}
// One drawing job per call.
static void refresh() {
for (uint8_t s = 0; s < STAGES; s++) {
if (state[s] != shownState[s]) {
drawMachine(s);
return;
}
}
for (uint8_t q = 0; q < 2; q++) {
if (queue[q] != shownQueue[q]) {
drawQueue(q);
return;
}
}
for (uint8_t s = 0; s < STAGES; s++) {
if (util[s] != shownUtil[s] || shownHot[s] != ((int)s == bottleneck)) {
drawBar(s);
return;
}
}
for (uint8_t s = 0; s < STAGES; s++) {
if (ipm[s] != shownIpm[s]) {
shownIpm[s] = ipm[s];
tft.number(176, (int16_t)(tileY(3) + 4 + s * 26), ipm[s], ILI9341_YELLOW, ILI9341_BLACK, 2, 3);
return;
}
}
if ((long)widgets != shownWidgets) {
shownWidgets = (long)widgets;
tft.number(8 + 8 * 12, 198, (long)widgets, ILI9341_YELLOW, ILI9341_BLACK, 2, 4);
return;
}
if (bottleneck != shownBottleneck) {
shownBottleneck = bottleneck;
tft.text(8, 220, bottleneck < 0 ? "NONE " : PADDED[bottleneck], bottleneck < 0 ? ILI9341_GREEN : ILI9341_RED,
ILI9341_BLACK, 2);
}
}
static void resetLine(unsigned long now) {
for (uint8_t s = 0; s < STAGES; s++) {
working[s] = false;
state[s] = IDLE_STARVED;
for (uint8_t b = 0; b < BUCKETS; b++) {
busy[s][b] = 0;
finished[s][b] = 0;
}
}
queue[0] = queue[1] = 0;
widgets = 0;
bucketNumber = 0;
startedAt = now;
}
void setup() {
Serial.begin(9600);
pinMode(BUTTON_PIN, INPUT_PULLUP);
for (uint8_t s = 0; s < STAGES; s++) {
pinMode(LAMP_PINS[s], OUTPUT);
}
tft.begin();
tft.rect(WORLD_X - 3, WORLD_Y - 3, 14 * TILE + 6, 10 * TILE + 6, 0x4a49, 2);
resetLine(millis());
drawLabels();
for (uint8_t s = 0; s < STAGES; s++) {
drawMachine(s);
shownState[s] = state[s];
}
for (uint8_t q = 0; q < 2; q++) {
drawQueue(q);
}
lastFrame = lastStats = lastReport = millis();
}
void loop() {
unsigned long now = millis();
unsigned long dt = now - lastFrame;
lastFrame = now;
if (pressedOnce(resetButton)) {
resetLine(now);
tone(PIEZO_PIN, 400, 80);
}
// The knobs: each stage takes 3000 ms per item with the knob down to 600 ms up.
for (uint8_t s = 0; s < STAGES; s++) {
cycleMs[s] = 3000UL - (unsigned long)analogRead(KNOB_PINS[s]) * 2400UL / 1023UL;
}
rollWindow(now);
for (uint8_t s = 0; s < STAGES; s++) {
stepStage(s, now, dt);
}
if (now - lastStats >= 500) {
lastStats = now;
measure(now);
}
if (now - lastReport >= 2000) {
lastReport = now;
report(now);
}
refresh();
}
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() {
Serial.begin(9600);
pinMode(BUTTON_PIN, INPUT_PULLUP);
for (uint8_t s = 0; s < STAGES; s++) {
pinMode(LAMP_PINS[s], OUTPUT);
}
tft.begin();
tft.rect(WORLD_X - 3, WORLD_Y - 3, 14 * TILE + 6, 10 * TILE + 6, 0x4a49, 2);
resetLine(millis());
drawLabels();
for (uint8_t s = 0; s < STAGES; s++) {
drawMachine(s);
shownState[s] = state[s];
}
for (uint8_t q = 0; q < 2; q++) {
drawQueue(q);
}
lastFrame = lastStats = lastReport = 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();
unsigned long dt = now - lastFrame;
lastFrame = now;
if (pressedOnce(resetButton)) {
resetLine(now);
tone(PIEZO_PIN, 400, 80);
}
// The knobs: each stage takes 3000 ms per item with the knob down to 600 ms up.
for (uint8_t s = 0; s < STAGES; s++) {
cycleMs[s] = 3000UL - (unsigned long)analogRead(KNOB_PINS[s]) * 2400UL / 1023UL;
}
rollWindow(now);
for (uint8_t s = 0; s < STAGES; s++) {
stepStage(s, now, dt);
}
if (now - lastStats >= 500) {
lastStats = now;
measure(now);
}
if (now - lastReport >= 2000) {
lastReport = now;
report(now);
}
// ... 2 more lines in the editorTroubleshooting
The usual mistakes with this circuit, and the fix for each.
- The screen stays white or black. TFT display
Check the SPI wiring: CS, DC, RST, MOSI, SCK. Power the module at 3.3 V logic, or use level shifting on 5 V boards. Call begin() and set the rotation. - Slow drawing or garbled colors. TFT display
Use hardware SPI pins and avoid redrawing the whole screen. Fill only changed areas. Confirm the library matches ILI9341 and the color order. - 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. - The LED is dim, burns out, or the pin reads wrong because the resistor value is off. Resistor
Read the color bands again. A 220 ohm to 330 ohm resistor suits a typical LED on 5 V. 10k is a common pull-up or pull-down value. Check with a multimeter if unsure. - A pull-up or pull-down resistor is missing, so the input floats and reads random values. Resistor
Tie the input to 5 V or ground through about 10k. Or use INPUT_PULLUP and wire the switch to ground. - The LED does not light, or it lights once and then dies. LED
LEDs have polarity. The longer leg (anode) goes toward the positive side. Always add a series resistor, about 220 ohms to 1 k ohm on a 5 V pin. - A blue or white LED stays dark on 3.3 V. LED
Blue and white LEDs drop about 3 V, so 3.3 V leaves almost nothing for the resistor. Use a 5 V supply, or a smaller resistor and a red or green LED.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.