Learning Goals 5 min
- Combine everything from L02-07 through L02-11 into one polished project — read, smooth, calibrate, map, threshold, alert.
- Build a small physical "product": a thermistor-based thermometer that prints temperature, lights an LED at warm, and beeps the buzzer at fever-level (37.5 °C).
- Tune the thresholds for your environment and write a one-paragraph "product spec" comment at the top of the sketch — the difference between a project and a sketch.
Warm-Up 10 min
So far Cluster B has been a series of single-skill lessons: read the ADC, smooth the readings, calibrate the sensor, map the range, convert to °C. Today you bolt them all together. The output is a small device on your desk that's genuinely useful — squeeze the thermistor between thumb and finger and the LED lights up; hold it long enough and the buzzer beeps.
Predict-the-spec
Before we code, picture the device. What thresholds make sense for a personal thermometer in Malaysia?
One reasonable spec
- Below 24 °C → status "cool", no LED, no buzzer.
- 24–30 °C → status "normal", no LED, no buzzer.
- 30–37 °C → status "warm", LED on, no buzzer.
- ≥ 37.5 °C → status "fever", LED on, buzzer pulses every 2 seconds.
37.5 °C is the standard fever threshold the Ministry of Health uses. These bands work for skin-pinch readings (~32–34 °C from a healthy person) and rise convincingly into "warm" territory if the person actually has a temperature.
New Concept · From sensor to product 20 min
The full pipeline
Today's sketch is the cumulative Level 2 sensor pipeline. Read top to bottom:
- Read raw ADC at A0 (L02-07).
- Smooth with a running average of the last N samples (L02-08). N = 10 works well.
- Convert smoothed raw → resistance → Kelvin → °C (L02-11).
- Constrain the reported °C to a sensible band (e.g. 10–50 °C). Outside that, something has gone wrong; clip and label as "sensor error".
- Classify into one of four states: cool, normal, warm, fever.
- Act: set the LED, beep the buzzer, print the status.
L02-09's calibration isn't needed here — we're after a real temperature, not a relative one. The β-equation already calibrates the thermistor against absolute Celsius.
Hardware on top of L02-11
Add two outputs to the L02-11 wiring:
- LED on pin ~9, in series with a 220 Ω resistor — for the warm/fever state.
- Piezo buzzer on pin 8 (any digital pin), one leg to pin 8, other to GND — for the fever alarm.
The buzzer (non-blocking-ish)
We want the buzzer to chirp every 2 seconds while in fever state — but we don't want the chirp to block the temperature reading. The trick: use millis() to track when we last beeped, and only chirp if 2 seconds have passed since the last one:
unsigned long lastBeep = 0;
void maybeBeep(bool fever) {
if (!fever) return;
if (millis() - lastBeep >= 2000) {
tone(8, 880, 120); // 120 ms beep at A5 (880 Hz)
lastBeep = millis();
}
}tone(pin, freq, duration) from L01-14 plays a non-blocking tone — it returns immediately, and the buzzer plays for the requested milliseconds in the background. While it's beeping, your loop() keeps reading the thermistor. Clean.
Reading classification
An if/else if/else chain handles the four bands cleanly:
const char* statusFor(float t) {
if (t < 24) return "cool";
if (t < 30) return "normal";
if (t < 37.5) return "warm";
return "FEVER";
}Returning a const char* (a string pointer) is C++ shorthand for "here's a string label, don't copy it". Perfect for status names. The capitalised "FEVER" signals urgency in the Serial Monitor.
Worked Example · The full thermometer 20 min
Step 1 — wiring
Three blocks on the breadboard:
- Sensor divider: +5V → thermistor → A0 → 10 kΩ → GND.
- Warm-state LED: Pin 9 → 220 Ω → LED anode → LED cathode → GND.
- Fever buzzer: Pin 8 → piezo + → piezo − → GND.
Three pins (A0, 9, 8) plus +5V/GND. Tight, tidy, fits on a half-size breadboard with space for labels.
Step 2 — the sketch
Save as personal-thermometer.ino:
// L02-12: Personal Thermometer
// Reads an NTC thermistor on A0, prints °C every 250 ms,
// lights LED on pin 9 in "warm" range, chirps buzzer on pin 8
// every 2 s while in "fever" range (≥ 37.5 °C).
//
// Hardware:
// A0 ← NTC + 10kΩ divider (thermistor to +5V, 10k to GND)
// D9 → 220Ω → LED → GND
// D8 → piezo → GND
const int THERM = A0;
const int LED = 9;
const int BUZZ = 8;
// Thermistor constants
const float R_FIXED = 10000.0;
const float R0 = 10000.0;
const float T0_K = 298.15;
const float BETA = 3950.0;
// Status thresholds (°C)
const float COOL_MAX = 24;
const float NORM_MAX = 30;
const float WARM_MAX = 37.5;
// Smoothing
const int N = 10;
int samples[N];
int idx = 0;
long total = 0;
// Beep cadence
unsigned long lastBeep = 0;
const unsigned long BEEP_INTERVAL = 2000;
int smoothedRaw() {
total -= samples[idx];
samples[idx] = analogRead(THERM);
total += samples[idx];
idx = (idx + 1) % N;
return total / N;
}
float rawToCelsius(int raw) {
if (raw <= 0 || raw >= 1023) return -100; // sentinel for sensor error
float R = R_FIXED * (1023.0 / raw - 1.0);
float tK = 1.0 / (1.0/T0_K + (1.0/BETA) * log(R / R0));
return tK - 273.15;
}
const char* statusFor(float t) {
if (t < -50) return "SENSOR ERROR";
if (t < COOL_MAX) return "cool";
if (t < NORM_MAX) return "normal";
if (t < WARM_MAX) return "warm";
return "FEVER";
}
void setup() {
pinMode(LED, OUTPUT);
pinMode(BUZZ, OUTPUT);
Serial.begin(9600);
for (int i = 0; i < N; i++) samples[i] = analogRead(THERM);
for (int i = 0; i < N; i++) total += samples[i];
}
void loop() {
int raw = smoothedRaw();
float t = rawToCelsius(raw);
const char* st = statusFor(t);
bool warm = (t >= NORM_MAX) && (t < WARM_MAX);
bool fever = (t >= WARM_MAX);
digitalWrite(LED, (warm || fever) ? HIGH : LOW);
if (fever && millis() - lastBeep >= BEEP_INTERVAL) {
tone(BUZZ, 880, 120);
lastBeep = millis();
}
Serial.print(t, 1);
Serial.print(" °C ");
Serial.println(st);
delay(250);
}Step 3 — upload & test
Open Serial Monitor. With the thermistor in still room air, the steady output should look like:
26.3 °C normal 26.4 °C normal 26.3 °C normal
Pinch the thermistor between your fingers for 30 seconds → temperature climbs to ~32–34 °C, status flips to warm, LED lights up. Hold a few more seconds; for most people it stops at "warm".
To test "fever": cup the thermistor inside your closed fist (more skin contact, less airflow) for 90+ seconds, or briefly squeeze it under a warm tap. Once it reads ≥ 37.5 °C the buzzer chirps every 2 seconds and the Serial Monitor prints FEVER in caps.
Step 4 — final "ship-ready" touches
The top-of-file comment block is the "product spec". Anyone reading your sketch (a teacher, a future-you, a friend) should be able to glance at it and know what the project does without reading any code. Keep it short and focused: what does it do, what hardware does it need, what are the thresholds, what does each pin do. The hardware list in the comment is your debug instructions when something stops working in six months.
Try It Yourself 20 min
Goal: Adjust the thresholds for an outdoor build. Imagine you're running this on a tropical balcony — set COOL_MAX = 26, NORM_MAX = 32, WARM_MAX = 38. Note: the body-fever value rises with ambient, so above 38 you'd expect a real temperature.
Goal: Add an RGB LED (from L02-05) and use it to show the status colour: blue = cool, green = normal, amber = warm, red = fever. Replace the single LED with three calls to setColour(r, g, b) based on status.
Hint
if (strcmp(st, "cool") == 0) setColour(0, 0, 255);
else if (strcmp(st, "normal") == 0) setColour(0, 200, 0);
else if (strcmp(st, "warm") == 0) setColour(255, 120, 0);
else if (strcmp(st, "FEVER") == 0) setColour(255, 0, 0);strcmp compares two C-strings — returns 0 if equal. We use it because st is a const char*, not a class string. C++ purists would refactor to an enum; for one project it's fine to live with strcmp.
Goal: Record a session. While the device runs, every 5 seconds print a CSV line: seconds_since_start, temperature_C, status. After running for 5 minutes you'll have ~60 lines you can paste into a spreadsheet and plot.
Hint
unsigned long lastLog = 0;
// in loop():
if (millis() - lastLog >= 5000) {
Serial.print(millis() / 1000); Serial.print(",");
Serial.print(t, 1); Serial.print(",");
Serial.println(st);
lastLog = millis();
}Paste the lines into Excel or Google Sheets → Insert Chart → line chart. You've made a primitive data logger. We'll build the proper SD-card version in L02-39.
Mini-Challenge · Pick a project, ship it 15 min
Choose one of these variations on the personal thermometer and ship it — meaning fully wired, fully coded, fully working, and good enough to leave on your desk.
- Drink-temperature checker — tape the thermistor to the outside of a mug. Status: too cold (< 40 °C), drinking temp (40–60 °C), too hot (> 60 °C). Adjust thresholds, change the buzzer to a single triumph chirp when the drink reaches drinking temp.
- Room comfort alarm — leave the thermistor in still air. Status: comfortable (24–28 °C), warm (28–32 °C), "turn on the fan" (> 32 °C). LED warning + occasional chirp.
- Fridge guard — put the thermistor on a long wire, dangle it into the fridge through the door seal. Alarm if temperature rises above 8 °C (someone left the door open).
It's a ship-ready project if:
- The top-of-file comment block explains what it does in 4–6 lines.
- All thresholds are
constat the top — no magic numbers in theloop(). - It runs for at least 5 minutes without you touching it, behaving correctly across the full temperature range.
- You can hand it to a classmate, they pinch the thermistor, and they correctly interpret what the device is telling them within 10 seconds. That's a UX win.
Recap 5 min
Cluster B done. The personal thermometer stitches together the six prior lessons: the ADC reads the divider voltage, the running average smooths it, the β-equation converts it to °C, map+constrain shape outputs, an if/else if chain classifies the band, the LED and buzzer carry the alert. The whole pipeline lives in helper functions — smoothedRaw, rawToCelsius, statusFor — so the main loop() reads like a recipe.
Next cluster (L02-13 onward) widens this same pattern to four more environmental sensors — DHT11, soil moisture, rain drop, piezo knock. Every one of them will use the read-smooth-convert-classify-act pipeline you just built.
- Pipeline
- A sequence of small steps that transforms data from raw → useful: read, smooth, convert, classify, act. Each step in its own helper function.
- Status string
- A short human-readable label (
"cool","warm","FEVER") used for logging and display, separate from the underlying number. const char*- A pointer to an immutable C string. Lightweight way to return a status label from a helper function.
- Non-blocking beep
- Using
tone()+ amillis()gate so the buzzer chirps periodically without freezing the main loop. - Sentinel value
- A magic value (here: -100) returned to signal "sensor error", distinct from any real temperature.
- Ship-ready
- The product mindset: tight spec, named constants, comment block at top, works without supervision. The difference between a sketch and a thing on your desk.
Homework 5 min
Run the thermometer for an hour. Pick a quiet spot — your bedroom, the kitchen table — and leave the thermometer running for a full hour. Every 10 minutes glance at the Serial Monitor and note:
- Current temperature.
- Current status.
- Whether the temperature is rising, falling, or steady.
Then on paper:
- Was the temperature stable? If not, what changed in the room?
- Did any of your bands feel wrong (e.g. you ran the room AC at 25 °C and the sketch said "cool" — would you adjust COOL_MAX)?
- How would you change the project for use in a more humid environment (Sabah, Sarawak) vs Highlands cool air?
Bring back next class:
- Your six-row hourly observation table.
- Your three answers above.
- The sketch saved as
hw-l02-12.ino. - (Optional) A short phone video of the device alarming when you pinch the thermistor — great way to document it.
Well done — you've completed Cluster B and shipped your first L2 sensor project. Tomorrow we widen the sensor toolkit with the TMP36, DHT11 and the rest of Cluster C.