dsc-itv2-client 1.0.21 → 1.0.23
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 +93 -3
- package/src/itv2-session.js +88 -34
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.23",
|
|
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
|
@@ -379,9 +379,23 @@ export class ITV2Client extends EventEmitter {
|
|
|
379
379
|
case CMD.TIME_DATE_BROADCAST:
|
|
380
380
|
this._handleTimeDateBroadcast(parsed);
|
|
381
381
|
break;
|
|
382
|
+
case CMD.ZONE_STATUS:
|
|
383
|
+
this._handleZoneStatusResponse(parsed);
|
|
384
|
+
break;
|
|
385
|
+
case CMD.PARTITION_STATUS:
|
|
386
|
+
this._handlePartitionStatusResponse(parsed);
|
|
387
|
+
break;
|
|
388
|
+
case CMD.GLOBAL_STATUS:
|
|
389
|
+
this._handleGlobalStatusResponse(parsed);
|
|
390
|
+
break;
|
|
382
391
|
default:
|
|
383
392
|
if (this.handshakeState === 'ESTABLISHED') {
|
|
384
|
-
|
|
393
|
+
const cmdHex = cmd ? '0x' + cmd.toString(16).padStart(4, '0') : 'null';
|
|
394
|
+
const cmdName = CMD_NAMES[cmd] || 'UNKNOWN';
|
|
395
|
+
this._logMinimal(`[Session] Received command ${cmdName} (${cmdHex})`);
|
|
396
|
+
if (parsed.commandData && parsed.commandData.length > 0) {
|
|
397
|
+
this._logMinimal(`[Session] Data (${parsed.commandData.length} bytes): ${parsed.commandData.toString('hex')}`);
|
|
398
|
+
}
|
|
385
399
|
this._sendPacket(this.session.buildSimpleAck());
|
|
386
400
|
}
|
|
387
401
|
break;
|
|
@@ -569,10 +583,17 @@ export class ITV2Client extends EventEmitter {
|
|
|
569
583
|
}
|
|
570
584
|
|
|
571
585
|
_handleCommandResponse(parsed) {
|
|
572
|
-
const
|
|
586
|
+
const data = parsed.commandData;
|
|
587
|
+
const responseCode = data?.[0] || 0;
|
|
573
588
|
const appSeqAsEcho = parsed.appSequence;
|
|
574
589
|
|
|
575
|
-
this._log(`[
|
|
590
|
+
this._log(`[Session] Got COMMAND_RESPONSE (app_seq: ${appSeqAsEcho}, response_code: ${responseCode})`);
|
|
591
|
+
|
|
592
|
+
// Log full data in established state for debugging query responses
|
|
593
|
+
if (this.handshakeState === 'ESTABLISHED' && data && data.length > 1) {
|
|
594
|
+
this._logMinimal(`[Command Response] Response code: ${responseCode}, data (${data.length} bytes): ${data.toString('hex')}`);
|
|
595
|
+
this.emit('command:response', { responseCode, appSequence: appSeqAsEcho, data });
|
|
596
|
+
}
|
|
576
597
|
|
|
577
598
|
const ack = this.session.buildSimpleAck();
|
|
578
599
|
this._sendPacket(ack);
|
|
@@ -655,6 +676,75 @@ export class ITV2Client extends EventEmitter {
|
|
|
655
676
|
this._sendPacket(this.session.buildSimpleAck());
|
|
656
677
|
}
|
|
657
678
|
|
|
679
|
+
// ==================== Status Query Response Handlers ====================
|
|
680
|
+
|
|
681
|
+
_handleZoneStatusResponse(parsed) {
|
|
682
|
+
const data = parsed.commandData;
|
|
683
|
+
this._logMinimal(`[Zone Status] Received zone status response`);
|
|
684
|
+
this._log(`[Zone Status] Raw data (${data?.length || 0} bytes): ${data?.toString('hex') || 'none'}`);
|
|
685
|
+
|
|
686
|
+
if (data && data.length >= 3) {
|
|
687
|
+
// Response format: [ZoneNumber 2B BE][StatusByte 1B][...more zones...]
|
|
688
|
+
const zoneNum = data.readUInt16BE(0);
|
|
689
|
+
const statusByte = data[2];
|
|
690
|
+
|
|
691
|
+
// Update internal state
|
|
692
|
+
const fullStatus = this.eventHandler.handleZoneStatus(zoneNum, statusByte);
|
|
693
|
+
|
|
694
|
+
this._logMinimal(`[Zone Status] Zone ${zoneNum}: ${fullStatus.open ? 'OPEN' : 'CLOSED'} (0x${statusByte.toString(16)})`);
|
|
695
|
+
|
|
696
|
+
// Emit event with full status
|
|
697
|
+
this.emit('zone:statusResponse', zoneNum, fullStatus);
|
|
698
|
+
this.emit('zone:status', zoneNum, fullStatus);
|
|
699
|
+
} else {
|
|
700
|
+
this._log(`[Zone Status] Empty or invalid response`);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
this._sendPacket(this.session.buildSimpleAck());
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
_handlePartitionStatusResponse(parsed) {
|
|
707
|
+
const data = parsed.commandData;
|
|
708
|
+
this._logMinimal(`[Partition Status] Received partition status response`);
|
|
709
|
+
this._log(`[Partition Status] Raw data (${data?.length || 0} bytes): ${data?.toString('hex') || 'none'}`);
|
|
710
|
+
|
|
711
|
+
if (data && data.length >= 3) {
|
|
712
|
+
// Response format: [PartitionNumber 2B BE][StatusBytes...]
|
|
713
|
+
const partitionNum = data.readUInt16BE(0);
|
|
714
|
+
const statusBytes = data.slice(2);
|
|
715
|
+
|
|
716
|
+
this._logMinimal(`[Partition Status] Partition ${partitionNum}: status bytes ${statusBytes.toString('hex')}`);
|
|
717
|
+
|
|
718
|
+
// Emit raw event - let consumers parse the detailed status
|
|
719
|
+
this.emit('partition:statusResponse', partitionNum, statusBytes);
|
|
720
|
+
} else {
|
|
721
|
+
this._log(`[Partition Status] Empty or invalid response`);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
this._sendPacket(this.session.buildSimpleAck());
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
_handleGlobalStatusResponse(parsed) {
|
|
728
|
+
const data = parsed.commandData;
|
|
729
|
+
this._logMinimal(`[Global Status] Received global status response`);
|
|
730
|
+
this._log(`[Global Status] Raw data (${data?.length || 0} bytes): ${data?.toString('hex') || 'none'}`);
|
|
731
|
+
|
|
732
|
+
if (data && data.length > 0) {
|
|
733
|
+
// Emit raw event with full data
|
|
734
|
+
this.emit('global:statusResponse', data);
|
|
735
|
+
|
|
736
|
+
// Log byte breakdown for debugging
|
|
737
|
+
this._log(`[Global Status] Byte breakdown:`);
|
|
738
|
+
for (let i = 0; i < data.length && i < 32; i++) {
|
|
739
|
+
this._log(` Byte ${i}: 0x${data[i].toString(16).padStart(2, '0')} (${data[i]})`);
|
|
740
|
+
}
|
|
741
|
+
} else {
|
|
742
|
+
this._log(`[Global Status] Empty response`);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
this._sendPacket(this.session.buildSimpleAck());
|
|
746
|
+
}
|
|
747
|
+
|
|
658
748
|
// ==================== Utility Methods ====================
|
|
659
749
|
|
|
660
750
|
_handleError(error) {
|
package/src/itv2-session.js
CHANGED
|
@@ -756,67 +756,121 @@ export class ITv2Session {
|
|
|
756
756
|
return this.buildCommand(CMD.REQUEST_ACCESS, payload);
|
|
757
757
|
}
|
|
758
758
|
|
|
759
|
+
// ==================== VarBytes Encoding ====================
|
|
760
|
+
|
|
759
761
|
/**
|
|
760
|
-
*
|
|
761
|
-
*
|
|
762
|
-
*
|
|
763
|
-
*
|
|
764
|
-
* 0x12 = Partition Status (0x0812)
|
|
765
|
-
* 0x13 = Zone Bypass Status (0x0813)
|
|
766
|
-
* 0x14 = System Trouble Status (0x0814)
|
|
767
|
-
* 0x15 = Alarm Memory Info (0x0815)
|
|
768
|
-
* 0x16 = Bus Status (0x0816)
|
|
769
|
-
* 0x17 = Trouble Detail (0x0817)
|
|
770
|
-
* 0x19 = Door Chime Status (0x0819)
|
|
771
|
-
* @param {number} partitionOrZone - Partition or zone number (0 = all)
|
|
762
|
+
* Encode a value as VarBytes (DSC protocol variable-length integer)
|
|
763
|
+
* Format: [length byte][value bytes in little-endian]
|
|
764
|
+
* @param {number} value - Value to encode
|
|
765
|
+
* @returns {Buffer} - VarBytes encoded buffer
|
|
772
766
|
*/
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
767
|
+
encodeVarBytes(value) {
|
|
768
|
+
if (value < 0x100) {
|
|
769
|
+
// 1 byte value
|
|
770
|
+
return Buffer.from([0x01, value & 0xFF]);
|
|
771
|
+
} else if (value < 0x10000) {
|
|
772
|
+
// 2 byte value (little-endian)
|
|
773
|
+
return Buffer.from([0x02, value & 0xFF, (value >> 8) & 0xFF]);
|
|
774
|
+
} else if (value < 0x1000000) {
|
|
775
|
+
// 3 byte value (little-endian)
|
|
776
|
+
return Buffer.from([0x03, value & 0xFF, (value >> 8) & 0xFF, (value >> 16) & 0xFF]);
|
|
777
|
+
} else {
|
|
778
|
+
// 4 byte value (little-endian)
|
|
779
|
+
return Buffer.from([0x04, value & 0xFF, (value >> 8) & 0xFF, (value >> 16) & 0xFF, (value >> 24) & 0xFF]);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// ==================== 0x0800 Command Request Wrapper ====================
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Build a 0x0800 Command Request wrapper
|
|
787
|
+
* This wraps inner commands for status queries - required by the DSC protocol
|
|
788
|
+
* Format: [AppSeqNum 1B][CommandToRequest 2B LE][InnerPayload...]
|
|
789
|
+
* @param {number} innerCommand - The command code to request (e.g., 0x0811)
|
|
790
|
+
* @param {Buffer} innerPayload - The payload for the inner command
|
|
791
|
+
* @returns {Buffer} - Complete packet ready to send
|
|
792
|
+
*/
|
|
793
|
+
buildCommandRequest(innerCommand, innerPayload = Buffer.alloc(0)) {
|
|
794
|
+
// Get next application sequence number
|
|
795
|
+
const appSeq = this.appSequence;
|
|
796
|
+
this.appSequence = (this.appSequence + 1) & 0xFF;
|
|
797
|
+
|
|
798
|
+
// Build 0x0800 payload: [AppSeqNum][CommandToRequest LE][InnerPayload]
|
|
799
|
+
const payload = Buffer.alloc(3 + innerPayload.length);
|
|
800
|
+
payload[0] = appSeq;
|
|
801
|
+
payload.writeUInt16LE(innerCommand, 1); // Command in little-endian
|
|
802
|
+
innerPayload.copy(payload, 3);
|
|
803
|
+
|
|
804
|
+
this.log(`[Session] Building 0x0800 Command Request: innerCmd=0x${innerCommand.toString(16)}, appSeq=${appSeq}`);
|
|
805
|
+
this.log(`[Session] Inner payload: ${innerPayload.toString('hex')}`);
|
|
806
|
+
|
|
776
807
|
return this.buildCommand(CMD.STATUS_REQUEST, payload);
|
|
777
808
|
}
|
|
778
809
|
|
|
810
|
+
// ==================== Status Query Methods (0x0800 Wrapped) ====================
|
|
811
|
+
|
|
779
812
|
/**
|
|
780
|
-
* Build global status request
|
|
813
|
+
* Build global status request (0x0810 wrapped in 0x0800)
|
|
781
814
|
*/
|
|
782
815
|
buildGlobalStatusRequest() {
|
|
783
|
-
//
|
|
784
|
-
return this.
|
|
816
|
+
// 0x0810 has no payload
|
|
817
|
+
return this.buildCommandRequest(CMD.GLOBAL_STATUS, Buffer.alloc(0));
|
|
785
818
|
}
|
|
786
819
|
|
|
787
820
|
/**
|
|
788
|
-
* Build zone status request
|
|
789
|
-
* @param {number}
|
|
821
|
+
* Build zone status request (0x0811 wrapped in 0x0800)
|
|
822
|
+
* @param {number} startZone - Starting zone number (0 = first zone)
|
|
823
|
+
* @param {number} numZones - Number of zones to query (default 128)
|
|
790
824
|
*/
|
|
791
|
-
buildZoneStatusRequest(
|
|
792
|
-
//
|
|
793
|
-
const
|
|
794
|
-
|
|
825
|
+
buildZoneStatusRequest(startZone = 0, numZones = 128) {
|
|
826
|
+
// 0x0811 payload: [ZoneNumber VarBytes][NumberOfZones VarBytes]
|
|
827
|
+
const zoneBytes = this.encodeVarBytes(startZone);
|
|
828
|
+
const countBytes = this.encodeVarBytes(numZones);
|
|
829
|
+
const payload = Buffer.concat([zoneBytes, countBytes]);
|
|
830
|
+
return this.buildCommandRequest(CMD.ZONE_STATUS, payload);
|
|
795
831
|
}
|
|
796
832
|
|
|
797
833
|
/**
|
|
798
|
-
* Build partition status request
|
|
834
|
+
* Build partition status request (0x0812 wrapped in 0x0800)
|
|
799
835
|
* @param {number} partition - Partition number (0 = all partitions)
|
|
836
|
+
* @param {number} numPartitions - Number of partitions to query (default 8)
|
|
800
837
|
*/
|
|
801
|
-
buildPartitionStatusRequest(partition = 0) {
|
|
802
|
-
//
|
|
803
|
-
const
|
|
804
|
-
|
|
838
|
+
buildPartitionStatusRequest(partition = 0, numPartitions = 8) {
|
|
839
|
+
// 0x0812 payload: [Partition VarBytes][NumberOfPartitions VarBytes]
|
|
840
|
+
const partBytes = this.encodeVarBytes(partition);
|
|
841
|
+
const countBytes = this.encodeVarBytes(numPartitions);
|
|
842
|
+
const payload = Buffer.concat([partBytes, countBytes]);
|
|
843
|
+
return this.buildCommandRequest(CMD.PARTITION_STATUS, payload);
|
|
805
844
|
}
|
|
806
845
|
|
|
807
846
|
/**
|
|
808
|
-
* Build zone bypass status request
|
|
847
|
+
* Build zone bypass status request (0x0813 wrapped in 0x0800)
|
|
809
848
|
* @param {number} zone - Zone number (0 = all zones)
|
|
849
|
+
* @param {number} numZones - Number of zones (default 128)
|
|
810
850
|
*/
|
|
811
|
-
buildZoneBypassStatusRequest(zone = 0) {
|
|
812
|
-
|
|
851
|
+
buildZoneBypassStatusRequest(zone = 0, numZones = 128) {
|
|
852
|
+
const zoneBytes = this.encodeVarBytes(zone);
|
|
853
|
+
const countBytes = this.encodeVarBytes(numZones);
|
|
854
|
+
const payload = Buffer.concat([zoneBytes, countBytes]);
|
|
855
|
+
return this.buildCommandRequest(CMD.ZONE_BYPASS_STATUS, payload);
|
|
813
856
|
}
|
|
814
857
|
|
|
815
858
|
/**
|
|
816
|
-
* Build system trouble status request
|
|
859
|
+
* Build system trouble status request (0x0814 wrapped in 0x0800)
|
|
817
860
|
*/
|
|
818
861
|
buildTroubleStatusRequest() {
|
|
819
|
-
|
|
862
|
+
// 0x0814 typically has no payload or minimal payload
|
|
863
|
+
return this.buildCommandRequest(CMD.SYSTEM_TROUBLE_STATUS, Buffer.alloc(0));
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Build status request command (legacy - simple format)
|
|
868
|
+
* @deprecated Use buildCommandRequest() with specific inner commands instead
|
|
869
|
+
*/
|
|
870
|
+
buildStatusRequest(statusType, partitionOrZone = 0) {
|
|
871
|
+
// Legacy format - kept for backwards compatibility
|
|
872
|
+
const payload = Buffer.from([statusType, partitionOrZone]);
|
|
873
|
+
return this.buildCommand(CMD.STATUS_REQUEST, payload);
|
|
820
874
|
}
|
|
821
875
|
|
|
822
876
|
/**
|