【实战】ESP32-CAM 通过 TCP 实现图像传输至电脑端

目录

前言

一、整体流程与协议设计

1.1 TCP 协议

1.2 整体流程

二、ESP32 端代码实现

三、PC 端代码实现

四、运行效果与调试

结语

程序源码


前言

在嵌入式视觉项目中,ESP32-CAM 凭借其低成本、集成 WiFi/BLE、支持 OV2640/OV7670 摄像头,成为轻量级图像采集的首选。

本文分享一个实时图像流传输项目,使用 VS Code + PlatformIO 开发环境,搭建一个基于 ESP32-CAM 的实时视频流传输系统:ESP32-CAM 采集 JPEG 图像,通过 WiFi TCP 发送至 PC,PC 端用 Python + OpenCV 显示。代码极简,拷贝即用,适合初学者和项目快速原型。

推荐教程

Python+ESP32-CAM 摄像头 拍照 录像 有课件(持续更新)

一、整体流程与协议设计

1.1 TCP 协议

TCP 是一种面向连接的可靠传输协议,广泛用于需要数据完整性的场景。它的核心特性与我们的图像传输项目密切相关:

面向连接与三次握手
通信前,客户端与服务端通过“三次握手”建立连接(SYN → SYN+ACK → ACK),确保双方就绪。代码中的 client.connect() 即触发该过程。

流式传输与粘包
TCP 是字节流,不保留消息边界。多次发送可能合并或拆分,导致接收端无法识别帧边界。为此我们在数据前添加 4字节长度字段,接收端先读长度,再循环读取完整帧,实现消息分割。

可靠传输与超时重传
TCP 使用确认应答(ACK)和超时重传保证数据无损。若发送方未收到 ACK,会重复发送。但重传可能造成长时间阻塞(默认超时可达数分钟),所以我们在项目中用 setTimeout(300) 缩短超时,防止因网络波动导致系统卡死。

1.2 整体流程

TCP 连接的建立与维护

ESP32 作为 TCP 客户端,在 loop() 中通过 client.connect(pc_ip, pc_port) 主动发起连接。TCP 三次握手成功后,连接进入 ESTABLISHED 状态。此后,只要连接不断开,双方即可通过该 socket 进行双向数据传输。代码中,每次循环都会检查 client.connected(),若连接断开(比如 PC 端重启、网络中断),则会触发重新连接,保证系统具备断线重连能力。

数据帧格式设计

我们采用 长度 + 数据 的经典 TLV(Type-Length-Value)变体:

  • 长度(4字节):使用 htonl() 转换为大端序(网络字节序),确保跨平台解析一致。接收端通过 int.from_bytes(raw_len, byteorder='big') 还原。

  • 数据(变长):JPEG 压缩后的图像原始字节流。

这种设计的优点:接收端可以首先获取帧长度,然后循环接收指定字节数,完整拼合一帧,避免了 TCP 流式传输中的“粘包”问题。同时,由于 JPEG 是自包含的格式,解码时只需将完整字节流交给 cv2.imdecode() 即可,无需额外解析。

分块发送的必要性

TCP 虽然提供可靠的字节流服务,但底层发送缓冲区大小有限(通常约 1.5KB~8KB)。如果一次 write() 写入几十 KB 的数据,可能导致底层分包,或者因缓冲区满而阻塞。为了提高效率和避免单次阻塞时间过长,我们将数据拆分为 1024 字节的小块进行循环发送:

  • 每一块独立调用 write(),若某一块发送失败(超时或返回值不足),可立即定位并中断本次传输。

  • 1024 字节远小于常见 MTU(1500 字节),能更好地适应网络层分片,减少 IP 层重组开销。

二、ESP32 端代码实现

platformio.ini

[env:esp32cam]
platform = espressif32
board = esp32cam
framework = arduino

main.c

#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClient.h>
#include "mywifi.h"
#include "camera.h"

const char* pc_ip = "你电脑的IP地址";
const int pc_port = 9091;
WiFiClient tcpClient;

void setup() 
{
  Serial.begin(115200);
  Serial.setDebugOutput(true);
  camera_init();
  wifi_connect();
}

