Clock Stop – Getting time just right with Arduino :: Member Project

Pendulum clocks have great sounding chimes – especially when they are paired up with Arduino to make sure those chimes happen right on time.  Check out this project by Bill Bryan, a member of Programming Electronics Academy.  Bill submitted this cool project that uses a wireless module, servo motor, OLED display, and more to make sure his clocks’ chimes come in right on time.

Why did you build this Arduino project?

I have a mechanical pendulum clock that won’t keep good time.

This program gets the time from the internet. It compares this time to the first strike of a chime hammer. The program will stop the pendulum if the clock is running fast.

I can’t speed the clock up, but I can slow it down by stopping the pendulum.

There are 3 IR emitter/sensors.

  • One on the first chime, note, strike hammer
  • One for the pendulum to locate where its located
  • One for the clock weights when they reach the bottom of the clock

Adafruit metro Arduino and OLED display on counter top IR sensor connected to clock mechanism

What type of Arduino board does your project use?

Adafruit Metro M4 Express AirLift (WiFi) – Lite

What was your biggest struggle as you worked through this project?

Timing aspects on stopping the pendulum.  The program holds the time to about a second. I’m still making additions.

OLED display and servo motor on desk

Was the training at Programming Electronics Academy able to help you build your skill?

Yes. It was Fantastic…

About Bill Bryan

Bill is now retired, but worked in sound studios and digital cinema projection.  He has been into electronics for over 60 years.  Bill just started doing  C++ a couple of years ago, but had been doing some Quick Basic programming in times past.  He says he is a bit of a time freak, hence this project, which allows him to hear the chime of the clock  through his house on time.

 

 

 

You can download the schematic here.

Arduino Code:

/*
  ---------------------------------------------------------------
        CLOCK STOP      Ver: 5 With NTP Client

           Ver: 5 using IR sensor on 3rd hammer
                     and display sleeping

     written by: Bill Bryan  3/19/20 Final Update 6/5/20
     changed time service and restored 0 second offset
     added 1/4 hour time grabs and other fixes.

     for Adafruit Metro M4 Lite and SSD1306 OLED display

  ----------------------------------------------------------------
*/


#include <NTPClient.h>
#include <WiFiUdp.h>
#include <WiFiNINA.h>
#include <Adafruit_SSD1306.h>
#include <splash.h>
#include <Servo.h>
#include <Adafruit_SleepyDog.h>

const char *ssid     = "ssid";
const char *password = "password";

WiFiUDP ntpUDP;

// By default 'pool.ntp.org' is used with
// 60 seconds update interval and no offset
NTPClient timeClient(ntpUDP, "utcnist.colorado.edu", 0, 600000);  // BB mod for 10 minute updates, 1 min updates 60,000
//NTPClient timeClient(ntpUDP);                                 // original
//NTPClient timeClient(ntpUDP, "pool.ntp.org", 1, 600000);      // BB mod for 10 minute updates, 1 min updates 60,000
// changed 0000 to 0001 (1 sec) to help fix propagation delay
// 5/1/20 turned back 0000 makes correct UTC hours 3600/hour
// You can specify the time server pool and the offset, in seconds)
// additionally you can specify the update interval (in milliseconds).


Adafruit_SSD1306 display = Adafruit_SSD1306(128, 64, &Wire);    //Setup for OLED dispaly

Servo myservo;  // create servo object to control a servo

int setAhead = 0;                               //set ahead for testing. 58 during testing...BB

