Project tutorial
Backward Running Analog Clock with Three Steppers

Backward Running Analog Clock with Three Steppers © GPL3+

Use three stepper motors to make the classic analog clock movement in the opposite direction.

  • 997 views
  • 4 comments
  • 8 respects

Components and supplies

11113 01
SparkFun Arduino Pro Mini 328 - 5V/16MHz
×1
Elegoo stepper motor and driver
Can purchase as five pack with driver affordably online
×3
External power Supply
External variable or 5Vdc power supply. Couple amps max probably enough. I have a 5 amp, 0-30Vdc
×1
12002 04
Breadboard (generic)
×1
misc
cardboard or equivalent to mount motors and attach watch movements and labels, plus hook-up wires and cable to be able to connect Pro Mini to computer via USB
×1

Apps and online services

About this project

I wanted to make an analog clock that ran counter-clockwise. A nice lesson to not assume the normal is very normal at all. There really is no reason other than convention for a clock to turn the direction around the circle it does.

Since the classic DIY positioning servos I've implemented have a 180-degree working range, I decided to use a stepper motor.

A circle is broken into 360 degrees also by convention only. However, the number 360 is quite convenient due to the quantity of numbers that divide evenly into 360. The number of hours, minutes and seconds divide evenly into 360. As a result each hour represents 30 degrees of rotational travel, each minute is 6 degrees of travel and same for seconds.

This code calculates absolute "MoveTo" positions with the information in mind.

However, there is a bug that I cannot resolve. The code uses the AccelStepper library and there is one move that is incorrect that I cannot find the bug in the code/logic. It is more obvious on the seconds hand since the problem arises every 60 seconds. Problem is same on hours and minutes but is more easily tolerable since happens twice per day on hour hand and only once per hour on the minute hand.

Exact steps to reproduce the problem:

The second hand makes the correct move of 6 degrees in the counter-clockwise direction from second value 58 to 59. Same for move from 59 to 0. The minute hand makes it's 6 degree move at the transition of second hand from 59 to 0. However, on the next second hand move (from 0-1), the second hand servo makes the long move in the clockwise direction.

The motor eventually catches up since it runs at faster than 6 degrees per second. Same problem exists with code for other motors. I experimented with negative speed commands and other things without success.

I prefer to not use incremental moves due to accumulated error and the need to write code that puts the motor ahead of the actual seconds value of the Arduino clock time to sync as clock time "passes" the current motor position or similar.

Code

Arduino codeArduino
#include <AccelStepper.h>
#include <MultiStepper.h>
#include <Time.h>
#include <TimeLib.h>
#include <EEPROM.h>

//Backward Analog World Clock (BABwatch_2)
//Scott Mangiacotti
//Tucson, Arizona USA
//September 2018


//Global constants
const int GIVE_BACK = 5;  //number milliseconds delay at end of each scan to ensure not using 100% CPU

//Constants for the steppers
const int STEPS = 4096;
const int HALF_STEP = 8;

const int HOUR_STEPPER_IN1 = 2;
const int HOUR_STEPPER_IN2 = 3;
const int HOUR_STEPPER_IN3 = 4;
const int HOUR_STEPPER_IN4 = 5;

const int MIN_STEPPER_IN1 = 6;
const int MIN_STEPPER_IN2 = 7;
const int MIN_STEPPER_IN3 = 8;
const int MIN_STEPPER_IN4 = 9;

const int SEC_STEPPER_IN1 = 10;
const int SEC_STEPPER_IN2 = 11;
const int SEC_STEPPER_IN3 = 12;
const int SEC_STEPPER_IN4 = 13;

//Global variables
AccelStepper gHourStepper(HALF_STEP, HOUR_STEPPER_IN1, HOUR_STEPPER_IN3, HOUR_STEPPER_IN2, HOUR_STEPPER_IN4);
AccelStepper gMinStepper(HALF_STEP, MIN_STEPPER_IN1, MIN_STEPPER_IN3, MIN_STEPPER_IN2, MIN_STEPPER_IN4);
AccelStepper gSecStepper(HALF_STEP, SEC_STEPPER_IN1, SEC_STEPPER_IN3, SEC_STEPPER_IN2, SEC_STEPPER_IN4);

int gHour;
int gMinute;
int gSecond;
int gDay;
int gMonth;
int gYear;
bool gTimeSynced = false;

bool gTempCW = false;

int gHourOffset = 0;     //units +/- degrees
int gMinuteOffset = 0;   //units +/- degrees
int gSecondOffset = 0;   //units +/- degrees

bool gSystemEnabled = false; //determines if servos are moved to match wallclock time every loop scan


