ESP32 S3-CAM图传模块+MQTT+华为云端实现云端更新图片数据(Arduino IDE开发环境, 附源码)

ESP32 S3-CAM图传模块+MQTT+华为云端实现云端更新图片数据(Arduino IDE开发环境)

这次探索的意图是想给我的家庭监控系统做一个图传功能,当家里发生火灾或者可燃气体超标的时候,可以通过这个功能获取家里的实时画面,画面不需要流畅的视频,能间隔一定时间获取单帧画面即可,所以才有了以下文章。整个系统的其他功能另有一块STM32F103C8T6主芯片开发板板负责所有图传以外的功能,这块ESP32-S3-CAM模块基本算是完全独立于主芯片。

一、物料准备

硬件:

  1. ESP32 S3-CAM(乐鑫)
  2. type-c数据线

软件:

  1. 华为云端MQTT服务器
  2. Arduino IDE

二、调试过程

总体思路,这次购买的模块,只能产生RGB565格式 ,这是一种红5绿6蓝5的双字节原始格式,而app端需要的是base64格式的jpeg图片 ,这种格式在web端可以直接显示。而mqtt传输中最好使用base64格式,所以最终方案,是将RGB565压缩为jepg格式,再将jpeg格式转化为base64格式,再传输至云端

转化途中自然需要大量缓存空间,我买的这个图传模块刚好有内部520KB+外部8MB的PSRAM,完全可以满足要求,整个过程可以简化为以下步骤:

  1. 配置Arduino开发环境
  2. 配置芯片引脚数据
  3. 相机初始化
  4. 连接wifi
  5. 连接MQTT云服务器(使用PubSubClient库)
  6. 循环获取画面并向服务器传送
    1. 摄像头拍照获取RGB565(双字节原始数据)
    2. 转化为JPEG(内置frame2jpg)
    3. 转化为base64(使用base64库)
    4. 组装MATT的JSON数据(使用ArduinoJson库)
    5. 传输至云端

