ESP32-CAM与搭载ST7735屏幕的ESP32开发板进行图像采集与无线显示

在物联网应用中,“图像采集 + 远程显示” 是一个常见场景,比如监控摄像头、可视化传感器等。恰好本人手上有一块ESP32CAM以及一个搭载ST7735屏幕的ESP32开发板,本文将以 ESP32-CAM(图像采集端)和带屏幕的 ESP32(显示端)为例,从硬件原理、数据流程到核心技术细节,详细讲解如何实现 “无线图像传输与显示” 全流程。

一、硬件部分:

ESP32CAM一块,搭载TFT液晶屏幕的ESP32的开发板一块,将屏幕和ESP32通过SPI接口相连。

二、软件部分

开发环境搭建及初始化

使用Espressif提供的ESP-IDF(ESP32CAM)和vscode下的PlatformIO(显示屏)插件进行开发。

ESP32CAM部分

使用乐鑫官方提供的组件esp_camera对ESP32CAM进行初始化,以及进行图像采集测试。具体如何操作可以参考我的另一篇博客:https://blog.youkuaiyun.com/wangchen_6/article/details/152414939?fromshare=blogdetail&sharetype=blogdetail&sharerId=152414939&sharerefer=PC&sharesource=wangchen_6&sharefrom=from_link

显示屏部分

使用vscode下PlatformIO来进行开发,使用TFT_eSPI来驱动ST7785芯片,在屏幕上显示内容。

图像采集与显示:

图像采集:

使用乐鑫官方提供的函数camera_fb_t *pic = esp_camera_fb_get();通过指向camera_fb_t结构体的指针pic来获取图像信息,具体想要了解可以查看esp_camera.h文件中camera_fb_t结构体的定义。

因为此处我的屏幕为128*160大小,所以JPEG格式采用FRAMESIZE_QQVGA,这是160*120像素大小的照片,只要在屏幕上横屏显示即可。

无线显示:

ESP32CAM端:
  1. WiFi连接:通过wifi_init_sta函数,设备以STA模式连接到指定的WiFi网络。一旦连接成功,设备会获取IP地址并开始监听TCP连接。

  2. TCP服务器:在tcp_server_task任务中,程序创建了一个TCP服务器,监听指定的端口(8080)。当有客户端连接时,服务器会开始发送图像数据。

  3. 图像发送:将采集到的图像数据通过TCP连接发送给客户端。每个图像帧前会发送一个帧头,包含帧的长度信息,以便客户端正确解析图像数据。

ESP32显示器:
  1. WiFi连接:连接到指定的WiFi网络。尝试连接到两个预设的服务器IP地址中的一个。(这里为什么是两个服务器IP呢,因为我有两块ESP32CAM😁)

  2. TCP客户端:与服务器建立TCP连接,接收图像数据。

  3. 图像接收与解码:从服务器接收JPEG图像数据。使用TJpg_Decoder库解码JPEG图像并显示在TFT屏幕上。

  4. 帧率计算:计算并显示当前的帧率。

  5. 屏幕显示:在TFT屏幕上显示图像、错误信息、当前连接的服务器IP和帧率。

  三、代码部分:

ESP32CAM:

#include <stdio.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <esp_log.h>
#include <string.h>
#include <esp_wifi.h>
#include <esp_event.h>
#include <esp_netif.h>
#include <lwip/sockets.h>
#include <nvs_flash.h>
#include <sensor.h>  // 摄像头传感器控制头文件

#include "myled.h"
#include "mycamera.h"

static const char *TAG = "wifi_camera_sender";
#define WIFI_SSID   "youer_SSID"//填入自己的wifi名
#define WIFI_PASS   "password"//填入自己的wifi密码
#define TCP_PORT    8080

// 图像配置(优化兼容性)
#define IMG_FORMAT  PIXFORMAT_JPEG
#define FRAME_SIZE  FRAMESIZE_QQVGA  // 160×120,与接收端匹配
#define JPEG_QUALITY 12 // 压缩率更高(0-63,值越大质量越低)
#define SEND_DELAY  100  // 300ms/帧(≈3FPS,降低接收端压力)