//Run once
void setup()
{

  //Setup static stepper properties
  gHourStepper.setMaxSpeed(1000.0);
  gHourStepper.setAcceleration(100.0);
  gHourStepper.setSpeed(999);

  gMinStepper.setMaxSpeed(1000.0);
  gMinStepper.setAcceleration(100.0);
  gMinStepper.setSpeed(999);

  gSecStepper.setMaxSpeed(1000.0);
  gSecStepper.setAcceleration(100.0);
  gSecStepper.setSpeed(999);
  
  //Initialize serial port
  Serial.begin(9600);

  //Post app data
  postAppData();
  
}


//Run continuous
void loop()
{

  //Parse into separate variables
  if (gTimeSynced == true)
  {
    gHour = hour();
    gMinute = minute();
    gSecond = second();
    gDay = day();
    gMonth = month();
    gYear = year();
  }
  
  if (gSystemEnabled == true)
  {
    if (gTimeSynced == true)
    {
      //Calculate HOUR hand position
      if (gHourStepper.currentPosition() ==  gHourStepper.targetPosition())
      {
        calculateHourMoveTo(gHour);
      }

      //Calculate MINUTE hand position
      if (gMinStepper.currentPosition() ==  gMinStepper.targetPosition())
      {
        calculateMinuteMoveTo(gMinute);
      }
      
      //Calculate SECOND hand position
      if (gSecStepper.currentPosition() ==  gSecStepper.targetPosition())
      {
        calculateSecondMoveTo(gSecond);
      }
      
    }
    else
    {
      gSystemEnabled = false;
      Serial.println("Time not synchronized");
      
    }
  }

  //Process serial messages
  if (Serial.available() > 0)
  {
    processMessage();
    
  }

  //Run the motors until desired position achieved
  gHourStepper.setSpeed(999);
  gHourStepper.runSpeedToPosition();

  gMinStepper.setSpeed(999);
  gMinStepper.runSpeedToPosition();

  gSecStepper.setSpeed(-999);
  gSecStepper.runSpeedToPosition();

  //Give some time back
  delay(GIVE_BACK);

}


//Based on hour value from wallclock time, calculate and move to hour position
//Hour to be passed in 24-hour time
void calculateHourMoveTo(int iHour)
{
  int iCWposition;
  int iCCWposition;
  
  //Validate
  if (iHour < 0 && iHour > 23)
  {
    Serial.println("Invalid hour parameter in function calculateHourMoveTo");
    return;
  }

  //Calculate hour servo position for clockwise clock
  //12 hours per day
  //360 degrees in a full circule
  //360/12 = 30 degrees per hour

  //Convert 24-hour format hour into 12-hour format
  if (iHour > 12)
  {
    iHour = iHour - 12;
  }

  //Calculate position based on above formula
  iCWposition = iHour * 30;

  //Deal with roll-over
  if (iCWposition == 360)
  {
    iCWposition = 0;
  }

  //Calculate counter-clockwise
  //360 - CW position calculated above
  iCCWposition = 360 - iCWposition;

  //Deal with roll-over
  if (iCCWposition == 360)
  {
    iCCWposition = 0;
  }

  //Move stepper. Above math calculates in degrees. Following converts into stepper "steps"
  double dStepperMove;
  dStepperMove = (iCCWposition/360.00) * STEPS;//ilem: convert offset into steps and add
  gHourStepper.moveTo(dStepperMove);
  
}


//Based on minute value from wallclock time, calculate and move to minute position
void calculateMinuteMoveTo(int iMinute)
{

  int iCWposition;
  int iCCWposition;
  
  //Validate
  if (iMinute < 0 && iMinute > 12)
  {
    Serial.println("Invalid minute parameter in function calculateMinuteMoveTo");
    return;
  }

  //Calculate minute servo position for clockwise clock
  //60 minutes per hour
  //360 degrees in a full circule
  //360/60 = 6 degrees per minute

  //Calculate position based on above formula
  iCWposition = iMinute * 6;

  //Deal with roll-over
  if (iCWposition == 360)
  {
    iCWposition = 0;
  }

  //Calculate counter-clockwise
  //360 - CW position calculated above
  iCCWposition = 360 - iCWposition;

  //Deal with roll-over
  if (iCCWposition == 360)
  {
    iCCWposition = 0;
  }

  //Move stepper. Above math calculates in degrees. Following converts into stepper "steps"
  double dStepperMove;
  dStepperMove = (iCCWposition/360.00) * STEPS;//ilem: convert offset into steps and add
  gMinStepper.moveTo(dStepperMove);
  
}


