Programming T-Display With ESPHome

Programming T-Display With ESPHome Configuration

The T-Display can be programmed completely without writing code by using ESPHome.

In this article, I walk you through a simple ESPHome configuration that turns your T-Display into a battery-operated WiFi coverage testing tool - while showing the relevant details you need to know to access all the built-in T-Display features from within your ESPHome configuration.

So once you have this little tool working, you can re-use the configuration to quick-start your own projects with T-Display and ESPHome.

WHen you are a seasoned C++ programmer and used to code firmware in platformio or ArduinoIDE, it may take a moment until you become friends with ESPHome configurations. Configurations are different. However, once you commit yourself to following this example, you can’t but notice how much easier it becomes to create quality firmware, and how many boring and tedious routine programming tasks are taken care of automatically by ESPHome. It would take considerable programming effort and skills in C++ to create a firmware as capable as the one that is auto-generated by the configuration in this example.

Prerequisites

To program T-Display in ESPHome, you need to install ESPHome.

ESPHome and Home Assistant may be a bit overwhelming at first when you are new to it: they consist of a number of moving parts that perfectly complement each other. You may want to quickly review the ESPHome overview.

Creating New Configuration

Start in ESPHome by adding a New Device. This provides you with a generic default configuration.

The default configuration covers your fundamentals, i.e. the board type, and accessing your WiFi. Once this part of the configuration is done, you never touch it again. All “programming” takes place below it.

Correcting Board Memory

Before you start programming anything, review the settings for your T-Display board:

esp32:
  board: esp32dev
  flash_size: 16MB  # important: if your board has 16MB, unlock it!
  framework:
    type: arduino

T-Display is a typical ESP32S microcontroller at its heart which is why it uses the generic esp32dev board definition. By default, this sets the flash RAM to 4MB (the minimum memory commonly used by ESP32S).

T-Display comes in two versions: with 4MB and with 16MB flash memory. The latter is the more common type sold today. It is crucial that you unlock your flash memory and tell ESPHome that your board has 16MB (if it does) by adding the line flash_size: 16MB.

If you are uncertain and would like to find out how much flash memory your T-Display really has, use the Adafruit ESPTool.

ESPHome requires considerable memory for its components. With a 4MB board (or with a 16MB board that is not specifying its capacity via flash_size: 16MB), you quickly run out of memory and can’t leverage the capabilities of your awesome T-Display.

Accessing Display, Buttons, External Battery

