// 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 const byte LS_MID = A1; // light sensor's middle pin: its + power byte lsSig = A0; // light sensor's S pin (A0 or A2, see findEnds()) byte lsGnd = A2; // light sensor's - pin const 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 10 const unsigned long TEST_MS = 180000UL; // test minutes after power-on const unsigned long TEST_FIRST_MS = 2000UL; // test: beep this soon after it gets dark const unsigned long TEST_REPEAT_MS = 10000UL; // test: then this often const unsigned long WAIT_MIN_MS = 120000UL; // prank: 2 to const unsigned long WAIT_MAX_MS = 300000UL; // 5 minutes between beeps const unsigned long SAMPLE_MS = 250; const unsigned long CHECK_MS = 5000; // re-check the light sensor's pins const 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 this const float LIGHT_FACTOR = 2.0; // light again: within 2x of the lit level... const float LIGHT_MAX = 0.25; // ...or brighter than this const float REF_MAX = 2.0; // the lit level never counts as dimmer than this const 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 measured const 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 it bool 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-ohms unsigned 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()); } }