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 :)

Friday, June 20, 2014

Two-faced ("Kandinsky") Wise Clock

I managed to get the "Kandinsky" two-faced Wise Clock working with most of the relevant apps. The physical build looks and feels solid.




The software changes cover both single Kandinsky (2 opposite displays) and the double Kandinsky (4 displays, 2 on each side).

This new feature helped with eliminating a few bugs related to X_MAX (mostly wrong or hard-coded values). There are still a few remaining, but they only come to light on 4-display Wise Clocks (list from Nick):

'BIG' - only displays on the left two displays;
'TIX' - only displays on the left two displays;
'Words' - looks odd;
'Stopw' - only displays on the left two displays but time is centered;
'Score' - again, not centered; plus the button functions are swapped;
'Pacmn' - doesn't work at all;
'Lived' - again, not centered;
'Anim' - again, not centered;
'UTC' - again, not centered;
'Life' - fails totally, appears to revert to previous APP.

So, lots of work still....

I also resurrected the Wise Clock chronometer for a customer. The updated software will feature new fonts (one shown in the photo below), countdown timer, two-face (of course :), setting the time from buttons etc. Note that this code is on a different trunk than the Wise Clock code. It uses the 1Hz SQW interrupt from DS3231 to update the time on the screen.



Tuesday, June 17, 2014

Stuff

A customer recently requested a two-faced Wise Clock, basically two displays back-to-back connected to the same Wise Clock board, and showing the same thing (in the same time) on both sides. This would be another "Kandinsky" clock (see this post for pictures of the original; name coined by Wyolum, I believe).

To include this feature in the current software was not trivial. The main idea was to define a new macro (WANT_TWO_FACE) and play with it around existing calls to HT1632 functions. This was the idea. The actual implementation is quite messy and spreads over 18 or so (mostly the "app") files. In any case, I only beta-tested it with 2 displays and works in all modes except "Big" (I didn't bother to investigate yet).

Here are a few photos.



(The second display has a defect, hence the crooked last 0.)

Next round of testing should cover the 4-display 2-faced clock, that is, 2 displays on each side, like a double dual Wise Clock (I know, it's confusing. I will probably ask permission to rename it "Single Kandinsky Wise Clock" and "Double Kandinsky Wise Clock" since I am running out of attributes. Just kidding.)

On a different front, my friend Nick built a 4-display Wise Clock 4 and sent me this photo (plus a set of laser-cut plates to build one myself too; thanks again Nick). Apparently, the software for the 4-display clock has some bugs, so expect soon a new code release (which will include the "two-faced" feature as well).



And lastly, I found this board on ebay. No, I did not buy it (and I am not going to). Just look carefully at the photo and think how you would actually use it, with a knob inserted on the pot's shaft.