Home IoT Hub Series · 21

[Expansion] Color TFT Display Upgrade with 2.4-inch ILI9341

Upgrade the hub display from a small OLED to a larger color ILI9341 TFT screen.

This expansion replaces the tiny OLED feeling with a color TFT display. The goal is a more readable hub screen with icons, colors, and clearer layout.

TFT wiring uses more pins than OLED, so check the display driver, voltage, and SPI pins before uploading.

[Expansion] Color TFT Display Upgrade with 2.4-inch ILI9341 hero image

Parts and cost

PartQtyPriceNote
ILI9341 2.4inch TFT LCD (SPI)1 pc$3.60240×320, the non-touch version is enough
Jumper wires (female-to-female)7 pcnoteOLED note
Total$3.60

Pin wiring

[Expansion] Color TFT Display Upgrade with 2.4-inch ILI9341 wiring diagram

Full code

[Expansion] Color TFT Display Upgrade with 2.4-inch ILI9341 - Code 2
// only the required part of User_Setup.h
#define ILI9341_DRIVER

#define TFT_CS   15  // D8
#define TFT_DC   0   // D3
#define TFT_RST  -1  // use the NodeMCU RST pin

#define SPI_FREQUENCY  40000000
#define SPI_READ_FREQUENCY  20000000
#define SPI_TOUCH_FREQUENCY  2500000
[Expansion] Color TFT Display Upgrade with 2.4-inch ILI9341 - Code 3
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <TFT_eSPI.h>

// =============================================
// Wi-Fi settings (enter yours)
// =============================================
const char* ssid     = "YOUR_SSID";
const char* password = "YOUR_PASSWORD";
const char* ap_ssid     = "IoT_Hub_Net";
const char* ap_password = "iothub1234";

// =============================================
// hardware pin
// =============================================
#define BUZZER_PIN 13  // D7 (GPIO13)

TFT_eSPI tft = TFT_eSPI();
ESP8266WebServer server(80);

// =============================================
// 20 pc note
// =============================================
#define SLOT_COUNT 20
String slot[SLOT_COUNT] = {
  "--", "--", "--", "--", "--",
  "--", "--", "--", "--", "--",
  "--", "--", "--", "--", "--",
  "--", "--", "--", "--", "--"
};

const char* label[SLOT_COUNT] = {
  "Living Temp","Living Hum","Outside Temp","AC",
  "Boiler","Stove","Gas Leak","Fire",
  "Front Door","Door Lock","Living Light","Fridge Temp",
  "CO2","Dust PM2.5","Power","Water",
  "Hot Water","Rain","Bath Hum","Pet Bowl"
};

// =============================================
// colors (RGB565)
// =============================================
#define C_BG       TFT_BLACK
#define C_TITLE_BG TFT_WHITE
#define C_CARD     TFT_DARKGREEN
#define C_CARD_WARN TFT_RED
#define C_LABEL    TFT_SILVER
#define C_VALUE    TFT_YELLOW

// =============================================
// note
// =============================================
unsigned long lastScroll = 0;
unsigned long lastBuzzer = 0;
int scrollOffset = 0;
const int ITEMS_PER_PAGE = 4;
const int SCROLL_INTERVAL = 3000;
bool warningActive = false;

