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,185 @@
1
+ /**
2
+ * DSC ITV2 Protocol Response Parsers
3
+ * Based on decompiled SDK structures
4
+ */
5
+
6
+ /**
7
+ * Parse Zone Status response (0x0811)
8
+ * Each zone status is a single byte with bit fields
9
+ */
10
+ export function parseZoneStatus(statusByte) {
11
+ return {
12
+ zoneNumber: null, // Set by caller
13
+ open: (statusByte & 0x01) !== 0, // Bit 0: Open/Closed
14
+ tamper: (statusByte & 0x02) !== 0, // Bit 1: Tamper
15
+ fault: (statusByte & 0x04) !== 0, // Bit 2: Fault
16
+ lowBattery: (statusByte & 0x08) !== 0, // Bit 3: Low Battery
17
+ delinquency: (statusByte & 0x10) !== 0, // Bit 4: Communication issue
18
+ alarm: (statusByte & 0x20) !== 0, // Bit 5: Alarm
19
+ alarmInMemory: (statusByte & 0x40) !== 0, // Bit 6: Alarm memory
20
+ bypassed: (statusByte & 0x80) !== 0, // Bit 7: Bypassed
21
+ rawByte: statusByte
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Format zone status as human-readable string
27
+ */
28
+ export function formatZoneStatus(zone) {
29
+ const state = zone.open ? 'OPEN' : 'CLOSED';
30
+ const flags = [];
31
+
32
+ if (zone.alarm) flags.push('ALARM');
33
+ if (zone.tamper) flags.push('TAMPER');
34
+ if (zone.fault) flags.push('FAULT');
35
+ if (zone.bypassed) flags.push('BYPASSED');
36
+ if (zone.lowBattery) flags.push('LOW-BATT');
37
+ if (zone.delinquency) flags.push('COMM-ERR');
38
+ if (zone.alarmInMemory) flags.push('ALARM-MEM');
39
+
40
+ const flagStr = flags.length > 0 ? ` [${flags.join(', ')}]` : '';
41
+ return `Zone ${zone.zoneNumber}: ${state}${flagStr}`;
42
+ }
43
+
44
+ /**
45
+ * Parse Partition Status response (0x0812)
46
+ * Status is encoded across multiple bytes using bit fields
47
+ */
48
+ export function parsePartitionStatus(statusBytes) {
49
+ if (!statusBytes || statusBytes.length < 3) {
50
+ return null;
51
+ }
52
+
53
+ // Helper: Check bit at specific index
54
+ const checkBit = (bitIndex) => {
55
+ const byteOffset = Math.floor(bitIndex / 8);
56
+ const bitMask = 1 << (bitIndex % 8);
57
+ return byteOffset < statusBytes.length && (statusBytes[byteOffset] & bitMask) !== 0;
58
+ };
59
+
60
+ // Armed state is encoded in bits 0-3 (lower nibble of first byte)
61
+ const armedValue = statusBytes[0] & 0x0F;
62
+ const armedStates = {
63
+ 1: 'Disarmed',
64
+ 2: 'Armed Away',
65
+ 3: 'Armed Stay',
66
+ 4: 'Armed Stay (No Entry)',
67
+ 5: 'Armed Stay (Instant)',
68
+ 6: 'Armed Away (Instant)'
69
+ };
70
+
71
+ return {
72
+ partitionNumber: null, // Set by caller
73
+ armedState: armedStates[armedValue] || `Unknown (${armedValue})`,
74
+ armedValue: armedValue,
75
+ ready: checkBit(0) || checkBit(1) || checkBit(2), // Bits 0-2: Ready state
76
+ alarm: checkBit(8), // Bit 8: Alarm
77
+ trouble: checkBit(9), // Bit 9: Trouble
78
+ bypass: checkBit(10), // Bit 10: Bypass active
79
+ busy: checkBit(11), // Bit 11: Busy (processing)
80
+ alarmMemory: checkBit(12), // Bit 12: Alarm in memory
81
+ audibleBell: checkBit(14), // Bit 14: Siren/bell
82
+ buzzer: checkBit(15), // Bit 15: Buzzer
83
+ blank: checkBit(19), // Bit 19: Display blank
84
+ rawBytes: Buffer.from(statusBytes).toString('hex')
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Format partition status as human-readable string
90
+ */
91
+ export function formatPartitionStatus(partition) {
92
+ const flags = [];
93
+
94
+ if (partition.alarm) flags.push('ALARM');
95
+ if (partition.trouble) flags.push('TROUBLE');
96
+ if (partition.bypass) flags.push('BYPASS');
97
+ if (partition.busy) flags.push('BUSY');
98
+ if (partition.audibleBell) flags.push('SIREN');
99
+ if (partition.buzzer) flags.push('BUZZER');
100
+ if (partition.alarmMemory) flags.push('ALARM-MEM');
101
+
102
+ const readyStr = partition.ready ? 'Ready' : 'Not Ready';
103
+ const flagStr = flags.length > 0 ? ` [${flags.join(', ')}]` : '';
104
+
105
+ return `Partition ${partition.partitionNumber}: ${partition.armedState} - ${readyStr}${flagStr}`;
106
+ }
107
+
108
+ /**
109
+ * Parse COMMAND_RESPONSE (0x0502)
110
+ * Response code indicates success/failure
111
+ */
112
+ export function parseCommandResponse(responseCode) {
113
+ const codes = {
114
+ 0x00: 'Success',
115
+ 0x01: 'Informational/Success',
116
+ 0x02: 'Invalid Command',
117
+ 0x03: 'Invalid Parameter',
118
+ 0x04: 'Access Denied',
119
+ 0x05: 'Busy',
120
+ 0x06: 'Not Ready',
121
+ 0x07: 'Timeout',
122
+ 0xFF: 'General Error'
123
+ };
124
+
125
+ return {
126
+ code: responseCode,
127
+ message: codes[responseCode] || `Unknown (0x${responseCode.toString(16).padStart(2, '0')})`,
128
+ success: responseCode === 0x00 || responseCode === 0x01
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Parse COMMAND_ERROR (0x0501)
134
+ */
135
+ export function parseCommandError(errorData) {
136
+ if (!errorData || errorData.length < 2) {
137
+ return { message: 'Unknown error', rawData: errorData?.toString('hex') || 'none' };
138
+ }
139
+
140
+ const errorCode = (errorData[0] << 8) | errorData[1];
141
+
142
+ const errorCodes = {
143
+ 0x0002: 'Authentication required / Access denied',
144
+ 0x1002: 'Global status query not supported or auth required',
145
+ 0x1102: 'Zone status query not supported or auth required',
146
+ 0x1202: 'Partition status query not supported or auth required',
147
+ 0x1302: 'Invalid capabilities query',
148
+ 0x1402: 'Invalid panel status query',
149
+ 0x0C01: 'Invalid buffer sizes',
150
+ 0x1100: 'Invalid zone number',
151
+ 0x1200: 'Invalid partition number'
152
+ };
153
+
154
+ return {
155
+ code: errorCode,
156
+ message: errorCodes[errorCode] || `Error code 0x${errorCode.toString(16).padStart(4, '0')}`,
157
+ rawData: errorData.toString('hex')
158
+ };
159
+ }
160
+
161
+ /**
162
+ * Extract variable-length value from buffer
163
+ */
164
+ export function parseVarLengthValue(buffer, offset = 0) {
165
+ if (offset >= buffer.length) return null;
166
+
167
+ const firstByte = buffer[offset];
168
+
169
+ // Determine size based on first byte
170
+ if (firstByte < 0xFD) {
171
+ // 1-byte value
172
+ return { value: firstByte, bytesRead: 1 };
173
+ } else if (firstByte === 0xFD && offset + 2 < buffer.length) {
174
+ // 2-byte value
175
+ const value = (buffer[offset + 1] << 8) | buffer[offset + 2];
176
+ return { value, bytesRead: 3 };
177
+ } else if (firstByte === 0xFE && offset + 4 < buffer.length) {
178
+ // 4-byte value
179
+ const value = (buffer[offset + 1] << 24) | (buffer[offset + 2] << 16) |
180
+ (buffer[offset + 3] << 8) | buffer[offset + 4];
181
+ return { value, bytesRead: 5 };
182
+ }
183
+
184
+ return null;
185
+ }
package/src/utils.js ADDED
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Utility functions for ITV2 protocol
3
+ */
4
+
5
+ import { writeFileSync, appendFileSync, existsSync, mkdirSync } from 'fs';
6
+ import { dirname, join } from 'path';
7
+ import { fileURLToPath } from 'url';
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const LOGS_DIR = join(__dirname, '..', 'logs');
11
+
12
+ // Ensure logs directory exists
13
+ if (!existsSync(LOGS_DIR)) {
14
+ mkdirSync(LOGS_DIR, { recursive: true });
15
+ }
16
+
17
+ /**
18
+ * Convert buffer to hex string with optional spacing
19
+ */
20
+ export function toHex(buffer, separator = ' ') {
21
+ if (!buffer || buffer.length === 0) return '';
22
+ return Array.from(buffer)
23
+ .map(b => b.toString(16).padStart(2, '0').toUpperCase())
24
+ .join(separator);
25
+ }
26
+
27
+ /**
28
+ * Convert hex string to buffer
29
+ */
30
+ export function fromHex(hexString) {
31
+ const clean = hexString.replace(/\s+/g, '');
32
+ const bytes = [];
33
+ for (let i = 0; i < clean.length; i += 2) {
34
+ bytes.push(parseInt(clean.substr(i, 2), 16));
35
+ }
36
+ return Buffer.from(bytes);
37
+ }
38
+
39
+ /**
40
+ * Format buffer for display with ASCII representation
41
+ */
42
+ export function hexDump(buffer, bytesPerLine = 16) {
43
+ const lines = [];
44
+ for (let i = 0; i < buffer.length; i += bytesPerLine) {
45
+ const slice = buffer.slice(i, Math.min(i + bytesPerLine, buffer.length));
46
+ const hex = toHex(slice, ' ').padEnd(bytesPerLine * 3 - 1);
47
+ const ascii = Array.from(slice)
48
+ .map(b => (b >= 32 && b <= 126) ? String.fromCharCode(b) : '.')
49
+ .join('');
50
+ lines.push(`${i.toString(16).padStart(4, '0')} ${hex} |${ascii}|`);
51
+ }
52
+ return lines.join('\n');
53
+ }
54
+
55
+ /**
56
+ * Get timestamp for logging
57
+ */
58
+ export function timestamp() {
59
+ return new Date().toISOString();
60
+ }
61
+
62
+ /**
63
+ * Log to console and file
64
+ */
65
+ export function log(message, data = null) {
66
+ const ts = timestamp();
67
+ const logLine = `[${ts}] ${message}`;
68
+ console.log(logLine);
69
+
70
+ if (data) {
71
+ if (Buffer.isBuffer(data)) {
72
+ console.log(hexDump(data));
73
+ } else {
74
+ console.log(data);
75
+ }
76
+ }
77
+
78
+ // Append to log file
79
+ const logFile = join(LOGS_DIR, `capture_${new Date().toISOString().split('T')[0]}.log`);
80
+ let fileContent = logLine + '\n';
81
+ if (data) {
82
+ if (Buffer.isBuffer(data)) {
83
+ fileContent += hexDump(data) + '\n';
84
+ } else if (typeof data === 'object') {
85
+ fileContent += JSON.stringify(data, null, 2) + '\n';
86
+ } else {
87
+ fileContent += data + '\n';
88
+ }
89
+ }
90
+ appendFileSync(logFile, fileContent);
91
+ }
92
+
93
+ /**
94
+ * Log raw packet data to binary file for later analysis
95
+ */
96
+ export function logRawPacket(direction, data) {
97
+ const ts = Date.now();
98
+ const filename = join(LOGS_DIR, `packets_${new Date().toISOString().split('T')[0]}.bin`);
99
+
100
+ // Format: [timestamp 8 bytes][direction 1 byte][length 2 bytes][data]
101
+ const header = Buffer.alloc(11);
102
+ header.writeBigInt64BE(BigInt(ts), 0);
103
+ header.writeUInt8(direction === 'TX' ? 0x01 : 0x02, 8);
104
+ header.writeUInt16BE(data.length, 9);
105
+
106
+ appendFileSync(filename, Buffer.concat([header, data]));
107
+ }
108
+
109
+ /**
110
+ * CRC-16 calculation with configurable polynomial and seed
111
+ */
112
+ export function crc16(data, polynomial = 0x1021, seed = 0xFFFF, reflectInput = false, reflectOutput = false, xorOut = 0x0000) {
113
+ let crc = seed;
114
+
115
+ for (let i = 0; i < data.length; i++) {
116
+ let byte = data[i];
117
+ if (reflectInput) {
118
+ byte = reflect8(byte);
119
+ }
120
+
121
+ crc ^= (byte << 8);
122
+
123
+ for (let j = 0; j < 8; j++) {
124
+ if (crc & 0x8000) {
125
+ crc = ((crc << 1) ^ polynomial) & 0xFFFF;
126
+ } else {
127
+ crc = (crc << 1) & 0xFFFF;
128
+ }
129
+ }
130
+ }
131
+
132
+ if (reflectOutput) {
133
+ crc = reflect16(crc);
134
+ }
135
+
136
+ return (crc ^ xorOut) & 0xFFFF;
137
+ }
138
+
139
+ /**
140
+ * Reflect bits in a byte
141
+ */
142
+ function reflect8(value) {
143
+ let result = 0;
144
+ for (let i = 0; i < 8; i++) {
145
+ if (value & (1 << i)) {
146
+ result |= (1 << (7 - i));
147
+ }
148
+ }
149
+ return result;
150
+ }
151
+
152
+ /**
153
+ * Reflect bits in a 16-bit value
154
+ */
155
+ function reflect16(value) {
156
+ let result = 0;
157
+ for (let i = 0; i < 16; i++) {
158
+ if (value & (1 << i)) {
159
+ result |= (1 << (15 - i));
160
+ }
161
+ }
162
+ return result;
163
+ }
164
+
165
+ /**
166
+ * Try multiple CRC algorithms to find matching one
167
+ */
168
+ export function tryCrcAlgorithms(data, expectedCrc) {
169
+ const algorithms = [
170
+ { name: 'CRC-16-CCITT', poly: 0x1021, seed: 0xFFFF, refIn: false, refOut: false, xorOut: 0x0000 },
171
+ { name: 'CRC-16-CCITT-FALSE', poly: 0x1021, seed: 0xFFFF, refIn: false, refOut: false, xorOut: 0x0000 },
172
+ { name: 'CRC-16-XMODEM', poly: 0x1021, seed: 0x0000, refIn: false, refOut: false, xorOut: 0x0000 },
173
+ { name: 'CRC-16-IBM', poly: 0x8005, seed: 0x0000, refIn: true, refOut: true, xorOut: 0x0000 },
174
+ { name: 'CRC-16-MODBUS', poly: 0x8005, seed: 0xFFFF, refIn: true, refOut: true, xorOut: 0x0000 },
175
+ { name: 'CRC-16-USB', poly: 0x8005, seed: 0xFFFF, refIn: true, refOut: true, xorOut: 0xFFFF },
176
+ { name: 'CRC-16-MAXIM', poly: 0x8005, seed: 0x0000, refIn: true, refOut: true, xorOut: 0xFFFF },
177
+ { name: 'CRC-16-DNP', poly: 0x3D65, seed: 0x0000, refIn: true, refOut: true, xorOut: 0xFFFF },
178
+ ];
179
+
180
+ const matches = [];
181
+ for (const alg of algorithms) {
182
+ const calc = crc16(data, alg.poly, alg.seed, alg.refIn, alg.refOut, alg.xorOut);
183
+ if (calc === expectedCrc) {
184
+ matches.push({ ...alg, calculated: calc });
185
+ }
186
+ }
187
+ return matches;
188
+ }
189
+
190
+ /**
191
+ * Byte unstuffing (HDLC-like)
192
+ * Escape byte 0x7D followed by XOR'd byte
193
+ */
194
+ export function byteUnstuff(data, escapeChar = 0x7D, xorValue = 0x20) {
195
+ const result = [];
196
+ let i = 0;
197
+ while (i < data.length) {
198
+ if (data[i] === escapeChar && i + 1 < data.length) {
199
+ result.push(data[i + 1] ^ xorValue);
200
+ i += 2;
201
+ } else {
202
+ result.push(data[i]);
203
+ i++;
204
+ }
205
+ }
206
+ return Buffer.from(result);
207
+ }
208
+
209
+ /**
210
+ * Byte stuffing (HDLC-like)
211
+ */
212
+ export function byteStuff(data, escapeChar = 0x7D, xorValue = 0x20, specialChars = [0x7E, 0x7D]) {
213
+ const result = [];
214
+ for (const byte of data) {
215
+ if (specialChars.includes(byte)) {
216
+ result.push(escapeChar);
217
+ result.push(byte ^ xorValue);
218
+ } else {
219
+ result.push(byte);
220
+ }
221
+ }
222
+ return Buffer.from(result);
223
+ }
224
+
225
+ /**
226
+ * Parse a 16-bit command code
227
+ */
228
+ export function parseCommand(high, low) {
229
+ return (high << 8) | low;
230
+ }
231
+
232
+ /**
233
+ * Create a 16-bit command code
234
+ */
235
+ export function makeCommand(code) {
236
+ return {
237
+ high: (code >> 8) & 0xFF,
238
+ low: code & 0xFF,
239
+ };
240
+ }