This alarm clock won’t stop beeping until you unscramble a word.

Demo Video

Using the keypad to enter letters

Close-up of the LCD screen

An early prototype, with a keypad and potentiometer. I initially wanted to do a sort of “bomb defusal” alarm clock that involved cutting wires, but chose instead to go with the keypad as a more elegant reusable puzzle.

Here I began to solder the components together. At this point I had added a speaker and real time clock. I chose to switch the potentiometer for a detented rotary encoder, which was more intuitive for navigating menus and included a button.

Detail of the connection between the real time clock and the bit of protoboard I used for power rails.

Here I began mounting pieces into the box. I originally wanted to lasercut an acrylic box for the project but I spent more time than anticipated troubleshooting the real time clock, speaker, and rotary encoder, and ended up having to go for a simpler enclosure.

The internals of the box. I mounted everything to the top so that I could still open and close the box to change the battery.

Response to in-class crit

  • “Maybe could laser cut something for a more permanent solution.”
  • “Very annoying and effective alarm that will probably wake you up better since you need to actually think for this puzzle which uses brain power.”
  • “It might be too hard when you wake up. I think you’ll end up unplugging it rather than solving the puzzle.”

As anticipated, a lot of the responses were that the enclosure was unrefined and a lasercut box would be more polished. One thing I didn’t consider was how to prevent methods of cheating, such as unplugging the Arduino or destroying the speaker. A sturdy box that’s screwed shut would be a good solution. You could still change the battery when needed, but it would take you several minutes to unscrew and open the box. People did like the concept though and said that the alarm and puzzle itself appeared very functional.

Self critique

I’m happy with how the project came out functionally. The menus and the puzzle are user-friendly and easy to interact with. My main goal was to have a hard puzzle that would be truly challenging and force me to have to think, which I satisfied. I wish I budgeted more time toward making a better/smaller/sturdier enclosure.

What I learned

Getting several components to interact took probably 3 times as long as I expected. I knew that all the components I was using had free libaries that I could use, but actually digging through the documentation and testing the behavior to get what I wanted was not always straightforward. There was lots of troubleshooting for each component, and some places where I had to check multiple places to see where my issue was. I got hung up for a while because one of my wires came out of the Arduino and my LCD display spontaneously stopped working.

Next steps

The project is functionally all there, but as I said before, I’d build a new enclosure for it that could only be taken apart with a screwdriver and lots of patience.

#include <Encoder.h>
#include  <Wire.h>
#include  <LiquidCrystal_I2C.h>
#include  <Keypad.h>
#include "DS3231.h"

const int NUM_WORDS = 32;

String words[] = {
  "change",
  "again",
  "animal",
  "begin",
  "began",
  "black",
  "machine",
  "field",
  "final",
  "ocean",
  "behind",
  "decide",
  "common",
  "check",
  "among",
  "dance",
  "engine",
  "million",
  "middle",
  "child",
  "climb",
  "office",
  "clean",
  "blood",
  "decimal",
  "imagine",
  "chief",
  "clock",
  "block",
  "chance",
  "claim",
  "chick"
};

const byte ROWS = 4;
const byte COLS = 4;

char keys[ROWS][COLS] = {
  {'a','b','c','d'},
  {'e','f','g','h'},
  {'i','j','k','l'},
  {'m','n','o','p'}
};

//byte rowPins[ROWS] = {5, 4, 3, 2}; //connect to the row pinouts of the keypad
//byte colPins[COLS] = {12, 8, 7, 6}; //connect to the column pinouts of the keypad
byte rowPins[ROWS] = {2, 3, 4, 5};
byte colPins[COLS] = {6, 7, 8, 12};

Keypad keypad = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS );

LiquidCrystal_I2C lcd(0x27,16,2);  // set the LCD address to 0x27 for a 16 chars and 2 line display

RTClib RTC;
Encoder knob(11, 9);

String currentWord, currentScramble;
String enteredSoFar;

String shuffle(String s)
{
  String result = String(s);
  for (int i = s.length() - 1; i > 0; i--) {
    int j = random(0, i + 1);
    char temp = result[i];
    result.setCharAt(i, result[j]);
    result.setCharAt(j, temp);
  }

  Serial.print("Original string: "); Serial.println(s);
  Serial.print("Shuffled string: "); Serial.println(result);

  return result;
}

void resetWord()
{
  currentWord = String(words[random(0, NUM_WORDS - 1)]);
  currentScramble = shuffle(currentWord);

  enteredSoFar = String("");
}

void setup()
{
  lcd.init();
  lcd.backlight();
  Serial.begin(9600);

  randomSeed(analogRead(0));

  pinMode(13, INPUT);

  resetWord();
}

enum State {
  REGULAR_CLOCK,
  SET_ALARM_H,
  SET_ALARM_M,
  CANCEL_ALARM,
  ALARM_WAITING,
  ALARM_RINGING,
  CONGRATS
};

