ESP32 搭建 HTTP 服务:接收图片并实时显示

在物联网与嵌入式开发中,ESP32 凭借强大的 WiFi 功能与丰富的外设支持,成为实现本地交互与远程控制的理想平台。通过搭建 HTTP 服务,可让 ESP32 具备接收网络数据、响应远程请求的能力。本文将围绕ESP32 HTTP 服务搭建、图片数据接收与实时显示展开实践,实现通过网页或客户端上传图片,由 ESP32 接收并驱动显示屏实时展示。该方案融合了网络通信、数据处理与外设驱动,适用于智能显示、远程监控、交互终端等典型 IoT 场景,兼具实用性与扩展性,可为嵌入式 Web 应用开发提供清晰的实现思路。


一、服务端配置

1 配置HTTP Server库

如下图所示,下载ESPAsyncWebSrv库:


2 配置HTTP Server代码

将以下代码复制到Arduino IDE中:

c 复制代码
//
// A simple server implementation showing how to:
//  * serve static messages
//  * read GET and POST parameters
//  * handle missing pages / 404s
//

#include <Arduino.h>
#ifdef ESP32
#include <WiFi.h>
#include <AsyncTCP.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#endif
#include <ESPAsyncWebSrv.h>

AsyncWebServer server(80);

const char* ssid = "wifi_name";
const char* password = "wifi_password";

const char* PARAM_MESSAGE = "message";

void notFound(AsyncWebServerRequest *request) {
    request->send(404, "text/plain", "Not found");
}

void setup() 
{

    Serial.begin(115200);
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);
    if (WiFi.waitForConnectResult() != WL_CONNECTED) 
    {
        Serial.printf("WiFi Failed!\n");
        return;
    }

    Serial.print("IP Address: ");
    Serial.println(WiFi.localIP());

    server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
    {
        request->send(200, "text/plain", "Hello, world");
    });



    // Send a GET request to <IP>/get?message=<message>
    server.on("/get", HTTP_GET, [] (AsyncWebServerRequest *request) 
    {
        String message;
        if (request->hasParam(PARAM_MESSAGE)) 
        {
            message = request->getParam(PARAM_MESSAGE)->value();
            Serial.printf("Get Value From Get Request: %s\n",message.c_str());
        } 
        else 
        {
            message = "No message sent";
        }
        request->send(200, "text/plain", "Hello, GET: " + message);
    });



    // Send a POST request to <IP>/post with a form field message set to <message>
    server.on("/post", HTTP_POST, [](AsyncWebServerRequest *request)
    {
        String message;
        if (request->hasParam(PARAM_MESSAGE, true)) 
        {
            message = request->getParam(PARAM_MESSAGE, true)->value();
            Serial.printf("Get Value From Post Request: %s\n",message.c_str());
        } 
        else 
        {
            message = "No message sent";
        }
        request->send(200, "text/plain", "Hello, POST: " + message);
    });

    server.onNotFound(notFound);

    server.begin();
}

void loop() {
}

注:这里的ssid和password记得要改成你的wifi名称和wifi密码

二、客户端配置

测试客户端时,注意要与esp32连接同一个wifi,保证两者互通

1 python客户端测试

python 复制代码
# esp_http_client.py
import requests
import time

# 请将这个IP地址替换为你的ESP设备实际IP地址
import requests
import time

# 请将这个IP地址替换为你的ESP设备实际IP地址
base_url = f"http://192.168.1.12"

def send_get_request(url, params=None):
    """发送GET请求并处理响应"""
    try:
        if params:
            print(f"请求参数: {params}")

        response = requests.get(url, params=params, timeout=5)

        # 打印响应信息
        print(f"响应状态码: {response.status_code}")
        print(f"响应内容: {response.text}")

        return response

    except requests.exceptions.Timeout:
        print("错误: 请求超时,请检查网络连接和ESP IP地址")
    except requests.exceptions.ConnectionError:
        print("错误: 无法连接到ESP设备,请检查IP地址和设备是否在线")
    except Exception as e:
        print(f"未知错误: {e}")
    return None


