WiFi Connected Clock with Westminster Chimes
revised d. bodnar  10-06-2017

Introduction
I have built a number of clocks over the years starting with a binary coded decimal kit that looked something like this when completed

http://www.jerryselectronics.com/diykits/kbc00100/kbc00100.htm   I recall building that in the 1970's.

More recently I have build a number of digital clocks that synchronize with the time servers that can be accessed over the Internet.  Several of them sit on top of my PC's monitor. 

The larger one is binary and the other two use an OLED display.  All three get their time from the Internet.  https://en.wikipedia.org/wiki/Network_Time_Protocol

see: https://www.instructables.com/id/Simplest-ESP8266-Local-Time-Internet-Clock-With-OL/

My latest digital clock is based on this video and the code found there.  https://www.youtube.com/watch?v=YUtqLjs-alo  Many thanks to John Rogers who posted the video.

It works very well but I wanted something that also gave me auditory announcements of the time.  The classic way of doing this is with Westminster Chimes.  Such chimes are found on most grandfather clocks as well as mantle clocks.

While the Arduino that I am using for this project is capable of producing rudimentary sounds that could resemble Westminster Chimes I opted to use an MP3 player that sounded out recordings of the chimes.  This gives much better audio quality and flexibility to use whatever sound you choose to play.

One of my clocks is shown here.  It continuously displays the time and sound the chimes at 15, 30, 45 minutes after the hour and on the hour where it chimes the number of the hour.

 

The MP3 Player & Sounds
I have used the MP3 player that is used in this project many times before.  It is the DFPlayer, an inexpensive, high quality MP3 player that stores sounds on a micro-SD card.  For more information on this device see: 
http://www.trainelectronics.com/Arduino/MP3Sound/  This page also suggests where it can be purchased.

Files for the MP3 player reside on a micro SD card in a folder named    mp3.

There are 6 sound files for the Westminster Chimes.

0001.mp3 - the chimes for the quarter hour
0002.mp3 - the chimes for the half hour
0003.mp3 - the chimes for the three-quarter hour
0004.mp3 - the chimes for the hour
0005.mp3 - the hour chime
0006.mp3 - the hour chime with a long trailing sound at the end (used as the last chime)

The sound files that I used are available here:   mp3.zip
A shorter version is here:
mp3-shorter.zip  To make these files I changed the Tempo in a sound editing program called Audacity.

 
Parts
Only a few parts are needed. 

The processor is a Wemos ESP8266 that can be ordered from BangGood and Amazon.

The DFPlayer MP3 player can be found at Amazon and BangGood as well.

The LED Matrix display can be found at Amazon and BangGood.
 

The display's visibility can be improved dramatically by adding a red filter.  I use a red, self-adhesive film that can be found on eBay, but red acrylic will work, too.

In addition you will need a 1K resistor, a 10K (a 50K or 100K will work, too) and an 8 ohm speaker.

The unit can be powered from a USB cable to the Wemos D1 or you can use a voltage regulator circuit that supplies 5 volts.

Schematic

As you can see from the schematic the wiring is very simple. 

Prototype
This photo shows my prototype that was built on a small circuit board.  The Wemos D1 processor with WiFi is on the right side of the board.  It is connected to a USB cable to supply power to the circuit.  The DFPlayer is to its left and contains a micro SD card with the sound files.  The speaker is connected to the red/black wire that goes off to the left of the photo.  Be sure to use an 8 ohm speaker with the DFPlayer.  If you only have a 4 ohm speaker put a 3 to 5 ohm resistor in series with the speaker.

The 4 module display has been placed behind a piece of red acrylic to make it more visible. 

This close-up shows the potentiometer (to the far right) that can be used to adjust the brightness of the display.

Most of the wiring is on the back of the board.  It follows the schematic.  The only part that is not shown on the schematic is the black capacitor that was placed on the power input connections to filter the DC power when I used a noisy external power supply.  It is not necessary if you power with USB through the Wemos D1.  The 1K resistor is under the white tubing.

Power can come from  a 5 volt USB power supply, USB cable, or batteries (3.7 to 5 volts will work)

Code - based on YouTube video from John Rogers & DFPlayer code found on my web page here:  http://www.trainelectronics.com/Arduino/MP3Sound/TalkingTemperature/

Code version - ESP_LEDMatrix_clock-w-POT-chimes-v1-12Hour-v2.3-startupDing-esp

The libraries used in this sketch can be found here:
ArduinoJson
ESP9266WiFi
DFPlayer_Mini_Mp3

Select "Clone or Download" and download the ZIP version.  Install in the Arduino IDE with Sketch/Include Library/add ZIP Library
///    FOUND HERE  https://www.youtube.com/watch?v=YUtqLjs-alo
// Thanks John Rogers !

