dsc-itv2-client 1.0.23 → 1.0.24
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/ITV2Client.js +145 -13
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsc-itv2-client",
|
|
3
3
|
"author": "fajitacat",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.24",
|
|
5
5
|
"description": "Reverse engineered DSC ITV2 Protocol Client Library for TL280R Communicator - Monitor and control DSC alarm panels",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"type": "module",
|
package/src/ITV2Client.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import {EventEmitter} from 'events';
|
|
7
7
|
import dgram from 'dgram';
|
|
8
|
+
import net from 'net';
|
|
8
9
|
import {ITv2Session, CMD, CMD_NAMES} from './itv2-session.js';
|
|
9
10
|
import {parseType1Initializer, type2InitializerTransform} from './itv2-crypto.js';
|
|
10
11
|
import {EventHandler} from './event-handler.js';
|
|
@@ -22,18 +23,23 @@ export class ITV2Client extends EventEmitter {
|
|
|
22
23
|
this.integrationId = options.integrationId;
|
|
23
24
|
this.accessCode = options.accessCode;
|
|
24
25
|
this.masterCode = options.masterCode || '5555';
|
|
25
|
-
this.port = options.port || 3073;
|
|
26
|
+
this.port = options.port || 3073; // UDP polling port
|
|
27
|
+
this.tcpPort = options.tcpPort || 3072; // TCP notification port
|
|
26
28
|
this.logLevel = options.logLevel || (options.debug === true ? 'verbose' : 'minimal'); // 'silent', 'minimal', 'verbose'
|
|
27
29
|
this.encryptionType = options.encryptionType || null; // null = auto-detect, 1 = Type 1, 2 = Type 2
|
|
28
30
|
|
|
29
31
|
// Internal state
|
|
30
|
-
this.server = null;
|
|
32
|
+
this.server = null; // UDP server
|
|
33
|
+
this.tcpServer = null; // TCP server
|
|
34
|
+
this.tcpClients = new Map(); // Active TCP connections
|
|
31
35
|
this.session = null;
|
|
32
36
|
this.eventHandler = new EventHandler();
|
|
33
37
|
this.handshakeState = 'WAITING';
|
|
34
38
|
this.panelAddress = null;
|
|
35
39
|
this.panelPort = null;
|
|
36
40
|
this.detectedEncryptionType = null; // Set during handshake
|
|
41
|
+
this._lastTransport = 'udp'; // Track last packet source
|
|
42
|
+
this._lastTcpSocket = null; // Track TCP socket for responses
|
|
37
43
|
|
|
38
44
|
// Bind methods
|
|
39
45
|
this._handlePacket = this._handlePacket.bind(this);
|
|
@@ -69,6 +75,9 @@ export class ITV2Client extends EventEmitter {
|
|
|
69
75
|
|
|
70
76
|
this.server.bind(this.port, '0.0.0.0');
|
|
71
77
|
|
|
78
|
+
// Start TCP server for notification port
|
|
79
|
+
this._startTcpServer();
|
|
80
|
+
|
|
72
81
|
// Register signal handlers (only once)
|
|
73
82
|
if (!this._boundShutdown) {
|
|
74
83
|
this._boundShutdown = async () => {
|
|
@@ -81,6 +90,97 @@ export class ITV2Client extends EventEmitter {
|
|
|
81
90
|
});
|
|
82
91
|
}
|
|
83
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Start TCP server for notification port (3072)
|
|
95
|
+
*/
|
|
96
|
+
_startTcpServer() {
|
|
97
|
+
this.tcpServer = net.createServer((socket) => {
|
|
98
|
+
const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
|
|
99
|
+
this._logMinimal(`[TCP] Client connected: ${clientId}`);
|
|
100
|
+
this.tcpClients.set(clientId, socket);
|
|
101
|
+
|
|
102
|
+
// Store panel address from TCP connection too
|
|
103
|
+
if (!this.panelAddress) {
|
|
104
|
+
this.panelAddress = socket.remoteAddress;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let buffer = Buffer.alloc(0);
|
|
108
|
+
|
|
109
|
+
socket.on('data', (data) => {
|
|
110
|
+
this._logMinimal(`[TCP] Received ${data.length} bytes from ${clientId}`);
|
|
111
|
+
this._logMinimal(`[TCP] Raw data: ${data.toString('hex')}`);
|
|
112
|
+
|
|
113
|
+
// Accumulate data in buffer
|
|
114
|
+
buffer = Buffer.concat([buffer, data]);
|
|
115
|
+
|
|
116
|
+
// Try to parse complete packets (look for 0x7E...0x7F framing)
|
|
117
|
+
this._processTcpBuffer(buffer, socket, (remaining) => {
|
|
118
|
+
buffer = remaining;
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
socket.on('close', () => {
|
|
123
|
+
this._logMinimal(`[TCP] Client disconnected: ${clientId}`);
|
|
124
|
+
this.tcpClients.delete(clientId);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
socket.on('error', (err) => {
|
|
128
|
+
this._log(`[TCP] Socket error for ${clientId}: ${err.message}`);
|
|
129
|
+
this.tcpClients.delete(clientId);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
this.tcpServer.on('error', (err) => {
|
|
134
|
+
this._log(`[TCP] Server error: ${err.message}`);
|
|
135
|
+
this.emit('error', err);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
this.tcpServer.listen(this.tcpPort, '0.0.0.0', () => {
|
|
139
|
+
this._logMinimal(`[TCP] Server listening on port ${this.tcpPort}`);
|
|
140
|
+
this.emit('tcp:listening', { port: this.tcpPort });
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Process TCP buffer looking for complete ITV2 packets
|
|
146
|
+
*/
|
|
147
|
+
_processTcpBuffer(buffer, socket, callback) {
|
|
148
|
+
// Look for Integration ID prefix followed by 0x7E
|
|
149
|
+
const idLength = 12; // Integration ID is 12 ASCII chars
|
|
150
|
+
|
|
151
|
+
while (buffer.length > idLength + 2) {
|
|
152
|
+
// Find start marker (0x7E) after integration ID
|
|
153
|
+
const startIdx = buffer.indexOf(0x7E, idLength - 1);
|
|
154
|
+
if (startIdx === -1) {
|
|
155
|
+
// No start marker found, keep last 12 bytes in case ID is split
|
|
156
|
+
callback(buffer.slice(Math.max(0, buffer.length - idLength)));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Find end marker (0x7F)
|
|
161
|
+
const endIdx = buffer.indexOf(0x7F, startIdx + 1);
|
|
162
|
+
if (endIdx === -1) {
|
|
163
|
+
// No end marker yet, wait for more data
|
|
164
|
+
callback(buffer);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Extract complete packet (including ID prefix)
|
|
169
|
+
const packetStart = Math.max(0, startIdx - idLength);
|
|
170
|
+
const packet = buffer.slice(packetStart, endIdx + 1);
|
|
171
|
+
|
|
172
|
+
this._logMinimal(`[TCP] Complete packet (${packet.length} bytes): ${packet.toString('hex')}`);
|
|
173
|
+
|
|
174
|
+
// Process the packet same as UDP
|
|
175
|
+
this._handlePacket(packet, 'tcp', socket);
|
|
176
|
+
|
|
177
|
+
// Continue with remaining buffer
|
|
178
|
+
buffer = buffer.slice(endIdx + 1);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
callback(buffer);
|
|
182
|
+
}
|
|
183
|
+
|
|
84
184
|
/**
|
|
85
185
|
* Stop the client and close the session gracefully
|
|
86
186
|
*/
|
|
@@ -105,6 +205,23 @@ export class ITV2Client extends EventEmitter {
|
|
|
105
205
|
this.server = null;
|
|
106
206
|
}
|
|
107
207
|
|
|
208
|
+
// Close TCP server
|
|
209
|
+
if (this.tcpServer) {
|
|
210
|
+
// Close all TCP client connections
|
|
211
|
+
for (const [clientId, socket] of this.tcpClients) {
|
|
212
|
+
socket.destroy();
|
|
213
|
+
}
|
|
214
|
+
this.tcpClients.clear();
|
|
215
|
+
|
|
216
|
+
await new Promise((resolve) => {
|
|
217
|
+
this.tcpServer.close(() => {
|
|
218
|
+
this._log('[Shutdown] TCP server closed');
|
|
219
|
+
resolve();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
this.tcpServer = null;
|
|
223
|
+
}
|
|
224
|
+
|
|
108
225
|
// Clear session and encryption state to prevent stale keys on reconnect
|
|
109
226
|
if (this.session) {
|
|
110
227
|
this.session.disableAes();
|
|
@@ -277,19 +394,29 @@ export class ITV2Client extends EventEmitter {
|
|
|
277
394
|
}
|
|
278
395
|
|
|
279
396
|
_sendPacket(packet) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
397
|
+
// Use the same transport that the last packet came from
|
|
398
|
+
if (this._lastTransport === 'tcp' && this._lastTcpSocket) {
|
|
399
|
+
this._log(`\n[TCP] TX (${packet.length} bytes)`);
|
|
400
|
+
if (this.debug) {
|
|
401
|
+
this._hexDump(packet);
|
|
402
|
+
}
|
|
403
|
+
this._lastTcpSocket.write(packet);
|
|
404
|
+
} else {
|
|
405
|
+
// UDP (default)
|
|
406
|
+
if (!this.panelAddress || !this.panelPort) {
|
|
407
|
+
this._log('[Error] No panel address/port set');
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
284
410
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
411
|
+
this._log(`\n[UDP] TX to ${this.panelAddress}:${this.panelPort} (${packet.length} bytes)`);
|
|
412
|
+
if (this.debug) {
|
|
413
|
+
this._hexDump(packet);
|
|
414
|
+
}
|
|
415
|
+
this.server.send(packet, this.panelPort, this.panelAddress);
|
|
288
416
|
}
|
|
289
|
-
this.server.send(packet, this.panelPort, this.panelAddress);
|
|
290
417
|
}
|
|
291
418
|
|
|
292
|
-
_handlePacket(data) {
|
|
419
|
+
_handlePacket(data, transport = 'udp', tcpSocket = null) {
|
|
293
420
|
try {
|
|
294
421
|
if (!this.session) {
|
|
295
422
|
// Create session with appropriate logger based on log level
|
|
@@ -304,8 +431,13 @@ export class ITV2Client extends EventEmitter {
|
|
|
304
431
|
return;
|
|
305
432
|
}
|
|
306
433
|
|
|
307
|
-
//
|
|
308
|
-
this.
|
|
434
|
+
// Store transport info for responses
|
|
435
|
+
this._lastTransport = transport;
|
|
436
|
+
this._lastTcpSocket = tcpSocket;
|
|
437
|
+
|
|
438
|
+
// Verbose logging
|
|
439
|
+
const source = transport === 'tcp' ? 'TCP' : `UDP ${this.panelAddress}:${this.panelPort}`;
|
|
440
|
+
this._log(`\n[${transport.toUpperCase()}] RX from ${source} (${data.length} bytes)`);
|
|
309
441
|
this._hexDump(data);
|
|
310
442
|
this._log(`[Packet] Integration ID: ${parsed.integrationId}`);
|
|
311
443
|
this._log(`[Packet] Sender Seq: ${parsed.senderSequence}, Receiver Seq: ${parsed.receiverSequence}`);
|