本教程中,您將學習如何使用 MQTT(消息隊列遙測傳輸)從 ESP32-CAM 板上發(fā)布圖像到多個瀏覽器客戶端。這個設(shè)置將使您能夠創(chuàng)建一個類似于實時視頻流的平臺,可以被無限數(shù)量的用戶查看。
在深入學習之前,請確保您已完成以下先決條件教程:
通過在這些基礎(chǔ)教程中獲得的知識,您將更好地能夠跟隨本教程。
在 MQTT CAM 代碼中,我們的主要重點是發(fā)布圖像而不訂閱其他事件。這個發(fā)布操作由一個定時器事件管理,根據(jù)指定的間隔發(fā)布圖像。
首先,讓我們創(chuàng)建一個定時器對象。這個定時器將在特定間隔觸發(fā) publishImage 函數(shù)。
timer = ba.timer(publishImage)
要與 ESP32 相機進行交互,可以這樣初始化一個相機對象:
cam = esp32.cam(cfg)
cfg 參數(shù)代表一個配置表。重要: 確保它與您特定的 ESP32-CAM 模塊的設(shè)置匹配。有關(guān)詳細信息,請參閱 Lua CAM API。
要監(jiān)視 MQTT 連接,使用以下回調(diào)函數(shù):
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 -- 繼續(xù)嘗試
end
上述函數(shù)在成功建立 MQTT 連接時啟動定時器。如果連接斷開,它會取消定時器,但會繼續(xù)嘗試重新連接。
圖像發(fā)布機制的核心是定時器回調(diào)函數(shù) publishImage。這個函數(shù)使用相機對象捕獲圖像,并通過 MQTT 發(fā)布。定時器邏輯支持各種定時器類型。特別是,這個版本作為 Lua 協(xié)程(類似于線程)運行。在這個協(xié)程中,它不斷循環(huán)并休眠,持續(xù)時間由 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
上述函數(shù)通過不在 MQTT 客戶端的發(fā)送隊列中填充兩個圖像來維護流程控制。cam:read 函數(shù)可能耗時,不是在人類時間上,而是在微控制器操作上。因此,我們將從 CAM 對象讀取的任務轉(zhuǎn)移到一個單獨的線程。雖然這一步并不是嚴格必要的,但它增強了在從 CAM 讀取的同時處理多個操作的應用程序的性能。要深入了解線程的復雜性,建議參考Barracuda App Server 關(guān)于線程的文檔。
完整的 MQTT CAM 代碼如下:
local topic = "/xedge32/espcam/USA/92629"
local broker = "broker.hivemq.com"
-- 'FREENOVE ESP32-S3 WROOM' CAM 板的設(shè)置
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 -- 定時器對象;在下面設(shè)置。
-- MQTT 連接/斷開回調(diào)
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 -- 繼續(xù)嘗試
end
-- 創(chuàng)建 MQTT 客戶端
local mqtt=require("mqttc").create(broker,onstatus)
-- 每 300 毫秒激活的定時器協(xié)程函數(shù)
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)
雖然我們已經(jīng)涵蓋了程序的大部分功能,但還有一些方面尚未涉及:
要可視化由 ESP32 相機發(fā)布的圖像,您可以使用一個 HTML 客戶端。以下客戶端將訂閱相機發(fā)布圖像的相同 MQTT 主題。該客戶端純粹在您的 Web 瀏覽器中運行,不需要任何服務器設(shè)置。
完整的 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> 元素,用作狀態(tài)消息的占位符。
在 JavaScript 部分,定義了兩個常量 topic 和 broker。這些必須與您的 mqttcam.xlua 文件中的主題和代理配置相對應。
客戶端使用 mqtt.connect() 方法向指定的代理發(fā)起 MQTT 連接。它使用安全的 WebSocket 端口 8884 進行連接。
在成功連接后,客戶端訂閱主題。預期在此主題上的任何傳入消息都將是二進制 JPEG 圖像。消息將被轉(zhuǎn)換為 Blob,并顯示為圖像元素的源。
frameCounter 變量計算傳入幀(或圖像)的數(shù)量,并將此計數(shù)顯示為圖像下方的文本消息。
通過在 Web 瀏覽器中打開此 HTML 文件,您將能夠?qū)崟r可視化被發(fā)布到指定 MQTT 主題的圖像。
ESP32 CAM板因其多功能性和價格實惠而廣受認可。然而,它們并非沒有挑戰(zhàn)。用戶在使用ESP32 CAM板時可能會面臨的一個重要問題是攝像頭讀取操作與內(nèi)置WiFi模塊之間的干擾。讓我們深入了解一下具體情況:
當ESP32 CAM板在運行時,特別是在攝像頭的讀取操作期間,會產(chǎn)生噪音。這種噪音會干擾內(nèi)置WiFi,導致:
為了解決這些問題,請考慮以下解決方案:
總之,雖然ESP32 CAM板是適用于多種應用的優(yōu)秀工具,但了解其局限性并知道如何規(guī)避這些局限性以確保最佳性能是至關(guān)重要的。
現(xiàn)目標:
圖1 多圖上傳效果
1、html代碼
<tr class=''>
<td width="90" align="right">相關(guān)多圖</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>
<!--結(jié)束-->
</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為數(shù)組的索引 點擊的是第幾個圖片
@mouseover="showzz(1,index)" 鼠標放到上邊 出現(xiàn)遮罩層 垃圾桶
@mouseleave="showzz(0,index)" 鼠標離開 遮罩層消失
@dragstart="drag($event,index)" 以下三個 用于拖拽排序
@drop="drop($event,index)"
@dragover='allowDrop($event)'
draggable="true" 設(shè)置為true 可以拖動
:key="item" 這里的key 要注意不能等于 index,要不然沒有動畫效果
img src的屬性 是 :src="item.picpath" 不能是src={{item.picpath}}
<div class='zzz none' :class="{'nonone':item.shs==1}"> 設(shè)置遮罩層 shs=1的時候顯示
上傳的選擇框設(shè)置為display:none隱藏
transition-group用法:
<transition-group name="list"> 實現(xiàn)拖拽的動畫效果 后邊的name可以隨意寫 ,但是要和css的.list-move {transition: transform 0.3s;} 【上邊自定義的name,我這里是list】-move 設(shè)置該css 動畫的時間
2、js代碼
new Vue({
el: '#app',
data(){
tagslist:[
'網(wǎng)站開發(fā)',//存放的標簽
'網(wǎng)站建設(shè)'
],
tagsdt:"", //綁定的標簽文本框
tagindex:"",//刪除標簽的序號(索引)
listData: [
/*
{'picpath':'/public/upload/image/20211107/1.jpg',shs:0}
shs 顯示遮罩層 ,垃圾桶刪除標志,0 不顯示 1顯示
*/
],
file:"file", //用于切換 file text 實現(xiàn)同一個圖片可以連續(xù)上傳
tis:'', //提示內(nèi)容
showzzc:0, //彈出框的顯示,隱藏 。0 隱藏 1顯示
showts:0, //1 彈出提示操作框 2 彈出提示確認框
lisindex:"", //記錄圖片的索引
datameth:"" //根據(jù)這里的參數(shù)操作不同的方法
}
},
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,不然會出現(xiàn)只能上傳一次,下次選擇當前圖片不能上傳的情況。
上邊的上傳是選取多個然后for循環(huán)逐個上傳的,也可以file使用數(shù)組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']就是數(shù)組,需要我們再次for循環(huán),得到單個的圖片信息,返回以后的信息因為是數(shù)組,push只能一次追加一個,就只能再次循環(huán),感覺很麻煩還不如開始就循環(huán),一個一個的上傳。
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,可以獲得輸入的內(nèi)容,這里需要判斷是否為空,如果不為空再push進數(shù)組: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);
再次強調(diào)上傳圖片,要驗證圖片的安全性,防止圖片木馬!
tml+js+php異步上傳圖片,不刷新頁面,圖片用的是選擇了就自動上傳,好處就是不用刷新頁面。
前端就是 input標簽,沒有form表單,用form表單的稍微有點差異,大家可以去百度
我是需要服務端返回我圖片存儲的地址,每次只上傳一張,不是php的原生上傳,代碼稍微有一點不一樣。代碼可能不規(guī)范,匆忙寫的。理解理解。
方法有很多種,大家可以參考。互相交流謝謝。
*請認真填寫需求信息,我們會在24小時內(nèi)與您取得聯(lián)系。