Sitting still for hours is bad for your back and your concentration. This desk buddy keeps count for you. Its "eyes" are the ultrasonic sensor: they see when you sit in front of it. While you work, its green cheek glows. After 45 minutes it turns red, chirps and sends a notification to your phone. As soon as you have been away from the desk for two minutes, it counts that as a break and starts again. The Blynk app shows how long you have been sitting now and today, and a slider sets how often you want a break.
What you'll learn
- How to use the ultrasonic sensor to tell whether someone is there
- How to count time in steps with a BlynkTimer
- How to decide what counts as a real break
- How to set a value from a slider in the app and keep it after a restart
What you need
- ESP32 board, expansion board and USB cable (ESP32 IoT Starter Kit)
- Ultrasonic sensor HC-SR04 (ESP32 IoT Starter Kit)
- 3 resistors, 10 kΩ, for the voltage divider (ESP32 IoT Starter Kit)
- Green and red LEDs and 2 resistors, 220 Ω (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 (desk buddy), or the cardboard version described there
- Breadboard and jumper wires (ESP32 IoT Starter Kit)
Wiring

| From | To |
|---|---|
| Ultrasonic VCC | 5V |
| Ultrasonic Trig | ESP32 GPIO 26 |
| Ultrasonic Echo | 10 kΩ resistor, then ESP32 GPIO 27 |
| ESP32 GPIO 27 | Two 10 kΩ resistors in a row, then GND |
| Ultrasonic GND | GND |
| ESP32 GPIO 25 | 220 Ω resistor, then the green LED's long leg |
| ESP32 GPIO 32 | 220 Ω resistor, then the red LED's long leg |
| Both LED short legs | GND |
| Passive buzzer + | ESP32 GPIO 33 |
| Passive buzzer − | GND |
Protect the ESP32
The HC-SR04 runs on 5 V and answers with 5 V on its Echo pin, but the ESP32's pins are made for 3.3 V. Three equal 10 kΩ resistors fix that: one from Echo to the ESP32 pin, and two in a row from the ESP32 pin to GND. The pin then sees two thirds of 5 V, about 3.3 V. The radar capstone explains it in detail.
Set up Blynk
In the Blynk console, create a new template called Desk Buddy (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 | Sitting now | Integer | 0–120 | Gauge |
| V1 | Today | Integer | 0–1000 | Value display |
| V2 | Break every | Integer | 15–90 | Slider |
| V3 | Status | String | – | Label |
Then open the template's Events & Notifications tab, add an event with the code take_a_break (name: Time for a break) 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 Gauge (V0), a Value display (V1), a Slider (V2) and a Label (V3). Link each widget to its datastream.
V0 and V1 are in minutes. Set the slider's step to 5.

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>.
Change TIME_ZONE to your own time zone. The example is for Central Europe; search for "POSIX time zone" and your city to find yours (for example EET-2EEST,M3.5.0/3,M10.5.0/4 for Estonia or GMT0BST,M3.5.0/1,M10.5.0 for the UK).
#define BLYNK_TEMPLATE_ID "YOUR_TEMPLATE_ID"
#define BLYNK_TEMPLATE_NAME "Desk Buddy"
#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>
#include <time.h>
char ssid[] = "YOUR_WIFI_NAME";
char pass[] = "YOUR_WIFI_PASSWORD";
const char* TIME_ZONE = "CET-1CEST,M3.5.0,M10.5.0/3"; // your time zone
const int TRIG_PIN = 26, ECHO_PIN = 27;
const int GREEN_PIN = 25, RED_PIN = 32, BUZZER_PIN = 33;
const int AT_DESK_CM = 90; // closer than this: you are there
const int BREAK_SECONDS = 120; // away this long counts as a break
BlynkTimer timer;
int breakEvery = 45; // minutes, set by the slider
long sitting = 0; // seconds since the last break
long todayTotal = 0; // seconds at the desk today
long away = 0; // seconds away from the desk
bool here = false, breakSent = false;
unsigned long lastChirp = 0;
int today = -1;
int measureCm() {
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
long us = pulseIn(ECHO_PIN, HIGH, 30000);
return us == 0 ? 400 : min(us / 58, 400L);
}
void chirp() {
tone(BUZZER_PIN, 1800, 100);
timer.setTimeout(200L, []() { tone(BUZZER_PIN, 2400, 150); });
lastChirp = millis();
}
void everyTwoSeconds() {
int a = measureCm(), b = measureCm(), c = measureCm();
int distance = max(min(a, b), min(max(a, b), c)); // median of three
here = distance < AT_DESK_CM;
if (here) {
sitting += 2;
todayTotal += 2;
away = 0;
} else {
away += 2;
if (away >= BREAK_SECONDS && sitting > 0) { // a real break: start again
sitting = 0;
breakSent = false;
Blynk.virtualWrite(V3, "On a break");
}
}
bool due = sitting >= breakEvery * 60L;
if (due && here && !breakSent) {
Blynk.logEvent("take_a_break", String("You have been sitting for ") + breakEvery + " minutes. Time to stretch!");
Blynk.virtualWrite(V3, "Time for a break!");
breakSent = true;
chirp();
}
if (due && here && millis() - lastChirp > 5UL * 60 * 1000) chirp(); // remind every 5 minutes
}
void lights() { // 4 times a second
bool due = sitting >= breakEvery * 60L;
digitalWrite(GREEN_PIN, here && !due);
digitalWrite(RED_PIN, here && due && (millis() / 500) % 2);
}
void report() { // every 30 seconds
Blynk.virtualWrite(V0, sitting / 60);
Blynk.virtualWrite(V1, todayTotal / 60);
if (here && sitting < breakEvery * 60L) Blynk.virtualWrite(V3, "Working");
else if (!here && away < BREAK_SECONDS) Blynk.virtualWrite(V3, "Just stepped away");
time_t now = time(nullptr); // new day: start counting again
if (now > 1700000000) {
struct tm local;
localtime_r(&now, &local);
if (today != -1 && local.tm_yday != today) todayTotal = 0;
today = local.tm_yday;
}
}
BLYNK_WRITE(V2) { // Break every ... minutes
breakEvery = constrain(param.asInt(), 15, 90);
}
BLYNK_CONNECTED() {
Blynk.syncVirtual(V2);
}
void setup() {
Serial.begin(115200);
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
pinMode(GREEN_PIN, OUTPUT);
pinMode(RED_PIN, OUTPUT);
Blynk.begin(BLYNK_AUTH_TOKEN, ssid, pass);
configTzTime(TIME_ZONE, "pool.ntp.org");
timer.setInterval(2000L, everyTwoSeconds);
timer.setInterval(250L, lights);
timer.setInterval(30000L, report);
}
void loop() {
Blynk.run();
timer.run();
}
Try it
- Set the slider to 15 minutes for a quick test, sit down and work. The green cheek glows.
- After 15 minutes the buddy turns red and blinks, chirps, and your phone shows "Time for a break".
- Stand up and leave the desk for two minutes. When you come back, the green cheek is on again and "Sitting now" has started from 0. Set the slider back to 45.
How it works
The ultrasonic sensor sends a short click of sound, too high to hear, and times the echo. Something closer than 90 cm, you, makes here true. Taking three readings and using the middle one, the median, ignores a single wrong echo.
The sketch doesn't need a clock to count minutes: everyTwoSeconds() runs every 2 seconds, so each call adds 2 to the seconds you have been sitting. Three timers do three jobs at their own speed: measuring every 2 s, the lights 4 times a second, and the app every 30 s.
Leaning back or fetching a pencil shouldn't count as a break. Only when you have been away for two minutes in a row does sitting go back to 0. The notification is sent once per sitting session, and the chirp repeats every five minutes until you get up.
Blynk.syncVirtual(V2) fetches the slider's value when the ESP32 connects, so your setting survives a restart.
Try this next
- Count the breaks you took today and show them in the app.
- Make the buddy remind you to drink: a blue light every hour.
- Send a weekly summary: your average sitting time per day.
Troubleshooting
-
The device shows "Offline" in the app: check the Serial Monitor at 115200 baud. "Invalid auth token" means the
BLYNK_AUTH_TOKENline is wrong; if it never connects, check the Wi-Fi name and password, and that your network is 2.4 GHz. -
It never sees you: the buddy points past you or you sit further than 90 cm. Turn it towards your chest, or raise
AT_DESK_CM. -
It sees you when you are gone: the chair back is in range. Lower
AT_DESK_CMuntil an empty chair counts as "away". - The slider does nothing: it must be linked to V2, with a range of 15–90.
- 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.