//Based on second value from wallclock time, calculate and move to second position
void calculateSecondMoveTo(int iSecond)
{

  int iCWposition;
  int iCCWposition;
  
  //Validate
  if (iSecond < 0 && iSecond > 12)
  {
    Serial.println("Invalid second parameter in function calculateSecondMoveTo");
    return;
  }

  //Calculate second servo position for clockwise clock
  //60 seconds per minute
  //360 degrees in a full circule
  //360/60 = 6 degrees per second

  //Calculate position based on above formula
  iCWposition = iSecond * 6;

  //Deal with roll-over
  if (iCWposition == 360)
  {
    iCWposition = 0;
  }

  //Calculate counter-clockwise
  //360 - CW position calculated above
  iCCWposition = 360 - iCWposition;

  //Deal with roll-over
  if (iCCWposition == 360)
  {
    iCCWposition = 0;
  }

  //Move stepper. Above math calculates in degrees. Following converts into stepper "steps"
  double dStepperMove;
  dStepperMove = (iCCWposition/360.00) * STEPS;//ilem: convert offset into steps and add
  gSecStepper.moveTo(dStepperMove);

//  if (iSecond == 0 || iSecond == 1)
//  {
//    Serial.println(dStepperMove);
//
//  }

}


//Set SBC time, declare synced and ready to run and display results
void setSystemTimeNow()
{

  setTime(gHour, gMinute, gSecond, gDay, gMonth, gYear);
  gTimeSynced = true;
  Serial.println("System time set successfully");
  
}


