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.