John Bradnam
Published © GPL3+

R2D2 Alarm Clock & Timer

A 3D printed clock in the form of the Star Wars R2D2 droid. Featuring Internet time, Alarm, Time format and Timer with prerecorded sounds

IntermediateFull instructions provided3 days1,058
R2D2 Alarm Clock & Timer

Things used in this project

Hardware components

ESP32 Development Kit
30 pin variant
×1
DF Player Mini
×1
Linear Regulator (7805)
Linear Regulator (7805)
×1
TM1637 0.56in 4 Digital 7 Segment Display Module
×1
DC Power socket
×1
LED (generic)
LED (generic)
1 x 5mm Red, 1 x 5mm White, 1 x 3mm Blue
×1
Rotary Encoder with Push-Button
Rotary Encoder with Push-Button
Spline shaft
×1
40mm 3W 8 ohm speaker
×1
Jumper Wires
DIYables Jumper Wires
20cm
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Custom parts and enclosures

STL Files

Files for 3D printing

Schematics

Schematic

PCB

Eagle files

Schematic and PCB in Eagle format

Code

R2D2_clock_timer_V5.ino

Arduino
/*----------------------------------------------
 * R2D2 Clock
 * 
 * Board: Node32s
 * 
 * 2023-06-12 V5 jlb
 *  Added Alarm functionality
 *  Added Time format functionality
 *  Added code to flash colon every second
 *--------------------------------------------*/  

#include "WiFiManager.h" //https://github.com/tzapu/WiFiManager WiFi Configuration Magic
#include "ezTime.h" // Time Library - get your POSIX Timezone 'string' to replace TZ/GMT if needed, below, here: https://support.cyberdata.net/index.php?/Knowledgebase/Article/View/438/10/posix-timezone-strings
#include "SevenSegmentExtended.h"
#include "SevenSegmentTM1637.h"
#include "SevenSegmentFun.h" // For Text readouts
#include "EspSoftwareSerial.h"
#include "DFMiniMp3.h"
#include <EEPROM.h>

//========================USEFUL VARIABLES=============================
const char* ssid     = "PUT_CLOCKS_OWN_WIFI_NAME_HERE";   //  R2D2 Access point network name (to connect to R2D2, to change/add your WiFi details)
const char* password = "SET_A_PASSWORD"; //  R2D2 Access point password
uint16_t notification_volume = 25;  //  Speaker volume
int Display_backlight = 45;   //  4 Digit Display brightness when operating normally (outside of initialisation)
int BlueLED_brightness = 25;  //  Two blue LED's (when connected to ESP32 pin 14, instead of VCC)
const char* ntpServer = "pool.ntp.org"; // Your chosen TimeServer
//const String timezone_posix_string = "BST0GMT,M3.2.0/2:00:00,M11.1.0/2:00:00";  // Insert your own POSIX Timezone string here. See link above for info.
const String timezone_posix_string = "AEST-9AEDT,M3.2.0/2:00:00,M11.1.0/2:00:00";  //Sydney, Australia.
//=====================================================================
// Avoid changing below here...

// Call WiFi Manager
WiFiManager wifiManager;

// Call a Timezone
Timezone TZ;

#define CLK 25
#define DT 26
#define SW 27
#define RED_LED 17
#define WHITE_LED 16
#define BLUE_LED 14  // 14 corresponds to GPIO14

enum SETUP { SHOW_TIME, TIMER, TIME_FORMAT, ALARM_ONOFF, ALARM_HOUR, ALARM_MIN };
SETUP setupMode = SHOW_TIME;
enum SHOW {HOURS,MINUTES};

//Uncomment to reset EEPROM data
//#define RESET_EEPROM
#define EEPROM_ADDRESS 0
#define EEPROM_MAGIC 0x0DAD0BAD
typedef struct {
  uint32_t magic;
  bool timeFormat;
  bool alarmOn;
  int8_t alarmHour;
  int8_t alarmMinute;
} EEPROM_DATA;

EEPROM_DATA EepromData;         //Current EEPROM settings

bool set24 = false;             //Time format being set
bool setAO = false;             //Alarm on/off being set
int8_t setAH = 0;               //Alarm Hour being set
int8_t setAM = 0;               //Alarm Minute being set
bool alarmCancelled = false;    //Set true if user cancelled alarm
bool colon = true;              //Colon to flash every second

