J Koger
Created March 30, 2016 © GPL3+

Time-of-day Color Watch

The O-Watch uses hours to set red level, minutes for green, and seconds for blue so that it changes color constantly throughout the day.

BeginnerProtip1 hour108
Time-of-day Color Watch

Things used in this project

Hardware components

O Watch Base Kit
O Watch Base Kit
×1

Story

Read more

Code

color_watch.ino

Arduino
Time-of-day color watch code for the O-Watch.
/*
 * Color Watch
 * 
 * Watch face that changes color based on the RGB values of the current time. It uses 16-bit
 * color depth to give us 5 bits of blue and red, and 6 bits of green BBBBBGGGGGGRRRRR.
 * The current time is displayed in white if the background is darker, or black if the
 * background is lighter. To make the transitions smooth, all text is drawn into a screen
 * buffer then sent all at once to the TinyScreen via display.writeBuffer(), similar to
 * how the Flappy Birdz demo for the O-Watch does it.
 * 
 * The color is based on red from hours (0 - 23), green from minutes (0 - 59), and blue
 * from seconds (0 - 59 modulo 32). Press the upper-right button to rotate the order from
 * RGB (H->R, M->G, S->B) to BRG (H->B, M->R, S->G), and again to GBR (H->G, M->B, S->R), and
 * again to go back to RGB.
 * 
 * Demonstrates the use of the Arduino Time library and Arduino Zero Real-Time-Clock
 * (RTC) library. Very bright and colorful. No dimmer logic on this one!
 * 
 * Inspired by Jack Hughes' Colour Clock http://lookatjack.com/
 *
 * Uses Arduino Time library http://playground.arduino.cc/code/time
 * Maintained by Paul Stoffregen https://github.com/PaulStoffregen/Time
 *
 * Uses Arduino Zero RTC library https://www.arduino.cc/en/Reference/RTC
 * Maintained by Arturo Guadalupi https://github.com/arduino-libraries/RTCZero
 *
 * This code is for the TinyScreen+ board by TinyCircuits used by O Watch http://tiny-circuits.com 
 *
 * This example is based on the Simple Watch code created by O Watch on 6 March 2016 http://theowatch.com
 * Modified to use RTC, to set the display brightness based on time of day, and to use __DATE__ and __TIME__
 * to preset the watch's date and time by J Koger 13 March 2016
 * Modified to display the date and time using a vector font by J Koger 20 March 2016
 * Modified to make a time-based color watch by J Koger 29 March 2016
 * 
*/

#include <TinyScreen.h> //include TinyScreen library
#include <TimeLib.h> //include the Arduino Time library
#include <RTCZero.h> //include the Arduino Zero's Real Time Clock library
#include <stdio.h>  // include the C++ standard IO library


// The dimensions of our lovely TinyScreen.
#define SCREEN_WIDTH  96
#define SCREEN_HEIGHT 64

// Determine which color goes with hours, minutes, seconds
#define RGB     1   // Default. Red = hours, green = minutes, blue = seconds
#define BRG     2   // Blue = hours, red = minutes, green = seconds
#define GBR     3   // Green = hours, blue = minutes, red = seconds

/* Create an rtc object */
RTCZero rtc;

// We'll dynamically change these values to set the current initial time
// No need to change them here.
byte seconds = 0;
byte minutes = 45;
byte hours = 9;

// We'll dynamically change these values to set the current initial date
// The preset values are only examples.
byte days = 13;
byte months = 3;
byte years = 16;

byte colorTimeMap = RGB;

TinyScreen display = TinyScreen(TinyScreenPlus); //Create the TinyScreen object


typedef struct
{
  uint8_t     col[SCREEN_WIDTH * 2];
} ScreenRow;

typedef struct
{
  ScreenRow   row[SCREEN_HEIGHT];
} ScreenBuffer;

ScreenBuffer   scrBuf;

/* Take the contents of our ScreenBuffer and send them
 * to the O-Watch's TinyScreen. We'd use DMA transfer
 * to speed this up, but that's a little experimental
 * right now.
 */
void sendBufferToScreen()
{
  display.startData();
  for (int y = 0; y < SCREEN_HEIGHT; y++)
  {
    display.writeBuffer(&(scrBuf.row[y].col[0]), SCREEN_WIDTH * 2);
  }
  display.endTransfer();
}


/*
 * Erase the screen buffer with specified color.
 */