#include "Arduino.h"
#include <ArduinoJson.h>
#include <ESP8266WiFi.h> //ESP8266 Core WiFi Library (you most likely already have this in your sketch)
#include <DFPlayer_Mini_Mp3.h>
WiFiClient client;
int i;
int day, month, year, dayOfWeek;
int summerTime = 0;
String date;
byte flag1 = 0; // quater hour
byte flag2 = 0; // half hour
byte flag3 = 0; // three quarter hour
byte flag4 = 0; // hour
#define NUM_MAX 4
//// pins: D0-16,D1-5,D2-4,D3-0,D4-2,D5-14,D6-12,D7-13,D8-15,RX-3,TX-1 ////
// for NodeMCU 1.0
#define DIN_PIN 13  // D7
#define CS_PIN  0  // D3
#define CLK_PIN 14  // D5

#define buusyPin 4  //d2
#define SerOut 16 //d0
int bsy = 0;
int z = 0;
#include "max7219.h"
#include "fonts.h"
#define HOSTNAME "ESP8266-OTA-"
int val = 0;       // variable to store the value coming from the sensor
// =======================================================================
// CHANGE YOUR CONFIG HERE:
// =======================================================================
const char* ssid     = "ssidssid";     // SSID of local network
const char* password = "password";   // Password on network
#define CO2_TX D4 //changed from D1 as it crashed with D1
#define CO2_RX D6  // note D6 is not used, just a place holder

void setup()
{
  Serial.begin(9600);
  mp3_set_serial (Serial);  //set Serial for DFPlayer-mini mp3 module
  mp3_reset();
  delay (400);
  mp3_set_volume (15);          // 15 is low for unpowered speaker
  delay (400);
  initMAX7219();
  sendCmdAll(CMD_SHUTDOWN, 1);
  Serial.print("Connecting WiFi ");
  WiFi.begin(ssid, password);
  printStringWithShift("Connecting", 15);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("MyIP: "); Serial.println(WiFi.localIP());
  printStringWithShift((String("  MyIP: ") + WiFi.localIP().toString()).c_str(), 15);
  delay(1500);
  mp3_play(5); // for testing
  dlayPrint();
}

// =============================DEFINE VARS==============================
#define MAX_DIGITS 4  // was 20
byte dig[MAX_DIGITS] = {0};
byte digold[MAX_DIGITS] = {0};
byte digtrans[MAX_DIGITS] = {0};
int updCnt = 0;
int dots = 0;
long dotTime = 0;
long clkTime = 0;
int dx = 0;
int dy = 0;
byte del = 0;
int h, m, s;
float utcOffset = -4;
long localEpoc = 0;
long localMillisAtUpdate = 0;

// =======================================================================
void loop() {
  val = analogRead(A0);    // read the value from the sensor
  val = val / 115 ;
  //Serial.println(val);
  sendCmdAll(CMD_INTENSITY, val); // adjust brightness based on pot setting
  if (updCnt <= 0) { // every 10 scrolls, ~450s=7.5m
    updCnt = 60;
    Serial.println("Getting data ...");
    printStringWithShift("   Setting Time...", 15);
    getTime();
    Serial.println("Data loaded");
    clkTime = millis();
  }
  if (millis() - clkTime > 60000 && !del && dots) { // clock for 30s, then scrolls for about 30s
    updCnt--;
    clkTime = millis();
  }
  if (millis() - dotTime > 500) {
    dotTime = millis();
    dots = !dots;
  }
  updateTime();
  if (m == 15 || m == 30 || m == 45 || m == 0) {
    showSimpleClock();
  }
  else showAnimClock();
  if (m == 15 & flag1 == 0) {
    Serial.print("h= ");
    Serial.print(h);
    Serial.println(" found 15");
    mp3_play(1);
    dlayPrint();
    flag1 = 1;
    flag2 = 0;
    flag3 = 0;
    flag4 = 0;
  }
  if (m == 30 & flag2 == 0) {
    Serial.print("h= ");
    Serial.print(h);
    Serial.println(" found 30");
    mp3_play(2);
    dlayPrint();
    flag1 = 0;
    flag2 = 1;
    flag3 = 0;
    flag4 = 0;
  }
  if (m == 45 & flag3 == 0) {
    Serial.print("h= ");
    Serial.print(h);
    Serial.println(" found 45");
    mp3_play(3);
    dlayPrint();
    flag1 = 0;
    flag2 = 0;
    flag3 = 1;
    flag4 = 0;
  }
  if (m == 0 & flag4 == 0) {
    Serial.print("h= ");
    Serial.print(h);
    Serial.println(" found 00");
    mp3_play(4);
    dlayPrint();
    flag1 = 0;
    flag2 = 0;
    flag3 = 0;
    flag4 = 1;
    if (h >= 2) {
      if (h >= 13) {
        h = h - 12;
      }
      if (h == 0) {
        h = 12; // at midnight do 12 chimes
      }
      for (i = 1; i <= h - 1; i++) {
        mp3_play(5); // 5 is chime without end tail
        dlayPrint();
      }
    }
    mp3_play(6); // 6 is chime with long tail sound
  }
}

