ESP32Cam网络摄像头的最全玩法

ESP32Cam网络摄像头的最全玩法

ESP32Cam是常见的带WiFi功能的网络摄像头,这里列举了十几种不同环境下的使用方法,主要分析不同的编程语言、不同的运行原理、不同的运行环境、不同的优缺点,每一种玩法都提供了已经调试过的编程源代码,在这篇博客里面,总有一款符合您的项目需要。

  • Arduino官方程序

   我们把ESP32Cam用数据线插入电脑,打开Arduino IDE,设备选择“AI_Thinker ESP32Cam”,端口选择对应的端口号(事先要安装好驱动)。

    如图所示,我们选择导入ESP32Cam的官方驱动程序。在打开的程序中,仅需要修改三行代码的参数(选择摄像头类型为“AI_Thinker ESP32Cam”,填写你家里的WiFi连接的名称和密码)。把这个程序烧写到ESP32Cam中,观察电脑Arduino IDE运行窗口,可以看到ESP32Cam连接WiFi,获得一个IP地址,用浏览器或手机访问这个IP地址,就能看到网页上有许多控制按钮,点击“Stream”按钮就能看到摄像头的视频了。

    如果运行窗口中反馈消息是WiFi连接不成功,很可能你连接的WiFi是5G,这个设备是无法连接5G的;如果浏览器访问不成功,则电脑或手机和ESP32Cam没有处在同一个网络中,也就是浏览器和摄像头连接的必须是同一个WiFi设备,在同一个网络桥段中。

    这个程序的优点是:官方程序,使用简单、程序稳定、适应性强。缺点是:文件太大有四个文件五六千行代码,不容易解读,不容易修改。

  • Arduino官方简化

官方程序提供的index.html网页代码是经过编译的十六进制数据(算是密文吧),无法阅读,也不知道是什么。但是只要我们用浏览器访问摄像头,ESP32Cam就会把这个网页代码全部发送到浏览器中,这时候我们可以在浏览器中查看网页源代码,官方程序中密文的就变成了可以阅读的明文了,这时候我们就可以阅读和修改这个index.html了(源代码有一千多行呢)。

我们主要是对源代码进行删减,去掉一些不重要的按钮及代码,仅仅保留下一个视频推送的代码,这样我们把原来四个文件六千行的官方程序,简化成了一个文件300行代码,程序照样能跑起来。

优点:把官方程序中的网页密文变成明文,简化程序方便阅读。缺点:这个需要有一点程序阅读和理解能力。

// 这个是 Arduino IDE 程序,是对官方程序CameraWebServer 的删减
// 让ESP32Cam 连接WiFi  开启网页服务功能

#include "esp_camera.h"
#include <WiFi.h>
#include "esp_http_server.h"

const char* ssid = "TP-LINK_8080";
const char* password = "123456789";

void startCameraServer();

/////////////////////////////////////////////////////////////////////////////////
//  这个是index.html网页的源代码
static const char mainPage[] = u8R"(
<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <title>ESP32 OV2460</title>
    </head>
    <body>
        <section class="main">
          <div id="content">
            <div id="sidebar">
              <nav id="menu">
                <section id="buttons">
                  <button id="toggle-stream">Start Stream</button>
                  <button id="stggle-stream">Stop Stream</button>
                </section>
              </nav>
            </div>
            <figure>
              <div id="stream-container" class="image-container hidden">
                <img id="stream" src="" crossorigin>
              </div>
            </figure>
          </div>
        </section>
      <script>
document.addEventListener('DOMContentLoaded', function (event) {
  var baseHost = document.location.origin
  var streamUrl = baseHost + ':81'

  function setWindow(start_x, start_y, end_x, end_y, offset_x, offset_y, total_x, total_y, output_x, output_y, scaling, binning, cb){
    fetchUrl(`${baseHost}/resolution?sx=${start_x}&sy=${start_y}&ex=${end_x}&ey=${end_y}&offx=${offset_x}&offy=${offset_y}&tx=${total_x}&ty=${total_y}&ox=${output_x}&oy=${output_y}&scale=${scaling}&binning=${binning}`, cb);
  }

  document
    .querySelectorAll('.close')
    .forEach(el => {
      el.onclick = () => {
        hide(el.parentNode)
      }
    })

  const view = document.getElementById('stream')
  const streamButton = document.getElementById('toggle-stream')
  const streamButton2 = document.getElementById('stggle-stream')

  streamButton.onclick = () => {
      view.src = `${streamUrl}/stream`
      show(viewContainer)
  }

  streamButton2.onclick = () => {
      window.stop();
  }
 
})

        </script>
    </body>
</html>
)";

/////////////////////////////////////////////////////////////////////////////////////////////
//  这个是网页服务  webserver 
// 开启调试信息
#if defined(ARDUINO_ARCH_ESP32) && defined(CONFIG_ARDUHAL_ESP_LOG)
#include "esp32-hal-log.h"
#endif
// 开启模块的存储 PSRAM
#ifdef BOARD_HAS_PSRAM
#define CONFIG_ESP_FACE_DETECT_ENABLED 1
#define CONFIG_ESP_FACE_RECOGNITION_ENABLED 0
#endif


#define PART_BOUNDARY "123456789000000000000987654321"
static const char *_STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
static const char *_STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
static const char *_STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\nX-Timestamp: %d.%06d\r\n\r\n";

httpd_handle_t stream_httpd = NULL;
httpd_handle_t camera_httpd = NULL;

static esp_err_t stream_handler(httpd_req_t *req)
{
    camera_fb_t *fb = NULL;
    struct timeval _timestamp;
    esp_err_t res = ESP_OK;
    size_t _jpg_buf_len = 0;
    uint8_t *_jpg_buf = NULL;
    char *part_buf[128];

    res = httpd_resp_set_type(req, _STREAM_CONTENT_TYPE);
    if (res != ESP_OK)
    {
        return res;
    }

    httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
    httpd_resp_set_hdr(req, "X-Framerate", "60");


    while (true)
    {
        fb = esp_camera_fb_get();
        if (!fb)
        {
            log_e("Camera capture failed");
            res = ESP_FAIL;
        }
        else
        {  // 从摄像头获取图片的数据
          _jpg_buf_len = fb->len;
          _jpg_buf = fb->buf;
        }
        if (res == ESP_OK)
        {
            res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY));
        }
        if (res == ESP_OK)
        {
            size_t hlen = snprintf((char *)part_buf, 128, _STREAM_PART, _jpg_buf_len, _timestamp.tv_sec, _timestamp.tv_usec);
            res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen);
        }
        if (res == ESP_OK)
        {
            res = httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len);
        }
        // 清除相关的内存
        if (fb)
        {
            esp_camera_fb_return(fb);
            fb = NULL;
            _jpg_buf = NULL;
        }
        else if (_jpg_buf)
        {
            free(_jpg_buf);
            _jpg_buf = NULL;
        }
        if (res != ESP_OK)
        {
            log_e("Send frame failed");
            break;
        }
    }
    return res;
}

static esp_err_t index_handler(httpd_req_t *req)
{
    httpd_resp_set_type(req, "text/html");
    //httpd_resp_set_hdr(req, "Content-Encoding", "gzip");
    httpd_resp_set_hdr(req, "Content-Encoding", "html");
    sensor_t *s = esp_camera_sensor_get();
    if (s != NULL) {
      //return httpd_resp_send(req, (const char *)index_ov2640_html_gz, index_ov2640_html_gz_len);
      const char* charHtml = mainPage;
      return  httpd_resp_send(req, (const char *)charHtml, strlen(charHtml));

    } else {
        log_e("Camera sensor not found");
        return httpd_resp_send_500(req);
    }
}

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
    };

    httpd_uri_t stream_uri = {
        .uri = "/stream",
        .method = HTTP_GET,
        .handler = stream_handler,
        .user_ctx = NULL
    };

    log_i("Starting web server on port: '%d'", config.server_port);
    if (httpd_start(&camera_httpd, &config) == ESP_OK)
    {
        httpd_register_uri_handler(camera_httpd, &index_uri);
    }
     
    config.server_port += 1;
    config.ctrl_port += 1;
    log_i("Starting stream server on port: '%d'", config.server_port);
    if (httpd_start(&stream_httpd, &config) == ESP_OK)
    {
        httpd_register_uri_handler(stream_httpd, &stream_uri);
    }
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////
//  这个是主程序

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

  // 摄像头引脚 AI_Thinker
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = 5;
  config.pin_d1 = 18;
  config.pin_d2 = 19;
  config.pin_d3 = 21;
  config.pin_d4 = 36;
  config.pin_d5 = 39;
  config.pin_d6 = 34;
  config.pin_d7 = 35;
  config.pin_xclk = 0;
  config.pin_pclk = 22;
  config.pin_vsync = 25;
  config.pin_href = 23;
  config.pin_sccb_sda = 26;
  config.pin_sccb_scl = 27;
  config.pin_pwdn = 32;
  config.pin_reset = -1;
  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;
    }
  }

  // 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();
  // drop down frame size for higher initial frame rate
  if(config.pixel_format == PIXFORMAT_JPEG){
    s->set_framesize(s, FRAMESIZE_QVGA);
  }


  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");
}

void loop() {
  // Do nothing. Everything is done in another task by the web server
  delay(10000);
}

  • Arduino视频推送

这次我们模仿官方程序的运行方式,但是我们没有使用esp_http_server.h这个官方专用的驱动库,而是使用通用的webserver.h驱动库。程序的运行方式和官方程序是一样的,我们让ESP32Cam开启网页服务webserver,提供了两个访问端口8080端口用于控制按钮,8082端口用于推送视频。

我们把这个程序写入到ESP32Cam中,如图所示,我们在电脑或手机浏览器中访问http://192.168.0.105:8080/网页中显示两个按钮,如果我们直接访问http://192.168.0.105:8082/则网页中直接显示摄像头的视频画面了。

优点:使用通用的驱动库,编程更加灵活了,我们可以有更大自由的编程空间。缺点:可能是里面的某些程序没有优化,当用户访问视频推送网页时,设备进入了“while”的视频推送循环,源源不断地把摄像头的图片一张一张地发送给客户端。但是如果客户端的浏览器离开(或网络断开),ESP32Cam会陷入在发送的循环中,造成发送等待等错误,设备会卡死。当客户端断开后重新连接访问,则设备无反应。

这个错误会在下一个程序中得到解决的方法。

// 这个是Arduino IDE 程序 使用通用的 WebServer 库
// 模仿官方程序,开启“8080/"端口推送主页,"8082:/stream"端口主动推送视频


#include "esp_camera.h"
#include <WiFi.h>
#include <WebServer.h>

// 创建两个Web服务器实例
WebServer controlServer(8080);     // 控制服务器(端口8080)
WebServer streamServer(8082);      // 视频流服务器(端口8082)

// 主页处理器
void handleRoot() {
  static const char* mainPage = u8R"(
  <!doctype html>
<html>
    <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
    <title>ESP32 OV2460</title>
    </head>
    <body><center><h2>ESP32Cam视频服务</h2><br>
    <img id="stream" src=''></center>
    <script>
document.addEventListener('DOMContentLoaded', function (event) {
  var baseHost = document.location.origin
  let strArray = [];
  strArray = baseHost.split(":");
  var streamUrl = strArray[0] + ':' + strArray[1] + ':8082/stream'
  
  function fetchUrl(url){
    fetch(url)
      .then(function (response) {
        if (response.status !== 200) {
          cb(response.status, response.statusText);
        } else {
          response.text().then(function(data){
            cb(200, data);
          }).catch(function(err) {
            cb(-1, err);
          });
        }
      })
      .catch(function(err) {
        cb(-1, err);
      });
  }

  const view = document.getElementById('stream');
  view.src = streamUrl;
})
        </script>
    </body>
</html>
  )";

  controlServer.send(200, "text/html", mainPage);
}

