ESP WATCH Using IPS Display ST7789V

by M_F_T in Circuits > Wearables

6129 Views, 74 Favorites, 0 Comments

ESP WATCH Using IPS Display ST7789V

IMG-20240519-WA0016.jpg
IMG-20240519-WA0019.jpg
IMG-20240520-WA0008.jpg
IMG-20240520-WA0004.jpg
IMG-20240520-WA0002.jpg

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

IMG-20240520-WA0013.jpg
IMG-20240520-WA0006.jpg
WhatsApp Image 2024-05-20 at 02.28.17_6c380c53.jpg
IMG-20240519-WA0020.jpg
IMG-20240520-WA0012.jpg
71Cxfd4FU2L._AC_UF1000,1000_QL80_.jpg

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

Screenshot 2024-05-20 092540.png
Screenshot 2024-05-20 092829.png
Screenshot 2024-05-20 092903.png
Screenshot 2024-05-20 092804.png
IMG-20240519-WA0020.jpg

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

IMG-20240520-WA0009.jpg
IMG-20240520-WA0002.jpg
IMG-20240520-WA0010.jpg

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

IMG-20240519-WA0021.jpg
Screenshot 2024-05-20 121305.png
IMG-20240520-WA0018.jpg
IMG-20240520-WA0020.jpg

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

Screenshot 2024-05-20 133611.png
Screenshot 2024-05-20 132131.png
Screenshot 2024-05-20 132425.png
Screenshot 2024-05-20 132454.png
Screenshot 2024-05-20 132536.png
Screenshot 2024-05-20 162214.png
Screenshot 2024-05-19 233850.png
IMG-20240519-WA0025.jpg
IMG-20240519-WA0026.jpg
Screenshot 2024-05-20 155843.png
Screenshot 2024-05-20 155627.png
Screenshot 2024-05-20 160034.png

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

WhatsApp Image 2024-05-20 at 02.28.17_6c380c53.jpg
WhatsApp Image 2024-05-20 at 17.31.15_b9cc9712.jpg
IMG-20240520-WA0011.jpg
Screenshot 2024-05-21 124432.png
IMG-20240521-WA0005.jpg
IMG-20240521-WA0007.jpg
IMG-20240521-WA0006.jpg
IMG-20240521-WA0003.jpg
IMG-20240521-WA0004.jpg
IMG-20240521-WA0002.jpg

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

IMG-20240520-WA0003.jpg
IMG-20240520-WA0004.jpg
IMG-20240520-WA0008.jpg
IMG-20240519-WA0019.jpg

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

IMG-20240520-WA0020.jpg


Here are some problems I encountered during the programming and soldering of the PCB:

  1. The code isn't efficient enough, as it keeps refreshing the display, which means it consumes more power to stay on.
  2. 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.
  3. 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.
  4. 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.
  5. 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