//Read data from serial port and process message from power user
//Format is: XXnnnn
//XX is a value between 1 - 32 and represents the command type or area (for example manual commands to the HOUR servo motor)
//nnnn is a value between 0-1000 and represents the value for the target command type
//For example, 01180 is type 02 and value 180. It represents HOUR servo motor move to position 180 degrees
//See documentation for command definitions and value ranges
void processMessage()
{
  int iMessage;
  int iControlCode;
  int iControlValue;

  //Read the data in the serial port buffer
  iMessage = Serial.parseInt();
  Serial.print("Message received: ");
  Serial.println(iMessage);


  //Process the serial port message
  if (iMessage > 0)
  {
    iControlCode = iMessage/1000;
    Serial.print("Control Code: ");
    Serial.println(iControlCode);
    
    iControlValue = iMessage % 1000;
    Serial.print("Control Value: ");
    Serial.println(iControlValue);
  }

  //Misc control and command codes
  if (iControlCode == 10)
  {
    //Control codes and commands
    if (iControlValue == 1)
    {
      gSystemEnabled = true;
      Serial.println("System Enabled");
      
    }
    else if (iControlValue == 2)
    {
      gSystemEnabled = false;
      Serial.println("System Disabled");
      
    }
    else if (iControlValue == 3)
    {
      gTempCW = !gTempCW;
      if (gTempCW == true)
      {
        Serial.println("manual moves command clockwise");
      }
      else
      {
        Serial.println("manual moves command counter-clockwise");
      }
      
    }
    else if (iControlValue == 4)
    {
      //spare
      
    }
    else if (iControlValue == 5)
    {
      setSystemTimeNow();
      
    }
    else if (iControlValue == 6)
    {
      postAppData();
      
    }
    else if (iControlValue == 7)
    {
      Serial.print(gHour);
      Serial.print(":");
      Serial.print(gMinute);
      Serial.print(":");
      Serial.print(gSecond);
      Serial.print(" ");
      Serial.print(gDay);
      Serial.print("-");
      Serial.print(gMonth);
      Serial.print("-");
      Serial.println(gYear);
      
    }
    else if (iControlValue == 8)
    {
      Serial.print("Hour stepper: ");
      Serial.print(gHourStepper.currentPosition());
      Serial.print(", Minute stepper: ");
      Serial.print(gMinStepper.currentPosition());
      Serial.print(", Second stepper: ");
      Serial.println(gSecStepper.currentPosition());
      
    }
    else if (iControlValue == 20)
    {
      debugTimeSet();
      setSystemTimeNow();
      
    }
    else
    {
      Serial.print("Invalid Control Value: ");
      Serial.println(iControlValue);
    }
  }


  if (iControlCode == 11)
  {
    //HOUR servo manual move commands
    if (iControlValue >= 0 && iControlValue <= 359)
    {
      double dStepperMove;
      dStepperMove = (iControlValue/360.00) * STEPS;  //the compiler will not do floating point math without the .00 on the constant

      gHourStepper.moveTo(dStepperMove);
      Serial.print("HOUR stepper commanded to position: ");
      Serial.println(dStepperMove);
      
    }
    else
    {
      Serial.print("Invalid Control Value: ");
      Serial.println(iControlValue);
    }
  }

  if (iControlCode == 12)
  {
    //MINUTE servo manual move commands
    if (iControlValue >= 0 && iControlValue <= 360)
    {
      double dStepperMove;
      dStepperMove = (iControlValue/360.00) * STEPS;  //the compiler will not do floating point math without the .00 on the constant
      
      gMinStepper.moveTo(dStepperMove);
      Serial.print("Minute stepper commanded to position: ");
      Serial.println(dStepperMove);
      
    }
    else
    {
      Serial.print("Invalid Control Value: ");
      Serial.println(iControlValue);
    }
  }

  if (iControlCode == 13)
  {
    //SECOND servo manual move commands
    if (iControlValue >= 0 && iControlValue <= 360)
    {
      double dStepperMove;
      dStepperMove = (iControlValue/360.00) * STEPS;  //the compiler will not do floating point math without the .00 on the constant

      if (gTempCW == false)
      {
        dStepperMove *= -1;
      }
      
      gSecStepper.moveTo(dStepperMove);
      Serial.print("Second stepper commanded to position: ");
      Serial.println(dStepperMove);
      
    }
    else
    {
      Serial.print("Invalid Control Value: ");
      Serial.println(iControlValue);
    }
  }

  //todo: save to EEPROM for all three motor offset values
  if (iControlCode == 14)
  {
    //HOUR offset commands
    if (iControlValue >= 0 && iControlValue <= 180)
    {
      gHourOffset = iControlValue;
      Serial.print("Hour servo offset: ");
      Serial.println(iControlValue);
      
    }
    else if (iControlValue >= 181 && iControlValue <= 360)
    {
      int iTempMath;
      iTempMath = 181 - iControlValue;
      gHourOffset = iTempMath;
      Serial.print("Hour servo value: ");
      Serial.print(iControlValue);
      Serial.print(" processed as servo offset: ");
      Serial.println(iTempMath);
    }
    else
    {
      Serial.print("Invalid Control Value: ");
      Serial.println(iControlValue);
    }
  }

  if (iControlCode == 15)
  {
    //MINUTE offset commands
    if (iControlValue >= 0 && iControlValue <= 180)
    {
      gMinuteOffset = iControlValue;
      Serial.print("Minute servo offset: ");
      Serial.println(iControlValue);
      
    }
    else if (iControlValue >= 181 && iControlValue <= 360)
    {
      int iTempMath;
      iTempMath = 181 - iControlValue;
      gMinuteOffset = iTempMath;
      Serial.print("Minute servo value: ");
      Serial.print(iControlValue);
      Serial.print(" processed as servo offset: ");
      Serial.println(iTempMath);
    }
    else
    {
      Serial.print("Invalid Control Value: ");
      Serial.println(iControlValue);
    }
  }

  if (iControlCode == 16)
  {
    //SECOND offset commands
    if (iControlValue >= 0 && iControlValue <= 180)
    {
      gSecondOffset = iControlValue;
      Serial.print("Second servo offset: ");
      Serial.println(iControlValue);
      
    }
    else if (iControlValue >= 181 && iControlValue <= 360)
    {
      int iTempMath;
      iTempMath = 181 - iControlValue;
      gSecondOffset = iTempMath;
      Serial.print("Second servo value: ");
      Serial.print(iControlValue);
      Serial.print(" processed as servo offset: ");
      Serial.println(iTempMath);
    }
    else
    {
      Serial.print("Invalid Control Value: ");
      Serial.println(iControlValue);
    }
  }

  if (iControlCode == 17)
  {
    //Manual HOUR setting
    if (iControlValue >= 0 && iControlValue <= 23)
    {
      gHour = iControlValue;
      gTimeSynced = false;  //user is trying to manually set time on  SBC
      Serial.print("Hour set to: ");
      Serial.println(iControlValue);
    }
  }

  if (iControlCode == 18)
  {
    //Manual MINUTE setting
    if (iControlValue >= 0 && iControlValue <= 59)
    {
      gMinute = iControlValue;
      gTimeSynced = false;  //user is trying to manually set time on  SBC
      Serial.print("Minute set to: ");
      Serial.println(iControlValue);
    }
  }

  if (iControlCode == 19)
  {
    //Manual SECOND setting
    if (iControlValue >= 0 && iControlValue <= 59)
    {
      gSecond = iControlValue;
      gTimeSynced = false;  //user is trying to manually set time on  SBC
      Serial.print("Second set to: ");
      Serial.println(iControlValue);
    }
  }

  if (iControlCode == 20)
  {
    //Manual DAY setting
    if (iControlValue >= 0 && iControlValue <= 31)
    {
      gDay = iControlValue;
      gTimeSynced = false;  //user is trying to manually set time on  SBC
      Serial.print("Day set to: ");
      Serial.println(iControlValue);
    }
  }

  if (iControlCode == 21)
  {
    //Manual MONTH setting
    if (iControlValue >= 0 && iControlValue <= 12)
    {
      gMonth = iControlValue;
      gTimeSynced = false;  //user is trying to manually set time on  SBC
      Serial.print("Month set to: ");
      Serial.println(iControlValue);
    }
  }

  if (iControlCode == 22)
  {
    //Manual YEAR setting
    if (iControlValue >= 0 && iControlValue <= 100)
    {
      gYear = 2000 + iControlValue;
      gTimeSynced = false;  //user is trying to manually set time on  SBC
      Serial.print("Year set to: ");
      Serial.println(gYear);
    }
  }
  
}