// 修改后的视频流处理函数
void handleVideoStream() {
  // 发送响应头部
  streamServer.setContentLength(CONTENT_LENGTH_UNKNOWN);
  streamServer.send(200, "multipart/x-mixed-replace;boundary=frame", "");
  
  // 持续捕获和发送帧
  while (true) {

    // 获取摄像头帧缓冲区
    camera_fb_t * fb = esp_camera_fb_get();
    
    // 检查获取是否成功
    if (!fb) {
      Serial.println("摄像头捕获失败");
      continue;
    }
    
    // 发送边界标记
    streamServer.sendContent("--frame\r\n");
    
    // 发送JPEG头部
    streamServer.sendContent("Content-Type: image/jpeg\r\n");
    
    // 发送内容长度
    String contentLengthHeader = "Content-Length: ";
    contentLengthHeader += String(fb->len);
    contentLengthHeader += "\r\n\r\n";
    streamServer.sendContent(contentLengthHeader);
    
    // 发送图像数据
    streamServer.sendContent((const char *)fb->buf, fb->len);
    
    // 发送结束标记
    streamServer.sendContent("\r\n");
    
    // 释放帧缓冲区
    esp_camera_fb_return(fb);
    
    // 短暂延迟确保稳定性
    delay(10);
  }
}


// WiFi配置
const char* ssid = "TP-LINK_8080";
const char* password = "123456789";


void setup() {
  Serial.begin(115200);
  delay(1000);
  
  // 摄像头引脚 AI_Thinker
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = 5;
  config.pin_d1 = 18;
  config.pin_d2 = 19;
  config.pin_d3 = 21;
  config.pin_d4 = 36;
  config.pin_d5 = 39;
  config.pin_d6 = 34;
  config.pin_d7 = 35;
  config.pin_xclk = 0;
  config.pin_pclk = 22;
  config.pin_vsync = 25;
  config.pin_href = 23;
  config.pin_sccb_sda = 26;
  config.pin_sccb_scl = 27;
  config.pin_pwdn = 32;
  config.pin_reset = -1;
  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;
  
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("摄像头初始化失败 0x%x", err);
    return;
  }
  
  // 连接WiFi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi连接成功");
  Serial.print("IP地址: ");
  Serial.println(WiFi.localIP());
  delay(20);
  // 设置路由
  controlServer.on("/", HTTP_GET, handleRoot);
  controlServer.begin();
  Serial.print(WiFi.localIP());
  Serial.println(":8080  网页服务");
  streamServer.on("/stream", HTTP_GET, handleVideoStream);
  streamServer.begin();
  Serial.print(WiFi.localIP());
  Serial.println(":8082  视频服务");

}

void loop() {
  controlServer.handleClient();
  streamServer.handleClient();
}

  • Arduino图片索取

在这个程序中,我们改变的ESP32Cam的视频推送模式,在前面的几个程序中,视频都是由ESP32Cam设备运行一个while循环,不断地把图片推送到客户端的浏览器中进行显示,容易出现网络断开设备卡死的现象。

我们这次采用的还是webserver.h通用库驱动,只是改变了视频的推送方式。如下图中的第102行代码,我们在浏览器中设置一个循环调用的机制,这样当浏览器运行“streamvideo”函数从服务器ESP32Cam中完成获取一张图片数据后,经过50毫秒的延时,重新调用运行这个“streamvideo”函数,再次访问ESP32Cam获取下一张图片。这样我们通过浏览器源源不断地访问索取摄像头的图片,也能显示摄像头的视频了。

优点:改变了ESP32Cam服务器主动发送图片的while模式,改用客户端浏览器主动访问索取图片,这样主动权在浏览器端,就能解决因为客户端的断线而造成设备卡死的现象(浏览器断开后,重新连接,或刷新,视频照样能显示)

// 这个是Arduino IDE 程序,让ESP32Cam 连接WiFi,开启网页服务
// 提供8080端口  “/” 路径用于推送主页  “/cap”用于推送一张图片
// 视频采用客户端浏览器中的循环调用访问的方式

#include "esp_camera.h"
#include <WiFi.h>
#include <WebServer.h>

const char* ssid = "TP-LINK_8080";
const char* password = "123456789";
WebServer server(8080);

void setup() {
  Serial.begin(115200);
  delay(1000);
  
  // 摄像头引脚 AI_Thinker
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = 5;
  config.pin_d1 = 18;
  config.pin_d2 = 19;
  config.pin_d3 = 21;
  config.pin_d4 = 36;
  config.pin_d5 = 39;
  config.pin_d6 = 34;
  config.pin_d7 = 35;
  config.pin_xclk = 0;
  config.pin_pclk = 22;
  config.pin_vsync = 25;
  config.pin_href = 23;
  config.pin_sccb_sda = 26;
  config.pin_sccb_scl = 27;
  config.pin_pwdn = 32;
  config.pin_reset = -1;
  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;
  
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("摄像头初始化失败 0x%x", err);
    return;
  }
  
  // 连接WiFi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi连接成功");
  Serial.print("IP地址: ");
  Serial.println(WiFi.localIP());
  delay(20);
  // 配置路由
  server.on("/", HTTP_GET, handleRoot);
  server.on("/cap", HTTP_GET, handleCap);
  server.onNotFound(handleNotFound);
  server.begin();
  Serial.println("Web server started");

}

void loop() {
  server.handleClient();
}

void handleRoot() {
  String html = R"=====(
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ESP32-CAM 网络摄像头</title>
</head>
<body><center>
    <h1>ESP32-CAR</h1><br>     
    <img id="video" /><br><br>
    <div class="child" id="status">系统就绪,请点击按钮开始操作</div>
</center></body>

    <script>
        let streamInterval;
        const status = document.getElementById('status');
        const video = document.getElementById('video');

        function streamVideo() {
            fetch('/cap', { method: 'GET', headers: { 'Cache-Control': 'no-cache' } })
            .then(response => { return response.blob(); })
            .then(blob => {
                const url = URL.createObjectURL(blob);
                const oldSrc = video.src;
                video.src = url;
                
                if (oldSrc) { URL.revokeObjectURL(oldSrc); }
                status.textContent = '视频流运行中...';
                // 这种延时重复更可靠
                streamInterval = setTimeout(streamVideo, 50);
            })
            .catch(error => {
                status.textContent = '视频流中断: ';
                streamInterval = setTimeout(streamVideo, 1000);
            });
        }

        window.addEventListener('load', streamVideo);
    </script>
</html>
)=====";
  server.send(200, "text/html", html);
}

void handleCap() {
  // 尝试获取相机帧
  camera_fb_t *fb = esp_camera_fb_get();
  if (!fb) {
    Serial.println("Camera capture failed");
    server.send(500, "text/plain", "Camera capture failed");
    return;
  }
  
  // 发送JPEG图像
  server.sendHeader("Content-Type", "image/jpeg");
  server.sendHeader("Content-Disposition", "inline; filename=capture.jpg");
  server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
  server.sendHeader("Pragma", "no-cache");
  server.sendHeader("Expires", "0");
  
  // 使用sendContentLength和send_P发送大文件
  server.setContentLength(fb->len);
  server.send(200, "image/jpeg", "");
  server.sendContent((const char*)fb->buf, fb->len);
  
  // 释放帧缓冲区
  esp_camera_fb_return(fb);
}

void handleNotFound() {
  String message = "404 - 页面未找到\n\n";
  server.send(404, "text/plain", message);
}

  • Python官方驱动

这次我们的编程语言改为micropython,我们需要使用烧写器在ESP32Cam中写入固件,这样我们就能在Thonny程序中想操作电脑文件一样来编写程序了(烧写时需要把0号端口接地,烧写完成后需要断开接地。ESP32Cam需要使用烧写器才能正确连接到Thonny)。

我们把官方驱动库文件microdot.py导入到ESP32Cam中,然后在设备中写入主程序main.py,点击运行程序,可以用浏览器访问到摄像头的视频了。从代码中我们可以看到,这个程序其实和前面的视频推送程序原理是一样的,在ESP32Cam中开启了网页服务,提供了两个可以供客户端访问的网页 “/” 和 “/video”,视频还是采用服务器端while循环主动把视频发送到客户端的模式。不过在第38行和第40行中,使用了一个“yield”的暂停机制,也就是在while中,每一次运行到这个暂停代码,程序都会返回一个运行结果,然后继续往下运行。正因为有了这个暂停机制,所以有效地解决了前面出现的因为客户端掉线而设备卡死的现象。

优点:这个程序采用python编程语言和文件管理,编程更加方便。缺点:你写的程序代码无法加密保护,被人能轻易看到你写的代码。

(Arduino的代码是经过加密的,他先把C语言的程序,编译成汇编语言,写入到ESP32Cam设备中,而且我们几乎无法从设备中读取到别人写入的代码;即使读出来了代码,也是汇编语言,很难反编译成C语言。因此Arduino对源代码代码有很好的保护,而micropython则用文件形式明文读取出来,对源代码没有一点点的保护了)

注意,下面的代码是两个文件  main.py  和 microdot.py

# 这个是micropython 程序  主程序 main.py
# 提供视频网页服务,采用主动发送的方式,使用 yield 暂停机制

from microdot import Microdot, send_file
import network
import time

# 创建MicroDot应用实例
app = Microdot()

# 连接WiFi
def connect_wifi(ssid, password):
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    if not wlan.isconnected():
        print('connecting to network...')
        wlan.connect(ssid, password)
        while not wlan.isconnected():
            time.sleep(1)
    print('network config:', wlan.ifconfig())

# 主页路由,返回嵌入的HTML内容
@app.route('/')
def index(request):
    html_content = '''<!doctype html>
<html>
  <head>
    <title>Microdot Video Streaming</title>
  </head>
  <body>
    <h2>Microdot Video Streaming</h2>
    <img src="/video_feed" width="30%">
  </body>
</html>'''
    return html_content, 200, {'Content-Type': 'text/html'}

@app.route('/video_feed')
def video_feed(request):
    def stream():
        yield b'--frame\r\n'
        while True:
            frame = camera.capture()
            yield b'Content-Type: image/jpeg\r\n\r\n' + frame + \
                b'\r\n--frame\r\n'
            #time.sleep_ms(50)
 
    return stream(), 200, {'Content-Type':
                           'multipart/x-mixed-replace; boundary=frame'}


import camera
def main():
    connect_wifi("TP-LINK_8080", "123456789")
    try:
        # AI_Thinker  Camera
        camera.init(0, format=camera.JPEG)
        print("摄像头初始化成功")
        
        app.run(port=5000)   # 将端口号设为5000
    except Exception as e:
        camera.deinit()
        print("摄像头初始化失败")
        
if __name__ == '__main__':
    main()





###########################################################################
############################################################################
# 这个是官方驱动库  microdot.py,用于在 ESP32Cam 中开启网页服务 WebServer


try:
    from sys import print_exception
except ImportError:  # pragma: no cover
    import traceback
 
    def print_exception(exc):
        traceback.print_exc()
try:
    import uerrno as errno
except ImportError:
    import errno
 
concurrency_mode = 'threaded'
 
try:  # pragma: no cover
    import threading
 
    def create_thread(f, *args, **kwargs):
        # use the threading module
        threading.Thread(target=f, args=args, kwargs=kwargs).start()
except ImportError:  # pragma: no cover
    def create_thread(f, *args, **kwargs):
        # no threads available, call function synchronously
        f(*args, **kwargs)
 
    concurrency_mode = 'sync'
 
try:
    import ujson as json
except ImportError:
    import json
 
try:
    import ure as re
except ImportError:
    import re
 
socket_timeout_error = OSError
try:
    import usocket as socket
except ImportError:
    try:
        import socket
        socket_timeout_error = socket.timeout
    except ImportError:  # pragma: no cover
        socket = None
 
MUTED_SOCKET_ERRORS = [
    32,  # Broken pipe
    54,  # Connection reset by peer
    104,  # Connection reset by peer
    128,  # Operation on closed socket
]
 
 
def urldecode_str(s):
    s = s.replace('+', ' ')
    parts = s.split('%')
    if len(parts) == 1:
        return s
    result = [parts[0]]
    for item in parts[1:]:
        if item == '':
            result.append('%')
        else:
            code = item[:2]
            result.append(chr(int(code, 16)))
            result.append(item[2:])
    return ''.join(result)
 
 
def urldecode_bytes(s):
    s = s.replace(b'+', b' ')
    parts = s.split(b'%')
    if len(parts) == 1:
        return s.decode()
    result = [parts[0]]
    for item in parts[1:]:
        if item == b'':
            result.append(b'%')
        else:
            code = item[:2]
            result.append(bytes([int(code, 16)]))
            result.append(item[2:])
    return b''.join(result).decode()
 
 