static int client_socket = -1;
static bool is_connected = false;  // 客户端连接状态标记

// WiFi事件处理(增强重连逻辑)
static void event_handler(void* arg, esp_event_base_t event_base,
                          int32_t event_id, void* event_data) {
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
        esp_wifi_connect();
    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
        ESP_LOGW(TAG, "WiFi断开,1秒后重试...");
        vTaskDelay(pdMS_TO_TICKS(1000));  // 延迟重连,避免频繁尝试
        esp_wifi_connect();
        is_connected = false;  // 重置连接状态
    } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
        ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
        ESP_LOGI(TAG, "获取IP: " IPSTR, IP2STR(&event->ip_info.ip));
        is_connected = true;
    }
}

// 初始化WiFi(增强稳定性)
static void wifi_init_sta(void) {
    ESP_ERROR_CHECK(nvs_flash_init());
    ESP_ERROR_CHECK(esp_netif_init());
    
    // 确保事件循环正确初始化
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_sta();

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    // 注册事件处理
    esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
                                        &event_handler, NULL, NULL);
    esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
                                        &event_handler, NULL, NULL);

    wifi_config_t wifi_config = {
        .sta = {
            .ssid = WIFI_SSID,
            .password = WIFI_PASS,
            .threshold.authmode = WIFI_AUTH_WPA2_PSK,  // 明确认证模式
            .sae_pwe_h2e = WPA3_SAE_PWE_BOTH,  // 支持WPA3兼容性
        },
    };
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
    ESP_ERROR_CHECK(esp_wifi_start());
    ESP_LOGI(TAG, "WiFi初始化完成,等待连接...");
}

// 配置摄像头参数(解决解码错误码1的核心)
static void camera_config_optimize(void) {
    sensor_t *s = esp_camera_sensor_get();
    if (!s) {
        ESP_LOGE(TAG, "获取摄像头传感器失败");
        return;
    }

    // 关键:设置为基线JPEG(非渐进式),确保TJpg_Decoder兼容
    s->set_quality(s, JPEG_QUALITY);  // 第二个参数0=基线编码
    s->set_framesize(s, FRAME_SIZE);          // 固定160×120
    s->set_pixformat(s, IMG_FORMAT);          // JPEG格式

    // 禁用自动增益/白平衡(减少图像噪声,降低JPEG复杂度)
    s->set_gain_ctrl(s, 1);  // 关闭自动增益
    s->set_whitebal(s, 1);   // 关闭自动白平衡
    ESP_LOGI(TAG, "摄像头参数优化完成(基线JPEG)");
}

// 安全发送函数(处理部分发送情况)
static ssize_t safe_send(int sock, const void *data, size_t len) {
    size_t total_sent = 0;
    while (total_sent < len) {
        ssize_t sent = send(sock, (const char*)data + total_sent, len - total_sent, 0);
        if (sent < 0) {
            ESP_LOGE(TAG, "发送失败: %d", errno);
            return -1;
        }
        total_sent += sent;
    }
    return total_sent;
}

