本文将深入探讨如何使用 ESP8266 微控制器、ILI9341 TFT LCD 显示屏(GBR 颜色格式)以及 Adafruit_GFX 和 Adafruit_ILI9341 库,构建一个功能完备、可通过 Wi-Fi 网络远程控制的多行文本显示系统。我们将详细讲解代码的各个组成部分、工作原理、配置方法、使用说明,并提供一些扩展思路。

1. 项目概述

本项目实现了一个运行在 ESP8266 上的小型 Web 服务器。用户可以通过任何支持 HTTP POST 请求的设备(例如,计算机、智能手机、平板电脑)向 ESP8266 发送特定格式的数据,从而动态控制 ILI9341 显示屏上显示的文本内容、颜色(支持十六进制代码和预定义颜色名称)以及字体大小。

主要功能和特点:

  • Wi-Fi 远程控制: 通过 Wi-Fi 网络,从任何连接到同一网络的设备控制显示屏。
  • 多行文本显示: 支持同时显示多行文本,行数可动态调整。
  • GBR 颜色格式: 针对特定批次或型号的 ILI9341 显示屏,这些显示屏使用 GBR 而非标准的 RGB565 颜色格式。代码中提供了切换颜色格式的选项。
  • 颜色定制: 可以为每行文本独立设置颜色。
  • 字体大小可调: 每行文本的字体大小可以在 1-7 之间调整。
  • 左对齐: 文本内容左对齐显示。
  • 可调行间距: 可以调整行与行之间的垂直间距(当前设置为 15 像素)。
  • 动态更新: 显示内容实时更新,无需重启 ESP8266。
  • 易于使用: 通过简单的 HTTP POST 请求即可控制显示屏,无需编写复杂的客户端程序。
  • 可扩展性: 代码结构清晰,易于添加新功能,例如:
    • 支持更多显示效果(居中、右对齐、滚动等)。
    • 集成传感器数据读取。
    • 添加更友好的 Web 用户界面。

2. 硬件准备

  • ESP8266 开发板: 任何基于 ESP8266 的开发板均可,例如 NodeMCU、WeMos D1 mini 等。
  • ILI9341 TFT LCD 显示屏: 2.2 英寸、2.4 英寸、2.8 英寸等尺寸的 ILI9341 显示屏均可。重要提示: 请务必确认您的 ILI9341 显示屏使用的是 GBR 颜色格式还是标准的 RGB565 格式。本文提供的代码默认支持 GBR 格式,但可以通过修改 useGBR 变量来切换到 RGB565 格式。
  • 杜邦线: 用于连接 ESP8266 和 ILI9341 显示屏。

接线图:

ESP8266 引脚 ILI9341 引脚 说明
D8 CS 片选信号 (Chip Select)
D1 DC (或 A0) 数据/命令选择
D2 RST 复位信号 (Reset)
D7 (MOSI) SDA 数据输入 (Serial Data)
D6 (MISO) (通常不连接) 数据输出 (通常不需要)
D5 (SCK) SCL 时钟信号 (Serial Clock)
3V3 VCC 电源正极 (3.3V)
GND GND 电源负极 (Ground)
3v3 LED/BLK 背光控制 (可选)

注意:

  • 不同型号的 ESP8266 开发板和 ILI9341 显示屏的引脚名称可能略有差异,请参考您使用的具体硬件的数据手册。
  • ILI9341 的 MOSI、MISO 和 SCK 引脚通常与 ESP8266 的硬件 SPI 引脚(D5-D7)相连,以实现高速通信。
  • 背光控制引脚(LED/BLK)可以连接到 ESP8266 的 3.3V 引脚(常亮背光),也可以连接到 ESP8266 的 PWM 引脚,以实现背光亮度调节。