void clearScreenBuffer(uint16_t color)
{
  for (int y = 0; y < SCREEN_HEIGHT; y++)
  {
    for (int x = 0; x < SCREEN_WIDTH; x++)
    {
        scrBuf.row[y].col[2 * x] = color >> 8;
        scrBuf.row[y].col[(2 * x) + 1] = color & 0xFF;
    }
  }
}

/*
 * Draw a single pixel into the screen buffer. Make
 * sure it has valid coordinates before drawing it.
 */
void drawPixelInBuffer(int x, int y, uint16_t color)
{
  if ((x >= 0) && (x < SCREEN_WIDTH) && (y >= 0) && (y < SCREEN_HEIGHT))
  {
    scrBuf.row[y].col[2 * x] = color >> 8;
    scrBuf.row[y].col[(2 * x) + 1] = color & 0xFF;
  }
}

/*
 * Print a string of ASCII characters into the screen buffer. This routine uses the
 * same font info that the TinyScreen routines do, so as to not use up more RAM with
 * a second copy of the font tables.
 */
void printStringInBuffer(int x, int y, char * s, const FONT_INFO& fontInfo, uint16_t textColor, uint16_t backgroundColor)
{
  // The character descriptor includes the character's width in pixels,
  // and offset into the fontInfo.bitmap table. fontInfo.charDesc
  // is a table of one descriptor for each printable ASCII character in the font.
  const FONT_CHAR_INFO* fontDescriptor = fontInfo.charDesc;
  
  // The font height is the height of the font in pixels.
  int fontHeight = fontInfo.height;

  // The character bitmap is one or more bytes per row of
  // pixel on/off bits. The number of bytes per row is
  // set by fontInfo.charDesc.width / 8 bits per pixel.
  // The offset of a character in the bitmap table is set
  // by fontInfo.charDesc.offset.
  const unsigned char* fontBitmap = fontInfo.bitmap;

  // The first ASCII character supported by the font. This
  // is typically the space ' ' character.
  int fontFirstCh = fontInfo.startCh;
  // The last ASCII character supported by the font this is
  // typically the tilde '~' character.
  int fontLastCh = fontInfo.endCh;

  // Walk through the string, and print each character in it.
  for (int curChar = 0; s[curChar] != '\0'; curChar++) // Stop at the string's null terminator.
  {
      // Determine the offset into tables for this character by
      // subtracting the ASCII value of the font's first supported character
      // from the ASCII value of this character.
      int chOffset = s[curChar] - fontFirstCh;
      
      // Start by getting the width in pixels of the current character.
      // This is stored in the same table used by the TinyScreen libraries,
      // so go get it by doing a near-memory access using the pgm_read_byte()
      // macro.  
      uint8_t chWidth = pgm_read_byte(&fontDescriptor[chOffset].width);

      // Figure out how many bytes per row based on the width of the character
      // in pixels divided by 8 pixels (bits) per byte. If the number of pixels
      // per row doesn't divide neatly by 8, add another byte for the leftovers.
      // For example, a character that is 1 pixel wide (like '!' in a thin font)
      // still gets 1 byte per row. 
      int bytesPerRow = chWidth / 8;
      if ((chWidth % 8) != 0)
        bytesPerRow++;

      // Figure out the offset into the bitmap table for this character.
      // The character bitmaps are upside down in the font tables from how
      // we look at the display, which is to say that the lowest row is first,
      // and the top row is last. So, we keep track of the offset into the bitmap
      // by starting at the last row of bytes in the bitmap and working backwards.
      uint16_t offset = pgm_read_word(&fontDescriptor[chOffset].offset) + (bytesPerRow * fontHeight) - 1;

      // Walk through the rows in the current character's bitmap, and look at each
      // bit in the current byte to determine if the equivalent pixel is on (draw the
      // pixel in textColor) or off (draw the pixel in backgroundColor).
      for (uint8_t curRow = 0; curRow < fontHeight; curRow++)
      {
        int totalBits = 0;
        for (int curByte = 0; curByte < bytesPerRow; curByte++)
        {
          // Get the one-byte bitmap for the current byte within the current row.
          // Because the bitmaps for characters with more than 1 byte per row are
          // arranged in row x column order, the second byte in the current row 
          // will be bytesPerRow after the first byte. So, overall, we start with
          // the offset into the array of bitmaps, fontDescriptor[chOffset].offset.
          // THEN, because the font bitmaps go from bottom to top, and we're drawing
          // from top to bottom, we need to advance our offset out to the final row
          // of the bitmap: add bytesPerRow * fontHeight bytes. THEN adjust BACK
          // from there to account for the current row. FINALLY we need to adjust for
          // how the bytes are arranged in the current row. Whew!
          uint8_t bitmap = pgm_read_byte(&fontBitmap[offset - curRow - ((bytesPerRow - curByte - 1) * fontHeight)]);

          // Now walk through the bits in the current bitmap byte to see which should be 'on'
          // (displayed in textColor) or 'off' (displayed in backgroundColor). Highest-order
          // bit is the leftmost pixel in the character.
          for (uint8_t curBit = 0, bitSelector = 0x80; curBit < 8; curBit++, bitSelector >>= 1)
          {
            if (bitmap & bitSelector)
              drawPixelInBuffer(x + totalBits, y + curRow, textColor);
            else
              drawPixelInBuffer(x + totalBits, y + curRow, backgroundColor);
            if (totalBits >= chWidth)
              break;
            totalBits++;
          }
        }
      }
      x += chWidth + 1;
    }
}