float counter = 0;
int currentStateCLK;
int lastStateCLK;
String currentDir = "";
unsigned long lastButtonPress = 0;

// timer
int timer_secs = 0;
int timer_mins = 0;
float inc_red_led = 0;

// the number of the Blue LED's pin
const int BlueChannel = 1;
// setting PWM properties
const int freq = 5000;
const int resolution = 8;

class Mp3Notify
{
  public:
    static void PrintlnSourceAction(DfMp3_PlaySources source, const char* action)
    {
      if (source & DfMp3_PlaySources_Sd)
      {
        Serial.print("SD Card, ");
      }
      if (source & DfMp3_PlaySources_Usb)
      {
        Serial.print("USB Disk, ");
      }
      if (source & DfMp3_PlaySources_Flash)
      {
        Serial.print("Flash, ");
      }
      Serial.println(action);
    }
    static void OnError(uint16_t errorCode)
    {
      // see DfMp3_Error for code meaning
      Serial.println();
      Serial.print("Com Error ");
      Serial.println(errorCode);
    }
    static void OnPlayFinished(DfMp3_PlaySources source, uint16_t track)
    {
      Serial.print("Play finished for #");
      Serial.println(track);
    }
    static void OnPlaySourceOnline(DfMp3_PlaySources source)
    {
      PrintlnSourceAction(source, "online");
    }
    static void OnPlaySourceInserted(DfMp3_PlaySources source)
    {
      PrintlnSourceAction(source, "inserted");
    }
    static void OnPlaySourceRemoved(DfMp3_PlaySources source)
    {
      PrintlnSourceAction(source, "removed");
    }
};

SevenSegmentExtended      blue1(21, 22); // CLK, DIO
SevenSegmentFun           words(21, 22); // CLK, DIO
SoftwareSerial            secondarySerial(18, 19); // TX, RX
DFMiniMp3<SoftwareSerial, Mp3Notify> mp3(secondarySerial);

void configModeCallback (WiFiManager *myWiFiManager) 
{
  Serial.println("Entered WiFi Manager config mode..");
  Serial.println(WiFi.softAPIP());

  Serial.println(myWiFiManager->getConfigPortalSSID());
}

//flag for saving data
bool shouldSaveConfig = false;

//callback notifying us of the need to save config
void saveConfigCallback () 
{
  Serial.println("Should save config");
  shouldSaveConfig = true;
}

void setup() 
{
  words.setBacklight(80);
  words.begin();
  words.scrollingText("R2D2 START", 2);
  
  pinMode(CLK, INPUT);
  pinMode(DT, INPUT);
  pinMode(SW, INPUT_PULLUP);
  pinMode(RED_LED, OUTPUT);
  pinMode(WHITE_LED, OUTPUT);
  pinMode(BLUE_LED, OUTPUT);
  // configure LED PWM functionalities
  ledcSetup(0, 5000, 8);
  // attach the channel to the GPIO to be controlled
  ledcAttachPin(RED_LED, 0);

  Serial.begin(115200);
    
  //set custom ip for portal
  wifiManager.setAPStaticIPConfig(IPAddress(192, 168, 2, 1), IPAddress(192, 168, 2, 1), IPAddress(255, 255, 255, 0));
  WiFiManagerParameter custom_text("<p>Welcome to R2D2: Please connect to WiFi using this portal</p>");
  wifiManager.addParameter(&custom_text);
  wifiManager.setAPCallback(configModeCallback);
  //first parameter is name of access point, second is the password
  wifiManager.autoConnect(ssid, password);
  words.scrollingText("CONNECT TO R2D2 WIFI", 4);
  wifiManager.setSaveConfigCallback(saveConfigCallback);
  wifiManager.setConfigPortalTimeout(600); // Hold for 10 mins if unable to connect.

  blue1.setBacklight(Display_backlight);
  blue1.begin();
  mp3.begin();
  mp3.setVolume(notification_volume);
  uint16_t count = mp3.getTotalTrackCount(DfMp3_PlaySource_Sd);
  Serial.print("files ");
  Serial.println(count);

  ledcAttachPin(BLUE_LED, BlueChannel);
  ledcWrite(BlueChannel, 200);

  Serial.print("IP assigned by DHCP is ");
  Serial.println(WiFi.localIP());
  ledcWrite(BlueChannel, 0);
  delay (2000);
  ledcWrite(BlueChannel, 200);

  Serial.println("\n Should be connected: Waiting for time sync");
  waitForSync();

  // Read the initial state of CLK
  lastStateCLK = digitalRead(CLK);
  TZ.setPosix(timezone_posix_string);
  Serial.println("UTC: " + UTC.dateTime());
  Serial.println("Local Time: " + TZ.dateTime("G") + ":" + TZ.dateTime("i"));
  ledcWrite(BlueChannel, BlueLED_brightness);

  //Read the alarm status
  EEPROM.begin(sizeof(EEPROM_DATA));
  readEepromData();

  showTime();
}