void loop() 
{
  // 检查并建立 TCP 连接
  if (!tcpClient.connected()) 
  {
    if (tcpClient.connect(pc_ip, pc_port)) 
    {
      Serial.println("TCP connected");
    } 
    else 
    {
      delay(1000);
      return;
    }
  }

  // 抓取一帧
  camera_fb_t *fb = esp_camera_fb_get();
  if (!fb) 
  {
    delay(10);
    return;
  }

  // 设置发送超时为 500ms(超过则放弃)
  tcpClient.setTimeout(500);
  
  // 发送长度(4字节)
  uint32_t len = htonl(fb->len);
  size_t written = tcpClient.write((uint8_t*)&len, 4);
  if (written != 4) 
  {
    // 发送长度失败,断开连接,释放帧,退出本次循环
    tcpClient.stop();
    esp_camera_fb_return(fb);
    Serial.println("Send length timeout, reconnect");
    return;
  }

  // 分块发送图像数据(每次 1024 字节)
  uint8_t *buf = fb->buf;
  size_t remaining = fb->len;
  const size_t chunk_size = 1024;
  bool send_ok = true;

  while (remaining > 0) 
  {
    size_t to_send = (remaining > chunk_size) ? chunk_size : remaining;
    size_t w = tcpClient.write(buf, to_send);
    if (w != to_send) 
    {
      // 某一分块未发送完,连接已坏
      send_ok = false;
      break;
    }
    buf += w;
    remaining -= w;
  }

  // 释放帧缓冲区
  esp_camera_fb_return(fb);

  // 如果发送失败,断开连接,下次循环会重连
  if (!send_ok) 
  {
    tcpClient.stop();
    Serial.println("Send data timeout, reconnect");
    return;
  }

  // 可以保留一个小延时来控制帧率
  delay(1);
}

camera.c

#include "camera.h"

void camera_init() 
{
    camera_config_t config;
    config.ledc_channel = LEDC_CHANNEL_0;
    config.ledc_timer = LEDC_TIMER_0;
    config.pin_d0 = Y2_GPIO_NUM;
    config.pin_d1 = Y3_GPIO_NUM;
    config.pin_d2 = Y4_GPIO_NUM;
    config.pin_d3 = Y5_GPIO_NUM;
    config.pin_d4 = Y6_GPIO_NUM;
    config.pin_d5 = Y7_GPIO_NUM;
    config.pin_d6 = Y8_GPIO_NUM;
    config.pin_d7 = Y9_GPIO_NUM;
    config.pin_xclk = XCLK_GPIO_NUM;
    config.pin_pclk = PCLK_GPIO_NUM;
    config.pin_vsync = VSYNC_GPIO_NUM;
    config.pin_href = HREF_GPIO_NUM;
    config.pin_sccb_sda = SIOD_GPIO_NUM;
    config.pin_sccb_scl = SIOC_GPIO_NUM;
    config.pin_pwdn = PWDN_GPIO_NUM;
    config.pin_reset = RESET_GPIO_NUM;
    
    // ====================== 核心优化参数 ======================
    config.xclk_freq_hz = 20000000;   // 恢复20M,速度更快
    config.frame_size = FRAMESIZE_HVGA; // 480x320(不卡的关键)
    config.pixel_format = PIXFORMAT_JPEG;
    config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
    config.fb_location = CAMERA_FB_IN_PSRAM;
    config.jpeg_quality = 12;    // 降低一点画质,大幅提升速度
    config.fb_count = 1;         // 单缓冲,不占内存

    // 无PSRAM自动降级
    if (!psramFound()) 
    {
        config.frame_size = FRAMESIZE_QVGA;
        config.fb_location = CAMERA_FB_IN_DRAM;
        config.jpeg_quality = 15;
    }

    // 初始化摄像头
    esp_err_t err = esp_camera_init(&config);
    if (err != ESP_OK) 
    {
        Serial.printf("Camera init failed with error 0x%x\n", err);
        ESP.restart();
    }

    sensor_t *s = esp_camera_sensor_get();
    s->set_vflip(s, 1);          // 垂直翻转
    s->set_hmirror(s, 1);        // 水平镜像
    
    s->set_brightness(s, 1);     
    s->set_contrast(s, 1);       
    s->set_saturation(s, -1);    
    
    // 特殊优化:锐化、降噪、曝光
    s->set_sharpness(s, 2);      
    s->set_denoise(s, 1);        
    s->set_exposure_ctrl(s, 1);  
    s->set_gain_ctrl(s, 1);      
    s->set_whitebal(s, 1);       
}

