基于ESP32_CAM与Qt Creator的智能视频监控项目(代码开源)

**前言:**本文为手把手教学的基于 ESP32_CAM 与 Qt Creator 的智能视频监控项目,项目使用的 MCU 为乐鑫的 ESP32_CAM 搭配 Qt Creator 制作上位机,Qt 的版本为 Qt 5.9.0。本项目的智能 ESP32 Camera 拥有多种视频格式解码、WIFI 灯源控制、WIFI Camera 和智能预警等功能。项目分为上位机与下位机两部分的代码编程,也包含简单的图像算法设计,算是一个 ESP32 很好的练手项目。希望这篇博文能给读者朋友的工程项目给予些许帮助,Respect(代码开源)!

**硬件与软件:**ESP32_CAM、iKun ESP32 Camera Studio、Arduino IDE、Qt 5.9.0

项目结果图:

一、ESP32_CAM 智能监控项目

1.1 ESP32_CAM 概述

ESP32_CAM 是一款超小型、低成本、集成 WiFi + 蓝牙 + 摄像头 的物联网视觉模组,由 AI-Thinker(安信可)主导设计,核心基于乐鑫 ESP32 SoC;无板载 USB-TTL,需外接串口模块烧录;采用 DIP-16 封装,可直插面包板 / 专用底板,是嵌入式视觉原型与量产小批量方案的热门选择。乐鑫官方为 ESP32_CAM 提供了标准的摄像头例程,研发工程师可以根据自己的需求进行摄像头相关的开发。

核心硬件规格:

  • 主控:ESP32(常见 WROVER-E 版本,双核 Xtensa LX6,最高 240MHz,600 DMIPS;520KB SRAM + 4MB Flash + 4MB PSRAM);支持 2.4GHz WiFi(802.11b/g/n)、蓝牙 4.2(BLE/BR/EDR);支持 STA/AP/STA+AP,SmartConfig/AirKiss 配网,OTA 升级
  • 摄像头:标配 OV2640 200 万像素(UXGA 1600×1200),DVP 接口;输出 YUV422、RGB565、JPEG;板载 LED 补光灯;部分兼容 OV7670
  • 尺寸与供电:27×40.5×4.5 mm;推荐 5V DC 供电(3.3V 易导致摄像头工作异常);深度睡眠功耗约 6mA
  • 扩展:MicroSD 卡槽本地存储;IO 资源因 DVP 占用较多,剩余引脚有限

1.2 ESP32_CAM 智能监控项目概述

本篇博客项目中作者制作的 ikun ESP32 Camera 拥有上位机与下位机,上位机为 Qt Creator 编写的 ikun ESP32 Camera Studio 软件,下位机为乐鑫官方发布的 ESP32_CAM。本项目基于 ESP32_CAM 与 Qt Creator的智能视频监控项目共有 4 项功能,包括:1、WIFI Camera 功能;2、多种视频流解码;3、远程 HTTP 控灯;4、视频流智能预警;

1、WIFI Camera:利用 Qt Creator 的 QNetworkAccessManager 函数来拉取 ESP32_CAM 发送过来的视频流;

2、多种视频流解码:使用 Qt Creator 提供的多种多媒体视频解码库兼容 ESP32_CAM 端发送过来的各种视频流;

3、远程 HTTP 控灯:ESP32CAM 作为 HTTP 服务器,监听指定端口,ESP32_CAM 利用 HTTP 请求,发送给 ESP32 的 IP 地址;

4、视频流智能预警:针对视频流进行像素级比较,计算RGB差异,当差异超过阈值时判定为图像变化,并保存预警照片;智能预警功能可以有效地监控家中出现未知异动之后都情况,亦如陌生人闯入或者火灾等情况,个人认为还是比较有用的功能!

二、ESP32_CAM 例程代码引用

乐鑫官方给 ESP32_CAM 提供了很多编写开发的 IDE 选择,包括:Arduino IDE(ESP32 库)和 ESP-IDF(乐鑫官方 SDK)。作者这里使用 Arduino IDE 进行 ESP_CAM 的开发。

1、本篇博客工程代码的基本框架可以从 Arduino IDE 的示例教程中获取,后续的项目代码在此基础上进行修改删减即可。读者朋友在正确导入ESP32工程库后,按照下图去创建项目的基础框架:

2、使用 Arduino IDE 对示例进行编译,确保当前编译调试环境是 OK 的;

3、将 Arduino IDE 示例代码进行保存,方便进行后续代码编写;

作者补充:

ESP32_CAM 开发板必须将 GPIO0 接地进入下载模式;串口 TX/RX 交叉、GND 共地;供电不足是烧录失败 / 复位的高频原因

三、ESP32 CAM Video Sur 代码与解析

3.1 代码库文件引入与变量定义

#include <WebServer.h> 引入 WebServer 库,方便 ESP32 启用 HTTP

#define CAMERA_MODEL_AI_THINKER // Has PSRAM 启用乐鑫的 AI_THINKER

#define LED_pin 4 ESP32_CAM 的 LED 是GPIO4(乐鑫官方提供原理图)

const char* ssid = "TP-LINK_E386"; ESP32_CAM 连接的 WIFI 名称

const char* password = "13852200640"; ESP32_CAM 连接的 WIFI 密码

WebServer server(80); 创建Web服务器对象,监听80端口(HTTP默认端口)

3.2 WIFI ESP32 Camera 代码

WIFI ESP32 Camera 这部分功能代码作者直接使用了乐鑫官方提供的代码,读者朋友根据本篇博客的第 2 章节去进行操作即可!作者这边针对官方提供的源码进行简单的解析!
乐鑫官方提供的 ESP32_CAM 例程代码核心就是 setup() 函数,包括:WIFI Camera 的 GPIO 引脚配置、摄像头的视频流格式、摄像头的数据格式初始化设置和 WIFI 连接等

cpp 复制代码
void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true);
  Serial.println();

  Serial.println("iKun ESP32 CAM Video Sur By:混分巨兽龙某某");

  //设置LED1引脚为输出模式
  pinMode(LED_pin, OUTPUT);
  //LED1引脚输出低电平,灯灭
  digitalWrite(LED_pin, LOW);

  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_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.frame_size = FRAMESIZE_UXGA;
  config.pixel_format = PIXFORMAT_JPEG; // for streaming
  //config.pixel_format = PIXFORMAT_RGB565; // for face detection/recognition
  config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
  config.fb_location = CAMERA_FB_IN_PSRAM;
  config.jpeg_quality = 12;
  config.fb_count = 1;
  
  // if PSRAM IC present, init with UXGA resolution and higher JPEG quality
  //                      for larger pre-allocated frame buffer.
  if(config.pixel_format == PIXFORMAT_JPEG){
    if(psramFound()){
      config.jpeg_quality = 10;
      config.fb_count = 2;
      config.grab_mode = CAMERA_GRAB_LATEST;
    } else {
      // Limit the frame size when PSRAM is not available
      config.frame_size = FRAMESIZE_SVGA;
      config.fb_location = CAMERA_FB_IN_DRAM;
    }
  } else {
    // Best option for face detection/recognition
    config.frame_size = FRAMESIZE_240X240;
#if CONFIG_IDF_TARGET_ESP32S3
    config.fb_count = 2;
#endif
  }