State currentState = REGULAR_CLOCK;
int alarmH = 0;
int alarmM = 0;

long lastUpdateTime = 0;
const int REFRESH_INTERVAL = 500;

long setAlarmTime = 0;
const int CANCEL_INTERVAL = 5000;

bool oldButtonState = true;
bool newButtonState;

bool cleanButtonPress() {
  newButtonState = digitalRead(13);
  bool returnVal = !newButtonState && oldButtonState;
  oldButtonState = newButtonState;
  return returnVal;
}

void loop()
{
  switch(currentState) {
    case REGULAR_CLOCK:
      {
      DateTime now = RTC.now();
        
      if (millis() - lastUpdateTime > REFRESH_INTERVAL) {
        lcd.clear();
        lcd.print(now.hour(), DEC);
        lcd.print(":");
        byte m = now.minute();
        lcd.print(m < 10 ? "0" : "");
        lcd.print(m, DEC);
        lastUpdateTime = millis();
      }

      if (cleanButtonPress()) {
        currentState = SET_ALARM_H;
      }
      }
      break;
    case SET_ALARM_H:
      alarmH = (knob.read() / 4) % 24;
      while(alarmH < 0) alarmH += 24;

      if (millis() - lastUpdateTime > 100) {
        lcd.clear();
        lcd.print("Hour:");
        lcd.print(alarmH);
        lcd.print(":");
        lcd.print(alarmM < 10 ? "0" : "");
        lcd.print(alarmM);
        lastUpdateTime = millis();
      }

      if (cleanButtonPress()) {
        currentState = SET_ALARM_M;
      }
    
      break;
    case SET_ALARM_M:
      alarmM = (knob.read() / 4) % 60;
      while(alarmM < 0) alarmM += 60;

      if (millis() - lastUpdateTime > 100) {
        lcd.clear();
        lcd.print("Minute:");
        lcd.print(alarmH);
        lcd.print(":");
        lcd.print(alarmM < 10 ? "0" : "");
        lcd.print(alarmM);
        lastUpdateTime = millis();
      }

      if (cleanButtonPress()) {
        currentState = CANCEL_ALARM;
        setAlarmTime = millis();
      }
    
      break;
    case CANCEL_ALARM:
      if (millis() - lastUpdateTime > REFRESH_INTERVAL) {
        lcd.clear();
        lcd.print("Set:");
        lcd.print(alarmH);
        lcd.print(":");
        lcd.print(alarmM < 10 ? "0" : "");
        lcd.print(alarmM);
        lcd.setCursor(0, 1);
        lcd.print("Cancel? (");
        lcd.print(((CANCEL_INTERVAL - (millis() - setAlarmTime)) / 1000) + 1);
        lcd.print(")");
        lastUpdateTime = millis();
      }

      if (cleanButtonPress()) {
        currentState = REGULAR_CLOCK;
      }

      if (millis() - setAlarmTime >= CANCEL_INTERVAL) {
        currentState = ALARM_WAITING;
      }
      
      break;
    case ALARM_WAITING:
      {
      DateTime now = RTC.now();
        
      if (millis() - lastUpdateTime > REFRESH_INTERVAL) {
        lcd.clear();
        lcd.print(now.hour(), DEC);
        lcd.print(":");
        byte m = now.minute();
        lcd.print(m < 10 ? "0" : "");
        lcd.print(m, DEC);
        lastUpdateTime = millis();

        lcd.setCursor(0, 1);
        lcd.print("Alarm at ");
        lcd.print(alarmH);
        lcd.print(":");
        lcd.print(alarmM < 10 ? "0" : "");
        lcd.print(alarmM);
        lastUpdateTime = millis();
      }

      if (now.hour() == alarmH && now.minute() == alarmM) {
        currentState = ALARM_RINGING;
      }
      
      }
      break;
    case ALARM_RINGING:
      {
      if (millis() % 1000 < 500) tone(10, 600);
      else noTone(10);
      
      int i = (knob.read() / 4) % currentScramble.length();
      while (i < 0) i += currentScramble.length();

      if (millis() - lastUpdateTime > REFRESH_INTERVAL) {
        lcd.clear();
        lcd.print(String(i + 1) + ": " + currentScramble[i] + " " + enteredSoFar);
        lastUpdateTime = millis();
      }
    
      char key = keypad.getKey();
    
      if (key) {
        enteredSoFar.concat(key);
        if (enteredSoFar != currentWord.substring(0, enteredSoFar.length())) {
          lcd.clear();
          lcd.print("WROOOOOOONG");
          enteredSoFar = String("");
          delay(700);
        } else if (enteredSoFar == currentWord) {
          resetWord();
          currentState = CONGRATS;
        }
      }
      }
      break;
    case CONGRATS:
      lcd.clear();
      lcd.print("Hooray!");
      lcd.setCursor(0,1);
      lcd.print("You're awake");
      delay(2000);
      currentState = REGULAR_CLOCK;
      break;
  }
}