dtu_datalogger_dtp_codec.js script

The following is an example script, which encodes or decodes a DTP (binary) data format.

var utils = Java.type('com.nokia.transformation.script.model.TransformUtil');
var Base64 = Java.type('java.util.Base64');

var key = new Uint8Array([0x79, 0x75, 0x79, 0x75, 0x79, 0x75, 0x79, 0x75, 0x6F, 0x70, 0x6F, 0x70, 0x6F, 0x70, 0x6F, 0x70]).buffer;

function DTPCodec() {

    const PARAMETER_BASE_URL = "/meter/0/"

    const PARAMETER_NAMES = [
        // 0 - 9
        "RecordingInterval", "Time", "CurrMeterValue", "SIMCardPin", "APN",
        "GPRSLogin", "GPRSPassword", "ServerIPAddress", "ServerPort", "SIMCardICCID",
        // 10 - 19
        "EncryptionKey", "ModemIMEI", "SIMCardIMSI", "FirmwareVersion", "not-in-use",
        "not-in-use", "not-in-use", "DeviceRestartWaitTime", "Meter1Value", "Meter2Value",
        // 20 - 29
        "Meter3Value", "Meter4Value", "ClosedResistance1", "ClosedResistance2", "ClosedResistance3",
        "ClosedResistance4", "OpenResistance1", "OpenResistance2", "OpenResistance3", "OpenResistance4",
        // 30 - 39
        "Input1State", "Input2State", "Input3State", "Input4State", "ResetSettingsToDefault",
        "ActiveSIMCardSelection", "GSM1SignalLevel", "GSM2SignalLevel", "GSMModemOpTime", "BatteryVoltage",
        // 40 - 49
        "GSMModemStatus", "SIMCardStatus", "GSMNetRegistrationStatus", "GPRSConnectionStatus", "TCPConnectionStatus",
        "ScheduleType", "ScheduledServerConnectionTime", "ServerConnectionBitmask", "Timezone", "AutomaticDST",
        // 50 - 59
        "CmdRecvMultipleDevSettings", "TrainingModeStatus", "ProcessorTemperature", "ReqArchiveData", "CmdStopArchiveTransmission",
        "CmdCompleteRequestsFromServer", "CmdSetTestMode", "CmdSetDepassivation", "FlashCardOpStatusReq", "CmdSetExtDevPowerSupply",
        // 60 - 69
        "SetDeepSleepMode", "CurrModemOpFreqRange", "GPRSClassSelection", "TransportModeActivation", "MaxNumPulsesInput1",
        "MaxNumPulsesInput2", "MaxNumPulsesInput3", "MaxNumPulsesInput4", "MaxNumDataTransmissionAttempts", "SporadicDataTransmissionDisabling",
        // 70 - 79
        "SIMCard2Pin", "SIMCard2APN", "SIMCard2GPRSLogin", "SIMCard2GPRSPassword", "EnableSMSNotifications",
        "SMSNotificationReportingPeriodDate","SMSNotificationDeliveryPhoneNumber", "ReportingPeriodNumDays", "NetRegistrationMaxTime", "BatteryVoltageBeforeConnectionSession",
        // 80 - 89
        "BatteryVoltageAfterConnectionSession", "SetSIM1ActivityControl", "SetSIM1MaxInactivityPeriod", "SetSIM1MaxNumRepeatedConnection", "SetSIM2ActivityControl",
        "SetSIM2MaxInactivityPeriod", "SetSIM2MaxNumRepeatedConnection", "ClosedResistanceInput5", "ClosedResistanceInput6", "OpenResistanceInput5",
        // 90 - 99
        "OpenResistanceInput6", "Input5State", "Input6State", "Input1TypeSelection", "Input2TypeSelection",
        "Input3TypeSelection", "Input4TypeSelection", "Input5TypeSelection", "Input6TypeSelection", "InputsTriggerLevelAutoDetection",
        // 100 - 119
        "SetPasswordForSettingsChange", "BlockingDevSettingsChange", "BlockStatusRequest", "SetSysPasswordForSIM1", "SetSysPasswordForSIM2",
        "CmdRestoreInputsToDefaultSettings", "Input1ActiveStateSelection", "Input2ActiveStateSelection", "Input3ActiveStateSelection", "Input4ActiveStateSelection",
        // 110 - 119
        "ReqBatteryCapacityDischarge", "ResetBatteryCapacityDischargeCounter", "CmdArchiveCleanup", "SetTransparentChannelMode", "ReqEmergencyRebootsCounter",
        "ReqCurrResistanceValues", "SIM1OperatorName", "SIM2OperatorName", "SetTransparentMode", "TimeoutWaitingForPacket",
        // 120 - 129
        "PacketSize", "SerialPortBaudRate", "ParityCheck", "StopBits", "DataBits",
        "SensorsPollingFrequency", "NetworkStatusLine", "not-in-use", "ReqCurrPowerSupplySource", "not-in-use",
        // 130 - 139
        "ReadDeviceName", "NBIotFreqBand", "TransparentChannelServerAddress", "TransparentChannelPortNumber", "AuthorizationAlgo",
        "AuthorizationKey", "ServerConnectionStatus", "TimeoutWaitingForDataInTransparentChannel", "MaxInactivityTimeInTransparentChannel", "EstablishConnectionInSeparateTransparentChannel"
        // End of params
        , "all" // For testing purpose
    ];

    const PARAMETER_DATALEN = [
            // 0 - 9
            4, 4, 16, 4,32, 32, 32, 32, 8, 21,
            // 10 - 19
            16, 16, 16, 16, 0, 0, 0, 4, 4, 4,
            // 20 - 29
            4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
            // 30 - 39
            1, 1, 1, 1, 4, 1, 1, 17, 4, 4,
            // 40 - 49
            1, 1, 1, 1, 1, 1, 2, 5, 1, 1,
            // 50 - 59
            8, 1, 4, 8, 1, 1, 1, 1, 1, 1,
            // 60 - 69
            1, 32, 1, 1, 4, 4, 4, 4, 1, 1,
            // 70 - 79
            4, 32, 32, 32, 1, 1, 16, 1, 2, 4,
            // 80 - 89
            4, 1, 2, 2, 1, 2, 2, 4, 4, 4,
            // 90 - 99
            4, 1, 1, 1, 1, 1, 1, 1, 1, 1,
            // 100 - 119
            32, 32, 1, 1, 1, 1, 1, 1, 1, 1,
            // 110 - 119
            4, 1, 1, 4, 1, 12, 32, 32, 1, 2,
            // 120 - 129
            2, 4, 1, 1, 1, 1, 128, 0, 1, 0, // 8-128
            // 130 - 139
            128, 16, 32, 8, 1, 32, 1, 4, 4, 1 // 1-128
    ];

    const PARAMETER_NAMES_IN_LOWERCASE = PARAMETER_NAMES.map(n => n.toLowerCase());
    const SERVER_CONN_ERRORS = [
        "The session passed without errors",
        "Incorrect PIN",
        "SIM card is not inserted",
        "GSM network registration failed",
        "GPRS connection failed",
        "Connection to server failed"
    ];

    const LITTLE_ENDIAN = true;
    const MAX_PAYLOADSIZE = 1024;

    function keyFromBase64(psk) {
        var key;
        try {
            var key = Base64.getDecoder().decode(psk);
        } catch(e) { throw new Error(`The PSK ${psk} should be base64 encoded`); }

        if (key.length < 16) {
            throw new Error(`The key should be 16 bytes in length`)
        }

        if (key.length == 16) {
            return new Uint8Array(key).buffer;
        }

        return new Uint8Array(key).buffer.slice(0,16);
    }

    ///////////////////////////////
    // Encoder function

    this.encode = function (impactRequest) {

        var request = getImpactRequestAsJson(impactRequest);
        var imei = BigInt("0x0003118888559BCB"); //863703030668235; default
        if (request.serialNumber && request.serialNumber.startsWith("urn:imei:")) {
            imei = BigInt(request.serialNumber.substr(9));
        }

        var psk = client.getPsk();
        console.log("PSK in encode: ", psk);
        if (psk != null) { key = keyFromBase64(psk); }

        var arrBuffer = new ArrayBuffer(MAX_PAYLOADSIZE);
        var view = new DataView(arrBuffer);

        var dataLen = 0;
        var unack=false;

        switch (request.type) {
            case "READ":
                dataLen = compileReadSettingsCommand(view, 0, request);
                break;
            case "WRITE":
                dataLen = compileConfigCommand(view, 0, request);
                break;
            case "EXECUTE":
            case "OBSERVE":
            case "DELETE":
            default:
                throw new Error(`request type ${request.type} not supported by the DTP`);
                break;
        }

        var bytes;
        if (dataLen > 0) {
            // 1. Padding
            const padLen = 8 - ((dataLen+2) % 8); // including 2-bytes CRC
            if (padLen != 8) {
                // array buffer is zero initialised, so no need of explicitly setting them to zero
                //for (i=0; i<padLen; i++) view.setUint8(dataLen++, 0);
                dataLen += padLen;
            }
            // 2. CRC-CCITT
            bytes = new Uint8Array(arrBuffer);
            crc = crcCCITT(bytes, 0xFFFF, dataLen);
            view.setUint16(dataLen, crc, LITTLE_ENDIAN); dataLen+=2;
//            console.log("before encrypt", dataLen, buf2hex(arrBuffer));

            bytes = encryptPayload(arrBuffer.slice(0, dataLen), imei, key);
        }

        console.log("downlink:", buf2hex(bytes.buffer), dataLen);

        return  utils.createBinaryResponse(bytes, _map, unack);
    }

    // private functions used by this.encode()

    function compileReadSettingsCommand(view, offset, request) {

        var paramNum = PARAMETER_NAMES_IN_LOWERCASE.indexOf(request.resource.toLowerCase());
        if (paramNum === -1) {
            throw new Error(`unknown resource name ${request.resource}`);
        }

        view.setUint8(offset++, 6); // Read settings command

        view.setUint8(offset++, paramNum);
        view.setUint8(offset++, 0); // Data length

        return offset;
    }

    function compileConfigCommand(view, offset, request) {

        var paramNum = PARAMETER_NAMES_IN_LOWERCASE.indexOf(request.resource.toLowerCase());
        if (paramNum === -1) {
            throw new Error(`unknown resource name ${request.resource}`);
        }

        view.setUint8(offset++, 1); // Device config command

        var paramLen = PARAMETER_DATALEN[paramNum];
        if (paramLen == 4) {
            view.setUint8(offset++, paramNum);
            view.setUint8(offset++, paramLen);
            view.setUint32(offset, request.resourceValue, LITTLE_ENDIAN); offset += 4;
        } else if (paramLen == 2) {
            view.setUint8(offset++, paramNum);
            view.setUint8(offset++, paramLen);
            view.setUint16(offset, request.resourceValue, LITTLE_ENDIAN); offset += 2;
        } else if (paramLen == 1) {
            view.setUint8(offset++, paramNum);
            view.setUint8(offset++, paramLen);
            view.setUint8(offset, request.resourceValue); offset ++;
        } else {
            // TODO
        }

        return offset;
    }

    function compileReadConfigResource(view, offset, request) {

        if (request.resource == RESRC_CONF_REPORTING_INTERVAL) {

                view.setUint8(offset, CONFIG_REPORTING_INTERVAL); offset++;

        } else if (request.resource == RESRC_CONF_STATUS_INTERVAL) {

                view.setUint8(offset, CONFIG_STATUS_INTERVAL); offset++;

        } else if (request.resource == RESRC_CONF_COUNTER) {

                view.setUint8(offset, CONFIG_COUNTER); offset++;

        } else if (request.resource == RESRC_CONF_TEMPERATURE_THRESHOLD) {

                view.setUint8(offset, CONFIG_TEMPERATURE_THRESHOLD); offset++;

        } else if (request.resource == RESRC_CONF_TEMPERATURE_DETECTION) {

                view.setUint8(offset, CONFIG_FUNCTIONS); offset++

        }

        return offset;
    }

    function compileUpdateResource(view, offset, json) {

        view.setUint8(offset, 0xFF); offset++;

        return offset;
    }

    ///////////////////////////////
    // Decoder Function

    this.decode = function (decodeCtx) {

        var bytes = decodeCtx.getUplinkMessage();

	    console.log("input to decoder", buf2hex(bytes));

        var psk = client.getPsk();
        console.log("PSK in decode: ", psk);
        if (psk != null) { key = keyFromBase64(psk); }

        var data = new Uint8Array(bytes);
        var view = new DataView(data.buffer);
        var offset = 0;

        if (data[0] !== 0xC0 || data[data.byteLength-1] !== 0xC2) {
            throw new Error (`payload delimiter is missing`);
        }

        offset++;

        const imei = view.getBigInt64(offset, LITTLE_ENDIAN);
        offset += 8;

        console.log("client IMEI", imei);

        var decrypted = decryptPayload (data, offset, data.byteLength-1-offset);
        var decryptedDataLen = decrypted.byteLength - 2; // excluding CRC

        offset = 0;
        view = new DataView(decrypted);

        var result = {
            success: true,
            resources: [],
        };

        var dataID;
        while ((dataID = view.getUint8(offset++))) {
            var notify=false;
            switch (dataID) {
                case 9: // Telemetery data transmission
                    notify=true;
                    offset = handleTelemetryMsg(imei, view, offset, decrypted.byteLength-1, result);
                    break;
                case 3: // Data from meters
                    notify=true;
                    offset = handleDataFromMeters(imei, view, offset, decrypted.byteLength-1, result);
                    break;
                case 7: // Read Settings Command Response
                    offset = handleReadSettingsResponse(imei, view, offset, decrypted.byteLength-1, result);
                    break;
                case 2: // Device Configuration Command Response
                    offset = handleConfigurationResponse(imei, view, offset, decrypted.byteLength-1, result);
                    break;
                default:
                    throw new Error(`invalid message with data ID ${dataID} at ${offset-1}`);
            }
        }

        return notify? formImpactNotifyFromJson(decodeCtx, result.resources, PARAMETER_BASE_URL)
                    : formImpactResponseFromJson(decodeCtx, result.resources, result.success, PARAMETER_BASE_URL);

    }

    // private functions used by this.decode()

    function handleTelemetryMsg(imei, view, offset, lastIndex, result) {

        const numParams = view.getUint8(offset++);
        for (let i=0; i<numParams; i++) {
            let paramNum = view.getUint8(offset++);
            if (paramNum > 139) { throw new Error(`parameter number ${paramNum} is outside the range [0-139]`); }

            let dataLen = view.getUint8(offset++);
            if (dataLen < 1 || dataLen > 64) { throw new Error(`parameter data len ${dataLen} is outside the range [1-64]`); }

            parseParameter(paramNum, dataLen, view, offset, lastIndex, result.resources)
            offset += dataLen;
        }

        // Send acknowledgment
        var ackData = new Uint8Array([0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF2, 0x46]);
        var bytes = encryptPayload (ackData.buffer, imei, key);
        utils.setContextAckData(_map, bytes);

        return offset;
    }

    function handleDataFromMeters(imei, view, offset, lastIndex, result) {

            var seq = view.getUint8(offset++);

            while (offset < lastIndex) {
                console.log(offset, lastIndex);
                var evtCode = view.getUint8(offset);
                if (evtCode == 0) { // reached the padding bytes
                    break;
                }
                offset++;

                var evtTime = view.getUint32(offset, LITTLE_ENDIAN); offset += 4;
                var evtDataLen = view.getUint8(offset++);

                if (evtCode == 1) { // Data slicing event (time interval has passed)
                    var endOfEvt = offset + evtDataLen;
                    while (offset < endOfEvt) {
                        var dataType = view.getUint8(offset++);
                        if (dataType >= 0 && dataType <= 3) { // Meter [1-4] value in pulses
                            result.resources.push({name: `${PARAMETER_NAMES[18+dataType]}`, value: view.getUint32(offset, LITTLE_ENDIAN)}); offset+=4;
                        } else {
                            throw new Error(`invalid data type ${dataType} for Data slicing event`);
                        }
                    }
                } else if (evtCode == 2) { // ADC event (break or short circuit)
                    var endOfEvt = offset + evtDataLen;
                    while (offset < endOfEvt) {
                        var dataType = view.getUint8(offset++);
                        var valueCode = view.getUint8(offset++);
                        var value = valueCode == 1 ? "shortcircuit" : valueCode == 2 ? "break" : "invalid";

                        if (dataType >= 7 && dataType <= 10) { // Input [1-4] state
                            result.resources.push({name: `Input${dataType-6}State`, value});
                        } else if (dataType == 25 || dataType == 26) { // Input [5-6] state
                            result.resources.push({name: `Input${dataType-20}State`, value});
                        } else {
                            throw new Error(`invalid data type ${dataType} for ADC event (break or short circuit)`);
                        }
                    }
                } else if (evtCode == 3) { // Device restart
                    var dataType = view.getUint8(offset++);
                    if (dataType == 6) { // Number of data logger restarts
                        if (evtDataLen != 5) {
                            throw new Error(`invalid event data len ${evtDataLen} for Device restart`);
                        }
                        result.resources.push({name: `restarts`, value: view.getUint32(offset, LITTLE_ENDIAN)}); offset+=4;
                    } else {
                        throw new Error(`invalid data type ${dataType} for Device restart`);
                    }
                } else if (evtCode == 4) { // Dry Contact triggering
                    var value = evtDataLen;
                    result.resources.push({name: `drycontact`, value});
                } else if (evtCode == 8) { // Button was pressed
                    var value = evtDataLen;
                    result.resources.push({name: `buttonPressed`, value});
                } else if (evtCode == 10) { // Inputs training start/end
                    var dataType = view.getUint8(offset++);
                    if (dataType == 11) { // Inputs training state (0 - off, 1 - on)
                        var valueCode = view.getUint8(offset++);
                        var value = valueCode == 0 ? "off" : "on";
                        result.resources.push({name: `inputTrainingState`, value});
                    } else {
                        throw new Error(`invalid data type ${dataType} for input training`);
                    }
                } else if (evtCode == 11) { // Dry Contact input was trained
                    var value = evtDataLen;
                    result.resources.push({name: `dryContactInputTrained`, value});
                } else if (evtCode == 12) { // GPRS connection failed
                    var endOfEvt = offset + evtDataLen;
                    while (offset < endOfEvt) {
                        var dataType = view.getUint8(offset++);
                        if (dataType == 23) { // SIM card number, where connection error occurred (0 - SIM 1, 1 - SIM 2)
                            var valueCode = view.getUint8(offset++);
                            var value = valueCode == 0 ? "SIM1" : valueCode == 1 ? "SIM2" : "unknownSIM";
                            result.resources.push({name: `cellularFailureOn`, value});
                        } else if (dataType == 20) { // Server connection error code
                            var valueCode = view.getUint8(offset++);
                            if (valueCode >= 0 && valueCode < SERVER_CONN_ERRORS.length) {
                                result.resources.push({name: `cellularFailureServerError`, value: SERVER_CONN_ERRORS[valueCode] });
                            } else {
                                throw new Error(`invalid server connection error code ${valueCode} for GPRS connection failure`);
                            }
                        } else {
                            throw new Error(`invalid data type ${dataType} for GPRS connection failure`);
                        }
                    }
                } else if (evtCode == 13) { // External power has been disabled
                    var value = evtDataLen;
                    result.resources.push({name: `powerDisabled`, value});
                } else if (evtCode == 14) { // External power has been re-enabled
                    var value = evtDataLen;
                    result.resources.push({name: `powerEnabled`, value});
                } else if (evtCode == 15) { // Input pulse repetition frequency exceeding
                    var dataType = view.getUint8(offset++);
                    if (dataType == 22) { // Input number with a pulse repetition frequency exceeding
                        result.resources.push({name: `pulseRepetition`, value: view.getUint8(offset++)});
                    } else {
                        throw new Error(`invalid data type ${dataType} for Input pulse repetition frequency exceeding`);
                    }
                } else if (evtCode == 16) { // End of the archive transmission (not saved in the journal)
                    var value = evtDataLen;
                    result.resources.push({name: `endOfArchive`, value});
                } else if (evtCode == 17) { // Maximum SIM card inactivity period is exceeded
                    var dataType = view.getUint8(offset++);
                    if (dataType == 24) { // SIM card number with inactivity period exceeded (0 - SIM 1, 1 - SIM 2)
                        var valueCode = view.getUint8(offset++);
                        var value = valueCode == 0 ? "SIM1" : valueCode == 1 ? "SIM2" : "unknownSIM";
                        result.resources.push({name: `inactive`, value});
                    } else {
                        throw new Error(`invalid data type ${dataType} for SIM card inactive`);
                    }
                } else {
                    throw new Error(`invalid event code: ${evtCode} in Data from Meters`);
                }
            }

            // Send acknowledgment
            var ackData = new Uint8Array([0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
            var ackDataView = new DataView(ackData.buffer);
            ackDataView.setUint8(1, seq);
            var crc = crcCCITT(ackData, 0xFFFF, ackData.byteLength-2);
            ackDataView.setUint16(ackData.byteLength-2, crc, LITTLE_ENDIAN);
            var bytes = encryptPayload (ackData.buffer, imei, key);
            utils.setContextAckData(_map, bytes);

            return offset;
    }

    function handleReadSettingsResponse(imei, view, offset, lastIndex, result) {

        let paramNum = view.getUint8(offset++);
        if (paramNum > 139) { throw new Error(`parameter number ${paramNum} is outside the range [0-139]`); }

        let execCode = view.getUint8(offset++);
        if (execCode < 0 || execCode > 4) { throw new Error(`execution code ${execCode} is outside the range [0-4]`); }

        let dataLen = view.getUint8(offset++);
        if (dataLen < 1 || dataLen > 64) { throw new Error(`parameter data len ${dataLen} is outside the range [1-64]`); }

        parseParameter(paramNum, dataLen, view, offset, lastIndex, result.resources)
        offset += dataLen;

        return offset;
    }

    function handleConfigurationResponse(imei, view, offset, lastIndex, result) {

        let paramNum = view.getUint8(offset++);
        if (paramNum > 139) { throw new Error(`parameter number ${paramNum} is outside the range [0-139]`); }

        let execCode = view.getUint8(offset++);
        if (execCode < 0 || execCode > 4) { throw new Error(`execution code ${execCode} is outside the range [0-4]`); }

        result.resources.push({name: PARAMETER_NAMES[paramNum], execCode});

        result.success = (execCode == 0);

        return offset;
    }


    function parseParameter(paramNum, paramLen, view, offset, lastIndex, resources) {

        switch (paramNum) {
            case 2:   // Current meter values array
                for (let i=1; i<=4; i++) {
                    let value = view.getUint32(offset, LITTLE_ENDIAN); offset += 4;
                    resources.push({name: `${PARAMETER_NAMES[paramNum]}${i}`, value});
                }
                break;
            case 9: // SIM card ICCID
                break;
            case 13: // Firmware version
                break;
            case 18: // Meter 1 value
            case 19: // Meter 2 value
            case 20: // Meter 3 value
            case 21: // Meter 4 value
                let value = view.getUint32(offset, LITTLE_ENDIAN); offset += 4;
                resources.push({name: PARAMETER_NAMES[paramNum], value: value});
                break;
            case 37: // GSM signal level
                break;
            case 47: // Server connection bitmask
                break;
            case 61: // Current modem operating frequency range
                break;
        }

        switch (paramLen) {
            case 4: resources.push({name: PARAMETER_NAMES[paramNum], value: view.getUint32(offset, LITTLE_ENDIAN)}); break;
            case 2: resources.push({name: PARAMETER_NAMES[paramNum], value: view.getUint16(offset, LITTLE_ENDIAN)}); break;
            case 1: resources.push({name: PARAMETER_NAMES[paramNum], value: view.getUint8(offset)}); break;
        }

        return resources;

    }

    function byteDestuff(data, offset, len) {
        var buf = new ArrayBuffer(len);
        var view = new Uint8Array(buf);
        var stuffCount=0;
        for (i=offset, j=0; i<len+offset; i++) {
            if (data[i] == 0xC4) {
                stuffCount++;
                if (data[i+1] == 0xC4) { view[j++] = 0xC4 }
                else if (data[i+1] == 0xC1) { view[j++] = 0xC0 }
                else if (data[i+1] == 0xC3) { view[j++] = 0xC2 }
                else { throw new Error(`data is wrongly stuffed at ${i} ${data[i].toString(16)} ${data[i+1].toString(16)}`); }

                i++;
            } else {
                view[j++] = data[i];
            }
        }

        return stuffCount > 0? buf.slice(0, len-stuffCount) : buf;
    }

    function byteStuff(data, offset, len, buf, bufOffset=0) {
        var input = new Uint8Array(data);
        var output = new Uint8Array(buf);
        var stuffCount=0;
        var j=bufOffset;

        for (i=offset; i<len+offset; i++) {
            if (input[i] == 0xC0) {
                output[j++] = 0xC4;
                output[j++] = 0xC1;
            } else if (input[i] == 0xC2) {
               output[j++] = 0xC4;
               output[j++] = 0xC3;
            } else if (input[i] == 0xC4) {
                output[j++] = 0xC4;
                output[j++] = 0xC4;
            } else {
                output[j++] = input[i];
            }
        }

        return j;
    }

    function decryptPayload (data, offset, len) {

        var dsBuf = byteDestuff(data, offset, len);
        console.log("destuffed", dsBuf.byteLength, buf2hex(dsBuf));

        var decrypted = xtea.decrypt(dsBuf, key);
        console.log("decrypted", decrypted.byteLength, buf2hex(decrypted));

        var bytes = new Uint8Array(decrypted);
        // swap the crc bytes, since they're in little endian form
        var len = bytes.length; var t=bytes[len-2];
        bytes[len-2] = bytes[len-1]; bytes[len-1] = t;
        if (crcCCITT(bytes) !== 0) throw new Error("CRC check failure");

        return decrypted;
    }

    function encryptPayload(dataBuf, imei, key) {

        if (dataBuf.byteLength % 8 != 0) {
            throw new Error("dataBuf for encryption is not properly byte aligned");
        }

        // 1. encryption
        var encrypted = xtea.encrypt(dataBuf, key);
        console.log("encrypted", encrypted.byteLength, buf2hex(encrypted));

        // 2a. prefix C0 + imei
        var arrBuffer = new ArrayBuffer(MAX_PAYLOADSIZE);
        view = new DataView(arrBuffer);

        let offset = 0;
        view.setUint8(offset++, 0xC0);
        view.setBigInt64(offset, imei, LITTLE_ENDIAN); offset += 8;

        // 2b. byte-stuffed data
        offset = byteStuff(encrypted, 0, dataBuf.byteLength, arrBuffer, offset);

        // 2c. suffix C2
        view.setUint8(offset++, 0xC2);

        return new Int8Array(arrBuffer.slice(0, offset));
    }
}

var codec = new DTPCodec();

(codec);

For more information on sample payload, refer to page 29 of https://adgt.cz/wp-content/uploads/2020/12/DTU-Remote-Data-Logger-series_Data-transmission-protocol-r.1.7-2020-11-27.pdf.