#if defined(CAMERA_MODEL_ESP_EYE)
  pinMode(13, INPUT_PULLUP);
  pinMode(14, INPUT_PULLUP);
#endif

  // camera init
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    return;
  }

  sensor_t * s = esp_camera_sensor_get();
  // initial sensors are flipped vertically and colors are a bit saturated
  if (s->id.PID == OV3660_PID) {
    s->set_vflip(s, 1); // flip it back
    s->set_brightness(s, 1); // up the brightness just a bit
    s->set_saturation(s, -2); // lower the saturation
  }
  // drop down frame size for higher initial frame rate
  if(config.pixel_format == PIXFORMAT_JPEG){
    s->set_framesize(s, FRAMESIZE_QVGA);
  }

#if defined(CAMERA_MODEL_M5STACK_WIDE) || defined(CAMERA_MODEL_M5STACK_ESP32CAM)
  s->set_vflip(s, 1);
  s->set_hmirror(s, 1);
#endif

#if defined(CAMERA_MODEL_ESP32S3_EYE)
  s->set_vflip(s, 1);
#endif

  WiFi.begin(ssid, password);
  WiFi.setSleep(false);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");

  startCameraServer();

  Serial.print("Camera Ready! Use 'http://");
  Serial.print(WiFi.localIP());
  Serial.println("' to connect");

  // LED灯控制
  server.on("/control", handleControlRequest); // LED控制接口
  // 启动服务器
  server.begin();
  Serial.println("HTTP服务器已启动");
}

3.3 ESP32_CAM 的 WIFI 控制灯代码

乐鑫官方提供的 ESP32_CAM 代码可以稳定运行 WIFI Camera 功能,并且预留了很多网络协议的库,这边作者引入 WebServer.h 库进行 HTTP 协议的 LED 小灯控制!

根据乐鑫官方提供的 ESP32_CAM 原理图,我们可以发送控制板载的 LED 的 GPIO 引脚为 GPIO4,我们只需要在 Qt 端上位机发送指令去控制 ESP32_CAM 的 GPIO 引脚高低电平即可。

作者编写了一个简单的 HTTP 服务器,监听 /control 接口,解析 var=led&val=40 或 var=led&val=41 参数,并控制 LED 灯。代码的关键核心:

1、WiFi 配置:你需要把 ssid 和 password 替换成自己的 WiFi 名称和密码,这样 ESP32-CAM 才能接入局域网;

2、LED 引脚:ESP32-CAM 的板载 LED 通常接在 GPIO4,且是低电平点亮(LOW 亮、HIGH 灭),如果你的硬件不同,需要调整 digitalWrite 的 HIGH/LOW。

3、HTTP 接口处理:服务器监听 /control 路径,对应 QT 端的 http://IP/control?var=led\&val=40 请求;通过 server.arg("val") 获取 QT 发送的参数值(40/41)。

cpp 复制代码
// 处理/control接口的请求
void handleControlRequest() {
  // 检查是否有var参数,且值为led
  if (server.hasArg("var") && server.arg("var") == "led") {
    // 检查是否有val参数
    if (server.hasArg("val")) {
      String valStr = server.arg("val");
      int val = valStr.toInt();
      
      // 根据参数值控制LED
      if (val == 40) {
        // val=40 关闭LED
        digitalWrite(LED_pin, LOW);  // 注意:ESP32-CAM的LED可能是低电平点亮,根据实际情况调整HIGH/LOW
        Serial.printf("LED已关闭 (val=%d)\n", val);
        server.send(200, "text/plain", "LED OFF"); // 返回成功响应
      } else if (val == 41) {
        // val=41 打开LED
        digitalWrite(LED_pin, HIGH);
        Serial.printf("LED已打开 (val=%d)\n", val);
        server.send(200, "text/plain", "LED ON"); // 返回成功响应
      } else {
        // 无效的参数值
        Serial.printf("无效的LED参数值: %d\n", val);
        server.send(400, "text/plain", "Invalid LED value"); // 返回错误响应
      }
    } else {
      // 缺少val参数
      server.send(400, "text/plain", "Missing 'val' parameter");
    }
  } else {
    // 不是控制LED的请求
    server.send(400, "text/plain", "Not a LED control request");
  }
}

void loop() {
  // 处理客户端的HTTP请求
  server.handleClient();
  delay(10);
}

3.4 ESP32_CAM 的完整代码

cpp 复制代码
/********************************** (C) COPYRIGHT *******************************
* File Name          : CameraWebServer.ino
* Author             : 混分巨兽龙某某
* Version            : V1.0.0
* Date               : 2026/02/21
* Description        : Intelligent Video Monitoring Project Based on ESP32_CAM and Qt Creator
********************************************************************************/
#include "esp_camera.h"
#include <WiFi.h>
#include <WebServer.h>

#define CAMERA_MODEL_AI_THINKER // Has PSRAM
#include "camera_pins.h"

#define LED_pin 4

const char* ssid = "TP-LINK_E386";
const char* password = "xxxxxxxx";

// 创建Web服务器对象,监听80端口(HTTP默认端口)
WebServer server(80);
void startCameraServer();

// 处理/control接口的请求
void handleControlRequest() {
  // 检查是否有var参数,且值为led
  if (server.hasArg("var") && server.arg("var") == "led") {
    // 检查是否有val参数
    if (server.hasArg("val")) {
      String valStr = server.arg("val");
      int val = valStr.toInt();
      
      // 根据参数值控制LED
      if (val == 40) {
        // val=40 关闭LED
        digitalWrite(LED_pin, LOW);  // 注意:ESP32-CAM的LED可能是低电平点亮,根据实际情况调整HIGH/LOW
        Serial.printf("LED已关闭 (val=%d)\n", val);
        server.send(200, "text/plain", "LED OFF"); // 返回成功响应
      } else if (val == 41) {
        // val=41 打开LED
        digitalWrite(LED_pin, HIGH);
        Serial.printf("LED已打开 (val=%d)\n", val);
        server.send(200, "text/plain", "LED ON"); // 返回成功响应
      } else {
        // 无效的参数值
        Serial.printf("无效的LED参数值: %d\n", val);
        server.send(400, "text/plain", "Invalid LED value"); // 返回错误响应
      }
    } else {
      // 缺少val参数
      server.send(400, "text/plain", "Missing 'val' parameter");
    }
  } else {
    // 不是控制LED的请求
    server.send(400, "text/plain", "Not a LED control request");
  }
}