def send_post_request(url, data=None):
    """发送POST请求并处理响应"""
    try:
        if data:
            print(f"POST数据: {data}")

        response = requests.post(url, data=data, timeout=5)

        # 打印响应信息
        print(f"响应状态码: {response.status_code}")
        print(f"响应内容: {response.text}")

        return response

    except requests.exceptions.Timeout:
        print("错误: 请求超时,请检查网络连接和ESP IP地址")
    except requests.exceptions.ConnectionError:
        print("错误: 无法连接到ESP设备,请检查IP地址和设备是否在线")
    except Exception as e:
        print(f"未知错误: {e}")
    return None


def main():


    # 1. 测试根路径 GET /
    print("\n--- 测试1: GET / ---")
    send_get_request(f"{base_url}/")
    time.sleep(1)

    # 2. 测试GET /get 不带参数
    print("\n--- 测试2: GET /get (无参数) ---")
    send_get_request(f"{base_url}/get")
    time.sleep(1)

    # 3. 测试GET /get 带参数
    print("\n--- 测试3: GET /get (带参数) ---")
    get_params = {"message": "Hello from Python GET!"}
    send_get_request(f"{base_url}/get", params=get_params)
    time.sleep(1)

    # 4. 测试POST /post 不带参数
    print("\n--- 测试4: POST /post (无参数) ---")
    send_post_request(f"{base_url}/post")
    time.sleep(1)

    # 5. 测试POST /post 带参数
    print("\n--- 测试5: POST /post (带参数) ---")
    post_data = {"message": "Hello from Python POST!"}
    send_post_request(f"{base_url}/post", data=post_data)

    # 6. 测试404页面
    print("\n--- 测试6: GET /nonexistent (404测试) ---")
    send_get_request(f"{base_url}/nonexistent")



if __name__ == "__main__":
    main()

可以看到python打印:

然后esp32那边也收到了消息:

2 curl测试

在使用libcurl之前别忘了安装curl库,使用以下命令:

c 复制代码
# 安装libcurl开发库
sudo apt update
sudo apt install libcurl4-openssl-dev

以下是使用libcurl实现的http客户端:

c 复制代码
//curl_test.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <curl/curl.h>

// 定义ESP设备的基础URL,请替换为实际IP
#define BASE_URL "http://192.168.1.12"

// 用于存储curl响应的结构体
struct MemoryStruct {
    char *memory;
    size_t size;
};

/**
 * 回调函数:将curl响应写入内存缓冲区
 */
static size_t WriteMemoryCallback(void *contents, size_t size, size_t nmemb, void *userp) {
    size_t realsize = size * nmemb;
    struct MemoryStruct *mem = (struct MemoryStruct *)userp;

    char *ptr = realloc(mem->memory, mem->size + realsize + 1);
    if (!ptr) {
        printf("错误: 内存分配失败\n");
        return 0;
    }

    mem->memory = ptr;
    memcpy(&(mem->memory[mem->size]), contents, realsize);
    mem->size += realsize;
    mem->memory[mem->size] = 0;

    return realsize;
}

/**
 * 发送GET请求(复刻Python的send_get_request函数)
 * @param url: 请求URL
 * @param params: GET参数(key=value格式,多个用&分隔,NULL表示无参数)
 */
