nordicmeterscript.js script
The following is an example script which encodes or decodes a Base64 format for LoRaWAN. This is an example implementation of https://www.nasys.no/wp-content/uploads/Cyble_Module_CM3030_2.pdf.
var utils = Java.type('com.nokia.transformation.script.model.TransformUtil');
var Base64 = Java.type('java.util.Base64');
function LoraCodec() {
const useJson = true;
const RESRC_USAGE = "Water/0/UsageinLiters";
const RESRC_STATUS = "Status";
const RESRC_STATUS_BATTERY = "Device/0/mV";
const RESRC_STATUS_TEMPERATURE = "Water/0/Temperature";
const RESRC_STATUS_TEMPERATURE_DETECTION_MODE = "Water/0/TempDetection";
const RESRC_STATUS_TEMPERATURE_DETECTION_STATUS = "TemperatureDetectionStatus";
const RESRC_STATUS_USER_TRIGGERED_STATUS = "Water/0/UserTriggered";
const RESRC_STATUS_IS_ALERT_STATUS = "Water/0/IsAlert";
const RESRC_CONFIGURATION = "Configuration";
const RESRC_CONF_REPORTING_INTERVAL = "ReportingInterval";
const RESRC_CONF_STATUS_INTERVAL = "StatusInterval";
const RESRC_CONF_COUNTER = "Counter";
const RESRC_CONF_TEMPERATURE_THRESHOLD = "TemperatureThreshold";
const RESRC_CONF_TEMPERATURE_DETECTION = "TemperatureDetection";
const RESRC_SHUTDOWN = "Shutdown";
const RESRC_SHUTDOWN_REASON = "ShutdownReason";
const RESRC_UPDATE = "Update";
const RESRC_CONF_REPORTING_INTERVAL_NEW = "Water/0/ReportingInterval";
const RESRC_BOOT = "Boot";
const RESRC_BOOT_REASON = "Device/0/BootReason";
const RESRC_BOOT_SERIAL = "Device/0/Serial";
const RESRC_BOOT_FIRMWARE = "Device/0/Firmware";
const ENDPOINT = "device/0/endPointClientName";
const resetReasons = new Map([
[ 0x10, "Calibration timeout"],
[ 0x31, "Shutdown by user (magnet)"]
]);
const fPorts = new Map([
[RESRC_USAGE, 14],
[RESRC_STATUS, 24],
[RESRC_CONFIGURATION, 50],
[RESRC_UPDATE, 51]
]);
const allowedReadParameters = [
RESRC_CONF_REPORTING_INTERVAL,
RESRC_CONF_STATUS_INTERVAL,
RESRC_CONF_COUNTER,
RESRC_CONF_TEMPERATURE_THRESHOLD,
RESRC_CONF_TEMPERATURE_DETECTION,
];
const allowedConfigParameters = [
RESRC_CONF_REPORTING_INTERVAL,
RESRC_CONF_STATUS_INTERVAL,
RESRC_CONF_COUNTER,
RESRC_CONF_TEMPERATURE_THRESHOLD,
RESRC_CONF_TEMPERATURE_DETECTION,
RESRC_CONF_REPORTING_INTERVAL_NEW
];
const allowedExecParameters = [
RESRC_UPDATE,
];
const TEMPERATURE_DETECTION_MODE_BIT = 2;
const TEMPERATURE_DETECTION_STATUS_BIT = 2;
const USER_TRIGGERED_STATUS_BIT = 6;
const IS_ALERT_STATUS_BIT = 7;
const CONFIG_REPORTING_INTERVAL = 0x01;
const CONFIG_STATUS_INTERVAL = 0x04;
const CONFIG_COUNTER = 0x08;
const CONFIG_TEMPERATURE_THRESHOLD = 0x10;
const CONFIG_FUNCTIONS = 0x80;
const CONFIG_FUNCTION_TEMPERATURE_DETECTION_ON = 0x04;
const CONFIG_FUNCTION_TEMPERATURE_DETECTION_OFF = 0x00;
const MAX_PAYLOADSIZE = 16;
const LITTLE_ENDIAN = true;
var notify=true;
this.decode = function (decodeCtx) {
var json = JSON.parse(decodeCtx.getRequest());
console.log('Incoming payload ' + JSON.stringify(json));
if(json.hasOwnProperty('notify2')){
var notify2 = json.notify2[0];
//console.log('notify2 '+JSON.stringify(notify2));
//console.log('data '+notify2["uplinkMsg/0/meta"]["params"]["payload"]);
var encrypteddata=notify2["uplinkMsg/0/meta"]["params"]["payload"]//JSON.parse(JSON.stringify(JSON.parse(notify2).uplink)).data;
var port = notify2["uplinkMsg/0/meta"]["params"]["port"]
console.log('port ' +port);
console.log('data ', +encrypteddata);
if (port == undefined || encrypteddata == undefined) {
throw new Error(`payload should have fPort and data`);
}
var data = Base64.getDecoder().decode(encrypteddata);
var arrBuffer = new Uint8Array(data).buffer;
var view = new DataView(arrBuffer);
var resources = checkPort(port);
return notify? formImpactNotifyFromJson(decodeCtx, resources) : formImpactResponseFromJson(decodeCtx, resources);
}else{
console.log('Incoming payload is a readResponse');
const myObj = JSON.parse(decodeCtx.getRequest())["response"][0]["uplinkMsg/0/meta"];
console.log('Incoming payload meta data '+JSON.stringify(myObj));
var encrypteddata = myObj["params"]["payload"];
var port = myObj["params"]["port"];
console.log(' Encrypted payload : '+encrypteddata);
console.log(' Port : '+port);
if (port == undefined || encrypteddata == undefined) {
throw new Error(`payload should have fPort and data`);
}
var data = Base64.getDecoder().decode(encrypteddata);
var arrBuffer = new Uint8Array(data).buffer;
var view = new DataView(arrBuffer);
var response = checkPort(port)//parseUsageStatusMsg(view,0,arrBuffer.byteLength);
json["response"][0]["uplinkMsg/0/meta"] = response;
console.log('Response after encryption: '+JSON.stringify(response));
console.log('Final response payload: '+JSON.stringify(json));
return utils.createJsonResponse(JSON.stringify(json));
}
}
function checkPort(port){
console.log("Port : ", port);
var res = []
switch (port) {
case 14: // fPort 14 Usage Message
res = parseUsageMsg(view, 0, arrBuffer.byteLength);
break;
case 24: // fPort 24 Status Message
res = parseUsageStatusMsg(view, 0, arrBuffer.byteLength);
break;
case 50: // fPort 50 Configuration Message ( only 1 parameter can be configured at once )
notify=false;
res = parseConfigMsg(view, 0, arrBuffer.byteLength);
break;
case 99: // fPort99 Boot/Debug message
res = parseBootDebugMsg(view, 0, arrBuffer.byteLength,decodeCtx.getClient());
break;
default:
throw new Error("invalid message with port ", port);
}
return res;
}
function parseBootDebugMsg(view, offset, len, serial) {
var header = view.getUint8(offset);
if (header == 0x00) {
return parseBootMsg(view, offset, len, serial);
} else if (header == 0x01) {
return parseShutdownMsg(view, offset, len, serial);
} else {
throw new Error(`boot message with invalid header`);
}
}
function parseUsageStatusMsg(view, offset, len) {
const expectedDataLen = 9;
console.log('###### Incoming payload length: '+len);
var resp = [];
resp = parseUsageMsg(view, offset, 4);
var batteryVoltage = view.getUint8(offset+4) * 0.016; // mV/16
resp.push({name: RESRC_STATUS_BATTERY, value: batteryVoltage});
var temperature = view.getUint8(offset+5); // °C
resp.push({name: RESRC_STATUS_TEMPERATURE, value: temperature})
var mode = view.getUint8(offset+7);
var status = view.getUint8(offset+8);
if(isBitSet(mode, TEMPERATURE_DETECTION_MODE_BIT)){
const tempDetectionMode = isBitSet(mode, TEMPERATURE_DETECTION_MODE_BIT) ? "on" : "off";
resp.push({name: RESRC_STATUS_TEMPERATURE_DETECTION_MODE, value: tempDetectionMode});
}
if(isBitSet(status, TEMPERATURE_DETECTION_STATUS_BIT)){
const tempDetectionStatus = isBitSet(status, TEMPERATURE_DETECTION_STATUS_BIT) ? "alert": "ok";
resp.push({name: RESRC_STATUS_TEMPERATURE_DETECTION_STATUS, value: tempDetectionStatus});
}
if(isBitSet(status, USER_TRIGGERED_STATUS_BIT)){
const userTriggeredStatus = isBitSet(status, USER_TRIGGERED_STATUS_BIT) ? true: false;
resp.push({name: RESRC_STATUS_USER_TRIGGERED_STATUS, value: userTriggeredStatus});
}
if(isBitSet(status, IS_ALERT_STATUS_BIT)){
const isAlertStatus = isBitSet(status, IS_ALERT_STATUS_BIT) ? true: false;
resp.push({name: RESRC_STATUS_IS_ALERT_STATUS, value: isAlertStatus});
}
return resp;
}
function parseUsageMsg(view, offset, len) {
const expectedDataLen = 4;
if (len != expectedDataLen) {
throw new Error(`data length of meter usage message should be ${expectedDataLen}`);
}
var usageCounter = view.getUint32(offset, LITTLE_ENDIAN);
return [ { name: RESRC_USAGE, value: usageCounter } ];
}
function parseBootMsg(view, offset, len, serialnumber) {
const expectedDataLen = 9;
if (len != expectedDataLen) {
throw new Error(`data length of boot message should be ${expectedDataLen}`);
}
var serial = view.getUint32(offset+1, LITTLE_ENDIAN);
var fwMajor = view.getUint8(offset+5);
var fwMinor = view.getUint8(offset+6);
var fwPatch = view.getUint8(offset+7);
var reasonCode = view.getUint8(offset+8);
var resetReason;
let keys = Array.from(resetReasons.keys());
if (keys.indexOf(reasonCode) == -1) {
throw new Error(`boot message with invalid reset reason`);
} else {
resetReason = resetReasons.get(reasonCode);
}
var serialnumber = serialnumber.replace('\"','').replace('\"','');
var resp = [{ name: RESRC_BOOT_REASON, value: resetReason },
{ name: RESRC_BOOT_SERIAL, value: serial },
{ name: RESRC_BOOT_FIRMWARE, value: `${fwMajor}.${fwMinor}.${fwPatch}` }];
console.log('returning respnew' + serialnumber);
var respnew = [{ resource: RESRC_BOOT_REASON, value: resetReason, "device/0/endPointClientName": serialnumber},
{ resource: RESRC_BOOT_SERIAL, value: serial, "device/0/endPointClientName": serialnumber},
{ resource: RESRC_BOOT_FIRMWARE, value: `${fwMajor}.${fwMinor}.${fwPatch}`, "device/0/endPointClientName": serialnumber}];
console.log('returning respnew' + JSON.stringify(respnew));
return respnew;
}
function isBitSet(byte, bitNum) {
return ((byte >> bitNum) & 0xFF) == 0x01;
}
function formImpactNotifyFromJson(decodeCtx, resources) {
console.log(' AAAAAAAAAA ' +decodeCtx.getClient());
//var resourceObj = "device/0/endPointClientName":decodeCtx.getClient();
//resources.push(resourceObj);
console.log('sending notify response ' + JSON.stringify({details: {"notify2":resources}}));
return utils.createJsonResponse(JSON.stringify({details: {"notify2":resources}}));
}
function formImpactResponseFromJson(decodeCtx, resources, success=true) {
if(decodeCtx.getRequest() != null) {
var corrId = decodeCtx.getRequest().getCorrelatorId();
var resp = {responseCode: success? 0: 1, requestStatus: 0, response: [], correlatorId: corrId};
var resourceObj = {"device/0/endPointClientName":decodeCtx.getClient()};
resources.forEach(resource => {
resourceObj[resource.name] = resource.value;
});
resp.response.push(resourceObj);
console.log('sending read response ' + JSON.stringify(resp));
return utils.createJsonResponse(JSON.stringify(resp));
} else {
throw new Error (`no corresponding request found`);
}
}
this.encode = function (encodeCtx) {
///var json = JSON.parse(encodeCtx.getRequest());
console.log('########## Incoming payload ' + encodeCtx);
var request = getImpactRequestAsJson(encodeCtx.getRequest());
var arrBuffer = new ArrayBuffer(MAX_PAYLOADSIZE);
var view = new DataView(arrBuffer);
var dataLen = 0;
var fPort;
var unack=true;
var b64Str=null;
var responseValue = null;
switch (request.type) {
case "READ":
unack = false;
if (allowedReadParameters.indexOf(request.resource) == -1) {
throw new Error (`read parameter should be one of these ${readParameters}`);
}
fPort = fPorts.get("Configuration");
dataLen = compileReadConfigResource(view, 0, request)
if (dataLen > 0) {
var bytes = new Int8Array(arrBuffer.slice(0, dataLen));
b64Str = Base64.getEncoder().encodeToString(bytes);
}
responseValue = {"details": {"resource":b64Str}};
break;
case "WRITE":
if (allowedConfigParameters.indexOf(request.resource) == -1) {
throw new Error (`config parameter should be one of these ${configParameters}`);
}
fPort = fPorts.get("Configuration");
dataLen = compileConfigResource(view, 0, request);
if (dataLen > 0) {
var bytes = new Int8Array(arrBuffer.slice(0, dataLen));
b64Str = Base64.getEncoder().encodeToString(bytes);
}
responseValue = {"details": {"resource":"DownlinkMsg/0/data", "value":b64Str}};
break;
default:
throw new Error(`request type ${request.type} not supported by the lora pulse device`);
break;
}
console.log("downlink:", fPort, b64Str);
console.log(" ?????? ", JSON.stringify({fPort, data: b64Str}));
return utils.createJsonResponse(JSON.stringify(responseValue));
}
function getImpactRequestAsJson(impactRequest) {
console.log("request encoder", impactRequest);
var request = {};
request.type = utils.getRequestType(impactRequest);
request.url = impactRequest.getResource();
impactRequest.set
request.correlator = impactRequest.getCorrelatorId();
if (!request.type || !request.url) {
throw new Error(`request type and resource URL are required in the input`);
}
var resourceItems = request.url.split("/");
request.resource = resourceItems[resourceItems.length - 1];
if (impactRequest.getValue) {
request.resourceValue = impactRequest.getValue();
}
console.log(" response encoder", JSON.stringify(request));
return request;
}
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 compileConfigResource(view, offset, request) {
console.log(" request data is ", JSON.stringify(request));
if (request.resource == RESRC_CONF_REPORTING_INTERVAL) {
if ((request.resourceValue < 600 && request.resourceValue != 0) || request.resourceValue > 86400) {
throw new Error(`value of reporting interval outside the expected range [0, 600 - 86400]`);
}
view.setUint8(offset, CONFIG_REPORTING_INTERVAL); offset++;
view.setUint32(offset, request.resourceValue, LITTLE_ENDIAN); offset += 4;
} else if (request.resource == RESRC_CONF_STATUS_INTERVAL) {
if ((request.resourceValue < 900 && request.resourceValue != 0) || request.resourceValue > 86400) {
throw new Error(`value of status interval outside the expected range [0, 600 - 86400]`);
}
view.setUint8(offset, CONFIG_STATUS_INTERVAL); offset++;
view.setUint32(offset, request.resourceValue, LITTLE_ENDIAN); offset += 4;
} else if (request.resource == RESRC_CONF_COUNTER) {
view.setUint8(offset, CONFIG_COUNTER); offset++;
view.setUint32(offset, request.resourceValue, LITTLE_ENDIAN); offset += 4;
} else if (request.resource == RESRC_CONF_TEMPERATURE_THRESHOLD) {
if (request.resourceValue > 255) {
throw new Error(`value of temperature threshold outside the expected range`);
}
view.setUint8(offset, CONFIG_TEMPERATURE_THRESHOLD); offset++;
view.setUint8(offset, request.resourceValue); offset++;
} else if (request.resource == RESRC_CONF_TEMPERATURE_DETECTION) {
if ([0, 1].indexOf(request.resourceValue) == -1 ) {
throw new Error(`value of temperature detection should be on of these [0, 1]`);
}
view.setUint8(offset, CONFIG_FUNCTIONS); offset++;
view.setUint8(offset, request.resourceValue ? CONFIG_FUNCTION_TEMPERATURE_DETECTION_ON : CONFIG_FUNCTION_TEMPERATURE_DETECTION_OFF ); offset++;
}
return offset;
}
function parseConfigMsg(view, offset, len) {
if (len != 2 && len != 5) {
throw new Error(`data length of configuration message should be 2 or 5`);
}
var hdr = view.getUint8(offset); offset++;
if ((hdr & CONFIG_REPORTING_INTERVAL) == CONFIG_REPORTING_INTERVAL) {
name = RESRC_CONF_REPORTING_INTERVAL;
value = view.getUint32(offset, LITTLE_ENDIAN);
} else if ((hdr & CONFIG_STATUS_INTERVAL) == CONFIG_STATUS_INTERVAL) {
name = RESRC_CONF_STATUS_INTERVAL;
value = view.getUint32(offset, LITTLE_ENDIAN);
} else if ((hdr & CONFIG_COUNTER) == CONFIG_COUNTER) {
name = RESRC_CONF_COUNTER;
value = view.getUint32(offset, LITTLE_ENDIAN);
} else if ((hdr & CONFIG_TEMPERATURE_THRESHOLD) == CONFIG_TEMPERATURE_THRESHOLD) {
name = RESRC_CONF_TEMPERATURE_THRESHOLD;
value = view.getUint8(offset);
} else if ((hdr & CONFIG_FUNCTIONS) == CONFIG_FUNCTIONS) {
name = RESRC_CONF_TEMPERATURE_DETECTION;
value = (view.getUint8(offset) & CONFIG_FUNCTION_TEMPERATURE_DETECTION_ON == CONFIG_FUNCTION_TEMPERATURE_DETECTION_ON) ? "on" : "off";
} else {
throw new Error(`unknown config parameter`);
}
return [ { name, value } ];
}
}
var codec = new LoraCodec();
(codec);