void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true);
  Serial.println();

  Serial.println("iKun ESP32 CAM Video Sur By:混分巨兽龙某某");

  //设置LED1引脚为输出模式
  pinMode(LED_pin, OUTPUT);
  //LED1引脚输出低电平,灯灭
  digitalWrite(LED_pin, LOW);

  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_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.frame_size = FRAMESIZE_UXGA;
  config.pixel_format = PIXFORMAT_JPEG; // for streaming
  //config.pixel_format = PIXFORMAT_RGB565; // for face detection/recognition
  config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
  config.fb_location = CAMERA_FB_IN_PSRAM;
  config.jpeg_quality = 12;
  config.fb_count = 1;
  
  // if PSRAM IC present, init with UXGA resolution and higher JPEG quality
  //                      for larger pre-allocated frame buffer.
  if(config.pixel_format == PIXFORMAT_JPEG){
    if(psramFound()){
      config.jpeg_quality = 10;
      config.fb_count = 2;
      config.grab_mode = CAMERA_GRAB_LATEST;
    } else {
      // Limit the frame size when PSRAM is not available
      config.frame_size = FRAMESIZE_SVGA;
      config.fb_location = CAMERA_FB_IN_DRAM;
    }
  } else {
    // Best option for face detection/recognition
    config.frame_size = FRAMESIZE_240X240;
#if CONFIG_IDF_TARGET_ESP32S3
    config.fb_count = 2;
#endif
  }

#if defined(CAMERA_MODEL_ESP_EYE)
  pinMode(13, INPUT_PULLUP);
  pinMode(14, INPUT_PULLUP);
#endif

  // camera init
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    return;
  }

  sensor_t * s = esp_camera_sensor_get();
  // initial sensors are flipped vertically and colors are a bit saturated
  if (s->id.PID == OV3660_PID) {
    s->set_vflip(s, 1); // flip it back
    s->set_brightness(s, 1); // up the brightness just a bit
    s->set_saturation(s, -2); // lower the saturation
  }
  // drop down frame size for higher initial frame rate
  if(config.pixel_format == PIXFORMAT_JPEG){
    s->set_framesize(s, FRAMESIZE_QVGA);
  }

#if defined(CAMERA_MODEL_M5STACK_WIDE) || defined(CAMERA_MODEL_M5STACK_ESP32CAM)
  s->set_vflip(s, 1);
  s->set_hmirror(s, 1);
#endif

#if defined(CAMERA_MODEL_ESP32S3_EYE)
  s->set_vflip(s, 1);
#endif

  WiFi.begin(ssid, password);
  WiFi.setSleep(false);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");

  startCameraServer();

  Serial.print("Camera Ready! Use 'http://");
  Serial.print(WiFi.localIP());
  Serial.println("' to connect");

  // LED灯控制
  server.on("/control", handleControlRequest); // LED控制接口
  // 启动服务器
  server.begin();
  Serial.println("HTTP服务器已启动");
}

void loop() {
  // 处理客户端的HTTP请求
  server.handleClient();
  delay(10);
}

四、ESP32 Camera Studio

4.1 WIFI Camera 部分代码

作者这边利用 Qt 的多媒体库从而可以轻松去解码各种视频流协议,支持的格式如下:

cpp 复制代码
/**
 * @file mainwindow.cpp
 * @brief ESP32-CAM QT客户端主窗口实现
 * 
 * 该文件实现了ESP32-CAM网络摄像头的视频流显示、分辨率调整等功能
 */

/**
 * @def WINDOW_TITLE
 * @brief 窗口标题宏定义
 */
#define WINDOW_TITLE "iKun ESP32 Camera Studio"

/**
 * @brief MainWindow构造函数
 * @param parent 父窗口指针
 * 
 * 初始化UI界面,设置窗口标题,添加分辨率选项,请求ESP32-CAM状态
 */
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
    , m_ledState(40)
    , m_safetyEnabled(false)
    , m_previousImage()
    , m_lastCaptureTime(QDateTime::currentDateTime())
{
    // 初始化UI界面
    ui->setupUi(this);
    
    // 设置窗口标题
    setWindowTitle(WINDOW_TITLE);
    
    // 分辨率选项列表
    QStringList vPixelList = {
        "96x96",
        "QQVGA(160x120)",
        "QCIF(176x144)",
        "HQVGA(240x176)",
        "240x240",
        "QVGA(320x240)",
        "CIF(400x296)",
        "HVGA(480x320)",
        "VGA(640x480)",
        "SVGA(800x600)",
        "XGA(1024x768)",
        "HD(1280x720)",
        "SXGA(1280x1024)",
        "UXGA(1600x1200)",
    };
    
    // 添加分辨率选项到下拉框
    ui->cbxPixel->addItems(vPixelList);
    
    // 请求ESP32-CAM当前状态
    RequestStatus();
}

/**
 * @brief MainWindow析构函数
 * 
 * 释放UI资源
 */
MainWindow::~MainWindow()
{
    delete ui;
}

开始取流的按键代码:

cpp 复制代码
/**
 * @brief 开始取流按钮点击事件处理
 * 
 * 连接到ESP32-CAM的视频流,开始获取视频数据
 */
void MainWindow::on_btnStart_clicked()
{
    // 如果已经连接,直接返回
    if (m_pvClient != nullptr) {
        return;
    }
    
    // 获取IP地址
    const QString sIPAddr = ui->txtAddr->text();
    if (sIPAddr.size() == 0) {
        QMessageBox::warning(this, "提示", "请输入IP地址");
        return;
    }
    
    // 构建视频流URL
    const QString sURL = "http://" + sIPAddr + ":81/stream";
    
    // 创建网络访问管理器
    QNetworkAccessManager *pvManager = new QNetworkAccessManager();
    
    // 创建网络请求
    QNetworkRequest vRequest;
    vRequest.setUrl(sURL);
    vRequest.setRawHeader("Connection", "Keep-Alive");
    vRequest.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
    
    // 发送GET请求获取视频流
    m_pvClient = pvManager->get(vRequest);
    
    // 连接readyRead信号,处理接收到的数据
    connect(m_pvClient, &QNetworkReply::readyRead, this, &MainWindow::readyRead);
}

结束取流的按键代码:

cpp 复制代码
/**
 * @brief 结束取流按钮点击事件处理
 * 
 * 断开与ESP32-CAM的连接,停止获取视频数据
 */
void MainWindow::on_btnStop_clicked()
{
    if (m_pvClient != nullptr) {
        // 断开信号连接
        disconnect(m_pvClient);
        
        // 中止请求
        m_pvClient->abort();
        
        // 延迟删除
        m_pvClient->deleteLater();
        
        // 重置指针
        m_pvClient = nullptr;
        
        // 清空缓冲区
        m_buffer.clear();
    }
}

视频流分辨率的解码:

cpp 复制代码
/**
 * @brief 请求ESP32-CAM状态
 * 
 * 获取ESP32-CAM的当前状态,包括分辨率等信息
 */