const int weightsThreshold = 512;               //point where weights are to low. Was 800. Lowered for sunlight
const int pendulumThreshold = 800;              //point where we see the pendulum was 900. Lowered for sunlight
const int beforeTop = 3540;                     //1 min is 3540 seconds before top of hour
const int afterTop = 60;                        //check 60 secs after top of hour. how slow are we?
int rawHour = 0;                                //varible for NTP hours
int rawMin = 0;                                 //varible for NTP minutes
int rawSec = 0;                                 //varible for NTP seconds
int displayMin = 0;                             //display UTC minutes
int displaySec = 0;                             //display UTC seconds
int lastClockSpeed = -60;                       //fast or slow amount last hour
int totalHourSec = 0;                           //1 hour total seconds = 3600
int pendulumLevel = 0;                          //pendulum level 0-1023
int weightsLevel = 0;                           //STOP CLOCK if below ???
int weightsCounter = 0;                         //used to see if weights are truely below threshold
int currentDisplaySec = 0;
int holdSecOffset = 0;                          //hold all the offsets for averaging
int zeroSecOffset = -60;                        //was -120. changed to -60
float loopCounter = 0;                          //used for offset averaging loop counter. Float for math
float averageSecOffset = 0;                     //offset averaging. Float for math division
const unsigned long activeDisplayTime = 150000; //display is on for 2-1/2 minutes in mills
const unsigned long activeWeightsTime = 1800;   //wait 1/2 hour in seconds to stop entire clock
unsigned long currenTime = 0;                   //current millis()
unsigned long displayOnStarTime = 0;            //start time for active display on time after sleep
unsigned long previousTime = 0;                 //previous millis() for 1 second loop
unsigned long pendulumTime = 0;                 //how long have we waited for the pendulum
unsigned long swingTime = 0;                    //pendulum swing timer
const unsigned long evenTime = 1000;            //the one second loop
unsigned long starTimer = 0;                    //number of seconds to use during holdback while loop
unsigned long endTimer = 0;                     //end of hold back timer
unsigned long timeToHoldBack = 0;               //number of seconds to holdback pendulum
unsigned long weightsOnStarTime = 0;            //start timer for weights in view 30 seconds to stop entire clock
String        ntpDisplay = "00:00:00";          //character for NTP display
const byte    servoHome = 32;                   //home position for servo
const byte    servoHold = 118;                  //hold position for servo
//const byte  soundPin = 8;                     //pin used for chime detection (NOT USED ANYMORE)
const byte    futurePin = 9;                    //pin used for hammer sensor
const byte    ledPin = 11;                      //output pin for blue LED. HIGH lights it
const byte    servoPin = 10;                    //output pin for servo
const byte    leftButton = 2;                   //left button (servo testing.  gate closed)
const byte    rightButton = 3;                  //right button (servo testing. gate open)
const byte    pendulumPin = 0;                  //pendulum connected to A0
const byte    weightsPin = 1;                   //clock weights connected to A1
byte          timerStatus = 0;                  //0-7 depending on state of action
boolean       buttonFlag = false ;              //needed for push button test of pendulum servo
boolean       beforeTopHourWindow = false;      //Top of hour window allows holdbacks and prevents false triggers
boolean       afterTopHourWindow = false;       //after top of hour window to see how slow we're running
boolean       pendulumFlag = false;             //flag needed for low weights shutdown
boolean       soundFlag = false;                //flag for chime detection
boolean       displayOn = true;                 //true = display on
boolean       hammerFlag = false;               //flag chime hammer strike cocking



void setup() {  // Start of Void Setup +++++++++++++++++++++++++++++++++++++

  digitalWrite(ledPin, LOW);
  pinMode(futurePin, INPUT_PULLUP);               //IR sensor mounted above 3rd chime hammer. Hit = LOW
  //pinMode(soundPin, INPUT);                     //Future input no used instead. (NOT USED ANYMORE)
  pinMode(ledPin, OUTPUT);
  pinMode(servoPin, OUTPUT);
  pinMode(leftButton, INPUT_PULLUP);
  pinMode(rightButton, INPUT_PULLUP);
  pinMode(pendulumPin, INPUT_PULLUP);
  pinMode(weightsPin, INPUT_PULLUP);

  // code for OLED display to run once:
  display.begin(SSD1306_SWITCHCAPVCC, 0x3D);      // Address 0x3C for 128x32
  display.display();
  display.setTextSize(1);
  display.setTextColor(WHITE);
  display.clearDisplay();
  display.setCursor(0, 0);

  WiFi.begin(ssid, password);

  while ( WiFi.status() != WL_CONNECTED ) {
    delay(1000);
    display.println ( "WiFi Login... " );
    display.println("");
    display.print("SSID: ");
    display.print(ssid);
    display.display();

    WiFi.begin(ssid, password);
  }

  timeClient.begin();

  myservo.attach(10);           // attaches the servo on pin 10 to the servo object
  myservo.write(servoHome);     // tell servo to go to release position

  Serial.begin(9600);

  int countdownMS = Watchdog.enable(8000);    // set Watchdog timer to 8 seconds to clear lockup

  displayOnStarTime = millis();               // Set time out for initial display on timer.

  // for testing only +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  //  ntpDisplay = timeClient.update();         //get the UTC string for display if we need it
  //
  //  rawMin = timeClient.getMinutes();
  //  setAhead = (58 - rawMin);
  //
  // for testing only +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++



} // End of Setup --------------------------------------



