Learning Goals 5 min
- Build the Cluster C capstone: a multi-sensor Weather Station that streams temperature, humidity and light to the Serial Monitor every 5 seconds in clean, spreadsheet-friendly CSV.
- Use the truth-table thinking from L02-19 to generate a single human-readable headline label ("Hot & humid", "Cool & bright", "Stormy") instead of three separate numbers.
- Run the station for at least 10 minutes, paste the CSV into a spreadsheet, and produce two charts: one for temperature over time, one for humidity over time. Your first real data product.
Warm-Up 10 min
Cluster C started with single-sensor lessons — TMP36, DHT11, LDR, soil, rain, piezo. Yesterday you combined two with boolean rules. Today the whole cluster comes together as one project: a Weather Station that watches three things at once, prints them to the Serial Monitor as CSV, and labels each reading with a friendly "headline" like a weather app would.
Predict the spec
What would you want your station to print every 5 seconds? Discuss with a neighbour for 2 minutes.
One reasonable spec
- CSV header on boot:
time_s,temp_C,humidity_pct,light_pct,headline - One line every 5 s:
10,27.4,72,42,Warm & humid - Headline is one of 4–6 short labels:
Cool & dim,Cool & bright,Warm & humid,Hot,Stormy,Comfortable. - NaN-safe: if a sensor fails, the row prints
?for that column and the rest of the row still goes out.
New Concept · Architecting a multi-sensor project 20 min
The architecture in three layers
For the first time we're writing a project big enough that organisation matters. Here's the three-layer mental model we'll keep using for the rest of L2 and beyond:
- Sensors layer — one helper per sensor:
readTempC(),readHumidity(),readLightPct(). Each helper returns a single number (or NaN on failure). No printing, no decisions, just reading. - Logic layer — takes the numbers, classifies them into booleans (
hot,humid,bright), then produces the headline label. - Output layer — prints the CSV row, drives any LEDs, optionally beeps a buzzer.
The main loop() just calls one function per layer. Below 30 lines for the main loop is the goal. Anyone glancing at the file should be able to read loop() as a recipe and know what the project does.
The CSV format and why
CSV (Comma-Separated Values) is the simplest way to get structured data out of an Arduino. Every modern spreadsheet imports it natively. Three rules:
- One header row at the top with column names.
- One data row per measurement, fields separated by commas.
- No trailing spaces in numeric columns — they confuse some import wizards.
Example output:
time_s,temp_C,humidity_pct,light_pct,headline 5,27.4,72,38,Warm & humid 10,27.5,71,41,Warm & humid 15,27.5,72,45,Warm & humid 20,27.6,74,49,Warm & humid
The headline rule (truth-table-style)
From the three booleans hot (T > 30 °C), humid (H > 75%) and bright (light > 70%) we can derive a single label. One reasonable rule:
| hot | humid | bright | Headline |
|---|---|---|---|
| yes | yes | — | Stormy |
| yes | no | yes | Hot & bright |
| yes | no | no | Hot & dim |
| no | yes | — | Warm & humid |
| no | no | yes | Cool & bright |
| no | no | no | Cool & dim |
Each row maps to a friendly two-word headline. Notice some rows merge — when hot && humid we don't care about brightness; both rows say "Stormy". Truth tables aren't scripture — collapse rows whenever the output is the same.
NaN-safe printing
If dht.readTemperature() returns NaN, we don't want our CSV row to read 5,nan,72,38,.... Print a ? instead — most spreadsheets treat it as a missing value rather than choking on the row:
if (isnan(t)) Serial.print("?");
else Serial.print(t, 1);
Serial.print(",");Worked Example · The full station 25 min
Step 1 — wiring (three blocks)
| Component | Pin |
|---|---|
| DHT11 module (data) | D2 |
| LDR voltage divider midpoint | A0 |
| (optional) RGB or single LED | D9 (PWM, with 220 Ω) |
Plus +5V and GND on each module. The DHT11 you've wired in L02-14; the LDR divider in L02-15. The third row is optional eye-candy: a single LED that brightens as light increases, or three LEDs for state.
Step 2 — the sketch (layered)
Save as weather-station-v1.ino:
// L02-20: Weather Station v1
// Reads DHT11 + LDR, prints CSV every 5 s with a headline label.
// Architecture: sensors layer · logic layer · output layer.
#include <DHT.h>
#define DHT_PIN 2
#define DHT_TYPE DHT11
const int LDR = A0;
DHT dht(DHT_PIN, DHT_TYPE);
// LDR auto-calibration (5 s on boot)
int ldrDark = 1023, ldrBright = 0;
const unsigned long LOG_MS = 5000;
unsigned long lastLog = 0;
// ---- SENSORS LAYER ----
float readTempC() { return dht.readTemperature();
}
float readHumidity() { return dht.readHumidity();
}
int readLightPct() {
int raw = analogRead(LDR);
int clipped = constrain(raw, ldrDark, ldrBright);
return map(clipped, ldrDark, ldrBright, 0, 100);
}
// ---- LOGIC LAYER ----
const char* headlineFor(float t, float h, int lightPct) {
if (isnan(t) || isnan(h)) return "sensor error";
bool hot = (t > 30);
bool humid = (h > 75);
bool bright = (lightPct > 70);
if (hot && humid) return "Stormy";
if (hot && bright) return "Hot & bright";
if (hot) return "Hot & dim";
if (humid) return "Warm & humid";
if (bright) return "Cool & bright";
return "Cool & dim";
}
// ---- OUTPUT LAYER ----
void printRow(float t, float h, int lightPct, const char* hl) {
Serial.print(millis() / 1000);
Serial.print(",");
if (isnan(t)) Serial.print("?");
else Serial.print(t, 1);
Serial.print(",");
if (isnan(h)) Serial.print("?");
else Serial.print(h, 0);
Serial.print(",");
Serial.print(lightPct);
Serial.print(",");
Serial.println(hl);
}
// ---- SETUP / LOOP ----
void calibrateLdr() {
Serial.println("# calibrating LDR (5 s)...");
unsigned long t0 = millis();
while (millis() - t0 < 5000) {
int r = analogRead(LDR);
if (r < ldrDark) ldrDark = r;
if (r > ldrBright) ldrBright = r;
}
if (ldrBright - ldrDark < 50) { ldrDark = 0;
ldrBright = 1023;
}
}
void setup() {
Serial.begin(9600);
dht.begin();
delay(500);
calibrateLdr();
Serial.println("time_s,temp_C,humidity_pct,light_pct,headline");
}
void loop() {
if (millis() - lastLog < LOG_MS) return;
lastLog = millis();
float t = readTempC();
float h = readHumidity();
int l = readLightPct();
const char* hl = headlineFor(t, h, l);
printRow(t, h, l, hl);
}Step 3 — upload and let it run
Open Serial Monitor. After the calibration line you should see clean CSV:
# calibrating LDR (5 s)... time_s,temp_C,humidity_pct,light_pct,headline 5,27.4,72,38,Warm & humid 10,27.5,71,41,Warm & humid 15,27.5,72,45,Warm & humid 20,27.6,74,49,Warm & humid
Step 4 — exercise each state
Run through every headline by manipulating sensors:
- Stormy — breathe heavily near the DHT11 for 30 s.
- Hot & bright — palm warmth on DHT + phone torch on LDR.
- Cool & dim — leave alone in a dim room.
- Cool & bright — leave alone in a brightly-lit room.
Each manipulation should produce the right headline within one log interval. If a state never appears, check the thresholds in headlineFor() and the calibrated LDR range.
Step 5 — collect 10 minutes, plot
Let the station run for 10 minutes. Then in the Serial Monitor: select all (Ctrl/Cmd+A) → copy → paste into a new Google Sheet or Excel. Drop the comment line at the top. Data → Split text to columns if needed.
Insert two charts:
- X = time_s, Y = temp_C — line chart titled "Temperature over time".
- X = time_s, Y = humidity_pct — line chart titled "Humidity over time".
You now have an actual data product: a one-page weather report with two charts and a label per row. With a screenshot of the charts and the CSV file, you'd have everything a science-fair judge wants to see.
Basic 7 min
Goal: Add an LED on D9 that brightens in proportion to the light percentage. (So your station has a quick visual indicator besides the Serial Monitor.) Use analogWrite(9, map(lightPct, 0, 100, 0, 255));
Challenge 1 7 min
Goal: Add a fourth sensor — a TMP36 or thermistor — on A1. Print its °C in a new column. Compare against the DHT11's temperature. Useful when one sensor disagrees with the other — which one do you trust?
Challenge 2 6 min
Goal: Compute a discomfort index = humidity × max(0, temperature − 25). High when it's both hot and humid. Print it as a new column and update the headline logic to use the discomfort index instead of separate hot/humid thresholds.
Challenge 3 · Ship the v1 15 min
The previous sections give you a working baseline. The mini-challenge is to make it ship-ready — meaning good enough to leave on your desk overnight and produce useful data in the morning. Pick at least three of these "polish" items and apply them:
- Top-of-file product spec. A 4–6 line comment that explains what the project does, what sensors it uses, what each pin does, and what the headline thresholds mean.
- All thresholds named. No magic numbers in
headlineFor. Promote 30, 75, 70 to named constants at the top. - Boot self-test. In
setup(), read each sensor once and print a one-line "sensor X: OK" or "sensor X: FAILED" report. Useful for debugging when something doesn't work. - Heartbeat LED. An LED on D13 that blinks once per log cycle — a quiet visual confirmation that the station is alive.
- Overflow-safe time column.
millis()wraps after 49 days; print hours+minutes instead of raw seconds for long runs.
It's ship-ready when:
- The top-of-file comment lets a stranger understand the project in 30 seconds.
- No
delay()longer than 200 ms in the main loop. - Booting the station shows a clear "all sensors OK" message.
- You can leave it running for 30 minutes unattended and come back to a clean, readable CSV.
Recap 5 min
Cluster C done. The Weather Station stitches the cluster's eight lessons into one project: temperature from L02-13/14, humidity from L02-14, light from L02-15, calibration from L02-09/15, the AND/OR rule-design from L02-19, the CSV pipeline from… well, today. The architecture lesson — sensors layer → logic layer → output layer — is the project shape we'll keep using for the rest of L2 (data logger, combination lock, weather v2). Tomorrow we widen the toolkit further with ultrasonic distance — Cluster D starts.
- Layered architecture
- Splitting a project into clear roles — sensors layer (reads), logic layer (decides), output layer (acts/prints). Keeps the main loop short and the helpers single-purpose.
- CSV (comma-separated values)
- A simple plain-text format for tabular data, one row per line, fields separated by commas. Imported by every spreadsheet on Earth.
- Header row
- The first line of a CSV listing column names — printed once in
setup()in our case, never repeated in the stream. - Headline label
- A short human-readable summary string ("Hot & humid", "Stormy") derived from the underlying numbers — gives a glance-able answer for non-engineers.
- Ship-ready
- The mindset of a finished product: tight spec comment, named constants, self-test, runs unattended. The bridge between "sketch" and "thing on your desk".
- Sensor self-test
- A small routine in
setup()that reads each sensor once and reports whether the read was sensible — catches a missing wire before the first data row goes out.
Extra Mission 5 min
Part 1 — Design a station for one place
Weather stations live in odd places — a school garden, a bike shed, a classroom window, a tent. Pick one. On paper, design a station for it.
Your design must include:
- A name, the place, and the one question it answers — for example "Is it good drying weather?".
- Which sensors it needs, and which it does not.
- The headline labels and the thresholds behind each one.
- How often it logs, and how long it should run.
Part 2 — Make it
Change today's weather station to match your design. Every threshold must be a named const. Run it for at least 30 minutes and save the CSV as weather-log.csv.
Bring back next class: the uploaded sketch and your weather-log.csv file.