Procházet zdrojové kódy

!1254 MQTTX在线测试客户端(新增物联网模块)
Merge pull request !1254 from X5ZJ/next

zuohuaijun před 2 roky
rodič
revize
0c8820d611

+ 1 - 0
Admin.NET/Admin.NET.Core/Job/EnumToDictJob.cs

@@ -59,6 +59,7 @@ public class EnumToDictJob : IJob
                                 dictData.Value = enumData.Value.ToString();
                                 dictData.Code = enumData.Name;
                                 dictData.OrderNo = enumData.Value + 10;
+								dictData.Name = enumData.Describe;
                                 dictData.Name = enumData.Describe;
                                 uSysDictData.Add(dictData);
                             }

+ 3 - 0
Web/package.json

@@ -32,10 +32,13 @@
 		"echarts-gl": "^2.0.9",
 		"echarts-wordcloud": "^2.1.0",
 		"element-plus": "^2.7.6",
+		"ezuikit": "^1.0.0",
+		"ezuikit-js": "^8.0.5",		
 		"js-cookie": "^3.0.5",
 		"js-table2excel": "^1.1.2",
 		"jsplumb": "^2.15.6",
 		"lodash-es": "^4.17.21",
+		"mqtt": "4.3.7",
 		"mitt": "^3.0.1",
 		"monaco-editor": "^0.50.0",
 		"nprogress": "^0.2.0",

+ 316 - 0
Web/src/views/elive/index.vue