CURLcode send_get_request(const char *url, const char *params) {
    CURL *curl_handle;
    CURLcode res;
    struct MemoryStruct chunk;
    char full_url[256] = {0};

    // 初始化内存缓冲区
    chunk.memory = malloc(1);
    chunk.size = 0;

    // 初始化curl
    curl_global_init(CURL_GLOBAL_ALL);
    curl_handle = curl_easy_init();

    if (curl_handle) {
        // 拼接完整URL(含参数)
        strcpy(full_url, url);
        if (params != NULL) {
            printf("请求参数: %s\n", params);
            strcat(full_url, "?");
            strcat(full_url, params);
        }

        // 设置curl选项
        curl_easy_setopt(curl_handle, CURLOPT_URL, full_url);
        curl_easy_setopt(curl_handle, CURLOPT_TIMEOUT, 5L);          // 总超时5秒
        curl_easy_setopt(curl_handle, CURLOPT_CONNECTTIMEOUT, 5L);   // 连接超时5秒
        curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
        curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, (void *)&chunk);
        curl_easy_setopt(curl_handle, CURLOPT_NOSIGNAL, 1L);         // 避免信号问题

        // 执行请求
        res = curl_easy_perform(curl_handle);

        if (res == CURLE_OK) {
            // 获取响应状态码
            long response_code;
            curl_easy_getinfo(curl_handle, CURLINFO_RESPONSE_CODE, &response_code);
            printf("响应状态码: %ld\n", response_code);
            printf("响应内容: %s\n", chunk.memory ? chunk.memory : "");
        } else {
            // 错误处理
            if (res == CURLE_OPERATION_TIMEDOUT) {
                printf("错误: 请求超时,请检查网络连接和ESP IP地址\n");
            } else if (res == CURLE_COULDNT_CONNECT || res == CURLE_COULDNT_RESOLVE_HOST) {
                printf("错误: 无法连接到ESP设备,请检查IP地址和设备是否在线\n");
            } else {
                printf("未知错误: %s\n", curl_easy_strerror(res));
            }
        }

        // 清理curl资源
        curl_easy_cleanup(curl_handle);
    } else {
        printf("错误: 初始化curl失败\n");
        res = CURLE_FAILED_INIT;
    }

    // 释放内存
    free(chunk.memory);
    curl_global_cleanup();

    return res;
}

/**
 * 发送POST请求(复刻Python的send_post_request函数)
 * @param url: 请求URL
 * @param data: POST数据(key=value格式,多个用&分隔,NULL表示无参数)
 */
CURLcode send_post_request(const char *url, const char *data) {
    CURL *curl_handle;
    CURLcode res;
    struct MemoryStruct chunk;

    // 初始化内存缓冲区
    chunk.memory = malloc(1);
    chunk.size = 0;

    // 初始化curl
    curl_global_init(CURL_GLOBAL_ALL);
    curl_handle = curl_easy_init();

    if (curl_handle) {
        // 设置curl选项
        curl_easy_setopt(curl_handle, CURLOPT_URL, url);
        curl_easy_setopt(curl_handle, CURLOPT_TIMEOUT, 5L);          // 总超时5秒
        curl_easy_setopt(curl_handle, CURLOPT_CONNECTTIMEOUT, 5L);   // 连接超时5秒
        curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
        curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, (void *)&chunk);
        curl_easy_setopt(curl_handle, CURLOPT_NOSIGNAL, 1L);         // 避免信号问题

        // 设置POST请求
        curl_easy_setopt(curl_handle, CURLOPT_POST, 1L);
        if (data != NULL) {
            printf("POST数据: %s\n", data);
            curl_easy_setopt(curl_handle, CURLOPT_POSTFIELDS, data);
        }

        // 执行请求
        res = curl_easy_perform(curl_handle);

        if (res == CURLE_OK) {
            // 获取响应状态码
            long response_code;
            curl_easy_getinfo(curl_handle, CURLINFO_RESPONSE_CODE, &response_code);
            printf("响应状态码: %ld\n", response_code);
            printf("响应内容: %s\n", chunk.memory ? chunk.memory : "");
        } else {
            // 错误处理
            if (res == CURLE_OPERATION_TIMEDOUT) {
                printf("错误: 请求超时,请检查网络连接和ESP IP地址\n");
            } else if (res == CURLE_COULDNT_CONNECT || res == CURLE_COULDNT_RESOLVE_HOST) {
                printf("错误: 无法连接到ESP设备,请检查IP地址和设备是否在线\n");
            } else {
                printf("未知错误: %s\n", curl_easy_strerror(res));
            }
        }

        // 清理curl资源
        curl_easy_cleanup(curl_handle);
    } else {
        printf("错误: 初始化curl失败\n");
        res = CURLE_FAILED_INIT;
    }

    // 释放内存
    free(chunk.memory);
    curl_global_cleanup();

    return res;
}

/**
 * 主函数:复刻原Python脚本的测试流程
 */
