orbotix/sphero.js

View on GitHub
lib/packet.js

Summary

Maintainability
C
1 day
Test Coverage
"use strict";

var inherits = require("util").inherits,
    EventEmitter = require("events").EventEmitter;

var utils = require("./utils"),
    RES_PARSER = require("./parsers/response.js"),
    ASYNC_PARSER = require("./parsers/async.js");

var MIN_BUFFER_SIZE = 6,
    FIELDS = {
      size: 5,
      sop1: {
        pos: 0,
        hex: 0xFF
      },
      sop2: {
        pos: 1,
        sync: 0xFF,
        async: 0xFE,
      },
      mrspHex: 0x00,
      seqHex: 0x00,
      mrspIdCode: 2,
      seqMsb: 3,
      dlenLsb: 4,
      checksum: 5,
      didHex: 0x00,
      cidHex: 0x01
    };

var Packet = module.exports = function(opts) {
  this.partialBuffer = new Buffer(0);
  this.partialCounter = 0;

  opts = opts || {};
  this.emitPacketErrors = opts.emitPacketErrors || false;
};

inherits(Packet, EventEmitter);

Packet.prototype.create = function(opts) {
  opts = opts || {};

  var sop1 = (opts.sop1 === undefined) ? FIELDS.sop1.hex : opts.sop1,
      sop2 = (opts.sop2 === undefined) ? FIELDS.sop2.sync : opts.sop2,
      did = (opts.did === undefined) ? FIELDS.didHex : opts.did,
      cid = (opts.cid === undefined) ? FIELDS.cidHex : opts.cid,
      seq = (opts.seq === undefined) ? FIELDS.seqHex : opts.seq,
      data = (!opts.data) ? [] : opts.data,
      // Add 1 to dlen, since it also counts the checksum byte
      dlen = data.length + 1,
      checksum = 0x00;

  this.emitPacketErrors = opts.emitPacketErrors || false;

  // Create array with packet bytes
  var packet = [
    sop1, sop2, did, cid, seq, dlen
  ].concat(data);

  // Get checksum for final byte in packet
  checksum = utils.checksum(packet.slice(2));

  // Add checksum to packet
  packet.push(checksum);

  return packet;
};

Packet.prototype.parse = function(buffer) {
  if (this.partialBuffer.length > 0) {
    buffer = Buffer.concat(
      [this.partialBuffer, buffer],
      buffer.length + this.partialBuffer.length
    );

    this.partialBuffer = new Buffer(0);
  } else {
    this.partialBuffer = new Buffer(buffer);
  }

  if (this._checkSOPs(buffer)) {
    // Check the packet is at least 6 bytes long
    if (this._checkMinSize(buffer)) {
      // Check the buffer length matches the
      // DLEN value specified in the buffer
      if (this._checkExpectedSize(buffer) > -1) {
        // If the packet looks good parse it
        return this._parse(buffer);
      }
    }

    this.partialBuffer = new Buffer(buffer);
  }

  return null;
};

Packet.prototype._parse = function(buffer) {
  var packet = {};
  packet.sop1 = buffer[FIELDS.sop1.pos];
  packet.sop2 = buffer[FIELDS.sop2.pos];

  var bByte2 = buffer[FIELDS.mrspIdCode],
      bByte3 = buffer[FIELDS.seqMsb],
      bByte4 = buffer[FIELDS.dlenLsb];

  if (FIELDS.sop2.sync === buffer[FIELDS.sop2.pos]) {
    packet.mrsp = bByte2;
    packet.seq = bByte3;
    packet.dlen = bByte4;
  } else {
    packet.idCode = bByte2;
    packet.dlenMsb = bByte3;
    packet.dlenLsb = bByte4;
  }

  packet.dlen = this._extractDlen(buffer);

  // Create new Buffer for data that is dlen -1 (minus checksum) in size
  packet.data = new Buffer(packet.dlen - 1);
  // Copy data from buffer into packet.data
  buffer.copy(packet.data, 0, FIELDS.size, FIELDS.size + packet.dlen - 1);
  packet.checksum = buffer[FIELDS.size + packet.dlen - 1];

  this._dealWithExtraBytes(buffer);

  return this._verifyChecksum(buffer, packet);
};

Packet.prototype._dealWithExtraBytes = function(buffer) {
  // If the packet was parsed successfully, and the buffer and
  // expected size of the buffer are the same,clean up the
  // partialBuffer, otherwise assign extrabytes to partialBuffer
  var expectedSize = this._checkExpectedSize(buffer);
  if (buffer.length > expectedSize) {
    this.partialBuffer = new Buffer(buffer.length - expectedSize);
    buffer.copy(this.partialBuffer, 0, expectedSize);
  } else {
    this.partialBuffer = new Buffer(0);
  }
};

Packet.prototype._verifyChecksum = function(buffer, packet) {
  var bSlice = buffer.slice(
        FIELDS.mrspIdCode,
        FIELDS.checksum + packet.dlen - 1
      ),
      checksum = utils.checksum(bSlice);

  // If we got an incorrect checksum we cleanup the packet,
  // partialBuffer, return null and emit an error event
  if (checksum !== packet.checksum) {
    packet = null;
    this.partialBuffer = new Buffer(0);
    if (this.emitPacketErrors) {
      this.emit("error", new Error("Incorrect checksum, packet discarded!"));
    }
  }

  return packet;
};