// TCP服务器任务(增强错误处理)
static void tcp_server_task(void *pvParameters) {
    int server_socket;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);

    // 创建套接字(设置SO_REUSEADDR,避免端口占用)
    server_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (server_socket < 0) {
        ESP_LOGE(TAG, "创建套接字失败: %d", errno);
        vTaskDelete(NULL);
    }
    int opt = 1;
    setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // 绑定地址和端口
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(TCP_PORT);
    if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        ESP_LOGE(TAG, "绑定失败: %d", errno);
        close(server_socket);
        vTaskDelete(NULL);
    }

    // 监听连接
    if (listen(server_socket, 1) < 0) {
        ESP_LOGE(TAG, "监听失败: %d", errno);
        close(server_socket);
        vTaskDelete(NULL);
    }
    ESP_LOGI(TAG, "TCP服务器启动,端口: %d", TCP_PORT);

    // 优化摄像头参数(关键!确保输出基线JPEG)
    camera_config_optimize();

    while (1) {
        // 等待客户端连接
        client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_addr_len);
        if (client_socket < 0) {
            ESP_LOGE(TAG, "接受连接失败: %d", errno);
            vTaskDelay(pdMS_TO_TICKS(1000));
            continue;
        }

        // 连接成功,设置发送缓冲区
        int send_buf = 4096;
        setsockopt(client_socket, SOL_SOCKET, SO_SNDBUF, &send_buf, sizeof(send_buf));
        ESP_LOGI(TAG, "客户端已连接,IP: " IPSTR);
        //myled_set(1);  // 点亮LED表示连接成功

        // 循环发送图像
        while (client_socket != -1) {
            // 检查WiFi连接状态
            if (!is_connected) {
                ESP_LOGW(TAG, "WiFi未连接,暂停发送");
                vTaskDelay(pdMS_TO_TICKS(1000));
                continue;
            }

            // 获取图像帧
            camera_fb_t *pic = esp_camera_fb_get();
            if (!pic) {
                ESP_LOGE(TAG, "图像采集失败");
                vTaskDelay(pdMS_TO_TICKS(1000));
                continue;
            }

            // 验证图像格式和尺寸
            if (pic->format != IMG_FORMAT) {
                ESP_LOGE(TAG, "图像格式错误(非JPEG)");
                esp_camera_fb_return(pic);
                vTaskDelay(pdMS_TO_TICKS(1000));
                continue;
            }
            if (pic->width != 160 || pic->height != 120) {
                ESP_LOGW(TAG, "图像尺寸不符(预期160×120,实际%d×%d)", pic->width, pic->height);
            }

            // 发送帧头(0xAA55AA55 + 4字节长度)
            uint8_t header[8] = {0xAA, 0x55, 0xAA, 0x55};
            uint32_t jpeg_len = htonl(pic->len);
            memcpy(&header[4], &jpeg_len, 4);
            if (safe_send(client_socket, header, 8) != 8) {
                ESP_LOGE(TAG, "帧头发送失败");
                esp_camera_fb_return(pic);
                break;
            }

            // 发送JPEG数据
            if (safe_send(client_socket, pic->buf, pic->len) != pic->len) {
                ESP_LOGE(TAG, "JPEG数据发送失败");
                esp_camera_fb_return(pic);
                break;
            }

            ESP_LOGI(TAG, "发送成功,大小: %zu字节,FPS: %.1f", 
                     pic->len, 1000.0 / SEND_DELAY);
            esp_camera_fb_return(pic);
            vTaskDelay(pdMS_TO_TICKS(SEND_DELAY));  // 控制帧率
        }

        // 断开连接处理
        close(client_socket);
        client_socket = -1;
        //myled_set(0);  // 熄灭LED表示断开
        ESP_LOGI(TAG, "客户端断开,等待新连接...");
    }
}

void app_main(void) {
    myled_init();
    xTaskCreate(myled_Blink_task, "myled_Blink_task", 2048, NULL, 5, NULL);
    
    ESP_LOGI(TAG, "初始化摄像头...");
    if (init_camera() != ESP_OK) {  // 假设init_camera返回状态码
        ESP_LOGE(TAG, "摄像头初始化失败,程序退出");
        return;
    }

    wifi_init_sta();
    xTaskCreate(tcp_server_task, "tcp_server", 8192, NULL, 4, NULL);  // 增大栈空间
}

 无线显示器端:

#include <WiFi.h>
#include <TFT_eSPI.h>
#include <TJpg_Decoder.h>
#include <SPIFFS.h>

// WiFi配置(需与ESP32-CAM连接同一网络)
const char* ssid = "wifi_SSID";//WIFI名
const char* password = "password";//WIFI密码

// 双服务器配置(需要使用连接wifi后显示的IP地址)
const char* server_ip1 = "192.168.127.135";  // 第一个服务器IP
const char* server_ip2 = "192.168.127.169";  // 第二个服务器IP
const uint16_t server_port = 8080;           // 端口一致

WiFiClient client;
TFT_eSPI tft;

// 屏幕参数(横屏160×128)
#define TFT_WIDTH  160
#define TFT_HEIGHT 128

// 接收缓冲区
static uint8_t recv_buf[15000];
static int buf_len = 0;

// 当前连接的服务器索引
static int current_server = 0;