int main(void) {
    char test_url[256] = {0};

    // 1. 测试根路径 GET /
    printf("\n--- 测试1: GET / ---\n");
    snprintf(test_url, sizeof(test_url), "%s/", BASE_URL);
    send_get_request(test_url, NULL);
    sleep(1);

    // 2. 测试GET /get 不带参数
    printf("\n--- 测试2: GET /get (无参数) ---\n");
    snprintf(test_url, sizeof(test_url), "%s/get", BASE_URL);
    send_get_request(test_url, NULL);
    sleep(1);

    // 3. 测试GET /get 带参数
    printf("\n--- 测试3: GET /get (带参数) ---\n");
    snprintf(test_url, sizeof(test_url), "%s/get", BASE_URL);
    send_get_request(test_url, "message=Hello from C GET!");
    sleep(1);

    // 4. 测试POST /post 不带参数
    printf("\n--- 测试4: POST /post (无参数) ---\n");
    snprintf(test_url, sizeof(test_url), "%s/post", BASE_URL);
    send_post_request(test_url, NULL);
    sleep(1);

    // 5. 测试POST /post 带参数
    printf("\n--- 测试5: POST /post (带参数) ---\n");
    snprintf(test_url, sizeof(test_url), "%s/post", BASE_URL);
    send_post_request(test_url, "message=Hello from C POST!");
    sleep(1);

    // 6. 测试404页面
    printf("\n--- 测试6: GET /nonexistent (404测试) ---\n");
    snprintf(test_url, sizeof(test_url), "%s/nonexistent", BASE_URL);
    send_get_request(test_url, NULL);

    return 0;
}

编译命令:

shell 复制代码
gcc -o curl_test curl_test.c -lcurl

三、tft库配置及图片显示

1 转化图片数据

这里记得转化图片之前,先将图片resize到你需要的尺寸,之后再使用Img2Lcd去按照下面参数转化图片:

2 图片显示demo

使用以下代码去进行图片显示:

c 复制代码
// ESP32-S3 TFT屏幕显示自定义图片数组
// 引脚定义:CS:14, DC/RS:47, RESET:21, SDI/MOSI:45, SCK:3, SDO/MISO:46, LED:0

#include <TFT_eSPI.h> 
#include <SPI.h>

// 创建TFT对象
TFT_eSPI mylcd = TFT_eSPI(); 

// ====================== 自定义图片数据 ======================
#define IMAGE_WIDTH  240    // 图片宽度(根据你的图片实际尺寸调整)
#define IMAGE_HEIGHT 320    // 图片高度(根据你的图片实际尺寸调整)

const unsigned char gImage_aaa[153600] = { /* 0X10,0X10,0X00,0XF0,0X01,0X40,0X01,0X1B, */
0X5B,0X4E,0X5B,0X4E,0X5B,0X4E,0X5B,0X6E,0X63,0X6E,0X63,0X6E,0X63,0X8E,0X63,0X8E,
0X6B,0XAF,0X6B,0XCF,0X73,0XEF,0X73,0XEF,0X73,0XEF,0X73,0XEF,0X73,0XEF,0X73,0XEF,
0X73,0XCF,0X6B,0XAE,0X6B,0XAF,0X6B,0XCF,0X8C,0XF4,0XB6,0X19,0XCE,0XDB,0XE7,0X3D,
....};
// 显示图片函数
void displayCustomImage() {
  // 设置屏幕旋转(0-3可选,根据你的屏幕和图片方向调整)
  mylcd.setRotation(0);
  
  // 清屏(黑色背景)
  mylcd.fillScreen(TFT_BLACK);
  
  // 核心:显示自定义图片数组
  // 注意:需要将unsigned char数组转换为uint16_t指针(RGB565格式)
  mylcd.pushImage(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT, (const uint16_t*)gImage_aaa);
}

void setup() {
  // 初始化串口(可选,用于调试)
  Serial.begin(115200);
  
  // 初始化TFT屏幕
  mylcd.init();
  
  // 显示自定义图片
  displayCustomImage();
  
  Serial.println("自定义图片显示完成!");
}

void loop() {
  // 只显示一次图片,loop函数为空
  delay(1000);
}

其中图片数组gImage_aaa为你转化出来的图片,可以看到初步可以使用esp32显示图片:

四、图片发送及服务端显示图片

1 复现图片转化过程

python 复制代码
import cv2
import numpy as np
import sys