@@ -0,0 +1,316 @@
+<template>
+  <div class="sys-video-container">
+    <el-container>
+      <el-header>视频监控(萤石云云直播)</el-header>
+      <el-container>
+        <el-aside width="200px">
+			  <el-tree :data="data" :props="defaultProps" @node-click="handleNodeClick" />
+		</el-aside>
+		
+        <el-main>
+			<div class="updateToken" props="ezviz_video">
+				<ul>
+					<li>
+						<el-span>密匙串:</el-span><el-input placeholder="密匙" show-word-limit type="text" id="txt_token" title="每周更新(开放平台,云直播,轻应用,代码示例)" v-model="ezviz_video.ezvizToken" @keyup.enter="update_Token" class="token_input" />
+					</li>
+					<li>
+						<el-span>视频流:</el-span><el-input placeholder="萤石云视频流地址" show-word-limit type="text" id="txt_url" title="密匙对应的视频流地址(高清后缀.h.live)" v-model="ezviz_video.ezvizUrl" @keyup.enter="update_Token" class="token_input" />
+					</li>
+				</ul>
+			</div>	
+			
+			<div class="video">
+				<div class="video-item">
+					<div class="item">
+						<div class="home" ref="viewtoolOne">
+							<div id="video-container">等待加载...</div>
+						</div>
+					</div>
+				</div>
+			</div>			
+		</el-main>
+      </el-container>
+    </el-container>
+  </div>
+</template>
+
+<!-- 
+库:https://github.com/Ezviz-OpenBiz/EZUIKit-JavaScript-npm
+安装:npm install ezuikit-js 或 pnpm add ezuikit-js
+-->
+<script lang="ts" setup name="video">
+import { reactive, ref,onMounted,nextTick,beforeDestroy } from "vue";
+import EZUIKit from 'ezuikit-js';//页面引用
+//import { ElNotification } from 'element-plus';
+//import { Search,ChatDotSquare,TopRight,Star,Operation,Setting,Connection,Discount,Open,Delete,Position,View,CopyDocument,DocumentChecked,VideoCamera} from '@element-plus/icons-vue';
+import mittBus from '/@/utils/mitt';//事件总线mitt 解决打包后错误Uncaught (in promise) ReferenceError: Cannot access 'oe' before initialization
+
+
+let ezvizPlayOne = ref(null);
+let ezvizPlayTwo = ref(null);
+let ezvizPlayThree = ref(null);
+let ezvizPlayFour = ref(null);
+let viewtoolOne = ref();
+let viewtoolTwo = ref();
+let viewtoolThree = ref();
+let viewtoolFour = ref();
+
+interface Tree {
+  label: string
+  children?: Tree[]
+}
+
+const defaultProps = {
+  children: 'children',
+  label: 'label',
+}
+
+const handleNodeClick = (data: Tree) => {
+  console.log(data)
+}
+
+
+
+// 更新token
+function update_Token(e){
+	//ezviz_video.ezvizToken=e.target.value;
+	console.log(e.target.value);
+	autoVideoOne("video-container");
+	
+}
+
+onMounted(async()=>{
+	autoVideoOne("video-container");
+	//console.log('https://open.ys7.com/console/ezuikit/template/detail.html?themeId=pcLive&editing=false');
+});
+
+//测试 ezopen://open.ys7.com/G39444019/1.live 和 at.3bvmj4ycamlgdwgw1ig1jruma0wpohl6-48zifyb39c-13t5am6-yukyi86mz
+//备用 ezopen://open.ys7.com/AA2615287/1.live 和 ra.5k88qgc34vgr9yva7rlub985blo9ph7k-92q0bl2r4r-0aygaog-5cofhebpm
+const ezviz_video = reactive({
+	ezvizToken: "ra.5k88qgc34vgr9yva7rlub985blo9ph7k-92q0bl2r4r-0aygaog-5cofhebpm",//需要修改每周(开放平台,云直播,轻应用,代码示例中找)演示设备
+	ezvizUrl: "ezopen://open.ys7.com/AA2615287/1.live",//高清直播拼接字符串   cosnt url = `ezopen://${item.identifyingCode}@open.ys7.com/${item.imei}/${item.channelNo}.hd.live`
+	//回放地址ezopen://open.ys7.com/AA2615287/1.rec
+});
+
+// beforeDestroy(()=>{
+// 	ezvizPlayOne.value  && ezvizPlayOne.value.stop() //销毁并停止直播视频
+// 	console.log('beforeDestroy');
+// });
+
+//监控1,参数https://blog.csdn.net/weixin_53791978/article/details/126489296
+function autoVideoOne(params) {
+    // 获取父节点的宽高
+	let divW = viewtoolOne.value.clientWidth
+	let divH = viewtoolOne.value.clientHeight
+	if (ezvizPlayOne.value != null) {
+		return
+	}
+	
+	//获取萤石token
+	ezvizPlayOne.value = new EZUIKit.EZUIKitPlayer({
+		autoplay: true,  // 默认播放
+		//视频播放包括元素
+		id: "video-container",//DIV容器
+		//萤石token,https://open.ys7.com/console/ezuikit/template/detail.html?themeId=pcLive&editing=false中查询实例代码
+		accessToken: ezviz_video.ezvizToken,//"ra.bl9n4hmb3c7w4fk6bbuumtmdcbbo66w0-3k7nal0q6y-0lp00m5-fi61isesz",
+		// ezopen://open.ys7.com/${设备序列号}/{通道号}.live
+		url: ezviz_video.ezvizUrl,//"ezopen://open.ys7.com/AA2615287/1.live", // 播放地址
+		template: "standard", // pcLive,simple - 极简版;standard-标准版;security - 安防版(预览回放);voice-语音版;theme-可配置主题;
+		useHardDev: true, // 开启高性能模式 依赖需高于7.7.x 截止到2023.11.7 建议保持最新版本为7.7.6
+		// header: ['capturePicture', 'zoom'], // 如果templete参数不为simple,该字段将被覆盖
+		//plugin: ['talk'], // 加载插件,talk-对讲
+		// 视频下方底部控件
+		//footer: ["talk", "broadcast", "hd", "fullScreen"], // 如果template参数不为simple,该字段将被覆盖
+		footer: ['talk', 'hd', 'fullScreen'], // 如果template参数不为simple,该字段将被覆盖
+		//audio: 0, // 是否默认开启声音 0 - 关闭 1 - 开启
+		// openSoundCallBack: data => console.log("开启声音回调", data),
+		// closeSoundCallBack: data => console.log("关闭声音回调", data),
+		// startSaveCallBack: data => console.log("开始录像回调", data),
+		// stopSaveCallBack: data => console.log("录像回调", data),
+		// capturePictureCallBack: data => console.log("截图成功回调", data),
+		// fullScreenCallBack: data => console.log("全屏回调", data),
+		// getOSDTimeCallBack: data => console.log("获取OSDTime回调", data),
+		width: divW,
+		height: divH,
+		handleError: (err) => {
+			if (err.type === "handleRunTimeInfoError" && err.data.nErrorCode === 5) {
+			  console.log("加密设备密码错误");
+			}
+		},		
+	});
+}
+
+
+
+const data: Tree[] = [
+  {
+    label: '节点A',
+    children: [
+      {
+        label: '菜单A-1',
+        children: [
+          {
+            label: '菜单A-1-1',
+          },
+        ],
+      },
+    ],
+  },
+  {
+    label: '节点B',
+    children: [
+      {
+        label: '菜单B-1',
+        children: [
+          {
+            label: '菜单B-1-1',
+          },
+        ],
+      },
+      {
+        label: '菜单B-2',
+        children: [
+          {
+            label: '菜单B-2-1',
+          },
+        ],
+      },
+    ],
+  },
+  {
+    label: '节点C',
+    children: [
+      {
+        label: '菜单C-1',
+        children: [
+          {
+            label: '菜单C-1-1',
+          },
+        ],
+      },
+      {
+        label: '菜单C-2',
+        children: [
+          {
+            label: '菜单C-2-1',
+          },
+        ],
+      },
+    ],
+  },
+]
+</script>
+
+<style lang="scss" scoped>
+.sys-video-container{
+overflow:hidden;
+height:100vh;
+
+}
+.common-layout{background-color: #ECF5FF;}
+
+.el-header{text-align: center;height: 45px;line-height: 45px;font-size: 22px;background-color: #eee;padding: 5px auto;}
+.el-aside{text-align: center;padding: 4px auto;overflow: hidden;}
+.el-aside .el-form{text-align: center;padding: 2px auto;margin: 4px;}
+.el-aside .el-button{margin: 2px;}
+.el-aside .el-input{font-size: 14px;padding: 2px;}
+.el-aside .el-card {
+  margin: 10px 0 10px auto;
+}
+.el-aside .el-card .el-button{width: 90px;}
+.el-form-item{font-weight:bold;}
+
+.el-main{background-color: #111;padding: 4px;width: 100%;overflow:hidden;color:#fff}
+
+.recvs {overflow-y: auto;overflow-x: hidden;width: 100%;height: 800px;}
+.rev_title{width: 100%;display: block;font-style:italic;color: #999;font-size: 14px;background-color: #fafafa;padding: 2px 4px;line-height: 25px;}
+.rev_conts{width: 100%;word-wrap: break-word;line-height: 1.5em;padding: 4px;line-height: 30px;}/*缩进text-indent:2em;*/
+.recvfontsize{text-align: center;display:block;padding-top: 4px;}
+.el-color-picker{margin-left: 4px;}
+.recv_count{text-align: left;}
+.recv_count p{line-height: 30px;}
+
+.header{
+ font-size:24px;
+ font-weight:bold;
+ margin:-10px auto 10px auto;
+}
+
+h1 {
+  font-size: 16px;
+  margin-top: 10px auto 20px auto;
+  padding:5px 0px 5px 0;
+}
+
+.el-col{
+ padding:4px;
+}
+
+.el-input{font-size: 13px;}
+.el-card {
+  margin-bottom: 12px;
+}
+.el-card__body {
+  padding: 24px;
+}
+
+.el-select {
+  width: 100%;
+}
+
+.text-right {
+  text-align: right;
+}
+
+.sub-btn {
+  margin-top: 30px;
+}
+
+.updateToken{display:block;line-height:30px;align:left;background:rgb(250,250,250,0.2);padding:5px;}
+
+.token_input{width:90%;}
+.hidden{display: none;}
+.w80{width: 80px;}
+.w100{width: 100px;}
+.log{font-size: 14px;color:#fff;background-color: black;}
+.center{text-align: center;}
+#ch1,#ch2,#ch3,#ch4,#ch5{width: 120px;}
+.el-tag{padding:auto 4px; margin: 5px;min-width: 60px;}
+
+el-tree span{line-height: 50px;}
+
+.video {
+	width: 100%;
+	height:100%;
+	overflow:hidden;
+	
+	.video-item {
+		display: flex;
+		padding: 5px;
+		overflow:hidden;
+		
+		.item {
+			flex: 1;
+			
+			min-height: 40%;
+			width: 100%;
+			margin: 0 5px;
+			background-color: #000000;
+			color:#fff;
+			border-radius: 2px;			
+
+			.home {
+				width: 100%;
+				height: 70vh;
+				overflow:hidden;
+				padding:0;
+				marigin:0;
+				aspect-ratio: 16/9; /* 设置任意宽高任意一项即可 然后使用aspect-ratio元素 动态设置比例 */
+				text-align: center;
+				justify-content: center;
+			}
+
+		}
+	}
+}
+</style>

+ 743 - 0
Web/src/views/mqttx/index.vue

@@ -0,0 +1,743 @@
+<template>
+  <div class="mqtt-box">
+	<h1 class="header">MQTTX在线测试客户端</h1>
+    <el-card :model="connection">
+      <h1>连接参数(Configuration)</h1>
+      <el-form label-position="top" :model="connection">
+        <el-row :gutter="20">
+          <el-col :span="8">
+			  <el-form-item prop="host" label="协议|主机|端口">
+			  <el-input v-model="connection.host" :disabled="connSuccess" type="password"  show-password>
+			  <template #prepend>
+				<el-select
+				  v-model="connection.protocol"
+				  class="w80"
+				  :disabled="connSuccess"
+				  @change="handleProtocolChange"
+				>
+				  <el-option label="ws://" value="ws"></el-option>
+				  <el-option label="wss://" value="wss"></el-option>
+				</el-select>
+			  </template>              
+			  <template #append>
+				<el-input
+				  v-model.number="connection.port"
+				  type="number"
+				  class="w80"
+				  :disabled="connSuccess"
+				  placeholder="8083/8084"
+				></el-input>
+			  </template>
+			  </el-input>
+            </el-form-item>
+          </el-col>
+          <el-col :span="0">
+            <el-form-item prop="clientId" label="标识(Client ID)唯一性">
+              <el-input v-model="connection.clientId"> </el-input>
+            </el-form-item>
+          </el-col>
+          <el-col :span="0">
+            <el-form-item prop="username" label="账号(Username)">
+              <el-input v-model="connection.username"></el-input>
+            </el-form-item>
+          </el-col>
+          <el-col :span="0">
+            <el-form-item prop="password" label="密码(Password)">
+              <el-input v-model="connection.password" type="password" show-password></el-input>
+            </el-form-item>
+          </el-col>
+          <el-col :span="4">
+            <el-form-item prop="regpacket" label="设备包名(Regpacket)">
+              <el-input v-model="connection.repacket" :disabled="connSuccess" @input="syncdhtreg"  @change="init_topic"></el-input>
+            </el-form-item>
+          </el-col>
+          <el-col :span="4">
+            <el-form-item prop="dhtRegpack" label="共享传感器(dhtRegpacket)">
+              <el-input v-model="connection.dhtRegpack" :disabled="connSuccess"  @change="init_topic"></el-input>
+            </el-form-item>
+          </el-col>		  
+          <el-col :span="8" class="text-right">
+            <el-button
+              type="primary"
+			  :icon="Setting"
+			  size="default"
+			  class="sub-btn"
+              :disabled="client.connected"
+              @click="createConnection"
+              :loading="btnLoadingType === 'connect'"
+			  :style="{ display: client.connected?'none':'' }"
+            >
+              {{ client.connected ? "已连接(Connected)" : "连接(Connect)" }}
+            </el-button>
+            <el-button
+              v-if="client.connected"
+              type="warning"
+			  size="default"
+			  :icon="Discount"
+              @click="destroyConnection"
+              :loading="btnLoadingType === 'disconnect'"
+            >
+              断开(Disconnect)
+            </el-button>
+          </el-col>
+        </el-row>
+      </el-form>
+    </el-card>
+	
+    <el-card shadow="hover">
+      <h1>订阅(Subscribe)</h1>
+      <el-form label-position="top" :model="subscription">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item prop="topic" label="订阅主题(Topic)">
+              <el-input
+                v-model="connection.subTopics"
+                :disabled="subscribedSuccess"
+				type="password"  show-password
+              ></el-input>
+            </el-form-item>
+          </el-col>
+          <el-col :span="4">
+            <el-form-item prop="qos" label="订阅质量(QoS)">
+              <el-select
+                v-model="subscription.qos"
+                :disabled="subscribedSuccess"
+              >
+                <el-option
+                  v-for="qos in qosList"
+                  :key="qos"
+                  :label="qos==0?'0 至多一次':qos==1?'1 至少一次':'2 仅仅一次'"
+                  :value="qos"
+                ></el-option>
+              </el-select>
+            </el-form-item>
+          </el-col>
+		  <el-col :span="8" class="text-right">
+		    <el-button
+		      type="primary"
+		  	:icon="Connection"
+		  	size="default"
+		      class="sub-btn"
+			  :style="{ display: subscribedSuccess?'none':'' }"
+		      :loading="btnLoadingType === 'subscribe'"
+		      :disabled="!client.connected || subscribedSuccess"
+		      @click="doSubscribe"
+		    >
+		      {{ subscribedSuccess ? "已订阅(Subscribed)" : "订阅(Subscribe)" }}
+		    </el-button>
+		    <el-button
+		      v-if="subscribedSuccess"
+		      type="warning"
+		  	:icon="Discount"
+		  	size="default"
+		      class="sub-btn"
+		      :loading="btnLoadingType === 'unsubscribe'"
+		      :disabled="!client.connected"
+		      @click="doUnSubscribe"
+		    >
+		      取消(Unsubscribe)
+		    </el-button>
+		  </el-col>
+        </el-row>
+		
+      </el-form>
+    </el-card>
+	
+    <el-card shadow="hover">
+      <h1>发布(Publish)</h1>
+      <el-form label-position="top" :model="publish">
+        <el-row :gutter="30">
+          <el-col :span="8">
+            <el-form-item prop="topic" label="发布主题(Topic)">
+              <el-input v-model="connection.pubTopic" type="password"  show-password></el-input>
+            </el-form-item>
+          </el-col>
+          <el-col :span="4">
+            <el-form-item prop="qos" label="发布质量(QoS)">
+              <el-select v-model="publish.qos">
+                <el-option
+                  v-for="qos in qosList"
+                  :key="qos"
+                  :label="qos==0?'0 至多一次':qos==1?'1 至少一次':'2 仅仅一次'"
+                  :value="qos"
+                ></el-option>
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="4">
+            <el-form-item prop="retain" label="发布保留(Retain)">
+              <el-select v-model="publish.retain">
+                <el-option value="false" label="false 不保留"></el-option>
+				<el-option value="true" label="true 不保留"></el-option>
+              </el-select>
+            </el-form-item>
+          </el-col>		  
+        </el-row>
+		
+		<el-row :gutter="30">
+			<el-col :span="16">
+			  <el-form-item prop="payload" label="操作指令(Payload)">
+				<el-input v-model="publish.payload" clearable maxlength="64" show-word-limit>
+						  <!--<template #prepend>
+								<el-button :icon="Operation" />
+							  </template> -->
+							  <template #append>
+								<el-select v-model="publish.payload" placeholder="选择指令" style="width: 115px">
+								  <el-option label="状态查询" value="55 AA AA AA AA 91 CF" />
+								  <el-option label="全部打开" value="55 AA AA AA AA 81 A4 01" />
+								  <el-option label="全部关闭" value="55 AA AA AA AA 81 A4 00" />
+								  <el-option label="一路开关" value="55 AA AA AA AA 81 BA 01" />
+								  <el-option label="二路开关" value="55 AA AA AA AA 81 BA 02" />
+								  <el-option label="三路开关" value="55 AA AA AA AA 81 BA 03" />
+								  <el-option label="四路开关" value="55 AA AA AA AA 81 BA 04" />
+								</el-select>
+							  </template>
+					</el-input>
+			  </el-form-item>
+			</el-col>
+			<el-col :span="8" class="text-right">
+			  <el-button
+				type="success"
+				:icon="Position"
+				size="default"
+				class="sub-btn"
+				:loading="btnLoadingType === 'publish'"
+				:disabled="!client.connected"		  
+				@click="doPublish(publish.payload,connection.pubTopic)">
+				发布(Publish)
+			  </el-button>
+			</el-col>
+		</el-row>
+      </el-form>
+    </el-card>
+	
+    <el-card shadow="hover">
+      <h1>
+		  <el-button @click="clsmsg" type="Success" size="default" :icon="Delete" title="点击清空历史记录">接收(Receive)</el-button>		  
+		   <el-tag size="default" title="接收次数">收 {{recvnum}}</el-tag>
+		   <el-tag size="default" :title="dht_tm">{{dht_wsd}}</el-tag>
+		   <el-tag size="default" title="设备已工作时长">{{parseInt(runSeconds)}} 秒</el-tag>
+		   	<el-button type="success" title="关闭一路" :disabled="!connection.onlineStatus|!client.connected" v-if="connection.ch1_Status" icon="ele-Check" size="default" id="ch1" v-preventReClick="2000" @click="switchLight('55 AA AA AA AA 81 01 00')">关闭</el-button>
+		   	<el-button type="warning" title="打开一路" :disabled="!connection.onlineStatus|!client.connected" v-else="!connection.ch1_Status" icon="ele-CloseBold" size="default" id="ch1"  v-preventReClick="2000" @click="switchLight('55 AA AA AA AA 81 01 01')">打开</el-button>
+		   	<el-button type="success" title="关闭二路" :disabled="!connection.onlineStatus|!client.connected" v-if="connection.ch2_Status" icon="ele-Check" size="default" id="ch2"  v-preventReClick="2000" @click="switchLight('55 AA AA AA AA 81 02 00')">关闭</el-button>
+		   	<el-button type="warning" title="打开二路" :disabled="!connection.onlineStatus|!client.connected" v-else="!connection.ch2_Status" icon="ele-CloseBold" size="default" id="ch2"  v-preventReClick="2000" @click="switchLight('55 AA AA AA AA 81 02 01')">打开</el-button>
+		   	<el-button type="success" title="关闭三路" :disabled="!connection.onlineStatus|!client.connected" v-if="connection.ch3_Status" icon="ele-Check" size="default" id="ch3"  v-preventReClick="2000" @click="switchLight('55 AA AA AA AA 81 03 00')">关闭</el-button>
+		   	<el-button type="warning" title="打开三路" :disabled="!connection.onlineStatus|!client.connected" v-else="!connection.ch3_Status" icon="ele-CloseBold" size="default" id="ch3"  v-preventReClick="2000" @click="switchLight('55 AA AA AA AA 81 03 01')">打开</el-button>
+		   	<el-button type="success" title="关闭四路" :disabled="!connection.onlineStatus|!client.connected" v-if="connection.ch4_Status" icon="ele-Check" size="default" id="ch4"  v-preventReClick="2000" @click="switchLight('55 AA AA AA AA 81 04 00')">关闭</el-button>
+		   	<el-button type="warning" title="打开四路" :disabled="!connection.onlineStatus|!client.connected" v-else="!connection.ch4_Status" icon="ele-CloseBold" size="default" id="ch4"  v-preventReClick="2000" @click="switchLight('55 AA AA AA AA 81 04 01')">打开</el-button>
+		   	<el-button type="danger" title="四路全部关闭" :disabled="!connection.onlineStatus|!client.connected" v-if="connection.all_Status" icon="ele-SwitchButton" size="default" id="ch5"	@click="switchLight('55 AA AA AA AA 81 A4 00')">全关</el-button>
+		   	<el-button type="success" title="四路全部打开" :disabled="!connection.onlineStatus|!client.connected" v-else="!connection.all_Status" icon="ele-Switch" size="default" id="ch5" @click="switchLight('55 AA AA AA AA 81 A4 01')">全开</el-button>
+		    
+			<el-alert v-if="!client.connected ||!connection.onlineStatus" title="网络服务断开或设备离线!" center type="warning" effect="light" />
+	  </h1>
+	  <!-- 绑定接收日志,只读 -->
+      <el-col :span="24">
+        <el-input
+          type="textarea"
+          :rows="8"
+		  id="recv"
+          v-model="receivedMessages"
+          readonly
+		  class="log"
+        ></el-input>
+      </el-col>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts" name="mqttx">
+	import { reactive, ref,onMounted,nextTick } from "vue";	
+	import { Search,ChatDotSquare,TopRight,Star,Operation,Setting,Connection,Discount,Open,Delete,Position} from '@element-plus/icons-vue';
+	import * as MQTT from "mqtt/dist/mqtt.min";//针对4.3.7版本的引用方法。5.7.x会提示错误 (import * as MQTT from "mqtt")
+	import mittBus from '/@/utils/mitt';//事件总线mitt 解决打包后错误Uncaught (in promise) ReferenceError: Cannot access 'oe' before initialization
+	
+// vue 3 + vite use MQTT.js refer to https://github.com/mqttjs/MQTT.js/issues/1269
+// https://github.com/mqttjs/MQTT.js#qos
+const qosList = [0, 1, 2];//质量
+const now = new Date();
+const recvnum=ref(0);
+const dht_wd=ref(0);//温度,湿度
+const dht_sd=ref(0);
+const dht_tm=ref("");//同步时间
+const dht_wsd=ref("温度0℃,湿度0%");
+const runSeconds=ref(0);//工作时长
+
+// mqtt客户端变量 let或const
+const client = ref({
+  connected: false,//未连接
+} as MQTT.MqttClient);
+
+const receivedMessages = ref("");
+const subscribedSuccess = ref(false);//订阅成功标志
+const connSuccess=ref(false);//连接成功标志
+const btnLoadingType = ref("");
+const retryTimes = ref(0);//重连次数
+
+/**
+ * this demo uses EMQX Public MQTT Broker (https://www.emqx.com/en/mqtt/public-mqtt5-broker), here are the details:
+ * 参考https://github.com/emqx/MQTT-Client-Examples
+ * 方法https://github.com/mqttjs/MQTT.js
+ * Broker host: broker.emqx.io
+ * WebSocket port: 8083
+ * WebSocket over TLS/SSL port: 8084
+ * ws -> 8083; wss -> 8084
+ * By default, EMQX allows clients to connect without authentication.
+ * https://docs.emqx.com/en/enterprise/v4.4/advanced/auth.html#anonymous-login
+ 
+ * for more options and details, please refer to https://github.com/mqttjs/MQTT.js#mqttclientstreambuilder-options
+ */
+const connection = reactive({
+  protocol: "ws",
+  host: "broker.emqx.io",
+  // ws -> 8083; wss -> 8084
+  port: 8083,
+  clientId: "emqx_vue3_" + Math.random().toString(16).substring(2, 8),
+  username: "",
+  password: "",
+  repacket: "d1ca1ff51f04",//注册包(改为您的注册包)
+  dhtRegpack:"d1ca1ff51f04",//温度注册包(可以相同可以共享传感器)
+  mqttToken:'0804d4c44c1f1bd11dea461481f19868',//授权TOKEN自己约定
+  keepalive: 30,
+  clean: true,//清除 clean session
+  connectTimeout: 30 * 1000, // ms 超时毫秒
+  reconnectPeriod: 5000, // ms 重连毫秒
+  resubscribe:true,//重新订阅
+  //定义您自己的主题
+  subTopic:'mqtt/admintnet/#0#/out',
+  willTopic:'mqtt/admintnet/#0#/will',
+  dhtTopic:'mqtt/admintnet/#0#/dht',  
+  pubTopic:'mqtt/admintnet/#0#/into',
+  subTopics:[],
+  pubPayload:'{"msg":"hellow vue3 mqtt."}',
+  onlineStatus:false,
+  ch1_Status:false,
+  ch2_Status:false,
+  ch3_Status:false,
+  ch4_Status:false,
+  all_Status:false,
+  isAC:null,//强电true
+
+});
+// 初始化主题
+const init_topic=()=>{
+	let st='mqtt/admintnet/#0#/out';//订阅主题
+	let pt='mqtt/admintnet/#0#/into';//发布主题
+	let ptbody='{"token":"{0}","cmd":"{1}","cmdpara":"{2}","clientid":"{3}"}';
+	let wt='mqtt/admintnet/#0#/will';//遗嘱主题
+	let dh='mqtt/admintnet/#0#/dht';//温湿度
+	connection.subTopic=st.replace("#0#",connection.repacket);
+	connection.willTopic=wt.replace("#0#",connection.repacket);	
+	connection.dhtTopic=dh.replace("#0#",connection.dhtRegpack);//温湿度	
+	connection.pubTopic=pt.replace("#0#",connection.repacket);
+	connection.subTopics=[connection.subTopic,connection.willTopic,connection.dhtTopic];
+	connection.pubPayload=ptbody;
+	//console.log(connection.subTopics);
+}
+
+//默认 注册包同步和传感器包名一致,反之不动
+const syncdhtreg=()=>{
+	connection.dhtRegpack=connection.repacket;
+}
+
+//字符串替换模拟  string.format(str,ar1,arn)
+const stringFormat =(formatted, args)=> {
+  for (let i = 0; i < args.length; i++) {
+    let regexp = new RegExp('\\{' + i + '\\}', 'gi')
+    formatted = formatted.replace(regexp, args[i])
+  }
+  return formatted
+}
+
+onMounted(async () => {
+	init_topic();
+	nextTick(() => {		
+	});
+});
+
+// topic & QoS for MQTT subscribing 订阅主题(多个)
+const subscription = ref({
+  topic: `$(connection.subTopics.value)`,
+  qos: 0 as MQTT.QoS,
+});
+
+// topic, QoS & payload for publishing message 发布主题
+const publish = ref({
+  topic: `${connection.pubTopic}`,
+  qos: 0 as MQTT.QoS,
+  retain:false,//保留否
+  payload: "55 AA AA AA AA 91 CF",//'{ "msg": "Hello, I am browser." }',
+});
+
+
+
+const initData = () => {
+  client.value = {
+    connected: false,
+  } as MQTT.MqttClient;
+  retryTimes.value = 0;
+  btnLoadingType.value = "";
+  subscribedSuccess.value = false;
+};
+
+const handleOnReConnect = () => {
+  retryTimes.value++;
+  connection.clientId="emqx_vue3_" + Math.random().toString(16).substring(2, 8);
+  console.log(retryTimes.value,"重试次数");
+  if (retryTimes.value > 5) {
+    try {
+      client.value.end();//重连超过5次断开
+      initData();
+      console.log("connection maxReconnectTimes limit, stop retry");
+	  appmessage(now.toLocaleString()+'|超出重连接次数,停止重试'+retryTimes.value)
+    } catch (error) {
+      console.log("handleOnReConnect catch error:", error);
+    }
+  }
+};
+
+    /**
+     * if protocol is "ws", connectUrl = "ws://broker.emqx.io:8083/mqtt"
+     * if protocol is "wss", connectUrl = "wss://broker.emqx.io:8084/mqtt"
+     * 
+     * /mqtt: MQTT-WebSocket uniformly uses /path as the connection path,
+     * which should be specified when connecting, and the path used on EMQX is /mqtt.
+     * 
+     * for more details about "mqtt.connect" method & options,
+     * please refer to https://github.com/mqttjs/MQTT.js#mqttconnecturl-options
+     */
+// create MQTT connection 创建连接
+const createConnection = () => {
+  try {
+    btnLoadingType.value = "connect";
+    const { protocol, host, port, ...options } = connection;
+    const connectUrl = `${protocol}://${host}:${port}/mqtt`;//组成新的连接字符串
+	console.log(connectUrl,"连接地址");
+    client.value = MQTT.connect(connectUrl, options);
+    if (client.value.on) {
+      // https://github.com/mqttjs/MQTT.js#event-connect
+      client.value.on("connect", () => {//v5.x  reconnect
+        btnLoadingType.value = "";
+		connSuccess.value=true;//client.value.connected;
+        console.log("connection successful",client.value.connected);
+		appmessage(now.toLocaleString()+'|连接服务成功');
+      });
+
+      // https://github.com/mqttjs/MQTT.js#event-reconnect 重连回调
+      client.value.on("reconnect", handleOnReConnect);
+      // https://github.com/mqttjs/MQTT.js#event-error
+      client.value.on("error", (error) => {
+        console.log("connection error:", error);
+		appmessage(now.toLocaleString()+'|发生错误:'+error);
+      });
+
+      // https://github.com/mqttjs/MQTT.js#event-message 接收消息,处理方法单独定义
+      client.value.on("message", (topic: string, message) => {
+		//处理方法
+		recvnum.value++;//接收次数累计
+		doAction(topic,message);//处理
+        receivedMessages.value = receivedMessages.value.concat(//拼接字符串输出
+          now.toLocaleString()+" "+`${topic}\r\n`+message.toString()+'\r\n'
+        );
+       // console.log(now.toLocaleString()+`收到消息: ${message} from topic: ${topic}`);
+		//滚动此方法可行
+		nextTick(() => {
+		   setTimeout(() => {
+		      syncBottom();//滚动到底部
+		   }, 50)
+		})
+      });
+    }
+  } catch (error) {
+    btnLoadingType.value = "";
+    console.log("mqtt.connect error:", error);
+  }
+};
+
+// 处理事件
+const doAction=(t,msg)=>{
+	let res =JSON.parse(msg.toString());//必须规范的json格式否则出错,双引号不能是单引号;;;后不安全但强大 eval('(' + message.toString() + ')'); //JSON.parse(message.toString());//json对象
+	
+	//消息不能带''否则错误
+	let regp=res.regpacket;//接收的注册包
+	let regs=connection.repacket;//订阅的注册包
+	let isOK=regp==regs?true:false;//是不是本设备的消息
+	if(!isOK||regp==null){
+		return;//不是丢弃
+	}
+	
+	if(t==connection.dhtTopic)
+	{//温湿度
+		let rp=res.regpacket;
+		let wd=res.temperature;
+		let sd=res.humidity;
+		let sj=res.time;
+		let sc=res.runsec;
+		if(rp!=connection.dhtRegpack)
+		{//来自订阅的温湿度包
+			return;
+		}
+		if(rp!=null)
+		{
+			dht_wd.value=wd;//实际应用时替换此3个变量即可
+			dht_sd.value=sd;
+			dht_tm.value="更新时间:"+sj;
+			runSeconds.value=sc;
+			dht_wsd.value="温度:"+dht_wd.value+"℃,湿度:"+dht_sd.value+"%";
+			//state.option.title.text="实时温湿度变化趋势图(运行"+parseInt(sc)+"秒)";
+			//updatechart(false);//实时数据(这种方法是实时推送,如果用 定时器 是定时显示的)updatewsd_time(false)
+		}
+	}
+	if(t==connection.willTopic)
+	{//遗嘱
+		if(res.redata=='offline')
+		{
+			connection.onlineStatus=false;
+		}else{
+			connection.onlineStatus=true;
+		}
+	}
+	if(t==connection.subTopic)
+	{
+		let rp0=res.regpacket;
+		if(rp0!=undefined){
+		   if(rp0==regs)
+		   {
+			   op(res.redata);//该设备执行指令其他放弃
+		   }
+		}
+	}
+}
+
+//处理开关状态(自定义的指令,需要修改为您自己的指令)
+const op=(cmd)=>{
+	if(cmd=="55 AA AA AA AA 82 01 01"){
+		connection.ch1_Status=true;
+	}
+	if(cmd=="55 AA AA AA AA 82 01 00"){
+		connection.ch1_Status=false;
+	}
+	if(cmd=="55 AA AA AA AA 82 02 01"){
+		connection.ch2_Status=true;
+	}
+	if(cmd=="55 AA AA AA AA 82 02 00"){
+		connection.ch2_Status=false;
+	}
+	if(cmd=="55 AA AA AA AA 82 03 01"){
+		connection.ch3_Status=true;
+	}
+	if(cmd=="55 AA AA AA AA 82 03 00"){
+		connection.ch3_Status=false;
+	}
+	if(cmd=="55 AA AA AA AA 82 04 01"){
+		connection.ch4_Status=true;
+	}
+	if(cmd=="55 AA AA AA AA 82 04 00"){
+		connection.ch4_Status=false;
+	}
+	if(cmd=="55 AA AA AA AA 82 A4 01"){
+		connection.ch1_Status=true;
+		connection.ch2_Status=true;
+		connection.ch3_Status=true;
+		connection.ch4_Status=true;
+	}
+	if(cmd=="55 AA AA AA AA 82 A4 00"){
+		connection.ch1_Status=false;
+		connection.ch2_Status=false;
+		connection.ch3_Status=false;
+		connection.ch4_Status=false;
+	}
+	if(cmd=="55 AA AA AA AA 84 AC 01"){
+		connection.isAC=true;
+	}
+	if(cmd=="55 AA AA AA AA 84 AC 00"){
+		connection.isAC=false;
+	}
+		if (connection.ch1_Status && connection.ch2_Status && connection.ch3_Status && connection.ch4_Status) {
+			connection.all_Status = true;
+		}
+		if (!connection.ch1_Status && !connection.ch2_Status && !connection.ch3_Status && !connection.ch4_Status) {
+			connection.all_Status = false;
+		}
+		if (cmd == "55 AA AA AA AA 84 AC 01") {
+			connection.isAC = true;
+		}
+		if (cmd == "55 AA AA AA AA 84 AC 00") {
+			connection.isAC = false;
+		}	
+}
+
+// 自动同步滚动(建议延时执行)textarea:any=null
+const syncBottom=()=>{
+		const textarea = document.getElementById('recv');
+		if(textarea)
+		{
+			textarea.scrollTop = textarea.scrollHeight-30;
+		}
+}
+
+// subscribe topic 开始订阅
+// https://github.com/mqttjs/MQTT.js#mqttclientsubscribetopictopic-arraytopic-object-options-callback
+const doSubscribe = () => {
+  btnLoadingType.value = "subscribe";
+  const { topic, qos } = subscription.value;
+  console.log(connection.subTopics,"订阅主题");
+  client.value.subscribe(connection.subTopics,{ qos },
+    (error: Error, granted: mqtt.ISubscriptionGrant[]) => {
+      btnLoadingType.value = "";
+      if (error) {
+        console.log("subscribe error:", error);
+        return;
+      }
+      subscribedSuccess.value = true;//订阅成功
+	  //连接成功,发布首个问询指令
+	  switchLight("55 AA AA AA AA 91 CF");//发送首页问询指令
+      console.log("订阅成功subscribe successfully:", granted);
+    }
+  );
+};
+
+// unsubscribe topic 取消订阅
+// https://github.com/mqttjs/MQTT.js#mqttclientunsubscribetopictopic-array-options-callback
+const doUnSubscribe = () => {
+  btnLoadingType.value = "unsubscribe";
+  const { topic, qos } = subscription.value;
+  client.value.unsubscribe(connection.subTopics, { qos }, (error) => {
+    btnLoadingType.value = "";
+    subscribedSuccess.value = false;
+    if (error) {
+      console.log("unsubscribe error:", error);
+      return;
+    }
+    console.log(`unsubscribed topic: ${topic}`);
+  });
+};
+
+// publish message发布消息
+// https://github.com/mqttjs/MQTT.js#mqttclientpublishtopic-message-options-callback
+const doPublish = (b,t) => {
+  //btnLoadingType.value = "publish";
+  const { topic, qos, payload,retain } = publish.value;
+  //console.log(t+b,"发布内容")
+  let paybody=stringFormat(connection.pubPayload,[connection.mqttToken,b??publish.value.payload,"",connection.clientId]);//标准格式payload
+  client.value.publish(t??connection.pubTopic, paybody, { qos }, (error) => {
+	  nextTick(() => {//测试延时
+	     setTimeout(() => {
+	        btnLoadingType.value = "";
+	     }, 50)
+	  })    
+    if (error) {
+		appmessage(now.toLocaleString()+'|发布消息错误.'+error);
+		console.log("publish error:", error);
+      return;
+    }    
+  });
+};
+
+// 消息追加消息框
+const appmessage=(msg)=>{
+	receivedMessages.value = receivedMessages.value.concat(//拼接字符串输出
+	  msg+'\r\n'
+	);
+}
+
+// 开关
+const switchLight=(cmd)=>{
+	if(!client.value.connected){
+		appmessage("尚未连接到服务!");
+		return;
+	}
+	let paybody=stringFormat(connection.pubPayload,[connection.mqttToken,cmd??publish.value.payload,"",connection.clientId]);
+	const { topic, qos, payload,retain } = publish.value;
+	//console.log(t+b,"发布内容")
+	client.value.publish(connection.pubTopic, paybody, { qos }, retain,(error) => {
+	  btnLoadingType.value = "";
+	  if (error) {
+	    console.log("publish error:", error);
+	    return;
+	  }
+	});
+}
+
+// disconnect 端口连接
+// https://github.com/mqttjs/MQTT.js#mqttclientendforce-options-callback
+const destroyConnection = () => {
+  if (client.value.connected) {
+    btnLoadingType.value = "disconnect";
+    try {
+      client.value.end(false, () => {
+        initData();
+		connSuccess.value=false;
+        //console.log("断开成功 disconnected successfully");
+		appmessage(now.toLocaleString()+'|连接已断开.');
+      });
+    } catch (error) {
+      btnLoadingType.value = "";
+      console.log("断开错误 disconnect error:", error);
+    }
+  }
+};
+
+// 端口随协议而改变
+const handleProtocolChange = (value: string) => {
+  connection.port = value === "wss" ? 8084 : 8083;
+};
+
+// 清空消息框
+const clsmsg = () => {
+	receivedMessages.value="";
+};
+
+
+</script>
+
+<style lang="scss" scoped>
+.mqtt-box {
+  max-width: 100%;
+  padding:4px;
+  margin: 10px auto 0 auto;
+}
+
+.header{
+ font-size:24px;
+ font-weight:bold;
+ margin:-12px auto 8px auto;
+}
+
+h1 {
+  font-size: 16px;
+  margin-top: 10px auto 20px auto;
+  padding:5px 0px 5px 0;
+}
+
+.el-col{
+ padding:4px;
+}
+
+.el-input{font-size: 13px;}
+.el-card {
+  margin-bottom: 12px;
+}
+.el-card__body {
+  padding: 24px;
+}
+
+.el-select {
+  width: 100%;
+}
+
+.text-right {
+  text-align: right;
+}
+
+.sub-btn {
+  margin-top: 20px;width:160px;
+}
+
+.hidden{display: none;}
+.w80{width: 80px;}
+.w100{width: 100px;}
+.log{font-size: 14px;color:#fff;background-color: black;}
+.center{text-align: center;}
+#ch1,#ch2,#ch3,#ch4,#ch5{width: 120px;}
+.el-tag{padding:auto 4px; margin: 5px;min-width: 60px;}
+</style>