import { CommandClasses , CRC16_CCITT , MessageOrCCLogEntry , validatePayload , ZWaveError , ZWaveErrorCodes , } from "@zwave-js/core" ; import { buffer2hex } from "@zwave-js/shared" ; import type { Driver } from "../driver/Driver" ; import { CCCommand , CCCommandOptions , CCResponseRole , CommandClass , commandClass , CommandClassDeserializationOptions , expectedCCResponse , gotDeserializationOptions , implementedVersion , SinglecastCC , } from "./CommandClass" ; export enum TransportServiceCommand { FirstSegment = 0xc0 , SegmentComplete = 0xe8 , SegmentRequest = 0xc8 , SegmentWait = 0xf0 , SubsequentSegment = 0xe0 } const MAX_SEGMENT_SIZE = 39 ; export const TransportServiceTimeouts = { /** Waiting time before requesting a missing segment at data rate R2 */ requestMissingSegmentR2 : 800 , /** Waiting time before requesting a missing segment at data rate R3 */ requestMissingSegmentR3 : 400 } ; @ commandClass ( CommandClasses [ "Transport Service" ] ) @ implementedVersion ( 2 ) export class TransportServiceCC extends CommandClass implements SinglecastCC < TransportServiceCC > { declare ccCommand : TransportServiceCommand ; declare nodeId : number ; public static getCCCommand ( data ) { const originalCCCommand = super . getCCCommand ( data ) ! ; return originalCCCommand & 248 ; } protected deserialize ( data ) { const ret = super . deserialize ( data ) as { ccId : CommandClasses ; ccCommand : number ; payload : Buffer ; } ; ret . payload = Buffer . concat ( [ Buffer . from ( [ ret . ccCommand & 0b111 ] ) , ret . payload , ] ) ; return ret ; } /** Encapsulates a command that should be sent in multiple segments */ public static encapsulate ( _driver , _cc ) { throw new Error ( "not implemented" ) ; } } interface TransportServiceCCFirstSegmentOptions extends CCCommandOptions { datagramSize : number ; sessionId : number ; headerExtension ? : Buffer | undefined ; partialDatagram : Buffer ; } export function isTransportServiceEncapsulation ( command ) { return ( command . ccId === CommandClasses [ "Transport Service" ] && ( command . ccCommand === TransportServiceCommand . FirstSegment || command . ccCommand === TransportServiceCommand . SubsequentSegment ) ) ; } @ CCCommand ( TransportServiceCommand . FirstSegment ) export class TransportServiceCCFirstSegment extends TransportServiceCC { public constructor ( driver , options ) { super ( driver , options ) ; if ( gotDeserializationOptions ( options ) ) { validatePayload ( this . payload . length >= 6 ) ; const headerBuffer = Buffer . from ( [ this . ccId , this . ccCommand | this . payload [ 0 ] , ] ) ; const ccBuffer = this . payload . slice ( 1 , - 2 ) ; let expectedCRC = CRC16_CCITT ( headerBuffer ) ; expectedCRC = CRC16_CCITT ( ccBuffer , expectedCRC ) ; const actualCRC = this . payload . readUInt16BE ( this . payload . length - 2 ) ; validatePayload ( expectedCRC === actualCRC ) ; this . datagramSize = this . payload . readUInt16BE ( 0 ) ; this . sessionId = this . payload [ 2 ] >>> 4 ; let payloadOffset = 3 ; const hasHeaderExtension = ! ! ( this . payload [ 2 ] & 0b1000 ) ; if ( hasHeaderExtension ) { const extLength = this . payload [ 3 ] ; this . headerExtension = this . payload . slice ( 4 , 4 + extLength ) ; payloadOffset += 1 + extLength ; } this . partialDatagram = this . payload . slice ( payloadOffset , - 2 ) ; validatePayload ( this . partialDatagram . length <= MAX_SEGMENT_SIZE ) ; } else { this . datagramSize = options . datagramSize ; this . sessionId = options . sessionId ; this . headerExtension = options . headerExtension ; this . partialDatagram = options . partialDatagram ; } } public datagramSize : number ; public sessionId : number ; public headerExtension : Buffer | undefined ; public partialDatagram : Buffer ; public encapsulated ! : CommandClass ; public serialize ( ) { this . ccCommand = ( this . ccCommand & 248 ) | ( ( this . datagramSize >>> 8 ) & 0b111 ) ; const ext = ! ! this . headerExtension && this . headerExtension . length >= 1 ; this . payload = Buffer . from ( [ this . datagramSize & 0xff , ( ( this . sessionId & 0b1111 ) << 4 ) | ( ext ? 0b1000 : 0 ) , ] ) ; if ( ext ) { this . payload = Buffer . concat ( [ this . payload , Buffer . from ( [ this . headerExtension ! . length ] ) , this . headerExtension ! , ] ) ; } this . payload = Buffer . concat ( [ this . payload , this . partialDatagram , Buffer . alloc ( 2 , 0 ) , ] ) ; const headerBuffer = Buffer . from ( [ this . ccId , this . ccCommand ] ) ; let crc = CRC16_CCITT ( headerBuffer ) ; crc = CRC16_CCITT ( this . payload . slice ( 0 , - 2 ) , crc ) ; this . payload . writeUInt16BE ( crc , this . payload . length - 2 ) ; return super . serialize ( ) ; } public expectMoreMessages ( ) { return true ; } public getPartialCCSessionId ( ) { return { ccCommand : undefined , sessionId : this . sessionId } ; } protected computeEncapsulationOverhead ( ) { return ( super . computeEncapsulationOverhead ( ) + 4 + ( this . headerExtension ?. length ? 1 + this . headerExtension . length : 0 ) ) ; } public toLogEntry ( ) { return { ... super . toLogEntry ( ) , message : { "session ID" : this . sessionId , "datagram size" : this . datagramSize , "byte range" : `0...${ this . partialDatagram . length - 1 }` , payload : buffer2hex ( this . partialDatagram ) } } ; } } interface TransportServiceCCSubsequentSegmentOptions extends TransportServiceCCFirstSegmentOptions { datagramOffset : number ; } @ CCCommand ( TransportServiceCommand . SubsequentSegment ) export class TransportServiceCCSubsequentSegment extends TransportServiceCC { public constructor ( driver , options ) { super ( driver , options ) ; if ( gotDeserializationOptions ( options ) ) { validatePayload ( this . payload . length >= 7 ) ; const headerBuffer = Buffer . from ( [ this . ccId , this . ccCommand | this . payload [ 0 ] , ] ) ; const ccBuffer = this . payload . slice ( 1 , - 2 ) ; let expectedCRC = CRC16_CCITT ( headerBuffer ) ; expectedCRC = CRC16_CCITT ( ccBuffer , expectedCRC ) ; const actualCRC = this . payload . readUInt16BE ( this . payload . length - 2 ) ; validatePayload ( expectedCRC === actualCRC ) ; this . datagramSize = this . payload . readUInt16BE ( 0 ) ; this . sessionId = this . payload [ 2 ] >>> 4 ; this . datagramOffset = ( ( this . payload [ 2 ] & 0b111 ) << 8 ) + this . payload [ 3 ] ; let payloadOffset = 4 ; const hasHeaderExtension = ! ! ( this . payload [ 2 ] & 0b1000 ) ; if ( hasHeaderExtension ) { const extLength = this . payload [ 4 ] ; this . headerExtension = this . payload . slice ( 5 , 5 + extLength ) ; payloadOffset += 1 + extLength ; } this . partialDatagram = this . payload . slice ( payloadOffset , - 2 ) ; validatePayload ( this . partialDatagram . length <= MAX_SEGMENT_SIZE ) ; } else { this . datagramSize = options . datagramSize ; this . datagramOffset = options . datagramOffset ; this . sessionId = options . sessionId ; this . headerExtension = options . headerExtension ; this . partialDatagram = options . partialDatagram ; } } public datagramSize : number ; public datagramOffset : number ; public sessionId : number ; public headerExtension : Buffer | undefined ; public partialDatagram : Buffer ; private _encapsulated ! : CommandClass ; public get encapsulated ( ) : CommandClass { return this . _encapsulated ; } public expectMoreMessages ( session ) { if ( ! ( session [ 0 ] instanceof TransportServiceCCFirstSegment ) ) { return true ; } const datagramSize = session [ 0 ] . datagramSize ; const chunkSize = session [ 0 ] . partialDatagram . length ; const received = new Array < boolean > ( Math . ceil ( datagramSize / chunkSize ) ) . fill ( false ) ; for ( const segment of [ ... session , this ] ) { const offset = segment instanceof TransportServiceCCFirstSegment ? 0 : segment . datagramOffset ; received [ offset / chunkSize ] = true ; } return ! received . every ( Boolean ) ; } public getPartialCCSessionId ( ) { return { ccCommand : undefined , sessionId : this . sessionId } ; } public mergePartialCCs ( partials ) { const datagram = Buffer . allocUnsafe ( this . datagramSize ) ; for ( const partial of [ ... partials , this ] ) { const offset = partial instanceof TransportServiceCCFirstSegment ? 0 : partial . datagramOffset ; if ( offset + partial . partialDatagram . length > datagram . length ) { throw new ZWaveError ( `The partial datagram offset and length in a segment are not compatible to the communicated datagram length` , ZWaveErrorCodes . PacketFormat_InvalidPayload ) ; } partial . partialDatagram . copy ( datagram , offset ) ; } this . _encapsulated = CommandClass . from ( this . driver , { data : datagram , fromEncapsulation : true , encapCC : this } ) ; } public serialize ( ) { this . ccCommand = ( this . ccCommand & 248 ) | ( ( this . datagramSize >>> 8 ) & 0b111 ) ; const ext = ! ! this . headerExtension && this . headerExtension . length >= 1 ; this . payload = Buffer . from ( [ this . datagramSize & 0xff , ( ( this . sessionId & 0b1111 ) << 4 ) | ( ext ? 0b1000 : 0 ) | ( ( this . datagramOffset >>> 8 ) & 0b111 ) , this . datagramOffset & 0xff , ] ) ; if ( ext ) { this . payload = Buffer . concat ( [ this . payload , Buffer . from ( [ this . headerExtension ! . length ] ) , this . headerExtension ! , ] ) ; } this . payload = Buffer . concat ( [ this . payload , this . partialDatagram , Buffer . alloc ( 2 , 0 ) , ] ) ; const headerBuffer = Buffer . from ( [ this . ccId , this . ccCommand ] ) ; let crc = CRC16_CCITT ( headerBuffer ) ; crc = CRC16_CCITT ( this . payload . slice ( 0 , - 2 ) , crc ) ; this . payload . writeUInt16BE ( crc , this . payload . length - 2 ) ; return super . serialize ( ) ; } protected computeEncapsulationOverhead ( ) { return ( super . computeEncapsulationOverhead ( ) + 5 + ( this . headerExtension ?. length ? 1 + this . headerExtension . length : 0 ) ) ; } public toLogEntry ( ) { return { ... super . toLogEntry ( ) , message : { "session ID" : this . sessionId , "datagram size" : this . datagramSize , "byte range" : `${ this . datagramOffset }...${ this . datagramOffset + this . partialDatagram . length - 1 }` , payload : buffer2hex ( this . partialDatagram ) } } ; } } interface TransportServiceCCSegmentRequestOptions extends CCCommandOptions { sessionId : number ; datagramOffset : number ; } function testResponseForSegmentRequest ( sent , received ) { return ( ( sent . datagramOffset === 0 && received instanceof TransportServiceCCFirstSegment && received . sessionId === sent . sessionId ) || ( sent . datagramOffset > 0 && received instanceof TransportServiceCCSubsequentSegment && sent . datagramOffset === received . datagramOffset && received . sessionId === sent . sessionId ) ) ; } @ CCCommand ( TransportServiceCommand . SegmentRequest ) @ expectedCCResponse ( TransportServiceCC , testResponseForSegmentRequest ) export class TransportServiceCCSegmentRequest extends TransportServiceCC { public constructor ( driver , options ) { super ( driver , options ) ; if ( gotDeserializationOptions ( options ) ) { validatePayload ( this . payload . length >= 3 ) ; this . sessionId = this . payload [ 1 ] >>> 4 ; this . datagramOffset = ( ( this . payload [ 1 ] & 0b111 ) << 8 ) + this . payload [ 2 ] ; } else { this . sessionId = options . sessionId ; this . datagramOffset = options . datagramOffset ; } } public sessionId : number ; public datagramOffset : number ; public serialize ( ) { this . payload = Buffer . from ( [ ( ( this . sessionId & 0b1111 ) << 4 ) | ( ( this . datagramOffset >>> 8 ) & 0b111 ) , this . datagramOffset & 0xff , ] ) ; return super . serialize ( ) ; } } interface TransportServiceCCSegmentCompleteOptions extends CCCommandOptions { sessionId : number ; } @ CCCommand ( TransportServiceCommand . SegmentComplete ) export class TransportServiceCCSegmentComplete extends TransportServiceCC { public constructor ( driver , options ) { super ( driver , options ) ; if ( gotDeserializationOptions ( options ) ) { validatePayload ( this . payload . length >= 2 ) ; this . sessionId = this . payload [ 1 ] >>> 4 ; } else { this . sessionId = options . sessionId ; } } public sessionId : number ; public serialize ( ) { this . payload = Buffer . from ( [ ( this . sessionId & 0b1111 ) << 4 ] ) ; return super . serialize ( ) ; } } interface TransportServiceCCSegmentWaitOptions extends CCCommandOptions { pendingSegments : number ; } @ CCCommand ( TransportServiceCommand . SegmentWait ) export class TransportServiceCCSegmentWait extends TransportServiceCC { public constructor ( driver , options ) { super ( driver , options ) ; if ( gotDeserializationOptions ( options ) ) { validatePayload ( this . payload . length >= 2 ) ; this . pendingSegments = this . payload [ 1 ] ; } else { this . pendingSegments = options . pendingSegments ; } } public pendingSegments : number ; public serialize ( ) { this . payload = Buffer . from ( [ this . pendingSegments ] ) ; return super . serialize ( ) ; } }