J Koger
Created March 21, 2016 © GPL3+

Vector Font O-Watch

Retro O-Watch shows the time using nothing but lines and a single color, like the old vector arcade games Asteroids and Battlezone.

BeginnerProtip1 hour212
Vector Font O-Watch

Things used in this project

Hardware components

O Watch Base Kit
O Watch Base Kit
×1

Story

Read more

Code

vector_font_watch.ino

Arduino
Vector Font O-Watch code
/*
 * Vector Font Analog Watch
 *
 * Demonstrates the use of the Arduino Time library and Arduino Zero Real-Time-Clock (RTC) library to make a watch
 * that uses a vector-based font, where all of the characters are based on (crude) line drawings. It still has
 * an analog clock face, just shrunk to 20 pixels diameter. Everything is displayed in light green on black,
 * reminiscent of the old video arcade games like Asteroids and Battlezone that drew true vector graphics on a
 * monochrome CRT display. 
 *
 * 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
 * 
*/

#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

/* 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;

int brightness = 15; // We'll set it, 3 - 15 based on time of day

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


#define COLOR_LIGHT_GREEN 0B01011101

/*
 * Vector fonts are usually defined as a series of floating-point X-Y coordinate
 * start and end points. We don't want to use floating point, and the biggest character
 * we cold draw would fill the entire screen at 64 pixels high, so let's make our 'unit size'
 * be 64 x 64 pixels (which scales quickly to 16 x 16 or 8 x 8 by shifting the values right 1 or
 * 2 binary digits, respectively).
 * Also, to avoid having 4 values per line (start and end coord pairs), we'll assume that the
 * next line starts where the last one left off. This means that we might need to double-back
 * on our drawing once or twice, but oh well. The first coordinate pair is where to start
 * the first line, the second where to end the first line and start the second line, etc.
 * 
 * Rather than have a separate value that indicates the number of lines (or endpoints) in the 
 * array for a character, we'll just flag with both coordinates being END_OF_POINTS.
 */

#define END_OF_POINTS   255
#define FULL_SCALE      0   // Don't shift values right (dont' binary divide by 2)
#define HALF_SCALE      1   // Shift values right 1 binary digit, 32 x 32
#define QUARTER_SCALE   2   // Shift right 2, 16 x 16
#define EIGHTH_SCALE    3   // Shift right 3, 8 x 8

typedef struct
{
  uint8_t   x,  // 0 - 63, x position of end of font line relative to upper-left corner (0,0)
            y;  // 0 - 63, y position of end of font line
  
} EndPoint;

/* We need the characters:
 *  0 1 2 3 4 5 6 7 8 9 : /
 *  for the time and date
 *  S U N D A Y M O T E W H R F I
 *  for the day of the week
 *  or
 *  A D E F H I M N O R S T U W Y
 *  We can do a nice indexed array for the numbers but
 *  the letters (and : and /) are a bit of a moshpit, so we'll just
 *  handle them with a big ol' switch statement. We could
 *  alternatively implement the entire alphabet so we could
 *  index them too, but that would involve creating 10 letters
 *  we won't use.
 */
