↩ Alle projecten en sketches This page in English
ESP32 · TFT_eSPI · touchscreen

ESP32-3248S035: geïntegreerd display (480 × 320px)

ESP32-3248S035 met geïntegreerd 3.5 inch aanraakscherm
ESP32-3248S035 op AliExpress — 3.5 inch capacitief aanraakscherm, 480×320px

Een handige combinatie van een ESP32 en een relatief groot aanraakscherm — hier gebruikt om live dynamische elektriciteitsprijzen weer te geven.

Wat heb je nodig?

ESP32-3248S035: geïntegreerd display (480 × 320px)

Dit is een handige combinatie van een ESP32 en een relatief groot aanraakscherm. Je vindt weinig voorbeelden van deze combinatie voor de Arduino IDE.

Het touch-gedeelte gebruiken we in dit voorbeeld niet (maar dat werkt wel perfect), je kan dat gewoon laden (en testen) uit de voorbeelden van de bibliotheek "TFT_eSPI".

In deze schets halen we via een API de dynamische elektriciteitsprijzen op (Europa), om ze op het display weer te geven. We kunnen met de flashknop de weergave wijzigen in numerieke gegevens of grafiek.

Uiteraard is er geen verbindingsschema om weer te geven, maar het is wel een hele zoektocht als je dit systeem wil configureren voor de Arduino IDE. We gebruiken weer de bibliotheek TFT-eSPI (Bodmer). Je moet een bestand "User_Setup.h" in de map van de bibliotheek "TFT_eSPI" plaatsen.

Inhoud van het bestand "User_Setup.h" voor de 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 — opgelet Om hier een gemakkelijk overzicht weer te geven passen we de inhoud van het bestand "User_Setup.h" aan. Dat is een snelle oplossing als je 1 scherm aan de praat wil krijgen en vlug resultaat wil zien.

Als je meerdere displays gebruikt en/of geen gegevens wil verliezen als je een nieuwe kopij van deze bibliotheek installeert, kan je beter de tip volgen van de ontwerper van de bibliotheek: github.com/Bodmer/TFT_eSPI/#tips.

Je kan de volledige inhoud van "User_Setup.h" uitcommenten als je zo werkt.

Live elektriciteitsprijzen op het scherm

De sketch verbindt via WiFiManager, haalt daarna elk uur (tussen 12:00 en 21:00 UTC, wanneer nieuwe prijzen voor de volgende dag beschikbaar komen) de actuele dynamische elektriciteitsprijzen op via een API, en toont die als grafiek of als cijfers in twee kolommen — te wisselen met de flash-knop. Als de eerste biedzone geen resultaten meer oplevert, probeert het script automatisch de tweede biedzone.

/*==========================================================================================
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);
}

Meer displays met de ESP8266 / ESP32

Benieuwd naar andere displays? Bekijk het volledige overzicht met o.a. TM1637, SSD1306, SH1106, ST7735, ST7789, e-paper en geïntegreerde displays zoals de Lilygo T-Display en de Waveshare ESP32-S3.