// 帧率计算变量
static unsigned long last_fps_time = 0;  // 上一次计算帧率的时间
static int frame_count = 0;              // 1秒内解码成功的帧数
static float current_fps = 0.0;          // 当前帧率

// 解码错误信息
const char* decode_error_str(int code) {
    switch(code) {
        case 0: return "Success";
        case 1: return "Unsupported format";
        case 2: return "Not JPEG";
        case 3: return "Invalid marker";
        case 4: return "No memory";
        default: return "Unknown error";
    }
}

// 绘制回调
bool tft_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t* bitmap) {
    if (x < 0) x = 0;
    if (y < 0) y = 0;
    if (x + w > TFT_WIDTH) w = TFT_WIDTH - x;
    if (y + h > TFT_HEIGHT) h = TFT_HEIGHT - y;
    if (w <= 0 || h <= 0) return 1;
    
    tft.pushImage(x, y, w, h, bitmap);
    return 1;
}

// 验证JPEG格式
bool is_valid_jpeg(uint8_t* data, uint32_t len) {
    if (len < 10 || data[0] != 0xFF || data[1] != 0xD8 || 
        data[len-2] != 0xFF || data[len-1] != 0xD9) {
        return false;
    }
    for (uint32_t i = 2; i < len - 2; i++) {
        if (data[i] == 0xFF && data[i+1] == 0xC0) {
            return true;
        }
    }
    return false;
}

// 连接到指定服务器
bool connect_to_server(const char* server_ip) {
    tft.fillScreen(TFT_BLACK);
    tft.setCursor(0, 0);
    tft.print("Connecting to ");
    tft.println(server_ip);
    Serial.printf("Connecting to %s:%d...\n", server_ip, server_port);

    if (client.connected()) {
        client.stop();
    }

    if (client.connect(server_ip, server_port)) {
        tft.println("Connected!");
        Serial.printf("Connected to %s\n", server_ip);
        buf_len = 0;
        // 重置帧率计算
        last_fps_time = millis();
        frame_count = 0;
        current_fps = 0.0;
        return true;
    } else {
        tft.println("Connect failed");
        Serial.printf("Failed to connect to %s\n", server_ip);
        return false;
    }
}

// 尝试连接两个服务器
bool connect_to_any_server() {
    tft.fillScreen(TFT_BLACK);
    tft.setCursor(0, 0);
    tft.println("Trying servers...");

    if (connect_to_server(server_ip1)) {
        current_server = 1;
        return true;
    }

    delay(1000);
    if (connect_to_server(server_ip2)) {
        current_server = 2;
        return true;
    }

    current_server = 0;
    tft.fillScreen(TFT_RED);
    tft.setCursor(0, 0);
    tft.println("All servers failed");
    Serial.println("Both servers are unreachable");
    return false;
}

// 计算并更新帧率
void update_fps() {
    unsigned long current_time = millis();
    frame_count++;  // 每成功解码一帧,计数+1

    // 每1秒计算一次帧率
    if (current_time - last_fps_time >= 1000) {
        current_fps = frame_count * 1000.0 / (current_time - last_fps_time);
        last_fps_time = current_time;
        frame_count = 0;  // 重置计数
        Serial.printf("Current FPS: %.1f\n", current_fps);  // 串口打印帧率
    }
}

void setup() {
    Serial.begin(115200);
    SPIFFS.begin(true);

    // 初始化屏幕
    tft.init();
    tft.setRotation(1);
    tft.fillScreen(TFT_BLACK);
    tft.setTextColor(TFT_WHITE);
    tft.setTextSize(1);
    tft.setCursor(0, 0);
    tft.println("Initializing...");

    // 初始化JPEG解码器
    TJpgDec.setJpgScale(1.0);
    TJpgDec.setSwapBytes(true);
    TJpgDec.setCallback(tft_output);

    // 连接WiFi
    WiFi.begin(ssid, password);
    tft.println("Connecting WiFi...");
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        tft.print(".");
        Serial.print(".");
    }
    tft.fillScreen(TFT_BLACK);
    tft.setCursor(0, 0);
    tft.println("WiFi connected");
    tft.print("IP: ");
    tft.println(WiFi.localIP());
    Serial.print("WiFi connected, IP: ");
    Serial.println(WiFi.localIP());

    // 初始化帧率计时
    last_fps_time = millis();
    frame_count = 0;

    // 尝试连接服务器
    connect_to_any_server();
}

