在物联网应用中,“图像采集 + 远程显示” 是一个常见场景,比如监控摄像头、可视化传感器等。恰好本人手上有一块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端:
-
WiFi连接:通过
wifi_init_sta函数,设备以STA模式连接到指定的WiFi网络。一旦连接成功,设备会获取IP地址并开始监听TCP连接。 -
TCP服务器:在
tcp_server_task任务中,程序创建了一个TCP服务器,监听指定的端口(8080)。当有客户端连接时,服务器会开始发送图像数据。 -
图像发送:将采集到的图像数据通过TCP连接发送给客户端。每个图像帧前会发送一个帧头,包含帧的长度信息,以便客户端正确解析图像数据。
ESP32显示器:
-
WiFi连接:连接到指定的WiFi网络。尝试连接到两个预设的服务器IP地址中的一个。(这里为什么是两个服务器IP呢,因为我有两块ESP32CAM😁)
-
TCP客户端:与服务器建立TCP连接,接收图像数据。
-
图像接收与解码:从服务器接收JPEG图像数据。使用
TJpg_Decoder库解码JPEG图像并显示在TFT屏幕上。 -
帧率计算:计算并显示当前的帧率。
-
屏幕显示:在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;
}
图像采集 + 远程显示
1491

被折叠的 条评论
为什么被折叠?



