DIY Smartwatch with multiple Watch Faces, Heart Rate Sensor, Compass and Games

Published  February 3, 2023   4
ESP32 Based Smart Watch

In the previous parts(Part 1, Part 2, Part 3) of this project, we investigated different aspects of our smart watch projects and learned to integrate and test each individual part. In this we are going to use what we have learned to create our own smartwatch from scratch, using a combination of hardware and software components. Throughout this part of the project, you will be guided through the process of assembling the hardware components, setting up the software, and configuring the settings and features of your smartwatch. By the end of this project, you will have a fully functional smartwatch that you can wear and use on a daily basis. So here we start to build your own Smart Watch.

We are calling this the 'Final Part' because we previously built 3 parts where we continuously added new features and sensors to make it the perfect Smart Watch.

This complete project was made possible by the compact PCB boards provided by PCBWAY. In this project we will also show you how you can place your order with them, to get your PCB boards delivered to your doorstep. 

ESP32 Smartwatch Features

  • 1.69” IPS TFT Display with a resolution of 280x240 pixels
  • Single button control
  • Deep Sleep power saving Mode
  • Auto wakeup with accelerometer
  • Auto brightness adjust using ambient light sensor
  • Digital compass for navigation
  • Heart Rate monitor
  • Multiple cool looking watch faces
  • Intuitive menu system
  • Inbuilt game for entertainment
  • Micro SD card
  • Vibration motor
  • Battery charging capability with deep discharge protection

Components Required to Build the ESP32 Smart Watch

All the parts you will require to build the smartwatch are listed below. The exact value of each component can be found in the schematics or the BOM.

  • TTGO Micro-32 V2.0 x1
  • IPS6404L PSRAM IC x1
  • 1.69” TFT display with ST7789V controller x1
  • MAX809T 3V Power supervisor Reset controller x1
  • CP2102 USB UART controller x1
  • MCP7383 1S battery charger x1
  • DW01 Battery protection IC x1
  • FS8205A MOSFET x1
  • NCP167AMX330TBG 3.3V LDO x1
  • XC6202P182MR 1.8V LDO x1
  • MPU6050 Accelerometer IC x1
  • HMC5883L Magnetometer Sensor IC x1
  • LSM303DLHC Accelerometer Magnetometer IC (Optional, replaces MPU6050 and HMC5883L) x1
  • MAX30102 Hear Rate sensor x1
  • BH1750FVI Ambient light sensor x1
  • S8050 SOT-23 NPN transistor x3
  • Micro SD Slot x1
  • 10mm Vibration Motor x1
  • 1N5819 SMD diode x2
  • LED5D5 TVS ESD protection Diodes(optional) x3
  • Micro USB Port x1
  • 0.5mm pitch 10 pin FPC connector x2
  • SMD Resistors
  • SMD Capacitors
  • PCB
  • Other tools and miscellaneous

ESP32 Smart Watch Complete Circuit Diagram

The complete circuit diagram for the ESP32 Smart Watch is shown below. It can also be downloaded in PDF format from the link given at the end.

ESP32 Smart Watch Complete Circuit Diagram

Let’s discuss the Schematics section by section for better understanding. The micro-USB port is used for both charging as well as programming purposes. The power and data connections from the micro-USB port is connected to TVS ESD protection diodes. These diodes will protect the entire circuit from any ESD spikes on the USB input. The 5V from the USB port is then connected the input of MCP7383 1S Li ion battery charger. The out put from the charging IC is then fed to the protection circuit built around the DW01 IC and the FS8205 MOSFET. This protection circuit combination will protect the battery from over current discharges and deep discharge.

ESP32 Smart Watch Protection Circuit Diagram

The power then passes through two LDOs. The main voltage regulator used in the circuit is the NCP167AMX330TBG from ON Semi. It can provide a maximum current of 700ma. The main advantage of using this chip is the size. The NCP167AMX comes in a 1mmx1mm 4-XDFN package. Which saves a lot of space. The second Low voltage regulator in the circuit is a XC620P182MR-G 1.8V LDO. This LDO is used for the MAX30102 Heart Rate sensor chip.

ESP32 Smart Watch LDO Circuit Diagram

The next section in the USB UART controller. This section is designed around the CP2102N from Silicon Labs. It supports a maximum speed of 12Mbps. Minimal number of external components along with the small QFN-24 package makes it better choice among other controller chips in the same category. The auto reset circuit for the ESP32 is built around two S8050 NPN transistors. The transistors are connected to the DTR and RTS pins of CP2102 and EN and RST pins of ESP32. This enables us to program the ESP32 without the need of a reset button.

ESP32 Smart Watch LDO Circuit Diagram

The MPU6050 accelerometer chip is used to detect motion. This feature enables us to wake the smart watch with a simple hand movement. Interrupt pin of the MPU6050 is connected to the GPIO14 of ESP32 controller. When a movement exceeding the set threshold is detected the MPU6050 will send an interrupt signal to the ESP32 to wake it from deep sleep.

ESP32 Smart Watch Accelerometer Circuit Diagram

The next sensor is the HMC5883 magnetometer sensor. This sensor is used to implement the digital compass functionality. While using this sensor make sure there are no magnetic interference or any metals nearby, which may produce wrong readings.

ESP32 Smart Watch  Magnetometer Sensor Circuit Diagram

In the PCB we have also reserved a space for LSM303 chip, which combines both accelerometer and magnetometer sensors. This sensor is included in case, we don’t want to use the MPU6050 and HMC5883L. It’s a reserved component. And you don’t have to populate it if you are using the MPU6050-HMC5883 combination.

ESP32 Smart Watch accelerometer magnetic sensors Circuit Diagram

Next, we have the BH1750 ambient light sensor. This sensor is used in order to achieve the auto brightness control. This sensor is placed at the front side below the TFT display. A small hole is provided in the enclosure to measure the ambient light. If auto brightness is enables the MCU will read the ambient light data from BH1750, and the display backlight will be adjusted accordingly.

ESP32 Smart Watch ambient light sensor Circuit Diagram

For measuring the heart rate, we have used the MAX30102 from Maxim Integrated. This sensor works on a 1.8v supply voltage and is capable of detecting heart rate using light sensors. The code is tuned in a way that the chip wont false trigger when the watch is placed in a surface other than the wrist or finger.

ESP32 Smart Watch heartrate sensor Circuit Diagram

We have also included a micro-SD slot and a vibration motor in the PCB for future developments. At this moment these are not configured or utilised in the code. The micro-SD shares the same SPI bas as the TFT display. It can be used to store data such as firmware files, monitoring logs or even watch face data or images. The vibration motor is controlled using a S8050 NPN transistor. A freewheeling diode is also connected across the motor to protect the circuit from any voltage spikes.

ESP32 Smart Watch SDCard and Vibration Motor Circuit Diagram

For the display we have used the 1.69” display with rounded corners. These IPS display gives a very nice display contrast and colour saturation. This display uses the ST7789 display driver. The ST7789 can support SPI bus frequency up to 100MHz. This will enable us to driver the display much faster providing better FPS. The backlight is controlled using a N-Channel MOSFET. PWM is used to control the brightness.

ESP32 Smart Watch TFT Display Circuit Diagram

The heart of this project is the TTGO Micro-32 V2.0 module from LILYGO. It is based on the ESP32-PICO D4 SIP with integrated ESP32 SoC, crystal oscillator, filter capacitors, RF matching links, and 4MB flash, in a single 7mm × 7mm QFN package. We have also used an IPS6404L PSRAM along with the module. The MAX809T MPU supervisor chips is used to make sure the ESP32-PICO-D4 is power cycled during the cold boot. This chip will keep the ESP32 under reset condition until the threshold voltage is reached. Once the threshold voltage is reached the MAX809T (3V Reset threshold) will reset the ESP32 and will clamp the enable pin to VCC.

ESP32 Smart Watch TTGO 32-micro Circuit Diagram

PCB for ESP32 Smart watch

For the PCB we have choose the two-board design. The top board contains the MCU along with display, UART controller, power circuitry, light sensor and the MPU6050 chip. The bottom bord contains HMC5883, LSM303, MAX30102, microSD slot and the vibration motor. The two board are connected using a 10 pin FPC cable with a pitch of 0.5mm.

ESP32 Smart Watch PCB Dimensions

Here is the 3D view of both the boards

ESP32 Smart Watch PCB Design

Here is all the componets on the main board labeled.

ESP32 Smart Watch PCB Components

And here is the daughter board with components marked.

ESP32 Smart Watch PCB Components

Here are the fully assembled board along with a TFT display.

ESP32 Smart Watch PCB with TFT Display

Here is the image of fully assembled smart watch.

assembled smart watch

Ordering the ESP32 based Smartwatch PCB from PCBWay

Now, after finalizing the design, you can proceed with ordering the PCB:

Step 1: Get into pcbway.com, and sign up if this is your first time. Then, in the PCB Prototype tab, enter the dimensions of your PCB, the number of layers, and the number of PCBs you require.

Step 2: Proceed by clicking on the ‘Quote Now’ button. You will be taken to a page to set a few additional parameters like the Board type, Layers, Material for PCB, Thickness, and more. Most of them are selected by default, if you are opting for any specific parameters, you can select it here.

Step 3: The final step is to upload the Gerber file and proceed with the payment. To make sure the process is smooth, PCBWAY verifies if your Gerber file is valid before proceeding with the payment. This way, you can be sure that your PCB is fabrication friendly and will reach you as committed.

Once you upload the Gerber file and make the payment, your job is done and you will get a confirmation email with all your details in your email address.

3D Printed Parts

smart watch 3d printed parts

We have designed a cool looking 3D printed enclosure for the smartwatch. The files for all the 3D printed parts can be downloaded from the GitHub link provided at the end of the article along with the Arduino sketch and bitmap file. It is recommended to print the parts with higher infill for better quality and sturdiness. Learn more about 3D printing and how to get started with it by following the link.

ESP32 Smart Watch GUI Navigation

The whole GUI is desgined in a way that we can navigate through every options using a single button. We can use short and long presses to navigate through them. You can finf the whole GUI flow in the below image. Blue line indicates single / short clicks , while the green line indicates long presses. In the Time Settings and Settings menu, you can navigate through each options or filed using short clocks. Select the option and use the long press to change the value.

smart watch gui design

Arduino Code for the ESP32 Smart Watch

Now let’s look at the code. As usual we have included all the necessary libraries to the code using include function, that included TFT_eSPI, ESP32Time, EEPROM, OneButton , QMC5883L, BH1750 and MAX30105 libraries. We have also included the bitmap image data along with the font files. After that we have defined all the necessary global variables. Later we have created instances for each individual component. We will use these instances to access the corresponding function.

#include <SPI.h>
#include <TFT_eSPI.h>  // Hardware-specific library
#include <ESP32Time.h>
#include "driver/gpio.h"
#include "esp_sleep.h"
#include <EEPROM.h>
#include "OneButton.h"
#include <QMC5883L.h>
#include <BH1750.h>  //BH1750 Library
#include "Free_Fonts.h"
#include "MAX30105.h"   // SparkFun librarry for MAX30102 sensor
#include "heartRate.h"  // Heartrate measurement algorithm
#include "dial240.h"    //Image data
#include "fonts.h"
#include "images.h"
#define PIN_INPUT 0
#define EEPROM_SIZE 25
#define FONT_SMALL NotoSansBold15
#define FONT_LARGE NotoSansBold36
#define TFT_GREY 0x5AEB
#define TFT_SKYBLUE 0x067D
#define color1 TFT_WHITE
#define color2 0x8410  //0x8410
#define color3 0x5ACB
#define color4 0x15B3
#define color5 0x00A3
#define colour6 0x0926
#define colour7 TFT_BLACK
#define Light_Green 0x07E8
#define background 0xB635
#define LCD_BACKLIGHT 4
#define TFTW 240          // screen width
#define TFTH 280          // screen height
#define TFTW2 (TFTW / 2)  // half screen width
#define TFTH2 (TFTH / 2)  // half screen height
#define SPEED 1
#define GRAVITY 9.8
#define JUMP_FORCE 2.15
#define SKIP_TICKS 20.0  // 1000 / 50fps
#define MAX_FRAMESKIP 5
#define BIRDW 16      // bird width
#define BIRDH 16      // bird height
#define BIRDW2 8      // half width
#define BIRDH2 8      // half height
#define PIPEW 24      // pipe width
#define GAPHEIGHT 42  // pipe gap height
#define FLOORH 30     // floor height (from bottom of the screen)
#define GRASSH 4      // grass height (inside floor, starts at floor y)
#define COLOR565(r, g, b) ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
#define BCKGRDCOL COLOR565(138, 235, 244)    // background
#define BIRDCOL COLOR565(255, 254, 174)      // bird
#define PIPECOL COLOR565(99, 255, 78)        // pipe
#define PIPEHIGHCOL COLOR565(250, 255, 250)  // pipe highlight
#define PIPESEAMCOL COLOR565(0, 0, 0)        // pipe seam
#define FLOORCOL COLOR565(246, 240, 163)     // floor
#define GRASSCOL COLOR565(141, 225, 87)      // grass (col2 is the stripe color)
#define GRASSCOL2 COLOR565(156, 239, 88)     // grass (col2 is the stripe color)
#define C0 BCKGRDCOL                         // bird sprite,bird sprite colors (Cx name for values to keep the array readable)
#define C1 COLOR565(195, 165, 75)
#define C2 BIRDCOL
#define C3 TFT_WHITE
#define C4 TFT_RED
#define C5 COLOR565(251, 216, 114)
ESP32Time rtc(0);   // RTC instance with offset in seconds
BH1750 lightMeter;  //BH1750 Instance
QMC5883L compass;
MAX30105 particleSensor;  //MAX30102 instance
OneButton button(PIN_INPUT, true);
TFT_eSPI tft = TFT_eSPI();  // Invoke custom library
TFT_eSprite img = TFT_eSprite(&tft);
static const unsigned int birdcol[] = {
  C0, C0, C1, C1, C1, C1, C1, C0, C0, C0, C1, C1, C1, C1, C1, C0,
  C0, C1, C2, C2, C2, C1, C3, C1, C0, C1, C2, C2, C2, C1, C3, C1,
  C0, C2, C2, C2, C2, C1, C3, C1, C0, C2, C2, C2, C2, C1, C3, C1,
  C1, C1, C1, C2, C2, C3, C1, C1, C1, C1, C1, C2, C2, C3, C1, C1,
  C1, C2, C2, C2, C2, C2, C4, C4, C1, C2, C2, C2, C2, C2, C4, C4,
  C1, C2, C2, C2, C1, C5, C4, C0, C1, C2, C2, C2, C1, C5, C4, C0,
  C0, C1, C2, C1, C5, C5, C5, C0, C0, C1, C2, C1, C5, C5, C5, C0,
  C0, C0, C1, C5, C5, C5, C0, C0, C0, C0, C1, C5, C5, C5, C0, C0
};
// bird structure
static struct BIRD {
  long x, y, old_y;
  long col;
  float vel_y;
} bird;
// pipe structure
static struct PIPES {
  long x, gap_y;
  long col;
} pipes;
// score
int score;
// temporary x and y var
static short tmpx, tmpy;
// ---------------
// draw pixel
// ---------------
// faster drawPixel method by inlining calls and using setAddrWindow and pushColor using macro to force inlining
#define _drawPixel(a, b, c) \
  tft.setAddrWindow(a, b, a, b); \
  tft.pushColor(c)