def image_to_c_array(image_path, output_path,target_size, byte_order="big"):
    """
    :param image_path: 输入图片路径
    :param output_path: 输出.c文件路径
    :param byte_order: 字节序:"big"(大端,Big Endian)或 "little"(小端,Little Endian)
    """
    try:
        # 使用OpenCV读取图片(注意:OpenCV默认读取为BGR格式)
        img = cv2.imread(image_path, cv2.IMREAD_COLOR)

        if img is None:
            raise ValueError(f"无法读取图片文件:{image_path}")

        # target_size = (240, 320)  # OpenCV resize的参数是 (宽, 高)
        img = cv2.resize(img, target_size, interpolation=cv2.INTER_LINEAR)
        
        # 转换为RGB格式 + 关键:将数组类型转为int32,避免移位溢出
        img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).astype(np.int32)
        height, width, _ = img_rgb.shape  # OpenCV的shape是 (高, 宽, 通道数)
        total_bytes = width * height * 2
        
        # 提取数组名称(和原逻辑一致)
        array_name = f"gImage_{image_path.split('.')[0].split('/')[-1]}"

        # 生成C语言数组头(完全匹配软件格式)
        c_code = f"const unsigned char {array_name}[{total_bytes}] = {{ \n"

        # 遍历像素并转换为RGB565(拆分为单个字节)
        count = 0  # 用于控制每行显示16个字节

        for y in range(height):
            for x in range(width):
                # OpenCV按[y, x]访问像素(和PIL的[x, y]相反)
                r, g, b = img_rgb[y, x]
                
                # RGB565核心转换公式(修复类型溢出问题)
                # R(5bit): (r & 0xF8)  G(6bit): (g & 0xFC)  B(5bit): (b & 0xF8)
                # 关键:所有运算先转为int,避免uint8溢出
                r_part = (int(r) & 0xF8) << 8
                g_part = (int(g) & 0xFC) << 3
                b_part = (int(b) & 0xF8) >> 3
                rgb565 = r_part | g_part | b_part
                
                # 拆分高低字节(关键逻辑和原代码一致)
                if byte_order == "big":
                    # 大端模式:高字节在前,低字节在后(软件默认)
                    high_byte = (rgb565 >> 8) & 0xFF  # 高8位
                    low_byte = rgb565 & 0xFF           # 低8位
                else:
                    # 小端模式:低字节在前,高字节在后
                    high_byte = rgb565 & 0xFF
                    low_byte = (rgb565 >> 8) & 0xFF
                
                # 添加到数组(格式和软件完全一致)
                c_code += f"0X{high_byte:02X},0X{low_byte:02X},"
                count += 2
                
                # 每行显示16个字节(8个像素),和软件格式对齐
                if count % 16 == 0:
                    c_code = c_code.rstrip(",")  # 移除最后一个逗号
                    c_code += ",\n"  # 换行
        
        # 收尾处理
        c_code = c_code.rstrip(",\n")  # 移除最后一行的逗号和换行
        c_code += "\n};"

        # 写入文件
        with open(output_path, "w", encoding="utf-8") as f:
            f.write(c_code)
        print(f"✅ 转换完成!输出文件:{output_path}")
        
    except Exception as e:
        print(f"❌ 转换失败:{e}")

if __name__ == "__main__":
    # 测试示例
    input_img = "beauty.png"
    output_c = "cv_data.h"
    # 模拟图中配置:水平扫描、RGB565、大端字节序
    image_to_c_array(input_img, output_c,(240, 320), byte_order="big")

2 服务端接收图片代码

c 复制代码
// ESP32-S3 TFT屏幕显示HTTP接收的动态图片
// 引脚定义:CS:14, DC/RS:47, RESET:21, SDI/MOSI:45, SCK:3, SDO/MISO:46, LED:0
#include <TFT_eSPI.h> 
#include <SPI.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebSrv.h>
#include <ArduinoJson.h>  // 用于解析JSON数组(可选,根据Python发送方式)

// ====================== WiFi配置 ======================
const char* WIFI_SSID = "wifi_name";
const char* WIFI_PWD  = "wifi_password";

// ====================== 屏幕配置 ======================
#define IMAGE_WIDTH  240    // 和Python端TARGET_SIZE一致
#define IMAGE_HEIGHT 320    // 和Python端TARGET_SIZE一致
#define IMAGE_BUF_SIZE (IMAGE_WIDTH * IMAGE_HEIGHT * 2)  // RGB565每个像素2字节