camera.h

#ifndef CAMERA_H
#define CAMERA_H

#include <Arduino.h>
#include "esp_camera.h"
#include "camera_pins.h"

void camera_init();

#endif 

camera_pins.h

#ifndef CAMERA_PINS_H
#define CAMERA_PINS_H

#define PWDN_GPIO_NUM  32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM  0
#define SIOD_GPIO_NUM  26
#define SIOC_GPIO_NUM  27

#define Y9_GPIO_NUM    35
#define Y8_GPIO_NUM    34
#define Y7_GPIO_NUM    39
#define Y6_GPIO_NUM    36
#define Y5_GPIO_NUM    21
#define Y4_GPIO_NUM    19
#define Y3_GPIO_NUM    18
#define Y2_GPIO_NUM    5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM  23
#define PCLK_GPIO_NUM  22

// 4 for flash led or 33 for normal led
#define LED_GPIO_NUM   4

#endif

三、PC 端代码实现

import socket
import cv2
import numpy as np

# 服务器监听地址和端口(与 ESP32 代码中的 pc_ip 和 pc_port 一致)
SERVER_IP = '0.0.0.0'   # 监听所有网络接口
SERVER_PORT = 9091

def main():
    # 创建 TCP 服务器
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_socket.bind((SERVER_IP, SERVER_PORT))
    server_socket.listen(1)

    print(f"等待 ESP32-CAM 连接(端口 {SERVER_PORT})...")
    client_socket, addr = server_socket.accept()
    print(f"已连接:{addr}")

    try:
        while True:
            # 1. 读取 4 字节的帧长度(大端序)
            raw_len = client_socket.recv(4)
            if not raw_len:
                break
            frame_len = int.from_bytes(raw_len, byteorder='big')

            # 2. 根据长度读取完整的 JPEG 数据
            img_data = b''
            while len(img_data) < frame_len:
                chunk = client_socket.recv(frame_len - len(img_data))
                if not chunk:
                    break
                img_data += chunk

            if len(img_data) < frame_len:
                break   # 数据不完整,断开连接

            # 3. 将 JPEG 数据解码为 OpenCV 图像
            nparr = np.frombuffer(img_data, np.uint8)
            img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)

            if img is not None:
                cv2.imshow("ESP32-CAM", img)
                # 按 'q' 退出
                if cv2.waitKey(1) & 0xFF == ord('q'):
                    break

    except Exception as e:
        print(f"接收错误:{e}")
    finally:
        client_socket.close()
        server_socket.close()
        cv2.destroyAllWindows()
        print("程序已退出")

if __name__ == "__main__":
    main()

四、运行效果与调试

烧录与运行

  1. 在 VS Code 中打开 PlatformIO 项目,修改 ssid/password 和 pc_ip

  2. 连接 ESP32-CAM 到电脑,点击 PlatformIO 的“上传”按钮进行编译烧录。

  3. 先运行 PC 端的 show.py(确保防火墙允许 9091 端口)。

  4. 给 ESP32-CAM 复位,等待连接成功。

  5. 屏幕上会弹出 OpenCV 窗口,显示实时视频流。

画面效果

结语

通过本项目的实战开发,我们从零搭建了一套基于 ESP32-CAM 的实时视频流传输系统,完整走过了硬件配置、嵌入式固件开发、TCP 通信协议设计以及 PC 端图像解码显示的全链路流程。

同时,这个项目的视频流有时会有小几秒卡顿的问题,如果你有相关优化思路或解决方案,欢迎随时交流探讨。

程序源码

esp32cam + python

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值