Sunday, June 22, 2014

Wise Clock chronometer completed

As I briefly mentioned in my previous post, I re-started working on the Wise Clock chronometer. That old version only displayed the time from the RTC, and lacked basic user functions like setting the time from buttons. Even though it was called "chronometer" it did not have a proper timer functionality (besides showing the time with seconds).

I implemented the new software(*) (available here) as a state machine, easy to describe, understand and upgrade. Below is the diagram with the states and transitions.


This clock uses the same board as Wise Clock 4, but a jumper connects SQW of DS3231 to D2 (INT2 interrupt pin of ATmega1284). This allows the software to take advantage of an ISR being called every second, which in turn updates the time on the display. Because this approach is in contrast to the existing Wise Clock 4 software (which did polling on RTC to update the time on the screen), its integration into the main trunk would be difficult, if not impossible.

In any case, the state machine "framework" can be replicated to most devices that can be described as a set of states with transitions between them. For example, for a device with 3 buttons like Wise Clock 4, the "framework" consists of 5 important functions:

1. the main loop() that checks the actions on the buttons;

void loop()
{
  int userAction = plusButton.checkButton();
  if (userAction != NO_ACTION)
  {
    // SET button was pressed;
    processPlusButton(userAction);
  }
  else
  {
    userAction = setButton.checkButton();
    if (userAction != NO_ACTION)
    {
      // SET button was pressed;
      processSetButton(userAction);
    }
    else
    {
      userAction = menuButton.checkButton();
      if (userAction != NO_ACTION)
      {
        // MENU button was pressed;
        processMenuButton(userAction);
      }
    }
  }
  executeState();
}

2. the function executeState(), which implements the functionality of each individual state;

void executeState()
{
  switch (crtState)
  {
    case STATE_SHOW_TIME:
      if (bUpdate)
      {
        bUpdate = false;
        getTimeFromRTC();
        displayDynamicTime(hours, minutes, seconds, &lastHour, &lastMin, &lastSec);
      }
      break;

    case STATE_COUNTDOWN:
      if (bUpdate)
      {
        bUpdate = false;
        if (isCountdownInProgress)
        {
          calculateTimeLeft();  // set cHours, sMinutes, cSeconds;
          displayDynamicCountdown(cHours, cMinutes, cSeconds, &lastCHour, &lastCMin, &lastCSec);

          if (cHours==0 && cMinutes==0 && cSeconds==0)
          {
            // countdown ends;
            isCountdownInProgress = false;
            showStaticTime(0, RED, 0, RED, 0, RED);
            beep();
          }
        }
      }
      break;

    case STATE_SET_TIME_HOUR:
      showStaticTime(hours, RED, minutes, crtColor, seconds, crtColor);
      break;

    case STATE_SET_TIME_MIN:
      showStaticTime(hours, crtColor, minutes, RED, seconds, crtColor);
      break;

    case STATE_SET_TIME_SEC:
      showStaticTime(hours, crtColor, minutes, crtColor, seconds, RED);
      break;

    case STATE_SET_CNTDWN_HOUR:
      showStaticTime(cHours, RED, cMinutes, ORANGE, cSeconds, ORANGE);
      break;

    case STATE_SET_CNTDWN_MIN:
      showStaticTime(cHours, ORANGE, cMinutes, RED, cSeconds, ORANGE);
      break;

    case STATE_SET_CNTDWN_SEC:
      showStaticTime(cHours, ORANGE, cMinutes, ORANGE, cSeconds, RED);
      break;
  }
}

3. one processing function per button (hence 3 functions: processPlusButton(), processMenuButton(), processSetButton()), each describing the transitions from every state based on user input;

void processPlusButton(int userAction)
{
  switch (crtState)
  {
    case STATE_SET_TIME_HOUR:
      if (userAction == BTN_PUSH)
      {
        hours++;
        if (hours>23) hours = 0;
      }
      break;

    case STATE_SET_TIME_MIN:
      if (userAction == BTN_PUSH)
      {
        minutes++;
        if (minutes>59) minutes = 0;
      }
      else if (userAction == BTN_DBL_PUSH)
      {
        minutes = minutes + 10;
        if (minutes>59) minutes = 0;
      }
      break;

    case STATE_SET_TIME_SEC:
      if (userAction == BTN_PUSH)
      {
        seconds++;
        if (seconds>59) seconds = 0;
      }
      else if (userAction == BTN_DBL_PUSH)
      {
        seconds = seconds + 10;
        if (seconds>59) seconds = 0;
      }
      break;

    case STATE_SET_CNTDWN_HOUR:
      if (userAction == BTN_PUSH)
      {
        cHours++;
        if (cHours>23) cHours = 0;
      }
      break;

    case STATE_SET_CNTDWN_MIN:
      if (userAction == BTN_PUSH)
      {
        cMinutes++;
        if (cMinutes>59) cMinutes = 0;
      }
      else if (userAction == BTN_DBL_PUSH)
      {
        cMinutes = cMinutes + 10;
        if (cMinutes>59) cMinutes = 0;
      }
      break;

    case STATE_SET_CNTDWN_SEC:
      if (userAction == BTN_PUSH)
      {
        cSeconds++;
        if (cSeconds>59) cSeconds = 0;
      }
      else if (userAction == BTN_DBL_PUSH)
      {
        cSeconds = cSeconds + 10;
        if (cSeconds>59) cSeconds = 0;
      }
      break;

    case STATE_SHOW_TIME:
      if (userAction == BTN_PUSH)
      {
        // change brightness;
        incrementBrightness();
      }
  }
}