void MainWindow::RequestStatus()
{
    // 清空缓冲区
    m_buffer.clear();
    
    // 获取IP地址
    const QString sIPAddr = ui->txtAddr->text();
    if (sIPAddr.size() == 0) {
        return;
    }
    
    // 构建状态请求URL
    const QString sUrl = "http://" + sIPAddr + "/status";
    qDebug() << sUrl;
    
    // 创建网络访问管理器
    QNetworkAccessManager manager;
    
    // 创建网络请求
    QNetworkRequest vRequest;
    QNetworkReply *reply;
    vRequest.setUrl(QUrl(sUrl));
    vRequest.setRawHeader("Connection", "Keep-Alive");
    vRequest.setRawHeader("Cache-Control", "no-cache");
    vRequest.setRawHeader("Pragma", "no-cache");
    vRequest.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
    
    // 发送GET请求
    reply = manager.get(vRequest);
    
    // 创建事件循环,等待请求完成
    QEventLoop loop;
    QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
    loop.exec();
    
    // 处理响应
    if (reply->error() == QNetworkReply::NoError) {
        // 读取响应数据
        QString jsonString = reply->readAll();
        
        // 解析JSON数据
        QJsonDocument jsonDocument = QJsonDocument::fromJson(jsonString.toUtf8());
        if (!jsonDocument.isNull()) {
            if (jsonDocument.isObject()) {
                QJsonObject jsonObject = jsonDocument.object();
                // 获取分辨率索引
                int framesize = jsonObject["framesize"].toInt();
                // 设置下拉框当前索引
                ui->cbxPixel->setCurrentIndex(framesize);
            }
        }
    } else {
        // 输出错误信息
        qDebug() << "Error: " << reply->errorString();
    }
    
    // 释放回复对象
    reply->deleteLater();
}


/**
 * @brief 分辨率下拉框选择变化事件处理
 * @param index 选择的分辨率索引
 * 
 * 当用户选择不同的分辨率时,发送请求到ESP32-CAM更改分辨率
 */
void MainWindow::on_cbxPixel_currentIndexChanged(int index)
{
    // 静态变量,用于跳过初始化时的第一次触发
    static bool isFirstSend = true;
    if (isFirstSend) {
        isFirstSend = false;
        return;
    }
    
    // 获取IP地址
    const QString sIPAddr = ui->txtAddr->text();
    if (sIPAddr.size() == 0) {
        return;
    }
    
    // 构建分辨率更改请求URL
    const QString sUrl = "http://" + sIPAddr + "/control?var=framesize&val=" + QString::number(index);
    qDebug() << sUrl;
    
    // 创建网络访问管理器
    QNetworkAccessManager manager;
    
    // 创建网络请求
    QNetworkRequest vRequest;
    QNetworkReply *reply;
    vRequest.setUrl(QUrl(sUrl));
    vRequest.setRawHeader("Connection", "Keep-Alive");
    vRequest.setRawHeader("Cache-Control", "no-cache");
    vRequest.setRawHeader("Pragma", "no-cache");
    vRequest.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
    
    // 发送GET请求
    reply = manager.get(vRequest);
    
    // 清空缓冲区
    m_mutex.lock();
    m_buffer.clear();
    m_mutex.unlock();
    
    // 创建事件循环,等待请求完成
    QEventLoop loop;
    QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
    loop.exec();
    
    // 处理错误
    if (reply->error() != QNetworkReply::NoError) {
        qDebug() << "Error: " << reply->errorString();
    }
    
    // 释放回复对象
    reply->deleteLater();
}

/**
 * @brief 网络数据就绪事件处理
 * 
 * 当接收到网络数据时触发,处理MJPEG视频流数据,解析并显示视频帧
 */
void MainWindow::readyRead()
{
    // 如果没有网络回复对象,直接返回
    if (m_pvClient == nullptr) {
        return;
    }
    
    // 读取网络数据到缓冲区
    m_mutex.lock();
    m_buffer.append(m_pvClient->readAll());
    m_mutex.unlock();
    
    // MJPEG流标记
    const QString sStartKey = "Content-Type: image/jpeg\r\nContent-Length:";
    const QString sEndKey = "\r\n--123456789000000000000987654321\r\n";
    
    // 循环处理缓冲区中的数据
    while (true) {
        // 查找JPEG图像的开始标记
        int nStartPos = m_buffer.indexOf(sStartKey);
        if (nStartPos < 0) {
            break;
        }
        
        // 查找JPEG图像的结束标记
        int nEndPos = m_buffer.indexOf(sEndKey, nStartPos);
        if (nEndPos < 0) {
            break;
        }
        
        // 调整开始位置
        nStartPos = m_buffer.indexOf("\r\n");
        if (nStartPos < 0) {
            break;
        }
        
        // 计算结束位置
        nEndPos += sEndKey.size();
        
        // 提取JPEG图像数据
        QByteArray vBuf = m_buffer.mid(nStartPos, nEndPos - nStartPos);
        if (vBuf.size() == 0) {
            continue;
        }
        
        // 解析并显示图像
        ParseFrame(vBuf);
        
        // 从缓冲区中移除已处理的数据
        m_mutex.lock();
        m_buffer = m_buffer.mid(nEndPos, m_buffer.size() - nEndPos);
        m_mutex.unlock();
    }
}

4.2 控灯部分代码

控制 LED 灯的方法有很多,作者这边利用了 QNetworkAccessManager 发送 HTTP GET 请求,使用QEventLoop等待网络请求完成。Qt 端上位机捕获并处理网络请求错误,在成功后切换LED状态,实现40和41的循环。作者补充:本人为了防止出现 HTTP 传输异常的情况,默认连续使用 2 次的 QNetworkAccessManager 发送 HTTP GET 请求,保证了 Qt 端数据的可以稳定控制下位机 ESP32_CAM 的 LED,读者朋友们平时也可以这样操作!

cpp 复制代码
/**
 * @brief LED控制按钮点击事件处理
 * 
 * 控制ESP32-CAM上的LED灯开关,实现参数循环切换:40 -> 41 -> 40 -> ...
 * 仅发送一次请求,不进行重试
 */
