diff --git a/pages/home-page/policy-Config/index.vue b/pages/home-page/policy-Config/index.vue index 063554d..eac4c0a 100644 --- a/pages/home-page/policy-Config/index.vue +++ b/pages/home-page/policy-Config/index.vue @@ -1,302 +1,296 @@ + .policeForm { + margin: 22rpx; + padding: 22rpx; + background-color: #fff; + box-shadow: 0px 4rpx 16rpx rgba(0, 0, 0, 0.1); + border-radius: 8rpx; + } + \ No newline at end of file diff --git a/uni_modules/yh-wsmqtt/package.json b/uni_modules/yh-wsmqtt/package.json new file mode 100644 index 0000000..974a84d --- /dev/null +++ b/uni_modules/yh-wsmqtt/package.json @@ -0,0 +1,25 @@ +{ + "id": "yh-wsmqtt", + "name": "websocket MQTT 通讯插件,纯websocket 实现", + "displayName": "websocket MQTT 通讯插件,纯websocket 实现", + "version": "1.0.0", + "description": "websocket MQTT 通讯插件,纯websocket 实现", + "keywords": [ + "websocket;MQTT;uni" + ], + "dcloudext": { + "type": "js_sdk", + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + } + }, + "uni_modules": { + "dependencies": [], + "encrypt": [] + } +} \ No newline at end of file diff --git a/uni_modules/yh-wsmqtt/wsmqtt/README.md b/uni_modules/yh-wsmqtt/wsmqtt/README.md new file mode 100644 index 0000000..a2fc8f0 --- /dev/null +++ b/uni_modules/yh-wsmqtt/wsmqtt/README.md @@ -0,0 +1,173 @@ +# MQTT WebSocket 客户端插件 + +基于 WebSocket 的 MQTT 客户端插件,支持 uni-app 多端运行。 + +## 功能特性 + +- ✅ 支持 MQTT 3.1.1 和 MQTT 5.0 协议 +- ✅ 基于 uni-app 的 WebSocket API 实现 +- ✅ 自动心跳保活机制 +- ✅ 支持自动重连 +- ✅ 提供 JavaScript SDK 和 Vue 组件两种使用方式 +- ✅ 支持订阅/发布消息 +- ✅ 完整的连接状态管理 +- ✅ 多端兼容:App、小程序、H5 + +## 安装 + +1. 将本插件文件夹 `wsmqtt` 复制到项目的 `uni_modules` 目录下 +2. 在需要使用的页面或组件中引入 + +## 使用方式 + +### 方式一:JavaScript SDK(推荐) + +```javascript +import MQTTClient from '@/uni_modules/wsmqtt/js_sdk/index.js' + +// 创建客户端实例 +const mqttClient = new MQTTClient({ + host: 'broker.emqx.io', + port: 8083, + path: '/mqtt', + clientId: 'client_' + Date.now(), + username: '', + password: '', + protocolVersion: 4, // 4 for MQTT 3.1.1, 5 for MQTT 5.0 + keepAlive: 60, + connectTimeout: 10000, + reconnectPeriod: 5000 +}) + +// 监听连接事件 +mqttClient.on('connect', () => { + console.log('连接成功') +}) + +// 监听消息事件 +mqttClient.on('message', (msg) => { + console.log('收到消息:', msg) +}) + +// 连接服务器 +mqttClient.connect() + +// 发布消息 +mqttClient.publish('test/topic', 'Hello MQTT', 0) + +// 订阅主题 +mqttClient.subscribe('test/topic', 0) + +// 取消订阅 +mqttClient.unsubscribe('test/topic') + +// 断开连接 +mqttClient.disconnect() +``` + +### 方式二:Vue 组件 + +```vue + + + +``` + +## API 文档 + +### MQTTClient 类 + +#### 构造函数 +```javascript +new MQTTClient(options) +``` + +#### 配置选项 +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| host | string | '' | MQTT 服务器地址 | +| port | number | 8083 | 端口号 | +| path | string | '/mqtt' | WebSocket 路径 | +| clientId | string | 'mqttx_' + Date.now() | 客户端 ID | +| username | string | '' | 用户名 | +| password | string | '' | 密码 | +| protocolVersion | number | 4 | 协议版本:3(MQTT 3.1), 4(MQTT 3.1.1), 5(MQTT 5.0) | +| clean | boolean | true | MQTT 3.1.1 Clean Session | +| cleanStart | boolean | false | MQTT 5.0 Clean Start | +| keepAlive | number | 60 | 心跳间隔(秒) | +| sessionExpiryInterval | number | 4294967295 | 会话过期时间 | +| connectTimeout | number | 10000 | 连接超时时间(毫秒) | +| reconnectPeriod | number | 0 | 重连间隔(毫秒),0 表示不自动重连 | + +#### 方法 +- `connect()`: 连接服务器 +- `disconnect()`: 断开连接 +- `publish(topic, message, qos)`: 发布消息 +- `subscribe(topic, qos)`: 订阅主题 +- `unsubscribe(topic)`: 取消订阅 +- `on(event, callback)`: 添加事件监听 +- `off(event, callback)`: 移除事件监听 +- `getStatus()`: 获取当前连接状态 +- `getSubscribedTopics()`: 获取已订阅的主题列表 + +#### 事件 +- `connect`: 连接成功 +- `disconnect`: 连接断开 +- `error`: 发生错误 +- `message`: 收到或发送消息 +- `statusChange`: 连接状态变化 +- `subscribed`: 订阅成功 +- `unsubscribed`: 取消订阅成功 + +## 示例 + +本项目包含完整的示例页面,位于 `pages/mqtt-demo/index.vue`,展示了插件的所有功能。 + +## 注意事项 + +1. 小程序端需要使用真机调试,部分模拟器可能不支持 WebSocket +2. 确保服务器支持 WebSocket 协议的 MQTT +3. 不同平台的 WebSocket 实现可能有差异,建议在各平台测试 + +## 更新日志 + +详见 [CHANGELOG.md](./changelog.md) + +## 许可证 + +MIT License \ No newline at end of file diff --git a/uni_modules/yh-wsmqtt/wsmqtt/changelog.md b/uni_modules/yh-wsmqtt/wsmqtt/changelog.md new file mode 100644 index 0000000..6e3ec0d --- /dev/null +++ b/uni_modules/yh-wsmqtt/wsmqtt/changelog.md @@ -0,0 +1,11 @@ +# 更新日志 + +## v1.0.0 (2026-01-15) + +### 新增 +- 初始版本发布 +- 基于 WebSocket 的 MQTT 客户端实现 +- 支持 MQTT 3.1.1 和 MQTT 5.0 协议 +- 提供完整的连接、发布、订阅、取消订阅功能 +- 内置心跳机制和自动重连 +- 提供 Vue 组件封装 \ No newline at end of file diff --git a/uni_modules/yh-wsmqtt/wsmqtt/components/mqtt-client.vue b/uni_modules/yh-wsmqtt/wsmqtt/components/mqtt-client.vue new file mode 100644 index 0000000..e53079d --- /dev/null +++ b/uni_modules/yh-wsmqtt/wsmqtt/components/mqtt-client.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/uni_modules/yh-wsmqtt/wsmqtt/js_sdk/index.js b/uni_modules/yh-wsmqtt/wsmqtt/js_sdk/index.js new file mode 100644 index 0000000..babca23 --- /dev/null +++ b/uni_modules/yh-wsmqtt/wsmqtt/js_sdk/index.js @@ -0,0 +1,792 @@ +/** + * MQTT 客户端插件 for uni-app + * 基于 WebSocket 实现 MQTT 协议 + */ +class MQTTClient { + /** + * 构造函数 + * @param {Object} options 配置选项 + */ + constructor(options = {}) { + // 默认配置 + this.config = { + host: '', + port: 8083, + path: '/mqtt', + clientId: 'mqttx_' + Date.now(), + username: '', + password: '', + protocolVersion: 4, // 4 for MQTT 3.1.1 + clean: true, + cleanStart: false, // for MQTT 5.0 + keepAlive: 60, + sessionExpiryInterval: 4294967295, + connectTimeout: 10000, + reconnectPeriod: 0 + }; + + // 合并用户配置 + Object.assign(this.config, options); + + // 状态 + this.socket = null; + this.connectionStatus = 'disconnected'; // 'disconnected', 'connecting', 'connected', 'error' + this.subscribedTopics = []; + this.messageId = 1; + this.connectTimeoutTimer = null; + this.reconnectTimer = null; + this.pingIntervalTimer = null; + + // 事件监听器 + this.eventListeners = { + connect: [], + disconnect: [], + error: [], + message: [], + statusChange: [] + }; + + // 绑定方法 + this.on = this.on.bind(this); + this.off = this.off.bind(this); + this.connect = this.connect.bind(this); + this.disconnect = this.disconnect.bind(this); + this.publish = this.publish.bind(this); + this.subscribe = this.subscribe.bind(this); + this.unsubscribe = this.unsubscribe.bind(this); + } + + /** + * 添加事件监听器 + * @param {string} event 事件名称: 'connect', 'disconnect', 'error', 'message', 'statusChange' + * @param {Function} callback 回调函数 + */ + on(event, callback) { + if (this.eventListeners[event]) { + this.eventListeners[event].push(callback); + } + } + + /** + * 移除事件监听器 + * @param {string} event 事件名称 + * @param {Function} callback 回调函数 + */ + off(event, callback) { + if (this.eventListeners[event]) { + const index = this.eventListeners[event].indexOf(callback); + if (index > -1) { + this.eventListeners[event].splice(index, 1); + } + } + } + + /** + * 触发事件 + * @param {string} event 事件名称 + * @param {...any} args 参数 + */ + emit(event, ...args) { + if (this.eventListeners[event]) { + this.eventListeners[event].forEach(callback => { + try { + callback(...args); + } catch (err) { + console.error(`Error in ${event} event listener:`, err); + } + }); + } + } + + /** + * 更新连接状态 + * @param {string} status 新状态 + */ + setConnectionStatus(status) { + if (this.connectionStatus !== status) { + this.connectionStatus = status; + this.emit('statusChange', status); + } + } + + /** + * 连接 MQTT 服务器 + */ + connect() { + const { host, port, path, clientId, username, password, protocolVersion, clean, cleanStart, keepAlive, sessionExpiryInterval, connectTimeout } = this.config; + + if (!host) { + this.emit('error', new Error('请输入主机地址')); + return; + } + + this.setConnectionStatus('connecting'); + this.emit('statusChange', 'connecting'); + + // 构建 WebSocket URL + let urlPath = path; + if (!urlPath.startsWith('/')) { + urlPath = '/' + urlPath; + } + let url = `ws://${host}:${port}${urlPath}`; + + // 关闭现有连接 + if (this.socket) { + this.socket.close(); + this.socket = null; + } + + // 建立 WebSocket 连接 + this.socket = uni.connectSocket({ + url, + protocols: ['mqttv3.1.1', 'mqtt'], + header: { + 'content-type': 'application/octet-stream' + }, + success: () => { + // 连接请求已发送 + }, + fail: (err) => { + this.setConnectionStatus('error'); + this.emit('error', new Error(`WebSocket 连接失败: ${JSON.stringify(err)}`)); + } + }); + + // 设置连接超时 + if (connectTimeout > 0) { + this.connectTimeoutTimer = setTimeout(() => { + if (this.connectionStatus === 'connecting') { + this.setConnectionStatus('error'); + this.emit('error', new Error(`连接超时 (${connectTimeout}ms)`)); + if (this.socket) { + this.socket.close(); + this.socket = null; + } + } + }, connectTimeout); + } + + // WebSocket 事件监听 + this.socket.onOpen(() => { + if (this.connectTimeoutTimer) { + clearTimeout(this.connectTimeoutTimer); + this.connectTimeoutTimer = null; + } + this.sendConnectPacket(clientId, username, password, protocolVersion, clean, cleanStart, keepAlive, sessionExpiryInterval); + }); + + this.socket.onMessage((res) => { + this.handleMQTTMessage(res.data); + }); + + this.socket.onError((err) => { + if (this.connectTimeoutTimer) { + clearTimeout(this.connectTimeoutTimer); + this.connectTimeoutTimer = null; + } + this.stopPingInterval(); + this.setConnectionStatus('error'); + this.emit('error', new Error(`WebSocket 错误: ${JSON.stringify(err)}`)); + }); + + this.socket.onClose((res) => { + if (this.connectTimeoutTimer) { + clearTimeout(this.connectTimeoutTimer); + this.connectTimeoutTimer = null; + } + this.stopPingInterval(); + + if (this.connectionStatus === 'connected' && this.config.reconnectPeriod > 0) { + this.reconnectTimer = setTimeout(() => { + this.connect(); + }, this.config.reconnectPeriod); + } + + this.setConnectionStatus('disconnected'); + this.emit('disconnect', res); + }); + } + + /** + * 断开连接 + */ + disconnect() { + if (this.connectTimeoutTimer) { + clearTimeout(this.connectTimeoutTimer); + this.connectTimeoutTimer = null; + } + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + } + this.stopPingInterval(); + + if (this.socket) { + this.sendDisconnectPacket(); + setTimeout(() => { + this.socket.close(); + this.socket = null; + this.setConnectionStatus('disconnected'); + this.subscribedTopics = []; + }, 100); + } + } + + /** + * 发布消息 + * @param {string} topic 主题 + * @param {string} message 消息内容 + * @param {number} qos QoS 等级 (0, 1, 2) + */ + publish(topic, message, qos = 0) { + if (this.connectionStatus !== 'connected') { + this.emit('error', new Error('请先连接')); + return; + } + + if (!topic) { + this.emit('error', new Error('请输入主题')); + return; + } + + try { + this.sendPublishPacket(topic, message, qos); + this.emit('message', { type: 'send', topic, message }); + } catch (error) { + this.emit('error', new Error(`发布失败: ${error.message}`)); + } + } + + /** + * 订阅主题 + * @param {string} topic 主题 + * @param {number} qos QoS 等级 (0, 1, 2) + */ + subscribe(topic, qos = 0) { + if (this.connectionStatus !== 'connected') { + this.emit('error', new Error('请先连接')); + return; + } + + if (!topic) { + this.emit('error', new Error('请输入订阅主题')); + return; + } + + try { + this.sendSubscribePacket(topic, qos); + if (!this.subscribedTopics.includes(topic)) { + this.subscribedTopics.push(topic); + } + } catch (error) { + this.emit('error', new Error(`订阅失败: ${error.message}`)); + } + } + + /** + * 取消订阅 + * @param {string} topic 主题 + */ + unsubscribe(topic) { + if (this.connectionStatus !== 'connected') { + this.emit('error', new Error('请先连接')); + return; + } + + if (!topic) { + this.emit('error', new Error('请输入取消订阅的主题')); + return; + } + + try { + this.sendUnsubscribePacket(topic); + const index = this.subscribedTopics.indexOf(topic); + if (index > -1) { + this.subscribedTopics.splice(index, 1); + } + } catch (error) { + this.emit('error', new Error(`取消订阅失败: ${error.message}`)); + } + } + + /** + * 获取当前连接状态 + * @returns {string} 连接状态 + */ + getStatus() { + return this.connectionStatus; + } + + /** + * 获取已订阅的主题列表 + * @returns {Array} 主题列表 + */ + getSubscribedTopics() { + return [...this.subscribedTopics]; + } + + // ========== 内部方法 ========== + + /** + * 发送数据包 + * @param {string|ArrayBuffer|Array} packet 数据包 + */ + sendPacket(packet) { + if (!this.socket) { + return; + } + + let arrayBuffer; + if (typeof packet === 'string') { + const bytes = this.ab2buffer(packet); + arrayBuffer = new Uint8Array(bytes).buffer; + } else if (packet instanceof ArrayBuffer) { + arrayBuffer = packet; + } else if (Array.isArray(packet)) { + arrayBuffer = new Uint8Array(packet).buffer; + } else { + this.emit('error', new Error('未知的数据包格式')); + return; + } + + uni.sendSocketMessage({ + data: arrayBuffer, + success: () => { + // 发送成功 + }, + fail: (err) => { + this.emit('error', new Error(`数据发送失败: ${JSON.stringify(err)}`)); + } + }); + } + + /** + * 处理 MQTT 消息 + * @param {*} data 原始数据 + */ + handleMQTTMessage(data) { + try { + const buffer = this.ab2buffer(data); + + if (buffer.length === 0) { + return; + } + + const packetType = buffer[0] & 0xF0; + const flags = buffer[0] & 0x0F; + + let pos = 1; + let remainingLength = 0; + let multiplier = 1; + let encodedByte; + + do { + if (pos >= buffer.length) { + return; + } + encodedByte = buffer[pos]; + remainingLength += (encodedByte & 0x7F) * multiplier; + multiplier *= 128; + pos++; + } while ((encodedByte & 0x80) !== 0 && pos < buffer.length); + + if (packetType === 0x20) { + this.handleConnackPacket(buffer, pos); + } else if (packetType === 0x30 || packetType === 0x32) { + this.handlePublishPacket(buffer, pos, flags); + } else if (packetType === 0x40) { + this.handlePubackPacket(buffer, pos); + } else if (packetType === 0x90) { + this.handleSubackPacket(buffer, pos); + } else if (packetType === 0xB0) { + this.handleUnsubackPacket(buffer, pos); + } else if (packetType === 0xD0) { + // 心跳响应 + } else if (packetType === 0xE0) { + this.handleDisconnectPacket(buffer, pos); + } + } catch (error) { + this.emit('error', new Error(`处理 MQTT 消息时出错: ${error.message}`)); + } + } + + /** + * 处理 CONNACK 包 + */ + handleConnackPacket(buffer, pos) { + if (buffer.length < pos + 2) { + this.emit('error', new Error('CONNACK 包长度不足')); + return; + } + + const flags = buffer[pos++]; + const reasonCode = buffer[pos++]; + + const reasonCodes = { + 0: '成功', + 128: '协议版本错误', + 129: '客户端ID被拒绝', + 130: '服务器不可用', + 131: '用户名密码错误', + 132: '未授权', + 133: '服务器未找到', + 134: '服务器不可用', + 135: '用户名密码无效' + }; + + const reasonText = reasonCodes[reasonCode] || `未知错误 (${reasonCode})`; + + if (reasonCode === 0) { + this.setConnectionStatus('connected'); + this.emit('connect'); + this.startPingInterval(); + } else { + this.setConnectionStatus('error'); + this.emit('error', new Error(`MQTT 连接失败: ${reasonText}`)); + } + } + + /** + * 处理 PUBLISH 包 + */ + handlePublishPacket(buffer, pos, flags) { + try { + const qos = (flags >> 1) & 0x03; + + if (buffer.length < pos + 2) { + this.emit('error', new Error('PUBLISH 包主题长度字段不完整')); + return; + } + const topicLen = buffer[pos++] * 256 + buffer[pos++]; + + if (buffer.length < pos + topicLen) { + this.emit('error', new Error('PUBLISH 包主题数据不完整')); + return; + } + const topicBytes = buffer.slice(pos, pos + topicLen); + const topic = this.bytesToString(topicBytes); + pos += topicLen; + + let packetId = 0; + if (qos > 0) { + if (buffer.length < pos + 2) { + this.emit('error', new Error('PUBLISH 包包ID字段不完整')); + return; + } + packetId = buffer[pos++] * 256 + buffer[pos++]; + } + + const payloadBytes = buffer.slice(pos); + const payload = this.bytesToString(payloadBytes); + + this.emit('message', { type: 'receive', topic, payload }); + + if (qos === 1) { + this.sendPubackPacket(packetId); + } + } catch (error) { + this.emit('error', new Error(`处理 PUBLISH 包时出错: ${error.message}`)); + } + } + + /** + * 处理 PUBACK 包 + */ + handlePubackPacket(buffer, pos) { + if (buffer.length >= pos + 2) { + const packetId = buffer[pos++] * 256 + buffer[pos++]; + // 可以触发事件 + } + } + + /** + * 处理 SUBACK 包 + */ + handleSubackPacket(buffer, pos) { + if (buffer.length >= pos + 2) { + const packetId = buffer[pos++] * 256 + buffer[pos++]; + this.emit('subscribed', packetId); + } + } + + /** + * 处理 UNSUBACK 包 + */ + handleUnsubackPacket(buffer, pos) { + if (buffer.length >= pos + 2) { + const packetId = buffer[pos++] * 256 + buffer[pos++]; + this.emit('unsubscribed', packetId); + } + } + + /** + * 处理 DISCONNECT 包 + */ + handleDisconnectPacket(buffer, pos) { + this.setConnectionStatus('disconnected'); + this.emit('disconnect', { type: 'server' }); + } + + /** + * 发送 CONNECT 包 + */ + sendConnectPacket(clientId, username, password, protocolVersion, clean, cleanStart, keepAlive, sessionExpiryInterval) { + try { + let packet = ''; + packet += String.fromCharCode(0x10); + + let variableHeader = ''; + variableHeader += String.fromCharCode(0x00, 0x04); + variableHeader += 'MQTT'; + variableHeader += String.fromCharCode(protocolVersion); + + let flags = 0x00; + if (protocolVersion === 5) { + if (cleanStart) flags |= 0x02; + } else { + if (clean) flags |= 0x02; + } + if (username) flags |= 0x80; + if (password) flags |= 0x40; + variableHeader += String.fromCharCode(flags); + + variableHeader += String.fromCharCode((keepAlive >> 8) & 0xFF, keepAlive & 0xFF); + + let payload = this.encodeString(clientId); + if (username) payload += this.encodeString(username); + if (password) payload += this.encodeString(password); + + const remainingLength = variableHeader.length + payload.length; + packet += this.encodeRemainingLength(remainingLength); + packet += variableHeader; + packet += payload; + + this.sendPacket(packet); + } catch (error) { + this.emit('error', new Error(`发送 CONNECT 包失败: ${error.message}`)); + } + } + + /** + * 发送 DISCONNECT 包 + */ + sendDisconnectPacket() { + let packet = String.fromCharCode(0xE0); + packet += String.fromCharCode(0x00); + this.sendPacket(packet); + } + + /** + * 发送 PUBLISH 包 + */ + sendPublishPacket(topic, message, qos) { + let packet = ''; + + const flags = 0x30 | (qos << 1); + packet += String.fromCharCode(flags); + + const topicBytes = this.encodeString(topic); + const msgBytes = this.stringToBytes(message); + + let remainingLength = topicBytes.length + msgBytes.length; + + if (qos > 0) { + remainingLength += 2; + packet += this.encodeRemainingLength(remainingLength); + packet += topicBytes; + packet += String.fromCharCode((this.messageId >> 8) & 0xFF, this.messageId & 0xFF); + this.messageId++; + } else { + packet += this.encodeRemainingLength(remainingLength); + packet += topicBytes; + } + + packet += msgBytes; + this.sendPacket(packet); + } + + /** + * 发送 SUBSCRIBE 包 + */ + sendSubscribePacket(topic, qos) { + let packet = String.fromCharCode(0x82); + + const topicBytes = this.encodeString(topic); + let remainingLength = 2 + 1 + topicBytes.length + 1; + + packet += this.encodeRemainingLength(remainingLength); + packet += String.fromCharCode((this.messageId >> 8) & 0xFF, this.messageId & 0xFF); + + packet += String.fromCharCode(0x00); + packet += topicBytes; + packet += String.fromCharCode(qos); + + this.messageId++; + this.sendPacket(packet); + } + + /** + * 发送 UNSUBSCRIBE 包 + */ + sendUnsubscribePacket(topic) { + let packet = String.fromCharCode(0xA2); + + const topicBytes = this.encodeString(topic); + let remainingLength = 2 + 1 + topicBytes.length; + + packet += this.encodeRemainingLength(remainingLength); + packet += String.fromCharCode((this.messageId >> 8) & 0xFF, this.messageId & 0xFF); + + packet += String.fromCharCode(0x00); + packet += topicBytes; + + this.messageId++; + this.sendPacket(packet); + } + + /** + * 发送 PUBACK 包 + */ + sendPubackPacket(packetId) { + try { + let packet = String.fromCharCode(0x40); + packet += String.fromCharCode(0x02); + packet += String.fromCharCode((packetId >> 8) & 0xFF, packetId & 0xFF); + this.sendPacket(packet); + } catch (error) { + this.emit('error', new Error(`发送 PUBACK 失败: ${error.message}`)); + } + } + + /** + * 发送 PINGREQ 包 + */ + sendPingPacket() { + try { + let packet = String.fromCharCode(0xC0); + packet += String.fromCharCode(0x00); + this.sendPacket(packet); + } catch (error) { + this.emit('error', new Error(`发送 PINGREQ 失败: ${error.message}`)); + } + } + + /** + * 启动心跳定时器 + */ + startPingInterval() { + this.stopPingInterval(); + const { keepAlive } = this.config; + if (keepAlive > 0) { + const interval = (keepAlive * 1000) * 0.8; + this.pingIntervalTimer = setInterval(() => { + if (this.connectionStatus === 'connected') { + this.sendPingPacket(); + } + }, interval); + } + } + + /** + * 停止心跳定时器 + */ + stopPingInterval() { + if (this.pingIntervalTimer) { + clearInterval(this.pingIntervalTimer); + this.pingIntervalTimer = null; + } + } + + // ========== 工具方法 ========== + + /** + * 将各种数据格式转换为字节数组 + */ + ab2buffer(data) { + try { + if (typeof data === 'string') { + const bytes = []; + for (let i = 0; i < data.length; i++) { + bytes.push(data.charCodeAt(i) & 0xFF); + } + return bytes; + } else if (data instanceof ArrayBuffer) { + return Array.from(new Uint8Array(data)); + } else if (Array.isArray(data)) { + return data; + } else { + return Array.from(data); + } + } catch (error) { + this.emit('error', new Error(`数据转换失败: ${error.message}`)); + return []; + } + } + + /** + * 字节数组转字符串 + */ + bytesToString(bytes) { + try { + if (!bytes || bytes.length === 0) return ''; + + let str = ''; + for (let i = 0; i < bytes.length; i++) { + const byte = bytes[i] & 0xFF; + if (byte >= 32 && byte <= 126) { + str += String.fromCharCode(byte); + } else { + str += `\\x${byte.toString(16).padStart(2, '0')}`; + } + } + return str; + } catch (error) { + this.emit('error', new Error(`字节转字符串失败: ${error.message}`)); + return ''; + } + } + + /** + * 编码字符串(添加长度前缀) + */ + encodeString(str) { + let result = ''; + const bytes = this.stringToBytes(str); + result += String.fromCharCode((bytes.length >> 8) & 0xFF); + result += String.fromCharCode(bytes.length & 0xFF); + for (let i = 0; i < bytes.length; i++) { + result += String.fromCharCode(bytes[i]); + } + return result; + } + + /** + * 字符串转字节数组 + */ + stringToBytes(str) { + const bytes = []; + for (let i = 0; i < str.length; i++) { + bytes.push(str.charCodeAt(i)); + } + return bytes; + } + + /** + * 编码剩余长度 + */ + encodeRemainingLength(length) { + let result = ''; + do { + let byte = length % 128; + length = Math.floor(length / 128); + if (length > 0) { + byte |= 0x80; + } + result += String.fromCharCode(byte); + } while (length > 0); + return result; + } +} + +// 导出类 +export default MQTTClient; \ No newline at end of file diff --git a/uni_modules/yh-wsmqtt/wsmqtt/package.json b/uni_modules/yh-wsmqtt/wsmqtt/package.json new file mode 100644 index 0000000..27d5c1e --- /dev/null +++ b/uni_modules/yh-wsmqtt/wsmqtt/package.json @@ -0,0 +1,36 @@ +{ + "name": "uni-plugin-mqtt-websocket", + "version": "1.0.0", + "description": "基于 WebSocket 的 MQTT 客户端插件,支持 uni-app 多端运行", + "main": "js_sdk/index.js", + "keywords": [ + "uni-app", + "mqtt", + "websocket", + "iot", + "real-time" + ], + "author": "开发者", + "license": "MIT", + "repository": { + "type": "git", + "url": "" + }, + "bugs": { + "url": "" + }, + "homepage": "", + "uni_modules": { + "id": "wsmqtt", + "displayName": "MQTT WebSocket 客户端", + "description": "基于 WebSocket 的 MQTT 客户端插件", + "category": "network", + "platforms": { + "app-plus": {}, + "mp-weixin": {}, + "mp-alipay": {}, + "mp-baidu": {}, + "mp-toutiao": {} + } + } +} \ No newline at end of file