Many older people live alone and are proud to be independent. Their families still worry: what if they fell, or felt ill, and nobody noticed for a day? Phoning every morning is one answer. This project is another, and it was designed together with that idea of independence: one big button that your grandparent presses once a day, for example after breakfast. The family's phones get a quiet "checked in" message. If the button hasn't been pressed by 10:00, it plays a friendly reminder tune. At 10:30, if there is still no press, the family gets a message: "No check-in yet, please give them a call." And if they ever need help, they hold the button for three seconds.
What you'll learn
- How the ESP32 gets the real time from the internet (NTP), with your time zone
- How to run a daily schedule and reset it at midnight
- How a single button can do two things: a short press and a long press
- How to queue messages so they are sent when Wi-Fi comes back
What you need
- ESP32 board, expansion board and USB cable (ESP32 IoT Starter Kit)
- 1 push button, as big as you have (ESP32 IoT Starter Kit)
- Green LED, red LED and 2 resistors, 220 Ω (ESP32 IoT Starter Kit)
- Passive buzzer (ESP32 IoT Starter Kit)
- Phones with the free ntfy app for the family
- A USB phone charger, so it can stay on all the time
- Breadboard and jumper wires (ESP32 IoT Starter Kit)
Do it together, and with their agreement. A check-in button only works if the person wants it and understands what it sends. Set it up with them, agree on the times, and let them decide who gets the messages. It is not an emergency service: in an emergency, always call the emergency number.
Wiring

