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);