// ====================== 全局变量 ======================
TFT_eSPI mylcd = TFT_eSPI(); 
AsyncWebServer server(80);  // 创建HTTP服务器,端口80
uint8_t gDynamicImageBuf[IMAGE_BUF_SIZE] = {0};  // 动态接收图片的缓冲区(替换原静态数组)
bool isImageReceived = false;  // 标记是否收到新图片

// ====================== WiFi连接函数 ======================
void connectWiFi() {
  Serial.print("连接WiFi: ");
  Serial.println(WIFI_SSID);
  WiFi.begin(WIFI_SSID, WIFI_PWD);
  
  int retry = 0;
  while (WiFi.status() != WL_CONNECTED && retry < 20) {
    delay(500);
    Serial.print(".");
    retry++;
  }
  
  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("\nWiFi连接成功!");
    Serial.print("ESP32 IP地址: ");
    Serial.println(WiFi.localIP());  // 打印IP,供Python端使用
  } else {
    Serial.println("\nWiFi连接失败,请检查配置!");
    while (1) delay(1000);  // 连接失败则卡死
  }
}

// ====================== 显示图片函数(兼容动态缓冲区) ======================
void displayCustomImage() {
  mylcd.setRotation(0);
  mylcd.fillScreen(TFT_BLACK);  // 清屏
  
  // 核心:显示动态接收的图片缓冲区(转换为uint16_t指针,RGB565格式)
  mylcd.pushImage(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT, (const uint16_t*)gDynamicImageBuf);
  Serial.println("✅ 新图片已显示在屏幕上!");
}

// ====================== HTTP请求处理函数 ======================
// 处理Python发送的二进制流(推荐方式)
void handlePostBinaryImage(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
  // 只接收和缓冲区大小匹配的数据
  if (total != IMAGE_BUF_SIZE) {
    if (index == 0) {
      Serial.printf("❌ 数据长度不匹配!期望:%d,实际:%d\n", IMAGE_BUF_SIZE, total);
      request->send(400, "text/plain", "Data size error!");
    }
    return;
  }

  // 分段接收数据并拷贝到缓冲区
  static size_t receivedLen = 0;
  if (index == 0) receivedLen = 0;  // 新请求开始,重置接收长度
  
  // 拷贝当前段数据到缓冲区
  memcpy(&gDynamicImageBuf[receivedLen], data, len);
  receivedLen += len;

  // 数据接收完成
  if (receivedLen == total) {
    isImageReceived = true;
    request->send(200, "text/plain", "Image received and displayed!");
    Serial.printf("✅ 接收二进制图片数据完成,长度:%d\n", receivedLen);
    displayCustomImage();  // 立即显示新图片
  }
}

// 可选:处理Python发送的JSON数组(调试用)
void handlePostJsonImage(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
  if (index != 0 || total != len) {  // 只处理完整的JSON数据
    return;
  }

  // 解析JSON
  DynamicJsonDocument doc(IMAGE_BUF_SIZE * 3);  // 足够的缓冲区
  DeserializationError error = deserializeJson(doc, data, len);
  if (error) {
    Serial.printf("❌ JSON解析失败:%s\n", error.c_str());
    request->send(400, "text/plain", "JSON parse error!");
    return;
  }

  // 提取图片数组
  JsonArray imgArray = doc["imagedata"];
  if (imgArray.size() != IMAGE_BUF_SIZE) {
    Serial.printf("❌ 数组长度不匹配!期望:%d,实际:%d\n", IMAGE_BUF_SIZE, imgArray.size());
    request->send(400, "text/plain", "Array size error!");
    return;
  }

  // 拷贝JSON数组到图片缓冲区
  for (int i = 0; i < IMAGE_BUF_SIZE; i++) {
    gDynamicImageBuf[i] = imgArray[i];
  }

  isImageReceived = true;
  request->send(200, "text/plain", "JSON image received!");
  Serial.println("✅ 接收JSON图片数据完成!");
  displayCustomImage();  // 显示新图片
}

