本教程中,您将学习如何使用 MQTT(消息队列遥测传输)从 ESP32-CAM 板上发布图像到多个浏览器客户端。这个设置将使您能够创建一个类似于实时视频流的平台,可以被无限数量的用户查看。
在深入学习之前,请确保您已完成以下先决条件教程:
通过在这些基础教程中获得的知识,您将更好地能够跟随本教程。
在 MQTT CAM 代码中,我们的主要重点是发布图像而不订阅其他事件。这个发布操作由一个定时器事件管理,根据指定的间隔发布图像。
首先,让我们创建一个定时器对象。这个定时器将在特定间隔触发 publishImage 函数。
timer = ba.timer(publishImage)
要与 ESP32 相机进行交互,可以这样初始化一个相机对象:
cam = esp32.cam(cfg)
cfg 参数代表一个配置表。重要: 确保它与您特定的 ESP32-CAM 模块的设置匹配。有关详细信息,请参阅 Lua CAM API。
要监视 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)
虽然我们已经涵盖了程序的大部分功能,但还有一些方面尚未涉及:
要可视化由 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>
在 HTML 文件的顶部,导入 MQTT JavaScript 库以启用 MQTT 功能。这在 <script data-fr-src=".......mqtt.min.js"></script> 行中找到。
HTML 主体包含一个 id 为 "image-container" 的 <div> 元素,用于容纳传入的图像,以及一个 id 为 "msg" 的 <p> 元素,用作状态消息的占位符。
在 JavaScript 部分,定义了两个常量 topic 和 broker。这些必须与您的 mqttcam.xlua 文件中的主题和代理配置相对应。
客户端使用 mqtt.connect() 方法向指定的代理发起 MQTT 连接。它使用安全的 WebSocket 端口 8884 进行连接。
在成功连接后,客户端订阅主题。预期在此主题上的任何传入消息都将是二进制 JPEG 图像。消息将被转换为 Blob,并显示为图像元素的源。
frameCounter 变量计算传入帧(或图像)的数量,并将此计数显示为图像下方的文本消息。
通过在 Web 浏览器中打开此 HTML 文件,您将能够实时可视化被发布到指定 MQTT 主题的图像。
ESP32 CAM板因其多功能性和价格实惠而广受认可。然而,它们并非没有挑战。用户在使用ESP32 CAM板时可能会面临的一个重要问题是摄像头读取操作与内置WiFi模块之间的干扰。让我们深入了解一下具体情况:
当ESP32 CAM板在运行时,特别是在摄像头的读取操作期间,会产生噪音。这种噪音会干扰内置WiFi,导致:
为了解决这些问题,请考虑以下解决方案:
总之,虽然ESP32 CAM板是适用于多种应用的优秀工具,但了解其局限性并知道如何规避这些局限性以确保最佳性能是至关重要的。
现目标:
图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,不然会出现只能上传一次,下次选择当前图片不能上传的情况。
上边的上传是选取多个然后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的原生上传,代码稍微有一点不一样。代码可能不规范,匆忙写的。理解理解。
方法有很多种,大家可以参考。互相交流谢谢。
*请认真填写需求信息,我们会在24小时内与您取得联系。