Learning Goals 5 min
By the end of this lesson you will be able to:
- Wire and code a complete sensor product — LDR + sensitivity pot + PWM LED — that decides for itself when to turn on, how bright to glow, and when to switch off.
- Apply hysteresis: turn on at one threshold and off at a slightly different one, so passing clouds and brief shadows don't make the light flicker.
- Use
map()to turn the LDR's distance below threshold into a smooth PWM brightness — so the LED gently fades up as it gets darker rather than snapping fully on the moment it crosses the line.
Warm-Up 10 min
Cluster F gave you four building blocks — the LDR, the pot, map(), boolean logic. Today you bolt them together into something that's recognisably a product, not a tutorial. A device that does its job quietly in the background, that responds to context, that you'd be happy to put on a bedside table.
Quick-fire puzzle
Imagine a £5 plug-in night light from the supermarket. It does one job — turn on when dark, off when bright. What separates the well-designed one from the bad one?
- What happens if a cloud passes the window and the room briefly gets dark for 2 seconds?
- What if someone slowly draws the blinds across an hour — does the light "snap" on at one specific moment, or do something nicer?
- What about a sleeping baby's room where the parent wants the LED dimmer than the default — does the user get a knob?
Reveal the answer
- A bad product flashes its LED on for those 2 seconds, then off. A good one ignores it — short transitions don't change state. Today's project uses hysteresis (different thresholds for turning on vs off) so brief changes near the boundary don't flicker the light.
- A bad product snaps on the instant the LDR crosses some hardcoded threshold. A good one gently fades up as the room darkens, getting brighter the darker it gets. The user feels the device "responding" rather than "switching".
- A premium product gives you a sensitivity knob — turn left for dimmer/less sensitive, right for brighter/more sensitive. Today's project has one built in using a pot on A1.
Those three differences — flicker resistance, smooth fade, user adjustment — are the polish that turn a sketch into a product. You'll build all three today.
New Concept — three polish moves 20 min
The big idea — turn "if/else" into "smoothly respond"
The L01-36 nightlight was four lines: read LDR, compare to threshold, drive LED. Today's version is twenty lines — and the extra sixteen each do one of three polish jobs:
- Hysteresis — two thresholds instead of one, to ignore brief shadows.
- Smooth fade — PWM brightness scaled to "how dark", not just on/off.
- User-adjustable sensitivity — a pot to dial in the threshold without re-uploading.
Each polish move is independent. You could ship a product with just hysteresis, or just fading, or just the knob. Combining all three is what makes the device feel finished.
Polish #1 — hysteresis (two thresholds)
A single threshold causes flicker: if your "dark" cut-off is 400 and the LDR is reading right around 400, every tiny variation flips the LED. Hysteresis uses two thresholds:
- A lower one to turn on: e.g. light ≤ 380 → turn LED on.
- A higher one to turn off: e.g. light ≥ 420 → turn LED off.
- In between (380–420) → keep whatever state we're already in.
// Hysteresis pattern — track the current state
if (light <= turnOnThreshold) ledOn = true;
else if (light >= turnOffThreshold) ledOn = false;
// otherwise: leave ledOn as it wasThe 40-unit gap is the dead band — the range where readings are ignored. It has to be wider than the typical fluctuation of "I'm at the threshold" to be effective. For the LDR, ±20 around the chosen threshold is usually enough.
You've seen this pattern before in real life: a thermostat doesn't switch the heating on and off every time the temperature wobbles by 0.1 °C — it has a built-in deadband too.
Polish #2 — smooth PWM fade
Instead of LED on/off, scale brightness to "how far below the threshold". The darker the room, the brighter the LED:
- Light at the threshold → brightness 0 (just barely on).
- Light at 0 (totally dark) → brightness 255 (full).
- Light in between → proportional brightness.
This is a job for map() with a reversed to-range (L01-41):
if (ledOn) {
int brightness = map(light, 0, threshold, 255, 0);
brightness = constrain(brightness, 0, 255);
analogWrite(LED_PIN, brightness);
}
else {
analogWrite(LED_PIN, 0);
}The constrain is a safety net: when ledOn is true but the light has crept up past the threshold (because hysteresis hasn't kicked us back off yet), map()'s integer maths can produce negative numbers. Clamping to 0–255 keeps analogWrite happy.
Polish #3 — user-adjustable sensitivity
Put a pot on A1. Read it. Use its value as the central threshold; derive the two hysteresis thresholds around it. The user turns the knob; the night light's "what counts as dark" changes live, no upload needed.
int pot = analogRead(POT_PIN);
int centre = map(pot, 0, 1023, 100, 900); // sweet spot range
int turnOnThreshold = centre - 20;
int turnOffThreshold = centre + 20;This is the L01-40 + L01-41 combo doing its job: a pot's range gets mapped onto the useful range of LDR readings, and the polish from hysteresis automatically follows. Why 100–900 and not 0–1023? Because LDR readings of 0 (impossibly dark) or 1023 (impossibly bright) don't trigger usefully — they live at the edges of the ADC's range and pinch the user's adjustment into nothing. Trimming to 100–900 gives almost the full range with no dead corners.
The whole pipeline as one diagram
Every loop pass, the data flows like this:
| Step | Input | Operation | Output |
|---|---|---|---|
| 1 | — | analogRead(LDR_PIN) | light (0..1023) |
| 2 | — | analogRead(POT_PIN) | pot (0..1023) |
| 3 | pot | map → ±20 deadband | two thresholds |
| 4 | light, thresholds | hysteresis logic | ledOn (true/false) |
| 5 | light, threshold, ledOn | map → constrain → analogWrite | LED brightness |
Five steps, none of them surprising on their own. Bolt them together and you get a finished device.
Why it matters
The "sensor + decision + smoothing + user knob" pattern is the template for every analog product you'll meet: thermostats, dimmers, fan controllers, hand-driers' proximity sensors, automatic doors, even early Bluetooth volume buttons. Today's twenty-line sketch is the smallest non-trivial example of the pattern. Once you can see all three polish moves in your own code, you can find them — and reproduce them — in every product you take apart.
Worked Example — wire it, then layer the polish 25 min
The wiring — three subsystems on one board
Three little circuits sharing one GND bus. Nothing new since L01-40.
- LDR + 10 kΩ partner on A0 (L01-36 layout). LDR on the +rail side, 10 kΩ on the −rail side.
- Potentiometer on A1 (L01-40 layout). Two end pins to the rails, wiper to A1.
- LED + 220 Ω on ~D9 (L01-07 layout). Anode through the resistor; cathode to the − rail.
- One GND wire and one 5 V wire from the rails to the Arduino.
Version 1 — binary, fixed-threshold (the L01-36 starting point)
Start with the simplest version. Just to confirm the wiring works.
// v1 — binary on/off, fixed threshold
const int LDR_PIN = A0;
const int LED_PIN = 9;
const int THRESHOLD = 400;
void setup() {
pinMode(LED_PIN, OUTPUT);
}
void loop() {
int light = analogRead(LDR_PIN);
digitalWrite(LED_PIN, light < THRESHOLD);
delay(50);
}Upload. Cover the LDR — LED on. Uncover — LED off. Try holding the LDR at the threshold (slowly raise your hand a centimetre at a time near the boundary). You'll see the LED flicker as readings cross 400 in either direction. This is the flicker problem we're about to fix.
Version 2 — add hysteresis
// v2 — hysteresis: two thresholds, kill the flicker
const int LDR_PIN = A0;
const int LED_PIN = 9;
const int ON_AT = 380;
const int OFF_AT = 420;
bool ledOn = false;
void setup() {
pinMode(LED_PIN, OUTPUT);
}
void loop() {
int light = analogRead(LDR_PIN);
if (light <= ON_AT) ledOn = true;
else if (light >= OFF_AT) ledOn = false;
digitalWrite(LED_PIN, ledOn);
delay(50);
}Upload. Now try the same "hold near the threshold" test — the LED should pick a state and stay there. Brief flickers in either direction don't crack through the 40-unit deadband. Real bedside lamps have exactly this property; now yours does too.
Version 3 — smooth fade + sensitivity knob (the full product)
// v3 — the finished product: hysteresis, smooth fade, user knob
const int LDR_PIN = A0;
const int POT_PIN = A1;
const int LED_PIN = 9;
const int DEADBAND = 20; // ± around centre
bool ledOn = false;
void setup() {
Serial.begin(9600);
pinMode(LED_PIN, OUTPUT);
}
void loop() {
// 1. Read both analog inputs
int light = analogRead(LDR_PIN);
int pot = analogRead(POT_PIN);
// 2. Derive thresholds from the pot
int centre = map(pot, 0, 1023, 100, 900);
int onAt = centre - DEADBAND;
int offAt = centre + DEADBAND;
// 3. Hysteresis state update
if (light <= onAt) ledOn = true;
else if (light >= offAt) ledOn = false;
// 4. Smooth fade based on how dark vs the centre
int brightness = 0;
if (ledOn) {
brightness = map(light, 0, centre, 255, 0);
brightness = constrain(brightness, 0, 255);
}
analogWrite(LED_PIN, brightness);
// 5. Live telemetry — turn off for production, leave on for tuning
Serial.print("light="); Serial.print(light);
Serial.print(" centre="); Serial.print(centre);
Serial.print(" on="); Serial.print(ledOn);
Serial.print(" bright="); Serial.println(brightness);
delay(50);
}Upload and drive it through every state
- Upload at 9600 baud. Open the Monitor.
- With normal room light and the pot in the middle, the LED should sit somewhere between off and faint. The Monitor shows live values for everything.
- Cover the LDR slowly. As the reading drops below
centre, the LED starts fading up. Cover completely — the LED reaches its brightest. The fade is smooth, not snappy. - Uncover. The LED fades back down, then snaps off at
offAt(which is 40 units above where it turned on). - Turn the pot. The "centre" value in the Monitor changes from one extreme to the other. The room's brightness hasn't changed — your knob has changed what the device considers dark.
- Hold the LDR at exactly its threshold. The reading hovers near
centre; the LED holds its current state cleanly thanks to the hysteresis deadband.
Trace one full cycle on paper
Assume pot is centred so centre = 500 (and therefore onAt = 480, offAt = 520). Light ledOn starts false. Fill in the four columns at each tick of an imagined slow-dimming event.
| Tick | light | Compare | New ledOn | Brightness |
|---|---|---|---|---|
| 1 | 700 (bright) | not ≤ 480 and ≥ 520 → off | false | 0 |
| 2 | 510 (dimmer) | not ≤ 480 and not ≥ 520 → no change | ____ | ____ |
| 3 | 460 (dark) | ____ | ____ | ____ (about half-bright since 460 is roughly mid-range) |
| 4 | 200 (very dark) | ____ | ____ | ____ (close to 255) |
| 5 | 490 (light coming back) | not ≤ 480 and not ≥ 520 → no change | ____ (stays true!) | ____ |
| 6 | 600 (bright) | ____ | ____ | 0 |
Tick 5 is the key insight — even though the room got brighter than where the LED first turned on, hysteresis keeps it on until the room gets clearly bright (above 520). The product feels "decisive" instead of "twitchy".
Try It Yourself — three product upgrades 15 min
Goal: A reverse-mode "morning light". Same hardware, opposite behaviour — the LED turns on as the room gets brighter, so it can act as a gentle "good morning" indicator on a bedside table. Bright morning = LED on; dark night = LED off.
Plan: flip both hysteresis comparisons and reverse the brightness map.
// Inverse hysteresis
if (light >= offAt) ledOn = true; // (note: variable name "offAt" is now backwards)
else if (light <= onAt) ledOn = false;
// Inverse brightness map
brightness = map(light, centre, 1023, 0, 255);Questions:
- The variable names
onAtandoffAtare now misleading. Suggest better names for this inverse mode. ____ - If your pot is set so centre = 700 (deliberately needs quite a lot of brightness to trigger), what kind of room would trigger this morning light most reliably — sunlit, cloudy, electric-lit? ____
- What's the user-experience problem with using one LED as both "night light" and "morning light" by flipping the code? ____ (Hint: how does the user select the mode?)
Goal: A "battery saver" mode. Add an auto-shutoff: after 30 minutes of continuous "LED on", the night light turns itself off and stays off for the rest of the session (until reset). Real bedside products often do this so they don't waste power if you leave the room.
Plan: keep a timestamp of "when did the LED turn on?" When it transitions from off to on, record onSinceMs = millis();. While on, check whether millis() - onSinceMs > 1800000UL (30 min in ms). If yes, force LED off and ignore further "turn on" attempts.
Hint: introduce a bool autoShutoff = false; flag. Set it after timeout. In the hysteresis update, refuse to set ledOn = true if autoShutoff is on.
Questions:
- Why
1800000ULinstead of just1800000? ____ (Hint: integer types and overflow.) - Why does the timer track "since the LED turned on" rather than "since the device powered up"? ____
- For testing, what change would you make so the timeout fires after 30 seconds instead of 30 minutes? ____
Goal: A "sensor health check". If the LDR is disconnected (gives consistently 0 or 1023, which can't happen with a working divider in a real room), the product blinks an SOS pattern on the LED instead of running normally. This is the kind of fault-detection real devices ship with.
Plan: keep a running average of the last N readings (say N = 20). If the average is < 5 OR > 1018, treat the sensor as faulty. While faulty, run a small blink loop that flashes the LED in groups of three rather than doing the normal mood-lamp behaviour.
Questions:
- Why an average rather than a single reading? ____ (Hint: noise could push one reading into the danger zone briefly.)
- What other "impossible" sensor states could you check for? ____ (Hint: a reading that doesn't change for 5 minutes — sensor stuck.)
- Real industrial sensors transmit a "health byte" alongside every reading. Why is that more robust than your simple range check? ____
Mini-Challenge — manual override button 10 min
"Three modes: AUTO, FORCE-ON, FORCE-OFF — cycle by button"
Add a button on D7 (INPUT_PULLUP, the L01-17 wiring). Each press of the button cycles the device through three modes:
- AUTO — the worked-example logic (LDR-driven with hysteresis and PWM fade).
- FORCE-ON — LED stays on at full brightness regardless of light.
- FORCE-OFF — LED stays off regardless of light.
- Next press loops back to AUTO.
Print the current mode name to the Monitor whenever it changes. Use L01-19 state-change detection so each press cycles the mode exactly once (not several times per held press).
It works if:
- Pressing the button cycles the mode each time, with the new mode printed to the Monitor.
- In AUTO mode, covering the LDR makes the LED fade up; uncovering fades it down.
- In FORCE-ON, the LED is at full bright regardless of LDR.
- In FORCE-OFF, the LED stays dark regardless of LDR.
- Holding the button does not cycle multiple times — only the press edge counts.
Reveal one valid sketch
// Auto night light + manual override (3 modes)
const int LDR_PIN = A0;
const int POT_PIN = A1;
const int LED_PIN = 9;
const int BTN_PIN = 7;
const int DEADBAND = 20;
int mode = 0; // 0 = AUTO, 1 = FORCE-ON, 2 = FORCE-OFF
int lastButton = HIGH;
bool ledOn = false;
void setup() {
Serial.begin(9600);
pinMode(LED_PIN, OUTPUT);
pinMode(BTN_PIN, INPUT_PULLUP);
Serial.println("mode = AUTO");
}
void loop() {
// === Mode cycling ===
int nowButton = digitalRead(BTN_PIN);
if (lastButton == HIGH && nowButton == LOW) {
mode = (mode + 1) % 3;
Serial.print("mode = ");
if (mode == 0) Serial.println("AUTO");
else if (mode == 1) Serial.println("FORCE-ON");
else Serial.println("FORCE-OFF");
delay(20); // debounce
}
lastButton = nowButton;
// === Mode-specific LED drive ===
int brightness = 0;
if (mode == 0) {
// AUTO — full hysteresis + fade logic
int light = analogRead(LDR_PIN);
int centre = map(analogRead(POT_PIN), 0, 1023, 100, 900);
if (light <= centre - DEADBAND) ledOn = true;
else if (light >= centre + DEADBAND) ledOn = false;
if (ledOn) {
brightness = constrain(map(light, 0, centre, 255, 0), 0, 255);
}
}
else if (mode == 1) {
brightness = 255;
}
// mode 2 (FORCE-OFF): brightness stays 0
analogWrite(LED_PIN, brightness);
}Two halves: the top half cycles modes on the button's press edge (L01-19); the bottom half does mode-specific work. The (mode + 1) % 3 trick (from L01-29) makes the cycle wrap automatically. The whole sketch is the worked example with a small state machine bolted on — and now the device has the basic "manual override" every real product ships with.
Recap 5 min
Cluster F's project is a sensor product, not a sketch. It reads two analog inputs (LDR + pot), runs them through three polish moves — hysteresis to ignore flicker, a smooth PWM fade to feel responsive, a user-adjustable sensitivity knob — and outputs a steady, pleasant glow that scales with how dark the room is. The whole pipeline is L01-36 (LDR), L01-37 (analog/digital pins), L01-38 (voltage divider maths), L01-40 (pot), L01-41 (map), and L01-42 (boolean logic) layered into one 20-line sketch. This is the template for every analog sensor product you'll build: read the world, decide cleanly, fade smoothly, give the user a knob.
- Hysteresis
- Using two thresholds — one to turn on, a higher one to turn off — with a "deadband" between them where state doesn't change. The standard cure for "flicker near the threshold" in any sensor-driven device. Thermostats, dimmers, motor controllers, even hard-drive speed throttles all use it.
- Deadband
- The range of sensor values between the two hysteresis thresholds, in which no state change happens. Width is chosen larger than typical sensor noise so brief fluctuations are ignored.
- Smooth response (vs. snap response)
- Driving an output with a continuous PWM value derived from a sensor (smooth) rather than just on/off (snap). Smooth feels designed; snap feels cheap.
- User-adjustable sensitivity
- A physical knob (pot) that lets the user shift the device's threshold without touching the code. Read with
analogRead, scaled withmap(), fed into the threshold logic. Turns a hardcoded sketch into a product anyone can tune. - Telemetry
- Serial print lines that report the device's internal state — useful during tuning, off during shipping. Pro devices often have a "debug mode" that exposes the same telemetry on demand.
- Mode (state machine)
- A small integer global tracking which behaviour the device is currently in (AUTO / FORCE-ON / FORCE-OFF in the mini-challenge). A button press cycles it; the main loop dispatches on it. Same pattern as L01-23 burglar alarm and L01-29 light show.
- Fault detection
- Code that watches for impossible sensor states (always-zero, always-max, never-changing) and flags them rather than acting on the bad data. Standard in industrial devices; nice to have in hobby ones.
Homework 5 min
Personalise your night light. Take the worked-example sketch (or the mini-challenge sketch with manual override) and add three personal touches that make it feel like your product. Pick from the list, or invent your own:
- Custom colour — swap the plain LED for an RGB (D9/D10/D11) and pick a signature colour (warm orange, cool blue, soft pink) for the "night light glow".
- Brief startup chirp — add a piezo on D8 and play a 200 ms "device alive" beep at power-on. Quiet but distinctive.
- Slow startup fade — when the device first powers on, fade the LED up over 3 seconds to its current target brightness, instead of jumping straight to it. Feels more like waking up than switching.
- "Goodnight" mode — a long press of the button (held for > 2 seconds) puts the LED into a slow 60-second fade-down to dark, then stays off until reset. Imitates a sleep-timer.
- Calibration on boot — first 5 seconds of
setup(), sample the LDR and pick a sensible centre threshold for the room's current brightness (the L01-36 stretch task pattern).
Also: a design reflection on paper.
- Of your three personalisations, which one feels the most "expensive product"? Why? ____
- The lesson kept the live
Serial.printlntelemetry on. Should the shipping product remove it? Argue both sides. ____ (Hint: power usage, memory, but also debugging customer issues remotely.) - Imagine your night light needs to be approved for sale to children under 5. What would you have to add or remove? ____ (Hint: certifications, voltage isolation, button shapes…)
- Real Philips Hue light bulbs run a much fancier version of this same algorithm — sensor input, threshold logic, PWM output. Roughly how much more code do you imagine they have? Where would that code go? ____
Bring back next class:
- The saved
.inofile (call itmy-night-light-product). - A 30-second phone video walking through your three personalisations.
- Your four written reflection answers, in your notebook.
Heads up for next class: Cluster F is done. Cluster G "Build, Reflect, Recap" begins with L01-44 "Reaction-Time Arcade" — a polished, score-keeping version of L01-22's reaction timer with replay, leaderboard print-out, and a proper end-screen. The remaining four lessons of Level 1 are all polishing-pass builds and the level-end recap.