ESP32 No-Sleep-In Wi-Fi Alarm Clock in 3D Printed Enclosure

by JD_K in Circuits > Microcontrollers

844 Views, 2 Favorites, 0 Comments

ESP32 No-Sleep-In Wi-Fi Alarm Clock in 3D Printed Enclosure

Title 3 PNG.png
TitleInstructables.png
EnclosureEXPL CompleteJPG.JPG

This is the final project for a course, Fundamentals of Product Design (ELIC 105), at Humber College in Toronto. The problem that each student had to solve was to create an alarm clock that would prevent the person from snoozing over-and-over, which some people do making them wake up very late for the beginning of their day. The clock had to be programmed on an ESP32 development board, and enclosed in a 3D printed enclosure that the student had to design in SolidWorks. The device had to have a web-based user interface hosted by the ESP32. 

This Instructable is not meant to be step-by-step instructions for putting the project together, but I have included all the relevant files, list of components & circuit diagram (in the Supplies section), the complete code (in Step 1), and the STL files for the enclosure (in Step 5), if you want to attempt the project. If you have any questions, please put the in the comments below. For those NOT interested in this alarm clock, this Instructable highlights different useful features of this project, some of the problem-solving that came up, and some explanation for each in the hopes that the reader can adapt any of them to their project. It is a long Instructable, but here is a list of these highlights; the bolded text each have their own section in the Instructable: 

Step 2: Highlights of the circuit:

  • Displays the time using a common cathode 4-digit, 7-segment display
  • Uses three (3) GPIO pins to operate the 12-pin display through the use of two generic shift registers (74HC595)
  • Example code for an ATtiny85, two shift registers and the display: I provide separate code below that is easily portable to other controllers such as the ATtiny85, and other projects that might use a 4-dgit 7-segment display

Step 3: Features of the ESP32 Programming:

  • An alarm clock that uses your local Wi-Fi to access the Network Time Protocol (NTP), which is more reliable than using a real time clock (RTC, e.g., DS1307) integrated circuit
  • Checking the alarm time and snooze time – making it hard to sleep in: After allowing for the customary 10min snooze, the alarm requires the user to keep the room light on, and will only snooze for 1 minute at a time maximum. I see it like getting poked every minute until you get up.
  • Passive buzzer and using ledc functions of the ESP32: for error/confirmation sounds for the user interface, and of course the alarm itself
  • Adjustable mode: daily alarm (Sun-Sat) or weekday-only (Mon-Fri) allowing you to sleep in on weekends. By having the alarm that automatically set for the next day, it reduces the risk of human error that would come with resetting the alarm
  • Multitasking using the real time operating system (free RTOS) to run both the loop() and the display
  • Optimizing programming of the multitasking: My hypothesis about the RTOS and the effect of different timing in the loop (delay versus millis) affects the performance of the multitasking (i.e. minimize flickering of the display). Not what you might think!
  • Employs a State-Machine programming structure in the loop (more efficient way of programming than having a series of ‘if’ statements)
  • Storing an image file (PNG file) on the ESP32 to be included in the webpage (SPIFFS). Uses the Serial Peripheral Interface Flash File System.

Step 4: Highlights of the ESP32 Webpage interface:

  • Customize the placement of elements on the ESP32 web page using HTML and CSS: Using HTML and CSS, customize the placement of elements on the screen. Works on cell phones, tablets and even computer screes, portrait and landscape
  • Cool Text Effect: Embedding the image into text - Placing elements in “layers” in the Z orientation to get artistic effects
  • Colouring using gradients to colour elements on the webpage
  • State changes for the buttons: colour changes when hovering over the buttons, and the buttons are visibly pressed when they are clicked

Step 5: Highlights of the 3D Printed Enclosure (STL files provided):

  • Simple to print: no support material needed, under 10x10x10cm size, plus the lid
  • Versatile design that can adapt to many modules (ESP32 Development board, Arduino Nano, RP2040, TP4056 lithium battery management module) with up to 3 standard sized perforated boards: 7x3cm, 7x5cm and 6x4cm
  • Interchangeable face plate allows for different interfaces between the internal circuitry and the outside world: displays, buttons, LEDs, potentiometers, rotary encoders, and sensors. These components can be mounted on the 7x3cm perfboard
  • Sturdy lid held on with two hidden M2 screws
  • Decorative (sort of) feet that can allow for easy mounting of the enclosure with screws

Note: This Instructable assumes you have some prior knowledge with working with the Arduino IDE and electronics. If this is your first time using the ESP32 that might be just fine, it was for me.

Supplies

Schematic_schem.png
IMG_4202.JPG
IMG_4201 PNG.png
IMG_4210 PNG.png
IMG_4211 PNG.png
IMG_4239 PNG.png
IMG_4240 PNG.png
IMG_4244 PNG.png
IMG_4246 PNG.png
IMG_4232 PNG.png
IMG_4259 png.png
IMG_4268 PNG.png
IMG_4270 PNG.png
IMG_4271 PNG.png

I have listed all the components in a table which you can download from a separate file below called "List of Components".

Tools

  • 3D printer with PLA filament
  • Laser cutter (optional)
  • Arduino IDE, version 1 because of the SPIFFS
  • Calipers
  • Soldering iron, solder, etc.
  • Square
  • Knife
  • File
  • Sand paper

The Circuit

The schematic of the circuit is provided above. If you plan to modify the pin layout, just be aware that some GPIO pins do not have all the same functions as others. For example, some pins do not have an internal pull up resistor, so pinMode(34,INPUT_PULLUP); will not work as expected because there isn’t a pull up resistor for pin 34. You can find a list of pins and functions in this tutorial by Random Nerd Tutorials. Initially, I had planned for the display. When it looked like I would not get the translucent acrylic, which was essential to be able to see it, I abandoned the plan. However, I learned about the multitasking that the ESP32 could do, and decided I wanted to take advantage of it. So, I added the circuitry back into the project for the display. It added a lot more work at that point, but I am glad I did. You can see in the pictures, the change of plans.

There are some 100 nF capacitors, these may be fine to leave out. Many people on the internet suggest placing one such capacitor as close as possible to the power pins of each integrated circuit (the shift registers in this case) as a general practice. I thought it might help with the analog measurement of the LDR.

Light Dependent Resistor (LDR)

