↩ All projects and sketches Deze pagina in het Nederlands
ESP32 · TFT_eSPI · touchscreen

ESP32-3248S035: integrated display (480 × 320px)

ESP32-3248S035 with integrated 3.5 inch touchscreen
ESP32-3248S035 on AliExpress — 3.5 inch capacitive touchscreen, 480×320px

A convenient combination of an ESP32 and a relatively large touchscreen — used here to show live dynamic electricity prices.

What do you need for this?

ESP32-3248S035: integrated display (480 × 320px)

This is a convenient combination of an ESP32 and a relatively large touch screen. You won't find many examples of this combination for the Arduino IDE.

We do not use the touch part in this example (but it works perfectly), you can just load (and test) that from the examples of the library "TFT_eSPI".

In this sketch, we retrieve dynamic electricity prices (Europe) via an API to show them on the display. We can change the view to numeric data or graph (with the flash button).

Obviously there is no circuit schematics to display, but it is quite a search if you want to configure this system for the Arduino IDE. We will again use the library TFT-eSPI (Bodmer). You need to place a file "User_Setup.h" in the folder of the library "TFT_eSPI".

Contents of the file "User_Setup.h" for the ESP32-3248S035:

#define ST7796_DRIVER
#define TFT_WIDTH  320
#define TFT_HEIGHT 480 // 
#define TFT_BL   27            // LED back-light control pin
#define TFT_BACKLIGHT_ON HIGH  // Level to turn ON back-light (HIGH or LOW)

#define TFT_MISO 12
#define TFT_MOSI 13  // In some display driver board, it might be written as "SDA" and so on.
#define TFT_SCLK 14
#define TFT_CS   15  // Chip select control pin
#define TFT_DC   2   // Data Command control pin
#define TFT_RST  -1  // Reset pin (could connect to Arduino RESET pin)

#define TOUCH_CS 33     // Chip select pin (T_CS) of touch screen

#define LOAD_GLCD   // Font 1. Original Adafruit 8 pixel font needs ~1820 bytes in FLASH
#define LOAD_FONT2  // Font 2. Small 16 pixel high font, needs ~3534 bytes in FLASH, 96 characters
#define LOAD_FONT4  // Font 4. Medium 26 pixel high font, needs ~5848 bytes in FLASH, 96 characters
#define LOAD_FONT6  // Font 6. Large 48 pixel font, needs ~2666 bytes in FLASH, only characters 1234567890:-.apm
#define LOAD_FONT7  // Font 7. 7 segment 48 pixel font, needs ~2438 bytes in FLASH, only characters 1234567890:-.
#define LOAD_FONT8  // Font 8. Large 75 pixel font needs ~3256 bytes in FLASH, only characters 1234567890:-.
// #define LOAD_FONT8N // Font 8. Alternative to Font 8 above, slightly narrower, so 3 digits fit a 160 pixel TFT
#define LOAD_GFXFF  // FreeFonts. Include access to the 48 Adafruit_GFX free fonts FF1 to FF48 and custom fonts

// Comment out the #define below to stop the SPIFFS filing system and smooth font code being loaded
// this will save ~20kbytes of FLASH
#define SMOOTH_FONT

#define SPI_FREQUENCY  65000000
#define SPI_READ_FREQUENCY  20000000
#define SPI_TOUCH_FREQUENCY  2500000  //2500000
TFT_eSPI — attention For easy overview, we modify the content of the file "User_Setup.h" directly. This is a quick and dirty solution if you want to get one display working and see quick results.

If you use multiple displays and/or don't want to lose data when installing a new copy of this library, you'd better follow the tip from the designer of the library: github.com/Bodmer/TFT_eSPI/#tips.

You can comment out the entire contents of "User_Setup.h" if you work this way.

Live electricity prices on the screen

The sketch connects via WiFiManager, then every hour (between 12:00 and 21:00 UTC, when new prices for the next day become available) fetches the current dynamic electricity prices via an API, and displays them as a graph or as figures in two columns — switchable with the flash button. If the first bidding zone stops returning results, the script automatically tries the second bidding zone.