uint8_t getPrintWidth(char *s, const FONT_INFO& fontInfo)
{
  const FONT_CHAR_INFO* fontDescriptor = fontInfo.charDesc;
  int fontFirstCh = fontInfo.startCh;
  uint8_t len, totalWidth = 0;
  len = strlen(s);
  for(uint8_t i = 0; i < len; i++)
  {
    totalWidth += pgm_read_byte(&fontDescriptor[s[i] - fontFirstCh].width) + 1;
  }
  return(totalWidth);
}


// Print a string on the TinyScreen centered horizontally, using the current font.
void printCentered(char *s, uint8_t yOffset, const FONT_INFO& fontInfo, uint16_t textColor, uint16_t backgroundColor)
{
  uint8_t   printWidth, xOffset;

  printWidth = getPrintWidth(s, fontInfo);
  xOffset = (SCREEN_WIDTH - printWidth) / 2;
  // display.setCursor(xOffset, yOffset);
  // display.print(s);
  printStringInBuffer(xOffset, yOffset, s, fontInfo, textColor, backgroundColor);
}


void setup()
{
  char s_month[5];
  int tmonth, tday, tyear, thour, tminute, tsecond;
  static const char month_names[] = "JanFebMarAprMayJunJulAugSepOctNovDec";

  display.begin();                            //Initializes TinyScreen board
  display.setFlip(1);                         //Flips the TinyScreen rightside up for O Watch
  display.setBitDepth(TSBitDepth16);          // 16-bit graphics, none of that puny 8-bit stuff for us!
  display.setBrightness(12);                  // Max brightness all the time!

  rtc.begin(); // initialize RTC
  
  // Set the time and date. Change this to your current date and time.
  // setTime(16,19,00,12,3,2016);    //values in the order hr,min,sec,day,month,year

  // Let's be lazy and let the compiler set the current date and time for us.
  // This will be a few seconds off due to the time it takes to compile the
  // .ino file and upload the app. But pretty close.

  // __DATE__ is a C++ preprocessor string with the current date in it.
  // It will look something like 'Mar  13  2016'.
  // So we need to pull those values out and convert the month string to a number.
  sscanf(__DATE__, "%s %d %d", s_month, &tday, &tyear);

  // Similarly, __TIME__ will look something like '09:34:17' so get those numbers.
  sscanf(__TIME__, "%d:%d:%d", &thour, &tminute, &tsecond);

  // Find the position of this month's string inside month_names, do a little
  // pointer subtraction arithmetic to get the offset, and divide the
  // result by 3 since the month names are 3 chars long.
  tmonth = (strstr(month_names, s_month) - month_names) / 3;

  months = tmonth + 1;  // The RTC library expects months to be 1 - 12.
  days = tday;
  years = tyear - 2000; // The RTC library expects years to be from 2000.
  hours = thour;
  minutes = tminute;
  seconds = tsecond;

  rtc.setTime(hours, minutes, seconds);
  rtc.setDate(days, months, years);
}


