Extending a TFT_eSPI Example With TTGO T-Display Using PlatformIO, With DumbDisplay

by Trevor Lee in Circuits > Arduino

1608 Views, 0 Favorites, 0 Comments

Extending a TFT_eSPI Example With TTGO T-Display Using PlatformIO, With DumbDisplay

poster.png

In this post I will demonstrate how I extend a TFT_eSPI library example, namely TFT_Clock.ino, to be a bit more customizable:

  • able to synchronize the clock's time with that of your phone
  • customize the clock's background color
  • enable putting the clock to "sleep" when idle

TTGO T-Display will be the target microcontroller board (ESP32), which is internally connected to an IPS ST7789V 1.14 Inch (160x128) TFT LCD.

VSCode with PlatformIO will be used for the development using Arduino framework.

DumbDisplay will be used for the UI of the said customizations -- a little remote control panel on your phone, driven by the microcontroller program.

Create PlatformIO Project

create01.png
create02.png
create03.png

First, create a PlatformIO project with the following initial project settings

  • Name: TDisplayClock
  • Board: Expressif ESP32 Dev Module
  • Framework: Arduino

Edit the generated platformio.ini configuration file, adding dependency declaration to the end of the file

lib_deps =
    bodmer/TFT_eSPI @ ^2.5.30
    SPI
    FS
    SPIFFS

Build the PlatformIO Project

build01.png
build02.png
build03.png

Since the initially created PlatformIO project is already a buildable project. Try building it so that all the specified dependencies are pulled.

After successful building, the TFT_eSPI dependency folder (.pio/libdeps/esp32dev/TFT_eSPI) should be populated with files of the TFT_eSPI library.

Modify the Code As the Sample TFT_Clock.ino

copysample01.png
copysample02.png
copysample03.png

The simplest way to modify your project to be the TFT_Clock.ino sample is

  • Open .pio/libdeps/esp32dev/TFT_eSPI/examples/160 x 128/TFT_Clock.ino, which is a sample sketch that comes with TFT_eSPI library
  • Save it as the src/main.cpp [of the project]

Now, your project has just become the sample.

Nevertheless, one more step before the sample (or should I say your project) will work correctly -- configure TFT_eSPI library for TTGOT T-Display LCD:

Configure TFT_eSPI Library for TTGO T-Display

configlib01.png
configlib02.png
configlib03.png

Notice that the TFT_eSPI library is pulled as an independent copy for your PlatformIO project. Hence, it is perfectly fine to modify it for your project.

To tailor TFT_eSPI library for TTGOT T-Display LCD, open .pio/libdeps/esp32dev/TFT_eSPI/User_Setup_Select.h

Modify its content

  • comment out the line
//#include <User_Setup.h>           // Default setup is root library folder 
  • uncomment out the line
#include <User_Setups/Setup25_TTGO_T_Display.h>    // Setup file for ESP32 and TTGO T-Display ST7789V SPI bus TFT

It is that easy.

Now, you are ready to upload the program to your TTGO T-Display.

Upload the Project (The Sample)

uploadsample01.png
uploadsample02.png

After uploading the project (the sample) to your TTGO T-Display. You should see a clock face displayed on the TFT LCD of TTGO T-Display. The clock looks synchronized with real-time; since the clock's time is coded to start with the time the program is built.

Next, I will show how to modify the program for the customizations mentioned previously. Before that, I guess is a good time to backup your work on src/main.cpp, by making a copy of it and renaming the copy to src/main-ori.txt (you need to rename it to extension .txt since otherwise it will be treated as source file and be involved in compilation).

Set Up the Project for Extending It

extend-setup01.png

Here is the recap of what customizations we will be adding to this project, extending the TFT_Clock.ino sample:

  • able to synchronize the clock's time with that of your phone
  • customize the clock's background color
  • enable putting the clock to "sleep" when idle

The core of these customizations will be using some help from DumbDisplay. Hence, the first step is to add DumbDisplay Arduino library as a dependency to this PlatformIO project

lib_deps =
    bodmer/TFT_eSPI @ ^2.5.30
    SPI
    FS
    SPIFFS
    https://github.com/trevorwslee/Arduino-DumbDisplay

Note that most of the dependency entries were there previously for driving the TFT LCD. The new one is

https://github.com/trevorwslee/Arduino-DumbDisplay

Code to Make Use of DumbDisplay

extend-init-01.png
extend-init-02.png
firstdd01.png
firstdd02.png
firstdd03.png