void loop() { //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++


  currenTime = millis();  //hold the current millis()


  // **************************************
  // ***    START OF ONE SECOND LOOP    ***
  // **************************************

  if ((currenTime - previousTime) >= evenTime) { // one second loop
    previousTime = currenTime;

    Watchdog.reset();       //reset Watchdog timer back to zero

    ntpDisplay = timeClient.update();  //get the UTC string for display

    rawMin = timeClient.getMinutes();
    rawSec = timeClient.getSeconds();

    // for updating display time to 58 minutes +++ TESTING ONLY +++

    displayMin  = rawMin + setAhead;  //setAhead = 0 in normal operation. 58 when used in testing
    if (displayMin >= 60) {
      (displayMin = displayMin - 60);
    }

    displaySec  = rawSec;

    pendulumLevel = analogRead(pendulumPin);                    //get level for display

    weightsLevel = analogRead(weightsPin);                      //get level for display


    totalHourSec = (displayMin * 60) + displaySec; //calc total current seconds in the hour

    /*
        *** Set timerStatus depending on conditions ***
            0 = Below window
            1 = In before window
            2 = In after  window
            3 = In before window with chime
            4 = In after  window with chime
            5 = Servo arm in the hold position
            6 = Servo arm now ready for release
            7 = Done with hold back
            8 = timer halt for pendulum weight
            9 = 15, 30, 45 minute display holds
         ___________________________________________________________________________
    */

    if ((totalHourSec > beforeTop) && (!soundFlag))  {                              // we're in the before top window.
      timerStatus = 1;                                                              // 1 = in the before top window
    }
    if ((totalHourSec > 0) && (totalHourSec <= afterTop) && (timerStatus <= 1)) {   // we're in the after top window
      timerStatus = 2;                                                              // 2 = in the after top window
    }
    if ((soundFlag) && (timerStatus == 1)) {                                        // 3 = before top and chime
      timerStatus = 3;
    }
    if ((soundFlag) && (timerStatus == 2)) {                                        // 4 = after top and chime
      timerStatus = 4;
      AvgLoop();                                                                    // do averaging and loop counter (once an hour)
    }
    if ((timerStatus == 1 ) && (!soundFlag)) {                                      // lets down count if we're in the before window and no chime
      ++zeroSecOffset;
    }
    if ((timerStatus == 2 ) && (!soundFlag)) {                                      // lets down count if we're in the after window and no chime
      ++zeroSecOffset;
    }


    if (totalHourSec == 870 || totalHourSec == 871)   {          // set flag for the first quarter hour display and captures
      timerStatus = 9;
    }
    if (totalHourSec == 1770 || totalHourSec == 1771) {          // set flag for the second quarter hour display and captures
      timerStatus = 9;
    }
    if (totalHourSec == 2670 || totalHourSec == 2671) {          // set flag for the third quarter hour display and captures
      timerStatus = 9;
    }


    // ****  Final varible clean up   ****
    if (totalHourSec == afterTop) {                              // time for final clean up now that we're 60 seconds past top of the hour
      timerStatus = 0;                                           // 0 = back to the bottom of the hour window
      soundFlag = false;
      zeroSecOffset = -60;                                       // reset offset counter
    }

    //
    // *** Routines for waking the display from sleep mode ***
    //

    if (weightsLevel < weightsThreshold) {                       //read the weights IR sensor for activating the display out of sleep mode
      displayOn = true;
      displayOnStarTime = currenTime;                            // save current time for the display active timer
    }

    if (currenTime > (displayOnStarTime + activeDisplayTime) && (displayOn)) {  // turn display off if 2-1/2 minutes 150000ms have past
      displayOn = false;                                                        // turn off only if display was true
      display.clearDisplay();                                                   // clear display since we're sleeping again
      display.display();                                                        // show an empty screen
    }





    // *** TURN ON AND OFF DISPLAY DURING THE HOUR ***
    // Turn on display top of the hour

    if (totalHourSec == 3525 || totalHourSec == 3526 ) {         //turn on display 75 seconds before top of the hour, every hour
      displayOn = true ;                                         //added extra second if update caused a miss.
      displayOnStarTime = currenTime;                            //start display on timer
    }
    if (totalHourSec == 75)  {                                   //turn off display 75 seconds after top of the hour, every hour
      displayOn = false ;
    }

    // Turn on display 1/4, 1/2, 3/4 hour
    if (timerStatus == 9) {                                      //turn on display 30 seconds before each quarter hour, every hour
      displayOn = true ;
      displayOnStarTime = currenTime;                            //start display on timer
    }

    //turn off display after 60 seconds and clean up
    if (totalHourSec == 930 || totalHourSec == 931 || totalHourSec == 1830 || totalHourSec == 1831 || totalHourSec == 2730 || totalHourSec == 2731 )  {
      displayOn = false ;
      displayOnStarTime = 0;                                     // reset display on timer
      timerStatus = 0;                                           // 0 = back to the bottom of the hour window
      zeroSecOffset = -60;                                       // reset offset counter
    }





    //
    // *** Low Weights Shutdown ***
    //

    if (weightsLevel < weightsThreshold) {                      // weights low

      weightsOnStarTime = currenTime;
      ++ weightsCounter;
    } else {
      weightsOnStarTime = 0;                                      //reset variables if the weights are not blocking sensor
      weightsCounter = 0;                                         //reset weights counter since weights are not blocking sensor
    }

    if (weightsCounter >= activeWeightsTime) {                    // weights low for 1/2 hour (1800 seconds). Shutdown clock
      // 3 hours = 1/4" in weight height
      Watchdog.disable();                                         //hold the watchdog timer from resetting the whole program
      digitalWrite(ledPin, HIGH);
      timerStatus = 8;                                            // 8 = hold for weights
      ScreenRefresh();                                            // go refresh the screen to show timerStatus of 8
      pendulumFlag = true;                                        // save pendulum action so we only ask for servo home position once

      do {
        delay(1);                                                 //1ms to rest the ADC
        pendulumLevel = analogRead(pendulumPin);                  //read pendulum sensor
      } while (pendulumLevel < pendulumThreshold);                 //hold here till pendulum is swung away from swing sensor end befor looking again

      do {
        delay(1);                                                 //1ms to rest the ADC
        pendulumLevel = analogRead(pendulumPin);                  //read pendulum sensor
      } while (pendulumLevel > pendulumThreshold);                 //hold till we see the pendulum again for capture

      do {                                                        // do loop to flash LED
        myservo.write(servoHold);                                 // tell servo to go to hold position
        digitalWrite(ledPin, LOW);                                // turn off LED
        delay(125);
        digitalWrite(ledPin, HIGH);                               // turn on LED
        delay(125);
        weightsLevel = analogRead(weightsPin);                    // get level to see if weights have been raised
      } while (weightsLevel <= weightsThreshold);                  // keep looping until weights are raised

    } else if (pendulumFlag) {                                     // reset if pendulum was in a hold position
      myservo.write(servoHome);                                   // tell servo to go to release position but only once
      digitalWrite(ledPin, LOW);                                  // turn off LED
      timerStatus = 0;
      pendulumFlag = false;
      Watchdog.enable(8000);                                      // reset Watchdog timer to 8 seconds

    }

    ScreenRefresh();                                              // go refresh the screen

    //=======================================================================
  } // -=-=-=-=-=-=-=-=-=-=  End of 1 second update  -=-=-=-=-=-=-=-=-=-=-=-=
  //=======================================================================



  // read the middle hammer. Is it cocking back for its next strike?

  if (digitalRead(futurePin) == LOW && timerStatus < 3 && timerStatus > 0 || digitalRead(futurePin) == LOW && timerStatus == 9) {
    hammerFlag = true;                                            //chime hammer is cocking back (low)
  }

  if ((digitalRead(futurePin) == HIGH) && (hammerFlag)) {
    soundFlag = true;                                             //hammer heading to strike chime (high)
    hammerFlag = false;                                           //reset flag
  }

  //------------------------------------------------------

  // quarter hour chime catch for freezing the zeroSecOffset

  if (soundFlag && timerStatus == 9) {
    timerStatus = 0;                // reset timerstatus
    zeroSecOffset = displaySec;     // set display to hold timer
    soundFlag = false;              // reset soundflag
  }


  //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  //
  //              ***  routines to hold back pendulum  ***
  //
  //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

  if (timerStatus == 3) {                                       //before window plus chime
    timerStatus = 5;                                            //change the timerStatus of 3 to 5
    //
    digitalWrite(ledPin, HIGH);                                 //turn on blue LED
    AvgLoop();                                                  // do averaging and loop counter (once an hour)

    timeToHoldBack = abs(zeroSecOffset);                        //var to use during holdback. This is negative number

    do {
      delay(10);                                                //10ms to rest the ADC
      pendulumLevel = analogRead(pendulumPin);                  //read pendulum sensor
    } while (pendulumLevel < pendulumThreshold);                 //hold here till pendulum is swung away from swing sensor end befor looking again

    do {
      delay(10);                                                //1ms to rest the ADC
      pendulumLevel = analogRead(pendulumPin);                  //read pendulum sensor
    } while (pendulumLevel > pendulumThreshold);                 //hold till we see the pendulum again for capture

    //delay(100);                                               //10th of a second. To help make smooth pendulum capture
    myservo.write(servoHold);                                   //tell servo to go to hold position


  } // finished with the first part. Pendulum stopped

  //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

  if (timerStatus == 5) {
    timerStatus = 6;
    pendulumTime = millis() + (timeToHoldBack * 1000);          //save current millis()
    //+++++++++++++++++++++++++++++++++++++++++++++++
    pendulumTime = pendulumTime + 1000;                         //add an additional 1 second to holdback 6/2/20
    //added to force back into positive time (slow)
    if (averageSecOffset <= -1) {                               //if the average is more that -1 then add an
      pendulumTime = pendulumTime + 1000;                       //additional second to the hold back.
    }
  }                                                             //+++++++++++++++++++++++++++++++++++++++++++++++

  if (millis() >= (pendulumTime) && (timerStatus == 6)) {       //we've hit our TimeToHoldBack, lets release
    myservo.write(servoHome);                                   //tell servo to go to release position
    digitalWrite(ledPin, LOW);                                  //turn off the blue LED
    timerStatus = 7;                                            //swing arm now open
  } // end of hold back time. Done with that hour.


  //---------------------------------------------------------------
  // servo motion routine test routine using left and right buttons
  //---------------------------------------------------------------
  if ((digitalRead(leftButton) == LOW) && (!buttonFlag)) {

    do {
      delay(1);                                                 //1ms to rest the ADC
      pendulumLevel = analogRead(pendulumPin);                  //read pendulum sensor
    } while (pendulumLevel < pendulumThreshold);                 //hold here till pendulum is swung away from swing sensor end before looking again

    do {
      delay(1);                                                 //1ms to rest the ADC
      pendulumLevel = analogRead(pendulumPin);                  //read pendulum sensor
    } while (pendulumLevel > pendulumThreshold);                 //hold till we see the pendulum again for capture


    myservo.write(servoHold);                                   // tell servo to go to hold position
    buttonFlag = true;
  }

  if ((digitalRead(rightButton) == LOW) && (buttonFlag == true)) {
    myservo.write(servoHome);                                   // tell servo to go to release position
    buttonFlag = false;
  }

  if (digitalRead(rightButton) == LOW) {                        //wake up display if button pushed
    displayOn = true;
    displayOnStarTime = currenTime;
  }

} // End of Void Loop -----------------------------------------------------