def urlencode(s):
    return s.replace('+', '%2B').replace(' ', '+').replace(
        '%', '%25').replace('?', '%3F').replace('#', '%23').replace(
            '&', '%26').replace('=', '%3D')
 
 
class NoCaseDict(dict):
    
    def __init__(self, initial_dict=None):
        super().__init__(initial_dict or {})
        self.keymap = {k.lower(): k for k in self.keys() if k.lower() != k}
 
    def __setitem__(self, key, value):
        kl = key.lower()
        key = self.keymap.get(kl, key)
        if kl != key:
            self.keymap[kl] = key
        super().__setitem__(key, value)
 
    def __getitem__(self, key):
        kl = key.lower()
        return super().__getitem__(self.keymap.get(kl, kl))
 
    def __delitem__(self, key):
        kl = key.lower()
        super().__delitem__(self.keymap.get(kl, kl))
 
    def __contains__(self, key):
        kl = key.lower()
        return self.keymap.get(kl, kl) in self.keys()
 
    def get(self, key, default=None):
        kl = key.lower()
        return super().get(self.keymap.get(kl, kl), default)
 
 
def mro(cls):  # pragma: no cover

    if hasattr(cls, 'mro'):
        return cls.__mro__
 
    def _mro(cls):
        m = [cls]
        for base in cls.__bases__:
            m += _mro(base)
        return m
 
    mro_list = _mro(cls)

    mro_pruned = []
    for i in range(len(mro_list)):
        base = mro_list.pop(0)
        if base not in mro_list:
            mro_pruned.append(base)
    return mro_pruned
 
 
class MultiDict(dict):

    def __init__(self, initial_dict=None):
        super().__init__()
        if initial_dict:
            for key, value in initial_dict.items():
                self[key] = value
 
    def __setitem__(self, key, value):
        if key not in self:
            super().__setitem__(key, [])
        super().__getitem__(key).append(value)
 
    def __getitem__(self, key):
        return super().__getitem__(key)[0]
 
    def get(self, key, default=None, type=None):
  
        if key not in self:
            return default
        value = self[key]
        if type is not None:
            value = type(value)
        return value
 
    def getlist(self, key, type=None):
        
        if key not in self:
            return []
        values = super().__getitem__(key)
        if type is not None:
            values = [type(value) for value in values]
        return values
 
 
class Request():
    
    max_content_length = 16 * 1024
    max_body_length = 16 * 1024
    max_readline = 2 * 1024
    socket_read_timeout = 0.1
 
    class G:
        pass
 
    def __init__(self, app, client_addr, method, url, http_version, headers,
                 body=None, stream=None, sock=None):
        #: The application instance to which this request belongs.
        self.app = app
        #: The address of the client, as a tuple (host, port).
        self.client_addr = client_addr
        #: The HTTP method of the request.
        self.method = method
        #: The request URL, including the path and query string.
        self.url = url
        #: The path portion of the URL.
        self.path = url
        #: The query string portion of the URL.
        self.query_string = None
        #: The parsed query string, as a
        #: :class:`MultiDict <microdot.MultiDict>` object.
        self.args = {}
        #: A dictionary with the headers included in the request.
        self.headers = headers
        #: A dictionary with the cookies included in the request.
        self.cookies = {}
        #: The parsed ``Content-Length`` header.
        self.content_length = 0
        #: The parsed ``Content-Type`` header.
        self.content_type = None
        #: A general purpose container for applications to store data during
        #: the life of the request.
        self.g = Request.G()
 
        self.http_version = http_version
        if '?' in self.path:
            self.path, self.query_string = self.path.split('?', 1)
            self.args = self._parse_urlencoded(self.query_string)
 
        if 'Content-Length' in self.headers:
            self.content_length = int(self.headers['Content-Length'])
        if 'Content-Type' in self.headers:
            self.content_type = self.headers['Content-Type']
        if 'Cookie' in self.headers:
            for cookie in self.headers['Cookie'].split(';'):
                name, value = cookie.strip().split('=', 1)
                self.cookies[name] = value
 
        self._body = body
        self.body_used = False
        self._stream = stream
        self.stream_used = False
        self.sock = sock
        self._json = None
        self._form = None
        self.after_request_handlers = []
 
    @staticmethod
    def create(app, client_stream, client_addr, client_sock=None):
        # request line
        line = Request._safe_readline(client_stream).strip().decode()
        if not line:
            return None
        method, url, http_version = line.split()
        http_version = http_version.split('/', 1)[1]
 
        # headers
        headers = NoCaseDict()
        while True:
            line = Request._safe_readline(client_stream).strip().decode()
            if line == '':
                break
            header, value = line.split(':', 1)
            value = value.strip()
            headers[header] = value
 
        return Request(app, client_addr, method, url, http_version, headers,
                       stream=client_stream, sock=client_sock)
 
    def _parse_urlencoded(self, urlencoded):
        data = MultiDict()
        if len(urlencoded) > 0:
            if isinstance(urlencoded, str):
                for k, v in [pair.split('=', 1)
                             for pair in urlencoded.split('&')]:
                    data[urldecode_str(k)] = urldecode_str(v)
            elif isinstance(urlencoded, bytes):  # pragma: no branch
                for k, v in [pair.split(b'=', 1)
                             for pair in urlencoded.split(b'&')]:
                    data[urldecode_bytes(k)] = urldecode_bytes(v)
        return data
 
    @property
    def body(self):
        """The body of the request, as bytes."""
        if self.stream_used:
            raise RuntimeError('Cannot use both stream and body')
        if self._body is None:
            self._body = b''
            if self.content_length and \
                    self.content_length <= Request.max_body_length:
                while len(self._body) < self.content_length:
                    data = self._stream.read(
                        self.content_length - len(self._body))
                    if len(data) == 0:  # pragma: no cover
                        raise EOFError()
                    self._body += data
                self.body_used = True
        return self._body
 
    @property
    def stream(self):
        """The input stream, containing the request body."""
        if self.body_used:
            raise RuntimeError('Cannot use both stream and body')
        self.stream_used = True
        return self._stream
 
    @property
    def json(self):
        if self._json is None:
            if self.content_type is None:
                return None
            mime_type = self.content_type.split(';')[0]
            if mime_type != 'application/json':
                return None
            self._json = json.loads(self.body.decode())
        return self._json
 
    @property
    def form(self):
        if self._form is None:
            if self.content_type is None:
                return None
            mime_type = self.content_type.split(';')[0]
            if mime_type != 'application/x-www-form-urlencoded':
                return None
            self._form = self._parse_urlencoded(self.body)
        return self._form
 
    def after_request(self, f):
        self.after_request_handlers.append(f)
        return f
 
    @staticmethod
    def _safe_readline(stream):
        line = stream.readline(Request.max_readline + 1)
        if len(line) > Request.max_readline:
            raise ValueError('line too long')
        return line
 
 
class Response():
    types_map = {
        'css': 'text/css',
        'gif': 'image/gif',
        'html': 'text/html',
        'jpg': 'image/jpeg',
        'js': 'application/javascript',
        'json': 'application/json',
        'png': 'image/png',
        'txt': 'text/plain',
    }
    send_file_buffer_size = 1024
 
    #: The content type to use for responses that do not explicitly define a
    #: ``Content-Type`` header.
    default_content_type = 'text/plain'
 
    #: Special response used to signal that a response does not need to be
    #: written to the client. Used to exit WebSocket connections cleanly.
    already_handled = None
 
    def __init__(self, body='', status_code=200, headers=None, reason=None):
        if body is None and status_code == 200:
            body = ''
            status_code = 204
        self.status_code = status_code
        self.headers = NoCaseDict(headers or {})
        self.reason = reason
        if isinstance(body, (dict, list)):
            self.body = json.dumps(body).encode()
            self.headers['Content-Type'] = 'application/json; charset=UTF-8'
        elif isinstance(body, str):
            self.body = body.encode()
        else:
            # this applies to bytes, file-like objects or generators
            self.body = body
 
    def set_cookie(self, cookie, value, path=None, domain=None, expires=None,
                   max_age=None, secure=False, http_only=False):
        http_cookie = '{cookie}={value}'.format(cookie=cookie, value=value)
        if path:
            http_cookie += '; Path=' + path
        if domain:
            http_cookie += '; Domain=' + domain
        if expires:
            if isinstance(expires, str):
                http_cookie += '; Expires=' + expires
            else:
                http_cookie += '; Expires=' + expires.strftime(
                    '%a, %d %b %Y %H:%M:%S GMT')
        if max_age:
            http_cookie += '; Max-Age=' + str(max_age)
        if secure:
            http_cookie += '; Secure'
        if http_only:
            http_cookie += '; HttpOnly'
        if 'Set-Cookie' in self.headers:
            self.headers['Set-Cookie'].append(http_cookie)
        else:
            self.headers['Set-Cookie'] = [http_cookie]
 
    def complete(self):
        if isinstance(self.body, bytes) and \
                'Content-Length' not in self.headers:
            self.headers['Content-Length'] = str(len(self.body))
        if 'Content-Type' not in self.headers:
            self.headers['Content-Type'] = self.default_content_type
            if 'charset=' not in self.headers['Content-Type']:
                self.headers['Content-Type'] += '; charset=UTF-8'
 
    def write(self, stream):
        self.complete()
 
        # status code
        reason = self.reason if self.reason is not None else \
            ('OK' if self.status_code == 200 else 'N/A')
        stream.write('HTTP/1.0 {status_code} {reason}\r\n'.format(
            status_code=self.status_code, reason=reason).encode())
 
        # headers
        for header, value in self.headers.items():
            values = value if isinstance(value, list) else [value]
            for value in values:
                stream.write('{header}: {value}\r\n'.format(
                    header=header, value=value).encode())
        stream.write(b'\r\n')
 
        # body
        can_flush = hasattr(stream, 'flush')
        try:
            for body in self.body_iter():
                if isinstance(body, str):  # pragma: no cover
                    body = body.encode()
                stream.write(body)
                if can_flush:  # pragma: no cover
                    stream.flush()
        except OSError as exc:  # pragma: no cover
            if exc.errno in MUTED_SOCKET_ERRORS:
                pass
            else:
                raise
 
    def body_iter(self):
        if self.body:
            if hasattr(self.body, 'read'):
                while True:
                    buf = self.body.read(self.send_file_buffer_size)
                    if len(buf):
                        yield buf
                    if len(buf) < self.send_file_buffer_size:
                        break
                if hasattr(self.body, 'close'):  # pragma: no cover
                    self.body.close()
            elif hasattr(self.body, '__next__'):
                yield from self.body
            else:
                yield self.body
 
    @classmethod
    def redirect(cls, location, status_code=302):
        if '\x0d' in location or '\x0a' in location:
            raise ValueError('invalid redirect URL')
        return cls(status_code=status_code, headers={'Location': location})
 
    @classmethod
    def send_file(cls, filename, status_code=200, content_type=None):
        if content_type is None:
            ext = filename.split('.')[-1]
            if ext in Response.types_map:
                content_type = Response.types_map[ext]
            else:
                content_type = 'application/octet-stream'
        f = open(filename, 'rb')
        return cls(body=f, status_code=status_code,
                   headers={'Content-Type': content_type})
 
 
class URLPattern():
    def __init__(self, url_pattern):
        self.url_pattern = url_pattern
        self.pattern = ''
        self.args = []
        use_regex = False
        for segment in url_pattern.lstrip('/').split('/'):
            if segment and segment[0] == '<':
                if segment[-1] != '>':
                    raise ValueError('invalid URL pattern')
                segment = segment[1:-1]
                if ':' in segment:
                    type_, name = segment.rsplit(':', 1)
                else:
                    type_ = 'string'
                    name = segment
                if type_ == 'string':
                    pattern = '[^/]+'
                elif type_ == 'int':
                    pattern = '\\d+'
                elif type_ == 'path':
                    pattern = '.+'
                elif type_.startswith('re:'):
                    pattern = type_[3:]
                else:
                    raise ValueError('invalid URL segment type')
                use_regex = True
                self.pattern += '/({pattern})'.format(pattern=pattern)
                self.args.append({'type': type_, 'name': name})
            else:
                self.pattern += '/{segment}'.format(segment=segment)
        if use_regex:
            self.pattern = re.compile('^' + self.pattern + '$')
 
    def match(self, path):
        if isinstance(self.pattern, str):
            if path != self.pattern:
                return
            return {}
        g = self.pattern.match(path)
        if not g:
            return
        args = {}
        i = 1
        for arg in self.args:
            value = g.group(i)
            if arg['type'] == 'int':
                value = int(value)
            args[arg['name']] = value
            i += 1
        return args
 
 