uint maxScore = 0;
float sx = 0, sy = 1, mx = 1, my = 0, hx = -1, hy = 0;  // Saved H, M, S x & y multipliers
float sdeg = 0, mdeg = 0, hdeg = 0;
uint16_t osx = 120, osy = 140, omx = 120, omy = 140, ohx = 120, ohy = 140;  // Saved H, M, S x & y coords
uint16_t x0 = 0, x1 = 0, yy0 = 0, yy1 = 0;
uint32_t targetTime = 0;                       // for next 1 second timeout
static uint8_t conv2d(const char* p);          // Forward declaration needed for IDE 1.6.x
uint8_t hh = 0, t_mm = 0, t_dd = 0, t_mn = 0;  //
uint32_t t_yr = 0;
uint8_t t_hh = 0, mm = 0, ss = 0;
unsigned long lastfacechange = 0;
unsigned long lastwake = 0;
unsigned long lastpressed = 0;
unsigned long lastvaluechange = 0;
bool initial = 1;
volatile int counter = 0;
float VALUE;
float lastValue = 0;
int lastsec = 0;
int pressstate = 0;
unsigned long lastDisplayUpdate = 0;
const byte RATE_SIZE = 4;  //Increase this for more averaging. 4 is good.
byte rates[RATE_SIZE];     //Array of heart rates
byte rateSpot = 0;
long lastBeat = 0;  //Time at which the last beat occurred
float beatsPerMinute;
int beatAvg;
bool beat = false;
double rad = 0.01745;
float x[360];
float y[360];
bool facechange = false;
bool Screenchange = false;
float px[360];
float py[360];
float lx[360];
float ly[360];
int r = 104;
int ssx = 120;
int ssy = 140;
String cc[12] = { "45", "40", "35", "30", "25", "20", "15", "10", "05", "0", "55", "50" };
String days[] = { "SUNDAY", "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY" };
String days1[] = { "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" };
int start[12];
int startP[60];
const int pwmFreq = 5000;
const int pwmResolution = 8;
const int pwmLedChannelTFT = 0;
int angle = 0;
bool onOff = 0;
bool debounce = 0;
int watchface = 0, Screen = 0, SubScreen = 0, Autoscreen, AutoBright, AutoscreenTime, Brigtnesslevel;
String h, m, s, d1, d2, m1, m2;
unsigned long pressStartTime;

The IRAM_ATTR functions is the interrupt function which is attached to the hardware interrupt with the GPIO0. As soon as pin change is detected this function will be invoked. This function will then invoke the button_tick function which is responsible for detecting key presses.

// This function is called from the interrupt when the signal on the PIN_INPUT has changed.
// do not use Serial in here.
void IRAM_ATTR checkTicks() {
  // include all buttons here to be checked
  button.tick();  // just call tick() to check the state.
}

The Shortclick function will be invoked if the OneButton library detects a short key press. Once it’s invoked, we will pass the next condition to it. This function will check the current task we are in and will change the variables correspondingly.

// this function will be called for short click.
void ShortClick() {
  Serial.println("singleClick() detected.");
  lastwake = millis();
  if (Screen == 0) {
    SubScreen++;
    if (SubScreen > 2) {
      SubScreen = 0;
      facechange = true;
    }
    if (SubScreen == 1) {
      particleSensor.wakeUp();
    } else {
      particleSensor.shutDown();
    }
  } else if (Screen == 1) {
    SubScreen++;
    if (SubScreen > 4) {
      SubScreen = 0;
    }
    Screenchange = true;
  } else if (Screen == 2) {
    watchface++;
    if (watchface > 5) {
      watchface = 0;
    }
    EEPROM.write(0, watchface);
    EEPROM.commit();
    facechange = true;
    Screenchange = true;
  } else if (Screen == 3) {
    SubScreen++;
    if (SubScreen > 5) {
      SubScreen = 0;
    }
    Screenchange = true;
  } else if (Screen == 4) {
    SubScreen++;
    if (SubScreen > 2) {
      SubScreen = 0;
    }
    Screenchange = true;
  } else if (Screen == 5) {
    Screen = 6;
    game_init();
    game_loop();
  } else if (Screen == 6) {
    Screen = 7;
  } else if (Screen == 7) {
    Screen = 5;
    Screenchange = true;
  }
  Serial.print("Sub ");
  Serial.println(SubScreen);
  tft.fillScreen(colour7);
  pressstate = 1;
  facechange = true;
  lastDisplayUpdate = millis();
  lastpressed = millis();
}
// ShortClick

LongPress function is invoked when a long key press, with a minimum duration of one second is detected. Once invoked it will detect the current running task and will manipulate the variables accordingly.

// long press
void LongPress() {
  Serial.println("pressStart()");
  pressStartTime = millis() - 1000;  // as set in setPressTicks()
  lastwake = millis();
  lastDisplayUpdate = millis();
  particleSensor.shutDown();
  if (Screen == 0) {
    Screen = 1;
    SubScreen = 0;
  } else if (Screen == 1) {
    if (SubScreen == 0) {
      Screen = 2;
      SubScreen = 0;
    } else if (SubScreen == 1) {
      Screen = 3;
      t_hh = rtc.getHour();
      t_mm = rtc.getMinute();
      t_dd = rtc.getDay();
      t_mn = rtc.getMonth();
      t_yr = rtc.getYear();
      Serial.println(rtc.getYear());
      Serial.println(t_yr);
      SubScreen = 0;
    } else if (SubScreen == 2) {
      Screen = 4;
      SubScreen = 0;
      facechange = true;
      Screenchange = true;
    } else if (SubScreen == 3) {
      Screen = 5;
      SubScreen = 0;
    } else if (SubScreen == 4) {
      Screen = 0;
      SubScreen = 0;
    }
  } else if (Screen == 2) {
    Screen = 1;
    SubScreen = 0;
  } else if (Screen == 3) {
    if (SubScreen == 0) {
      t_hh++;
      if (t_hh > 23) {
        t_hh = 0;
      }
    } else if (SubScreen == 1) {
      t_mm++;
      if (t_mm > 59) {
        t_mm = 0;
      }
    } else if (SubScreen == 2) {
      t_dd++;
      if (t_dd > 31) {
        t_dd = 0;
      }
    } else if (SubScreen == 3) {
      t_mn++;
      if (t_mn > 12) {
        t_mn = 0;
      }
    } else if (SubScreen == 4) {
      t_yr++;
      if (t_yr > 2041) {
        t_yr = 0;
      }
    } else {
      rtc.setTime(0, t_mm, t_hh, t_dd, t_mn, t_yr);
      Screen = 1;
      SubScreen = 1;
    }
  } else if (Screen == 4) {
    if (SubScreen == 0) {
      AutoBright++;
      if (AutoBright > 5) {
        AutoBright = 0;
      }
      if (AutoBright > 0) {
        analogWrite(LCD_BACKLIGHT, AutoBright * 50);
      }
      EEPROM.write(2, AutoBright);
      EEPROM.commit();
    } else if (SubScreen == 1) {
      Autoscreen++;
      if (Autoscreen > 5) {
        Autoscreen = 0;
      }
      EEPROM.write(1, Autoscreen);
      EEPROM.commit();
    } else if (SubScreen == 2) {
      Screen = 1;
      SubScreen = 2;
    }
  } else if (Screen == 5) {
    Screen = 1;
    SubScreen = 0;
  } else if (Screen == 7) {
    Screen = 1;
    SubScreen = 0;
  }
  facechange = true;
  Screenchange = true;
  pressstate = 1;
  lastpressed = millis();
}

In the setup function we have initialised all the required libraries and pins. The setup function will also check if the EEPROM save areas are used or not. If these locations have the default or blank values, it will load the factory default values to the location and initialise the watch to that value. The interrupt attachments are also done within the setup function, that includes deep sleep wakeup interrupt and button tick interrupt.

void setup(void) {
  Serial.begin(115200);
  Serial.println("ESP32 Watch OS.");
  gpio_hold_dis((gpio_num_t)LCD_BACKLIGHT);
  pinMode(LCD_BACKLIGHT, OUTPUT);
  digitalWrite(LCD_BACKLIGHT, LOW);
  EEPROM.begin(EEPROM_SIZE);
  EEPROM.writeInt(10, 0);
  EEPROM.commit();
  if (EEPROM.read(0) > 3) {
    EEPROM.write(0, 4);
    EEPROM.commit();
  }
  watchface = EEPROM.read(0);
  if (EEPROM.read(1) > 5) {
    EEPROM.write(1, 5);
    EEPROM.commit();
  }
  Autoscreen = EEPROM.read(1);
  if (EEPROM.read(2) > 5) {
    EEPROM.write(2, 5);
    EEPROM.commit();
  }
  AutoBright = EEPROM.read(2);
  //rtc.setTime(ss, mm, hh, 0, 0, 0);  // 26th Jjuly 2022 compile date
  particleSensor.begin(Wire, I2C_SPEED_FAST);
  particleSensor.setup();                     //Configure sensor with default settings
  particleSensor.setPulseAmplitudeRed(0x0A);  //Turn Red LED to low to indicate sensor is running
  particleSensor.setPulseAmplitudeIR(0xFF);   //Turn Red LED to low to indicate sensor is running
  particleSensor.setPulseAmplitudeGreen(0);   //Turn off Green LED
  particleSensor.shutDown();
  compass.init();
  compass.setSamplingRate(50);
  tft.init();
  tft.setRotation(0);
  //tft.setColorDepth(16);
  tft.setSwapBytes(true);
  tft.fillScreen(colour7);
  int xw = tft.width() / 2;  // xw, yh is middle of screen
  int yh = tft.height() / 2;
  tft.setPivot(xw, yh);  // Set pivot to middle of TFT screen
  img.createSprite(240, 280);
  img.setTextDatum(4);
  img1.createSprite(240, 70);
  img1.setSwapBytes(true);
  img2.createSprite(240, 70);
  img2.setSwapBytes(true);
  targetTime = millis() + 1000;
  facechange = true;
  esp_sleep_enable_ext0_wakeup(GPIO_NUM_0, 0);  //1 = High, 0 = Low
  // setup interrupt routine
  // when not registering to the interrupt the sketch also works when the tick is called frequently.
  attachInterrupt(digitalPinToInterrupt(PIN_INPUT), checkTicks, CHANGE);
  // link the xxxclick functions to be called on xxxclick event.
  button.attachClick(ShortClick);
  button.setPressTicks(1000);  // that is the time when LongPressStart is called
  button.attachLongPressStart(LongPress);
  lightMeter.begin(BH1750::CONTINUOUS_HIGH_RES_MODE);  //Init BH1750 library
  if (AutoBright == 0) {
    unsigned int lv = constrain(lightMeter.readLightLevel(), 50, 500);
    analogWrite(LCD_BACKLIGHT, lv / 2);
  } else {
    analogWrite(LCD_BACKLIGHT, AutoBright * 50);
  }
  lastwake = millis();
}
int lastAngle = 0;
float circle = 100;
bool dir = 0;
int rAngle = 359;

The loop function will call all the mains functions periodically, that included button_tick, watchtask and the game functions. The button_tick function is responsible for the key press detection and distinguish between shot press and long press. The watchtask function is responsible for all the major functions that includes watch face display, heart rate monitoring, digital compass, menu handling and all the settings and navigations. In the meantime, the game function will handles the flappy bird game, the included in the watch OS.

void loop() {
  button.tick();
  if (Screen < 5) {
    watchtask();
  } else {
    game();
  }
}

As mentioned earlier the watchtask will handle most the tasks related to the smart watch. This function will check for if the brightness settings and will adjust the backlight PWM accordingly. It will also check for the screen time out settings and will put the ESP32 to deep sleep accordingly. All the sub functions will be called accordingly based on the current active task. We have used gpio_deep_sleep_hold_en function along with the gpio_hold_en function to keep the backlight pin active low during the deep sleep. Without these functions the GPIO will be released from any set states, and it will affect the backlight control.