EndPoint
vector0[] =
{
  {0,0}, {0,63}, {63,63}, {63,0}, {0,0}, {END_OF_POINTS, END_OF_POINTS}
},
vector1[] =
{
  {63,0}, {63,63}, {END_OF_POINTS, END_OF_POINTS}
},
vector2[] =
{
  {0,0}, {63,0}, {63,32}, {0,32}, {0,63}, {63,63}, {END_OF_POINTS, END_OF_POINTS}
},
vector3[] =
{
  {0,0}, {63,0}, {63,32}, {0,32}, {63,32}, {63,63}, {0,63}, {END_OF_POINTS, END_OF_POINTS}
},
vector4[] =
{
  {0,0}, {0,32}, {63,32}, {63,0}, {63,63}, {END_OF_POINTS, END_OF_POINTS}
},
vector5[] =
{
  {63,0}, {0,0}, {0,32}, {63,32}, {63,63}, {0,63}, {END_OF_POINTS, END_OF_POINTS}
},
vector6[] =
{
  {0,0}, {0,63}, {63,63}, {63,32}, {0,32}, {END_OF_POINTS, END_OF_POINTS}
},
vector7[] =
{
  {0,0}, {63,0}, {63,63}, {END_OF_POINTS, END_OF_POINTS}
},
vector8[] =
{
  {0,0}, {0,63}, {63,63}, {63,0}, {0,0}, {0,32}, {63,32}, {END_OF_POINTS, END_OF_POINTS}
},
vector9[] =
{
  {0,0}, {63,0}, {63,63}, {63,32}, {0,32}, {0,0}, {END_OF_POINTS, END_OF_POINTS}
},
vectorA[] =
{
  {0,63}, {0,0}, {63,0}, {63,63}, {63,32}, {0,32}, {END_OF_POINTS, END_OF_POINTS}
},
vectorD[] =
{
  {0,0}, {0,63}, {63,32}, {0,0}, {END_OF_POINTS, END_OF_POINTS}
},
vectorE[] =
{
  {63,0}, {0,0}, {0,32}, {63,32}, {0,32}, {0,63}, {63,63}, {END_OF_POINTS, END_OF_POINTS}
},
vectorF[] =
{
  {63,0}, {0,0}, {0,32}, {63,32}, {0,32}, {0,63}, {END_OF_POINTS, END_OF_POINTS}
},
vectorH[] =
{
  {0,0}, {0,63}, {0,32}, {63,32}, {63,63}, {63,0}, {END_OF_POINTS, END_OF_POINTS}
},
vectorI[] =
{
  {0,0}, {63,0}, {32,0}, {32,63}, {0,63}, {63,63}, {END_OF_POINTS, END_OF_POINTS}
},
vectorM[] =
{
  {0,63}, {0,0}, {32,32}, {63,0}, {63,63}, {END_OF_POINTS, END_OF_POINTS}
},
vectorN[] =
{
  {0,63}, {0,0}, {63,63}, {63,0}, {END_OF_POINTS, END_OF_POINTS}
},
vectorO[] =
{
  {0,32}, {32,0}, {63,32}, {32,63}, {0,32}, {END_OF_POINTS, END_OF_POINTS}
},
vectorR[] =
{
  {0,63}, {0,0}, {63,16}, {0,32}, {63,63}, {END_OF_POINTS, END_OF_POINTS}
},
vectorS[] =
{
  {63,0}, {0,16}, {63,48}, {0,63}, {END_OF_POINTS, END_OF_POINTS}
},
vectorT[] =
{
  {0,0}, {63,0}, {32,0}, {32,63}, {END_OF_POINTS, END_OF_POINTS}
},
vectorU[] =
{
  {0,0}, {0,63}, {63,63}, {63,0}, {END_OF_POINTS, END_OF_POINTS}
},
vectorW[] =
{
  {0,0}, {16,63}, {32,0}, {48,63}, {63,0}, {END_OF_POINTS, END_OF_POINTS}
},
vectorY[] =
{
  {0,0}, {32,32}, {32,63}, {32,32}, {63,0}, {END_OF_POINTS, END_OF_POINTS}
},
vectorColon[] =
{
  {24,24}, {24,48}, {40,48}, {40,24}, {24,24}, {24,36}, {40,36}, {END_OF_POINTS, END_OF_POINTS}
},
vectorSlash[] =
{
  {63,0}, {0,63}, {END_OF_POINTS, END_OF_POINTS}
},
vectorDash[] =
{
  {16,32}, {48,32}, {END_OF_POINTS, END_OF_POINTS}
};

// A nice neat indexed array (or table) for the numbers, so we just
// need to look up the appropriate vectore in the table.
EndPoint *vectorNumbers[] = {vector0, vector1, vector2, vector3, vector4, vector5,
  vector6, vector7, vector8, vector9};


// Draw the given vector of EndPoints (aka number or letter) with lines in the color defined by
// (red, green, blue) using the RGB mode of TinyScreen.drawLine().
//
// (vectors[0].x, vectors[0].y) define the offset of the first line's starting point from
// (startX, startY). The second EndPoint, (vectors[1].x, vectors[1].y) define both the
// offset of the first line's ending point AND the offset of the second line's starting point.
// The third Endpoint defines both the second line's ending point AND the third line's starting
// point, and so on.
//
// The scale value is used to determine how much the offsets should be scaled down by. FULL_SCALE
// indicates no downscaling, so the character is drawn at 64 x 64 pixels scale, HALF_SCALE indicates
// the offsets should be divided by 2 (shifted right 1 binary digit) or at 32 x 32 scale, etc.