Now, let's make use of DumbDisplay Arduino library to drive DumbDisplay Android app as UI for the said customizations.

The previous step already installed the DumbDisplay Arduino Library to the project, and you can download and install the DumbDisplay Android app from Google Play Store.

First, modify the code to bring DumbDisplay into the "picture" of the project.

At the beginning of src/main.cpp, add the code segment

#include "esp32dumbdisplay.h"
DumbDisplay dumbdisplay(new DDBluetoothSerialIO("TTGO"));

LcdDDLayer* syncButton = NULL;
  • The code segment includes the needed DumbDisplay header file specific for using ESP Bluetooth connectivity IO object -- esp32dumbdisplay.h
  • Indeed, Bluetooth is how your TTGO T-Display will be connecting to your DumbDisplay app. Notice that the Bluetooth device name for your TTGO T-Display is set to "TTGO"
  • The code segment also declares the global variable syncButton -- a pointer to an LCD layer object, that will be created when connected to DumbDisplay app

At the beginning of the loop() block, add the code segment

void loop() {
  // "passively" make connection with DumbDisplay app non-block
  DDConnectPassiveStatus connectStatus;
  dumbdisplay.connectPassive(&connectStatus);
  if (connectStatus.connected) {
    if (syncButton == NULL) {
      syncButton = dumbdisplay.createLcdLayer(14, 1);
      syncButton->border(2, DD_COLOR_darkgreen, "raised");
      syncButton->writeLine(" 🔄  Sync Time");
      syncButton->enableFeedback("fl");
    }
  }
...
}
  • At the beginning of every loop, first try to make connection to DumbDisplay app non-block by calling connectPassive()
  • If connected and syncButton is not yet assigned with a created LCD layer, create one by calling createLcdLayer() and assign it to the syncButton global variable

The code modification so far only enables connection to DumbDisplay app; once connected, a "Sync Time" button is shown. Try uploading the program to your TTGO T-Display and make connection with DumbDisplay app, and see that it is working.

Synchronize the Time of the Clock

The "Sync Time" button is already clickable; but will not do anything yet.

To actually perform synchronization of the clock's time with that of DumbDisplay app, you need to further modify the code -- will need a [DumbDisplay] "tunnel" to make a "get current time" service request to DumbDisplay app.

First, after declaring the global variable syncButton, declare one more global variable for the "tunnel" object

LcdDDLayer* syncButton = NULL;
BasicDDTunnel* datetimeTunnel = NULL;
  • The newly added global variable is datetimeTunnel, which is a pointer for pointing to a created "tunnel" object

In the loop() block, after checked connected, and syncButton assigned with an actual LCD layer object, add the code

  if (connectStatus.connected) {
    if (syncButton == NULL) {
...
    }
    if (syncButton->getFeedback()) {
      // "sync" button clicked ==> create a "tunnel" to get current date time
      dumbdisplay.logToSerial("getting time for sync ...");
      datetimeTunnel = dumbdisplay.createDateTimeServiceTunnel();
      datetimeTunnel->reconnectTo("now:hhmmss");  // ask DumbDisplay app for current time in "hhmmss" format
    }
    if (datetimeTunnel != NULL) {
      String nowStr;
      if (datetimeTunnel->readLine(nowStr)) {
      // got current time "feedback" from DumbDisplay app => use it to sync hh/mm/ss
        dumbdisplay.logToSerial("... got sync time " + nowStr);
        int now = nowStr.toInt();
        hh = now / 10000;
        mm = (now / 100) % 100;
        ss = now % 100;
        dumbdisplay.deleteTunnel(datetimeTunnel);
        datetimeTunnel = NULL;
        dumbdisplay.tone(2000, 100);
        initial = 1;
      }
    }
  }
...
  • When "Sync Time" button -- syncButton LCD layer -- is clicked, syncButton->getFeedback() will return something (actually a "feedback" object) to indicate that the layer is clicked. In such a case, create a "get current time" "tunnel" by calling createDateTimeServiceTunnel(), and assign the created "tunnel" object to datetimeTunnel. Note that 1) the request is asynchronous; and 2) the "current time" reply will be returned as a string with pattern hhmmss (for only the time part)
  • In case datetimeTunnel is not NULL, i.e. [asynchronous] request made to get "current time" from DumbDisplay app, check that can read the "current time" reply. If so, turn the "current time" reply, which is a string with pattern hhmmss, into an integer and extract components from it to update the hh, mm and ss global variables. Note that the hh, mm and ss global variables are used by the TFT_Clock.ino sample code to keep track of the clock's time.
  • After updating the clock's time, 1) the "tunnel" is deleted and datetimeTunnel set to NULL; 2) a tone is played to DumbDisplay app; and 3) initial is set to 1 tricking the TFT_Clock.ino sample code to completely redraw the clock face.

