Factory 10: Build your own, on an Arduino Uno
Design your own factory with a keypad: type two-digit codes to place belts, drills, presses and chests, then run it and watch ore turn into plates. 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.
Advanced About 35 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
- Controllers
- Games
- Choosing and reading a resistor
- How an LED lights and why it needs a resistor
- Difficulty: Advanced
- Time: About 35 minutes
- Board: Arduino Uno R3
- Parts: 9
How it works
Design your own factory with a keypad: type two-digit codes to place belts, drills, presses and chests, then run it and watch ore turn into plates.
Arduino Uno R3 runs the sketch below. The circuit around it: TFT display, 240x320, Keypad, 4x4, Resistor, 220 Ω, LED, green, Piezo speaker. pinMode sets LAMP_PIN as an output. Serial.begin opens the serial port at 9600 baud. tone plays a note on PIEZO_PIN. millis reads the time since power-up, so the sketch can time things without stopping.
Parts list
9 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 × Keypad, 4x4In Vending Machine Kit, Escape Box Kit.
- 1 × Resistor, 220 ΩIn Starter Kit, Claw Machine Kit, Halloween Prop Kit.
- 1 × LED, greenIn Starter Kit, Halloween Prop Kit, Logic Lab Kit.
- 1 × Piezo speakerIn Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 3 × 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 |
| Keypad, 4x4 pin C4 | Arduino Uno R3 pin 12 |
| 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 |
| Keypad, 4x4 pin C3 | Arduino Uno R3 pin 8 |
| Keypad, 4x4 pin C2 | Arduino Uno R3 pin 7 |
| Keypad, 4x4 pin C1 | Arduino Uno R3 pin 6 |
| Keypad, 4x4 pin R4 | Arduino Uno R3 pin 5 |
| Keypad, 4x4 pin R3 | Arduino Uno R3 pin 4 |
| Keypad, 4x4 pin R2 | Arduino Uno R3 pin 3 |
| Keypad, 4x4 pin R1 | Arduino Uno R3 pin 2 |
| Arduino Uno R3 pin GND | Ground |
| Resistor, 220 Ω pin 1 | Arduino Uno R3 pin A4 |
| Piezo speaker pin 1 | Arduino Uno R3 pin A5 |
| 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 |
| LED, green pin A | Resistor, 220 Ω pin 2 |
| LED, green pin C | Ground |
| Piezo speaker pin 2 | Ground |
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Factory 10: Build your own. A keypad is your toolbox.
//
// Put together everything from the series: drills dig ore, belts carry it,
// presses turn ore into plates, chests collect whatever arrives. You lay the
// factory out yourself with a 4x4 keypad, then run it.
//
// The keys:
// A B C D move the cursor up, down, left, right
// 0 to 9 type a two-digit tile code
// # place the tile for the code on the cursor
// * run or stop the factory
//
// The codes:
// 00 erase 10 belt right 11 belt down 12 belt left 13 belt up
// 20 drill 30 press 40 chest 50 wall
// 99 load a demo factory 98 clear everything
//
// A drill pushes ore onto any belt or into any chest or press next to it. A
// belt moves one item a step in the way its arrow points. A press takes one ore
// and, a moment later, puts out one plate. A chest accepts anything and counts it.
//
// 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
// Keypad rows R1..R4 -> pins 2, 3, 4, 5 columns C1..C4 -> pins 6, 7, 8, 12
// Run lamp: A4 through 220 ohm to GND Piezo: A5 to GND
//
// # What this teaches
//
// 1. Input as a small state machine. Typing a code is two digits and then '#'.
// The sketch remembers how many digits it has, throws a third one away by
// starting over, and only acts when '#' arrives with a complete code.
// 2. A world as data. The whole factory is two arrays, one saying what each
// tile is and one saying what it is carrying. The screen is just a view of
// those arrays, redrawn only where they change.
// 3. A simulation step. Every 250 ms each tile looks at its neighbors and
// makes its one small move. Complicated behavior, a working production
// line, comes from many tiles each following one simple rule. The same idea
// runs games of life and traffic models.
// 4. Everything before this is here too: states, timers, ratios, queues
// and flow. Build a factory that makes plates, then build one that makes
// more.
#include "mokxi_ili9341.h"
#include "mokxi_keypad.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 LAMP_PIN = A4;
const uint8_t PIEZO_PIN = A5;
SoftSpi spi(TFT_SCK, TFT_MOSI, TFT_CS);
Ili9341 tft(spi, TFT_DC);
const uint8_t ROW_PINS[4] = {2, 3, 4, 5};
const uint8_t COL_PINS[4] = {6, 7, 8, 12};
Keypad pad(ROW_PINS, COL_PINS);
const int16_t TILE = 16;
const int16_t WORLD_X = 8;
const int16_t WORLD_Y = 8;
const uint8_t COLS = 14;
const uint8_t ROWS = 10;
const uint8_t CELLS = COLS * ROWS;
// What a tile is.
enum Tile : uint8_t { EMPTY, BELT_R, BELT_D, BELT_L, BELT_U, DRILL, PRESS, CHEST, WALL };
// What a tile carries.
enum Item : uint8_t { NONE, ORE, PLATE };
const uint16_t STEEL = 0x8c71;
const uint16_t DARK = 0x10a2;
const uint16_t BELT = 0x31a6;
const uint16_t ARROW = 0x7bef;
const uint16_t ORE_COLOUR = 0xfc60;
const uint16_t PLATE_COLOUR = 0xc618;
const uint16_t PRESS_COLOUR = 0x780f;
const unsigned long TICK_MS = 250;
const uint8_t DRILL_TICKS = 4; // a drill makes an ore every 4 ticks
const uint8_t PRESS_TICKS = 3; // a press takes 3 ticks per plate
uint8_t tile[CELLS];
uint8_t item[CELLS];
uint8_t timer[CELLS]; // drills and presses count their work here
bool moved[CELLS]; // an item moves once per tick
bool dirty[CELLS]; // this tile must be drawn again
uint8_t cursor = 4 * COLS + 2;
bool running = false;
unsigned long lastTick = 0;
unsigned int oreIn = 0, plateIn = 0; // what the chests have collected
unsigned int shownOre = 65535, shownPlates = 65535;
bool shownRunning = true;
int shownCode = -2;
int8_t shownMeaning = -2;
// The code being typed: -1 for nothing, otherwise how many digits so far.
int digits = 0;
int codeValue = 0;
static uint8_t at(uint8_t c, uint8_t r) { return (uint8_t)(r * COLS + c); }
static uint8_t colOf(uint8_t i) { return (uint8_t)(i % COLS); }
static uint8_t rowOf(uint8_t i) { return (uint8_t)(i / COLS); }
static bool isBelt(uint8_t t) { return t >= BELT_R && t <= BELT_U; }
// The tile a belt points at, or 255 if that is off the edge.
static uint8_t facing(uint8_t i) {
int c = colOf(i), r = rowOf(i);
switch (tile[i]) {
case BELT_R: c++; break;
case BELT_D: r++; break;
case BELT_L: c--; break;
case BELT_U: r--; break;
}
if (c < 0 || c >= COLS || r < 0 || r >= ROWS) {
return 255;
}
return at((uint8_t)c, (uint8_t)r);
}
// ---- drawing ---------------------------------------------------------------
static void drawItem(int16_t x, int16_t y, uint8_t it) {
if (it == ORE) {
tft.fillRect((int16_t)(x + 5), (int16_t)(y + 5), 6, 6, ORE_COLOUR);
} else if (it == PLATE) {
tft.fillRect((int16_t)(x + 4), (int16_t)(y + 6), 8, 4, PLATE_COLOUR);
}
}
static void drawTile(uint8_t i) {
int16_t x = (int16_t)(WORLD_X + colOf(i) * TILE);
int16_t y = (int16_t)(WORLD_Y + rowOf(i) * TILE);
uint8_t t = tile[i];
tft.fillRect(x, y, TILE, TILE, ILI9341_BLACK);
if (isBelt(t)) {
tft.fillRect(x, y, TILE, TILE, BELT);
// an arrow: a shaft along the belt and a head at the front
if (t == BELT_R || t == BELT_L) {
int16_t head = t == BELT_R ? (int16_t)(x + 11) : (int16_t)(x + 1);
tft.fillRect((int16_t)(x + 2), (int16_t)(y + 7), 12, 2, ARROW);
tft.fillRect(head, (int16_t)(y + 5), 4, 6, ARROW);
} else {
int16_t head = t == BELT_D ? (int16_t)(y + 11) : (int16_t)(y + 1);
tft.fillRect((int16_t)(x + 7), (int16_t)(y + 2), 2, 12, ARROW);
tft.fillRect((int16_t)(x + 5), head, 6, 4, ARROW);
}
drawItem(x, y, item[i]);
} else if (t == DRILL) {
tft.fillRect(x, y, TILE, TILE, STEEL);
tft.fillRect((int16_t)(x + 4), (int16_t)(y + 4), 8, 8, ORE_COLOUR);
} else if (t == PRESS) {
tft.fillRect(x, y, TILE, TILE, STEEL);
tft.fillRect((int16_t)(x + 3), (int16_t)(y + 3), 10, 10, PRESS_COLOUR);
if (item[i] != NONE) {
tft.fillRect((int16_t)(x + 6), (int16_t)(y + 6), 4, 4, item[i] == ORE ? ORE_COLOUR : PLATE_COLOUR);
}
} else if (t == CHEST) {
tft.fillRect(x, y, TILE, TILE, STEEL);
tft.fillRect((int16_t)(x + 3), (int16_t)(y + 3), 10, 10, DARK);
} else if (t == WALL) {
tft.fillRect(x, y, TILE, TILE, 0x4208);
tft.rect(x, y, TILE, TILE, DARK, 2);
}
if (i == cursor) {
tft.rect(x, y, TILE, TILE, ILI9341_YELLOW, 2);
}
dirty[i] = false;
}
static void drawLabels() {
tft.text(8, 176, "ORE", ILI9341_WHITE, ILI9341_BLACK, 2);
tft.text(104, 176, "PLATES", ILI9341_WHITE, ILI9341_BLACK, 2);
tft.text(8, 198, "CODE", ILI9341_WHITE, ILI9341_BLACK, 2);
}
// What a code means, for the line under the screen.
static const char *meaningOf(int code) {
switch (code) {
case 0: return "ERASE ";
case 10: return "BELT RIGHT ";
case 11: return "BELT DOWN ";
case 12: return "BELT LEFT ";
case 13: return "BELT UP ";
case 20: return "DRILL ";
case 30: return "PRESS ";
case 40: return "CHEST ";
case 50: return "WALL ";
case 99: return "LOAD DEMO ";
case 98: return "CLEAR ALL ";
default: return "NO SUCH CODE ";
}
}
static void report() {
uint8_t tiles = 0;
for (uint8_t i = 0; i < CELLS; i++) {
if (tile[i] != EMPTY) {
tiles++;
}
}
Serial.print("tiles ");
Serial.print(tiles);
Serial.print(" run ");
Serial.print(running ? 1 : 0);
Serial.print(" ore ");
Serial.print(oreIn);
Serial.print(" plates ");
Serial.println(plateIn);
}
// One drawing job per call: a changed tile first, then the numbers.
static void refresh() {
for (uint8_t i = 0; i < CELLS; i++) {
if (dirty[i]) {
drawTile(i);
return;
}
}
if (oreIn != shownOre) {
shownOre = oreIn;
tft.number(8 + 4 * 12, 176, oreIn, ILI9341_YELLOW, ILI9341_BLACK, 2, 3);
} else if (plateIn != shownPlates) {
shownPlates = plateIn;
tft.number(104 + 7 * 12, 176, plateIn, ILI9341_YELLOW, ILI9341_BLACK, 2, 3);
} else if (running != shownRunning) {
shownRunning = running;
tft.text(8 + 11 * 12, 198, running ? "RUN " : "STOP", running ? ILI9341_GREEN : ILI9341_RED, ILI9341_BLACK, 2);
} else if (codeValue + 100 * digits != shownCode) {
shownCode = codeValue + 100 * digits;
// the digits typed so far, with dashes for the ones still to come
char text[3] = {'-', '-', 0};
if (digits >= 1) {
text[0] = (char)('0' + codeValue / 10);
}
if (digits >= 2) {
text[1] = (char)('0' + codeValue % 10);
}
tft.text(8 + 5 * 12, 198, text, ILI9341_CYAN, ILI9341_BLACK, 2);
tft.text(8, 220, digits == 2 ? meaningOf(codeValue) : " ", ILI9341_WHITE, ILI9341_BLACK, 2);
}
}
// ---- building --------------------------------------------------------------
static void place(uint8_t i, uint8_t t) {
tile[i] = t;
item[i] = NONE;
timer[i] = 0;
dirty[i] = true;
}
static void clearAll() {
for (uint8_t i = 0; i < CELLS; i++) {
place(i, EMPTY);
}
oreIn = plateIn = 0;
}
// A small working factory to start from: two drills, a press and a chest.
static void loadDemo() {
clearAll();
place(at(2, 4), DRILL);
for (uint8_t c = 3; c <= 5; c++) {
place(at(c, 4), BELT_R);
}
place(at(6, 4), PRESS);
place(at(7, 4), BELT_R);
place(at(8, 4), BELT_R);
place(at(9, 4), CHEST);
place(at(4, 2), DRILL);
place(at(4, 3), BELT_D);
}
// Act on a complete code. Returns false for a code that means nothing.
static bool applyCode(int code) {
switch (code) {
case 0: place(cursor, EMPTY); return true;
case 10: place(cursor, BELT_R); return true;
case 11: place(cursor, BELT_D); return true;
case 12: place(cursor, BELT_L); return true;
case 13: place(cursor, BELT_U); return true;
case 20: place(cursor, DRILL); return true;
case 30: place(cursor, PRESS); return true;
case 40: place(cursor, CHEST); return true;
case 50: place(cursor, WALL); return true;
case 99: loadDemo(); return true;
case 98: clearAll(); return true;
}
return false;
}
// ---- the simulation --------------------------------------------------------
// Could the tile at `to` take an item of this kind from `from` right now?
static bool accepts(uint8_t to, uint8_t from, uint8_t what) {
uint8_t t = tile[to];
if (t == CHEST) {
return true;
}
if (t == PRESS) {
return what == ORE && item[to] == NONE;
}
if (isBelt(t)) {
return item[to] == NONE && facing(to) != from; // not onto a belt that points back
}
return false;
}
static void deliver(uint8_t to, uint8_t what) {
if (tile[to] == CHEST) {
if (what == ORE) {
oreIn++;
} else {
plateIn++;
}
tone(PIEZO_PIN, what == ORE ? 700 : 1100, 25);
report();
} else {
item[to] = what == ORE && tile[to] == PRESS ? ORE : what;
timer[to] = 0;
moved[to] = true;
dirty[to] = true;
}
}
// Offer an item to the four neighbors of `from`, in a fixed order. True if taken.
static bool pushOut(uint8_t from, uint8_t what) {
uint8_t c = colOf(from), r = rowOf(from);
uint8_t neighbors[4] = {255, 255, 255, 255};
if (c + 1 < COLS) neighbors[0] = at((uint8_t)(c + 1), r);
if (r + 1 < ROWS) neighbors[1] = at(c, (uint8_t)(r + 1));
if (c > 0) neighbors[2] = at((uint8_t)(c - 1), r);
if (r > 0) neighbors[3] = at(c, (uint8_t)(r - 1));
for (uint8_t k = 0; k < 4; k++) {
uint8_t n = neighbors[k];
if (n != 255 && accepts(n, from, what)) {
deliver(n, what);
return true;
}
}
return false;
}
static void stepTile(uint8_t i) {
switch (tile[i]) {
case DRILL:
timer[i]++;
if (timer[i] >= DRILL_TICKS) {
if (pushOut(i, ORE)) {
timer[i] = 0;
} else {
timer[i] = DRILL_TICKS; // wait, full, until there is room
}
}
break;
case PRESS:
if (item[i] == ORE && !moved[i]) {
timer[i]++;
if (timer[i] >= PRESS_TICKS) {
item[i] = PLATE;
timer[i] = 0;
dirty[i] = true;
}
} else if (item[i] == PLATE && pushOut(i, PLATE)) {
item[i] = NONE;
dirty[i] = true;
}
break;
default:
if (isBelt(tile[i]) && item[i] != NONE && !moved[i]) {
uint8_t to = facing(i);
if (to != 255 && accepts(to, i, item[i])) {
uint8_t what = item[i];
item[i] = NONE;
dirty[i] = true;
deliver(to, what);
}
}
break;
}
}
static void tick() {
for (uint8_t i = 0; i < CELLS; i++) {
moved[i] = false;
}
for (uint8_t i = 0; i < CELLS; i++) {
stepTile(i);
}
// a second sweep the other way lets a queue of items shuffle forward together
for (int i = CELLS - 1; i >= 0; i--) {
if (isBelt(tile[i])) {
stepTile((uint8_t)i);
}
}
}
static void moveCursor(int dc, int dr) {
int c = colOf(cursor) + dc, r = rowOf(cursor) + dr;
if (c < 0 || c >= COLS || r < 0 || r >= ROWS) {
return;
}
dirty[cursor] = true;
cursor = at((uint8_t)c, (uint8_t)r);
dirty[cursor] = true;
}
static void handleKey(char key) {
if (key >= '0' && key <= '9') {
if (digits >= 2) {
digits = 0; // a third digit starts a new code
codeValue = 0;
}
codeValue = codeValue * 10 + (key - '0');
digits++;
tone(PIEZO_PIN, 500, 15);
} else if (key == '#') {
if (digits == 2 && applyCode(codeValue)) {
tone(PIEZO_PIN, 900, 40);
report();
} else {
tone(PIEZO_PIN, 180, 120); // nothing to place, or not a code
}
digits = 0;
codeValue = 0;
} else if (key == 'A') {
if (digits > 0) {
digits = 0; // A with a code half typed cancels it
codeValue = 0;
} else {
moveCursor(0, -1);
}
} else if (key == 'B') {
moveCursor(0, 1);
} else if (key == 'C') {
moveCursor(-1, 0);
} else if (key == 'D') {
moveCursor(1, 0);
} else if (key == '*') {
running = !running;
digitalWrite(LAMP_PIN, running ? HIGH : LOW);
lastTick = millis();
report();
}
}
void setup() {
Serial.begin(9600);
pinMode(LAMP_PIN, OUTPUT);
pad.begin();
tft.begin();
tft.rect(WORLD_X - 3, WORLD_Y - 3, 14 * TILE + 6, 10 * TILE + 6, 0x4a49, 2);
for (uint8_t i = 0; i < CELLS; i++) {
dirty[i] = true;
}
drawLabels();
report();
}
void loop() {
char key = pad.pressed();
if (key != 0) {
handleKey(key);
}
if (running && millis() - lastTick >= TICK_MS) {
lastTick = millis();
tick();
}
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(LAMP_PIN, OUTPUT);
pad.begin();
tft.begin();
tft.rect(WORLD_X - 3, WORLD_Y - 3, 14 * TILE + 6, 10 * TILE + 6, 0x4a49, 2);
for (uint8_t i = 0; i < CELLS; i++) {
dirty[i] = true;
}
drawLabels();
report();
}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() {
char key = pad.pressed();
if (key != 0) {
handleKey(key);
}
if (running && millis() - lastTick >= TICK_MS) {
lastTick = millis();
tick();
}
refresh();
}Troubleshooting
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. - Pressing one key reports several keys, or the wrong key. Keypad
Match the keymap and the row and column pin order to your keypad's pinout. Without diodes, pressing three keys at once can ghost a fourth. - No keys are detected. Keypad
Rows need pull-ups, or use INPUT_PULLUP, while one column is driven low at a time. The Keypad library does this for you if you pass the pins in the right order. - 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.