// routine to stay here till busy pin goes low once then goes high after speech item completes
void dlayPrint()
{
  int bsyflag = 0;
  Serial.println(" ");
  Serial.print("busypin ");
  for ( z = 0; z <= 1000; z++) {
    if (millis() - dotTime > 500) {
      dotTime = millis();
      dots = !dots;
    }
    bsy = digitalRead(buusyPin);
    Serial.print(bsy);
    delay(20);
    if (bsyflag == 1 && bsy == 1) {
      break;
    }
    if (bsy == 0) {
      bsyflag = 1;
    }
  }
  Serial.println(" ");
  Serial.println("done");
}

// =======================================================================
void showSimpleClock()
{
  dx = dy = 0;
  clr();
  showDigit(h / 10,  0, dig6x8);
  showDigit(h % 10,  8, dig6x8);
  showDigit(m / 10, 17, dig6x8);
  showDigit(m % 10, 25, dig6x8);
  showDigit(s / 10, 34, dig6x8);
  showDigit(s % 10, 42, dig6x8);
  setCol(15, dots ? B00100100 : 0);
  setCol(32, dots ? B00100100 : 0);
  refreshAll();
}

// =======================================================================

void showAnimClock()
{
  byte digPos[6] = {0, 8, 17, 25, 34, 42};
  int digHt = 12;
  int num = 6;
  int i;
  if (flag4 <= 0) {
    //    m = 0; // for testing
  }
  if (del == 0) {
    del = digHt;
    for (i = 0; i < num; i++) digold[i] = dig[i];
    dig[0] = h / 10 ? h / 10 : 10;
    dig[1] = h % 10;
    dig[2] = m / 10;
    dig[3] = m % 10;
    dig[4] = s / 10;
    dig[5] = s % 10;
    for (i = 0; i < num; i++)  digtrans[i] = (dig[i] == digold[i]) ? 0 : digHt;
  } else
    del--;

  clr();
  for (i = 0; i < num; i++) {
    if (digtrans[i] == 0) {
      dy = 0;
      showDigit(dig[i], digPos[i], dig6x8);
    } else {
      dy = digHt - digtrans[i];
      showDigit(digold[i], digPos[i], dig6x8);
      dy = -digtrans[i];
      showDigit(dig[i], digPos[i], dig6x8);
      digtrans[i]--;
    }
  }
  dy = 0;
  setCol(15, dots ? B00100100 : 0);
  setCol(32, dots ? B00100100 : 0);
  updateTime();
  refreshAll();
  delay(30);
}

// =======================================================================

void showDigit(char ch, int col, const uint8_t *data)
{
  if (dy < -8 | dy > 8) return;
  int len = pgm_read_byte(data);
  int w = pgm_read_byte(data + 1 + ch * len);
  col += dx;
  for (int i = 0; i < w; i++)
    if (col + i >= 0 && col + i < 8 * NUM_MAX) {
      byte v = pgm_read_byte(data + 1 + ch * len + 1 + i);
      if (!dy) scr[col + i] = v; else scr[col + i] |= dy > 0 ? v >> dy : v << -dy;
    }
}

// =======================================================================

void setCol(int col, byte v)
{
  if (dy < -8 | dy > 8) return;
  col += dx;
  if (col >= 0 && col < 8 * NUM_MAX)
      if (!dy) scr[col] = v; else scr[col] |= dy > 0 ? v >> dy : v << -dy;
}

// =======================================================================

int showChar(char ch, const uint8_t *data)
{
  int len = pgm_read_byte(data);
  int i, w = pgm_read_byte(data + 1 + ch * len);
  for (i = 0; i < w; i++)
    scr[NUM_MAX * 8 + i] = pgm_read_byte(data + 1 + ch * len + 1 + i);
  scr[NUM_MAX * 8 + i] = 0;
  return w;
}

// =======================================================================
int dualChar = 0;