void drawVectorChar(EndPoint *vectors, uint8_t xStart, uint8_t yStart, uint8_t scale, uint8_t red, uint8_t green, uint8_t blue)
{
  EndPoint  *curVector = vectors;
  uint8_t   curX, curY, nextX, nextY;

  display.drawRect(xStart, yStart, 64 >> scale, 64 >> scale, 1, 0, 0, 0);

  curX = curVector->x;
  curY = curVector->y;
  curVector++;
  while (1)
  {
    nextX = curVector->x;
    nextY = curVector->y;
    if ((nextX == END_OF_POINTS) && (nextY == END_OF_POINTS))
      break;
    display.drawLine(xStart + (curX >> scale), yStart + (curY >> scale), xStart + (nextX >> scale), yStart + (nextY >> scale), red >> 3, green >> 2, blue >> 3);
    curX = nextX;
    curY = nextY;
    curVector++;
  }
}


// Draw the indicated number in a vector font.
void drawVectorNumber(uint8_t number, uint8_t xStart, uint8_t yStart, uint8_t scale, uint8_t red, uint8_t green, uint8_t blue)
{
  if (number > 9)
    number = 9;
  drawVectorChar(vectorNumbers[number], xStart, yStart, scale, red, green, blue);
}


// Draw the indicated letter in a vector font. We'll use a gigantic switch statement to
// look up the appropriate vector for the requested letter.
void drawVectorLetter(char letter, uint8_t xStart, uint8_t yStart, uint8_t scale, uint8_t red, uint8_t green, uint8_t blue)
{
  EndPoint  *vector;

  //  A D E F H I M N O R S T U W Y : /
  switch (letter)
  {
    case 'A':
    case 'a':
      vector = vectorA;
      break;
    case 'D':
    case 'd':
      vector = vectorD;
      break;
    case 'E':
    case 'e':
      vector = vectorE;
      break;
    case 'F':
    case 'f':
      vector = vectorF;
      break;
    case 'H':
    case 'h':
      vector = vectorH;
      break;
    case 'I':
    case 'i':
      vector = vectorI;
      break;
    case 'M':
    case 'm':
      vector = vectorM;
      break;
    case 'N':
    case 'n':
      vector = vectorN;
      break;
    case 'O':
    case 'o':
      vector = vector0;
      break;
    case 'R':
    case 'r':
      vector = vectorR;
      break;
    case 'S':
    case 's':
      vector = vector5;
      break;
    case 'T':
    case 't':
      vector = vectorT;
      break;
    case 'U':
    case 'u':
      vector = vectorU;
      break;
    case 'W':
    case 'w':
      vector = vectorW;
      break;
    case 'Y':
    case 'y':
      vector = vectorY;
      break;
    case ':':
      vector = vectorColon;
      break;
    case '-':
      vector = vectorDash;
      break;
    default:
      vector = vectorSlash;
  }
  
  drawVectorChar(vector, xStart, yStart, scale, red, green, blue);
}


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.on();                               //Turns TinyScreen display on
  display.fontColor(TS_8b_White,TS_8b_Black); //Set the font color, font background

  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);
}

// Draw a circle. Slow but works. Taken from the O-Watch TinyScreen tutorial.
// http://www.theowatch.com/learn/o-watch-tinyscreen-programming/learn-tinyscreen-demo/
// 
void drawCircle(int x0, int y0, int radius, uint8_t color)
{
  int x = radius;
  int y = 0;
  int radiusError = 1-x;
 
  while(x >= y)
  {
    //drawPixel(x,y,color);//set pixel (x,y) to specified color. This is slow because we need to send commands setting the x and y, then send the pixel data.
    display.drawPixel(x + x0, y + y0, color);
    display.drawPixel(y + x0, x + y0, color);
    display.drawPixel(-x + x0, y + y0, color);
    display.drawPixel(-y + x0, x + y0, color);
    display.drawPixel(-x + x0, -y + y0, color);
    display.drawPixel(-y + x0, -x + y0, color);
    display.drawPixel(x + x0, -y + y0, color);
    display.drawPixel(y + x0, -x + y0, color);
    y++;
    if (radiusError<0)
    {
      radiusError += 2 * y + 1;
    }
    else
    {
      x--;
      radiusError += 2 * (y - x) + 1;
    }
  }
}


