Learning Goals 5 min
- Build the Cluster G capstone: a multi-sensor data logger that writes time-stamped CSV rows to an SD card and uses EEPROM to auto-generate a unique filename per boot.
- Apply Cluster F discipline (tick functions, debounced button, no
delay) to a sketch that's now juggling sensors, file I/O, EEPROM, and a status LED. - Add a "safe stop" button: pressing it cleanly closes the file and lights an LED to indicate it's safe to power-down without losing data.
Warm-Up 10 min
You've already built single-sensor SD loggers in yesterday's practice. Today is the ship-ready version: multi-sensor, auto-numbered log files, status LEDs, a button to cleanly stop logging, and a top-of-file comment that names the device. The kind of thing you'd build for a real science project — leave it running on a balcony for a week and come back to a perfect CSV.
Plan the device
Before any code, decide:
- What sensors? (Examples: DHT11 + LDR + HC-SR04 = temperature, humidity, light, distance to wall.)
- Sample rate? (DHT11 = 2 s minimum; the rest can match.)
- How long should it run? (24 hours = 43 200 samples × ~50 bytes = ~2 MB. Fits easily on any modern card.)
- What does each LED indicate? (Status: green = OK, amber = writing, red = error.)
New Concept · The four roles of a logger 25 min
Architecture: four ticks
The logger has exactly four concerns. Each becomes a tick function:
- Sensor tick — read all sensors, store the latest values in RAM, mark them as fresh.
- Logger tick — every N seconds, write one CSV row containing the latest fresh values.
- Button tick — watch the stop button; on press, close the file and transition to STOPPED state.
- Status LED tick — express the current state (running, writing, error, stopped) as LED behaviour.
Each tick does one thing, returns fast, and is testable in isolation.
The state machine
Three logger states:
| State | What's happening | LED behaviour |
|---|---|---|
| RUNNING | Sensors being polled and rows being written | Green slow blink |
| WRITING | Briefly during the actual SD write (~10 ms) | Amber flash |
| STOPPED | File closed; safe to pull power | Green steady ON |
| ERROR | SD card failed, sensor unrecoverable, etc. | Red blinking |
Unique-per-boot filenames (recap from L02-41 stretch)
Save the next-log-number in EEPROM. On boot: read it, increment, write back. Use the value as the filename suffix. Result: LOG001.CSV, LOG002.CSV, ... with no chance of two boots overwriting each other.
Per-row write strategy
Two choices for handling the SD file:
- Open-write-close per row. Slow (~30 ms per row including open/close overhead) but safe — a power-loss costs at most 1 row.
- Keep the file open, flush periodically. Faster (less overhead) but a power-loss can lose minutes of data buffered between flushes.
For 1 Hz logging on a UNO, option 1 is plenty fast and bulletproof. Use option 2 only if you're logging faster than 10 Hz.
Worked Example · Ship-ready data logger 25 min
Step 1 — wiring
| Component | Pin |
|---|---|
| SD module (MOSI/MISO/SCK/CS) | D11 / D12 / D13 / D10 |
| DHT11 data | D8 |
| LDR divider midpoint | A0 |
| Stop button (INPUT_PULLUP) | D2 |
| Status LED (green) | D9 (PWM) |
| Error LED (red) | D6 |
If you have a third sensor (HC-SR04, soil probe), wire it too — the sketch below has space for one more easily.
Step 2 — the sketch
Save as sensor-data-logger.ino:
// L02-42: Sensor Data Logger
// Logs DHT11 (temperature + humidity) and LDR (light percent) to
// a unique CSV file on SD. Press D2 to cleanly stop. EEPROM tracks
// next file number so each boot writes a fresh LOGnnn.CSV.
#include <SPI.h>
#include <SD.h>
#include <EEPROM.h>
#include <DHT.h>
const int CS = 10, DHT_PIN = 8, LDR = A0;
const int BTN = 2, GREEN = 9, RED = 6;
const int LOG_NUM_ADDR = 0;
const unsigned long SAMPLE_MS = 2000; // DHT11 needs ≥1 s; 2 s is comfortable
DHT dht(DHT_PIN, DHT11);
File logFile;
char filename[16];
enum State { RUNNING, STOPPED, ERROR_STATE };
State state = RUNNING;
float curT = 0, curH = 0;
int curLight = 0;
bool curValid = false;
unsigned long lastSample = 0;
unsigned long lastBlink = 0;
int blinkState = LOW;
unsigned long writeFlashUntil = 0;
// ---- Debounced stop button ----
struct Button {
int pin;
unsigned long lastChange;
int rawState, stableState, prevStable;
bool justPressed;
void begin(int p) {
pin = p; pinMode(pin, INPUT_PULLUP);
rawState = stableState = prevStable = digitalRead(pin);
lastChange = millis(); justPressed = false;
}
void tick() {
int now = digitalRead(pin);
if (now != rawState) { rawState = now; lastChange = millis(); }
justPressed = false;
if (millis() - lastChange >= 50 && rawState != stableState) {
prevStable = stableState; stableState = rawState;
if (stableState == LOW && prevStable == HIGH) justPressed = true;
}
}
};
Button btn;
void setup() {
pinMode(GREEN, OUTPUT); pinMode(RED, OUTPUT);
Serial.begin(9600);
btn.begin(BTN);
dht.begin();
if (!SD.begin(CS)) {
Serial.println("SD init failed");
state = ERROR_STATE;
return;
}
// Pick a unique filename via EEPROM
unsigned int logNum;
EEPROM.get(LOG_NUM_ADDR, logNum);
if (logNum == 0xFFFF) logNum = 0;
logNum++;
EEPROM.put(LOG_NUM_ADDR, logNum);
sprintf(filename, "log%03u.csv", logNum);
Serial.print("Logging to "); Serial.println(filename);
// Write header
logFile = SD.open(filename, FILE_WRITE);
if (logFile) {
logFile.println("seconds,temp_C,humidity_pct,light_pct");
logFile.close();
} else {
Serial.println("Failed to open log file");
state = ERROR_STATE;
}
}
void tickSensors() {
if (millis() - lastSample < SAMPLE_MS) return;
lastSample = millis();
curT = dht.readTemperature();
curH = dht.readHumidity();
curLight = map(constrain(analogRead(LDR), 0, 1023), 0, 1023, 0, 100);
curValid = !isnan(curT) && !isnan(curH);
if (curValid) tickWriteRow();
}
void tickWriteRow() {
logFile = SD.open(filename, FILE_WRITE);
if (!logFile) { state = ERROR_STATE; return; }
char tb[8], hb[8], buf[40];
dtostrf(curT, 4, 1, tb);
dtostrf(curH, 4, 0, hb);
sprintf(buf, "%lu,%s,%s,%d", millis() / 1000, tb, hb, curLight);
logFile.println(buf);
logFile.close();
writeFlashUntil = millis() + 60; // brief amber/green flash
}
void tickStatusLed() {
switch (state) {
case RUNNING: {
// Slow green blink, with extra brightness during write flash
if (millis() - lastBlink >= 1000) {
lastBlink = millis();
blinkState = !blinkState;
}
int bright = blinkState ? 80 : 0;
if (millis() < writeFlashUntil) bright = 255;
analogWrite(GREEN, bright);
digitalWrite(RED, LOW);
break;
}
case STOPPED:
analogWrite(GREEN, 255);
digitalWrite(RED, LOW);
break;
case ERROR_STATE: {
bool on = ((millis() / 200) % 2) == 0;
analogWrite(GREEN, 0);
digitalWrite(RED, on ? HIGH : LOW);
break;
}
}
}
void tickButton() {
btn.tick();
if (btn.justPressed && state == RUNNING) {
state = STOPPED;
Serial.println(">> STOPPED. Safe to power down.");
}
}
void loop() {
tickButton();
if (state == RUNNING) tickSensors();
tickStatusLed();
}Step 3 — power up
The Arduino boots, EEPROM advances the log number, and the SD card gets a new file like LOG002.CSV. Serial prints the filename. Green LED slow-blinks → logger is running.
Step 4 — observe the write flashes
Every 2 seconds the green LED briefly flares full-bright — that's a sample being written. The blink rhythm resumes immediately. Your eyes can see the logger is alive and producing data.
Step 5 — leave it for 30 minutes
Walk away. Come back. The logger should still be blinking. The Serial output is silent (no spam). On the card, the CSV file is growing by one row every 2 seconds.
Step 6 — stop cleanly
Press the stop button. Serial prints "STOPPED. Safe to power down." Green LED switches to steady ON. The file is closed and committed. Now you can safely unplug.
Step 7 — pull the card
Eject the card, plug into your laptop. Open LOG002.CSV. You should see 900+ rows of clean CSV data. Drop into Excel → chart the three values over time. Three smooth curves: temperature mostly steady, humidity drifting with the weather, light dropping if you logged across sunset. A real data-science deliverable.
Try It Yourself 15 min
Goal: Add a sample-counter to the Serial log. Every 10 samples, print "# Samples: 10", "# Samples: 20", etc. Useful to see at a glance how the run is going.
Hint
Add a global int sampleCount = 0; and inside tickWriteRow:
sampleCount++;
if (sampleCount % 10 == 0) {
Serial.print("# Samples: "); Serial.println(sampleCount);
}Goal: Add a fourth sensor (HC-SR04 distance or soil moisture). Update the CSV header and per-row format. Confirm the data file still imports cleanly into a spreadsheet.
Hint
One more global, one more line in tickSensors, one more column in the sprintf. The pattern is purely additive — no restructure needed. That's the payoff of the layered architecture.
Goal: Add a restart/resume button. A second button on D3 that, while in STOPPED state, reopens a fresh file (with the next EEPROM number) and goes back to RUNNING. So you can field-stop, swap a battery, restart — no power cycle.
Hint
Add a second debounced button. In its press handler, if state is STOPPED, redo the EEPROM filename increment and the header write, then set state to RUNNING. Make sure to logFile.close() first to flush any pending writes.
Mini-Challenge · Ship a real field deployment 15 min
Take the logger off the bench and put it somewhere meaningful for a sustained run. Some ideas:
- Inside your fridge (carefully — only the sensor goes in, the Arduino stays outside). Log temperature every 5 minutes for 24 hours.
- On a balcony pointed at the sky. Log light + temperature for a full day. Plot the sun's curve.
- In a houseplant's pot. Log soil moisture + light for a week. See if you can detect when each watering happened.
- By a window. Log temperature + light every minute for a day. Notice the lag between sunrise and the room warming up.
It's a real deployment when:
- The Arduino runs unattended for at least 4 hours.
- The CSV file imports into a spreadsheet without errors.
- You can draw at least one chart from the data that tells a story.
- You can show the device + the data to someone who didn't see you build it, and they can guess what the project is about within 30 seconds.
Recap 5 min
Cluster G ends with the Sensor Data Logger — a UNO-resident science instrument with auto-numbered files, multi-sensor capture, status LEDs, and a clean stop button. The architectural lessons of the last three clusters all show up here: sensor functions from Cluster C, helper functions + state machines from Cluster D, LCDs (in Weather Station v2 next lesson), and non-blocking ticks from Cluster F. The SD card + EEPROM combo gives you both bulk storage (for the data) and small persistent state (for the filename counter). Cluster H now starts — three weeks of integration projects, then schematic literacy, debugging strategies, and a recap before the L2 assessment.
- Data logger
- An embedded device whose primary job is to sample sensors periodically and persist the readings, typically as CSV on an SD card. Found in weather stations, scientific instruments, vehicle telemetry, industrial monitoring.
- Field deployment
- Running a device in its real intended environment (outdoors, in a fridge, in a vehicle) rather than on a bench. Different challenges: power, temperature, vibration, network.
- Time-stamped row
- Every CSV row begins with a time value (seconds since boot, milliseconds, or a real-time-clock timestamp). Lets the data be plotted against time without ambiguity.
- Auto-numbered filename
- Generating a fresh filename per run (
LOG001,LOG002, ...) so each session is independent. The counter lives in EEPROM so it survives power-cycles. - Header row
- The first line of a CSV listing column names. Written once at the start of each new file. Required by every spreadsheet importer.
- Safe stop
- Closing files and flushing buffers before power-off. Without it, the last few seconds of data can be lost. The dedicated stop button + STOPPED state make safe-stop explicit.
- Status LEDs
- Visual indicators that tell a user what the device is doing without a screen. Green = healthy, amber = activity, red = error. Same convention as routers, fridges, smoke alarms.
- Open-write-close per row
- The bulletproof SD pattern: open the file just before writing one row, write it, close immediately. Power-loss costs at most one row. Slow (~30 ms per row) but worth it at low rates.
- Cluster G payoff
- You can now build instruments that take readings while you sleep and present clean data the next morning. The line between "school project" and "real-world tool".
Homework 5 min
Run a 12-hour deployment. Find a real spot (your bedroom, an outdoor balcony, near a window) and run the logger for 12 hours overnight or daytime. Then:
- Power down via the stop button (don't yank the USB).
- Pull the card. Open the CSV in a spreadsheet.
- Create a chart with time on the x-axis and your three+ sensor values on the y-axis (you may need to use a secondary axis for the units to make sense).
- Annotate the chart with at least three things you can identify (sunrise, when the AC kicked in, you opened the window, etc.).
Bring back next class:
- Your annotated chart (screenshot or printed).
- The CSV file (on a USB or in a shared drive).
- A 1-paragraph description of what the data shows.
- Cluster H starts tomorrow — three multi-cluster integration builds, beginning with Weather Station v2 (the logger + the LCD from Cluster E).