Brody's Builds
← All projects
Dark Beeper Prank
ArduinoEasy10 minutesWork in progress

Dark Beeper Prank

Hide it in someone's room: once the lights go out, it beeps eight times at random every few minutes, and goes silent the moment they turn the light on to look for it.

Board: Arduino UNO (Elegoo UNO R3)

Work in progress. The code is finished and was tested on the board, but not yet with the light sensor and buzzer plugged in. Try it and see!

What you need

What it is

The classic hidden-beeper prank, but sneakier. In a lit room it does nothing at all. Once the lights go out, it waits a random 2 to 5 minutes, then goes beep beep beep beep beep beep beep beep. Then it waits again. The moment someone turns the light on to hunt for it, it goes quiet.

No wires and no breadboard: both parts plug straight into the Arduino.

How it works

  • The light sensor module is a light-dependent resistor (it lets more current through in bright light) next to a fixed 10k resistor. The program measures the voltage between them to tell how bright the room is.
  • "Dark" is relative: it remembers how bright the room normally is, and counts as dark when it's at least 4 times dimmer. So it works in bright rooms and dim rooms.
  • Either way round: different light sensor modules are built differently, so the program tests its pins to work out which end is which. It also checks whether the buzzer is the active kind (beeps by itself, white sticker) or the passive kind (needs a tone), and which way round it is.
  • Stealth: it only talks to the computer during the test minutes, so the board's TX light stays dark.

Build it

1Upload the code

Upload it first (see Getting started).

2Plug in the two parts

Unplug the USB, then:

  • Light sensor into A0, A1, A2, either way round.
  • Buzzer into 8, 9, 10, either way round.
Where they plug in
Where they plug in

3Test it

Plug the USB back in. Two tiny chirps mean it's ready. (A long buzz after the chirps means the light sensor isn't found: check it's in A0 to A2.)

The first 3 minutes are test minutes: cup your hand over the light sensor and it beeps 8 times after 2 seconds, then every 10 seconds while it's covered. After that, it switches to the real prank timing.

4Hide it

  • Run it from a USB power bank or a phone charger.
  • Put a small piece of tape over the board's green ON light, so it doesn't glow in the dark.
  • Hide it with the light sensor peeking out, so it can see whether the room light is on.

Serial Monitor

At 9600 baud: ? shows the status (light level, dark or not, which buzzer it found), and b beeps right now.

Change it

Near the top of the code: WAIT_MIN_MS and WAIT_MAX_MS are the wait between bursts (2 and 5 minutes), BEEPS is how many beeps, and BEEP_HZ is the pitch.

If it doesn't work

  • A long buzz after the chirps: the light sensor isn't plugged in properly.
  • It never beeps in the dark: the room may not be dark enough compared to how it was when it started. Start it with the light on.
  • Be nice: it's a prank, not a punishment. Tell them where it is eventually!

The code

Copy it into the Arduino app, or download the file. New to this? Start with Getting started.