void debugTimeSet()
{
  
  gTimeSynced = false;
  
  gYear = 2018;
  gMonth = 10;
  gDay = 13;

  gHour = 13;
  gMinute = 49;
  gSecond = 0;

  Serial.println("Debug time set initiated");
  
}


void postAppData()
{
//Backward Analog World Clock (BABwatch)
//Scott Mangiacotti
//Tucson, Arizona USA
//November 2016

  Serial.println("BABwatch");
  Serial.println("Backward Analog World Clock");
  Serial.println("V2");
  Serial.println("By Scott Mangiacotti");
  Serial.println("Tucson, Arizona USA");
  Serial.println("September 2018");
  Serial.println("-----");
  
}
Arduino codeArduino
#include <AccelStepper.h>
#include <MultiStepper.h>
#include <Time.h>
#include <TimeLib.h>
#include <EEPROM.h>

//Backward Analog World Clock (BABwatch_2)
//Scott Mangiacotti
//Tucson, Arizona USA
//September 2018


//Global constants
const int GIVE_BACK = 5;  //number milliseconds delay at end of each scan to ensure not using 100% CPU

//Constants for the steppers
const int STEPS = 4096;
const int HALF_STEP = 8;

const int HOUR_STEPPER_IN1 = 2;
const int HOUR_STEPPER_IN2 = 3;
const int HOUR_STEPPER_IN3 = 4;
const int HOUR_STEPPER_IN4 = 5;

const int MIN_STEPPER_IN1 = 6;
const int MIN_STEPPER_IN2 = 7;
const int MIN_STEPPER_IN3 = 8;
const int MIN_STEPPER_IN4 = 9;

const int SEC_STEPPER_IN1 = 10;
const int SEC_STEPPER_IN2 = 11;
const int SEC_STEPPER_IN3 = 12;
const int SEC_STEPPER_IN4 = 13;

//Global variables
AccelStepper gHourStepper(HALF_STEP, HOUR_STEPPER_IN1, HOUR_STEPPER_IN3, HOUR_STEPPER_IN2, HOUR_STEPPER_IN4);
AccelStepper gMinStepper(HALF_STEP, MIN_STEPPER_IN1, MIN_STEPPER_IN3, MIN_STEPPER_IN2, MIN_STEPPER_IN4);
AccelStepper gSecStepper(HALF_STEP, SEC_STEPPER_IN1, SEC_STEPPER_IN3, SEC_STEPPER_IN2, SEC_STEPPER_IN4);

int gHour;
int gMinute;
int gSecond;
int gDay;
int gMonth;
int gYear;
bool gTimeSynced = false;

bool gTempCW = false;

int gHourOffset = 0;     //units +/- degrees
int gMinuteOffset = 0;   //units +/- degrees
int gSecondOffset = 0;   //units +/- degrees

bool gSystemEnabled = false; //determines if servos are moved to match wallclock time every loop scan


//Run once
void setup()
{

  //Setup static stepper properties
  gHourStepper.setMaxSpeed(1000.0);
  gHourStepper.setAcceleration(100.0);
  gHourStepper.setSpeed(999);

  gMinStepper.setMaxSpeed(1000.0);
  gMinStepper.setAcceleration(100.0);
  gMinStepper.setSpeed(999);

  gSecStepper.setMaxSpeed(1000.0);
  gSecStepper.setAcceleration(100.0);
  gSecStepper.setSpeed(999);
  
  //Initialize serial port
  Serial.begin(9600);

  //Post app data
  postAppData();
  
}


//Run continuous
void loop()
{

  //Parse into separate variables
  if (gTimeSynced == true)
  {
    gHour = hour();
    gMinute = minute();
    gSecond = second();
    gDay = day();
    gMonth = month();
    gYear = year();
  }
  
  if (gSystemEnabled == true)
  {
    if (gTimeSynced == true)
    {
      //Calculate HOUR hand position
      if (gHourStepper.currentPosition() ==  gHourStepper.targetPosition())
      {
        calculateHourMoveTo(gHour);
      }

      //Calculate MINUTE hand position
      if (gMinStepper.currentPosition() ==  gMinStepper.targetPosition())
      {
        calculateMinuteMoveTo(gMinute);
      }
      
      //Calculate SECOND hand position
      if (gSecStepper.currentPosition() ==  gSecStepper.targetPosition())
      {
        calculateSecondMoveTo(gSecond);
      }
      
    }
    else
    {
      gSystemEnabled = false;
      Serial.println("Time not synchronized");
      
    }
  }

  //Process serial messages
  if (Serial.available() > 0)
  {
    processMessage();
    
  }

  //Run the motors until desired position achieved
  gHourStepper.setSpeed(999);
  gHourStepper.runSpeedToPosition();

  gMinStepper.setSpeed(999);
  gMinStepper.runSpeedToPosition();

  gSecStepper.setSpeed(-999);
  gSecStepper.runSpeedToPosition();

  //Give some time back
  delay(GIVE_BACK);

}


