Introduction: Lilygo T-Watch 2020 Arduino Framework

About: Lifelong Maker and Educator.

Lilygo has put out a new version of it's development platform TWatch. It is the T-Watch 2020. It is based on an ESP32 chip with a touch screen, accelerometer, real time clock, power controller, and more. For people who have been waiting for a commercial grade watch that can be programmed, this is a dream come true. It is available at

I was super excited so I ordered one as soon as they became available. When I received it, I got it to work with the Arduino IDE. But as I started to look at the sample code, I realized I needed some documentation to help me get started. As I searched, I didn't find much. The best sample app to show some of the functions of the watch is the SimpleWatch app in the LVGL section. Looking at the code, it is anything but simple. It is heavy in C++ and object oriented architecture and it runs under the RTOS (real-time operating system). If you are not familiar with these, they can be daunting.

So, not being an expert programmer, I set out to create a simple Arduino C-based framework for the watch that will get you up and running and able to add your own apps really easily.

The first thing to know about the watch (and this took me a while to figure out) is that there is no user accessible button. The button on the side of the watch turns the watch on and off.

Hold it for 2 seconds to power on the watch and 6 seconds to turn it off.

From the AXP202 Datasheet:

Power Enable Key (PEK)
The Power Enable/ Sleep/Wakeup Key can be connected between PWRON pin and GND of AXP202. AXP202 can automatically identify the “Long-press” and “Short-press” and then correspond respectively.

It looks like the power button is directly connected to the AXP202 and not the ESP32.

This turns out to be pretty useful. The AXP202 datasheet has pretty good information on this chip and you can do a little reverse engineering using it and the TWatch axp202 library. When the watch is shut down, it looks like it secures power to most of the hardware which does a pretty good job of reducing power. I have had the watch sitting on the counter for two days without operating and it only lost about 15% power. This prevented me from having to worry a lot about getting the watch into low power mode. I just turn it off when I am not using it.

Again, from the datasheet:

Power Off
When you push-and-hold PEK longer than IRQLEVEL, HOST can write “1” into“REG32H [7]” to inform AXP202 to shutdown, which can disable all power output except LDO1.

When you view the library, the functionint AXP20X_Class::shutdown() manipulates this bit 7 so just call the shutdown function and it will have the same effect as long pressing the button.

There is an IRQ input from the AXP202 to the ESP32 (AXP202_INT). There are a bunch of interrupts generated from the AXP202. If you need to use the button, you can poll the shortpress interrupt as follows:

ttgo->power->readIRQ(); // This reads the interupts<br>
if (ttgo->power->isPEKShortPressIRQ()) { // This will be true if the button was short pressed
      ttgo->power->clearIRQ(); // Clear the interrupts so you can receive some more
    } else {

See the example code in Examples->TTGO Watch Library->Basic Unit->AXP20X_IRQ.

Since most interaction happens on the touch screen, you may want to look into the LVGL framework. This allows you to make standard GUI elements (buttons, lists, etc) and respond to touches. You could probably use the accelerometer also as a gesture input.


1) T-Watch 2020

2) Arduino IDE

Step 1: The Framework

My code is borrowed heavily from the example code written by Lewis He.

The framework is set up to display the time. Every second the screen will update and flash the colon. This is a good visual that the watch is not hung up.

When you touch the screen, you will see the Menu. Touching up and down will allow you to cycle through menu items. When the desired item is displayed in the center, click the center to launch it.

Pretty straight forward, and that is the point.

The video shows the basic function and a couple of simple apps I included to let you see how to access the accelerometer, battery monitor, touch screen, and more.

Step 2: Get Your Watch Connected

The new Arduino method of loading libraries and boards makes this pretty straight forward.

If you don't already have the ESP32 board library loaded, in the File menu, select Preferences.

In the section Additional Boards Manager URLs, click the box on the right of the text block. This will bring up a box that has all of your current URLs for boards you are using. Add the following to the list:

Click OK until you are out of the Preferences menu.

Then go into Tools->Board->Boards Manager

Search for esp32 and install the latest version.

To load the library, go to the GitHub site, download the repository in zip format, and then import it into Arduino using the Library import ZIP function.

Plug in your watch, start Arduino IDE, Select the watch on the Board section, open a sample application in the TTGO section of Examples (try SimpleWatch in the LVGD section), compile and download. Hopefully all went well and your watch is ready to be programmed.

Step 3: Load the Framework and Add Your App

Download the Arduino Framework code in the individual INO files in this Instructable (sorry, I could not get the zip file to be accepted by Instrucables). Place them all into a folder in your Arduino sketch folder (Name the folder something you will remember). Open it in the IDE (may have to restart the IDE).

You will see that there are a bunch of separate tabs to the program. The Main tab is labeled TWatch_Framework_0. It is where the setup() and loop() subroutines are. The other important tab is the Menu tab.

If you have verified you can download the examples to your watch, you should now be able to download this to your watch.

To add your own app to this watch do the following:

1) In the Menu tab you will see the code:

const int maxApp = 6; // number of apps
String appName[maxApp] = {"Clock", "Jupiter", "Accel", "Battery", "Touch", "Set Time"}; // app names

Increment the constant maxApp by 1. This is the total number of apps.

Append the name of your app to the appName string array. It can be inserted anywhere. But be advised, the menu function returns the number of the selected app. It has to match the switch-case to select the correct app.

2) In the Main tab, add a call to your app as a case in the switch statement in the main loop().

switch (modeMenu()) { // Call modeMenu. The return is the desired app number

      case 0: // Zero is the clock, just exit the switch
      case 1:
      case 2:
      case 3:
      case 4:
      case 5:

3) Add a tab, and write your code as a subroutine or function that is called from the case statement. See the appTouch tab as an example.

That is it. With the tabs I have already, you should be able to quickly code a new app that can be called from the menu. Take a look at the methods I use to exit apps. You want to make sure that the user is not pressing the touch screen when the app exits or you will immediately go back to the menu instead of the time display.

I so hope this helps. I saw a lot of people initially complaining about the lack of documentation and support. But this is a great product that will give you your own programmable watch with lots and lots of geek cred.

Just show people at a party the current orientation of Jupter's moons and you'll see.

******* Right now, Instructables will not upload the zipped code file. I uploaded the individual tabs. **********

******* Download them all, place them in a directory in the Arduino sketch folder and open the Framework ino file ****

Step 4: Some Helpful Apps

As I come up with apps for my watch, I'll post useful ones here:

This app sets the watch time from the internet. Just add your WiFi credentials.

#include <WiFi.h>
#include "time.h"

