@jotiotech/node-red-sia-premise-device 0.2.2 → 1.0.0
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/SIA_device.html +1 -1
- package/src/SIA_device.js +223 -138
- package/src/SIA_server.html +2 -2
- package/src/SIA_server.js +63 -37
- package/src/table_crc.js +36 -0
package/package.json
CHANGED
package/src/SIA_device.html
CHANGED
package/src/SIA_device.js
CHANGED
|
@@ -1,98 +1,147 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
1
|
+
// Const { Iconv } = require('iconv');
|
|
3
2
|
|
|
4
3
|
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
|
-
|
|
4
|
+
const deviceSeqNumbers = new Map();
|
|
5
|
+
const crypto = require('node:crypto');
|
|
6
|
+
const tableCRC = require('./table_crc.js');
|
|
44
7
|
|
|
45
8
|
function SIADeviceNode(config) {
|
|
46
9
|
RED.nodes.createNode(this, config);
|
|
47
10
|
|
|
48
11
|
// TODO: eventually we should disconnect from the server after each message as there shouldn't be a high volume of messages
|
|
49
12
|
const server = RED.nodes.getNode(config.server);
|
|
50
|
-
|
|
13
|
+
const pendingMessages = new Map();
|
|
51
14
|
const siaConfig = server.getSIAConfig();
|
|
15
|
+
const node = this;
|
|
52
16
|
console.log('SIA Config:', siaConfig);
|
|
53
17
|
|
|
18
|
+
// Cleanup on close
|
|
54
19
|
|
|
55
|
-
|
|
20
|
+
// --- LISTENER FUNCTION ---
|
|
21
|
+
const dataListener = data => {
|
|
22
|
+
const response = data.toString();
|
|
23
|
+
let seq; let type;
|
|
24
|
+
|
|
25
|
+
// Format: <LF><CRC><OLLL><"ID"><seq>...
|
|
26
|
+
const match = response.match(/"(\*?ACK|\*?DUH|NAK)"(\d{4})/);
|
|
27
|
+
console.log('-----------------------------------------\n'
|
|
28
|
+
+ ' SERVER RESPONSE \n'
|
|
29
|
+
+ '-----------------------------------------');
|
|
56
30
|
|
|
31
|
+
console.log('Received message:', response);
|
|
57
32
|
|
|
33
|
+
if (!match) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
58
36
|
|
|
59
|
-
|
|
60
|
-
|
|
37
|
+
type = match[1].replace('*', ''); // "ACK", "DUH", or "NAK"
|
|
38
|
+
seq = match[2]; // The 4-digit sequence number as a string
|
|
39
|
+
|
|
40
|
+
// Handle uncorrectable NAK (seq 0000), likely for a timestamp error
|
|
41
|
+
if (type === 'NAK' && seq === '0000') {
|
|
42
|
+
node.warn('Received uncorrelatable NAK (seq 0000). Erroring all pending requests for this node.');
|
|
43
|
+
for (const [key, pending] of pendingMessages.entries()) {
|
|
44
|
+
if (pending.node === node) {
|
|
45
|
+
clearTimeout(pending.timeoutTimer);
|
|
46
|
+
pending.node.error('Received uncorrelatable NAK (seq 0000) from server', pending.originalMsg);
|
|
47
|
+
pendingMessages.delete(key);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (pendingMessages.has(seq)) {
|
|
54
|
+
const pending = pendingMessages.get(seq);
|
|
55
|
+
if (pending.node !== node) {
|
|
56
|
+
return;
|
|
57
|
+
} // Just in case we ever run multiple nodes
|
|
58
|
+
|
|
59
|
+
clearTimeout(pending.timeoutTimer);
|
|
60
|
+
switch (type) {
|
|
61
|
+
case 'ACK': { // Success!
|
|
62
|
+
pending.node.log(`ACK received for seq ${seq}`);
|
|
63
|
+
pending.node.send([null, {payload:{
|
|
64
|
+
error : false,
|
|
65
|
+
status : "received ACK",
|
|
66
|
+
response: response
|
|
67
|
+
}
|
|
68
|
+
}]); // Send to second output
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
case 'NAK': { // NAK response
|
|
73
|
+
pending.node.error(`Received NAK from server for seq ${seq}`, pending.originalMsg);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
case 'DUH': { // DUH response
|
|
78
|
+
pending.node.error(`Received DUH from server for seq ${seq}`, pending.originalMsg);
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
pendingMessages.delete(seq);
|
|
84
|
+
} else {
|
|
85
|
+
node.log(`Received unknown or stale response for seq ${seq}`);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
node.on('close', done => {
|
|
90
|
+
if (server) {
|
|
91
|
+
server.removeListener('data', dataListener);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const [key, pending] of pendingMessages.entries()) {
|
|
95
|
+
if (pending.node === node) {
|
|
96
|
+
clearTimeout(pending.timeoutTimer);
|
|
97
|
+
pendingMessages.delete(key);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
done();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
server.connect();
|
|
105
|
+
server.on('data', dataListener);
|
|
106
|
+
|
|
107
|
+
// --- HELPER FUNCTIONS ---
|
|
108
|
+
function utf8toWin1252(string_) {
|
|
109
|
+
tmpBuf = Buffer.from(string_, 'utf8');
|
|
61
110
|
return tmpBuf.toString('latin1');
|
|
62
111
|
}
|
|
63
112
|
|
|
64
|
-
function calculateCRCIBM16(
|
|
65
|
-
data = new Buffer.from(
|
|
66
|
-
let
|
|
113
|
+
function calculateCRCIBM16(string_) {
|
|
114
|
+
data = new Buffer.from(string_);
|
|
115
|
+
let {length} = data;
|
|
67
116
|
let buffer = 0;
|
|
68
117
|
let crc;
|
|
69
|
-
while (
|
|
70
|
-
|
|
118
|
+
while (length--) {
|
|
119
|
+
crc = ((crc >>> 8) ^ (tableCRC[(crc ^ (data[buffer++])) & 0xFF]));
|
|
71
120
|
}
|
|
121
|
+
|
|
72
122
|
return crc.toString(16).padStart(4, '0').toUpperCase();
|
|
73
123
|
}
|
|
74
124
|
|
|
75
|
-
function encrypt(encryptionType, encryptionKey,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
125
|
+
function encrypt(encryptionType, encryptionKey, string_) {
|
|
126
|
+
try {
|
|
127
|
+
const encryptionKeyBuf = new Buffer.from(encryptionKey, 'hex');
|
|
128
|
+
const iv = new Buffer.alloc(16);
|
|
129
|
+
iv.fill(0);
|
|
130
|
+
const cipher = crypto.createCipheriv(encryptionType, encryptionKeyBuf, iv);
|
|
131
|
+
cipher.setAutoPadding(false);
|
|
132
|
+
let encoded = cipher.update(string_, 'utf8', 'hex');
|
|
133
|
+
encoded += cipher.final('hex');
|
|
134
|
+
return (encoded ? encoded : undefined);
|
|
135
|
+
} catch {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
88
138
|
}
|
|
89
139
|
|
|
90
|
-
|
|
91
|
-
function generateTimestamp(){
|
|
140
|
+
function generateTimestamp() {
|
|
92
141
|
const now = new Date();
|
|
93
142
|
|
|
94
143
|
// Pad single-digit numbers with a leading zero
|
|
95
|
-
const pad =
|
|
144
|
+
const pad = number_ => number_.toString().padStart(2, '0');
|
|
96
145
|
|
|
97
146
|
const hours = pad(now.getHours());
|
|
98
147
|
const minutes = pad(now.getMinutes());
|
|
@@ -102,115 +151,121 @@ module.exports = function (RED) {
|
|
|
102
151
|
const year = now.getFullYear();
|
|
103
152
|
|
|
104
153
|
return `_${hours}:${minutes}:${seconds},${month}-${day}-${year}`;
|
|
105
|
-
|
|
106
154
|
}
|
|
107
155
|
|
|
108
|
-
function generatePadding(data){
|
|
109
|
-
buffer = new Buffer.from(data); //
|
|
110
|
-
|
|
156
|
+
function generatePadding(data) {
|
|
157
|
+
buffer = new Buffer.from(data); // We need to get byte length of the message body, not string length
|
|
158
|
+
const messageLength = buffer.length;
|
|
111
159
|
|
|
112
160
|
let padding = '';
|
|
113
|
-
if(
|
|
114
|
-
const paddingNeeded = 15 - (
|
|
115
|
-
//
|
|
116
|
-
for(let i=0; i<paddingNeeded; i++){
|
|
161
|
+
if (messageLength % 16 !== 0) {
|
|
162
|
+
const paddingNeeded = 15 - (messageLength % 16);
|
|
163
|
+
// Generate random uppercase letters (only letters) for paddingNeeded
|
|
164
|
+
for (let i = 0; i < paddingNeeded; i++) {
|
|
117
165
|
const randomChar = String.fromCharCode(65 + Math.floor(Math.random() * 26));
|
|
118
166
|
padding += randomChar;
|
|
119
167
|
}
|
|
120
|
-
|
|
168
|
+
|
|
169
|
+
// Generate byte array
|
|
121
170
|
console.log('Padding needed:', paddingNeeded, 'Generated padding:', padding);
|
|
122
|
-
return padding+'|';
|
|
123
|
-
}
|
|
124
|
-
return '';
|
|
125
|
-
}
|
|
171
|
+
return padding + '|';
|
|
172
|
+
}
|
|
126
173
|
|
|
174
|
+
return '';
|
|
175
|
+
}
|
|
127
176
|
|
|
128
|
-
function validateHexString(
|
|
129
|
-
if(
|
|
130
|
-
console.error(`${
|
|
177
|
+
function validateHexString(string_, stringName, minLength, maxLength) {
|
|
178
|
+
if (string_.length < minLength || string_.length > maxLength) {
|
|
179
|
+
console.error(`${stringName} must be between ${minLength} and ${maxLength} characters long`);
|
|
131
180
|
return false;
|
|
132
|
-
|
|
133
181
|
}
|
|
134
182
|
|
|
135
|
-
if(
|
|
136
|
-
console.error(`${
|
|
183
|
+
if (string_.length > 0 && !/^[\dA-Fa-f]+$/.test(string_)) {
|
|
184
|
+
console.error(`${stringName} must be hexadecimal characters only (0-9, A-F)`);
|
|
137
185
|
return false;
|
|
138
186
|
}
|
|
187
|
+
|
|
139
188
|
return true;
|
|
140
189
|
}
|
|
141
190
|
|
|
142
|
-
function
|
|
143
|
-
if(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
191
|
+
function iterateMessageCount(id) {
|
|
192
|
+
if (deviceSeqNumbers.has(id)) {
|
|
193
|
+
let count = deviceSeqNumbers.get(id);
|
|
194
|
+
count = (count % 9999) + 1; // Wrap around at 999
|
|
195
|
+
deviceSeqNumbers.set(id, count);
|
|
196
|
+
} else {
|
|
197
|
+
deviceSeqNumbers.set(id, 1);
|
|
149
198
|
}
|
|
150
199
|
}
|
|
151
200
|
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
let payload = {
|
|
159
|
-
"account" : "AABBCC",
|
|
160
|
-
"accountPrefix" : "5678"
|
|
161
|
-
}
|
|
162
|
-
siaConfig.receiverNumber = "1234";
|
|
201
|
+
// --- PROCESS NEW MESSAGE ---
|
|
202
|
+
node.on('input', message => {
|
|
203
|
+
console.log('-----------------------------------------\n'
|
|
204
|
+
+ ' MESSAGE START \n'
|
|
205
|
+
+ '-----------------------------------------');
|
|
206
|
+
const payload = message.payload;
|
|
163
207
|
|
|
164
208
|
const deviceAccount = payload.account;
|
|
165
209
|
const deviceAccountPrefix = payload.accountPrefix || '0';
|
|
166
210
|
|
|
167
211
|
// INPUT VALIDATION
|
|
212
|
+
if (!validateHexString(deviceAccount, 'Device account', 3, 16)) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
168
215
|
|
|
169
|
-
if(!validateHexString(
|
|
170
|
-
|
|
171
|
-
|
|
216
|
+
if (!validateHexString(deviceAccountPrefix, 'Device account prefix', 1, 6)) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
172
219
|
|
|
173
|
-
|
|
220
|
+
if (!validateHexString(siaConfig.receiverNumber, 'Receiver number', 0, 6)) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
174
223
|
|
|
175
|
-
|
|
224
|
+
const deviceIdentifier = 'L' + deviceAccountPrefix + '#' + deviceAccount;
|
|
176
225
|
|
|
226
|
+
iterateMessageCount(deviceIdentifier);
|
|
177
227
|
|
|
178
|
-
|
|
228
|
+
// PART: <"id"><seq><Rrcvr><Lpref><#acct>[
|
|
179
229
|
let messageBodyStart = '"'; // <"id"><seq><Rrcvr><Lpref><#acct>[
|
|
180
|
-
if(siaConfig.encryptionEnabled)
|
|
230
|
+
if (siaConfig.encryptionEnabled) {
|
|
181
231
|
messageBodyStart += '*';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
messageBodyStart += 'SIA-DCS"'; // ID placeholder
|
|
235
|
+
const seqNumber = deviceSeqNumbers.get(deviceIdentifier).toString().padStart(4, '0');
|
|
236
|
+
|
|
237
|
+
messageBodyStart += seqNumber; // Message sequence number
|
|
238
|
+
if (siaConfig.receiverNumber.length > 0) {
|
|
239
|
+
messageBodyStart += 'R' + siaConfig.receiverNumber;
|
|
240
|
+
}
|
|
182
241
|
|
|
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
242
|
messageBodyStart += deviceIdentifier;
|
|
188
243
|
messageBodyStart += '[';
|
|
189
244
|
|
|
190
|
-
|
|
191
245
|
// PART: #<acct>|<data>][<extende data>]
|
|
192
|
-
let messageBodyData = '#'+deviceAccount + '|';
|
|
246
|
+
let messageBodyData = '#' + deviceAccount + '|'; // <pad>|...data...][x…data…]
|
|
193
247
|
|
|
194
248
|
// data format
|
|
195
249
|
// (N)(id-number)(DCS)(zone)
|
|
196
250
|
// there's also a format of group-zone
|
|
197
251
|
|
|
198
|
-
// messageBodyData += "
|
|
199
|
-
messageBodyData +=
|
|
252
|
+
// messageBodyData += "Nri129^FA"; //
|
|
253
|
+
messageBodyData += payload.body;
|
|
254
|
+
// messageBodyData += 'Nri129/FA1234'; //
|
|
200
255
|
// optional extended data can be added here, for example
|
|
201
256
|
|
|
202
257
|
// PART: ]<timestamp>, finalization of body
|
|
203
258
|
let messageBody = ''; // <"id"><seq><Rrcvr><Lpref><#acct>[<pad>|...data...][x…data…]<timestamp>
|
|
204
|
-
|
|
259
|
+
const timestamp = generateTimestamp(); // TODO: this should be optional
|
|
205
260
|
|
|
206
|
-
if(siaConfig.encryptionEnabled){ //
|
|
207
|
-
let toEncrypt = messageBodyData+']'+timestamp;
|
|
261
|
+
if (siaConfig.encryptionEnabled) { // Everything past [ till <CR> is to be encrypted
|
|
262
|
+
let toEncrypt = messageBodyData + ']' + timestamp;
|
|
208
263
|
toEncrypt = generatePadding(toEncrypt) + toEncrypt;
|
|
209
264
|
messageBody = messageBodyStart + encrypt(siaConfig.encryptionType, siaConfig.encryptionKey, toEncrypt);
|
|
210
|
-
}else{
|
|
265
|
+
} else {
|
|
211
266
|
messageBody += messageBodyStart;
|
|
212
267
|
messageBody += messageBodyData;
|
|
213
|
-
messageBody += ']'; //
|
|
268
|
+
messageBody += ']'; // End of data fields
|
|
214
269
|
// optional extended data
|
|
215
270
|
messageBody += timestamp;
|
|
216
271
|
}
|
|
@@ -218,21 +273,51 @@ module.exports = function (RED) {
|
|
|
218
273
|
// Append <LF><crc><>
|
|
219
274
|
msgCount = (new Buffer.from(messageBody)).length;
|
|
220
275
|
|
|
221
|
-
let
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
276
|
+
let message_ = '\n'; // <LF><crc><0LLL><"id"><seq><Rrcvr><Lpref><#acct>[<pad>|...data...][x…data…]<timestamp><CR>
|
|
277
|
+
const crc = calculateCRCIBM16(messageBody);
|
|
278
|
+
console.log("Final message CRC: ", crc);
|
|
279
|
+
|
|
280
|
+
message_ += crc; // CRC is 4 ASCII characters
|
|
281
|
+
message_ += '0' + msgCount.toString(16).toUpperCase().padStart(3, '0'); // Carriage return
|
|
282
|
+
message_ += messageBody;
|
|
283
|
+
message_ += '\r';
|
|
284
|
+
|
|
285
|
+
console.log('Final message:', message_);
|
|
286
|
+
console.log('Sending message to server');
|
|
287
|
+
|
|
288
|
+
pendingMessages.set(seqNumber, {
|
|
289
|
+
node,
|
|
290
|
+
originalMsg: message,
|
|
291
|
+
timestamp: Date.now(),
|
|
292
|
+
timeoutTimer: setTimeout(() => {
|
|
293
|
+
if (pendingMessages.has(seqNumber)) {
|
|
294
|
+
node.error(`Timeout waiting for ACK on seq ${seqNumber}`, message);
|
|
295
|
+
node.status({fill: 'red', shape: 'dot', text: 'Timeout ' + seqNumber});
|
|
296
|
+
node.send([null, {
|
|
297
|
+
payload: {
|
|
298
|
+
error: true,
|
|
299
|
+
status: 'timeout on message',
|
|
300
|
+
message: message
|
|
301
|
+
},
|
|
302
|
+
}]);
|
|
303
|
+
pendingMessages.delete(seqNumber);
|
|
304
|
+
}
|
|
305
|
+
}, 5000), // 5 Second timeout
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
node.log(`Sending SIA message seq: ${seqNumber}`);
|
|
309
|
+
node.status({fill: 'blue', shape: 'dot', text: 'Sent ' + seqNumber});
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
server.write(message_);
|
|
313
|
+
|
|
314
|
+
node.send([{
|
|
315
|
+
payload: {
|
|
316
|
+
error: false,
|
|
317
|
+
status: 'sent message',
|
|
318
|
+
message: message_
|
|
319
|
+
},
|
|
320
|
+
}, null]); // Send to first output
|
|
236
321
|
});
|
|
237
322
|
}
|
|
238
323
|
|
package/src/SIA_server.html
CHANGED
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
<input type="text" id="node-config-input-name" placeholder="Name">
|
|
57
57
|
</div>
|
|
58
58
|
<div class="form-row">
|
|
59
|
-
<label for="node-config-input-receiverHost">Receiver
|
|
59
|
+
<label for="node-config-input-receiverHost">Receiver</label>
|
|
60
60
|
<input type="text" id="node-config-input-receiverHost" placeholder="Receiver Host">
|
|
61
61
|
</div>
|
|
62
62
|
<div class="form-row">
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
</div>
|
|
66
66
|
<div class="form-row">
|
|
67
67
|
<label for="node-config-input-receiverNumber">Receiver Number</label>
|
|
68
|
-
<input type="
|
|
68
|
+
<input type="text" id="node-config-input-receiverNumber" placeholder="Receiver Number">
|
|
69
69
|
</div>
|
|
70
70
|
<div class="form-row">
|
|
71
71
|
<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;
|