整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

通过MQTT将ESP32-CAM图像流传输到多个浏览器

本教程中,您将学习如何使用 MQTT(消息队列遥测传输)从 ESP32-CAM 板上发布图像到多个浏览器客户端。这个设置将使您能够创建一个类似于实时视频流的平台,可以被无限数量的用户查看。

先决条件

在深入学习之前,请确保您已完成以下先决条件教程:

  • 您的第一个 Xedge32 项目:本教程涵盖了 ESP32 上运行 Xedge32 的基本设置和配置说明。
  • 您的第一个 MQTT Lua 程序:本教程介绍了 MQTT 的基础知识以及如何编写一个简单的 Lua 程序来与 MQTT 进行交互。

通过在这些基础教程中获得的知识,您将更好地能够跟随本教程。

通过 MQTT 发布 ESP32-CAM 图像

在 MQTT CAM 代码中,我们的主要重点是发布图像而不订阅其他事件。这个发布操作由一个定时器事件管理,根据指定的间隔发布图像。

设置定时器

首先,让我们创建一个定时器对象。这个定时器将在特定间隔触发 publishImage 函数。

timer = ba.timer(publishImage)

要与 ESP32 相机进行交互,可以这样初始化一个相机对象:

cam = esp32.cam(cfg)

cfg 参数代表一个配置表。重要: 确保它与您特定的 ESP32-CAM 模块的设置匹配。有关详细信息,请参阅 Lua CAM API。

处理 MQTT 连接状态

要监视 MQTT 连接,使用以下回调函数:

local function onstatus(type, code, status)
   if "mqtt" == type and "connect" == code and 0 == status.reasoncode then
      timer:set(300, false, true)  -- 每 300 毫秒激活定时器
      return true  -- 接受连接
   end
   timer:cancel()
   return true  -- 继续尝试
end

上述函数在成功建立 MQTT 连接时启动定时器。如果连接断开,它会取消定时器,但会继续尝试重新连接。

通过定时器回调发布图像

图像发布机制的核心是定时器回调函数 publishImage。这个函数使用相机对象捕获图像,并通过 MQTT 发布。定时器逻辑支持各种定时器类型。特别是,这个版本作为 Lua 协程(类似于线程)运行。在这个协程中,它不断循环并休眠,持续时间由 coroutine.yield(true) 定义。

function publishImage()
   local busy = false
   while true do
      if mqtt:status() < 2 and not busy then
         busy = true -- 线程忙碌
         ba.thread.run(function()
            local image = cam:read()
            mqtt:publish(topic, image)
            busy = false -- 不再运行
         end)
      end
      coroutine.yield(true)  -- 休眠
   end
end

上述函数通过不在 MQTT 客户端的发送队列中填充两个图像来维护流程控制。cam:read 函数可能耗时,不是在人类时间上,而是在微控制器操作上。因此,我们将从 CAM 对象读取的任务转移到一个单独的线程。虽然这一步并不是严格必要的,但它增强了在从 CAM 读取的同时处理多个操作的应用程序的性能。要深入了解线程的复杂性,建议参考Barracuda App Server 关于线程的文档。

完整的 MQTT CAM 代码如下:

local topic = "/xedge32/espcam/USA/92629"
local broker = "broker.hivemq.com"

-- 'FREENOVE ESP32-S3 WROOM' CAM 板的设置
local cfg={
   d0=11, d1=9, d2=8, d3=10, d4=12, d5=18, d6=17, d7=16,
   xclk=15, pclk=13, vsync=6, href=7, sda=4, scl=5, pwdn=-1,
   reset=-1, freq="20000000", frame="HD"
}

-- 打开相机
local cam,err=esp32.cam(cfg)
assert(cam, err) -- 如果 'cfg' 不正确,会抛出错误

local timer -- 定时器对象;在下面设置。

-- MQTT 连接/断开回调
local function onstatus(type,code,status)
   -- 如果连接到代理成功
   if "mqtt" == type and "connect" == code and 0 == status.reasoncode then
      timer:set(300,false,true) -- 每 300 毫秒激活定时器
      trace"Connected"
      return true -- 接受连接
   end
   timer:cancel()
   trace("Disconnect or connect failed",type,code)
   return true -- 继续尝试
end

-- 创建 MQTT 客户端
local mqtt=require("mqttc").create(broker,onstatus)

