Hardware components | ||||||
| × | 1 |
Now that our O-Watches can keep and display accurate time, let's give things a retro look, and make the display look like it's taken out of the old Asteroids or Battlezone arcade games.
These early video games used monochrome monitors (either a single light green or white foreground color on a black background), and drew vector graphics for everything on the screen (there were no pixels or bitmaps on the screens, only lines).
NOTE: A better version of this watch is included in Mult-O-Watch.
It is strongly recommended that you get the previous Simple RTC Watch working before trying this.
In order to make our O-Watches draw everything using only lines, we'll first need to implement our own font-drawing routines, since the regular TinyScreen fonts are bitmap-based. One of the nice advantages of line-based fonts (generally called outline-based or vector-based fonts) is that they can be scaled to different sizes, and look just as good at any size.
On modern computers, most fonts are actually outline-based (or are stroke-based, which are types of outline-based fonts with additional drawing hints and details included) . Popular outline-based fonts include PostScript fonts and TrueType fonts.
We'll make our own outline font just for the O-Watch. It's pretty crude, since we'll only use straight lines, not fancy Bezier curves like most outline fonts. And we'll only handle the characters needed to display the date, time, and day of the week. Our font will be further limited because we don't want to use expensive floating-point math. To avoid floating point, we'll describe the characters in our font using an integer 64 x 64 grid, and only support scaling down by powers of 2 to 32 x 32, 16 x 16, or 8 x 8.
For the current implementation of this watch face, we'll draw the time using 16 x 16 scale, and the date and day of the week using 8 x 8 scale.
We'll still include an analog clock, but it'll be small. (To be honest, there is one part of our analog clock that isn't line-based: we still use the drawCircle() routine to draw the analog clock face's outline, which uses TinyScreen's drawPixel() function. Everything else is line-based.)
To replicate the look of the old monochrome vector-based games, we'll draw everything on the O-Watch's screen in light green.
For fun, we'll use the RGB mode of TinyScreen's drawLine() routine to draw the lines in our font.
/*
* 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();
}
}
Comments