unsigned char convertPolish(unsigned char _c)
{
  unsigned char c = _c;
  if (c == 196 || c == 197 || c == 195) {
    dualChar = c;
    return 0;
  }
  if (dualChar) {
    switch (_c) {
      case 133: c = 1 + '~'; break; // 'ą'
      case 135: c = 2 + '~'; break; // 'ć'
      case 153: c = 3 + '~'; break; // 'ę'
      case 130: c = 4 + '~'; break; // 'ł'
      case 132: c = dualChar == 197 ? 5 + '~' : 10 + '~'; break; // 'ń' and 'Ą'
      case 179: c = 6 + '~'; break; // ''
      case 155: c = 7 + '~'; break; // 'ś'
      case 186: c = 8 + '~'; break; // 'ź'
      case 188: c = 9 + '~'; break; // 'ż'
      //case 132: c = 10+'~'; break; // 'Ą'
      case 134: c = 11 + '~'; break; // 'Ć'
      case 152: c = 12 + '~'; break; // 'Ę'
      case 129: c = 13 + '~'; break; // 'Ł'
      case 131: c = 14 + '~'; break; // 'Ń'
      case 147: c = 15 + '~'; break; // ''
      case 154: c = 16 + '~'; break; // 'Ś'
      case 185: c = 17 + '~'; break; // 'Ź'
      case 187: c = 18 + '~'; break; // 'Ż'
      default:  break;
    }
    dualChar = 0;
    return c;
  }
  switch (_c) {
    case 185: c = 1 + '~'; break;
    case 230: c = 2 + '~'; break;
    case 234: c = 3 + '~'; break;
    case 179: c = 4 + '~'; break;
    case 241: c = 5 + '~'; break;
    case 243: c = 6 + '~'; break;
    case 156: c = 7 + '~'; break;
    case 159: c = 8 + '~'; break;
    case 191: c = 9 + '~'; break;
    case 165: c = 10 + '~'; break;
    case 198: c = 11 + '~'; break;
    case 202: c = 12 + '~'; break;
    case 163: c = 13 + '~'; break;
    case 209: c = 14 + '~'; break;
    case 211: c = 15 + '~'; break;
    case 140: c = 16 + '~'; break;
    case 143: c = 17 + '~'; break;
    case 175: c = 18 + '~'; break;
    default:  break;
  }
  return c;
}

// =======================================================================

void printCharWithShift(unsigned char c, int shiftDelay) {
  c = convertPolish(c);
  if (c < ' ' || c > '~' + 25) return;
  c -= 32;
  int w = showChar(c, font);
  for (int i = 0; i < w + 1; i++) {
    delay(shiftDelay);
    scrollLeft();
    refreshAll();
  }
}

// =======================================================================

void printStringWithShift(const char* s, int shiftDelay) {
  while (*s) {
    printCharWithShift(*s, shiftDelay);
    s++;
  }
}
// =======================================================================

void getTime()
{
  WiFiClient client;
  if (!client.connect("www.google.com", 80)) {
    Serial.println("connection to google failed");
    return;
  }

  client.print(String("GET / HTTP/1.1\r\n") +
               String("Host: www.google.com\r\n") +
               String("Connection: close\r\n\r\n"));
  int repeatCounter = 0;
  while (!client.available() && repeatCounter < 10) {
    delay(500);
    //Serial.println(".");
    repeatCounter++;
  }

  String line;
  client.setNoDelay(false);
  while (client.connected() && client.available()) {
    line = client.readStringUntil('\n');
    line.toUpperCase();
    if (line.startsWith("DATE: ")) {
      date = "     " + line.substring(6, 22);
      date.toUpperCase();
      //      decodeDate(date);
      h = line.substring(23, 25).toInt();
      m = line.substring(26, 28).toInt();
      s = line.substring(29, 31).toInt();
      summerTime = checkSummerTime();
      if (h + utcOffset + summerTime > 23) {
        if (++day > 31) {
          day = 1;
          month++;
        };  // needs better patch
        if (++dayOfWeek > 7) dayOfWeek = 1;
      }
      localMillisAtUpdate = millis();
      localEpoc = (h * 60 * 60 + m * 60 + s);
    }
  }
  client.stop();
}

// =======================================================================

int checkSummerTime()
{
  if (month > 3 && month < 10) return 1;
  if (month == 3 && day >= 31 - (((5 * year / 4) + 4) % 7) ) return 1;
  if (month == 10 && day < 31 - (((5 * year / 4) + 1) % 7) ) return 1;
  return 0;
}
// =======================================================================

void updateTime()
{
  long curEpoch = localEpoc + ((millis() - localMillisAtUpdate) / 1000);
  long epoch = round(curEpoch + 3600 * (utcOffset + summerTime) + 86400L) % 86400L;
  h = ((epoch  % 86400L) / 3600) % 24;
  m = (epoch % 3600) / 60;
  s = epoch % 60;
}

// =======================================================================

 

 

Going Farther
The clock with its accompanying Westminster chimes has been operating in my workshop for some weeks and provides a very reliable time and pleasant tones every 15 minutes.  I plan on making high quality recordings of my mantle clock's chimes to substitute for the sound I now have on the micro SD card. 

The next project will be to add hands to the clock and to mount it in an appropriate enclosure. 

I hope you have a chance to experiment with WiFi connected clocks, too!