-- 每 300 毫秒激活的定时器协程函数
function publishImage()
   local busy=false
   while true do
      --trace(mqtt:status(), busy)
      -- 流程控制:如果排队的 MQTT 消息少于 2 条
      if mqtt:status() < 2 and not busy then
         busy=true
         ba.thread.run(function()
            local image,err=cam:read()
            if image then
               mqtt:publish(topic,image)
            else
               trace("cam:read()",err)
            end
            busy=false
         end)
      end
      coroutine.yield(true) -- 休眠
   end
end
timer = ba.timer(publishImage)

虽然我们已经涵盖了程序的大部分功能,但还有一些方面尚未涉及:

  • 主题和代理配置:local topic = "/xedge32/espcam/USA/92629":设置图像将被发布到的 MQTT 主题。将此主题更改为您的地址。local broker = "broker.hivemq.com":指定 MQTT 代理的地址。此示例中使用公共的 HiveMQ 代理。
  • ESP32 相机配置(cfg): 这个块设置了 ESP32 CAM 板的特定引脚配置和设置。用适合您硬件的设置替换这些设置。
  • 创建 MQTT 客户端: MQTT 客户端是通过 require("mqttc").create(broker, onstatus) 函数创建的,传入代理地址和 onstatus 回调。
  • 为 publishImage 创建定时器对象: 通过调用 ba.timer 并传入 publishImage 回调来创建定时器,它将在固定间隔内激活。这是不断捕获和发布图像的机制。

使用 JavaScript 驱动的 HTML 客户端订阅 CAM 图像

要可视化由 ESP32 相机发布的图像,您可以使用一个 HTML 客户端。以下客户端将订阅相机发布图像的相同 MQTT 主题。该客户端纯粹在您的 Web 浏览器中运行,不需要任何服务器设置。

完整的 HTML 客户端代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <title>Cam Images Over MQTT</title>
  <script data-fr-src="https://cdnjs.cloudflare.com/ajax/libs/mqtt/5.0.0-beta.3/mqtt.min.js"></script>
<script>

const topic="/xedge32/espcam/USA/92629";
const broker="broker.hivemq.com";

window.addEventListener("load", (event) => {
    let img = document.getElementById("image");
    let msg = document.getElementById("msg");
    let frameCounter=0;
    const options = {
        clean: true,
        connectTimeout: 4000,
        port: 8884 // 安全的 WebSocket 端口
    };
    const client = mqtt.connect("mqtts://"+broker+"/mqtt",options);

    client.on('connect', function () {
        msg.textContent="Connected; Waiting for images...";
        client.subscribe(topic);
    });

    client.on("message", (topic, message) => {
        const blob = new Blob([message], { type: 'image/jpeg' });
        img.src = URL.createObjectURL(blob);
        frameCounter++;
        msg.textContent = `Frames: ${frameCounter}`;
    });
});
</script>
</head>
<body>
    <h2>Cam Images Over MQTT</h2>
   <div id="image-container">
    <img id="image"/>
  </div>
    <p id="msg">Connecting...</p>
</body>
</html>

MQTT JavaScript 客户端

在 HTML 文件的顶部,导入 MQTT JavaScript 库以启用 MQTT 功能。这在 <script data-fr-src=".......mqtt.min.js"></script> 行中找到。

页面布局

HTML 主体包含一个 id 为 "image-container" 的 <div> 元素,用于容纳传入的图像,以及一个 id 为 "msg" 的 <p> 元素,用作状态消息的占位符。

MQTT 配置

在 JavaScript 部分,定义了两个常量 topic 和 broker。这些必须与您的 mqttcam.xlua 文件中的主题和代理配置相对应。

连接到 MQTT 代理

客户端使用 mqtt.connect() 方法向指定的代理发起 MQTT 连接。它使用安全的 WebSocket 端口 8884 进行连接。

处理传入消息

在成功连接后,客户端订阅主题。预期在此主题上的任何传入消息都将是二进制 JPEG 图像。消息将被转换为 Blob,并显示为图像元素的源。

帧计数器

frameCounter 变量计算传入帧(或图像)的数量,并将此计数显示为图像下方的文本消息。

通过在 Web 浏览器中打开此 HTML 文件,您将能够实时可视化被发布到指定 MQTT 主题的图像。