void appWiFiTime() {

  // WiFi settings ******* Use your network values **********
  const char* ssid     = "put your ssid here";
  const char* password = "put your passcode here";

  const char* ntpServer = "";
  const long  gmtOffset_sec = -18000;
  const int   daylightOffset_sec = 3600;

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {}

  configTime(-18000, 3600 , "", "");

  struct tm timeinfo;
  if (!getLocalTime(&timeinfo)) {
    ttgo->tft->drawString("Failed",  5, 30, 1);
  } else {
    ttgo->tft->setCursor(0, 130);
    ttgo->tft->print(&timeinfo, "%A, %B %d %Y\n%H:%M:%S");
    ttgo->rtc->setDateTime(timeinfo.tm_year, timeinfo.tm_mon + 1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);



I set up a call to give a quick motor vibration. Make sure you setup the motor pin in your setup() routine to be an OUTPUT (pinMode(4, OUTPUT); )

void quickBuzz() {
  digitalWrite(4, HIGH);
  digitalWrite(4, LOW);

Here is the code to retrieve the current Bitcoin price from the server. Again, I am not an expert programmer so you will see there is little error checking and waiting for conformation, but this works on my watch so it must be close. Remember to put in your own wifi credentials.

#include <ArduinoJson.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
void appBitcoin() {

  // WiFi settings
  const char* ssid     = "ssid";
  const char* password = "passcode";

  // API server
  const char* host = "";

  int16_t x, y;

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {}

  while (ttgo->getTouch(x, y)) {}

  WiFiClient client;
  const int httpPort = 80;
  client.connect(host, httpPort);

  // We now create a URI for the request
  String url = "/v1/bpi/currentprice.json";

  // This will send the request to the server
  client.print(String("GET ") + url + " HTTP/1.1\r\n" +
               "Host: " + host + "\r\n" +
               "Connection: close\r\n\r\n");

  // Read all the lines of the reply from server and print them to Serial
  String answer;
  while (!client.available()) {}
  while (client.available()) {
    String line = client.readStringUntil('\r');
    answer += line;


  // Convert to JSON
  String jsonAnswer;
  int jsonIndex;

  for (int i = 0; i < answer.length(); i++) {
    if (answer[i] == '{') {
      jsonIndex = i;

  // Get JSON data
  jsonAnswer = answer.substring(jsonIndex);

  // Get rate as float
  int rateIndex = jsonAnswer.indexOf("rate_float");
  String priceString = jsonAnswer.substring(rateIndex + 12, rateIndex + 19);
  float price = priceString.toFloat();

  ttgo->tft->setCursor(40, 90);

  ttgo->tft->println("Bitcoin Price");
  ttgo->tft->setCursor(70, 130);


  while (!ttgo->getTouch(x, y)) {} // wait until you touching
  while (ttgo->getTouch(x, y)) {}

This app returns the analog value proportional to the AXP202 temperature. It is a raw value and the data sheet I saw didn't have any discussion on the conversion to actual temperature so you will have to calibrate it yourself. My value is about 170 when the watch has been off for a while and I just turn it on and 190 when it has been running for a while.

void appTemp() {<br>  int16_t x, y;
  while (ttgo->getTouch(x, y)) { }
  while (!ttgo->getTouch(x, y)) { // Wait for press
    float temp = ttgo->power->getTemp();
    ttgo->tft->setTextColor(TFT_YELLOW, TFT_BLACK);
    ttgo->tft->setCursor(40, 100);
    ttgo->tft->print("Temp: ");
  while (ttgo->getTouch(x, y)) {} // wait until you stop touching

Step 5: A STTNG-like Drop-in Replacement for DisplayTime()

I wrote a replacement function for the displayTime() subroutine to mimic a ST-like screen display. The easy way would have been to use a bitmap image and overwrite the text. But, I just created it with display paint commands. If you use the above files, this would just replace the displayTime() tab. Here is the code:

// An advanced Time Display
// This is a drop-in replacement for the displayTime()
// In the original Instructable. s such, it redraws GUI
// every minute so you will see a little flicker.

void displayTime(boolean fullUpdate) {
  ttgo->power->adc1Enable(AXP202_VBUS_VOL_ADC1 | AXP202_VBUS_CUR_ADC1 | AXP202_BATT_CUR_ADC1 | AXP202_BATT_VOL_ADC1, true);

  // Get the current data
  RTC_Date tnow = ttgo->rtc->getDateTime();

  hh = tnow.hour;
  mm = tnow.minute;
  ss = tnow.second;
  dday =;
  mmonth = tnow.month;
  yyear = tnow.year;


  if (fullUpdate) {

    //Draw the back graphics - Top of display
    ttgo->tft->fillRoundRect(0, 0, 239, 120, 40, TFT_PURPLE);
    ttgo->tft->fillRoundRect(40, 20, 196, 80, 20, TFT_BLACK);
    ttgo->tft->fillRect(80, 20, 159, 80, TFT_BLACK);
    ttgo->tft->fillRect(170, 0, 45, 20, TFT_BLACK);
    ttgo->tft->fillRect(110, 0, 4, 20, TFT_BLACK);
    ttgo->tft->fillRect(0, 45, 50, 7, TFT_BLACK);
    ttgo->tft->fillRect(0, 70, 50, 7, TFT_BLACK);
    ttgo->tft->fillRect(215, 0, 24, 20, TFT_DARKCYAN);

    //Draw the back graphics - Bottom of display
    ttgo->tft->fillRoundRect(0, 130, 239, 109, 40, TFT_MAROON);
    ttgo->tft->fillRoundRect(40, 150, 199, 88, 20, TFT_BLACK);
    ttgo->tft->fillRect(0, 179, 50, 10, TFT_BLACK);
    ttgo->tft->fillRect(100, 160, 40, 10, TFT_YELLOW);
    ttgo->tft->fillRect(140, 160, 40, 10, TFT_DARKGREEN);
    ttgo->tft->fillRect(180, 160, 40, 10, TFT_RED);
    ttgo->tft->setTextColor(TFT_WHITE, TFT_BLACK);
    ttgo->tft->drawString("Temp", 66, 158, 2);
    ttgo->tft->fillRoundRect(119, 210, 120, 29, 15, TFT_DARKCYAN);

    // Display Temp Marker - you may need to adjust the x value based on your normal ADC results
    float temp = ttgo->power->getTemp();
    ttgo->tft->fillRoundRect(int(temp) - 20, 170, 10, 20, 5, TFT_WHITE);

    // Display Time
    // Font 7 is a 7-seg display but only contains
    // characters [space] 0 1 2 3 4 5 6 7 8 9 0 : .

    ttgo->tft->setTextColor(0xFBE0, TFT_BLACK);
    int xpos = 55;
    if (hh < 10) xpos += ttgo->tft->drawChar('0', xpos, 35, 7);
    xpos += ttgo->tft->drawNumber(hh, xpos, 35, 7);
    xpos += 3;
    xpos += ttgo->tft->drawChar(':', xpos, 35, 7);
    if (mm < 10) xpos += ttgo->tft->drawChar('0', xpos, 35, 7);
    ttgo->tft->drawNumber(mm, xpos, 35, 7);

    // Display Battery Level
    ttgo->tft->drawString("Power", 124, 2, 2);
    int per = ttgo->power->getBattPercentage();
    per = ttgo->power->getBattPercentage();
    ttgo->tft->drawString(String(per) + "%", 179, 2, 2);
    ttgo->tft->drawString(String(dday), 50, 188, 6);

    // Turn off the battery adc
    ttgo->power->adc1Enable(AXP202_VBUS_VOL_ADC1 | AXP202_VBUS_CUR_ADC1 | AXP202_BATT_CUR_ADC1 | AXP202_BATT_VOL_ADC1, false);

    // Draw Month
    String mStr;
    switch (mmonth) {
      case 1: mStr = "Jan"; break;
      case 2: mStr = "Feb"; break;
      case 3: mStr = "Mar"; break;
      case 4: mStr = "Apr"; break;
      case 5: mStr = "May"; break;
      case 6: mStr = "Jun"; break;
      case 7: mStr = "Jul"; break;
      case 8: mStr = "Aug"; break;
      case 9: mStr = "Sep"; break;
      case 10: mStr = "Oct"; break;
      case 11: mStr = "Nov"; break;
      case 12: mStr = "Dec"; break;
    ttgo->tft->drawString(mStr, 9, 194, 2);

  // Build a bargraph every 10 seconds
  int secmod = ss % 10;
  if (secmod) { // Show growing bar every 10 seconds
    ttgo->tft->fillRect(126 + secmod * 10, 215, 6, 15, TFT_ORANGE);
  } else {
    ttgo->tft->fillRoundRect(119, 210, 120, 29, 15, TFT_DARKCYAN);