Hardware components | ||||||
| × | 1 | ||||
Software apps and online services | ||||||
|
Here is a modified version of the interesting O-Watch project: Mini Word Clock on the O-Watch by J Koger. The word clock uses a fixed grid of letters to spell out the time, for example "FI V E PAST ELEVEN". After studying that source code, I attempted to animate the grid of letters - a digital rain effect - inspired by the 1999 movie The Matrix.
I wanted to use a color gradient for the letters as they scrolled down the face of the watch. For this I decided to change the bit depth of the TinyScreen from 8 to 16. The 16bit colors are in BGR565 format. There are online color picker tools to help.
The animation uses a combination of random starting points as well as a fixed number of repeats for each droplet. A delay for each frame controls the velocity of the falling droplets. Adjusting all of these values in the source code can lead to different visual effects.
/*
* Animated Word Watch
*
* Demonstrates the use of the Arduino Zero Real-Time-Clock (RTC) library to make a simple 8 x 8 'Word Clock'
* display of the current hours and minutes.
*
* This type of display only shows the current minutes (to within 5 minutes) and current hour, using a grid of 8 x 8 letters. The letters
* needed to display the current time (for example, 'HALF PAST TEN') are 'lit up' (drawn with a bright color like yellow), and the
* unnneeded letters are 'turned off' (drawn with a dull color like gray).
*
* Since the TinyScreen is 64 pixels high, the 8 x 8 grid of letters will take up most of the display, since the
* smallest font provided on the O-Watch is 7 pixels tall (plus 1 pixel gap between rows): (7 + 1) * 8 = 64 pixels needed
* to display 8 rows. That doesn't leave any room for the date, day of the week, or seconds. It's also a bit hard to read
* since the letters are pretty, well, tiny.
*
* 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 time as an 8 x 8 character 'Micro Word Clock' by J Koger 16 March 2016, using a tweaked
* version of the code from Daniel Rojas' https://github.com/formatc1702/Micro-Word-Clock
*
* Modified to display an animated digital rain effect by Bob S on 19 March 2016
*
*/
#include <TinyScreen.h> //include TinyScreen 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
// Here we define several colors for the gradient on the droplet trail, from bright to dark
#define DROPLETTRAILLENGTH 9
const uint16_t droplettrail[DROPLETTRAILLENGTH] = { 0x9FF6, 0x07E7, 0x0606, 0x0545, 0x0464, 0x0323, 0x0282, 0x0141, 0x0080 };
// Our colors for letters that are off and
// for letters that are lit.
#define LETTER_OFF droplettrail[DROPLETTRAILLENGTH-2]
#define LETTER_LIT droplettrail[1]
// This is the 8 x 8 grid of letters used to display the current time.
// We'll only 'light up' the letters needed to show the current
// time, and draw all the other ones in a 'dark' color.
char wordClock[8][9] = {
"HATWENTY",
"FIFVTEEN",
"LF*PASTO",
"NINEIGHT",
"ONETHREE",
"TWELEVEN",
"FOURFIVE",
"SIXSEVEN"
};
// This is an array used to determine which letters should be 'lit'
// to display the current minutes (or, really, the current 5-minute
// segment. There is one grid for each segment, and since there are 12
// 5-minute segments in an hour, there are 12 grids in the array.
//
// For example, if the current time is 10:06, then it is roughly
// 5 minutes past 10. So, we'll want the letters F I V E and P A S T
// to be lit. Look in the second grid for 'five past'; it has 1's for
// each of the wanted letters and 0's for all of the unused letters.
//
// The lit/unlit letter indicators are stored as bits within bytes
// (a byte holds 8 bits). We could use a byte for each letter, but
// bits only need 1/8 the space. And it'll give us an excuse to have
// some fun with bit comparisons later on!
unsigned char letterMinutes[12][8] ={
{ // o'clock
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00000000
}
, { // five past
0B00000000,
0B11010100,
0B00011110,
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00000000
}
, { // ten past
0B00000000,
0B00001101,
0B00011110,
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00000000
}
,{ // fifteen past
0B00000000,
0B11101111,
0B00011110,
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00000000
}
,{ // twenty past
0B00111111,
0B00000000,
0B00011110,
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00000000
}
,{ // twentyfive past
0B00111111,
0B11010100,
0B00011110,
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00000000
}
,{ // half past
0B11000000,
0B00000000,
0B11011110,
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00000000
}
,{ // twentyfive to
0B00111111,
0B11010100,
0B00000011,
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00000000
}
,{ // twenty to
0B00111111,
0B00000000,
0B00000011,
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00000000
}
,{ // fifteen to
0B00000000,
0B11101111,
0B00000011,
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00000000
}
,{ // ten to
0B00000000,
0B00001101,
0B00000011,
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00000000
}
,{ // five to
0B00000000,
0B11010100,
0B00000011,
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00000000
}
};
// These are the corresponding
// grids to determine which letters
// should be 'lit' to show the
// current hour, 1 grid for each of
// the 12 hours.
unsigned char letterHours[12][8] ={
{ // twelve
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B11110110,
0B00000000,
0B00000000
}
,{ // one
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B11100000,
0B00000000,
0B00000000,
0B00000000
}
,{ // two
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B11000000,
0B01000000,
0B00000000
}
,{ // three
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00011111,
0B00000000,
0B00000000,
0B00000000
}
,{ // four
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B11110000,
0B00000000
}
,{ // five
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00001111,
0B00000000
}
,{ // six
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B11100000
}
,{ // seven
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00011111
}
,{ // eight
0B00000000,
0B00000000,
0B00000000,
0B00011111,
0B00000000,
0B00000000,
0B00000000,
0B00000000
}
,{ // nine
0B00000000,
0B00000000,
0B00000000,
0B11110000,
0B00000000,
0B00000000,
0B00000000,
0B00000000
}
,{ // ten
0B00000000,
0B00000000,
0B00000000,
0B00000001,
0B00000001,
0B00000001,
0B00000000,
0B00000000
}
,{ // eleven
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00000000,
0B00111111,
0B00000000,
0B00000000
}
};
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(1); // allow 16bit colors BGR565
display.on(); //Turns TinyScreen display on
display.fontColor(LETTER_OFF, TS_16b_Black); //Set the font color, font background
display.setFont(thinPixel7_10ptFontInfo); //Set the font type
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);
}
// animated digital rain affect
void displayTime() {
hours = rtc.getHours();
minutes = rtc.getMinutes();
seconds = rtc.getSeconds();
// set brightness
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);
display.clearScreen();
display.on();
// Convert 24-hour time into the 12-hour format for
// the word clock.
int wordHours, wordMinutes;
wordHours = hours;
if (minutes >= 35) // After half-past the hour, the minute words start referencing the next hour.
wordHours++;
wordHours %= 12; // Now 0 - 11, which is what we need for lookups in the grid.
// Convert minutes to 5-minute segments
wordMinutes = minutes / 5;
// Get the letter-selection grids for the current hour and 5-minute segment.
unsigned char *thisLetterMinute = letterMinutes[wordMinutes];
unsigned char *thisLetterHour = letterHours[wordHours];
// the droplet in each column starts at a different position, mostly offscreen
randomSeed(minutes*seconds);
signed char droplet[8];
for (int column = 0; column < 8; column++) {
droplet[column] = - random(2*DROPLETTRAILLENGTH);
}
// keep track of how many times a droplet goes offscreen
unsigned int dropletOffscreen[8] = {0,0,0,0,0,0,0,0};
// total times droplets go offscreen
const int dropletRuns = 3;
bool notdone = true;
while( notdone ) {
// move droplets down the rows of the columns
for (int row = 0; row < 8; row++)
{
unsigned char letterColumn = B10000000;
for (int column = 0; column < 8; column++, letterColumn = letterColumn >> 1)
{
// Set the drawing cursor to the position of the current letter.
display.setCursor((column * 10) + 10, (row * 8));
if (row <= (droplet[column] - DROPLETTRAILLENGTH)) {
// leave a ghost of the unlit letters for the final run, when highlighting the time
if (dropletOffscreen[column] >= dropletRuns) {
// Check to see if the current letter should be off or lit.
if (((thisLetterMinute[row] & letterColumn) == 0) &&
((thisLetterHour[row] & letterColumn) == 0)) {
// ghost of non-used letters
display.fontColor(LETTER_OFF, TS_16b_Black);
} else {
// highlight the letters needed to display the time
display.fontColor(LETTER_LIT, TS_16b_Black);
}
} else {
// hidden, after the trail
display.fontColor(TS_16b_Black, TS_16b_Black);
}
} else if (row <= droplet[column] && row > (droplet[column] - DROPLETTRAILLENGTH)) {
// set the color for the droplet or its trail
display.fontColor(droplettrail[droplet[column] - row ], TS_16b_Black);
} else {
// hidden, before the head
display.fontColor(TS_16b_Black, TS_16b_Black);
}
// randomize until the last frames, then display the correct word clock face
if (dropletOffscreen[column] >= dropletRuns && row < droplet[column]) {
display.print(wordClock[row][column]);
} else {
display.print(wordClock[random(8)][random(8)]);
}
}
}
// move the droplets to the next row, count any that moved offscreen
for (int column = 0; column < 8; column++) {
droplet[column] ++;
// if droplet has gone offscreen, and it hasn't completed it's runs, then increment its counter
if (droplet[column] > (7 + DROPLETTRAILLENGTH)) {
if (dropletOffscreen[column] < dropletRuns) {
// we have more runs to complete, reset the droplet to the top
droplet[column] = - random(DROPLETTRAILLENGTH);
}
// count it
dropletOffscreen[column] ++;
}
// check bounds
if (dropletOffscreen[column] > dropletRuns){
// hold the end position
droplet[column] = (7 + DROPLETTRAILLENGTH);
}
}
// if all droplets are done, break from the while loop
notdone = false;
for (int column = 0; column < 8; column++) {
if ( dropletOffscreen[column] <= dropletRuns ){
// not done yet
notdone = true;
}
}
// delay between frames
delay(45);
}
// hold display of the time for a few seconds
delay(5000);
display.off();
}
void loop()
{
int button = display.getButtons();
if (button) {
displayTime();
}
delay(10);
}
Comments