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
}
Attachments
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.