void loop() {
    // 检查连接状态
    if (!client.connected()) {
        tft.fillScreen(TFT_RED);
        tft.setCursor(0, 0);
        tft.println("Disconnected");
        tft.println("Reconnecting...");
        Serial.println("Disconnected, reconnecting...");
        delay(1000);
        if (!connect_to_any_server()) {
            delay(2000);
            return;
        }
    }

    // 接收数据
    unsigned long last_recv = millis();
    while (client.available() && buf_len < sizeof(recv_buf)) {
        int r = client.read(&recv_buf[buf_len], sizeof(recv_buf) - buf_len);
        if (r > 0) {
            buf_len += r;
            last_recv = millis();
        }
    }

    // 5秒无数据则断开
    if (millis() - last_recv > 5000) {
        Serial.println("No data for 5s, disconnect");
        client.stop();
        return;
    }

    // 搜索帧头
    int frame_start = -1;
    for (int i = 0; i <= buf_len - 8; i++) {
        if (recv_buf[i] == 0xAA && recv_buf[i+1] == 0x55 &&
            recv_buf[i+2] == 0xAA && recv_buf[i+3] == 0x55) {
            frame_start = i;
            break;
        }
    }

    if (frame_start == -1) {
        if (buf_len > 7) {
            memmove(recv_buf, &recv_buf[buf_len - 7], 7);
            buf_len = 7;
        }
        return;
    }

    // 解析JPEG长度
    uint32_t jpeg_len = ntohl(*(uint32_t*)&recv_buf[frame_start + 4]);
    if (jpeg_len < 500 || jpeg_len > 3000) {
        Serial.printf("Invalid length: %d, discard\n", jpeg_len);
        memmove(recv_buf, &recv_buf[frame_start + 1], buf_len - (frame_start + 1));
        buf_len -= (frame_start + 1);
        return;
    }

    // 检查数据完整性
    int total_frame_len = frame_start + 8 + jpeg_len;
    if (buf_len < total_frame_len) {
        return;
    }

    // 提取JPEG数据
    uint8_t* jpeg_data = &recv_buf[frame_start + 8];

    // 验证JPEG格式
    if (!is_valid_jpeg(jpeg_data, jpeg_len)) {
        Serial.println("Invalid JPEG (not baseline)");
        memmove(recv_buf, &recv_buf[total_frame_len], buf_len - total_frame_len);
        buf_len = buf_len - total_frame_len;
        tft.fillScreen(TFT_RED);
        tft.setCursor(0, 0);
        tft.println("Invalid JPEG");
        return;
    }

    // 解码并显示
    tft.fillScreen(TFT_BLACK);
    Serial.println("Decoding JPEG...");
    int decode_result = TJpgDec.drawJpg(0, 0, jpeg_data, jpeg_len);
    
    if (decode_result != 0) {
        tft.setCursor(0, 0);
        tft.println("Decode failed");
        tft.print("Err: ");
        tft.println(decode_error_str(decode_result));
        Serial.printf("Decode error: %d (%s)\n", decode_result, decode_error_str(decode_result));
    } else {
        Serial.println("Decode success");
        // 解码成功,更新帧率
        update_fps();

        // 显示当前服务器和帧率(屏幕底部)
        tft.setTextColor(TFT_GREEN);  // 帧率用绿色显示
        tft.setCursor(0, TFT_HEIGHT - 20);  // 服务器信息位置上移
        tft.print("Server: ");
        tft.println(current_server == 1 ? server_ip1 : server_ip2);

        tft.setCursor(0, TFT_HEIGHT - 10);  // 帧率显示在最底部
        tft.print("FPS: ");
        tft.print(current_fps, 1);  // 保留1位小数
    }

    // 清理缓冲区
    int remaining = buf_len - total_frame_len;
    if (remaining > 0) {
        memmove(recv_buf, &recv_buf[total_frame_len], remaining);
    }
    buf_len = remaining;
}

                        

图像采集 + 远程显示

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值