class HTTPException(Exception):
    def __init__(self, status_code, reason=None):
        self.status_code = status_code
        self.reason = reason or str(status_code) + ' error'
 
    def __repr__(self):  # pragma: no cover
        return 'HTTPException: {}'.format(self.status_code)
 
 
class Microdot():
    def __init__(self):
        self.url_map = []
        self.before_request_handlers = []
        self.after_request_handlers = []
        self.after_error_request_handlers = []
        self.error_handlers = {}
        self.shutdown_requested = False
        self.debug = False
        self.server = None
 
    def route(self, url_pattern, methods=None):
        def decorated(f):
            self.url_map.append(
                (methods or ['GET'], URLPattern(url_pattern), f))
            return f
        return decorated
 
    def get(self, url_pattern):
        return self.route(url_pattern, methods=['GET'])
 
    def post(self, url_pattern):
        return self.route(url_pattern, methods=['POST'])
 
    def put(self, url_pattern):
        return self.route(url_pattern, methods=['PUT'])
 
    def patch(self, url_pattern):
        return self.route(url_pattern, methods=['PATCH'])
 
    def delete(self, url_pattern):
        return self.route(url_pattern, methods=['DELETE'])
 
    def before_request(self, f):
        self.before_request_handlers.append(f)
        return f
 
    def after_request(self, f):
        self.after_request_handlers.append(f)
        return f
 
    def after_error_request(self, f):
        self.after_error_request_handlers.append(f)
        return f
 
    def errorhandler(self, status_code_or_exception_class):
        def decorated(f):
            self.error_handlers[status_code_or_exception_class] = f
            return f
        return decorated
 
    def mount(self, subapp, url_prefix=''):
        for methods, pattern, handler in subapp.url_map:
            self.url_map.append(
                (methods, URLPattern(url_prefix + pattern.url_pattern),
                 handler))
        for handler in subapp.before_request_handlers:
            self.before_request_handlers.append(handler)
        for handler in subapp.after_request_handlers:
            self.after_request_handlers.append(handler)
        for handler in subapp.after_error_request_handlers:
            self.after_error_request_handlers.append(handler)
        for status_code, handler in subapp.error_handlers.items():
            self.error_handlers[status_code] = handler
 
    @staticmethod
    def abort(status_code, reason=None):
        raise HTTPException(status_code, reason)
 
    def run(self, host='0.0.0.0', port=5000, debug=False, ssl=None):
        self.debug = debug
        self.shutdown_requested = False
 
        self.server = socket.socket()
        ai = socket.getaddrinfo(host, port)
        addr = ai[0][-1]
 
        if self.debug:  # pragma: no cover
            print('Starting {mode} server on {host}:{port}...'.format(
                mode=concurrency_mode, host=host, port=port))
        self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.server.bind(addr)
        self.server.listen(5)
 
        if ssl:
            self.server = ssl.wrap_socket(self.server, server_side=True)
 
        while not self.shutdown_requested:
            try:
                sock, addr = self.server.accept()
            except OSError as exc:  # pragma: no cover
                if exc.errno == errno.ECONNABORTED:
                    break
                else:
                    print_exception(exc)
            except Exception as exc:  # pragma: no cover
                print_exception(exc)
            else:
                create_thread(self.handle_request, sock, addr)
 
    def shutdown(self):
        self.shutdown_requested = True
 
    def find_route(self, req):
        f = 404
        for route_methods, route_pattern, route_handler in self.url_map:
            req.url_args = route_pattern.match(req.path)
            if req.url_args is not None:
                if req.method in route_methods:
                    f = route_handler
                    break
                else:
                    f = 405
        return f
 
    def handle_request(self, sock, addr):
        if Request.socket_read_timeout and \
                hasattr(sock, 'settimeout'):  # pragma: no cover
            sock.settimeout(Request.socket_read_timeout)
        if not hasattr(sock, 'readline'):  # pragma: no cover
            stream = sock.makefile("rwb")
        else:
            stream = sock
 
        req = None
        res = None
        try:
            req = Request.create(self, stream, addr, sock)
            res = self.dispatch_request(req)
        except socket_timeout_error as exc:  # pragma: no cover
            if exc.errno and exc.errno not in [60, 110]:
                print_exception(exc)  # not a timeout
        except Exception as exc:  # pragma: no cover
            print_exception(exc)
        try:
            if res and res != Response.already_handled:  # pragma: no branch
                res.write(stream)
            stream.close()
        except OSError as exc:  # pragma: no cover
            if exc.errno in MUTED_SOCKET_ERRORS:
                pass
            else:
                print_exception(exc)
        except Exception as exc:  # pragma: no cover
            print_exception(exc)
        if stream != sock:  # pragma: no cover
            sock.close()
        if self.shutdown_requested:  # pragma: no cover
            self.server.close()
        if self.debug and req:  # pragma: no cover
            print('{method} {path} {status_code}'.format(
                method=req.method, path=req.path,
                status_code=res.status_code))
 
    def dispatch_request(self, req):
        after_request_handled = False
        if req:
            if req.content_length > req.max_content_length:
                if 413 in self.error_handlers:
                    res = self.error_handlers[413](req)
                else:
                    res = 'Payload too large', 413
            else:
                f = self.find_route(req)
                try:
                    res = None
                    if callable(f):
                        for handler in self.before_request_handlers:
                            res = handler(req)
                            if res:
                                break
                        if res is None:
                            res = f(req, **req.url_args)
                        if isinstance(res, tuple):
                            body = res[0]
                            if isinstance(res[1], int):
                                status_code = res[1]
                                headers = res[2] if len(res) > 2 else {}
                            else:
                                status_code = 200
                                headers = res[1]
                            res = Response(body, status_code, headers)
                        elif not isinstance(res, Response):
                            res = Response(res)
                        for handler in self.after_request_handlers:
                            res = handler(req, res) or res
                        for handler in req.after_request_handlers:
                            res = handler(req, res) or res
                        after_request_handled = True
                    elif f in self.error_handlers:
                        res = self.error_handlers[f](req)
                    else:
                        res = 'Not found', f
                except HTTPException as exc:
                    if exc.status_code in self.error_handlers:
                        res = self.error_handlers[exc.status_code](req)
                    else:
                        res = exc.reason, exc.status_code
                except Exception as exc:
                    print_exception(exc)
                    exc_class = None
                    res = None
                    if exc.__class__ in self.error_handlers:
                        exc_class = exc.__class__
                    else:
                        for c in mro(exc.__class__)[1:]:
                            if c in self.error_handlers:
                                exc_class = c
                                break
                    if exc_class:
                        try:
                            res = self.error_handlers[exc_class](req, exc)
                        except Exception as exc2:  # pragma: no cover
                            print_exception(exc2)
                    if res is None:
                        if 500 in self.error_handlers:
                            res = self.error_handlers[500](req)
                        else:
                            res = 'Internal server error', 500
        else:
            if 400 in self.error_handlers:
                res = self.error_handlers[400](req)
            else:
                res = 'Bad request', 400
 
        if isinstance(res, tuple):
            res = Response(*res)
        elif not isinstance(res, Response):
            res = Response(res)
        if not after_request_handled:
            for handler in self.after_error_request_handlers:
                res = handler(req, res) or res
        return res
 
 
abort = Microdot.abort
Response.already_handled = Response()
redirect = Response.redirect
send_file = Response.send_file


  • Python图片索取

在这个程序中,我们放弃了官方驱动库microdot.py,改用通用的网络库usocket,在这里我们还是采用了客户端浏览器循环获取图片的方式。

优点:没有官方驱动库microdot.py,编程方式更灵活。

# 这个是 micropython 程序 使用 usocket 内置驱动库
# 让ESP32Cam开启视频网页服务,采用客户端浏览器主动获取图片的方式


import usocket as socket
import network
import time
import os

def get_homepage(conn):  # 显示index主页
    html = """<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ESP32-CAM 网络摄像头</title>
</head>
<body><center>
    <h1>ESP32-CAR</h1><br>     
    <img id="video" /><br><br>
    <div class="child" id="status">系统就绪,请点击按钮开始操作</div>
</center></body>

    <script>
        let streamInterval;
        const status = document.getElementById('status');
        const video = document.getElementById('video');

        function streamVideo() {
            fetch('/cap', { method: 'GET', headers: { 'Cache-Control': 'no-cache' } })
            .then(response => { return response.blob(); })
            .then(blob => {
                const url = URL.createObjectURL(blob);
                const oldSrc = video.src;
                video.src = url;
                
                if (oldSrc) { URL.revokeObjectURL(oldSrc); }
                status.textContent = '视频流运行中...';
                // 这种延时重复更可靠
                streamInterval = setTimeout(streamVideo, 50);
            })
            .catch(error => {
                status.textContent = '视频流中断: ';
                streamInterval = setTimeout(streamVideo, 1000);
            });
        }

        window.addEventListener('load', streamVideo);
    </script>
</html>"""
    response = "HTTP/1.1 200 OK\r\n"
    response += "Content-Type: text/html; charset=utf-8\r\n"
    response += "Connection: close\r\n"
    response += "\r\n"
    response += html
    conn.send(response.encode('utf-8'))

import gc
def get_cappage(conn):  # 显示index主页
    try:
        buf = camera.capture()
        if buf:
            frame = ( # 构造视频帧
                    "HTTP/1.1 200 OK\r\n"
                    "Content-Type: image/jpeg\r\n"
                    "Content-Length: {}\r\n"
                    "Connection: close\r\n"
                    "\r\n".format(len(buf))
                ).encode('utf-8') + buf + b"\r\n"

            conn.send(frame)
            del buf   # 内存管理  回收
            gc.collect()
            return True
        else:
            time.sleep(30)
                          
    except OSError:
        return False
    except Exception as e:
        return False
    finally:
        return False

def get_errorpage(conn):   # 显示error页
    response = "HTTP/1.1 500 Internal Server Error\r\n"
    response += "Content-Type: text/html\r\n"
    response += "Connection: close\r\n"
    response += "\r\n"
    response += "<h1>500 服务器内部错误</h1>"
    conn.send(response.encode('utf-8'))

# 启动服务器
def start_server(port=80):
    # 创建socket
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
    # 绑定地址和端口
    s.bind(('', port))
    # 开始监听
    s.listen(1)
    print("服务器启动,监听端口:", port)
    
    while True:
        try:
            # 服务器堵塞,等待接受连接
            conn, addr = s.accept()
            #print("客户端连接:", addr)
            
            # 设置超时
            conn.settimeout(3.0)
            
            # 接收数据
            request = conn.recv(1024)
            request_str = request.decode('utf-8')
            if request_str:    # 处理请求并生成响应
                try:
                    request_line = request_str.split('\n')[0]
                    method, path, version = request_line.split()
                    #print("方法:", method, "路径:", path)
       
                    # 根据路径返回不同内容
                    if path == '/' or path == '/index.html':
                        get_homepage(conn)
                    elif path.startswith("/cap"):
                        get_cappage(conn)
                    else:
                        get_errorpage(conn)
            
                except Exception as e:
                    get_errorpage(conn)

            conn.close()    # 关闭连接
                       
        except Exception as e:
            print("连接处理出错:", e)
            try:
                conn.close()
            except:
                pass# 配置WiFi连接
    