// ====================== 初始化函数 ======================
void setup() {
  Serial.begin(115200);
  delay(100);

  // 1. 初始化屏幕
  mylcd.init();
  mylcd.fillScreen(TFT_BLACK);  // 初始清屏
  Serial.println("✅ TFT屏幕初始化完成");

  // 2. 连接WiFi
  connectWiFi();

  // 3. 配置HTTP服务器路由
  // 路由1:接收二进制流(对应Python方案1,推荐)
  server.on("/post_image", HTTP_POST, 
    [](AsyncWebServerRequest *request) {},  // 无参数处理
    NULL,
    handlePostBinaryImage  // 二进制数据处理函数
  );

  // 路由2:接收JSON数组(对应Python方案2,可选,调试用)
  // server.on("/post_json_image", HTTP_POST, handlePostJsonImage);

  // 启动服务器
  server.begin();
  Serial.println("✅ HTTP服务器已启动,等待接收图片...");

  // 初始显示黑色背景(无图片时)
  mylcd.fillScreen(TFT_BLACK);
}

// ====================== 主循环 ======================
void loop() {
  // 无需循环操作,HTTP请求由异步服务器处理
  delay(1000);
}

3 客户端转化代码并发送

python 复制代码
import cv2
import numpy as np
import requests

ESP32_IP = "192.168.1.12"
BASE_URL = f"http://{ESP32_IP}/post_image"
TARGET_SIZE = (240, 320)  # (宽度, 高度)
TXT_FILE_PATH = "rgb565_c_array.txt"  # 输出C数组格式的txt文件

def image_to_rgb565_c_array(image_path, target_size, byte_order="big"):
    """
    将图片转换为RGB565格式,并生成C语言数组格式的字符串(UTF-8可显示)
    :return: (c_array_str, byte_data, width, height)
             c_array_str: C数组格式的字符串
             byte_data: 原始RGB565字节数据(用于发送)
    """
    img = cv2.imread(image_path, cv2.IMREAD_COLOR)
    if img is None:
        raise ValueError(f"无法读取图片:{image_path}")
    
    # 调整尺寸
    img = cv2.resize(img, target_size, interpolation=cv2.INTER_LINEAR)
    height, width, _ = img.shape
    img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).astype(np.int32)
    total_bytes = width * height * 2
    
    # 提取数组名称(和参考代码逻辑一致)
    array_name = f"gImage_{image_path.split('.')[0].split('/')[-1]}"
    
    # 初始化C数组字符串和字节数据
    c_code = f"const unsigned char {array_name}[{total_bytes}] = {{ \n"
    byte_data = bytearray()
    count = 0  # 控制每行16个字节
    
    for y in range(height):
        for x in range(width):
            r, g, b = img_rgb[y, x]
            
            # RGB565转换(和参考代码完全一致)
            r_part = (int(r) & 0xF8) << 8
            g_part = (int(g) & 0xFC) << 3
            b_part = (int(b) & 0xF8) >> 3
            rgb565 = r_part | g_part | b_part
            
            # 拆分高低字节(大端模式)
            if byte_order == "big":
                high_byte = (rgb565 >> 8) & 0xFF
                low_byte = rgb565 & 0xFF
            else:
                high_byte = rgb565 & 0xFF
                low_byte = (rgb565 >> 8) & 0xFF
            
            # 保存原始字节数据(用于后续发送)
            byte_data.append(high_byte)
            byte_data.append(low_byte)
            
            # 按C数组格式拼接字符串(0Xxx,0Xxx,)
            c_code += f"0X{high_byte:02X},0X{low_byte:02X},"
            count += 2
            
            # 每行显示16个字节(8个像素),换行(和参考代码对齐)
            if count % 16 == 0:
                c_code = c_code.rstrip(",")  # 移除行尾多余逗号
                c_code += ",\n"  # 换行
    
    # 收尾处理(移除最后一行的逗号和换行,补全数组结尾)
    c_code = c_code.rstrip(",\n")
    c_code += "\n};"
    
    return c_code, bytes(byte_data), width, height

def save_c_array_to_utf8_txt(c_array_str, txt_path):
    """将C数组格式的字符串以UTF-8编码写入txt文件(无乱码)"""
    try:
        with open(txt_path, "w", encoding="utf-8") as f:
            f.write(c_array_str)
        print(f"✅ C数组格式已写入UTF-8 txt文件:{txt_path}")
    except Exception as e:
        raise ValueError(f"写入txt失败:{e}")