//Based on hour value from wallclock time, calculate and move to hour position
//Hour to be passed in 24-hour time
void calculateHourMoveTo(int iHour)
{
  int iCWposition;
  int iCCWposition;
  
  //Validate
  if (iHour < 0 && iHour > 23)
  {
    Serial.println("Invalid hour parameter in function calculateHourMoveTo");
    return;
  }

  //Calculate hour servo position for clockwise clock
  //12 hours per day
  //360 degrees in a full circule
  //360/12 = 30 degrees per hour

  //Convert 24-hour format hour into 12-hour format
  if (iHour > 12)
  {
    iHour = iHour - 12;
  }

  //Calculate position based on above formula
  iCWposition = iHour * 30;

  //Deal with roll-over
  if (iCWposition == 360)
  {
    iCWposition = 0;
  }

  //Calculate counter-clockwise
  //360 - CW position calculated above
  iCCWposition = 360 - iCWposition;

  //Deal with roll-over
  if (iCCWposition == 360)
  {
    iCCWposition = 0;
  }

  //Move stepper. Above math calculates in degrees. Following converts into stepper "steps"
  double dStepperMove;
  dStepperMove = (iCCWposition/360.00) * STEPS;//ilem: convert offset into steps and add
  gHourStepper.moveTo(dStepperMove);
  
}


//Based on minute value from wallclock time, calculate and move to minute position
void calculateMinuteMoveTo(int iMinute)
{

  int iCWposition;
  int iCCWposition;
  
  //Validate
  if (iMinute < 0 && iMinute > 12)
  {
    Serial.println("Invalid minute parameter in function calculateMinuteMoveTo");
    return;
  }

  //Calculate minute servo position for clockwise clock
  //60 minutes per hour
  //360 degrees in a full circule
  //360/60 = 6 degrees per minute

  //Calculate position based on above formula
  iCWposition = iMinute * 6;

  //Deal with roll-over
  if (iCWposition == 360)
  {
    iCWposition = 0;
  }

  //Calculate counter-clockwise
  //360 - CW position calculated above
  iCCWposition = 360 - iCWposition;

  //Deal with roll-over
  if (iCCWposition == 360)
  {
    iCCWposition = 0;
  }

  //Move stepper. Above math calculates in degrees. Following converts into stepper "steps"
  double dStepperMove;
  dStepperMove = (iCCWposition/360.00) * STEPS;//ilem: convert offset into steps and add
  gMinStepper.moveTo(dStepperMove);
  
}


//Based on second value from wallclock time, calculate and move to second position
void calculateSecondMoveTo(int iSecond)
{

  int iCWposition;
  int iCCWposition;
  
  //Validate
  if (iSecond < 0 && iSecond > 12)
  {
    Serial.println("Invalid second parameter in function calculateSecondMoveTo");
    return;
  }

  //Calculate second servo position for clockwise clock
  //60 seconds per minute
  //360 degrees in a full circule
  //360/60 = 6 degrees per second

  //Calculate position based on above formula
  iCWposition = iSecond * 6;

  //Deal with roll-over
  if (iCWposition == 360)
  {
    iCWposition = 0;
  }

  //Calculate counter-clockwise
  //360 - CW position calculated above
  iCCWposition = 360 - iCWposition;

  //Deal with roll-over
  if (iCCWposition == 360)
  {
    iCCWposition = 0;
  }

  //Move stepper. Above math calculates in degrees. Following converts into stepper "steps"
  double dStepperMove;
  dStepperMove = (iCCWposition/360.00) * STEPS;//ilem: convert offset into steps and add
  gSecStepper.moveTo(dStepperMove);

//  if (iSecond == 0 || iSecond == 1)
//  {
//    Serial.println(dStepperMove);
//
//  }

}


//Set SBC time, declare synced and ready to run and display results
void setSystemTimeNow()
{

  setTime(gHour, gMinute, gSecond, gDay, gMonth, gYear);
  gTimeSynced = true;
  Serial.println("System time set successfully");
  
}