void watchtask() {
  if (pressstate == 1 && digitalRead(0) == 1) {
    pressstate = 0;
  }
  if (AutoBright == 0) {
    unsigned int lv = constrain(lightMeter.readLightLevel(), 50, 500);
    analogWrite(LCD_BACKLIGHT, lv / 2);
    Serial.print("Light");
    Serial.println(lv / 2);
  }
  if (Autoscreen != 0 && millis() - lastwake > Autoscreen * 60000) {
    analogWrite(LCD_BACKLIGHT, 0);
    delay(1000);
    tft.fillScreen(colour7);
    gpio_deep_sleep_hold_en();
    gpio_hold_en((gpio_num_t)LCD_BACKLIGHT);
    esp_deep_sleep_start();
  }
  if (Screen == 0) {
    if (SubScreen == 0) {
      watchfacedsp();
    } else if (SubScreen == 1) {
      HRApp();
    } else {
      CompassApp();
    }
  } else if (Screen == 1 && Screenchange == true) {
    if (SubScreen == 0) {
      tft.pushImage(0, 0, 240, 280, facechangeicon);
    } else if (SubScreen == 1) {
      tft.pushImage(0, 0, 240, 280, timeseticon);
    } else if (SubScreen == 2) {
      tft.pushImage(0, 0, 240, 280, settingsicon);
    } else if (SubScreen == 3) {
      tft.pushImage(0, 0, 240, 280, gamesicon);
    } else if (SubScreen == 4) {
      tft.pushImage(0, 0, 240, 280, exiticon);
    }
  } else if (Screen == 3 && Screenchange == true) {
    timesetiings();
    Screenchange = false;
  } else if (Screen == 2 && Screenchange == true) {
    watchfacedsp();
    Screenchange = false;
  } else if (Screen == 4 && Screenchange == true) {
    settings();
    Screenchange = false;
  } else if (Screen == 3 && millis() - lastpressed > 2000 && millis() - lastvaluechange > 500 && pressstate == 1) {
    if (SubScreen == 0) {
      t_hh++;
      if (t_hh > 23) {
        t_hh = 0;
      }
      facechange = true;
      Screenchange = true;
    } else if (SubScreen == 1) {
      t_mm++;
      if (t_mm > 59) {
        t_mm = 0;
      }
      facechange = true;
      Screenchange = true;
    } else if (SubScreen == 2) {
      facechange = true;
      Screenchange = true;
      t_dd++;
      if (t_dd > 31) {
        t_dd = 0;
      }
    } else if (SubScreen == 3) {
      t_mn++;
      if (t_mn > 12) {
        t_mn = 0;
      }
      facechange = true;
      Screenchange = true;
    } else if (SubScreen == 4) {
      t_yr++;
      if (t_yr > 2041) {
        t_yr = 0;
      }
      facechange = true;
      Screenchange = true;
    }
    lastvaluechange = millis();
  }
}

The timesettings functions handles the time settings menu. Using this function, we can set the correct date and time. Short press to change the filed and long press to change the value.

void timesetiings() {
  tft.pushImage(0, 0, 240, 280, TimeSettings);
  tft.setTextColor(TFT_BLACK, TFT_WHITE);
  tft.setFreeFont(FF18);
  int tt_hh = t_hh;
  if (tt_hh > 12) {
    tt_hh = tt_hh - 12;
  }
  if (tt_hh < 10) {
    tft.drawString("0" + String(tt_hh), 25, 96);
  } else {
    tft.drawString(String(tt_hh), 25, 96);
  }
  if (t_mm < 10) {
    tft.drawString("0" + String(t_mm), 93, 96);
  } else {
    tft.drawString(String(t_mm), 93, 96);
  }
  if (t_hh < 13) {
    tft.drawString("AM", 164, 96);
  } else {
    tft.drawString("PM", 164, 96);
  }
  if (t_dd < 10) {
    tft.drawString("0" + String(t_dd), 25, 183);
  } else {
    tft.drawString(String(t_dd), 25, 183);
  }
  if (t_mn < 10) {
    tft.drawString("0" + String(t_mn), 93, 183);
  } else {
    tft.drawString(String(t_mn), 93, 183);
  }
  if (t_yr < 2022) {
    t_yr = 2022;
  }
  tft.drawString(String(t_yr), 161, 183);
  if (SubScreen == 0) {
    tft.drawRoundRect(9, 81, 58, 48, 6, Light_Green);
  } else if (SubScreen == 1) {
    tft.drawRoundRect(77, 81, 58, 48, 6, Light_Green);
  } else if (SubScreen == 2) {
    tft.drawRoundRect(9, 168, 58, 48, 6, Light_Green);
  } else if (SubScreen == 3) {
    tft.drawRoundRect(77, 168, 58, 48, 6, Light_Green);
  } else if (SubScreen == 4) {
    tft.drawRoundRect(144, 168, 88, 48, 6, Light_Green);
  } else {
    tft.drawRoundRect(79, 226, 88, 48, 6, Light_Green);
  }
}

Using the settings menu, we can manage the brightness and screen time out settings. We can either set the watch to adjust the screen brightness according to the BH1750 ambient light sensor reading or we can set it manually from 20-100% in 20% steps. For screen time out we can choose either to keep the screen on all the time or we can set the screen time out from 1 minute to 5 minutes in 1-minute steps.

void settings() {
  tft.pushImage(0, 0, 240, 280, Settingspage);
  tft.setTextColor(TFT_BLACK, TFT_WHITE);
  tft.setFreeFont(FF18);
  switch (AutoBright) {
    case 0: tft.drawString("  Auto  ", 80, 100); break;
    case 1: tft.drawString("  20%   ", 85, 100); break;
    case 2: tft.drawString("  40%   ", 85, 100); break;
    case 3: tft.drawString("  60%   ", 85, 100); break;
    case 4: tft.drawString("  80%   ", 85, 100); break;
    case 5: tft.drawString(" 100%   ", 80, 100); break;
  }
  switch (Autoscreen) {
    case 0: tft.drawString("Always On", 65, 185); break;
    case 1: tft.drawString("1 Minute ", 75, 185); break;
    case 2: tft.drawString("2 Minute ", 75, 185); break;
    case 3: tft.drawString("3 Minute ", 75, 185); break;
    case 4: tft.drawString("4 Minute ", 75, 185); break;
    case 5: tft.drawString("5 Minute ", 75, 185); break;
  }
  if (SubScreen == 0) {
    tft.drawRoundRect(16, 85, 208, 48, 6, Light_Green);
  } else if (SubScreen == 1) {
    tft.drawRoundRect(16, 169, 208, 48, 6, Light_Green);
  } else {
    tft.drawRoundRect(66, 225, 108, 48, 6, Light_Green);
  }
}

The HRApp function handles the heart rate sensor. As soon as this function is called, we will activate the MAX30102 sensor and will start reading. If no wrist or finger is detected the watch will display error message. Once detected the watch will detect the beatings and will calculates the heart rate in bps. Once we exit this function the watch will power down the MAX30102 to low power mode to save power.

void HRApp() {
  long irValue = particleSensor.getIR();  //Reading the IR value it will permit us to know if there's a finger on the sensor or not
  //Also detecting a heartbeat
  if (checkForBeat(irValue) == true)  //If a heart beat is detected
  {
    long delta = millis() - lastBeat;  //Measure duration between two beats
    lastBeat = millis();
    beatsPerMinute = 60 / (delta / 1000.0);  //Calculating the BPM
    if (beatsPerMinute < 255 && beatsPerMinute > 20)  //To calculate the average we strore some values (4) then do some math to calculate the average {
      rates[rateSpot++] = (byte)beatsPerMinute;  //Store this reading in the array
      rateSpot %= RATE_SIZE;                     //Wrap variable
      //Take average of readings
      beatAvg = 0;
      for (byte x = 0; x < RATE_SIZE; x++)
        beatAvg += rates[x];
      beatAvg /= RATE_SIZE;
    }
  }
  if (millis() - lastDisplayUpdate > 500) {
    if (irValue < 60000) {  //If no finger is detected it inform the user and put the average BPM to 0 or it will be stored for the next measure
      beatAvg = 0;
      img.loadFont(FONT_SMALL);
      img.setCursor(80, 120);
      img.setTextColor(TFT_CYAN, colour7);
      img.fillSprite(colour7);
      img.println("Please Place ");
      img.setCursor(80, 140);
      img.println("your finger ");
      img.pushSprite(0, 0);
    } else {
      img.fillSprite(colour7);  //Clear the display
      if (beat == true) {
        img.pushImage(0, 0, 240, 280, hr1);
      } else {
        img.pushImage(0, 0, 240, 280, hr2);
      }
      beat = !beat;
      img.setTextColor(TFT_CYAN, colour7);
      img.loadFont(FONT_LARGE);
      img.setCursor(100, 130);
      img.print(beatAvg);
      img.setCursor(100, 175);
      img.loadFont(FONT_SMALL);
      img.print("BPM ");
      img.pushRotated(0);
    }
    lastDisplayUpdate = millis();
  }
  Serial.print("IR=");
  Serial.print(irValue);
  Serial.print(", BPM=");
  Serial.print(beatsPerMinute);
  Serial.print(", Avg BPM=");
  Serial.print(beatAvg);
  if (irValue < 60000)
    Serial.print(" No finger?");
  if (millis() - lastBeat > 5000) {
    beatsPerMinute = 0;
    beatAvg = 0;
  }
  Serial.println();
}

Similarly, the CompassApp function will communicate with the HMC5883L sensor and will calculate the headings accordingly. Once the angle is calculated the function will display the compass dial in the appropriate orientation, indicating the directions.

void CompassApp() {
  for (int i = 0; i < 10; i++) {
    angle = angle + compass.readHeading();
  }
  angle = angle / 10;
  img.fillSprite(colour7);
  img.pushImage(0, 20, 240, 240, dial240);
  img.pushRotated(angle);  // create rotated image as per the angle from the compass sensor
  angle = 0;
}

For displaying the selected watch face we will use the watchfacedp function. This function will check for the current set watch face, and it will display the current time using that specific watch face. Current time is read from the internal RTC registries. The internal RTC will keep running even if the ESP32 goes to deep sleep, keeping the exact time.

