Time-Zone-Alterable Word Clock
A wall-mountable clock that displays words which correspond with the current time. These words are shown through a laser-cut sheet and LED lighting behind it, with select words being illuminated at a time. It is electronically to a globe, on which one may move a token around the surface to set a time zone for the clock. The token attaches magnetically, with a magnetic follower inside of the globe. Electronics are situated inside of the globe and calculate the position of the token, conveying that data to set the clock’s time zone.
| Engineer | School | Area of Interest | Grade |
|---|---|---|---|
| Ellie L | Santa Teresa High School | TBD | Incoming Junior |
Final Milestone
The final milestone consisted of constructing the entire globe portion of the project, which involved extremely delicate reed switches. To preserve these switches and their glass cases, I soldered them together and created a framework of wires before hotgluing both carefully bent leads to the inside of the globe surface (which we hand-sawed open). One side of each reedswitch was wired to a core wire, which was wired to the A2 pin on the Arduino Nano ESP32. This pin sent a constant LOW signal, which was picked up by other pins on the microcontroller when the switch was closed. The code iterates through each of these pins and checks which are closed to update the gmtOffset variable, thereby changing the time zone in the next iteration through the loop() function.
An issue that I had to address was getting the wires to come out from the globe neatly. I wanted to allow movement/rotation of the globe initially, but time was not permitting. Instead, there was a convenient hole in the axis that mounted thw globe on its respective stand, and wires could come out from there. I routed each wire and soldered it to the appropriate pin on the nano, and the prewritten code ran smoothly. To keep it organized, I wrapped soldering wtih electrical tape (although heat shrink would be more ideal for the future) and bundled the wires with zipties to complete the project.
Second Milestone
This was a relatively quick milestone to complete, given that it consisted mostly of transferring all the electrical components on my breakboard over to a perfboard. This required some soldering and reinforcing some of the wires so that they wouldn’t snap. It also meant that the electronics would be permanently binded with the clock at this point, since the power injection wires would be soldered to the perfboard. While testing, I realized that the load on the linear voltage regulator was too large and temporarily changed to a switching voltage regulator, which I had to tune, but that ended up failing as well and instead I swtiched back to the linear voltage regulator and just dimmed the LEDs to prevent it from drawing too much current. This was around the same time when I added a heat sink for the linear voltage regulator to keep it from overheating.
During the process of transferring electronics over to the perfboard, I also programmed in a start sequence with rows of white LEDs sweeping toward the middle, then red LEDs circling the outside of the clock. This required additional matrices to be made to store the correct IDs and ranges for the sequences.
First Milestone
My first milestone consists of the entire word clock portion of the project, which took me well over half of the progrma to build. Using hand-sawed acrylic for the backing and sides, as well as a clouded acrylic sheet for the front panel, the majority of the structure and frame are assembled with hot glue and superglue. Inside of the square frame, 201 individual LEDs are daisy chained in series, each with a power and ground wire as well as a data wire that runs from one to the other. The LEDs are cut from a typical individually addressable LED strip, and I soldered each one according to the spacing of letters that I designed on my grid. There is a cardstock grid that isolates each LED from the other, allowing for them to light up individually without the light bleeding over.
In order to make the letter grid, I planned out the most space-efficient option for displaying the time, which included optimizations such as having one constant display of “twenty” for all corresponding minute values, and having numbers such as “fourteen” also double as “four” and “twenty four.” Having tihs structure in mind, I wrote out a grid of letters and transferred them over to Canva for more precise measurement and font. I downloaded this design and uploaded it to a Cricut workspace in order to laser cut them out of a 12 inch by 12 inch black cardstock sheet.
As each LED is wired in series, a burnt out or malfunctioned LED along the chain can and will cause all succeeding LEDs to stop responding or dim. To debug the chain as I soldered, I used a multimeter to check for the proper voltage and used a mode on the multimeter to trigger a beeping sound confirming a connection through the data wire. Through this process, I was able to replace LEDs accordingly and light up the entire grid. Also due to the fact that they are wired in series, power is injected in two additional locations along the chain to prevent a major voltage drop that would cause inconsistency and dimming. In future iterations, I may experiment with parallel wiring for power, even if data must be wired in series.
The display can be powered by plugging a cord into a typical wall outlet, as the specific cord steps the voltage down to 9V, and a linear voltage regulator on the breadboard steps the voltage down further from 9V to 5V, which can power both the Arduino Nano ESP32 and the LED grid. To program the display, I used the FastLED and Time libraries, which have the tools or functions necessary to sync the time over WiFi and allow those to correspond with pin IDs and ranges. Said IDs and ranges are stored in matrices within the code for easy access and to make the code cleaner to read.
Schematics
Here’s where you’ll put images of your schematics. Tinkercad and Fritzing are both great resoruces to create professional schematic diagrams, though BSE recommends Tinkercad becuase it can be done easily and for free in the browser.
Code
The entirety of the project’s code, which is commented to show the purpose and functionality of individual lines/sections:
Main C++ Code
//necessary libararies
#include<FastLED.h>
#include<Arduino.h>
#include<TimeLib.h>
#include<WiFi.h>
//needs to be included because C++ doesn't automatically include all tools, but to make things faster only includes them if you explicitly say you need them
#include<unordered_map>
#include<string>
//these are the network credentials, replace with WiFi SSID and password
const char* SSID = "WiFi SSID Here";
const char* password = "WiFi Password Here";
//uses pool.ntp.org which is a public bunch of global timeservers
const char* ntpServer = "pool.ntp.org";
//both of these are in seconds
//default timezone offset for San Jose
long gmtOffset = -8*3600;
//and then offset for daylight savings time
long daylightOffset = 3600;
//assorted variables
int hours;
int minutes;
int i = 0;
int reedSwitch = 0;
int reedSwitchLast = 0;
int startBrightness = 0;
int rotateInt1 = 8;
int rotateInt2 = 7;
int rotateInt3 = 6;
int colorNum = 0;
int colorOrderNum = 1;
int id1;
int id2;
int id3;
//configurations for the hours start IDs and ranges
int minConfigs[31][2] = {
{0, 0},
{66, 3}, {81, 3}, {93, 5}, {84, 4}, {77, 4},
{137, 3}, {102, 5}, {73, 5}, {113, 4}, {14, 3},
{17, 6}, {22, 6}, {47, 8}, {84, 8}, {34, 7},
{133, 7}, {98, 9}, {70, 8}, {113, 8}, {56, 6},
{66, 3}, {81, 3}, {93, 5}, {84, 4}, {77, 4},
{137, 3}, {102, 5}, {73, 5}, {113, 4}, {56, 6}
};
int countryColors[9][9] = {
{179, 25, 66, 10, 49, 97, 255, 255, 255},
{200, 16, 46, 218, 37, 29, 255, 205, 0},
{179, 25, 66, 10, 49, 97, 255, 255, 255},
{30, 181, 58, 252, 209, 22, 0, 163, 221},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0}};
/*
locations:
A3 --> San Jose, CA, 1 top
A4 --> Saigon, Vietnam, 3 bottom
A5 --> New York, NY, 3 top
A6 --> Arusha, Tanzania, 2 bottom
A7 --> Sydney, Australia, 2 top
D2 --> Alesund, Norway, 4 bottom
D3 --> Istanbul, Turkiye, none
D4 --> Cusco, Peru, 1 bottom
D5 --> Tokyo, Japan
*/
//first value of element is pin that is being read, second is gmtOffset
std::unordered_map<uint8_t, int> timezones = {
{A3, -8*3600}, {A4, 3600*7}, {A5, -3600*5}, {A6, 3600*3}, {A7, 3600*11},
{D2, 3600}, {D3, 3600*3}, {D4, -3600*5}, {D5, -3600*9}
};
//configurations for the hours start IDs and ranges
int hourConfigs[13][2] = {
{148, 6},
//^^added this since I forgot midnight is technically 0
{176, 3}, {154, 3}, {163, 5}, {143, 4}, {192, 4},
{179, 3}, {168, 5}, {159, 5}, {172, 4}, {157, 3},
{187, 6}, {148, 6}
};
//lists for the initialization sequences
int rowVals[14] =
{14*0, 14*1, 14*2, 14*3, 14*4,
14*5, 14*6, 14*7, 14*8, 14*9,
14*10, 14*11, 14*12, 14*13};
int rotateVals[54] =
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
14, 41, 42, 69, 70, 97, 98, 125, 126, 153, 154, 181, 182,
183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195,
168, 167, 140, 139, 112, 111, 84, 83, 56, 55, 28, 27};
//constants for the LEDs
#define NUM_LEDS 201
#define DATA_PIN 2
#define LED_TYPE WS2812B
#define COLOR_ORDER GRB
//sets number of LEDs
CRGB leds[NUM_LEDS];
//code here runs once at the very start
void setup() {
//FastLED initialization
FastLED.addLeds<LED_TYPE, DATA_PIN, COLOR_ORDER>(leds, NUM_LEDS);
Serial.begin(115200);
//connect to the wifi with the credentials
WiFi.begin(SSID, password);
//stalls while the connection to WiFi is being made
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.print("connected sucessfully");
//configurate pins for the reed switches
pinMode(A2, OUTPUT);
for (auto& [pin, offset] : timezones) {
pinMode(pin, INPUT_PULLUP);
}
//initialization sequence part where the white LEDs join in rows from the top and bottom to the middle
//cycles through brightness and rows
FastLED.setBrightness(0);
for (int i = 0; i < 7; ++i) {
fill_solid(&leds[rowVals[i]], 14, CRGB::White);
fill_solid(&leds[rowVals[13-i]], 14, CRGB::White);
for (int b = 1; b < 3; ++b) {
startBrightness += b;
FastLED.setBrightness(startBrightness);
FastLED.show();
delay(20);
}
}
//red rotation sequence for initialization, basically just lights up in a circle
//there's a dedicated unordered map for the IDs correspnding to this sequence
for (int i = 0; i < 54; ++i) {
leds[rotateVals[rotateInt1]] = CRGB::Red;
leds[rotateVals[rotateInt2]] = CRGB(181, 0, 0);
leds[rotateVals[rotateInt3]] = CRGB(102, 0, 0);
if (rotateInt1 == 53) {
rotateInt1 = 0;
} else {
rotateInt1 += 1;
}
if (rotateInt2 == 53) {
rotateInt2 = 0;
} else {
rotateInt2 += 1;
}
if (rotateInt3 == 53) {
rotateInt3 = 0;
} else {
rotateInt3 += 1;
}
FastLED.show();
delay(20);
}
//makes the initialization LED sequence fade out slowly
for (int b = startBrightness; b > 0; b -= 2) {
FastLED.setBrightness(b);
FastLED.show();
delay(20);
}
//set default brightness
FastLED.setBrightness(150);
}
//defines function setTime which takes parameters to set the RGB values of the time
void setTime(int red, int green, int blue) {
//lights up all constants
//"Hello!" "it" "is" "made by Ellie Le =D"
fill_solid(&leds[0], 6, CRGB(red, green, blue));
fill_solid(&leds[7], 2, CRGB(red, green, blue));
fill_solid(&leds[10], 2, CRGB(red, green, blue));
fill_solid(&leds[196], 5, CRGB(red, green, blue));
//conditionals to check if we're in the first or second half of the hour
if (minutes <= 30 && minutes != 0) {
//illuminate "past"
fill_solid(&leds[127], 4, CRGB(red, green, blue));
} else if (minutes > 30) {
//illuminate "to"
minutes = 60 - minutes;
fill_solid(&leds[140], 2, CRGB(red, green, blue));
//adds to hours because of the way it's read (i.e. it is 2:54 is 6 to 3, not 6 to 2)
hours += 1;
}
//light up am and pm based on which half of the day it is (i.e. if the hours are greater than 12)
if (hours >= 12) {
hours = (12 - hours)*-1;
fill_solid(&leds[182], 2, CRGB(red, green, blue));
} else if (hours < 12 || hours == 0) {
fill_solid(&leds[184], 2, CRGB(red, green, blue));
}
if (minutes >= 20 && minutes != 30) {
//lights up "twenty" if the minute is any value in the twenties
fill_solid(&leds[56], 6, CRGB(red, green, blue));
}
if (minutes == 15 || minutes == 30) {
// light up "a" when the minute is 15 or 30 for "a half" and "a quarter"
leds[13] = CRGB(red, green, blue);
}
//access values in the matrices which store LED IDs and ranges that correspond to certain numerical valus
//display the ones that correspond to the time at that moment
//for example, if the minute is 26, it will go to the 26th element in the matrix, set the first value of that element as the start ID, and set the 2nd value as the range
//for that example specifically, this part only illuminates "six" and "twenty" is lit up separately
fill_solid(&leds[minConfigs[minutes][0]], minConfigs[minutes][1], CRGB(red, green, blue));
fill_solid(&leds[hourConfigs[hours][0]], hourConfigs[hours][1], CRGB(red, green, blue));
//transfers all the settings (including the reset all black later in the code) over to the LEDs to be phsyically shown
FastLED.show();
}
//main code goes here and runs repeatedly
void loop() {
//configuring all the time, which would typically go in setup() but here goes in loop() because the gmtOffset variable will update according to the token's location
//also sets the offset for daylight savings time, as well as the server (we're using a typical online server that is used for many of these projects)
configTime(gmtOffset, daylightOffset, ntpServer);
//makes a group of 9 variables, that follow a structure predefined by tm
//tm accomodates for the getLocalTime() function to populate it with the values for the current time when called
struct tm timeInfo;
//the ampersand allows getLocalTime to directly modify timeInfo's 9 variables by giving it the memory address for the timeInfo containers
//if getting the time doesn't fail (which is what the conditional is meant to detect), getLocalTime populates timeInfo, which is kind of like 9 individual variables structured in the tm format
if(!getLocalTime(&timeInfo)){
Serial.println("Failed to obtain time");
return;
}
//set the following variables to the current hour and time respectively, using functions from the time library and specific formats from the tm struct
hours = timeInfo.tm_hour;
minutes = timeInfo.tm_min;
//reset: turns all LEDs off to set up for the new time if the time changes, otherwise the new time is just written over again
//the reason this doesn't create a flashing effect is because the code for turning all the LEDs off is stored but not shown yet; it's shown after the code for the LEDs is also written
fill_solid(&leds[0], NUM_LEDS, CRGB::Black);
//sequence for if the time zone changes
//setTime() is a function defined earlier, so you just have ot pass in parameters for a red color and have that fade in and out
//reedSwitchLast stores the data for which reed switch was last set off, and reedSwitch stores the data for the new reed switch set off
//iterate through a for loop that allows brightness to be increased and then another that allows it to be decreased
if (reedSwitchLast != reedSwitch) {
//extra code for another feature that wasn't finished
/*
for (auto& [pin, offset] : timezones) {
if (digitalRead(pin) != 0) {
colorNum += 1;
}
}
for (int i = 0; i < 13; ++i) {
if (colorOrderNum == 1) {
id1 = 0;
id2 = 1;
id3 = 2;
}
if (colorOrderNum == 2) {
id1 = 3;
id2 = 4;
id3 = 5;
}
if (colorOrderNum == 3) {
id1 = 6;
id2 = 7;
id3 = 8;
}
fill_solid(&leds[rowVals[i]], 14, CRGB(countryColors[colorNum][id1], countryColors[colorNum][id2], countryColors[colorNum][id3]));
for (int b = 1; b < 3; ++b) {
startBrightness += b;
FastLED.setBrightness(startBrightness);
FastLED.show();
delay(20);
}
if (colorOrderNum == 3) {
colorOrderNum = 1;
} else {
colorOrderNum += 1;
}
}
for (int b = startBrightness; b > 0; b -= 2) {
FastLED.setBrightness(b);
FastLED.show();
delay(20);
}
*/
for (int i = 0; i < 2; ++i) {
for (int i = 0; i < 100; i += 10) {
FastLED.setBrightness(i);
setTime(255, 0, 0);
delay(10);
}
for (int i = 100; i > 0; i -= 10) {
FastLED.setBrightness(i);
setTime(255, 0, 0);
delay(10);
}
fill_solid(&leds[0], NUM_LEDS, CRGB::Black);
}
//update reedSwitchLast so that the cycle doesn't go on forever
reedSwitchLast = reedSwitch;
}
//A2 send a LOW signal to all of the reed switches, which is checked for on multiple other pins to see which reed siwtches are closed
//iterates through a matrix that contains the pin IDs for each pin on the nano that is wired to a reed switch and checks if the switch is closed
//then, if the switch is set off, it takes the 2nd value in that matrix element and sets the GMT offset to that value, which is updated at the beginning of the loop
digitalWrite(A2, LOW);
for (auto& [pin, offset] : timezones) {
if (digitalRead(pin) == 0) {
gmtOffset = offset;
reedSwitch = pin;
}
}
//set the default brightness for the display and show the time with default white color
FastLED.setBrightness(150);
setTime(255, 255, 255);
//debug code which would allow you to light up the entire grid and see where a break is in the LED chain
//fill_solid(&leds[0], NUM_LEDS, CRGB::White);
delay(400);
}
Configurations in platformio.ini
This project was programmed on VS Code with the Platform IO extension, which configures an environment on the IDE for programming microcontrollers such as the Arduino Nano ESP32. The following are specifications required for Platform IO, including libraries used:
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:arduino_nano_esp32]
platform = espressif32
board = arduino_nano_esp32
framework = arduino
lib_deps =
fastled/FastLED@^3.10.3
paulstoffregen/Time@^1.6.1
Bill of Materials
The essential costs for the project and cost of each item, as well as their function in the overall project:
| Part | Note | Price | Link |
|---|---|---|---|
| Arduino Nano ESP32 | the project’s microcontroller or the “brain” of the project which stores and processes code; the central operator that controls other components | $18.30 | |
| Link | |||
| WS2812B LED Strip (order 2) | LEDs to light up words corresponding to the time; can be cut and soldered as I did for individual LEDs for each letter or just soldered in rows | $8.99 | Link |
| Frosted Acrylic Sheet (12”x12”) | prevents harsh light from coming through the word cutouts; allows for a more consistent lighting of each word | $15.00 | Link |
| Clear Acrylic Sheet (12”x12”) Pack of 2 | composes the backing for the clock (surface for the LEDs) and the sides to make it stable/boxy | $18.30 | |
| Link | |||
| Assorted Cardstock (multiple colors; 12”x12”) | what I used to make the isolating grid to stop light from bleeding into other letters; I also used a laser cutter at home to make the front panel with the letter grid that I designed | $18.30 | |
| Link | |||
| Assorted Capacitors and Resistors (other smaller kits available) | used 2 capacitors to manage voltage drop and 1 resistor to keep the data wire for the LEDs safely operating | $25.99 | |
| Link | |||
| Perfboards | a basis for the smaller components involved in the project, such as the microcontroller, capacitors, and resistors | $9.99 | |
| Link | |||
| Assorted Wire Spools | connects all electrical components and carries power or data | $15.99 | |
| Link | |||
| 9V DC Adapter | provides power that can be handled by the linear voltage regulator from a typical power outlet | $9.35 | |
| Link | |||
| Linear Voltage Regulator (9V to 5V) | steps down the voltage from the DC converter to 5V, which can both power the LED grid and the Arduino Nano ESP32 | $0.42 | |
| Link | |||
| DC Barrel Jack Adapter | allows for wiring the DC adapter to the perfboard | $1.90 | Link |
| 8” World Globe | the main stage of the globe portion; self explanatory | $19.00 | Link |
| Glass Reed Switches (10 pcs) | detects when the magnet token is nearby and closes a circuit, which is read by the microcontroller | $9.99 | Link |
| Ceramic Disc Magnets (20 pcs) | used for the airplane token and also for inside the globe to keep the token suspended | $4.99 |
Other Resources/Examples
Similar projects, tutorials, and resoruces that I used to help me build and troubleshoot this project:
Other Bluestamp Portfolios
Example portfolios that can be used for reference and insight into the program:
To Angeline, Kin, Ishaan, and Justin –> thank you!