//----------------------------------------------------------------------
// Main program loop
//----------------------------------------------------------------------

void loop() 
{
  if (minuteChanged()) 
  {
    colon = !colon;
    showTime();
    
    //Test if alarm gone off
    Serial.println("Time " + String(TZ.hour()) + ":" + String(TZ.minute()));
    Serial.println("alarmOn=" + String(EepromData.alarmOn) + ", alarmCancelled=" + String(alarmCancelled));
    Serial.println("alarmTime=" + String(EepromData.alarmHour) + ":" + String(EepromData.alarmMinute));
    
    if (alarmCancelled && (TZ.hour() != EepromData.alarmHour || TZ.minute() != EepromData.alarmMinute))
    {
      alarmCancelled = false;
    }
    else if (EepromData.alarmOn && !alarmCancelled && TZ.hour() == EepromData.alarmHour && TZ.minute() == EepromData.alarmMinute)
    {
      Serial.println("*** ALARM ON ****");
      turnOnAlarm();
      alarmCancelled = true;
    }
  }
  else if (secondChanged())
  {
    colon = !colon;
    showTime();
  }

  ledcWrite(0, inc_red_led);
  digitalWrite(WHITE_LED, LOW);

  //If we detect LOW signal, button is pressed
  if (digitalRead(SW) == LOW) 
  {
    //if 50ms have passed since last LOW pulse, it means that the
    //button has been pressed, released and pressed again
    if (millis() - lastButtonPress > 50) 
    {
      Serial.println("Timer Button pressed!");
      bool repeatLoop = true;
      while (repeatLoop)
      {
        setupMode = (setupMode == ALARM_MIN) ? SHOW_TIME : (SETUP)((int)setupMode + 1);
        switch (setupMode)
        {
          case SHOW_TIME: 
            showTime();
            repeatLoop = false; 
            break;
            
          case TIMER: 
            repeatLoop = setupTimer();
            if (!repeatLoop)
            {
              setupMode = SHOW_TIME;
              showTime();
            }
            break;
  
          case TIME_FORMAT:
            //Store settings so we can detect if they changed
            set24 = EepromData.timeFormat;
            setAO = EepromData.alarmOn;
            setAH = EepromData.alarmHour;
            setAM = EepromData.alarmMinute;
            setTimeFormat();
            break;
          
          case ALARM_ONOFF: 
            setAlarmState(); break;
            break;
            
          case ALARM_HOUR: 
            setAlarmHour(); 
            break;
            
          case ALARM_MIN: 
            setAlarmMinute(); 
            //Test if alarm status has changed
            if (setAO != EepromData.alarmOn || setAH != EepromData.alarmHour || setAM != EepromData.alarmMinute || set24 != EepromData.timeFormat)
            {
              //Write changes back to EEPROM
              EepromData.timeFormat = set24;
              EepromData.alarmOn = setAO;
              EepromData.alarmHour = setAH;
              EepromData.alarmMinute = setAM;
              writeEepromData();
            }
            break;
        }
      }
    }

    // Remember last button press event
    lastButtonPress = millis();
  }

  inc_red_led = (inc_red_led <= 254) ? inc_red_led + 0.1 : 0;
  delay(1);
}

//----------------------------------------------------------------------
// Display the current time
//  24hr format has leading zeros, 12hr format doesn't
//----------------------------------------------------------------------

