lora_cyble_b64_codec.js script
The following is an example script which encodes or decodes a Base64 data format. 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 LoraCybleCodec() {
const useJson = true;
const RESRC_USAGE = "Usage";
const RESRC_STATUS = "Status";
const RESRC_STATUS_BATTERY = "BatteryVoltage";
const RESRC_STATUS_TEMPERATURE = "Temperature";
const RESRC_STATUS_TEMPERATURE_DETECTION_MODE = "TemperatureDetectionMode";
const RESRC_STATUS_TEMPERATURE_DETECTION_STATUS = "TemperatureDetectionStatus";
const RESRC_STATUS_USER_TRIGGERED_STATUS = "UserTriggeredStatus";
const RESRC_STATUS_IS_ALERT_STATUS = "IsAlertStatus";
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_BOOT = "Boot";
const RESRC_BOOT_REASON = "BootReason";
const RESRC_BOOT_SERIAL = "Serial";
const RESRC_BOOT_FIRMWARE = "Firmware";
const RESRC_SHUTDOWN = "Shutdown";
const RESRC_SHUTDOWN_REASON = "ShutdownReason";
const RESRC_UPDATE = "Update";
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
];
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;
///////////////////////////////
// Encoder function
this.encode = function (impactRequest) {
var request = getImpactRequestAsJson(impactRequest);
var arrBuffer = new ArrayBuffer(MAX_PAYLOADSIZE);
var view = new DataView(arrBuffer);
var dataLen = 0;
var fPort;
var unack=true;
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)
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);
break;
case "EXECUTE":
unack = true;
if (allowedExecParameters.indexOf(request.resource) == -1) {
throw new Error (`exec parameter should be one of these ${configParameters}`);
}
fPort = fPorts.get(request.resource);
dataLen = compileUpdateResource(view, 0, request);
break;
case "OBSERVE":
case "DELETE":
default:
throw new Error(`request type ${request.type} not supported by the lora pulse device`);
break;
}
var b64Str=null;
if (dataLen > 0) {
var bytes = new Int8Array(arrBuffer.slice(0, dataLen));
b64Str = Base64.getEncoder().encodeToString(bytes);
}
console.log("downlink:", fPort, b64Str);
return utils.createBinaryResponse(string2Bytes(JSON.stringify({fPort, data: b64Str})), _map, unack);
}
// private functions used by this.encode()
function compileConfigResource(view, offset, 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 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();
var text = bytes2String(bytes);
var json = JSON.parse(text);
if (json.fPort == undefined || json.data == undefined) {
throw new Error(`payload should have fPort and data`);
}
console.log("input to decoder", text);
var data = Base64.getDecoder().decode(json.data);
var arrBuffer = new Uint8Array(data).buffer;
var view = new DataView(arrBuffer);
var resources = [];
var notify=true;
switch (json.fPort) {
case 14: // fPort 14 Usage Message
resources = parseUsageMsg(view, 0, arrBuffer.byteLength);
break;
case 24: // fPort 24 Status Message
resources = parseUsageStatusMsg(view, 0, arrBuffer.byteLength);
break;
case 99: // fPort99 Boot/Debug message
resources = parseBootDebugMsg(view, 0, arrBuffer.byteLength);
break;
case 50: // fPort 50 Configuration Message ( only 1 parameter can be configured at once )
notify=false;
resources = parseConfigMsg(view, 0, arrBuffer.byteLength);
break;
default:
throw new Error("invalid message with fport ", json.fPort);
}
console.log("response", JSON.stringify(resources));
return notify? formImpactNotifyFromJson(decodeCtx, resources) : formImpactResponseFromJson(decodeCtx, resources);
}
// private functions used by this.decode()
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 parseUsageStatusMsg(view, offset, len) {
const expectedDataLen = 9;
if (len != expectedDataLen) {
throw new Error(`data length of status message should be ${expectedDataLen} received $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);
const tempDetectionMode = isBitSet(mode, TEMPERATURE_DETECTION_MODE_BIT) ? "on" : "off";
resp.push({name: RESRC_STATUS_TEMPERATURE_DETECTION_MODE, value: tempDetectionMode});
const tempDetectionStatus = isBitSet(status, TEMPERATURE_DETECTION_STATUS_BIT) ? "alert": "ok";
resp.push({name: RESRC_STATUS_TEMPERATURE_DETECTION_STATUS, value: tempDetectionStatus});
const userTriggeredStatus = isBitSet(status, USER_TRIGGERED_STATUS_BIT) ? true: false;
resp.push({name: RESRC_STATUS_USER_TRIGGERED_STATUS, value: userTriggeredStatus});
const isAlertStatus = isBitSet(status, IS_ALERT_STATUS_BIT) ? true: false;
resp.push({name: RESRC_STATUS_IS_ALERT_STATUS, value: isAlertStatus});
return resp;
}
function parseBootDebugMsg(view, offset, len) {
var header = view.getUint8(offset);
if (header == 0x00) {
return parseBootMsg(view, offset, len);
} else if (header == 0x01) {
return parseShutdownMsg(view, offset, len);
} else {
throw new Error(`boot message with invalid header`);
}
}
function parseBootMsg(view, offset, len) {
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 resp = [{ name: RESRC_BOOT_REASON, value: resetReason },
{ name: RESRC_BOOT_SERIAL, value: serial },
{ name: RESRC_BOOT_FIRMWARE, value: `${fwMajor}.${fwMinor}.${fwPatch}` }];
return resp;
}
function parseShutdownMsg(view, offset, len) {
const expectedDataLen = 11;
if (len != expectedDataLen) {
throw new Error(`data length of shutdown message should be ${expectedDataLen}`);
}
var reasonCode = view.getUint8(offset+1);
var reason;
let keys = Array.from(resetReasons.keys());
if (keys.indexOf(reasonCode) == -1) {
throw new Error(`shutdown message with invalid reason`);
} else {
reason = resetReasons.get(reasonCode);
}
var resp = parseUsageStatusMsg(view, offset+2, len-2);
resp.push({ name: RESRC_SHUTDOWN_REASON, value: reason });
return resp;
}
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 LoraCybleCodec();
(codec);