// =============================================
// setup
// =============================================
void setup() {
  Serial.begin(115200);

  pinMode(BUZZER_PIN, OUTPUT);
  digitalWrite(BUZZER_PIN, LOW);

  // TFT secondsnote
  tft.init();
  tft.setRotation(1); // note×320)
  tft.fillScreen(C_BG);
  tft.setTextWrap(false);

  // note
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.drawString("IoT Hub v3.0", 70, 140, 4);
  tft.drawString("iothub.co.kr", 80, 180, 2);
  delay(1000);

  // Wi-Fi connection
  WiFi.mode(WIFI_AP_STA);
  WiFi.begin(ssid, password);

  tft.fillScreen(C_BG);
  tft.drawString("WiFi connecting...", 30, 100, 2);
  Serial.print("Connecting to WiFi");
  int dots = 0;
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    tft.drawString(".", 180 + dots * 8, 100, 2);
    dots++;
  }
  Serial.println();
  Serial.print("STA IP: ");
  Serial.println(WiFi.localIP());

  WiFi.softAP(ap_ssid, ap_password);
  Serial.print("AP  IP: ");
  Serial.println(WiFi.softAPIP());

  // web server
  server.on("/set", handleSet);
  server.on("/", handleRoot);
  server.on("/status", handleStatus);
  server.onNotFound(handleNotFound);
  server.begin();
  Serial.println("HTTP server started");

  // IP note
  tft.fillScreen(C_BG);
  tft.drawString("IoT Hub Ready", 20, 30, 4);
  tft.drawString("LAN: " + WiFi.localIP().toString(), 10, 90, 2);
  tft.drawString("AP : " + WiFi.softAPIP().toString(), 10, 120, 2);
  delay(3000);
}

// =============================================
// loop
// =============================================
void loop() {
  server.handleClient();
  updateTFT();
  updateBuzzer();
}

// =============================================
// alert status check
// =============================================
bool checkWarning() {
  for (int i = 0; i < SLOT_COUNT; i++) {
    if ((i == 6 || i == 7 || i == 8)
        && slot[i] != "0" && slot[i] != "--") return true;
    if (i == 19 && slot[i] != "--" && slot[i].toInt() < 50) return true;
  }
  return false;
}

// =============================================
// buzzer note
// =============================================
void updateBuzzer() {
  warningActive = checkWarning();
  if (warningActive) {
    unsigned long now = millis();
    if (now - lastBuzzer < 500) return;
    lastBuzzer = now;
    digitalWrite(BUZZER_PIN, !digitalRead(BUZZER_PIN));
  } else {
    digitalWrite(BUZZER_PIN, LOW);
  }
}

// =============================================
// GET /set?no1=value&no2=value ...
// =============================================
void handleSet() {
  int updated = 0;
  for (int i = 1; i <= SLOT_COUNT; i++) {
    String paramName = "no" + String(i);
    if (server.hasArg(paramName)) {
      slot[i - 1] = server.arg(paramName);
      updated++;
    }
  }
  server.send(200, "application/json", "{\"updated\":" + String(updated) + "}");
}

// =============================================
// GET / → HTML note
// =============================================
void handleRoot() {
  String html = "<!DOCTYPE html><html lang=\"ko\"><head>"
  "<meta charset=\"UTF-8\">"
  "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">"
  "<title>IoT Hub TFT</title>"
  "<style>"
  "*{margin:0;padding:0;box-sizing:border-box;}"
  "body{font-family:system-ui,sans-serif;background:#1a1a2e;color:#eee;padding:16px;}"
  "h1{text-align:center;font-size:1.3rem;margin-bottom:4px;color:#e94560;}"
  ".sub{text-align:center;font-size:0.7rem;color:#555;margin-bottom:16px;}"
  ".grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(185px,1fr));gap:10px;}"
  ".card{background:#16213e;border-radius:8px;padding:12px;border-left:4px solid #0f3460;}"
  ".card.warn{border-left-color:#e94560;background:#1c0e1a;}"
  ".card .no{font-size:0.6rem;color:#555;margin-bottom:2px;}"
  ".card .label{font-size:0.8rem;color:#a0a0b0;margin-bottom:4px;}"
  ".card .value{font-size:1.2rem;font-weight:700;}"
  ".footer{text-align:center;margin-top:16px;font-size:0.7rem;color:#444;}"
  "</style></head><body>"
  "<h1>IoT Hub</h1><div class=\"sub\">TFT Edition</div>"
  "<div class=\"grid\">";

  for (int i = 0; i < SLOT_COUNT; i++) {
    bool isWarn = false;
    String val = slot[i];
    if ((i == 6 || i == 7 || i == 8) && val != "0" && val != "--") isWarn = true;
    if (i == 19 && val != "--" && val.toInt() < 50) isWarn = true;
    html += "<div class=\"card" + String(isWarn ? " warn" : "") + "\">";
    html += "<div class=\"no\">#" + String(i + 1) + "</div>";
    html += "<div class=\"label\">" + String(label[i]) + "</div>";
    html += "<div class=\"value\">" + val + "</div></div>\n";
  }

  html += "</div><div class=\"footer\">IoT Hub TFT | iothub.co.kr</div></body></html>";
  server.send(200, "text/html; charset=utf-8", html);
}