void showTime()
{
  if (EepromData.timeFormat)
  {
    showPartialTime(HOURS, TZ.dateTime("H").toInt(), true, true, colon);
  }
  else
  {
    showPartialTime(HOURS, TZ.dateTime("g").toInt(), false, true, colon);
  }
  showPartialTime(MINUTES, TZ.dateTime("i").toInt(), true, true, colon);
}

//---------------------------------------------------------------------
// Write two time digits to display
//  s - SHOW constant
//  num - (0 to 99) 
//  leadingZeros - true to have leading zeros
//  on - true to show digit, false to show blank
//  c - true to show colon
//---------------------------------------------------------------------

void showPartialTime(SHOW s, int num, bool leadingZeros, bool on, bool c)
{
  num = max(min(num, 99), 0);
  int b = (s == HOURS) ? 0 : 2;
  for (int i = 0; i < 2; i++)
  {
    if (on && (num > 0 || i == 0 || leadingZeros))
    {
      blue1.setColonOn(c);
      blue1.printRaw(blue1.encode((int16_t)(num % 10)), b + 1-i);
    }
    else
    {
      blue1.printRaw((uint8_t)TM1637_CHAR_SPACE, b + 1-i);
    }
    num = num / 10;
  }
}

//----------------------------------------------------------------------
// Show/Change the time format
//  - Rotary  Encoder changes alarm state
//----------------------------------------------------------------------

void setTimeFormat()
{
  Serial.println("In time format!");
  showTimeFormat();
  digitalWrite(RED_LED, HIGH);
  digitalWrite(WHITE_LED, HIGH);
  mp3.playMp3FolderTrack(2);
  delay(500);
  while (digitalRead(SW) == HIGH) 
  {
    switch (readRotaryEncoder())
    {
      case 1: set24 = true; showTimeFormat(); break;
      case -1: set24 = false; showTimeFormat(); break;
      default: break;
    }
    delay(1);
  }
  Serial.println("Exit time format!");
}

//----------------------------------------------------------------------
// Show the current time format
//----------------------------------------------------------------------

void showTimeFormat()
{
  blue1.printRaw(TM1637_CHAR_T, 0);
  blue1.printRaw(TM1637_CHAR_f, 1);
  if (set24)
  {
    blue1.printRaw(TM1637_CHAR_2, 2);
    blue1.printRaw(TM1637_CHAR_4, 3);
  }
  else
  {
    blue1.printRaw(TM1637_CHAR_1, 2);
    blue1.printRaw(TM1637_CHAR_2, 3);
  }
}

//----------------------------------------------------------------------
// Show/Change alarm state
//  - Rotary  Encoder changes alarm state
//----------------------------------------------------------------------

void setAlarmState()
{
  Serial.println("In alarm state!");
  showAlarmState();
  digitalWrite(RED_LED, HIGH);
  digitalWrite(WHITE_LED, HIGH);
  mp3.playMp3FolderTrack(2);
  delay(500);
  while (digitalRead(SW) == HIGH) 
  {
    switch (readRotaryEncoder())
    {
      case 1: setAO = true; showAlarmState(); break;
      case -1: setAO = false; showAlarmState(); break;
      default: break;
    }
    delay(1);
  }
  Serial.println("Exit alarm State!");
}

//----------------------------------------------------------------------
// Show the current alarm state
//----------------------------------------------------------------------

void showAlarmState()
{
  blue1.printRaw(TM1637_CHAR_A, 0);
  blue1.printRaw((uint8_t)TM1637_CHAR_SPACE, 1);
  blue1.printRaw(TM1637_CHAR_o, 2);
  blue1.printRaw((setAO) ? TM1637_CHAR_n : TM1637_CHAR_f, 3);
}

//----------------------------------------------------------------------
// Show/Change alarm hours
//----------------------------------------------------------------------

void setAlarmHour()
{
  Serial.println("In alarm hour!");
  showAlarmHour();
  digitalWrite(RED_LED, HIGH);
  digitalWrite(WHITE_LED, HIGH);
  mp3.playMp3FolderTrack(2);
  delay(500);
  while (digitalRead(SW) == HIGH) 
  {
    switch (readRotaryEncoder())
    {
      case 1: setAH = (setAH + 1) % 24; showAlarmHour(); break;
      case -1: setAH = (setAH + 23) % 24; showAlarmHour(); break;
      default: break;
    }
    delay(1);
  }
  Serial.println("Exit alarm hour!");
}