# 主函数
import camera
def main():
    # WiFi连接
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    if not wlan.isconnected():
        print('connecting to network...')
        wlan.connect("TP-LINK_8080", "123456789")
        while not wlan.isconnected():
            time.sleep(1)
    print('network config:', wlan.ifconfig())
    
    try:  # 摄像头初始化 AI_Think_ESP32Cam
        camera.init(0, d0=5, d1=18, d2=19, d3=21, d4=36, d5=39, d6=34, d7=35, xclk=0, pclk=22, href=23, vsync=25, siod=26, sioc=27, pwdn=32, reset=-1, format=camera.JPEG, framesize=camera.FRAME_HQVGA)
        #camera.init(0, format=camera.JPEG)
        print("摄像头初始化成功")
        
        start_server(8080)   # 开启网页服务
        print("服务器开启")
    except Exception as e:
        camera.deinit()
        print("摄像头初始化失败")
    except KeyboardInterrupt:
        camera.deinit()
        print("\n服务器停止")

if __name__ == "__main__":
    main()




  • Python云台控制

现在的网络摄像头一般都配有云台控制,也就是在摄像头APP中,增加“左转”“右转”按钮,可以在手机中控制摄像头中的舵机,让摄像头转动方向,这样就能观察到不同角度的视频图画内容了。

我们在前面的这个程序中,修改网页index.html的布局,增加两个按钮。并且在ESP32Cam中增加一个“/do”的访问路径,用于接受客户端传入的转动命令参数,并把参数打印出来(这里没有连接舵机,仅仅打印传入的指令参数而已)。

这里我们修改了ESP32Cam的WiFi连接方式,改为热点模式,当ESP32Cam设备通电时,会开启“ESP32_SERVER”的WiFi热点(密码12345678),用手机连接这个WiFi热点,用浏览器访问设备主页http://192.168.4.1:8080/,就能显示视频、进行云台控制了。

优点:增加了云台控制的功能。

# 这个是 micropython 程序,让ESP32Cam 开启视频网页服务
# 在网页中增加的云台控制的按钮

import usocket as socket
import network
import time
import os

def get_homepage(conn):  # 显示index主页
    html = """<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ESP32-CAM 网络摄像头</title>
</head>
<body><center>
    <h2>ESP32-CAR</h2><br>
    <button style="font-size: 24px;" onclick="sendcommer('/doll')">left</button>
    <button style="font-size: 24px;" onclick="sendcommer('/dorr')">right</button><br><br>
    <img id="video" /><br><br>
    <div class="child" id="status">系统就绪,请点击按钮开始操作</div>
</center></body>

    <script>
        let streamInterval;
        const status = document.getElementById('status');
        const video = document.getElementById('video');
        
        function sendcommer(path) {
            fetch(path, { method: 'GET', headers: { 'Cache-Control': 'no-cache' } })
        }

        function streamVideo() {
            fetch('/cap', { method: 'GET', headers: { 'Cache-Control': 'no-cache' } })
            .then(response => { return response.blob(); })
            .then(blob => {
                const url = URL.createObjectURL(blob);
                const oldSrc = video.src;
                video.src = url;
                
                if (oldSrc) { URL.revokeObjectURL(oldSrc); }
                status.textContent = '视频流运行中...';
                // 这种延时重复更可靠
                streamInterval = setTimeout(streamVideo, 50);
            })
            .catch(error => {
                status.textContent = '视频流中断: ';
                streamInterval = setTimeout(streamVideo, 1000);
            });
        }

        window.addEventListener('load', streamVideo);
    </script>
</html>"""
    response = "HTTP/1.1 200 OK\r\n"
    response += "Content-Type: text/html; charset=utf-8\r\n"
    response += "Connection: close\r\n"
    response += "\r\n"
    response += html
    conn.send(response.encode('utf-8'))

import gc
def get_cappage(conn):  # 显示index主页
    try:
        buf = camera.capture()
        if buf:
            frame = ( # 构造视频帧
                    "HTTP/1.1 200 OK\r\n"
                    "Content-Type: image/jpeg\r\n"
                    "Content-Length: {}\r\n"
                    "Connection: close\r\n"
                    "\r\n".format(len(buf))
                ).encode('utf-8') + buf + b"\r\n"

            conn.send(frame)
            del buf   # 内存管理  回收
            gc.collect()
            return True
        else:
            time.sleep(30)
                          
    except OSError:
        return False
    except Exception as e:
        return False
    finally:
        return False

def get_dopage(conn, path):   # 接收云台控制
    print(path)  # 这里仅打印,没有增加舵机控制
    
    response = "HTTP/1.1 500 OK\r\n"
    response += "Content-Type: text/html\r\n"
    response += "Connection: close\r\n"
    response += "\r\n"
    conn.send(response.encode('utf-8'))

def get_errorpage(conn):   # 显示error页
    response = "HTTP/1.1 500 Internal Server Error\r\n"
    response += "Content-Type: text/html\r\n"
    response += "Connection: close\r\n"
    response += "\r\n"
    response += "<h1>500 服务器内部错误</h1>"
    conn.send(response.encode('utf-8'))

# 启动服务器
def start_server(port=80):
    # 创建socket
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
    # 绑定地址和端口
    s.bind(('', port))
    # 开始监听
    s.listen(1)
    print("服务器启动,监听端口:", port)
    
    while True:
        try:
            # 服务器堵塞,等待接受连接
            conn, addr = s.accept()
            #print("客户端连接:", addr)
            
            # 设置超时
            conn.settimeout(3.0)
            
            # 接收数据
            request = conn.recv(1024)
            request_str = request.decode('utf-8')
            if request_str:    # 处理请求并生成响应
                try:
                    request_line = request_str.split('\n')[0]
                    method, path, version = request_line.split()
                    #print("方法:", method, "路径:", path)
       
                    # 根据路径返回不同内容
                    if path == '/' or path == '/index.html':
                        get_homepage(conn)
                    elif path.startswith("/cap"):
                        get_cappage(conn)
                    elif path.startswith("/do"):
                        get_dopage(conn, path)
                    else:
                        get_errorpage(conn)
            
                except Exception as e:
                    get_errorpage(conn)

            conn.close()    # 关闭连接
                       
        except Exception as e:
            print("连接处理出错:", e)
            try:
                conn.close()
            except:
                pass# 配置WiFi连接
    
# 主函数
import camera
def main():
    # 创建WiFi接入点对象
    ap = network.WLAN(network.AP_IF)
    ap.active(True)
    ap.config(essid='ESP32_AP', authmode=network.AUTH_WPA_WPA2_PSK, password='12345678')
    time.sleep(2)
    print('IP地址:', ap.ifconfig()[0])
    
    try:   # 摄像头初始化 AI_Think_ESP32Cam
        camera.init(0, d0=5, d1=18, d2=19, d3=21, d4=36, d5=39, d6=34, d7=35, xclk=0, pclk=22, href=23, vsync=25, siod=26, sioc=27, pwdn=32, reset=-1, format=camera.JPEG, framesize=camera.FRAME_HQVGA)
        #camera.init(0, format=camera.JPEG)
        print("摄像头初始化成功")
        
        start_server(8080)   # 开启网页服务
        print("服务器开启")
    except Exception as e:
        camera.deinit()
        print("摄像头初始化失败")
    except KeyboardInterrupt:
        camera.deinit()
        print("\n服务器停止")

if __name__ == "__main__":
    main()

  • Python液晶屏显示

在这个程序中,我们给ESP32Cam连接一块液晶屏(ST7789驱动的240x240彩色液晶屏),导入st7789.py驱动库,写入程序就可以在液晶屏中看到摄像头的视频了。

液晶屏有几个接口,分别接到ESP32Cam设备:GND接地,VCC接OUT电源,SCL时钟接14,SDA数据接13,RST重启接12,DC命令选择接2,CS设备选择接15。

优点:这个程序直接从摄像头获取图片的RGB565数据,然后发送到液晶屏中显示,程序简单,运行稳定,视频流畅。缺点:因为液晶屏需要图片是RGB格式,这样的图片格式未经压缩,数据量更大,不利于图片存储和传输。

在实际的项目应用中很少用RGB格式图片,都用JPG的压缩格式,在下一个程序会有相关解决方案。

# 这个是 micropython 程序,给 ESP32Cam 连接一块 st7789 液晶屏
# 从摄像头获取 RGB565 格式的图片数据,然后直接发送给液晶屏显示出来


from machine import Pin, SPI
import st7789
import time
spi = SPI(2, baudrate=30000000, sck=Pin(14), mosi=Pin(13))
display = st7789.ST7789(spi, cs=Pin(15, Pin.OUT), dc=Pin(2, Pin.OUT), rst=Pin(12, Pin.OUT))

import camera
def main(): 
    try:  # 摄像头初始化
        camera.init(0, format=camera.RGB565, framesize=camera.FRAME_HQVGA)
        print("摄像头初始化成功")
        time.sleep(0.5)
        while(True):
            try:
                time.sleep(0.1)
                buf = camera.capture()
                if buf:
                    display.display(buf)
            except Exception as e:
                camera.deinit()
                break
    except KeyboardInterrupt:
        camera.deinit()
        print("摄像头初始化失败")

if __name__ == "__main__":
    main()












import framebuf
import time

class ST7789:
    def __init__(self, spi, cs, dc, rst, width=240, height=240):
        self.spi = spi
        self.cs = cs
        self.dc = dc
        self.rst = rst
        self.width = width
        self.height = height
        self.cs.init(self.cs.OUT, value=1)
        self.dc.init(self.dc.OUT, value=0)
        self.rst.init(self.rst.OUT, value=1)
        self.reset()
        self.init_display()
        time.sleep_ms(100)
        self.fill()
        time.sleep_ms(100)

    def reset(self):
        self.rst(0)
        time.sleep_ms(100)
        self.rst(1)
        time.sleep_ms(100)

    def write_cmd(self, cmd):
        self.dc(0)
        self.cs(0)
        self.spi.write(cmd)
        self.cs(1)

    def write_data(self, data):
        self.dc(1)
        self.cs(0)
        self.spi.write(data)
        self.cs(1)

    def init_display(self):
        self.write_cmd(b'\x11')  # Sleep out
        time.sleep_ms(120)
        self.write_cmd(b'\x36')
        self.write_data(b'\x00')  # MADCTL: RGB mode
        self.write_cmd(b'\x3A')
        self.write_data(b'\x05')  # COLMOD: 16-bit color
        self.write_cmd(b'\xB2')
        self.write_data(b'\x0C\x0C\x00\x33\x33')
        self.write_cmd(b'\xB7')
        self.write_data(b'\x35')
        self.write_cmd(b'\xBB')
        self.write_data(b'\x19')
        self.write_cmd(b'\xC0')
        self.write_data(b'\x2C')
        self.write_cmd(b'\xC2')
        self.write_data(b'\x01')
        self.write_cmd(b'\xC3')
        self.write_data(b'\x12')
        self.write_cmd(b'\xC4')
        self.write_data(b'\x20')
        self.write_cmd(b'\xC6')
        self.write_data(b'\x0F')
        self.write_cmd(b'\xD0')
        self.write_data(b'\xA4\xA1')
        self.write_cmd(b'\xE0')
        self.write_data(b'\xD0\x04\x0D\x11\x1A\x2B\x3F\x56\x4B\x0B\x16\x12\x1D\x24')
        self.write_cmd(b'\xE1')
        self.write_data(b'\xD0\x04\x0C\x11\x1A\x2C\x3F\x44\x51\x0B\x16\x12\x1D\x24')
        self.write_cmd(b'\x21')  # Inversion ON
        self.write_cmd(b'\x29')  # Display ON

    def fill(self):
        self.write_cmd(b'\x2A')
        self.write_data(b'\x00\x00')
        self.write_data(b'\x00\xef')
        self.write_cmd(b'\x2B')
        self.write_data(b'\x00\x00')
        self.write_data(b'\x00\xef')
        self.write_cmd(b'\x2C')
        self.write_data(b'\x00\x00' * 240 * 240)
        
    def display(self, img):
        self.write_cmd(b'\x2A')
        self.write_data(b'\x00\x00')
        self.write_data(b'\x00\xef')
        self.write_cmd(b'\x2B')
        self.write_data(b'\x00\x20')
        self.write_data(b'\x00\xcf')
        self.write_cmd(b'\x2C')
        self.write_data(img)


  • Arduino客户端连接

在这个程序中,我们把ESP32Cam安装到小车上面,开启WiFi热点和网页服务。我们再拿一个ESP32(我用的是nodeS型)作为遥控器,并增加一块液晶屏。为了提高图片传输速度,我们对图片数据采用JPG压缩数据。