This is used in series with a 22 kOhm resistor (see the circuit diagram), such that when it is darker, the voltage between LDR and this resistor is about zero volts. The voltage increases in brighter light because the LDR has less resistance when light is shone on it. The ESP32 uses GPIO 34 to take an analog measurement of this voltage. When the alarm first goes off, assuming it is still dark at that moment, a function called checkAmbientLight() takes several analog measurements to determine a threshold that will be used later to check if the person switched on the light and that they keep it on. The third 100 nF capacitor is across the LDR circuit, and I keep the ground wire from the 22kOhm resistor separate from the other ground wires to help with accuracy of the analog measurement. Only the gross level of light intensity is needed for this project, but these two precautions for accuracy were easy to implement. 

How It Works

DIY ESP32 Digital Alarm Clock with Web interface - It won't let you sleep in

To use the code, you must have the AsyncTCP.h (github link) and the ESPAsyncWebServer.h (github link) libraries. You must change the code to the name of the Wi-Fi you plan to access, usually your home Wi-Fi, and its password:

const char* ssid = "NetWorkName";
const char* password = "PassWord";

You should also change the time zone depending on where you are in the world (Toronto is in the Eastern Standard Time or Eastern Daylight Time Zone, Depending on the time of year), and this can be done by consulting this website and changing what is in the quotation marks in this line of code:

const char* TZ_INFO = "EST5EDT";

Then you should be able to upload the code to the ESP32 development board. You will need to load the image (see the section on SPIFFS below to see how). Then when you start the ESP32, open the Serial Monitor on the Arduino IDE (I am using version 1.8.57), and make sure the baud rate is the same as Serial.Begin(115200);

The ESP32 will attempt to access the Wi-Fi, and when it does, it will print a number. This number is the address of the web page hosted by the ESP32. If you then use a smart phone, tablet or computer on the same Wi-Fi, enter this number as a web address. The web page should open up with the current date and time. There is a slide switch for the alarm mode on the device. You can set the alarm with the input field in the blue circle, and press the ‘Activate Alarm’ button. You should hear a pleasant confirmation sound from the buzzer. If the alarm is set in the ‘daily alarm’ mode, the LED on the device should turn on. If the alarm is set in the ‘weekday-only’ mode, then the LED on the device will flash on and off every two seconds. When the alarm is going off, you can snooze with the button on the device. To turn the alarm off, press the “Alarm Off” button in the red circle on the web page. It should still be set for the next day. To turn off all alarms, press the ‘Disable All Alarms’ button, also in the red circle on the web page and you will not hear the alarm until you set it again.

When the alarm is going off, you can press the snooze button and snooze for another 10 minutes. From that point, the light has to be on and the alarm will not turn off if the light is not on. The snooze will only last one minute at that point. As a backup, he alarm will only go for 2 hours at a time. If you left it set and went away for a week, it will go off for 2 hours each day, but not 24-hours a day.

If you are trying to decipher the code, here is a list of the functions and a brief description of each. Hopefully it helps to make more sense.

Highlights of the Circuit

ATtiny85_display circuit.png
Flickering.png

Displays the time using a common-cathode 4-digit, 7-segment display

On a seven-segment display, each segment is an LED light and is labelled ‘a’ to ‘g’. In a common cathode display, all seven of these have their cathodes connected to a single pin. So, if that pin is low, and some are the a-g pins are high, then those segments will be lit up. In a 4-digit display, each digit has its own cathode. Then all four ‘a’ segments, use the same pin; all four ‘b’ segments use the same pin, etc. Since this is a clock, one extra pin, which is part of the 3rd digit (from the right) is used for the colon (‘:’). Thus, there are 12 pins to control the 29 LEDs. The microcontroller has to quickly pull one cathode low and bring the correct combination of segments high to illuminate one digit. These are switched off, and the next digit’s cathode is pulled low and the segments for that number are pulled high, etc. If the switching can happen fast enough, then these four digits appear to all be continuously on because of a phenomenon of “persistence of vision”. If the switch is not fast enough, the numbers will appear to flicker, or even worse, flash continuously. In this project, the microcontroller uses only three GPIO pins to control the 12 pins of the display by using two shift registers.

Uses three (3) GPIO pins to operate the 12-pin display through the use of two generic shift registers (74hc595)

These 16-pin, through-hole, shift registers require three pins from the microcontroller. These are connected to “shift register clock input’ (SHCP, aka the ‘latch’ pin, on pin 11 of the IC), “storage register clock input: (STCP, aka the “clock” pin, on pin 12), and “serial data input” (DS, aka the “data” pin, on pin 14). The microcontroller uses three GPIO pins (21, 19, and 18, respectively on the ESP32) to provide the input. Both shift registers have their latch and clock pins connected together. Then pin Q7’ which is pin 9 of the IC, is connected to the data pin of the second shift register. This way, the function, displayOneDigit(), brings the latch pin HIGH so that the shift registers can accept data, then shift outs one byte (8 bits) that will determine which digit on the display will be affected, and then a second byte to determine which segment will be lit. This second byte is sent starting from segment ‘a’ and ending in segment ‘g’, and if necessary, the last bit is also put high for the colon. The clock pin will cycle high and low, repeatedly, and on the rising edge of that signal it will take the value of the data pin, which will be either high or low, as the value of the next bit. Once both bytes have been transmitted, the latch goes low and the dislayOneDigit() function ends. Although discussed later in the multitasking section, there is a quick pause and then the cycle repeats for the next digit. By going through one digit after another fast enough, the display appears to have all four digits and the colon lit simultaneously.

As you can imagine, the shift registers are not very smart, they do not know what numbers are. So, it cannot accept a number like ‘2’ if you want to show a 2. You actually have to send the pattern to represent which LEDs in the seven segments are lit. The segment “a” starts at the top and the order goes clockwise around to the top left, with the central segment being ‘g’. So, to get a ‘2’ to show, the ESP32 will send the first byte for the digit, then for the ‘2’ the 8 bits sent right to left is ‘01011011’, which represents the segments ‘: g f e d c b a’. The conversion of the number into the correct pattern saved to an array, is handled by the function parseTimeData(in Hour, int Minute). The pattern for the numbers is handled by getDigit(byte number). The two patterns for each number are used in displayOneDigit(byte n).

Example code for an ATtiny85, two shift registers and the display

