Build a focus timer with a glowing light cube

A printed cube glows red while you focus and green when it's time for a break. Start it from the Blynk app or the button, and get a phone notification when the break begins.
  • Level:Advanced
  • Duration: 1 h
  • Ages 11+
  • Platform: ESP32

Tools for this lesson

Helpful for this build. Open a tool to see tips and more projects that use it.

Build a focus timer with a glowing light cube – EEMENTOR STEM™ lesson

Many people study and work in blocks of 25 minutes of focus followed by a 5-minute break; it is called the Pomodoro technique. In this lesson you build a desk light for it. Start a session from the Blynk app or the button on the board: the cube glows red while you focus, and the app shows the minutes left. When the focus time is over, the cube turns green, a buzzer chirps and Blynk sends a notification to your phone. After the break it turns blue and waits for the next session.

What you'll learn

  • How to build a timer with BlynkTimer instead of delay()
  • How to keep the device, the app and the button in step
  • How to count completed sessions
  • How to send a Blynk event

What you need

  • ESP32 board, expansion board and USB cable (ESP32 IoT Starter Kit)
  • RGB LED module (45-in-1 Sensor Kit)
  • 3 resistors, 220 Ω (ESP32 IoT Starter Kit)
  • 1 push button (ESP32 IoT Starter Kit)
  • Passive buzzer (ESP32 IoT Starter Kit)
  • A free Blynk account and the Blynk IoT app on a phone
  • A 2.4 GHz Wi-Fi network
  • The printed parts from the 3D printing box below (light cube), or the cardboard version described there
  • Breadboard and jumper wires (ESP32 IoT Starter Kit)

Wiring

Breadboard wiring diagram: Build a focus timer with a glowing light cube
Breadboard view. Check each connection against the wiring table below.
From To
RGB module R 220 Ω resistor, then GPIO 25
RGB module G 220 Ω resistor, then GPIO 26
RGB module B 220 Ω resistor, then GPIO 33
RGB module − GND
Push button, one leg GPIO 4
Push button, the diagonally opposite leg GND
Passive buzzer + GPIO 23
Passive buzzer − GND

Sensor modules from different makers can label and order their pins differently. Always follow the labels printed on your module.

Set up Blynk

In the Blynk console, create a new template called Focus Timer (hardware ESP32, connection type Wi-Fi) and add these datastreams. Lesson 1 of this path shows each step in detail.

Virtual pin Name Data type Range Phone widget
V0 Running Integer 0–1 Switch
V1 Focus minutes Integer 5–60 Slider
V2 Minutes left Integer 0–60 Gauge or Value display
V3 Phase String – Label
V4 Sessions today Integer 0–50 Value display

Then open the template's Events & Notifications tab, add an event with the code break_time (name: Break time) and turn on push notifications for it. The event code must match the sketch exactly.

Save the template, create a new device from it and copy its three BLYNK_… lines. In the phone app, open the device, tap the wrench icon and add a Switch (V0), a Slider (V1), a Gauge (V2), a Label (V3) and a Value display (V4). Link each widget to its datastream.

Example screen for: Build a focus timer with a glowing light cube
Example screen with sample values. Yours may look different.

The sketch

Replace the three BLYNK_… lines at the top with the ones from your device's Device info page, and add your Wi-Fi name and password (2.4 GHz only). The BLYNK_… lines must stay above #include <BlynkSimpleEsp32.h>.

#define BLYNK_TEMPLATE_ID "YOUR_TEMPLATE_ID"
#define BLYNK_TEMPLATE_NAME "Focus Timer"
#define BLYNK_AUTH_TOKEN "YOUR_AUTH_TOKEN"
#define BLYNK_PRINT Serial          // show connection messages in the Serial Monitor

#include <WiFi.h>
#include <WiFiClient.h>
#include <BlynkSimpleEsp32.h>

char ssid[] = "YOUR_WIFI_NAME";
char pass[] = "YOUR_WIFI_PASSWORD";

const int RED_PIN = 25, GREEN_PIN = 26, BLUE_PIN = 33;
const int BUTTON_PIN = 4;
const int BUZZER_PIN = 23;
const int BREAK_MINUTES = 5;

const byte IDLE = 0, FOCUS = 1, BREAK = 2;
byte phase = IDLE;
int focusMinutes = 25;
int secondsLeft = 0;
int sessions = 0;
bool lastButton = HIGH;
BlynkTimer timer;

void colour(int r, int g, int b) {
  analogWrite(RED_PIN, r);
  analogWrite(GREEN_PIN, g);
  analogWrite(BLUE_PIN, b);
}