def parse_c_array_from_txt(txt_path):
    """从C数组格式的txt文件解析出原始RGB565字节数据(用于发送)"""
    try:
        with open(txt_path, "r", encoding="utf-8") as f:
            content = f.read()
        
        # 提取数组内容(去掉C语法部分,只保留0Xxx格式的数值)
        # 步骤1:截取{}之间的内容
        start_idx = content.find("{") + 1
        end_idx = content.find("}")
        array_content = content[start_idx:end_idx].strip()
        
        # 步骤2:拆分所有0Xxx格式的数值
        hex_values = [v.strip() for v in array_content.split(",") if v.strip()]
        
        # 步骤3:转换为字节数据
        byte_data = bytearray()
        for hex_val in hex_values:
            # 去掉0X/0x前缀,转十进制整数
            val = int(hex_val.replace("0X", "").replace("0x", ""), 16)
            byte_data.append(val)
        
        print(f"✅ 从txt解析出字节数据,长度:{len(byte_data)}")
        return bytes(byte_data)
    except Exception as e:
        raise ValueError(f"解析txt中的C数组失败:{e}")

def send_image_to_esp32(image_path):
    try:
        # 1. 转换图片为C数组字符串 + 原始字节数据
        c_array_str, raw_byte_data, width, height = image_to_rgb565_c_array(
            image_path, TARGET_SIZE, byte_order="big"
        )
        print(f"📌 图片转换完成,尺寸:{width}x{height},总字节数:{len(raw_byte_data)}")
        
        # 2. 将C数组格式写入UTF-8 txt文件(无乱码)
        save_c_array_to_utf8_txt(c_array_str, TXT_FILE_PATH)
        
        # 3. 从txt文件解析回原始字节数据
        send_data = parse_c_array_from_txt(TXT_FILE_PATH)
        
        # 4. 校验数据一致性
        if raw_byte_data == send_data:
            print("✅ 数据读写一致性校验通过")
        else:
            print("❌ 数据读写一致性校验失败!")
            return
        
        # 5. 发送给ESP32
        print(f"📤 发送解析后的字节流,长度:{len(send_data)}")
        response = requests.post(
            BASE_URL,
            data=send_data,
            headers={"Content-Type": "application/octet-stream"},
            timeout=10
        )
        print(f"✅ 响应状态码: {response.status_code}")
        print(f"✅ 响应内容: {response.text}")
    except Exception as e:
        print(f"❌ 发送失败: {e}")

if __name__ == "__main__":
    input_image_path = "beauty1.jpg"  # 替换为你的图片路径
    send_image_to_esp32(input_image_path)

遇到的报错

  • assert failed: tcp_alloc /IDF/components/lwip/lwip/src/core/tcp.c:1851 (required to lock the TCPIP core functionality!)

    解决办法:重新安装esp32 sdk到3.0.7版本问题解决:

    原文地址:Required to lock TCPIP core functionality!

相关推荐
Wzx1980122 小时前
HTTP深度解析
网络·网络协议·http
MinterFusion2 小时前
如何在Windows下查看本机的IP地址
网络·windows·tcp/ip·ip地址·明德融创
理人综艺好会2 小时前
http和https的了解
网络协议·http·https
Evand J2 小时前
计算机四级——《网络工程》科目,易错点总结【纯手工总结】
网络协议·网络安全·网络工程·计算机四级
特立独行的猫a2 小时前
ESP32小智AI的WebSocket 调试工具的实现,小智AI后台交互过程揭秘(二、技术原理与实现过程详解 )
人工智能·websocket·网络协议·esp32·调试工具·小智ai
灰子学技术2 小时前
自定义 Host 头访问 HTTPS 服务时的网关处理逻辑
网络·网络协议·http·https
ARM+FPGA+AI工业主板定制专家2 小时前
基于ARM+FPGA+AI的船舶状态智能监测系统(一)总体设计
网络·arm开发·人工智能·机器学习·fpga开发·自动驾驶
taxunjishu2 小时前
Profinet转MODBUS TCP汽车零部件工业自动化柔性产线方案
网络·自动化·汽车
甘露s2 小时前
从明文到加密:HTTP 与 HTTPS 的本质区别与建立全解析
网络协议·http·https