跨界开发:前端也能玩硬件 - ESP32Cam摄像头示例代码分析(2)

531 阅读9分钟

概述

往期内容:

  1. 跨界开发:前端也能玩硬件 - ESP32Cam摄像头示例(1)作为一名前端开发者,我将分享如何从0开始,将前端技能与ES - 掘金 (juejin.cn)

第一篇文章中,我们成功展示了如何在ESP32Cam系列开发板上运行一个基础的Demo项目,并成功启动了WebServer和摄像头的实时捕获功能。通过这一系列操作,相信你已经掌握了如何将代码烧录到开发板,并实现简单的项目搭建。

接下来,在本篇内容中将分析我们在基础Demo中使用的代码,帮助你更好地理解到重要代码的含义。我们还将修改WebServer中展示的HTML文件,定制网页的显示内容,使你能够为项目增加更多的个性化功能。

第二篇主要内容:

  • 详细分析示例代码的关键部分,包括WebServer启动逻辑和摄像头配置信息。
  • 教你如何修改WebServer中的HTML代码,定制网页内容和样式。

示例代码分析

开发板型号定义

在代码的开始部分,可以看到一系列关于摄像头型号的定义。通过取消注释相应的摄像头型号,我们可以告诉程序使用哪种开发板以及相应的引脚布局。

// ===================
// Select camera model
// ===================
//#define CAMERA_MODEL_WROVER_KIT // Has PSRAM
#define CAMERA_MODEL_ESP_EYE  // Has PSRAM
//#define CAMERA_MODEL_ESP32S3_EYE // Has PSRAM
//#define CAMERA_MODEL_M5STACK_PSRAM // Has PSRAM
//#define CAMERA_MODEL_M5STACK_V2_PSRAM // M5Camera version B Has PSRAM
//#define CAMERA_MODEL_M5STACK_WIDE // Has PSRAM
//#define CAMERA_MODEL_M5STACK_ESP32CAM // No PSRAM
//#define CAMERA_MODEL_M5STACK_UNITCAM // No PSRAM
//#define CAMERA_MODEL_M5STACK_CAMS3_UNIT  // Has PSRAM
//#define CAMERA_MODEL_AI_THINKER // Has PSRAM
//#define CAMERA_MODEL_TTGO_T_JOURNAL // No PSRAM
//#define CAMERA_MODEL_XIAO_ESP32S3 // Has PSRAM
// ** Espressif Internal Boards **
//#define CAMERA_MODEL_ESP32_CAM_BOARD
//#define CAMERA_MODEL_ESP32S2_CAM_BOARD
//#define CAMERA_MODEL_ESP32S3_CAM_LCD
//#define CAMERA_MODEL_DFRobot_FireBeetle2_ESP32S3 // Has PSRAM
//#define CAMERA_MODEL_DFRobot_Romeo_ESP32S3 // Has PSRAM

具体有关引脚定义的内容可以查看 camera_pins.h 文件中的内容,示例中为每个型号配置相应的引脚定义,如果选择了错误的开发板型号,可能会导致引脚错误开发板无法工作。后续烧录 micropython 时引脚信息也可能会使用到

// camera_pins.h文件中的内容示例
#if defined(CAMERA_MODEL_WROVER_KIT)
// ...
#elif defined(CAMERA_MODEL_ESP_EYE)
#define PWDN_GPIO_NUM  -1 // 电源关闭引脚,定义为-1代表并没有启用
#define RESET_GPIO_NUM -1 // 重置引脚,定义为-1代表并没有启用
#define XCLK_GPIO_NUM  4 // 开发板提供给摄像头的外部时钟信号
// 以下引脚是 SCCB(类似于I2C协议)的数据和时钟引脚,用于摄像头与ESP32之间的数据通信
#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
// 这些引脚用于同步摄像头数据传输。VSYNC用于垂直同步,而HREF用于逐行读取图像
#define VSYNC_GPIO_NUM 5
#define HREF_GPIO_NUM  27
// 像素时钟引脚
#define PCLK_GPIO_NUM  25
// 控制板载LED灯
#define LED_GPIO_NUM 22
#elif defined(CAMERA_MODEL_M5STACK_PSRAM)
// ...