//----------------------------------------------------------------------
// Show the current alarm hour
//----------------------------------------------------------------------

void showAlarmHour()
{
  uint8_t buffer[2];
  
  blue1.printRaw(TM1637_CHAR_A, 0);
  blue1.printRaw(TM1637_CHAR_h, 1);
  buffer[0] = blue1.encode(int16_t(setAH / 10));
  buffer[1] = blue1.encode(int16_t(setAH % 10));
  blue1.printRaw(buffer, 2, 2);
}

//----------------------------------------------------------------------
// Clock is in ALARM_MIN mode
//----------------------------------------------------------------------

void setAlarmMinute()
{
  Serial.println("In alarm minute!");
  showAlarmMinute();
  digitalWrite(RED_LED, HIGH);
  digitalWrite(WHITE_LED, HIGH);
  mp3.playMp3FolderTrack(2);
  delay(500);
  while (digitalRead(SW) == HIGH) 
  {
    switch (readRotaryEncoder())
    {
      case 1: setAM = (setAM + 1) % 60; showAlarmMinute(); break;
      case -1: setAM = (setAM + 59) % 60; showAlarmMinute(); break;
      default: break;
    }
    delay(1);
  }
  Serial.println("Exit alarm minute!");
}

//----------------------------------------------------------------------
// Show the current alarm hour
//----------------------------------------------------------------------

void showAlarmMinute()
{
  uint8_t buffer[2];
  
  blue1.printRaw(TM1637_CHAR_A, 0);
  blue1.printRaw(TM1637_CHAR_n, 1);
  buffer[0] = blue1.encode(int16_t(setAM / 10));
  buffer[1] = blue1.encode(int16_t(setAM % 10));
  blue1.printRaw(buffer, 2, 2);
}

//---------------------------------------------------------------
// Sound the alarm
//  This will play for one minute or until the rotary switch is pressed
//---------------------------------------------------------------

void turnOnAlarm()
{
  int counter = 0;
  while (digitalRead(SW) == HIGH && !minuteChanged()) 
  {
    if (counter < 1)
    {
      playFolderTrackWithLights(1);
    }
    else if (counter < 5)
    {
      playFolderTrackWithLights(2);
    }
    else if (counter < 7)
    {
      playFolderTrackWithLights(4);
    }
    else if (counter < 8)
    {
      playFolderTrackWithLights(3);
    }
    else
    {
      playFolderTrackWithLights(5);
      playFolderTrackWithLights(7);
    }
    counter++;
    
    waitMilliseconds(200);
  }
  mp3.stop();
  digitalWrite(WHITE_LED, LOW);
  while (digitalRead(SW) == LOW)
  {
    //Wait until button released so it doesn't trigger setup
  }
}

//----------------------------------------------------------------------
// Handle timer
//  - Rotary Encoder changes seconds
//  - Switch starts timer or changes mode if timer is at zero
//  Returns true if timer not set
//----------------------------------------------------------------------

bool setupTimer() 
{
  Serial.println("In Timer State!");
  blue1.printTime(88, 88, false);
  digitalWrite(RED_LED, HIGH);
  digitalWrite(WHITE_LED, HIGH);
  mp3.playMp3FolderTrack(2);
  delay(500);
  while (digitalRead(SW) == HIGH) 
  {
    counter += float(readRotaryEncoder() * 10);
    if (counter < 0)
    {
      counter = 0;
    }

    timer_mins = counter / 60;
    timer_secs =  ((counter / 60) - timer_mins) * 60;
    blue1.printTime(timer_mins, timer_secs, false);

    delay(1);
  }
  bool timerNotUsed = (counter == 0);
  if (counter > 0)
  {
    Countdown(counter);
  }
  Serial.println("Exit Timer State!");
  return timerNotUsed;
}

//----------------------------------------------------------------------
// Handle timer
//  - Countdown the time and play sound when done
//----------------------------------------------------------------------