//Read data from serial port and process message from power user
//Format is: XXnnnn
//XX is a value between 1 - 32 and represents the command type or area (for example manual commands to the HOUR servo motor)
//nnnn is a value between 0-1000 and represents the value for the target command type
//For example, 01180 is type 02 and value 180. It represents HOUR servo motor move to position 180 degrees
//See documentation for command definitions and value ranges
void processMessage()
{
  int iMessage;
  int iControlCode;
  int iControlValue;

  //Read the data in the serial port buffer
  iMessage = Serial.parseInt();
  Serial.print("Message received: ");
  Serial.println(iMessage);


  //Process the serial port message
  if (iMessage > 0)
  {
    iControlCode = iMessage/1000;
    Serial.print("Control Code: ");
    Serial.println(iControlCode);
    
    iControlValue = iMessage % 1000;
    Serial.print("Control Value: ");
    Serial.println(iControlValue);
  }

  //Misc control and command codes
  if (iControlCode == 10)
  {
    //Control codes and commands
    if (iControlValue == 1)
    {
      gSystemEnabled = true;
      Serial.println("System Enabled");
      
    }
    else if (iControlValue == 2)
    {
      gSystemEnabled = false;
      Serial.println("System Disabled");
      
    }
    else if (iControlValue == 3)
    {
      gTempCW = !gTempCW;
      if (gTempCW == true)
      {
        Serial.println("manual moves command clockwise");
      }
      else
      {
        Serial.println("manual moves command counter-clockwise");
      }
      
    }
    else if (iControlValue == 4)
    {
      //spare
      
    }
    else if (iControlValue == 5)
    {
      setSystemTimeNow();
      
    }
    else if (iControlValue == 6)
    {
      postAppData();
      
    }
    else if (iControlValue == 7)
    {
      Serial.print(gHour);
      Serial.print(":");
      Serial.print(gMinute);
      Serial.print(":");
      Serial.print(gSecond);
      Serial.print(" ");
      Serial.print(gDay);
      Serial.print("-");
      Serial.print(gMonth);
      Serial.print("-");
      Serial.println(gYear);
      
    }
    else if (iControlValue == 8)
    {
      Serial.print("Hour stepper: ");
      Serial.print(gHourStepper.currentPosition());
      Serial.print(", Minute stepper: ");
      Serial.print(gMinStepper.currentPosition());
      Serial.print(", Second stepper: ");
      Serial.println(gSecStepper.currentPosition());
      
    }
    else if (iControlValue == 20)
    {
      debugTimeSet();
      setSystemTimeNow();
      
    }
    else
    {
      Serial.print("Invalid Control Value: ");
      Serial.println(iControlValue);
    }
  }


  if (iControlCode == 11)
  {
    //HOUR servo manual move commands
    if (iControlValue >= 0 && iControlValue <= 359)
    {
      double dStepperMove;
      dStepperMove = (iControlValue/360.00) * STEPS;  //the compiler will not do floating point math without the .00 on the constant

      gHourStepper.moveTo(dStepperMove);
      Serial.print("HOUR stepper commanded to position: ");
      Serial.println(dStepperMove);
      
    }
    else
    {
      Serial.print("Invalid Control Value: ");
      Serial.println(iControlValue);
    }
  }

  if (iControlCode == 12)
  {
    //MINUTE servo manual move commands
    if (iControlValue >= 0 && iControlValue <= 360)
    {
      double dStepperMove;
      dStepperMove = (iControlValue/360.00) * STEPS;  //the compiler will not do floating point math without the .00 on the constant
      
      gMinStepper.moveTo(dStepperMove);
      Serial.print("Minute stepper commanded to position: ");
      Serial.println(dStepperMove);
      
    }
    else
    {
      Serial.print("Invalid Control Value: ");
      Serial.println(iControlValue);
    }
  }

  if (iControlCode == 13)
  {
    //SECOND servo manual move commands
    if (iControlValue >= 0 && iControlValue <= 360)
    {
      double dStepperMove;
      dStepperMove = (iControlValue/360.00) * STEPS;  //the compiler will not do floating point math without the .00 on the constant

      if (gTempCW == false)
      {
        dStepperMove *= -1;
      }
      
      gSecStepper.moveTo(dStepperMove);
      Serial.print("Second stepper commanded to position: ");
      Serial.println(dStepperMove);
      
    }
    else
    {
      Serial.print("Invalid Control Value: ");
      Serial.println(iControlValue);
    }
  }

  //todo: save to EEPROM for all three motor offset values
  if (iControlCode == 14)
  {
    //HOUR offset commands
    if (iControlValue >= 0 && iControlValue <= 180)
    {
      gHourOffset = iControlValue;
      Serial.print("Hour servo offset: ");
      Serial.println(iControlValue);
      
    }
    else if (iControlValue >= 181 && iControlValue <= 360)
    {
      int iTempMath;
      iTempMath = 181 - iControlValue;
      gHourOffset = iTempMath;
      Serial.print("Hour servo value: ");
      Serial.print(iControlValue);
      Serial.print(" processed as servo offset: ");
      Serial.println(iTempMath);
    }
    else
    {
      Serial.print("Invalid Control Value: ");
      Serial.println(iControlValue);
    }
  }

  if (iControlCode == 15)
  {
    //MINUTE offset commands
    if (iControlValue >= 0 && iControlValue <= 180)
    {
      gMinuteOffset = iControlValue;
      Serial.print("Minute servo offset: ");
      Serial.println(iControlValue);
      
    }
    else if (iControlValue >= 181 && iControlValue <= 360)
    {
      int iTempMath;
      iTempMath = 181 - iControlValue;
      gMinuteOffset = iTempMath;
      Serial.print("Minute servo value: ");
      Serial.print(iControlValue);
      Serial.print(" processed as servo offset: ");
      Serial.println(iTempMath);
    }
    else
    {
      Serial.print("Invalid Control Value: ");
      Serial.println(iControlValue);
    }
  }

  if (iControlCode == 16)
  {
    //SECOND offset commands
    if (iControlValue >= 0 && iControlValue <= 180)
    {
      gSecondOffset = iControlValue;
      Serial.print("Second servo offset: ");
      Serial.println(iControlValue);
      
    }
    else if (iControlValue >= 181 && iControlValue <= 360)
    {
      int iTempMath;
      iTempMath = 181 - iControlValue;
      gSecondOffset = iTempMath;
      Serial.print("Second servo value: ");
      Serial.print(iControlValue);
      Serial.print(" processed as servo offset: ");
      Serial.println(iTempMath);
    }
    else
    {
      Serial.print("Invalid Control Value: ");
      Serial.println(iControlValue);
    }
  }

  if (iControlCode == 17)
  {
    //Manual HOUR setting
    if (iControlValue >= 0 && iControlValue <= 23)
    {
      gHour = iControlValue;
      gTimeSynced = false;  //user is trying to manually set time on  SBC
      Serial.print("Hour set to: ");
      Serial.println(iControlValue);
    }
  }

  if (iControlCode == 18)
  {
    //Manual MINUTE setting
    if (iControlValue >= 0 && iControlValue <= 59)
    {
      gMinute = iControlValue;
      gTimeSynced = false;  //user is trying to manually set time on  SBC
      Serial.print("Minute set to: ");
      Serial.println(iControlValue);
    }
  }

  if (iControlCode == 19)
  {
    //Manual SECOND setting
    if (iControlValue >= 0 && iControlValue <= 59)
    {
      gSecond = iControlValue;
      gTimeSynced = false;  //user is trying to manually set time on  SBC
      Serial.print("Second set to: ");
      Serial.println(iControlValue);
    }
  }

  if (iControlCode == 20)
  {
    //Manual DAY setting
    if (iControlValue >= 0 && iControlValue <= 31)
    {
      gDay = iControlValue;
      gTimeSynced = false;  //user is trying to manually set time on  SBC
      Serial.print("Day set to: ");
      Serial.println(iControlValue);
    }
  }

  if (iControlCode == 21)
  {
    //Manual MONTH setting
    if (iControlValue >= 0 && iControlValue <= 12)
    {
      gMonth = iControlValue;
      gTimeSynced = false;  //user is trying to manually set time on  SBC
      Serial.print("Month set to: ");
      Serial.println(iControlValue);
    }
  }

  if (iControlCode == 22)
  {
    //Manual YEAR setting
    if (iControlValue >= 0 && iControlValue <= 100)
    {
      gYear = 2000 + iControlValue;
      gTimeSynced = false;  //user is trying to manually set time on  SBC
      Serial.print("Year set to: ");
      Serial.println(gYear);
    }
  }
  
}