void MainWindow::on_btnLED_clicked()
{
    // 获取IP地址
    const QString sIPAddr = ui->txtAddr->text();
    if (sIPAddr.size() == 0) {
        QMessageBox::warning(this, "提示", "请输入IP地址");
        return;
    }
    
    // 构建LED控制URL,使用当前m_ledState值
    const QString sUrl = "http://" + sIPAddr + "/control?var=led&val=" + QString::number(m_ledState);
    qDebug() << "LED control URL:" << sUrl;
    
    // 创建网络访问管理器
    QNetworkAccessManager manager;
    
    // 创建网络请求
    QNetworkRequest vRequest;
    vRequest.setUrl(QUrl(sUrl));
    vRequest.setRawHeader("Connection", "Keep-Alive");
    vRequest.setRawHeader("Cache-Control", "no-cache");
    vRequest.setRawHeader("Pragma", "no-cache");
    vRequest.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
    vRequest.setRawHeader("Accept", "*/*");
    
    // 发送GET请求
    QNetworkReply *reply = manager.get(vRequest);
    
    // 创建事件循环,等待请求完成
    QEventLoop loop;
    QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
    loop.exec();
    
    // 处理响应
    if (reply->error() != QNetworkReply::NoError) {

        // 创建网络访问管理器(设置2级重传机制,避免网络异常开灯失败)
        QNetworkAccessManager manager1;

        // 创建网络请求
        QNetworkRequest vRequest1;
        vRequest1.setUrl(QUrl(sUrl));
        vRequest1.setRawHeader("Connection", "Keep-Alive");
        vRequest1.setRawHeader("Cache-Control", "no-cache");
        vRequest1.setRawHeader("Pragma", "no-cache");
        vRequest1.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
        vRequest1.setRawHeader("Accept", "*/*");

        // 发送GET请求
        QNetworkReply *reply = manager1.get(vRequest1);

        // 创建事件循环,等待请求完成
        QEventLoop loop;
        QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
        loop.exec();

        // 处理响应
        if (reply->error() != QNetworkReply::NoError) {
            qDebug() << "LED control error: " << reply->errorString();
            QMessageBox::warning(this, "错误", "控制LED失败: " + reply->errorString());
            reply->deleteLater();
        }else{
            qDebug() << "LED control successful, val:" << m_ledState;
            // 切换LED状态:40 -> 41 -> 40 -> ...
            m_ledState = (m_ledState == 40) ? 41 : 40;
            reply->deleteLater();
        }
    } else {
        qDebug() << "LED control successful, val:" << m_ledState;
        
        // 切换LED状态:40 -> 41 -> 40 -> ...
        m_ledState = (m_ledState == 40) ? 41 : 40;
        reply->deleteLater();
    }
}

4.3 智能预警部分代码

作者针对这种很实用的智能预警功能进行了很巧妙的图像算法设计(智能预警:可以用于监控家中是否进入陌生人或者火灾等异常状况)。
智能预警的功能实现:

  • 智能预警检测 :当 boxSafety 复选框被勾选时,启用智能预警功能

  • 图像变化检测 :每隔 1 秒比较前后两帧图像,检测是否发生变化

  • 自动保存 :当检测到图像变化时,自动保存当前画面到桌面的 "ikun ESP32 Camera" 文件夹

  • 文件夹自动创建 :每次运行时会自动创建保存文件夹,确保文件能够正确保存

智能预警的技术核心:

  • 图像比较算法 :使用像素级比较,计算RGB差异,当差异超过阈值时判定为图像变化

  • 时间间隔控制 :使用QDateTime记录时间戳,确保每1秒进行一次比较

  • 文件系统操作 :使用QDir创建文件夹,QStandardPaths获取桌面路径

  • 多线程安全 :使用QMutex保护共享资源

cpp 复制代码
/**
 * @brief 比较两个图像是否发生了变化
 * @param img1 第一个图像
 * @param img2 第二个图像
 * @return 是否发生了变化
 */
bool MainWindow::compareImages(const QImage &img1, const QImage &img2)
{
    // 检查图像是否有效
    if (img1.isNull() || img2.isNull()) {
        return false;
    }
    
    // 检查图像大小是否相同
    if (img1.size() != img2.size()) {
        return true;
    }
    
    // 计算图像差异
    int diffCount = 0;
    int threshold = 10; // 像素差异阈值
    int maxDiff = img1.width() * img1.height() * 0.05; // 最大差异像素数(5%)
    
    for (int y = 0; y < img1.height(); y++) {
        for (int x = 0; x < img1.width(); x++) {
            QRgb pixel1 = img1.pixel(x, y);
            QRgb pixel2 = img2.pixel(x, y);
            
            // 计算RGB差异
            int rDiff = qAbs(qRed(pixel1) - qRed(pixel2));
            int gDiff = qAbs(qGreen(pixel1) - qGreen(pixel2));
            int bDiff = qAbs(qBlue(pixel1) - qBlue(pixel2));
            
            // 如果差异超过阈值,增加差异计数
            if (rDiff > threshold || gDiff > threshold || bDiff > threshold) {
                diffCount++;
                
                // 如果差异超过最大允许值,直接返回true
                if (diffCount > maxDiff) {
                    return true;
                }
            }
        }
    }
    
    return diffCount > maxDiff;
}

/**
 * @brief 保存图像到指定文件夹
 * @param img 要保存的图像
 */
void MainWindow::saveImage(const QImage &img)
{
    // 创建保存目录
    QString desktopPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
    QString saveDir = desktopPath + "/ikun ESP32 Camera";
    QDir dir(saveDir);
    if (!dir.exists()) {
        if (!dir.mkpath(saveDir)) {
            qDebug() << "Failed to create save directory";
            return;
        }
    }
    
    // 生成文件名(基于时间戳)
    QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss_zzz");
    QString filename = saveDir + "/capture_" + timestamp + ".jpg";
    
    // 保存图像
    if (img.save(filename)) {
        qDebug() << "Image saved to:" << filename;
    } else {
        qDebug() << "Failed to save image";
    }
}

/**
 * @brief 解析视频帧数据
 * @param vBuf 包含JPEG图像的数据
 * 
 * 从MJPEG流中提取并解析单个JPEG图像,然后显示在UI界面上
 * 当智能预警功能启用时,检测前后1秒图像的变化并保存变化的画面
 */