3. 软件准备

  • Arduino IDE: 用于编写、编译和上传代码到 ESP8266。
  • ESP8266 开发板支持包: 在 Arduino IDE 中安装 ESP8266 开发板支持包。
  • 库:
    • Adafruit_GFX: Adafruit 提供的通用图形库。
    • Adafruit_ILI9341: Adafruit 提供的 ILI9341 显示屏驱动库。
    • ESP8266WiFi: ESP8266 Wi-Fi 功能库。
    • ESP8266WebServer: ESP8266 Web 服务器库。
    • map: C++标准库

在 Arduino IDE 中,通过“工具”->“库管理器”搜索并安装这些库。

4. 代码详解

4.1 代码结构

代码主要由以下几个部分组成:

  • 引入库: 包含项目所需的库文件。
  • 全局常量和变量: 定义 Wi-Fi SSID 和密码、引脚定义、显示屏对象、Web 服务器对象、数据结构等。
  • 函数声明: 声明项目中使用到的函数。
  • setup() 函数: 初始化硬件、连接 Wi-Fi、启动 Web 服务器。
  • loop() 函数: 循环处理客户端请求。
  • 辅助函数:
    • connectToWiFi(): 连接到 Wi-Fi 网络。
    • handleSet(): 处理客户端发送的设置显示内容的请求。
    • handleNotFound(): 处理未找到的请求(404 错误)。
    • updateDisplay(): 将数据更新到 ILI9341 显示屏上。
    • parseColor(): 解析颜色字符串(十六进制或预定义名称)。

4.2 关键代码段解析

1. 全局变量:

struct LineData {
    String text;
    uint16_t color;
    uint8_t size;
    int16_t y;
};

std::map<int, LineData> lines;
bool useGBR = true; // 关键:控制颜色格式
  • LineData 结构体:用于存储每一行文本的数据(文本内容、颜色、字体大小、y 坐标)。
  • linesstd::map 类型,用于存储所有行的数据。 使用 map 可以方便地根据行号(键)来访问和更新每一行的数据,并且 map 会自动按键(行号)排序。
  • useGBR关键变量。 true 表示使用 GBR 颜色格式,false 表示使用 RGB565 格式。 请根据您的 ILI9341 显示屏的实际格式进行设置。

2. handleSet() 函数:

void handleSet() {
    lines.clear();

    for (int i = 0; i < 100; i++) {
        String lineKey = "line" + String(i);

        if (server.hasArg(lineKey)) {
            LineData line;
            line.text = server.arg(lineKey);

            String colorKey = "color" + String(i);
            if (server.hasArg(colorKey)) {
                line.color = parseColor(server.arg(colorKey));
            } else {
                line.color = ILI9341_WHITE;
            }

            String sizeKey = "size" + String(i);
            if (server.hasArg(sizeKey)) {
                int size = server.arg(sizeKey).toInt();
                line.size = (size >= 1 && size <= 7) ? size : 2;
            } else {
                line.size = 2;
            }

            lines[i] = line;
        }
    }

    updateDisplay();
    server.send(200, "text/plain", "OK");
}
  • 核心逻辑:
    • 循环遍历可能的行号(0-99)。
    • 只处理存在 lineN 参数的行。
    • 对于每一行,获取文本内容(lineN)、颜色(colorN,如果存在)和字体大小(sizeN,如果存在)。
    • 如果 colorN 或 sizeN 不存在,则使用默认值(白色、大小 2)。
    • 将每一行的数据存储到 lines map 中。
  • 优点:
    • 灵活:支持任意行数的设置,支持跳行设置(例如,直接设置 line0 和 line3,而没有 line1 和 line2)。
    • 高效:只处理提供了数据的行,避免了不必要的计算。
    • 健壮:对 sizeN 参数进行了范围检查,防止无效的字体大小。

3. parseColor() 函数:

