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.
- package/LICENSE +21 -0
- package/README.md +173 -0
- package/package.json +28 -0
- package/src/device/channel/CommChannel.js +73 -0
- package/src/device/channel/SerialChannel.js +112 -0
- package/src/device/channel/TcpChannel.js +84 -0
- package/src/device/channel/TcpServerChannel.js +149 -0
- package/src/device/channel/UdpChannel.js +96 -0
- package/src/device/core/CommDispatcher.js +387 -0
- package/src/device/core/DeviceCore.js +236 -0
- package/src/device/core/commDispatcherManager.js +105 -0
- package/src/device/core/dispatchers/SerialDispatcher.js +77 -0
- package/src/device/core/dispatchers/TcpDispatcher.js +72 -0
- package/src/device/core/dispatchers/TcpServerDispatcher.js +99 -0
- package/src/device/core/dispatchers/UdpDispatcher.js +77 -0
- package/src/device/core/factories/serialFactory.js +32 -0
- package/src/device/core/factories/tcpFactory.js +24 -0
- package/src/device/core/factories/tcpserverFactory.js +61 -0
- package/src/device/core/factories/udpFactory.js +47 -0
- package/src/device/model/InteractionPattern.js +16 -0
- package/src/device/model/SchedulingStrategy.js +13 -0
- package/src/device/model/Task.js +155 -0
- package/src/device/utils/ByteUtils.js +62 -0
- package/src/device/utils/CheckUtils.js +57 -0
- package/src/device/utils/HexUtils.js +100 -0
- package/src/device/utils/NetworkUtils.js +50 -0
- package/src/drivers/test/DeviceTest.js +49 -0
- package/src/index.js +14 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
const InteractionPattern = require('./InteractionPattern')
|
|
2
|
+
|
|
3
|
+
class Task {
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 全局序列生成器
|
|
7
|
+
*/
|
|
8
|
+
static seqGenerator = 0
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 写入数据 Buffer
|
|
12
|
+
* @type {Buffer}
|
|
13
|
+
*/
|
|
14
|
+
writeBytes
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 回调函数 (readBytes, writeBytes)
|
|
18
|
+
* @type {Function}
|
|
19
|
+
*/
|
|
20
|
+
dataReceived
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 响应超时时间
|
|
24
|
+
* @type {number}
|
|
25
|
+
*/
|
|
26
|
+
timeout = 500
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 重试次数
|
|
30
|
+
* @type {number}
|
|
31
|
+
*/
|
|
32
|
+
retryCount = 0
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 优先级(越小越优先)
|
|
36
|
+
* @type {number}
|
|
37
|
+
*/
|
|
38
|
+
priority = 0
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 通信模式
|
|
42
|
+
*/
|
|
43
|
+
interactionPattern = InteractionPattern.FIRE_AND_FORGET
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 序列号
|
|
47
|
+
*/
|
|
48
|
+
sequence
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 构造函数
|
|
52
|
+
* @param {*} writeBytes 写入的字节数组
|
|
53
|
+
* @param {*} priority 优先级
|
|
54
|
+
* @param {*} retryCount 重试次数
|
|
55
|
+
* @param {*} dataReceived 回调函数
|
|
56
|
+
* @param {*} timeout 超时时间
|
|
57
|
+
* @param {*} interactionPattern 任务交互模式
|
|
58
|
+
*/
|
|
59
|
+
constructor(
|
|
60
|
+
writeBytes,
|
|
61
|
+
priority,
|
|
62
|
+
retryCount,
|
|
63
|
+
dataReceived,
|
|
64
|
+
timeout,
|
|
65
|
+
interactionPattern
|
|
66
|
+
) {
|
|
67
|
+
this.writeBytes = writeBytes
|
|
68
|
+
this.priority = priority
|
|
69
|
+
this.retryCount = retryCount
|
|
70
|
+
this.dataReceived = dataReceived
|
|
71
|
+
this.interactionPattern = interactionPattern
|
|
72
|
+
|
|
73
|
+
if (typeof timeout === 'number') {
|
|
74
|
+
this.timeout = timeout
|
|
75
|
+
}
|
|
76
|
+
this.sequence = Task.seqGenerator++
|
|
77
|
+
}
|
|
78
|
+
compareTo(other) {
|
|
79
|
+
|
|
80
|
+
// 优先级(越小越优先)
|
|
81
|
+
let res = this.priority - other.priority
|
|
82
|
+
|
|
83
|
+
// 序列号(先进先出)
|
|
84
|
+
if (res === 0) {
|
|
85
|
+
res = this.sequence - other.sequence
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return res
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* getter: writeBytes
|
|
93
|
+
*/
|
|
94
|
+
getWriteBytes() {
|
|
95
|
+
return this.writeBytes
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* getter: callback
|
|
100
|
+
*/
|
|
101
|
+
getDataReceived() {
|
|
102
|
+
return this.dataReceived
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* timeout
|
|
107
|
+
*/
|
|
108
|
+
getTimeout() {
|
|
109
|
+
return this.timeout
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* retry
|
|
114
|
+
*/
|
|
115
|
+
getRetryCount() {
|
|
116
|
+
return this.retryCount
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* priority
|
|
121
|
+
*/
|
|
122
|
+
getPriority() {
|
|
123
|
+
return this.priority
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* sequence
|
|
128
|
+
*/
|
|
129
|
+
getSequence() {
|
|
130
|
+
return this.sequence
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* set retry
|
|
135
|
+
*/
|
|
136
|
+
setRetryCount(retryCount) {
|
|
137
|
+
this.retryCount = retryCount
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* getter interactionPattern
|
|
142
|
+
*/
|
|
143
|
+
getSchedulingStrategy() {
|
|
144
|
+
return this.interactionPattern
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* setter interactionPattern
|
|
149
|
+
*/
|
|
150
|
+
setInteractionPattern(interactionPattern) {
|
|
151
|
+
this.interactionPattern = interactionPattern
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = Task
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* 截取 Buffer
|
|
4
|
+
*
|
|
5
|
+
* @param {Buffer|Uint8Array} buffer 原始数据
|
|
6
|
+
* @param {number} start 起始索引
|
|
7
|
+
* @param {number} end 结束索引 (包含此位)
|
|
8
|
+
* @returns {Buffer} 新的子数组
|
|
9
|
+
*/
|
|
10
|
+
const slice = (buffer, start, end) => {
|
|
11
|
+
if (!buffer || !(buffer instanceof Uint8Array)) {
|
|
12
|
+
throw new Error("Input must be a Buffer or Uint8Array");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (start < 0 || end >= buffer.length || start > end) {
|
|
16
|
+
throw new Error(`Invalid start or end index: start=${start}, end=${end}, length=${buffer.length}`);
|
|
17
|
+
}
|
|
18
|
+
return Buffer.from(buffer.subarray(start, end + 1));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 合并多个 Buffer
|
|
23
|
+
*
|
|
24
|
+
* @param {...(Buffer|Uint8Array)} buffers 多个数组参数
|
|
25
|
+
* @returns {Buffer} 合并后的新 Buffer
|
|
26
|
+
*/
|
|
27
|
+
const merge = (...buffers) => {
|
|
28
|
+
if (!buffers || buffers.length === 0) {
|
|
29
|
+
return Buffer.alloc(0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 过滤掉 null 或 undefined 的部分
|
|
33
|
+
const validBuffers = buffers.filter(b => b !== null && b !== undefined);
|
|
34
|
+
|
|
35
|
+
if (validBuffers.length === 0) {
|
|
36
|
+
return Buffer.alloc(0);
|
|
37
|
+
}
|
|
38
|
+
return Buffer.concat(validBuffers);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* CIDR 转子网掩码 byte 数组
|
|
42
|
+
* 例如:24 => 255.255.255.0 => [0xFF, 0xFF, 0xFF, 0x00]
|
|
43
|
+
*/
|
|
44
|
+
const cidrToSubnetBytes = (cidr) => {
|
|
45
|
+
const mask = [];
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < 4; i++) {
|
|
48
|
+
if (cidr >= 8) {
|
|
49
|
+
mask.push(255);
|
|
50
|
+
cidr -= 8;
|
|
51
|
+
} else if (cidr > 0) {
|
|
52
|
+
const value = (0xFF << (8 - cidr)) & 0xFF;
|
|
53
|
+
mask.push(value);
|
|
54
|
+
cidr = 0;
|
|
55
|
+
} else {
|
|
56
|
+
mask.push(0);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return mask;
|
|
61
|
+
}
|
|
62
|
+
module.exports = { slice, merge, cidrToSubnetBytes }
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* 针对分子筛硬件的CRC8计算
|
|
4
|
+
* @param {Buffer|Uint8Array} data
|
|
5
|
+
* @param {number} offset
|
|
6
|
+
* @param {number} length
|
|
7
|
+
* @returns {number}
|
|
8
|
+
*/
|
|
9
|
+
const calculateCrc8 = (data, offset, length) => {
|
|
10
|
+
let crc = 0xFF
|
|
11
|
+
|
|
12
|
+
for (let idx = 0; idx < length; idx++) {
|
|
13
|
+
let ch1 = data[idx + offset] & 0xFF
|
|
14
|
+
|
|
15
|
+
for (let i = 0; i < 8; i++) {
|
|
16
|
+
if (((crc ^ ch1) & 0x80) > 0) {
|
|
17
|
+
crc = ((crc << 1) ^ 0x1D) & 0xFF
|
|
18
|
+
} else {
|
|
19
|
+
crc = (crc << 1) & 0xFF
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
ch1 = (ch1 << 1) & 0xFF
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (crc ^ 0xFF) & 0xFF
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 计算 Modbus CRC16
|
|
31
|
+
* 遵循低位在前(Little-endian)的标准输出
|
|
32
|
+
*
|
|
33
|
+
* @param {Uint8Array|number[]} data 数据字节数组
|
|
34
|
+
* @returns {number[]} [CRC低字节, CRC高字节]
|
|
35
|
+
*/
|
|
36
|
+
const getModbusCRC16 = (data) => {
|
|
37
|
+
let crc = 0xFFFF;
|
|
38
|
+
|
|
39
|
+
for (const b of data) {
|
|
40
|
+
crc ^= (b & 0xFF);
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < 8; i++) {
|
|
43
|
+
if ((crc & 0x0001) !== 0) {
|
|
44
|
+
crc = (crc >>> 1) ^ 0xA001;
|
|
45
|
+
} else {
|
|
46
|
+
crc >>>= 1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const low = crc & 0xFF;
|
|
52
|
+
const high = (crc >>> 8) & 0xFF;
|
|
53
|
+
|
|
54
|
+
return [low, high];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { calculateCrc8, getModbusCRC16 }
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* 将 16 进制字符串转回 Buffer
|
|
4
|
+
*/
|
|
5
|
+
const hexToBytes = (hexString) => {
|
|
6
|
+
if (!hexString || typeof hexString !== 'string') {
|
|
7
|
+
return Buffer.alloc(0)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const cleanHex = hexString.replace(/\s+/g, '')
|
|
12
|
+
return Buffer.from(cleanHex, 'hex')
|
|
13
|
+
} catch (e) {
|
|
14
|
+
console.error('非法 16 进制字符串:', e.message)
|
|
15
|
+
return Buffer.alloc(0)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Buffer 转 HEX字符串
|
|
21
|
+
*/
|
|
22
|
+
const bytesToHexString = (bytes) => {
|
|
23
|
+
if (!bytes || bytes.length === 0) {
|
|
24
|
+
return ''
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const hex = Buffer.from(bytes).toString('hex')
|
|
28
|
+
|
|
29
|
+
return hex
|
|
30
|
+
.replace(/.{2}/g, '$& ')
|
|
31
|
+
.trim()
|
|
32
|
+
.toUpperCase()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 单个字节转 HEX 字符串
|
|
37
|
+
* @param {number} byte - 0-255 的整数
|
|
38
|
+
*/
|
|
39
|
+
const byteToHexString = (byte) => {
|
|
40
|
+
// 确保是 0xFF 范围内的单字节,并转为16进制字符串
|
|
41
|
+
// padStart(2, '0') 确保结果始终是两位数,例如 9 变成 09
|
|
42
|
+
return (byte & 0xFF)
|
|
43
|
+
.toString(16)
|
|
44
|
+
.padStart(2, '0')
|
|
45
|
+
.toUpperCase();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 10进制转2位HEX
|
|
50
|
+
*/
|
|
51
|
+
const toHexByteFast = (value) => {
|
|
52
|
+
return (value & 0xFF)
|
|
53
|
+
.toString(16)
|
|
54
|
+
.toUpperCase()
|
|
55
|
+
.padStart(2, '0')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 单字节转HEX
|
|
60
|
+
*/
|
|
61
|
+
const byteToHex = (b) => {
|
|
62
|
+
return this.toHexByteFast(b)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 10进制转2字节数组(高字节在前)
|
|
67
|
+
* 例如:
|
|
68
|
+
* 258 => [0x01, 0x02]
|
|
69
|
+
*/
|
|
70
|
+
const intTo2Bytes = (value) => {
|
|
71
|
+
return Buffer.from([
|
|
72
|
+
(value >> 8) & 0xFF,
|
|
73
|
+
value & 0xFF
|
|
74
|
+
])
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 10进制转2字节数组(低字节在前)
|
|
79
|
+
* @param {*} value
|
|
80
|
+
* @returns
|
|
81
|
+
*/
|
|
82
|
+
const intTo2BytesLE = (value) => {
|
|
83
|
+
return Buffer.from([
|
|
84
|
+
value & 0xFF, // 先放低位
|
|
85
|
+
(value >> 8) & 0xFF // 后放高位
|
|
86
|
+
])
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 10进制转1字节数组
|
|
91
|
+
* @param {*} value
|
|
92
|
+
* @returns
|
|
93
|
+
*/
|
|
94
|
+
const intTo1Byte = (value) => {
|
|
95
|
+
return Buffer.from([
|
|
96
|
+
value & 0xFF
|
|
97
|
+
])
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = { hexToBytes, bytesToHexString, byteToHexString, toHexByteFast, byteToHex, intTo2Bytes, intTo2BytesLE, intTo1Byte }
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 获取真实局域网 IPv4
|
|
5
|
+
* 自动过滤:
|
|
6
|
+
* - 127.0.0.1
|
|
7
|
+
* - VMware
|
|
8
|
+
* - VirtualBox
|
|
9
|
+
* - Hyper-V
|
|
10
|
+
* - Docker
|
|
11
|
+
* - WSL
|
|
12
|
+
* - vEthernet
|
|
13
|
+
*/
|
|
14
|
+
const getLocalIP = () => {
|
|
15
|
+
const interfaces = os.networkInterfaces();
|
|
16
|
+
|
|
17
|
+
for (const name of Object.keys(interfaces)) {
|
|
18
|
+
|
|
19
|
+
// 过滤虚拟网卡
|
|
20
|
+
const lowerName = name.toLowerCase();
|
|
21
|
+
|
|
22
|
+
if (
|
|
23
|
+
lowerName.includes('vmware') ||
|
|
24
|
+
lowerName.includes('virtual') ||
|
|
25
|
+
lowerName.includes('hyper-v') ||
|
|
26
|
+
lowerName.includes('vethernet') ||
|
|
27
|
+
lowerName.includes('docker') ||
|
|
28
|
+
lowerName.includes('wsl')
|
|
29
|
+
) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const net of interfaces[name]) {
|
|
34
|
+
|
|
35
|
+
// 只要 IPv4 且不是回环地址
|
|
36
|
+
if (
|
|
37
|
+
net.family === 'IPv4' &&
|
|
38
|
+
!net.internal
|
|
39
|
+
) {
|
|
40
|
+
return net.address;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
getLocalIP
|
|
50
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const DeviceCore = require('../../device/core/DeviceCore');
|
|
2
|
+
const checkUtils = require('../../device/utils/checkUtils');
|
|
3
|
+
const hexUtils = require('../../device/utils/hexUtils');
|
|
4
|
+
/**
|
|
5
|
+
* 测试设备
|
|
6
|
+
*/
|
|
7
|
+
class DeviceTest extends DeviceCore {
|
|
8
|
+
constructor() {
|
|
9
|
+
super();
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 发送
|
|
13
|
+
*/
|
|
14
|
+
async tesetSend() {
|
|
15
|
+
// 构建的完整协议帧
|
|
16
|
+
const completeFrame = Buffer.from([0x02, 0x03, 0x00, 0x0A, 0x00, 0x03, 0x25, 0xFA]);
|
|
17
|
+
|
|
18
|
+
return await this.sendSync(completeFrame, {
|
|
19
|
+
retry: 0,
|
|
20
|
+
timeout: 500,
|
|
21
|
+
parser: (readyBytes, writeBytes) => {
|
|
22
|
+
|
|
23
|
+
// 数据长度校验,应为 6
|
|
24
|
+
if (readyBytes[2] !== 6) {
|
|
25
|
+
throw new Error(`返回数据长度异常:${readyBytes[2]}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (readyBytes.length < 11) {
|
|
29
|
+
throw new Error(`返回数据长度不足:${readyBytes.length}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const readUInt16 = (offset) =>
|
|
33
|
+
((readyBytes[offset] << 8) | readyBytes[offset + 1]) >>> 0;
|
|
34
|
+
|
|
35
|
+
const internalTemperature = readUInt16(3) / 10 - 25;
|
|
36
|
+
const externalTemperature = readUInt16(5) / 10 - 25;
|
|
37
|
+
const humidity = readUInt16(7) / 10;
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
internalTemperature,
|
|
41
|
+
externalTemperature,
|
|
42
|
+
humidity
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = DeviceTest;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const CommDispatcherManager = require('./device/core/commDispatcherManager')
|
|
2
|
+
const DeviceTest = require('./drivers/test/DeviceTest')
|
|
3
|
+
|
|
4
|
+
const main = async () => {
|
|
5
|
+
|
|
6
|
+
const dispatcher = CommDispatcherManager.create("tcp", "192.168.1.113:8234")
|
|
7
|
+
const device = new DeviceTest();
|
|
8
|
+
device.setCommDispatcher(dispatcher)
|
|
9
|
+
dispatcher.setDevice(device)
|
|
10
|
+
console.log(await device.tesetSend())
|
|
11
|
+
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
main();
|