device-communication-node 1.0.0-beta.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.
@@ -0,0 +1,96 @@
1
+ const dgram = require('dgram')
2
+ const CommChannel = require('./CommChannel')
3
+
4
+ class UdpChannel extends CommChannel {
5
+ constructor(remoteHost, remotePort, localPort = 0) {
6
+ super()
7
+
8
+ this.remoteHost = remoteHost
9
+ this.remotePort = remotePort
10
+ this.localPort = localPort
11
+
12
+ this.socket = null
13
+ }
14
+
15
+ getIsOpen() {
16
+ return this.isOpen
17
+ }
18
+
19
+ getConfig() {
20
+ return {
21
+ remoteHost: this.remoteHost,
22
+ remotePort: this.remotePort,
23
+ localPort: this.localPort
24
+ }
25
+ }
26
+
27
+ open() {
28
+ return this._dedupeOpen(() => {
29
+
30
+ return new Promise((resolve, reject) => {
31
+
32
+ this.socket = dgram.createSocket('udp4')
33
+
34
+ const onError = (err) => {
35
+ reject(new Error(`UDP初始化失败: ${err.message}`))
36
+ }
37
+
38
+ this.socket.on('error', onError)
39
+
40
+ this.socket.on('message', (msg, rinfo) => {
41
+ this.onReceive(rinfo, msg)
42
+ })
43
+
44
+ this.socket.bind(this.localPort || 0, () => {
45
+
46
+ try {
47
+ this.socket.setBroadcast(true)
48
+ } catch (e) { }
49
+
50
+ this.localPort = this.socket.address().port
51
+
52
+ this.triggerOpen(this.socket)
53
+
54
+ this.socket.removeListener('error', onError)
55
+
56
+ resolve()
57
+ })
58
+ })
59
+ })
60
+ }
61
+
62
+ async close() {
63
+ if (!this.socket) return
64
+
65
+ return new Promise((resolve) => {
66
+ this.socket.close(() => {
67
+ this.isOpen = false
68
+ this.socket = null
69
+ this.triggerClose()
70
+ resolve()
71
+ })
72
+ })
73
+ }
74
+
75
+ send(data) {
76
+ if (!this.isOpen || !this.socket) {
77
+ throw new Error('UDP未打开')
78
+ }
79
+
80
+ return new Promise((resolve, reject) => {
81
+ this.socket.send(
82
+ data,
83
+ 0,
84
+ data.length,
85
+ this.remotePort,
86
+ this.remoteHost,
87
+ (err) => {
88
+ if (err) reject(err)
89
+ else resolve()
90
+ }
91
+ )
92
+ })
93
+ }
94
+ }
95
+
96
+ module.exports = UdpChannel
@@ -0,0 +1,387 @@
1
+ const EventEmitter = require('events');
2
+ const Task = require('../model/Task');
3
+ const InteractionPattern = require('../model/InteractionPattern');
4
+ const SchedulingStrategy = require('../model/SchedulingStrategy');
5
+ const hexUtils = require('../utils/hexUtils');
6
+
7
+ /**
8
+ * 通信调度器
9
+ */
10
+ class CommDispatcher extends EventEmitter {
11
+ constructor() {
12
+ super();
13
+ this.priorityQueue = [];
14
+ this.concurrentQueue = [];
15
+
16
+ this.currentTask = null;
17
+ this.lastReadBytes = null;
18
+ this.writeTimeout = 50;
19
+ this.responseTimeout = 500;
20
+ this.devices = new Set();
21
+
22
+ this.onAllTasksCompleted = null;
23
+ this.processing = false;
24
+
25
+ // 当前等待响应的 Promise resolve 函数
26
+ this._currentResolver = null;
27
+ // 销毁标记
28
+ this.isDisposed = false;
29
+ }
30
+
31
+ /**
32
+ * 动态添加设备实例(支持多设备挂载)
33
+ * @param {DeviceCore} device
34
+ */
35
+ setDevice(device) {
36
+ if (device) {
37
+ this.devices.add(device);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * 移除设备
43
+ */
44
+ async removeDevice(device) {
45
+ if (!device) return;
46
+
47
+ this.devices.delete(device);
48
+
49
+ if (this.devices.size === 0) {
50
+ await this.dispose();
51
+ }
52
+ }
53
+
54
+ /**
55
+ * 是否还有设备挂载
56
+ */
57
+ hasDevice() {
58
+ return this.devices.size > 0;
59
+ }
60
+
61
+ // --- 抽象方法由子类实现 ---
62
+ getConnectionString() {
63
+ throw new Error('getConnectionString() 必须由子类实现');
64
+ }
65
+
66
+ isOpen() {
67
+ throw new Error('isOpen() 必须由子类实现');
68
+ }
69
+
70
+ async open() {
71
+ throw new Error('open() 必须由子类实现');
72
+ }
73
+
74
+ async close() {
75
+ throw new Error('close() 必须由子类实现');
76
+ }
77
+
78
+ async write(task) {
79
+ throw new Error('write() 必须由子类实现');
80
+ }
81
+
82
+ getCharset() {
83
+ throw new Error('getCharset() 必须由子类实现');
84
+ }
85
+
86
+ /**
87
+ * 入队操作
88
+ * @param {*} schedulingStrategy 队列策略
89
+ * @param {*} interactionPattern 交互模式
90
+ * @param {*} writeBytes 发送字节数组
91
+ * @param {*} priority 优先级
92
+ * @param {*} retryCount 重试次数
93
+ * @param {*} timeout 单条命令超时时间
94
+ * @param {*} callback 回调函数
95
+ * @returns
96
+ */
97
+ enqueue(schedulingStrategy, interactionPattern, writeBytes, priority, retryCount, timeout, callback) {
98
+ if (this.isDisposed) return;
99
+ if (!writeBytes || writeBytes.length === 0) return;
100
+
101
+ const task = new Task(
102
+ writeBytes,
103
+ priority,
104
+ retryCount,
105
+ callback,
106
+ timeout || this.responseTimeout,
107
+ interactionPattern
108
+ );
109
+
110
+ if (schedulingStrategy === SchedulingStrategy.PRIORITY) {
111
+ this.priorityQueue.push(task);
112
+ // 优先级排序:数值越小,优先级越大
113
+ this.priorityQueue.sort((a, b) => a.compareTo(b));
114
+ } else {
115
+ this.concurrentQueue.push(task);
116
+ }
117
+
118
+ // 触发调度循环
119
+ this.processNext().catch(err => {
120
+ console.error('[Dispatcher] 调度循环崩溃:', err);
121
+ this.processing = false;
122
+ });
123
+ }
124
+
125
+
126
+ /**
127
+ * 获取下一个任务
128
+ */
129
+ getNextTask() {
130
+ if (this.priorityQueue.length > 0) return this.priorityQueue.shift();
131
+ if (this.concurrentQueue.length > 0) return this.concurrentQueue.shift();
132
+ return null;
133
+ }
134
+
135
+ /**
136
+ * 核心调度循环
137
+ */
138
+ async processNext() {
139
+ if (this.processing || this.isDisposed) return;
140
+ this.processing = true;
141
+
142
+ try {
143
+ let task;
144
+ while ((task = this.getNextTask()) !== null) {
145
+ let initialRetry = task.retryCount;
146
+ let retries = initialRetry;
147
+ let success = false;
148
+ let responseData = null;
149
+ let lastError = null; // 用来记录该任务历次尝试中最后的异常原因
150
+
151
+ while (retries >= 0 && !this.isDisposed) {
152
+ const currentAttempt = initialRetry - retries + 1;
153
+ const isRetry = currentAttempt > 1;
154
+ console.log(`[Dispatcher] 执行任务: ${hexUtils.bytesToHexString(task.writeBytes)} (尝试 ${currentAttempt}/${initialRetry + 1})${isRetry ? ' - 重试中' : ''}`);
155
+
156
+ try {
157
+ // 自动重连逻辑
158
+ if (!this.isOpen()) {
159
+ await this.open();
160
+ }
161
+
162
+ this.currentTask = task;
163
+ this.lastReadBytes = null;
164
+
165
+ // 执行写入
166
+ await Promise.race([
167
+ this.write(task),
168
+ new Promise((_, reject) => setTimeout(() => reject(new Error('物理层写入卡死/超时')), this.writeTimeout))
169
+ ]);
170
+ // 等待响应
171
+ if (task.interactionPattern === InteractionPattern.WAIT_RESPONSE) {
172
+ try {
173
+
174
+ await this.waitResponse(task.timeout);
175
+
176
+ responseData = this.lastReadBytes;
177
+ if (responseData && responseData.length > 0) {
178
+ success = true;
179
+ } else {
180
+ success = false;
181
+ lastError = new Error('设备响应数据为空');
182
+ }
183
+ } catch (waitErr) {
184
+ success = false;
185
+ lastError = waitErr;
186
+ }
187
+ } else {
188
+ success = true;
189
+ }
190
+
191
+ if (success) {
192
+ lastError = null;
193
+ break;
194
+ }
195
+
196
+ } catch (err) {
197
+
198
+ console.error(`[Dispatcher] 物理层或指令写入异常 (剩余重试:${retries}):`, err.message);
199
+ lastError = err;
200
+
201
+ if (this._currentResolver) {
202
+
203
+ this._currentResolver = null;
204
+ }
205
+
206
+ // 硬件异常时,触发断开重连机制
207
+ try {
208
+ await this.close();
209
+ } catch (e) {
210
+ }
211
+ }
212
+
213
+ retries--;
214
+ if (!success && retries >= 0 && !this.isDisposed) {
215
+ const backoff = 50 + (initialRetry - retries) * 30;
216
+ await this.sleep(backoff);
217
+ }
218
+ }
219
+
220
+ // 执行回调(透传第三个参数 lastError)
221
+ if (task.dataReceived && !this.isDisposed) {
222
+ try {
223
+ // 把最终收集到的错误(超时或硬件故障)原封不动扔给外部
224
+ task.dataReceived(responseData, task.writeBytes, lastError);
225
+ } catch (e) {
226
+ console.error('[Dispatcher] 回调执行失败:', e.message);
227
+ }
228
+ }
229
+
230
+ // 任务间隙间隔(取所有设备中最大的写入间隔)
231
+ let maxInterval = 0;
232
+ for (const device of this.devices) {
233
+ if (device.writeIntervalTime > maxInterval) {
234
+ maxInterval = device.writeIntervalTime;
235
+ }
236
+ }
237
+
238
+ if (maxInterval > 0 && !this.isDisposed) {
239
+ await this.sleep(maxInterval);
240
+ }
241
+
242
+ this.currentTask = null;
243
+ this.lastReadBytes = null;
244
+ }
245
+ } finally {
246
+ this.processing = false;
247
+ if (this.onAllTasksCompleted && !this.isDisposed) {
248
+ try {
249
+ this.onAllTasksCompleted();
250
+ } catch (e) {
251
+ }
252
+ }
253
+ }
254
+ }
255
+
256
+ /**
257
+ * 等待响应的 Promise 封装
258
+ */
259
+ waitResponse(timeout) {
260
+ return new Promise((resolve, reject) => {
261
+ let done = false;
262
+
263
+ const timer = setTimeout(() => {
264
+ if (done) return;
265
+ done = true;
266
+
267
+ this._currentResolver = null;
268
+ reject(new Error('响应超时'));
269
+ }, timeout);
270
+
271
+ this._currentResolver = {
272
+ resolve: (data) => {
273
+ if (done) return;
274
+ done = true;
275
+ clearTimeout(timer);
276
+ this._currentResolver = null;
277
+ resolve(data);
278
+ },
279
+ reject: (err) => {
280
+ if (done) return;
281
+ done = true;
282
+ clearTimeout(timer);
283
+ this._currentResolver = null;
284
+ reject(err);
285
+ }
286
+ };
287
+ });
288
+ }
289
+
290
+ /**
291
+ * 接收数据入口
292
+ */
293
+ receive(readBytes) {
294
+ // 只要还有设备在使用此通道,就允许接收
295
+ if (this.isDisposed || this.devices.size === 0) return;
296
+
297
+ // 任选一个设备来做粘包拆包
298
+ const tempDevice = this.devices.values().next().value;
299
+
300
+ tempDevice.receive(readBytes, (completeFrame) => {
301
+ this.handleCompleteFrame(completeFrame);
302
+ });
303
+ }
304
+
305
+ /**
306
+ * 处理设备拼好的完整帧
307
+ */
308
+ handleCompleteFrame(frame) {
309
+ // 处理<请求-响应>任务
310
+ if (this.currentTask && this._currentResolver) {
311
+ // 遍历所有设备,看这帧响应属于谁(或者只要有一个匹配上就通过)
312
+ let matched = false;
313
+ for (const dev of this.devices) {
314
+ if (dev.isMatch(this.currentTask.writeBytes, frame)) {
315
+ matched = true;
316
+ break;
317
+ }
318
+ }
319
+
320
+ if (matched) {
321
+ this.lastReadBytes = frame;
322
+ this._currentResolver.resolve(frame);
323
+ this._currentResolver = null;
324
+ return;
325
+ }
326
+ }
327
+
328
+ // 处理<主动上报>
329
+ // 将数据帧广播给当前通道下的<所有>设备实例
330
+ for (const dev of this.devices) {
331
+ try {
332
+ dev.onAutoReport(frame);
333
+ } catch (e) {
334
+ console.error(`[Dispatcher] 广播设备主动上报异常:`, e.message);
335
+ }
336
+ }
337
+ }
338
+
339
+ sleep(ms) {
340
+ return new Promise(resolve => setTimeout(resolve, ms));
341
+ }
342
+
343
+ /**
344
+ * 资源释放
345
+ */
346
+ async dispose() {
347
+ if (this.isDisposed) {
348
+ return;
349
+ }
350
+
351
+ this.isDisposed = true;
352
+
353
+ // 唤醒等待中的任务
354
+ if (this._currentResolver) {
355
+ this._currentResolver.reject(new Error('Dispatcher disposed'));
356
+ this._currentResolver = null;
357
+ }
358
+
359
+ // 清空任务
360
+ this.priorityQueue.length = 0;
361
+ this.concurrentQueue.length = 0;
362
+ this.currentTask = null;
363
+ this.lastReadBytes = null;
364
+
365
+ // 解除所有设备与 Dispatcher 的关联
366
+ for (const device of this.devices) {
367
+ if (device.commDispatcher === this) {
368
+ device.commDispatcher = null;
369
+ }
370
+ }
371
+ this.devices.clear();
372
+
373
+ // 关闭通信链路
374
+ try {
375
+ if (this.isOpen()) {
376
+ await this.close();
377
+ }
378
+ } catch (e) {
379
+ console.error('[Dispatcher] 关闭失败:', e);
380
+ }
381
+
382
+ // 移除所有事件监听
383
+ this.removeAllListeners();
384
+ }
385
+ }
386
+
387
+ module.exports = CommDispatcher;
@@ -0,0 +1,236 @@
1
+ const InteractionPattern = require('../model/InteractionPattern')
2
+ const SchedulingStrategy = require('../model/SchedulingStrategy')
3
+ const HexUtils = require('../utils/HexUtils');
4
+
5
+ /**
6
+ * 设备核心控制层
7
+ */
8
+ class DeviceCore {
9
+ constructor() {
10
+ /** @type {CommDispatcher} 通信调度器 */
11
+ this.commDispatcher = null;
12
+ /** 写入间隔时间 (ms) */
13
+ this.writeIntervalTime = 50;
14
+ /** 接收缓冲区 */
15
+ this.receiveBuffer = Buffer.alloc(0);
16
+ }
17
+
18
+ /**
19
+ * 设置调度器并绑定生命周期回调
20
+ */
21
+ setCommDispatcher(commDispatcher) {
22
+ this.commDispatcher = commDispatcher;
23
+ if (this.commDispatcher) {
24
+ this.commDispatcher.onAllTasksCompleted = () => this.onAllTasksCompleted();
25
+ }
26
+ }
27
+
28
+ /**
29
+ * 获取连接字符串
30
+ */
31
+ getConnectionString() {
32
+ this.commDispatcher ? this.commDispatcher.getConnectionString() : '未知名称';
33
+ }
34
+
35
+ /**
36
+ * 设置写入间隔时间(ms)
37
+ * @param {*} interval
38
+ */
39
+ setWriteIntervalTime(interval) {
40
+ this.writeIntervalTime = interval;
41
+ }
42
+
43
+ /**
44
+ * 获取实例
45
+ * @returns
46
+ */
47
+ static instance() {
48
+ if (!this._instance) {
49
+ this._instance = new this();
50
+ }
51
+ return this._instance;
52
+ }
53
+
54
+ /**
55
+ * 获取字符集
56
+ * @returns
57
+ */
58
+ getCharset() {
59
+ return this.commDispatcher ? this.commDispatcher.getCharset() : 'utf8';
60
+ }
61
+
62
+ /**
63
+ * 打开设备连接
64
+ */
65
+ async open() {
66
+ if (!this.commDispatcher) throw new Error("Dispatcher 未设置");
67
+ await this.commDispatcher.open();
68
+ }
69
+
70
+ /**
71
+ * 关闭设备连接
72
+ */
73
+ async close() {
74
+ if (!this.commDispatcher) throw new Error("Dispatcher 未设置");
75
+ await this.commDispatcher.close();
76
+ }
77
+
78
+ /**
79
+ * 基础写入方法
80
+ */
81
+ _baseWrite(schedulingStrategy, interactionPattern, writeBytes, priority, retryCount, timeout, dataReceived) {
82
+ if (!this.commDispatcher) return;
83
+ this.commDispatcher.enqueue(
84
+ schedulingStrategy,
85
+ interactionPattern,
86
+ writeBytes,
87
+ priority,
88
+ retryCount,
89
+ timeout,
90
+ dataReceived
91
+ );
92
+ }
93
+
94
+ /**
95
+ * 发送字节数据,动态参数选项支持重试、超时和回调
96
+ */
97
+ sendBytes(bytes, options = {}) {
98
+ const {
99
+ schedulingStrategy = SchedulingStrategy.FIFO,
100
+ interactionPattern = InteractionPattern.WAIT_RESPONSE,
101
+ priority = 10,
102
+ retry = 0,
103
+ timeout = this.commDispatcher ? this.commDispatcher.responseTimeout : 1000,
104
+ callback = (r, w) => this.defaultCallback(r, w)
105
+ } = options;
106
+
107
+ this._baseWrite(schedulingStrategy, interactionPattern, bytes, priority, retry, timeout, callback);
108
+ }
109
+
110
+ /**
111
+ * 同步写入并等待结果
112
+ * @param {Buffer|Uint8Array} frameBytes
113
+ * @param {Object} options {priority, retry, timeout, parser}
114
+ */
115
+ async sendSync(frameBytes, { priority = 10, retry = 0, timeout = 1000, parser = (r, w) => r } = {}) {
116
+ return new Promise((resolve, reject) => {
117
+ this.sendBytes(frameBytes, {
118
+ schedulingStrategy: SchedulingStrategy.FIFO,
119
+ interactionPattern: InteractionPattern.WAIT_RESPONSE,
120
+ priority,
121
+ retry,
122
+ timeout,
123
+ callback: (readBytes, writeBytes, error) => {
124
+ if (error || !readBytes) {
125
+ return reject(error || new Error('设备响应超时(重试耗尽)'));
126
+ }
127
+ try {
128
+ const result = parser(readBytes, writeBytes);
129
+ resolve(result);
130
+ } catch (e) {
131
+ reject(e);
132
+ }
133
+ }
134
+ });
135
+ });
136
+ }
137
+
138
+ /**
139
+ * 紧急发送 (插队) 并同步等待结果
140
+ * @param {Buffer|Uint8Array} frameBytes
141
+ * @param {Object} options {retry, timeout, parser}
142
+ */
143
+ async sendUrgentSync(frameBytes, { retry = 0, timeout = 1000, parser = (r, w) => r } = {}) {
144
+ return new Promise((resolve, reject) => {
145
+ this.sendBytes(frameBytes, {
146
+ schedulingStrategy: SchedulingStrategy.PRIORITY,
147
+ interactionPattern: InteractionPattern.WAIT_RESPONSE,
148
+ priority: 0,
149
+ retry,
150
+ timeout,
151
+ callback: (readBytes, writeBytes, error) => {
152
+ if (error || !readBytes) {
153
+ return reject(error || new Error('紧急指令响应超时'));
154
+ }
155
+ try {
156
+ const result = parser(readBytes, writeBytes);
157
+ resolve(result);
158
+ } catch (e) {
159
+ reject(e);
160
+ }
161
+ }
162
+ });
163
+ });
164
+ }
165
+
166
+ /**
167
+ * 基础校验
168
+ */
169
+ validate(readBytes) {
170
+ return readBytes && readBytes.length > 0;
171
+ }
172
+
173
+ /**
174
+ * 帧匹配校验
175
+ */
176
+ isMatch(writeBytes, readBytes) {
177
+ return this.validate(readBytes);
178
+ }
179
+
180
+ /**
181
+ * 帧解析逻辑 - 基类默认实现 (不解决拆包粘包)
182
+ * 子类需重写此方法以实现特定协议的拆包
183
+ */
184
+ parseFrame(onFrameReady) {
185
+ // 默认实现:直接把当前收到的全部内容当成一帧抛出
186
+ if (this.receiveBuffer.length > 0) {
187
+ const frame = Buffer.from(this.receiveBuffer);
188
+ this.receiveBuffer = Buffer.alloc(0); // 清空
189
+ if (this.validate(frame)) {
190
+ onFrameReady(frame);
191
+ }
192
+ }
193
+ }
194
+
195
+ /**
196
+ * 主动上报处理接口
197
+ */
198
+ onAutoReport(frame) {
199
+ const now = new Date().toISOString().replace('T', ' ').substring(0, 19);
200
+ console.log(`[${now}] 主动上报帧: ${HexUtils.bytesToHexString(frame)}`);
201
+ }
202
+
203
+ /**
204
+ * 默认回调
205
+ */
206
+ defaultCallback(readBytes, writeBytes) {
207
+ console.log("--- 默认回调 ---");
208
+ console.log("发送:", HexUtils.bytesToHexString(writeBytes));
209
+ console.log("接收:", HexUtils.bytesToHexString(readBytes));
210
+ }
211
+
212
+ /**
213
+ * 队列清空通知
214
+ */
215
+ onAllTasksCompleted() {
216
+ // 子类重写
217
+ }
218
+
219
+ /**
220
+ * 接收入口
221
+ * @param {Buffer} rawBytes 接收到的的原始碎包
222
+ * @param {Function} onFrameReady 拼好完整包后的回调
223
+ */
224
+ receive(rawBytes, onFrameReady) {
225
+ // 拼接到缓冲区
226
+ this.receiveBuffer = Buffer.concat([
227
+ this.receiveBuffer,
228
+ Buffer.from(rawBytes)
229
+ ]);
230
+
231
+ // 调用解析逻辑 (支持拆包粘包)
232
+ this.parseFrame(onFrameReady);
233
+ }
234
+ }
235
+
236
+ module.exports = DeviceCore;