void watchfacedsp() {
  if (facechange) {
    tft.fillScreen(colour7);
    if (watchface == 1) {
      tft.setTextSize(0);
      tft.pushImage(0, 0, 240, 280, Casio2);
      tft.setTextColor(0x0081, background);
      tft.fillRoundRect(48, 127, 128, 48, 5, background);
    } else if (watchface == 2) {
      tft.setTextSize(0);
      tft.pushImage(0, 0, 240, 280, Casio1);
      tft.setTextColor(0x0081, background);
      tft.fillRoundRect(48, 127, 128, 48, 5, background);
    } else if (watchface == 4) {
      tft.pushImage(0, 0, 240, 280, cdface1);
      img2.pushImage(0, 0, 240, 100, cdface11);
      tft.setTextColor(TFT_WHITE);
      tft.setFreeFont(FF18);
      tft.setTextSize(2);
    } else if (watchface == 5) {
      tft.pushImage(0, 0, 240, 280, cdface2);
      img2.pushImage(0, 0, 240, 100, cdface12);
      tft.setTextColor(TFT_WHITE);
      tft.setFreeFont(FF18);
      tft.setTextSize(2);
    }
    facechange = false;
    lastfacechange = millis();
  }
  if (watchface == 0) {
    int b = 0;
    int b2 = 0;
    for (int i = 0; i < 360; i++) {
      x[i] = (r * cos(rad * i)) + ssx;
      y[i] = (r * sin(rad * i)) + ssy;
      px[i] = ((r - 16) * cos(rad * i)) + ssx;
      py[i] = ((r - 16) * sin(rad * i)) + ssy;
      lx[i] = ((r - 26) * cos(rad * i)) + ssx;
      ly[i] = ((r - 26) * sin(rad * i)) + ssy;
      if (i % 30 == 0) {
        start[b] = i;
        b++;
      }
      if (i % 6 == 0) {
        startP[b2] = i;
        b2++;
      }
    }
    rAngle = rAngle - 2;
    angle = rtc.getSecond() * 6;
    s = String(rtc.getSecond());
    m = String(rtc.getMinute());
    h = String(rtc.getHour());
    if (m.toInt() < 10)
      m = "0" + m;
    if (h.toInt() < 10)
      h = "0" + h;
    if (s.toInt() < 10)
      s = "0" + s;
    if (rtc.getDay() > 10) {
      d1 = rtc.getDay() / 10;
      d2 = rtc.getDay() % 10;
    } else {
      d1 = "0";
      d2 = String(rtc.getDay());
    }
    if (rtc.getMonth() > 10) {
      m1 = rtc.getMonth() / 10;
      m2 = rtc.getMonth() % 10;
    } else {
      m1 = "0";
      m2 = String(rtc.getMonth());
    }
    if (angle >= 360)
      angle = 0;
    if (rAngle <= 0)
      rAngle = 359;
    if (dir == 0)
      circle = circle + 0.5;
    else
      circle = circle - 0.5;
    if (circle > 140)
      dir = !dir;
    if (circle < 100)
      dir = !dir;
    if (angle > -1) {
      lastAngle = angle;
      VALUE = ((angle - 270) / 3.60) * -1;
      if (VALUE < 0)
        VALUE = VALUE + 100;
      img.fillSprite(colour7);
      img.fillCircle(ssx, ssy, 124, colour7);
      img.setTextColor(TFT_WHITE, colour7);
      img.drawString(days[rtc.getDayofWeek()], circle, 140, 2);
      for (int i = 0; i < 12; i++)
        if (start[i] + angle < 360) {
          img.drawString(cc[i], x[start[i] + angle], y[start[i] + angle], 2);
          img.drawLine(px[start[i] + angle], py[start[i] + angle], lx[start[i] + angle], ly[start[i] + angle], color1);
        } else {
          img.drawString(cc[i], x[(start[i] + angle) - 360], y[(start[i] + angle) - 360], 2);
          img.drawLine(px[(start[i] + angle) - 360], py[(start[i] + angle) - 360], lx[(start[i] + angle) - 360], ly[(start[i] + angle) - 360], color1);
        }
      img.setFreeFont(&DSEG7_Modern_Bold_20);
      img.drawString(s, ssx, ssy - 36);
      img.setFreeFont(&DSEG7_Classic_Regular_28);
      img.drawString(h + ":" + m, ssx, ssy + 28);
      img.setTextFont(0);
      img.fillRect(70, 86, 12, 20, color3);
      img.fillRect(84, 86, 12, 20, color3);
      img.fillRect(150, 86, 12, 20, color3);
      img.fillRect(164, 86, 12, 20, color3);
      img.setTextColor(0x35D7, colour7);
      img.drawString("MONTH", 84, 78);
      img.drawString("DAY", 162, 78);
      img.setTextColor(TFT_SKYBLUE, colour7);
      img.drawString("Circuit Digest", 120, 194);
      img.drawString("***", 120, 124);
      img.setTextColor(TFT_WHITE, color3);
      img.drawString(m1, 77, 96, 2);
      img.drawString(m2, 91, 96, 2);
      img.drawString(d1, 157, 96, 2);
      img.drawString(d2, 171, 96, 2);
      for (int i = 0; i < 60; i++)
        if (startP[i] + angle < 360)
          img.fillCircle(px[startP[i] + angle], py[startP[i] + angle], 1, color1);
        else
          img.fillCircle(px[(startP[i] + angle) - 360], py[(startP[i] + angle) - 360], 1, color1);
      img.fillTriangle(ssx - 1, ssy - 70, ssx - 5, ssy - 56, ssx + 4, ssy - 56, TFT_ORANGE);
      img.fillCircle(px[rAngle], py[rAngle], 6, TFT_RED);
      img.pushSprite(0, 0);
    }
  } else if (rtc.getSecond() != lastsec || Screen == 3) {
    if (watchface == 1 || watchface == 2) {
      /*
      String med;
      if (rtc.getSecond() % 2) {
        med = ":";
      } else {
        med = " ";
      }
      */
      tft.setFreeFont(&DSEG7_Classic_Bold_30);
      if (rtc.getHour() > 9 && rtc.getMinute() > 9) {
        tft.drawString(String(rtc.getHour()) + ":" + String(rtc.getMinute()), 46, 135);
      } else if (rtc.getHour() < 10 && rtc.getMinute() > 9) {
        tft.drawString("0" + String(rtc.getHour()) + ":" + String(rtc.getMinute()), 46, 135);
      } else if (rtc.getHour() > 9 && rtc.getMinute() < 10) {
        tft.drawString(String(rtc.getHour()) + ":0" + String(rtc.getMinute()), 46, 135);
      } else {
        tft.drawString("0" + String(rtc.getHour()) + ":0" + String(rtc.getMinute()), 46, 135);
      }
      tft.setFreeFont(&DSEG7_Classic_Bold_20);
      if (rtc.getSecond() < 10) {
        tft.drawString("0" + String(rtc.getSecond()), 154, 145);
      } else {
        tft.drawString(String(rtc.getSecond()), 154, 145);
      }
      tft.setFreeFont(&DSEG14_Classic_Bold_18);
      tft.drawString(days1[rtc.getDayofWeek()], 94, 106);
      tft.drawString(String(rtc.getDay()), 156, 106);
    } else if (watchface == 3) {
      img.setTextColor(TFT_WHITE, colour7);  // Adding a background colour erases previous text automatically
      // Draw clock face
      img.fillCircle(120, 140, 118, TFT_GREEN);
      img.fillCircle(120, 140, 110, colour7);
      // Draw 12 lines
      for (int i = 0; i < 360; i += 30) {
        sx = cos((i - 90) * 0.0174532925);
        sy = sin((i - 90) * 0.0174532925);
        x0 = sx * 114 + 120;
        yy0 = sy * 114 + 140;
        x1 = sx * 100 + 120;
        yy1 = sy * 100 + 140;
        img.drawLine(x0, yy0, x1, yy1, TFT_GREEN);
      }
      // Draw 60 dots
      for (int i = 0; i < 360; i += 6) {
        sx = cos((i - 90) * 0.0174532925);
        sy = sin((i - 90) * 0.0174532925);
        x0 = sx * 102 + 120;
        yy0 = sy * 102 + 140;
        // Draw minute markers
        img.drawPixel(x0, yy0, TFT_WHITE);
        // Draw main quadrant dots
        if (i == 0 || i == 180) img.fillCircle(x0, yy0, 2, TFT_WHITE);
        if (i == 90 || i == 270) img.fillCircle(x0, yy0, 2, TFT_WHITE);
      }
      img.fillCircle(120, 141, 3, TFT_WHITE);
      // Pre-compute hand degrees, x & y coords for a fast screen update
      sdeg = rtc.getSecond() * 6;                      // 0-59 -> 0-354
      mdeg = rtc.getMinute() * 6 + sdeg * 0.01666667;  // 0-59 -> 0-360 - includes seconds
      hdeg = rtc.getHour() * 30 + mdeg * 0.0833333;    // 0-11 -> 0-360 - includes minutes and seconds
      hx = cos((hdeg - 90) * 0.0174532925);
      hy = sin((hdeg - 90) * 0.0174532925);
      mx = cos((mdeg - 90) * 0.0174532925);
      my = sin((mdeg - 90) * 0.0174532925);
      sx = cos((sdeg - 90) * 0.0174532925);
      sy = sin((sdeg - 90) * 0.0174532925);
      if (rtc.getSecond() == 0 || initial) {
        initial = 0;
        // Erase hour and minute hand positions every minute
        img.drawLine(ohx, ohy, 120, 141, colour7);
        ohx = hx * 62 + 121;
        ohy = hy * 62 + 141;
        img.drawLine(omx, omy, 120, 141, colour7);
        omx = mx * 84 + 120;
        omy = my * 84 + 141;
      }
      // Redraw new hand positions, hour and minute hands not erased here to avoid flicker
      img.drawLine(osx, osy, 120, 141, colour7);
      osx = sx * 90 + 121;
      osy = sy * 90 + 141;
      img.drawLine(osx, osy, 120, 141, TFT_RED);
      img.drawLine(ohx, ohy, 120, 141, TFT_WHITE);
      img.drawLine(omx, omy, 120, 141, TFT_WHITE);
      img.drawLine(osx, osy, 120, 141, TFT_RED);
      img.fillCircle(120, 141, 3, TFT_RED);
      img.pushSprite(0, 0);
    } else if (watchface == 4 || watchface == 5) {
      img1.setTextColor(TFT_WHITE, TFT_BLACK);
      img1.setFreeFont(FF24);
      img1.fillSprite(TFT_BLACK);
      //img1.setTextSize(2);
      if (rtc.getHour() > 9 && rtc.getMinute() > 9) {
        img1.drawString(String(rtc.getHour()) + ":" + String(rtc.getMinute()), 66, 30);
      } else if (rtc.getHour() < 10 && rtc.getMinute() > 9) {
        img1.drawString("0" + String(rtc.getHour()) + ":" + String(rtc.getMinute()), 66, 30);
      } else if (rtc.getHour() > 9 && rtc.getMinute() < 10) {
        img1.drawString(String(rtc.getHour()) + ":0" + String(rtc.getMinute()), 66, 30);
      } else {
        img1.drawString("0" + String(rtc.getHour()) + +":0" + String(rtc.getMinute()), 66, 30);
      }
      img1.setFreeFont(FF22);
      if (rtc.getSecond() < 10) {
        img1.drawString("0" + String(rtc.getSecond()), 190, 40);
      } else {
        img1.drawString(String(rtc.getSecond()), 190, 40);
      }
      img1.drawString(days1[rtc.getDayofWeek()] + " " + String(rtc.getDay()) + " " + String(rtc.getYear()), 54, 0);
      //img1.drawString(String(rtc.getDay()), 156, 0);
      img2.pushSprite(0, 180);
      img1.pushSprite(0, 180, TFT_BLACK);
    }
    lastsec = rtc.getSecond();
  }
}
static uint8_t conv2d(const char* p) {
  uint8_t v = 0;
  if ('0' <= *p && *p <= '9')
    v = *p - '0';
  return 10 * v + *++p - '0';
}
void game() {
  if (Screen == 5 && Screenchange == 1) {
    Screenchange = 0;
    tft.fillScreen(TFT_BLACK);
    tft.fillRect(10, TFTH2 - 20, TFTW - 20, 1, TFT_WHITE);
    tft.fillRect(10, TFTH2 + 32, TFTW - 20, 1, TFT_WHITE);
    tft.setTextColor(TFT_WHITE);
    tft.setFreeFont(0);
    tft.setTextSize(2);
    // half width - num char * char width in pixels
    tft.setCursor(TFTW2 - (6 * 9), TFTH2 - 16);
    tft.println("FLAPPY");
    tft.setTextSize(2);
    tft.setCursor(TFTW2 - (6 * 9), TFTH2 + 8);
    tft.println("-BIRD-");
  } else if (Screen == 7 && Screenchange == 1) {
    Screenchange = 0;
    tft.fillScreen(TFT_BLACK);
    maxScore = EEPROM.readInt(10);
    if (score > maxScore) {
      EEPROM.writeInt(10, score);
      EEPROM.commit();
      maxScore = score;
      tft.setTextColor(TFT_RED);
      tft.setTextSize(2);
      tft.setCursor(TFTW2 - (13 * 6), TFTH2 - 26);
      tft.println("NEW HIGHSCORE");
    }
    tft.setTextColor(TFT_WHITE);
    tft.setTextSize(3);
    // half width - num char * char width in pixels
    tft.setCursor(TFTW2 - (9 * 9), TFTH2 - 6);
    tft.println("GAME OVER");
    tft.setTextSize(2);
    tft.setCursor(10, 10);
    tft.print("score: ");
    tft.print(score);
    tft.setCursor(TFTW2 - (12 * 6), TFTH2 + 18);
    tft.println("press button");
    tft.setCursor(10, 28);
    tft.print("Max Score:");
    tft.print(maxScore);
    tft.setTextSize(0);
    facechange = 1;
  }
}
static uint8_t conv2d(const char* p) {
  uint8_t v = 0;
  if ('0' <= *p && *p <= '9')
    v = *p - '0';
  return 10 * v + *++p - '0';
}

The game functions handle the inbuilt flappy game. This function is responsible for the start and end screens. It will also call the game initialisation function and the game_loop functions once triggered.

void game() {
  if (Screen == 5 && Screenchange == 1) {
    Screenchange = 0;
    tft.fillScreen(TFT_BLACK);
    tft.fillRect(10, TFTH2 - 20, TFTW - 20, 1, TFT_WHITE);
    tft.fillRect(10, TFTH2 + 32, TFTW - 20, 1, TFT_WHITE);
    tft.setTextColor(TFT_WHITE);
    tft.setFreeFont(0);
    tft.setTextSize(2);
    // half width - num char * char width in pixels
    tft.setCursor(TFTW2 - (6 * 9), TFTH2 - 16);
    tft.println("FLAPPY");
    tft.setTextSize(2);
    tft.setCursor(TFTW2 - (6 * 9), TFTH2 + 8);
    tft.println("-BIRD-");
  } else if (Screen == 7 && Screenchange == 1) {
    Screenchange = 0;
    tft.fillScreen(TFT_BLACK);
    maxScore = EEPROM.readInt(10);
    if (score > maxScore) {
      EEPROM.writeInt(10, score);
      EEPROM.commit();
      maxScore = score;
      tft.setTextColor(TFT_RED);
      tft.setTextSize(2);
      tft.setCursor(TFTW2 - (13 * 6), TFTH2 - 26);
      tft.println("NEW HIGHSCORE");
    }
    tft.setTextColor(TFT_WHITE);
    tft.setTextSize(3);
    // half width - num char * char width in pixels
    tft.setCursor(TFTW2 - (9 * 9), TFTH2 - 6);
    tft.println("GAME OVER");
    tft.setTextSize(2);
    tft.setCursor(10, 10);
    tft.print("score: ");
    tft.print(score);
    tft.setCursor(TFTW2 - (12 * 6), TFTH2 + 18);
    tft.println("press button");
    tft.setCursor(10, 28);
    tft.print("Max Score:");
    tft.print(maxScore);
    tft.setTextSize(0);
    facechange = 1;
  }
}

The game_init function is responsible for clearing the display prior to starting the games along with setting the initial game variable values.

void game_init() {
  // clear screen
  tft.fillScreen(BCKGRDCOL);
  // reset score
  score = 0;
  // init bird
  bird.x = 144;
  bird.y = bird.old_y = TFTH2 - BIRDH;
  bird.vel_y = -JUMP_FORCE;
  tmpx = tmpy = 0;
  // generate new random seed for the pipe gape
  randomSeed(analogRead(12));
  // init pipe
  pipes.x = 0;
  pipes.gap_y = random(20, TFTH - 60);
}

