Learning Goals 5 min
- Build the Cluster E capstone: a self-contained digital thermometer that reads the DHT11 (or thermistor or TMP36) and shows temperature + humidity + status on the LCD, with NO Serial Monitor required.
- Add a single button to toggle between two screens: "Now" (current readings) and "Stats" (min, max and average since boot). A real product's tiny but useful feature.
- Apply every Cluster E lesson — wiring, library, cursor control, custom characters, sprintf formatting — into one polished ship-ready sketch you could leave plugged in for days.
Warm-Up 10 min
Cluster E started with bare-LCD wiring (L02-27), added the library (L02-28), tamed ghosting (L02-29), unlocked custom characters (L02-30), and formatted numbers properly (L02-31). Today it all comes together as one thing on your desk: a standalone digital thermometer with a screen, a button, and zero need for a computer.
Predict the spec
Sketch the two screens of your product before reading on. What goes on row 0 vs row 1 of the "Now" screen? What goes on the "Stats" screen?
One reasonable spec
Now screen:
27.3*C humid 67% RH 27 min
(where * is the custom degree symbol)
Stats screen:
Lo 24.1 Hi 29.8 Avg 26.4* over 1h
Button press cycles. Both screens auto-refresh.
New Concept · Two screens, one button 20 min
The state machine — small enough to keep in your head
Two states (NOW, STATS), one button transitioning between them. Same enum + switch pattern as the Smart Bin Lid (L02-26):
enum Screen { NOW, STATS };
Screen screen = NOW;
void handleButton() {
static bool last = HIGH;
bool now = digitalRead(BUTTON);
if (last == HIGH && now == LOW) {
screen = (screen == NOW) ? STATS : NOW;
needRedraw = true;
}
last = now;
}The button is wired with INPUT_PULLUP (button between pin and GND, no external resistor). digitalRead returns HIGH when released, LOW when pressed. The last == HIGH && now == LOW check detects the moment of pressing — not the whole duration it's held. Same state-change pattern from L01-19.
Running statistics — no array needed
To track Lo, Hi and Avg over many readings, you don't need an array. Just keep:
tMin,tMax— updated whenever a new reading exceeds them.tSum(a long float) — total of all readings.tCount— how many readings have been added to the sum.- Average =
tSum / tCountany time you need it.
float tMin = 1000, tMax = -1000;
double tSum = 0;
unsigned long tCount = 0;
void recordTemp(float t) {
if (isnan(t)) return;
if (t < tMin) tMin = t;
if (t > tMax) tMax = t;
tSum += t;
tCount++;
}RAM cost: 20 bytes. Equivalent to a long ring-buffer of past values, but with infinite history. The trade-off is you can't plot the trend — for that you'd want the SD-card logger (L02-42).
Avoiding the "redraw too often" flicker
Three triggers for redraw:
- The current screen's data changed (new sensor reading came in).
- The user pressed the button (screen changed).
- Boot (initial draw).
A single boolean needRedraw handles all three. Set it when any trigger fires; clear it after the redraw runs.
Layout discipline
For each screen, define a small helper drawNow() / drawStats(). Each function:
- Optionally clears the screen.
- Re-prints all static text.
- Updates all dynamic values with
sprintf+dtostrf.
This is the template-once pattern from L02-29 applied per-screen instead of globally. Each screen has its own template; we only run one at a time.
Worked Example · Two-screen thermometer 25 min
Step 1 — wiring
| Component | Pin |
|---|---|
| LCD (bare, RS/EN/D4-D7) | D12, D11, D5, D4, D3, D2 |
| Contrast pot wiper | LCD V0 |
| DHT11 data | D8 |
| Mode button | D7 (with INPUT_PULLUP, button to GND) |
If you use an I²C LCD, A4 and A5 go to SDA/SCL — frees up D2–D5 and D11/12. Use whichever you wired in L02-27.
Step 2 — the sketch
Save as digital-thermometer.ino:
// L02-32: Digital Thermometer With LCD
// Standalone two-screen temperature + humidity display.
// Button on D7 toggles between "Now" and "Stats" screens.
//
// Pins: LCD RS=12 EN=11 D4=5 D5=4 D6=3 D7=2 ; DHT=D8 ; BUTTON=D7
#include <LiquidCrystal.h>
#include <DHT.h>
const int BUTTON = 7;
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
DHT dht(8, DHT11);
enum Screen { NOW, STATS };
Screen screen = NOW;
bool needRedraw = true;
float tMin = 1000, tMax = -1000;
double tSum = 0;
unsigned long tCount = 0;
float curT = 0, curH = 0;
bool curValid = false;
unsigned long lastSample = 0;
const unsigned long SAMPLE_INTERVAL = 2000;
byte degChar[8] = { 0b01100, 0b10010, 0b10010, 0b01100, 0, 0, 0, 0 };
const char* comfortFor(float h) {
if (h < 40) return "dry ";
if (h < 60) return "ok ";
if (h < 80) return "humid";
return "tropc";
}
void recordTemp(float t) {
if (isnan(t)) return;
if (t < tMin) tMin = t;
if (t > tMax) tMax = t;
tSum += t;
tCount++;
}
void drawNow() {
lcd.clear();
if (!curValid) {
lcd.setCursor(0, 0); lcd.print("--.-");
lcd.write(byte(0)); lcd.print("C");
lcd.setCursor(0, 1); lcd.print("--% RH");
return;
}
char tb[8], buf[17];
dtostrf(curT, 4, 1, tb);
sprintf(buf, "%sC %s", tb, comfortFor(curH));
lcd.setCursor(0, 0); lcd.print(buf);
lcd.setCursor(4, 0); lcd.write(byte(0)); // degree symbol
unsigned long mins = millis() / 60000;
sprintf(buf, "%2d%% RH %3lum", (int)curH, mins);
lcd.setCursor(0, 1); lcd.print(buf);
}
void drawStats() {
lcd.clear();
if (tCount == 0) {
lcd.setCursor(0, 0); lcd.print("No readings yet");
return;
}
char lo[7], hi[7], av[7], buf[17];
dtostrf(tMin, 4, 1, lo);
dtostrf(tMax, 4, 1, hi);
dtostrf((float)(tSum / tCount), 4, 1, av);
sprintf(buf, "Lo %s Hi %s", lo, hi);
lcd.setCursor(0, 0); lcd.print(buf);
sprintf(buf, "Avg %s n=%lu", av, tCount);
lcd.setCursor(0, 1); lcd.print(buf);
}
void handleButton() {
static bool last = HIGH;
bool now = digitalRead(BUTTON);
if (last == HIGH && now == LOW) {
screen = (screen == NOW) ? STATS : NOW;
needRedraw = true;
}
last = now;
}
void setup() {
pinMode(BUTTON, INPUT_PULLUP);
lcd.begin(16, 2);
lcd.createChar(0, degChar);
dht.begin();
}
void loop() {
handleButton();
if (millis() - lastSample >= SAMPLE_INTERVAL) {
lastSample = millis();
curT = dht.readTemperature();
curH = dht.readHumidity();
curValid = !isnan(curT) && !isnan(curH);
if (curValid) recordTemp(curT);
needRedraw = true;
}
if (needRedraw) {
if (screen == NOW) drawNow();
else drawStats();
needRedraw = false;
}
}Step 3 — upload, see screen 1
Power up. After about 2 seconds (the first sample) the Now screen should appear:
27.3°C humid 67% RH 0m
The minutes counter ticks up — useful idle indicator that the sketch is alive.
Step 4 — press the button, see screen 2
Press the button once. Screen flips to Stats:
Lo 27.0 Hi 27.0 Avg 27.0 n=1
(n is the count of valid samples taken so far. Wait a few minutes and you'll see n grow, Lo/Hi spread, Avg settle.)
Press the button again → back to Now. Repeated presses cycle.
Step 5 — let it run, observe stats accumulating
Run for 10 minutes. Breathe on the DHT11 a few times to push the temperature around. Then flip to Stats. You should see Lo < current < Hi, with Avg somewhere in the middle and n in the high hundreds.
Step 6 — disconnect the DHT11 data wire
The Now screen should show --.-°C and --% RH. The Stats screen still shows the last accumulated values — it doesn't reset on a single bad reading. Reconnect → fresh readings flow.
Try It Yourself 15 min
Goal: Add a third screen: an "About" screen showing the project name and your initials. Cycle through three screens with the button press instead of toggling between two.
Hint
enum Screen { NOW, STATS, ABOUT };
// in handleButton:
screen = (Screen)((screen + 1) % 3);Add a drawAbout() with two centred lines: project name on row 0, your name + date on row 1. Pure-static screen — perfect for a debut showcase.
Goal: On the Now screen, add a small trend arrow indicating whether the temperature is rising, falling, or steady compared to the previous reading. Re-use the up/down arrow custom characters from L02-30.
Hint
Store the previous reading and compare with a small dead-band:
float prevT = 0;
// in the sample block, after curT is set:
byte trendChar = 5; // default to "no change" slot
if (prevT != 0) {
if (curT > prevT + 0.2) trendChar = 2; // up arrow slot
else if (curT < prevT - 0.2) trendChar = 3; // down arrow slot
else trendChar = 5;
}
prevT = curT;
// in drawNow: lcd.setCursor(15, 0); lcd.write(byte(trendChar));The dead-band (0.2 °C) stops the arrow from flickering between up and down on a stable reading. Same idea as the rain-sensor hysteresis.
Goal: Add a "long-press to reset stats" behaviour. Holding the button for 2 seconds wipes Lo / Hi / Avg / n back to defaults. Useful for starting a new day's monitoring without unplugging.
Hint
Track when the button went down, and trigger the reset when the held duration crosses 2 s:
static unsigned long pressedAt = 0;
static bool wasPressed = false;
bool now = (digitalRead(BUTTON) == LOW);
if (now && !wasPressed) {
pressedAt = millis();
} else if (now && wasPressed && millis() - pressedAt > 2000) {
tMin = 1000; tMax = -1000; tSum = 0; tCount = 0;
needRedraw = true;
pressedAt = millis() + 100000; // disable until release
} else if (!now && wasPressed && millis() - pressedAt < 1000) {
// it was a short press, not long press — flip screen
screen = (screen == NOW) ? STATS : NOW;
needRedraw = true;
}
wasPressed = now;Long-vs-short press is a small UX bonus that earns trust. Almost every consumer device with one button uses it.
Mini-Challenge · Ship the v1 15 min
Polish the thermometer to ship-ready status — the standard L2 capstone checklist:
- Top-of-file spec: name, what it does, hardware, button behaviour. 4–6 lines.
- Boot splash: a 2-second "Adam's Thermo v1" intro screen before the Now screen appears.
- Custom degree symbol on Now and Stats screens.
- Long-press reset from the stretch task above.
- Stable layout — no ghosting at any digit transition.
- Runs unattended for 1+ hour with reasonable values accumulating in stats.
It's ship-ready when:
- You can leave the device on a shelf in the kitchen and someone walking by can read the current temperature in one glance.
- Pressing the button reveals the stats — no manual needed.
- The boot splash leaves no doubt this is a finished product, not a school project.
- The serial output is empty (no debug spam) — the LCD is the only UI.
Recap 5 min
Cluster E done. The two-screen LCD thermometer combines everything from the cluster — wiring, library setup, cursor positioning, custom characters, fixed-width formatting — plus a small two-state state machine and a single-button UI. The big-picture lesson is that the LCD turns an Arduino sketch into a standalone product: no Serial Monitor, no computer, just a power source and a screen. That's the threshold from "circuit on a desk" to "thing you could hand someone". Cluster F is up next — Non-Blocking Time — where we'll formalise the millis() pattern that's already been quietly powering every Cluster D and E sketch.
- Two-screen UI
- The simplest interactive UI: two displays of the same underlying data, switched by a single button. Common in alarm clocks, kitchen timers, fitness trackers, almost every small gadget.
- Running statistics
- Computing min, max, sum and count incrementally as each new sample arrives. No array of past values needed; constant RAM regardless of how long you run.
- Redraw flag
- A single boolean that flips to true whenever the displayed data changes, and gates the actual screen-draw call. Avoids flicker from redrawing every loop iteration.
- State-change button
- Detecting the moment a button is pressed (transition HIGH → LOW), not the whole duration it's held. The basis of every "each press does one thing" UI.
- Long-press
- A second button gesture: short press = primary action, hold = secondary action. Implementable with two timestamps and a duration threshold.
- Ship-ready
- The standard L2 capstone bar: spec comment, named constants, boot self-check, runs unattended, polished UI. The bridge from "sketch" to "product".
- Standalone device
- An embedded project that needs nothing beyond power to display useful information. No computer, no Serial Monitor, no debug attachment. The LCD is what makes this possible.
Homework 5 min
Run your thermometer for 24 hours. If you can — power the project from a phone charger (USB), leave it in a single room, and let it accumulate data overnight. The next day:
- Note the current Now-screen values.
- Flip to Stats: note Lo, Hi, Avg, n.
- Note the time-since-boot from the Now screen.
On paper, answer:
- By how many degrees did your room swing in 24 hours?
- Roughly when do you think Lo was hit? When was Hi hit? (Probably night and afternoon respectively.)
- Was the average closer to Lo or Hi? Does that tell you anything about the room?
- One thing you'd add to your thermometer to make it more useful for a real bedroom (LED night-light? buzzer fever alarm? logging to SD?).
If 24 hours isn't possible, run for 2–4 hours and adapt the questions accordingly. The point is to actually use the product you built.
Bring back next class:
- Your final readings and stats values.
- Your four written answers.
- A photo of the device on its final shelf with the Now screen visible.
- Cluster F starts tomorrow — we crack open the
millis()pattern that's been silently doing all this work.