准备代码

步骤 1:准备 Lua 脚本如下

  1. 如在教程您的第一个 Xedge32 项目中所述,当 Xedge32 驱动的 ESP32 运行时,使用浏览器导航到 Xedge IDE。
  2. 创建一个名为 "cam" 的新 Xedge 应用程序,并启用 LSP。
  3. 展开左侧面板树视图中可见的 cam 应用程序。
  4. 右键单击 cam 应用程序,然后在上下文菜单中单击 新建文件。
  5. 输入 camtest.lsp,然后点击 Enter。
  6. 打开 GitHub 上的 camtest.lsp 文件,然后单击 复制原始文件 按钮。
  7. 转到 Xedge IDE 浏览器窗口,将内容粘贴到 camtest.lsp 文件中。
  8. 重要: 调整 camtest.lsp 中的 cfg 设置,以匹配您特定的 ESP32 CAM 板设置。有关详细信息,请参阅 Lua CAM API。
  9. 点击保存,然后点击打开以测试您的相机设置。确保在继续之前看到 LSP 脚本生成的图像。
  10. 右键单击 cam 应用程序,然后在上下文菜单中单击 新建文件。
  11. 输入 mqttcam.xlua,然后点击 Enter。
  12. 打开 GitHub 上的 mqttcam.xlua 文件,然后单击 复制原始文件 按钮。
  13. 转到 Xedge IDE 浏览器窗口,将内容粘贴到 mqttcam.xlua 文件中。
  14. 使用 Xedge 编辑器,从 camtest.lsp 复制 cfg 设置,并将其替换为步骤 9 中测试的设置。
  15. 点击 保存并运行 按钮以保存并启动示例。

步骤 2:准备 HTML/JS 文件如下

  1. 下载 mqttcam.html,在任何编辑器中打开文件,并确保 HTML 文件中的主题与您在 Lua 脚本中设置的主题相匹配。
  2. 保存 mqttcam.html 文件。
  3. 打开 mqttcam.html:双击 mqttcam.html 文件,或将其拖放到浏览器中。注意:此文件设计为直接从文件系统打开。您不需要 Web 服务器来托管此文件。
  4. 观察输出:网页将显示由 ESP32 CAM 发布的图像。接收到的帧数将显示在图像下方。
    SELECT * FROM potential_issues;

ESP32 CAM板的潜在问题及解决方案

ESP32 CAM板因其多功能性和价格实惠而广受认可。然而,它们并非没有挑战。用户在使用ESP32 CAM板时可能会面临的一个重要问题是摄像头读取操作与内置WiFi模块之间的干扰。让我们深入了解一下具体情况:

问题:干扰和WiFi信号衰减

当ESP32 CAM板在运行时,特别是在摄像头的读取操作期间,会产生噪音。这种噪音会干扰内置WiFi,导致:

  • 信号范围减小: WiFi有效传输和接收数据的距离明显减少。
  • 吞吐量降低: WiFi网络传输数据的速度和效率会受到相当大的影响。

解决方案

为了解决这些问题,请考虑以下解决方案:

  1. 使用带有外置天线的CAM板: 几种ESP32 CAM板配备了或支持使用外置天线。通过使用这样的板并连接外置天线,您可以增强WiFi信号强度和范围,减轻摄像头操作造成的一些干扰。
  2. 集成W5500以太网芯片: 如果您的应用需要稳定和强大的数据传输,请考虑集成W5500以太网芯片。通过使用以太网而不是WiFi,您可以有效地避开与ESP32 CAM板上的WiFi相关的干扰问题。Xedge32配备了集成的以太网驱动程序。当与支持它的硬件配对使用时,比如W5500芯片,它可以实现平稳且无干扰的数据传输,确保您的应用保持稳定和高效。

总之,虽然ESP32 CAM板是适用于多种应用的优秀工具,但了解其局限性并知道如何规避这些局限性以确保最佳性能是至关重要的。