CameraWebServer.ino 文件中以下内容里使用了开发板对应的引脚信息,大家只要选好了自己的开发板型号并确定是包含自己开发板的型号的话,这里就可以无需进行额外设置。

// 如果错误的设置了开发板很有可能导致程序无法运行!!!
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;

摄像头相关配置

接下来我会为大家讲解代码中有关摄像头的设置,这些配置大部分都可以在WebServer的前端页面中找到,而默认示例中的配置也是为了特定的硬件条件下以最佳性能运行,所以大家无需更改也可以运行

  config.xclk_freq_hz = 20000000; // 外部时钟频率,决定了图像数据的采样速度,这里设置为20MHz
  config.frame_size = FRAMESIZE_UXGA; // 设置分辨率
  config.pixel_format = PIXFORMAT_JPEG;  // 像素格式设置为JPEG
  //config.pixel_format = PIXFORMAT_RGB565; // RGB565 是一种无损像素格式,适合面部检测或识别等需要高色彩准确度的场景
  config.grab_mode = CAMERA_GRAB_WHEN_EMPTY; // 参数表示摄像头只有在帧缓冲区空闲时才抓取新帧,避免帧缓冲区过载
  config.fb_location = CAMERA_FB_IN_PSRAM; // 设置帧缓冲区在PSRAM中
  config.jpeg_quality = 12; // 设置JPEG图片质量,数值越低质量越高
  config.fb_count = 1; // 预分配的帧缓冲区
  // 分辨率列表:
  // 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

WebServer服务前端页面

修改前端页面我将分别使用 PythonNodejs 来实现

准备工作

WebServer服务的完整代码在 app_httpd.cpp 中,可通过 CameraWebServer.ino 中的以下行,通过ctrl+左键的方式跳转到app_httpd.cpp文件中

void startCameraServer();

在startCameraServer函数种我们可以看到以下内容,而我们如果希望修改该html文件,我们只需要关注 index_uri 这个的相关配置,其他定义的都是一些相关摄像头的接口