| From | To |
|---|---|
| Push button, one leg | ESP32 GPIO 27 |
| Push button, the diagonally opposite leg | GND |
| ESP32 GPIO 25 | 220 Ω resistor, then the green LED's long leg |
| ESP32 GPIO 26 | 220 Ω resistor, then the red LED's long leg |
| LEDs' short legs | GND |
| Passive buzzer + | ESP32 GPIO 33 |
| Passive buzzer − | GND |
Set up ntfy
The phone messages go through ntfy, a free notification service that needs no account. The ntfy lesson explains it step by step.
- Install the ntfy app from Google Play or the App Store.
- Make up a topic name that nobody could guess, such as
eementor-family-4k8p2x. Use only letters, numbers, - and _. - In the app, tap +, type your topic name and tap Subscribe. Everyone who should get the messages subscribes to the same topic on their own phone.
- Test it: open
https://ntfy.sh/your-topic-namein a browser, type a message and send it. It should pop up on the phones.
Important: on the free ntfy.sh server, anyone who knows your topic name can read your messages. Keep the name long and random, and never send personal details through it.
The sketch
Replace YOUR_WIFI_NAME and YOUR_WIFI_PASSWORD with your own network details (2.4 GHz only).
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, GMT0BST,M3.5.0/1,M10.5.0 for the UK, <+04>-4 for the UAE and Oman, or <+03>-3 for Saudi Arabia, Qatar, Kuwait and Bahrain.
Put your topic in TOPIC and the person's name in NAME. Change the three times if they get up earlier or later. Everything else is built into the ESP32 board package.
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <Preferences.h>
#include <time.h>
const char* WIFI_NAME = "YOUR_WIFI_NAME";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* TOPIC = "YOUR_SECRET_TOPIC"; // for example eementor-family-4k8p2x
const char* NAME = "Grandma"; // who presses the button
const char* TIME_ZONE = "CET-1CEST,M3.5.0,M10.5.0/3";
const int CHECKIN_FROM = 7 * 60; // 07:00 the button starts asking (minutes after midnight)
const int REMIND_AT = 10 * 60; // 10:00 a reminder tune if not pressed yet
const int ALERT_AT = 10 * 60 + 30; // 10:30 tell the family
const int NIGHT_FROM = 21 * 60; // 21:00 lights off for the night
const int BUTTON_PIN = 27, GREEN_PIN = 25, RED_PIN = 26, BUZZER_PIN = 33;
Preferences prefs;
int today = -1;
bool checkedIn = false, alerted = false;
unsigned long lastReminder = 0, pressStart = 0, helpUntil = 0, lastWifiTry = 0, lastSendTry = 0;
bool lastButton = HIGH, longDone = false;
// Up to three messages wait here until they have been sent
struct Message { bool waiting; String title, text, priority, tags; };
Message outbox[3];
enum { CHECKIN, MISSED, HELP };
void queue(int slot, String title, String text, String priority, String tags) {
outbox[slot] = {true, title, text, priority, tags};
lastSendTry = 0; // try straight away
}
bool send(Message& m) {
WiFiClientSecure client;
client.setInsecure();
HTTPClient http;
http.setTimeout(5000);
http.begin(client, String("https://ntfy.sh/") + TOPIC);
http.addHeader("Title", m.title);
http.addHeader("Priority", m.priority);
http.addHeader("Tags", m.tags);
int code = http.POST(m.text);
http.end();
Serial.printf("ntfy answered %d\n", code);
return code == 200;
}
void melody(const int* notes, int n) {
for (int i = 0; i < n; i++) {
tone(BUZZER_PIN, notes[i], 160);
delay(200);
}
}
const int HAPPY[] = {523, 659, 784, 1047};
const int REMINDER[] = {784, 659, 784, 659};
String clockText(struct tm& t) {
char buf[6];
strftime(buf, sizeof(buf), "%H:%M", &t);
return String(buf);
}
void shortPress(bool timeOk, struct tm& t) {
if ((long)(helpUntil - millis()) > 0) { // pressed again after asking for help
helpUntil = 0;
queue(HELP, String(NAME) + " is OK now", String(NAME) + " pressed the button again: they are OK now.", "high", "white_check_mark");
melody(HAPPY, 4);
return;
}
if (checkedIn) { // already done today: just a little "yes"
tone(BUZZER_PIN, 1047, 100);
return;
}
checkedIn = true;
if (timeOk) prefs.putInt("day", t.tm_yday); // remembered even after a power cut
melody(HAPPY, 4);
String when = timeOk ? " at " + clockText(t) : "";
String late = alerted ? " (later than usual)" : "";
queue(CHECKIN, String(NAME) + " checked in", String(NAME) + " pressed the button" + when + late + ". All is well.", "low", "white_check_mark");
Serial.println("Checked in");
}
void askForHelp() {
helpUntil = millis() + 600000; // red light blinks for 10 minutes
queue(HELP, String(NAME) + " needs help", String(NAME) + " held the help button. Please call them now, or go and check.", "urgent", "sos");
for (int i = 0; i < 3; i++) {
tone(BUZZER_PIN, 440, 150);
delay(250);
}
Serial.println("Help requested");
}
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(GREEN_PIN, OUTPUT);
pinMode(RED_PIN, OUTPUT);
prefs.begin("checkin", false);
WiFi.begin(WIFI_NAME, WIFI_PASSWORD);
configTzTime(TIME_ZONE, "pool.ntp.org", "time.google.com");
Serial.println("Check-in button starting");
}
void loop() {
unsigned long now = millis();
struct tm t;
bool timeOk = getLocalTime(&t, 10);
int minutes = timeOk ? t.tm_hour * 60 + t.tm_min : -1;
// A new day: start again (unless the button was already pressed today before a restart)
if (timeOk && t.tm_yday != today) {
today = t.tm_yday;
checkedIn = prefs.getInt("day", -1) == today;
alerted = false;
lastReminder = 0;
Serial.printf("Today is day %d, checked in: %s\n", today, checkedIn ? "yes" : "no");
}
// Button: short press = check in, hold 3 s = help
bool button = digitalRead(BUTTON_PIN);
if (lastButton == HIGH && button == LOW) {
pressStart = now;
longDone = false;
}
if (button == LOW && !longDone && now - pressStart > 3000) {
longDone = true;
askForHelp();
}
if (lastButton == LOW && button == HIGH && !longDone && now - pressStart > 50) shortPress(timeOk, t);
lastButton = button;
// The daily schedule
if (timeOk && !checkedIn && minutes >= REMIND_AT && minutes < NIGHT_FROM && (lastReminder == 0 || now - lastReminder > 600000)) {
lastReminder = now; // a friendly tune every 10 minutes
melody(REMINDER, 4);
}
if (timeOk && !checkedIn && !alerted && minutes >= ALERT_AT) {
alerted = true;
queue(MISSED, "No check-in yet", "No check-in from " + String(NAME) + " yet today. Please give them a call.", "high", "warning");
}
// Lights
bool helpOn = (long)(helpUntil - now) > 0;
bool day = timeOk && minutes >= CHECKIN_FROM && minutes < NIGHT_FROM;
if (helpOn) digitalWrite(RED_PIN, (now / 200) % 2);
else digitalWrite(RED_PIN, !timeOk && (now / 1000) % 2); // slow red: no time yet, check the Wi-Fi
digitalWrite(GREEN_PIN, day && (checkedIn || (now / 1000) % 2));
// Wi-Fi and the outbox
if (WiFi.status() != WL_CONNECTED && now - lastWifiTry > 30000) {
lastWifiTry = now;
WiFi.disconnect();
WiFi.begin(WIFI_NAME, WIFI_PASSWORD);
}
if (WiFi.status() == WL_CONNECTED && (lastSendTry == 0 || now - lastSendTry > 30000)) {
for (int i = 0; i < 3; i++) {
if (!outbox[i].waiting) continue;
lastSendTry = now;
if (send(outbox[i])) outbox[i].waiting = false;
else break; // no luck: try again in 30 s
}
}
}
Try it
- For testing, set the three times a few minutes from now, for example
CHECKIN_FROM = 15 * 60 + 2(15:02),REMIND_AT = 15 * 60 + 4andALERT_AT = 15 * 60 + 5. - Upload the sketch and open the Serial Monitor at 115200 baud. A slowly blinking red LED means it doesn't know the time yet; it stops once Wi-Fi and the time are there.
- At the check-in time, the green LED starts blinking: "press me". Don't press it yet. At the reminder time the buzzer plays a tune, and at the alert time the phones get "No check-in yet".
- Now press the button briefly: a happy tune, the green LED stays on, and the phones get "checked in (later than usual)".
- Hold the button for 3 seconds: three low beeps, the red LED blinks and the phones get an urgent "needs help" message. Press briefly to send "is OK now". Put the real times back before you give it away.
How it works
The ESP32 has no battery-backed clock, so it asks a time server on the internet. configTzTime() starts that with NTP (Network Time Protocol) and sets your time zone, including summer time. From then on getLocalTime() fills a tm structure with the hour, the minute and tm_yday, the day of the year. The sketch turns the time into "minutes after midnight", so 10:30 is simply 630, and comparing times becomes comparing numbers.
When tm_yday changes, a new day has started and the check-in resets. The day of the last check-in is also saved with Preferences in the ESP32's flash memory. Without it, a power cut at noon would make the button forget the morning press and wrongly tell the family that nobody checked in.
Messages are never sent straight from the button code. They go into a small outbox first, and the end of loop() sends whatever is waiting. If the Wi-Fi is down, the message stays in the outbox and is tried again every 30 seconds. Important messages like "needs help" should never be lost just because the router was restarting.
client.setInsecure() keeps the connection encrypted but skips checking the server's certificate. That keeps the sketch short and is fine for a learning project. For a device you leave running for years, load the server's root certificate with client.setCACert() instead.
Try this next
- Send a daily "the button is working" message at noon, so the family notices if it has lost power or Wi-Fi.
- Use a big arcade button or put the button in a box with a large, soft lid.
- Add a second check-in in the evening.
- Add the PIR motion sensor: if it saw movement in the morning, send "probably fine, but didn't press the button" instead.
Troubleshooting
- The red LED blinks slowly and nothing happens: it has no time yet. Check the Wi-Fi name and password and that the network has internet access.
-
The times are an hour or more off: the time zone is wrong. Check
TIME_ZONE. - A short press sends "needs help": the press was longer than 3 seconds. Press and release quickly.
- No message on the phone: look for "ntfy answered" in the Serial Monitor. 200 means it was sent: check the topic name in the app letter by letter. -1 means no internet; 429 means too many messages in a short time.
- It never connects to Wi-Fi: check the network name and password (both are case-sensitive) and make sure the network is 2.4 GHz.