Build a treasure box that tells Blynk when someone moves it

An MPU-6050 hidden in a printed treasure box feels when the box is picked up, shaken or tipped. Arm it from the Blynk app: it flashes, beeps and sends a notification to your phone.
  • 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 treasure box that tells Blynk when someone moves it – EEMENTOR STEM™ lesson

Every treasure needs a guard. This box looks ordinary, but under its false floor an MPU-6050 feels every movement. When you arm it from the Blynk app, it remembers which way is down. If someone picks the box up, shakes it or tips it, the red LED flashes, the buzzer sounds for ten seconds and your phone gets a notification, wherever you are. Disarm it from the app before you open it yourself. The app also counts how many times it was moved.

What you'll learn

  • How an accelerometer knows which way is down
  • How to measure a tilt as the angle between two directions
  • How to give yourself time to walk away before the alarm is armed
  • How to switch a device on and off from the Blynk app and keep the switch after a restart

What you need

  • ESP32 board, expansion board and USB cable (ESP32 IoT Starter Kit)
  • GY-521 MPU-6050 module (45-in-1 Sensor Kit)
  • Active buzzer module (45-in-1 Sensor Kit)
  • Red LED and 1 resistor, 220 Ω (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 (treasure box), or the cardboard version described there
  • Breadboard and jumper wires (ESP32 IoT Starter Kit)

Wiring

Breadboard wiring diagram: Build a treasure box that tells Blynk when someone moves it
Breadboard view. Check each connection against the wiring table below.
From To
MPU-6050 VCC and GND 3V3 and GND
MPU-6050 SCL ESP32 GPIO 22
MPU-6050 SDA ESP32 GPIO 21
Buzzer module S ESP32 GPIO 26
Buzzer module + and − 3V3 and GND
ESP32 GPIO 25 220 Ω resistor, then the red LED's long leg
LED short leg 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 Treasure Box (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 Armed Integer 0–1 Switch
V1 Status String – Label
V2 Moves Integer 0–1000 Value display
V3 Siren Integer 0–1 Switch

Then open the template's Events & Notifications tab, add an event with the code box_moved (name: Box moved) 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 two Switches (V0 and V3), a Label (V1) and a Value display (V2). Link each widget to its datastream.

Example screen for: Build a treasure box that tells Blynk when someone moves it
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 "Treasure Box"
#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 <Wire.h>

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

const int MPU = 0x68;
const int BUZZER_PIN = 26;
const int LED_PIN = 25;
const float SHAKE = 0.08;                  // shake in g that sets it off
const float TILT = 15;                     // tilt in degrees that sets it off
const unsigned long GRACE = 10000;         // 10 s to put the box down after arming
const unsigned long SIREN = 10000;         // alarm lasts 10 s

BlynkTimer timer;
bool armed = false;
bool siren = true;
unsigned long armAt = 0;                   // when arming really starts (0 = done)
float refX = 0, refY = 0, refZ = 1;        // "down" when it was armed
float baseline = 1.0;
int moves = 0;
unsigned long alarmUntil = 0, lastNotify = 0;

void readAccel(float& x, float& y, float& z) {
  Wire.beginTransmission(MPU);
  Wire.write(0x3B);
  Wire.endTransmission(false);
  Wire.requestFrom(MPU, 6);
  int16_t ax = Wire.read() << 8 | Wire.read();
  int16_t ay = Wire.read() << 8 | Wire.read();
  int16_t az = Wire.read() << 8 | Wire.read();
  x = ax / 16384.0;
  y = ay / 16384.0;
  z = az / 16384.0;
}

// Angle in degrees between "down" now and "down" when armed
float tilt(float x, float y, float z) {
  float dot = x * refX + y * refY + z * refZ;
  float len = sqrt(x * x + y * y + z * z) * sqrt(refX * refX + refY * refY + refZ * refZ);
  if (len == 0) return 0;
  return acos(constrain(dot / len, -1.0f, 1.0f)) * 180 / PI;
}

void status(const char* text) {
  Blynk.virtualWrite(V1, text);
  Serial.println(text);
}

void alarm() {
  if (millis() >= alarmUntil) {            // a new alarm, not the same one going on
    moves++;
    Blynk.virtualWrite(V2, moves);
    status("Moved!");
  }
  alarmUntil = millis() + SIREN;
  if (lastNotify == 0 || millis() - lastNotify > 60000) {   // at most one notification a minute
    Blynk.logEvent("box_moved", "Someone is moving your treasure box!");
    lastNotify = millis();
  }
}

void check() {                             // 50 times a second
  float x, y, z;
  readAccel(x, y, z);
  float g = sqrt(x * x + y * y + z * z);
  baseline = baseline * 0.95 + g * 0.05;
  float shake = fabs(g - baseline);

  if (armed && armAt != 0 && millis() >= armAt) {   // grace time over: remember "down"
    refX = x; refY = y; refZ = z;
    armAt = 0;
    status("Armed");
  }
  if (armed && armAt == 0 && (shake > SHAKE || tilt(x, y, z) > TILT)) alarm();

  bool on = millis() < alarmUntil && (millis() / 200) % 2;
  digitalWrite(LED_PIN, on || (armed && armAt != 0 && (millis() / 1000) % 2));   // slow blink while arming
  digitalWrite(BUZZER_PIN, on && siren);
}

BLYNK_WRITE(V0) {                          // Armed switch
  armed = param.asInt();
  alarmUntil = 0;
  if (armed) {
    armAt = millis() + GRACE;
    status("Arming in 10 s...");
  } else {
    armAt = 0;
    status("Off");
  }
}

BLYNK_WRITE(V3) {                          // Siren switch
  siren = param.asInt();
}

BLYNK_CONNECTED() {
  Blynk.syncVirtual(V0, V3);               // keep the switches after a restart
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
  pinMode(BUZZER_PIN, OUTPUT);
  Wire.begin(21, 22);
  Wire.beginTransmission(MPU);
  Wire.write(0x6B);                        // power register
  Wire.write(0);                           // wake up
  Wire.endTransmission();
  Blynk.begin(BLYNK_AUTH_TOKEN, ssid, pass);
  timer.setInterval(20L, check);
}

void loop() {
  Blynk.run();
  timer.run();
}

Try it

  1. Put the box on a table and turn on Armed in the app. The LED blinks slowly for 10 seconds while you step back, then the status says "Armed".
  2. Lift the box: the LED flashes, the buzzer sounds and your phone gets a notification. The Moves counter goes up.
  3. Turn Armed off to stop the alarm. Turn Siren off for a silent alarm that only notifies you.

How it works

Even when the box stands still, the accelerometer feels gravity pulling down. The three numbers x, y and z together are an arrow pointing down, seen from the box. When you arm it, the sketch waits 10 seconds and then saves that arrow as refX, refY and refZ.

If the box is tipped, the arrow points somewhere else. The dot product of the old and the new arrow, divided by their lengths, is the cosine of the angle between them, and acos() turns it back into degrees. More than 15° means someone has tipped it.

Lifting the box straight up hardly tips it, but it causes a short jolt. A slow average of the total acceleration, baseline, stays near 1 g, so any quick difference from it is a shake. Either sign sets off the alarm.

Blynk.syncVirtual() asks the server for the switches' last values when the ESP32 connects, so the box stays armed even after a power cut.

Try this next

  • Add the photoresistor module under the lid to notice when the box is opened, even without moving it.
  • Make the alarm louder each time: a longer siren after every move.
  • Send the angle it was tipped to the app, to see how roughly it was handled.

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.
  • It goes off by itself: the box stands on a table that wobbles, or the module is loose. Fix it with sticky tack, or raise SHAKE to 0.12.
  • The buzzer sounds all the time: some buzzer modules switch on with LOW. Swap them round: digitalWrite(BUZZER_PIN, !(on && siren)).
  • Nothing happens when you lift it: check SDA on GPIO 21, SCL on GPIO 22, and that Armed is on and the 10 s are over.
  • 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