我们通电服务器端的ESP32Cam,设备开启WiFi热点;通电遥控器自动连接WiFi,并从小车获取图片JPG数据,经过JPG解码成RGB565数据,发送给液晶屏显示。

优点:图片数据经过JPG压缩,数据减少很多(压缩后数据仅原来的30%左右,这样可以极大提高WiFi传输速度,提高液晶屏图片的刷新帧率)。缺点:需要在ESP32中对图片进行JPG解码,micropython中没有很好的解压驱动库,所以这两个程序都需要再Arduino中用C语言进行编译。

还有一点需要说明一下,就是摄像头本身,可以根据需要输出RGB565格式的图片数据,也可以输出JPG格式的图片数据。其实摄像头采用的是硬件JPG压缩,摄像头的数据,经过压缩JPG压缩芯片,直接能输出JPG压缩数据。但是ESP32没有JPG的硬件压缩功能,只能采用软件解压的方式,目前比较成熟的是运行在Arduino IDE中的用C语言写的TJpg_Decoder软件解压驱动库。目前micropython中尚未发现相关的软件解压驱动库,但是也有人把这个TJpg_Decoder的C语言解压驱动库编译到了ESP32的固件中了,具体怎样使用我也没有深入去了解。

注意,下面是两个 Arduino IDE程序,一个是服务器端的小车程序,一个是客户端的遥控器程序。

// 这是 Arduino 程序,是服务器端小车的程序
// 让 ESP32Cam 开启WiFi热点,开启视频网页服务


#include "esp_camera.h"
#include <WiFi.h>
#include <WebServer.h>

const char* ssid = "ESP32_Server";
const char* password = "12345678";
WebServer server(8080);

void setup() {
  Serial.begin(115200);
  delay(100);
  
  // 引脚定义 (AI Thinker ESP32-CAM)
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = 5;
  config.pin_d1 = 18;
  config.pin_d2 = 19;
  config.pin_d3 = 21;
  config.pin_d4 = 36;
  config.pin_d5 = 39;
  config.pin_d6 = 34;
  config.pin_d7 = 35;
  config.pin_xclk = 0;
  config.pin_pclk = 22;
  config.pin_vsync = 25;
  config.pin_href = 23;
  config.pin_sscb_sda = 26;
  config.pin_sscb_scl = 27;
  config.pin_pwdn = 32;
  config.pin_reset = -1;
  config.xclk_freq_hz = 20000000;
  config.pixel_format = PIXFORMAT_JPEG;
  config.frame_size = FRAMESIZE_HQVGA;  // 可调整为FRAMESIZE_SVGA或更小
  config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
  config.fb_location = CAMERA_FB_IN_PSRAM;
  config.jpeg_quality = 12;           // 0-63 数值越小质量越高
  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;
  }
  
  
  // 设置WiFi为热点模式,这样就会提供192.168.4.1 的服务器IP 
  WiFi.mode(WIFI_OFF);
  delay(1000);
  WiFi.mode(WIFI_AP);
  WiFi.softAP(ssid, password);
  IPAddress IP = WiFi.softAPIP();
  String ipstr = IP.toString();
  Serial.println(ipstr);

  // 配置路由
  server.on("/", HTTP_GET, handleRoot);
  server.on("/cap", HTTP_GET, handleCap);
  server.on("/do", HTTP_GET, handleDo);
  server.onNotFound(handleNotFound);
  server.begin();
  Serial.println("Web server started");

}

void loop() {
  server.handleClient();
}

void handleRoot() {
  String html = R"=====(
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ESP32-CAM 网络摄像头</title>
</head>
<body><center>
    <h2>ESP32-CAR</h2></br>     
    <button style="font-size: 24px;" onclick="doaction('val=1')">left</button>
    <button style="font-size: 24px;" onclick="doaction('val=2')">right</button><br><br>
    <img id="video" /><br><br>
    <div class="child" id="status">系统就绪,请点击按钮开始操作</div>
</center></body>

    <script>
        let streamInterval;
        const status = document.getElementById('status');
        const video = document.getElementById('video');

        function doaction(vals) {
            fetch('/do?' + vals, { method: 'GET', headers: { 'Cache-Control': 'no-cache' } })
        }

        function streamVideo() {
            fetch('/cap', { method: 'GET', headers: { 'Cache-Control': 'no-cache' } })
            .then(response => { return response.blob(); })
            .then(blob => {
                const url = URL.createObjectURL(blob);
                const oldSrc = video.src;
                video.src = url;
                
                if (oldSrc) { URL.revokeObjectURL(oldSrc); }
                status.textContent = '视频流运行中...';
                // 这种延时重复更可靠
                streamInterval = setTimeout(streamVideo, 50);
            })
            .catch(error => {
                status.textContent = '视频流中断: ';
                streamInterval = setTimeout(streamVideo, 1000);
            });
        }

        window.addEventListener('load', streamVideo);
    </script>
</html>
)=====";
  server.send(200, "text/html", html);
}

void handleCap() {
  // 尝试获取相机帧
  camera_fb_t *fb = esp_camera_fb_get();
  if (!fb) {
    Serial.println("Camera capture failed");
    server.send(500, "text/plain", "Camera capture failed");
    return;
  }
  
  // 发送JPEG图像
  server.sendHeader("Content-Type", "image/jpeg");
  server.sendHeader("Content-Disposition", "inline; filename=capture.jpg");
  server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
  server.sendHeader("Pragma", "no-cache");
  server.sendHeader("Expires", "0");
  
  // 使用sendContentLength和send_P发送大文件
  server.setContentLength(fb->len);
  server.send(200, "image/jpeg", "");
  server.sendContent((const char*)fb->buf, fb->len);
  
  // 释放帧缓冲区
  esp_camera_fb_return(fb);
}

void handleDo() {
  String doval = server.arg("val");
  if (doval == "1") {
    Serial.println("topic = left");
  } else if (doval == "2") {
    Serial.println("topic = right");
  } 
  server.send(200, "text/plain", "ok");
}

void handleNotFound() {
  String message = "404 - 页面未找到\n\n";
  server.send(404, "text/plain", message);
}








///////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////
//  这个是 Arduino 程序,用于遥控器端  ESP Node 32s 连接一块 st7789   (已验证)
//  连接小车上面的热点、服务器,定时下载图片数据,经过JPG解码,发送到屏幕显示



#include <WiFi.h>
#include <HTTPClient.h>
#include "image.h"

#include <TJpg_Decoder.h>
#include <SPI.h>
#include <TFT_eSPI.h>
//scl=14 sda=13 cs=25 dc=26 rst=27


TFT_eSPI tft = TFT_eSPI();
bool tft_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t* bitmap)
{
  tft.pushImage(x, y, w, h, bitmap);
  return 1;
}

const char* ssid = "ESP32_Server";
const char* password = "12345678";

void downloadImage() {
  HTTPClient http;
  http.begin("http://192.168.4.1:8080/cap");
  int httpResponseCode = http.GET();

  if (httpResponseCode > 0) {
    //Serial.printf("HTTP Response code: %d\n", httpResponseCode);
    WiFiClient* client = http.getStreamPtr();

    int image_length = http.getSize();
    uint8_t* image_byte = (uint8_t*)malloc(image_length);   // 10K
    int image_index = 0;
    
    if (image_byte) {
      while (client->available() || client->connected()) {
        if (image_index >= image_length) {
          break;
        }else{
          if (client->available()) {
            image_byte[image_index++] = client->read();
          }
        }
      }

      if(image_data[0]==0xff && image_data[1]==0xd8){  
        TJpgDec.setJpgScale(1);
        TJpgDec.setSwapBytes(true);
        TJpgDec.setCallback(tft_output);
        TJpgDec.drawJpg(0,40,(const uint8_t*)image_byte, image_index);
        delay(5);
        Serial.printf("Downloaded %d bytes  JPG\n", image_index);
      }else{
        Serial.printf("Downloaded %d bytes  NOJPG\n", image_index);
      }

      free(image_byte);
      image_byte = NULL;
    }
  } else {
    Serial.printf("Failed to download image, error: %s\n", http.errorToString(httpResponseCode).c_str());
  }

  http.end();
}

void setup() {
  Serial.begin(115200);
  delay(300);
  tft.init();  // 初始化 TFT 屏幕
  tft.setRotation(0);           // 调整屏幕方向,根据需要选择合适的值(0、1、2、3)
  tft.fillScreen(TFT_BLACK);    // 设置屏幕背景颜色为黑色, 填充整个屏幕
  tft.setTextSize(3);           // 设置文本大小
  tft.setTextColor(TFT_WHITE);  // 设置文本颜色为白色
  tft.setCursor(40, 0);            // 设置文本光标位置,参数:x 坐标,y 坐标
  tft.println("wait...");  // 打印文本,参数:文本内容

  int arrayLength = sizeof(image_data);
  const uint8_t* arrayPointer = image_data;
  TJpgDec.setJpgScale(1);
  TJpgDec.setSwapBytes(true);
  TJpgDec.setCallback(tft_output);
  TJpgDec.drawJpg(0,40,(const uint8_t*)arrayPointer, arrayLength);
  delay(1000);

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
  }
  Serial.println("Connected to WiFi");

  delay(1000);
  tft.fillScreen(TFT_BLACK);    // 设置屏幕背景颜色为黑色, 填充整个屏幕
  tft.setCursor(40, 0);            // 设置文本光标位置,参数:x 坐标,y 坐标
  tft.println("car...");  // 打印文本,参数:文本内容
}

void loop() {
  delay(100);
  if (WiFi.status() == WL_CONNECTED) {
    downloadImage();
  }
}

  • Python连接PC

前面的所有程序中,ESP32Cam都工作在服务器模式,提供网页服务webserver,用户通过浏览器能访问获取摄像头的图片。但是,从这个程序开始,ESP32Cam将工作在客户端的模式,服务器端改为PC电脑。

我们在电脑中,用Python编写一个Flask服务器,在电脑中开启三条线程:主线程用于主窗口,显示一张图片(视频);子线程server提供一个主页,给客户端的浏览器访问,用于向客户浏览器不断地发送图片;另一子线程updata用于负责接收摄像头ESP32Cam上传的图片数据。

如图所示,在电脑中运行Python程序,主窗口显示一张存储在本地同文件夹中的图片,ESP32Cam通电后,连接家里的WiFi,并访问电脑端的“/updata”网页,主动把摄像头的图片发送到电脑端。摄像头的视频就显示在主窗口中了。这时候如果用手机或浏览器访问“/server”,电脑中的Python服务器就会把摄像头的图片不断地发送到用户的浏览器中了。

优点:我们把ESP32Cam设置为客户端模式,让电脑PC机作为服务器,这样就能利用PC的强大运算和运行能力,接入更多的摄像头、联网设备,这样用户只要访问家里的PC机,就能查看到需要的摄像头图像,就能操控到更多的联网设备了。现在的智能家居正是居于这样的架构基础之上的。

注意,这里是两个程序,一个是运行在电脑端的python程序,一个是运行在ESP32Cam端的micropython程序

# 这个是运行于PC端的FLSK网页服务器,Python程序, 包含个三个线程
# 主线程用于主窗口,显示一张图片;子线程server提供一个主页,用于向客户浏览器发送图片;另一子线程用于负责接收摄像头图片



from flask import Flask, Response, request  # 提供网页服务
import io                                   # 提供内存操作
import tkinter as tk                        # 创建程序主窗口
from PIL import Image, ImageTk
import threading                            # 创建子进程操作
import time

fapp = Flask(__name__)        # 网页服务实例
uapp = Flask(__name__)        # 网页服务实例
image_Data = bytearray()      # 图片的全局共享数据





