Build a daily reminder box that notices when it's opened

A printed box lights up and chimes at the times you choose. A light sensor inside notices when the lid is opened, and if nobody opens it within half an hour, a reminder goes to your phone.
  • Level:Advanced
  • Duration: 2 h 30 min
  • Ages 12+
  • Platform: ESP32

Tools for this lesson

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

Build a daily reminder box that notices when it's opened – EEMENTOR STEM™ lesson

Some things have to happen every day: take your vitamins, feed the fish, put in a brace, pack your sports kit. This capstone builds a reminder box. At the times you choose, a red light blinks and the box chimes. As soon as someone opens the lid, the light sensor inside notices: the light turns green and the reminder is done. If nobody opens it within 30 minutes, the box sends a message to a phone, for example a parent's. A web page shows today's reminders and when the box was opened.

What you'll learn

  • How to plan a day with a list of reminder times
  • How to detect a lid opening with a light sensor
  • How to escalate: first a light, then a sound, then a phone message
  • How to combine internet time, a web page and notifications in one sketch

What you need

  • ESP32 board, expansion board and USB cable (ESP32 IoT Starter Kit)
  • Photoresistor module (45-in-1 Sensor Kit)
  • Red and green LEDs and 2 resistors, 220 Ω (ESP32 IoT Starter Kit)
  • Passive buzzer (ESP32 IoT Starter Kit)
  • A phone with the free ntfy app
  • A 2.4 GHz Wi-Fi network
  • The printed parts from the 3D printing box below (reminder box), or the cardboard version described there
  • Breadboard and jumper wires (ESP32 IoT Starter Kit)

Wiring

Breadboard wiring diagram: Build a daily reminder box that notices when it's opened
Breadboard view. Check each connection against the wiring table below.
From To
Photoresistor module S ESP32 GPIO 34
Photoresistor module + and − 3V3 and GND
ESP32 GPIO 25 220 Ω resistor, then the red LED's long leg
ESP32 GPIO 26 220 Ω resistor, then the green LED's long leg
Both LED short legs GND
Passive buzzer + ESP32 GPIO 27
Passive buzzer − GND

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

Note: this box is a helpful reminder, not a medical device. Never rely on it for medicines that must be taken on time, and keep medicines out of reach of children.

Set up ntfy

Install the ntfy app and subscribe to a long, random topic name such as eementor-box-k4w9p2, as in the motion alert lesson. Topics on the free server are public, so never use a name that is easy to guess, and never send personal details through it.

Calibrate the light sensor

Upload the sketch and open the Serial Monitor at 115200 baud. It prints the light reading every few seconds. Note the value with the lid closed and open, and set OPEN_LIGHT about halfway between them. Some modules give a higher number for more light, others a lower one; set BRIGHTER_IS_HIGHER to match yours.

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 or GMT0BST,M3.5.0/1,M10.5.0 for the UK).

Put your topic into TOPIC and your reminders into REMINDERS: the hour, the minute and a short name.

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <WebServer.h>
#include <time.h>

const char* WIFI_NAME = "YOUR_WIFI_NAME";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* TIME_ZONE = "CET-1CEST,M3.5.0,M10.5.0/3";   // your time zone
const char* TOPIC = "YOUR_SECRET_TOPIC";

struct Reminder {
  int hour, minute;
  const char* name;
};
const Reminder REMINDERS[] = {{7, 30, "Morning vitamins"}, {19, 0, "Evening"}};
const int COUNT = sizeof(REMINDERS) / sizeof(Reminder);
const int MISSED_AFTER = 30;            // minutes until the phone message

const int LIGHT_PIN = 34;
const int RED_PIN = 25, GREEN_PIN = 26, BUZZER_PIN = 27;
const int OPEN_LIGHT = 1500;            // see "Calibrate the light sensor"
const bool BRIGHTER_IS_HIGHER = false;

WebServer server(80);
int doneAt[COUNT];                      // minute of the day it was done, -1 = not yet
bool messageSent[COUNT];
int lastDay = -1;
bool wasOpen = false;
unsigned long lastChime = 0;

int light() {
  int v = analogRead(LIGHT_PIN);
  return BRIGHTER_IS_HIGHER ? v : 4095 - v;
}

bool lidOpen() {
  return light() > (BRIGHTER_IS_HIGHER ? OPEN_LIGHT : 4095 - OPEN_LIGHT);
}

String clock(int minuteOfDay) {
  char text[6];
  snprintf(text, sizeof(text), "%02d:%02d", minuteOfDay / 60, minuteOfDay % 60);
  return text;
}

void notify(const String& message) {
  WiFiClientSecure client;
  client.setInsecure();
  HTTPClient http;
  http.begin(client, String("https://ntfy.sh/") + TOPIC);
  http.addHeader("Title", "Reminder box");
  http.addHeader("Tags", "alarm_clock");
  int code = http.POST(message);
  http.end();
  Serial.printf("ntfy answered %d\n", code);
}