My original thought was that I would have a dedicated ATtiny85 to control the display. This microcontroller has 6 digital pins, and thus is the reason I had set up the display with two shift registers. There are man tutorials on how to program the ATtiny85, so I won’t describe that here. At one point I had decided to forego the display entirely, but changed my mind near the end of the semester to see if I could get the ESP32 to multitask the control of the display. So, I just ported the code and wiring that was set up for the ATtiny85 to the ESP32. If I thought about it, and used four GPIO pins of the ESP32 for the cathodes of the display, the second shift register would be unnecessary, and the multitasking to the display much faster. In any case, I am including the code I used for an ATtiny85 when I prototyped the shift registers and the display if you just want to try without any time keeping. In this code, I used an Arduino to send numbers to the ATtiny85 and it would display them on the display. For example, if I sent 1256, then the display would show 12:56. This code can be adapted if you have shift registers and another method of tracking time (real time clock or GPS, etc.), or if you had a sensor and wanted to visualize the value on the display. Make sure the shift registers, Arduino and ATtiny85 all have their ground lines connected. The sample code has the Receive pin (rx) f the ATtiny85 as D3, and the Transmit pin (tx) is D4. Since both ATtiny85 and Arduino are communicating to the computer, connect the Tx pin of the Arduino to D4 of the Attiny85 (physical pin 3); and if you want to print from the ATtiny85 then the Arduino Rx pin to D3 (physical pin 2 of the ATtiny85). Ensure that the Arduino has a “BareMinimum” sketch loaded onto it, and that its serial monitor sends a ‘new line’ ending, or both new line and carriage return. I was using 5 volts for everything in this set up.  

Regarding the 2N2222 bipolar junctional transistors for each cathode of the display. If running at 5 volts, and the red LEDs of the display each drop 1.8V for a forward voltage, then each segment would draw ((5V-1.8V)/220 Ohm) about 14.5 mA. So, if all seven segments were on, the current would theoretically be about 100 mA, which is too much for the second shift register to sink in one of its pins. So, instead of pulling the output pin of the second shift register low to sink current, it is pulled high to use the transistor as a switch, and connect the cathode of the display through the transistor to ground. (You would have to use the transistors if you were using these same 220 Ohm resistors & GPIO pins instead of the second shift register.) If you increase the resistor values between the first shift register and the display, these transistors may not be needed, but then you have to reverse the binary pattern of zeros and ones in the getDigit() function.

Features of the ESP32 Programming

BlackBackground Moon.png
SPIFFS_dataFolder.png
image.png

An alarm clock that uses your local Wi-Fi to access the Network Time Protocol (NTP)

Admittedly, this part of the project, I understand the least. I have made many changes, but the main part of the code was provided as part of the course. I have done many internet searches to try to understand the coding with terms like: “Arduino IDE tm” or “Arduino IDE localtime_r”. These were helpful. From these searches, I learned:

These are some of the global variables related to keeping time that come at the beginning of the code:

tm localTime; // a structure with made up of several variables (see below) for different aspects of time.
const char* NTP_SERVER = "pool.ntp.org"; // For more info: https://www.ntppool.org/en/
const char* TZ_INFO = "EST5EDT";     // enter your time zone from this chart: https://remotemonitoringsystems.ca/time-zone-abbreviations.php
tm timeinfo;
time_t now; // time_t is a type of variable, like 1707600003 with the last digit advancing each sec. This seems to be a number of seconds since some time in 1970

This is the function that will access the NTP time to update your time variables when it is called. Here the argument int sec is the number of seconds before this function times out:

// This function gets the time from the Network Time Protocol from the internet to get the current time for our clock
bool getNTPtime(int sec) {
 {
  uint32_t start = millis();
  do {
   time(&now);  
   localtime_r(&now, &timeinfo);
   delay(10);

  } while (((millis() - start) <= (1000 * sec)) && (timeinfo.tm_year < (2016 - 1900)));
  if (timeinfo.tm_year <= (2016 - 1900)) return false; // the NTP call was not successful
 }
 return true;
}

Then in the loop(), the getNTPtime(int sec) function is called to access the time over Wi-Fi, at which point the time the individual time variables from the timeinfo structure can be accessed. Here is a function that can be called to show some of these updated time variables:

void showTime(tm localTime) {
 Serial.println('\n');
 Serial.print(dayOfTheWeek(timeinfo)); // Serial.print(localTime.tm_wday); a number representing the number of days since Sunday (i.e., Sunday=0)
 Serial.print(", ");
 Serial.print(localTime.tm_hour);
 Serial.print(':');
 Serial.print(localTime.tm_min);
 Serial.print(':');
 Serial.println(localTime.tm_sec);
}