void AvgLoop()                                                 // do averaging and loop counter (once an hour)
{ // save for last clock speed
  lastClockSpeed = zeroSecOffset;                                // for last hour speed for display
  holdSecOffset = holdSecOffset + int(zeroSecOffset);            // add last offset for averaging
  ++loopCounter ;                                                // add 1 to loop counter
  averageSecOffset = holdSecOffset / loopCounter;                // average out the total offsets for display

  // During weight crank ups clock will stop keeping time. Moving the minute hand forward to fix the stopped time
  // will distroy the true averaging. If the lastClockSpeed is < -5 then lets reset the average and loop counter. 6/2/20

  if (lastClockSpeed < - 5) {
    holdSecOffset = 0;
    averageSecOffset = 0;
    loopCounter = 0;
  }

} //End of void AvgLoop()


void ScreenRefresh()
{

  display.clearDisplay();
  display.setTextSize(1);
  display.setCursor(0, 0);

  display.print("   UTC Time: ");
  display.println(timeClient.getFormattedTime());

  display.print("   Pendulum: ");
  display.println(pendulumLevel);

  display.print("  Clock Wgt: ");
  display.println(weightsLevel);


  if (averageSecOffset < 0) {           // minus average
    display.print("Avg & Loops:");
  } else {
    display.print("Avg & Loops: ");    // positive average
  }
  display.print(averageSecOffset);     // normally two decimal places with float
  display.print(" :");
  display.println(loopCounter, 0);     // no decimal points



  if (lastClockSpeed < 0) {
    display.print("LastHr FAST:");
    display.print(lastClockSpeed);
    display.print(" Secs");
  } else {
    display.print("LastHr SLOW: ");
    display.print(lastClockSpeed);
    display.print(" Secs");
  }


  display.setTextSize(2);
  display.setCursor(5, 50); // slid over the 2x mins & Sec by using "5"

  if (displayMin < 10) {
    display.print("0");
  }
  display.print(displayMin);
  display.print(":");

  if (displaySec < 10) {
    display.print("0");
  }
  display.print(displaySec);

  if (zeroSecOffset == 0) {         //If zero
    display.print("  ");
    display.print(zeroSecOffset);
  } else if (zeroSecOffset < 0) {   //if minus
    display.print(" ");
    display.print(zeroSecOffset);
  } else {
    display.print(" +");            //if plus
    display.print(zeroSecOffset);
  }

  // ******** Display timerStatus ***** TESTING ONLY - NOT USED NORMALLY ***
  if (timerStatus > 0) {            // only display if greater than zero
    display.setCursor(114, 9);     // was 4 now row 10 (4+5) Changed 5/28
    display.print(timerStatus);     // what mode are we in. Only used during design
  }

  if (!displayOn) {                // only display when true.
    display.clearDisplay();
  }

  display.display();              // Redisplay entire screen

}   // End of screen refresh

installing Arduino libraries

Installing Arduino Libraries | Beginners Guide

IoT sewage project

Pumping poo! An IoT sewage project

ESP32 webOTA updates

How to update ESP32 firmware using web OTA [Guide + Code]

error message Brackets Thumbnail V1

expected declaration before ‘}’ token [SOLVED]

Compilation SOLVED | 1

Compilation error: expected ‘;’ before [SOLVED]

Learn how to structure your code

1 Comments

  1. Tom Gross on June 30, 2020 at 1:49 pm

    Cool project, thinking outside the box. But Could you accomplish the same thing by changing the length of the pendulum?

Leave a Comment