// Which reminder is due now and not done yet? -1 if none.
int dueReminder(int now) {
  for (int i = 0; i < COUNT; i++) {
    int at = REMINDERS[i].hour * 60 + REMINDERS[i].minute;
    int next = (i + 1 < COUNT) ? REMINDERS[i + 1].hour * 60 + REMINDERS[i + 1].minute : 24 * 60;
    if (now >= at && now < next && doneAt[i] < 0) return i;
  }
  return -1;
}

void handleRoot() {
  String html = "<!doctype html><meta name=viewport content='width=device-width'><meta http-equiv=refresh content=30>"
                "<title>Reminder box</title><body style='font-family:sans-serif;max-width:420px;margin:24px auto;padding:0 16px'><h1>Today</h1><ul>";
  for (int i = 0; i < COUNT; i++) {
    html += "<li>" + clock(REMINDERS[i].hour * 60 + REMINDERS[i].minute) + " " + REMINDERS[i].name + ": ";
    html += doneAt[i] >= 0 ? "done at " + clock(doneAt[i]) : "not yet";
    html += "</li>";
  }
  html += "</ul><p>The lid is " + String(lidOpen() ? "open" : "closed") + ".</p>";
  server.send(200, "text/html", html);
}

void setup() {
  Serial.begin(115200);
  pinMode(RED_PIN, OUTPUT);
  pinMode(GREEN_PIN, OUTPUT);
  for (int i = 0; i < COUNT; i++) {
    doneAt[i] = -1;
    messageSent[i] = false;
  }
  WiFi.begin(WIFI_NAME, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) delay(500);
  Serial.print("Open http://");
  Serial.println(WiFi.localIP());
  configTzTime(TIME_ZONE, "pool.ntp.org");
  server.on("/", handleRoot);
  server.begin();
}

void loop() {
  server.handleClient();
  struct tm t;
  if (!getLocalTime(&t, 10)) return;          // wait for internet time
  int now = t.tm_hour * 60 + t.tm_min;

  if (t.tm_yday != lastDay) {                 // a new day: everything is "not yet" again
    lastDay = t.tm_yday;
    for (int i = 0; i < COUNT; i++) {
      doneAt[i] = -1;
      messageSent[i] = false;
    }
  }

  int due = dueReminder(now);
  bool open = lidOpen();
  if (open && !wasOpen) {
    Serial.printf("Lid opened at %s\n", clock(now).c_str());
    if (due >= 0) {
      doneAt[due] = now;                      // reminder done!
      tone(BUZZER_PIN, 1800, 80);
      due = -1;
    }
  }
  wasOpen = open;

  // red blinks while a reminder is due, green shows the last one was done
  digitalWrite(RED_PIN, due >= 0 && (millis() / 500) % 2);
  bool anyDone = false;
  for (int i = 0; i < COUNT; i++) if (doneAt[i] >= 0) anyDone = true;
  digitalWrite(GREEN_PIN, due < 0 && anyDone);

  if (due >= 0) {
    if (millis() - lastChime > 5UL * 60 * 1000 || lastChime == 0) {   // chime every 5 minutes
      lastChime = millis();
      tone(BUZZER_PIN, 880, 150);
      delay(200);
      tone(BUZZER_PIN, 1175, 300);
    }
    int at = REMINDERS[due].hour * 60 + REMINDERS[due].minute;
    if (now - at >= MISSED_AFTER && !messageSent[due]) {
      messageSent[due] = true;
      notify(String(REMINDERS[due].name) + ": the box hasn't been opened yet.");
    }
  } else {
    lastChime = 0;
  }

  static unsigned long lastPrint = 0;
  if (millis() - lastPrint > 3000) {
    lastPrint = millis();
    Serial.printf("Light %d (raw %d), lid %s\n", light(), analogRead(LIGHT_PIN), open ? "open" : "closed");
  }
}

How it works

The reminders are a list of times. A reminder is "due" from its time until the next reminder starts, unless it was already done. dueReminder() works this out every time loop() runs, by turning each time into a minute of the day (7:30 is 450) so that comparing times is simple maths.

The box escalates step by step: first only a blinking red light, then a chime every five minutes, and after 30 minutes a message to a phone. That's how good reminders work: gentle first, louder only when needed. Once the message is sent, messageSent stops it from being sent again.

Opening the lid is spotted as an edge: the moment the light changes from dark to bright (open && !wasOpen). Leaving the lid open doesn't count twice. At midnight a new day starts and every reminder goes back to "not yet".

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

  • Add a weekend list with different times.
  • Log every opening to Google Sheets, like in the RFID check-in lesson.
  • Let the person press a button module to snooze the reminder for ten minutes.

Troubleshooting

  • The reminder is marked done by itself: the box isn't dark enough inside, or OPEN_LIGHT is wrong. Calibrate again with the lid closed.
  • Nothing happens at the reminder time: check TIME_ZONE. The Serial Monitor shows the light readings, so the sketch is running once the time is known.
  • No phone message: check the topic, and wait the full 30 minutes (or set MISSED_AFTER to 1 for a test).
  • 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.
  • 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