void MainWindow::ParseFrame(const QByteArray &vBuf)
{
    // 定义常量
    const QString sBodyKey = "\r\n\r\n";
    const QString sItemKey = "\r\n";
    const QString sLengthKey = "Content-Length";
    
    // 查找body的起始位置
    int nBodyIndex = vBuf.indexOf(sBodyKey);
    if (nBodyIndex <= 0) {
        return;
    }
    
    // 提取头部信息
    QString sHead = vBuf.mid(0, nBodyIndex);
    QStringList vHeadSplit = sHead.split(sItemKey);
    
    // 解析头部信息到映射表
    QMap<QString, QString> vHeadMap;
    for (const QString &vItem : vHeadSplit) {
        QStringList vItemSplit = vItem.split(": ");
        if (vItemSplit.size() == 2) {
            vHeadMap[vItemSplit[0]] = vItemSplit[1];
        }
    }
    
    // 获取图像数据长度
    uint32_t nLength = 0;
    if (vHeadMap.count(sLengthKey) > 0) {
        nLength = vHeadMap[sLengthKey].toUInt();
    }
    if (nLength == 0) {
        return;
    }
    
    // 提取图像数据
    QByteArray sBody = vBuf.mid(nBodyIndex + sBodyKey.size(), nLength);
    
    // 加载图像数据
    QPixmap vPixmap;
    if (!vPixmap.loadFromData(sBody)) {
        qDebug() << "Failed to load image";
        return;
    }
    
    // 调整图像大小以适应显示区域
    QPixmap vImage = vPixmap.scaled(ui->lblVideo->width(), ui->lblVideo->height(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
    
    // 设置标签的大小策略
    ui->lblVideo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
    
    // 显示图像
    ui->lblVideo->setPixmap(vImage);
    
    // 智能预警功能
    if (m_safetyEnabled) {
        // 转换为QImage用于比较
        QImage currentImage = vPixmap.toImage();
        
        // 获取当前时间
        QDateTime currentTime = QDateTime::currentDateTime();
        
        // 计算与上次捕获的时间差
        qint64 elapsed = m_lastCaptureTime.msecsTo(currentTime);
        
        // 每隔1秒进行一次图像比较
        if (elapsed >= 1000) {
            // 检查是否有前一秒的图像可比较
            if (!m_previousImage.isNull()) {
                // 比较前后图像是否发生了变化
                if (compareImages(m_previousImage, currentImage)) {
                    // 图像发生变化,保存当前画面
                    saveImage(currentImage);
                }
            }
            
            // 更新前一秒图像和时间戳
            m_previousImage = currentImage;
            m_lastCaptureTime = currentTime;
        }
    }
}

4.4 ESP32 Camera Studio 的完整代码

cpp 复制代码
/********************************** (C) COPYRIGHT *******************************
* File Name          : CameraWebServer.ino
* Author             : 混分巨兽龙某某
* Version            : V1.0.0
* Date               : 2026/02/21
* Description        : Intelligent Video Monitoring Project Based on ESP32_CAM and Qt Creator
********************************************************************************/
#include "mainwindow.h"
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkRequest>
#include <QDebug>
#include <QPixmap>
#include <QMessageBox>
#include <QComboBox>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QThread>
#include <QStandardPaths>
#include <QDir>
#include <QImage>
#include "ui_mainwindow.h"

/**
 * @file mainwindow.cpp
 * @brief ESP32-CAM QT客户端主窗口实现
 * 
 * 该文件实现了ESP32-CAM网络摄像头的视频流显示、分辨率调整等功能
 */

/**
 * @def WINDOW_TITLE
 * @brief 窗口标题宏定义
 */
#define WINDOW_TITLE "iKun ESP32 Camera Studio"

/**
 * @brief MainWindow构造函数
 * @param parent 父窗口指针
 * 
 * 初始化UI界面,设置窗口标题,添加分辨率选项,请求ESP32-CAM状态
 */
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
    , m_ledState(40)
    , m_safetyEnabled(false)
    , m_previousImage()
    , m_lastCaptureTime(QDateTime::currentDateTime())
{
    // 初始化UI界面
    ui->setupUi(this);
    
    // 设置窗口标题
    setWindowTitle(WINDOW_TITLE);
    
    // 分辨率选项列表
    QStringList vPixelList = {
        "96x96",
        "QQVGA(160x120)",
        "QCIF(176x144)",
        "HQVGA(240x176)",
        "240x240",
        "QVGA(320x240)",
        "CIF(400x296)",
        "HVGA(480x320)",
        "VGA(640x480)",
        "SVGA(800x600)",
        "XGA(1024x768)",
        "HD(1280x720)",
        "SXGA(1280x1024)",
        "UXGA(1600x1200)",
    };
    
    // 添加分辨率选项到下拉框
    ui->cbxPixel->addItems(vPixelList);
    
    // 请求ESP32-CAM当前状态
    RequestStatus();
}

/**
 * @brief MainWindow析构函数
 * 
 * 释放UI资源
 */
MainWindow::~MainWindow()
{
    delete ui;
}


/**
 * @brief 开始取流按钮点击事件处理
 * 
 * 连接到ESP32-CAM的视频流,开始获取视频数据
 */
void MainWindow::on_btnStart_clicked()
{
    // 如果已经连接,直接返回
    if (m_pvClient != nullptr) {
        return;
    }
    
    // 获取IP地址
    const QString sIPAddr = ui->txtAddr->text();
    if (sIPAddr.size() == 0) {
        QMessageBox::warning(this, "提示", "请输入IP地址");
        return;
    }
    
    // 构建视频流URL
    const QString sURL = "http://" + sIPAddr + ":81/stream";
    
    // 创建网络访问管理器
    QNetworkAccessManager *pvManager = new QNetworkAccessManager();
    
    // 创建网络请求
    QNetworkRequest vRequest;
    vRequest.setUrl(sURL);
    vRequest.setRawHeader("Connection", "Keep-Alive");
    vRequest.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
    
    // 发送GET请求获取视频流
    m_pvClient = pvManager->get(vRequest);
    
    // 连接readyRead信号,处理接收到的数据
    connect(m_pvClient, &QNetworkReply::readyRead, this, &MainWindow::readyRead);
}


/**
 * @brief 结束取流按钮点击事件处理
 * 
 * 断开与ESP32-CAM的连接,停止获取视频数据
 */
void MainWindow::on_btnStop_clicked()
{
    if (m_pvClient != nullptr) {
        // 断开信号连接
        disconnect(m_pvClient);
        
        // 中止请求
        m_pvClient->abort();
        
        // 延迟删除
        m_pvClient->deleteLater();
        
        // 重置指针
        m_pvClient = nullptr;
        
        // 清空缓冲区
        m_buffer.clear();
    }
}

/**
 * @brief 比较两个图像是否发生了变化
 * @param img1 第一个图像
 * @param img2 第二个图像
 * @return 是否发生了变化
 */
bool MainWindow::compareImages(const QImage &img1, const QImage &img2)
{
    // 检查图像是否有效
    if (img1.isNull() || img2.isNull()) {
        return false;
    }
    
    // 检查图像大小是否相同
    if (img1.size() != img2.size()) {
        return true;
    }
    
    // 计算图像差异
    int diffCount = 0;
    int threshold = 10; // 像素差异阈值
    int maxDiff = img1.width() * img1.height() * 0.05; // 最大差异像素数(5%)
    
    for (int y = 0; y < img1.height(); y++) {
        for (int x = 0; x < img1.width(); x++) {
            QRgb pixel1 = img1.pixel(x, y);
            QRgb pixel2 = img2.pixel(x, y);
            
            // 计算RGB差异
            int rDiff = qAbs(qRed(pixel1) - qRed(pixel2));
            int gDiff = qAbs(qGreen(pixel1) - qGreen(pixel2));
            int bDiff = qAbs(qBlue(pixel1) - qBlue(pixel2));
            
            // 如果差异超过阈值,增加差异计数
            if (rDiff > threshold || gDiff > threshold || bDiff > threshold) {
                diffCount++;
                
                // 如果差异超过最大允许值,直接返回true
                if (diffCount > maxDiff) {
                    return true;
                }
            }
        }
    }
    
    return diffCount > maxDiff;
}

/**
 * @brief 保存图像到指定文件夹
 * @param img 要保存的图像
 */
void MainWindow::saveImage(const QImage &img)
{
    // 创建保存目录
    QString desktopPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
    QString saveDir = desktopPath + "/ikun ESP32 Camera";
    QDir dir(saveDir);
    if (!dir.exists()) {
        if (!dir.mkpath(saveDir)) {
            qDebug() << "Failed to create save directory";
            return;
        }
    }
    
    // 生成文件名(基于时间戳)
    QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss_zzz");
    QString filename = saveDir + "/capture_" + timestamp + ".jpg";
    
    // 保存图像
    if (img.save(filename)) {
        qDebug() << "Image saved to:" << filename;
    } else {
        qDebug() << "Failed to save image";
    }
}

/**
 * @brief 解析视频帧数据
 * @param vBuf 包含JPEG图像的数据
 * 
 * 从MJPEG流中提取并解析单个JPEG图像,然后显示在UI界面上
 * 当智能预警功能启用时,检测前后1秒图像的变化并保存变化的画面
 */
void MainWindow::ParseFrame(const QByteArray &vBuf)
{
    // 定义常量
    const QString sBodyKey = "\r\n\r\n";
    const QString sItemKey = "\r\n";
    const QString sLengthKey = "Content-Length";
    
    // 查找body的起始位置
    int nBodyIndex = vBuf.indexOf(sBodyKey);
    if (nBodyIndex <= 0) {
        return;
    }
    
    // 提取头部信息
    QString sHead = vBuf.mid(0, nBodyIndex);
    QStringList vHeadSplit = sHead.split(sItemKey);
    
    // 解析头部信息到映射表
    QMap<QString, QString> vHeadMap;
    for (const QString &vItem : vHeadSplit) {
        QStringList vItemSplit = vItem.split(": ");
        if (vItemSplit.size() == 2) {
            vHeadMap[vItemSplit[0]] = vItemSplit[1];
        }
    }
    
    // 获取图像数据长度
    uint32_t nLength = 0;
    if (vHeadMap.count(sLengthKey) > 0) {
        nLength = vHeadMap[sLengthKey].toUInt();
    }
    if (nLength == 0) {
        return;
    }
    
    // 提取图像数据
    QByteArray sBody = vBuf.mid(nBodyIndex + sBodyKey.size(), nLength);
    
    // 加载图像数据
    QPixmap vPixmap;
    if (!vPixmap.loadFromData(sBody)) {
        qDebug() << "Failed to load image";
        return;
    }
    
    // 调整图像大小以适应显示区域
    QPixmap vImage = vPixmap.scaled(ui->lblVideo->width(), ui->lblVideo->height(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
    
    // 设置标签的大小策略
    ui->lblVideo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
    
    // 显示图像
    ui->lblVideo->setPixmap(vImage);
    
    // 智能预警功能
    if (m_safetyEnabled) {
        // 转换为QImage用于比较
        QImage currentImage = vPixmap.toImage();
        
        // 获取当前时间
        QDateTime currentTime = QDateTime::currentDateTime();
        
        // 计算与上次捕获的时间差
        qint64 elapsed = m_lastCaptureTime.msecsTo(currentTime);
        
        // 每隔1秒进行一次图像比较
        if (elapsed >= 1000) {
            // 检查是否有前一秒的图像可比较
            if (!m_previousImage.isNull()) {
                // 比较前后图像是否发生了变化
                if (compareImages(m_previousImage, currentImage)) {
                    // 图像发生变化,保存当前画面
                    saveImage(currentImage);
                }
            }
            
            // 更新前一秒图像和时间戳
            m_previousImage = currentImage;
            m_lastCaptureTime = currentTime;
        }
    }
}

/**
 * @brief 请求ESP32-CAM状态
 * 
 * 获取ESP32-CAM的当前状态,包括分辨率等信息
 */
void MainWindow::RequestStatus()
{
    // 清空缓冲区
    m_buffer.clear();
    
    // 获取IP地址
    const QString sIPAddr = ui->txtAddr->text();
    if (sIPAddr.size() == 0) {
        return;
    }
    
    // 构建状态请求URL
    const QString sUrl = "http://" + sIPAddr + "/status";
    qDebug() << sUrl;
    
    // 创建网络访问管理器
    QNetworkAccessManager manager;
    
    // 创建网络请求
    QNetworkRequest vRequest;
    QNetworkReply *reply;
    vRequest.setUrl(QUrl(sUrl));
    vRequest.setRawHeader("Connection", "Keep-Alive");
    vRequest.setRawHeader("Cache-Control", "no-cache");
    vRequest.setRawHeader("Pragma", "no-cache");
    vRequest.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
    
    // 发送GET请求
    reply = manager.get(vRequest);
    
    // 创建事件循环,等待请求完成
    QEventLoop loop;
    QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
    loop.exec();
    
    // 处理响应
    if (reply->error() == QNetworkReply::NoError) {
        // 读取响应数据
        QString jsonString = reply->readAll();
        
        // 解析JSON数据
        QJsonDocument jsonDocument = QJsonDocument::fromJson(jsonString.toUtf8());
        if (!jsonDocument.isNull()) {
            if (jsonDocument.isObject()) {
                QJsonObject jsonObject = jsonDocument.object();
                // 获取分辨率索引
                int framesize = jsonObject["framesize"].toInt();
                // 设置下拉框当前索引
                ui->cbxPixel->setCurrentIndex(framesize);
            }
        }
    } else {
        // 输出错误信息
        qDebug() << "Error: " << reply->errorString();
    }
    
    // 释放回复对象
    reply->deleteLater();
}


/**
 * @brief 分辨率下拉框选择变化事件处理
 * @param index 选择的分辨率索引
 * 
 * 当用户选择不同的分辨率时,发送请求到ESP32-CAM更改分辨率
 */
void MainWindow::on_cbxPixel_currentIndexChanged(int index)
{
    // 静态变量,用于跳过初始化时的第一次触发
    static bool isFirstSend = true;
    if (isFirstSend) {
        isFirstSend = false;
        return;
    }
    
    // 获取IP地址
    const QString sIPAddr = ui->txtAddr->text();
    if (sIPAddr.size() == 0) {
        return;
    }
    
    // 构建分辨率更改请求URL
    const QString sUrl = "http://" + sIPAddr + "/control?var=framesize&val=" + QString::number(index);
    qDebug() << sUrl;
    
    // 创建网络访问管理器
    QNetworkAccessManager manager;
    
    // 创建网络请求
    QNetworkRequest vRequest;
    QNetworkReply *reply;
    vRequest.setUrl(QUrl(sUrl));
    vRequest.setRawHeader("Connection", "Keep-Alive");
    vRequest.setRawHeader("Cache-Control", "no-cache");
    vRequest.setRawHeader("Pragma", "no-cache");
    vRequest.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
    
    // 发送GET请求
    reply = manager.get(vRequest);
    
    // 清空缓冲区
    m_mutex.lock();
    m_buffer.clear();
    m_mutex.unlock();
    
    // 创建事件循环,等待请求完成
    QEventLoop loop;
    QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
    loop.exec();
    
    // 处理错误
    if (reply->error() != QNetworkReply::NoError) {
        qDebug() << "Error: " << reply->errorString();
    }
    
    // 释放回复对象
    reply->deleteLater();
}

/**
 * @brief 网络数据就绪事件处理
 * 
 * 当接收到网络数据时触发,处理MJPEG视频流数据,解析并显示视频帧
 */
void MainWindow::readyRead()
{
    // 如果没有网络回复对象,直接返回
    if (m_pvClient == nullptr) {
        return;
    }
    
    // 读取网络数据到缓冲区
    m_mutex.lock();
    m_buffer.append(m_pvClient->readAll());
    m_mutex.unlock();
    
    // MJPEG流标记
    const QString sStartKey = "Content-Type: image/jpeg\r\nContent-Length:";
    const QString sEndKey = "\r\n--123456789000000000000987654321\r\n";
    
    // 循环处理缓冲区中的数据
    while (true) {
        // 查找JPEG图像的开始标记
        int nStartPos = m_buffer.indexOf(sStartKey);
        if (nStartPos < 0) {
            break;
        }
        
        // 查找JPEG图像的结束标记
        int nEndPos = m_buffer.indexOf(sEndKey, nStartPos);
        if (nEndPos < 0) {
            break;
        }
        
        // 调整开始位置
        nStartPos = m_buffer.indexOf("\r\n");
        if (nStartPos < 0) {
            break;
        }
        
        // 计算结束位置
        nEndPos += sEndKey.size();
        
        // 提取JPEG图像数据
        QByteArray vBuf = m_buffer.mid(nStartPos, nEndPos - nStartPos);
        if (vBuf.size() == 0) {
            continue;
        }
        
        // 解析并显示图像
        ParseFrame(vBuf);
        
        // 从缓冲区中移除已处理的数据
        m_mutex.lock();
        m_buffer = m_buffer.mid(nEndPos, m_buffer.size() - nEndPos);
        m_mutex.unlock();
    }
}

/**
 * @brief LED控制按钮点击事件处理
 * 
 * 控制ESP32-CAM上的LED灯开关,实现参数循环切换:40 -> 41 -> 40 -> ...
 * 仅发送一次请求,不进行重试
 */
void MainWindow::on_btnLED_clicked()
{
    // 获取IP地址
    const QString sIPAddr = ui->txtAddr->text();
    if (sIPAddr.size() == 0) {
        QMessageBox::warning(this, "提示", "请输入IP地址");
        return;
    }
    
    // 构建LED控制URL,使用当前m_ledState值
    const QString sUrl = "http://" + sIPAddr + "/control?var=led&val=" + QString::number(m_ledState);
    qDebug() << "LED control URL:" << sUrl;
    
    // 创建网络访问管理器
    QNetworkAccessManager manager;
    
    // 创建网络请求
    QNetworkRequest vRequest;
    vRequest.setUrl(QUrl(sUrl));
    vRequest.setRawHeader("Connection", "Keep-Alive");
    vRequest.setRawHeader("Cache-Control", "no-cache");
    vRequest.setRawHeader("Pragma", "no-cache");
    vRequest.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
    vRequest.setRawHeader("Accept", "*/*");
    
    // 发送GET请求
    QNetworkReply *reply = manager.get(vRequest);
    
    // 创建事件循环,等待请求完成
    QEventLoop loop;
    QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
    loop.exec();
    
    // 处理响应
    if (reply->error() != QNetworkReply::NoError) {

        // 创建网络访问管理器(设置2级重传机制,避免网络异常开灯失败)
        QNetworkAccessManager manager1;

        // 创建网络请求
        QNetworkRequest vRequest1;
        vRequest1.setUrl(QUrl(sUrl));
        vRequest1.setRawHeader("Connection", "Keep-Alive");
        vRequest1.setRawHeader("Cache-Control", "no-cache");
        vRequest1.setRawHeader("Pragma", "no-cache");
        vRequest1.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
        vRequest1.setRawHeader("Accept", "*/*");

        // 发送GET请求
        QNetworkReply *reply = manager1.get(vRequest1);

        // 创建事件循环,等待请求完成
        QEventLoop loop;
        QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
        loop.exec();

        // 处理响应
        if (reply->error() != QNetworkReply::NoError) {
            qDebug() << "LED control error: " << reply->errorString();
            QMessageBox::warning(this, "错误", "控制LED失败: " + reply->errorString());
            reply->deleteLater();
        }else{
            qDebug() << "LED control successful, val:" << m_ledState;
            // 切换LED状态:40 -> 41 -> 40 -> ...
            m_ledState = (m_ledState == 40) ? 41 : 40;
            reply->deleteLater();
        }
    } else {
        qDebug() << "LED control successful, val:" << m_ledState;
        
        // 切换LED状态:40 -> 41 -> 40 -> ...
        m_ledState = (m_ledState == 40) ? 41 : 40;
        reply->deleteLater();
    }
}

/**
 * @brief 智能预警复选框状态变化事件处理
 * @param arg1 复选框的状态(Qt::Checked或Qt::Unchecked)
 * 
 * 启用或禁用智能预警功能
 */
void MainWindow::on_boxSafety_stateChanged(int arg1)
{
    // 检查复选框是否被选中
    if (arg1 == Qt::Checked) {
        // 启用智能预警功能
        m_safetyEnabled = true;
        qDebug() << "智能预警功能已启用";
        
        // 重置时间戳和前一帧图像
        m_lastCaptureTime = QDateTime::currentDateTime();
        m_previousImage = QImage();
        
        // 显示提示信息
        QMessageBox::information(this, "提示", "智能预警功能已启用\n当检测到图像变化时,会自动保存画面到桌面的'ikun ESP32 Camera'文件夹");
    } else {
        // 禁用智能预警功能
        m_safetyEnabled = false;
        qDebug() << "智能预警功能已禁用";
    }
}

五、ESP32_CAM 智能视频监控演示

六、代码开源

代码地址: 基于ESP32-CAM与QtCreator的智能视频监控项目代码资源-CSDN下载

如果积分不够的朋友,点波关注,评论区留下邮箱,作者无偿提供源码和后续问题解答。求求啦关注一波吧 !!!

相关推荐
正点原子15 小时前
《ESP32-S3使用指南—IDF版 V1.6》第十章 ESP32-P4存储器类型
单片机·物联网·嵌入式
A.A呐15 小时前
【QT第三章】常用控件2
开发语言·qt
笨笨马甲15 小时前
Qt 实现三维坐标系的方法
开发语言·qt
谁动了我的代码?16 小时前
VNC中使用QT的GDB调试,触发断点时与界面窗口交互导致整个VNC冻结
开发语言·qt·svn
肖恭伟17 小时前
QtCreator Linux ubuntu24.04问题集合
linux·windows·qt
vegetablesssss18 小时前
QT国际化翻译
qt
困死,根本不会18 小时前
Qt Designer 基础操作学习笔记
开发语言·笔记·qt·学习·microsoft
喜欢喝果茶.19 小时前
Qt MQTT部署
开发语言·qt
浅碎时光80719 小时前
Qt 窗口 (菜单 工具栏 状态栏 浮动窗口 对话框)
qt
GIS阵地19 小时前
一场由Qt5 painter的drawRect引起的血雨腥风
开发语言·qt·gis·qgis