对接mqtt
This commit is contained in:
9
.hbuilderx/launch.json
Normal file
9
.hbuilderx/launch.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"version" : "1.0",
|
||||||
|
"configurations" : [
|
||||||
|
{
|
||||||
|
"playground" : "standard",
|
||||||
|
"type" : "uni-app:app-ios"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -53,6 +53,7 @@
|
|||||||
"@dcloudio/uni-mp-xhs": "3.0.0-4070620250821001",
|
"@dcloudio/uni-mp-xhs": "3.0.0-4070620250821001",
|
||||||
"@dcloudio/uni-quickapp-webview": "3.0.0-4070620250821001",
|
"@dcloudio/uni-quickapp-webview": "3.0.0-4070620250821001",
|
||||||
"@dcloudio/uni-ui": "^1.4.28",
|
"@dcloudio/uni-ui": "^1.4.28",
|
||||||
|
"mqtt": "^3.0.0",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
"vue-i18n": "^9.1.9"
|
"vue-i18n": "^9.1.9"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -61,7 +61,7 @@
|
|||||||
<view class="progress-bar">
|
<view class="progress-bar">
|
||||||
<view class="progress-fill temperature-fill" :style="{ width: temperaturePercent + '%' }"></view>
|
<view class="progress-fill temperature-fill" :style="{ width: temperaturePercent + '%' }"></view>
|
||||||
</view>
|
</view>
|
||||||
<text class="param-range">15°C - 35°C</text>
|
<text class="param-range">0°C - 100°C</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@ -74,7 +74,7 @@
|
|||||||
<view class="progress-bar">
|
<view class="progress-bar">
|
||||||
<view class="progress-fill humidity-fill" :style="{ width: humidityPercent + '%' }"></view>
|
<view class="progress-fill humidity-fill" :style="{ width: humidityPercent + '%' }"></view>
|
||||||
</view>
|
</view>
|
||||||
<text class="param-range">30% - 70%</text>
|
<text class="param-range">0% - 100%</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@ -87,7 +87,7 @@
|
|||||||
<view class="progress-bar">
|
<view class="progress-bar">
|
||||||
<view class="progress-fill cleanliness-fill" :style="{ width: cleanlinessPercent + '%' }"></view>
|
<view class="progress-fill cleanliness-fill" :style="{ width: cleanlinessPercent + '%' }"></view>
|
||||||
</view>
|
</view>
|
||||||
<text class="param-range">60% - 100%</text>
|
<text class="param-range">暂无数据</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@ -165,28 +165,35 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, defineEmits, onMounted, onUnmounted } from 'vue';
|
import { ref, defineEmits, onMounted, onUnmounted, nextTick } from 'vue';
|
||||||
// import mqttClient from '@/utils/mqttClient';
|
import { createMqtt, closeMqtt, judgeBeat, getConnectionStatus } from '@/utils/sendMqtt';
|
||||||
import { MQTT_CONFIG, DataParser } from '@/config/mqtt';
|
import { DataParser } from '@/config/mqtt';
|
||||||
|
|
||||||
const emit = defineEmits(['openSettings']);
|
const emit = defineEmits(['openSettings']);
|
||||||
|
|
||||||
// 环境参数数据
|
// 环境参数数据
|
||||||
const temperature = ref(25);
|
const temperature = ref(0);
|
||||||
const humidity = ref(45);
|
const humidity = ref(0);
|
||||||
const cleanliness = ref(80);
|
const cleanliness = ref('-'); // 暂无洁净度数据
|
||||||
|
|
||||||
// MQTT连接状态
|
// MQTT连接状态
|
||||||
const isConnected = ref(false);
|
const isConnected = ref(false);
|
||||||
const lastUpdateTime = ref('');
|
const lastUpdateTime = ref('');
|
||||||
|
const isPageLoaded = ref(false); // 页面加载状态
|
||||||
|
|
||||||
// 计算百分比值用于进度条显示
|
// 计算百分比值用于进度条显示 (0-100范围)
|
||||||
const temperaturePercent = ref(50);
|
const temperaturePercent = ref(0); // 0°C对应0%
|
||||||
const humidityPercent = ref(60);
|
const humidityPercent = ref(0); // 0%对应0%
|
||||||
const cleanlinessPercent = ref(40);
|
const cleanlinessPercent = ref(0); // 暂无数据
|
||||||
|
|
||||||
// MQTT数据处理
|
// MQTT数据处理
|
||||||
const handleDeviceData = (data) => {
|
const handleDeviceData = (data) => {
|
||||||
|
// 只有在页面加载完成后才处理数据
|
||||||
|
if (!isPageLoaded.value) {
|
||||||
|
console.log('页面尚未加载完成,忽略MQTT数据:', data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log('收到设备数据:', data);
|
console.log('收到设备数据:', data);
|
||||||
|
|
||||||
// 更新最后更新时间
|
// 更新最后更新时间
|
||||||
@ -197,33 +204,32 @@ const handleDeviceData = (data) => {
|
|||||||
case 'AC': // 空调设备
|
case 'AC': // 空调设备
|
||||||
if (data.Data && data.Data.BSQWD !== undefined) {
|
if (data.Data && data.Data.BSQWD !== undefined) {
|
||||||
temperature.value = parseFloat(data.Data.BSQWD);
|
temperature.value = parseFloat(data.Data.BSQWD);
|
||||||
temperaturePercent.value = ((temperature.value - 15) / 20) * 100; // 15-35°C范围
|
temperaturePercent.value = Math.max(0, Math.min(100, temperature.value)); // 0-100°C范围
|
||||||
console.log('更新空调温度:', temperature.value);
|
console.log('更新空调温度:', temperature.value);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'WSD': // 温湿度传感器
|
case 'WSD': // 温湿度传感器
|
||||||
if (data.Data) {
|
if (data.Data) {
|
||||||
|
// WD为温度,SD为湿度
|
||||||
if (data.Data.WD !== undefined) {
|
if (data.Data.WD !== undefined) {
|
||||||
temperature.value = parseFloat(data.Data.WD);
|
temperature.value = parseFloat(data.Data.WD);
|
||||||
temperaturePercent.value = ((temperature.value - 15) / 20) * 100;
|
temperaturePercent.value = Math.max(0, Math.min(100, temperature.value)); // 0-100°C范围
|
||||||
console.log('更新温湿度传感器温度:', temperature.value);
|
console.log('更新WSD温度(WD):', temperature.value);
|
||||||
}
|
}
|
||||||
if (data.Data.SD !== undefined) {
|
if (data.Data.SD !== undefined) {
|
||||||
humidity.value = parseFloat(data.Data.SD);
|
humidity.value = parseFloat(data.Data.SD);
|
||||||
humidityPercent.value = (humidity.value / 80) * 100; // 0-80%范围
|
humidityPercent.value = Math.max(0, Math.min(100, humidity.value)); // 0-100%范围
|
||||||
console.log('更新湿度:', humidity.value);
|
console.log('更新WSD湿度(SD):', humidity.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'PM': // PM2.5传感器
|
case 'PM': // PM2.5传感器
|
||||||
if (data.Data && data.Data.PM25 !== undefined) {
|
if (data.Data && data.Data.PM25 !== undefined) {
|
||||||
// 将PM2.5值转换为洁净度百分比 (PM2.5越低,洁净度越高)
|
// 暂时不处理PM2.5数据,洁净度显示为"-"
|
||||||
const pm25Value = parseFloat(data.Data.PM25);
|
const pm25Value = parseFloat(data.Data.PM25);
|
||||||
cleanliness.value = Math.max(0, Math.min(100, 100 - (pm25Value / 2))); // 简单转换
|
console.log('收到PM2.5数据:', pm25Value, '洁净度保持显示"-"');
|
||||||
cleanlinessPercent.value = cleanliness.value;
|
|
||||||
console.log('更新PM2.5/洁净度:', pm25Value, cleanliness.value);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -232,32 +238,49 @@ const handleDeviceData = (data) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// // 连接MQTT并订阅数据
|
// 连接MQTT并订阅数据
|
||||||
// const connectMQTT = async () => {
|
const connectMQTT = async () => {
|
||||||
// try {
|
try {
|
||||||
// await mqttClient.connect();
|
console.log('开始连接MQTT...');
|
||||||
// isConnected.value = true;
|
createMqtt();
|
||||||
|
|
||||||
// // 订阅设备数据主题
|
// 延迟检查连接状态
|
||||||
// const subscribeSuccess = mqttClient.subscribe(MQTT_CONFIG.topics.deviceData, handleDeviceData);
|
setTimeout(() => {
|
||||||
|
isConnected.value = getConnectionStatus();
|
||||||
|
if (isConnected.value) {
|
||||||
|
console.log('MQTT连接成功,开始监听数据...');
|
||||||
|
uni.showToast({
|
||||||
|
title: 'MQTT连接成功',
|
||||||
|
icon: 'success',
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('MQTT连接失败');
|
||||||
|
uni.showToast({
|
||||||
|
title: 'MQTT连接失败',
|
||||||
|
icon: 'error',
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
// if (subscribeSuccess) {
|
} catch (error) {
|
||||||
// console.log('MQTT订阅成功,等待设备数据...');
|
console.error('MQTT连接失败:', error);
|
||||||
// } else {
|
isConnected.value = false;
|
||||||
// console.error('MQTT订阅失败');
|
|
||||||
// }
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error('MQTT连接失败:', error);
|
|
||||||
// isConnected.value = false;
|
|
||||||
|
|
||||||
// // 显示连接失败提示
|
uni.showToast({
|
||||||
// uni.showToast({
|
title: 'MQTT连接失败',
|
||||||
// title: 'MQTT连接失败',
|
icon: 'error',
|
||||||
// icon: 'error',
|
duration: 3000
|
||||||
// duration: 3000
|
});
|
||||||
// });
|
}
|
||||||
// }
|
};
|
||||||
// };
|
|
||||||
|
// 开始监听MQTT数据
|
||||||
|
const startMqttListener = () => {
|
||||||
|
console.log('开始监听MQTT数据...');
|
||||||
|
uni.$on('mqttData', handleDeviceData);
|
||||||
|
};
|
||||||
|
|
||||||
// 打开设置弹窗
|
// 打开设置弹窗
|
||||||
const openSettings = () => {
|
const openSettings = () => {
|
||||||
@ -276,7 +299,7 @@ const getHumidityStatus = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getCleanlinessStatus = () => {
|
const getCleanlinessStatus = () => {
|
||||||
if (cleanliness.value < 70) return 'status-warning';
|
// 洁净度暂无数据,始终显示为正常状态
|
||||||
return 'status-normal';
|
return 'status-normal';
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -310,14 +333,27 @@ const getCurrentTime = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 组件挂载时连接MQTT
|
// 组件挂载时连接MQTT
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
// connectMQTT();
|
console.log('EnvironmentParams组件已挂载');
|
||||||
|
|
||||||
|
// 恢复MQTT连接
|
||||||
|
connectMQTT();
|
||||||
|
|
||||||
|
// 等待DOM更新完成
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// 等待页面完全渲染后再开始监听MQTT数据
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('页面加载完成,开始监听MQTT数据...');
|
||||||
|
isPageLoaded.value = true; // 标记页面已加载完成
|
||||||
|
startMqttListener();
|
||||||
|
}, 2000); // 2秒后开始监听,确保页面和MQTT连接都已就绪
|
||||||
});
|
});
|
||||||
|
|
||||||
// 组件卸载时断开连接
|
// 组件卸载时断开连接
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
// mqttClient.unsubscribe(MQTT_CONFIG.topics.deviceData);
|
uni.$off('mqttData', handleDeviceData);
|
||||||
// mqttClient.disconnect();
|
closeMqtt();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 移除模拟数据变化,使用真实MQTT数据
|
// 移除模拟数据变化,使用真实MQTT数据
|
||||||
|
|||||||
@ -1,31 +1,65 @@
|
|||||||
// MQTT配置文件
|
// MQTT配置文件
|
||||||
|
// 使用条件编译适配不同平台
|
||||||
|
|
||||||
|
let mqtturl;
|
||||||
|
|
||||||
|
// #ifdef H5
|
||||||
|
mqtturl = "ws://122.51.194.184:8083/mqtt";
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef APP-PLUS || MP-WEIXIN
|
||||||
|
mqtturl = "wx://122.51.194.184:8083/mqtt";
|
||||||
|
//#endif
|
||||||
|
|
||||||
|
// #ifdef MP-ALIPAY
|
||||||
|
mqtturl = "alis://122.51.194.184:8083/mqtt";
|
||||||
|
//#endif
|
||||||
|
|
||||||
|
console.log('🔧 MQTT环境检测:', {
|
||||||
|
mqtturl: mqtturl,
|
||||||
|
// #ifdef H5
|
||||||
|
platform: 'H5',
|
||||||
|
// #endif
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
platform: 'APP-PLUS',
|
||||||
|
// #endif
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
platform: 'MP-WEIXIN',
|
||||||
|
// #endif
|
||||||
|
// #ifdef MP-ALIPAY
|
||||||
|
platform: 'MP-ALIPAY',
|
||||||
|
// #endif
|
||||||
|
});
|
||||||
|
|
||||||
export const MQTT_CONFIG = {
|
export const MQTT_CONFIG = {
|
||||||
// EMQX服务器地址
|
// EMQX服务器地址 - 使用条件编译选择协议
|
||||||
broker: 'ws://122.51.194.184:8083/mqtt', // WebSocket MQTT端口
|
broker: mqtturl,
|
||||||
// broker: 'mqtt://122.51.194.184:1883', // 标准MQTT端口
|
|
||||||
|
|
||||||
// 连接选项
|
// 连接选项
|
||||||
options: {
|
options: {
|
||||||
clientId: 'mobile-inspection-system-' + Math.random().toString(16).substr(2, 8),
|
clientId: 'mobile-inspection-system-' + Math.random().toString(16).substr(2, 8),
|
||||||
// 暂时不设置账号密码
|
username: 'dmbroker',
|
||||||
// username: '',
|
password: 'qwer1234',
|
||||||
// password: '',
|
keepalive: 30,
|
||||||
keepalive: 60,
|
|
||||||
clean: true,
|
clean: true,
|
||||||
reconnectPeriod: 5000,
|
reconnectPeriod: 1000,
|
||||||
connectTimeout: 30 * 1000,
|
connectTimeout: 5000,
|
||||||
|
protocolVersion: 4,
|
||||||
|
rejectUnauthorized: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
// 主题配置
|
// 主题配置
|
||||||
topics: {
|
topics: {
|
||||||
// 设备数据主题
|
// 设备数据主题
|
||||||
deviceData: 'hdydcj_01_UP',
|
// deviceData: 'hdydcj_01_down',
|
||||||
|
deviceData: 'HDYDCJ_01_DOWN',
|
||||||
|
|
||||||
// 设备类型
|
// 设备类型
|
||||||
deviceTypes: {
|
deviceTypes: {
|
||||||
WSD: '温湿度', // 温湿度传感器
|
WSD: '温湿度', // 温湿度传感器
|
||||||
AC: '空调', // 空调设备
|
AC: '空调', // 空调设备
|
||||||
PM: 'PM2.5', // PM2.5传感器
|
PM: 'PM2.5', // PM2.5传感器
|
||||||
|
// SIMPLE: '简单数据', // 简单数据(如"11"、"22")
|
||||||
// 可以根据需要添加更多设备类型
|
// 可以根据需要添加更多设备类型
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -36,13 +70,45 @@ export const DataParser = {
|
|||||||
// 解析设备数据
|
// 解析设备数据
|
||||||
parseDeviceData(rawData) {
|
parseDeviceData(rawData) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(rawData)
|
console.log('🔧 开始解析设备数据...')
|
||||||
if (Array.isArray(data) && data.length > 0) {
|
console.log('原始数据:', rawData)
|
||||||
return data[0] // 取第一个设备数据
|
console.log('数据类型:', typeof rawData)
|
||||||
|
|
||||||
|
// 根据截图,数据可能是简单的数字(如"11"、"22")
|
||||||
|
// 先尝试解析为JSON,如果失败则作为简单数据处理
|
||||||
|
let data
|
||||||
|
try {
|
||||||
|
data = JSON.parse(rawData)
|
||||||
|
console.log('解析后的JSON数据:', data)
|
||||||
|
console.log('是否为数组:', Array.isArray(data))
|
||||||
|
|
||||||
|
if (Array.isArray(data) && data.length > 0) {
|
||||||
|
console.log('✅ 返回数组第一个元素:', data[0])
|
||||||
|
return data[0] // 取第一个设备数据
|
||||||
|
} else if (data && typeof data === 'object') {
|
||||||
|
console.log('✅ 返回对象数据:', data)
|
||||||
|
return data // 直接返回对象
|
||||||
|
}
|
||||||
|
} catch (jsonError) {
|
||||||
|
console.log('⚠️ 不是JSON格式,作为简单数据处理')
|
||||||
|
// 处理简单数据(如"11"、"22")
|
||||||
|
const simpleData = {
|
||||||
|
Device: 'SIMPLE',
|
||||||
|
timestamp: Math.floor(Date.now() / 1000),
|
||||||
|
Data: {
|
||||||
|
value: rawData,
|
||||||
|
type: 'simple_data'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('✅ 返回简单数据:', simpleData)
|
||||||
|
return simpleData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('⚠️ 数据格式不符合预期')
|
||||||
return null
|
return null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('解析设备数据失败:', error)
|
console.error('❌ 解析设备数据失败:', error)
|
||||||
|
console.error('原始数据:', rawData)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
11
src/main.js
11
src/main.js
@ -2,9 +2,20 @@ import {
|
|||||||
createSSRApp
|
createSSRApp
|
||||||
} from "vue";
|
} from "vue";
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
|
|
||||||
export function createApp() {
|
export function createApp() {
|
||||||
const app = createSSRApp(App);
|
const app = createSSRApp(App);
|
||||||
return {
|
return {
|
||||||
app,
|
app,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #ifndef MP
|
||||||
|
// 处理 wx.connectSocket promisify 兼容问题,强制返回 SocketTask
|
||||||
|
uni.connectSocket = (function(connectSocket) {
|
||||||
|
return function(options) {
|
||||||
|
options.success = options.success || function() {}
|
||||||
|
return connectSocket.call(this, options)
|
||||||
|
}
|
||||||
|
})(uni.connectSocket)
|
||||||
|
// #endif
|
||||||
|
|||||||
@ -1,156 +0,0 @@
|
|||||||
// // MQTT客户端封装
|
|
||||||
// import mqtt from 'mqtt'
|
|
||||||
// import { MQTT_CONFIG, DataParser } from '@/config/mqtt'
|
|
||||||
|
|
||||||
// class MQTTClient {
|
|
||||||
// constructor() {
|
|
||||||
// this.client = null
|
|
||||||
// this.isConnected = false
|
|
||||||
// this.subscriptions = new Map()
|
|
||||||
// this.messageHandlers = new Map()
|
|
||||||
// this.reconnectAttempts = 0
|
|
||||||
// this.maxReconnectAttempts = 5
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 连接MQTT服务器
|
|
||||||
// async connect() {
|
|
||||||
// try {
|
|
||||||
// console.log('正在连接MQTT服务器:', MQTT_CONFIG.broker)
|
|
||||||
// this.client = mqtt.connect(MQTT_CONFIG.broker, MQTT_CONFIG.options)
|
|
||||||
|
|
||||||
// return new Promise((resolve, reject) => {
|
|
||||||
// this.client.on('connect', () => {
|
|
||||||
// console.log('MQTT连接成功')
|
|
||||||
// this.isConnected = true
|
|
||||||
// this.reconnectAttempts = 0
|
|
||||||
// resolve()
|
|
||||||
// })
|
|
||||||
|
|
||||||
// this.client.on('error', (error) => {
|
|
||||||
// console.error('MQTT连接失败:', error)
|
|
||||||
// this.isConnected = false
|
|
||||||
// reject(error)
|
|
||||||
// })
|
|
||||||
|
|
||||||
// this.client.on('message', (topic, message) => {
|
|
||||||
// this.handleMessage(topic, message)
|
|
||||||
// })
|
|
||||||
|
|
||||||
// this.client.on('reconnect', () => {
|
|
||||||
// this.reconnectAttempts++
|
|
||||||
// console.log(`MQTT重连中... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`)
|
|
||||||
|
|
||||||
// if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
||||||
// console.error('MQTT重连次数超限,停止重连')
|
|
||||||
// this.client.end()
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
// this.client.on('close', () => {
|
|
||||||
// console.log('MQTT连接关闭')
|
|
||||||
// this.isConnected = false
|
|
||||||
// })
|
|
||||||
|
|
||||||
// this.client.on('offline', () => {
|
|
||||||
// console.log('MQTT客户端离线')
|
|
||||||
// this.isConnected = false
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error('MQTT连接异常:', error)
|
|
||||||
// throw error
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 订阅主题
|
|
||||||
// subscribe(topic, handler) {
|
|
||||||
// if (!this.isConnected) {
|
|
||||||
// console.warn('MQTT未连接,无法订阅主题:', topic)
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
|
|
||||||
// this.client.subscribe(topic, (error) => {
|
|
||||||
// if (error) {
|
|
||||||
// console.error('订阅主题失败:', topic, error)
|
|
||||||
// return false
|
|
||||||
// } else {
|
|
||||||
// console.log('订阅主题成功:', topic)
|
|
||||||
// this.subscriptions.set(topic, true)
|
|
||||||
// this.messageHandlers.set(topic, handler)
|
|
||||||
// return true
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// return true
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 取消订阅
|
|
||||||
// unsubscribe(topic) {
|
|
||||||
// if (this.subscriptions.has(topic)) {
|
|
||||||
// this.client.unsubscribe(topic)
|
|
||||||
// this.subscriptions.delete(topic)
|
|
||||||
// this.messageHandlers.delete(topic)
|
|
||||||
// console.log('取消订阅主题:', topic)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 发布消息
|
|
||||||
// publish(topic, message) {
|
|
||||||
// if (!this.isConnected) {
|
|
||||||
// console.warn('MQTT未连接,无法发布消息')
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const payload = typeof message === 'object' ? JSON.stringify(message) : message
|
|
||||||
// this.client.publish(topic, payload, (error) => {
|
|
||||||
// if (error) {
|
|
||||||
// console.error('发布消息失败:', topic, error)
|
|
||||||
// return false
|
|
||||||
// } else {
|
|
||||||
// console.log('发布消息成功:', topic, payload)
|
|
||||||
// return true
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// return true
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 处理接收到的消息
|
|
||||||
// handleMessage(topic, message) {
|
|
||||||
// try {
|
|
||||||
// const handler = this.messageHandlers.get(topic)
|
|
||||||
// if (handler) {
|
|
||||||
// const rawData = message.toString()
|
|
||||||
// console.log('收到MQTT消息:', topic, rawData)
|
|
||||||
|
|
||||||
// // 解析数据
|
|
||||||
// const parsedData = DataParser.parseDeviceData(rawData)
|
|
||||||
// if (parsedData) {
|
|
||||||
// handler(parsedData)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error('处理消息失败:', topic, error)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 获取连接状态
|
|
||||||
// getConnectionStatus() {
|
|
||||||
// return {
|
|
||||||
// isConnected: this.isConnected,
|
|
||||||
// reconnectAttempts: this.reconnectAttempts,
|
|
||||||
// subscriptions: Array.from(this.subscriptions.keys())
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 断开连接
|
|
||||||
// disconnect() {
|
|
||||||
// if (this.client) {
|
|
||||||
// this.client.end()
|
|
||||||
// this.isConnected = false
|
|
||||||
// this.subscriptions.clear()
|
|
||||||
// this.messageHandlers.clear()
|
|
||||||
// console.log('MQTT连接已断开')
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export default new MQTTClient()
|
|
||||||
162
src/utils/sendMqtt.js
Normal file
162
src/utils/sendMqtt.js
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
// MQTT工具包 - 兼容H5、App、微信小程序
|
||||||
|
// 参考:https://blogs.seecsdn.cn/online/2025-09-27/0ecf1401e23b25be5f8c7e4377d8b5dd.html
|
||||||
|
|
||||||
|
let mqtturl;
|
||||||
|
|
||||||
|
// #ifdef H5
|
||||||
|
mqtturl = "ws://122.51.194.184:8083/mqtt";
|
||||||
|
import * as mqtt from "mqtt/dist/mqtt.min.js";
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef APP-PLUS || MP-WEIXIN
|
||||||
|
mqtturl = "wx://122.51.194.184:8083/mqtt";
|
||||||
|
import * as mqtt from "mqtt/dist/mqtt.min.js";
|
||||||
|
//#endif
|
||||||
|
|
||||||
|
// #ifdef MP-ALIPAY
|
||||||
|
mqtturl = "alis://122.51.194.184:8083/mqtt";
|
||||||
|
import * as mqtt from "@/utils/mqtt.min.js";
|
||||||
|
//#endif
|
||||||
|
|
||||||
|
function messageid2() {
|
||||||
|
// 2位随机数
|
||||||
|
return Math.floor(Math.random() * (99 - 10)) + 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
var client;
|
||||||
|
|
||||||
|
// 创建MQTT连接
|
||||||
|
const createMqtt = () => {
|
||||||
|
let options = {
|
||||||
|
keepalive: 30,
|
||||||
|
clientId: `mobile-inspection-system-${messageid2()}`, // 客户端ID
|
||||||
|
protocolId: 'MQTT',
|
||||||
|
username: "dmbroker",
|
||||||
|
password: "qwer1234",
|
||||||
|
protocolVersion: 4,
|
||||||
|
clean: true,
|
||||||
|
reconnectPeriod: 1000, // reconnectPeriod为1000毫秒,这意味着在连接丢失之后,客户端将在1秒后尝试重新连接。
|
||||||
|
connectTimeout: 5000, // 5s超时时间 意味着mqtt-reconnect函数5秒钟触发一次
|
||||||
|
topic: "HDYDCJ_01_DOWN",
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
// #ifdef MP-ALIPAY
|
||||||
|
my: my,//注意这里的my
|
||||||
|
//#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!client) {
|
||||||
|
console.log('🔧 开始创建MQTT连接...');
|
||||||
|
console.log('🔧 MQTT URL:', mqtturl);
|
||||||
|
console.log('🔧 连接选项:', options);
|
||||||
|
|
||||||
|
client = mqtt.connect(mqtturl, options);
|
||||||
|
initEventHandleMqtt(options.topic);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('❌ MQTT连接创建失败:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//建立连接
|
||||||
|
const initEventHandleMqtt = (topicUrl) => {
|
||||||
|
// 当连接成功时触发
|
||||||
|
client.on("connect", function() {
|
||||||
|
uni.hideLoading();
|
||||||
|
console.log("✅ MQTT连接成功");
|
||||||
|
//订阅主题
|
||||||
|
client.subscribe(topicUrl, function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.error("❌ MQTT订阅主题失败:", err);
|
||||||
|
} else {
|
||||||
|
console.log("✅ MQTT订阅主题成功:", topicUrl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
//如果mqttws订阅主题成功,那么这里就是当接收到自己订阅主题的处理逻辑
|
||||||
|
client.on("message", function(topic, message) {
|
||||||
|
try {
|
||||||
|
console.log('📨 收到MQTT消息:');
|
||||||
|
console.log('主题:', topic);
|
||||||
|
console.log('消息内容:', message.toString());
|
||||||
|
|
||||||
|
// 获取信息
|
||||||
|
const mqttData = JSON.parse(message.toString());
|
||||||
|
console.log('📋 解析后的数据:', mqttData);
|
||||||
|
|
||||||
|
// 传递信息
|
||||||
|
uni.$emit("mqttData", mqttData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 处理MQTT消息失败:', error);
|
||||||
|
console.error('原始消息:', message.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 当断开连接后,经过重连间隔时间重新自动连接到 Broker 时触发
|
||||||
|
client.on('reconnect', function() {
|
||||||
|
console.log('🔄 MQTT重新连接中...');
|
||||||
|
uni.showLoading({
|
||||||
|
title: "重新连接"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 当客户端无法成功连接时或发生解析错误时触发,参数 error 为错误信息
|
||||||
|
client.on("error", function(err) {
|
||||||
|
console.error('❌ MQTT连接错误:', err);
|
||||||
|
uni.showToast({
|
||||||
|
title: 'MQTT连接错误',
|
||||||
|
icon: 'error',
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 在收到 Broker 发送过来的断开连接的报文时触发
|
||||||
|
client.on('disconnect', function() {
|
||||||
|
console.log('⚠️ MQTT连接断开');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 在断开连接以后触发
|
||||||
|
client.on("close", function() {
|
||||||
|
console.log('🔌 MQTT连接关闭');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 当客户端下线时触发
|
||||||
|
client.on("offline", function() {
|
||||||
|
console.log('📴 MQTT客户端离线');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//强制断开Mqtt
|
||||||
|
const closeMqtt = () => {
|
||||||
|
if (client) {
|
||||||
|
console.log('🔌 强制断开MQTT连接');
|
||||||
|
client.end();
|
||||||
|
client = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用pingResp心跳判断客户端和服务端是否还在连接着
|
||||||
|
const judgeBeat = () => {
|
||||||
|
if (client && client.pingResp === false) {
|
||||||
|
console.log('💔 MQTT心跳停止,准备重连');
|
||||||
|
uni.showLoading({
|
||||||
|
title: "心跳停止,等待重连..."
|
||||||
|
});
|
||||||
|
closeMqtt();
|
||||||
|
createMqtt();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取连接状态
|
||||||
|
const getConnectionStatus = () => {
|
||||||
|
return client && client.connected;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
createMqtt,
|
||||||
|
closeMqtt,
|
||||||
|
judgeBeat,
|
||||||
|
getConnectionStatus,
|
||||||
|
client,
|
||||||
|
}
|
||||||
744
src/utils/uniMqttClient.js
Normal file
744
src/utils/uniMqttClient.js
Normal file
@ -0,0 +1,744 @@
|
|||||||
|
// uni-app兼容的MQTT客户端
|
||||||
|
import { MQTT_CONFIG, DataParser } from '@/config/mqtt'
|
||||||
|
|
||||||
|
// 兼容性处理:为App环境提供TextEncoder和TextDecoder
|
||||||
|
if (typeof TextEncoder === 'undefined') {
|
||||||
|
global.TextEncoder = class {
|
||||||
|
encode(str) {
|
||||||
|
const utf8 = unescape(encodeURIComponent(str));
|
||||||
|
const result = new Uint8Array(utf8.length);
|
||||||
|
for (let i = 0; i < utf8.length; i++) {
|
||||||
|
result[i] = utf8.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof TextDecoder === 'undefined') {
|
||||||
|
global.TextDecoder = class {
|
||||||
|
constructor(encoding = 'utf-8') {
|
||||||
|
this.encoding = encoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
decode(bytes) {
|
||||||
|
if (this.encoding === 'utf-8') {
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
result += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(escape(result));
|
||||||
|
} catch (e) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} else if (this.encoding === 'ascii') {
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
result += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class UniMqttClient {
|
||||||
|
constructor() {
|
||||||
|
this.socketTask = null
|
||||||
|
this.isConnected = false
|
||||||
|
this.subscriptions = new Map()
|
||||||
|
this.messageHandlers = new Map()
|
||||||
|
this.reconnectAttempts = 0
|
||||||
|
this.maxReconnectAttempts = 5
|
||||||
|
this.reconnectTimer = null
|
||||||
|
this.heartbeatTimer = null
|
||||||
|
this.messageId = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接MQTT服务器
|
||||||
|
async connect() {
|
||||||
|
try {
|
||||||
|
console.log('🔧 ===== MQTT连接开始 =====')
|
||||||
|
console.log('🔧 正在连接MQTT服务器:', MQTT_CONFIG.broker)
|
||||||
|
console.log('🔧 环境检测:', {
|
||||||
|
isApp: typeof plus !== 'undefined' || typeof window === 'undefined',
|
||||||
|
plus: typeof plus,
|
||||||
|
window: typeof window
|
||||||
|
})
|
||||||
|
|
||||||
|
// 发送日志到页面
|
||||||
|
this.sendLogToPage('===== MQTT连接开始 =====', 'info')
|
||||||
|
this.sendLogToPage(`正在连接MQTT服务器: ${MQTT_CONFIG.broker}`, 'info')
|
||||||
|
this.sendLogToPage(`环境检测: isApp=${typeof plus !== 'undefined' || typeof window === 'undefined'}, plus=${typeof plus}, window=${typeof window}`, 'info')
|
||||||
|
|
||||||
|
// 添加连接超时保护
|
||||||
|
const connectTimeout = setTimeout(() => {
|
||||||
|
console.error('🔧 MQTT连接超时')
|
||||||
|
this.sendLogToPage('MQTT连接超时', 'error')
|
||||||
|
this.handleConnectionError(new Error('Connection timeout'))
|
||||||
|
}, 10000) // 10秒超时
|
||||||
|
|
||||||
|
// 解析WebSocket URL
|
||||||
|
const wsUrl = MQTT_CONFIG.broker.replace('ws://', '').replace('wss://', '')
|
||||||
|
const [host, path] = wsUrl.split('/')
|
||||||
|
const [hostname, port] = host.split(':')
|
||||||
|
|
||||||
|
const protocol = MQTT_CONFIG.broker.startsWith('wss://') ? 'wss' : 'ws'
|
||||||
|
const fullUrl = `${protocol}://${hostname}:${port || (protocol === 'wss' ? 443 : 80)}/${path || ''}`
|
||||||
|
|
||||||
|
console.log('🔧 WebSocket连接地址:', fullUrl)
|
||||||
|
console.log('🔧 解析结果:', { hostname, port, path, protocol })
|
||||||
|
|
||||||
|
this.sendLogToPage(`WebSocket连接地址: ${fullUrl}`, 'info')
|
||||||
|
this.sendLogToPage(`解析结果: hostname=${hostname}, port=${port}, path=${path}, protocol=${protocol}`, 'info')
|
||||||
|
|
||||||
|
this.socketTask = uni.connectSocket({
|
||||||
|
url: fullUrl,
|
||||||
|
protocols: ['mqtt'],
|
||||||
|
success: () => {
|
||||||
|
console.log('🔧 WebSocket连接请求发送成功')
|
||||||
|
this.sendLogToPage('WebSocket连接请求发送成功', 'info')
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
console.error('🔧 WebSocket连接失败:', error)
|
||||||
|
console.error('🔧 连接参数:', { url: fullUrl, protocols: ['mqtt'] })
|
||||||
|
this.sendLogToPage(`WebSocket连接失败: ${JSON.stringify(error)}`, 'error')
|
||||||
|
this.handleConnectionError(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.socketTask.onOpen(() => {
|
||||||
|
console.log('🔧 WebSocket连接成功')
|
||||||
|
console.log('🔧 开始发送MQTT CONNECT消息...')
|
||||||
|
this.sendLogToPage('WebSocket连接成功', 'success')
|
||||||
|
this.sendLogToPage('开始发送MQTT CONNECT消息...', 'info')
|
||||||
|
// 发送MQTT CONNECT消息
|
||||||
|
this.sendConnectMessage()
|
||||||
|
.then(() => {
|
||||||
|
console.log('🔧 MQTT CONNECT成功,连接建立')
|
||||||
|
this.sendLogToPage('MQTT CONNECT成功,连接建立', 'success')
|
||||||
|
this.isConnected = true
|
||||||
|
this.reconnectAttempts = 0
|
||||||
|
this.startHeartbeat()
|
||||||
|
clearTimeout(connectTimeout) // 清除超时定时器
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('🔧 MQTT CONNECT失败:', error)
|
||||||
|
this.sendLogToPage(`MQTT CONNECT失败: ${error.message}`, 'error')
|
||||||
|
clearTimeout(connectTimeout) // 清除超时定时器
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.socketTask.onMessage((res) => {
|
||||||
|
this.handleMessage(res.data)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.socketTask.onError((error) => {
|
||||||
|
console.error('🔧 WebSocket错误:', error)
|
||||||
|
console.error('🔧 错误详情:', JSON.stringify(error))
|
||||||
|
this.sendLogToPage(`WebSocket错误: ${JSON.stringify(error)}`, 'error')
|
||||||
|
clearTimeout(connectTimeout) // 清除超时定时器
|
||||||
|
this.handleConnectionError(error)
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.socketTask.onClose((closeInfo) => {
|
||||||
|
console.log('🔧 WebSocket连接关闭')
|
||||||
|
console.log('🔧 关闭信息:', closeInfo)
|
||||||
|
this.sendLogToPage(`WebSocket连接关闭: ${JSON.stringify(closeInfo)}`, 'warning')
|
||||||
|
this.isConnected = false
|
||||||
|
this.stopHeartbeat()
|
||||||
|
this.attemptReconnect()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('MQTT连接异常:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理连接错误
|
||||||
|
handleConnectionError(error) {
|
||||||
|
this.isConnected = false
|
||||||
|
this.stopHeartbeat()
|
||||||
|
console.error('MQTT连接错误:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试重连
|
||||||
|
attemptReconnect() {
|
||||||
|
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||||
|
this.reconnectAttempts++
|
||||||
|
console.log(`MQTT重连中... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`)
|
||||||
|
this.sendLogToPage(`MQTT重连中... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`, 'warning')
|
||||||
|
|
||||||
|
this.reconnectTimer = setTimeout(() => {
|
||||||
|
this.connect().catch(error => {
|
||||||
|
console.error('重连失败:', error)
|
||||||
|
this.sendLogToPage(`重连失败: ${error.message}`, 'error')
|
||||||
|
})
|
||||||
|
}, 5000)
|
||||||
|
} else {
|
||||||
|
console.error('MQTT重连次数超限,停止重连')
|
||||||
|
this.sendLogToPage('MQTT重连次数超限,停止重连', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送日志到页面
|
||||||
|
sendLogToPage(message, type = 'info') {
|
||||||
|
try {
|
||||||
|
// 通过全局事件发送日志
|
||||||
|
if (typeof window !== 'undefined' && window.dispatchEvent) {
|
||||||
|
window.dispatchEvent(new CustomEvent('mqtt-log', {
|
||||||
|
detail: { message, type }
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 忽略错误,避免影响MQTT连接
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始心跳
|
||||||
|
startHeartbeat() {
|
||||||
|
this.heartbeatTimer = setInterval(() => {
|
||||||
|
if (this.isConnected && this.socketTask) {
|
||||||
|
// 发送PING消息
|
||||||
|
this.sendPing()
|
||||||
|
}
|
||||||
|
}, 30000) // 30秒心跳
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止心跳
|
||||||
|
stopHeartbeat() {
|
||||||
|
if (this.heartbeatTimer) {
|
||||||
|
clearInterval(this.heartbeatTimer)
|
||||||
|
this.heartbeatTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送MQTT CONNECT消息
|
||||||
|
sendConnectMessage() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// 保存Promise的resolve和reject函数
|
||||||
|
this.connectResolve = resolve
|
||||||
|
this.connectReject = reject
|
||||||
|
try {
|
||||||
|
const options = MQTT_CONFIG.options
|
||||||
|
const clientId = options.clientId || 'uni-mqtt-client-' + Math.random().toString(16).substr(2, 8)
|
||||||
|
|
||||||
|
// 构建CONNECT消息
|
||||||
|
const protocolName = 'MQTT'
|
||||||
|
const protocolLevel = 4 // MQTT 3.1.1
|
||||||
|
|
||||||
|
// 构建CONNECT消息的各个部分
|
||||||
|
const protocolNameBytes = new TextEncoder().encode(protocolName)
|
||||||
|
const clientIdBytes = new TextEncoder().encode(clientId)
|
||||||
|
|
||||||
|
// 可变头部
|
||||||
|
let connectFlags = 0x02 // Clean Session
|
||||||
|
if (options.username) {
|
||||||
|
connectFlags |= 0x80 // Username flag
|
||||||
|
}
|
||||||
|
if (options.password) {
|
||||||
|
connectFlags |= 0x40 // Password flag
|
||||||
|
}
|
||||||
|
|
||||||
|
const variableHeaderParts = [
|
||||||
|
new Uint8Array([0x00, protocolNameBytes.length]), // 协议名长度
|
||||||
|
protocolNameBytes, // 协议名
|
||||||
|
new Uint8Array([protocolLevel]), // 协议级别
|
||||||
|
new Uint8Array([connectFlags]), // 连接标志
|
||||||
|
new Uint8Array([0x00, options.keepalive || 60]), // 保持连接时间
|
||||||
|
]
|
||||||
|
|
||||||
|
// 载荷
|
||||||
|
let payloadParts = [
|
||||||
|
new Uint8Array([0x00, clientIdBytes.length]), // 客户端ID长度
|
||||||
|
clientIdBytes, // 客户端ID
|
||||||
|
]
|
||||||
|
|
||||||
|
// 如果有用户名和密码,添加到载荷
|
||||||
|
if (options.username) {
|
||||||
|
const usernameBytes = new TextEncoder().encode(options.username)
|
||||||
|
payloadParts.push(
|
||||||
|
new Uint8Array([0x00, usernameBytes.length]), // 用户名长度
|
||||||
|
usernameBytes // 用户名
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.password) {
|
||||||
|
const passwordBytes = new TextEncoder().encode(options.password)
|
||||||
|
payloadParts.push(
|
||||||
|
new Uint8Array([0x00, passwordBytes.length]), // 密码长度
|
||||||
|
passwordBytes // 密码
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并所有部分
|
||||||
|
const allParts = [
|
||||||
|
new Uint8Array([0x10]), // CONNECT消息类型
|
||||||
|
this.encodeRemainingLength(variableHeaderParts.reduce((sum, part) => sum + part.length, 0) +
|
||||||
|
payloadParts.reduce((sum, part) => sum + part.length, 0)),
|
||||||
|
...variableHeaderParts,
|
||||||
|
...payloadParts
|
||||||
|
]
|
||||||
|
|
||||||
|
const connectMessage = this.concatUint8Arrays(allParts)
|
||||||
|
|
||||||
|
console.log('🔧 发送MQTT CONNECT消息')
|
||||||
|
console.log('🔧 CONNECT消息长度:', connectMessage.length)
|
||||||
|
console.log('🔧 CONNECT消息内容 (Hex):', Array.from(connectMessage).map(b => b.toString(16).padStart(2, '0')).join(' '))
|
||||||
|
|
||||||
|
this.socketTask.send({
|
||||||
|
data: connectMessage.buffer,
|
||||||
|
success: () => {
|
||||||
|
console.log('🔧 CONNECT消息发送成功,等待CONNACK响应...')
|
||||||
|
// 设置超时等待CONNACK
|
||||||
|
this.connackTimeout = setTimeout(() => {
|
||||||
|
console.error('🔧 等待CONNACK超时 (5秒)')
|
||||||
|
if (this.connectReject) {
|
||||||
|
this.connectReject(new Error('CONNACK timeout'))
|
||||||
|
this.connectReject = null
|
||||||
|
}
|
||||||
|
}, 5000)
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
console.error('🔧 CONNECT消息发送失败:', error)
|
||||||
|
console.error('🔧 发送失败详情:', JSON.stringify(error))
|
||||||
|
if (this.connectReject) {
|
||||||
|
this.connectReject(error)
|
||||||
|
this.connectReject = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('构建CONNECT消息失败:', error)
|
||||||
|
if (this.connectReject) {
|
||||||
|
this.connectReject(error)
|
||||||
|
this.connectReject = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送PING消息
|
||||||
|
sendPing() {
|
||||||
|
try {
|
||||||
|
// MQTT PINGREQ消息 (0xC0, 0x00)
|
||||||
|
const pingMessage = new Uint8Array([0xC0, 0x00])
|
||||||
|
this.socketTask.send({
|
||||||
|
data: pingMessage.buffer
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('发送心跳失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订阅主题
|
||||||
|
subscribe(topic, handler) {
|
||||||
|
if (!this.isConnected) {
|
||||||
|
console.warn('MQTT未连接,无法订阅主题:', topic)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// MQTT SUBSCRIBE消息
|
||||||
|
const messageId = this.messageId++
|
||||||
|
const topicBytes = new TextEncoder().encode(topic)
|
||||||
|
const topicLength = topicBytes.length
|
||||||
|
|
||||||
|
const payloadParts = [
|
||||||
|
new Uint8Array([0x00, topicLength]), // 主题长度
|
||||||
|
topicBytes, // 主题内容
|
||||||
|
new Uint8Array([0x00]) // QoS 0
|
||||||
|
]
|
||||||
|
|
||||||
|
const payload = this.concatUint8Arrays(payloadParts)
|
||||||
|
const remainingLength = 2 + payload.length // messageId + payload
|
||||||
|
|
||||||
|
const subscribeMessage = this.concatUint8Arrays([
|
||||||
|
new Uint8Array([0x82]), // SUBSCRIBE消息类型
|
||||||
|
this.encodeRemainingLength(remainingLength),
|
||||||
|
new Uint8Array([(messageId >> 8) & 0xFF, messageId & 0xFF]), // messageId
|
||||||
|
payload
|
||||||
|
])
|
||||||
|
|
||||||
|
console.log('📤 发送MQTT SUBSCRIBE消息')
|
||||||
|
console.log('订阅主题:', topic)
|
||||||
|
console.log('消息ID:', messageId)
|
||||||
|
|
||||||
|
this.socketTask.send({
|
||||||
|
data: subscribeMessage.buffer
|
||||||
|
})
|
||||||
|
|
||||||
|
this.subscriptions.set(topic, true)
|
||||||
|
this.messageHandlers.set(topic, handler)
|
||||||
|
console.log('订阅主题成功:', topic)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('订阅主题失败:', topic, error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消订阅
|
||||||
|
unsubscribe(topic) {
|
||||||
|
if (this.subscriptions.has(topic)) {
|
||||||
|
try {
|
||||||
|
// MQTT UNSUBSCRIBE消息
|
||||||
|
const messageId = this.messageId++
|
||||||
|
const topicLength = Buffer.byteLength(topic, 'utf8')
|
||||||
|
const payload = Buffer.concat([
|
||||||
|
Buffer.from([0x00, topicLength]), // 主题长度
|
||||||
|
Buffer.from(topic, 'utf8') // 主题内容
|
||||||
|
])
|
||||||
|
|
||||||
|
const remainingLength = 2 + payload.length // messageId + payload
|
||||||
|
const unsubscribeMessage = Buffer.concat([
|
||||||
|
Buffer.from([0xA2]), // UNSUBSCRIBE消息类型
|
||||||
|
this.encodeRemainingLength(remainingLength),
|
||||||
|
Buffer.from([(messageId >> 8) & 0xFF, messageId & 0xFF]), // messageId
|
||||||
|
payload
|
||||||
|
])
|
||||||
|
|
||||||
|
this.socketTask.send({
|
||||||
|
data: unsubscribeMessage.buffer
|
||||||
|
})
|
||||||
|
|
||||||
|
this.subscriptions.delete(topic)
|
||||||
|
this.messageHandlers.delete(topic)
|
||||||
|
console.log('取消订阅主题:', topic)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('取消订阅失败:', topic, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发布消息
|
||||||
|
publish(topic, message) {
|
||||||
|
if (!this.isConnected) {
|
||||||
|
console.warn('MQTT未连接,无法发布消息')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = typeof message === 'object' ? JSON.stringify(message) : message
|
||||||
|
const topicLength = Buffer.byteLength(topic, 'utf8')
|
||||||
|
const payloadBuffer = Buffer.from(payload, 'utf8')
|
||||||
|
|
||||||
|
const remainingLength = 2 + topicLength + payloadBuffer.length
|
||||||
|
const publishMessage = Buffer.concat([
|
||||||
|
Buffer.from([0x30]), // PUBLISH消息类型
|
||||||
|
this.encodeRemainingLength(remainingLength),
|
||||||
|
Buffer.from([(topicLength >> 8) & 0xFF, topicLength & 0xFF]), // 主题长度
|
||||||
|
Buffer.from(topic, 'utf8'), // 主题内容
|
||||||
|
payloadBuffer // 消息内容
|
||||||
|
])
|
||||||
|
|
||||||
|
this.socketTask.send({
|
||||||
|
data: publishMessage.buffer
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('发布消息成功:', topic, payload)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('发布消息失败:', topic, error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理接收到的消息
|
||||||
|
handleMessage(data) {
|
||||||
|
try {
|
||||||
|
const buffer = new Uint8Array(data)
|
||||||
|
const messageType = (buffer[0] >> 4) & 0x0F
|
||||||
|
const flags = buffer[0] & 0x0F
|
||||||
|
|
||||||
|
// 打印协议内容
|
||||||
|
console.log('📦 收到MQTT协议消息:')
|
||||||
|
console.log('消息类型:', messageType, this.getMessageTypeName(messageType))
|
||||||
|
|
||||||
|
if (messageType === 2) { // CONNACK消息
|
||||||
|
this.handleConnackMessage(buffer)
|
||||||
|
} else if (messageType === 3) { // PUBLISH消息
|
||||||
|
this.handlePublishMessage(buffer)
|
||||||
|
} else if (messageType === 9) { // SUBACK消息
|
||||||
|
console.log('订阅确认收到')
|
||||||
|
} else if (messageType === 13) { // PINGRESP消息
|
||||||
|
console.log('心跳响应收到')
|
||||||
|
} else {
|
||||||
|
console.log('收到未知消息类型:', messageType)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('处理消息失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取消息类型名称
|
||||||
|
getMessageTypeName(type) {
|
||||||
|
const typeNames = {
|
||||||
|
0: 'RESERVED',
|
||||||
|
1: 'CONNECT',
|
||||||
|
2: 'CONNACK',
|
||||||
|
3: 'PUBLISH',
|
||||||
|
4: 'PUBACK',
|
||||||
|
5: 'PUBREC',
|
||||||
|
6: 'PUBREL',
|
||||||
|
7: 'PUBCOMP',
|
||||||
|
8: 'SUBSCRIBE',
|
||||||
|
9: 'SUBACK',
|
||||||
|
10: 'UNSUBSCRIBE',
|
||||||
|
11: 'UNSUBACK',
|
||||||
|
12: 'PINGREQ',
|
||||||
|
13: 'PINGRESP',
|
||||||
|
14: 'DISCONNECT',
|
||||||
|
15: 'RESERVED'
|
||||||
|
}
|
||||||
|
return typeNames[type] || 'UNKNOWN'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理CONNACK消息
|
||||||
|
handleConnackMessage(buffer) {
|
||||||
|
try {
|
||||||
|
console.log('🔧 收到CONNACK消息')
|
||||||
|
|
||||||
|
if (buffer.length >= 4) {
|
||||||
|
const returnCode = buffer[3]
|
||||||
|
console.log('🔧 CONNACK返回码:', returnCode)
|
||||||
|
|
||||||
|
if (returnCode === 0) {
|
||||||
|
console.log('🔧 ✅ MQTT连接成功')
|
||||||
|
this.isConnected = true
|
||||||
|
this.reconnectAttempts = 0
|
||||||
|
// 清除超时定时器
|
||||||
|
if (this.connackTimeout) {
|
||||||
|
clearTimeout(this.connackTimeout)
|
||||||
|
this.connackTimeout = null
|
||||||
|
}
|
||||||
|
// 触发连接成功的Promise resolve
|
||||||
|
if (this.connectResolve) {
|
||||||
|
this.connectResolve()
|
||||||
|
this.connectResolve = null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('🔧 ❌ MQTT连接被拒绝,返回码:', returnCode)
|
||||||
|
const errorMessages = {
|
||||||
|
1: '连接被拒绝,不支持的协议版本',
|
||||||
|
2: '连接被拒绝,不合格的客户端标识符',
|
||||||
|
3: '连接被拒绝,服务端不可用',
|
||||||
|
4: '连接被拒绝,无效的用户名或密码',
|
||||||
|
5: '连接被拒绝,未授权'
|
||||||
|
}
|
||||||
|
console.error('🔧 错误原因:', errorMessages[returnCode] || '未知错误')
|
||||||
|
this.isConnected = false
|
||||||
|
// 清除超时定时器
|
||||||
|
if (this.connackTimeout) {
|
||||||
|
clearTimeout(this.connackTimeout)
|
||||||
|
this.connackTimeout = null
|
||||||
|
}
|
||||||
|
// 触发连接失败的Promise reject
|
||||||
|
if (this.connectReject) {
|
||||||
|
this.connectReject(new Error(`MQTT连接被拒绝: ${errorMessages[returnCode] || '未知错误'}`))
|
||||||
|
this.connectReject = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('🔧 CONNACK消息长度不足:', buffer.length)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('🔧 解析CONNACK消息失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理PUBLISH消息
|
||||||
|
handlePublishMessage(buffer) {
|
||||||
|
try {
|
||||||
|
let offset = 1
|
||||||
|
|
||||||
|
// 解析剩余长度
|
||||||
|
const { length, bytesRead } = this.decodeRemainingLength(buffer, offset)
|
||||||
|
offset += bytesRead
|
||||||
|
|
||||||
|
console.log('🔍 PUBLISH消息解析:')
|
||||||
|
console.log('剩余长度:', length)
|
||||||
|
|
||||||
|
// 解析主题长度
|
||||||
|
const topicLength = (buffer[offset] << 8) | buffer[offset + 1]
|
||||||
|
offset += 2
|
||||||
|
|
||||||
|
console.log('主题长度:', topicLength)
|
||||||
|
|
||||||
|
// 解析主题
|
||||||
|
const topic = new TextDecoder().decode(buffer.slice(offset, offset + topicLength))
|
||||||
|
offset += topicLength
|
||||||
|
|
||||||
|
console.log('主题名称:', topic)
|
||||||
|
|
||||||
|
// 解析消息内容
|
||||||
|
const messageBytes = buffer.slice(offset)
|
||||||
|
console.log('🔍 消息字节数据:')
|
||||||
|
console.log('字节长度:', messageBytes.length)
|
||||||
|
console.log('字节数据 (Hex):', Array.from(messageBytes).map(b => b.toString(16).padStart(2, '0')).join(' '))
|
||||||
|
console.log('字节数据 (Dec):', Array.from(messageBytes).join(' '))
|
||||||
|
|
||||||
|
// 尝试不同的编码方式
|
||||||
|
let messageData
|
||||||
|
try {
|
||||||
|
// 首先尝试UTF-8解码
|
||||||
|
messageData = new TextDecoder('utf-8').decode(messageBytes)
|
||||||
|
console.log('✅ UTF-8解码成功')
|
||||||
|
} catch (error) {
|
||||||
|
console.log('❌ UTF-8解码失败,尝试其他编码')
|
||||||
|
try {
|
||||||
|
// 尝试ASCII解码
|
||||||
|
messageData = new TextDecoder('ascii').decode(messageBytes)
|
||||||
|
console.log('✅ ASCII解码成功')
|
||||||
|
} catch (error2) {
|
||||||
|
console.log('❌ ASCII解码失败,使用原始字节')
|
||||||
|
messageData = Array.from(messageBytes).map(b => String.fromCharCode(b)).join('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📨 收到MQTT消息:')
|
||||||
|
console.log('主题:', topic)
|
||||||
|
console.log('解码后消息:', messageData)
|
||||||
|
console.log('消息内容 (Hex):', Array.from(new TextEncoder().encode(messageData)).map(b => b.toString(16).padStart(2, '0')).join(' '))
|
||||||
|
|
||||||
|
// 数据类型判断和JSON转换
|
||||||
|
console.log('🔍 数据类型分析:')
|
||||||
|
console.log('数据类型:', typeof messageData)
|
||||||
|
|
||||||
|
// 尝试解析JSON
|
||||||
|
try {
|
||||||
|
console.log('🔄 尝试解析JSON...')
|
||||||
|
|
||||||
|
const jsonData = JSON.parse(messageData)
|
||||||
|
console.log('✅ JSON解析成功!')
|
||||||
|
console.log('解析后的数据类型:', typeof jsonData)
|
||||||
|
|
||||||
|
// 如果是数组,显示数组信息
|
||||||
|
if (Array.isArray(jsonData)) {
|
||||||
|
console.log('📋 数组信息:')
|
||||||
|
console.log('数组长度:', jsonData.length)
|
||||||
|
jsonData.forEach((item, index) => {
|
||||||
|
console.log(`数组[${index}]:`, typeof item, item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是对象,显示对象信息
|
||||||
|
if (jsonData && typeof jsonData === 'object' && !Array.isArray(jsonData)) {
|
||||||
|
console.log('📋 对象信息:')
|
||||||
|
console.log('对象键:', Object.keys(jsonData))
|
||||||
|
Object.entries(jsonData).forEach(([key, value]) => {
|
||||||
|
console.log(`${key}:`, typeof value, value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (jsonError) {
|
||||||
|
console.log('❌ JSON解析失败:', jsonError.message)
|
||||||
|
console.log('原始数据可能不是有效的JSON格式')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析设备数据并调用订阅的回调函数
|
||||||
|
console.log('🔍 开始解析设备数据...')
|
||||||
|
const parsedData = DataParser.parseDeviceData(messageData)
|
||||||
|
if (parsedData) {
|
||||||
|
console.log('✅ 设备数据解析成功')
|
||||||
|
// 调用订阅的回调函数
|
||||||
|
const handler = this.messageHandlers.get(topic)
|
||||||
|
if (handler) {
|
||||||
|
try {
|
||||||
|
handler(parsedData)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 消息处理器执行失败:', error)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ 未找到主题处理器:', topic)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('❌ 设备数据解析失败或数据为空')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析PUBLISH消息失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并Uint8Array数组
|
||||||
|
concatUint8Arrays(arrays) {
|
||||||
|
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0)
|
||||||
|
const result = new Uint8Array(totalLength)
|
||||||
|
let offset = 0
|
||||||
|
for (const arr of arrays) {
|
||||||
|
result.set(arr, offset)
|
||||||
|
offset += arr.length
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编码剩余长度
|
||||||
|
encodeRemainingLength(length) {
|
||||||
|
const bytes = []
|
||||||
|
do {
|
||||||
|
let byte = length % 128
|
||||||
|
length = Math.floor(length / 128)
|
||||||
|
if (length > 0) {
|
||||||
|
byte |= 0x80
|
||||||
|
}
|
||||||
|
bytes.push(byte)
|
||||||
|
} while (length > 0)
|
||||||
|
return new Uint8Array(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解码剩余长度
|
||||||
|
decodeRemainingLength(buffer, offset) {
|
||||||
|
let multiplier = 1
|
||||||
|
let value = 0
|
||||||
|
let bytesRead = 0
|
||||||
|
let byte
|
||||||
|
|
||||||
|
do {
|
||||||
|
if (offset + bytesRead >= buffer.length) {
|
||||||
|
throw new Error('Invalid remaining length')
|
||||||
|
}
|
||||||
|
byte = buffer[offset + bytesRead]
|
||||||
|
value += (byte & 0x7F) * multiplier
|
||||||
|
multiplier *= 128
|
||||||
|
bytesRead++
|
||||||
|
} while ((byte & 0x80) !== 0)
|
||||||
|
|
||||||
|
return { length: value, bytesRead }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取连接状态
|
||||||
|
getConnectionStatus() {
|
||||||
|
return {
|
||||||
|
isConnected: this.isConnected,
|
||||||
|
reconnectAttempts: this.reconnectAttempts,
|
||||||
|
subscriptions: Array.from(this.subscriptions.keys())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 断开连接
|
||||||
|
disconnect() {
|
||||||
|
if (this.socketTask) {
|
||||||
|
this.socketTask.close()
|
||||||
|
this.isConnected = false
|
||||||
|
this.subscriptions.clear()
|
||||||
|
this.messageHandlers.clear()
|
||||||
|
this.stopHeartbeat()
|
||||||
|
|
||||||
|
if (this.reconnectTimer) {
|
||||||
|
clearTimeout(this.reconnectTimer)
|
||||||
|
this.reconnectTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('MQTT连接已断开')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new UniMqttClient()
|
||||||
Reference in New Issue
Block a user