mm_mqtt_client 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,122 @@
1
- # Temporary Holding Version
1
+ # MQTT 客户端(mm_mqtt_client)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ ## 说明
4
+ 基于 `mqtt.js` 封装的独立 MQTT 客户端,提供连接、订阅、发布与 RPC 请求能力。模块不依赖全局变量与原型扩展,可通过 `Admin` 按作用域缓存实例。
5
+
6
+ ## 功能特点
7
+
8
+ - 支持连接、重连、断开
9
+ - 支持主题订阅、取消订阅与多回调
10
+ - 支持发布消息与 JSON 消息发送
11
+ - 支持 RPC 请求:`req` 回调方式、`reqAsync` Promise 方式
12
+ - 支持响应主题模板 `{topic}`
13
+ - 请求超时自动回收,超时后回调错误信息
14
+ - 服务端可调用客户端注册在 `methods` 中的方法
15
+
16
+ ## 依赖
17
+
18
+ - `mm_helper`
19
+ - `mqtt`
20
+
21
+ ## 安装
22
+
23
+ ```bash
24
+ npm install mm_mqtt_client
25
+ ```
26
+
27
+ ## 快速开始
28
+
29
+ ```javascript
30
+ const { MqttClient } = require('mm_mqtt_client');
31
+
32
+ const client = new MqttClient({
33
+ host: '127.0.0.1',
34
+ port: '8083',
35
+ protocol: 'ws',
36
+ client_id: 'service123',
37
+ username: 'admin',
38
+ password: 'asd123'
39
+ });
40
+
41
+ await client.run();
42
+
43
+ // 订阅主题
44
+ client.subscribe('topic/demo', (msg) => {
45
+ console.log('收到消息:', msg);
46
+ });
47
+
48
+ // 发布 JSON 消息
49
+ client.send('topic/demo', { id: 1, method: 'test', params: {} });
50
+
51
+ // RPC 请求(回调方式)
52
+ client.req('topic/demo', 'getUser', { id: 1 }, (res, error) => {
53
+ if (error) {
54
+ console.error(error);
55
+ return;
56
+ }
57
+ console.log('响应:', res);
58
+ });
59
+
60
+ // RPC 请求(Promise 方式)
61
+ const res = await client.reqAsync('topic/demo', 'getUser', { id: 1 });
62
+ ```
63
+
64
+ ## API
65
+
66
+ ### 类 `MqttClient`
67
+
68
+ | 方法 | 说明 | 参数 | 返回值 |
69
+ |---|---|---|---|
70
+ | run | 连接 MQTT 服务器 | - | Promise |
71
+ | init | 重置配置与连接状态 | config | void |
72
+ | reconnect | 重连,超过重试次数后重新初始化 | - | void |
73
+ | end | 断开连接 | - | void |
74
+ | on | 监听底层客户端事件 | event_name, func | void |
75
+ | subscribe | 订阅主题 | topic, func, options | Number(订阅标识) |
76
+ | unsubscribe | 取消订阅 | topic, key | void |
77
+ | publish | 发布消息 | topic, message, options | void |
78
+ | send | 发送 JSON 消息 | topic, msg, options | void |
79
+ | req | 发起 RPC 请求 | topic, method, params, func, options | void |
80
+ | reqAsync | 发起 RPC 请求并等待响应 | topic, method, params, options | Promise |
81
+ | receive | 处理收到的消息(请求响应/方法调用/普通消息) | topic, body | Promise |
82
+ | message | 触发主题订阅回调 | topic, msg | void |
83
+ | getResTopic | 按模板生成响应主题 | topic | String |
84
+ | getOptions | 合并发布/订阅选项 | options, type | Object |
85
+ | getMessageId | 生成请求消息 ID | client_id, method | String |
86
+
87
+ ### 导出
88
+
89
+ | 导出 | 说明 | 参数 | 返回值 |
90
+ |---|---|---|---|
91
+ | MqttClient | MQTT 客户端类 | config | MqttClient |
92
+ | admin | 管理器实例 | - | Admin |
93
+ | mqttAdmin | 获取指定作用域的客户端实例 | scope, config | MqttClient |
94
+
95
+ ## 配置项
96
+
97
+ | 配置 | 类型 | 默认值 | 说明 |
98
+ |---|---|---|---|
99
+ | host | String | `127.0.0.1` | 主机地址 |
100
+ | port | String | `8083` | 端口号 |
101
+ | protocol | String | `ws` | 协议类型 |
102
+ | client_id | String | `service123` | 客户端 ID |
103
+ | username | String | `admin` | 用户名 |
104
+ | password | String | `asd123` | 密码 |
105
+ | clean | Boolean | `false` | 是否清除会话 |
106
+ | subscribe_topic | Array | `[]` | 订阅主题列表 |
107
+ | response_topic | String | `{topic}_ack` | 响应主题模板 |
108
+ | publish | Object | `{ qos: 0, retain: false }` | 发布选项 |
109
+ | subscribe | Object | `{ qos: 0 }` | 订阅选项 |
110
+ | timeout | Number | `3000` | 请求超时时间(毫秒) |
111
+ | retry | Number | `5` | 重试次数 |
112
+
113
+ ## 目录结构
114
+
115
+ ```text
116
+ mm_mqtt_client/
117
+ ├── lib/
118
+ │ └── mqtt_client.js # MqttClient 实现
119
+ ├── index.js # 模块入口
120
+ ├── test.js # 自包含测试(不连接外部 broker)
121
+ └── package.json
122
+ ```
package/index.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * mm_mqtt_client 模块入口
3
+ * @description 提供独立的 MQTT 客户端,支持连接、订阅、发布与 RPC 请求
4
+ */
5
+ const MqttClient = require('./lib/mqtt_client');
6
+ const Admin = require('mm_corejs').Admin;
7
+
8
+ const admin = new Admin();
9
+ admin.setClass(MqttClient);
10
+
11
+ module.exports = {
12
+ MqttClient,
13
+ admin,
14
+ mqttAdmin(scope, config) {
15
+ if (!scope) {
16
+ scope = 'os';
17
+ }
18
+ return admin.get(scope, config);
19
+ }
20
+ };
@@ -0,0 +1,479 @@
1
+ const MQTT = require('mqtt');
2
+ require('mm_helper');
3
+
4
+ /**
5
+ * MQTT客户端类
6
+ * @class
7
+ */
8
+ class MqttClient {
9
+ /**
10
+ * 构造函数
11
+ * @param {Object} config 配置参数
12
+ */
13
+ constructor(config) {
14
+ /**
15
+ * 配置参数
16
+ */
17
+ this.config = Object.assign({
18
+ /**
19
+ * 主机地址
20
+ */
21
+ host: "127.0.0.1",
22
+ /**
23
+ * 端口号
24
+ */
25
+ port: "8083",
26
+ /**
27
+ * 协议类型
28
+ */
29
+ protocol: "ws",
30
+ /**
31
+ * 客户端ID
32
+ */
33
+ client_id: "service123",
34
+ /**
35
+ * 用户名
36
+ */
37
+ username: "admin",
38
+ /**
39
+ * 密码
40
+ */
41
+ password: "asd123",
42
+ /**
43
+ * 是否清除会话
44
+ */
45
+ clean: false,
46
+ /**
47
+ * 订阅主题
48
+ */
49
+ subscribe_topic: [
50
+ // "{topic}",
51
+ // "{topic}_ack"
52
+ ],
53
+ /**
54
+ * 响应主题
55
+ */
56
+ response_topic: "{topic}_ack",
57
+ /**
58
+ * 发布选项
59
+ */
60
+ publish: {
61
+ /**
62
+ * QoS等级
63
+ */
64
+ qos: 0,
65
+ /**
66
+ * 是否保留消息
67
+ */
68
+ retain: false
69
+ },
70
+ /**
71
+ * 订阅选项
72
+ */
73
+ subscribe: {
74
+ /**
75
+ * QoS等级
76
+ */
77
+ qos: 0
78
+ },
79
+ /**
80
+ * 超时时间
81
+ */
82
+ timeout: 3000,
83
+ /**
84
+ * 重试次数
85
+ */
86
+ retry: 5
87
+ }, config);
88
+
89
+ // mqtt客户端服务器
90
+ this.client = null;
91
+
92
+ /**
93
+ * 消息队列 (ID => 消息对象)
94
+ */
95
+ this._queue = {};
96
+
97
+ /**
98
+ * 订阅集合 (主题 => 函数列表)
99
+ */
100
+ this._subs = {};
101
+
102
+ /**
103
+ * 方法集合
104
+ */
105
+ var _this = this;
106
+ this.methods = {
107
+ /**
108
+ * 提供给服务端查看开放的函数数量
109
+ * @returns {Array} 方法名称列表
110
+ */
111
+ getMethod: function () {
112
+ return Object.keys(_this.methods)
113
+ }
114
+ };
115
+ /**
116
+ * 重试次数
117
+ */
118
+ this._retry = 0;
119
+ /**
120
+ * 是否正在连接
121
+ */
122
+ this._connecting = false;
123
+ /**
124
+ * 消息ID计数器
125
+ */
126
+ this._key_num = 0;
127
+
128
+ this._logger = console;
129
+ }
130
+ }
131
+
132
+ /**
133
+ * 日志记录
134
+ * @param {String} level 日志级别 (e.g., 'info', 'warn', 'error')
135
+ * @param {String} message 日志消息
136
+ * @param {...*} args 可选参数
137
+ */
138
+ MqttClient.prototype.log = function (level, message, ...args) {
139
+ this._logger[level](`[${this.constructor.name}] ${message}`, ...args);
140
+ }
141
+
142
+ /**
143
+ * 重新连接
144
+ */
145
+ MqttClient.prototype.reconnect = function () {
146
+ /**
147
+ * 重试次数增加
148
+ */
149
+ this._retry += 1;
150
+ if (this._retry > this.config.retry) {
151
+ try {
152
+ this.client.end();
153
+ this.init();
154
+ } catch (error) {
155
+ this.log('error', "Reconnect failed:", error);
156
+ }
157
+ }
158
+ }
159
+
160
+ /**
161
+ * 初始化
162
+ * @param {Object} config 配置参数
163
+ */
164
+ MqttClient.prototype.init = function (config) {
165
+ Object.assign(this.config, config);
166
+ this.client = {
167
+ connected: false,
168
+ };
169
+ this._retry = 0;
170
+ this._connecting = false;
171
+ };
172
+
173
+ /**
174
+ * 获取响应主题
175
+ * @param {String} topic 主题
176
+ * @returns {String} 响应主题
177
+ */
178
+ MqttClient.prototype.getResTopic = function (topic) {
179
+ return this.config.response_topic.replace('{topic}', topic);
180
+ }
181
+
182
+ /**
183
+ * 获取MQTT选项
184
+ * @param {Object} options 选项参数
185
+ * @param {String} options.qos QoS等级
186
+ * @param {Boolean} options.retain 是否保留消息
187
+ * @param {String} type 选项类型 (publish 或 subscribe)
188
+ * @returns {Object} MQTT选项
189
+ */
190
+ MqttClient.prototype.getOptions = function (options, type = 'publish') {
191
+ return Object.assign({
192
+ qos: this.config[type].qos,
193
+ retain: this.config[type].retain || false
194
+ }, options || {});
195
+ }
196
+
197
+ /**
198
+ * 运行MQTT
199
+ * @returns {Promise} 连接结果
200
+ */
201
+ MqttClient.prototype.run = function () {
202
+ this._connecting = true;
203
+ var err;
204
+ try {
205
+ var config = {
206
+ clientId: this.config.client_id,
207
+ username: this.config.username,
208
+ password: this.config.password,
209
+ clean: this.config.clean,
210
+ }
211
+ this.client = MQTT.connect(config);
212
+ if (this.client.on) {
213
+ return new Promise((resolve, reject) => {
214
+ this.client.on('connect', (packet, err) => {
215
+ this._connecting = false;
216
+ if (err) {
217
+ resolve(null);
218
+ reject(err);
219
+ } else {
220
+ this.client.on('message', async (topic, message) => {
221
+ await this.receive(topic, message.toString());
222
+ });
223
+ resolve(packet);
224
+ }
225
+ });
226
+ this.client.on("reconnect", this.reconnect.bind(this));
227
+ this.client.on("error", (error) => {
228
+ this.log('error', "Connection failed", error);
229
+ });
230
+ });
231
+ }
232
+ } catch (error) {
233
+ this._connecting = false;
234
+ err = error;
235
+ }
236
+ return new Promise((resolve, reject) => {
237
+ resolve(null);
238
+ reject(err);
239
+ });
240
+ };
241
+
242
+ /**
243
+ * 监听事件
244
+ * @param {String} event_name 事件名称
245
+ * @param {Function} func 回调函数
246
+ */
247
+ MqttClient.prototype.on = function (event_name, func) {
248
+ this.client.on(event_name, func);
249
+ };
250
+
251
+ /**
252
+ * 接收消息
253
+ * @param {String} topic 订阅板块
254
+ * @param {Object} msg 消息主体
255
+ */
256
+ MqttClient.prototype.message = function (topic, msg) {
257
+ if (this._subs[topic]) {
258
+ var list = this._subs[topic];
259
+ try {
260
+ for (var key in list) {
261
+ list[key](msg);
262
+ }
263
+ } catch (err) {
264
+ this.log('error', "Error in message callback:", err);
265
+ }
266
+ }
267
+ }
268
+
269
+ /**
270
+ * 接收消息
271
+ * @param {String} topic 订阅板块
272
+ * @param {Object} body 消息主体
273
+ */
274
+ MqttClient.prototype.receive = async function (topic, body) {
275
+ var json;
276
+ if (body && (body.indexOf('[') === 0) || body.indexOf('{') === 0) {
277
+ try {
278
+ json = JSON.parse(body);
279
+ } catch { }
280
+ }
281
+ if (json) {
282
+ var id = json.id;
283
+ if (json.result && id) {
284
+ var lt = this._queue;
285
+ var has = false;
286
+ for (var key in lt) {
287
+ var o = lt[key];
288
+ if (id === o.id) {
289
+ o.has_res = true;
290
+ o.func(json.result);
291
+ delete lt[key];
292
+ has = true;
293
+ break;
294
+ }
295
+ }
296
+ if (has) {
297
+ return;
298
+ }
299
+ } else if (json.method) {
300
+ var func;
301
+ var method = this.methods;
302
+ var arr = json.method.split('.');
303
+ for (var i = 0; i < arr.length; i++) {
304
+ var m = method[arr[i]];
305
+ if (m) {
306
+ method = m;
307
+ }
308
+ }
309
+ if (method && typeof (method) == 'function') {
310
+ func = method;
311
+ }
312
+ if (func) {
313
+ var ret = await func(json.params);
314
+ if (ret) {
315
+ var res = {};
316
+ if (id) {
317
+ res.id = id
318
+ }
319
+ res.result = ret;
320
+ this.send(this.getResTopic(topic), res);
321
+ }
322
+ return;
323
+ }
324
+ }
325
+ this.message(topic, json);
326
+ return
327
+ }
328
+ this.message(topic, body);
329
+ };
330
+
331
+
332
+ /**
333
+ * 订阅主题
334
+ * @param {String} topic - 主题名称
335
+ * @param {Function} func - 回调函数
336
+ * @param {Object} options - 订阅选项
337
+ * @param {Number} options.qos - QoS等级,默认值为0
338
+ * @returns {Number} 订阅标识
339
+ */
340
+ MqttClient.prototype.subscribe = function (topic, func, options) {
341
+ if (typeof topic !== 'string') throw new TypeError('topic must be string');
342
+ if (func && typeof func !== 'function') throw new TypeError('func must be function');
343
+ this.client.subscribe(topic, this.getOptions(options, 'subscribe'));
344
+ if (func) {
345
+ if (!this._subs[topic]) {
346
+ this._subs[topic] = {};
347
+ }
348
+ var key = this._key_num + 1;
349
+ this._subs[topic][key] = func;
350
+ return key;
351
+ }
352
+ }
353
+
354
+ /**
355
+ * 取消订阅
356
+ * @param {String} topic - 主题名称
357
+ * @param {Number} key - 订阅标识
358
+ */
359
+ MqttClient.prototype.unsubscribe = function (topic, key) {
360
+ if (typeof topic !== 'string') throw new TypeError('topic must be string');
361
+
362
+ this.client.unsubscribe(topic);
363
+ if (key) {
364
+ if (this._subs[topic]) {
365
+ delete this._subs[topic][key];
366
+ }
367
+ } else {
368
+ this._subs[topic] = {};
369
+ }
370
+ }
371
+
372
+ /**
373
+ * 断开连接
374
+ */
375
+ MqttClient.prototype.end = function () {
376
+ this.client.end();
377
+ };
378
+
379
+ /**
380
+ * 发布消息
381
+ * @param {String} topic - 主题名称
382
+ * @param {Object} message - 消息内容
383
+ * @param {Object} options - 发布选项
384
+ * @param {Number} options.qos - QoS等级,默认值为0
385
+ * @param {Boolean} options.retain - 是否保留消息,默认值为false
386
+ */
387
+ MqttClient.prototype.publish = function (topic, message, options) {
388
+ if (typeof topic !== 'string') throw new TypeError('topic must be string');
389
+ if (!this.client || !this.client.publish) {
390
+ this.log('warn', 'MQTT client not connected, message not sent:', topic);
391
+ return;
392
+ }
393
+
394
+ this.client.publish(topic, message, this.getOptions(options));
395
+ };
396
+
397
+ /**
398
+ * 发送消息
399
+ * @param {String} topic - 主题名称
400
+ * @param {Object} msg - 消息对象
401
+ * @param {Object} options - 发布选项
402
+ * @param {Number} options.qos - QoS等级,默认值为0
403
+ * @param {Boolean} options.retain - 是否保留消息,默认值为false
404
+ */
405
+ MqttClient.prototype.send = function (topic, msg, options) {
406
+ if (typeof topic !== 'string') throw new TypeError('topic must be string');
407
+
408
+ this.publish(topic, JSON.stringify(msg), this.getOptions(options));
409
+ };
410
+
411
+ /**
412
+ * 获取消息ID
413
+ * @param {String} client_id - 客户端ID
414
+ * @param {String} method - 方法名
415
+ * @returns {String} 消息ID
416
+ */
417
+ MqttClient.prototype.getMessageId = function (client_id, method) {
418
+ return client_id + "_" + method + "_" + Date.now();
419
+ }
420
+
421
+ /**
422
+ * 发送请求
423
+ * @param {String} topic - 主题名称
424
+ * @param {String} method - 方法名
425
+ * @param {Object} params - 参数
426
+ * @param {Function} func - 回调函数
427
+ * @param {Object} options - 请求选项
428
+ * @param {Number} options.qos - QoS等级,默认值为0
429
+ * @param {Boolean} options.retain - 是否保留消息,默认值为false
430
+ */
431
+ MqttClient.prototype.req = function (topic, method, params, func, options) {
432
+ if (typeof topic !== 'string') throw new TypeError('topic must be string');
433
+ if (typeof method !== 'string') throw new TypeError('method must be string');
434
+ if (func && typeof func !== 'function') throw new TypeError('func must be function');
435
+ var id = this.getMessageId(this.config.client_id, method);
436
+ var msg = {
437
+ id,
438
+ method,
439
+ params
440
+ };
441
+
442
+ if (func) {
443
+ msg.func = func;
444
+ this._queue[id] = msg;
445
+ setTimeout(() => {
446
+ if (!msg.has_res) {
447
+ msg.func(null, "request timeout!");
448
+ }
449
+ for (var key in this._queue) {
450
+ var o = this._queue[key];
451
+ if (id === o.id) {
452
+ delete this._queue[key];
453
+ break;
454
+ }
455
+ }
456
+ }, this.config.timeout);
457
+ }
458
+ this.send(topic, msg, this.getOptions(options));
459
+ };
460
+
461
+ /**
462
+ * @description 发送请求并等待响应,超时时间为3秒。
463
+ * @param {String} topic - 主题名称
464
+ * @param {String} method 请求方法
465
+ * @param {Object} params 传递参数
466
+ * @param {Object} options 请求选项
467
+ * @returns {Object} 返回响应结果
468
+ */
469
+ MqttClient.prototype.reqAsync = function (topic, method, params, options) {
470
+ var _this = this;
471
+ return new Promise((resolve, reject) => {
472
+ _this.req(topic, method, params, (res, error) => {
473
+ resolve(res);
474
+ reject(error);
475
+ }, this.getOptions(options));
476
+ });
477
+ };
478
+
479
+ module.exports = MqttClient;
package/package.json CHANGED
@@ -1,6 +1,35 @@
1
1
  {
2
2
  "name": "mm_mqtt_client",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.1",
4
+ "description": "MQTT客户端模块,基于 mqtt.js 提供连接、订阅、发布与 RPC 请求能力(基于 mm_helper,无全局/原型扩展依赖)",
5
+ "main": "index.js",
6
+ "dependencies": {
7
+ "mm_helper": "^1.0.0",
8
+ "mqtt": "^5.15.2"
9
+ },
10
+ "scripts": {
11
+ "test": "node test.js"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://gitee.com/qiuwenwu91/mm_mqtt_client.git"
16
+ },
17
+ "keywords": [
18
+ "mm_mod",
19
+ "mm_adapter",
20
+ "mm_mqtt_client",
21
+ "mqtt",
22
+ "client",
23
+ "Iot",
24
+ "app",
25
+ "game"
26
+ ],
27
+ "layer": "adapter",
28
+ "group": "in_adapter",
29
+ "author": "邱文武",
30
+ "license": "ISC",
31
+ "bugs": {
32
+ "url": "https://gitee.com/qiuwenwu91/mm_mqtt_client/issues"
33
+ },
34
+ "homepage": "https://gitee.com/qiuwenwu91/mm_mqtt_client#readme"
35
+ }
package/test.js ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * mm_mqtt_client 模块自包含测试
3
+ * @description 仅验证 MqttClient 类的实例化与核心 API 存在性,不连接外部 MQTT broker
4
+ */
5
+ const { MqttClient } = require('./index.js');
6
+
7
+ let pass_count = 0;
8
+ let fail_count = 0;
9
+
10
+ /**
11
+ * 断言测试结果
12
+ * @param {string} name 测试名称
13
+ * @param {boolean} cond 断言条件
14
+ * @param {string} detail 失败详情
15
+ */
16
+ function assert(name, cond, detail) {
17
+ if (cond) {
18
+ pass_count++;
19
+ console.log(' [通过] ' + name);
20
+ } else {
21
+ fail_count++;
22
+ console.log(' [失败] ' + name + (detail ? ' => ' + detail : ''));
23
+ }
24
+ }
25
+
26
+ /**
27
+ * 主测试流程
28
+ */
29
+ function main() {
30
+ console.log('== mm_mqtt_client 模块功能测试 ==');
31
+ const client = new MqttClient({
32
+ host: '127.0.0.1',
33
+ port: '8083',
34
+ protocol: 'ws',
35
+ client_id: 'test123',
36
+ username: 'admin',
37
+ password: 'asd123',
38
+ clean: false
39
+ });
40
+
41
+ assert('MqttClient 是构造函数', typeof MqttClient === 'function');
42
+ assert('MqttClient 实例化成功', client instanceof MqttClient);
43
+ assert('存在 subscribe 方法', typeof client.subscribe === 'function');
44
+ assert('存在 publish 方法', typeof client.publish === 'function');
45
+ assert('存在 send 方法', typeof client.send === 'function');
46
+ assert('存在 req 方法', typeof client.req === 'function');
47
+ assert('存在 reqAsync 方法', typeof client.reqAsync === 'function');
48
+ assert('存在 methods 对象', !!client.methods);
49
+
50
+ console.log('\n== 测试结果: 通过 ' + pass_count + ', 失败 ' + fail_count + ' ==');
51
+ if (fail_count > 0) {
52
+ process.exitCode = 1;
53
+ }
54
+ }
55
+
56
+ main();