Packet.prototype.parseAsyncData = function(payload, ds) {
  var parser = ASYNC_PARSER[payload.idCode];

  return this._parseData(parser, payload, ds);
};

Packet.prototype.parseResponseData = function(cmd, payload) {
  if (!cmd || cmd.did === undefined || cmd.cid === undefined) {
    return payload;
  }

  var parserId = cmd.did.toString(16) + ":" + cmd.cid.toString(16),
      parser = RES_PARSER[parserId];

  return this._parseData(parser, payload);
};

Packet.prototype._parseData = function(parser, payload, ds) {
  var data = payload.data,
      pData, fields, field;


  if (parser && (data.length > 0)) {

    ds = this._checkDSMasks(ds, parser);

    if (ds === -1) {
      return payload;
    }

    fields = parser.fields;

    pData = {
      desc: parser.desc,
      idCode: parser.idCode,
      event: parser.event,
      did: parser.did,
      cid: parser.cid,
      packet: payload
    };


    var dsIndex = 0,
        dsFlag = 0,
        i = 0;

    while (i < fields.length) {
      field = fields[i];

      dsFlag = this._checkDSBit(ds, field);

      if (dsFlag === 1) {
        field.from = dsIndex;
        field.to = dsIndex = dsIndex + 2;
      } else if (dsFlag === 0) {
        i = this._incParserIndex(i, fields, data, dsFlag, dsIndex);
        continue;
      }

      pData[field.name] = this._parseField(field, data, pData);

      i = this._incParserIndex(i, fields, data, dsFlag, dsIndex);
    }
  } else {
    pData = payload;
  }

  return pData;
};

Packet.prototype._checkDSMasks = function(ds, parser) {
  if (parser.idCode === 0x03) {
    if (!(ds && ds.mask1 != null && ds.mask2 != null)) {
      return -1;
    }
  } else {
    return null;
  }

  return ds;
};

Packet.prototype._incParserIndex = function(i, fields, data, dsFlag, dsIndex) {
  i++;

  if ((dsFlag >= 0) && (i === fields.length) && (dsIndex < data.length)) {
    i = 0;
  }

  return i;
};

Packet.prototype._checkDSBit = function(ds, field) {
  if (!ds) {
    return -1;
  }

  if (Math.abs(ds[field.maskField] & field.bitmask) > 0) {
    return 1;
  }

  return 0;
};

Packet.prototype._parseField = function(field, data, pData) {
  var pField;
  var width;

  data = data.slice(field.from, field.to);
  pField = utils.bufferToInt(data);

  switch (field.type) {
    case "number":
      if (field.format === "hex") {
        pField = "0x" + pField.toString(16).toUpperCase();
      }
      break;
    case "string":
      pField = data.toString(field.format).replace(/\0/g, "0");
      break;
    case "raw":
      pField = new Buffer(data);
      break;
    case "predefined":
      if (field.mask != null) {
        pField &= field.mask;
      }
      pField = field.values[pField];
      break;
    case "bitmask":
      pField = this._parseBitmaskField(pField, field, pData);
      break;
    case "signed":
      width = 8 * (field.to - field.from);
      if (pField >= Math.pow(2, width - 1)) {
        pField = pField - Math.pow(2, width);
      }
      break;
    default:
      this.emit("error", new Error("Data could not be parsed!"));
      pField = "Data could not be parsed!";
      break;
  }

  return pField;
};

Packet.prototype._parseBitmaskField = function(val, field, pData) {
  var pField = {};

  if (val > field.range.top) {
    val = utils.twosToInt(val, 2);
  }

  if (pData[field.name]) {
    pField = pData[field.name];
    pField.value.push(val);
  } else {
    pField = {
      sensor: field.sensor,
      range: field.range,
      units: field.units,
      value: [val]
    };
  }

  return pField;
};

Packet.prototype._checkSOPs = function(buffer) {
  return (this._checkSOP1(buffer)) ? this._checkSOP2(buffer) : false;
};

Packet.prototype._checkSOP1 = function(buffer) {
  return (buffer[FIELDS.sop1.pos] === FIELDS.sop1.hex);
};

Packet.prototype._checkSOP2 = function(buffer) {
  var sop2 = buffer[FIELDS.sop2.pos];

  if (sop2 === FIELDS.sop2.sync) {
    return "sync";
  } else if (sop2 === FIELDS.sop2.async) {
    return "async";
  }

  return false;
};

Packet.prototype._checkExpectedSize = function(buffer) {
  // Size = buffer fields size (SOP1, SOP2, MSRP, SEQ and DLEN) + DLEN value
  var expectedSize = FIELDS.size + this._extractDlen(buffer),
      bufferSize = buffer.length;

  return (bufferSize < expectedSize) ? -1 : expectedSize;
};

Packet.prototype._checkMinSize = function(buffer) {
  return (buffer.length >= MIN_BUFFER_SIZE);
};

Packet.prototype._extractDlen = function(buffer) {
  if (buffer[FIELDS.sop2.pos] === FIELDS.sop2.sync) {
    return buffer[FIELDS.dlenLsb];
  }

  // We shift the dlen MSB 8 bits and then do a binary OR
  // between the two values to obtain the dlen value
  return (buffer[FIELDS.seqMsb] << 8) | buffer[FIELDS.dlenLsb];
};