// Draw a line from a given start point at a given angle for a given length.
//
// Don't use any floating point math such as the sine() function because it
// takes a long time to run and eats battery life on a computer without
// a floating point processor, like the Arduino Zero in the O-Watch.
// Instead, use a good old-fashioned hard-coded sine lookup table and
// a couple of integer math tricks.
//
// The underlying idea from a geometry standpoint is that we need to figure
// out the equivalent X,Y endpoint of a line that goes from the center of the
// circle (0,0) out to a length of radius 1.0 (the 'unit circle') at a given
// angle and multiply the length of our hand by that X,Y endpoint, then slide
// both the center of the circle and the enpoint over so that they match our actual.
// circle's coordinates.
// The sine and cosine of the unit circle are the X,Y endpoint that we need.
//
// Modified from: https://codebender.cc/example/glcd/clockFace#AnalogClock.cpp

void drawHand(int xStart, int yStart, int angle, int radius, unsigned char color)

// xStart, yStart are starting point of hand (center of clock face)
// angle is location of hand on dial (0-60)
// radius is length of hand
{
  // Sines for hand positions from 0 to 15 minutes. All of the other
  // hand position sines can be determined by flipping x and y. These
  // are all multiplied by 256 so that we can use integers to store them.
  // Cosines can be also be determined by taking the sine of the angle minus
  // 15 degrees and flipping x and y.
  static int sine[16] = {0, 27, 54, 79, 104, 128, 150, 171, 190, 201, 221, 233, 243, 250, 254, 255};
  int xEnd, yEnd, quadrant, x_flip, y_flip;

  // calculate which quadrant the hand lies in
  quadrant = angle/15 ;

  // We could have a bigger table of sine values that handle every hand position from 0 to 60,
  // but we can avoid using that memory by doing some quick reflections and rotations.
  switch ( quadrant )
  {
    case 0 : x_flip = 1 ; y_flip = -1 ; break ;
    case 1 : angle = abs(angle-30) ; x_flip = y_flip = 1 ; break ;
    case 2 : angle = angle-30 ; x_flip = -1 ; y_flip = 1 ; break ;
    case 3 : angle = abs(angle-60) ; x_flip = y_flip = -1 ; break ; 
    default:  x_flip = y_flip = 1; // this should not happen
  }

  xEnd = xStart;
  yEnd = yStart;
  // OK, here's the tricky part. Look up the sine for the hand
  // angle, then multiply it by the hand length. Then, because
  // our sine table values are all pre-multiplied by 256 so we
  // can store them as integers, we need to divide by 256 to
  // correct for that. Which is, in integer math, the same as
  // saying that we'll shift the values right by 8 bit positions.
  // Doing a shift-right is faster than doing an integer divide by 256
  // (the compiler is probably smart enough to turn that
  // divide by 256 into a right shift anyway, but let's do
  // an explicit shift because it's cool).
  //
  // The equivalent floating-point call would look something like:
  // xEnd = sine(angle) * radius;
  xEnd += (x_flip * (( sine[angle] * radius ) >> 8));
  yEnd += (y_flip * (( sine[15-angle] * radius ) >> 8));

  display.drawLine(xStart, yStart, xEnd, yEnd, color);
}