/*==========================================================================================
This sketch was written for the ESP32-3248S035 with integrated display (480 * 320 pixels). 
If a WiFi connection cannot be made, a web server is started to store the WiFi credentials.
Dynamic electricity prices (Europe) are retrieved via an API and displayed on the screen. 
The flash button switches the display between figures in two columns or graph. 
Prices for the next day are available between 1200 hours and 2100 hours (UTC). 
The script will check (starting at 1200 UTC) every hour if new API data is available. 
The script keeps repeating this every hour until it succeeds. If the first bidding zone  
returns no results any more, the script tries the second zone. If only the second bidding 
zone produces results, every hour an attempt is made to obtain the data for the first zone. 
=============================================================================================
Arduino IDE - board: ESP32 Dev Module or DOIT ESP32 DEVKIT V1
https://nl.aliexpress.com/item/1005006398547061.html
https://nl.aliexpress.com/item/1005005900820162.html
==========================================================================================*/

#include <WiFiManager.h>  // https://github.com/tzapu/WiFiManager/
#include <TFT_eSPI.h>     // https://github.com/Bodmer/TFT_eSPI
#include <ArduinoJson.h>  // https://github.com/bblanchon/ArduinoJson
#include <HTTPClient.h>   // for API connection
#include <Preferences.h>  // no need to install this library

TFT_eSPI tft = TFT_eSPI();  // User_Setup.h settings available on: https://espgo.be/index-en.html#2035
WiFiManager myWiFi;
Preferences flash;

#define SHOW_ITEMS 34  // max number of records you want to process
#define this_Ssid "S035_ESP"

struct tm tInfo;  // https://cplusplus.com/reference/ctime/tm/
bool chart = true, bidLU = false, freshStart = true, cursorUpdated = false;
unsigned long lastAction;
constexpr uint8_t VALUE_RED = 100, FLASH_BUTTON = 0;  // electricity price in red color if higher than VALUE_RED
const char* biddingZone[] = { "BE", "DE-LU" };        // default & backup bidding zone
double prijzen[SHOW_ITEMS];                           // most recent prices
double prices[SHOW_ITEMS * 2];                        // all prices returned by the API
double priceNow;                                      // price of current hour
time_t epo[SHOW_ITEMS];                               // most recent epoch times
time_t epoc[SHOW_ITEMS * 2];                          // all epoch times returned by the API

void setup() {
  const char* time_Zone = "CET-1CEST,M3.5.0,M10.5.0/3";  // https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
  pinMode(FLASH_BUTTON, INPUT_PULLUP);
  for (const uint8_t &pin : {4, 16, 17}) pinMode(pin, OUTPUT), digitalWrite(pin, HIGH); // red, green & blue LEDs off
  tft_Setup();
  getBidZoneFromFlash();
  myWiFi.setAPCallback(messageNoConnection);
  myWiFi.autoConnect(this_Ssid);
  configTzTime(time_Zone, "be.pool.ntp.org");
  showStartupData();
}

void loop() {
  display_Time();
  if (!digitalRead(FLASH_BUTTON)) changeView();  // flash button changes between chart or list of figures
  // if (priceNow < 0) start_Energy-Guzzling_device();  // priceNow = current electricity price
  // else stop_Energy-Guzzling_device();
}

void tft_Setup() {  // no sprites because the data on the screen is rather static
  tft.init();
  tft.setRotation(3);
  tft.setTextWrap(false);
  tft.fillScreen(TFT_BLACK);  // delete output from previous sessions
  tft.drawCentreString("Connecting to WIFi", 240, 160, 4);
}

void getBidZoneFromFlash() {
  flash.begin("bid_zone", true);         // read from flash, true = read only
  bidLU = flash.getBool("bid_zone", 0);  // retrieve the last set bidding zone - default to first in the array [0]
  flash.end();
}