void debugTimeSet()
{
  
  gTimeSynced = false;
  
  gYear = 2018;
  gMonth = 10;
  gDay = 13;

  gHour = 13;
  gMinute = 49;
  gSecond = 0;

  Serial.println("Debug time set initiated");
  
}


void postAppData()
{
//Backward Analog World Clock (BABwatch)
//Scott Mangiacotti
//Tucson, Arizona USA
//November 2016

  Serial.println("BABwatch");
  Serial.println("Backward Analog World Clock");
  Serial.println("V2");
  Serial.println("By Scott Mangiacotti");
  Serial.println("Tucson, Arizona USA");
  Serial.println("September 2018");
  Serial.println("-----");
  
}

Custom parts and enclosures

Instructions
babwatch_2_miIiFYfm00.xlsx

Schematics

Schematic

Comments

Similar projects you might like

Analog Clock with LED Matrix and Arduino

Project tutorial by LAGSILVA

  • 12,237 views
  • 8 comments
  • 39 respects

Talking Clock 2 - New Version (Bilingual: EN-PT)

Project tutorial by LAGSILVA

  • 7,347 views
  • 19 comments
  • 33 respects

Simple Arduino Digital Clock Without RTC

Project in progress by Annlee Fores

  • 96,298 views
  • 23 comments
  • 80 respects

Complete Digital Clock Including Alarm and Motion Sensor

Project tutorial by LAGSILVA

  • 12,677 views
  • 11 comments
  • 50 respects

Arduino Without External Clock Crystal on ATmega328

Project tutorial by Techmirtz

  • 9,468 views
  • 2 comments
  • 11 respects

Amazing Binary Clock using LED Matrix Module

Project tutorial by LAGSILVA

  • 9,351 views
  • 10 comments
  • 50 respects
Add projectSign up / Login