// =============================================
// GET /status → JSON
// =============================================
void handleStatus() {
  String json = "{";
  for (int i = 0; i < SLOT_COUNT; i++) {
    if (i > 0) json += ",";
    json += "\"no" + String(i + 1) + "\":\"" + slot[i] + "\"";
  }
  json += ",\"warning\":" + String(warningActive ? "true" : "false") + "}";
  server.send(200, "application/json", json);
}

// =============================================
// 404
// =============================================
void handleNotFound() {
  server.send(404, "text/plain", "404 - Use /set?no1=value  or  /  or  /status");
}

// =============================================
// TFT note
// =============================================
void updateTFT() {
  unsigned long now = millis();
  if (now - lastScroll < SCROLL_INTERVAL) return;
  lastScroll = now;

  tft.fillScreen(C_BG);

  // note
  tft.fillRect(0, 0, 240, 28, TFT_WHITE);
  tft.setTextColor(TFT_BLACK, TFT_WHITE);
  char buf[40];
  sprintf(buf, "IoT Hub  [%d/%d]",
    scrollOffset / ITEMS_PER_PAGE + 1,
    (SLOT_COUNT + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE);
  tft.drawString(buf, 4, 6, 2);

  for (int i = 0; i < ITEMS_PER_PAGE; i++) {
    int idx = scrollOffset + i;
    if (idx >= SLOT_COUNT) break;

    int y = 36 + i * 52;
    String val = slot[idx];

    bool isWarn = false;
    uint16_t cardBg = C_CARD;
    if ((idx == 6 || idx == 7 || idx == 8) && val != "0" && val != "--") {
      isWarn = true; cardBg = C_CARD_WARN;
    }
    if (idx == 19 && val != "--" && val.toInt() < 50) {
      isWarn = true; cardBg = C_CARD_WARN;
    }

    // card note
    tft.fillRoundRect(4, y, 232, 48, 6, cardBg);

    // note
    tft.setTextColor(C_LABEL, cardBg);
    tft.drawString(label[idx], 12, y + 4, 2);

    // note
    tft.drawLine(12, y + 24, 228, y + 24, TFT_WHITE);

    // value (note
    tft.setTextColor(isWarn ? TFT_WHITE : C_VALUE, cardBg);
    tft.drawString(val, 16, y + 28, 4);

    // note
    tft.setTextColor(C_LABEL, cardBg);
    tft.drawString(getUnit(idx), 180, y + 28, 2);
  }

  scrollOffset += ITEMS_PER_PAGE;
  if (scrollOffset >= SLOT_COUNT) scrollOffset = 0;
}

// note
String getUnit(int idx) {
  switch (idx) {
    case 0: case 2: case 11: case 16: return "C";
    case 1: case 18: return "%";
    case 12: return "ppm";
    case 13: return "ug";
    case 14: return "W";
    case 15: return "L";
    case 19: return "g";
    default: return "";
  }
}

Build notes

  • Upload a simple color test first. Once the screen works, move the hub dashboard drawing code onto the TFT.
  • Power on the hub brain first so the node can join IoT_Hub_Net.
  • Use the same slot number written in the source code.
  • Open the serial monitor at 115200 baud and confirm HTTP 200.
  • If the value does not appear on the hub, recheck wiring, GPIO number, and Wi-Fi connection.