void Countdown (float timer_counter) 
{
  mp3.playMp3FolderTrack(8);
  delay(1000);
  
  while (digitalRead(SW) == HIGH)
  {
    for (int i = 10 ; i > 0; i--) 
    {
      timer_counter = timer_counter - 0.1;
      timer_mins = timer_counter / 60;
      timer_secs =  ((timer_counter / 60) - timer_mins) * 60;
      blue1.printTime(timer_mins, timer_secs, false);
      delay(100);
      digitalWrite(RED_LED, LOW);
    }

    if (timer_counter <= 0) 
    {
      playFolderTrackWithLights(3);
      playFolderTrackWithLights(5);
      playFolderTrackWithLights(7);
      break;
    };
  }
  while (digitalRead(SW) == LOW)
  {
    //Wait until button released so it doesn't trigger setup
  }
  counter = 0;
}

//----------------------------------------------------------------------
// Play a MP3 track and flash the white light
//----------------------------------------------------------------------

void playFolderTrackWithLights(int track)
{      
  mp3.playMp3FolderTrack(track);
  
  for ( int i = 0 ; i < 9 ; i++) 
  {
    ledcWrite(0, 255);
    waitMilliseconds(random(10, 150));
    digitalWrite(WHITE_LED, HIGH);
    waitMilliseconds(random(10, 150));
    ledcWrite(0, 0);
    waitMilliseconds(random(10, 150));
    digitalWrite(WHITE_LED, LOW);
    waitMilliseconds(random(10, 150));
  }
}

//----------------------------------------------------------------------
// Read the rotary encoder
//  - Returns 0, -1 or 1
//----------------------------------------------------------------------

int readRotaryEncoder()
{
  int result = 0;
  // Read the current state of CLK
  currentStateCLK = digitalRead(CLK);
  // If last and current state of CLK are different, then pulse occurred
  // React to only 1 state change to avoid double count
  if (currentStateCLK != lastStateCLK  && currentStateCLK == 1) 
  {

    // If the DT state is different than the CLK state then
    // the encoder is rotating CCW so decrement
    if (digitalRead(DT) != currentStateCLK) 
    {
      result = -1;
      currentDir = "CCW";
    } 
    else 
    {
      // Encoder is rotating CW so increment
      result = 1;
      currentDir = "CW";
    }

    Serial.print("Direction: ");
    Serial.print(currentDir);
    Serial.print(" | Counter: ");
    Serial.println(counter);
  }

  // Remember last CLK state
  lastStateCLK = currentStateCLK;
  return result;  
}

//----------------------------------------------------------------------
// Wait a number of milliseconds while updating MP3 player
//  msWait - time to wait in mS
//----------------------------------------------------------------------

void waitMilliseconds(uint16_t msWait) 
{
  uint32_t start = millis();

  while ((millis() - start) < msWait && digitalRead(SW) == HIGH)
  {
    // calling mp3.loop() periodically allows for notifications
    // to be handled without interrupts
    mp3.loop();
    delay(1);
  }
}

//---------------------------------------------------------------
// Write the EepromData structure to EEPROM
//---------------------------------------------------------------

void writeEepromData()
{
  //This function uses EEPROM.update() to perform the write, so does not rewrites the value if it didn't change.
  EEPROM.put(EEPROM_ADDRESS,EepromData);
  EEPROM.commit();
}

//---------------------------------------------------------------
//Read the EepromData structure from EEPROM, initialise if necessary
//---------------------------------------------------------------

void readEepromData()
{
  //Eprom
  EEPROM.get(EEPROM_ADDRESS,EepromData);
  #ifndef RESET_EEPROM
  if (EepromData.magic != EEPROM_MAGIC)
  #endif
  {
    EepromData.magic = EEPROM_MAGIC;
    EepromData.timeFormat = false;
    EepromData.alarmOn = false;
    EepromData.alarmHour = 6;
    EepromData.alarmMinute = 0;
    writeEepromData();
    EEPROM.commit();
  }
}

EspSoftwareSerial Library

Arduino
No preview (download only).

ezTime.zip

Arduino
ezTime library
No preview (download only).

Credits

John Bradnam

John Bradnam

141 projects • 167 followers
Thanks to jeje95 and viaraix.

Comments