参考资料

  • Lua MQTT API
  • Lua timer API
  • Lua CAM API```

现目标:

  1. 可以一次选取多个图片上传;
  2. 上传的图片可以拖动排序;
  3. 点击图片提示删除弹窗,确认后删除该图片;
  4. 点击上传按钮图标,即可浏览图片,隐藏选择文件的文本框;
  5. 选择图片以后自动上传;
  6. 拖拽排序的时候使用transition-group,实现动画效果;

图1 多图上传效果


1、html代码

<tr class=''>
<td width="90" align="right">相关多图</td>
<td >
<div class='yllist yllist_x_duotu'>
<dl>
<!--存放上传的图片-->
<transition-group name="list">
<dd   v-for="(item,index) in listData " draggable="true" :key="item" 
	@click="del(index)" 
	@mouseover="showzz(1,index)"  
	@mouseleave="showzz(0,index)" 
	@dragstart="drag($event,index)" 
	@drop="drop($event,index)"
	@dragover='allowDrop($event)'
	>
    <img :src="item.picpath">
    <div class='zzz none'  :class="{'nonone':item.shs==1}">
    <div class='zzimg '><i class="fa fa-trash-o" aria-hidden="true"></i></div>
	</div>
</dd>
<!--结束-->
</transition-group>
<dd  @click="upbtn"  class='btnclass'><i class="fa fa-camera-retro" aria-hidden="true"></i>
<input type='file' id='multiple' accept="image/*"   multiple="multiple" style='display:none' @change="autoup"  name="ss">
</dd>
</dl>
<div class='clear'></div>
<div>
<span class='itemms'>说明:可以拖动改变顺序</span>
</div>
</div>
</td>
</tr>

说明:

@click="del(index)" 点击删除图片 index为数组的索引 点击的是第几个图片

@mouseover="showzz(1,index)" 鼠标放到上边 出现遮罩层 垃圾桶

@mouseleave="showzz(0,index)" 鼠标离开 遮罩层消失

@dragstart="drag($event,index)" 以下三个 用于拖拽排序

@drop="drop($event,index)"

@dragover='allowDrop($event)'

draggable="true" 设置为true 可以拖动

:key="item" 这里的key 要注意不能等于 index,要不然没有动画效果

img src的属性 是 :src="item.picpath" 不能是src={{item.picpath}}

<div class='zzz none' :class="{'nonone':item.shs==1}"> 设置遮罩层 shs=1的时候显示

上传的选择框设置为display:none隐藏

transition-group用法:

<transition-group name="list"> 实现拖拽的动画效果 后边的name可以随意写 ,但是要和css的.list-move {transition: transform 0.3s;} 【上边自定义的name,我这里是list】-move 设置该css 动画的时间

2、js代码

new Vue({
    el: '#app',
    data(){
		  tagslist:[
		    '网站开发',//存放的标签
			  '网站建设'
		  ],
		  tagsdt:"",  //绑定的标签文本框
		  tagindex:"",//删除标签的序号(索引)
		  listData: [
				  /*
			      {'picpath':'/public/upload/image/20211107/1.jpg',shs:0}
			      
				  shs   显示遮罩层 ,垃圾桶删除标志,0  不显示   1显示
				  */
		  ],
      file:"file",      //用于切换 file  text  实现同一个图片可以连续上传
		  tis:'',           //提示内容
		  showzzc:0,         //弹出框的显示,隐藏 。0 隐藏  1显示
		  showts:0,          //1 弹出提示操作框  2 弹出提示确认框 
		  lisindex:"",      //记录图片的索引
		  datameth:""       //根据这里的参数操作不同的方法
		 }
    },
    methods:{
	  tags:function(){
		  if(this.tagsdt){
		      this.tagslist.push(this.tagsdt); 
		  }
		  this.tagsdt="";
	  },
	  deltag:function(f){
		  this.showzzc=1;
		  this.showts=1;
		  this.tagindex=f;
		  this.datameth='tag';
	  },
	  hidetc:function(){
		  this.showzzc=0;
	  },
 	   del:function(key){
			this.showzzc=1;
			this.showts=1;
			this.lisindex=key;
			this.datameth="delpic";
			//this.listData.splice(key, 1);
		},
	  isdelc:function(){
			if(this.datameth=="delpic"){
		        this.listData.splice(this.lisindex, 1); 
			}
			if(this.datameth=="tag"){
		        this.tagslist.splice(this.tagindex, 1); 
			}
			this.showzzc=0;
	  }, 
  	showzz:function(meth,key){
			//console.log(this.listData[key].shs);
			if(!this.listData[key].shs){
			    this.$set(this.listData[key],'shs',0);
			}
			this.listData[key].shs=meth;
	  }, 
	  upbtn:function(){
		  document.getElementById("multiple").click();
	  },
	  autoup:function(){
		  let that=this;
      that.file="text";  //切换text  file
		  let ups=document.getElementById( "multiple");
		  let formdata = new FormData();
		  if(ups.files[0]){
			  if(ups.files.length>4){
				  this.showzzc=1;
				  this.showts=2;
				  this.tis="一次最多可以选择4张图片上传!";
          that.file="file";
				  return false;
			  }
        for(m=0;m<=ups.files.length-1;m++){
			      formdata.append("file", ups.files[m]);
			      axios.post("/api/uppic", formdata)
					  .then(function (response) {
					    if(response.data.error=='0000'){
							    that.listData.push(response.data.pic);
                  that.file="file";//重新切换为file
							  //console.log(JSON.stringify(that.listData));
						  }else{
							  that.showzzc=1;
							  that.showts=2;
							  that.tis=response.data.msg;
                that.file="file";
							  return false;
						  }
					  })
					  .catch(function (error) {
						  console.log(error);
					  });
          }
			    console.log(ups.outerHTML);
		  }  
    }
})

注意:上传图片以后一定要that.file="file",切换回file,不然会出现只能上传一次,下次选择当前图片不能上传的情况。

  1. 去掉js中的for循环和html的multiple="multiple"选择多个图片,就是单图的上传;
  2. 因为删除弹出提示的地方有2个,实际可能更多,在点击确认的时候不知道操作哪个删除,所以点删除图片或者是点删除信息标签的时候,向datameth赋不同的值,这样我们点击弹窗的确认,读取datameth的值,使用if判断,就可以知道要删除图片还是删除标签;
  3. 上传图片,删除图片用到的弹窗,在前边十七章有说明;

上边的上传是选取多个然后for循环逐个上传的,也可以file使用数组file[]批量提交,如下:

for(m=0;m<=ups.files.length-1;m++){
			      formdata.append("file[]", ups.files[m]);
}
axios.post("/api/uppic", formdata)

但是这样做的话,后台使用

foreach($_FILES as $k=>$v){
}

得到的$v['name']就是数组,需要我们再次for循环,得到单个的图片信息,返回以后的信息因为是数组,push只能一次追加一个,就只能再次循环,感觉很麻烦还不如开始就循环,一个一个的上传。

3、信息标签html

<tr class=''>
<td width="90" align="right">信息标签</td>
<td>
<div class="layui-input-inline tagslist" >
    <span class='tagspan' v-for="(tag,key) in tagslist"   :key="key" @click="deltag(key)">{{tag}}</span>
</div>
<input type="text"  class='inpMain' id='tags' style='width:150px;'  @blur="tags" v-model="tagsdt" /> <span class='itemms'>点击标签可以删除</span>
 <span class='itemms'></span>
</td>
</tr>

输入文本框绑定tagsdt,当我们鼠标离开该文本框的时候,通过blur使用tags方法读取绑定的tagsdt,可以获得输入的内容,这里需要判断是否为空,如果不为空再push进数组:this.tagslist.push(this.tagsdt);

4、php后端代码

foreach($_FILES as $k=>$v){
       $v['name'],$v['size'],$v['tem_name']
  就是图片的基本信息,使用move_uploaded_file移动到指定文件夹
  			$imags['picpath']=$path;
				$imags['shs']=0;
}
exit(json_encode(array('error'=>'0000','pic'=>$imags),JSON_UNESCAPED_UNICODE));

move_uploaded_file用法:

当前文件:$v["tmp_name"],

目标文件:ROOT_PATH.$images_dir.$newname

move_uploaded_file($v["tmp_name"], ROOT_PATH.$images_dir.$newname);

再次强调上传图片,要验证图片的安全性,防止图片木马!

tml+js+php异步上传图片,不刷新页面,图片用的是选择了就自动上传,好处就是不用刷新页面。

前端就是 input标签,没有form表单,用form表单的稍微有点差异,大家可以去百度

我是需要服务端返回我图片存储的地址,每次只上传一张,不是php的原生上传,代码稍微有一点不一样。代码可能不规范,匆忙写的。理解理解。

方法有很多种,大家可以参考。互相交流谢谢。