Source: decoding/index.js

'use strict';

/**
 * Provides a generic decoding interface into which multiple decoding profiles
 * may be hooked. Implemented profiles are
 * {@link module:decoding/lora-serialization|lora-serialization},
 * {@link module:decoding/sensebox_home|sensebox/home},
 * {@link module:decoding/debug|debug}
 * @module decoding
 * @license MIT
 */

const { transformAndValidateArray } = require('openSenseMapAPI').decoding,
  profileDebug = require('./debug'),
  profileLoraserial = require('./lora-serialization'),
  profileSenseboxhome = require('./sensebox_home');

const profiles = {
  'debug': profileDebug,
  'lora-serialization': profileLoraserial,
  'sensebox/home': profileSenseboxhome
};

/**
 * Decodes a Buffer to an array of measurements according to a bufferTransformer
 * @private
 * @param {Buffer} buffer - the data to decode
 * @param {Array} bufferTransformer - defines how the data is transformed.
 *        Each element specifies a transformation for one measurement.
 * @example <caption>Interface of bufferTransformer elements</caption>
 * {
 *   bytes: Number,        // amount of bytes to consume for this measurement
 *   sensorId: String,     // corresponding sensor_id for this measurement
 *   transformer: Function // function that accepts an Array of bytes and
 *                         // returns the measurement value
 *   onResult: Function    // hook that recieves the all decoded measurements
 *                         // once decoding has finished. may modify the
 *                         // measurement array.
 * }
 * @return {Array} decoded measurements
 */
const bufferToMeasurements = function bufferToMeasurements (buffer, bufferTransformer) {
  const result = [];
  let maskLength = 0, currByte = 0;

  // check mask- & buffer-length
  for (const mask of bufferTransformer) {
    maskLength = maskLength + mask.bytes;
  }

  if (maskLength !== buffer.length) {
    throw new Error(`incorrect amount of bytes, should be ${maskLength}`);
  }

  // feed each bufferTransformer element
  for (const mask of bufferTransformer) {
    const maskedBytes = buffer.slice(currByte, currByte + mask.bytes);

    result.push({
      sensor_id: mask.sensorId,
      value: mask.transformer(maskedBytes)
    });

    currByte = currByte + mask.bytes;
  }

  // apply onResult hook from bufferTransformer elements
  for (const mask of bufferTransformer) {
    if (typeof mask.onResult === 'function') {
      mask.onResult(result);
    }
  }

  return result;
};

/**
 * Transforms a buffer to a validated set of measurements according to a boxes
 * TTN configuration.
 * @param {Buffer} buffer - The data to be decoded
 * @param {Box} box - A box from the DB for lookup of TTN config & sensors
 * @param {String} [timestamp=new Date()] - Timestamp to attach to the measurements.
 * @return {Promise} Once fulfilled returns a validated array of measurements
 *         (no actual async ops are happening)
 */
const decodeBuffer = function decodeBuffer (buffer, box, timestamp) {
  return Promise.resolve().then(function () {
    // should never be thrown, as we find a box by it's ttn config
    if (!box.integrations || !box.integrations.ttn) {
      throw new Error('box has no TTN configuration');
    }

    // select bufferTransformer according to profile
    const profile = profiles[box.integrations.ttn.profile];

    if (!profile) {
      throw new Error(`profile '${box.integrations.ttn.profile}' is not supported`);
    }

    const bufferTransformer = profile.createBufferTransformer(box);

    // decode buffer using bufferTransformer
    const measurements = bufferToMeasurements(buffer, bufferTransformer);

    // if a timestamp is provided, set it for all the measurements
    if (timestamp && measurements.length && !measurements[0].createdAt) {
      measurements.map(m => {
        m.createdAt = timestamp;

        return m;
      });
    }

    // validate decoded measurements
    return transformAndValidateArray(measurements);
  });
};

/**
 * proxy for decodeBuffer, which converts the input data from base64 to a buffer first
 * @see module:decoding~decodeBuffer
 * @param {String} base64String
 * @param {Box} box
 * @param {String} [timestamp=new Date()]
 * @return {Promise} Once fulfilled returns a validated array of measurements
 *         (no actual async ops are happening)
 */
const decodeBase64 = function decodeBase64 (base64String, box, timestamp) {
  const buf = Buffer.from(base64String, 'base64');

  return decodeBuffer(buf, box, timestamp);
};

module.exports = {
  decodeBuffer,
  decodeBase64
};