class create_window:
    def __init__(self):  # 创建主窗口 在主窗口中显示图片
        self.root = tk.Tk()
        self.root.title("视频服务")
        self.root.geometry("400x300")

        self.image_label = tk.Label(self.root)
        self.image_label.pack(pady=20)

        self.getimage()   # 这是一个图片自动更新的操作函数
        
    def run(self): 
        self.root.mainloop() # 让主窗口运行起来

        # 主窗口在这里进入一个无限循环,用于响应用户的所有操作,独占程序的主进程

    def getimage(self):
        global image_Data
        try:      # 读取图片的全局共享数据,更新主窗口的图片显示
            image_stream = io.BytesIO(image_Data)           # 把字节数组转为字节流
            pil_image = Image.open(image_stream)            # 把字节流转为image对象
            tk_image = ImageTk.PhotoImage(pil_image)        # 把image对象转为photo对象
            self.image_label.image = tk_image               # 把photo对象赋值显示出来
            self.image_label.config(image=tk_image)
        
        except Exception as e:
            pass

        self.root.after(100, self.getimage)  # 主程序每隔100毫秒  就运行一次本函数

        # 这是一个巧妙的回调,当图片的全局共享数据发生变化,图片显示也会跟着变化

        # 达到了图片自动更新的功能,甚至可以实现视频播放的效果

        

    

def start_server():          # 在子线程中创建网页服务  在网页中显示图片
    
    def generate_frames():   
        global image_Data
        while True:         # 图片文件比较大,需要利用yield把图片数据分段发送给用户端
            yield b'--frame\r\nContent-Type: image/jpeg\r\n\r\n' + image_Data + b'\r\n'
            time.sleep(0.03)


    @fapp.route('/')    # 这个是网页服务的主页,用户流量网页服务器的网址时,会显示一张图片
    def index():
        return '''<html><head><title>视频流演示</title></head>
     <body><img src="/video_feed" width="600" height="450" /></body></html>'''

    @fapp.route('/video_feed')  # 这个就是嵌套在主页中的图片,图片数据时采用分段发送的
    def video_feed():
        return Response(generate_frames(), mimetype='multipart/x-mixed-replace; boundary=frame')

    fapp.run(host='0.0.0.0', port=5000, threaded=True)

    # 开启flask网页服务器,用户可以浏览访问这个网页服务器,网站是本地计算机内网IP + 端口号

    # 比如 http://192.168.X.X:5000 ,这时候访问的手机和运行服务器的PC必须在同一网络,也就是连接同一个路由器

    # 这个flask网页服务器一经启动 run,会独占一个无限循环,所以必须运行在一个独立的子进程中


def start_updata():          # 在子线程中创建接收摄像头数据的服务

    @uapp.route("/updata", methods=["POST"])
    def updata():
        buf = bytearray()
        buf = request.get_data()  #  直接接收二进制
        global image_Data
        image_Data = bytearray()
        image_Data.extend(buf)
        return "over"     #  返回一个消息给客户端

    uapp.run(host='0.0.0.0', port=5050, threaded=True)


    

def main():                  # 这里是整个程序的主函数
    global image_Data        # 指向全局变量,用于存储图片数据,提供给全局的所有功能块共享
    with open('D:\\daling\\image.jpg', 'rb') as file:
        image_Data = file.read()  #从本地文件中读取一张图片的数据

      # 创建一个网页服务的子线程
    server_thread = threading.Thread(target=start_server, daemon=True)
    server_thread.start()

      # 创建一个网页服务的子线程
    updata_thread = threading.Thread(target=start_updata, daemon=True)
    updata_thread.start()

    wapp = create_window()  # 显示程序主窗口
    wapp.run()              # 把程序的主进程赋予主窗口

if __name__ == '__main__':
    main()
    

    


    

    
#########################################################################
# 这个是运行于ESP32cam的客户端程序 micropython程序
# 用于访问服务器端的 /updata 网页,并不断地主动上传摄像头的图片
    

import usocket
def request(method, url, data=None):
    try:
        proto, dummy, host, path = url.split("/", 3)
    except ValueError:
        proto, dummy, host = url.split("/", 2)
        path = ""

    if ":" in host:
        host, port = host.split(":", 1)
        port = int(port)

    ai = usocket.getaddrinfo(host, port)
    addr = ai[0][4]
    s = usocket.socket()
    s.connect(addr)    #  发送图片数据    
    s.write(b"%s /%s HTTP/1.0\r\n" % (method, path))
    s.write(b"Host: %s\r\n" % host)
    s.write(b"Content-Length: %d\r\n" % len(data))
    s.write(b"\r\n")
    s.write(data)

    while True:  # 接收服务器返回的消息
        t = s.readline()
        if not t or t == b"\r\n":
            break
    t = s.readline()   # 获取真正的返回信息,放在空行后面(从最后一行读取)
    s.close()
    s = None
    return t


import network
def wifiConnect():
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
     
    if not wlan.isconnected():
        print('connecting to network...')
        wlan.connect("TP-LINK_8080","123456789")
        while not wlan.isconnected():
            pass
    print('网络配置:', wlan.ifconfig())


import camera
def init_camera():
    try:   # 配置摄像头参数
        camera.init(0, format=camera.JPEG, framesize=camera.FRAME_HQVGA)
        print("摄像头初始化成功")
        return True
    except:   # 硬件重启
        print("摄像头初始化失败")
        return False


import time
import gc
def main():
    wifiConnect()
    time.sleep(1)
    if not init_camera():
        return
    
    while True:
        # time.sleep(0.01)
        try:
            buf = camera.capture()
            if buf is not None:
                r = request('POST', 'http://192.168.0.110:5050/updata', data = buf)
                print(r)  # 发送摄像头图片,并从服务器获得返回的信息
                del buf
                gc.collect()
                
        except Exception as e:
            print(f"拍照过程中出现错误: {e}")
            break

    
    # 释放摄像头资源
    camera.deinit()
    print("程序结束")

# 程序入口
if __name__ == "__main__":
    main()







Python连接腾讯云

我们在这个程序中,让PC中的Python程序作为服务器,ESP32Cam作为客户端。

我们事先登录腾讯云官网,并申请一个“车牌识别”的服务,从官网中下载一个Python的官方示例程序,并对这个示例程序就行修改,增加Flask服务器的功能。

如图所示,1. 运行ESP32Cam中的主程序main.py时,设备读取事先准备的一张带车牌信息的图片che01.jpg(必要时可以更改为从摄像头获取图片,摄像头的图片需要包含有车牌信息的);2. ESP32Cam把图片信息发送到PC计算机的Flask服务器中;3. Flask通过腾讯的官方程序把图片信息发送到腾讯官网;4. 腾讯官网的服务器对图片中的车牌进行识别,并把识别结果返回给PC计算机;5. PC计算机最后把识别结果发送给ESP32Cam,并在运行窗口中打印出来了。

优点:现在的车牌识别应用非常广泛,我们知道这个运行原理,也能做出车牌识别的项目了,而且这个车牌识别是在腾讯官网的AI大模型的服务器中进行识别的,听说一张图片中如果有好几辆汽车的车牌信息,可以一次性全部识别出来呢。缺点:我们的这个程序是ESP32Cam负责车牌图片信息采集;电脑Flask服务器负责图片上传;腾讯云负责车牌识别,需要三方的共同配合。当然如果你的编程能力较强的话,可以绕过电脑Flask服务器这个中间二道贩子,直接把拍摄到的图片提交到腾讯云,这样也是可以的。

注意,这里有两个程序,一个是运行在电脑端的Python服务器程序,一个是运行在ESP32Cam中的micropython客户端程序。

#  这个是运行在电脑端的 python 程序,   用Flask搭建一个网页服务
#  提供"/updata,用于接收来自ESP32cam 上传的(有车牌信息的)图片数据
#  并提交到腾讯云官网解析,获得识别的车牌结果,并返回给ESP32cam




import os
import hashlib
import hmac
import json
import sys
import time
from datetime import datetime
import binascii
if sys.version_info[0] <= 2:
    from httplib import HTTPSConnection
else:
    from http.client import HTTPSConnection


def sign(key, msg):  # 加密算法
    return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()


def getNumber(ImageData):
    secret_id = "AKIDpS6WhdJjuqXDn8DGeplRsJgplieLp7Az"
    secret_key = "hAOOEqTgmgQ5byTqyud7RpX6So37ysfF"
    token = ""
    service = "ocr"
    host = "ocr.tencentcloudapi.com"
    region = "ap-guangzhou"
    version = "2018-11-19"
    action = "LicensePlateOCR"

    img = binascii.b2a_base64(ImageData)
    imgstr = img.decode('utf-8')
    imgstr = imgstr.rstrip('\n')
    payload = "{\"ImageBase64\":\"data:image/jpeg;base64," + imgstr + "\"}"
    payload = payload.rstrip('\n')

    params = json.loads(payload)
    endpoint = "https://ocr.tencentcloudapi.com"
    algorithm = "TC3-HMAC-SHA256"
    timestamp = int(time.time())
    #date = datetime.datetime.utcfromtimestamp(timestamp).strftime("%Y-%m-%d")
    date = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d")

        # ************* 步骤 1:拼接规范请求串 *************
    http_request_method = "POST"
    canonical_uri = "/"
    canonical_querystring = ""
    ct = "application/json; charset=utf-8"
    canonical_headers = "content-type:%s\nhost:%s\nx-tc-action:%s\n" % (ct, host, action.lower())
    signed_headers = "content-type;host;x-tc-action"
    hashed_request_payload = hashlib.sha256(payload.encode("utf-8")).hexdigest()
    canonical_request = (http_request_method + "\n" +
            canonical_uri + "\n" +
            canonical_querystring + "\n" +
            canonical_headers + "\n" +
            signed_headers + "\n" +
            hashed_request_payload)

        # ************* 步骤 2:拼接待签名字符串 *************
    credential_scope = date + "/" + service + "/" + "tc3_request"
    hashed_canonical_request = hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()
    string_to_sign = (algorithm + "\n" +
            str(timestamp) + "\n" +
            credential_scope + "\n" +
            hashed_canonical_request)

        # ************* 步骤 3:计算签名 *************
    secret_date = sign(("TC3" + secret_key).encode("utf-8"), date)
    secret_service = sign(secret_date, service)
    secret_signing = sign(secret_service, "tc3_request")
    signature = hmac.new(secret_signing, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()

        # ************* 步骤 4:拼接 Authorization *************
    authorization = (algorithm + " " +
            "Credential=" + secret_id + "/" + credential_scope + ", " +
            "SignedHeaders=" + signed_headers + ", " +
            "Signature=" + signature)

        # ************* 步骤 5:构造并发起请求 *************
    headers = {
        "Authorization": authorization,
        "Content-Type": "application/json; charset=utf-8",
        "Host": host,
        "X-TC-Action": action,
        "X-TC-Timestamp": timestamp,
        "X-TC-Version": version
    }
    if region:
        headers["X-TC-Region"] = region
    if token:
        headers["X-TC-Token"] = token

    try:
        req = HTTPSConnection(host)
        req.request("POST", "/", headers=headers, body=payload.encode("utf-8"))
        resp = req.getresponse()
        data = resp.read()
        req.close()

        pos = data.find(b'Number')
        if pos > 0:   
            data = data[pos+9:]
            pos = data.find(b',')
            number_value = data[:pos-1]
            return(number_value)
        else:
            return("None")     
    except Exception as err:
        return("None")


from flask import Flask, Response, request  # 提供网页服务
app = Flask(__name__)        # 网页服务实例

@app.route("/updata", methods=["POST"])
def updata():
    buf = bytearray()
    buf = request.get_data()  #  直接接收二进制
    Number = getNumber(buf)    # 调用前面的提交解析函数
    print(Number)
    return Response(Number)   # 返回给客户端

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8080, threaded=True)








#  这个是简约版的ESP32cam程序,micropython 程序,用于读取设备中的che01.jpg
#  传递给电脑端"/updata",并从电脑端获得识别结果

import network
import time
wlan = network.WLAN(network.STA_IF)
wlan.active(True)    
if not wlan.isconnected():
    print('connecting to network...')
    wlan.connect("TP-LINK_8080","123456789")
    while not wlan.isconnected():
        time.sleep(0.5)
print('网络配置:', wlan.ifconfig())

with open('che01.jpg', 'rb') as file:
    data = file.read()  #从本地文件中读取一张图片的数据

import usocket
ai = usocket.getaddrinfo("192.168.0.112", 8080)
addr = ai[0][4]
s = usocket.socket()
s.connect(addr)    #  发送图片数据    
s.write(b"%s %s HTTP/1.0\r\n" % ("POST", "/updata"))
s.write(b"Host: %s\r\n" % "192.168.0.112")
s.write(b"Content-Length: %d\r\n" % len(data))
s.write(b"\r\n")
s.write(data)

