Introduction: WakeUpMan: a Cassette Player Alarm Clock

Some time ago, I made a digital alarm clock that turns on my Sony Cassette player when the alarm goes off. It uses a relay to control the DC cable going into the Walkman, to automatically turn it on at the right time. As a challenge, it doesn't use an RTC for the timing, instead using only the arduino millis() function. Here's how I made this.



Supplies

Alarm Clock circuit:

  • Arduino Uno R3
  • 3 Push buttons
  • 16x2 I2C LCD
  • 5V Relay
  • 9V AC/DC Adapter and/or 6AA Battery Pack
  • 2 Battery clips

Cassette Player: (depends on the model you're using. In my case, a Sony TCM-50DV):

  • Sony TCM-50DV Cassette-Corder
  • Universal AC/DC Adapter and/or 2AA Battery Pack
  • 1 Battery clip
  • Cool tape to play (example: Wham! Greatest Hits)

Enclosure:

  • 500x500x3 millimeter sheet of MDF

Tools:

  • USB A-B Cable (only for uploading the code to the arduino)
  • Copper Wiring
  • Soldering Iron
  • Soldering Tin
  • Lasercutter
  • Hot glue gun

Step 1: Designing

At first I wanted to used a servo to physically press a button on my walkman. After lots of tests, I finally concluded that the servo just didn't have the power to push the button. As the buttons of the walkman are entirely mechanical, i also couldn't simulate a button press with electronics.

So the only way for me to automatically turn on the walkman, was by having the button always pressed, but cutting the power when it shouldn't be playing. This can be done using a relay, which can be controlled by the arduino to switch a different circuit. I decided to use the DC input of the walkman for this, as i didn't want to tamper with the batteries.

So now the plan was basically to make an alarm clock that can turn on a DC cable when it goes off. This actually makes the design much more flexible, as I can now use this on all sorts of different devices.

Step 2: Writing the Code

First, I had to decide what parts my program needed, and whether to write them myself or use pre-existing libraries. There are already great and easy to use libraries for using buttons and LCD's. However, I still wanted to challenge myself, so I decided to write my own time and menu functionality. I chose not to use an RTC (Real-Time Clock) module, to see how accurate the arduino could be on its own.


Components:

Button Debouncing: ezButton Library

LCD Communication: LiquidCrystal_I2C Library

Time: Custom (no RTC)

Menu: Custom


First I wrote the Time component in the Arduino IDE, to be able to test it on real hardware. Then I wrote the Menu in a seperate Wokwi project, to iterate quickly on different hardware layouts virtually. When they were both done, i could merge them together in one big file.

Feel free to use all or parts of this code, as long as you properly link back to this page.

// WakeUpMan Arduino Code v0.1.1
// Written By David de Gelder, March 2024


// ## Libraries


#include <LiquidCrystal_I2C.h>
#include <ezButton.h>


// ## Time Declarations


typedef struct Time {
public:
  int hours = 0;
  int minutes = 0;
  int seconds = 0;
  int elapsed_mills = 0;
  int current_mills = 0;
} Time;


Time t;     // Current Time, automatically changing
Time alarm; // Time to sound alarm, static unless manually changed


// Used when setting a time
Time tempTime;
Time* editing_time;
int* editing_var;

// ## UI declarations
// This menu system allows for nested menus with simple 3-button navigation between them.


typedef struct MenuItem {
public:
  String name; // title of this menu item, displayed on the first line.
  int length; // amounts of options
  String options[10]; //
} MenuItem;


MenuItem mainMenu = { "Menu", 4,
                      "Set Time",
                      "Set Alarm",
                      "Demo Alarm",
                      "Close" };

// Example of possible nested menu, currently unused:
/*
MenuItem alarmMenu = { "Set Alarm", 6,
                       "Everyday",
                       "Tomorrow",
                       "Workweek",
                       "Weekend",
                       "Per Day",
                       "Back" };
*/


MenuItem currentMenu = mainMenu;
int currentOption = 0; // index to the MenuItem.options array


// Possible states of the display
enum State { IN_MENU,
             INPUT_TIME,
             SHOW_TIME,
             DISPLAY_OFF,
             SOUND_ALARM};


int state = SHOW_TIME;


// Button events
bool buttonLeft, buttonRight, buttonEnter, anyButtonDown, anyButtonPressed;

// ## Hardware Connections ##


ezButton button1(2), button2(3), button3(4);
LiquidCrystal_I2C lcd(0x3F, 16, 2);
int relayPin = 12;

// Runs once at device startup/reset
void setup() {
  // Open Serial
  Serial.begin(9600);  // For debugging


  // Set default time and alarm
  t = setTime(t, "07:30");
  alarm = setTime(alarm, "07:30");


  // Ready buttons
  button1.setDebounceTime(50);
  button2.setDebounceTime(50);
  button3.setDebounceTime(50);


  // Ready LCD
  lcd.init();
  lcd.backlight();
  lcd.clear();


  // Ready Relay
  pinMode(relayPin, OUTPUT);
}


// Core system loop
void loop() {
  // Update current time
  updateTime();


  // Sound alarm when needed
  if (checkAlarm(&alarm)) {
    soundAlarm();
  }
 
  // Check for button presses
  processInput();


  // Handle input and display to LCD
  display();
}


// ## Display Functions


// Display correct screen to LCD based on current UI state.
void display() {
  switch (state) {
    case IN_MENU:
      navigateMenu();
      break;
    case INPUT_TIME:
      inputTime();
      break;
    case SHOW_TIME:
      displayClock();
      break;
    case SOUND_ALARM:
      displayAlarm();
      break;
  }
}


void navigateMenu() {
  // Scroll Left/right through options
  if (buttonLeft) {
    currentOption -= 1;
  }
  if (buttonRight) {
    currentOption += 1;
  }


  // Cycles options when reaching the end/beginning
  currentOption = mod(currentOption, currentMenu.length);


  // Only display when changes are made, to avoid flashing
  if (anyButtonDown) {
    displayMenu();
  }


  // Enter submenu
  if (buttonEnter) {
    menuEnter(currentMenu.options[currentOption]);
  }
}


void displayMenu() {
  // Print menu title to first line
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(currentMenu.name);


  // Print navigation arrows to 2nd line
  lcd.setCursor(0, 1);
  lcd.print("<              >");


  // Print current selected options to 2nd line
  lcd.setCursor(2, 1);
  lcd.print(currentMenu.options[currentOption]);
}


void menuEnter(String option) {
  lcd.clear();
  currentOption = 0; // Select first option again


  // Navigate through the menu based on the selected option.


  if (option == "Back") {
    currentMenu = mainMenu;
  }


  if (option == "Set Alarm") {
    state = INPUT_TIME;
    startInputTime(&alarm);
  }


  if (option == "Set Time") {
    state = INPUT_TIME;
    startInputTime(&t);
  }


  if (option == "Close") {
    lcd.clear();
    state = SHOW_TIME;
  }


  if (option == "Demo Alarm"){ // Set time to 5 seconds before the alarm goes off
    t = setTime(t, formatTime(alarm, "%02d:%02d"));
    t.minutes -= 1;
    t.seconds = 55;
    menuEnter("Close");
  }
}


void startInputTime(Time* edit) {
  editing_time = edit; //pointer to Time variable to edit
  tempTime = *editing_time;
  editing_var = &tempTime.hours; // Edit hours first


  lcd.clear();


  // First line
  lcd.setCursor(0, 0);
  lcd.print("Set Time:");


  // Second line
  lcd.setCursor(5, 1);
  lcd.print(formatTime(tempTime, "%02d:%02d"));


  // Show cursor where user can edit the variable
  lcd.setCursor(6, 1);
  lcd.cursor();
}


void inputTime() {
  // Increase/Decrease current variable
  if (buttonLeft) {
    *editing_var -= 1;
  }


  if (buttonRight) {
    *editing_var += 1;
  }


  // Keep the numbers within possible range
  tempTime.hours = mod(tempTime.hours, 24);
  tempTime.minutes = mod(tempTime.minutes, 60);


  // Update display on changes
  if (anyButtonPressed) {
    lcd.setCursor(5, 1);
    lcd.print(formatTime(tempTime, "%02d:%02d"));
  }


  // When User presses enter
  if (buttonEnter) {
    // first time, move from hours to minutes
    if (editing_var == &tempTime.hours) {
      editing_var = &tempTime.minutes;
    // second time, apply changes
    } else if (editing_var == &tempTime.minutes) {
      *editing_time = setTime(*editing_time, formatTime(tempTime, "%02d:%02d"));


      // Go back to showing time
      state = SHOW_TIME;
      lcd.noCursor();
      lcd.clear();
    }
  }


  // Move cursor to currently edited variable
  if (editing_var == &tempTime.hours) { lcd.setCursor(6, 1); }
  if (editing_var == &tempTime.minutes) { lcd.setCursor(9, 1); }
}


void displayClock() {
  // First Line: Current Time
  lcd.setCursor(0,0);
  lcd.print("===");


  lcd.setCursor(13,0);
  lcd.print("===");


  lcd.setCursor(4, 0);
  lcd.print(formatTime(t, "%02d:%02d:%02d"));


  // Second Line: Message based on time
  lcd.setCursor(0, 1);
  lcd.print(getMessage(t));


  // Turn off screen
  if (buttonLeft){
    lcd.noBacklight();
  }


  // Turn on Screen
  if (buttonRight){
    lcd.backlight();
  }


  // Go into Menu
  if (buttonEnter) {
    lcd.backlight();
    state = IN_MENU;
    menuEnter("Back");
  }
}


void displayAlarm() {
  // First line: Current Time
  lcd.setCursor(0,0);
  lcd.print("===");


  lcd.setCursor(13,0);
  lcd.print("===");


  lcd.setCursor(4, 0);
  lcd.print(formatTime(t, "%02d:%02d:%02d"));


  // Second line: WAKE UP!
  lcd.setCursor(0, 1);
  lcd.print("--Wake Up Man!--");


  // Turn off alarm when any button pressed
  if (anyButtonPressed)
  {
    state = SHOW_TIME;
    digitalWrite(relayPin, LOW);
  }


}


// ## Time Functions


void updateTime() {
  bool queuePrintTime = false;


  // Get elapsed mills since last update
  t.elapsed_mills = millis() - t.current_mills;


  if (t.elapsed_mills >= 1000) {
    // Extra logic to handle potential delays
    t.seconds += floor(t.elapsed_mills / 1000);
    t.current_mills = millis() - t.elapsed_mills % 1000;


    // queue printTime() after handling minutes & hours
    queuePrintTime = true;
  }


  if (t.seconds >= 60) {
    t.seconds -= 60;
    t.minutes++;
  }


  if (t.minutes == 60) {
    t.minutes = 0;
    t.hours++;
  }


  if (t.hours == 24) {
    t.hours = 0;
    //next day
  }


  if (queuePrintTime) { printTime(t); }
}


Time setTime(Time oldTime, String string) {
  // Set a time using a string formatted HH:MM:SS
  Time newTime;
  newTime = oldTime;


  newTime.hours = string.substring(0, 2).toInt();
  newTime.minutes = string.substring(3, 5).toInt();
  newTime.seconds = 0;


  return newTime;
}


String formatTime(Time time, const char* format) {
  char buffer[10];
  sprintf(buffer, format, time.hours, time.minutes, time.seconds);
  return buffer;
}


void printTime(Time time) {
  Serial.println(formatTime(t,"%02d:%02d:%02d"));
}


bool checkAlarm(Time* a) {
  return t.hours == a->hours && t.minutes == a->minutes && t.seconds == 0;
}


void soundAlarm() {
  // Activate Relay
  digitalWrite(relayPin, HIGH);
  state = SOUND_ALARM;
}


String getMessage(Time time){
  // Custom messages based on time of day


  if (time.hours >= 6 & time.hours < 12){
    return "Good Morning! :)";
  }


  if (time.hours >= 12 & time.hours < 18){
    return "Have a nice day!";
  }


  if (time.hours >= 18 & time.hours < 22){
    return "Good Evening!";
  }


   if (time.hours >= 22 | time.hours < 6){
    return "Good Night! ZzzZzz";
  }

}


// ## Utility Functions


void processInput() {
  button1.loop();
  button2.loop();
  button3.loop();


  buttonLeft = button1.isPressed();
  buttonRight = button2.isPressed();
  buttonEnter = button3.isPressed();


  anyButtonDown = ((button1.getStateRaw() == LOW) | (button2.getStateRaw() == LOW) | (button3.getStateRaw() == LOW));
  anyButtonPressed = (buttonLeft | buttonRight | buttonEnter);
}


int mod(int a, int b) {
  // Inbuilt % operator doesn't handle negative numbers nicely, this does.
  return (a % b + b) % b; // don't ask me why this works
}


Step 3: Testing the Timing

To see if this software timing stayed in sync, it let it run for about an hour. I printed every second the arduino ticked, along with the real time from the computer. You can see that at first there's a delay of -0.85 seconds. This is because it's hard and unnecessary to sync the time to the millisecond at the start. Over the course of 48 minutes and 10 seconds, the delay increased to +1.66 seconds. That's a 2,51 seconds drift over 2890 seconds elapsed, which is 3,13 seconds per hour. That means the clock drifts by about a minute every day. It's not very accurate for a clock, but still pretty impressive for just software. If you set the time correctly every week, you won't notice much of this drift.

Of course, for better accuracy you could use an RTC (real-time clock) to keep track of the time, but this was more fun and educational. After all, this project was meant to teach me about Arduino and electronics.

Step 4: Prepping the Cables

Finding the right power connectors for old hardware can be a pain, especially when it comes to barrel jacks. There are so many combinations of inner/outer diameters, voltages and polarities that you can't just look at a connector and directly see what cable you need. That's why I bought a universal AC/DC adapter with many different connectors and voltages for this project. Luckily, one of the connectors fit my Sony TCM-50DV.

It also turned out that all of these connector bits used the same barrel jack size as the Arduino, which came with some added advantages. For instance, it came with a handy connector where I could clamp in my own cables. I used this as an input port on the back of my box. This also meant that I could use my Arduino power adapter with all the extension bits from the universal adapter.

I took apart the cheap barrel jack cable, cutting it in half and keeping only the part with the barrel connector. I led the positive wire through COM and NO ports of the relay, and then connected this wire and the original ground wire with a battery clip with reversed poles. This meant I now had a barrel jack cable that I could clip onto a battery, and turn on or off using the relay.


Step 5: Putting It Together

Now it's time to put it all together on a breadboard and see if it works! Luckily it did :)

Step 6: Making the Enclosure

I used Makercase to generate a basic 140x100x70 mm box to lasercut. I then measured the buttons, LCD and barrel jack input and added cutouts for them. I also added two interior walls with trough-holes for wiring, to mount hardware to and keep everything organized. Finally I added some decoration in the form of text, in a Walkman-logo font I found.

I glued the outside panels together with hot glue, leaving gaps for the inner walls to slot in. I left the top panel loose, for easy access.

Step 7: Soldering

I glued all the buttons to a small wooden strip i had left over, to keep them in place. I then soldered all the ground connections together and to the buttons, as well as each signal wire to its button. I also soldered every 5V positive wire together. I'm still not great at soldering, but it all managed to keep together.

Step 8: Assemble in the Case

At last I could finish everything up by putting it in the case. I used double sided tape to attach the arduino to a divider, and attached the relay on the other side of it. Then I placed another divider to keep the power input and batteries separated from the rest. Finally I could close the case, attach the adapter to my barrel jack, turn on the power of my Walkman, and wait for the alarm to go off.