* INCOMING (DECRYPTED) MESSAGE HANDLER **
function(thisObject, data, rinfo) {
    if (typeof data === "undefined") {
        // no data; abort.
        return;
    }
    if (Object.prototype.toString.call(data) != "[object Uint8Array]") 
    {
        // no data; abort.
        return;
    }

    let HEADER_MINIMUM_SIZE = 2;
    let ZIP_WRAPPED_HEADER_MINIMUM_SIZE = 7;
    if (data.length < HEADER_MINIMUM_SIZE)
    {
        // not enough data for any packet; abort.
        return;
    }

    let isWrappedInZipFrame = false;

    // get the "initial" command class and command; in case of Z/IP-wrapped packets, these are the "header" (wrapping) command class and command
    let offset = 0;
    // command class
    let initialCommandClass = data[offset];
    offset += 1;
    // command
    let initialCommand = data[offset];
    offset += 1;

    if (initialCommandClass !== PrivateCommandClass.Zip || initialCommand !== ZipCommand.ZipPacket)
    {
        // mesage is not enclosed in a Z/IP packet; handle this special case first
        isWrappedInZipFrame = false;

        if (initialCommandClass === PrivateCommandClass.ZipNd && initialCommand === ZipNdCommand.NodeAdvertisement) {
            // allowed command
        } else {
            // if we do not recognize the command as an allowable "non-wrapped" incoming command/response, abort now
            return;
        }
    }
    else {
        // otherwise, the packet is a Z/IP packet; make sure it is at least large enough for a minimal Z/IP packet
        isWrappedInZipFrame = true;

        if (data.length < ZIP_WRAPPED_HEADER_MINIMUM_SIZE)
        {
            // not enough data for minimal Z/IP packet; abort.
            return;
        }
    }

    // parse message
    let ackResponse = null;
    let nackResponse = null;
    let nackWaiting = null;
    let nackQueueFull = null;
    let headerExtensionsIncluded = null;
    let sequenceNumber = null;
    let headerExtensionsLength = null;
    let commandClass = null;
    let command = null;
    let payload = null;
    if (isWrappedInZipFrame) {
        // AckRequest | AckResponse | NackResponse | NackFlags (Waiting, QueueFull, OptionError) | Reserved
        ackResponse = ((data[offset] & ZIP_PACKET_FLAGS0.ACK_RES) != 0);
        nackResponse = ((data[offset] & ZIP_PACKET_FLAGS0.NACK_RES) != 0);
        nackWaiting = ((data[offset] & ZIP_PACKET_FLAGS0.WAIT_RES) != 0);
        nackQueueFull = ((data[offset] & ZIP_PACKET_FLAGS0.NACK_QF) != 0);
        offset += 1;
        // HeaderExtIncluded | ZWaveCmdIncluded | MoreInformation | SecureOrigin | Reserved
        headerExtensionsIncluded = ((data[offset] & ZIP_PACKET_FLAGS1.HDR_EXT_INCL) != 0);
        offset += 1;
        // SequenceNumber
        sequenceNumber = data[offset];
        offset += 1;
        // Res | SourceEndPoint
        offset += 1;
        // BitAddress | DestinationEndpoint
        offset += 1;
        // Header Extensions (optional)
        headerExtensionsLength = 0;
        if (headerExtensionsIncluded) {
            // first byte of header extensions: total length
            headerExtensionsLength = data[offset];
            // NOTE: we are currently ignoring extensions

            offset += headerExtensionsLength;
        }
        //
        if (data.length >= ZIP_WRAPPED_HEADER_MINIMUM_SIZE + headerExtensionsLength + 2) {
            // command class
            commandClass = data[offset];
            offset += 1;
            // command
            command = data[offset];
            offset += 1;
        }
    } else {
        commandClass = initialCommandClass;
        command = initialCommand;
    }
    //
    // payload data
    payload = Buffer.alloc(data.length - offset);
    data.copy(payload, 0, offset, data.length);
    offset += payload.length;

    if (isWrappedInZipFrame) {
        // if this message is an ACK response, trigger any pending ACK 
        if (ackResponse) {
            let ackRegistration = thisObject.getAndRemoveAckRegistration(sequenceNumber);
            if (typeof ackRegistration !== "undefined") {
                if (typeof ackRegistration.timeoutObject !== "undefined") {
                    clearTimeout(ackRegistration.timeoutObject);
                }
                if (!thisObject.checkTimeoutHasOccured(ackRegistration.timeoutTimestamp)) {
                    ackRegistration.callback(true, false); // true = ACK, false = no Z/IP protocol timeout
                }
            }
        }
        else if (nackResponse) {
            if (nackQueueFull) {
                // queue full: retry sending message in ten seconds
                this.updateAckTimeout(sequenceNumber, 12000); // 10 seconds + 2 seconds wait time
                //
                thisObject.dtlsSocket.send(ackRegistration.data.msg, ackRegistration.data.offset, ackRegistration.data.length, ackRegistration.data.port, ackRegistration.data.host,
                    ackRegistration.data.pskIdentity, ackRegistration.data.pskPassword, ackRegistration.data.callback);            
            } else if (nackWaiting) {
                // add 90 seconds to the ACK timeout timer 
                thisObject.updateAckTimeout(sequenceNumber, 90000);
            } else {
                // message was rejected
                let ackRegistration = thisObject.getAndRemoveAckRegistration(sequenceNumber);
                if (typeof ackRegistration !== "undefined") {
                    if (typeof ackRegistration.timeoutObject !== "undefined") {
                        clearTimeout(ackRegistration.timeoutObject);
                    }
                    if (!thisObject.checkTimeoutHasOccured(ackRegistration.timeoutTimestamp)) {
                        ackRegistration.callback(false, false); // false = NAK, false = no Z/IP protocol timeout
                    }
                }       
            } 
        }
    }

    if (commandClass !== null && command !== null) {
        // for data packets: treat this message as a response and check it against the "pending command callbacks" list
        for (let iCallback = 0; iCallback < thisObject.pendingCommandCallbacks.length; iCallback++)
        {
            let commandCallback = thisObject.pendingCommandCallbacks[iCallback];
            if (commandCallback.commandClass === commandClass && commandCallback.command === command && commandCallback.isWrappedInZipFrame === isWrappedInZipFrame)
            {
                // command class and command matches; check timeout
                if (thisObject.checkTimeoutHasOccured(commandCallback.timeoutTimestamp))
                {
                    // command has timed out; skip this entry as its callback will automatically be removed at timeout
                    continue;
                }

                // command class and command match and timeout has not expired; check match identifier if one exists
                if (commandClass == ZWaveIP.CommandClass.NetworkManagementProxy && command == ZWaveIP.NetworkManagementProxyCommand.NodeListReport) {
                    if (payload.length < 1 || payload[0] != commandCallback.matchIdentifier) {
                        // NOTE: matchIdentifier is the sequenceNumber
                        // match identifier does not match; skip this entry
                        continue;
                    }
                }
                else if(commandClass == PrivateCommandClass.ZipNd && command == ZipNdCommand.NodeAdvertisement) {
                    if (payload.length < 2 || payload[1] != commandCallback.matchIdentifier) {
                        // NOTE: matchIdentifier is the nodeId
                        // match identifier does not match; skip this entry
                        continue;
                    }
                }
                // else...

                // if we reach here, the response matches; deactivate the callback timer, remove the callback entry and then call the callback
                //
                let callback = commandCallback.callback;
                // cancel the callback timeout timer
                clearTimeout(commandCallback.timeoutObject);
                // remove the callback from the pending callbacks list
                thisObject.pendingCommandCallbacks.splice(iCallback, 1);
                // call the callback
                commandCallback.callback({commandClass: commandClass, command: command, data: payload}, false /* did not time out */);
                break;
            }
        }
    }

    // if there was no callback registered, simply discard this packet.
}