基于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下载

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

相关推荐
Non importa2 小时前
二分法:算法新手第三道坎
c语言·c++·笔记·qt·学习·算法·leetcode
tokepson8 小时前
关于 MicroPython + ESP32-S3 的使用流程
嵌入式·esp32·micropython·技术
爱看书的小沐15 小时前
【小沐学CAD】基于OCCT读取和显示STEP模型文件(QT、MFC、glfw)
qt·mfc·opengl·stp·step·opencascade·occt
Quz18 小时前
QML与JavaScript 交互的四种方式
javascript·qt·交互
xyty332018 小时前
QImageReader 的全局静态锁原理
c++·windows·qt
Zevalin爱灰灰1 天前
方法论——如何设计控制策略架构
算法·架构·嵌入式
Hello.Reader1 天前
Tauri vs Qt跨平台桌面(与移动)应用选型的“底层逻辑”与落地指南
开发语言·qt·tauri
忘忧记1 天前
python QT sqlsite版本 图书管理系统
开发语言·python·qt
fly的fly1 天前
浅析 QT远程部署及debug方案
qt·物联网·arm