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