@jotiotech/node-red-sia-premise-device 0.1.0 → 0.2.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/README.md +2 -0
- package/package.json +13 -9
- package/src/SIA_device.html +44 -0
- package/src/SIA_device.js +233 -15
- package/src/SIA_server.html +75 -0
- package/src/SIA_server.js +127 -0
- package/SIA_device.html +0 -37
- package/SIA_device.js +0 -43
- package/SIA_server.html +0 -47
- package/SIA_server.js +0 -14
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jotiotech/node-red-sia-premise-device",
|
|
3
|
-
"version": "0.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"engines": {
|
|
5
|
+
"node": ">=12.0.0"
|
|
6
|
+
},
|
|
7
|
+
"license": "MIT",
|
|
8
8
|
"description": "",
|
|
9
|
-
"dependencies": {
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"retry": "^0.13.1"
|
|
11
|
+
},
|
|
10
12
|
"keywords": [
|
|
11
|
-
"node-red",
|
|
13
|
+
"node-red",
|
|
14
|
+
"parsing"
|
|
12
15
|
],
|
|
13
16
|
"node-red": {
|
|
14
|
-
|
|
17
|
+
"version": ">=1.0.0",
|
|
15
18
|
"nodes": {
|
|
16
|
-
|
|
19
|
+
"SIA_device": "src/SIA_device.js",
|
|
20
|
+
"SIA_server": "src/SIA_server.js"
|
|
17
21
|
}
|
|
18
22
|
}
|
|
19
23
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('SIA-device', {
|
|
3
|
+
category: 'common',
|
|
4
|
+
color: '#0B99D8',
|
|
5
|
+
defaults: {
|
|
6
|
+
name: { value: "" },
|
|
7
|
+
server: { value: "", type: "SIA-server", required: true }, // This will
|
|
8
|
+
},
|
|
9
|
+
align: 'left',
|
|
10
|
+
icon: "icon.png",
|
|
11
|
+
inputs: 1,
|
|
12
|
+
outputs: 0,
|
|
13
|
+
label: function () {
|
|
14
|
+
return this.name || "SIA Device";
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<style>
|
|
22
|
+
|
|
23
|
+
.input-button-wrap>input {
|
|
24
|
+
width: 100% !important;
|
|
25
|
+
}
|
|
26
|
+
select {
|
|
27
|
+
width: 100% !important;
|
|
28
|
+
}
|
|
29
|
+
.editor-button {
|
|
30
|
+
height: 30px;
|
|
31
|
+
padding: 0 10px;
|
|
32
|
+
}
|
|
33
|
+
</style>
|
|
34
|
+
|
|
35
|
+
<script type="text/x-red" data-template-name="SIA-device">
|
|
36
|
+
<div class="form-row">
|
|
37
|
+
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
|
|
38
|
+
<input type="text" id="node-input-name" placeholder="Name">
|
|
39
|
+
</div>
|
|
40
|
+
<div class="form-row">
|
|
41
|
+
<label for="node-input-server"><i class="icon-globe"></i> <span data-i18n="modbus-contrib.label.server"></span></label>
|
|
42
|
+
<input type="text" id="node-input-server">
|
|
43
|
+
</div>
|
|
44
|
+
</script>
|
package/src/SIA_device.js
CHANGED
|
@@ -1,22 +1,240 @@
|
|
|
1
|
-
|
|
2
|
-
const __importDefault = (this && this.__importDefault) || function (module_) {
|
|
3
|
-
return (module_ && module_.__esModule) ? module_ : {default: module_};
|
|
4
|
-
};
|
|
1
|
+
// const { Iconv } = require('iconv');
|
|
5
2
|
|
|
6
|
-
const Net = require('node:net');
|
|
7
3
|
|
|
8
|
-
|
|
4
|
+
module.exports = function (RED) {
|
|
5
|
+
const tableCRC = [
|
|
6
|
+
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241,
|
|
7
|
+
0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440,
|
|
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
|
+
];
|
|
9
39
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
console.log(serverConfig);
|
|
13
|
-
}
|
|
40
|
+
const deviceMap = new Map();
|
|
41
|
+
const crypto = require('crypto');
|
|
14
42
|
|
|
15
|
-
connect() {
|
|
16
43
|
|
|
17
|
-
}
|
|
18
44
|
|
|
45
|
+
function SIADeviceNode(config) {
|
|
46
|
+
RED.nodes.createNode(this, config);
|
|
47
|
+
|
|
48
|
+
// TODO: eventually we should disconnect from the server after each message as there shouldn't be a high volume of messages
|
|
49
|
+
const server = RED.nodes.getNode(config.server);
|
|
50
|
+
|
|
51
|
+
const siaConfig = server.getSIAConfig();
|
|
52
|
+
console.log('SIA Config:', siaConfig);
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
server.connect();
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
function utf8toWin1252(str) {
|
|
60
|
+
tmpBuf = Buffer.from(str, 'utf8');
|
|
61
|
+
return tmpBuf.toString('latin1');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function calculateCRCIBM16(str){
|
|
65
|
+
data = new Buffer.from(str);
|
|
66
|
+
let len = data.length;
|
|
67
|
+
let buffer = 0;
|
|
68
|
+
let crc;
|
|
69
|
+
while (len--) {
|
|
70
|
+
crc = ((crc >>> 8) ^ (tableCRC[(crc ^ (data[buffer++])) & 0xff]));
|
|
71
|
+
}
|
|
72
|
+
return crc.toString(16).padStart(4, '0').toUpperCase();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function encrypt(encryptionType, encryptionKey, str) {
|
|
76
|
+
try {
|
|
77
|
+
const encryptionKeyBuf = new Buffer.from(encryptionKey, 'hex');
|
|
78
|
+
let iv = new Buffer.alloc(16);
|
|
79
|
+
iv.fill(0);
|
|
80
|
+
let cipher = crypto.createCipheriv(encryptionType, encryptionKeyBuf, iv);
|
|
81
|
+
cipher.setAutoPadding(false);
|
|
82
|
+
let encoded = cipher.update(str, 'utf8', 'hex');
|
|
83
|
+
encoded += cipher.final('hex');
|
|
84
|
+
return (encoded ? encoded : undefined);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
function generateTimestamp(){
|
|
92
|
+
const now = new Date();
|
|
93
|
+
|
|
94
|
+
// Pad single-digit numbers with a leading zero
|
|
95
|
+
const pad = (num) => num.toString().padStart(2, '0');
|
|
96
|
+
|
|
97
|
+
const hours = pad(now.getHours());
|
|
98
|
+
const minutes = pad(now.getMinutes());
|
|
99
|
+
const seconds = pad(now.getSeconds());
|
|
100
|
+
const month = pad(now.getMonth() + 1); // Months are 0-indexed
|
|
101
|
+
const day = pad(now.getDate());
|
|
102
|
+
const year = now.getFullYear();
|
|
103
|
+
|
|
104
|
+
return `_${hours}:${minutes}:${seconds},${month}-${day}-${year}`;
|
|
105
|
+
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function generatePadding(data){
|
|
109
|
+
buffer = new Buffer.from(data); // we need to get byte length of the message body, not string length
|
|
110
|
+
let msgLength = buffer.length;
|
|
111
|
+
|
|
112
|
+
let padding = '';
|
|
113
|
+
if(msgLength % 16 !== 0){
|
|
114
|
+
const paddingNeeded = 15 - (msgLength % 16);
|
|
115
|
+
// generate random uppercase letters (only letters) for paddingNeeded
|
|
116
|
+
for(let i=0; i<paddingNeeded; i++){
|
|
117
|
+
const randomChar = String.fromCharCode(65 + Math.floor(Math.random() * 26));
|
|
118
|
+
padding += randomChar;
|
|
119
|
+
}
|
|
120
|
+
// generate byte array
|
|
121
|
+
console.log('Padding needed:', paddingNeeded, 'Generated padding:', padding);
|
|
122
|
+
return padding+'|';
|
|
123
|
+
}else
|
|
124
|
+
return '';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
function validateHexString(str, strName, minLength, maxLength){ // NOTE LGTM
|
|
129
|
+
if(str.length < minLength || str.length > maxLength){
|
|
130
|
+
console.error(`${strName} must be between ${minLength} and ${maxLength} characters long`);
|
|
131
|
+
return false;
|
|
132
|
+
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if(str.length !=0 && !/^[0-9A-Fa-f]+$/.test(str)){
|
|
136
|
+
console.error(`${strName} must be hexadecimal characters only (0-9, A-F)`);
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function iterateMsgCount(id){ // NOTE LGTM
|
|
143
|
+
if(!deviceMap.has(id)){
|
|
144
|
+
deviceMap.set(id, 1);
|
|
145
|
+
}else{
|
|
146
|
+
let count = deviceMap.get(id);
|
|
147
|
+
count = (count % 9999) + 1; // wrap around at 999
|
|
148
|
+
deviceMap.set(id, count);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
19
151
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
152
|
+
// utf8toWin1252("a test ñ");
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
this.on('input', message => {
|
|
156
|
+
// const payload = message.payload;
|
|
157
|
+
|
|
158
|
+
let payload = {
|
|
159
|
+
"account" : "AABBCC",
|
|
160
|
+
"accountPrefix" : "5678"
|
|
161
|
+
}
|
|
162
|
+
siaConfig.receiverNumber = "1234";
|
|
163
|
+
|
|
164
|
+
const deviceAccount = payload.account;
|
|
165
|
+
const deviceAccountPrefix = payload.accountPrefix || '0';
|
|
166
|
+
|
|
167
|
+
// INPUT VALIDATION
|
|
168
|
+
|
|
169
|
+
if(!validateHexString(deviceAccount, 'Device account', 3, 16)) return;
|
|
170
|
+
if(!validateHexString(deviceAccountPrefix, 'Device account prefix', 1, 6)) return;
|
|
171
|
+
if(!validateHexString(siaConfig.receiverNumber, 'Receiver number', 0, 6)) return;
|
|
172
|
+
|
|
173
|
+
const deviceIdentifier = 'L'+ deviceAccountPrefix + '#' + deviceAccount;
|
|
174
|
+
|
|
175
|
+
iterateMsgCount(deviceIdentifier);
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
// PART: <"id"><seq><Rrcvr><Lpref><#acct>[
|
|
179
|
+
let messageBodyStart = '"'; // <"id"><seq><Rrcvr><Lpref><#acct>[
|
|
180
|
+
if(siaConfig.encryptionEnabled)
|
|
181
|
+
messageBodyStart += '*';
|
|
182
|
+
|
|
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
|
+
messageBodyStart += deviceIdentifier;
|
|
188
|
+
messageBodyStart += '[';
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
// PART: #<acct>|<data>][<extende data>]
|
|
192
|
+
let messageBodyData = '#'+deviceAccount + '|'; // <pad>|...data...][x…data…]
|
|
193
|
+
|
|
194
|
+
// data format
|
|
195
|
+
// (N)(id-number)(DCS)(zone)
|
|
196
|
+
// there's also a format of group-zone
|
|
197
|
+
|
|
198
|
+
// messageBodyData += "NFA"; //
|
|
199
|
+
messageBodyData += "Nri129/FA1234"; //
|
|
200
|
+
// optional extended data can be added here, for example
|
|
201
|
+
|
|
202
|
+
// PART: ]<timestamp>, finalization of body
|
|
203
|
+
let messageBody = ''; // <"id"><seq><Rrcvr><Lpref><#acct>[<pad>|...data...][x…data…]<timestamp>
|
|
204
|
+
let timestamp = generateTimestamp(); // TODO: this should be optional
|
|
205
|
+
|
|
206
|
+
if(siaConfig.encryptionEnabled){ // everything past [ till <CR> is to be encrypted
|
|
207
|
+
let toEncrypt = messageBodyData+']'+timestamp;
|
|
208
|
+
toEncrypt = generatePadding(toEncrypt) + toEncrypt;
|
|
209
|
+
messageBody = messageBodyStart + encrypt(siaConfig.encryptionType, siaConfig.encryptionKey, toEncrypt);
|
|
210
|
+
}else{
|
|
211
|
+
messageBody += messageBodyStart;
|
|
212
|
+
messageBody += messageBodyData;
|
|
213
|
+
messageBody += ']'; // end of data fields
|
|
214
|
+
// optional extended data
|
|
215
|
+
messageBody += timestamp;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Append <LF><crc><>
|
|
219
|
+
msgCount = (new Buffer.from(messageBody)).length;
|
|
220
|
+
|
|
221
|
+
let msg = '\n'; // <LF><crc><0LLL><"id"><seq><Rrcvr><Lpref><#acct>[<pad>|...data...][x…data…]<timestamp><CR>
|
|
222
|
+
msg += calculateCRCIBM16(messageBody); // CRC is 4 ASCII characters
|
|
223
|
+
msg += '0'+msgCount.toString(16).toUpperCase().padStart(3, '0'); // Carriage return
|
|
224
|
+
msg += messageBody;
|
|
225
|
+
msg += '\r';
|
|
226
|
+
|
|
227
|
+
// let hexMsg = '';
|
|
228
|
+
// for(let i=0; i<msg.length; i++){
|
|
229
|
+
// hexMsg += msg.charCodeAt(i).toString(16).padStart(2, '0').toUpperCase() + ' ';
|
|
230
|
+
// }
|
|
231
|
+
// console.log('Final message in hex: ', hexMsg);
|
|
232
|
+
|
|
233
|
+
server.connect();
|
|
234
|
+
server.write(msg);
|
|
235
|
+
server.close();
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
RED.nodes.registerType('SIA-device', SIADeviceNode);
|
|
240
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
<script type="text/javascript">
|
|
11
|
+
RED.nodes.registerType('SIA-server', {
|
|
12
|
+
category: 'config',
|
|
13
|
+
defaults: {
|
|
14
|
+
|
|
15
|
+
name: { value: "" },
|
|
16
|
+
receiverHost: { value: "", required: true },
|
|
17
|
+
receiverPort: { value: "", required: true },
|
|
18
|
+
receiverNumber : { value: ""},
|
|
19
|
+
encryptionKey: { value: ""},
|
|
20
|
+
},
|
|
21
|
+
label: function () {
|
|
22
|
+
return this.name || 'sia-server@' + this.receiverHost;
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
oneditprepare: function () {
|
|
26
|
+
let previous = null;
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if(this.encryptionKey == null){
|
|
30
|
+
this.encryptionKey = "";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let allowedLengths = [0, 32, 48, 64];
|
|
34
|
+
if(!allowedLengths.includes(this.encryptionKey.length)){
|
|
35
|
+
Node.error('Encryption key must be empty or 128, 192, 256 bits long in HEX');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if(!/^[0-9A-Fa-f]+$/.test(this.encryptionKey)){ // hexadecimal characters only
|
|
39
|
+
Node.error('Encryption key must be in hexadecimal characters only (0-9, A-F)');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
// check if encryptionKey is 128 bit 192 or 256 bit long
|
|
45
|
+
// encryptionKey needs to be a hex string
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<script type="text/x-red" data-template-name="SIA-server">
|
|
54
|
+
<div class="form-row">
|
|
55
|
+
<label for="node-config-input-name"><i class="icon-tag"></i> Name</label>
|
|
56
|
+
<input type="text" id="node-config-input-name" placeholder="Name">
|
|
57
|
+
</div>
|
|
58
|
+
<div class="form-row">
|
|
59
|
+
<label for="node-config-input-receiverHost">Receiver IP</label>
|
|
60
|
+
<input type="text" id="node-config-input-receiverHost" placeholder="Receiver Host">
|
|
61
|
+
</div>
|
|
62
|
+
<div class="form-row">
|
|
63
|
+
<label for="node-config-input-receiverPort">Receiver Port</label>
|
|
64
|
+
<input type="number" id="node-config-input-receiverPort" placeholder="Receiver Port">
|
|
65
|
+
</div>
|
|
66
|
+
<div class="form-row">
|
|
67
|
+
<label for="node-config-input-receiverNumber">Receiver Number</label>
|
|
68
|
+
<input type="number" id="node-config-input-receiverNumber" placeholder="Receiver Number">
|
|
69
|
+
</div>
|
|
70
|
+
<div class="form-row">
|
|
71
|
+
<label for="node-config-input-encryptionKey">Encryption Key</label>
|
|
72
|
+
<input type="password" id="node-config-input-encryptionKey" placeholder="SIA Password">
|
|
73
|
+
</div>
|
|
74
|
+
</script>
|
|
75
|
+
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
module.exports = function (RED) {
|
|
2
|
+
'use strict';
|
|
3
|
+
// SOURCE-MAP-REQUIRED
|
|
4
|
+
|
|
5
|
+
const Net = require('node:net');
|
|
6
|
+
const Retry = require('retry');
|
|
7
|
+
|
|
8
|
+
function SIAServer(config) {
|
|
9
|
+
RED.nodes.createNode(this, config);
|
|
10
|
+
|
|
11
|
+
this.receiverHost = config.receiverHost;
|
|
12
|
+
this.receiverPort = Number.parseInt(config.receiverPort);
|
|
13
|
+
this.receiverNumber = config.receiverNumber || '';
|
|
14
|
+
// This.siaAccount = config.siaAccount;
|
|
15
|
+
// this.siaAccountPrefix = config.siaAccountPrefix || 'L0';
|
|
16
|
+
this.encryptionKey = config.encryptionKey || '';
|
|
17
|
+
this.encryptionEnabled = this.encryptionKey.length > 0;
|
|
18
|
+
|
|
19
|
+
switch (this.encryptionKey.length) {
|
|
20
|
+
case 32:
|
|
21
|
+
this.encryptionType = 'aes-128-cbc';
|
|
22
|
+
break;
|
|
23
|
+
case 48:
|
|
24
|
+
this.encryptionType = 'aes-192-cbc';
|
|
25
|
+
break;
|
|
26
|
+
case 64:
|
|
27
|
+
this.encryptionType = 'aes-256-cbc';
|
|
28
|
+
break;
|
|
29
|
+
default:
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
const client = new Net.Socket();
|
|
36
|
+
|
|
37
|
+
process.on('uncaughtException', error => {
|
|
38
|
+
console.error('Uncaught Exception:', error);
|
|
39
|
+
client.destroy();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
this.connect = function () {
|
|
43
|
+
// If(!client.destroyed && client.readyState == 'open') {
|
|
44
|
+
// console.log('Already connected to SIA server at ' + this.receiverHost + ':' + this.receiverPort);
|
|
45
|
+
// return;
|
|
46
|
+
// }
|
|
47
|
+
|
|
48
|
+
if (!this.receiverHost || !this.receiverPort) {
|
|
49
|
+
console.error('Receiver host or port not set');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log('Connecting to SIA server at', this.receiverHost, this.receiverPort);
|
|
54
|
+
|
|
55
|
+
const operation = Retry.operation({
|
|
56
|
+
retries: 5,
|
|
57
|
+
factor: 2,
|
|
58
|
+
minTimeout: 1000,
|
|
59
|
+
maxTimeout: 20_000,
|
|
60
|
+
randomize: true,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const tmpRef = this;
|
|
64
|
+
|
|
65
|
+
let errorListener = null;
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
const connectCallback = () => {
|
|
69
|
+
console.log('Connected to SIA server at', tmpRef.receiverHost, tmpRef.receiverPort);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
operation.attempt(currentAttempt => {
|
|
73
|
+
|
|
74
|
+
if (errorListener) { // will not be active on first attempt
|
|
75
|
+
client.off('error', errorListener);
|
|
76
|
+
client.off('connect', connectCallback);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
errorListener = error => {
|
|
80
|
+
console.log(`Connection error: ${error.message}`);
|
|
81
|
+
if (operation.retry(error)) {
|
|
82
|
+
console.log(`Retrying connection to SIA server, attempt number: ${currentAttempt}`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.error('Failed to connect to SIA server:', error);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
client.once('error', errorListener);
|
|
90
|
+
|
|
91
|
+
client.connect(tmpRef.receiverPort, tmpRef.receiverHost, connectCallback);
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
this.getSIAConfig = function () {
|
|
96
|
+
return {
|
|
97
|
+
receiverNumber: this.receiverNumber,
|
|
98
|
+
encryptionKey: this.encryptionKey,
|
|
99
|
+
encryptionEnabled: this.encryptionEnabled,
|
|
100
|
+
encryptionType: this.encryptionType,
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
this.write = function (data) {
|
|
105
|
+
if (!client.destroyed && client.readyState == 'open') {
|
|
106
|
+
console.log('Sending data to SIA server:', data);
|
|
107
|
+
client.write(data);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
this.close = function () {
|
|
112
|
+
if (!client.destroyed) {
|
|
113
|
+
client.end();
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Handle sending of data
|
|
118
|
+
// handle failure to connect
|
|
119
|
+
// payload won't be prepared here but in SIA device
|
|
120
|
+
|
|
121
|
+
// client.write('Hello, server.');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
RED.nodes.registerType('SIA-server', SIAServer);
|
|
125
|
+
|
|
126
|
+
/* istanbul ignore next */
|
|
127
|
+
};
|
package/SIA_device.html
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
<script type="text/javascript">
|
|
2
|
-
RED.nodes.registerType('SIA-device', {
|
|
3
|
-
category: 'common',
|
|
4
|
-
color: '#0B99D8',
|
|
5
|
-
defaults: {
|
|
6
|
-
name: { value: "" },
|
|
7
|
-
server: { value: "", type: "SIA-server", required: true }, // This will
|
|
8
|
-
},
|
|
9
|
-
align: 'left',
|
|
10
|
-
icon: "icon.png",
|
|
11
|
-
inputs: 1,
|
|
12
|
-
outputs: 0,
|
|
13
|
-
label: function () {
|
|
14
|
-
return this.name || "SIA Device";
|
|
15
|
-
},
|
|
16
|
-
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
</script>
|
|
20
|
-
|
|
21
|
-
<style>
|
|
22
|
-
|
|
23
|
-
.input-button-wrap>input {
|
|
24
|
-
width: 100% !important;
|
|
25
|
-
}
|
|
26
|
-
</style>
|
|
27
|
-
|
|
28
|
-
<script type="text/x-red" data-template-name="BJ-parser">
|
|
29
|
-
<div class="form-row">
|
|
30
|
-
<label for="node-input-name"><i class="icon-tag"></i> Name</label>
|
|
31
|
-
<input type="text" id="node-input-name" placeholder="Name">
|
|
32
|
-
</div>
|
|
33
|
-
<div class="form-row">
|
|
34
|
-
<label for="node-input-server">SIA Server</label>
|
|
35
|
-
<input type="text" id="node-input-server" placeholder="Select SIA Server">
|
|
36
|
-
</div>
|
|
37
|
-
</script>
|
package/SIA_device.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
const clientModule = __importDefault(require('./src/SIA_device.js'));
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
module.exports = function (RED) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
function ParserNode(config) {
|
|
8
|
-
RED.nodes.createNode(this, config);
|
|
9
|
-
|
|
10
|
-
this.serverConfig = RED.nodes.getNode(config.server);
|
|
11
|
-
const siaModule = new clientModule.default(serverConfig);
|
|
12
|
-
siaModule.connect();
|
|
13
|
-
|
|
14
|
-
this.on('input', message => {
|
|
15
|
-
parsedPayload = message.payload;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// const parser = new parserModule.default(message.statusSchema, parserConfig);
|
|
19
|
-
// message.payload.outJson = parser.processPayload(parsedPayload);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if(parsedPayload === undefined){
|
|
23
|
-
this.send(message);
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
if(message.payload.parsingError == true){
|
|
27
|
-
this.send(message);
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
this.send(message);
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
RED.nodes.registerType('SIA-device', ParserNode);
|
|
36
|
-
RED.nodes.registerType('SIA-server', require('./sia-server.js'));
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
package/SIA_server.html
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
<script type="text/javascript">
|
|
2
|
-
RED.nodes.registerType('SIA-server', {
|
|
3
|
-
category: 'config',
|
|
4
|
-
defaults: {
|
|
5
|
-
name: { value: "" },
|
|
6
|
-
receiverIP: { value: "", required: true },
|
|
7
|
-
receiverPort: { value: "", required: true },
|
|
8
|
-
siaAccount: { value: "", required: true },
|
|
9
|
-
siaPassword: { value: "", required: true },
|
|
10
|
-
encryptionEnabled: { value: false },
|
|
11
|
-
passwordInHex: { value: false },
|
|
12
|
-
},
|
|
13
|
-
label: function () {
|
|
14
|
-
return this.name || "SIA Server";
|
|
15
|
-
},
|
|
16
|
-
});
|
|
17
|
-
</script>
|
|
18
|
-
<script type="text/x-red" data-template-name="SIA-server">
|
|
19
|
-
<div class="form-row">
|
|
20
|
-
<label for="node-config-input-name"><i class="icon-tag"></i> Name</label>
|
|
21
|
-
<input type="text" id="node-config-input-name" placeholder="Name">
|
|
22
|
-
</div>
|
|
23
|
-
<div class="form-row">
|
|
24
|
-
<label for="node-config-input-receiverIP">Receiver IP</label>
|
|
25
|
-
<input type="text" id="node-config-input-receiverIP" placeholder="Receiver IP">
|
|
26
|
-
</div>
|
|
27
|
-
<div class="form-row">
|
|
28
|
-
<label for="node-config-input-receiverPort">Receiver Port</label>
|
|
29
|
-
<input type="text" id="node-config-input-receiverPort" placeholder="Receiver Port">
|
|
30
|
-
</div>
|
|
31
|
-
<div class="form-row">
|
|
32
|
-
<label for="node-config-input-siaAccount">SIA Account</label>
|
|
33
|
-
<input type="text" id="node-config-input-siaAccount" placeholder="SIA Account">
|
|
34
|
-
</div>
|
|
35
|
-
<div class="form-row">
|
|
36
|
-
<label for="node-config-input-siaPassword">SIA Password</label>
|
|
37
|
-
<input type="password" id="node-config-input-siaPassword" placeholder="SIA Password">
|
|
38
|
-
</div>
|
|
39
|
-
<div class="form-row">
|
|
40
|
-
<label for="node-config-input-encryptionEnabled">Encryption Enabled</label>
|
|
41
|
-
<input type="checkbox" id="node-config-input-encryptionEnabled" style="display: inline-block; width: auto; vertical-align: middle;">
|
|
42
|
-
</div>
|
|
43
|
-
<div class="form-row">
|
|
44
|
-
<label for="node-config-input-passwordInHex">Password in Hex</label>
|
|
45
|
-
<input type="checkbox" id="node-config-input-passwordInHex" style="display: inline-block; width: auto; vertical-align: middle;">
|
|
46
|
-
</div>
|
|
47
|
-
</script>
|
package/SIA_server.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
module.exports = function(RED) {
|
|
2
|
-
function SIAServerNode(config) {
|
|
3
|
-
RED.nodes.createNode(this, config);
|
|
4
|
-
// Store the configuration values
|
|
5
|
-
this.name = config.name;
|
|
6
|
-
this.receiverIP = config.receiverIP;
|
|
7
|
-
this.receiverPort = config.receiverPort;
|
|
8
|
-
this.siaAccount = config.siaAccount;
|
|
9
|
-
this.siaPassword = config.siaPassword;
|
|
10
|
-
this.encryptionEnabled = config.encryptionEnabled;
|
|
11
|
-
this.passwordInHex = config.passwordInHex;
|
|
12
|
-
}
|
|
13
|
-
RED.nodes.registerType("SIA-server", SIAServerNode);
|
|
14
|
-
};
|