bool timeElapsed(unsigned long myMillis) {  // avoid blocking task "delay(millisec)"
  return millis() - lastAction >= myMillis;
}

void showStartupData() {
  tft.fillScreen(TFT_BLACK);
  tft.drawCentreString("Retrieving SNTP time", 240, 160, 4);
  getLocalTime(&tInfo);
  tft.fillScreen(TFT_BLACK);
  tft.setTextColor(TFT_ORANGE);
  tft.drawCentreString("Retrieving Json Data", 240, 160, 4);
}

void changeView() {
  if (timeElapsed(800)) {  // UI debouncing (flash button)
    chart = !chart;
    chart ? show_Chart() : show_Data();
    showCurrentPrice();
    lastAction = millis();
  }
}

void display_Time() {
  char theDate[11], theTime[9];
  getLocalTime(&tInfo);  // SNTP sync at startup and every 3 hours thereafter (ESP32)
  time_t now;
  time(&now);  // assign current epoch time (UTC) to "now"
  tft.drawRect(-1, 26, 482, 2, WiFi.isConnected() ? TFT_GREEN : TFT_RED);
  strftime(theDate, sizeof(theDate), "%d-%m-%G", &tInfo);  // https://cplusplus.com/reference/ctime/strftime/
  strftime(theTime, sizeof(theTime), "%T", &tInfo);
  tft.setTextColor(TFT_GREEN, TFT_BLACK), tft.drawString(theDate, 2, 0, 4);
  tft.setTextColor(TFT_WHITE, TFT_BLACK), tft.drawString(theTime, 139, 0, 4);
  tft.drawRightString(bidLU ? "Lux" : "Bel", 480, 8, 2);
  if ((freshStart) || ((((epo[SHOW_ITEMS - 1] - now) / 3600) < 9) && (tInfo.tm_min == 2) && (tInfo.tm_sec == 1))) {
    if (WiFi.isConnected()) get_JsonData();  // ^^from 1200 hrs UTC: try to retrieve new data every hour until it succeeds
    chart ? show_Chart() : show_Data(), showCurrentPrice();
    freshStart = false;
  }
  if (tInfo.tm_min == 0) {
    if (tInfo.tm_sec == 0) tryDefaultZone();       // if not default bidding zone, try default zone every new hour
    if (tInfo.tm_sec == 1) cursorUpdated = false;  // reset update flag
  }
}

void tryDefaultZone() {  // if current bidding zone isn't the default bidding zone, try default zone
  if (!cursorUpdated) {  // prevent updating several times per cycle
    cursorUpdated = true;
    if (bidLU) {                         // if the current bidding zone isn't the default zone
      flash.begin("bid_zone", false);    // flash memory (false = also write)
      flash.putBool("bid_zone", false);  // store default bidding zone to flash memory
      flash.end();
      ESP.restart();  // restart because want to retrieve the data of the default bidding zone.
    }
    chart ? show_Chart() : show_Data();  // refresh local data every hour
    showCurrentPrice();
  }
}

