Skip to content

Lopaka Basics 04: Live Data ​

A 320×240 ILI9341 display showing a Living room dashboard with a red LIVE dot, Temperature 36.3 °C, Humidity 67%, a blue humidity bar and the caption Updated every 0.5 s

Two screens. Same board, same sensor, same numbers. But one of them blinks every time a number changes. Slow it down, and you can see why: the whole picture gets wiped, then painted back, piece by piece.

In this episode you will find out what loop() does, why a colour display flickers when an OLED does not, and the three rules that stop it. Then you will turn the numbers of a Lopaka design into variables, feed them from a temperature and humidity sensor, and run it on a simulated ESP32 without a single blink.

What you will build: a 320×240 colour room monitor with live temperature and humidity numbers and a humidity bar, updated twice a second. What you need: a browser. We run the code on a simulated ESP32 with an ILI9341 display and a DHT22 sensor; a real board is optional.

setup() and loop() ​

Every Arduino sketch has two parts:

  • setup() runs once, at power-up.
  • loop() runs after it, again and again, for as long as the board has power.

Live data lives in loop(): read the sensor, turn the numbers into text, draw them, and repeat. Every lap draws the screen again.

setup() runs once, at power-up; loop() is a ring with three stations, read the sensor, make text and draw, and the lap counter shows 4

The OLED draws in memory first ​

The OLED from the first episodes never blinked. Its library, U8g2, draws into memory first: a frame buffer of one kilobyte for 128 × 64 pixels, as you saw in episode 1. Only the finished picture goes to the display. Page mode does the same in eight strips of 128 bytes, one after another.

The graphics library draws into a 1,024-byte frame buffer in RAM, 128 × 64 pixels divided by 8, and the OLED gets only finished pixels

A colour frame would need 150 KB ​

A 320×240 colour screen stores two bytes per pixel, so one frame takes 320 × 240 × 2 = 153,600 bytes, about 150 KB. That is almost half of the RAM an ESP32 gives your sketch. So TFT_eSPI paints straight onto the display, like Adafruit GFX in episode 3.

Memory for one frame: the OLED frame is a tiny 1 KB bar, the colour TFT frame is a long bar, 320 × 240 × 2 bytes = 150 KB per frame

You see every step ​

When the library paints straight onto the display, you see every step of the drawing. If the sketch clears the screen at the start of every lap, the display goes black first, and the picture comes back piece by piece.

Slow motion of the flickering screen: the display is wiped to black and the dashboard is painted back piece by piece, so far only the title, the card outlines, the numbers 36.3 and 48%

In our test (ESP32 in the simulator, SPI at 27 MHz), clearing the 320×240 screen took 48 milliseconds and drawing the dashboard another 68. Almost 50 milliseconds of black on every update is enough for your eye to catch it.

No buffer, straight onto the display: the screen is half repainted, the timeline below shows the clear and the draw

Three rules against flicker ​

  1. Don't clear the screen inside loop(). No fillScreen() on every lap.
  2. Give every live number a background colour. Without one, the new digits are drawn on top of the old ones and pile up into a mess. With one, each digit is drawn in a box of the background colour that erases what was under it.
  3. Keep other shapes out from under a live number. A filled card under the number would be painted over it on every lap, and the number would blink. An outlined card does not touch the pixels behind the digits.

Lopaka takes care of the first two.

The three rules: don't clear the screen inside loop(), fillScreen crossed out; a background behind every live number, a black box erases the old digits; nothing else under a live number, a filled orange card is wrong and an outlined one is right

Step 1: design the room monitor ​

A new TFT_eSPI project ​

Click + New Project and pick the TFT_eSPI, M5, Lovyan platform. TFT_eSPI is one of the most common graphics libraries for colour displays on the ESP32. Set the Title to "Room Monitor" and the Screen Size to 320×240, keep the black Background Color and click Save.

The Create New Project dialog with Platform TFT_eSPI, M5, Lovyan, the title Room Monitor, Screen Size 320×240 and Background Color #000000

Draw the dashboard ​

Draw the screen the same way as last time: a title, two outlined cards, two labels, two big numbers, a degree sign, a bar for humidity, a caption and a red LIVE mark. These are the values we used:

LayerToolValues in the inspectorFontColour
Living roomtextX 12, Y 30FreeSansBold12pt7b#FFFFFF
temperature cardrectangleX 12, Y 46, W 142, H 120, R 10#FF8200
humidity cardrectangleX 166, Y 46, W 142, H 120, R 10#4C8DFF
TemperaturetextX 24, Y 72FreeSans9pt7b#FF8200
HumiditytextX 178, Y 72FreeSans9pt7b#4C8DFF
23.5textX 24, Y 140FreeSansBold24pt7b#FFFFFF
48%textX 178, Y 140FreeSansBold24pt7b#FFFFFF
CtextX 132, Y 122FreeSansBold12pt7b#FFFFFF
degree signellipseX 124, Y 104, RX 3, RY 3#FFFFFF
barrectangle, FilledX 12, Y 182, W 142, H 18#4C8DFF
empty part of the barrectangle, FilledX 154, Y 182, W 154, H 18#1F2937
Updated every 0.5 stextX 12, Y 228FreeSans9pt7b#6B7280
LIVE dotellipse, FilledX 260, Y 16, RX 4, RY 4#EF4444
LIVEtextX 272, Y 27FreeSans9pt7b#EF4444

The first text in a TFT_eSPI project starts in the small Adafruit 5x7 font, and every text after that starts in the last font you picked.

The cards are outlines, following rule 3. The bar is two rectangles side by side, the filled part and the empty part, so no pixel of the bar is painted twice. If you drew the whole dark bar and the blue part over it, the blue part would blink on every lap.

The Y of a text ​

Look at the big temperature number in the code:

cpp
tft.drawString("23.5", 24, 104);

The inspector says Y 140, the code says 104. As in U8g2, the inspector's Y is the baseline, the line the digits stand on. TFT_eSPI's drawString takes the top of the text instead, so Lopaka converts the number for you. Colours are converted too: #FF8200 becomes 0xFC00 in RGB565, the two-byte colour format from episode 3.

Right now these numbers are plain text. They will never change.

Step 2: layers become variables ​

Name the layer ​

To make a number live, turn it into a variable. First, name the layer: double-click string 6, the 23.5 text, in the Layers panel and rename it to temp.

Click the variable button ​

Select the layer and click the variable button at the right end of its text field in the inspector (its tooltip says Add variable). The button turns orange, and two things change in the code:

cpp
const char* temp_text = "23.5";
cpp
// temp
tft.drawString(temp_text, 24, 104);

Lopaka declares a variable at the top of the code, named after the layer, with the text as its starting value. The draw call now uses the variable instead of the fixed text. Your sketch can put any text into temp_text, and the next call of the draw function shows it.

The temp layer selected with the variable button next to its text field turned on; the code starts with const char* temp_text = "23.5"

Humidity and the bar ​

Do the same for humidity, and give the bar variable sizes:

  1. Rename string 7, the 48% text, to hum and click the variable button next to its text: hum_text.
  2. Rename the filled bar to hum_bar and click the variable button next to W: hum_bar_w.
  3. Rename the empty part of the bar to hum_rest and click the variable buttons next to X and W: hum_rest_x and hum_rest_w.

The bar gets a variable width, and its empty part a variable position and width, so the sketch still paints every pixel of the bar once, whatever the humidity.

cpp
int hum_bar_w = 142;
int hum_rest_w = 154;
int hum_rest_x = 154;
const char* hum_text = "48%";
const char* temp_text = "23.5";
cpp
// hum_bar
tft.fillRect(12, 182, hum_bar_w, 18, 0x4C7F);
// hum_rest
tft.fillRect(hum_rest_x, 182, hum_rest_w, 18, 0x1946);

A text becomes a const char*, a number such as a width becomes an int. The layer name and the property make the variable name: hum_bar + w.

The hum_rest layer selected with variable buttons on for X and W; the code starts with int hum_bar_w = 142, int hum_rest_w = 154, int hum_rest_x = 154 and the two text variables

A background behind every number ​

Select temp and set its Back colour to #000000, black like the screen. Do the same for hum. The code now sets two colours for the text, the digits and the box behind them:

cpp
// temp
tft.setTextColor(0xFFFF, 0x0);
tft.setFreeFont(&FreeSansBold24pt7b);
tft.drawString(temp_text, 24, 104);
// hum
tft.drawString(hum_text, 178, 104);

0xFFFF is white, 0x0 is black. The humidity number uses the same colours, so Lopaka does not repeat the call. That is rule 2.

The temp layer with Back set to #000000 in the inspector; the code shows tft.setTextColor(0xFFFF, 0x0) before drawString(temp_text, 24, 104)

Switch off Clear/Fill display ​

Now rule 1. In the list of switches next to the code, find Clear/Fill display. In a new TFT_eSPI project it is on, and the draw function starts by filling the whole screen:

cpp
void drawScreen_1(void) {
    tft.fillScreen(0x0);

That one line causes the blink: the function runs on every lap of loop(), so the screen goes black on every lap. Switch Clear/Fill display off, and the line is gone.

The code panel with Clear/Fill display switched on: the draw function starts with tft.fillScreen(0x0); Include fonts is off

Switch off Include fonts ​

One more switch: turn off Include fonts. TFT_eSPI already brings these FreeFonts with it, so the sketch needs no font files.

Step 4: your own loop ​

Write around the generated block ​

The rest is a few lines of your own. You can write the whole sketch right here in the code panel. Lopaka keeps your code around its generated block, between // [BEGIN lopaka generated] and // [END lopaka generated], and it stays when you change the design or reload the page.

Click above the BEGIN line and type the includes and the objects:

cpp
#include <TFT_eSPI.h>
#include <DHTesp.h>

TFT_eSPI tft;
DHTesp dht;
char t[8], h[8];

Then, below the END line, add setup() and loop():

cpp
void setup() {
  tft.begin();
  tft.setRotation(1);
  tft.fillScreen(TFT_BLACK);
  tft.setTextPadding(94);
  dht.setup(27, DHTesp::DHT22);
}

void loop() {
  TempAndHumidity d = dht.getTempAndHumidity();
  snprintf(t, sizeof t, "%.1f", d.temperature);
  snprintf(h, sizeof h, "%.0f%%", d.humidity);
  temp_text = t;
  hum_text = h;
  hum_bar_w = d.humidity * 2.96;
  hum_rest_x = 12 + hum_bar_w;
  hum_rest_w = 296 - hum_bar_w;
  drawScreen_1();
  delay(500);
}

The code panel after the END lopaka generated marker: void setup() with tft.begin, setRotation, fillScreen, setTextPadding(94) and dht.setup, then void loop() reading the sensor; Include fonts and Clear/Fill display are off

What setup() does ​

setup() starts the display, turns it to landscape with setRotation(1), and paints it black once, then starts the sensor on pin 27. The one fillScreen here is fine: it runs at power-up, not on every lap.

setTextPadding(94) gives every live number a fixed box 94 pixels wide. TFT_eSPI fills the text background only as wide as the new text. When "23.5" changes to a narrower value such as "9.8", the right end of the old digits would stay on the screen. 94 pixels is the width of a two-digit value such as "48%" in this font, and the box stops before the degree sign.

What loop() does ​

loop() reads temperature and humidity, writes them into the text variables, turns humidity into the bar width, and calls the draw function. Then it waits half a second, and goes again:

  • snprintf turns a number into text: "%.1f" keeps one decimal, so 23.46 becomes "23.5", and "%.0f%%" gives "48%". %% prints the percent sign.
  • temp_text = t; points the variable at that text. The buffers t and h are declared outside loop(), so the text is still there when the draw function reads it.
  • The bar is 296 pixels long at 100%, from x = 12 to 308, so one percent is 2.96 pixels. The empty part starts where the filled part ends and takes the rest.
  • delay(500) gives two updates a second.

Step 5: run it ​

In the simulator ​

Click Copy above the code. It copies the whole panel, your code included. In the simulator from episode 1:

  1. Create a new ESP32 project.
  2. Add an ILI9341 display and connect it: VCC and LED to 3V3, GND to GND, CS to 15, RST to 2, D/C to 0, MOSI to 13, SCK to 14, MISO to 12.
  3. Add a DHT22 sensor: VCC to 3V3, GND to GND, SDA to 27.
  4. In the Library Manager, add TFT_eSPI and DHT sensor library for ESPx (the library with DHTesp.h).
  5. Paste the code into sketch.ino and press Play.

TFT_eSPI reads its pins from a settings file in the library folder. The simulator uses the library's default settings: an ILI9341 on these ESP32 pins.

Click the DHT22 and drag its sliders. The numbers and the bar follow the sliders without a single blink.

The simulator running the sketch: the DHT22 sliders are set to 18.2 °C and 31.0%, and the ILI9341 next to the ESP32 shows Temperature 18.2 °C and Humidity 31% with a short blue bar

The complete code from the Copy button, with the Lopaka block in the middle:

cpp
#include <TFT_eSPI.h>
#include <DHTesp.h>

TFT_eSPI tft;
DHTesp dht;
char t[8], h[8];

// [BEGIN lopaka generated]
int hum_bar_w = 142;
int hum_rest_w = 154;
int hum_rest_x = 154;
const char* hum_text = "48%";
const char* temp_text = "23.5";
void drawScreen_1(void) {
    // string 1
    tft.setTextColor(0xFFFF);
    tft.setTextSize(1);
    tft.setFreeFont(&FreeSansBold12pt7b);
    tft.drawString("Living room", 12, 12);
    // rect 2
    tft.drawRoundRect(12, 46, 142, 120, 10, 0xFC00);
    // rect 3
    tft.drawRoundRect(166, 46, 142, 120, 10, 0x4C7F);
    // string 4
    tft.setTextColor(0xFC00);
    tft.setFreeFont(&FreeSans9pt7b);
    tft.drawString("Temperature", 24, 58);
    // string 5
    tft.setTextColor(0x4C7F);
    tft.drawString("Humidity", 178, 58);
    // temp
    tft.setTextColor(0xFFFF, 0x0);
    tft.setFreeFont(&FreeSansBold24pt7b);
    tft.drawString(temp_text, 24, 104);
    // hum
    tft.drawString(hum_text, 178, 104);
    // string 8
    tft.setTextColor(0xFFFF);
    tft.setFreeFont(&FreeSansBold12pt7b);
    tft.drawString("C", 132, 104);
    // ellipse 9
    tft.drawEllipse(127, 107, 3, 3, 0xFFFF);
    // hum_bar
    tft.fillRect(12, 182, hum_bar_w, 18, 0x4C7F);
    // hum_rest
    tft.fillRect(hum_rest_x, 182, hum_rest_w, 18, 0x1946);
    // string 12
    tft.setTextColor(0x6B90);
    tft.setFreeFont(&FreeSans9pt7b);
    tft.drawString("Updated every 0.5 s", 12, 214);
    // ellipse 13
    tft.fillEllipse(264, 20, 4, 4, 0xEA28);
    // string 14
    tft.setTextColor(0xEA28);
    tft.drawString("LIVE", 272, 13);

}
// [END lopaka generated]

void setup() {
  tft.begin();
  tft.setRotation(1);
  tft.fillScreen(TFT_BLACK);
  tft.setTextPadding(94);
  dht.setup(27, DHTesp::DHT22);
}

void loop() {
  TempAndHumidity d = dht.getTempAndHumidity();
  snprintf(t, sizeof t, "%.1f", d.temperature);
  snprintf(h, sizeof h, "%.0f%%", d.humidity);
  temp_text = t;
  hum_text = h;
  hum_bar_w = d.humidity * 2.96;
  hum_rest_x = 12 + hum_bar_w;
  hum_rest_w = 296 - hum_bar_w;
  drawScreen_1();
  delay(500);
}

Compiled for an ESP32, the sketch uses 24% of the program space and 6% of the RAM.

The same sketch with Clear/Fill display ​

The split screen below runs the same sketch twice: on the left with Clear/Fill display switched on, on the right ours. The only difference is the tft.fillScreen(0x0); line at the start of drawScreen_1(). The left screen is caught halfway through a redraw, with only the first digit back. On the right, only the digits change.

Two simulated displays side by side: with Clear/Fill display on, the left screen is caught mid-redraw with only the digit 3 visible; with Clear/Fill display off, the right screen shows 36.3 °C and 48%

On a real board ​

Install TFT_eSPI and DHT sensor library for ESPx from the Library Manager in the Arduino IDE, wire the display and the sensor the same way and upload. Things to check on real hardware:

  • TFT_eSPI settings. The library takes the display driver and the pins from User_Setup.h in its own folder. The default is an ILI9341 with the pins above. For a different display or other pins, change that file, not the sketch.
  • Boot pins. On a real ESP32, GPIO 0 and GPIO 12 also decide how the board starts. If the board does not boot with the display connected, move D/C and MISO to other pins in User_Setup.h.
  • The sensor. A DHT22 gives a new reading at most every two seconds. The library returns the last reading in between, so the screen still redraws twice a second. If the numbers show nan, the sensor did not answer: check its wiring.

Recap ​

  1. loop() redraws your screen forever: read, make text, draw, repeat.
  2. Name a layer, get a variable: the variable button turns a text or a size into a variable named after the layer, and your loop feeds it.
  3. No clearing plus a background means no blink: switch off Clear/Fill display and give every live number a Back colour.

Next episode: From Browser to Board. We take a design off the screen and onto a real board, and go through five reasons a new display stays black. Read episode 5.

Ready to try? Open Lopaka and make your own screen live.