void displayTime()
{
  uint16_t      red, green, blue, backgroundColor, timeColor;
  char          s[64];

  display.on();                               //Turns TinyScreen display on

  for (int i = 0; i < 200; i++) // Display for 200 tenth-seconds, look for button press every tenth-second
  {
    // Get the complete date and time now since they're used for the weekday
    // and brightness calculations.
    months = rtc.getMonth();
    days = rtc.getDay();
    years = rtc.getYear();
    hours = rtc.getHours();
    minutes = rtc.getMinutes();
    seconds = rtc.getSeconds();
    setTime(hours,minutes,seconds,days,months,2000 +  years);    //values in the order hr,min,sec,day,month,year

    // Figure out the color based on the current time. Red for hours (5 bits), green for minutes (6 bits), blue for seconds
    // (5 bits). Then glom them together into a 16 bit unsigned int: BBBBBGGGGGGRRRRR.
    if (colorTimeMap == RGB)
    {
      red = hours;  // 0 - 23, less than 5 full bits (16 + 8 + 4 + 2 + 1 = 31).
      green = minutes; // 0 - 59, less than 6 full bits (32 + 16 + 8 + 4 + 2 + 1 = 63).
      blue = seconds % 32; // 0 - 59, greater than 5 full bits, so take modulus.
    }
    else if (colorTimeMap == BRG)
    {
      blue = hours;  // 0 - 23, less than 5 full bits (16 + 8 + 4 + 2 + 1 = 31).
      red = minutes % 32; // 0 - 59, greater than 5 full bits, so take modulus.
      green = seconds; // 0 - 59, less than 6 full bits (32 + 16 + 8 + 4 + 2 + 1 = 63).
    }
    else // colorTimeMap == GBR
    {
      green = hours;  // 0 - 23, less than 6 full bits (32 + 16 + 8 + 4 + 2 + 1 = 63).
      blue = minutes % 32; // 0 - 59, greater than 5 full bits, so take modulus.
      red = seconds % 32; // 0 - 59, greater than 5 full bits, so take modulus.
    }
    
    backgroundColor = (blue << 11) | (green << 5) | red;

    // Switch from white text to black if any of the color components is really bright.
    if ((red >= 24) || (green >= 48) || (blue >= 24))
      timeColor = TS_16b_Black;
    else
      timeColor = TS_16b_White;

    //display.drawRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, TSRectangleFilled, backgroundColor);
    clearScreenBuffer(backgroundColor);

    // display.fontColor(timeColor, backgroundColor); //Set the font color, font background

    // Print date in US format MM:DD:YY (Switch the order in which day, month, year that you like to use)
    // display.setFont(liberationSans_12ptFontInfo);   //Set the font type
    sprintf(s, "%02d/%02d/20%02d", months, days, years);
    printCentered(s, 1, liberationSans_12ptFontInfo, timeColor, backgroundColor);
    // printCentered(s, 1);

    setTime(hours,minutes,seconds,days,months,2000 +  years);    //values in the order hr,min,sec,day,month,year
    strcpy(s, dayStr(weekday()));
    printCentered(s, 17, liberationSans_12ptFontInfo, timeColor, backgroundColor);
    // printCentered(s, 17);

    // display time in HH:MM:SS 24 hour format
    display.setFont(liberationSans_16ptFontInfo);   //Set the font type
    sprintf(s, "%02d:%02d:%02d", hours, minutes, seconds);
    printCentered(s, 35, liberationSans_16ptFontInfo, timeColor, backgroundColor);
    // printCentered(s, 35);

    // Display the RGB info
    display.setFont(thinPixel7_10ptFontInfo);   //Set the font type
    if (colorTimeMap == RGB)
      sprintf(s, "R%02d-G%02d-B%02d", red, green, blue);
    else if (colorTimeMap == BRG)
      sprintf(s, "B%02d-R%02d-G%02d", blue, red, green);
    else
      sprintf(s, "G%02d-B%02d-R%02d", green, blue, red);
    printCentered(s, 55, thinPixel7_10ptFontInfo, timeColor, backgroundColor);
    // printCentered(s, 55);

    sendBufferToScreen();

    delay(95); //display for ~0.1 seconds
    
    if (display.getButtons() & TSButtonUpperRight)
    {
      rotateRGB();
      i = 0;
    }
  }

  display.off();                               //Turns TinyScreen display off
}


void rotateRGB()
{
  if (colorTimeMap == RGB)
    colorTimeMap = BRG;
  else if (colorTimeMap == BRG)
    colorTimeMap = GBR;
  else
    colorTimeMap = RGB;
}


void loop()
{
 uint8_t   buttons;
  
  if ((buttons = display.getButtons()))
  {
    if (buttons & TSButtonUpperRight)
      rotateRGB();
    displayTime();
  }
}

Credits

J Koger

J Koger

10 projects • 4 followers
Thanks to Jack Hughes.

Comments