DarkBeeper.ino · 399 lines Download
// Dark Beeper (prank): once the room is dark, it goes "beep, beep, beep..."// (8 beeps) at random moments every 2-5 minutes. Turn the light on and it// goes quiet straight away, so nobody can find it.//// Wiring: none. Both modules plug straight into the board://   Light sensor (photoresistor module): into A0, A1, A2, either way round//   Buzzer module, either kind: into 8, 9, 10, either way round//     (a loose 2-leg buzzer works too: 2 wires to pins 8 and 10)//   An active buzzer (white sticker) beeps by itself when powered, but only one//   way round; a passive one (no sticker) needs a tone. detectBuzzer() tells//   them apart and finds the active one's + leg.//// At power-on: two tiny chirps = ready. A long buzz after them = the light// sensor isn't found (check it's in A0-A2).// The first 3 minutes are test minutes: cover the light sensor with your hand// and it beeps after 2 s (then every 10 s while covered). After that it's the// real prank timing.//// The light sensor is a light-dependent resistor (LDR) in series with a fixed// 10k resistor, powered from its middle pin. Which end pin is S, and which of// the two parts sits on the + side, varies, so checkSensor() works both out.//// Serial (9600): "?" = status, "b" = beep now. It only prints during the test// minutes or after the PC sends something, so the TX light stays dark.#include <EEPROM.h>const byte LS_MID = A1;            // light sensor's middle pin: its + powerbyte lsSig = A0;                   // light sensor's S pin (A0 or A2, see findEnds())byte lsGnd = A2;                   // light sensor's - pinconst byte BUZ_MID = 9;            // buzzer's middle pin (power, on modules that use it)const byte BUZ_BITS = _BV(PB0) | _BV(PB2);   // buzzer ends: pins 8 and 10const unsigned long TEST_MS = 180000UL;        // test minutes after power-onconst unsigned long TEST_FIRST_MS = 2000UL;    // test: beep this soon after it gets darkconst unsigned long TEST_REPEAT_MS = 10000UL;  // test: then this oftenconst unsigned long WAIT_MIN_MS = 120000UL;    // prank: 2 toconst unsigned long WAIT_MAX_MS = 300000UL;    //        5 minutes between beepsconst unsigned long SAMPLE_MS = 250;const unsigned long CHECK_MS = 5000;           // re-check the light sensor's pinsconst byte BEEPS = 8;const unsigned int BEEP_HZ = 2800;const unsigned int BEEP_ON_MS = 120;const unsigned int BEEP_OFF_MS = 180;// Light is the LDR's resistance in units of the module's 10k resistor:// about 0.1-0.5 in a lit room, 10 or more with the lights off.const float DARK_FACTOR = 4.0;   // dark: 4x dimmer than the lit level...const float DARK_MIN = 0.5;      // ...and dimmer than thisconst float LIGHT_FACTOR = 2.0;  // light again: within 2x of the lit level...const float LIGHT_MAX = 0.25;    // ...or brighter than thisconst float REF_MAX = 2.0;       // the lit level never counts as dimmer than thisconst float REF_CREEP = 1.0005;  // per sample: the lit level follows slow dimming (dusk)const float PULLUP_K = 35.0;     // the chip's built-in pull-up in kilo-ohms (really 20-50)const int EE_SIDE = 0;           // which side the LDR is on, once measuredconst byte SIDE_TOP = 0xA1;      // LDR between + and S, 10k between S and -const byte SIDE_BOTTOM = 0xA2;   // 10k between + and S, LDR between S and -enum BuzKind : byte { BUZ_TONE, BUZ_ACTIVE_8, BUZ_ACTIVE_10 };BuzKind buzKind = BUZ_TONE;      // passive (or not found): play a tone; active: just power itbool ldrOnTop = true;bool sideMeasured = false;bool sensorFound = false;bool buzMidPowered = false;bool dark = false;bool pcTalked = false;bool testOverSaid = false;bool sounding = false;float light = 0;float litRef = -1;               // the room's lit level (-1 = not seen yet)float lastTop = 0, lastBot = 0;  // last measurements against the pull-up, kilo-ohmsunsigned long nextBurstAt = 0;unsigned long lastSample = 0;unsigned long lastCheck = 0;// ---------- Sound: Timer1 flips pins 8 and 10 together ----------ISR(TIMER1_COMPA_vect) {  PINB = BUZ_BITS;          // writing 1s to PINB toggles those pins}// Each end in turn gets the pull-up while the other is LOW. An active buzzer// lets current through only from its + leg (pulling that pin LOW) and blocks// the other way. A passive one reads the same both ways: LOW (coil) or HIGH// (piezo, or nothing plugged in).void detectBuzzer() {  pinMode(10, OUTPUT);  digitalWrite(10, LOW);  pinMode(8, INPUT_PULLUP);  delay(2);  bool a = digitalRead(8);  digitalWrite(8, LOW);     // pull-up off before it becomes an output, so no HIGH blip  pinMode(8, OUTPUT);  pinMode(10, INPUT_PULLUP);  delay(2);  bool b = digitalRead(10);  digitalWrite(10, LOW);  pinMode(10, OUTPUT);  if (!a && b) buzKind = BUZ_ACTIVE_8;  else if (a && !b) buzKind = BUZ_ACTIVE_10;  else buzKind = BUZ_TONE;  // anything unclear gets the tone, which an active buzzer also sounds on}void soundOn(unsigned int hz) {  if (buzKind != BUZ_TONE) {               // active: power its + leg, it makes its own beep    PORTB = (PORTB & ~BUZ_BITS) | (buzKind == BUZ_ACTIVE_8 ? _BV(PB0) : _BV(PB2));    sounding = true;    return;  }  unsigned int top = 1000000UL / hz - 1;   // Timer1 ticks at 2 MHz, toggling twice per cycle  noInterrupts();  OCR1A = top;  if (TCNT1 > top) TCNT1 = 0;  if (!sounding) {    PORTB = (PORTB & ~BUZ_BITS) | _BV(PB0);   // start with 8 HIGH, 10 LOW    TCNT1 = 0;    TIFR1 = _BV(OCF1A);    TIMSK1 |= _BV(OCIE1A);    sounding = true;  }  interrupts();}void soundOff() {  noInterrupts();  TIMSK1 &= ~_BV(OCIE1A);  PORTB &= ~BUZ_BITS;       // both ends LOW: nothing through the buzzer  sounding = false;  interrupts();}void blip(unsigned int hz, unsigned int ms) {  soundOn(hz);  delay(ms);  soundOff();}// ---------- Light sensor ----------bool testing() {  return millis() < TEST_MS;}bool chatty() {  return testing() || pcTalked;}void say(const __FlashStringHelper *msg) {  if (chatty()) Serial.println(msg);}void powerSensor() {  pinMode(LS_MID, OUTPUT);  digitalWrite(LS_MID, HIGH);  pinMode(lsSig, INPUT);  pinMode(lsGnd, OUTPUT);  digitalWrite(lsGnd, LOW);}int readPin(byte pin) {  analogRead(pin);          // first reading after switching pins can be off  delayMicroseconds(100);  return analogRead(pin);}// The S pin is the one where a voltage shows up. Read the other way round,// the input is the - pin, which sits at 0 because S (driven LOW then) holds it.void findEnds() {  pinMode(LS_MID, OUTPUT);  digitalWrite(LS_MID, HIGH);  pinMode(A0, INPUT);  pinMode(A2, OUTPUT);  digitalWrite(A2, LOW);  int a = readPin(A0);  pinMode(A2, INPUT);  pinMode(A0, OUTPUT);  digitalWrite(A0, LOW);  int b = readPin(A2);  pinMode(A0, INPUT);  if (a >= b + 3) {    lsSig = A0;    lsGnd = A2;  } else if (b >= a + 3) {    lsSig = A2;    lsGnd = A0;  }                         // too close to tell (very dark or very bright): keep the last answer}// Resistance between S and lowPin (driven LOW), measured against S's// built-in pull-up while floatPin is left unconnected. In kilo-ohms.float measureAgainstPullup(byte lowPin, byte floatPin) {  pinMode(floatPin, INPUT);  pinMode(lowPin, OUTPUT);  digitalWrite(lowPin, LOW);  pinMode(lsSig, INPUT_PULLUP);  delay(2);  int v = readPin(lsSig);  pinMode(lsSig, INPUT);  if (v >= 1020) return 9999;  return PULLUP_K * v / (1023 - v);}// Finds the S pin, whether the sensor is there at all, and which side the LDR// is on: the fixed 10k always measures 6-20k against the pull-up, so if one// side reads like that and the other clearly doesn't, the other is the LDR.void checkSensor() {  findEnds();  lastTop = measureAgainstPullup(LS_MID, lsGnd);   // between + (middle) and S  lastBot = measureAgainstPullup(lsGnd, LS_MID);   // between S and -  powerSensor();  bool wasFound = sensorFound;  sensorFound = !(lastTop > 300 && lastBot > 300);  if (sensorFound && !wasFound) say(F("Light sensor found"));  if (!sensorFound && wasFound) say(F("Light sensor not found - is it in A0, A1, A2?"));  bool topFixed = lastTop > 6 && lastTop < 20;  bool botFixed = lastBot > 6 && lastBot < 20;  bool topOdd = lastTop < 4 || lastTop > 30;  bool botOdd = lastBot < 4 || lastBot > 30;  bool top = ldrOnTop;  if (botFixed && topOdd) top = true;  else if (topFixed && botOdd) top = false;  else return;  sideMeasured = true;  if (top != ldrOnTop) litRef = -1;   // the old readings meant the opposite  ldrOnTop = top;  EEPROM.update(EE_SIDE, top ? SIDE_TOP : SIDE_BOTTOM);}float readLight() {  long sum = 0;  analogRead(lsSig);  for (byte i = 0; i < 8; i++) sum += analogRead(lsSig);  float s = sum / 8.0;  if (s < 0.5) s = 0.5;  if (s > 1022.5) s = 1022.5;  return ldrOnTop ? (1023 - s) / s : s / (1023 - s);}unsigned long randomWait() {  return random(WAIT_MIN_MS, WAIT_MAX_MS + 1);}void setDark(bool on) {  dark = on;  if (on) {    nextBurstAt = millis() + (testing() ? TEST_FIRST_MS : randomWait());    say(F("Dark - beeps coming"));  } else {    say(F("Light - quiet"));  }}void updateDark(float x) {  light = x;  if (!sensorFound) {    if (dark) setDark(false);    return;  }  if (litRef < 0) litRef = min(x, REF_MAX);  if (!dark) {    if (x < litRef) litRef = x;    else litRef = min(min(litRef * REF_CREEP, x), REF_MAX);    if (x > DARK_FACTOR * litRef && x > DARK_MIN) setDark(true);  } else if (x < LIGHT_FACTOR * litRef || x < LIGHT_MAX) {    if (x < litRef) litRef = x;    setDark(false);  }}// 8 beeps. Unless forced, it stops the moment the light comes on.void beepBurst(bool force) {  say(F("Beep x8"));  detectBuzzer();  for (byte i = 0; i < BEEPS; i++) {    blip(BEEP_HZ, BEEP_ON_MS);    delay(BEEP_OFF_MS);    updateDark(readLight());    if (!dark && !force) break;  }}// ---------- Serial ----------void report() {  Serial.print(F("Light sensor: "));  if (!sensorFound) {    Serial.println(F("NOT found - is it in A0, A1, A2?"));  } else {    Serial.print(F("found, S on "));    Serial.print(lsSig == A0 ? F("A0") : F("A2"));    Serial.print(F(", LDR on the "));    Serial.print(ldrOnTop ? F("+ side") : F("- side"));    Serial.println(sideMeasured ? F(" (measured)") : F(" (guess)"));  }  Serial.print(F("  + side "));  Serial.print(lastTop, 1);  Serial.print(F("k, - side "));  Serial.print(lastBot, 1);  Serial.println(F("k (against the pull-up)"));  Serial.print(F("Light: "));  Serial.print(light, 2);  Serial.print(F("  lit level "));  Serial.print(litRef, 2);  Serial.println(dark ? F("  -> DARK") : F("  -> light"));  detectBuzzer();  Serial.print(F("Buzzer: "));  if (buzKind == BUZ_ACTIVE_8) Serial.print(F("active (white sticker), + on pin 8"));  else if (buzKind == BUZ_ACTIVE_10) Serial.print(F("active (white sticker), + on pin 10"));  else Serial.print(F("passive, or not plugged in"));  Serial.println(buzMidPowered ? F("; middle pin powered") : F("; middle pin not used"));  if (testing()) {    Serial.print(F("Test minutes: "));    Serial.print((TEST_MS - millis()) / 1000);    Serial.println(F(" s left"));  } else {    Serial.println(F("Prank timing (2-5 min)"));  }  if (dark) {    Serial.print(F("Next beeps in "));    long left = (long)(nextBurstAt - millis());    Serial.print(left > 0 ? left / 1000 : 0);    Serial.println(F(" s"));  }}void handleSerial() {  while (Serial.available()) {    char c = Serial.read();    pcTalked = true;    if (c == '?') report();    else if (c == 'b') beepBurst(true);  }}// ---------- Main ----------void setup() {  Serial.begin(9600);  pinMode(8, OUTPUT);  pinMode(10, OUTPUT);  PORTB &= ~BUZ_BITS;  // Power the buzzer's middle pin, unless it's joined to an end (then it's not a power pin).  pinMode(BUZ_MID, INPUT_PULLUP);  delay(5);  buzMidPowered = digitalRead(BUZ_MID);  if (buzMidPowered) {    pinMode(BUZ_MID, OUTPUT);    digitalWrite(BUZ_MID, HIGH);  } else {    pinMode(BUZ_MID, INPUT);  }  TCCR1A = 0;  TCCR1B = _BV(WGM12) | _BV(CS11);  // Timer1: CTC mode, 16 MHz / 8 = 2 MHz  if (EEPROM.read(EE_SIDE) == SIDE_BOTTOM) ldrOnTop = false;  randomSeed(analogRead(A3) * 31UL + analogRead(A4) * 7UL + micros());  checkSensor();  lastCheck = millis();  Serial.println(F("Dark beeper ready. Test minutes: cover the light sensor and it beeps."));  detectBuzzer();  blip(BEEP_HZ, 40);         // two tiny chirps: it's alive  delay(100);  blip(BEEP_HZ, 40);  if (!sensorFound) {    delay(300);    blip(400, 600);          // long (and low, on a passive buzzer) buzz: light sensor not found  }  report();}void loop() {  unsigned long now = millis();  handleSerial();  if (!testOverSaid && !testing()) {    testOverSaid = true;    say(F("Test minutes over - prank timing now (2-5 min)"));  }  if (now - lastCheck >= CHECK_MS) {    lastCheck = now;    checkSensor();  }  if (now - lastSample >= SAMPLE_MS) {    lastSample = now;    updateDark(readLight());  }  if (dark && (long)(millis() - nextBurstAt) >= 0) {    beepBurst(false);    nextBurstAt = millis() + (testing() ? TEST_REPEAT_MS : randomWait());  }}

Related projects