The game functions handle the inbuilt flappy game. This function is responsible for the start and end screens. It will also call the game initialisation function and the game_loop functions once triggered.

void game() {
  if (Screen == 5 && Screenchange == 1) {
    Screenchange = 0;
    tft.fillScreen(TFT_BLACK);
    tft.fillRect(10, TFTH2 - 20, TFTW - 20, 1, TFT_WHITE);
    tft.fillRect(10, TFTH2 + 32, TFTW - 20, 1, TFT_WHITE);
    tft.setTextColor(TFT_WHITE);
    tft.setFreeFont(0);
    tft.setTextSize(2);
    // half width - num char * char width in pixels
    tft.setCursor(TFTW2 - (6 * 9), TFTH2 - 16);
    tft.println("FLAPPY");
    tft.setTextSize(2);
    tft.setCursor(TFTW2 - (6 * 9), TFTH2 + 8);
    tft.println("-BIRD-");
  } else if (Screen == 7 && Screenchange == 1) {
    Screenchange = 0;
    tft.fillScreen(TFT_BLACK);
    maxScore = EEPROM.readInt(10);
    if (score > maxScore) {
      EEPROM.writeInt(10, score);
      EEPROM.commit();
      maxScore = score;
      tft.setTextColor(TFT_RED);
      tft.setTextSize(2);
      tft.setCursor(TFTW2 - (13 * 6), TFTH2 - 26);
      tft.println("NEW HIGHSCORE");
    }
    tft.setTextColor(TFT_WHITE);
    tft.setTextSize(3);
    // half width - num char * char width in pixels
    tft.setCursor(TFTW2 - (9 * 9), TFTH2 - 6);
    tft.println("GAME OVER");
    tft.setTextSize(2);
    tft.setCursor(10, 10);
    tft.print("score: ");
    tft.print(score);
    tft.setCursor(TFTW2 - (12 * 6), TFTH2 + 18);
    tft.println("press button");
    tft.setCursor(10, 28);
    tft.print("Max Score:");
    tft.print(maxScore);
    tft.setTextSize(0);
    facechange = 1;
  }
}

The game_init function is responsible for clearing the display prior to starting the games along with setting the initial game variable values.

void game_init() {
  // clear screen
  tft.fillScreen(BCKGRDCOL);
  // reset score
  score = 0;
  // init bird
  bird.x = 144;
  bird.y = bird.old_y = TFTH2 - BIRDH;
  bird.vel_y = -JUMP_FORCE;
  tmpx = tmpy = 0;
  // generate new random seed for the pipe gape
  randomSeed(analogRead(12));
  // init pipe
  pipes.x = 0;
  pipes.gap_y = random(20, TFTH - 60);
}

The main function handles the whole game is the game_loop function. It is responsible for all the game graphics as well as game dynamics. It will monitor the key press as well. Key press detection for the game is done directly with in this function without the OneButton library. Sprites and other fast rendering techniques are used for smooth game performance.

void game_loop() {
  // ===============
  // prepare game variables
  // draw floor
  // ===============
  // instead of calculating the distance of the floor from the screen height each time store it in a variable
  const unsigned char GAMEH = TFTH - FLOORH;
  // draw the floor once, we will not overwrite on this area in-game
  // black line
  tft.drawFastHLine(0, GAMEH, TFTW, TFT_BLACK);
  // grass and stripe
  tft.fillRect(0, GAMEH + 1, TFTW2, GRASSH, GRASSCOL);
  tft.fillRect(TFTW2, GAMEH + 1, TFTW2, GRASSH, GRASSCOL2);
  // black line
  tft.drawFastHLine(0, GAMEH + GRASSH, TFTW, TFT_BLACK);
  // mud
  tft.fillRect(0, GAMEH + GRASSH + 1, TFTW, FLOORH - GRASSH, FLOORCOL);
  // grass x position (for stripe animation)
  long grassx = TFTW;
  // game loop time variables
  double delta, old_time, next_game_tick, current_time;
  next_game_tick = current_time = millis();
  // passed pipe flag to count score
  bool passed_pipe = false;
  // temp var for setAddrWindow
  unsigned char px;
  while (true) {
    yield();
    int loops = 0;
    while (millis() > next_game_tick && loops < MAX_FRAMESKIP) {
      // ===============
      // input
      // ===============
      if (digitalRead(0) == LOW) {
        // if the bird is not too close to the top of the screen apply jump force
        if (bird.y > BIRDH2 * 0.5)
          bird.vel_y = -JUMP_FORCE;
        // else zero velocity
        else
          bird.vel_y = 0;
      }
      // ===============
      // update
      // ===============
      // calculate delta time
      // ---------------
      old_time = current_time;
      current_time = millis();
      delta = (current_time - old_time) / 1000;
      // bird
      // ---------------
      bird.vel_y += GRAVITY * delta;
      bird.y += bird.vel_y;
      // pipe
      // ---------------
      pipes.x -= SPEED;
      // if pipe reached edge of the screen reset its position and gap
      if (pipes.x < -PIPEW) {
        pipes.x = TFTW;
        pipes.gap_y = random(10, GAMEH - (10 + GAPHEIGHT));
      }
      // ---------------
      next_game_tick += SKIP_TICKS;
      loops++;
    }
    // ===============
    // draw
    // ===============
    // pipe
    // ---------------
    // we save cycles if we avoid drawing the pipe when outside the screen
    if (pipes.x >= 0 && pipes.x < TFTW) {
      // pipe color
      tft.drawFastVLine(pipes.x + 3, 0, pipes.gap_y, PIPECOL);
      tft.drawFastVLine(pipes.x + 3, pipes.gap_y + GAPHEIGHT + 1, GAMEH - (pipes.gap_y + GAPHEIGHT + 1), PIPECOL);
      // highlight
      tft.drawFastVLine(pipes.x, 0, pipes.gap_y, PIPEHIGHCOL);
      tft.drawFastVLine(pipes.x, pipes.gap_y + GAPHEIGHT + 1, GAMEH - (pipes.gap_y + GAPHEIGHT + 1), PIPEHIGHCOL);
      // bottom and top border of pipe
      _drawPixel(pipes.x, pipes.gap_y, PIPESEAMCOL);
      _drawPixel(pipes.x, pipes.gap_y + GAPHEIGHT, PIPESEAMCOL);
      // pipe seam
      _drawPixel(pipes.x, pipes.gap_y - 6, PIPESEAMCOL);
      _drawPixel(pipes.x, pipes.gap_y + GAPHEIGHT + 6, PIPESEAMCOL);
      _drawPixel(pipes.x + 3, pipes.gap_y - 6, PIPESEAMCOL);
      _drawPixel(pipes.x + 3, pipes.gap_y + GAPHEIGHT + 6, PIPESEAMCOL);
    }
    // erase behind pipe
    if (pipes.x <= TFTW)
      tft.drawFastVLine(pipes.x + PIPEW, 0, GAMEH, BCKGRDCOL);
    // bird
    // ---------------
    tmpx = BIRDW - 1;
    do {
      px = bird.x + tmpx + BIRDW;
      // clear bird at previous position stored in old_y
      // we can't just erase the pixels before and after current position
      // because of the non-linear bird movement (it would leave 'dirty' pixels)
      tmpy = BIRDH - 1;
      do {
        _drawPixel(px, bird.old_y + tmpy, BCKGRDCOL);
      } while (tmpy--);
      // draw bird sprite at new position
      tmpy = BIRDH - 1;
      do {
        _drawPixel(px, bird.y + tmpy, birdcol[tmpx + (tmpy * BIRDW)]);
      } while (tmpy--);
    } while (tmpx--);
    // save position to erase bird on next draw
    bird.old_y = bird.y;
    // grass stripes
    // ---------------
    grassx -= SPEED;
    if (grassx < 0)
      grassx = TFTW;
    tft.drawFastVLine(grassx % TFTW, GAMEH + 1, GRASSH - 1, GRASSCOL);
    tft.drawFastVLine((grassx + 64) % TFTW, GAMEH + 1, GRASSH - 1, GRASSCOL2);
    // ===============
    // collision
    // ===============
    // if the bird hit the ground game over
    if (bird.y > GAMEH - BIRDH)
      break;
    // checking for bird collision with pipe
    if (bird.x + BIRDW >= pipes.x - BIRDW2 && bird.x <= pipes.x + PIPEW - BIRDW) {
      // bird entered a pipe, check for collision
      if (bird.y < pipes.gap_y || bird.y + BIRDH > pipes.gap_y + GAPHEIGHT)
        break;
      else
        passed_pipe = true;
    }
    // if bird has passed the pipe increase score
    else if (bird.x > pipes.x + PIPEW - BIRDW && passed_pipe) {
      passed_pipe = false;
      // erase score with background color
      tft.setTextColor(BCKGRDCOL);
      tft.setCursor(TFTW2, 4);
      tft.print(score);
      // set text color back to white for new score
      tft.setTextColor(TFT_WHITE);
      // increase score since we successfully passed a pipe
      score++;
    }
    // update score
    // ---------------
    tft.setCursor(TFTW2, 4);
    tft.print(score);
  }
  // add a small delay to show how the player lost
  Screen = 7;
  Screenchange = 1;
  delay(1200);
}

Supporting Files

You can download all the necessary files from the Circuit Digest GitHub repo, from the following link

Code
#include <SPI.h>
#include <TFT_eSPI.h>  // Hardware-specific library
#include <ESP32Time.h>
#include "driver/gpio.h"
#include "esp_sleep.h"
#include <EEPROM.h>
#include "OneButton.h"
#include <QMC5883L.h>
#include <BH1750.h>  //BH1750 Library
#include "Free_Fonts.h"
#include "MAX30105.h"   // SparkFun librarry for MAX30102 sensor
#include "heartRate.h"  // Heartrate measurement algorithm
#include "dial240.h"    //Image data
#include "fonts.h"
#include "images.h"
#define PIN_INPUT 0
#define EEPROM_SIZE 25
#define FONT_SMALL NotoSansBold15
#define FONT_LARGE NotoSansBold36
#define TFT_GREY 0x5AEB
#define TFT_SKYBLUE 0x067D
#define color1 TFT_WHITE
#define color2 0x8410  //0x8410
#define color3 0x5ACB
#define color4 0x15B3
#define color5 0x00A3
#define colour6 0x0926
#define colour7 TFT_BLACK
#define Light_Green 0x07E8
#define background 0xB635
#define LCD_BACKLIGHT 4
#define TFTW 240          // screen width
#define TFTH 280          // screen height
#define TFTW2 (TFTW / 2)  // half screen width
#define TFTH2 (TFTH / 2)  // half screen height
#define SPEED 1
#define GRAVITY 9.8
#define JUMP_FORCE 2.15
#define SKIP_TICKS 20.0  // 1000 / 50fps
#define MAX_FRAMESKIP 5
#define BIRDW 16      // bird width
#define BIRDH 16      // bird height
#define BIRDW2 8      // half width
#define BIRDH2 8      // half height
#define PIPEW 24      // pipe width
#define GAPHEIGHT 42  // pipe gap height
#define FLOORH 30     // floor height (from bottom of the screen)
#define GRASSH 4      // grass height (inside floor, starts at floor y)
#define COLOR565(r, g, b) ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
#define BCKGRDCOL COLOR565(138, 235, 244)    // background
#define BIRDCOL COLOR565(255, 254, 174)      // bird
#define PIPECOL COLOR565(99, 255, 78)        // pipe
#define PIPEHIGHCOL COLOR565(250, 255, 250)  // pipe highlight
#define PIPESEAMCOL COLOR565(0, 0, 0)        // pipe seam
#define FLOORCOL COLOR565(246, 240, 163)     // floor
#define GRASSCOL COLOR565(141, 225, 87)      // grass (col2 is the stripe color)
#define GRASSCOL2 COLOR565(156, 239, 88)     // grass (col2 is the stripe color)
#define C0 BCKGRDCOL                         // bird sprite,bird sprite colors (Cx name for values to keep the array readable)
#define C1 COLOR565(195, 165, 75)
#define C2 BIRDCOL
#define C3 TFT_WHITE
#define C4 TFT_RED
#define C5 COLOR565(251, 216, 114)

ESP32Time rtc(0);   // RTC instance with offset in seconds
BH1750 lightMeter;  //BH1750 Instance
QMC5883L compass;
MAX30105 particleSensor;  //MAX30102 instance
OneButton button(PIN_INPUT, true);
TFT_eSPI tft = TFT_eSPI();  // Invoke custom library
TFT_eSprite img = TFT_eSprite(&tft);
TFT_eSprite img1 = TFT_eSprite(&tft);
TFT_eSprite img2 = TFT_eSprite(&tft);



static const unsigned int birdcol[] = {
  C0, C0, C1, C1, C1, C1, C1, C0, C0, C0, C1, C1, C1, C1, C1, C0,
  C0, C1, C2, C2, C2, C1, C3, C1, C0, C1, C2, C2, C2, C1, C3, C1,
  C0, C2, C2, C2, C2, C1, C3, C1, C0, C2, C2, C2, C2, C1, C3, C1,
  C1, C1, C1, C2, C2, C3, C1, C1, C1, C1, C1, C2, C2, C3, C1, C1,
  C1, C2, C2, C2, C2, C2, C4, C4, C1, C2, C2, C2, C2, C2, C4, C4,
  C1, C2, C2, C2, C1, C5, C4, C0, C1, C2, C2, C2, C1, C5, C4, C0,
  C0, C1, C2, C1, C5, C5, C5, C0, C0, C1, C2, C1, C5, C5, C5, C0,
  C0, C0, C1, C5, C5, C5, C0, C0, C0, C0, C1, C5, C5, C5, C0, C0
};
// bird structure
static struct BIRD {
  long x, y, old_y;
  long col;
  float vel_y;
} bird;
// pipe structure
static struct PIPES {
  long x, gap_y;
  long col;
} pipes;
// score
int score;
// temporary x and y var
static short tmpx, tmpy;
// ---------------
// draw pixel
// ---------------
// faster drawPixel method by inlining calls and using setAddrWindow and pushColor using macro to force inlining
#define _drawPixel(a, b, c) \
  tft.setAddrWindow(a, b, a, b); \
  tft.pushColor(c)

