Source: decoding/lora-serialization.js

'use strict';

/**
 * This decoding can decode payloads constructed with {@link https://github.com/thesolarnomad/lora-serialization|lora-serialization}.
 * @module decoding/lora-serialization
 * @license MIT
 */

const loraSerialization = require('lora-serialization').decoder,
  { findSensorIds } = require('./helpers');

/**
 * returns a bufferTransfomer for transformation of a buffer to measurements.
 * The box is required to have a an array of ttn.decodeOptions
 * @see module:decoding~bufferToMeasurements
 * @param {Box} box - The box to retrieve decodeOptions and sensorIds from
 * @return {Array} A bufferTransformer for the box
 * @example <caption>decodeOptions format</caption>
 * ttn: {
 *   profile: 'lora-serialization',
 *   decodeOptions: [{
 *     sensor_id: '588876b67dd004f79259bd8a',
 *     decoder: 'temperature' // one of [temperature, humidity, uint8, uint16]
 *   }, {
 *     // sensor_type, sensor_title, sensor_unit is allowed as well
 *     sensor_type: '588876b67dd004f79259bd8b',
 *     decoder: 'VEML6070'
 *   }]
 * }
 */
const createBufferTransformer = function createBufferTransformer (box) {
  const byteMask = box.integrations.ttn.decodeOptions,
    bufferTransf = [],
    sensorMatchings = [];

  if (!byteMask || byteMask.constructor !== Array) {
    throw new Error('profile \'lora-serialization\' requires valid decodeOptions');
  }

  let expectedSensorCount = byteMask.length;

  // construct sensorMatchings to find the correct sensorIds
  for (const el of byteMask) {
    const match = {};
    if (el.sensor_id) {
      match['_id'] = [el.sensor_id];
    }
    if (el.sensor_title) {
      match['title'] = [el.sensor_title];
    }
    if (el.sensor_type) {
      match['sensorType'] = [el.sensor_type];
    }
    if (el.sensor_unit) {
      match['unit'] = [el.sensor_unit];
    }

    // exception for unixtime decoder, as its result will not be a
    // measurement for a sensor but used as timestamp for all measurements
    if (el.decoder === 'unixtime') {
      expectedSensorCount--;
    } else if (!Object.keys(match).length) {
      throw new Error('invalid decodeOptions. requires at least one of [sensor_id, sensor_title, sensor_type]');
    }

    sensorMatchings.push(match);
  }

  const sensorIds = findSensorIds(box.sensors, sensorMatchings);

  if (Object.keys(sensorIds).length !== expectedSensorCount) {
    throw new Error('box does not contain sensors mentioned in byteMask');
  }

  // create the transformer elements for each measurement.
  // use a separate counter for sensorIds, b/c unixtime decoders
  // have no entry in sensorIds!
  for (let i = 0, processedSensorIds = 0; i < byteMask.length; i++) {
    const transformer = loraSerialization[byteMask[i].decoder];

    if (
      typeof transformer !== 'function' ||
      ['decode', 'latLng'].includes(byteMask[i].decoder) // function blacklist
    ) {
      throw new Error(`'${byteMask[i].decoder}' is not a supported transformer`);
    }

    const mask = {
      transformer,
      bytes: transformer.BYTES,
    };

    // if a unixtime decoder is provided, use its value as
    // timestamp for all measurements via the onResult hook.
    if (byteMask[i].decoder === 'unixtime') {
      mask.sensorId = 'MEASURE_TIMESTAMP';
      mask.onResult = applyTimestamps;
    } else {
      mask.sensorId = sensorIds[processedSensorIds++][i];
    }

    bufferTransf.push(mask);
  }

  return bufferTransf;
};

/**
 * transforms the measurements as onResult hook of the
 * unixtime decoder IN PLACE
 * @param {Array} measurements The generated measurements of the full payload
 *                             prior to validation.
 * @private
 */
const applyTimestamps = function applyTimestamps (measurements) {
  let timestamp;

  // find "measurement" from the unixtime decoder
  // and discard it.
  for (let k = 0; k < measurements.length; k++) {
    if (measurements[k].sensor_id === 'MEASURE_TIMESTAMP') {
      timestamp = new Date(measurements[k].value * 1000).toISOString();
      measurements.splice(k, 1);
      break;
    }
  }

  // apply the value to all remaining measurements
  for (const m of measurements) {
    m.createdAt = timestamp;
  }
};

module.exports = {
  createBufferTransformer
};