T-Display comes with a wealth of extra features that distinguishes it from generic ESP32S boards. These extras need to be defined in your configuration so you can use them. Here is how:

  • TFT Color Display: The built-in display is supported by ESPHome by the model type TTGO TDisplay 135x240 within the display platform st7789v. The display backlight can be controlled by a light component (including dimming and effects).
  • Two Push Buttons: both push buttons can be accessed as an inverted binary_sensor (they are both low active by nature). Either expose the buttons as entities (so in Home Assistant configuration.xaml you can tie actions to them), or tie on_press and other events directly to them. In the sample configuration below, one button dims the display backlight whereas the other makes it brighter.
  • Voltage Monitor: T-Display supports an external LiIon battery (including charger) and has a dedicated voltage monitor. The voltage can be retrieved via a sensor component in the adc platform (*analog-to-digital converter input`).

Sample Configuration

Below sample configuration aims to focus on the specific T-Display features while at the same time providing a practical example.

Add (not replace) the following code to your configuration:

# make sure state persists after 10sec of operation:
preferences:
  flash_write_interval: 10s

# store wifi connectivity state:
globals:
  - id: isOnline
    type: bool
    restore_value: no
    initial_value: 'true'  # since the board is offline at first, this triggers refresh


# retrieve current WiFi signal strength:
sensor:
  - platform: wifi_signal
    name: "WiFi Signal dB"
    id: wifi_signal_db
    update_interval: 1s

  - platform: copy 
    source_id: wifi_signal_db
    name: "WiFi Signal Percent"
    id: wifi_signal_percent
    filters:
      - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
    unit_of_measurement: "Signal %"
    entity_category: "diagnostic"
    device_class: ""

  # handle voltage sensor
  - platform: adc
    pin: GPIO34
    name: "Voltage"
    update_interval: 1s
    accuracy_decimals: 2
    attenuation: 12dB
    samples: 10
    filters:
      - multiply: 2.0
      - median:
          window_size: 7
          send_every: 4
          send_first_at: 3

# handle the two built-in push buttons:
binary_sensor:
  - platform: gpio
    name: 'Button Left'
    id: button_left
        
    pin:
      number: GPIO0
      inverted: True
      mode:
        input: True
        pullup: True

    filters: 
      - delayed_on: 10ms
      - delayed_off: 10ms

    # optional: this dims the display backlight:
    on_press:
      - light.dim_relative:
          id: back_light
          relative_brightness: 5%
          transition_length: 0.1s
          brightness_limits:
              min_brightness: 20%
      - delay: 0.1s

  - platform: gpio
    name: 'Button Right'
    id: button_right

    pin:
      number: GPIO35
      inverted: True
      mode:
        input: True

    filters: 
      - delayed_on: 10ms
      - delayed_off: 10ms

    # optional: this increases brightness of display backlight:
    on_press:
      - light.dim_relative:
          id: back_light
          relative_brightness: -5%
          transition_length: 0.1s
          brightness_limits:
            min_brightness: 20%
      - delay: 0.1s

# handle built-in display backlight:
# use a separate "light" component to enable dimming rather than
# using the "display" property for backlight gpio
output:
  - platform: ledc
    pin: 4
    id: display_backlight_pwm

light:
  - platform: monochromatic
    output: display_backlight_pwm
    name: "Display Backlight"
    id: back_light
    restore_mode: RESTORE_AND_ON
    effects:
      - pulse:
          name: noWifiConnection
          min_brightness: 60%
          max_brightness: 80%

# handle the built-in TFT color display:
spi:
  clk_pin: GPIO18
  mosi_pin: GPIO19

display:
  - platform: st7789v
    id: display1
    cs_pin: GPIO5
    dc_pin: GPIO16
    reset_pin: GPIO23
    backlight_pin: no
    model: TTGO TDisplay 135x240
    update_interval: 1s
    rotation: 90°
    
    # optional: handle the WiFi connectivity icons and the signal strength
    lambda: |-
      // Draw the current SSID
      it.printf(0, 0, id(lato), "SSID: %s", WiFi.SSID().c_str());

      // Draw Wi-Fi signal strength bar graph
      int signal_strength = id(wifi_signal_percent).state; // Get the current signal strength
      int bar_length = 240 * signal_strength / 100; // Map the signal strength to a length

      // Draw the bar
      it.filled_rectangle(0, 30, bar_length, 2, GREEN);  // Bar at (10,30), length, height 20, filled with GREEN
      
      // Check Wi-Fi connection status
      if (WiFi.status() == WL_CONNECTED) {
        if (id(isOnline) == false) { 
          id(back_light).turn_on().set_effect("none").perform();       
          id(isOnline) = true;
        }
        it.image(216, 0, id(wifiOn), GREEN);
        
      } else {
        if (id(isOnline) == true) { 
          id(back_light).turn_on().set_effect("noWifiConnection").perform();         
          id(isOnline) = false;
        }
        it.image(216, 0, id(wifiOff), RED); 
         
      }


font:
  - file:
      type: gfonts
      family: Lato
      weight: 400
    id: lato
    size: 20
  - file:
      type: gfonts
      family: Lato
      weight: 700
    id: latobold
    size: 24
  - file:
      type: gfonts
      family: Lato
      weight: 900
    id: latoblack
    size: 30
  - file:
      type: gfonts
      family: Lato
      weight: 900
    id: latoblackheader
    size: 50

color:
  - id: WHITE
    red: 100%
    green: 100%
    blue: 100%
    white: 100%
  - id: RED
    red: 100%
    green: 0%
    blue: 0%
    white: 0%
  - id: GREEN
    red: 0%
    green: 100%
    blue: 0%
    white: 0%
  - id: BLUE
    red: 0%
    green: 0%
    blue: 100%
    white: 0%
  - id: YELLOW
    red: 100%
    green: 100%
    blue: 0%
    white: 0%
  - id: ORANGE
    red: 100%
    green: 65%
    blue: 0%
    white: 0%
  - id: BLACK
    red: 0%
    green: 0%
    blue: 0%
    white: 0%


image:
  - file: mdi:wifi
    id: wifiOn
    resize: 24x24
  - file: mdi:wifi-off
    id: wifiOff
    resize: 24x24

Next, upload the configuration to your T-Display board. The most robust and simplest way is to use a USB cable, and connect the board directly with the computer that runs ESPHome (which might be a Raspberry Pi if you are using ESPHome inside Home Assistant).

Use Case: WiFi Coverage Tester

The sample configuration primarily illustrates how to access the T-Display features, but it has of course also a use case already: your T-Display serves as a WiFi coverage tester device.

Once the firmware is uploaded, the board does this:

  • Offline: at first, it shows a pulsating display with a red WiFi icon.
  • Connected: once the board successfully connects to your WiFi, the pulsating display backlight stops, and the WiFi icon turns green. The name of the WiFi it connected to is displayed.
  • Signal Strength Tester: from now on, the device acts as a WiFi coverage tester: a green bar visualizes the WiFi coverage at your current position. You can now use the device to test WiFi coverage in your home, and also judge the T-Display WiFi sensitivity and quality of its antenna.
  • Display Dimming: press the push buttons to adjust the brightness of the display backlight. The setting you choose is stored as your new preference, and when you turn off and later turn on the device, once it connected to WiFi, the display brightness is restored to your last setting.

Connect the board via USB-C to a powerbank, or hook up a single LiIon battery to the JST 1.25 jack at its bottom side, and move around to see the green bar change.

Home Assistant

If you use ESPHome in combination with Home Assistant, the already useful stand-alone device gets extra benefits. So when you receive a notification from Home Assistant that a new device was discovered, add your new T-Display device to Home Assistant.

On the devices page in Settings/Devices & services/ESPHome, you now see all of its exposed entities:

  • Display Backlight: in the Controls tile, you can turn the TFT display backlight on or off. This uses a nice 1 sec transition effect. The current dimming level that you may have set via the push buttons is kept.
  • Buttons: press one of the two push buttons to see their state change in the Sensors tile.
  • Voltage: the current voltage is also shown in the Sensors tile.
  • WiFi Signal Strength: the tile Diagnostic shows the current WiFi strength both in dBm and percent.

Power Consumption Monitoring

Since Home Assistant automatically logs sensor readings, you can now easily evaluate battery-driven scenarios: provided you connected a LiIon battery to your T-Display, let it run for a while. Then click on the Voltage that is displayed in the Sensors tile.

Don’t worry about the high voltage spikes in the picture: when I started developing the configuration above, some settings were needed that I missed at first. Fortunately, you can start with a good configuration right away.

Power Consumption Monitoring

Click Show more, then set a timespan, i.e. the past 30 minutes. You can now see how the battery voltage changed. This provides great clues about power consumption, and expected mileage that you get out of your battery.

Smoothening Sensor Data

The visible voltage flucuation of 0.05V in the graph above can be eliminated by a simple median filter: it is very effective in removing outliers:

 filters:
  - multiply: 2.0
  - median:
      window_size: 7
      send_every: 4
      send_first_at: 3

This is illustrated by the next graph that monitored voltage drop over a course of two hours:

Initially, the update frequency was 1s, and a uniform voltage fluctuation is visible. Next, the update frequency was raised to 5s, and averaging with 20 samples was employed. This just reduced the data points but kept the outliers.

Ultimately, the median filter was added which very efficiently took care of the outliers and produced a smooth curve correctly resembling the LiIon battery voltage drop over time.

Understanding ESPHome Configuration

Now that you know what the device does, let’s take a final look at the ESPHome configuration to see how things work, and what your options are to adjust and modify the configuration to your needs. This time, I am focusing step by step on individual T-Display features that the configuration addresses:

Push Buttons

The two push buttons are implemented as regular binary_sensor of type gpio. Since both buttons are low active, the sensor value is reversed: inverted: True.

# handle the two built-in push buttons:
binary_sensor:
  - platform: gpio
    name: 'Button Left'
    id: button_left
        
    pin:
      number: GPIO0
      inverted: True
      mode:
        input: True
        pullup: True

To debounce the buttons, a delay filter is added:

    filters: 
      - delayed_on: 10ms
      - delayed_off: 10ms

To illustrate some button functionality in the sample device, on_press dims the display backlight:

    # optional: this dims the display backlight:
    on_press:
      - light.dim_relative:
          id: back_light
          relative_brightness: 5%
          transition_length: 0.1s
          brightness_limits:
              min_brightness: 20%
      - delay: 0.1s

The second button is implemented similarly. Its GPIO35 is however a ESP32S input-only and has no pullup or pulldown resistors:

  - platform: gpio
    name: 'Button Right'
    id: button_right

    pin:
      number: GPIO35
      inverted: True
      mode:
        input: True

    filters: 
      - delayed_on: 10ms
      - delayed_off: 10ms

    # optional: this increases brightness of display backlight:
    on_press:
      - light.dim_relative:
          id: back_light
          relative_brightness: -5%
          transition_length: 0.1s
          brightness_limits:
            min_brightness: 20%
      - delay: 0.1s

Display

The display consists of a number of components: the SPI interface, the backlight, and the actual display itself.

SPI Interface

Since the display is using the fast SPI interface to connect to the microcontroller, spi needs to be defined, and the approproate GPIOs specified.

# handle the built-in TFT color display:
spi:
  clk_pin: GPIO18
  mosi_pin: GPIO19

Display

The display is using the ST7789 display controller. This controller is used widely with many different TFT displays up to a resolution of 240x320 pixels.

ESPHome supports ST7789 with an own platform which is used in this example:

display:
  - platform: st7789v

The GPIOs for CS and DC are specified. Note that I am not specifying the GPIO4 for backlight_pin, and leave this to no: in order to better control the backlight and use dimming and transition effects, the backlight is separately declared as generic light.

display:
  - platform: st7789v
    id: display1
    cs_pin: GPIO5
    dc_pin: GPIO16
    reset_pin: GPIO23
    backlight_pin: no
    model: TTGO TDisplay 135x240

New Optional Display Component: ILI9xxx

While this works great, this platform has meanwhile been superseeded by the ILI9xxx component which is much more hardware neutral and supports a wide range of display controllers.

If you’d like to use the more modern generic ILI9xxx TFT LCD Series component, replace the section with this:

display:
  - platform: ili9xxx
    model: ST7789V
    id: display1
    cs_pin: GPIO5
    dc_pin: GPIO16
    reset_pin: GPIO23
    dimensions:
      height: 240
      width: 135
      offset_height: 40
      offset_width: 52
    invert_colors: true

Since the ILI9xxx TFT LCD Series is a more hardware-abstracted component that supports a wide range of display drivers, you add the specs of your particular display in dimensions:. The T-Display display requires invert_colors: true to take into account how it stores color information and ensure correct colors.

You may also want to check out the second part of this article where the configuration is using the new ILI9xxx TFT LCD Series component and is optimized for battery use and low power consumption.

Display Content

The display content is generated using a lambda expression. update_interval specifies how often the screen is redrawn, and inside the lambda, it represents the display object and its methods:

    update_interval: 1s
    rotation: 90°
    
    # optional: handle the WiFi connectivity icons and the signal strength
    lambda: |-
      // Draw the current SSID
      it.printf(0, 0, id(lato), "SSID: %s", WiFi.SSID().c_str());

      // Draw Wi-Fi signal strength bar graph
      int signal_strength = id(wifi_signal_percent).state; // Get the current signal strength
      int bar_length = 240 * signal_strength / 100; // Map the signal strength to a length

      // Draw the bar
      it.filled_rectangle(0, 30, bar_length, 2, GREEN);  // Bar at (10,30), length, height 20, filled with GREEN
      
      // Check Wi-Fi connection status
      if (WiFi.status() == WL_CONNECTED) {
        if (id(isOnline) == false) { 
          id(back_light).turn_on().set_effect("none").perform();       
          id(isOnline) = true;
        }
        it.image(216, 0, id(wifiOn), GREEN);
        
      } else {
        if (id(isOnline) == true) { 
          id(back_light).turn_on().set_effect("noWifiConnection").perform();         
          id(isOnline) = false;
        }
        it.image(216, 0, id(wifiOff), RED); 
         
      }

I am using printf() to print formatted text, and I use graphics primitives like filled_rectangle() that can all be looked up in the ESPHome documentation.

In this example, the display: component handles all screen updates viq a lambda: the display automatically clears all content and redraws it completely every second (update_interval: 1s). That’s simple but not very efficient. See the second part of this article which illustrates how you can efficiently only update those parts of the display that actually need new content.

Drawing Icons

image() is an especially useful method to draw images onto the display that can be defined via google material fonts:

image:
  - file: mdi:wifi
    id: wifiOn
    resize: 24x24
  - file: mdi:wifi-off
    id: wifiOff
    resize: 24x24

If you are looking for a particular icon, browse to Pictogrammers, and enter a keyword or search phrase into their search text box to quickly view all matching icons and their names.

Switching Light Effects

It took me probably the most research to find out the awkward way how lambda code can change the settings of light components:

id(back_light).turn_on().set_effect("noWifiConnection").perform();

This was necessary in order to switch the light effect for the display backlight when WiFi connectivity changes.

Backlight

The backlight is controlled separately via light, using an output rather than a gpio binary_sensor. This way, the backlight can be smoothly transitioned, and brightness be adjusted using PWM:

# handle built-in display backlight:
# use a separate "light" component to enable dimming rather than
# using the "display" property for backlight gpio
output:
  - platform: ledc
    pin: 4
    id: display_backlight_pwm

light:
  - platform: monochromatic
    output: display_backlight_pwm
    name: "Display Backlight"
    id: back_light
    restore_mode: RESTORE_AND_ON
    effects:
      - pulse:
          name: noWifiConnection
          min_brightness: 60%
          max_brightness: 80%

As part of light, you can also define effects. The configuration defines a noWifiConnection effect of type pulse that is controlled by the lambda inside display (see above): when there is no WiFi connection, the effect is turned on, else off.

The light also takes care of restoring the original dim level. Just make sure you set your preferences for the flash_write_interval: in order to protect the flash memory from constant writes, ESPHome writes persistent information only in intervals. That’s a good thing because you don’t want to save settings while the user may still be trying to figure out the best dimming level.

The sample configuration uses a 10sec interval: the current dimming level is preserved once it did not change for at least 10 seconds (if you reboot or reset the board before, your recent adjustments are lost).

# make sure state persists after 10sec of operation:
preferences:
  flash_write_interval: 10s

Global Variables

The configuration uses one global variable (globals): isOnline is boolean and defaults to true:

# store wifi connectivity state:
globals:
  - id: isOnline
    type: bool
    restore_value: no
    initial_value: 'true'  

The purpose of this variable is to handle the display backlight effect switching: if you don’t care about effect switching, remove both the lambda code and this variable.

Here is why the variable is needed with display backlight effect switching:

The display content is refreshed every 1s (update_interval: 1s), so the displays’ lambda code is executed each second, and it is responsible for redrawing the entire display content. So there is nothing to gain here, the (re-)drawing cannot be skipped under any conditions, even if connectivity has not changed.

However, backlight effects should not be restarted every second. They should just be turned on or off once, based on a single connectivity change. That’s where the global variable comes into play: if this variable differs from the current connectivity status, then it’s time to change the backlight effect (and set the variable to the new mode).

Fonts

The same goes for google fonts:

font:
  - file:
      type: gfonts
      family: Lato
      weight: 400
    id: lato
    size: 20
  - file:
      type: gfonts
      family: Lato
      weight: 700
    id: latobold
    size: 24

(...)

Battery Voltage

T-Display measures the external battery voltage at GPIO34, and uses the ADC input (analog-to-digital).

  # handle voltage sensor
  - platform: adc
    pin: GPIO34
    name: "Voltage"
    update_interval: 1s
    accuracy_decimals: 2

This imposes a few challenges, though: you can use the adc platform to read analog-in GPIOs, but ESP32 can only read voltages of up to 1.1V. That’s why they have different attenuation modes.

In order to correctly read the battery voltage, the GPIO must be turned into the 12dB attenuation mode, or else this input always shows the same (capped) value:

    attenuation: 12dB

In addition, the board uses a voltage divider that needs to be taken into account (multiply: 2.0), and since the measured voltage may fluctuate a bit, it makes sense to define a number of samples, and use the average instead of raw measured values:

    samples: 10
    filters:
      - multiply: 2.0

The configuration uses 20 samples. Still, this resulted in a consistent voltage fluctuation of 0.05V. You may want to increase the samples, and also increase the update_interval from 1s to something like 5s to get rid of this.

WiFi Strength

The WiFi strength is taken from a sensor from the wifi_signal platform. Obviously, this sensor is only required if you actually want to read and display the WiFi strength. You can remove all of this if you’d rather want to display different things.

sensor:
  - platform: wifi_signal
    name: "WiFi Signal dB"
    id: wifi_signal_db
    update_interval: 1s

Since this sensor returns dBm, a copy sensor is used with a lambda to convert dBm in percent:

  - platform: copy 
    source_id: wifi_signal_db
    name: "WiFi Signal Percent"
    id: wifi_signal_percent
    filters:
      - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
    unit_of_measurement: "Signal %"
    entity_category: "diagnostic"
    device_class: ""

Deep Sleep

To keep this configuration simple and focus on the most important parts, it is not using deep sleep. Obviously, with a development board that can be powered by a battery, looking into deep sleep will become important once you are looking for a way to turn off the device.

The easiest and most effective way would certainly be to implement a physical power switch: place it in the positive battery lead. Such a switch would of course not turn off the device when it is USB powered.

Using one of the built-in push buttons to send the microcontroller to deep sleep on a long press, and waking it by pressing the other button sounds tempting. However, it is not as simple as that. While the ESP32 only consumes 10uA in deep sleep, the microcontroller is not the only power consumer on this board. In fact, when you just send the microcontroller to deep sleep, it consumes hefty 9mA and will drain your battery in no time.

In one of the upcoming articles, I’ll look into optimization strategies to cut down power consumption in deep sleep.

Slow Website?

This website is very fast, and pages should appear instantly. If this site is slow for you, then your routing may be messed up, and this issue does not only affect done.land, but potentially a few other websites and downloads as well. Here are simple steps to speed up your Internet experience and fix issues with slow websites and downloads..

Comments

Please do leave comments below. I am using utteran.ce, an open-source and ad-free light-weight commenting system.

Here is how your comments are stored

Whenever you leave a comment, a new github issue is created on your behalf.

  • All comments become trackable issues in the Github Issues section, and I (and you) can follow up on them.

  • There is no third-party provider, no disrupting ads, and everything remains transparent inside github.

Github Users Yes, Spammers No

To keep spammers out and comments attributable, all you do is log in using your (free) github account and grant utteranc.es the permission to submit issues on your behalf.

If you don’t have a github account yet, go get yourself one - it’s free and simple.

If for any reason you do not feel comfortable with letting the commenting system submit issues for you, then visit Github Issues directly, i.e. by clicking the red button Submit Issue at the bottom of each page, and submit your issue manually. You control everything.

Discussions

For chit-chat and quick questions, feel free to visit and participate in Discussions. They work much like classic forums or bulletin boards. Just keep in mind: your valued input isn’t equally well trackable there.

  Show on Github    Submit Issue

(content created Oct 03, 2024 - last updated Oct 17, 2024)