void displayTime()
{

  static uint8_t
    clockCenterX = 79,
    clockCenterY = 16,
    clockCircleRadius = 15,
    clockCircleColor = COLOR_LIGHT_GREEN,
    clockMinuteHandLength = 11,
    clockMinuteHandColor = COLOR_LIGHT_GREEN,
    clockHourHandLength = 8,
    clockHourHandColor = COLOR_LIGHT_GREEN,
    clockSecondHandLength = 13,
    clockSecondHandColor = COLOR_LIGHT_GREEN;

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

  // Draw the circle for the analog clock. According to the TinyScreen
  // tutorial http://www.theowatch.com/learn/o-watch-tinyscreen-programming/
  // the TinyScreen is 96 pixels wide by 64 high, with (0,0) in the
  // upper-left corner, and (95,63) in the lower right.
  // This is the only part of the time display that doesn't need to be redrawn
  // as long as the hands stay inside the circle.
  


  drawCircle(clockCenterX, clockCenterY, clockCircleRadius, clockCircleColor);
  
  for (int i = 1; i < 15; i++) // Display for 15*1000 milliseconds (15 seconds), update display each second
  {
    // To correctly handle switching date at midnight while the time display is on,
    // update -everything-, not just the seconds.

    display.setFont(liberationSansNarrow_12ptFontInfo);   //Set the font type

    // 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();

    // First draw the clock hands, to minimize them flickering.

    // The hour is 0 - 23. But we need it
    // to be an 'angle' of 0 - 59. So
    // convert 24-hour time to 12 hour time,
    // and then multiply by 60 / 12 = 5.
    int hourAngle;
    if (hours >= 12)
      hourAngle = hours - 12;
    else
      hourAngle = hours;
    hourAngle *= 5;
    // To give the hour hand a real
    // analog-clock feel, it shouldn't point
    // straight at the hour, but should move between
    // the hours as the minute hand sweeps around.
    // So, let's nudge it forward a smidge by
    // the minutes scaled down to the space within 1 hour, or
    // divide them by 12.
    hourAngle += minutes / 12;
    
    
    drawHand(clockCenterX, clockCenterY, hourAngle, clockHourHandLength, clockHourHandColor);
    drawHand(clockCenterX, clockCenterY, minutes, clockMinuteHandLength, clockMinuteHandColor);
    drawHand(clockCenterX, clockCenterY, seconds, clockSecondHandLength, clockSecondHandColor);
    

    if (hours <= 12)
      brightness = hours + 3; // 0 hours = 3 brightness, noon = 15
    else if (hours >= 18)
      brightness = (24 - hours) * 2 + 2;  // 23 hours = 4 brightness, 18 hours = 14
    else
      brightness = 15; // full brightness all afternoon
    if (brightness < 3)
      brightness = 3;
    if (brightness > 15)
      brightness = 15;
    display.setBrightness(brightness);                  //Set display brightness 0 - 15

    drawVectorNumber(months / 10, 0, 0, EIGHTH_SCALE, 128, 255, 128);
    drawVectorNumber(months % 10, 10, 0, EIGHTH_SCALE, 128, 255, 128);
    drawVectorLetter('/', 20, 0, EIGHTH_SCALE, 128, 255, 128);

    drawVectorNumber(days / 10, 30, 0, EIGHTH_SCALE, 128, 255, 128);
    drawVectorNumber(days % 10, 40, 0, EIGHTH_SCALE, 128, 255, 128);

    setTime(hours,minutes,seconds,days,months,2000 +  years);    //values in the order hr,min,sec,day,month,year
    char wkday[16];
    strcpy(wkday, dayStr(weekday()));
    drawVectorLetter(wkday[0], 0, 20, EIGHTH_SCALE, 128, 255, 128);
    drawVectorLetter(wkday[1], 10, 20, EIGHTH_SCALE, 128, 255, 128);
    drawVectorLetter(wkday[2], 20, 20, EIGHTH_SCALE, 128, 255, 128);

    // display time in HH:MM:SS 24 hour format
    drawVectorNumber(hours / 10, 0, 40, QUARTER_SCALE, 128, 255, 128);
    drawVectorNumber(hours % 10, 20, 40, QUARTER_SCALE, 128, 255, 128);
    drawVectorLetter(':', 40, 40, QUARTER_SCALE, 128, 255, 128);

    drawVectorNumber(minutes / 10, 60, 40, QUARTER_SCALE, 128, 255, 128);
    drawVectorNumber(minutes % 10, 80, 40, QUARTER_SCALE, 128, 255, 128);
   
    delay(1000); //display for 1 seconds
    
    // Now erase the clock hands by drawing them in black.
    drawHand(clockCenterX, clockCenterY, hourAngle, clockHourHandLength, TS_8b_Black);
    drawHand(clockCenterX, clockCenterY, minutes, clockMinuteHandLength, TS_8b_Black);
    drawHand(clockCenterX, clockCenterY, seconds, clockSecondHandLength, TS_8b_Black);
  }

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

void loop()
{
   if (display.getButtons())
   {
      displayTime();
   }
}

Credits

J Koger

J Koger

10 projects • 4 followers

Comments