Handling CRC-16 Encapsulation Commands (Crc16Encap)

devicetype
command
zwave

(codersaur) #1

I’m currently cleaning up the code on some of my device handlers, and want to check the appropriate way to handle Crc16Encap commands.

Quick example of a command, for reference:
Crc16Encap(checksum: 125, command: 2, commandClass: 50, data: [33, 68, 0, 0, 0, 194, 0, 0, 77])

The only example of a zwaveEvent() handler I can find is from @erocm1231.

def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) {
	def versions = [0x20: 1, 0x25: 1, ...]
	def version = versions[cmd.commandClass as Integer]
	def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass)
	def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data)
	if (encapsulatedCommand) {
		zwaveEvent(encapsulatedCommand)
	}
}

While this works, and is fairly self-explanatory, one thing that bugs me is that at no point is the checksum actually verified (which is supposed to be the whole point of this command class)!

So my question is, does the zwave.parse() method verify the crc16 checksum when it creates a Crc16Encap command, or should it be verified as part of the zwaveEvent() handler?

If the latter, is there a groovy library or code example which can be used to calculate the CRC16? I’ve found an example of the ZW-CheckCrc16 algorithm written in c in the appendix of this Z-wave Alliance document, but would rather not translate it if it’s not needed or already been done.


(Eric M) #2

Don’t quote me on this because I am curious as well for a definitive answer, but I swear I have seen a crc failure logged when working with some devices. So it seems like the checking is done on the back end before handing it off to the parse method. Again, I’d like to hear that confirmed from someone more familiar with the issue.


(codersaur) #3

I think I just answered the first part of my own question. I decided to test what would happen if I deliberately corrupted Crc16Encap messages, so I added the following code to the parse() method of my device handler:

// TEST: Corrupt CRC16 messages:
if (description.contains("command: 5601")) {
	description = description.replace("payload: ", "payload: 99 ")
    log.debug "Corrupted CRC16 command. New message is: ${description}"
}

This code adds an extra byte at the start of the payload, which means the checksum will be wrong.

It turns out the zwave.parse() willl still happily parse it as a Crc16Encap command, although zwaveEvent() then fails to extract the encapsulated command as I added an un-listed command class to the payload (this is expected).

zwaveEvent(): Could not extract command from Crc16Encap(checksum: 169, command: 49, commandClass: 153, data: [5, 4, 34, 0, 49, 95])
zwaveEvent(): CRC-16 Encapsulation Command received: Crc16Encap(checksum: 169, command: 49, commandClass: 153, data: [5, 4, 34, 0, 49, 95])
Corrupted CRC16 command. New message is: zw device: 10, command: 5601, payload: 99 31 05 04 22 00 31 5F A9

In conclusion, zwave.parse() does not appear to verify the checksum, which means that the checksum should probably be validated within zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd).


(codersaur) #4

Regardless of if it’s really required, I challenged myself to write my own checksum validation (I was bored this evening and was interested to learn how checksums work anyway). :stuck_out_tongue:

Updated zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) as follows:

def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) {
    logger("zwaveEvent(): CRC-16 Encapsulation Command received: ${cmd}","trace")

    // Check CRC:
    def bytesToCheck = [0x56, 0x01, cmd.commandClass, cmd.command]
    bytesToCheck.addAll(cmd.data)
    bytesToCheck << cmd.checksum // By adding the checksum at the end, the new checksum should be zero.

    if ( 0 != zwaveCrc16(bytesToCheck as byte[]) ) {
        logger("zwaveEvent(): CRC-16 Checksum Failure. Command: ${cmd}","error")
    }
    else { // Checksum OK.
        def versions = getCommandClassVersions()
        def version = versions[cmd.commandClass as Integer]
        def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass)
        def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data)
        if (!encapsulatedCommand) {
            logger("zwaveEvent(): Could not extract command from ${cmd}","error")
        } else {
            return zwaveEvent(encapsulatedCommand)
        }
    }	
}

Which references this function:

/**
 *  zwaveCrc16(bytes)
 *
 *  Calculates the 16-bit CRC (CRC-CCITT) for a byte array.
 *
 *  Uses initial crc of 0x1D0F, and poly of 0x1021, as per Z-wave specification.
 *  
 *  Reference: http://z-wave.sigmadesigns.com/wp-content/uploads/2016/08/SDS12652-13-Z-Wave-Command-Class-Specification-N-Z.pdf
 **/
private zwaveCrc16(byte[] bytes) {
    short crc = 0x1D0F // It's important this is a short (16-bit)
    short poly = 0x1021

    bytes.each { workData ->
        //for (bitMask = 0x80; bitMask != 0; bitMask >>= 1) {
        // Need to use a different way to iterate, as rightshift operator '>>' and unsigned rightshift '>>>' do not appear to work in SmartThings.
        [0b10000000,0b01000000,0b00100000,0b00010000,0b00001000,0b00000100,0b00000010,0b00000001].each { bitMask ->
            def newBit = ((workData & bitMask) != 0) ^ ((crc & 0x8000) != 0);
            crc <<= 1;
            if (newBit) { crc ^= poly; }
         }
    }
    return crc
}

So far, so good, I’ve not seen any checksum failures, so we’ll see if any turn up.

Any feedback on my code is appreciated, I learnt a lot of new stuff about bitwise operations today.


Suddenly missingpropertyexception on configuration report?
(codersaur) #5

The changes to the z-wave library in the last 24 hours have resulted in checksum values no longer validating successfully using the code I posted above. @duncan, @Brad_ST, can you advise what’s changed, and how to validate the checksum value correctly? Thanks.


(Duncan) #6

cmd.checksum is now 16 bits as it’s supposed to be instead of the first byte being included in data. I think you should be able to take it out of bytesToCheck and test cmd.checksum == zwaveCrc16(bytesToCheck as byte[]). But the hub does this check before sending the command to the cloud so it should never fail.


Suddenly missingpropertyexception on configuration report?
(codersaur) #7

Great, thanks @duncan.

The following code now works, though it’s not really required:

def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) {
	logger("zwaveEvent(): CRC-16 Encapsulation Command received: ${cmd}","trace")

	// Check CRC:
	def bytesToCheck = [0x56, 0x01, cmd.commandClass, cmd.command]
	bytesToCheck.addAll(cmd.data)

	if ( cmd.checksum != zwaveCrc16(bytesToCheck as byte[]) ) {
		logger("zwaveEvent(): CRC-16 Checksum Failure. Command: ${cmd}","error")
	}
	else { // Checksum OK.
		def versions = getCommandClassVersions()
		def version = versions[cmd.commandClass as Integer]
		def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass)
		def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data)
		if (!encapsulatedCommand) {
			logger("zwaveEvent(): Could not extract command from ${cmd}","error")
		} else {
			return zwaveEvent(encapsulatedCommand)
		}
	}	
}