ESP WATCH Using IPS Display ST7789V
by M_F_T in Circuits > Wearables
7664 Views, 90 Favorites, 0 Comments
ESP WATCH Using IPS Display ST7789V
Hi everyone! In this project, I'll show you what I've created: an ESP watch using the ESP32 and a 1.69-inch TFT-LCD display with the ST7789V driver. The size is compact enough to be convenient. I made this project because I lost my watch, and rather than buying a new one, I thought it would be more interesting to make my own. This way, I can design and program it freely, adding whatever features I want. At the time, smartwatches were really cool to me, so here it is.
on this watch i made, you can control the date and time, and also the brightness and even have a picture inside it.
(this is my first instructables)
Supplies
here are the list of item that i used for the project
For The Main Circuit
-ST7789V 240x280 Pxl 1.69" full colour TFT IPS Display (1x)
-ESP32-WROOM32-D 4MB Flash WiFi Module (1x) (you can also use the same type ESP32 with the same pinout)
-IRLML2502 MOSFET(1x)
-Inductor 0620 or 0420 3,3uH-10uH (1x)
-STI3408B Voltage Regulator
-Lithium Polymer Battery 603040, 750mAh-800mAh
-22mm Xiaomi Watch S1 Strap / Band
-Capacitor 0603 :
- 100nf (2x)
- 22uf (1x)
- 10uf (1x)
- 2.2uf (1x)
-Resistor SMD 0603 :
- 22 Ohm (1x)
- 220 Ohm (1x)
- 1K (1x)
- 10k (6x)
- 220k (2x)
- 440K (1x)
-Pin header 2.54mm 1x4 (1x)
-Micro 2 Pin Tactile Push Button SMD 3x4x2 (2x)
-Side Button SMD Tactile Switches ROHS 3x6x 3x6x3.5
-Some Dupont Wire
-PCB Watch ordered from the JLCPCB
-Soldering Iron, Flux and some soldering tin
For The Charging
-MH-CD42 Charge Discharge Module
-Connector-XH 2.54mm 1x2 pin (1x)
-Type-C Female
-Resistor SMD 0603 4.7K (2x)
-PCB Charging Station ordered from the JLCPCB
Note: You can also use any Lithium charger module available at the market, i use this personally because i want to make the charging station and its the one available for me right now (well, thats the plan)
For the Chase
for time saving and detailed result, in this case i use Resin 3D printer, and the resin i used is "ABS-Like Resin from SUNLU".
You can also used any normal FDM 3D print for this project, but the case design will be a little bit bulky and you need to set the tolerance. i will later design in then share it here.
The Printer i use is "Anycubic Photon Mono SE"
Note: all of the part i listed there maybe isnt available on your local marketplace or very expensive, i recommend using Aliexpress to buy few of those component
ESP Watch PCB Design
this is the schematic and the PCB design for my watch, i use ESP32 WROOM 32D, you can also use another type such as WROOM 32U or 32E. before making the PCB i make a simple prototype using ST7789V and GC9A01 module on the market and some button.
The Design
If we look at the PCB design, there is a slight incision at the top of the PCB, this is intended to make it easier for the user to remove the ESP Module if an error or damage occurs. And also so that the antenna can receive the signal properly
The width of the track for the signal I used is 0.254 mm.
The width of the track for the VCC I used is 0.5 mm.
and for the Copper Area is a GND (top and bottom)
The PCB i ordered it from JLCBPCB, you can order it for only 2$ (without the shipping cost)
Here are the Wiring
- MOSI - 23
- SCK - 18
- CS - 5
- DC - 17
- RST - 16
- Backlight - 4
- Battery Read - 34
- Button1 - 35
- Button2 - 32
- Button3 - 33
- Button4 - 25
ESP32
The Module i use is an ESP32 WROOM 32D, its an SoC (System on Chip) with Bluetooth and WiFi capabilites, it have dual core, Low Power Consumption, High Speed and Cost effective (cheap).
Within the price, the small size and high perfomance, this Chip by Espressif Systems is really the perfect solution for this project.
you can see where i placed in on the shcematic
ST7789V
ST7789V that i used is an full colour TFT-LCD with size of 1.69", 240x280 pixel and logic gate of 3.3V. it accpets 8bits/16bits with 12pin.
To control it im using SPI from the ESP and to control the backlight i use Some MOSFET.
IRLML2502 is an N-channel MOSFET (Enhancement Type) which can fully open when the Gate is at 3.3V which is perfect for the esp32 and also came in small package (SOT-23), it also can drive higher current. To set the signal to stay low on the MOSFET and on the ESP32 pin , i use 440KΩ resistor.
To protect the Backlight LED i use 22Ω Resistor
Note: To protect your ESP32 from voltage back current from the MOSFET, use 1K ohm from the mosfet to signal pin of your ESP32
For more detailed information, here is the datasheet Link : ST7789V
Voltage Regulator
Usually, on a standard ESP32 DevKit, an AMS1117 3.3V regulator is used, which is fine in most cases. However, it is not suitable when using a single LiPo battery, as the AMS1117 has a significant voltage dropout. When the battery voltage drops to around 3.5V, the AMS1117 cannot function optimally. Therefore, I used a step-down converter with a low dropout (LDO) voltage and to maximize the use of the Battery such as STI3408B.
and also small enough to fit on the PCB.
The STI3408B is a 1.5MHz, 1.2A synchronous step-down converter, ideal for powering projects that need only a single lithium battery. It has an input range of 1.2V to 6V and can step down to 1.2V. Regarding noise, I have not encountered any issues so far while using this converter for various projects. To mitigate potential noise problems, I use a larger capacitor and a larger inductor than recommended.
Using this IC we can set the output voltage to always 3.3V even if the input changing from 4.2 to 3.3
To set the output to 3.3V, you need to create a voltage divider from the output of the IC after the inductor and connect it to the FB pin of the IC. Connect a 1kΩ resistor from the 3.3V output (after the inductor) to the feedback pin, and a 220Ω resistor from the feedback pin to ground (GND). With this , we can achieve 3.3 V output for the System.
more like LX----->inductor------>1K Ohm ---->FB<---- 220R Ohm <----GND
just like in the schematic i gave
you can see the datasheet for the Typical Application or minimum requirement for the IC
Battery Read
To read the battery voltage of the system, I created a simple voltage divider using a 2x 220kΩ resistor. The output of the voltage divider is connected to pin 34 of the ESP32. The reason for using a voltage divider is that the ESP32 ADC can only handle up to 3.3V on the input. By using the voltage divider, the battery voltage is scaled down to a safe level for the ADC. This scaled-down voltage is then multiplied by the appropriate factor in the system code to get the actual battery voltage. This setup is used to monitor the battery, allowing you to know when it needs charging or when the ESP32 should stop working to prevent deep discharge.
I use the same system as "seeed studio" How to Check the Battery Voltage.
thought it might vary based on the other ESP32 ADC
Button
There are a few buttons on my design, some for input and some for programming the ESP32.
For the input buttons, there are four of them connected to ESP GPIO pins 35, 32, 33, and 25. Each button is pulled down using a 10K resistor, so the logic is 0. When a button is pressed, it sends a HIGH signal to the respective ESP32 GPIO pin.
For the Programming, there is a Button to Enable pin and to GPIO 0 or so a BOOT button in the ESP Devkit. The function is the same as normal Devkit.
To make the ESP enter the Upload Program Mode, you need to press the Boot Button (GPIO 0) and then the EN button, then release the EN button after that, release the Boot Button (GPIO 0)
PINHEADER
As you can see, there are four pins on the side of the PCB. Their purpose is to upload code and charge the watch’s battery. Initially, this was designed as a temporary solution, but it seems to have become a permanent one for now (lol). I charge the watch through these pins now. They are used for debugging and uploading code, and include RX, TX, 5V+ input, and GND. The plan was to remove them after uploading the code and charge the watch using bare copper that would make direct contact with the charging station pin. However, for now, we will leave them as they are.
PCB Assembly
After designing and ordering the PCB, you need to assemble it. You can use a normal hand soldering iron; there's no need for a hot air soldering tool. Just make sure you don't overheat the ESP32 module.
- First, solder the voltage regulator IC STI3408B (or the step-down converter) and other components for the voltage regulator such as resistors, inductors, and capacitors.
- Then, use a multimeter to ensure the regulator is outputting 3.3V. If it is not outputting 3.3V, the IC might be damaged, you might have placed the resistor in the wrong position, used the wrong value (this has happened to me a few times), or there might be a short circuit due to unclean soldering.
- After confirming that 3.3V is being output, place and solder the ESP32 module. Then, solder the rest of the buttons.
- Next, solder the pin headers so you can check if the ESP32 is functioning by programming a simple code such as Serial.println("Hello world"). Note: Refer to Step 5 to see how to code it.
- Once everything is verified, solder the MOSFET and the main TFT-LCD display ST7789V. Your smartwatch is then ready to be programmed.
For the lithium battery, you can solder it later after you finish programming and uploading the code.
If you want to solder it directly and then upload the code, it won't be a problem, but I recommend leaving the 5V input disconnected from the ESP watch to prevent overcharging the battery. You only need to connect the RX, TX, and GND to the UART.
Charging Station
Note: This charging station design is not mandatory, you can skip this step and just use any Lithium Charging module that is available at the market.
I’m using the MH-CD42 as the charging station board. I’ve designed another PCB to pair it with the module. I’ve added a Type-C connector and some pinouts for ease of use.
The MH-CD42 is a power management board that offers both charging and discharging capabilities. It provides lithium battery protection features, including Over Voltage, Short Circuit, and Over Temperature protection. The board also features a 4-level LED indicator. It consistently delivers a 5V output and can provide a current of up to 2.1 Amps. Additionally, it can charge the battery at a rate of 2 Amps.
On the PCB i designed for the charging station, i used a Type-c with 12 pin where i connect the CC1 and CC2 to the ground, so i can use the Type-C to Type-C cable.
So yeah thats why i start using this instead of the traditional TP4056. but its not a problem if you still want to use it. its just my preference.
Case Design & Printing
For the case design, I first used CorelDRAW to draw the total size of the PCB, the strap, the case design, placement, and other elements in 2D form. I measured everything using calipers and estimated the right sizes, all without adding tolerances. Then, using the data I gathered and noted (or typed, in this case), I used SolidWorks to design the 3D case and added the necessary tolerances. For resin 3D printing, a tolerance of about 0.1mm to 0.15mm is needed. After designing, I assembled the parts in SolidWorks to check if they fit together properly. I also designed the case without the debugging pins. Finally, I converted the design to STL format.
Note: For 3D printing with FDM, the tolerance is about 0.24mm to 0.34mm, depending on the design and size. My 3D printer is an Ender 3 V2 with a 0.2mm nozzle, so that tolerance is sufficient. for now i design it for the Resin 3D print
After designing all the necessary parts, I used Lychee Slicer Resin to prepare them for printing. The settings can be seen in the picture I uploaded above. For the supports, I set them to Medium with Ultra density. You don't need to follow this configuration; I used it because I didn't want to spend too much time on support adjustments. However, a problem occurred where there were too many supports, and when I tried to remove them, the base of my design cracked, as shown in the picture. (I'm just not that patient, lol).
you can watch some Youtube how to handle resin print, dont be like me. and remember to always handle it with care and protection.
Coding
im using USB to UART CP2102 to upload the program to my ESP32 Watch, you can also use other UART as i mentioned before.
Connect the ESP Watch RX to USB UART TX and ESP Watch TX to USB RX. then connect the ground.
and make sure you have the driver installed.
im using VSCode
to code the ESP we use Platform.io , and the board i select is "uPesy ESP32 Wroom Devkit" you can aslo use Denky32 but i prefere to use that.
To control the display, i use library TFT_eSPI from Bodmer and in the user_setup i copy and place the ST7789 240x280 , with setup ID of 203
here are the code for the user Setup
// ST7789 240 x 280 display with no chip select line
#define USER_SETUP_ID 203
#define ST7789_DRIVER // Configure all registers
#define TFT_WIDTH 240
#define TFT_HEIGHT 280
#define CGRAM_OFFSET // Library will add offsets required
//#define TFT_RGB_ORDER TFT_RGB // Colour order Red-Green-Blue
//#define TFT_RGB_ORDER TFT_BGR // Colour order Blue-Green-Red
//#define TFT_INVERSION_ON
//#define TFT_INVERSION_OFF
// DSTIKE stepup
//#define TFT_DC 23
//#define TFT_RST 32
//#define TFT_MOSI 26
//#define TFT_SCLK 27
// Generic ESP32 setup
#define TFT_MISO 19
#define TFT_MOSI 23
#define TFT_SCLK 18
#define TFT_CS 5 // Not connected
#define TFT_DC 17
#define TFT_RST 16 // Connect reset to ensure display initialises
#define LOAD_GLCD // Font 1. Original Adafruit 8 pixel font needs ~1820 bytes in FLASH
#define LOAD_FONT2 // Font 2. Small 16 pixel high font, needs ~3534 bytes in FLASH, 96 characters
#define LOAD_FONT4 // Font 4. Medium 26 pixel high font, needs ~5848 bytes in FLASH, 96 characters
#define LOAD_FONT6 // Font 6. Large 48 pixel font, needs ~2666 bytes in FLASH, only characters 1234567890:-.apm
#define LOAD_FONT7 // Font 7. 7 segment 48 pixel font, needs ~2438 bytes in FLASH, only characters 1234567890:.
#define LOAD_FONT8 // Font 8. Large 75 pixel font needs ~3256 bytes in FLASH, only characters 1234567890:-.
//#define LOAD_FONT8N // Font 8. Alternative to Font 8 above, slightly narrower, so 3 digits fit a 160 pixel TFT
#define LOAD_GFXFF // FreeFonts. Include access to the 48 Adafruit_GFX free fonts FF1 to FF48 and custom fonts
#define SMOOTH_FONT
// #define SPI_FREQUENCY 27000000
#define SPI_FREQUENCY 40000000
#define SPI_READ_FREQUENCY 20000000
#define SPI_TOUCH_FREQUENCY 2500000
// #define SUPPORT_TRANSACTIONS
and for the main code here it is
#include <Arduino.h>
#include "TFT_eSPI.h"
#include "MINGO.h"
#include "Flower_240x280.h"
#define BL 4
#define inLed 2
#define v_R 34
#define b_1 35
#define b_2 33
#define b_3 25
#define b_4 32
// TFT Setup
TFT_eSPI tft = TFT_eSPI(); // Invoke custom library
TFT_eSprite spritte = TFT_eSprite(&tft); // Sprite object "spritte" created
// Menu Setup
int screenW = 240;
int screenH= 280;
int textWidth;
int x;
bool mainMenu = true ;
bool subMenu = false;
bool wait = false;
//SubMenu
const int menuCount = 4;
bool menuChange = true;
int selectedOption = 0;
const char* menuText[menuCount] = {"Brightness","Time","Setting","About"};
// Time setting menu
// page handler
int page IRAM_ATTR = 0;
bool pageChange = true; // tos top the code changing the page, before the previous page finish loading
bool pageRefresh = true; //Clean the page
//time
volatile int sec IRAM_ATTR= 50;
volatile int minute IRAM_ATTR= 59;
volatile int hrs IRAM_ATTR= 23;
volatile int days IRAM_ATTR= 7;
volatile int months IRAM_ATTR= 4;
volatile int yrs IRAM_ATTR= 2024;
volatile int days_Max IRAM_ATTR;
volatile int daysOfWeekCount IRAM_ATTR;
const char* daysOfWeek[] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
const char* monthsOfYear[] = {"NULL","January", "February", "March", "April", "May", "June", "July", "August", "September", "Octobor", "November", "December"};
const unsigned long interval = 1000;
//
// input
int voltage;
bool inp1,inp2,inp3,inp4;
// brightness controll
int mapRange(int input, int input_start, int input_end, int output_start, int output_end) {
return (input - input_start) * (output_end - output_start) / (input_end - input_start) + output_start;
}
int brightness = 80;
int Level = 4;
// Time Menu controll
int timeMenu_Select = 0;
const int timeMenu = 3;
bool timePage = false;
int clockSelect = 0;
int dateSelect = 0;
////////////////////////////////////////////////////////////////////
void input(void *pvParameters);
void v_Read(void *pvParameters);
void tft_page(void *pvParameters);
void onTimer(TimerHandle_t xTimer);
void setup()
{
Serial.begin(115200);
pinMode(b_1, INPUT);pinMode(b_2, INPUT);pinMode(b_3, INPUT);pinMode(b_4, INPUT);pinMode(v_R, INPUT); //settup mode
pinMode(BL, OUTPUT);pinMode(inLed, OUTPUT);
//Setup TFT
tft.init();
tft.setRotation(0);
tft.setSwapBytes(true);
tft.fillScreen(TFT_ORANGE);
analogWrite(BL, 0);
// for (int i = 0; i < 3; i++) {
// digitalWrite(inLed, HIGH); // Turn on the LED
// delay(500); // Wait for 500 millisec (0.5 sec)
// digitalWrite(inLed, LOW); // Turn off the LED
// delay(500); // Wait for another 500 millisec
// }
digitalWrite(inLed, HIGH);
analogWrite(BL, brightness);
xTaskCreatePinnedToCore(input,"button", 1024, NULL, 1, NULL, 1); // core 1
xTaskCreatePinnedToCore(v_Read,"Voltage Read",1024, NULL, 5, NULL, 0);
xTaskCreatePinnedToCore(tft_page,"Page of TFT",20000, NULL, 2, NULL, 1);
TimerHandle_t timerHandle = xTimerCreate("timer", pdMS_TO_TICKS(interval), pdTRUE, 0, onTimer); // name, period, auto reload, timer id, callback
if (timerHandle != NULL) {
xTimerStart(timerHandle, 0);
}
}
void loop() {
//Serial.printf("%02d:%02d:%02d\n", hrs, minute, sec);
//Serial.print("months : ");
//Serial.println(monthsOfYear[months]);
//Serial.print("day : ");
//Serial.println(daysOfWeek[days]);
//button read//
// if (inp1 == HIGH ){
// Serial.print("35");
//}
//page read//
//Serial.print("PG: ");
//Serial.println(page);
//Serial.print("change: ");
//Serial.println(pageChange);
//Serial.print("Refresh: ");
//Serial.print(pageRefresh);
delay(1000);
}
void input (void *pvParameters){
while(1){
inp1=digitalRead(b_1);
inp2=digitalRead(b_2);
inp3=digitalRead(b_3);
inp4=digitalRead(b_4);
if(inp1==HIGH && pageChange ==true)
{
pageChange=false;
pageRefresh=true;
page++; //NEXT page
}
if(inp2==HIGH && pageChange ==true)
{
pageChange=false;
pageRefresh=true;
page--; // Prev Page
}
if(page ==1 && mainMenu == true&& inp3==HIGH && pageChange ==true && wait==false) //menu select controll
{
pageRefresh=true;
selectedOption = (selectedOption + 1) % menuCount;
Serial.println(selectedOption);
Serial.println("main");
}
if(inp4==HIGH && pageChange ==true && mainMenu == true && page == 1 && wait== false) //menu enter control
{
wait = true;
pageChange = false;
mainMenu = false;
subMenu= true;
pageRefresh=true;
Serial.print("INP4");
}
if(selectedOption == 1 && inp3==HIGH && subMenu==true && mainMenu==false&& wait==false) //time setting menu controll
{
timePage =false;
pageRefresh=true;
//Serial.print("press clock menu");
timeMenu_Select = (timeMenu_Select + 1) % timeMenu ;
//Serial.println(timeMenu_Select);
}
//page number handler
page>3?page=0:page<0?page=3:page=page;
vTaskDelay(200);
}
}
void tft_page(void *pvParameters)
{
while(1)
{
if(page==0 && mainMenu == true) // main
{
if(pageRefresh==true){
tft.fillScreen(TFT_BLACK);
pageRefresh= false;
Serial.print("refresh");
}
if(pageRefresh==false){
//Page
tft.setTextColor(TFT_WHITE, TFT_BLACK);
// years //
tft.setTextSize(3);
tft.setCursor(85, 73); //xy
tft.printf("%04d\n", yrs);
// date/month //
tft.setTextSize(3);
tft.setCursor(75,101); //xy
tft.printf("%02d/%02d\n", days, months);
// time
tft.setTextSize(4);
tft.setCursor(28, 130); //xy
tft.printf("%02d:%02d:%02d\n", hrs, minute, sec);
tft.setTextSize(3);
// day
textWidth = tft.textWidth(daysOfWeek[daysOfWeekCount]);
x = (screenW - textWidth) / 2;
tft.setCursor(x, 165); //xy
tft.print(daysOfWeek[daysOfWeekCount]);
textWidth = tft.textWidth(monthsOfYear[months]);
// months
x = (screenW - textWidth) / 2;
tft.setCursor(x, 193); //xy
tft.print(monthsOfYear[months]);
vTaskDelay(100);
pageChange=true;
}
}
else if(page==1 && mainMenu == true && wait==false) //menu
{ wait ==true;
if(pageRefresh==true){
tft.fillScreen(TFT_BLACK);
pageRefresh= false;
}
if(pageRefresh==false){
//isi
tft.setTextSize(2);
const int menuItemWidth = 200; // Width of each menu item
const int menuItemHeight = 50; // Height of each menu item
const int menuItemSpacing = 10; // Spacing between menu items
int totalMenuHeight = menuCount * (menuItemHeight + menuItemSpacing) - menuItemSpacing;
int menuStartY = (tft.height() - totalMenuHeight) / 2;
for (int i = 0; i < menuCount; i++) {
int x = (tft.width() - menuItemWidth) / 2;
int y = menuStartY + i * (menuItemHeight + menuItemSpacing);
int xText = (screenW - tft.textWidth(menuText[i]))/ 2;
if (i == selectedOption) {
tft.fillRoundRect(x, y, menuItemWidth, menuItemHeight, 10, TFT_YELLOW);
tft.setTextColor(TFT_BLACK);
} else {
tft.drawRoundRect(x, y, menuItemWidth, menuItemHeight, 10, TFT_WHITE);
tft.setTextColor(TFT_WHITE);
}
tft.drawString(String(menuText[i]) , xText, y + 15);
// Serial.println("wifiSub");
}
pageChange=true;
}
vTaskDelay(100);
wait== false;
}
else if(page==2 && mainMenu == true) //flower
{
if(pageRefresh==true){
tft.fillScreen(TFT_BLACK);
pageRefresh= false;
}
if(pageRefresh==false){
// isi
tft.pushImage(0,0,screenW,screenH,Flower_240x280);
vTaskDelay(100);
pageChange=true;
}
}
else if(page==3 && mainMenu == true) // colour
{
if(pageRefresh==true){
tft.fillScreen(TFT_BLACK);
pageRefresh= false;
}
if(pageRefresh==false){
// isi
tft.fillScreen(TFT_PINK);
vTaskDelay(100);
pageChange=true;
}
}
//Sub menu
if(mainMenu == false && subMenu ==true &&selectedOption == 0 ){ // brightness control
String text = "Brightness";
if(pageRefresh==true){
tft.fillScreen(TFT_BLACK);
pageRefresh= false;
}
if(pageRefresh==false){
// isi
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.setTextSize(3);
x = (screenW - tft.textWidth(text)) / 2;
tft.setCursor(x,100);
tft.print(text);
tft.setTextSize(3);
x = screenW/2;
tft.setCursor(x,180);
tft.print(Level);
if(inp1 == HIGH){ //increased brightness
if(Level < 6){
Level++;
brightness = mapRange(Level, 1, 6, 20 ,255);
analogWrite(BL, brightness);
vTaskDelay(100);}}
if(inp2 == HIGH){ //decreased brightness
if(Level > 1){
Level--;
brightness = mapRange(Level, 1, 6, 20 ,255);
analogWrite(BL, brightness);
vTaskDelay(100);}}
vTaskDelay(100);
if(inp4 == HIGH && wait==false){ //return to main
Serial.print("Ouch");
mainMenu=true;
subMenu=false;
pageChange=true;
page=1;
pageRefresh=true;
vTaskDelay(100);
}
wait=false;
}
}
if(selectedOption == 1 && mainMenu == false && subMenu ==true ){ // Time Control
if(pageRefresh==true){
tft.fillScreen(TFT_BLACK);
pageRefresh= false;
}
if(pageRefresh==false){
timePage ==false;
// isi
const int timeMenuWidth = 180; // Width of each menu item
const int timeMenuHeight = 60; // Height of each menu item
const int timeMenuSpacing = 10; // Spacing between menu items
String TimeMenuList[timeMenu]={"Clock","Date","Back"};
int totalMenuHeight = timeMenu * (timeMenuHeight + timeMenuSpacing) - timeMenuSpacing;
int menuStartY = (tft.height() - totalMenuHeight) / 2;
for (int i = 0; i < timeMenu; i++) {
int x = (tft.width() - timeMenuWidth) / 2;
int y = menuStartY + i * (timeMenuHeight +timeMenuSpacing);
if (i == timeMenu_Select) {
tft.fillRoundRect(x, y, timeMenuWidth, timeMenuHeight, 10, TFT_YELLOW);
tft.setTextColor(TFT_BLACK);
} else {
tft.drawRoundRect(x, y, timeMenuWidth, timeMenuHeight, 10, TFT_WHITE);
tft.setTextColor(TFT_WHITE);
}
int xText = (screenW - tft.textWidth(TimeMenuList[i]))/ 2;
tft.drawString(String(TimeMenuList[i]) , xText, y + 15);
// Serial.println("wifiSub");
}
vTaskDelay(100);
if(inp4==HIGH && wait==false && timeMenu_Select==0){ // to time
wait=true;
subMenu=false;
pageRefresh=true;
timePage=true;
Serial.println("to time");
}
if(inp4==HIGH && wait==false && timeMenu_Select==1){ // to date
wait=true;
subMenu=false;
pageRefresh=true;
timePage=true;
Serial.println("to date");
}
if(inp4 == HIGH && wait==false && timeMenu_Select==2 ){ //return to main
Serial.print("Ouch");
mainMenu=true;
subMenu=false;
pageChange=true;
page=1;
pageRefresh=true;
vTaskDelay(100);
}
wait=false;
}
}
if(selectedOption == 2 && mainMenu == false && subMenu ==true ){ //setting
if(pageRefresh==true){
tft.fillScreen(TFT_BLACK);
pageRefresh= false;
}
if(pageRefresh==false){
// isi
tft.pushImage(0,0,screenW,screenH,MINGO);
vTaskDelay(100);
if(inp4 == HIGH && wait==false){ //return to main
Serial.print("Ouch");
mainMenu=true;
subMenu=false;
pageChange=true;
page=1;
pageRefresh=true;
vTaskDelay(100);
}
wait=false;
}
}
if(selectedOption == 3 && mainMenu == false && subMenu ==true){ //about the esp32
if(pageRefresh==true){
tft.fillScreen(TFT_BLACK);
pageRefresh= false;
}
if(pageRefresh==false){
// isi
const char *message[] = {"ESP32-WROOM","ESP32-D0WDQ6","Chip-v4.4.3","ID:DCC84E9EF0C8","Speed-240 Mhz","Flash:4.19 MB","F-Speed:80 Mhz","Flash Mode:0","F-Used:674.06 KB","Cores:2","RAM:256 KB"};
menuChange=false;
tft.setTextSize(2);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
textWidth = tft.textWidth(message[0]);
x = (screenW - textWidth) / 2;
tft.setCursor(x, 27); //xy
tft.print(message[0]);
textWidth = tft.textWidth(message[1]);
x = (screenW - textWidth) / 2;
tft.setCursor(x, 45); //xy
tft.print(message[1]);
textWidth = tft.textWidth(message[2]);
x = (screenW - textWidth) / 2;
tft.setCursor(x, 62); //xy
tft.print(message[2]);
textWidth = tft.textWidth(message[3]);
x = (screenW - textWidth) / 2;
tft.setCursor(x, 79); //xy
tft.print(message[3]);
textWidth = tft.textWidth(message[4]);
x = (screenW - textWidth) / 2;
tft.setCursor(x, 95); //xy
tft.print(message[4]);
textWidth = tft.textWidth(message[5]);
x = (screenW - textWidth) / 2;
tft.setCursor(x, 112); //xy
tft.print(message[5]);
textWidth = tft.textWidth(message[6]);
x = (screenW - textWidth) / 2;
tft.setCursor(x, 128); //xy
tft.print(message[6]);
textWidth = tft.textWidth(message[7]);
x = (screenW - textWidth) / 2;
tft.setCursor(x, 145); //xy
tft.print(message[7]);
textWidth = tft.textWidth(message[8]);
x = (screenW - textWidth) / 2;
tft.setCursor(x, 163); //xy
tft.print(message[8]);
textWidth = tft.textWidth(message[9]);
x = (screenW - textWidth) / 2;
tft.setCursor(x, 180); //xy
tft.print(message[9]);
textWidth = tft.textWidth(message[10]);
x = (screenW - textWidth) / 2;
tft.setCursor(x, 198); //xy
tft.print(message[10]);
vTaskDelay(100);
if(inp4 == HIGH && wait==false){ //return to main
Serial.print("Ouch");
mainMenu=true;
subMenu=false;
pageChange=true;
page=1;
pageRefresh=true;
vTaskDelay(100);
}
wait=false;
}
}
//clock setting page
if(timeMenu_Select==0 && mainMenu == false && timePage ==true ){ //setting
if(pageRefresh==true){
tft.fillScreen(TFT_BLACK);
pageRefresh= false;
}
if(pageRefresh==false){
// isi
int menuColumnCount = 3;
int menuRowCount = 1;
const int menuItemSize = 60; // Smaller size for the menu items
const int menuItemSpacing = 10; // Space between menu items
const int menuItemCornerRadius = 5; // Rounded corner radius
const int highlightBorderWidth = 3; // Width of the highlight border
int clockMenuCount = menuColumnCount*menuRowCount;
int clockNOW[] = {hrs,minute,sec};
int totalMenuWidth = menuColumnCount * (menuItemSize + menuItemSpacing) - menuItemSpacing;
int totalMenuHeight = menuRowCount * (menuItemSize + menuItemSpacing) - menuItemSpacing;
int menuStartX = screenW/2 - totalMenuWidth / 2;
int menuStartY = screenH/2 - totalMenuHeight / 2;
for (int i = 0; i < clockMenuCount; i++) {
int row = i / menuColumnCount;
int col = i % menuColumnCount;
int x = menuStartX + col * (menuItemSize + menuItemSpacing);
int y = menuStartY + row * (menuItemSize + menuItemSpacing);
bool isSelected = (i == clockSelect);
tft.setTextSize(2);
if (isSelected) {
// Draw a highlighted rounded rectangle outline around the menu item
tft.drawRoundRect(x - highlightBorderWidth, y - highlightBorderWidth, menuItemSize + 2 * highlightBorderWidth, menuItemSize + 2 * highlightBorderWidth, menuItemCornerRadius + highlightBorderWidth, TFT_ORANGE);
tft.setTextColor(TFT_ORANGE,TFT_BLACK);
} else {
tft.drawRoundRect(x, y, menuItemSize, menuItemSize, menuItemCornerRadius, TFT_WHITE);
tft.setTextColor(TFT_WHITE,TFT_BLACK);
}
// Draw menu item logos and text
tft.drawString(String(clockNOW[i]), x + 15, y + menuItemSize / 2 - 5);
}
vTaskDelay(200);
if(inp1==HIGH){
Serial.println("HIGH 1");
if(clockSelect == 0){
hrs++;
if(hrs>23){
hrs=0;
}
}
if(clockSelect == 1){
minute++;
if(minute > 59){
minute=0;
}
}
if(clockSelect == 2){
sec=59;
}
}
if(inp2==HIGH){
Serial.println("HIGH 2");
if(clockSelect == 0){
hrs--;
if(hrs<0){
hrs=23;
}
}
if(clockSelect == 1){
minute--;
if(minute < 0){
minute=59;
}
}
if(clockSelect == 2){
sec = 0;
}
}
if(inp3==HIGH && wait==false){
pageRefresh=true;
Serial.print("press clock select");
clockSelect = (clockSelect + 1) % 3 ;
Serial.println(clockSelect);
vTaskDelay(100);
}
if(inp4 == HIGH && wait==false ){ //return to main
Serial.print("Ouch");
mainMenu=true;
subMenu=false;
timePage=false;
pageChange=true;
page=1;
pageRefresh=true;
vTaskDelay(100);
}
wait=false;
}
}
if(timeMenu_Select==1 && mainMenu == false && timePage ==true ){ //setting
if(pageRefresh==true){
tft.fillScreen(TFT_BLACK);
pageRefresh= false;
}
if(pageRefresh==false){
tft.setTextSize(3);
const int menuItemWidth = 200; // Width of each menu item
const int menuItemHeight = 50; // Height of each menu item
const int menuItemSpacing = 10; // Spacing between menu items
const int dateOption = 3;
int date_num[]={days,months,yrs};
int totalMenuHeight = dateOption * (menuItemHeight + menuItemSpacing) - menuItemSpacing;
int menuStartY = (tft.height() - totalMenuHeight) / 2;
for (int i = 0; i < dateOption; i++) {
int x = (tft.width() - menuItemWidth) / 2;
int y = menuStartY + i * (menuItemHeight + menuItemSpacing);
if (i == dateSelect) {
tft.fillRoundRect(x, y, menuItemWidth, menuItemHeight, 10, TFT_YELLOW);
tft.setTextColor(TFT_BLACK);
} else {
tft.drawRoundRect(x, y, menuItemWidth, menuItemHeight, 10, TFT_WHITE);
tft.setTextColor(TFT_WHITE);
}
tft.drawString(String(date_num[i]) , x+10, y + 15);
// Serial.println("wifiSub");
}
vTaskDelay(200);
wait=false;
if(inp1 ==HIGH&&wait==false){
if(dateSelect==0){
days++;
if(days>days_Max){
days=1;
}
}
if(dateSelect==1){
months++;
if(months>12){
months=1;
}
}
if(dateSelect==2){
yrs++;
}
}
if(inp2 ==HIGH&&wait==false) {
if(dateSelect==0){
days--;
if(days < 1){
days=days_Max;
}
}
if(dateSelect==1){
months--;
if(months<1){
months=12;
}
}
if(dateSelect==2){
yrs--;
if (yrs <0){
yrs=9999;
}
}
}
if(inp3 ==HIGH &&wait==false){
pageRefresh=true;
Serial.print("press date select");
dateSelect = (dateSelect + 1) % 3 ;
Serial.println(dateSelect);
vTaskDelay(100);
}
if(inp4 ==HIGH && wait==false){
Serial.print("Ouch");
mainMenu=true;
subMenu=false;
timePage=false;
pageChange=true;
page=1;
pageRefresh=true;
vTaskDelay(100);
}
}
}
}
}
void v_Read(void *pvParameters) {
while(1)
{
uint32_t Vbatt = 0;
for(int i = 0; i < 16; i++) {
Vbatt = Vbatt + analogReadMilliVolts(v_R); // ADC with correction
}
float Vbattf = 2 * Vbatt / 16 / 1000.0; // attenuation ratio 1/2, mV --> V
Serial.println(Vbattf, 3);
delay(1000);
}
}
void onTimer(TimerHandle_t xTimer) {
sec++;
if (sec > 59) {
sec = 0;
minute++;
if (minute > 59) {
minute = 0;
hrs++;
if (hrs > 23) {
hrs = 0;
days++;
pageRefresh = true;
if (months == 2) {
if (yrs % 4 == 0) {
days_Max = 29;
} else {
days_Max = 28;
}
} else if (months == 4 || months == 6 || months == 9 || months == 11) {
days_Max = 30;
} else {
days_Max = 31;
}
if (days > days_Max) {
days = 1;
months++;
if (months > 12) {
months = 1;
yrs++;
}
}
}
}
}
daysOfWeekCount = (days + (13 * (months + 1)) / 5 + yrs % 100 + (yrs % 100) / 4 + (yrs / 100) / 4 + 5 * (yrs / 100)) % 7;
daysOfWeekCount = (daysOfWeekCount + 6) % 7;
}
The code I provided includes a precision timer from the ESP32 core and a clock. It can control the brightness, display an image, and allows you to set up the date and time.
i still havent a time yet to inlude NTP sync to the clock, i might added it later
i test the clock time for 2 week and the drift isnt reaching 1 second.
i will publish the code on my github
Assembly
after that you can assembly the ESP Watch, but remember to solder the battery first,
- make sure the cable isnt tangling or get blocked by anything inside the case
- then put the battery inside first
- put the side button
- then put the ESP Watch PCB on it
- after that close it using the top part of the case
- i recommend using a glue for smartphone backdoor, since if there is a problem inside the ESP Watch, you can peel it.
- then put on the strap
Problem and Future Development Planning
Here are some problems I encountered during the programming and soldering of the PCB:
- The code isn't efficient enough, as it keeps refreshing the display, which means it consumes more power to stay on.
- There is no over-discharge protection on the ESP watch. The battery gets depleted even when it reaches 3.3V. The ESP watch continues to consume power, causing the battery voltage to drop to a dangerous level of 2.9V. To prevent this, we need to add a battery protection circuit on the PCB.
- There is no capacitor on the button for pressing the input, which creates a lot of noise when pressing it. In the future, I plan to add a capacitor and use some code to debounce it.
- There are still some pins hanging outside the case. In the future, I will remove them and add a simple charging port for the ESP watch.
- There is an issue when updating the display in the clock selection menu. The TFT doesn't update the number properly, leaving it hanging there until we switch to another menu.
Reference
Here are some references I inspired from
ESP32 super smart watch tutorial by Gabriel McFarlane
Build Your Own Smartwatch from Scratch - You Won't Believe How Cool it Looks! by Circuit Digest