while True:  # 接收服务器返回的消息
    t = s.readline()
    if not t or t == b"\r\n":
        break
print(s.readline())   # 获取真正的返回信息,放在空行后面(从最后一行读取)
s.close()
s = None






//////////////////////////////////////////////////////////////////////////////
//  这个是拍照版的ESP32cam程序,用于带车牌的图片需要由摄像头拍摄
//  传递给电脑端"/updata",并从电脑端获得识别结果,有些代码还没修改




import usocket
def request(method, url, data=None):
    try:
        proto, dummy, host, path = url.split("/", 3)
    except ValueError:
        proto, dummy, host = url.split("/", 2)
        path = ""

    if ":" in host:
        host, port = host.split(":", 1)
        port = int(port)

    ai = usocket.getaddrinfo(host, port)
    addr = ai[0][4]
    s = usocket.socket()
    s.connect(addr)    #  发送图片数据    
    s.write(b"%s /%s HTTP/1.0\r\n" % (method, path))
    s.write(b"Host: %s\r\n" % host)
    s.write(b"Content-Length: %d\r\n" % len(data))
    s.write(b"\r\n")
    s.write(data)

    while True:  # 接收服务器返回的消息
        t = s.readline()
        if not t or t == b"\r\n":
            break
    t = s.readline()   # 获取真正的返回信息,放在空行后面(从最后一行读取)
    s.close()
    s = None
    return t


import network
def wifiConnect():
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
     
    if not wlan.isconnected():
        print('connecting to network...')
        wlan.connect("TP-LINK_8080","123456789")
        while not wlan.isconnected():
            pass
    print('网络配置:', wlan.ifconfig())


import camera
def init_camera():
    try:   # 配置摄像头参数
        camera.init(0, format=camera.JPEG, framesize=camera.FRAME_HQVGA)
        print("摄像头初始化成功")
        return True
    except:   # 硬件重启
        print("摄像头初始化失败")
        return False


import time
import gc
def main():
    wifiConnect()
    time.sleep(1)
    if not init_camera():
        return
    
    while True:
        # time.sleep(0.01)
        try:
            buf = camera.capture()
            if buf is not None:
                r = request('POST', 'http://192.168.0.110:5050/updata', data = buf)
                print(r)  # 发送摄像头图片,并从服务器获得返回的信息
                del buf
                gc.collect()
                
        except Exception as e:
            print(f"拍照过程中出现错误: {e}")
            break

    
    # 释放摄像头资源
    camera.deinit()
    print("程序结束")

# 程序入口
if __name__ == "__main__":
    main()

Python连接巴法云

在这个程序中,我们让ESP32Cam连接到巴法云,除了进行话题消息的传送外,我们还把摄像头拍摄到的图片,上传到巴法云服务器的图存储中,达到共享摄像头图片的功能。

首先我们登录巴法云,在“TCP设备云”中创建一个“door”的话题;在“图存储”中创建一个“tupian”话题,在微信推送中绑定一个接收消息的微信号。

我们给ESP32Cam通电后,程序会先给绑定的微信号发送一条测试消息(微信会“叮”接收到这条消息),然后会像“door”话题发送一条测试消息(我们可以在浏览器中登录巴法云,看到话题中也存在这条新消息了),我们在浏览器中往“door”话题发送一条“cap”的拍照消息,ESP32Cam会开启摄像头拍下一张照片(我们为了简单一点,在这个程序中改为读取一张图片),并把图片发送到巴法云的云存储中,我们用电脑或手机浏览器都能看到这张图片的内容了。

优点:这个时一个真正的巴法云物联网应用案例,涉及到的设备可以多种多样,只要这些设备能够上网连接到巴法云,就能像加入到同一个微信群一样共享消息。每个设备都能往微信群(话题)里面发送消息,微信群里一有新的消息,每个设备也都能第一时间接收到这条新消息(并知道是哪个群成员发送的,要发送给谁)这样相关的设备就能按照消息的指令要求做出响应了。

在这个真正的物联网应用中,所有的共享消息都发送到巴法云的官方服务器中,然后由服务器进行转发,所以这里所有的设备只要能连接互联网intenet,就能发送和接收到群消息了,所以就没有地域空间的限制,可以真正实现超远距离的通讯。

注意,这里有两个程序,都是写入到ESP32Cam中的micropython程序,一个是巴法云驱动库程序,一个是主程序。

# 这个是巴法云驱动 bemfa_driver.py, 提供了ESP32Cam 与巴法云服务器的通讯
# 这个驱动包括  WiFi连接,巴法云服务器连接,话题订阅,发送消息到话题,从话题接收消息
# 和服务器保持心跳重连、发送微信推送、发送图片到图存储 等功能模块


import network
import socket
import urequests
import time
import json

class BemfaDriver:
    def __init__(self):
        self.server = "bemfa.com"
        self.port = 8344
        self.device_id = ""
        self.socket = None
        self.connected = False
        self.wifi_connected = False
        self.last_heartbeat = 0
        self.heartbeat_interval = 30  # 心跳间隔30秒
        self.wechatMsg = ""          # 如果不为空,会推送到微信,可随意修改,修改为自己需要发送的消息
        self.topic = "door"         # 创客云话题
        
    def connect_wifi(self, ssid, password):
        """连接WiFi网络"""
        self.ssid = ssid
        self.password = password
        self.wlan = network.WLAN(network.STA_IF)
        self.wlan.active(True)

        if not self.wlan.isconnected():
            print('connecting to network...')
            self.wlan.connect(ssid, password)
            timeout = 30
            while not self.wlan.isconnected() and timeout > 0:
                time.sleep(1)
                timeout -= 1
                
        if self.wlan.isconnected():
            print('network config:', self.wlan.ifconfig())
            self.wifi_connected = True
            return True
        else:
            print('WiFi connection failed!')
            return False
    
    def connect_bemfa(self, device_id, topic):
        """连接巴法云服务器"""
        self.device_id = device_id
        self.topic = topic
        try:
            if not self.wlan.isconnected():
                print("wifi not connect")
                return False
            
            addr_info = socket.getaddrinfo(self.server, self.port)
            addr = addr_info[0][-1]
            
            self.socket = socket.socket()
            self.socket.connect(addr)
            
            # 发送连接协议
            connect_msg = f"cmd=1&uid={self.device_id}&topic={self.topic}\r\n"
            self.socket.send(connect_msg.encode())
            
            # 等待服务器响应, 此时会收到指令大概为 cmd=1&uid=xxx&topic=mypic&msg=on
            time.sleep(0.5)
            response = self.socket.recv(256)
            print(response)
            if "cmd=1" in response:
                self.connected = True
                self.last_heartbeat = time.time()
                return True
            else:
                print("Connection failed")
                return False
                
        except Exception as e:
            print("Connection error:", e)
            return False
    
    def send_data(self, msg):
        try:   # 发送数据到巴法云
            message = f"cmd=2&uid={self.device_id}&topic={self.topic}&msg={msg}\r\n"
            self.socket.send(message.encode())
            return True
        except Exception as e:
            print("Send error:", e)
            self.connected = False
            return False
    
    def send_wechat(self, msg):
        try:     # 发送数据到微信推送
            API_URL = "http://api.bemfa.com/api/wechat/v1/weget.php?type=1&uid=" + self.device_id + "&device=" + self.topic + "&msg=" + msg
            sended = False
            response = urequests.get(API_URL) # 使用urequests发起GET请求
            if response.status_code == 200:
                sended = True            
            response.close()
            print("send over")
            return sended
        except Exception as e:
            print('HTTP Request Failed:', e)
            return False
        
    def send_poto(self, image_data):
        try:
            # 设置请求头部
            headers = {
                "Content-Type": "image/jpg",
                "Authorization": self.device_id,
                "Authtopic": "tupian",
                "wechatmsg": "",
                "wecommsg": "",
                "picpath": ""
            }
        
            # 发送POST请求上传图片
            response = urequests.post(
                "http://images.bemfa.com/upload/v1/upimages.php",
                data=image_data,
                headers=headers
            )
        
            # 检查响应状态
            if response.status_code in [200, 201]:
                print(f"图片上传成功! 状态码: {response.status_code}")
                print(f"响应内容: {response.text}")
                response.close()
                return True
            else:
                print(f"上传失败! 状态码: {response.status_code}")
                print(f"错误信息: {response.text}")
                response.close()
                return False
            
        except Exception as e:
            print(f"上传过程中发生错误: {e}")
            return False
                
    def maintain_connection(self):
        """维持连接并处理心跳包"""
        try:
            if self.connected:
                # 检查并回复心跳包
                current_time = time.time()
                if current_time - self.last_heartbeat >= self.heartbeat_interval:
                    heartbeat_msg = "ping\r\n"
                    self.socket.send(heartbeat_msg.encode())
            
                    response = self.socket.recv(256)
                    if "cmd=0" in response:
                        self.connected = True
                        self.last_heartbeat = current_time
                    else:
                        print("Connection failed")
                        self.connected = False
                        
                #  获取从服务器返回的消息
                data = self.socket.recv(256)
                if data:
                    data = data.strip()  # 去除前后空格
                    print(data)
                    
                    substr = "&msg=".encode('ascii')
                    if data.find(substr) > 0:
                        msg = data[data.find(substr)+5:len(data)]
                        substr = "cmd=".encode('ascii')
                        if msg.find(substr) > 0:
                            msg = msg[0:msg.find(substr)-2]
                            
                        return msg           
                    else:
                        return None
                else:
                    return None        
            else:
                if self.wlan.isconnected():  # 如果服务器断开,则尝试重新连接
                    self.connect_bemfa(self.device_id, self.topic)
            
        except Exception as e:
            self.connected = False
            print(f"Receive exception: {e}")
            return None

    def disconnect(self):
        """断开连接"""
        if self.wlan.isconnected():
            self.wlan.disconnect()
            
        if self.socket:
            try:
                self.socket.close()
            except:
                pass
        self.connected = False
        print("Disconnected from Bemfa Cloud")




        
#  这个是主程序main.py,微信推送,话题发送,话题接收
#  当接收到“cap”消息时,读取本地图片(可更改为摄像头图片)发送图存储



import time
from machine import Pin, SoftI2C
led = Pin(2, Pin.OUT)  # 板载LED

from bemfa_driver import BemfaDriver
WIFI_SSID = "TP-LINK_8080"   # wifi配置
WIFI_PASSWORD = "123456789"
device_id = "fd9b976796e79f4f219be9ea591b0691"   # 巴法云密钥
topic = "door"                 # 巴法云话题
driver = BemfaDriver()
runing = False

def main():
    time.sleep(0.5)
    if driver.connect_wifi(WIFI_SSID, WIFI_PASSWORD):
        runing=True
        print("WiFi连接成功!")
    else:
        runing=False
        print("WiFi连接失败!")
    
    time.sleep(0.5)
    if driver.connect_bemfa(device_id, "door"):
        runing=True
        print("巴法云连接成功!")
    else:
        runing=False
        print("巴法云连接失败!")
    
    time.sleep(0.5)
    if runing:  # 向door话题发送一个测试消息
        if driver.send_wechat("hello2"): 
            print("微信发送成功!")
        else:
            print("微信发送失败!")
            
        time.sleep(1)
        if driver.send_data("hello"): 
            print("消息发送成功!")
        else:
            print("消息发送失败!")
    
    while runing:
        time.sleep(1)
        try:   # 维持连接心跳 获取返回的消息
            message = driver.maintain_connection()
            if message is not None:
                msg = message.decode('ascii')
                print(msg)
                # 查找特定的消息,拍照 / 开灯 / 关灯
                if msg == "cap":
                    with open("image.jpg", "rb") as f:
                        buffer = f.read()
                    if driver.send_poto(buffer): 
                        print("图片发送成功!")
                    else:
                        print("图片发送失败!")
                elif "msg=on" in message:
                    led.on()
                elif "msg=off" in message:
                    led.off()
   
        except KeyboardInterrupt:
            print("\n程序被用户中断")
            break
        except Exception as e:
            print("主循环错误:", e)
            break
        
    driver.disconnect()
    print("程序已退出")


if __name__ == "__main__":
    main()


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tongyue

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值