以下提几个重点,完整代码贴在文章最后:

  1. 配置Arduino开发环境

    根据下图配置,配置可以根据taobao商家给的开发说明去配,注意这一步千万不能省略而且要配仔细,我本人就是因为PSRAM配置项没有开启,导致代码中的PSRAM初始化一直没有成功,卡了我两小时

  1. 配置芯片引脚数据

    这也可以从taobao给的示例代码中获取,省去手写的繁琐

    c 复制代码
    // 选择相机型号
    #define CAMERA_MODEL_ESP32S3_EYE
    
    #if defined(CAMERA_MODEL_WROVER_KIT)
    #define PWDN_GPIO_NUM    -1
    #define RESET_GPIO_NUM   -1
    #define XCLK_GPIO_NUM    21
    #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      19
    #define Y4_GPIO_NUM      18
    #define Y3_GPIO_NUM       5
    #define Y2_GPIO_NUM       4
    #define VSYNC_GPIO_NUM   25
    #define HREF_GPIO_NUM    23
    #define PCLK_GPIO_NUM    22
    
    #elif defined(CAMERA_MODEL_ESP_EYE)
    #define PWDN_GPIO_NUM    -1
    #define RESET_GPIO_NUM   -1
    #define XCLK_GPIO_NUM    4
    #define SIOD_GPIO_NUM    18
    #define SIOC_GPIO_NUM    23
    
    #define Y9_GPIO_NUM      36
    #define Y8_GPIO_NUM      37
    #define Y7_GPIO_NUM      38
    #define Y6_GPIO_NUM      39
    #define Y5_GPIO_NUM      35
    #define Y4_GPIO_NUM      14
    #define Y3_GPIO_NUM      13
    #define Y2_GPIO_NUM      34
    #define VSYNC_GPIO_NUM   5
    #define HREF_GPIO_NUM    27
    #define PCLK_GPIO_NUM    25
    
    #define LED_GPIO_NUM     22
    
    #elif defined(CAMERA_MODEL_M5STACK_PSRAM)
    #define PWDN_GPIO_NUM     -1
    #define RESET_GPIO_NUM    15
    #define XCLK_GPIO_NUM     27
    #define SIOD_GPIO_NUM     25
    #define SIOC_GPIO_NUM     23
    
    #define Y9_GPIO_NUM       19
    #define Y8_GPIO_NUM       36
    #define Y7_GPIO_NUM       18
    #define Y6_GPIO_NUM       39
    #define Y5_GPIO_NUM        5
    #define Y4_GPIO_NUM       34
    #define Y3_GPIO_NUM       35
    #define Y2_GPIO_NUM       32
    #define VSYNC_GPIO_NUM    22
    #define HREF_GPIO_NUM     26
    #define PCLK_GPIO_NUM     21
    
    #elif defined(CAMERA_MODEL_M5STACK_V2_PSRAM)
    #define PWDN_GPIO_NUM     -1
    #define RESET_GPIO_NUM    15
    #define XCLK_GPIO_NUM     27
    #define SIOD_GPIO_NUM     22
    #define SIOC_GPIO_NUM     23
    
    #define Y9_GPIO_NUM       19
    #define Y8_GPIO_NUM       36
    #define Y7_GPIO_NUM       18
    #define Y6_GPIO_NUM       39
    #define Y5_GPIO_NUM        5
    #define Y4_GPIO_NUM       34
    #define Y3_GPIO_NUM       35
    #define Y2_GPIO_NUM       32
    #define VSYNC_GPIO_NUM    25
    #define HREF_GPIO_NUM     26
    #define PCLK_GPIO_NUM     21
    
    #elif defined(CAMERA_MODEL_M5STACK_WIDE)
    #define PWDN_GPIO_NUM     -1
    #define RESET_GPIO_NUM    15
    #define XCLK_GPIO_NUM     27
    #define SIOD_GPIO_NUM     22
    #define SIOC_GPIO_NUM     23
    
    #define Y9_GPIO_NUM       19
    #define Y8_GPIO_NUM       36
    #define Y7_GPIO_NUM       18
    #define Y6_GPIO_NUM       39
    #define Y5_GPIO_NUM        5
    #define Y4_GPIO_NUM       34
    #define Y3_GPIO_NUM       35
    #define Y2_GPIO_NUM       32
    #define VSYNC_GPIO_NUM    25
    #define HREF_GPIO_NUM     26
    #define PCLK_GPIO_NUM     21
    
    #define LED_GPIO_NUM       2
    
    #elif defined(CAMERA_MODEL_M5STACK_ESP32CAM)
    #define PWDN_GPIO_NUM     -1
    #define RESET_GPIO_NUM    15
    #define XCLK_GPIO_NUM     27
    #define SIOD_GPIO_NUM     25
    #define SIOC_GPIO_NUM     23
    
    #define Y9_GPIO_NUM       19
    #define Y8_GPIO_NUM       36
    #define Y7_GPIO_NUM       18
    #define Y6_GPIO_NUM       39
    #define Y5_GPIO_NUM        5
    #define Y4_GPIO_NUM       34
    #define Y3_GPIO_NUM       35
    #define Y2_GPIO_NUM       17
    #define VSYNC_GPIO_NUM    22
    #define HREF_GPIO_NUM     26
    #define PCLK_GPIO_NUM     21
    
    #elif defined(CAMERA_MODEL_M5STACK_UNITCAM)
    #define PWDN_GPIO_NUM     -1
    #define RESET_GPIO_NUM    15
    #define XCLK_GPIO_NUM     27
    #define SIOD_GPIO_NUM     25
    #define SIOC_GPIO_NUM     23
    
    #define Y9_GPIO_NUM       19
    #define Y8_GPIO_NUM       36
    #define Y7_GPIO_NUM       18
    #define Y6_GPIO_NUM       39
    #define Y5_GPIO_NUM        5
    #define Y4_GPIO_NUM       34
    #define Y3_GPIO_NUM       35
    #define Y2_GPIO_NUM       32
    #define VSYNC_GPIO_NUM    22
    #define HREF_GPIO_NUM     26
    #define PCLK_GPIO_NUM     21
    
    #elif defined(CAMERA_MODEL_AI_THINKER)
    #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
    
    #elif defined(CAMERA_MODEL_TTGO_T_JOURNAL)
    #define PWDN_GPIO_NUM      0
    #define RESET_GPIO_NUM    15
    #define XCLK_GPIO_NUM     27
    #define SIOD_GPIO_NUM     25
    #define SIOC_GPIO_NUM     23
    
    #define Y9_GPIO_NUM       19
    #define Y8_GPIO_NUM       36
    #define Y7_GPIO_NUM       18
    #define Y6_GPIO_NUM       39
    #define Y5_GPIO_NUM        5
    #define Y4_GPIO_NUM       34
    #define Y3_GPIO_NUM       35
    #define Y2_GPIO_NUM       17
    #define VSYNC_GPIO_NUM    22
    #define HREF_GPIO_NUM     26
    #define PCLK_GPIO_NUM     21
    
    #elif defined(CAMERA_MODEL_XIAO_ESP32S3)
    #define PWDN_GPIO_NUM     -1
    #define RESET_GPIO_NUM    -1
    #define XCLK_GPIO_NUM     10
    #define SIOD_GPIO_NUM     40
    #define SIOC_GPIO_NUM     39
    
    #define Y9_GPIO_NUM       48
    #define Y8_GPIO_NUM       11
    #define Y7_GPIO_NUM       12
    #define Y6_GPIO_NUM       14
    #define Y5_GPIO_NUM       16
    #define Y4_GPIO_NUM       18
    #define Y3_GPIO_NUM       17
    #define Y2_GPIO_NUM       15
    #define VSYNC_GPIO_NUM    38
    #define HREF_GPIO_NUM     47
    #define PCLK_GPIO_NUM     13
    
    #elif defined(CAMERA_MODEL_ESP32_CAM_BOARD)
    // The 18 pin header on the board has Y5 and Y3 swapped
    #define USE_BOARD_HEADER 0 
    #define PWDN_GPIO_NUM    32
    #define RESET_GPIO_NUM   33
    #define XCLK_GPIO_NUM     4
    #define SIOD_GPIO_NUM    18
    #define SIOC_GPIO_NUM    23
    
    #define Y9_GPIO_NUM      36
    #define Y8_GPIO_NUM      19
    #define Y7_GPIO_NUM      21
    #define Y6_GPIO_NUM      39
    #if USE_BOARD_HEADER
    #define Y5_GPIO_NUM      13
    #else
    #define Y5_GPIO_NUM      35
    #endif
    #define Y4_GPIO_NUM      14
    #if USE_BOARD_HEADER
    #define Y3_GPIO_NUM      35
    #else
    #define Y3_GPIO_NUM      13
    #endif
    #define Y2_GPIO_NUM      34
    #define VSYNC_GPIO_NUM    5
    #define HREF_GPIO_NUM    27
    #define PCLK_GPIO_NUM    25
    
    #elif defined(CAMERA_MODEL_ESP32S3_CAM_LCD)
    #define PWDN_GPIO_NUM     -1
    #define RESET_GPIO_NUM    -1
    #define XCLK_GPIO_NUM     40
    #define SIOD_GPIO_NUM     17
    #define SIOC_GPIO_NUM     18
    
    #define Y9_GPIO_NUM       39
    #define Y8_GPIO_NUM       41
    #define Y7_GPIO_NUM       42
    #define Y6_GPIO_NUM       12
    #define Y5_GPIO_NUM       3
    #define Y4_GPIO_NUM       14
    #define Y3_GPIO_NUM       47
    #define Y2_GPIO_NUM       13
    #define VSYNC_GPIO_NUM    21
    #define HREF_GPIO_NUM     38
    #define PCLK_GPIO_NUM     11
    
    #elif defined(CAMERA_MODEL_ESP32S2_CAM_BOARD)
    // The 18 pin header on the board has Y5 and Y3 swapped
    #define USE_BOARD_HEADER 0
    #define PWDN_GPIO_NUM     1
    #define RESET_GPIO_NUM    2
    #define XCLK_GPIO_NUM     42
    #define SIOD_GPIO_NUM     41
    #define SIOC_GPIO_NUM     18
    
    #define Y9_GPIO_NUM       16
    #define Y8_GPIO_NUM       39
    #define Y7_GPIO_NUM       40
    #define Y6_GPIO_NUM       15
    #if USE_BOARD_HEADER
    #define Y5_GPIO_NUM       12
    #else
    #define Y5_GPIO_NUM       13
    #endif
    #define Y4_GPIO_NUM       5
    #if USE_BOARD_HEADER
    #define Y3_GPIO_NUM       13
    #else
    #define Y3_GPIO_NUM       12
    #endif
    #define Y2_GPIO_NUM       14
    #define VSYNC_GPIO_NUM    38
    #define HREF_GPIO_NUM     4
    #define PCLK_GPIO_NUM     3
    
    #elif defined(CAMERA_MODEL_ESP32S3_EYE)
    #define PWDN_GPIO_NUM -1
    #define RESET_GPIO_NUM -1
    #define XCLK_GPIO_NUM 15
    #define SIOD_GPIO_NUM 4
    #define SIOC_GPIO_NUM 5
    
    #define Y2_GPIO_NUM 11
    #define Y3_GPIO_NUM 9
    #define Y4_GPIO_NUM 8
    #define Y5_GPIO_NUM 10
    #define Y6_GPIO_NUM 12
    #define Y7_GPIO_NUM 18
    #define Y8_GPIO_NUM 17
    #define Y9_GPIO_NUM 16
    
    #define VSYNC_GPIO_NUM 6
    #define HREF_GPIO_NUM 7
    #define PCLK_GPIO_NUM 13
    
    #elif defined(CAMERA_MODEL_DFRobot_FireBeetle2_ESP32S3) || defined(CAMERA_MODEL_DFRobot_Romeo_ESP32S3)
    #define PWDN_GPIO_NUM     -1
    #define RESET_GPIO_NUM    -1
    #define XCLK_GPIO_NUM     45
    #define SIOD_GPIO_NUM     1
    #define SIOC_GPIO_NUM     2
    
    #define Y9_GPIO_NUM       48
    #define Y8_GPIO_NUM       46
    #define Y7_GPIO_NUM       8
    #define Y6_GPIO_NUM       7
    #define Y5_GPIO_NUM       4
    #define Y4_GPIO_NUM       41
    #define Y3_GPIO_NUM       40
    #define Y2_GPIO_NUM       39
    #define VSYNC_GPIO_NUM    6
    #define HREF_GPIO_NUM     42
    #define PCLK_GPIO_NUM     5
    
    #else
    #error "Camera model not selected"
    #endif

