@yarkivaev/source-to-sink 1.1.0 → 1.1.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/package.json +1 -1
- package/src/modbusSource.js +50 -7
- package/test/test_modbusSource.js +23 -0
package/package.json
CHANGED
package/src/modbusSource.js
CHANGED
|
@@ -4,12 +4,35 @@ import pollingSource from './pollingSource.js';
|
|
|
4
4
|
/**
|
|
5
5
|
* Idle state for Modbus connection.
|
|
6
6
|
*
|
|
7
|
-
* @returns {object} State with connected() returning false
|
|
7
|
+
* @returns {object} State with connected() returning false and no retry handle
|
|
8
8
|
*/
|
|
9
9
|
function idle() {
|
|
10
10
|
return {
|
|
11
11
|
connected() {
|
|
12
12
|
return false;
|
|
13
|
+
},
|
|
14
|
+
retrying() {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Retrying state for Modbus connection.
|
|
22
|
+
*
|
|
23
|
+
* @param {object} handle - Timer handle from setTimeout
|
|
24
|
+
* @returns {object} State with retrying() returning true
|
|
25
|
+
*/
|
|
26
|
+
function retrying(handle) {
|
|
27
|
+
return {
|
|
28
|
+
connected() {
|
|
29
|
+
return false;
|
|
30
|
+
},
|
|
31
|
+
retrying() {
|
|
32
|
+
return true;
|
|
33
|
+
},
|
|
34
|
+
cancel() {
|
|
35
|
+
clearTimeout(handle);
|
|
13
36
|
}
|
|
14
37
|
};
|
|
15
38
|
}
|
|
@@ -25,6 +48,9 @@ function connected(client) {
|
|
|
25
48
|
connected() {
|
|
26
49
|
return true;
|
|
27
50
|
},
|
|
51
|
+
retrying() {
|
|
52
|
+
return false;
|
|
53
|
+
},
|
|
28
54
|
disconnect() {
|
|
29
55
|
client.close();
|
|
30
56
|
}
|
|
@@ -51,9 +77,10 @@ function connected(client) {
|
|
|
51
77
|
* @param {number} interval - Polling interval in seconds
|
|
52
78
|
* @param {object} collector - Collector with accept() method
|
|
53
79
|
* @param {object} clk - Clock with millis() method
|
|
80
|
+
* @param {object} [log] - Logger with error() method for connection failures
|
|
54
81
|
* @returns {object} Source with start() and stop() methods
|
|
55
82
|
*/
|
|
56
|
-
export default function modbusSource(host, port, address, count, interval, collector, clk) {
|
|
83
|
+
export default function modbusSource(host, port, address, count, interval, collector, clk, log = console) {
|
|
57
84
|
if (typeof host !== 'string' || host.length === 0) {
|
|
58
85
|
throw new Error('Host must be a non-empty string');
|
|
59
86
|
}
|
|
@@ -72,24 +99,40 @@ export default function modbusSource(host, port, address, count, interval, colle
|
|
|
72
99
|
const result = await client.readHoldingRegisters(address, count);
|
|
73
100
|
return [result.data];
|
|
74
101
|
};
|
|
102
|
+
const maxDelay = 300;
|
|
75
103
|
const source = pollingSource(fetch, interval, collector, clk);
|
|
104
|
+
let delay = interval;
|
|
105
|
+
const attempt = () => {
|
|
106
|
+
client.connectTCP(host, { port }).then(() => {
|
|
107
|
+
delay = interval;
|
|
108
|
+
state = connected(client);
|
|
109
|
+
source.start();
|
|
110
|
+
}).catch((err) => {
|
|
111
|
+
log.error(`Connection to ${host}:${port} failed, retrying in ${delay}s: ${err.message}`);
|
|
112
|
+
const handle = setTimeout(attempt, delay * 1000);
|
|
113
|
+
state = retrying(handle);
|
|
114
|
+
delay = Math.min(delay * 2, maxDelay);
|
|
115
|
+
});
|
|
116
|
+
};
|
|
76
117
|
return {
|
|
77
118
|
/**
|
|
78
119
|
* Connects to the Modbus device and starts polling.
|
|
79
120
|
*/
|
|
80
|
-
|
|
81
|
-
if (state.connected()) {
|
|
121
|
+
start() {
|
|
122
|
+
if (state.connected() || state.retrying()) {
|
|
82
123
|
return;
|
|
83
124
|
}
|
|
84
|
-
|
|
85
|
-
state = connected(client);
|
|
86
|
-
source.start();
|
|
125
|
+
attempt();
|
|
87
126
|
},
|
|
88
127
|
/**
|
|
89
128
|
* Stops polling and disconnects from the Modbus device.
|
|
90
129
|
*/
|
|
91
130
|
stop() {
|
|
92
131
|
source.stop();
|
|
132
|
+
if (state.retrying()) {
|
|
133
|
+
state.cancel();
|
|
134
|
+
state = idle();
|
|
135
|
+
}
|
|
93
136
|
if (state.connected()) {
|
|
94
137
|
state.disconnect();
|
|
95
138
|
state = idle();
|
|
@@ -93,4 +93,27 @@ describe('modbusSource', () => {
|
|
|
93
93
|
source.stop();
|
|
94
94
|
assert.strictEqual(true, true, 'Should not throw when stopping without start');
|
|
95
95
|
});
|
|
96
|
+
|
|
97
|
+
it('retries connection on failure', async () => {
|
|
98
|
+
let logged = false;
|
|
99
|
+
const log = { error: () => { logged = true; } };
|
|
100
|
+
const collector = { accept: () => {} };
|
|
101
|
+
const clk = fakeClock(Math.floor(Math.random() * 10000));
|
|
102
|
+
const source = modbusSource(`\u00df${Math.random()}`, 502, 4000, 14, 5, collector, clk, log);
|
|
103
|
+
source.start();
|
|
104
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
105
|
+
source.stop();
|
|
106
|
+
assert.strictEqual(logged, true, 'Should log connection failure and retry');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('does not crash process on connection failure', async () => {
|
|
110
|
+
const log = { error: () => {} };
|
|
111
|
+
const collector = { accept: () => {} };
|
|
112
|
+
const clk = fakeClock(Math.floor(Math.random() * 10000));
|
|
113
|
+
const source = modbusSource(`\u0420${Math.random()}`, 502, 4000, 14, 5, collector, clk, log);
|
|
114
|
+
source.start();
|
|
115
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
116
|
+
source.stop();
|
|
117
|
+
assert.strictEqual(true, true, 'Should not crash on unreachable host');
|
|
118
|
+
});
|
|
96
119
|
});
|