void show_Chart() {
  tft.fillRect(0, 28, 480, 292, TFT_BLACK);       // erase any previous graphic data
  tft.setTextColor(tft.color565(160, 160, 160));  // set color once and adjust only when needed
  for (uint8_t i = 0; i < 6; i++) {               // draw horizontal grid lines and value labels
    tft.drawFastHLine(0, 40 + i * 50, 480, tft.color565(48, 48, 48));
    for (uint8_t j = 0; j < 4; j++) {
      tft.drawRightString(String(i * 100 - 100), j * 150 + 23, 286 - i * 50, 1);
    }
  }
  for (uint8_t i = 0; i < SHOW_ITEMS; i++) {
    if (epo[i]) {                           // only process if there is valid data
      struct tm* cor = localtime(&epo[i]);  // convert epoch to local time
      bool isCurrentHour = (cor->tm_hour == tInfo.tm_hour) && (cor->tm_mday == tInfo.tm_mday);
      if (isCurrentHour) {
        priceNow = prijzen[i];
        tft.drawFastVLine(6 + i * 14, 26, 292, TFT_MAGENTA);
        tft.fillTriangle((i * 14) - 4, 300, (i * 14) - 4, 310, 6 + i * 14, 305, TFT_MAGENTA);
        tft.fillTriangle((i * 14) - 4, 55, (i * 14) - 4, 65, 6 + i * 14, 60, TFT_MAGENTA);
      }
      uint16_t myColor;  // determine the color based on the value of prijzen[i]
      if (prijzen[i] < 0) myColor = TFT_YELLOW;
      else if (prijzen[i] < 50) myColor = TFT_GREEN;
      else if (prijzen[i] < VALUE_RED) myColor = TFT_CYAN;
      else myColor = TFT_RED;
      int price_scaled = prijzen[i] / 2;           // scale prices for display
      int height_offset = (480 - prijzen[i]) / 2;  // calculate offset for positive values
      if (prijzen[i] < 0) {                        // draw the price bar
        tft.fillRect(i * 14, 240, 13, 0 - price_scaled, myColor);
        tft.drawFastVLine(i * 14 + 13, 240, (0 - prijzen[i]) / 2, TFT_BLACK);
      } else {
        tft.fillRect(i * 14, height_offset, 13, price_scaled, myColor);
        tft.drawFastVLine(i * 14 + 13, height_offset, price_scaled, TFT_BLACK);
      }
      tft.setCursor(1 + i * 14, 230, 1);  // draw the hour label at the bottom of the chart
      tft.printf("%02d", cor->tm_hour);
      tft.setTextColor(TFT_RED);  // draw the hour label again (red)
      tft.setCursor(1 + i * 14, 243, 1);
      tft.printf("%02d", cor->tm_hour);
      tft.setTextColor(prijzen[i] > 0 ? TFT_BLACK : TFT_YELLOW);  // Reset text color to default
    }
  }
}

void show_Data() {  // display Json data in 2 columns (time / price)
  tft.fillRect(0, 28, 480, 292, TFT_BLACK);
  tft.drawRect(240, 26, 2, 292, WiFi.isConnected() ? TFT_GREEN : TFT_RED);
  struct tm* cor;
  uint16_t xPos, yPos;
  for (uint8_t i = 0; i < SHOW_ITEMS; i++) {
    if (epo[i]) {                 // check for valid data
      cor = localtime(&epo[i]);   // convert epoch to local time
      xPos = (i < 18) ? 0 : 260;  // calculate position for cursor
      yPos = 33 + (i % 18) * 16;
      tft.setTextColor(TFT_LIGHTGREY, TFT_BLACK);  // display index number
      tft.setCursor(xPos, yPos, 2);
      tft.printf(" %02d. ", i + 1);
      if ((cor->tm_hour == tInfo.tm_hour) && (cor->tm_mday == tInfo.tm_mday)) {  // check if current hour/day matches
        priceNow = prijzen[i];
        tft.setTextColor(TFT_MAGENTA, TFT_BLACK);
      } else tft.setTextColor(TFT_WHITE, TFT_BLACK);
      tft.printf("%02d-%02d-%02d %02d:%02d", cor->tm_mday, cor->tm_mon + 1, cor->tm_year + 1900, cor->tm_hour, cor->tm_min);
      if (prijzen[i] < 0) tft.setTextColor(TFT_YELLOW, TFT_BLACK);  // set color based on price value
      else if (prijzen[i] < 50) tft.setTextColor(TFT_GREEN, TFT_BLACK);
      else if (prijzen[i] < VALUE_RED) tft.setTextColor(TFT_CYAN, TFT_BLACK);
      else tft.setTextColor(TFT_RED, TFT_BLACK);
      tft.drawRightString(String(prijzen[i]), (i < 18 ? 220 : 480), yPos, 2);  // display the price on the right
    }
  }
}