These two functions can be printed, for example, Serial.print(dayOfTheWeek(timeinfo); or Serial.print(month(timeinfo); as they are return a String variable:

String dayOfTheWeek(tm localTime) {
 DaY = localTime.tm_wday; // ‘DaY’ is one of the global variables at the beginning of the code
 char wday[10] = { 0 };
 if (isnan(DaY)) {
  // Serial.println("Failed to get day of the week");
  return "--";
 } else {
  switch (DaY) {
   case 0:
    strcpy(wday, "SUNDAY");
    break;
   case 1:
    strcpy(wday, "MONDAY");
    break;
   case 2:
    strcpy(wday, "TUESDAY");
    break;
   case 3:
    strcpy(wday, "WEDNESDAY");
    break;
   case 4:
    strcpy(wday, "THURSDAY");
    break;
   case 5:
    strcpy(wday, "FRIDAY");
    break;
   case 6:
    strcpy(wday, "SATURDAY");
    break;
  }
  return String(wday);
 }
}


// Get the Months
// https://mikaelpatel.github.io/Arduino-RTC/d8/d5a/structtm.html
String month(tm localTime) {
 int t = localTime.tm_mon;
 char mon[10] = { 0 };
 if (isnan(t)) {
  // Serial.println("Failed to get Month");
  return "--";
 } else {
  switch (t) {
   case 0:
    strcpy(mon, "January"); // starts counting from zero
    break;
   case 1:
    strcpy(mon, "February");
    break;
   case 2:
    strcpy(mon, "March");
    break;
   case 3:
    strcpy(mon, "April");
    break;
   case 4:
    strcpy(mon, "May");
    break;
   case 5:
    strcpy(mon, "June");
    break;
   case 6:
    strcpy(mon, "July");
    break;
   case 7:
    strcpy(mon, "August");
    break;
   case 8:
    strcpy(mon, "September");
    break;
   case 9:
    strcpy(mon, "October");
    break;
   case 10:
    strcpy(mon, "November");
    break;
   case 11:
    strcpy(mon, "December");
    break;
  }
  return String(mon);
 }
}


Checking the Alarm and snooze times

In the loop() the time is updated about 5 times per second with the getNTPtime() function and delay(200) which causes a 200-millisecond delay. Then if the alarm is set, then a function called checkAlarmTime is called with takes two arguments alarmHour and alarmMinute. These two variables were set when the alarm was set with the web interface. If these match the current hour and minute, then the buzzerFlag variable is given a value of 1, discussed below, and the alarm state is changed to alarmSounding (more about the different states in the section of employing a ‘state machine’). With this new state, the next cycle through the loop(), ambient light intensity is measured with the help of a function called checkAmbientLight(). This should occur within the first second of the alarm, and will create a threshold above the intensity of the ambient light. This threshold will be used to judge if the lights are on in off in later stages of the alarm. If the snooze button is pressed setSnooze(int delay) will turn off the buzzerFlag and set a snooze time by adding the ‘delay’ value. The first snooze will have a 'delay' value of 10, so in the setSnooze() function, this is added to the current time as the end of the snooze. For example, if the buzzer sounds for 45 seconds until the person presses the snooze button, and the ‘delay’ is for 10 minutes, then the snooze will last 9 minutes and 15 seconds because that is how long it will take for the minutes on the clock to increment 10 more. Similarly in later states of the alarm/wake process, if the snooze button is pressed, the ‘delay’ is only one (1), so the most the snooze will last is one minute. If it is 30 seconds into that minute when the snooze is pressed, there is only 30 seconds until the alarm re-activates. It is important to keep the alarmHour and alarmMinute, separate from the snoozeHour and snoozeMinute variables. These snooze numbers control when the alarm will go off next. By keeping them separate the alarm numbers are not changed, while the snooze variables can each time the person decides to snooze a little longer. This separation allows for the alarm to be automatically set for the same time each day without needing to reset it each day.

Regarding the function checkAmbientLight(), it takes a few values to get an 'average' for the light level. Then creates a threshold by adding 200 to this. However, if the average is high to begin with, like if the person fell asleep with the lights on, or is sleeping in the daylight, then it is unlikely to get brighter when the get up, so the threshold is set just below the ambient light level. This is to prevent future readings of the light level to be affected by noise on the analog measurement, or if their arm crosses the clock and causing the alarm to behave as if lights were turned off when it is just a slight variation.

Passive buzzer and using ledc functions of the ESP32

The buzzer is a passive buzzer with its anode connected to GPIO pin 32 (indicated by the global variable ‘buzzerPin’). The buzzer is controlled by a Boolean variable called buzzerFlag, mentioned above. At the end of the loop(), this flag is checked to see if it is set to 1, and if it is, a function named buzzbuzz() is called. This function will turn on the buzzer using a series of functions specific to the ESP32 that all start with ‘ledc’. These turn the buzzer on, and control the pitch and volume. The alarm uses two tones, which draws more attention and therefore would be more effective at waking the person. This duration of each tone can be modified by changing the global variable toneDuration. This is usually 1200 mSec, but is randomized in later stages of the waking process to again help wake the person if they are snoozing instead of getting up.

These are in the setup:

 ledcSetup(0, 800, 12);    // configure LED PWM functionalities; (ledChannel, freq, resolution)
 ledcAttachPin(buzzerPin, 0); // attach the channel to the GPIO to be controlled; (ledPin, ledChannel)

These are in buzzbuzz(), you can see that the frequency of the tone switches between 800 and 262 Hz:

ledcWrite(0, 255); // Ensure the buzzer will sound; 255 is Maximum; (ledChannel, dutyCycle)
 if (toggleTone == 1) {
  ledcWriteTone(0, 800); // buzzer on channel 0, 800 Hz
 } else {
  ledcWriteTone(0, 262); // buzzer on channel 0; middle C is "c4" which is about 262 Hz

  This is used to turn the alarm off:

  ledcWrite(0, 0); //Setting PWM to 0 = "off"; (ledChannel, dutyCycle)

Using another couple of functions, playSound(in Tone, in Duration) and confirmSound(int tone1, int duration1, int tone2, int duration2), the device plays either a confirmation sound, or an error sound, which tells the person whether a command from the web interface was successful or not. These can be used in any ESP32 project with a user interface, along with the code that set up the buzzer in the setup(). To play similar sounds as this project, just call the function confirmSound(262, 300, 659, 200); for a confirmation/success sound, or confirmSound(2500, 200, 200, 500); for an error sound.

Adjustable mode: daily alarm (Sun-Sat) or weekday-only (Mon-Fri) 

As mentioned above, one feature that is helpful for an alarm is that it can be turned off without disabling it for the next day. If one has to reset the alarm after turning it off, they might forget (when they are still sleepy from waking up) or they might make a mistake re-entering the alarm time. These sources of human error would make the alarm clock less reliable. 

The other addition is the ability to switch from this daily alarm to a weekday-only alarm. This is controlled by a small slide switch on the 7x5cm perfboard. If you have trouble getting up during the week for work or school, then perhaps you are the type of person who wants to sleep in one the weekend. This mode, using a Boolean flag called dailyMode set to zero, will allow for that. When the checkAlarm() function is called, the localTime.tm_wday variable is checked if it equals either a zero or a six, which represents Sunday or Saturday, respectively. In the time structure that we called ‘localTime’ in the global variables, the weekday is a number from 0-6 representing each day of the week. It is updated each time the NTP time is updated in the loop. This could be done with the NTP time without the need for a web interface.

Multitasking using the real time operating system (free RTOS) 

The ESP32 has a dual core, and both can operate independently. ‘Core 1’ generally runs most of the Arduino code, and ‘core 0’ runs the Wi-Fi and Bluetooth. The ESP32 also has a real time operating system called ‘freeRTOS’. Just like a computer’s RTOS, freeRTOS has some rudimentary multitasking ability. Using the RTOS, it is possible to program one or more tasks to run on ‘core 0’ while the main program runs simultaneously on core 1. In fact, there are several tutorials online that show how to do this. However, on closer inspection, these seem to all be simple tasks, like blinking an LED. More complex tasks might cause the microcontroller to periodically reset. This happened when I tried to add displayOneDigit() with a small delay as part of a task on core 0. This displayOneDigit() function is just four lines of code, so I thought with the delay there would be time for the Wi-Fi functionality on core 0. However the microcontroller was constantly resetting after just a few seconds of running, so I abandoned trying to use core 0 for the display. Based on what examples I see online, and my limited experience here, I don’t think there is much use for multitasking on core 0 if it is also serving a Wi-Fi connection.

Ultimately, these efforts were not in vain however. By taking advantage of the RTOS, the task for controlling the display could be run on core 1 instead of core 0. Multiple tasks can be added to core 1. So the task was added with displayOneDigit(). The task employed a mutex (mutually exclusive) semaphore, called ‘baton’ based on a YouTube tutorial by Andreas Spiess. The baton is used to prevent the microcontroller from trying to read the time data at the same time it is trying to save to it. A collision of these two events could cause the program to become unstable and reset. The main goal of the multitasking was to get the clock functions in the loop() to work while still showing the time on the display without any flickering or flashing. That would mean the microcontroller was not able to run the display fast enough. Some minor flickering does occur but it isn’t obvious with a quick glance at the display. 

To get the multitasking, there are two declarations with the global variables, one is to declare the name of the task using ‘TanskHandle_t” and the other is to declare the semaphore 'baton'.

TaskHandle_t displayTimeTask0;
SemaphoreHandle_t baton;

This task handle will be used to name the task in the setup(). In my case, this handle is ‘displayTimeTask0’. In the setup() the parameters of this task, including the name of the function for the task are specified in xTaskCreatePinnedToCore(). For more information about all the parameters, I will direct you to the previously mentioned tutorial by Andreas Spiess. In my project, this function is called codeForTask0( void * parameter). To run infinitely so that the multitasking runs infinitely, it uses a ‘for’ loop which works well because the index of the ‘for’ loop is used to switch from one digit of the display to the next, and when the last digit is displayed, the index is reset to zero and starts again.

The semaphore is declared in the setup() with the coding for the task handle:

baton = xSemaphoreCreateMutex();


xTaskCreatePinnedToCore(
codeForTask0,  /* Task function */
"displayTimeTask0",  /* name of task */
1000,   /* Stack size of task */
NULL,   /*parameter of the task */
1,   /* priority of the task */
&displayTimeTask0,   /* Task handle to keep track of created task */
1);   /* Indicates which core */


And to use it in a function, the baton is “taken” by the function right before the variables are accessed: 

xSemaphoreTake(baton, portMAX_DELAY); 

The delay in this function is to specify how long the task is supposed to wait for the baton. In this case portMAX_DELAY should have it wait the maximum amount, which apparently can be modified, but I did not get a chance to look into this. Then the baton is given back right after so that the other function can access the variables:

xSemaphoreGive(baton);

You can find these two 'xSemaphore' lines of code in the function parseTimeData(int Hour, int Minute), and in the above mentioned task codeForTask0(void * parameter) These both access the array timeValues[ ], but with the semaphore, no collision takes place.

The final outcome of this exploration into multitasking was satisfactory. It did require some experimentation with how to program the loop() (see next section). The issue was if the loop() takes up too much of the processor’s time, the display begins to flicker. Since the display has to illuminate one digit at a time in rapid succession to get the appearance of all four digit being illuminated simultaneously, there cannot be much delay between illuminating digits. Perhaps the display would work better with a timer interrupt instead of multitasking, but this experiment was worth exploring to see what the RTOS multitasking could do.

Optimizing programming of the multitasking: My hypothesis about the RTOS and the effect of different timing in the loop affects the performance of the multitasking (i.e. minimize flickering of the display)

As both the loop() and the task are serviced by core 1 by “taking turns”, the coding in the loop() appeared to slow down the display and affect the flickering. In the original sketch provided for this college project, the loop() had delay(200);. The purpose of this is to, presumably, slow the cycling through the loop so that the update of the NTP time and comparing the time to the alarm time only occurs about five times per second. As a simplified hypothetical:

loop() {
// update the time
// check the alarm
delay(200);
}

Generally, in Arduino programming, a delay() like this, although very simple to implement, is discouraged because it stops the microcontroller until the delay is finished. No other lines of code in the loop() are run during that time. What is usually encouraged is the use of the millis() and comparing it to a target time in the future:

loop() {
if (millis() > targetTime) {
targetTime = millis() + 200;
// update the time
// check the alarm
}
// The microcontroller would be free to run other code here
}

Although usually recommended over the use of a delay(), this second structure of coding actually made the display flicker a little more. Both methods probably made the updating of the time and checking the alarm occur about 5 times per second, but the later caused the microcontroller to cycle through the loop() as fast as possible over and over. This would be a constant demand on core 1 when it must also share resources for the display task. Whereas the delay() actually frees up core 1 and permits the display task to run more frequently. So, although the use of the millis() is usually preferred, in this case the delay() worked better. That's my hypothesis anyway.

Employs a State-Machine programming structure 

The alarm/waking is a process, in which there are different phases (i.e., states) to this process. In an attempt for more efficient coding, the main part of the loop() is programmed as a 'state machine' to only go through these phases as necessary, and just run the code relevant to that phase. This is done with the use of an 'enum' variable, and a switch case. The enum is declared with the global variables, and defines the names of the states used in the alarm-waking process. Here we called the enum ‘listOfStates’:

enum listOfStates {
noAlarm,    // Time is running but no alarm will be active in this state
alarmSet,    // Alarm is set, but not sounding. This state will allow for a daily alarm and will check if the alarm hour/min equal current hour/min
alarmSounding, // Buzzer triggered because: Alarm hour/min equal current hour/min. Ambient light level is sampled
firstSnooze,  // Alarm sounded already but the person has snoozed first time.
overSleeping,  // subsequent snoozing is oversleeping
alarmOff,    // This temporary state for the alarm being turned off right away. Alarm hour/min will still equal current hour/min, after 1 min, will change to alarmSet
  // each of these is now like a number, with noAlarm==0, alarmSet==1, etc.
};

Then, after these have been defined, an instance of this new enum “variable” is created so that we can refer to it by a name, which we define as ‘alarmState’. It is started off in the ‘noAlarm’ state:

listOfStates alarmState = noAlarm;

In the loop(), using a switch case reduces the number of conditional statements the microcontroller has to evaluate to just what is necessary during that state. Once the microcontroller goes through all the code for that state and comes to a ‘break();’, it then exits the rest of the switch case section, bypassing all the remaining code, and continues through the program. And by reducing the number of lines of code to get through in the loop(), the more frequent the delay(200) command is run. It is this delay() that frees up core 1 in the loop() and would allow core 1 to service the task that operates the display. This was important for minimizing the flickering of the display as both the task to run the display and the loop() are running concurrently on core 1. As a further example, if the clock was in the ‘alarmSet’ state, and there were three ‘if’ statements within the alarmSet case as shown below:

switch (alarmState) {
   
  case noAlarm: 
      // code for noAlarm removed…
     break;

  case alarmSet:  
    checkAlarmTime(alarmHour, alarmMinute);
    if (dailyMode) {digitalWrite(LEDpin,HIGH);} else {flashLED(1000);}
    if (digitalRead(buttonPin)==0) showAlarm();
    if (buzzerFlag==1) alarmState=alarmSounding;
    break;



This code would cause some minor flickering. And if the alarm is being used on a daily basis, then the display would have that minor flickering 24-hours a day because the clock would be in the alarmSet state every day, all day. If, however, the last statement, ‘if (buzzerFlag==1)..’ is moved into the above function ‘checkAlarmTime()’, the functionality is the same but the reduction by one ‘if’ statement eliminated the flickering. YOu can see this 'if (buzzerFlag==1)..' line of code is in face in the checkAlarmTime() in the final version of the code. 

Storing an image file (PNG file) on the ESP32 to be included in the webpage (SPIFFS)

The Serial Peripheral Interface File Flash System (SPIFFS) is a method to be able to save files other than the Arduino coding to the ESP32. It apparently will not work with the Arduino IDE 2.0 so the programming must be done with the version 1 of the IDE. The procedure is summarized below:

To be able to upload and use the image on the web page the SPIFFS.h library from Github must be installed and included in the code.

Then in the setup(), the SPIFFS must be initialized such as the following Arduino code:

 if(!SPIFFS.begin(true)){
  Serial.println("An Error has occurred while mounting SPIFFS");
  return;
 }

The ESP32 is also referred to the image file by name, ‘image.png’ in the setup():

server.on("/image", HTTP_GET, [](AsyncWebServerRequest* request) {
   request->send(SPIFFS, "/image.png", "image/png");
 });

In the section for the HTML code (which comes before the setup()), the image can be placed by referring to it by name, in this case the PNG file is called “image”:

<img class="pic" src="image"></img>

To include the image file, it has to be saved by the same name as what is specified in the html code, ‘image.png’ in this case. This file must be in a folder named ‘data’ (see picture of the data folder next to the code), and this folder must reside in the same folder as the Arduino ‘.ino’ file for the project itself. The file is then uploaded to the ESP32 separately from the Arduino code. To do so, in the Arduino IDE make sure the ESP32 development board must be connected properly with the correct com port (for PC). Then user must navigate to ‘Tools’, then to ‘ESP32 Sketch data Upload’. The Arduino code itself must also be uploaded to the ESP32 as usual. So, because the main code and the SPIFFS file are uploaded separately, if the Arduino code is updated and uploaded, it will not erase the image file.  

The image.png file is at the top of this section. It is hard to see because the transparent portions show up white, so I have included a copy with a black background just to see it better here. If you open the white file with the moons, you should be able to download it. It is transparent to allow for the text effect on the web page. Basically, the image in this project was Photoshopped, starting with the image of the full moon on Wikipedia. The background was removed with the 'background eraser' in Photoshop. Several copies of the image of the moon were made in the same image file on separate layers. The 'eraser' tool was used to make the appearance of the phases of each moon. The solar eclipse image was drawn in the center of the file by drawing several copies of a bold white circle, then using different ‘blur’ tools within photoshop to give it the wispy appearance. If someone wants me to go through this, let me know... Anyways, more on the text effect in the section on the text effect.

Highlights of the ESP32 Webpage Interface

ScreenShot web interface.png
State changes.png

Customize the placement of elements on the ESP32 web page using HTML and CSS.

A major challenge when developing the Hypertext Markup Language (HTML) and Cascading Style Sheets (CSS) code for the web interface is that not everything that works for a normal web page worked for the ESP32 web interface. Here is an example snippet of code that works in the w3schools online text editor and as an ‘.html’ file written in Notepad and opened with Chome: 

<!DOCTYPE html>
<html><head><style>

body {background-color: blue;}

.box{
  position: absolute;
  top: 50%;
  left: 25%;
  width: 50%;
  height: 30%;
  background-color: black;
}

</style></head>

<body>
   <div>
     <span class="box"></span>
   </div>
</body></html>

The result should be a blue screen with a black box that starts in the centre the screen and is 50% the width of the screen and 30% of the height. Pretty simple. This DID NOT WORK on the ESP32 however. The result was a blank blue screen. So, there is nothing visible to help figure out where the problem might be. If the result was a box of the wrong size, or position or colour, then there is a hint where to look for an error; but not for a blank screen. Ultimately, I was able to figure out a reliable way to position elements on the screen without using percentages for position and sizing. Essentially, a “container” should be placed within the page. This can be set a number of pixels from the top (e.g., 10 pixels) and is easy to center horizontally using: margin: 10 px auto;. It does not need any border or color and can therefore be invisible if needed. Then in the HTML code, other elements within the <div> and </div> tags can be placed relative to that container. This will allow these elements to be placed relative to the centered container on whatever screen (computer, smart phone) is being used for the web interface, regardless of whether it is landscape or portrait. Importantly they have a predictable position relative to each other.

Here is an example of using a container to place the two circles with coding that works when uploaded to the ESP32: the blue circle is up and to the right form the container’s position, and the red is down from the container and to the left. (There is a line that is commented out, that if you uncomment, you can see the container.) There is text written within container1, but because container1 has no colour itself, the text is floating in space. Multiple containers can be placed on the web page and used in this way to place elements in the x, y locations on the page. And elements (e.g., buttons) or text can then go within each, for example shapeBlue and shapeRed have button in the web page for this project.

<!DOCTYPE html>
<html><head><style>

body {background-color: blue;}

.container1 {
 display: flex; /* needed to place the shapes within it */
 margin: 120px auto; /* centers the container1 120 pixels from the top, affected by </br> spacing in the HTML below */
 width: 300px;
 height: 150px;
 align-items: center;
 justify-content: center;
 text-align: center;
 color: white; /* colour of the text */
 /* uncomment the line below to see the box */
 /* background-color: black; */
}

.shapeBlue{
 position: absolute;
 translate: 150px -100px; /* move the blueShape: x-pos (pos=right), y-pos (neg=up) */
 width: 180px;
 height: 180px;
 border-radius: 90px;
 background-color: RoyalBlue;
 display: flex; /* if displaying anything within shapeBlue */
 align-content: center; /* centering anything that will be placed within shapeBlue */
}

.shapeRed{
 z-index: -1; /* Now placed "under" container1 */
 position: absolute;
 translate: -150px 100px;
 width: 180px;
 height: 180px;
 border-radius: 90px;
 background-color: red; 
 display: flex;
}
 </style> </head>

<body>

<br/> <!-- This does affect the position of container1 -->

<div class="container1">
   <p>Text Floating in Space</p>
  <div class="shapeBlue"></div>   
  <div class="shapeRed"></div>   
</div>
</body></html>


Cool Text Effect: Embedding the image into text

This effect was based on the tutorial by Code Artist . I was not able to use any animation like the Code Artist does, but I was able to embed the text within an image which was still a neat effect. By using the method for positioning in the previous section, I was able to achieve this effect. Essentially, the date in large, light-blue, font at the top of the screen appears embedded in the moon image. To do this:

In the first part of the CSS, I declare the font, and include some aligning details, as well as establish the background colour of the whole page. If you want a different colour, you can do an internet search for an “online colour picker” and choose what colour you want. Then copy the code for that colour. Here the colour code is #02070d:

body { font-family: arial black; padding-top: 10px; text-align: center; align-items: center; background-color: #02070d;}

Then I create a container called “center1” and give it a height and width. The margin command places it in the center left-to-right. The line “display: flex;” is used if you want to place elements relative to this container. The next element refers to the image. If you go to the ‘properties’ of the image file, and click on ‘Details” you can see the dimensions. Mine happened to be 477 pixels wide and 75 pixels high so I specified that here in the CSS of the image, and basically for center1 as well:

.center1 {
 height: 75px; 
 width: 480px; 
 margin: auto;
 display: flex;
 justify-content: center;
 align-items: center;
}
.pic{
 width: 477px;
 height: 75px;
 margin: auto;
 }

Then for the text of the date at the top of the web page they are centred within the container because of the container’s justify-content: center; which centres things right-to-left (horizontally), and align-items: center; which centres things up-and-down (vertically). Then because both given the position characteristic of absolute, which ignores the position of other elements in the container (they are not placed next to each other). Text1 is given a light blue colour; and with z-index: -1; it is placed UNDER the image. Text2 is transparent; placed ABOVE the picture with z-index: 1; and is given a dark outline of 3 pixels wide:

.text1 {
 position: absolute;
 font-size: 50px;
 font-weight: bold;
 color: LightSkyBlue;
 z-index: -1;
}

.text2 {
 position: absolute;
 font-size: 50px;
 font-weight: bold;
 color: transparent;
 z-index: 1;
 -webkit-text-stroke: 3px #230414;"
}

These all come together with the HTML, where the image, called “pic” and the two texts are together within the same <div> and </div> tags of the center1 container:

<div class="center1">
<img class="pic" src="image"></img>
 <span class="text1">%DAY%,&nbsp%MONTH%&nbsp%DATE%</span>
 <span class="text2">%DAY%,&nbsp%MONTH%&nbsp%DATE%</span>
</div>

The %Day%, %MONTH%, and %DATE% come from the series of setInterval(function ( )) functions that come in the script portion of the HTML code. Each of these functions gets the data for the day, month and time at regular intervals set in the functions. Here is an example for the Day of the week. It is set up to check every 1000 milliseconds, way too frequent but I ran out of time to fix it.

 // ****In BODY instead of %DAY% put a value getting from the function **** //
setInterval(function ( ) {
 var xhttp = new XMLHttpRequest();
 // Serial.println(xhttp);
 xhttp.onreadystatechange = function() {
   if (this.readyState == 4 && this.status == 200) {
     document.getElementById("DAY").innerHTML = this.responseText;
     }
 };
 xhttp.open("GET", "/DAY", true);
 xhttp.send();
}, 1000 ) ;


Colouring using gradients to colour elements on the webpage

Along the lines of code that worked for websites but not on the ESP32 webpage, here is an example taken from the Code Artist’s Glassmorphia example. It produces the amazing look of the time displayed on the glass and the red and blue circles blurred by its translucency. Code Artist, uses a subtle gradient pattern to colour the circles, but I could not get it to work. Not to say that there isn’t anything wrong with the Code Artist’s code, this just did not seem to work with the ESP32 webpages:

background: linear-gradient(
    45deg,
    #ff5b84,
    #eb3461
  );

After some searches, I was able to come up with coloring with gradient. Here are some example code for a gradient with two colours, or with three colours. You can place these into the shapeRed and shapeBlue CSS code above instead of their background-color commands and paste them into w3school’s text editor to see how they look:

background-image: linear-gradient(to bottom right, RoyalBlue, Indigo);


background-image: linear-gradient(to bottom right, Maroon, Tomato, Maroon);


State changes for the buttons

There are three push buttons on the webpage that react with state changes. Most of this programming comes from another tutorial by Random Nerd Tutorials. The first effect is the background colour changing when the cursor or mouse hovers over the button. This is useful to indicate to the person that the button has some effect and is not just an image. To choose colours, search on the internet for an “online colour picker”, and try different colours until you get one that you like. These colour pickers then give you the code for that colour in one of serval methods. In the code below, the ‘:hover’ indicates that the following code is to be run when the mouse/cursor hovers over the button, and #5c1a01 is the darker red for the buttons that turn off the alarm. 

button:hover {background-color: #5c1a01}

Then when the button is clicked or pressed, the state-change effect is that the button looks like it is pressed down. This is accomplished by the ‘:active’ modifier to 'button' that indicates that the following code is to run when the button is pressed. The line transform: translateY(2px); gives the appearance of the button moving downward. 

 button:active {
 background-color: #5c1a01;
 box-shadow: 0 4px #666;
 transform: translateY(2px);
 }

In the HTML, both the red buttons are within “container2” which is positioned over top of the red circle. More specifically, the red circle, called redShape, has z-index: -1; which places it under container2

Both buttons, on a mouse click or when touched on a touch screen, will call a function called toggleCheckbox() and will send a command, either ‘off’ to turn the alarm off for the day, or ‘disable’ to turn off all alarms.

<div class="container2">   
    <button class="button" onmousedown="toggleCheckbox('off');" ontouchstart="toggleCheckbox('off');">ALARM OFF</buton>
    <button class="button2" onmousedown="toggleCheckbox('disable');" ontouchstart="toggleCheckbox('disable');">DISABLE ALL ALARMS</button>
</div>

In the script, this toggleCheckbox() is the first function, and sends the ‘off’ or ‘disable’ command to the ESP32.

// ***** Function for the online buttons - Only need one function for BOTH buttons *****
  function toggleCheckbox(x) {
   var xhr = new XMLHttpRequest();
   xhr.open("GET", "/" + x, true);
   xhr.send();
   this.value = "";
  }

Then in the setup() of the ESP32 are the lines of code that direct the ESP32 what to do when each command is sent from the web page. For example, the ‘off’ command changes the alarmState to alarmOff, turns off the alarm by setting the buzzerFlag to zero, and plays the confirmation sound so the user gets a verification that the alarm was turned off. 

 // Receive an HTTP GET request for the "Alarm off"
 server.on("/off", HTTP_GET, [](AsyncWebServerRequest* request) {
  request->send(200, "text/plain", "ok");
  if ((alarmState >= alarmSounding) && (alarmState < alarmOff)) {
   alarmState = alarmOff;  
   buzzerFlag = 0;
  } else {
   confirmSound(2500, 200, 200, 500);  
  }
 });

Highlights of the 3D Printed Enclosure

Enclosure2Lcorrected_JPG1.JPG
EnclosureFINAL_FrntPlate_PNG14.PNG.png
InnerWall.png
PerfBoardSupport Closer.png
Lid_PNG2.PNG.png
Lid cutout.png
LidAndPillarsPNG.PNG.png
EnclosureFINAL_FrntPlate_PNG11.PNG.png
EnclosureEXPL CompletePNG.PNG
IMG_4195 PNG.png
IMG_4208 PNG.png
IMG_4193 PNG.png
IMG_4213 PNG.png
IMG_4223 PNG.png
IMG_4225 PNG.png
IMG_4197 PNG.png
IMG_4147 PNG.png
IMG_4153 PNG.png
IMG_4266 PNG.png
IMG_4267 PNG.png

As mentioned, I had to design and print an enclosure as a requirement of the project. It was supposed to be either 3D printed, or laser cut, or both. Since I did not see myself ever using this clock, I wanted to design something versatile that I could use in a future project. I have some calipers that I used to double and triple check the dimensions of all the parts, and the spacing of pins and holes, so that I could get an exact placement of each part. Generally, I gave about 0.25mm spacing for clearance, but that must depend on your 3D printer. And since it was my first 3D print, I wanted the design not to need any support material. The most significant part of this was the placement of the hole for the USB cable. To get the hole without any need for support material, it was pushed up so he hole is part of the lid.

For my first 3D print, I am quite pleased with the enclosure, and have included the files below if you want to print it. You might find it useful even if you have no interest in the clock or even in the ESP32. The enclosure is basically a box, with a sliding lid. If you are not doing the same project as this, you would only need the box and the lid for 3D printing. The front plate is a separate piece of acrylic that is 7cm wide, 3.9cm high and 3mm thick. This seems like a standard thickness of different materials. My original was cut with the laser cutter, but when I added the display, I found a 6 inch square piece of black translucent acrylic in Toronto for about $5 Canadian dollars. This acrylic was not too difficult to cut by hand. I tried to score it several knife cuts along a ruler. When I tried to bend it, most of it broke on the line but not all of it. When I used a small hack saw, it was rather easy to cut, so I don' think I will try to score and cut it again. I filed the edges, and drilled out the holes using the laser cut piece as a template. Any holes that needed widening, I used a rat-tail file. All this to say, that the front plate allows for the enclosure to be easily customized without having to 3D print the whole enclosure.  

The enclosure is designed to hold three generic perforated circuit boards: one 7x3cm, one 7x5cm and one 6x4cm. This project only used the first two, but there are stand-offs with holes for M2 nuts and screws in the floor to hold the 6x4cm perfboard. The 7x3cm board sits vertically 0.82cm behind the front plate. This distance is just enough to fit the display. This meant that two small supports were 3D printed for the buzzer and the LDR, which are exposed through the front plate, as well as the pushbutton (10mm tall). The front plate and vertical perfboard slide into slots. These are a perfect snug fit and do not need any adhesive or screws. When all the screws are in place, shaking the enclosure is silent - no rattling.

The microUSB cable plugs into the ESP32 module. Since the board uses male header pins and were plugged into female header pins on the 7x5cm board, then almost any small module that uses these pins should be able to be plugged in with a similar sized microUSB or miniUSB or USB-C cable. So, an Arduino Nano, RP2040 development board, or a TP4056 module for a lithium rechargeable battery. This later module does not use the typical 2.54mm pin spacing, but it would fit, with some minor angling of the pins. Caution for any board you intend to use, there isn’t a lot of overhead room so the other components on the development board (or module) must not be higher than the microUSB port. Most perfboards seem to be 1.6mm thick, and the smaller USB connectors I mention above that I have used all just fit under the lid. However, a module with male header pins, plugged into female header pin on the 7x5cm perfboard, with a typical through hole integrated circuit in an IC socket, would be too high and would prevent the lid from closing.

The lid is fairly sturdy, and is held in with two M2 nuts glued into the pillars on the lid, and two 10mm M2 screws from the underside of the enclosure and through the two pillars. The inside of the pillars are wide enough for a screwdriver from the bottom and the end of these channels have a fillet for easy insertion of the screws. The horizontal perfboards (7x5cm and 6x4cm) are similarly held with M2 screws with the nuts glued into place.

There are four feet on the outside of the box. These are included to give some character to the appearance, so the enclosure was more than just a box. I added a small dimple at the centre bottom of an impression on the top of each foot. These are meant to be used to drill a pilot hole for a screw hole if this enclosure was to be mounted onto something more permanent or if attached vertically.

I have included the SolidWorks technical drawings at the end of the Instructable. They were required for the completion of the project. The box is rather complex with numbers everywhere, and would have probably been better on two pages. Maybe someone will find it helpful.

Note: there was an error in my print, but I did not have the time to correct it before sending the STL files to be printed. The shelf for the lid at the top of the enclosure partially blocked the 7x3cm perfboard from sliding in. Without the time to correct this, I knew I would have to trim both sides of this perfboard. However, the error is corrected in the SLT files here by enlarging the front of the lid so the shelf does not extend to where the vertical perfboard slot is. If you compare the images to the photos, you can see the small different where the front of the lid mates with the box.

Conclusion

This was my first time using an ESP32, designing in a 3D CAD program and 3D printing. Plus with the time being limited to one semester to learn all these things and implement them into the project, there are a lot of areas for improvement. The display for example, occasionally it has some minor flickering. Overall, it works fairly well, but simplifying to one shift register, trying register level programming (instead of Arduino commands), or just using a separate microcontroller like I had originally intended, would have made the display a little better. The HTML CSS code needs lots of work, it has redundant parts and some of the date variables are updated way to often. Another future consideration, is the next version of the enclosure. I like the versatility of the front panel, which was easy to cut, even by hand. Plus the design of the enclosure lends itself to a lot of options for a complex circuit, or a battery-operated project. However, if I were to design another enclosure, I would try to make something less “boxy”. To make the clock more reliable, I would add a backup rechargeable battery and a real time clock (RTC) integrated circuit. These would allow the clock to work even if the power or the Wi-Fi stopped. The ESP32 would trickle charge the battery, and periodically update the RTC. So it should keep decent time if unplugged and moved to another location where it is plugged in again. Then even if away from the local Wi-Fi for weeks at a time, it would still keep accurate time. Maybe the at that point a Bluetooth interface could be used since the local Wi-Fi was not available for the web interface. But with the RTC, the clock would show accurate time as long as it was plugged in to keep the battery charged at the new location. Like I said, lots of areas for improvement.

This project has a lot of good points, and I certainly learned a lot by developing it, and putting it all together. It let me try a lot of different things I have never tried before and it forced me to learn 3D design and printing. I hope that through this Instructable, you are able to get enough information or ideas to use in your own project. If you have feedback or questions, please leave them below. Thanks for reading.