void startCameraServer() {
  httpd_config_t config = HTTPD_DEFAULT_CONFIG();
  config.max_uri_handlers = 16;

  httpd_uri_t index_uri = {
    .uri = "/",
    .method = HTTP_GET,
    .handler = index_handler,
    .user_ctx = NULL
#ifdef CONFIG_HTTPD_WS_SUPPORT
    ,
    .is_websocket = true,
    .handle_ws_control_frames = false,
    .supported_subprotocol = NULL
#endif
  };
  
  // ...

通过ctrl+左键的方式到 index_handler 中,就可以看到如下关键代码,大家根据自身的摄像头型号去设置对应的HTML文件。

// 是OV3660时返回的页面
if (s->id.PID == OV3660_PID) {
  return httpd_resp_send(req, (const char *)index_ov3660_html_gz, index_ov3660_html_gz_len);
// 是OV5640时返回的页面
} else if (s->id.PID == OV5640_PID) {
  return httpd_resp_send(req, (const char *)index_ov5640_html_gz, index_ov5640_html_gz_len);
} else {
  // 其他摄像头返回的页面
  return httpd_resp_send(req, (const char *)index_ov2640_html_gz, index_ov2640_html_gz_len);
}

同样使用ctrl+左键的方式跳转到定义 index_ovxxxx_html_gz 的文件中,将对应的html_gz内容完整复制下来,接下来我会分别使用 PythonNodejs 来实现解压文件内容 / 修改文件内容 / 重新压缩实现修改WebServer的前端页面

Python 方式

  1. 将内容写到一个文件中便于操作
primeData = bytes([
    # 对应的html_gz内容
    0x1F, 0x8B, 0x08, 0x08, 0x23, 0xFC, 0x69, ...
])
with open("index_ov2640_html_gz", "wb") as f:
  f.write(primeData)
  1. 解压内容为 HTML 文件
import gzip
with gzip.open('index_ov2640_html_gz', 'rb') as f:
    html_data = f.read()

# 保存解压后的 HTML 文件
with open('index_ov2640_html.html', 'wb') as f:
    f.write(html_data)
  1. 将内容解压成了HTML文件后就是各位前端大佬的强项了,这里进行的页面修改我就不多赘述了,大家可以根据喜好进行修改。修改后重新压缩为字节数据
import gzip
# 读取修改后的 HTML 文件
with open('index_ov2640_html.html', 'rb') as f:
    html_data = f.read()

# 压缩为 gzip
compressed_data = gzip.compress(html_data)

# 打印数组内容用于更新 camera_index.h
print("const uint8_t index_ov2640_html_gz[] = {")
for i, byte in enumerate(compressed_data):
    if i % 12 == 0 and i != 0:
        print()
    print(f"  0x{byte:02X},", end="")
print("};")

# 打印长度
print(f"#define index_ov2640_html_gz_len {len(compressed_data)}")
  1. 接下来就是将打印的以下内容复制到camera_index.h中对应的HTML文件内容即可

const uint8_t index_ov2640_html_gz[ ] = { 0x1F, 0x8B, 0x08, 0x00, 0xC1, 0xE7, 0xDF, 0x66, 0x02, 0xFF, 0xED, 0x3D, ... }

  1. 将修改过后的代码重新编译上传到开发板即可看到修改是否生效

Nodejs方式

  1. 将内容写到一个文件中便于操作
const fs = require('fs')
const zlib = require('zlib')

const bufferBytes = [
    // 对应的html_gz内容
    0x1F, 0x8B, 0x08, 0x08, 0x23, 0xFC, 0x69, ...
]
const buffer = Buffer.from(bufferBytes)
fs.writeFile('index_ov2640_html_gz', buffer, (err)=>{
    if(err){
        console.log('写入到文件出错', err)
        return
    }
    console.log('写入成功')
})
  1. 解压内容为 HTML 文件
fs.readFile('./index_ov2640_html_gz', (err, data) => {
        if (err) {
            console.log('读取文件出错', err)
            return
        }
        zlib.gunzip(data, (err, decompressed) => {
            if (err) {
                console.log('unzip出错', err)
                return
            }
            fs.writeFile('index_ov2640_html.html', decompressed, (err) => {
                if (err) {
                    console.log('写入出错', err)
                    return
                }
                console.log('写入成功')
            });
        });
    });
  1. 将内容解压成了HTML文件后就是各位前端大佬的强项了,这里进行的页面修改我就不多赘述了,大家可以根据喜好进行修改。修改后重新压缩为字节数据
// 读取修改后的 HTML 文件
fs.readFile('index_ov2640_html.html', (err, modifiedHtmlData) => {
    if (err) {
        console.log('读取修改后的HTML出现错误', err)
        return
    };
    // 压缩为新的 gzip
    zlib.gzip(modifiedHtmlData, (err, compressedData) => {
      if (err) {
        console.log('压缩为新的gzip出现错误', err)
        return
      };
        // 打印数组内容用于更新 camera_index.h
        console.log("const uint8_t index_ov2640_html_gz[] = {");

        for (let i = 0; i < compressedData.length; i++) {
        // 每 12 个字节换行一次
        if (i % 12 === 0 && i !== 0) {
            console.log();
        }
        // 打印格式化的字节
        process.stdout.write(`  0x${compressedData[i].toString(16).toUpperCase().padStart(2, '0')},`);
        }

        console.log("};");

        // 打印压缩数据的长度
        console.log(`#define index_ov2640_html_gz_len ${compressedData.length}`);
    });
});
  1. 接下来就是将打印的以下内容复制到camera_index.h中对应的HTML文件内容即可

const uint8_t index_ov2640_html_gz[ ] = { 0x1F, 0x8B, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0xED, 0x3D, ... }

  1. 将修改过后的代码重新编译上传到开发板即可看到修改是否生效

Arduino_edit_Webserver.png

总结

本篇内容对Arduino中的ESP32Cam示例代码进行了分析,提到了有关引脚定义 / 摄像头配置 / 修改示例中的前端页面,希望有帮助到大家,下一篇内容我将和大家分享一下ESP32系列如何烧录micropython