uint maxScore = 0;
float sx = 0, sy = 1, mx = 1, my = 0, hx = -1, hy = 0;  // Saved H, M, S x & y multipliers
float sdeg = 0, mdeg = 0, hdeg = 0;
uint16_t osx = 120, osy = 140, omx = 120, omy = 140, ohx = 120, ohy = 140;  // Saved H, M, S x & y coords
uint16_t x0 = 0, x1 = 0, yy0 = 0, yy1 = 0;
uint32_t targetTime = 0;                       // for next 1 second timeout
static uint8_t conv2d(const char* p);          // Forward declaration needed for IDE 1.6.x
uint8_t hh = 0, t_mm = 0, t_dd = 0, t_mn = 0;  //
uint32_t t_yr = 0;
uint8_t t_hh = 0, mm = 0, ss = 0;
unsigned long lastfacechange = 0;
unsigned long lastwake = 0;
unsigned long lastpressed = 0;
unsigned long lastvaluechange = 0;
bool initial = 1;
volatile int counter = 0;
float VALUE;
float lastValue = 0;
int lastsec = 0;
int pressstate = 0;
unsigned long lastDisplayUpdate = 0;
const byte RATE_SIZE = 4;  //Increase this for more averaging. 4 is good.
byte rates[RATE_SIZE];     //Array of heart rates
byte rateSpot = 0;
long lastBeat = 0;  //Time at which the last beat occurred
float beatsPerMinute;
int beatAvg;
bool beat = false;
double rad = 0.01745;
float x[360];
float y[360];
bool facechange = false;
bool Screenchange = false;
float px[360];
float py[360];
float lx[360];
float ly[360];
int r = 104;
int ssx = 120;
int ssy = 140;
String cc[12] = { "45", "40", "35", "30", "25", "20", "15", "10", "05", "0", "55", "50" };
String days[] = { "SUNDAY", "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY" };
String days1[] = { "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" };
int start[12];
int startP[60];
const int pwmFreq = 5000;
const int pwmResolution = 8;
const int pwmLedChannelTFT = 0;
int angle = 0;
bool onOff = 0;
bool debounce = 0;
int watchface = 0, Screen = 0, SubScreen = 0, Autoscreen, AutoBright, AutoscreenTime, Brigtnesslevel;
String h, m, s, d1, d2, m1, m2;
unsigned long pressStartTime;


// This function is called from the interrupt when the signal on the PIN_INPUT has changed.
// do not use Serial in here.
void IRAM_ATTR checkTicks() {
  // include all buttons here to be checked
  button.tick();  // just call tick() to check the state.
}


// this function will be called for short click.
void ShortClick() {
  Serial.println("singleClick() detected.");
  lastwake = millis();
  if (Screen == 0) {
    SubScreen++;
    if (SubScreen > 2) {
      SubScreen = 0;
      facechange = true;
    }
    if (SubScreen == 1) {
      particleSensor.wakeUp();
    } else {
      particleSensor.shutDown();
    }
  } else if (Screen == 1) {
    SubScreen++;
    if (SubScreen > 4) {
      SubScreen = 0;
    }
    Screenchange = true;
  } else if (Screen == 2) {
    watchface++;
    if (watchface > 5) {
      watchface = 0;
    }

    EEPROM.write(0, watchface);
    EEPROM.commit();
    facechange = true;
    Screenchange = true;
  } else if (Screen == 3) {
    SubScreen++;
    if (SubScreen > 5) {
      SubScreen = 0;
    }
    Screenchange = true;
  } else if (Screen == 4) {
    SubScreen++;
    if (SubScreen > 2) {
      SubScreen = 0;
    }
    Screenchange = true;
  } else if (Screen == 5) {
    Screen = 6;
    game_init();
    game_loop();
  } else if (Screen == 6) {
    Screen = 7;
  } else if (Screen == 7) {
    Screen = 5;
    Screenchange = true;
  }

  Serial.print("Sub ");
  Serial.println(SubScreen);
  tft.fillScreen(colour7);
  pressstate = 1;
  facechange = true;
  lastDisplayUpdate = millis();
  lastpressed = millis();
}  // ShortClick


// long press
void LongPress() {
  Serial.println("pressStart()");
  pressStartTime = millis() - 1000;  // as set in setPressTicks()
  lastwake = millis();
  lastDisplayUpdate = millis();
  particleSensor.shutDown();
  if (Screen == 0) {
    Screen = 1;
    SubScreen = 0;
  } else if (Screen == 1) {
    if (SubScreen == 0) {
      Screen = 2;
      SubScreen = 0;
    } else if (SubScreen == 1) {
      Screen = 3;
      t_hh = rtc.getHour();
      t_mm = rtc.getMinute();
      t_dd = rtc.getDay();
      t_mn = rtc.getMonth();
      t_yr = rtc.getYear();
      Serial.println(rtc.getYear());
      Serial.println(t_yr);
      SubScreen = 0;
    } else if (SubScreen == 2) {
      Screen = 4;
      SubScreen = 0;
      facechange = true;
      Screenchange = true;
    } else if (SubScreen == 3) {
      Screen = 5;
      SubScreen = 0;
    } else if (SubScreen == 4) {
      Screen = 0;
      SubScreen = 0;
    }
  } else if (Screen == 2) {
    Screen = 1;
    SubScreen = 0;
  } else if (Screen == 3) {
    if (SubScreen == 0) {
      t_hh++;
      if (t_hh > 23) {
        t_hh = 0;
      }
    } else if (SubScreen == 1) {
      t_mm++;
      if (t_mm > 59) {
        t_mm = 0;
      }
    } else if (SubScreen == 2) {
      t_dd++;
      if (t_dd > 31) {
        t_dd = 0;
      }
    } else if (SubScreen == 3) {
      t_mn++;
      if (t_mn > 12) {
        t_mn = 0;
      }
    } else if (SubScreen == 4) {
      t_yr++;
      if (t_yr > 2041) {
        t_yr = 0;
      }
    } else {
      rtc.setTime(0, t_mm, t_hh, t_dd, t_mn, t_yr);
      Screen = 1;
      SubScreen = 1;
    }
  } else if (Screen == 4) {
    if (SubScreen == 0) {
      AutoBright++;
      if (AutoBright > 5) {
        AutoBright = 0;
      }
      if (AutoBright > 0) {
        analogWrite(LCD_BACKLIGHT, AutoBright * 50);
      }
      EEPROM.write(2, AutoBright);
      EEPROM.commit();
    } else if (SubScreen == 1) {
      Autoscreen++;
      if (Autoscreen > 5) {
        Autoscreen = 0;
      }
      EEPROM.write(1, Autoscreen);
      EEPROM.commit();
    } else if (SubScreen == 2) {
      Screen = 1;
      SubScreen = 2;
    }
  } else if (Screen == 5) {
    Screen = 1;
    SubScreen = 0;
  } else if (Screen == 7) {
    Screen = 1;
    SubScreen = 0;
  }

  facechange = true;
  Screenchange = true;
  pressstate = 1;
  lastpressed = millis();
}


void setup(void) {
  Serial.begin(115200);
  Serial.println("ESP32 Watch OS.");
  gpio_hold_dis((gpio_num_t)LCD_BACKLIGHT);
  pinMode(LCD_BACKLIGHT, OUTPUT);
  digitalWrite(LCD_BACKLIGHT, LOW);
  EEPROM.begin(EEPROM_SIZE);
  EEPROM.writeInt(10, 0);
  EEPROM.commit();
  if (EEPROM.read(0) > 3) {
    EEPROM.write(0, 4);
    EEPROM.commit();
  }
  watchface = EEPROM.read(0);

  if (EEPROM.read(1) > 5) {
    EEPROM.write(1, 5);
    EEPROM.commit();
  }
  Autoscreen = EEPROM.read(1);

  if (EEPROM.read(2) > 5) {
    EEPROM.write(2, 5);
    EEPROM.commit();
  }
  AutoBright = EEPROM.read(2);
  //rtc.setTime(ss, mm, hh, 0, 0, 0);  // 26th Jjuly 2022 compile date

  particleSensor.begin(Wire, I2C_SPEED_FAST);

  particleSensor.setup();                     //Configure sensor with default settings
  particleSensor.setPulseAmplitudeRed(0x0A);  //Turn Red LED to low to indicate sensor is running
  particleSensor.setPulseAmplitudeIR(0xFF);   //Turn Red LED to low to indicate sensor is running
  particleSensor.setPulseAmplitudeGreen(0);   //Turn off Green LED
  particleSensor.shutDown();
  compass.init();
  compass.setSamplingRate(50);

  tft.init();
  tft.setRotation(0);
  //tft.setColorDepth(16);
  tft.setSwapBytes(true);
  tft.fillScreen(colour7);
  int xw = tft.width() / 2;  // xw, yh is middle of screen
  int yh = tft.height() / 2;
  tft.setPivot(xw, yh);  // Set pivot to middle of TFT screen
  img.createSprite(240, 280);
  img.setTextDatum(4);
  img1.createSprite(240, 70);
  img1.setSwapBytes(true);
  img2.createSprite(240, 70);
  img2.setSwapBytes(true);
  targetTime = millis() + 1000;
  facechange = true;
  esp_sleep_enable_ext0_wakeup(GPIO_NUM_0, 0);  //1 = High, 0 = Low
  // setup interrupt routine
  // when not registering to the interrupt the sketch also works when the tick is called frequently.
  attachInterrupt(digitalPinToInterrupt(PIN_INPUT), checkTicks, CHANGE);

  // link the xxxclick functions to be called on xxxclick event.
  button.attachClick(ShortClick);

  button.setPressTicks(1000);  // that is the time when LongPressStart is called
  button.attachLongPressStart(LongPress);
  lightMeter.begin(BH1750::CONTINUOUS_HIGH_RES_MODE);  //Init BH1750 library
  if (AutoBright == 0) {
    unsigned int lv = constrain(lightMeter.readLightLevel(), 50, 500);
    analogWrite(LCD_BACKLIGHT, lv / 2);
  } else {
    analogWrite(LCD_BACKLIGHT, AutoBright * 50);
  }

  lastwake = millis();
}
int lastAngle = 0;
float circle = 100;
bool dir = 0;
int rAngle = 359;


void loop() {
  button.tick();
  if (Screen < 5) {
    watchtask();
  } else {
    game();
  }
}



void watchtask() {

  if (pressstate == 1 && digitalRead(0) == 1) {
    pressstate = 0;
  }
  if (AutoBright == 0) {
    unsigned int lv = constrain(lightMeter.readLightLevel(), 50, 500);
    analogWrite(LCD_BACKLIGHT, lv / 2);
    Serial.print("Light");
    Serial.println(lv / 2);
  }
  if (Autoscreen != 0 && millis() - lastwake > Autoscreen * 60000) {
    analogWrite(LCD_BACKLIGHT, 0);
    delay(1000);

    tft.fillScreen(colour7);
    gpio_deep_sleep_hold_en();
    gpio_hold_en((gpio_num_t)LCD_BACKLIGHT);
    esp_deep_sleep_start();
  }
  if (Screen == 0) {
    if (SubScreen == 0) {
      watchfacedsp();
    } else if (SubScreen == 1) {
      HRApp();
    } else {
      CompassApp();
    }
  } else if (Screen == 1 && Screenchange == true) {
    if (SubScreen == 0) {
      tft.pushImage(0, 0, 240, 280, facechangeicon);
    } else if (SubScreen == 1) {
      tft.pushImage(0, 0, 240, 280, timeseticon);
    } else if (SubScreen == 2) {
      tft.pushImage(0, 0, 240, 280, settingsicon);
    } else if (SubScreen == 3) {
      tft.pushImage(0, 0, 240, 280, gamesicon);
    } else if (SubScreen == 4) {
      tft.pushImage(0, 0, 240, 280, exiticon);
    }
  } else if (Screen == 3 && Screenchange == true) {

    timesetiings();
    Screenchange = false;
  } else if (Screen == 2 && Screenchange == true) {
    watchfacedsp();
    Screenchange = false;
  } else if (Screen == 4 && Screenchange == true) {
    settings();
    Screenchange = false;
  } else if (Screen == 3 && millis() - lastpressed > 2000 && millis() - lastvaluechange > 500 && pressstate == 1) {
    if (SubScreen == 0) {
      t_hh++;
      if (t_hh > 23) {
        t_hh = 0;
      }
      facechange = true;
      Screenchange = true;
    } else if (SubScreen == 1) {
      t_mm++;
      if (t_mm > 59) {
        t_mm = 0;
      }
      facechange = true;
      Screenchange = true;
    } else if (SubScreen == 2) {
      facechange = true;
      Screenchange = true;
      t_dd++;
      if (t_dd > 31) {
        t_dd = 0;
      }
    } else if (SubScreen == 3) {
      t_mn++;
      if (t_mn > 12) {
        t_mn = 0;
      }
      facechange = true;
      Screenchange = true;
    } else if (SubScreen == 4) {
      t_yr++;
      if (t_yr > 2041) {
        t_yr = 0;
      }
      facechange = true;
      Screenchange = true;
    }
    lastvaluechange = millis();
  }
}



