A night light
A TK01 XL LED that comes on when the room gets dark. The sketch calibrates itself at start-up: it reads the room lit, then the block covered, and puts two lines between them, on below 40 % of the way up and off above 60 %. The gap between them is what stops it flickering at dusk.
Calibrate, do not guess
A threshold copied from someone else's sketch is wrong in your room. Their lamp is a different kind, their sensor is up to 30 % more or less sensitive than yours, and their board may read in different units. The one reliable way to choose a line is to read the two conditions you care about, with the block you have, where it will live.
So the sketch starts by measuring. It reads the room lit, asks you to cover the sensor with a finger, and reads that too. The finger stands in for night. Both readings use the 50 ms average from the flicker under room lights, so a rippling lamp cannot skew them.
Two lines, not one
Leave it on One line, halfway and let it get dark. As the evening fades the reading does not fall smoothly: a shadow crosses it, a car passes, a little ripple is left over. Near the line every wobble crosses it one way and then the other, and the light flickers on and off before it settles.
Pick Two lines. The LED now comes on below 40 % of the way from dark to lit and goes off only above 60 %. A wobble smaller than that gap cannot undo the switch, and the light changes once. That gap is called hysteresis.
Wiring the LED
The TK01 XL LED needs two wires, as in its own book: GND to GND, and SIGNAL to D9 on an Uno, GPIO 4 on an ESP32, GPIO 5 on an ESP32-S3, GP15 on a Pico. They are the pins the NTC thermistor's alarm uses. On the ESP32-S3, GPIO 4 is the light sensor, which is why the LED is on GPIO 5.
Point the LED away from the sensor. This is the one mistake the gap cannot fix: the LED comes on, its light reaches the sensor, the room reads as bright, and it turns itself off. If its light adds more than the gap, it flashes on and off for ever.
Running it
Open the serial monitor at 115200 before the board starts, or press reset with it open. Leave the lights on for the first three seconds; cover the clear part with a fingertip when it asks; uncover it when it says done. It prints the two lines it chose, then a reading every quarter of a second or so. Cover the sensor again and the LED comes on; uncover it and it goes off.
The code
The 50 ms average from the flicker article, a calibration in setup, and a TK01 XL LED that turns on below the lower line and off only above the upper one. Follow the prompts in the serial monitor while it starts.
/*
Ambient Light Sensor - a night light TK20 / /p/tk20
Wiring, the TK20. Count from the square pad, parts up, header at
the bottom:
GND -> GND
VCC -> 5V on an Uno; 3V3 on an ESP32, ESP32-S3 or Pico
NC -> nothing (unconnected on the board)
SIGNAL -> A0 on an Uno, GPIO 34 on an ESP32, GPIO 4 on an
ESP32-S3, GP26 on a Raspberry Pi Pico
The TK01 XL LED, counted the same way. Point it away from the TK20:
GND -> GND
NC -> nothing (both of its NC pins)
SIGNAL -> D9 on an Uno, GPIO 4 on an ESP32, GPIO 5 on an
ESP32-S3, GP15 on a Raspberry Pi Pico
Arduino IDE
Tools > Board your board, e.g. Arduino Uno
Tools > Port the one that appears when you plug in
Tools > USB CDC On Boot Enabled (ESP32-S3 only)
No library needed.
*/
// The analog pin SIGNAL is wired to.
// Uno: A0. ESP32: 34. ESP32-S3: 4. Pico: 26.
const int LIGHT_PIN = A0;
// The TK01's SIGNAL. Uno: 9. ESP32: 4. ESP32-S3: 5. Pico: 15.
const int LED_PIN = 9;
// Uno and Pico only: the ADC's full scale, in mV.
// Uno: 5000. Pico: 3300.
const float FULL_SCALE_MV = 5000.0;
// 5 cycles of a 100 Hz ripple, 6 of a 120 Hz one.
const unsigned long WINDOW_MS = 50;
const float ON_FRACTION = 0.4; // on below 40 % of dark-to-lit
const float OFF_FRACTION = 0.6; // off only above 60 %
float onBelowMv;
float offAboveMv;
bool lampOn = false;
float readMilliVolts() {
#if defined(ARDUINO_ARCH_ESP32)
return analogReadMilliVolts(LIGHT_PIN); // calibrated in the chip
#else
return analogRead(LIGHT_PIN) * FULL_SCALE_MV / 1023.0;
#endif
}
float readSteadyMilliVolts() {
float sum = 0;
long n = 0;
unsigned long start = millis();
while (millis() - start < WINDOW_MS) {
sum += readMilliVolts();
n++;
}
return sum / n;
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
delay(1000);
Serial.println("Calibrating. Leave the room lit...");
delay(3000);
float lit = readSteadyMilliVolts();
Serial.println("Now cover the sensor with a finger...");
delay(4000);
float dark = readSteadyMilliVolts();
Serial.println("Done. Uncover it.");
if (lit - dark < 20) {
Serial.println("Lit and dark are too close: press reset.");
}
onBelowMv = dark + ON_FRACTION * (lit - dark);
offAboveMv = dark + OFF_FRACTION * (lit - dark);
Serial.print("On below ");
Serial.print(onBelowMv, 0);
Serial.print(" mV, off above ");
Serial.print(offAboveMv, 0);
Serial.println(" mV");
}
void loop() {
float mv = readSteadyMilliVolts();
if (!lampOn && mv < onBelowMv) lampOn = true;
if (lampOn && mv > offAboveMv) lampOn = false;
digitalWrite(LED_PIN, lampOn ? HIGH : LOW);
Serial.print(mv, 0);
Serial.println(lampOn ? " mV on" : " mV");
delay(200);
}Calibration runs every time the board starts, so the lines fit the room and the sensor you have. ON_FRACTION and OFF_FRACTION set the gap; make them equal to see the flicker the figure shows. Keep the TK01's light off the sensor, or it will switch itself off.
The same night light in MicroPython: the 50 ms average, a calibration at start-up, and a TK01 that turns on below the lower line and off only above the upper one.
"""
Ambient Light Sensor - a night light, MicroPython TK20 / /p/tk20
Wiring, the TK20. Count from the square pad, parts up, header at
the bottom:
GND -> GND
VCC -> 3V3 (never 5V: bright light takes SIGNAL towards VCC)
NC -> nothing (unconnected on the board)
SIGNAL -> GPIO 34 on an ESP32, GPIO 4 on an ESP32-S3,
GP26 on a Raspberry Pi Pico
The TK01 XL LED, counted the same way. Point it away from the TK20:
GND -> GND
NC -> nothing (both of its NC pins)
SIGNAL -> GPIO 4 on an ESP32, GPIO 5 on an ESP32-S3,
GP15 on a Raspberry Pi Pico
Thonny
Run > Configure interpreter MicroPython (ESP32) or
MicroPython (Raspberry Pi Pico)
Save it to the board as main.py to run it on every power-up.
Nothing to install: machine, sys and time are built in.
"""
import sys
import time
from machine import ADC, Pin
# The GPIO number SIGNAL is wired to. ESP32: 34. ESP32-S3: 4. Pico: 26.
LIGHT_PIN = 4
# The TK01's SIGNAL. ESP32: 4. ESP32-S3: 5. Pico: 15.
LED_PIN = 5
WINDOW_MS = 50 # 5 cycles of a 100 Hz ripple, 6 of a 120 Hz one
ON_FRACTION = 0.4 # on below 40 % of dark-to-lit
OFF_FRACTION = 0.6 # off only above 60 %
adc = ADC(Pin(LIGHT_PIN))
ESP = sys.platform == "esp32" # ESP32 and ESP32-S3
if ESP:
adc.atten(ADC.ATTN_11DB) # the full range, to about 3.1 V
led = Pin(LED_PIN, Pin.OUT)
def read_millivolts():
if ESP:
return adc.read_uv() / 1000 # calibrated in the chip
return adc.read_u16() * 3300 / 65535
def read_steady_millivolts():
total = 0
n = 0
start = time.ticks_ms()
while time.ticks_diff(time.ticks_ms(), start) < WINDOW_MS:
total += read_millivolts()
n += 1
return total / n
time.sleep_ms(1000)
print("Calibrating. Leave the room lit...")
time.sleep_ms(3000)
lit = read_steady_millivolts()
print("Now cover the sensor with a finger...")
time.sleep_ms(4000)
dark = read_steady_millivolts()
print("Done. Uncover it.")
if lit - dark < 20:
print("Lit and dark are too close: stop and run it again.")
on_below = dark + ON_FRACTION * (lit - dark)
off_above = dark + OFF_FRACTION * (lit - dark)
print("On below %.0f mV, off above %.0f mV" % (on_below, off_above))
lamp_on = False
while True:
mv = read_steady_millivolts()
if not lamp_on and mv < on_below:
lamp_on = True
if lamp_on and mv > off_above:
lamp_on = False
led.value(1 if lamp_on else 0)
print("%.0f mV%s" % (mv, " on" if lamp_on else ""))
time.sleep_ms(200)There is no Uno here: an Uno cannot run MicroPython. The prompts appear in Thonny's shell. Stop it with Ctrl-C; the LED stays as it was, so switch it off in the shell with led.value(0).
When it does not work
The TK01's own light is reaching the sensor. It comes on, lights the sensor, reads as bright, and turns itself off. Point the LED away from the sensor, or put something opaque between them. Hysteresis cannot fix this if the LED adds more light than the gap between the two lines.
The two calibration readings were nearly the same: the room was already dark, or the hand did not cover the sensor. Press reset and try again with the lights on for the first reading and a finger pressed over the clear part for the second.
Move the lines. A smaller ON_FRACTION, such as 0.2, waits until it is darker; a larger one, such as 0.5, comes on earlier. Keep OFF_FRACTION about 0.2 above it so the gap stays. On an ESP32, a room under about 43 lux reads the same as covered, so the light cannot tell dusk from night below that.
Short bright bursts turn it off only while they last, because it only needs to climb above the off line once. Add a rule that the reading must stay above the off line for a few seconds before the LED goes off; millis() can time it.
Edit this page — content/books/ambient-light-sensor/a-night-light.mdx
Questions about this product
See what other owners have asked, and read their solutions.
Ambient Light Sensor
Loading discussions…
Discuss this article
Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.