dsc-itv2-client 1.0.7

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.
@@ -0,0 +1,502 @@
1
+ /**
2
+ * DSC ITV2 Protocol Client
3
+ * Event-driven client for DSC alarm panels using the ITV2 protocol
4
+ */
5
+
6
+ import { EventEmitter } from 'events';
7
+ import dgram from 'dgram';
8
+ import { ITv2Session, CMD, CMD_NAMES } from './itv2-session.js';
9
+ import { parseType1Initializer, type2InitializerTransform } from './itv2-crypto.js';
10
+ import { EventHandler } from './event-handler.js';
11
+ import { parseCommandError } from './response-parsers.js';
12
+
13
+ /**
14
+ * ITV2Client - Main library class
15
+ * Handles DSC panel communication and emits events for status updates
16
+ */
17
+ export class ITV2Client extends EventEmitter {
18
+ constructor(options = {}) {
19
+ super();
20
+
21
+ // Configuration
22
+ this.integrationId = options.integrationId;
23
+ this.accessCode = options.accessCode;
24
+ this.masterCode = options.masterCode || '5555';
25
+ this.port = options.port || 3073;
26
+ this.logLevel = options.logLevel || (options.debug === true ? 'verbose' : 'minimal'); // 'silent', 'minimal', 'verbose'
27
+ this.encryptionType = options.encryptionType || null; // null = auto-detect, 1 = Type 1, 2 = Type 2
28
+
29
+ // Internal state
30
+ this.server = null;
31
+ this.session = null;
32
+ this.eventHandler = new EventHandler();
33
+ this.handshakeState = 'WAITING';
34
+ this.panelAddress = null;
35
+ this.panelPort = null;
36
+ this.detectedEncryptionType = null; // Set during handshake
37
+
38
+ // Bind methods
39
+ this._handlePacket = this._handlePacket.bind(this);
40
+ this._handleError = this._handleError.bind(this);
41
+ }
42
+
43
+ /**
44
+ * Start the client and listen for panel connection
45
+ */
46
+ async start() {
47
+ return new Promise((resolve, reject) => {
48
+ this.server = dgram.createSocket('udp4');
49
+
50
+ this.server.on('error', (err) => {
51
+ this._log(`[Error] UDP server error: ${err.message}`);
52
+ this.emit('error', err);
53
+ reject(err);
54
+ });
55
+
56
+ this.server.on('message', (msg, rinfo) => {
57
+ this.panelAddress = rinfo.address;
58
+ this.panelPort = rinfo.port;
59
+ this._handlePacket(msg);
60
+ });
61
+
62
+ this.server.on('listening', () => {
63
+ const address = this.server.address();
64
+ this._log(`[UDP] Server listening on ${address.address}:${address.port}`);
65
+ this.emit('listening', address);
66
+ resolve();
67
+ });
68
+
69
+ this.server.bind(this.port, '0.0.0.0');
70
+ });
71
+ }
72
+
73
+ /**
74
+ * Stop the client and close the session gracefully
75
+ */
76
+ async stop() {
77
+ if (this.session && this.handshakeState === 'ESTABLISHED') {
78
+ try {
79
+ const endSessionPacket = this.session.buildEndSession();
80
+ this._sendPacket(endSessionPacket);
81
+ this._log('[Shutdown] END_SESSION sent to panel');
82
+ } catch (err) {
83
+ this._log(`[Shutdown] Error sending END_SESSION: ${err.message}`);
84
+ }
85
+ }
86
+
87
+ if (this.server) {
88
+ await new Promise((resolve) => {
89
+ this.server.close(() => {
90
+ this._log('[Shutdown] UDP server closed');
91
+ resolve();
92
+ });
93
+ });
94
+ }
95
+
96
+ this.emit('session:closed');
97
+ }
98
+
99
+ /**
100
+ * Arm partition in stay mode
101
+ */
102
+ armStay(partition, code) {
103
+ if (!this._checkEstablished()) return;
104
+ const packet = this.session.buildPartitionArm(partition, 0, code || this.masterCode);
105
+ this._sendPacket(packet);
106
+ }
107
+
108
+ /**
109
+ * Arm partition in away mode
110
+ */
111
+ armAway(partition, code) {
112
+ if (!this._checkEstablished()) return;
113
+ const packet = this.session.buildPartitionArm(partition, 1, code || this.masterCode);
114
+ this._sendPacket(packet);
115
+ }
116
+
117
+ /**
118
+ * Disarm partition
119
+ */
120
+ disarm(partition, code) {
121
+ if (!this._checkEstablished()) return;
122
+ const packet = this.session.buildPartitionDisarm(partition, code || this.masterCode);
123
+ this._sendPacket(packet);
124
+ }
125
+
126
+ /**
127
+ * Get current zone states
128
+ */
129
+ getZones() {
130
+ return this.eventHandler.getZoneStates();
131
+ }
132
+
133
+ /**
134
+ * Get current partition states
135
+ */
136
+ getPartitions() {
137
+ return this.eventHandler.getPartitionStates();
138
+ }
139
+
140
+ // ==================== Internal Methods ====================
141
+
142
+ _checkEstablished() {
143
+ if (this.handshakeState !== 'ESTABLISHED') {
144
+ this.emit('error', new Error('Session not established'));
145
+ return false;
146
+ }
147
+ return true;
148
+ }
149
+
150
+ _sendPacket(packet) {
151
+ if (!this.panelAddress || !this.panelPort) {
152
+ this._log('[Error] No panel address/port set');
153
+ return;
154
+ }
155
+
156
+ this._log(`\n[UDP] TX to ${this.panelAddress}:${this.panelPort} (${packet.length} bytes)`);
157
+ if (this.debug) {
158
+ this._hexDump(packet);
159
+ }
160
+ this.server.send(packet, this.panelPort, this.panelAddress);
161
+ }
162
+
163
+ _handlePacket(data) {
164
+ try {
165
+ if (!this.session) {
166
+ // Create session with appropriate logger based on log level
167
+ const logger = this.logLevel === 'verbose' ? this._log.bind(this) : () => {};
168
+ this.session = new ITv2Session(this.integrationId, this.accessCode, logger);
169
+ this.emit('session:connecting');
170
+ this._logMinimal(`[Session] Panel connecting from ${this.panelAddress}`);
171
+ }
172
+
173
+ const parsed = this.session.parsePacket(data);
174
+ if (!parsed) {
175
+ this._log('[Error] Failed to parse packet');
176
+ return;
177
+ }
178
+
179
+ // Verbose logging only
180
+ this._log(`\n[UDP] RX from ${this.panelAddress}:${this.panelPort} (${data.length} bytes)`);
181
+ this._hexDump(data);
182
+ this._log(`[Packet] Integration ID: ${parsed.integrationId}`);
183
+ this._log(`[Packet] Sender Seq: ${parsed.senderSequence}, Receiver Seq: ${parsed.receiverSequence}`);
184
+ this._log(`[Packet] Command: ${CMD_NAMES[parsed.command] || parsed.command}`);
185
+ if (parsed.appSequence !== null) {
186
+ this._log(`[Packet] App Seq: ${parsed.appSequence}`);
187
+ }
188
+ if (parsed.commandData) {
189
+ this._log(`[Packet] Data (${parsed.commandData.length} bytes): ${parsed.commandData.toString('hex').toUpperCase().match(/.{1,2}/g).join(' ')}`);
190
+ }
191
+
192
+ // Route based on command
193
+ this._routePacket(parsed);
194
+
195
+ } catch (err) {
196
+ this._log(`[Error] ${err.message}`);
197
+ this.emit('error', err);
198
+
199
+ // Reset on parse errors
200
+ if (this.handshakeState !== 'WAITING' && this.handshakeState !== 'SENT_CMD_RESPONSE_1') {
201
+ this._log('[Recovery] Parse error, resetting to wait for panel restart');
202
+ this.handshakeState = 'WAITING';
203
+ }
204
+ }
205
+ }
206
+
207
+ _routePacket(parsed) {
208
+ const cmd = parsed.command;
209
+
210
+ // Handle ACKs
211
+ if (cmd === null || cmd === undefined || cmd === CMD.SIMPLE_ACK) {
212
+ this._handleSimpleAck(parsed);
213
+ return;
214
+ }
215
+
216
+ // Route based on command type
217
+ switch (cmd) {
218
+ case CMD.OPEN_SESSION:
219
+ this._handleOpenSession(parsed);
220
+ break;
221
+ case CMD.REQUEST_ACCESS:
222
+ this._handleRequestAccess(parsed);
223
+ break;
224
+ case CMD.COMMAND_RESPONSE:
225
+ this._handleCommandResponse(parsed);
226
+ break;
227
+ case CMD.COMMAND_ERROR:
228
+ this._handleCommandError(parsed);
229
+ break;
230
+ case CMD.POLL:
231
+ this._handlePoll(parsed);
232
+ break;
233
+ case CMD.LIFESTYLE_ZONE_STATUS:
234
+ this._handleLifestyleZoneStatus(parsed);
235
+ break;
236
+ case CMD.TIME_DATE_BROADCAST:
237
+ this._handleTimeDateBroadcast(parsed);
238
+ break;
239
+ default:
240
+ if (this.handshakeState === 'ESTABLISHED') {
241
+ this._log(`[Session] Unhandled command ${CMD_NAMES[cmd] || '0x' + cmd?.toString(16)}`);
242
+ this._sendPacket(this.session.buildSimpleAck());
243
+ }
244
+ break;
245
+ }
246
+ }
247
+
248
+ // ==================== Handshake Handlers ====================
249
+
250
+ _handleOpenSession(parsed) {
251
+ const data = parsed.commandData;
252
+ if (!data || data.length < 14) {
253
+ this._log('[Error] Invalid OPEN_SESSION data');
254
+ return;
255
+ }
256
+
257
+ const deviceType = data[0];
258
+ const deviceId = data.readUInt16BE(1);
259
+ const firmware = data.readUInt16BE(3);
260
+ const protocol = data.readUInt16BE(5);
261
+ const txBuffer = data.readUInt16BE(7);
262
+ const rxBuffer = data.readUInt16BE(9);
263
+ const encryptionType = data[13];
264
+
265
+ this._log(`[Session] OPEN_SESSION received:`);
266
+ this._log(` Device Type: ${deviceType}`);
267
+ this._log(` Encryption Type: ${encryptionType}`);
268
+ this._logMinimal('[Handshake] Starting session establishment...');
269
+
270
+ // Handle panel restart mid-handshake
271
+ if (this.handshakeState !== 'WAITING' && this.handshakeState !== 'SENT_CMD_RESPONSE_1') {
272
+ this._log(`[Session] Panel restarting handshake (was in state ${this.handshakeState}), resetting session`);
273
+ this.session = new ITv2Session(this.integrationId, this.accessCode, this._log.bind(this));
274
+ }
275
+
276
+ // Send COMMAND_RESPONSE echoing device type
277
+ this._log(`[Handshake] Sending COMMAND_RESPONSE success (echoing device type ${deviceType})`);
278
+ const response = this.session.buildCommandResponseWithAppSeq(deviceType, 0x00);
279
+ this._sendPacket(response);
280
+ this.handshakeState = 'SENT_CMD_RESPONSE_1';
281
+ }
282
+
283
+ _handleSimpleAck(parsed) {
284
+ const expectedSeq = this.session.localSequence;
285
+ this._log(`[Handshake] Got ACK ${parsed.receiverSequence}, state=${this.handshakeState}`);
286
+
287
+ if (this.handshakeState === 'SENT_CMD_RESPONSE_1') {
288
+ // Panel ACKed our first COMMAND_RESPONSE, send OPEN_SESSION
289
+ this._log(`[Handshake] Got ACK 1, sending our OPEN_SESSION`);
290
+ const openSessionPayload = Buffer.from([
291
+ 0x01, 0x80, 0x00, 0x00, // Device ID/type
292
+ 0x02, 0x01, // Firmware
293
+ 0x02, 0x41, // Protocol version
294
+ 0x02, 0x00, // TX buffer
295
+ 0x02, 0x00, // RX buffer
296
+ 0x00, 0x01, 0x01 // Capabilities/encryption
297
+ ]);
298
+ const packet = this.session.buildOpenSessionResponse(openSessionPayload);
299
+ this._sendPacket(packet);
300
+ this.handshakeState = 'SENT_OPEN_SESSION';
301
+ } else if (this.handshakeState === 'WAITING_TO_SEND_REQUEST_ACCESS') {
302
+ // Panel ACKed our COMMAND_RESPONSE to their REQUEST_ACCESS
303
+ this._log(`[Handshake] Got ACK, now sending our REQUEST_ACCESS`);
304
+
305
+ // Enable encryption with SEND key
306
+ this.session.sendAesActive = true;
307
+ this.session.sendAesKey = this.session.derivedSendKey;
308
+ this._log(`[Handshake] Encrypting REQUEST_ACCESS with SEND key`);
309
+
310
+ // Build REQUEST_ACCESS based on detected encryption type
311
+ let reqAccessPacket;
312
+ if (this.detectedEncryptionType === 2) {
313
+ this._log(`[Handshake] Using Type 2 REQUEST_ACCESS`);
314
+ reqAccessPacket = this.session.buildRequestAccessType2();
315
+ } else {
316
+ this._log(`[Handshake] Using Type 1 REQUEST_ACCESS`);
317
+ reqAccessPacket = this.session.buildRequestAccessType1();
318
+ }
319
+
320
+ this._sendPacket(reqAccessPacket);
321
+
322
+ // Enable receive encryption
323
+ this.session.receiveAesActive = true;
324
+ this.session.receiveAesKey = this.session.pendingReceiveKey;
325
+ this._log(`[Handshake] Enabled receive encryption`);
326
+
327
+ this.handshakeState = 'SENT_REQUEST_ACCESS';
328
+ } else if (this.handshakeState === 'SENT_REQUEST_ACCESS') {
329
+ // Panel ACKed our REQUEST_ACCESS - session established!
330
+ this.handshakeState = 'ESTABLISHED';
331
+ this._logMinimal(`✅ Session established (Type ${this.detectedEncryptionType} encryption)`);
332
+ this._log(`[Handshake] *** SESSION ESTABLISHED ***`);
333
+ this._log(`[Handshake] Encryption Type: ${this.detectedEncryptionType}`);
334
+ this._log(`[Handshake] SEND key: ${this.session.derivedSendKey?.toString('hex')}`);
335
+ this._log(`[Handshake] RECV key: ${this.session.pendingReceiveKey?.toString('hex')}`);
336
+
337
+ this.emit('session:established', {
338
+ encryptionType: this.detectedEncryptionType,
339
+ sendKey: this.session.derivedSendKey?.toString('hex'),
340
+ recvKey: this.session.pendingReceiveKey?.toString('hex')
341
+ });
342
+ }
343
+ }
344
+
345
+ _handleRequestAccess(parsed) {
346
+ const data = parsed.commandData;
347
+ if (!data || data.length < 17) {
348
+ this._log('[Error] Invalid REQUEST_ACCESS data');
349
+ return;
350
+ }
351
+
352
+ const initType = data[0];
353
+ const initializer = data.slice(1);
354
+
355
+ this._log(`[Handshake] Got panel's REQUEST_ACCESS`);
356
+ this._log(`[Handshake] Processing panel's REQUEST_ACCESS (panel goes first)`);
357
+
358
+ // Determine encryption type based on initializer length
359
+ if (initializer.length === 48) {
360
+ // Type 1 encryption
361
+ this.detectedEncryptionType = 1;
362
+ this._log(`[Handshake] Encryption Type 1 (48-byte initializer)`);
363
+
364
+ const sendKey = parseType1Initializer(this.accessCode, initializer, this.integrationId, this.logLevel === 'verbose');
365
+ this._logMinimal('[Handshake] Type 1 encryption negotiated');
366
+ this._log(`[Handshake] Type 1 SEND key derived`);
367
+ this.session.derivedSendKey = sendKey;
368
+
369
+ } else if (initializer.length === 16) {
370
+ // Type 2 encryption
371
+ this.detectedEncryptionType = 2;
372
+ this._log(`[Handshake] Encryption Type 2 (16-byte initializer)`);
373
+
374
+ // Type 2: Symmetric transform with 32-hex access code
375
+ let accessCode32 = this.accessCode;
376
+ if (accessCode32.length === 8) {
377
+ accessCode32 = accessCode32 + accessCode32 + accessCode32 + accessCode32;
378
+ }
379
+
380
+ const sendKey = type2InitializerTransform(accessCode32, initializer);
381
+ this._logMinimal('[Handshake] Type 2 encryption negotiated');
382
+ this._log(`[Handshake] Type 2 SEND key derived`);
383
+ this.session.derivedSendKey = sendKey;
384
+
385
+ } else {
386
+ this._log(`[Error] Unknown initializer length: ${initializer.length}`);
387
+ return;
388
+ }
389
+
390
+ // Send plaintext COMMAND_RESPONSE
391
+ this._log(`[Handshake] Sending PLAINTEXT COMMAND_RESPONSE to panel's REQUEST_ACCESS`);
392
+ const appSeq = parsed.appSequence;
393
+ const response = this.session.buildCommandResponseWithAppSeq(appSeq, 0x00);
394
+ this._sendPacket(response);
395
+
396
+ this.handshakeState = 'WAITING_TO_SEND_REQUEST_ACCESS';
397
+ }
398
+
399
+ _handleCommandResponse(parsed) {
400
+ const responseCode = parsed.commandData?.[0] || 0;
401
+ const appSeqAsEcho = parsed.appSequence;
402
+
403
+ this._log(`[Handshake] Got COMMAND_RESPONSE (echoed app_seq: ${appSeqAsEcho}, response_code: ${responseCode}), sending ACK`);
404
+
405
+ const ack = this.session.buildSimpleAck();
406
+ this._sendPacket(ack);
407
+
408
+ if (this.handshakeState === 'SENT_OPEN_SESSION') {
409
+ // Panel accepted our OPEN_SESSION
410
+ this.handshakeState = 'WAITING_REQUEST_ACCESS';
411
+ }
412
+ }
413
+
414
+ _handleCommandError(parsed) {
415
+ const errorData = parsed.commandData;
416
+ const error = parseCommandError(errorData);
417
+
418
+ this._log(`[Error] COMMAND_ERROR: ${error.message} (${error.rawData})`);
419
+ this.emit('command:error', error);
420
+
421
+ // Send ACK
422
+ this._sendPacket(this.session.buildSimpleAck());
423
+ }
424
+
425
+ _handlePoll(parsed) {
426
+ this._log(`[Session] Got POLL, sending ACK`);
427
+ this._sendPacket(this.session.buildSimpleAck());
428
+ }
429
+
430
+ // ==================== Notification Handlers ====================
431
+
432
+ _handleLifestyleZoneStatus(parsed) {
433
+ const data = parsed.commandData;
434
+
435
+ if (data && data.length >= 2) {
436
+ const zoneNum = data[0];
437
+ const statusValue = data[1];
438
+
439
+ // Update internal state and get full parsed status
440
+ const fullStatus = this.eventHandler.handleZoneStatus(zoneNum, statusValue);
441
+
442
+ // Emit events with full status object
443
+ this.emit('zone:status', zoneNum, fullStatus);
444
+
445
+ if (fullStatus.open) {
446
+ this.emit('zone:open', zoneNum);
447
+ this._log(`[Zone ${zoneNum}] OPEN (status: ${statusValue})`);
448
+ } else {
449
+ this.emit('zone:closed', zoneNum);
450
+ this._log(`[Zone ${zoneNum}] CLOSED (status: ${statusValue})`);
451
+ }
452
+ }
453
+
454
+ this._sendPacket(this.session.buildSimpleAck());
455
+ }
456
+
457
+ _handleTimeDateBroadcast(parsed) {
458
+ const data = parsed.commandData;
459
+ if (data && data.length >= 5) {
460
+ this._log(`[Time/Date] Received broadcast`);
461
+ this.emit('notification:timeDate', data);
462
+ }
463
+ this._sendPacket(this.session.buildSimpleAck());
464
+ }
465
+
466
+ // ==================== Utility Methods ====================
467
+
468
+ _handleError(error) {
469
+ this._log(`[Error] ${error.message}`);
470
+ this.emit('error', error);
471
+ }
472
+
473
+ _log(message) {
474
+ if (this.logLevel === 'verbose') {
475
+ const timestamp = new Date().toISOString();
476
+ console.log(`[${timestamp}] ${message}`);
477
+ }
478
+ }
479
+
480
+ _logMinimal(message) {
481
+ if (this.logLevel === 'minimal' || this.logLevel === 'verbose') {
482
+ console.log(message);
483
+ }
484
+ }
485
+
486
+ _hexDump(buffer) {
487
+ if (this.logLevel !== 'verbose') return;
488
+
489
+ for (let i = 0; i < buffer.length; i += 16) {
490
+ const chunk = buffer.slice(i, i + 16);
491
+ const hex = Array.from(chunk)
492
+ .map((b) => b.toString(16).padStart(2, '0').toUpperCase())
493
+ .join(' ');
494
+ const ascii = Array.from(chunk)
495
+ .map((b) => (b >= 32 && b <= 126 ? String.fromCharCode(b) : '.'))
496
+ .join('');
497
+
498
+ const offset = i.toString(16).padStart(4, '0').toUpperCase();
499
+ console.log(`${offset} ${hex.padEnd(48)} |${ascii}|`);
500
+ }
501
+ }
502
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * ITV2 Protocol Constants
3
+ * Based on reverse engineering of DSC Neo DLLs and TSP Interactive SDK documentation
4
+ */
5
+
6
+ // Connection settings (per SDK documentation)
7
+ export const DEFAULT_HOST = '192.168.0.144'; // TL280R IP (not used - it connects to us)
8
+ export const DEFAULT_UDP_PORT = 3073; // Integration Polling Port (section [430] = 0x0C01)
9
+ export const DEFAULT_TCP_PORT = 3072; // Integration Notification Port (section [429] = 0x0C00)
10
+ export const DEFAULT_PORT = DEFAULT_TCP_PORT; // Legacy alias
11
+
12
+ // Channel functions (from SDK)
13
+ export const ChannelFunction = {
14
+ NO_FUNCTION: 0,
15
+ POLL_ONLY: 1,
16
+ POLL_AND_NOTIFY: 2,
17
+ NOTIFICATIONS: 3,
18
+ };
19
+
20
+ // Command codes (16-bit: high byte = category, low byte = command)
21
+ export const Commands = {
22
+ // Response codes (0x00xx, 0x05xx)
23
+ SIMPLE_ACK: 0x0000,
24
+ COMMAND_ERROR: 0x0501,
25
+ COMMAND_RESPONSE: 0x0502,
26
+
27
+ // Session management (0x06xx)
28
+ POLL: 0x0600,
29
+ OPEN_SESSION: 0x060A,
30
+ END_SESSION: 0x060B,
31
+ BUFFER_SIZES: 0x060C,
32
+ REQUEST_ACCESS: 0x060E,
33
+ SYSTEM_CAPABILITIES: 0x0613,
34
+
35
+ // Zone/Lifestyle events (0x02xx)
36
+ LIFESTYLE_ZONE_STATUS: 0x0210,
37
+ EXIT_DELAY: 0x0230,
38
+ ENTRY_DELAY: 0x0231,
39
+ ARMING_DISARMING: 0x0232,
40
+ ARMING_PREALERT: 0x0233,
41
+ PARTITION_QUICK_EXIT: 0x0238,
42
+ PARTITION_READY_STATUS: 0x0239,
43
+ PARTITION_AUDIBLE_BELL: 0x023B,
44
+ PARTITION_ALARM_MEMORY: 0x023C,
45
+ MISC_PREALERT: 0x023D,
46
+ PARTITION_TROUBLE_STATUS: 0x023F,
47
+ PARTITION_BYPASS_STATUS: 0x0240,
48
+
49
+ // Configuration (0x07xx)
50
+ ZONE_ASSIGNMENT_CONFIG: 0x0770,
51
+ CONFIGURATION: 0x0771,
52
+ PARTITION_ASSIGNMENT_CONFIG: 0x0772,
53
+
54
+ // Status queries (0x08xx)
55
+ GLOBAL_STATUS: 0x0810,
56
+ ZONE_STATUS: 0x0811,
57
+ PARTITION_STATUS: 0x0812,
58
+ ZONE_BYPASS_STATUS: 0x0813,
59
+ SINGLE_ZONE_BYPASS_STATUS: 0x0820,
60
+ SYSTEM_TROUBLE_STATUS: 0x0822,
61
+ TROUBLE_DETAIL: 0x0823,
62
+ ZONE_ALARM_STATUS: 0x0840,
63
+ MISC_ALARM_STATUS: 0x0841,
64
+
65
+ // LCD/Keypad (0x0Fxx)
66
+ LCD_UPDATE: 0x0F02,
67
+ LCD_CURSOR: 0x0F03,
68
+ LED_STATUS: 0x0F04,
69
+
70
+ // Access codes (0x47xx)
71
+ ACCESS_CODES_RESPONSE: 0x4736,
72
+ ACCESS_CODES_PARTITION_RESPONSE: 0x4738,
73
+ };
74
+
75
+ // Command names for logging
76
+ export const CommandNames = Object.fromEntries(
77
+ Object.entries(Commands).map(([name, code]) => [code, name])
78
+ );
79
+
80
+ // Get command name from code
81
+ export function getCommandName(code) {
82
+ return CommandNames[code] || `UNKNOWN_${code.toString(16).toUpperCase().padStart(4, '0')}`;
83
+ }
84
+
85
+ // CRC-16 common polynomials to try
86
+ export const CRC16_POLYNOMIALS = {
87
+ CCITT: 0x1021,
88
+ IBM: 0x8005,
89
+ T10_DIF: 0x8BB7,
90
+ DNP: 0x3D65,
91
+ MODBUS: 0x8005,
92
+ };
93
+
94
+ // Common CRC-16 seeds
95
+ export const CRC16_SEEDS = {
96
+ ZERO: 0x0000,
97
+ FFFF: 0xFFFF,
98
+ ONE: 0x0001,
99
+ };
100
+
101
+ // Possible frame markers (to be determined from captures)
102
+ export const FRAME_MARKERS = {
103
+ POSSIBLE_START: [0x7E, 0xFE, 0xFF, 0xAA, 0x55, 0x02],
104
+ POSSIBLE_END: [0x7E, 0x03, 0x0D, 0x0A],
105
+ ESCAPE: 0x7D,
106
+ };