对接mqtt

This commit is contained in:
吉浩茹
2025-09-27 14:59:48 +08:00
parent 8f6dcca19f
commit e4ea3312b7
8 changed files with 1093 additions and 220 deletions

9
.hbuilderx/launch.json Normal file
View File

@ -0,0 +1,9 @@
{
"version" : "1.0",
"configurations" : [
{
"playground" : "standard",
"type" : "uni-app:app-ios"
}
]
}

View File

@ -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"
}, },

View File

@ -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数据

View File

@ -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('🔧 开始解析设备数据...')
console.log('原始数据:', rawData)
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) { if (Array.isArray(data) && data.length > 0) {
console.log('✅ 返回数组第一个元素:', data[0])
return 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
} }
}, },

View File

@ -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

View File

@ -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
View 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
View 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()