@yarkivaev/source-to-sink 1.3.1 → 1.3.2
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/index.js +2 -0
- package/package.json +1 -1
- package/src/modbusRtuBusSource.js +118 -0
- package/test/test_modbusRtuBusSource.js +47 -0
package/index.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* - lokiSource: Loki polling source (accepts URL)
|
|
16
16
|
* - modbusSource: Modbus TCP polling source (accepts host/port)
|
|
17
17
|
* - modbusRtuSource: Modbus RTU polling source (accepts serial path)
|
|
18
|
+
* - modbusRtuBusSource: Modbus RTU multi-slave bus on one serial path
|
|
18
19
|
* - mqttSink: MQTT publishing sink (accepts URL)
|
|
19
20
|
*
|
|
20
21
|
* @example
|
|
@@ -39,5 +40,6 @@ export { default as stompSource } from './src/stompSource.js';
|
|
|
39
40
|
export { default as stompSend } from './src/stompSend.js';
|
|
40
41
|
export { default as lokiSource } from './src/lokiSource.js';
|
|
41
42
|
export { default as modbusSource, modbusRtuSource } from './src/modbusSource.js';
|
|
43
|
+
export { default as modbusRtuBusSource } from './src/modbusRtuBusSource.js';
|
|
42
44
|
export { default as mqttSink } from './src/mqttSink.js';
|
|
43
45
|
export { default as sqliteSink } from './src/sqliteSink.js';
|
package/package.json
CHANGED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import ModbusRTU from 'modbus-serial';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validates RTU bus source arguments.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} path - Serial device path
|
|
7
|
+
* @param {object} serial - Serial options with baudRate
|
|
8
|
+
* @param {Array} slaves - Slave descriptors
|
|
9
|
+
*/
|
|
10
|
+
function assertBusArgs(path, serial, slaves) {
|
|
11
|
+
if (typeof path !== 'string' || path.length === 0) {
|
|
12
|
+
throw new Error('Serial path must be a non-empty string');
|
|
13
|
+
}
|
|
14
|
+
if (!serial || typeof serial.baudRate !== 'number' || serial.baudRate <= 0) {
|
|
15
|
+
throw new Error('Serial baudRate must be a positive number');
|
|
16
|
+
}
|
|
17
|
+
if (!Array.isArray(slaves) || slaves.length === 0) {
|
|
18
|
+
throw new Error('Slaves must be a non-empty array');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Reads one slave holding-register block and forwards it to its collector.
|
|
24
|
+
*
|
|
25
|
+
* @param {object} client - ModbusRTU client
|
|
26
|
+
* @param {object} slave - slaveId and collector
|
|
27
|
+
* @param {number} address - PDU start address
|
|
28
|
+
* @param {number} count - Register count
|
|
29
|
+
* @returns {Promise<void>}
|
|
30
|
+
*/
|
|
31
|
+
function readSlave(client, slave, address, count) {
|
|
32
|
+
client.setID(slave.slaveId);
|
|
33
|
+
return client.readHoldingRegisters(address, count).then((result) => {
|
|
34
|
+
slave.collector.accept(result.data);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Polls all slaves on the bus sequentially.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} client - ModbusRTU client
|
|
42
|
+
* @param {Array} slaves - Slave descriptors
|
|
43
|
+
* @param {number} address - PDU start address
|
|
44
|
+
* @param {number} count - Register count
|
|
45
|
+
* @returns {Promise<void>}
|
|
46
|
+
*/
|
|
47
|
+
function pollSlaves(client, slaves, address, count) {
|
|
48
|
+
return slaves.reduce((chain, slave) => {
|
|
49
|
+
return chain.then(() => {
|
|
50
|
+
return readSlave(client, slave, address, count);
|
|
51
|
+
});
|
|
52
|
+
}, Promise.resolve());
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Modbus RTU bus source: one serial port, several slave IDs polled in sequence.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* const source = modbusRtuBusSource(
|
|
60
|
+
* '/dev/ttyUSB0',
|
|
61
|
+
* { baudRate: 9600, stopBits: 2, parity: 'none' },
|
|
62
|
+
* [{ slaveId: 1, collector: t1 }, { slaveId: 2, collector: t2 }],
|
|
63
|
+
* { address: 1280, count: 30, interval: 5 }
|
|
64
|
+
* );
|
|
65
|
+
* source.start();
|
|
66
|
+
*
|
|
67
|
+
* @param {string} path - Serial device path
|
|
68
|
+
* @param {object} serial - baudRate, dataBits, stopBits, parity
|
|
69
|
+
* @param {Array<{slaveId: number, collector: object}>} slaves - Slave poll targets
|
|
70
|
+
* @param {object} poll - address, count, interval, optional log
|
|
71
|
+
* @returns {object} Source with start() and stop()
|
|
72
|
+
*/
|
|
73
|
+
export default function modbusRtuBusSource(path, serial, slaves, poll) {
|
|
74
|
+
assertBusArgs(path, serial, slaves);
|
|
75
|
+
const log = poll.log || console;
|
|
76
|
+
const client = new ModbusRTU();
|
|
77
|
+
const line = {
|
|
78
|
+
baudRate: serial.baudRate,
|
|
79
|
+
dataBits: serial.dataBits || 8,
|
|
80
|
+
stopBits: serial.stopBits || 1,
|
|
81
|
+
parity: serial.parity || 'none'
|
|
82
|
+
};
|
|
83
|
+
let handle = null;
|
|
84
|
+
let opened = false;
|
|
85
|
+
function runPoll() {
|
|
86
|
+
pollSlaves(client, slaves, poll.address, poll.count).catch((err) => {
|
|
87
|
+
log.error(`RTU bus poll on ${path} failed: ${err.message}`);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function attempt() {
|
|
91
|
+
client.connectRTUBuffered(path, line).then(() => {
|
|
92
|
+
opened = true;
|
|
93
|
+
handle = setInterval(runPoll, poll.interval * 1000);
|
|
94
|
+
runPoll();
|
|
95
|
+
}).catch((err) => {
|
|
96
|
+
log.error(`Connection to ${path} failed, retrying: ${err.message}`);
|
|
97
|
+
handle = setTimeout(attempt, poll.interval * 1000);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
start() {
|
|
102
|
+
if (!(opened || handle)) {
|
|
103
|
+
attempt();
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
stop() {
|
|
107
|
+
if (handle) {
|
|
108
|
+
clearInterval(handle);
|
|
109
|
+
clearTimeout(handle);
|
|
110
|
+
handle = null;
|
|
111
|
+
}
|
|
112
|
+
if (opened) {
|
|
113
|
+
client.close();
|
|
114
|
+
opened = false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { describe, it } from 'mocha';
|
|
3
|
+
import modbusRtuBusSource from '../src/modbusRtuBusSource.js';
|
|
4
|
+
|
|
5
|
+
describe('modbusRtuBusSource', () => {
|
|
6
|
+
it('throws on empty serial path', () => {
|
|
7
|
+
assert.throws(
|
|
8
|
+
() => {
|
|
9
|
+
return modbusRtuBusSource('', { baudRate: 9600 }, [{ slaveId: 1, collector: { accept() {} } }], {
|
|
10
|
+
address: 1280,
|
|
11
|
+
count: 30,
|
|
12
|
+
interval: 5
|
|
13
|
+
});
|
|
14
|
+
},
|
|
15
|
+
/Serial path must be a non-empty string/u,
|
|
16
|
+
'Should reject empty serial path'
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('throws on empty slaves list', () => {
|
|
21
|
+
assert.throws(
|
|
22
|
+
() => {
|
|
23
|
+
return modbusRtuBusSource(`/dev/ttyUSB${Math.floor(Math.random() * 9)}`, { baudRate: 9600 }, [], {
|
|
24
|
+
address: 1280,
|
|
25
|
+
count: 30,
|
|
26
|
+
interval: 5
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
/Slaves must be a non-empty array/u,
|
|
30
|
+
'Should reject empty slaves array'
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('creates source with start method for multiple slaves', () => {
|
|
35
|
+
const path = `/dev/ttyUSB${Math.floor(Math.random() * 9)}`;
|
|
36
|
+
const source = modbusRtuBusSource(
|
|
37
|
+
path,
|
|
38
|
+
{ baudRate: 9600, stopBits: 2 },
|
|
39
|
+
[
|
|
40
|
+
{ slaveId: 1, collector: { accept() {} } },
|
|
41
|
+
{ slaveId: 2, collector: { accept() {} } }
|
|
42
|
+
],
|
|
43
|
+
{ address: 1280, count: 30, interval: 5 }
|
|
44
|
+
);
|
|
45
|
+
assert.strictEqual(typeof source.start, 'function', 'Should expose start for RTU bus source');
|
|
46
|
+
});
|
|
47
|
+
});
|