void show() {
  if (phase == FOCUS) colour(255, 0, 0);
  else if (phase == BREAK) colour(0, 255, 0);
  else colour(0, 0, 60);
  const char* names[] = {"Ready", "Focus", "Break"};
  Blynk.virtualWrite(V0, phase == IDLE ? 0 : 1);
  Blynk.virtualWrite(V2, (secondsLeft + 59) / 60);
  Blynk.virtualWrite(V3, names[phase]);
  Blynk.virtualWrite(V4, sessions);
}

void chirp() {
  tone(BUZZER_PIN, 1047, 120);
  delay(160);
  tone(BUZZER_PIN, 1319, 160);
}

void startFocus() {
  phase = FOCUS;
  secondsLeft = focusMinutes * 60;
  show();
}

void stopAll() {
  phase = IDLE;
  secondsLeft = 0;
  show();
}

void everySecond() {
  if (phase == IDLE) return;
  secondsLeft--;
  if (secondsLeft % 60 == 0) Blynk.virtualWrite(V2, secondsLeft / 60);
  if (secondsLeft > 0) return;
  if (phase == FOCUS) {
    sessions++;
    phase = BREAK;
    secondsLeft = BREAK_MINUTES * 60;
    chirp();
    Blynk.logEvent("break_time", String("Session ") + sessions + " done. Take a " + BREAK_MINUTES + "-minute break!");
  } else {
    phase = IDLE;
    chirp();
  }
  show();
}

BLYNK_CONNECTED() {
  Blynk.syncVirtual(V1);         // get the focus time saved in the app
  show();
}

BLYNK_WRITE(V0) {                // the switch in the app
  if (param.asInt() == 1 && phase == IDLE) startFocus();
  else if (param.asInt() == 0) stopAll();
}

BLYNK_WRITE(V1) {                // the slider: minutes of focus
  focusMinutes = constrain(param.asInt(), 5, 60);
}

void setup() {
  Serial.begin(115200);
  pinMode(RED_PIN, OUTPUT);
  pinMode(GREEN_PIN, OUTPUT);
  pinMode(BLUE_PIN, OUTPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  colour(0, 0, 60);
  Blynk.begin(BLYNK_AUTH_TOKEN, ssid, pass);
  timer.setInterval(1000L, everySecond);
}

void loop() {
  Blynk.run();
  timer.run();
  bool button = digitalRead(BUTTON_PIN);
  if (button == LOW && lastButton == HIGH) {     // the button starts or stops
    if (phase == IDLE) startFocus();
    else stopAll();
    delay(200);
  }
  lastButton = button;
}

Try it

  1. Upload the sketch and open the Serial Monitor at 115200 baud until Blynk says it is connected. The cube glows a soft blue.
  2. Set the slider to 5 minutes for a quick test and turn on the switch in the app. The cube turns red and the gauge counts down.
  3. After 5 minutes the cube turns green, the buzzer chirps and your phone shows the "Break time" notification.
  4. Press the button on the board to stop a session; the switch in the app follows.

How it works

BlynkTimer calls everySecond() once a second while loop() keeps running, so the button and the Blynk connection never have to wait. The timer only counts down secondsLeft; everything that changes when a phase ends happens in one place, which keeps the program easy to follow.

The device is the "boss" of the state. Whether you press the button or flip the switch in the app, the sketch changes phase and then calls show(), which updates the light and every widget. That way the app always shows what the cube is doing. BLYNK_CONNECTED() asks for the slider's value after every connection, so your chosen focus time survives a restart.

Try this next

  • Make every fourth break 15 minutes long, as in the original Pomodoro technique.
  • Fade the cube slowly from red to orange in the last minute, so the end doesn't surprise you.
  • Add a Superchart in Blynk for Sessions today to see your week.

Troubleshooting

  • The device shows "Offline" in the app: check the Serial Monitor at 115200 baud. "Invalid auth token" means the BLYNK_AUTH_TOKEN line is wrong; if it never connects, check the Wi-Fi name and password, and that your network is 2.4 GHz.
  • No notification arrives: check that the event code is exactly break_time and that notifications are turned on for it in the template and on your phone.
  • The cube colours are mixed up: swap the wires on GPIO 25, 26 and 33.
  • The cube is too dark: print it with thinner walls (vase mode) and in natural or white filament.
  • A printed part is too tight or too loose: printers differ by a few tenths of a millimetre. Sand or file a tight hole, and fix a loose part with a drop of hot glue or a strip of tape.

Build it yourself

Everything used in this lesson comes in one kit, with parts tested by our engineers.

ESP32 IoT Starter Kit | Arduino IDE Compatible | Free Tutorials $57.00

Shop the kit