@jotiotech/node-red-sia-premise-device 0.2.2 → 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 +13 -2
- package/package.json +1 -1
- package/src/SIA_device.html +1 -1
- package/src/SIA_device.js +222 -139
- package/src/SIA_server.html +2 -11
- package/src/SIA_server.js +63 -37
- package/src/table_crc.js +36 -0
package/README.md
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
# SIA Device
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Simple node-red library that manages connection to SIA server and helps with creation of packets to send to if from connected devices.
|
|
4
|
+
It just acts as a middle man that for now takes the SIA body and creates rest of the packet to send to the server and handles the ACK or lack of ACK from it.
|
|
5
|
+
|
|
6
|
+
For now just basic body is supported (not extended one with things like position and what not) and the user needs to create the body in correct SIA format that will get interpreted by the server.
|
|
7
|
+
However in future releases it's planned to have a much more streamline approach where user just sends code, name, id, zone area and what not and node will create the body by itself.
|
|
8
|
+
|
|
9
|
+
Server is configured in it's special node, on the input to SIA Device node user must input following parameters. Account setting will be provided by your SIA server administrator.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
payload.account - hex string with account number
|
|
13
|
+
payload.accountPrefix - hex string with account prefix
|
|
14
|
+
payload.body - SIA payload body, e.g. Nri129^^pi4^FA
|
|
15
|
+
```
|
|
4
16
|
|
|
5
|
-
Warning: ONLY ASCII characters are supported, if you ever try to use UTF8 it will not be converted to Windows-1252 and on the receiving end you will end up with gibberish.
|
|
6
17
|
|
package/package.json
CHANGED
package/src/SIA_device.html
CHANGED
package/src/SIA_device.js
CHANGED
|
@@ -1,98 +1,145 @@
|
|
|
1
|
-
// const { Iconv } = require('iconv');
|
|
2
|
-
|
|
3
|
-
|
|
4
1
|
module.exports = function (RED) {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40,
|
|
9
|
-
0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841,
|
|
10
|
-
0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40,
|
|
11
|
-
0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41,
|
|
12
|
-
0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641,
|
|
13
|
-
0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040,
|
|
14
|
-
0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240,
|
|
15
|
-
0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441,
|
|
16
|
-
0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41,
|
|
17
|
-
0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840,
|
|
18
|
-
0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41,
|
|
19
|
-
0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40,
|
|
20
|
-
0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640,
|
|
21
|
-
0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041,
|
|
22
|
-
0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
|
|
23
|
-
0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441,
|
|
24
|
-
0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41,
|
|
25
|
-
0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840,
|
|
26
|
-
0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41,
|
|
27
|
-
0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40,
|
|
28
|
-
0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640,
|
|
29
|
-
0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041,
|
|
30
|
-
0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241,
|
|
31
|
-
0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440,
|
|
32
|
-
0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40,
|
|
33
|
-
0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841,
|
|
34
|
-
0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40,
|
|
35
|
-
0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41,
|
|
36
|
-
0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641,
|
|
37
|
-
0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
const deviceMap = new Map();
|
|
41
|
-
const crypto = require('crypto');
|
|
42
|
-
|
|
43
|
-
|
|
2
|
+
const deviceSeqNumbers = new Map();
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const tableCRC = require('./table_crc.js');
|
|
44
5
|
|
|
45
6
|
function SIADeviceNode(config) {
|
|
46
7
|
RED.nodes.createNode(this, config);
|
|
47
8
|
|
|
48
9
|
// TODO: eventually we should disconnect from the server after each message as there shouldn't be a high volume of messages
|
|
49
10
|
const server = RED.nodes.getNode(config.server);
|
|
50
|
-
|
|
11
|
+
const pendingMessages = new Map();
|
|
51
12
|
const siaConfig = server.getSIAConfig();
|
|
13
|
+
const node = this;
|
|
52
14
|
console.log('SIA Config:', siaConfig);
|
|
53
15
|
|
|
16
|
+
// Cleanup on close
|
|
54
17
|
|
|
55
|
-
|
|
18
|
+
// --- LISTENER FUNCTION ---
|
|
19
|
+
const dataListener = data => {
|
|
20
|
+
const response = data.toString();
|
|
21
|
+
let seq; let type;
|
|
22
|
+
|
|
23
|
+
// Format: <LF><CRC><OLLL><"ID"><seq>...
|
|
24
|
+
const match = response.match(/"(\*?ACK|\*?DUH|NAK)"(\d{4})/);
|
|
25
|
+
console.log('-----------------------------------------\n'
|
|
26
|
+
+ ' SERVER RESPONSE \n'
|
|
27
|
+
+ '-----------------------------------------');
|
|
56
28
|
|
|
29
|
+
console.log('Received message:', response);
|
|
57
30
|
|
|
31
|
+
if (!match) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type = match[1].replace('*', ''); // "ACK", "DUH", or "NAK"
|
|
36
|
+
seq = match[2]; // The 4-digit sequence number as a string
|
|
37
|
+
|
|
38
|
+
// Handle uncorrectable NAK (seq 0000), likely for a timestamp error
|
|
39
|
+
if (type === 'NAK' && seq === '0000') {
|
|
40
|
+
node.warn('Received uncorrelatable NAK (seq 0000). Erroring all pending requests for this node.');
|
|
41
|
+
for (const [key, pending] of pendingMessages.entries()) {
|
|
42
|
+
if (pending.node === node) {
|
|
43
|
+
clearTimeout(pending.timeoutTimer);
|
|
44
|
+
pending.node.error('Received uncorrelatable NAK (seq 0000) from server', pending.originalMsg);
|
|
45
|
+
pendingMessages.delete(key);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (pendingMessages.has(seq)) {
|
|
52
|
+
const pending = pendingMessages.get(seq);
|
|
53
|
+
if (pending.node !== node) {
|
|
54
|
+
return;
|
|
55
|
+
} // Just in case we ever run multiple nodes
|
|
56
|
+
|
|
57
|
+
clearTimeout(pending.timeoutTimer);
|
|
58
|
+
switch (type) {
|
|
59
|
+
case 'ACK': { // Success!
|
|
60
|
+
pending.node.log(`ACK received for seq ${seq}`);
|
|
61
|
+
pending.node.send([null, {payload:{
|
|
62
|
+
error : false,
|
|
63
|
+
status : "received ACK",
|
|
64
|
+
response: response
|
|
65
|
+
}
|
|
66
|
+
}]); // Send to second output
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
case 'NAK': { // NAK response
|
|
71
|
+
pending.node.error(`Received NAK from server for seq ${seq}`, pending.originalMsg);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
case 'DUH': { // DUH response
|
|
76
|
+
pending.node.error(`Received DUH from server for seq ${seq}`, pending.originalMsg);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
58
80
|
|
|
59
|
-
|
|
60
|
-
|
|
81
|
+
pendingMessages.delete(seq);
|
|
82
|
+
} else {
|
|
83
|
+
node.log(`Received unknown or stale response for seq ${seq}`);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
node.on('close', done => {
|
|
88
|
+
if (server) {
|
|
89
|
+
server.removeListener('data', dataListener);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const [key, pending] of pendingMessages.entries()) {
|
|
93
|
+
if (pending.node === node) {
|
|
94
|
+
clearTimeout(pending.timeoutTimer);
|
|
95
|
+
pendingMessages.delete(key);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
done();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
server.connect();
|
|
103
|
+
server.on('data', dataListener);
|
|
104
|
+
|
|
105
|
+
// --- HELPER FUNCTIONS ---
|
|
106
|
+
function utf8toWin1252(string_) {
|
|
107
|
+
tmpBuf = Buffer.from(string_, 'utf8');
|
|
61
108
|
return tmpBuf.toString('latin1');
|
|
62
109
|
}
|
|
63
110
|
|
|
64
|
-
function calculateCRCIBM16(
|
|
65
|
-
data = new Buffer.from(
|
|
66
|
-
let
|
|
111
|
+
function calculateCRCIBM16(string_) {
|
|
112
|
+
data = new Buffer.from(string_);
|
|
113
|
+
let {length} = data;
|
|
67
114
|
let buffer = 0;
|
|
68
115
|
let crc;
|
|
69
|
-
while (
|
|
70
|
-
|
|
116
|
+
while (length--) {
|
|
117
|
+
crc = ((crc >>> 8) ^ (tableCRC[(crc ^ (data[buffer++])) & 0xFF]));
|
|
71
118
|
}
|
|
119
|
+
|
|
72
120
|
return crc.toString(16).padStart(4, '0').toUpperCase();
|
|
73
121
|
}
|
|
74
122
|
|
|
75
|
-
function encrypt(encryptionType, encryptionKey,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
123
|
+
function encrypt(encryptionType, encryptionKey, string_) {
|
|
124
|
+
try {
|
|
125
|
+
const encryptionKeyBuf = new Buffer.from(encryptionKey, 'hex');
|
|
126
|
+
const iv = new Buffer.alloc(16);
|
|
127
|
+
iv.fill(0);
|
|
128
|
+
const cipher = crypto.createCipheriv(encryptionType, encryptionKeyBuf, iv);
|
|
129
|
+
cipher.setAutoPadding(false);
|
|
130
|
+
let encoded = cipher.update(string_, 'utf8', 'hex');
|
|
131
|
+
encoded += cipher.final('hex');
|
|
132
|
+
return (encoded ? encoded : undefined);
|
|
133
|
+
} catch {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
88
136
|
}
|
|
89
137
|
|
|
90
|
-
|
|
91
|
-
function generateTimestamp(){
|
|
138
|
+
function generateTimestamp() {
|
|
92
139
|
const now = new Date();
|
|
93
140
|
|
|
94
141
|
// Pad single-digit numbers with a leading zero
|
|
95
|
-
const pad =
|
|
142
|
+
const pad = number_ => number_.toString().padStart(2, '0');
|
|
96
143
|
|
|
97
144
|
const hours = pad(now.getHours());
|
|
98
145
|
const minutes = pad(now.getMinutes());
|
|
@@ -102,115 +149,121 @@ module.exports = function (RED) {
|
|
|
102
149
|
const year = now.getFullYear();
|
|
103
150
|
|
|
104
151
|
return `_${hours}:${minutes}:${seconds},${month}-${day}-${year}`;
|
|
105
|
-
|
|
106
152
|
}
|
|
107
153
|
|
|
108
|
-
function generatePadding(data){
|
|
109
|
-
buffer = new Buffer.from(data); //
|
|
110
|
-
|
|
154
|
+
function generatePadding(data) {
|
|
155
|
+
buffer = new Buffer.from(data); // We need to get byte length of the message body, not string length
|
|
156
|
+
const messageLength = buffer.length;
|
|
111
157
|
|
|
112
158
|
let padding = '';
|
|
113
|
-
if(
|
|
114
|
-
const paddingNeeded = 15 - (
|
|
115
|
-
//
|
|
116
|
-
for(let i=0; i<paddingNeeded; i++){
|
|
159
|
+
if (messageLength % 16 !== 0) {
|
|
160
|
+
const paddingNeeded = 15 - (messageLength % 16);
|
|
161
|
+
// Generate random uppercase letters (only letters) for paddingNeeded
|
|
162
|
+
for (let i = 0; i < paddingNeeded; i++) {
|
|
117
163
|
const randomChar = String.fromCharCode(65 + Math.floor(Math.random() * 26));
|
|
118
164
|
padding += randomChar;
|
|
119
165
|
}
|
|
120
|
-
|
|
166
|
+
|
|
167
|
+
// Generate byte array
|
|
121
168
|
console.log('Padding needed:', paddingNeeded, 'Generated padding:', padding);
|
|
122
|
-
return padding+'|';
|
|
123
|
-
}
|
|
124
|
-
return '';
|
|
125
|
-
}
|
|
169
|
+
return padding + '|';
|
|
170
|
+
}
|
|
126
171
|
|
|
172
|
+
return '';
|
|
173
|
+
}
|
|
127
174
|
|
|
128
|
-
function validateHexString(
|
|
129
|
-
if(
|
|
130
|
-
console.error(`${
|
|
175
|
+
function validateHexString(string_, stringName, minLength, maxLength) {
|
|
176
|
+
if (string_.length < minLength || string_.length > maxLength) {
|
|
177
|
+
console.error(`${stringName} must be between ${minLength} and ${maxLength} characters long`);
|
|
131
178
|
return false;
|
|
132
|
-
|
|
133
179
|
}
|
|
134
180
|
|
|
135
|
-
if(
|
|
136
|
-
console.error(`${
|
|
181
|
+
if (string_.length > 0 && !/^[\dA-Fa-f]+$/.test(string_)) {
|
|
182
|
+
console.error(`${stringName} must be hexadecimal characters only (0-9, A-F)`);
|
|
137
183
|
return false;
|
|
138
184
|
}
|
|
185
|
+
|
|
139
186
|
return true;
|
|
140
187
|
}
|
|
141
188
|
|
|
142
|
-
function
|
|
143
|
-
if(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
189
|
+
function iterateMessageCount(id) {
|
|
190
|
+
if (deviceSeqNumbers.has(id)) {
|
|
191
|
+
let count = deviceSeqNumbers.get(id);
|
|
192
|
+
count = (count % 9999) + 1; // Wrap around at 999
|
|
193
|
+
deviceSeqNumbers.set(id, count);
|
|
194
|
+
} else {
|
|
195
|
+
deviceSeqNumbers.set(id, 1);
|
|
149
196
|
}
|
|
150
197
|
}
|
|
151
198
|
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
let payload = {
|
|
159
|
-
"account" : "AABBCC",
|
|
160
|
-
"accountPrefix" : "5678"
|
|
161
|
-
}
|
|
162
|
-
siaConfig.receiverNumber = "1234";
|
|
199
|
+
// --- PROCESS NEW MESSAGE ---
|
|
200
|
+
node.on('input', message => {
|
|
201
|
+
console.log('-----------------------------------------\n'
|
|
202
|
+
+ ' MESSAGE START \n'
|
|
203
|
+
+ '-----------------------------------------');
|
|
204
|
+
const payload = message.payload;
|
|
163
205
|
|
|
164
206
|
const deviceAccount = payload.account;
|
|
165
207
|
const deviceAccountPrefix = payload.accountPrefix || '0';
|
|
166
208
|
|
|
167
209
|
// INPUT VALIDATION
|
|
210
|
+
if (!validateHexString(deviceAccount, 'Device account', 3, 16)) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
168
213
|
|
|
169
|
-
if(!validateHexString(
|
|
170
|
-
|
|
171
|
-
|
|
214
|
+
if (!validateHexString(deviceAccountPrefix, 'Device account prefix', 1, 6)) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
172
217
|
|
|
173
|
-
|
|
218
|
+
if (!validateHexString(siaConfig.receiverNumber, 'Receiver number', 0, 6)) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
174
221
|
|
|
175
|
-
|
|
222
|
+
const deviceIdentifier = 'L' + deviceAccountPrefix + '#' + deviceAccount;
|
|
176
223
|
|
|
224
|
+
iterateMessageCount(deviceIdentifier);
|
|
177
225
|
|
|
178
|
-
|
|
226
|
+
// PART: <"id"><seq><Rrcvr><Lpref><#acct>[
|
|
179
227
|
let messageBodyStart = '"'; // <"id"><seq><Rrcvr><Lpref><#acct>[
|
|
180
|
-
if(siaConfig.encryptionEnabled)
|
|
228
|
+
if (siaConfig.encryptionEnabled) {
|
|
181
229
|
messageBodyStart += '*';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
messageBodyStart += 'SIA-DCS"'; // ID placeholder
|
|
233
|
+
const seqNumber = deviceSeqNumbers.get(deviceIdentifier).toString().padStart(4, '0');
|
|
234
|
+
|
|
235
|
+
messageBodyStart += seqNumber; // Message sequence number
|
|
236
|
+
if (siaConfig.receiverNumber.length > 0) {
|
|
237
|
+
messageBodyStart += 'R' + siaConfig.receiverNumber;
|
|
238
|
+
}
|
|
182
239
|
|
|
183
|
-
messageBodyStart += 'SIA-DCS"' // ID placeholder
|
|
184
|
-
messageBodyStart += deviceMap.get(deviceIdentifier).toString().padStart(4, '0'); // message sequence number
|
|
185
|
-
if(siaConfig.receiverNumber.length >0)
|
|
186
|
-
messageBodyStart += 'R'+siaConfig.receiverNumber;
|
|
187
240
|
messageBodyStart += deviceIdentifier;
|
|
188
241
|
messageBodyStart += '[';
|
|
189
242
|
|
|
190
|
-
|
|
191
243
|
// PART: #<acct>|<data>][<extende data>]
|
|
192
|
-
let messageBodyData = '#'+deviceAccount + '|';
|
|
244
|
+
let messageBodyData = '#' + deviceAccount + '|'; // <pad>|...data...][x…data…]
|
|
193
245
|
|
|
194
246
|
// data format
|
|
195
247
|
// (N)(id-number)(DCS)(zone)
|
|
196
248
|
// there's also a format of group-zone
|
|
197
249
|
|
|
198
|
-
// messageBodyData += "
|
|
199
|
-
messageBodyData +=
|
|
250
|
+
// messageBodyData += "Nri129^FA"; //
|
|
251
|
+
messageBodyData += payload.body;
|
|
252
|
+
// messageBodyData += 'Nri129/FA1234'; //
|
|
200
253
|
// optional extended data can be added here, for example
|
|
201
254
|
|
|
202
255
|
// PART: ]<timestamp>, finalization of body
|
|
203
256
|
let messageBody = ''; // <"id"><seq><Rrcvr><Lpref><#acct>[<pad>|...data...][x…data…]<timestamp>
|
|
204
|
-
|
|
257
|
+
const timestamp = generateTimestamp(); // TODO: this should be optional
|
|
205
258
|
|
|
206
|
-
if(siaConfig.encryptionEnabled){ //
|
|
207
|
-
let toEncrypt = messageBodyData+']'+timestamp;
|
|
259
|
+
if (siaConfig.encryptionEnabled) { // Everything past [ till <CR> is to be encrypted
|
|
260
|
+
let toEncrypt = messageBodyData + ']' + timestamp;
|
|
208
261
|
toEncrypt = generatePadding(toEncrypt) + toEncrypt;
|
|
209
262
|
messageBody = messageBodyStart + encrypt(siaConfig.encryptionType, siaConfig.encryptionKey, toEncrypt);
|
|
210
|
-
}else{
|
|
263
|
+
} else {
|
|
211
264
|
messageBody += messageBodyStart;
|
|
212
265
|
messageBody += messageBodyData;
|
|
213
|
-
messageBody += ']'; //
|
|
266
|
+
messageBody += ']'; // End of data fields
|
|
214
267
|
// optional extended data
|
|
215
268
|
messageBody += timestamp;
|
|
216
269
|
}
|
|
@@ -218,21 +271,51 @@ module.exports = function (RED) {
|
|
|
218
271
|
// Append <LF><crc><>
|
|
219
272
|
msgCount = (new Buffer.from(messageBody)).length;
|
|
220
273
|
|
|
221
|
-
let
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
274
|
+
let message_ = '\n'; // <LF><crc><0LLL><"id"><seq><Rrcvr><Lpref><#acct>[<pad>|...data...][x…data…]<timestamp><CR>
|
|
275
|
+
const crc = calculateCRCIBM16(messageBody);
|
|
276
|
+
console.log("Final message CRC: ", crc);
|
|
277
|
+
|
|
278
|
+
message_ += crc; // CRC is 4 ASCII characters
|
|
279
|
+
message_ += '0' + msgCount.toString(16).toUpperCase().padStart(3, '0'); // Carriage return
|
|
280
|
+
message_ += messageBody;
|
|
281
|
+
message_ += '\r';
|
|
282
|
+
|
|
283
|
+
console.log('Final message:', message_);
|
|
284
|
+
console.log('Sending message to server');
|
|
285
|
+
|
|
286
|
+
pendingMessages.set(seqNumber, {
|
|
287
|
+
node,
|
|
288
|
+
originalMsg: message,
|
|
289
|
+
timestamp: Date.now(),
|
|
290
|
+
timeoutTimer: setTimeout(() => {
|
|
291
|
+
if (pendingMessages.has(seqNumber)) {
|
|
292
|
+
node.error(`Timeout waiting for ACK on seq ${seqNumber}`, message);
|
|
293
|
+
node.status({fill: 'red', shape: 'dot', text: 'Timeout ' + seqNumber});
|
|
294
|
+
node.send([null, {
|
|
295
|
+
payload: {
|
|
296
|
+
error: true,
|
|
297
|
+
status: 'timeout on message',
|
|
298
|
+
message: message
|
|
299
|
+
},
|
|
300
|
+
}]);
|
|
301
|
+
pendingMessages.delete(seqNumber);
|
|
302
|
+
}
|
|
303
|
+
}, 5000), // 5 Second timeout
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
node.log(`Sending SIA message seq: ${seqNumber}`);
|
|
307
|
+
node.status({fill: 'blue', shape: 'dot', text: 'Sent ' + seqNumber});
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
server.write(message_);
|
|
311
|
+
|
|
312
|
+
node.send([{
|
|
313
|
+
payload: {
|
|
314
|
+
error: false,
|
|
315
|
+
status: 'sent message',
|
|
316
|
+
message: message_
|
|
317
|
+
},
|
|
318
|
+
}, null]); // Send to first output
|
|
236
319
|
});
|
|
237
320
|
}
|
|
238
321
|
|
package/src/SIA_server.html
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
Copyright (c) since the year 2016 Klaus Landsdorf (http://plus4nodered.com/)
|
|
3
|
-
Copyright 2016 - Jason D. Harper, Argonne National Laboratory
|
|
4
|
-
Copyright 2015,2016 - Mika Karaila, Valmet Automation Inc.
|
|
5
|
-
All rights reserved.
|
|
6
|
-
node-red-contrib-modbus
|
|
7
|
-
|
|
8
|
-
@author <a href="mailto:klaus.landsdorf@bianco-royal.de">Klaus Landsdorf</a> (Bianco Royal)
|
|
9
|
-
-->
|
|
10
1
|
<script type="text/javascript">
|
|
11
2
|
RED.nodes.registerType('SIA-server', {
|
|
12
3
|
category: 'config',
|
|
@@ -56,7 +47,7 @@
|
|
|
56
47
|
<input type="text" id="node-config-input-name" placeholder="Name">
|
|
57
48
|
</div>
|
|
58
49
|
<div class="form-row">
|
|
59
|
-
<label for="node-config-input-receiverHost">Receiver
|
|
50
|
+
<label for="node-config-input-receiverHost">Receiver</label>
|
|
60
51
|
<input type="text" id="node-config-input-receiverHost" placeholder="Receiver Host">
|
|
61
52
|
</div>
|
|
62
53
|
<div class="form-row">
|
|
@@ -65,7 +56,7 @@
|
|
|
65
56
|
</div>
|
|
66
57
|
<div class="form-row">
|
|
67
58
|
<label for="node-config-input-receiverNumber">Receiver Number</label>
|
|
68
|
-
<input type="
|
|
59
|
+
<input type="text" id="node-config-input-receiverNumber" placeholder="Receiver Number">
|
|
69
60
|
</div>
|
|
70
61
|
<div class="form-row">
|
|
71
62
|
<label for="node-config-input-encryptionKey">Encryption Key</label>
|
package/src/SIA_server.js
CHANGED
|
@@ -8,29 +8,36 @@ module.exports = function (RED) {
|
|
|
8
8
|
function SIAServer(config) {
|
|
9
9
|
RED.nodes.createNode(this, config);
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const node = this;
|
|
12
|
+
|
|
13
|
+
node.receiverHost = config.receiverHost;
|
|
14
|
+
node.receiverPort = Number.parseInt(config.receiverPort);
|
|
15
|
+
node.receiverNumber = config.receiverNumber || '';
|
|
14
16
|
// This.siaAccount = config.siaAccount;
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
switch (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
default:
|
|
30
|
-
return undefined;
|
|
31
|
-
}
|
|
17
|
+
// node.siaAccountPrefix = config.siaAccountPrefix || 'L0';
|
|
18
|
+
node.encryptionKey = config.encryptionKey || '';
|
|
19
|
+
node.encryptionEnabled = node.encryptionKey.length > 0;
|
|
20
|
+
|
|
21
|
+
switch (node.encryptionKey.length) {
|
|
22
|
+
case 32: {
|
|
23
|
+
node.encryptionType = 'aes-128-cbc';
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
case 48: {
|
|
28
|
+
node.encryptionType = 'aes-192-cbc';
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
32
31
|
|
|
32
|
+
case 64: {
|
|
33
|
+
node.encryptionType = 'aes-256-cbc';
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
33
36
|
|
|
37
|
+
default: {
|
|
38
|
+
node.encryptionType = '';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
34
41
|
|
|
35
42
|
const client = new Net.Socket();
|
|
36
43
|
|
|
@@ -39,18 +46,39 @@ module.exports = function (RED) {
|
|
|
39
46
|
client.destroy();
|
|
40
47
|
});
|
|
41
48
|
|
|
42
|
-
|
|
49
|
+
client.on('data', data => {
|
|
50
|
+
// Emits to any SIA-device listening via server.on('data', ...)
|
|
51
|
+
console.log("Received some message", data);
|
|
52
|
+
node.emit('data', data);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
client.on('error', error => {
|
|
56
|
+
console.error('SIA Socket Error:', error.message);
|
|
57
|
+
node.emit('socketError', error); // Notify devices if needed
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
client.on('connect', () => {
|
|
61
|
+
node.emit('connected');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
client.on('close', () => {
|
|
65
|
+
node.emit('closed');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// -------------
|
|
69
|
+
|
|
70
|
+
node.connect = function () {
|
|
43
71
|
// If(!client.destroyed && client.readyState == 'open') {
|
|
44
|
-
// console.log('Already connected to SIA server at ' +
|
|
72
|
+
// console.log('Already connected to SIA server at ' + node.receiverHost + ':' + node.receiverPort);
|
|
45
73
|
// return;
|
|
46
74
|
// }
|
|
47
75
|
|
|
48
|
-
if (!
|
|
76
|
+
if (!node.receiverHost || !node.receiverPort) {
|
|
49
77
|
console.error('Receiver host or port not set');
|
|
50
78
|
return;
|
|
51
79
|
}
|
|
52
80
|
|
|
53
|
-
console.log('Connecting to SIA server at',
|
|
81
|
+
console.log('Connecting to SIA server at', node.receiverHost, node.receiverPort);
|
|
54
82
|
|
|
55
83
|
const operation = Retry.operation({
|
|
56
84
|
retries: 5,
|
|
@@ -60,18 +88,16 @@ module.exports = function (RED) {
|
|
|
60
88
|
randomize: true,
|
|
61
89
|
});
|
|
62
90
|
|
|
63
|
-
const
|
|
91
|
+
const temporaryRef = node;
|
|
64
92
|
|
|
65
93
|
let errorListener = null;
|
|
66
94
|
|
|
67
|
-
|
|
68
95
|
const connectCallback = () => {
|
|
69
|
-
console.log('Connected to SIA server at',
|
|
96
|
+
console.log('Connected to SIA server at', temporaryRef.receiverHost, temporaryRef.receiverPort);
|
|
70
97
|
};
|
|
71
98
|
|
|
72
99
|
operation.attempt(currentAttempt => {
|
|
73
|
-
|
|
74
|
-
if (errorListener) { // will not be active on first attempt
|
|
100
|
+
if (errorListener) { // Will not be active on first attempt
|
|
75
101
|
client.off('error', errorListener);
|
|
76
102
|
client.off('connect', connectCallback);
|
|
77
103
|
}
|
|
@@ -88,27 +114,27 @@ module.exports = function (RED) {
|
|
|
88
114
|
|
|
89
115
|
client.once('error', errorListener);
|
|
90
116
|
|
|
91
|
-
client.connect(
|
|
117
|
+
client.connect(temporaryRef.receiverPort, temporaryRef.receiverHost, connectCallback);
|
|
92
118
|
});
|
|
93
119
|
};
|
|
94
120
|
|
|
95
|
-
|
|
121
|
+
node.getSIAConfig = function () {
|
|
96
122
|
return {
|
|
97
|
-
receiverNumber:
|
|
98
|
-
encryptionKey:
|
|
99
|
-
encryptionEnabled:
|
|
100
|
-
encryptionType:
|
|
123
|
+
receiverNumber: node.receiverNumber,
|
|
124
|
+
encryptionKey: node.encryptionKey,
|
|
125
|
+
encryptionEnabled: node.encryptionEnabled,
|
|
126
|
+
encryptionType: node.encryptionType,
|
|
101
127
|
};
|
|
102
128
|
};
|
|
103
129
|
|
|
104
|
-
|
|
130
|
+
node.write = function (data) {
|
|
105
131
|
if (!client.destroyed && client.readyState == 'open') {
|
|
106
132
|
console.log('Sending data to SIA server:', data);
|
|
107
133
|
client.write(data);
|
|
108
134
|
}
|
|
109
135
|
};
|
|
110
136
|
|
|
111
|
-
|
|
137
|
+
node.close = function () {
|
|
112
138
|
if (!client.destroyed) {
|
|
113
139
|
client.end();
|
|
114
140
|
}
|
package/src/table_crc.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const tableCRC = [
|
|
2
|
+
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241,
|
|
3
|
+
0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440,
|
|
4
|
+
0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40,
|
|
5
|
+
0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841,
|
|
6
|
+
0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40,
|
|
7
|
+
0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41,
|
|
8
|
+
0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641,
|
|
9
|
+
0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040,
|
|
10
|
+
0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240,
|
|
11
|
+
0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441,
|
|
12
|
+
0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41,
|
|
13
|
+
0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840,
|
|
14
|
+
0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41,
|
|
15
|
+
0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40,
|
|
16
|
+
0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640,
|
|
17
|
+
0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041,
|
|
18
|
+
0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
|
|
19
|
+
0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441,
|
|
20
|
+
0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41,
|
|
21
|
+
0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840,
|
|
22
|
+
0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41,
|
|
23
|
+
0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40,
|
|
24
|
+
0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640,
|
|
25
|
+
0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041,
|
|
26
|
+
0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241,
|
|
27
|
+
0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440,
|
|
28
|
+
0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40,
|
|
29
|
+
0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841,
|
|
30
|
+
0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40,
|
|
31
|
+
0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41,
|
|
32
|
+
0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641,
|
|
33
|
+
0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
module.exports = tableCRC;
|