void processMenuButton(int userAction)
{
  ht1632_clear();
  switch (crtState)
  {
    case STATE_SHOW_TIME:
      if (userAction == BTN_PUSH)
      {
// push MENU button to start setting up the start time for countdown;
        crtState = STATE_SET_CNTDWN_HOUR;
      }
      else if (userAction == BTN_HOLD)
      {
        crtState = STATE_SET_TIME_HOUR;
      }
      break;

    case STATE_SET_TIME_HOUR:
    case STATE_SET_TIME_MIN:
    case STATE_SET_TIME_SEC:
      if (userAction == BTN_PUSH)
      {
//----------------------------------------------------------------
// pressed the MENU button while setting the time;
// exit setting the time and resume time showing;
//----------------------------------------------------------------
        // save the time;
        setRtcTime();
        // return to the main screen (showing the time);
        crtColor = GREEN;
crtState = STATE_SHOW_TIME;
        showStaticTime((is24Hmode? hours : hours%12), crtColor, minutes, crtColor, seconds, crtColor);
        // also update the lastSec (for smooth rolling);
        lastSec = seconds;
      }
      break;

    case STATE_SET_CNTDWN_HOUR:
    case STATE_SET_CNTDWN_MIN:
    case STATE_SET_CNTDWN_SEC:
      if (userAction == BTN_PUSH)
      {
//----------------------------------------------------------------
// pressed the MENU button while setting the countdown time;
// exit setting the countdown time and start the countdown now;
//----------------------------------------------------------------
        isCountdownInProgress = true;
// total number of seconds for countdown;
cntDownTime = (long)((cHours * 3600l) + (cMinutes * 60) + cSeconds);
        // start displaying the countdown;
        crtColor = ORANGE;
        showStaticTime(cHours, crtColor, cMinutes, crtColor, cSeconds, crtColor);
        crtState = STATE_COUNTDOWN;
      }
      break;

    case STATE_COUNTDOWN:
      // pressing MENU while counting down will revert to normal clock mode;
      isCountdownInProgress = false;
      cHours = cMinutes = cSeconds = 0;
      // return to the main screen (showing the time);
      crtColor = GREEN;
      crtState = STATE_SHOW_TIME;
      showStaticTime((is24Hmode? hours : hours%12), crtColor, minutes, crtColor, seconds, crtColor);
      break;
  }
}

void processSetButton(int userAction)
{
  switch (crtState)
  {
    case STATE_SET_TIME_HOUR:
      if (userAction == BTN_PUSH)
      {
        crtState = STATE_SET_TIME_MIN;
      }
      break;

    case STATE_SET_TIME_MIN:
      if (userAction == BTN_PUSH)
      {
        crtState = STATE_SET_TIME_SEC;
      }
      break;

    case STATE_SET_TIME_SEC:
      if (userAction == BTN_PUSH)
      {
        crtState = STATE_SET_TIME_HOUR;
      }
      break;

    case STATE_SET_CNTDWN_HOUR:
      if (userAction == BTN_PUSH)
      {
        crtState = STATE_SET_CNTDWN_MIN;
      }
      break;

    case STATE_SET_CNTDWN_MIN:
      if (userAction == BTN_PUSH)
      {
        crtState = STATE_SET_CNTDWN_SEC;
      }
      break;

    case STATE_SET_CNTDWN_SEC:
      if (userAction == BTN_PUSH)
      {
        crtState = STATE_SET_CNTDWN_HOUR;
      }
      break;

    case STATE_SHOW_TIME:
      if (userAction == BTN_DBL_PUSH)
      {
        // change from 24H to 12H mode and viceversa;
        is24Hmode = !is24Hmode;
      }
      else if (userAction == BTN_PUSH)
      {
        // change font while showing time;
        crtFont++;
        if (crtFont >= NUMBER_OF_FONTS) crtFont=0;
        ht1632_clear();
      }
      else if (userAction == BTN_HOLD)
      {
        // change display mode between rolling and replace;
        displayMode++;
        if (displayMode > 1) displayMode=0;
      }
      // force screen refresh;
      showStaticTime((is24Hmode? hours : hours%12), crtColor, minutes, crtColor, seconds, crtColor);
      break;
  }
}

I did not use timeouts in the current state machine. I thought that timeouts would complicate the user experience, considering that one of the most important functions is the input of countdown time. Timing out from that input was not an option, I think. Therefore, the user is not forced to set up the time in a certain interval.

(*) NOTE: I developed the current software using Arduino 22 (because I started from the old version and I was too lazy to copy it over). To port to Arduino 1.0 and above, the "include WProgram.h" needs to be changed. But you already know the drill :)

No comments:

Post a Comment