完整代码:

c 复制代码
#include "WiFi.h"
#include "esp_camera.h"
#include "esp_timer.h"
#include "img_converters.h"
#include "Arduino.h"
#include "soc/soc.h"           // Disable brownour problems
#include "soc/rtc_cntl_reg.h"  // Disable brownour problems
#include "driver/rtc_io.h"
#include "camera_pins.h"
#include <PubSubClient.h>
#include "base64.hpp"
#include <libb64/cencode.h>
#include <ArduinoJson.h>

// Wifi Info
const char* ssid = "wifi-username";
const char* password = "wifi-password";

// MQTT Info
const char* client_id = "monitor1_0_0_2025080613";
const char* mqtt_server = "99dc6497df.st1.iotda-device.cn-north-4.myhuaweicloud.com";
const int mqtt_port = 1883;
const char* mqtt_user = "mqtt_user";
const char* mqtt_password = "mqtt_password";

// Topic Info
const char* mqtt_TopicName = "$oc/devices/{device_id}/sys/properties/report";

WiFiClient mqttClient;
PubSubClient client(mqttClient);

void setup_camera() {
  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 = 15000000;
  /* @param frame_size   One of
   *                     - FRAMESIZE_96X96,    // 96x96
   *                     - FRAMESIZE_QQVGA,    // 160x120
   *                     - FRAMESIZE_QCIF,     // 176x144
   *                     - FRAMESIZE_HQVGA,    // 240x176
   *                     - FRAMESIZE_240X240,  // 240x240
   *                     - FRAMESIZE_QVGA,     // 320x240
   *                     - FRAMESIZE_CIF,      // 400x296
   *                     - FRAMESIZE_HVGA,     // 480x320
   *                     - FRAMESIZE_VGA,      // 640x480
   *                     - FRAMESIZE_SVGA,     // 800x600
   *                     - FRAMESIZE_XGA,      // 1024x768
   *                     - FRAMESIZE_HD,       // 1280x720
   *                     - FRAMESIZE_SXGA,     // 1280x1024
   *                     - FRAMESIZE_UXGA,     // 1600x1200
   *                     - FRAMESIZE_FHD,      // 1920x1080
   *                     - FRAMESIZE_P_HD,     //  720x1280
   *                     - FRAMESIZE_P_3MP,    //  864x1536
   *                     - FRAMESIZE_QXGA,     // 2048x1536
   *                     - FRAMESIZE_QHD,      // 2560x1440
   *                     - FRAMESIZE_WQXGA,    // 2560x1600
   *                     - FRAMESIZE_P_FHD,    // 1080x1920
   *                     - FRAMESIZE_QSXGA,    // 2560x1920
   */
  config.frame_size = FRAMESIZE_SVGA;
  config.pixel_format = PIXFORMAT_RGB565;  // for streaming
  config.fb_location = CAMERA_FB_IN_PSRAM;
  config.fb_count = 1;

  // 相机初始化
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    return;
  } else {
    Serial.printf("Camera init succeed");
  }
}