After you build and upload the program, you should be able to really synchronize the clock on your TTGO T-Display with the time of your phone, by clicking the "Sync Time" button.

Make It Able to Reconnect

At this point, your code can already connect with your phone to synchronize the clock (with the help of DumbDisplay app). However, you can only connect one time. More precisely, reconnection (after disconnect) is not working correctly, unless you reset your TTGO T-Display.

In fact, the way connection is made with DumbDisplay app is specially called "passive":

  • it is non-blocking
  • it is catered for reconnection

There is just a little bit more code change needed -- after checked connected, add code like

  if (connectStatus.connected) {
    if (connectStatus.reconnecting) {
      // if reconnecting (i.e. lost previous connection, "master reset" DumbDisplay)
      dumbdisplay.masterReset();
      syncButton = NULL;
      datetimeTunnel = NULL;
      tft.setTextColor(TFT_GREY, TFT_GREY);
      tft.drawCentreString("Connected",66,160,4);
      return;  // exit this loop
    }
    if (syncButton == NULL) {
...
      tft.setTextColor(TFT_RED, TFT_GREY);
      tft.drawCentreString("Connected",66,160,4);
    }
...
}
...
  • If it is reconnecting (i.e. waiting for connection after disconnect), "master reset" DumbDisplay. After "master reset", all layer objects and "tunnel" objects previously created are no longer valid, and can simply be set to NULL (to indicate not-yet-created). Before exiting the loop call, erase the "Connected" message below the clock face.
  • Note that after creating the syncButton layer object, code is added to print the "Connected" message below the clock face.

That's all the code needed to make the program reconnectable with DumbDisplay app.

Build it, upload it, and try it.

Adding Idle Sleep

Since ESP32 supports sleep mode, why not add idle sleep to the project?

OK. For this simple project, a few things:

  1. Idle means not connected to DumbDisplay app
  2. Pressing the left button (pin 0) of TTGO T-Display wakes it from idle sleep
  3. Clock states (like clock time) should be kept unchanged after awaken up

To add idle sleep to the project, declare a few more global things

LcdDDLayer* syncButton = NULL;
BasicDDTunnel* datetimeTunnel = NULL;

int maxIdleMillis = 30 * 1000;  // -1 means disable idle sleep
long idleStartMillis;

#define WAKE_BUTTON_PIN_NUM   GPIO_NUM_0
  • maxIdleMillis is the maximum idle milliseconds before going to sleep;
  • idleStartMillis is used to keep track of how long it has been idle for
  • WAKE_BUTTON_PIN_NUM defines the pin used to wake the board from sleep

Change the hh/mm/ss variable to use the RTC memory

//uint8_t hh=conv2d(__TIME__), mm=conv2d(__TIME__+3), ss=conv2d(__TIME__+6);  // Get H, M, S from compile time
RTC_DATA_ATTR uint8_t hh = 0;
RTC_DATA_ATTR uint8_t mm = 0;
RTC_DATA_ATTR uint8_t ss = 0;

It is necessary to put them in the RTC memory so that the values of the variables can be preserved during sleep. Notice that all hh, mm and ss are set to 0 initially.

At the beginning of the setup() block

void setup(void) {
  idleStartMillis = millis();
...
}
  • Initially set the maxIdleMillis to the current time

To actually check and put TTGO T-Display to sleep, the code around checking DumbDisplay app connected need modifications like

  if (connectStatus.connected) {
    idleStartMillis = millis();
...
} else {
    // not connected to DumbDisplay app ==> check for idle; go to sleep if idle for too long
    if (maxIdleMillis != -1) {
      long diffMillis = millis() - idleStartMillis;
      if (diffMillis >= maxIdleMillis) {
        esp_sleep_enable_ext0_wakeup(WAKE_BUTTON_PIN_NUM, 0);
        esp_deep_sleep_start();    
        // the above call will not return
      }
    }
}
  • It is important that idleStartMillis be set to the current time every loop cycle if checked connected
  • If not connected, check it has been idle for how long; if too long, put TTGO T-Display to sleep.
  • esp_sleep_enable_ext0_wakeup() is called to set up that pin 0 will be signal source for waking up from sleep
  • esp_deep_sleep_start() is called to put TTGO T-Display to sleep
  • After waking up, the global variables will be initialized again and the setup() block will be called again, like a fresh run of the program. However, those global variables in the RTC memory will keep their values during sleep.

