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()

2万+

被折叠的 条评论
为什么被折叠?



