dsc-itv2-client 2.0.7 → 2.0.10
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 +95 -17
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsc-itv2-client",
|
|
3
3
|
"author": "fajitacat",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.10",
|
|
5
5
|
"description": "Reverse engineered DSC ITV2 Protocol Client Library for TL280/TL280E - Monitor and control DSC Neo alarm panels with real-time zone/partition status, arming, and trouble detail",
|
|
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 net from 'net';
|
|
8
|
+
import dgram from 'dgram';
|
|
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';
|
|
@@ -23,12 +24,16 @@ export class ITV2Client extends EventEmitter {
|
|
|
23
24
|
this.accessCode = options.accessCode;
|
|
24
25
|
this.masterCode = options.masterCode || '5555';
|
|
25
26
|
this.port = options.port || 3072; // TCP port (panel connects to us)
|
|
27
|
+
this.udpPort = options.udpPort || null; // UDP port (optional, panel polls us)
|
|
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
32
|
this.tcpServer = null; // TCP server
|
|
31
33
|
this.tcpSocket = null; // Active panel TCP connection
|
|
34
|
+
this.udpSocket = null; // UDP socket
|
|
35
|
+
this._udpRemote = null; // { address, port } of panel over UDP
|
|
36
|
+
this._activeTransport = null; // 'tcp' or 'udp' — whichever the panel connected on
|
|
32
37
|
this.session = null;
|
|
33
38
|
this.eventHandler = new EventHandler();
|
|
34
39
|
this.handshakeState = 'WAITING';
|
|
@@ -48,17 +53,20 @@ export class ITV2Client extends EventEmitter {
|
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
/**
|
|
51
|
-
* Start the TCP server and listen for panel connection
|
|
56
|
+
* Start the TCP server (and optional UDP socket) and listen for panel connection
|
|
52
57
|
* The panel initiates a TCP connection to us on port 3072 (default)
|
|
58
|
+
* If udpPort is configured, also listen for UDP datagrams
|
|
53
59
|
*/
|
|
54
60
|
async start() {
|
|
55
|
-
|
|
61
|
+
// Start TCP server
|
|
62
|
+
await new Promise((resolve, reject) => {
|
|
56
63
|
this.tcpServer = net.createServer((socket) => {
|
|
57
64
|
const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
|
|
58
65
|
this._logMinimal(`[TCP] Panel connected: ${clientId}`);
|
|
59
66
|
|
|
60
67
|
this.panelAddress = socket.remoteAddress;
|
|
61
68
|
this.tcpSocket = socket;
|
|
69
|
+
this._activeTransport = 'tcp';
|
|
62
70
|
|
|
63
71
|
let buffer = Buffer.alloc(0);
|
|
64
72
|
|
|
@@ -75,12 +83,15 @@ export class ITV2Client extends EventEmitter {
|
|
|
75
83
|
socket.on('close', () => {
|
|
76
84
|
this._logMinimal(`[TCP] Panel disconnected: ${clientId}`);
|
|
77
85
|
this.tcpSocket = null;
|
|
78
|
-
this.
|
|
79
|
-
|
|
80
|
-
this.
|
|
81
|
-
this.session
|
|
86
|
+
if (this._activeTransport === 'tcp') {
|
|
87
|
+
this._activeTransport = null;
|
|
88
|
+
this.handshakeState = 'WAITING';
|
|
89
|
+
if (this.session) {
|
|
90
|
+
this.session.disableAes();
|
|
91
|
+
this.session = null;
|
|
92
|
+
}
|
|
93
|
+
this.emit('session:closed');
|
|
82
94
|
}
|
|
83
|
-
this.emit('session:closed');
|
|
84
95
|
});
|
|
85
96
|
|
|
86
97
|
socket.on('error', (err) => {
|
|
@@ -101,16 +112,60 @@ export class ITV2Client extends EventEmitter {
|
|
|
101
112
|
this.emit('listening', address);
|
|
102
113
|
resolve();
|
|
103
114
|
});
|
|
115
|
+
});
|
|
104
116
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
117
|
+
// Start UDP socket if configured
|
|
118
|
+
if (this.udpPort) {
|
|
119
|
+
await this._startUdpSocket();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Register signal handlers (only once)
|
|
123
|
+
if (!this._boundShutdown) {
|
|
124
|
+
this._boundShutdown = async () => {
|
|
125
|
+
await this.stop();
|
|
126
|
+
process.exit(0);
|
|
127
|
+
};
|
|
128
|
+
process.on('SIGINT', this._boundShutdown);
|
|
129
|
+
process.on('SIGTERM', this._boundShutdown);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Start UDP socket for panel communication
|
|
135
|
+
* UDP datagrams are self-contained packets (no stream reassembly needed)
|
|
136
|
+
*/
|
|
137
|
+
async _startUdpSocket() {
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
this.udpSocket = dgram.createSocket('udp4');
|
|
140
|
+
|
|
141
|
+
this.udpSocket.on('message', (msg, rinfo) => {
|
|
142
|
+
this._log(`[UDP] Received ${msg.length} bytes from ${rinfo.address}:${rinfo.port}`);
|
|
143
|
+
|
|
144
|
+
// Track remote for responses
|
|
145
|
+
this._udpRemote = { address: rinfo.address, port: rinfo.port };
|
|
146
|
+
this.panelAddress = rinfo.address;
|
|
147
|
+
|
|
148
|
+
// If panel is using UDP, mark it as active transport
|
|
149
|
+
if (this._activeTransport !== 'tcp') {
|
|
150
|
+
this._activeTransport = 'udp';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// UDP datagrams are complete packets — process directly
|
|
154
|
+
this._handlePacket(msg);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
this.udpSocket.on('error', (err) => {
|
|
158
|
+
this._log(`[Error] UDP socket error: ${err.message}`);
|
|
159
|
+
this.emit('error', err);
|
|
160
|
+
reject(err);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
this.udpSocket.bind(this.udpPort, '0.0.0.0', () => {
|
|
164
|
+
const address = this.udpSocket.address();
|
|
165
|
+
this._logMinimal(`[UDP] Listening on port ${address.port}`);
|
|
166
|
+
this.emit('listening:udp', address);
|
|
167
|
+
resolve();
|
|
168
|
+
});
|
|
114
169
|
});
|
|
115
170
|
}
|
|
116
171
|
|
|
@@ -180,6 +235,18 @@ export class ITV2Client extends EventEmitter {
|
|
|
180
235
|
this.tcpServer = null;
|
|
181
236
|
}
|
|
182
237
|
|
|
238
|
+
// Close UDP socket
|
|
239
|
+
if (this.udpSocket) {
|
|
240
|
+
await new Promise((resolve) => {
|
|
241
|
+
this.udpSocket.close(() => {
|
|
242
|
+
this._log('[Shutdown] UDP socket closed');
|
|
243
|
+
resolve();
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
this.udpSocket = null;
|
|
247
|
+
this._udpRemote = null;
|
|
248
|
+
}
|
|
249
|
+
|
|
183
250
|
// Clear heartbeat
|
|
184
251
|
if (this._heartbeatInterval) {
|
|
185
252
|
clearInterval(this._heartbeatInterval);
|
|
@@ -205,6 +272,7 @@ export class ITV2Client extends EventEmitter {
|
|
|
205
272
|
}
|
|
206
273
|
this.handshakeState = 'WAITING';
|
|
207
274
|
this.panelAddress = null;
|
|
275
|
+
this._activeTransport = null;
|
|
208
276
|
this.detectedEncryptionType = null;
|
|
209
277
|
this._panelOpenSessionData = null;
|
|
210
278
|
|
|
@@ -627,6 +695,13 @@ export class ITV2Client extends EventEmitter {
|
|
|
627
695
|
}
|
|
628
696
|
|
|
629
697
|
_sendPacket(packet) {
|
|
698
|
+
if (this._activeTransport === 'udp' && this.udpSocket && this._udpRemote) {
|
|
699
|
+
this._log(`[UDP] TX ${packet.length} bytes`);
|
|
700
|
+
this._hexDump(packet);
|
|
701
|
+
this.udpSocket.send(packet, this._udpRemote.port, this._udpRemote.address);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
|
|
630
705
|
if (!this.tcpSocket || this.tcpSocket.destroyed) {
|
|
631
706
|
this._logMinimal('[Error] No panel connection');
|
|
632
707
|
return;
|
|
@@ -654,9 +729,11 @@ export class ITV2Client extends EventEmitter {
|
|
|
654
729
|
this._log(`[Packet] Integration ID: ${parsed.integrationId}`);
|
|
655
730
|
this._log(`[Packet] Sender Seq: ${parsed.senderSequence}, Receiver Seq: ${parsed.receiverSequence}`);
|
|
656
731
|
this._log(`[Packet] Command: ${CMD_NAMES[parsed.command] || parsed.command}`);
|
|
732
|
+
|
|
657
733
|
if (parsed.appSequence !== null) {
|
|
658
734
|
this._log(`[Packet] App Seq: ${parsed.appSequence}`);
|
|
659
735
|
}
|
|
736
|
+
|
|
660
737
|
if (parsed.commandData) {
|
|
661
738
|
this._log(`[Packet] Data (${parsed.commandData.length} bytes): ${parsed.commandData.toString('hex').toUpperCase().match(/.{1,2}/g).join(' ')}`);
|
|
662
739
|
}
|
|
@@ -1542,8 +1619,9 @@ export class ITV2Client extends EventEmitter {
|
|
|
1542
1619
|
this.emit('zone:alarm', alarmData);
|
|
1543
1620
|
this.emit('partition:alarm', alarmData);
|
|
1544
1621
|
} else {
|
|
1622
|
+
// Zone alarm cleared — but partition stays in alarm until disarmed.
|
|
1623
|
+
// Only emit zone-level restore, not partition-level.
|
|
1545
1624
|
this.emit('zone:alarm:restored', alarmData);
|
|
1546
|
-
this.emit('partition:alarm:restored', alarmData);
|
|
1547
1625
|
}
|
|
1548
1626
|
}
|
|
1549
1627
|
} catch (e) {
|