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