void showCurrentPrice() {  // price (upper right corner of display)
  tft.setCursor(264, 0, 4);
  tft.setTextColor(TFT_SILVER, TFT_BLACK);
  tft.print("/MWh:");
  tft.fillCircle(255, 10, 9, TFT_SILVER);  // euro symbol
  tft.fillCircle(255, 10, 7, TFT_BLACK), tft.fillRect(258, 5, 8, 12, TFT_BLACK);
  tft.drawRect(247, 8, 9, 2, TFT_SILVER), tft.drawRect(249, 11, 6, 2, TFT_SILVER);
  tft.setTextColor(priceNow < VALUE_RED ? TFT_CYAN : TFT_RED);
  if (priceNow < 50) tft.setTextColor(TFT_GREEN);
  if (priceNow < 0) tft.setTextColor(TFT_YELLOW);
  tft.fillRect(344, 0, 100, 22, TFT_BLACK);
  tft.setCursor(344, 0, 4);
  tft.printf("%3.2F", priceNow);
}

void get_JsonData() {
  String my_link;
  uint8_t API_ITEMS;
  memset(prijzen, 0, sizeof(prijzen));
  memset(prices, 0, sizeof(prices));
  memset(epo, 0, sizeof(epo));
  memset(epoc, 0, sizeof(epoc));
  time_t now = mktime(&tInfo) - (mktime(&tInfo) % 3600);  // convert "struct tm" to time_t and round down to the whole hour
  my_link = String("https://api.energy-charts.info/price?bzn=") + biddingZone[bidLU] + "&start="
            + String(now - (24 * 3600)) + "&end="
            + String(now + (SHOW_ITEMS * 3600));
  HTTPClient http;
  http.begin(my_link);
  if (timeElapsed(3000)) {  // allows the http connection to be complete
    lastAction = millis();
    if (http.GET() >= 200 && http.GET() < 300) {
      JsonDocument doc;
      String json = http.getString();
      auto error = deserializeJson(doc, json.c_str());
      if (error) {
        tft.drawCentreString("deserializeJson() failed", 240, 200, 4);
        tft.drawCentreString(error.c_str(), 240, 250, 4);
      }
      for (uint8_t i = 0; i < SHOW_ITEMS * 2; i++) {  // actually more data than we can show on display
        prices[i] = doc["price"][i];
        epoc[i] = doc["unix_seconds"][i];
        API_ITEMS = i;  // count number of returned items
      }
    } else {  // http.get has failed
      tft.fillScreen(TFT_BLACK);
      tft.drawCentreString("Error in API response", 240, 160, 4);
      tft.drawCentreString("Restarting device", 240, 190, 4);
      flash.begin("bid_zone", false);     // true = read only
      flash.putBool("bid_zone", !bidLU);  // try other bidding zone
      flash.end();
      delay(1000);
      ESP.restart();
    }
  }
  http.end();
  uint8_t j = SHOW_ITEMS + 1;
  for (uint8_t i = API_ITEMS; i >= 0; i--) {  // select the most recent Json data
    if (epoc[i] != 0) {                       // ignore empty data
      j--;                                    // counter for results
      prijzen[j - 1] = prices[i];             // transferring valid prices from API results
      epo[j - 1] = epoc[i];                   // also hours to "local" array
      if (j == 0) break;                      // stop when we have the [SHOW_ITEMS] most recent items
    }
  }
}

void messageNoConnection(WiFiManager* myWiFi) {
  const char* connect[] = { "WiFi: no connection", "Connect to hotspot", this_Ssid, "Start a browser - address",
                      "192.168.4.1", "to save network", "and password" };
  tft.fillScreen(TFT_NAVY), tft.setTextColor(TFT_YELLOW);
  for (uint8_t i = 0; i < 7; i++) tft.drawCentreString(connect[i], 240, 40 + i * 30, 4);
}

More displays with the ESP8266 / ESP32

Curious about other displays? Check out the full overview, including TM1637, SSD1306, SH1106, ST7735, ST7789, e-paper and integrated displays like the Lilygo T-Display and the Waveshare ESP32-S3.