lora_pulse_hex_codec.js script

The following is an example script which encodes or decodes a Hexa decimal data format. This is an example implementation of https://www.bmeters.com/wp-content/uploads/2020/06/Manual_LORA_PULSE_1.1.pdf.

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

function LoraPulseCodec() {

	const downlinkTypes = new Map([
			["set", 0x01],
			["query", 0x02],
			["action", 0x03]
	]);
	const setCmdIndices = new Map([
		["reportingInterval", 0x26],
		["iwmSetCounter", 0x2C],
		["iwmResetStatus", 0x2D],
		["iwmPulseRatio", 0x2E],
	]);
	const queryCmdIndices = new Map([
		["firmwareVersion", 0x03],
		["cpuVoltage", 0x06],
		["cpuTemperature", 0x0A],
		["applicationType", 0x25],
		["reportingInterval", 0x26],
		["iwmCounterStatus", 0x2B],
		["iwmPulseRatio", 0x2E],
	]);
	const actionCmdIndices = new Map([
		["deviceReset", 0x05],
	]);
	const hdrLen = 2;
	const STATUS_RESET_DETECTED = 0x01;
	const STATUS_FRAUD_DETECTED = 0x02;
	const MAX_PAYLOADSIZE = 16;

	const uplinkTypes = [ 0x01, 0x02 ];

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

	this.encode = function (impactRequest) {
        var request = getImpactRequestAsJson(impactRequest);

	    var type=null;
	    var unack=true;
        switch (request.type) {
	        case "READ":
	            unack = false;
	            type = "query";
	            break;
	        case "WRITE":
	            type = "set";
                if (request.resourceValue == undefined) {
                    throw new Error(`value not given for request type ${request.type}`);
                }
	            break;
	        case "EXECUTE":
	            type = "action";
	            break;
	        case "OBSERVE":
	        case "DELETE":
	        default:
	            throw new Error(`request type ${request.type} not supported by the lora pulse device`);
	    }

		var json = { type, index: request.resource };
		if (type == "set") {
            json.data = request.resourceValue;
		}

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

		let keys = Array.from(downlinkTypes.keys());
		if (keys.indexOf(json.type) == -1) {
			throw new Error (`type should be one of these ${keys}`);
		}

		view.setUint8(0, downlinkTypes.get(json.type));

		var index = 0x00;

		if (json.type == "set") {
			let keys = Array.from(setCmdIndices.keys());
			if (keys.indexOf(json.index) == -1) {
				throw new Error (`invalid ${json.index} ! index for set type should be one of these ${keys}`);
			}

			index = setCmdIndices.get(json.index);

		} else if (json.type == "query") {
			let keys = Array.from(queryCmdIndices.keys());
			if (keys.indexOf(json.index) == -1) {
				throw new Error (`invalid ${json.index} ! index for query type should be one of these ${keys}`);
			}

			index = queryCmdIndices.get(json.index);

		} else /* if (json.type == "action") */ {
			let keys = Array.from(actionCmdIndices.keys());
			if (keys.indexOf(json.index) == -1) {
				throw new Error (`invalid ${json.index} ! index for action type should be one of these ${keys}`);
			}

			index = actionCmdIndices.get(json.index);
		}

		view.setUint8(1, index);

		var dataLen = 0;
		if (json.type == "set") {
			switch (index) {
				case 0x26:
					dataLen = compile_reporting_interval(view, json.data);
					break;
				case 0x2C:
					dataLen = compile_iwm_set_counter(view, json.data);
					break;
				case 0x2D:
					dataLen = compile_iwm_reset_status(view, json.data);
					break;
				case 0x2E:
					dataLen = compile_iwm_pulse_ratio(view, json.data);
					break;
				default:
					throw new Error (`bug in program! precheck not properly done.`);
			}
		}

		var resBuffer = new Int8Array(arrBuffer.slice(0, hdrLen + dataLen));

		console.log("downlink:", buf2hex(resBuffer));

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

    // private functions used by this.encode()
 
	function compile_reporting_interval(view, minutes) {

		const dataLen = 2;

		if (minutes == undefined) {
			throw new Error ("set reporting interval requires minutes value");
		}

		// range: 1 to 10080 minutes
		if (minutes < 1 || minutes > 10080) {
			throw new Error("reporting interval is outside the expected range of 1 - 10080 minutes");
		}

		view.setUint16(hdrLen, minutes) ;

		return dataLen;
	}

	function compile_iwm_set_counter(view, counter) {

		const dataLen = 4;

		if (counter == undefined) {
			throw new Error ("set counter requires counter value");
		}

		// range: 0 to 4294967295
		if (counter > 0xFFFFFFFF) {
			throw new Error("counter value is outside the expected range of 0 - 4294967295");
		}

		view.setUint32(hdrLen, counter) ;

		return dataLen;
	}

	function compile_iwm_reset_status(view, status) {

		const dataLen = 1;

		if (status == undefined) {
			throw new Error ("set status requires status value { bit0 = resetDetected, bit1 = fraudDetected }");
		}

		if (status > 0x03) {
		    throw new Error ("set status value should be one of these [0,1,2]");
		}

		view.setUint8(hdrLen, 0x00) ; // bit value=0 means reset

		return dataLen;
	}

	function compile_iwm_pulse_ratio(view, pulseRatio) {

		const dataLen = 2;

		if (pulseRatio == undefined) {
			throw new Error ("set pulse ratio requires pulseRatio value");
		}

		// range: 1 - 10000 Liters/pulse
		if (pulseRatio < 1 || pulseRatio > 10000) {
			throw new Error("pulse ratio is outside the expected range of 1 - 10000 Liters/pulse");
		}

		view.setUint16(hdrLen, pulseRatio) ;

		return dataLen;
	}

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

	this.decode = function (decodeCtx) {

	    var bytes = decodeCtx.getUplinkMessage();

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

        var arrBuffer = new Uint8Array(bytes).buffer;

		if (arrBuffer.byteLength < hdrLen) {
			throw new Error(`payload should be atleast ${hdrLen} bytes`);
		}

		var view = new DataView(arrBuffer);

		var type = view.getUint8(0);
		var index = view.getUint8(1);

		if (uplinkTypes.indexOf(type) == -1) {
			throw new Error(`uplink types should be one of these ${uplinkTypes}, received ${type}`);
		}

        var resources = [];
        var notify=false;
		switch (index) {
			case 0x03:
			    resources = parse_firmware_version(type, view, arrBuffer.byteLength);
				break;
			case 0x06:
			    resources = parse_cpu_voltage(type, view, arrBuffer.byteLength);
				break;
			case 0x0A:
			    resources = parse_cpu_temperature(type, view, arrBuffer.byteLength);
				break;
			case 0x25:
			    resources = parse_application_type(type, view, arrBuffer.byteLength);
				break;
			case 0x26:
			    resources = parse_reporting_interval(type, view, arrBuffer.byteLength);
				break;
			case 0x2B:
			    notify=null; // we not sure if it is read-response or notify
				resources = parse_iwm_datacounter_status(type, view, arrBuffer.byteLength);
				break;
			case 0x2E:
			    resources = parse_iwm_pulse_ratio(type, view, arrBuffer.byteLength);
				break;
			default:
				throw new Error(`invalid command index ${index}`);
		}

        // type 0x01 is data and 0x02 is nack
        console.log("response", type == 0x01, JSON.stringify(resources));

        return (notify == false)? formImpactResponseFromJson(decodeCtx, resources, type == 0x01) : formImpactResponseOrNotifyFromJson(decodeCtx, resources, type == 0x01);
	}

	function parse_firmware_version(type, view, len) {
		const expectedDataLen = 6;
		const index = "firmwareVersion";

		if (type == 0x02) { // nack
			return [{ name: index }];
		}

		if (len != hdrLen+expectedDataLen) {
			throw new Error(`data length of firmware version should be ${expectedDataLen}`);
		}

		var major = view.getUint16(hdrLen);
		var minor = view.getUint16(hdrLen+2);
		var patch = view.getUint16(hdrLen+4);

		return [{ name: index, value: `${major}.${minor}.${patch}` }];
	}

	function parse_cpu_voltage(type, view, len) {
		const expectedDataLen = 1;
		const index = "cpuVoltage";

		if (type == 0x02) { // nack
			return [{ name: index }];
		}


		if (len != hdrLen+expectedDataLen) {
			throw new Error(`data length of CPU voltage should be ${expectedDataLen}`);
		}

		// voltage encoding: 25mV/LSB
		// range: 0-3.6V
		var voltage = view.getUint8(hdrLen) * 0.025;

		if (voltage > 3.6) {
			throw new Error("CPU voltage is outside the expected range of 0-3.6V");
		}

		return [{ name: index, value: voltage }];
	}

	function parse_cpu_temperature(type, view, len) {
		const expectedDataLen = 2;
		const index = "cpuTemperature";

		if (type == 0x02) { // nack
			return [{ name: index }];
		}


		if (len != hdrLen+expectedDataLen) {
			throw new Error(`data length of CPU temperature should be ${expectedDataLen}`);
		}

		// temperature encoding: 0.01C/LSB
		// range: -15 to 125 C
		var temperature = view.getInt16(hdrLen) * 0.01;

		if (temperature < -15 || temperature > 125) {
			throw new Error("CPU temperature is outside the expected range of [-15, +125] C");
		}

		return [{ name: index, value: temperature }];
	}

	function parse_application_type(type, view, len) {
		const expectedDataLen = 1;
		const index = "applicationType";

		if (type == 0x02) { // nack
			return [{ name: index }];
		}


		if (len != hdrLen+expectedDataLen) {
			throw new Error(`data length of application type ${expectedDataLen}`);
		}

		var appType = view.getUint8(hdrLen) ;

		if ([0,1].indexOf(appType) == -1) {
			throw new Error(`unknown application type ${appType}`);
		}

		return [{ name: index, value: appType == 0 ? "standard": "LoV" }];
	}

	function parse_reporting_interval(type, view, len) {
		const expectedDataLen = 2;
		const index = "reportingInterval";

		if (type == 0x02) { // nack
			return [{ name: index }];
		}


		if (len != hdrLen+expectedDataLen) {
			throw new Error(`data length of reporting interval ${expectedDataLen}`);
		}

		var minutes = view.getUint16(hdrLen) ;

		// range: 1 to 10080 minutes
		if (minutes < 1 || minutes > 10080) {
			throw new Error("porting interval is outside the expected range of 1 - 10080 minutes");
		}

		return [{ name: index, value: minutes }];
	}

	function parse_iwm_datacounter_status(type, view, len) {
		const expectedDataLen = 5;
		const index = "iwmCounterStatus";

		if (type == 0x02) { // nack
			return [{ name: index }];
		}

		if (len != hdrLen+expectedDataLen) {
			throw new Error(`data length of IWM data counter and status ${expectedDataLen}`);
		}

		var counter = view.getUint32(hdrLen) ;
		var status = view.getUint8(hdrLen+4) ;

		if (status > 0x03) {
			throw new Error("reserved bits in status should not be set");
		}

		var resp = [{ name: index, value: counter }];

		if ((status & STATUS_RESET_DETECTED) == STATUS_RESET_DETECTED) {
		    resp.push({name: "resetDetected", value: true});
		}

        if ((status & STATUS_FRAUD_DETECTED) == STATUS_FRAUD_DETECTED) {
		    resp.push({name: "fraudDetected", value: true});
		}

		return resp;
	}

	function parse_iwm_pulse_ratio(type, view, len) {
		const expectedDataLen = 2;
		const index = "iwmPulseRatio";

		if (type == 0x02) { // nack
			return [{ name: index }];
		}


		if (len != hdrLen+expectedDataLen) {
			throw new Error(`data length of IWM data pulse ratio ${expectedDataLen}`);
		}

		var ratio = view.getUint16(hdrLen) ;

		// range: 1 - 10000 Liters/pulse
		if (ratio < 1 || ratio > 10000) {
			throw new Error("porting interval is outside the expected range of 1 - 10000 Liters/pulse");
		}

		return [{ name: index, value: ratio }];
	}

}

var codec = new LoraPulseCodec();

(codec);