uint16_t parseColor(String colorString) {
    // ... (预定义颜色名称处理) ...

    if (colorString.startsWith("#") && colorString.length() == 7) {
        long number = strtol(colorString.substring(1).c_str(), NULL, 16);
        uint8_t r = (number >> 16) & 0xFF;
        uint8_t g = (number >> 8) & 0xFF;
        uint8_t b = number & 0xFF;
        uint16_t color565;

        if (useGBR) {
            // GBR 转换:  G(5) B(6) R(5)
            color565 = ((g & 0xF8) << 8) | ((b & 0xFC) << 3) | (r >> 3);
            Serial.println("Using GBR conversion");
        } else {
            // RGB565 转换 (默认)
            color565 = tft.color565(r, g, b);
             Serial.println("Using RGB conversion");
        }

        // ... (串口调试输出) ...

        return color565;
    }
    return ILI9341_WHITE;
}
  • GBR 转换 (关键): color565 = ((g & 0xF8) << 8) | ((b & 0xFC) << 3) | (r >> 3);
    • 将 8 位的 G、B、R 值转换为 16 位的 GBR565 格式。
  • RGB565 转换 (备用): color565 = tft.color565(r, g, b); 如果 useGBR 为 false,则执行此代码。
  • 调试输出: 便于调试颜色问题

4. updateDisplay() 函数:

void updateDisplay() {
    tft.fillScreen(ILI9341_BLACK);
    int16_t yPos = 20;

    for (auto& [lineNum, line] : lines) {
        line.y = yPos;
        int16_t xPos = 20; // 左对齐

        tft.setCursor(xPos, line.y);
        tft.setTextColor(line.color);
        tft.setTextSize(line.size);
        tft.println(line.text);

        yPos += line.size * 6 * (tft.getRotation() % 2 == 0 ? 1 : 2) + 15; // 可调行间距
    }
}
  • 左对齐: xPos 固定为 20 像素。
  • 可调行间距: yPos 的计算公式中,+ 15 控制行间距。