During sleep, you would not be able to make connection between TTGO T-Display and DumbDisplay app. However, you should be able to make connection once TTGO T-Display is awakened.

Note that when DumbDisplay app is connected and is in the foreground, your phone will not go to sleep. If DumbDisplay is put to the background, connection will still be kept.

Build it, upload it, and try it.

Customize Clock's Background Color

withslider.png

The remaining customization is to customize the clock's background color. To enable this customization

  • On TTGO T-Display coding clock face side, a few places need to be modified.
  • On the UI side, a DumbDisplay "slider" layer is used to adjust the clock's background color, ranging from 0 to 127 (i.e. color range close to the blue spectrum)

First, add the following global declarations

JoystickDDLayer* blueSlider;
RTC_DATA_ATTR uint32_t clockBackgroundColor = 0;
  • blueSlider is used to point to the "slider" layer object for adjusting the clock's background color; note that the "slider" is actually a Joystick layer
  • clockBackgrounColor is the clock's background color

The actual "slider" layer object is created and set up like

    if (syncButton == NULL) {
...
      blueSlider = dumbdisplay.createJoystickLayer(127, "hori");
      blueSlider->moveToPos(clockBackgroundColor, 0);
      blueSlider->border(5, DD_COLOR_darkgray, "round", 3);
      dumbdisplay.configAutoPin(DDAutoPinConfig('V').build());
      dumbdisplay.backgroundColor(DD_INT_COLOR(clockBackgroundColor));
    }
  • a "slider" object is created by calling createJoystickLayer() giving it 1) maximum value (127); and 2) its orientation ("hori" in this case)
  • update the "slider" position according to colorBackgroundColor
  • Auto pin the created layers vertically by calling configAutoPin()

Handing of "feedback" due to sliding blueSlider "slider" is like

  if (connectStatus.connected) {
    ...
const DDFeedback* fb = blueSlider->getFeedback();
    if (fb != NULL) {
      // got "feedback" from the "slider" => use it's x value as color background color
      clockBackgroundColor = fb->x;
      dumbdisplay.backgroundColor(DD_INT_COLOR(clockBackgroundColor));
      initial = 1;
    }
 } else {
...
}
  • When the blueSlider "slider" is dragged, check for "feedback" by calling getFeedback()
  • If "feedback" got (DDFeedback object), check the "feedback" x value (from 0 to 127)
  • Set colorBackgroundColor to the x value of the "feedback"
  • Also set the "slider" background color accordingly as well
  • Set initial to 1 to force redraw of the clock face

Several places of the original clock face code need be modified as well

    if (ss==0 || initial) {
      tft.fillCircle(64, 64, 48, clockBackgroundColor); // draw the clock background circle
...
      // Erase hour and minute hand positions every minute
      tft.drawLine(ohx, ohy, 65, 65, clockBackgroundColor/*TFT_BLACK*/);
...
      tft.drawLine(omx, omy, 65, 65, clockBackgroundColor/*TFT_BLACK*/);
...
}
...
      // Redraw new hand positions, hour and minute hands not erased here to avoid flicker
      tft.drawLine(osx, osy, 65, 65, clockBackgroundColor/*TFT_BLACK*/);
  • Notice that the clock's background is actually a circle additionally drawn

That is it. Build, upload and try the updated program.

Further Extensions

full.png

There are further extensions not mentioned in the previous sections. You can copy the complete code from here, which is a sketch for the Arduino IDE. Yes, even though Arduino IDE is not referred to in this post, the project can be developed using Arduino IDE in a similar fashion. For a briefing on installing DumbDisplay Arduino library, you may refer to my previous post -- Blink Test With Virtual Display, DumbDisplay

Other than the previously mentioned customizations, the additional extensions are:

  • the clock's time is also shown in the UI
  • can use the UI to disable idle sleep

You can simply copy the complete code to the project's src/main.cpp. Build it, upload it, and try it.

Enjoy!

TDisplayDDClock

Even as a simple clock implementation like this, there are still at least two desirable capabilities not there

  1. Should somehow keep the clock time advancing during sleep. This might call for a hardware counter or a real-time clock???
  2. It is desirable to wake TTGO T-Display from sleep when DumbDisplay app tries to establish connection. This might call for hardware feature like PC's "wake-on-LAN"???

Anyway. Hope you also find the development journey interesting. Enjoy!

Peace be with you! May God bless you! Jesus loves you!