void timesetiings() {
  tft.pushImage(0, 0, 240, 280, TimeSettings);
  tft.setTextColor(TFT_BLACK, TFT_WHITE);
  tft.setFreeFont(FF18);
  tft.setTextSize(0);
  int tt_hh = t_hh;
  if (tt_hh > 12) {
    tt_hh = tt_hh - 12;
  }
  if (tt_hh < 10) {
    tft.drawString("0" + String(tt_hh), 25, 96);
  } else {
    tft.drawString(String(tt_hh), 25, 96);
  }

  if (t_mm < 10) {
    tft.drawString("0" + String(t_mm), 93, 96);
  } else {
    tft.drawString(String(t_mm), 93, 96);
  }
  if (t_hh < 13) {
    tft.drawString("AM", 164, 96);
  } else {
    tft.drawString("PM", 164, 96);
  }
  if (t_dd < 10) {
    tft.drawString("0" + String(t_dd), 25, 183);
  } else {
    tft.drawString(String(t_dd), 25, 183);
  }
  if (t_mn < 10) {
    tft.drawString("0" + String(t_mn), 93, 183);
  } else {
    tft.drawString(String(t_mn), 93, 183);
  }
  if (t_yr < 2022) {
    t_yr = 2022;
  }
  tft.drawString(String(t_yr), 161, 183);
  if (SubScreen == 0) {
    tft.drawRoundRect(9, 81, 58, 48, 6, Light_Green);
  } else if (SubScreen == 1) {
    tft.drawRoundRect(77, 81, 58, 48, 6, Light_Green);
  } else if (SubScreen == 2) {
    tft.drawRoundRect(9, 168, 58, 48, 6, Light_Green);
  } else if (SubScreen == 3) {
    tft.drawRoundRect(77, 168, 58, 48, 6, Light_Green);
  } else if (SubScreen == 4) {
    tft.drawRoundRect(144, 168, 88, 48, 6, Light_Green);
  } else {
    tft.drawRoundRect(79, 226, 88, 48, 6, Light_Green);
  }
}



void settings() {
  tft.pushImage(0, 0, 240, 280, Settingspage);
  tft.setTextColor(TFT_BLACK, TFT_WHITE);
  tft.setFreeFont(FF18);
  tft.setTextSize(0);
  switch (AutoBright) {
    case 0: tft.drawString("  Auto  ", 80, 100); break;
    case 1: tft.drawString("  20%   ", 85, 100); break;
    case 2: tft.drawString("  40%   ", 85, 100); break;
    case 3: tft.drawString("  60%   ", 85, 100); break;
    case 4: tft.drawString("  80%   ", 85, 100); break;
    case 5: tft.drawString(" 100%   ", 80, 100); break;
  }
  switch (Autoscreen) {
    case 0: tft.drawString("Always On", 65, 185); break;
    case 1: tft.drawString("1 Minute ", 75, 185); break;
    case 2: tft.drawString("2 Minute ", 75, 185); break;
    case 3: tft.drawString("3 Minute ", 75, 185); break;
    case 4: tft.drawString("4 Minute ", 75, 185); break;
    case 5: tft.drawString("5 Minute ", 75, 185); break;
  }

  if (SubScreen == 0) {
    tft.drawRoundRect(16, 85, 208, 48, 6, Light_Green);
  } else if (SubScreen == 1) {
    tft.drawRoundRect(16, 169, 208, 48, 6, Light_Green);
  } else {
    tft.drawRoundRect(66, 225, 108, 48, 6, Light_Green);
  }
}


void HRApp() {
  long irValue = particleSensor.getIR();  //Reading the IR value it will permit us to know if there's a finger on the sensor or not
  //Also detecting a heartbeat
  if (checkForBeat(irValue) == true)  //If a heart beat is detected
  {

    long delta = millis() - lastBeat;  //Measure duration between two beats
    lastBeat = millis();

    beatsPerMinute = 60 / (delta / 1000.0);  //Calculating the BPM

    if (beatsPerMinute < 255 && beatsPerMinute > 20)  //To calculate the average we strore some values (4) then do some math to calculate the average
    {
      rates[rateSpot++] = (byte)beatsPerMinute;  //Store this reading in the array
      rateSpot %= RATE_SIZE;                     //Wrap variable

      //Take average of readings
      beatAvg = 0;
      for (byte x = 0; x < RATE_SIZE; x++)
        beatAvg += rates[x];
      beatAvg /= RATE_SIZE;
    }
  }
  if (millis() - lastDisplayUpdate > 500) {
    if (irValue < 60000) {  //If no finger is detected it inform the user and put the average BPM to 0 or it will be stored for the next measure
      beatAvg = 0;
      img.loadFont(FONT_SMALL);
      img.setCursor(80, 120);
      img.setTextColor(TFT_CYAN, colour7);
      img.fillSprite(colour7);
      img.println("Please Place ");
      img.setCursor(80, 140);
      img.println("your finger ");
      img.pushSprite(0, 0);
    } else {
      img.fillSprite(colour7);  //Clear the display
      if (beat == true) {
        img.pushImage(0, 0, 240, 280, hr1);
      } else {
        img.pushImage(0, 0, 240, 280, hr2);
      }
      beat = !beat;

      img.setTextColor(TFT_CYAN, colour7);
      img.loadFont(FONT_LARGE);
      img.setCursor(100, 130);
      img.print(beatAvg);
      img.setCursor(100, 175);
      img.loadFont(FONT_SMALL);
      img.print("BPM ");
      img.pushRotated(0);
    }
    lastDisplayUpdate = millis();
  }
  Serial.print("IR=");
  Serial.print(irValue);
  Serial.print(", BPM=");
  Serial.print(beatsPerMinute);
  Serial.print(", Avg BPM=");
  Serial.print(beatAvg);

  if (irValue < 60000)
    Serial.print(" No finger?");
  if (millis() - lastBeat > 5000) {
    beatsPerMinute = 0;
    beatAvg = 0;
  }
  Serial.println();
}


void CompassApp() {
  for (int i = 0; i < 10; i++) {
    angle = angle + compass.readHeading();
  }
  angle = random(355, 360);
  angle = angle / 10;
  img.fillSprite(colour7);
  img.pushImage(0, 20, 240, 240, dial240);
  img.pushRotated(angle);  // create rotated image as per the angle from the compass sensor
  angle = 0;
}
void watchfacedsp() {
  if (facechange) {
    tft.fillScreen(colour7);
    if (watchface == 1) {
      tft.setTextSize(0);
      tft.pushImage(0, 0, 240, 280, Casio2);
      tft.setTextColor(0x0081, background);
      tft.fillRoundRect(48, 127, 128, 48, 5, background);
    } else if (watchface == 2) {
      tft.setTextSize(0);
      tft.pushImage(0, 0, 240, 280, Casio1);
      tft.setTextColor(0x0081, background);
      tft.fillRoundRect(48, 127, 128, 48, 5, background);
    } else if (watchface == 4) {
      tft.pushImage(0, 0, 240, 280, cdface1);
      img2.pushImage(0, 0, 240, 100, cdface11);
      tft.setTextColor(TFT_WHITE);
      tft.setFreeFont(FF18);
      tft.setTextSize(2);
    } else if (watchface == 5) {
      tft.pushImage(0, 0, 240, 280, cdface2);
      img2.pushImage(0, 0, 240, 100, cdface12);
      tft.setTextColor(TFT_WHITE);
      tft.setFreeFont(FF18);
      tft.setTextSize(2);
    }
    facechange = false;
    lastfacechange = millis();
  }
  if (watchface == 0) {
    int b = 0;
    int b2 = 0;
    for (int i = 0; i < 360; i++) {
      x[i] = (r * cos(rad * i)) + ssx;
      y[i] = (r * sin(rad * i)) + ssy;
      px[i] = ((r - 16) * cos(rad * i)) + ssx;
      py[i] = ((r - 16) * sin(rad * i)) + ssy;

      lx[i] = ((r - 26) * cos(rad * i)) + ssx;
      ly[i] = ((r - 26) * sin(rad * i)) + ssy;

      if (i % 30 == 0) {
        start[b] = i;
        b++;
      }

      if (i % 6 == 0) {
        startP[b2] = i;
        b2++;
      }
    }

    rAngle = rAngle - 2;

    angle = rtc.getSecond() * 6;

    s = String(rtc.getSecond());
    m = String(rtc.getMinute());
    h = String(rtc.getHour());

    if (m.toInt() < 10)
      m = "0" + m;

    if (h.toInt() < 10)
      h = "0" + h;

    if (s.toInt() < 10)
      s = "0" + s;


    if (rtc.getDay() > 10) {
      d1 = rtc.getDay() / 10;
      d2 = rtc.getDay() % 10;
    } else {
      d1 = "0";
      d2 = String(rtc.getDay());
    }

    if (rtc.getMonth() > 10) {
      m1 = rtc.getMonth() / 10;
      m2 = rtc.getMonth() % 10;
    } else {
      m1 = "0";
      m2 = String(rtc.getMonth());
    }


    if (angle >= 360)
      angle = 0;

    if (rAngle <= 0)
      rAngle = 359;



    if (dir == 0)
      circle = circle + 0.5;
    else
      circle = circle - 0.5;

    if (circle > 140)
      dir = !dir;

    if (circle < 100)
      dir = !dir;



    if (angle > -1) {
      lastAngle = angle;

      VALUE = ((angle - 270) / 3.60) * -1;
      if (VALUE < 0)
        VALUE = VALUE + 100;



      img.fillSprite(colour7);
      img.fillCircle(ssx, ssy, 124, colour7);

      img.setTextColor(TFT_WHITE, colour7);

      img.drawString(days[rtc.getDayofWeek()], circle, 140, 2);


      for (int i = 0; i < 12; i++)
        if (start[i] + angle < 360) {
          img.drawString(cc[i], x[start[i] + angle], y[start[i] + angle], 2);
          img.drawLine(px[start[i] + angle], py[start[i] + angle], lx[start[i] + angle], ly[start[i] + angle], color1);
        } else {
          img.drawString(cc[i], x[(start[i] + angle) - 360], y[(start[i] + angle) - 360], 2);
          img.drawLine(px[(start[i] + angle) - 360], py[(start[i] + angle) - 360], lx[(start[i] + angle) - 360], ly[(start[i] + angle) - 360], color1);
        }
      img.setFreeFont(&DSEG7_Modern_Bold_20);
      img.drawString(s, ssx, ssy - 36);
      img.setFreeFont(&DSEG7_Classic_Regular_28);
      img.drawString(h + ":" + m, ssx, ssy + 28);
      img.setTextFont(0);

      img.fillRect(70, 86, 12, 20, color3);
      img.fillRect(84, 86, 12, 20, color3);
      img.fillRect(150, 86, 12, 20, color3);
      img.fillRect(164, 86, 12, 20, color3);

      img.setTextColor(0x35D7, colour7);
      img.drawString("MONTH", 84, 78);
      img.drawString("DAY", 162, 78);
      img.setTextColor(TFT_SKYBLUE, colour7);
      img.drawString("Circuit Digest", 120, 194);
      img.drawString("***", 120, 124);
      img.setTextColor(TFT_WHITE, color3);
      img.drawString(m1, 77, 96, 2);
      img.drawString(m2, 91, 96, 2);
      img.drawString(d1, 157, 96, 2);
      img.drawString(d2, 171, 96, 2);
      for (int i = 0; i < 60; i++)
        if (startP[i] + angle < 360)
          img.fillCircle(px[startP[i] + angle], py[startP[i] + angle], 1, color1);
        else
          img.fillCircle(px[(startP[i] + angle) - 360], py[(startP[i] + angle) - 360], 1, color1);
      img.fillTriangle(ssx - 1, ssy - 70, ssx - 5, ssy - 56, ssx + 4, ssy - 56, TFT_ORANGE);
      img.fillCircle(px[rAngle], py[rAngle], 6, TFT_RED);
      img.pushSprite(0, 0);
    }
  } else if (rtc.getSecond() != lastsec || Screen == 3) {
    if (watchface == 1 || watchface == 2) {
      /*
      String med;
      if (rtc.getSecond() % 2) {
        med = ":";
      } else {
        med = " ";
      }
      */
      tft.setFreeFont(&DSEG7_Classic_Bold_30);
      if (rtc.getHour() > 9 && rtc.getMinute() > 9) {
        tft.drawString(String(rtc.getHour()) + ":" + String(rtc.getMinute()), 46, 135);
      } else if (rtc.getHour() < 10 && rtc.getMinute() > 9) {

        tft.drawString("0" + String(rtc.getHour()) + ":" + String(rtc.getMinute()), 46, 135);
      } else if (rtc.getHour() > 9 && rtc.getMinute() < 10) {

        tft.drawString(String(rtc.getHour()) + ":0" + String(rtc.getMinute()), 46, 135);
      } else {

        tft.drawString("0" + String(rtc.getHour()) + ":0" + String(rtc.getMinute()), 46, 135);
      }
      tft.setFreeFont(&DSEG7_Classic_Bold_20);
      if (rtc.getSecond() < 10) {
        tft.drawString("0" + String(rtc.getSecond()), 154, 145);
      } else {
        tft.drawString(String(rtc.getSecond()), 154, 145);
      }
      tft.setFreeFont(&DSEG14_Classic_Bold_18);
      tft.drawString(days1[rtc.getDayofWeek()], 94, 106);
      tft.drawString(String(rtc.getDay()), 156, 106);
    } else if (watchface == 3) {
      img.setTextColor(TFT_WHITE, colour7);  // Adding a background colour erases previous text automatically
      // Draw clock face
      img.fillCircle(120, 140, 118, TFT_GREEN);
      img.fillCircle(120, 140, 110, colour7);
      // Draw 12 lines
      for (int i = 0; i < 360; i += 30) {
        sx = cos((i - 90) * 0.0174532925);
        sy = sin((i - 90) * 0.0174532925);
        x0 = sx * 114 + 120;
        yy0 = sy * 114 + 140;
        x1 = sx * 100 + 120;
        yy1 = sy * 100 + 140;
        img.drawLine(x0, yy0, x1, yy1, TFT_GREEN);
      }
      // Draw 60 dots
      for (int i = 0; i < 360; i += 6) {
        sx = cos((i - 90) * 0.0174532925);
        sy = sin((i - 90) * 0.0174532925);
        x0 = sx * 102 + 120;
        yy0 = sy * 102 + 140;
        // Draw minute markers
        img.drawPixel(x0, yy0, TFT_WHITE);
        // Draw main quadrant dots
        if (i == 0 || i == 180) img.fillCircle(x0, yy0, 2, TFT_WHITE);
        if (i == 90 || i == 270) img.fillCircle(x0, yy0, 2, TFT_WHITE);
      }
      img.fillCircle(120, 141, 3, TFT_WHITE);
      // Pre-compute hand degrees, x & y coords for a fast screen update
      sdeg = rtc.getSecond() * 6;                      // 0-59 -> 0-354
      mdeg = rtc.getMinute() * 6 + sdeg * 0.01666667;  // 0-59 -> 0-360 - includes seconds
      hdeg = rtc.getHour() * 30 + mdeg * 0.0833333;    // 0-11 -> 0-360 - includes minutes and seconds
      hx = cos((hdeg - 90) * 0.0174532925);
      hy = sin((hdeg - 90) * 0.0174532925);
      mx = cos((mdeg - 90) * 0.0174532925);
      my = sin((mdeg - 90) * 0.0174532925);
      sx = cos((sdeg - 90) * 0.0174532925);
      sy = sin((sdeg - 90) * 0.0174532925);
      if (rtc.getSecond() == 0 || initial) {
        initial = 0;
        // Erase hour and minute hand positions every minute
        img.drawLine(ohx, ohy, 120, 141, colour7);
        ohx = hx * 62 + 121;
        ohy = hy * 62 + 141;
        img.drawLine(omx, omy, 120, 141, colour7);
        omx = mx * 84 + 120;
        omy = my * 84 + 141;
      }
      // Redraw new hand positions, hour and minute hands not erased here to avoid flicker
      img.drawLine(osx, osy, 120, 141, colour7);
      osx = sx * 90 + 121;
      osy = sy * 90 + 141;
      img.drawLine(osx, osy, 120, 141, TFT_RED);
      img.drawLine(ohx, ohy, 120, 141, TFT_WHITE);
      img.drawLine(omx, omy, 120, 141, TFT_WHITE);
      img.drawLine(osx, osy, 120, 141, TFT_RED);
      img.fillCircle(120, 141, 3, TFT_RED);
      img.pushSprite(0, 0);
    } else if (watchface == 4 || watchface == 5) {
      img1.setTextColor(TFT_WHITE, TFT_BLACK);
      img1.setFreeFont(FF24);
      img1.fillSprite(TFT_BLACK);
      //img1.setTextSize(2);

      if (rtc.getHour() > 9 && rtc.getMinute() > 9) {
        img1.drawString(String(rtc.getHour()) + ":" + String(rtc.getMinute()), 66, 30);
      } else if (rtc.getHour() < 10 && rtc.getMinute() > 9) {

        img1.drawString("0" + String(rtc.getHour()) + ":" + String(rtc.getMinute()), 66, 30);
      } else if (rtc.getHour() > 9 && rtc.getMinute() < 10) {

        img1.drawString(String(rtc.getHour()) + ":0" + String(rtc.getMinute()), 66, 30);
      } else {

        img1.drawString("0" + String(rtc.getHour()) + +":0" + String(rtc.getMinute()), 66, 30);
      }
      img1.setFreeFont(FF22);
      if (rtc.getSecond() < 10) {
        img1.drawString("0" + String(rtc.getSecond()), 190, 40);
      } else {
        img1.drawString(String(rtc.getSecond()), 190, 40);
      }
      img1.drawString(days1[rtc.getDayofWeek()] + " " + String(rtc.getDay()) + " " + String(rtc.getYear()), 54, 0);
      //img1.drawString(String(rtc.getDay()), 156, 0);
      img2.pushSprite(0, 180);
      img1.pushSprite(0, 180, TFT_BLACK);
    }
    lastsec = rtc.getSecond();
  }
}