5. 使用说明

  1. 配置 Wi-Fi:

    • 在代码中找到 ssid 和 password 变量,将它们修改为您自己的 Wi-Fi 网络的名称和密码。
  2. 设置颜色格式:

    • 如果您确定您的 ILI9341 显示屏使用 GBR 颜色格式,请确保 useGBR 变量设置为 true
    • 如果您确定您的 ILI9341 显示屏使用标准的 RGB565 颜色格式,请将 useGBR 变量设置为 false
  3. 调整显示参数(可选):

    • 屏幕方向: 您可以通过修改 setup() 函数中的 tft.setRotation(0) 来更改屏幕方向。0 表示竖屏,1 表示横屏,2 表示竖屏反向,3 表示横屏反向。
    • 左边距: 您可以通过修改 updateDisplay() 函数中的 xPos 变量来调整文本的左边距。
    • 行间距: 您可以通过修改 updateDisplay() 函数中的 yPos 计算公式中的 + 15 来调整行间距。
  4. 上传代码:

    • 将 ESP8266 开发板连接到计算机。
    • 在 Arduino IDE 中选择正确的开发板和端口。
    • 点击“上传”按钮,将代码上传到 ESP8266。
  5. 获取 ESP8266 的 IP 地址:

    • 打开 Arduino IDE 的串口监视器(波特率设置为 115200)。
    • ESP8266 启动后,会自动连接到 Wi-Fi 网络。
    • 连接成功后,串口监视器会显示 ESP8266 的 IP 地址。
  6. 发送控制命令:

    • 使用提供的 Python 脚本示例(或其他任何可以发送 HTTP POST 请求的工具,例如 Postman、curl)。
    • 修改 Python 脚本中的 esp8266_ip 变量,将其替换为您在上一步中获取到的 ESP8266 的 IP 地址。
    • 根据需要修改 Python 脚本中的 data 字典,设置要显示的文本、颜色和字体大小。
      • lineN: 行号(从 0 开始)。
      • colorN: 颜色(十六进制颜色代码,如 #FF0000 表示红色;或预定义颜色名称,如 red)。
      • sizeN: 字体大小(1-7)。
    • 运行 Python 脚本。

Python 脚本示例 (GBR 格式下):

import requests

esp8266_ip = "192.168.31.131"  # 将此处的 IP 地址替换为您的 ESP8266 的实际 IP 地址
url = f"http://{esp8266_ip}/set"

# 要发送的数据 (GBR颜色)
data = {
    "line0": "Red (GBR)",
    "color0": "#00FF00",  # 在 GBR 格式下,这将显示为红色
    "size0": "2",

    "line1": "Green (GBR)",
    "color1": "#FF0000",  # 在 GBR 格式下,这将显示为绿色
    "size1": "3",

    "line2": "Blue (GBR)",
    "color2": "#0000FF",  # 蓝色
    "size2": "4",

     "line3": "Yellow (GBR)",
    "color3": "#00FFFF",  # GBR格式下的黄色
    "size3": "1",
}

try:
    response = requests.post(url, data=data)
    response.raise_for_status()  # 如果请求失败,会引发异常
    print("请求成功!")
    print(f"响应状态码: {response.status_code}")
    print(f"响应内容: {response.text}")

except requests.exceptions.RequestException as e:
    print(f"请求失败: {e}")

RGB565格式的Python脚本(useGBR = false):

import requests

esp8266_ip = "192.168.31.131"  # 将此处的 IP 地址替换为您的 ESP8266 的实际 IP 地址
url = f"http://{esp8266_ip}/set"

# 要发送的数据 (RGB565)
data = {
    "line0": "Red (RGB)",
    "color0": "#FF0000",  # 红色
    "size0": "2",

    "line1": "Green (RGB)",
    "color1": "#00FF00",  # 绿色
    "size1": "3",

    "line2": "Blue (RGB)",
    "color2": "#0000FF",  # 蓝色
    "size2": "4",

     "line3": "Yellow (RGB)",
    "color3": "#FFFF00",  # 黄色
    "size3": "1",
}

try:
    response = requests.post(url, data=data)
    response.raise_for_status()  # 如果请求失败,会引发异常
    print("请求成功!")
    print(f"响应状态码: {response.status_code}")
    print(f"响应内容: {response.text}")

except requests.exceptions.RequestException as e:
    print(f"请求失败: {e}")

重要提示:

  • GBR 颜色格式: 如果您的显示屏使用 GBR 格式,您需要使用 GBR 格式对应的十六进制颜色代码。例如,要显示红色,您应该使用 #00FF00(在 RGB 格式中,这是绿色)。
  • RGB565 颜色格式: 如果您将代码中的 useGBR 设置为 false,则应使用标准的 RGB565 十六进制颜色代码。
  • 跳行设置: 您可以跳过某些行号。例如,您可以只设置 line0line2 和 line5,而跳过 line1line3 和 line4
  • 参数名唯一性: 确保 data 字典中的每个键(lineNcolorNsizeN)都是唯一的。

6. 扩展思路

  • 更多对齐方式: 除了左对齐,还可以实现居中对齐和右对齐。
  • 滚动文本: 实现单行或多行文本的水平或垂直滚动。
  • 显示图片: 使用更高级的图形库(如 LVGL),可以在显示屏上显示图片。
  • 传感器数据: 将 ESP8266 与各种传感器(如温度、湿度、光照传感器)连接,并将传感器数据实时显示在屏幕上。
  • Web 用户界面: 使用 HTML、CSS 和 JavaScript 构建一个更友好的 Web 用户界面,方便用户通过浏览器控制显示屏。
  • MQTT 集成: 将显示屏集成到智能家居系统中,通过 MQTT 协议与其他设备通信。
  • OTA 更新: 添加 OTA(Over-The-Air)无线更新功能,可以通过 Wi-Fi 更新 ESP8266 上的固件,无需连接 USB 线。

7. 总结

本项目提供了一个功能完备、易于使用且可扩展的基于 ESP8266 和 ILI9341 的 Wi-Fi 可控多行文本显示解决方案。通过灵活的参数设置和 GBR/RGB565 颜色格式的支持,用户可以轻松地定制显示内容。希望这篇文章能帮助您理解并构建自己的 Wi-Fi 控制显示屏!

完整的 Arduino 代码

#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <map>

const char* ssid = "yang1234";
const char* password = "y123456789";

#define TFT_CS   D8
#define TFT_DC   D1
#define TFT_RST  D2

Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
ESP8266WebServer server(80);

struct LineData {
    String text;
    uint16_t color;
    uint8_t size;
    int16_t y;
};

std::map<int, LineData> lines;

// 全局变量,控制是否使用 GBR 格式
bool useGBR = true; //  GBR 格式

void handleSet();
void handleNotFound();
void updateDisplay();
void connectToWiFi();
uint16_t parseColor(String colorString);

void setup() {
    Serial.begin(115200);
    tft.begin();
    SPI.setFrequency(1000000);
    tft.setRotation(0);

    uint8_t caset_data[] = {0x00, 0x00, 0x00, 0xEF};
    tft.sendCommand(ILI9341_CASET, caset_data, 4);
    uint8_t raset_data[] = {0x00, 0x00, 0x01, 0x3F};
    tft.sendCommand(ILI9341_PASET, raset_data, 4);
    tft.writeCommand(ILI9341_RAMWR);

    uint8_t madctl_data = 0xE8;
    tft.sendCommand(ILI9341_MADCTL, &madctl_data, 1);

    tft.fillScreen(ILI9341_BLACK);
    connectToWiFi();

    server.on("/set", HTTP_POST, handleSet);
    server.onNotFound(handleNotFound);
    server.begin();
    Serial.println("HTTP server started");
    updateDisplay();
}

void loop() {
    server.handleClient();
}

void connectToWiFi() {
    WiFi.begin(ssid, password);
    Serial.print("Connecting to Wi-Fi");
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    Serial.println("\nConnected! IP address:");
    Serial.println(WiFi.localIP());
}

void handleSet() {
    lines.clear();

    for (int i = 0; i < 100; i++) {
        String lineKey = "line" + String(i);

        if (server.hasArg(lineKey)) {
            LineData line;
            line.text = server.arg(lineKey);

            String colorKey = "color" + String(i);
            if (server.hasArg(colorKey)) {
                line.color = parseColor(server.arg(colorKey));
            } else {
                line.color = ILI9341_WHITE;
            }

            String sizeKey = "size" + String(i);
            if (server.hasArg(sizeKey)) {
                int size = server.arg(sizeKey).toInt();
                line.size = (size >= 1 && size <= 7) ? size : 2;
            } else {
                line.size = 2;
            }

            lines[i] = line;
        }
    }

    updateDisplay();
    server.send(200, "text/plain", "OK");
}

void handleNotFound() {
    server.send(404, "text/plain", "404 Not Found");
}

void updateDisplay() {
    tft.fillScreen(ILI9341_BLACK);
    int16_t yPos = 20;

    for (auto& [lineNum, line] : lines) {
        line.y = yPos;

         // --- 左对齐 ---
        int16_t xPos = 20; // 左边距为 20 像素

        tft.setCursor(xPos, line.y);
        tft.setTextColor(line.color);
        tft.setTextSize(line.size);
        tft.println(line.text);

        yPos += line.size * 6 * (tft.getRotation() % 2 == 0 ? 1 : 2) + 15; // 15 像素行间距
    }
}
uint16_t parseColor(String colorString) {
    if (colorString == "red")       return ILI9341_RED;
    if (colorString == "green")     return ILI9341_GREEN;
    if (colorString == "blue")      return ILI9341_BLUE;
    if (colorString == "yellow")    return ILI9341_YELLOW;
    if (colorString == "cyan")      return ILI9341_CYAN;
    if (colorString == "magenta")   return ILI9341_MAGENTA;
    if (colorString == "white")     return ILI9341_WHITE;
    if (colorString == "black")     return ILI9341_BLACK;
    if (colorString == "orange")    return ILI9341_ORANGE;

    if (colorString.startsWith("#") && colorString.length() == 7) {
        long number = strtol(colorString.substring(1).c_str(), NULL, 16);
        uint8_t r = (number >> 16) & 0xFF;
        uint8_t g = (number >> 8) & 0xFF;
        uint8_t b = number & 0xFF;
        uint16_t color565;

        if (useGBR) {
            // GBR 转换:  G(5) B(6) R(5)
            color565 = ((g & 0xF8) << 8) | ((b & 0xFC) << 3) | (r >> 3);
            Serial.println("Using GBR conversion");
        } else {
            // RGB565 转换 (默认)
            color565 = tft.color565(r, g, b);
             Serial.println("Using RGB conversion");
        }

        // --- 串口调试输出 ---
        Serial.print("Color String: ");
        Serial.println(colorString);
        Serial.print("R: "); Serial.println(r);
        Serial.print("G: "); Serial.println(g);
        Serial.print("B: "); Serial.println(b);
        Serial.print("565 Color: "); Serial.println(color565);

        return color565;
    }
    return ILI9341_WHITE;
}

最终的方案

#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <map>
#include <vector>

const char* ssid = "yang1234"; //  请替换为你的 Wi-Fi SSID
const char* password = "y123456789"; //  请替换为你的 Wi-Fi 密码

#define TFT_CS   D8
#define TFT_DC   D1
#define TFT_RST  D2

Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
ESP8266WebServer server(80);

struct LineData {
    String text;
    uint16_t color;
    uint8_t size;
    int16_t y;
};

std::map<int, LineData> lines;

// --- 图片数据结构 ---
struct ImageData {
    std::vector<uint16_t> pixels;
    int16_t width;
    int16_t height;
    int16_t y_offset; // 图片的 Y 轴偏移量
};

ImageData image;

// 全局变量,控制是否使用 GBR 格式
bool useGBR = true; //  GBR 格式

void handleSet();
void handleImage(); // 新增图片处理函数
void handleNotFound();
void updateDisplay();
void connectToWiFi();
uint16_t parseColor(String colorString);

void setup() {
    Serial.begin(115200);
    tft.begin();
    SPI.setFrequency(1000000);
    tft.setRotation(0);

    uint8_t caset_data[] = {0x00, 0x00, 0x00, 0xEF};
    tft.sendCommand(ILI9341_CASET, caset_data, 4);
    uint8_t raset_data[] = {0x00, 0x00, 0x01, 0x3F};
    tft.sendCommand(ILI9341_PASET, raset_data, 4);
    tft.writeCommand(ILI9341_RAMWR);

    uint8_t madctl_data = 0xE8;
    tft.sendCommand(ILI9341_MADCTL, &madctl_data, 1);

    tft.fillScreen(ILI9341_BLACK);
    connectToWiFi();

    server.on("/set", HTTP_POST, handleSet);
    server.on("/image", HTTP_POST, handleImage); //  处理图片请求
    server.onNotFound(handleNotFound);
    server.begin();
    Serial.println("HTTP server started");
    updateDisplay();
}

void loop() {
    server.handleClient();
}

void connectToWiFi() {
    WiFi.begin(ssid, password);
    Serial.print("Connecting to Wi-Fi");
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    Serial.println("\nConnected! IP address:");
    Serial.println(WiFi.localIP());
    Serial.print("请在Python脚本中将 esp8266_ip  替换为: ");
    Serial.println(WiFi.localIP());
}

void handleSet() {
    lines.clear();

    for (int i = 0; i < 100; i++) {
        String lineKey = "line" + String(i);

        if (server.hasArg(lineKey)) {
            LineData line;
            line.text = server.arg(lineKey);

            String colorKey = "color" + String(i);
            if (server.hasArg(colorKey)) {
                line.color = parseColor(server.arg(colorKey));
            } else {
                line.color = ILI9341_WHITE;
            }

            String sizeKey = "size" + String(i);
            if (server.hasArg(sizeKey)) {
                int size = server.arg(sizeKey).toInt();
                line.size = (size >= 1 && size <= 7) ? size : 2;
            } else {
                line.size = 2;
            }

            lines[i] = line;
        }
    }

    updateDisplay();
    server.send(200, "text/plain", "OK");
}

// --- 处理图片上传 ---
void handleImage() {
    if (server.hasArg("width") && server.hasArg("height") && server.hasArg("data")) {
        int width = server.arg("width").toInt();
        int height = server.arg("height").toInt();
        String data = server.arg("data");

        if (width > 0 && height > 0 && data.length() > 0) {
            image.width = width;
            image.height = height;
            image.pixels.clear();
            image.pixels.reserve(width * height); // 预留空间

            // 将接收到的字符串数据转换为像素数据
            for (int i = 0; i < data.length(); i += 4) { // 假设每像素 4 字符 (16进制)
                if (i + 4 <= data.length()) {
                    String hexColor = data.substring(i, i + 4);
                    unsigned long colorValue = strtoul(hexColor.c_str(), NULL, 16);
                    image.pixels.push_back(colorValue);
                }
            }
            image.y_offset = 0; // 默认图片 Y 轴偏移量为 0
            if (server.hasArg("y_offset")) {
                 image.y_offset = server.arg("y_offset").toInt();
            }


            updateDisplay(); // 更新显示
            server.send(200, "text/plain", "Image OK");
        } else {
            server.send(400, "text/plain", "Invalid image parameters");
        }
    } else {
        server.send(400, "text/plain", "Image parameters missing");
    }
}


void handleNotFound() {
    server.send(404, "text/plain", "404 Not Found");
}

void updateDisplay() {
    tft.fillScreen(ILI9341_BLACK);
    int16_t yPos = 20;

    for (auto& [lineNum, line] : lines) {
        line.y = yPos;

         // --- 左对齐 ---
        int16_t xPos = 20; // 左边距为 20 像素

        tft.setCursor(xPos, line.y);
        tft.setTextColor(line.color);
        tft.setTextSize(line.size);
        tft.println(line.text);

        yPos += line.size * 6 * (tft.getRotation() % 2 == 0 ? 1 : 2) + 15; // 15 像素行间距
    }

    // --- 显示图片 ---
    if (!image.pixels.empty()) {
        int16_t startY = yPos + image.y_offset; // 图片的起始 Y 坐标,在文字下方
        for (int y = 0; y < image.height; y++) {
            for (int x = 0; x < image.width; x++) {
                if (x < tft.width() && (startY + y) < tft.height()) { // 边界检查
                     tft.drawPixel(x, startY + y, image.pixels[y * image.width + x]);
                }
            }
        }
    }
}

uint16_t parseColor(String colorString) {
    if (colorString == "red")       return ILI9341_RED;
    if (colorString == "green")     return ILI9341_GREEN;
    if (colorString == "blue")      return ILI9341_BLUE;
    if (colorString == "yellow")    return ILI9341_YELLOW;
    if (colorString == "cyan")      return ILI9341_CYAN;
    if (colorString == "magenta")   return ILI9341_MAGENTA;
    if (colorString == "white")     return ILI9341_WHITE;
    if (colorString == "black")     return ILI9341_BLACK;
    if (colorString == "orange")    return ILI9341_ORANGE;

    if (colorString.startsWith("#") && colorString.length() == 7) {
        long number = strtol(colorString.substring(1).c_str(), NULL, 16);
        uint8_t r = (number >> 16) & 0xFF;
        uint8_t g = (number >> 8) & 0xFF;
        uint8_t b = number & 0xFF;
        uint16_t color565;

        if (useGBR) {
            // GBR 转换:  G(5) B(6) R(5)
            color565 = ((g & 0xF8) << 8) | ((b & 0xFC) << 3) | (r >> 3);
            Serial.println("Using GBR conversion");
        } else {
            // RGB565 转换 (默认)
            color565 = tft.color565(r, g, b);
             Serial.println("Using RGB conversion");
        }

        // --- 串口调试输出 ---
        Serial.print("Color String: ");
        Serial.println(colorString);
        Serial.print("R: "); Serial.println(r);
        Serial.print("G: "); Serial.println(g);
        Serial.print("B: "); Serial.println(b);
        Serial.print("565 Color: "); Serial.println(color565);

        return color565;
    }
    return ILI9341_WHITE;
}