//拍照发送到mqtt
void getimg() {  
  // 1. 获取RGB565图像
  camera_fb_t* rgb565_fb = esp_camera_fb_get();
  if (!rgb565_fb) {
    Serial.println("Get RGB565 img failed!\n");
    return;
  }

  // 2. 转换为JPEG
  uint8_t* jpg_buf = NULL;  // 存储转换后的 JPEG 数据
  size_t jpg_len = 0;       // 存储 JPEG 数据长度
  bool success = frame2jpg(rgb565_fb, 50, &jpg_buf, &jpg_len);   // 第二个参数是 JPEG 质量(0-100,数值越大质量越好)
  esp_camera_fb_return(rgb565_fb);  // 释放原始 RGB565 帧缓冲区(转换后不再需要)
  if (!success) {
    Serial.println("RGB565 to JPEG failed\n");
    free(jpg_buf);
    return;
  }
  Serial.printf("frame to jpeg succeed,JPEG length:%d byte\r\n", jpg_len);

  // 3. 计算 Base64 输出缓冲区大小
  size_t base64_buf_size = ((jpg_len + 2) / 3) * 4 + 1;  // +1 确保有空间放 null 终止符
  char* base64_buf = (char*)malloc(base64_buf_size);
  if (!base64_buf) {
    Serial.println("Base64 malloc failed!\n");
    free(jpg_buf);  // 释放 JPEG 缓冲区
    return;
  }

  // 4. 使用库函数 encode_base64() 进行编码,函数参数:(输入数据, 输入长度, 输出缓冲区) ,库会自动添加 null 终止符,因此 base64_buf 可直接作为字符串使用
  size_t base64_len = encode_base64(jpg_buf, jpg_len, (unsigned char*)base64_buf);
  Serial.printf("Base64 encode succeed,length: %dbyte\r\n", base64_len);
  
  // 5. 构造MQTT JSON结构
  JsonDocument doc;  
  JsonArray services = doc["services"].to<JsonArray>();
  JsonObject service = services.add<JsonObject>();
  service["service_id"] = "Monitor";  // 设置service_id
  JsonObject properties = service["properties"].to<JsonObject>();
  properties["img"] = base64_buf;    // 传入Base64图像数据
  String jsonString;
  serializeJson(doc, jsonString);

  // 6.直接发送出去
  if (client.publish(mqtt_TopicName, jsonString.c_str())) {
    Serial.printf("send succeed,JPEG lengh: %dbyte,Base64 length: %dbyte\r\n",
                  jpg_len, base64_len);
  } else {
    Serial.println("MQTT send failed\r\n");
  }

  // 7. 释放 JPEG 缓冲区,释放 Base64 缓冲区(必须!)
  free(jpg_buf);
  free(base64_buf);
}

void setup_wifi() {
  delay(10);
  // 连接WIFI
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);

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

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.print("IP address : ");
  Serial.println(WiFi.localIP());
}

//MQTT重连
void reconnect() {
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    if (client.connect(client_id, mqtt_user, mqtt_password)) {
      Serial.println("connected");
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      delay(5000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true);
  Serial.println();
  // 启用PSRAM(必须在相机初始化前执行)
  if (psramFound()) {
    Serial.printf("PSRAM :%d bytes\r\n", ESP.getPsramSize());  // 应输出8388608字节(8MB)
  } else {
    Serial.println("PSRAM init failed!\r\n");
  }
  setup_camera();                            //设置相机
  setup_wifi();                              //连接WIFI
  client.setServer(mqtt_server, mqtt_port);  //连接MQTT Broker
  client.setKeepAlive(60);                   // 强制设置为60秒,
  client.setBufferSize(65535);
  if (client.connect(client_id, mqtt_user, mqtt_password)) {
    Serial.println("mqtt connected");
  }
}

void loop() {
  getimg();

  if (!client.connected()) {
    reconnect();
  }

  delay(3000);
}