static uint8_t conv2d(const char* p) {
  uint8_t v = 0;
  if ('0' <= *p && *p <= '9')
    v = *p - '0';
  return 10 * v + *++p - '0';
}

void game() {
  if (Screen == 5 && Screenchange == 1) {
    Screenchange = 0;
    tft.fillScreen(TFT_BLACK);
    tft.fillRect(10, TFTH2 - 20, TFTW - 20, 1, TFT_WHITE);
    tft.fillRect(10, TFTH2 + 32, TFTW - 20, 1, TFT_WHITE);
    tft.setTextColor(TFT_WHITE);
    tft.setFreeFont(0);
    tft.setTextSize(2);
    // half width - num char * char width in pixels
    tft.setCursor(TFTW2 - (6 * 9), TFTH2 - 16);
    tft.println("FLAPPY");
    tft.setTextSize(2);
    tft.setCursor(TFTW2 - (6 * 9), TFTH2 + 8);
    tft.println("-BIRD-");
  } else if (Screen == 7 && Screenchange == 1) {
    Screenchange = 0;
    tft.fillScreen(TFT_BLACK);
    maxScore = EEPROM.readInt(10);

    if (score > maxScore) {
      EEPROM.writeInt(10, score);
      EEPROM.commit();
      maxScore = score;
      tft.setTextColor(TFT_RED);
      tft.setTextSize(2);
      tft.setCursor(TFTW2 - (13 * 6), TFTH2 - 26);
      tft.println("NEW HIGHSCORE");
    }
    tft.setTextColor(TFT_WHITE);
    tft.setTextSize(3);
    // half width - num char * char width in pixels
    tft.setCursor(TFTW2 - (9 * 9), TFTH2 - 6);
    tft.println("GAME OVER");
    tft.setTextSize(2);
    tft.setCursor(10, 10);
    tft.print("score: ");
    tft.print(score);
    tft.setCursor(TFTW2 - (12 * 6), TFTH2 + 18);
    tft.println("press button");
    tft.setCursor(10, 28);
    tft.print("Max Score:");
    tft.print(maxScore);
    tft.setTextSize(0);
    facechange = 1;
  }
}

void game_init() {
  // clear screen
  tft.fillScreen(BCKGRDCOL);
  // reset score
  score = 0;
  // init bird
  bird.x = 144;
  bird.y = bird.old_y = TFTH2 - BIRDH;
  bird.vel_y = -JUMP_FORCE;
  tmpx = tmpy = 0;
  // generate new random seed for the pipe gape
  randomSeed(analogRead(12));
  // init pipe
  pipes.x = 0;
  pipes.gap_y = random(20, TFTH - 60);
}

void game_loop() {

  // ===============
  // prepare game variables
  // draw floor
  // ===============
  // instead of calculating the distance of the floor from the screen height each time store it in a variable
  const unsigned char GAMEH = TFTH - FLOORH;
  // draw the floor once, we will not overwrite on this area in-game
  // black line
  tft.drawFastHLine(0, GAMEH, TFTW, TFT_BLACK);
  // grass and stripe
  tft.fillRect(0, GAMEH + 1, TFTW2, GRASSH, GRASSCOL);
  tft.fillRect(TFTW2, GAMEH + 1, TFTW2, GRASSH, GRASSCOL2);
  // black line
  tft.drawFastHLine(0, GAMEH + GRASSH, TFTW, TFT_BLACK);
  // mud
  tft.fillRect(0, GAMEH + GRASSH + 1, TFTW, FLOORH - GRASSH, FLOORCOL);
  // grass x position (for stripe animation)
  long grassx = TFTW;
  // game loop time variables
  double delta, old_time, next_game_tick, current_time;
  next_game_tick = current_time = millis();
  // passed pipe flag to count score
  bool passed_pipe = false;
  // temp var for setAddrWindow
  unsigned char px;
  while (true) {
    yield();

    int loops = 0;
    while (millis() > next_game_tick && loops < MAX_FRAMESKIP) {
      // ===============
      // input
      // ===============
      if (digitalRead(0) == LOW) {
        // if the bird is not too close to the top of the screen apply jump force
        if (bird.y > BIRDH2 * 0.5)
          bird.vel_y = -JUMP_FORCE;
        // else zero velocity
        else
          bird.vel_y = 0;
      }
      // ===============
      // update
      // ===============
      // calculate delta time
      // ---------------
      old_time = current_time;
      current_time = millis();
      delta = (current_time - old_time) / 1000;
      // bird
      // ---------------
      bird.vel_y += GRAVITY * delta;
      bird.y += bird.vel_y;
      // pipe
      // ---------------
      pipes.x -= SPEED;
      // if pipe reached edge of the screen reset its position and gap
      if (pipes.x < -PIPEW) {
        pipes.x = TFTW;
        pipes.gap_y = random(10, GAMEH - (10 + GAPHEIGHT));
      }
      // ---------------
      next_game_tick += SKIP_TICKS;
      loops++;
    }

    // ===============
    // draw
    // ===============
    // pipe
    // ---------------
    // we save cycles if we avoid drawing the pipe when outside the screen
    if (pipes.x >= 0 && pipes.x < TFTW) {
      // pipe color
      tft.drawFastVLine(pipes.x + 3, 0, pipes.gap_y, PIPECOL);
      tft.drawFastVLine(pipes.x + 3, pipes.gap_y + GAPHEIGHT + 1, GAMEH - (pipes.gap_y + GAPHEIGHT + 1), PIPECOL);
      // highlight
      tft.drawFastVLine(pipes.x, 0, pipes.gap_y, PIPEHIGHCOL);
      tft.drawFastVLine(pipes.x, pipes.gap_y + GAPHEIGHT + 1, GAMEH - (pipes.gap_y + GAPHEIGHT + 1), PIPEHIGHCOL);
      // bottom and top border of pipe
      _drawPixel(pipes.x, pipes.gap_y, PIPESEAMCOL);
      _drawPixel(pipes.x, pipes.gap_y + GAPHEIGHT, PIPESEAMCOL);
      // pipe seam
      _drawPixel(pipes.x, pipes.gap_y - 6, PIPESEAMCOL);
      _drawPixel(pipes.x, pipes.gap_y + GAPHEIGHT + 6, PIPESEAMCOL);
      _drawPixel(pipes.x + 3, pipes.gap_y - 6, PIPESEAMCOL);
      _drawPixel(pipes.x + 3, pipes.gap_y + GAPHEIGHT + 6, PIPESEAMCOL);
    }
    // erase behind pipe
    if (pipes.x <= TFTW)
      tft.drawFastVLine(pipes.x + PIPEW, 0, GAMEH, BCKGRDCOL);
    // bird
    // ---------------
    tmpx = BIRDW - 1;
    do {
      px = bird.x + tmpx + BIRDW;
      // clear bird at previous position stored in old_y
      // we can't just erase the pixels before and after current position
      // because of the non-linear bird movement (it would leave 'dirty' pixels)
      tmpy = BIRDH - 1;
      do {
        _drawPixel(px, bird.old_y + tmpy, BCKGRDCOL);
      } while (tmpy--);
      // draw bird sprite at new position
      tmpy = BIRDH - 1;
      do {
        _drawPixel(px, bird.y + tmpy, birdcol[tmpx + (tmpy * BIRDW)]);
      } while (tmpy--);
    } while (tmpx--);
    // save position to erase bird on next draw
    bird.old_y = bird.y;
    // grass stripes
    // ---------------
    grassx -= SPEED;
    if (grassx < 0)
      grassx = TFTW;

    tft.drawFastVLine(grassx % TFTW, GAMEH + 1, GRASSH - 1, GRASSCOL);
    tft.drawFastVLine((grassx + 64) % TFTW, GAMEH + 1, GRASSH - 1, GRASSCOL2);
    // ===============
    // collision
    // ===============
    // if the bird hit the ground game over
    if (bird.y > GAMEH - BIRDH)
      break;
    // checking for bird collision with pipe
    if (bird.x + BIRDW >= pipes.x - BIRDW2 && bird.x <= pipes.x + PIPEW - BIRDW) {
      // bird entered a pipe, check for collision
      if (bird.y < pipes.gap_y || bird.y + BIRDH > pipes.gap_y + GAPHEIGHT)
        break;
      else
        passed_pipe = true;
    }
    // if bird has passed the pipe increase score
    else if (bird.x > pipes.x + PIPEW - BIRDW && passed_pipe) {
      passed_pipe = false;
      // erase score with background color
      tft.setTextColor(BCKGRDCOL);
      tft.setCursor(TFTW2, 4);
      tft.print(score);
      // set text color back to white for new score
      tft.setTextColor(TFT_WHITE);
      // increase score since we successfully passed a pipe
      score++;
    }
    // update score
    // ---------------
    tft.setCursor(TFTW2, 4);
    tft.print(score);
  }
  // add a small delay to show how the player lost
  Screen = 7;
  Screenchange = 1;
  delay(1200);
}
Video

Have any question realated to this Article?

Ask Our Community Members

Comments