API Docs for: 0.5.12
Show:

File: lib/phidgets.js

'use strict';

var crypto = require('crypto');
var net = require('net');
var util = require('util');
var EventEmitter = require('events').EventEmitter;

/****************************************************************************************/
/**************************************  Phidget  ***************************************/
/****************************************************************************************/

/**
 * The `Phidgets` library module makes it easy to connect to various sensor and controller
 * boards made by [Phidgets Inc.](http://www.phidgets.com) This library works under
 * [Node.js](http://www.nodejs.org) and other compatible frameworks such as
 * [io.js](http://www.iojs.org)
 *
 * For support, go to the GitHub [project page](https://github.com/evantahler/nodePhidgets).
 *
 * Please note that this library is an open source project that is not affiliated with
 * Phidgets Inc.
 *
 * @module phidgets
 *
 * @todo Add support for more Phidget boards
 */
module.exports = {};

/**
 * The `Phidget` class is an abstract class providing common properties and methods to all
 * the board-specific child classes. This class cannot be instantiated directly. Please
 * instantiate one of the child classes instead:
 *
 *    * {{#crossLink "PhidgetInterfaceKit"}}{{/crossLink}}
 *    * {{#crossLink "PhidgetLED"}}{{/crossLink}}
 *    * {{#crossLink "PhidgetBridge"}}{{/crossLink}}
 *    * {{#crossLink "PhidgetStepper"}}{{/crossLink}}
 *    * {{#crossLink "PhidgetTemperatureSensor"}}{{/crossLink}}
 *
 * This object extends Node.js'
 * [`events.EventEmitter` object](https://nodejs.org/api/events.html#events_class_events_eventemitter).
 * See that object's documentation for inherited methods.
 *
 * @class Phidget
 * @extends events.EventEmitter
 * @constructor
 * @throws {Error} This class is abstract, you cannot instantiate it directly.
 * @throws {Error} Unsupported device type.
 */
var Phidget = function(type) {

  var self = this;

  if (this.constructor === Phidget) {
    throw new Error("This class is abstract, you cannot instantiate it directly.");
  }

  /**
   * [read-only] Array of all the devices supported by this library.
   * @property supportedDevices
   * @type {string[]}
   * @readOnly
   */
  Object.defineProperty(this, 'supportedDevices', {
    enumerable: true,
    writable: false,
    value: [
      'PhidgetInterfaceKit',
      'PhidgetBridge',
      'PhidgetLED',
      'PhidgetRFID',
      'PhidgetStepper',
      'PhidgetTemperatureSensor'
    ]
  });

  if (this.supportedDevices.indexOf(type) < 0) {
    throw new Error("Unsupported device type.");
  }

  /**
   * [read-only] The type of device (i.e. PhidgetInterfaceKit, PhidgetLED, etc.).
   *
   * @property type
   * @type {String}
   * @readOnly
   */
  Object.defineProperty(this, 'type', {
    enumerable: true,
    writable: false,
    value: type
  });

  /**
   * [read-only]  Whether the device is ready for use or not. A device must be 'opened'
   * before it can be used.
   *
   * @property ready
   * @type {Boolean}
   * @readOnly
   */
  Object.defineProperty(this, 'ready', {
    enumerable: true,
    get: function () {
      return (self._ready);
    }
  });

  /**
   * Whether to try to automatically reopen the device if it gets remotely closed.
   * @property reopen
   * @type {Boolean}
   * @default true
   */
  this.reopen = true;

  /**
   * The host name or address of the Phidgets WebService to connect to.
   * @property host
   * @type {String}
   * @default 127.0.0.1
   */
  this.host = "127.0.0.1";

  /**
   * The port of the Phidgets webservice to connect to.
   * @property port
   * @type {int}
   * @default 5001
   */
  this.port = 5001;

  /**
   * The unique serial number of the device. If specified, it will be used to connect to
   * the matching device.
   *
   * @property serial
   * @type {int}
   * @default undefined
   */
  this.serial = undefined;

  /**
   * The unique label of the device. The label must have a maximum length of 10
   * characters. If you try to set a longer label, the remainder will be truncated. Labels
   * are supported only on newer devices and are remembered even when the device is
   * unplugged. A label can only be set after a Phidget has been 'opened'. Trying to set
   * the label before that will fail silently.
   *
   * @property label
   * @type {String}
   * @default undefined
   */
  Object.defineProperty(this, 'label', {
    enumerable: true,
    get: function () {
      return (self._label);
    },
    set: function(value) {
      if (self.ready) {
        self._label = value.substr(0, 10);
        self._sendPck(self._makePckString('Label'), self._label, true);
      }
    }
  });

  /**
   * [read-only] The unique ID of the Phidget WebService the device is currently connected
   * to.
   *
   * @property serverId
   * @type {int}
   * @default undefined
   */
  Object.defineProperty(this, 'serverId', {
    enumerable: true,
    get: function () {
      return (self._serverId);
    }
  });

  /**
   * The password to connect to the WebService. If specified, it will be used when opening
   * a new connection. As soon as connected the password property will be erased. THIS IS
   * CURRENTLY SET TO PRIVATE BECAUSE IT'S NOT IMPLEMENTED YET!
   *
   * @property password
   * @type {String}
   * @default undefined
   * @private
   */
  this.password = undefined;

  /**
   * [read-only] Human-readable version of the board's name (i.e. "Phidget InterfaceKit
   * 8/8/8". This information is only available some time after the connection has been
   * successfully opened.
   *
   * @property name
   * @type {String}
   * @default undefined
   */
  Object.defineProperty(this, 'name', {
    enumerable: true,
    get: function () {
      return (self._name);
    }
  });

  /**
   * [read-only] This number distinguishes between revisions of a specific type of
   * Phidget. It is only useful for debugging purposes. This information is only available
   * some time after the connection has been successfully opened.
   *
   * @property version
   * @type {String}
   * @default undefined
   */
  Object.defineProperty(this, 'version', {
    enumerable: true,
    get: function () {
      return (self._version);
    }
  });

  /**
   * The delay (in milliseconds) between report updates sent from the webservice.
   *
   * @property interReportPeriod
   * @type {int}
   * @default 8
   */
  Object.defineProperty(this, 'interReportPeriod', {
    enumerable: true,
    get: function () {
      return (self._interReportPeriod);
    },
    set: function(value) {
      self._interReportPeriod = parseInt(value);
      self._sendLine("report " + self._interReportPeriod + " report");
    }
  });

  this._client = null;
  this._delimiter = '\r\n';
  this._inputBuffer = '';
  this._interReportPeriod = 8;
  this._label = undefined;
  this._name = undefined;
  this._openTimeOutDuration = 1000;
  this._openTimeOutId = undefined;
  this._protocol = "1.0.10";
  this._ready = false;
  this._reopenCount = 0;
  this._reopenDelay = 1000;
  this._reopenMaxCount = 5;
  this._version = undefined;

};


/**
 * This is an alias for the `on()` method.
 * @method addListener
 * @param event {String} The event to add the listener for.
 * @param listener {Function} The callback function to execute when the event is
 * triggered.
 * @chainable
 */

/**
 * Adds a listener to the end of the listeners array for the specified event. No checks
 * are made to see if the listener has already been added. Multiple calls passing the same
 * combination of event and listener will result in the listener being added multiple
 * times.
 *
 * This method is inherited from Node.js'
 * [`events.EventEmitter` object](https://nodejs.org/api/events.html#events_class_events_eventemitter).
 * See that object's documentation for more details methods.
 *
 * @method on
 * @param event {String} The event to add the listener for.
 * @param listener {Function} The callback function to execute when the event is
 * triggered.
 * @chainable
 */

/**
 * Adds a one time listener for the event. This listener is invoked only the next time the
 * event is fired, after which it is removed.
 *
 * This method is inherited from Node.js'
 * [`events.EventEmitter` object](https://nodejs.org/api/events.html#events_class_events_eventemitter).
 * See that object's documentation for more details methods.
 *
 * @method once
 * @param event {String} The event to add the listener for.
 * @param listener {Function} The callback function to execute when the event is
 * triggered.
 * @chainable
 */

/**
 * Removes all listeners, or those of the specified event.
 *
 * This method is inherited from Node.js'
 * [`events.EventEmitter` object](https://nodejs.org/api/events.html#events_class_events_eventemitter).
 * See that object's documentation for more details methods.
 *
 * @method removeAllListeners
 * @param [event] {String} The event to remove the listeners for.
 * @chainable
 */

/**
 * Removes a listener from the listener array for the specified event. `removeListener()`
 * will remove, at most, one instance of a listener from the listener array. If any single
 * listener has been added multiple times to the listener array for the specified event,
 * then `removeListener()` must be called multiple times to remove each instance.
 *
 * This method is inherited from Node.js'
 * [`events.EventEmitter` object](https://nodejs.org/api/events.html#events_class_events_eventemitter).
 * See that object's documentation for more details methods.
 *
 * @method removeListener
 * @param [event] {String} The event to remove the listeners for.
 * @param listener {Function} The callback function to execute when the event is
 * triggered.
 * @chainable
 */
util.inherits(Phidget, EventEmitter);

/**
 * Opens a connection to a Phidget device. Opening a connection is a two-step process.
 * First, a connection to the Phidget WebService (which must be running) is established.
 * Then, a session to the specified device (which must be plugged in) is opened.
 *
 * @method open
 * @param {Object} [options={}] Options
 * @param {String} [options.host="127.0.0.1"] Hostname or IP address to connect to
 * @param {int} [options.port=5001] Port to connect to
 * @param {int} [options.serial] Serial number of the device to connect to
 * @param {String} [options.label] Label of the device to connect to (can be set in the
 *        Phidgets control panel).
 * @returns {Phidget} Returns the Phidget to allow method chaining.
 * @chainable
 */
Phidget.prototype.open = function(options) {

  var self = this;

  options = options || {};
  if (options.host)     { this.host = options.host; }
  if (options.port)     { this.port = options.port; }
  if (options.serial)   { this.serial = options.serial; }
  if (options.label)    { this._label = options.label; }
  if (options.password) { this.password = options.password; }

  /**
   * Event emitted when an attempt to open a Phidget has been initiated.
   *
   * @event opening
   * @param {Phidget} emitter The actual Phidget object that emitted the event.
   */
  self.emit('opening', self);

  // The function passed as a parameter is only executed when the 'connect' event is
  // received (only on success). That's why the 'error' handler needs to be below.
  self._client = net.createConnection(self.port, self.host, function() {

    // Makes the data come in as a string instead of a Buffer object
    self._client.setEncoding('utf8');
    self._client.setKeepAlive("enable", 10000);

    self._client.on('end', function() {
      self._handleConnectionEnd();
    });

    self._client.on('data', function(d) {
      self._handleData(d);
    });

    // Open connection to Phidget WebService
    self._sendLine("995 authenticate, version=" + self._protocol);

  });

  self._client.on('error', function(e) {
    self._handleConnectionError(e);
  });

  return self;

};

/**
 * Closes a previously opened connection to a Phidget device.
 *
 * @method close
 * @returns {Phidget} Returns the Phidget to allow method chaining.
 * @chainable
 */
Phidget.prototype.close = function() {

  var self = this;

  if (!self.ready) { return self; }

  self.reopen = false;
  clearTimeout(self._openTimeOutId);

  var message  = "/PCK/Client/0.0.0.0/" + self._randomId + "/" + self.type;
  if (self.label) {
    message += "/-1/" + self.label;
  } else if (self.serial && self.serial > 0) {
    message += "/" + self.serial;
  }
  self._sendPck(message, 'Close', false);
  self._sendLine("quit");

  self._terminateConnection();
  return self;

};

/** @private */
Phidget.prototype._terminateConnection = function() {

  var self = this;

  self._client.removeAllListeners(['end', 'error', 'data']);
  self._client.destroy();
  self._ready = false;

  /**
   * Event emitted when the connection to a phidget has been remotely closed.
   *
   * @event closed
   * @param {Phidget} emitter The actual object that emitted the event.
   */
  self.emit('closed', self);

};

/** @private */
Phidget.prototype._handleConnectionEnd = function() {

  var self = this;

  self._terminateConnection();

  // Attempt to reopen if configured as such
  if(self.reopen === true && self._reopenCount < self._reopenMaxCount) {

    self._reopenCount++;

    // Instead of trying to reconnect right away, we give a little time for the network or
    // device to come back
    setTimeout(
      function() {
        /**
         * Event emitted when an attempt to automatically re-open a closed Phidget is
         * being carried on.
         *
         * @event reopening
         * @param {Phidget} emitter The actual Phidget object that emitted the event.
         * @param {Object} data Additional data regarding the event.
         * @param {int} data.attempt The number of re-opening attempts performed.
         * @param {int} data.max The maximum number of attempts that will be tried before
         * failing.
         */
        self.emit(
          'reopening',
          self,
          {attempt: self._reopenCount, max: self._reopenMaxCount}
        );
        self.open();
      },
      self._reopenDelay
    );

  }



};

/** @private */
Phidget.prototype._handleConnectionError = function(e) {

  var self = this;

  /**
   * Event emitted when an error occurs while trying to open a phidget
   *
   * @event error
   * @param {Phidget} emitter The actual object that emitted the event.
   * @param {Error} error The error object
   * @param {String} error.address The network address
   * @param {String} error.code The error code
   * @param {String} error.errno The error number
   * @param {String} error.message The error message
   * @param {String} error.port The network port
   */
  self.emit('error', self, e);

  self._handleConnectionEnd();

};

/**
 * This function is called each time data is received from the Phidget WebSerice. It adds
 * the data to the input buffer and checks if full lines (separated by '\n') can be
 * reconstructed. If full lines are found they are handed over to the `_parseLineInput()`
 * method for processing.
 *
 * @method _handleData
 * @param chunk {String} A chunk of utf8 encoded text to parse
 * @private
 */
Phidget.prototype._handleData = function(chunk) {

  var self = this;
  var index, line;

  self._inputBuffer += chunk;

  while( (index = self._inputBuffer.indexOf('\n')) > -1 ) {

    line = self._inputBuffer.slice(0, index);
    self._inputBuffer = self._inputBuffer.slice(index + 1);
    line = line.replace(/\u0000/gi, "");
    line = line.replace(/\u0001/gi, "");

    /**
     * Event emitted when a new line of data has been received from the web service. This
     * is mostly useful for debugging purposes (hence the @private denomination). It will
     * let you view all data coming in.
     *
     * @event received
     * @param {Phidget} emitter The actual Phidget object that emitted the event.
     * @param {String} data The actual string data received.
     * @private
     */
    self.emit('received', self, line);

    self._parseLineInput(line);

  }

};

/**
 * Parses a single line of data typically received from the Phidget WebService. If the
 * line is a *report* line, the function hands it off to the `_parsePskKey()` method.
 * Otherwise, it deals with it locally.
 *
 * @method _parseLineInput
 * @param line {String} A non-terminated line of utf8 text
 * @private
 */
Phidget.prototype._parseLineInput = function(line) {

  var self = this;
  var err;

  // Is it a report line ?
  if (line.indexOf('report') === 0 ) {

    if (line.indexOf(' is pending, key ') > -1 ) {

      // It has a key: hand it over, we are done.
      self._parsePskKey(line.split(" is pending, key ")[1], self);

    } else if (
      line.indexOf("report 200-that's all for now") === 0 &&
      self.ready === false
    ) {

      // The Phidget is attached
      self._ready = true;
      clearTimeout(self._openTimeOutId);
      self._reopenCount = 0;

      /**
       * Event emitted when a phidget is successfully opened.
       *
       * @event opened
       * @param {Phidget} emitter The actual Phidget object that emitted the event.
       */
      self.emit("opened", self);

    }

    return;

  }

  var status = parseInt(line.split(" ")[0]);

  if (status === 200) {             // Information

    // Ignored: "200 set successful" and "200 inter-report period adjusted to 8ms."

  } else if (status === 994) {    // Version mismatch

    err = new Error('Protocol version mismatch.', 'PROTOCOL_MISMATCH');
    err.details = line;
    self.emit('error', self, err);

  } else if (status === 996) {   // Authenticated or no authentication necessary

    // Adjust inter-report period (triggering the setter will send the request to the
    // Phidget WebService.
    self.interReportPeriod = self._interReportPeriod;

    // Open session to a specific device. The factors deciding which device to connect to
    // are: type, label (optional) and serial (optional).
    self._randomId = Math.floor(Math.random() * 99999);
    var pck  = "/PCK/Client/0.0.0.0/" + self._randomId + "/" + self.type;
    if (self._label) {
      pck += "/-1/" + self._label;
    } else if (self.serial && self.serial > 0) {
      pck += "/" + self.serial;
    }
    self._sendPck(pck, 'Open', false);

    // Issue "listen" command
    self._sendLine("listen /PSK/" + self.type + " lid0");

    // Here we have a challenge: if we connect successfully to the web service but the
    // device is not plugged in, we do not know. That's why the device is set as ready
    // only when the first occurrence of the following line is received: "report
    // 200-that's all for now". If that line is not received within a certain delay, we
    // must issue a timeout.
    self._openTimeOutId = setTimeout(
      function() {
        /**
         * Event emitted when an attempt to open a Phidget times out.
         *
         * @event timeout
         * @param {Phidget} emitter The actual Phidget object that emitted the event.
         */
        self.emit('timeout', self);
        self._handleConnectionEnd();
      },
      self._openTimeOutDuration
    );

  } else if (status === 998) {     // Authentication failed

    err = new Error('The authentication attempt failed.', 'AUTHENTICATION_FAILED');
    err.details = line;
    self.emit('error', self, err);
    self.reopen = false;
    self._terminateConnection();

  } else if (status === 999) {   // Authentication required

    if (!self.password) {

      err = new Error(
        'A connection to this Phidget WebService requires a valid password.',
        'PASSWORD_REQUIRED'
      );
      err.details = line;
      self.emit('error', self, err);
      self.reopen = false;
      self._terminateConnection();

    } else {

      var ticket = line.split(" ")[1] + self.password;
      self.password = '';
      var hash = crypto.createHash('md5');
      hash.update(ticket, 'utf8');
      self._sendLine("997 " + hash.digest('hex'));

    }

  }

};

/**
 * Parses a /PSK string and performs appropriate action.
 *
 * @param oPsk {String} Original /PSK string typically coming from the Phidget WebService
 * @param self
 * @private
 */
Phidget.prototype._parsePskKey = function(oPsk, self) {

  // Remove front /PSK/ and retrieve necessary values
  var psk     = oPsk.split("/PSK/")[1];
  var parts   = psk.split(' ')[0].split('/');         // [PhidgetLED,label,123456,Input,4]

  var device  = parts[0];
  var label   = parts[1];
  var serial  = parseInt(parts[2]);
  var keyword = parts[3];                             // Sensor, Name, DataRate, etc.
  var index   = parseInt(parts[4]);                   // not always present
  var value   = psk.split('"')[1];                    // 69, PhidgetLED, label, 904, etc.
  var status  = psk.split('" (')[1].replace(")", ''); // added, changed, removing, etc.

  // Grab the serial as soon as we see it. This is also our first chance to initialize the board
  // with desired phidget-specific initial settings.
  if ( !self.serial && parseInt(parts[2]) > 0 ) {
    self.serial = parseInt(parts[2]);
    self._setPhidgetSpecificInitialState();
  }

  // Make sure to dispatch the incoming data only if its meant for this device (because there could
  // be more than one).
  if (serial !== this.serial) { return; }

  if ( keyword === 'Status' && status === 'removing') {

    self._handleConnectionEnd();

  } else if ( keyword === 'Name' && status === 'added' ) {

    self._name = value;

  } else if ( keyword === 'Version' && status === 'added' ) {

    self._version = value;

  } else if ( keyword === 'Label' && status === 'added' ) {

    self._label = label;

  } else if ( keyword === 'ID' && status === 'added' ) {

    self._serverId = value;

  } else if ( keyword === 'InitKeys' && status === 'added' ) {

    // ignored

  } else if ( keyword === 'Status' && status === 'added' ) {

    // ignored

  } else {

    self._parsePhidgetSpecificData({
      device: device,
      label: label,
      serial: serial,
      keyword: keyword,
      index: index,
      value: value,
      status: status
    });

  }

};

/**
 * Parses Phidget-specific data received from the Phidget WebService. This function is
 * meant to be overridden by subclasses.
 *
 * @method _parsePhidgetSpecificData
 * @param data {Object} An object containing the received data
 * @param data.device {String} The device identifier (e.g. PhidgetInterfaceKey,
 *        PhidgetLED, etc.).
 * @param data.label {String} The custom label set for the device.
 * @param data.serial {int} The serial number of the device.
 * @param data.keyword {String} A keyword identifying the type of information conveyed. It
 *        could be 'Input', 'Version', 'DataRate', etc.
 * @param data.index {int} The numerical index (for indexed keys only)
 * @param data.value {String} The actual value.
 * @param data.status {String} The status of the key. It could be: 'added', 'changed',
 *        'removing', etc.
 * @protected
 */
Phidget.prototype._parsePhidgetSpecificData = function (data) {};

/**
 * Sets phidget-specific state before the 'opened' event is triggered. This is a good
 * place for subclasses to assign initial values to the board. This is meant to be
 * overridden by subclasses.
 *
 * @method _setPhidgetSpecificInitialState
 * @protected
 */
Phidget.prototype._setPhidgetSpecificInitialState = function () {};

/**
 * Returns a /PCK string built from the specified parameters. PCK strings are the keys
 * sent out to control the board.
 *
 * @method _makePckString
 * @param keyword {String} The operation keyword to use
 * @param [index] {int} The index of the output to use
 * @private
 */
Phidget.prototype._makePckString = function (keyword, index) {
  var self = this;
  var pck = '/PCK/' + self.type + '/' + self.serial + '/' + keyword;
  if (index > -1) { pck += '/' + index; }
  return pck;
};

/**
 * Sends the /PCK string (with attached value) to the webservice.
 *
 * @method _sendPck
 * @param key {String} A /PCK string (typically form the _makePckString() method)
 * @param value {int|string} The value to set
 * @param [persistent=false] {Boolean} Whether the value should persist or whether its for
 *        the session only.
 * @private
 */
Phidget.prototype._sendPck = function (key, value, persistent) {
  var self = this;
  var request = "set " + key + "=\"" + value + "\"";
  if(!persistent) { request += " for session"; }
  self._sendLine(request);
};

/**
 * Sends a line of data to the webservice
 *
 * @method _sendLine
 * @param line {String} A non-terminated line of data to send
 * @private
 */
Phidget.prototype._sendLine = function (line) {
  var self = this;
  self._client.write(line + self._delimiter);

  /**
   * Event emitted when a new line of data has been sent to the Phidget WebService. This
   * is mostly useful for debugging purposes (hence the @private denomination). It will
   * let you view all data going out.
   *
   * @event sent
   * @param emitter {Phidget} The actual Phidget object that emitted the event.
   * @param data {String} The actual string of sent data.
   * @private
   */
  self.emit('sent', self, line);
};

/**
 * Returns the value after making sure it falls between min and max.
 *
 * @method _forceBetween
 * @param value {int|Number} The value to check
 * @param min {int} The minimum value desired
 * @param max {int} The maximum value desired
 * @return int
 * @protected
 */
Phidget.prototype._forceBetween = function(value, min, max) {
  return (Math.min(max, Math.max(min, value)));
};

module.exports.Phidget = Phidget;


/****************************************************************************************/
/*********************************  PhidgetInterfaceKit  ********************************/
/****************************************************************************************/

/**
 * The `PhidgetInterfaceKit` class allows you to control and receive data from all Phidget
 * interface kit boards :
 *
 *  * PhidgetInterfaceKit 8/8/8 normal and mini-format
 *  * PhidgetInterfaceKit 2/2/2
 *  * PhidgetInterfaceKit 0/16/16
 *  * PhidgetInterfaceKit 8/8/8 (with and without hub)
 *  * etc.
 *
 * Not all of these boards have been tested. If you possess one and can verify its
 * compatibility, let us know.
 *
 * This object extends the `Phidget` object which extends Node.js' [`events.EventEmitter` object](https://nodejs.org/api/events.html#events_class_events_eventemitter).
 * See that object's documentation for inherited methods.
 *
 * @class PhidgetInterfaceKit
 * @constructor
 * @extends Phidget
 */
var PhidgetInterfaceKit = function() {
  PhidgetInterfaceKit.super_.call(this, 'PhidgetInterfaceKit');

  var self = this;

  /**
   * [read-only] An object containing information about the digital inputs of the device.
   * Here are a few examples of how to retrieve information in that object:
   *
   *     PhidgetInterfaceKit.inputs[5].value         // Input 5 current value
   *     PhidgetInterfaceKit.inputs.count            // Total number of inputs on the device
   *
   * @property inputs {Object}
   * @property inputs.count {int} The total number of inputs on the device.
   * @property inputs[int].value {int} The current value of the specified input.
   */
  this.inputs = {};

  /**
   * [read-only] An object containing information about the analog sensor inputs of the
   * device. Here are a few examples of how to retrieve information in that object:
   *
   *     PhidgetInterfaceKit.sensors[5].value         // Sensor 5 current value
   *     PhidgetInterfaceKit.sensors.count            // Total number of sensors on the device
   *     PhidgetInterfaceKit.sensors[3].sensitivity   // Sensor 3 sensitivity level
   *
   * @property sensors {Object}
   * @property sensors.count {int} The total number of sensors on the device.
   * @property sensors[int].rawValue {int} The current raw value of the specified sensor.
   * @property sensors[int].sensitivity {int} The sensitivity threshold of the specified
   *           sensor.
   * @property sensors[int].updateInterval {int} The update interval of the specified
   *           sensor.
   * @property sensors[int].value {int} The current value of the specified sensor.
   */
  this.sensors = {};

  /**
   * [read-only] An object containing information about the digital outputs of the device.
   * Here are a few examples of how to retrieve information in that object:
   *
   *     PhidgetInterfaceKit.outputs[5].value     // Output 5 current value
   *     PhidgetInterfaceKit.outputs.count        // Total number of outputs on the device
   *
   * @property outputs {Object}
   * @property outputs.count {int} The total number of outputs on the device.
   * @property outputs[int].value {int} The current value of the specified output.
   */
  this.outputs = {};

  /**
   * Determines whether ratiometric values should be used or not for analog sensors. If
   * this property is defined before the phidget is opened, it will be set as soon as
   * possible after opening it. If it is defined after the board is opened and ready, it
   * will be set right away.
   *
   * @property ratiometric {Boolean}
   * @default undefined
   */
  Object.defineProperty(this, 'ratiometric', {
    enumerable: true,
    get: function () {
      return (self._ratiometric);
    },
    set: function (value) {
      self._ratiometric = value;
      if (self.ready) {
        self._sendPck(self._makePckString('Ratiometric'), value ? 1 : 0, true);
      }
    }
  });

  /** @private */
  this._ratiometric = undefined;
  this._shortestUpdateInterval = undefined;
  this._longestUpdateInterval = undefined;
  this._interruptRate = undefined;

};

util.inherits(PhidgetInterfaceKit, Phidget);

/**
 * Sets the specified output to active (true) or inactive (false). This method should only
 * be used after the board is 'opened'. Calling it before will fail silently.
 *
 * @method setOutput
 * @param index {int|Array} The output number to set (or array of output numbers)
 * @param [value=false] {Boolean} The value you wish to set the output to.
 * @returns {PhidgetInterfaceKit} Returns the PhidgetInterfaceKit to allow method
 *          chaining.
 * @chainable
 */
PhidgetInterfaceKit.prototype.setOutput = function(index, value) {

  var self = this;
  if (self.ready !== true) { return self; }

  index = [].concat(index);
  value = (value === true);
  var vOut = (value === true) ? 1 : 0;

  for (var i = 0; i < index.length; i++) {
    var pos = parseInt(index[i]);
    if (!self.outputs[pos]) { self.outputs[pos] = {}; }
    self.outputs[pos].value = value;
    self._sendPck(self._makePckString('Output', pos), vOut, true);

    /**
     * Event emitted when an output's status is changed.
     *
     * @event output
     * @param {PhidgetInterfaceKit} emitter The actual PhidgetInterfaceKit object that
     *        emitted the event.
     * @param {Object} data An object containing the output data and related information
     * @param {int} data.index The output's index number
     * @param {int} data.value The output's new value
     */
    self.emit(
      "output",
      self,
      { "index": pos, "value": self.outputs[pos].value }
    );

  }

  return self;

};

/**
 * Sets the update interval of a sensor. The update interval is the number of
 * milliseconds between update notifications. It must be a multiple of 8 between 8 and
 * 1000.
 *
 * The shorter the interval is and the more frequent the updates will be. However, shorter
 * intervals are more demanding on the cpu. This function accepts a single sensor number
 * or an array of sensor numbers to set. This method should only be used after the board
 * is 'opened'. Calling it before will fail silently.
 *
 * @method setUpdateInterval
 * @param index {int|Array} The sensor's number (or an array of sensor numbers)
 * @param [value=16] {int} The number of milliseconds you wish to set the interval to.
 * @returns {PhidgetInterfaceKit} Returns the PhidgetInterfaceKit to allow method
 *          chaining.
 * @chainable
 */
PhidgetInterfaceKit.prototype.setUpdateInterval = function(index, value) {

  var self = this;
  if (self.ready !== true) { return self; }

  index = [].concat(index);
  if (value % 8 !== 0) { value = 16; }
  value = self._forceBetween(value, self._shortestUpdateInterval, self._longestUpdateInterval);

  for (var i = 0; i < index.length; i++) {
    var pos = parseInt(index[i]);
    if (!self.sensors[pos]) { self.sensors[pos] = {}; }
    self.sensors[pos].updateInterval = value;
    self._sendPck(self._makePckString('DataRate', pos), value, true);
  }

  return self;

};

/**
 * Sets the sensitivity threshold of a sensor. The threshold is measured in `sensorvalue`
 * (0-1000). It is the smallest change that will trigger an update notification from the
 * sensor. Sensitivity threshold and update intervals are mutually exclusive. If you set
 * the sensitivity of a sensor, the update interval will be ignored and vice versa.
 *
 * This function accepts a single sensor number or an array of sensor numbers for which to
 * set the sensitivity.
 *
 * This method should only be used after the board is 'opened'. Calling it before will
 * fail silently.
 *
 * @method setSensitivity
 * @param index {int|Array} The sensor's number (or an array of sensor numbers)
 * @param [value=10] {int} The number sensitivity threshold to assign (0-1000)
 * @returns {PhidgetInterfaceKit} Returns the PhidgetInterfaceKit to allow method
 *          chaining.
 * @chainable
 */
PhidgetInterfaceKit.prototype.setSensitivity = function(index, value) {

  var self = this;
  if (self.ready !== true) { return self; }

  index = [].concat(index);
  value = value || 10;
  value = self._forceBetween(value, 0, 1000);

  for (var i = 0; i < index.length; i++) {
    var pos = parseInt(index[i]);
    if (!self.sensors[pos]) { self.sensors[pos] = {}; }
    self.sensors[pos].sensitivity = value;
    self._sendPck(self._makePckString('Trigger', pos), value, true);
  }

  return self;

};

PhidgetInterfaceKit.prototype._setPhidgetSpecificInitialState = function () {
  var self = this;
  if (self.ratiometric !== undefined) {
    self._sendPck(self._makePckString('Ratiometric'), (self.ratiometric ? 1 : 0), true);
  }
};

/** @private See overridden method for details. */
PhidgetInterfaceKit.prototype._parsePhidgetSpecificData = function (data) {

  var self = this;

  if (data.keyword === "Input") {

    if (!self.inputs[data.index]) { self.inputs[data.index] = {}; }
    self.inputs[data.index].value = (data.value === '1');

    /**
     * Event emitted when the status of a binary input changes.
     *
     * @event input
     * @param {PhidgetInterfaceKit} emitter The actual PhidgetInterfaceKit object that
     *        emitted the event.
     * @param {Object} data An object containing the input data and related information
     * @param {int} data.index The input's index number
     * @param {Boolean} data.value The input's received value
     */
    self.ready && self.emit(
      "input",
      self,
      { "index": data.index, "value": self.inputs[data.index].value }
    );

  } else if (data.keyword === "Sensor") {

    // The event is emitted after RawSensor is received to make sure both have been
    // received (it's always right after).
    if (!self.sensors[data.index]) { self.sensors[data.index] = {}; }
    self.sensors[data.index].value = parseInt(data.value);

  } else if (data.keyword === 'RawSensor') {

    /**
     * Event emitted when analog sensor data is received
     *
     * @event sensor
     * @param {PhidgetInterfaceKit} emitter The actual PhidgetInterfaceKit object that
     *        emitted the event.
     * @param {Object} data An object containing the sensor data and related information
     * @param {int} data.index The sensor's index number
     * @param {int} data.value The sensor's received value
     */
    if (!self.sensors[data.index]) { self.sensors[data.index] = {}; }
    self.sensors[data.index].rawValue = parseInt(data.value);
    self.ready && self.emit(
      "sensor",
      self,
      {
        "index": data.index,
        "value": self.sensors[data.index].value,
        "rawValue": self.sensors[data.index].rawValue
      }
    );

  } else if (data.keyword === "Output") {

    if (!self.outputs[data.index]) { self.outputs[data.index] = {}; }
    self.outputs[data.index].value = (data.value === '1');

  } else if (data.keyword === 'Ratiometric') {
    self._ratiometric = (data.value === '1');
  } else if (data.keyword === 'NumberOfInputs') {
    self.inputs.count = parseInt(data.value);
  } else if (data.keyword === 'NumberOfOutputs') {
    self.outputs.count = parseInt(data.value);
  } else if (data.keyword === 'NumberOfSensors') {
    self.sensors.count = parseInt(data.value);
  } else if (data.keyword === 'DataRate') {
    if (!self.sensors[data.index]) { self.sensors[data.index] = {}; }
    self.sensors[data.index].updateInterval = parseInt(data.value);
  } else if (data.keyword === 'DataRateMax') {
    self._shortestUpdateInterval = parseInt(data.value);
  } else if (data.keyword === 'DataRateMin') {
    self._longestUpdateInterval = parseInt(data.value);
  } else if (data.keyword === 'Trigger') {
    if (!self.sensors[data.index]) { self.sensors[data.index] = {}; }
    self.sensors[data.index].sensitivity = parseInt(data.value);
  } else if (data.keyword === 'InterruptRate') {
    self._interruptRate = parseInt(data.value);
  }

};

module.exports.PhidgetInterfaceKit = PhidgetInterfaceKit;

/****************************************************************************************/
/*************************************  PhidgetLED  *************************************/
/****************************************************************************************/

/**
 * The PhidgetLED class allows you to control a PhidgetLED-64 Advanced board.
 *
 * This object extends the `Phidget` object which itself extends Node.js'
 * [`events.EventEmitter` object](https://nodejs.org/api/events.html#events_class_events_eventemitter).
 * See that object's documentation for inherited methods.
 *
 * @class PhidgetLED
 * @constructor
 * @extends Phidget
 */
var PhidgetLED = function() {
  PhidgetLED.super_.call(this, 'PhidgetLED');

  var self = this;

  /**
   * [read-only] An object containing information about all LED outputs of the device.
   * Here are a few examples of how to retrieve information in that object:
   *
   *     PhidgetLED.leds[5].value     // LED 5 current value
   *     PhidgetLED.leds.count        // Total number of LED outputs on the device
   *
   * @property leds {Object}
   * @property leds.count {int} The total number of physical LED outputs on the device.
   * @property leds[int].value {int} The current brightness value of the specified LED
   *           output (between 0 and 100)
   * @property leds[int].currentLimit {int} The current limit of the specified LED output
   *           (in mA).
   */
  this.leds = {};

  /**
   * The global voltage for all led outputs. When setting the voltage, you must use one of
   * the values in the `PhidgetLED.supportedVoltages` array. Valid values currently are
   * (in volts):
   *
   * * 1.7
   * * 2.75
   * * 3.9
   * * 5
   *
   * Trying to set the voltage to another value will fail silently. This is not supported
   * by all PhidgetLED boards.
   *
   * @property voltage {Number}
   */
  Object.defineProperty(this, 'voltage', {
    enumerable: true,
    get: function () {
      return (self._voltage);
    },
    set: function(value) {
      if (self.ready) {
        var index = self.supportedVoltages.indexOf(value) + 1;
        if (index > 0) {
          self._sendPck(self._makePckString('Voltage'), index, true);
          self._voltage = value;
        }
      }
    }
  });

  /**
   * The global "current limit" for all led outputs. When setting the global current
   * limit, you must use one of the values in the
   * `PhidgetLED.supportedGlobalCurrentLimits` array. Valid values currently are (in mA):
   * 20, 40, 60, 80. Trying to set the current limit to another value will fail silently.
   *
   * This is not supported by all PhidgetLED boards.
   *
   * @property currentLimit {Number}
   */
  Object.defineProperty(this, 'currentLimit', {
    enumerable: true,
    get: function () {
      return (self._currentLimit);
    },
    set: function(value) {
      if (self.ready) {
        var index = self.supportedGlobalCurrentLimits.indexOf(value) + 1;
        if (index > 0) {
          self._sendPck(self._makePckString('CurrentLimit'), index, true);
          self._currentLimit = value;
        }
      }
    }
  });

  /**
   * [read-only] Array of supported voltages (in volts).
   * @property supportedVoltages {Array}
   */
  Object.defineProperty(this, 'supportedVoltages', {
    enumerable: true,
    writable: false,
    value: [1.7, 2.75, 3.9, 5]
  });

  /**
   * [read-only] An array of values that are valid when setting the global current limit
   * (in mA).
   * @property supportedGlobalCurrentLimits {Array}
   */
  Object.defineProperty(this, 'supportedGlobalCurrentLimits', {
    enumerable: true,
    writable: false,
    value: [20, 40, 60, 80]
  });

  this._voltage = undefined;
  this._currentLimit = undefined;

};

util.inherits(PhidgetLED, Phidget);

/**
 * Adjusts the brightness of a LED.
 *
 * @method setBrightness
 * @param index {int|Array} The LED output number for which to adjust the brightness (or
 *        array of LED output numbers)
 * @param [value=100] {int} The value (0-100) you wish to adjust the brightness to.
 * @returns {Phidget} Returns the Phidget to allow method chaining
 * @chainable
 */
PhidgetLED.prototype.setBrightness = function(index, value) {

  var self = this;
  if (!self.ready || index === undefined) { return self; }

  index = [].concat(index);
  value = self._forceBetween(value, 0, 100) || 100;

  for (var i = 0; i < index.length; i++) {
    var pos = self._forceBetween(index[i], 0, 63);
    self._sendPck(self._makePckString('Brightness', pos), value, true);
    self.leds[pos].value = value;
    /**
     * Event emitted right after a LED's brightness has been changed.
     *
     * @event brightness
     * @param {PhidgetInterfaceKit} emitter The actual PhidgetInterfaceKit object that
     *        emitted the event.
     * @param {Object} data An object containing the brightness data and related
     *        information
     * @param {int} data.index The LED output index number
     * @param {int} data.value The brightness value
     */
    self.ready && self.emit(
      "brightness",
      self,
      { "index": pos, "value": self.leds[pos].value }
    );
  }

  return self;

};

/**
 * Sets the current limit (in mA) for a specific LED output (or an array of LED outputs).
 * The value must be between 0 and 80 mA. If the value provided is outside this range, the
 * closest acceptable value will be used instead.
 *
 * @method setCurrentLimit
 * @param index {int|Array} The LED output number (or array of LED output numbers) for
 *        which to adjust the current limit
 * @param [value=20] {Number} The value you wish to adjust the current limit to. Should be within
 *        the range defined by `PhidgetLED.currentLimitRange`.
 * @returns {Phidget} Returns the Phidget to allow method chaining
 * @chainable
 */
PhidgetLED.prototype.setCurrentLimit = function(index, value) {

  var self = this;
  if (!self.ready || index === undefined) { return self; }

  index = [].concat(index);
  value = self._forceBetween(value, 0, 80) || 20;

  for (var i = 0; i < index.length; i++) {
    var pos = self._forceBetween(index[i], 0, self.leds.count);
    self._sendPck(self._makePckString('CurrentLimitIndexed', pos), value, true);
    self.leds[pos].currentLimit = value;
  }

  return self;

};

PhidgetLED.prototype._parsePhidgetSpecificData = function (data) {

  var self = this;

  if (data.keyword === "NumberOfLEDs") {
    self.leds.count = parseInt(data.value);
  } else if (data.keyword === "Voltage") {
    var v = self._forceBetween(data.value, 1, 4);
    self._voltage = self.supportedVoltages[v - 1];
  } else if (data.keyword === "CurrentLimit") {
    self._currentLimit = self.supportedGlobalCurrentLimits[data.value - 1];
  } else if (data.keyword === "Brightness") {
    if (!self.leds[data.index]) { self.leds[data.index] = {}; }
    self.leds[data.index].value = parseInt(data.value);
  } else if (data.keyword === "CurrentLimitIndexed") {
    if (!self.leds[data.index]) { self.leds[data.index] = {}; }
    self.leds[data.index].currentLimit = parseInt(data.value);
  }

};

module.exports.PhidgetLED = PhidgetLED;


/****************************************************************************************/
/************************************  PhidgetBridge  ***********************************/
/****************************************************************************************/

/**
 * The `PhidgetBridge` class allows you to receive data from PhidgetBridge boards. Beware
 * that each input's enabled/disabled status remains even when the device is powered off.
 * As a precaution, you can call `setEnabled()` each time your code starts:
 *
 *       phidgets = require('phidgets'),
 *       bridge = new phidgets.PhidgetBridge();
 *
 *       function onSensorData(emitter, data) {
 *          console.log(data);
 *       }
 *
 *       bridge.on("opened", function() {
 *          bridge.setEnabled(0, true);
 *          bridge.on('sensor', onSensorData);
 *       });
 *
 *       bridge.open();
 *
 * This object extends the `Phidget` object which extends Node.js' [`events.EventEmitter` object](https://nodejs.org/api/events.html#events_class_events_eventemitter).
 * See that object's documentation for inherited methods.
 *
 * @class PhidgetBridge
 * @constructor
 * @extends Phidget
 */
var PhidgetBridge = function() {
  PhidgetBridge.super_.call(this, 'PhidgetBridge');

  var self = this;

  /**
   * [read-only] An object containing information about all the bridge's sensors. Here are
   * a few examples of how to retrieve information in that object:
   *
   *     PhidgetBridge.sensors[2].value         // Sensor 2 current value
   *     PhidgetBridge.sensors.count            // Total number of sensors on the device
   *     PhidgetBridge.sensors[3].gain          // Sensor 3 sensitivity level
   *     PhidgetBridge.sensors[0].enabled       // Whether sensor 0 is currently enabled
   *
   * @property sensors {Object}
   * @property sensors.count {int} The total number of sensors on the device.
   * @property sensors[int].gain {int} The gain for this sensor (1, 8, 16, 32, 64 or 128)
   * @property sensors[int].updateInterval {int} The update interval of the specified
   *           sensor.
   * @property sensors[int].value {Number}  The current value of the specified sensor in
   *                                        mV/V. If the sensor is not enabled, this will
   *                                        be `null`.
   * @property sensors[int].min {Number}    The minimum value that the sensor can measure
   *                                        in mV/V. This value will depend on the
   *                                        selected gain. At a gain of 1, the maximum is
   *                                        -1000mV/V.
   * @property sensors[int].max {Number}    The minimum value that the sensor can measure
   *                                        in mV/V. This value will depend on the
   *                                        selected gain. At a gain of 1, the maximum is
   *                                        1000mV/V.
   */
  this.sensors = {};

  /**
   * The duration (in milliseconds) between update notifications (must be multiple of 8).
   * The shorter the interval is, the more frequent the updates will be sent by the
   * device.
   *
   * @property updateInterval {int}
   * @default 16
   */
  Object.defineProperty(this, 'updateInterval', {
    enumerable: true,
    get: function () {
      return (self._updateInterval);
    },
    set: function (value) {

      if (value % 8 !== 0) { value = 16; }

      self._updateInterval = parseInt(value);
      if (self.ready) {
        self._sendPck(self._makePckString('DataRate'), self._updateInterval, true);
      }
    }
  });

  /** @private */
  this._updateInterval = 8;

  /** @private */
  this._validGains = {
    "1": 1,
    "8": 2,
    "16": 3,
    "32": 4,
    "64": 5,
    "128": 6
  }

};

util.inherits(PhidgetBridge, Phidget);

/**
 * Sets the gain of a specific sensor (or array of sensors). Valid values are 1, 8, 16,
 * 32, 64 or 128. The highest the gain, the better the resolution. For that reason, it’s
 * best to use the highest gain possible that can still measure the full range of your
 * sensor.
 *
 * @method setGain
 * @param index {Number|Array} The sensor's number (or an array of sensor numbers)
 * @param [value=1] {Number} The gain to assign (1, 8, 16, 32, 64 or 128)
 * @returns {PhidgetBridge} Returns the PhidgetBridge to allow method chaining.
 * @chainable
 */
PhidgetBridge.prototype.setGain = function(index, value) {

  var self = this;
  if (self.ready !== true) { return self; }

  var normalized = 7; // 7 means unknown according to the API
  index = [].concat(index);

  if (self._validGains[value]) {
    normalized = self._validGains[value];
  } else {
    value = undefined;
  }

  for (var i = 0; i < index.length; i++) {
    var pos = parseInt(index[i]);
    if (!self.sensors[pos]) { self.sensors[pos] = {}; }
    self.sensors[pos].gain = value;
    self._sendPck(self._makePckString('Gain', pos), normalized, true);
  }

  return self;

};

/**
 *
 * Enables or disables a sensor. Beware that this setting remains even when the bridge is
 * powered off.
 *
 * @method setEnabled
 * @param index {int|Array} The sensor's number (or an array of sensor numbers)
 * @param [value=true] {Boolean} The status to set
 * @returns {PhidgetBridge} Returns the PhidgetBridge to allow method chaining.
 * @chainable
 */
PhidgetBridge.prototype.setEnabled = function(index, value) {

  var self = this;
  if (self.ready !== true) { return self; }

  index = [].concat(index);
  value = (value === true);

  for (var i = 0; i < index.length; i++) {
    var pos = parseInt(index[i]);
    if (!self.sensors[pos]) { self.sensors[pos] = {}; }
    self.sensors[pos].enabled = value;

    // Values cannot be received when the input is disabled
    self.sensors[pos].value = value ? 0 : null;

    self._sendPck(self._makePckString('Enabled', pos), value ? 1 : 0, true);
  }

  return self;

};

PhidgetBridge.prototype._setPhidgetSpecificInitialState = function () {};

/** @private See overridden method for details. */
PhidgetBridge.prototype._parsePhidgetSpecificData = function (data) {

  var self = this;

  if (data.keyword === 'BridgeValue') {

    /**
     * Event emitted when sensor data is received.
     *
     * @event sensor
     * @param {PhidgetBridge} emitter The actual PhidgetBridge object that emitted the
     * event.
     * @param {Object} data An object containing the sensor data and related information
     * @param {int} data.index The sensor's index number
     * @param {Number} data.value The sensor's received value
     */
    if (!self.sensors[data.index]) { self.sensors[data.index] = {}; }
    self.sensors[data.index].value = parseFloat(data.value);
    self.ready && self.emit(
      "sensor",
      self,
      {
        "index": data.index,
        "value": self.sensors[data.index].value
      }
    );

  } else if (data.keyword === 'Enabled') {
    if (!self.sensors[data.index]) { self.sensors[data.index] = {}; }
    self.sensors[data.index].enabled = (parseInt(data.value) === 1);
    self.sensors[data.index].value = self.sensors[data.index].enabled ? 0 : null;
  } else if (data.keyword === 'Gain') {
    if (!self.sensors[data.index]) { self.sensors[data.index] = {}; }
    self.sensors[data.index].gain = parseInt(data.value);
  } else if (data.keyword === 'BridgeMin') {
    if (!self.sensors[data.index]) { self.sensors[data.index] = {}; }
    self.sensors[data.index].min = parseFloat(data.value);
  } else if (data.keyword === 'BridgeMax') {
    if (!self.sensors[data.index]) { self.sensors[data.index] = {}; }
    self.sensors[data.index].max = parseFloat(data.value);
  } else if (data.keyword === 'NumberOfInputs') {
    self.sensors.count = parseInt(data.value);
  } else if (data.keyword === 'DataRate') {
    self.updateInterval = parseInt(data.value);
  } else if (data.keyword === 'DataRateMax') {
    self._shortestUpdateInterval = parseInt(data.value);
  } else if (data.keyword === 'DataRateMin') {
    self._longestUpdateInterval = parseInt(data.value);
  }

};

module.exports.PhidgetBridge = PhidgetBridge;










/****************************************************************************************/
/************************************  PhidgetStepper  **********************************/
/****************************************************************************************/

/**
 * The `PhidgetStepper` class is used to control the stepper motors connected to a
 * PhidgetStepper board. It can be used to set a target position for the motors while
 * controlling their maximum velocity and acceleration.
 *
 * Here is a simple example that moves the motor connected to output 0 to its 200 position
 * and then brings it back to position 0. Before making it move, you must first "engage"
 * the motor. Also, you should always explicitely set the desired acceleration and
 * velocity (speed).
 *
 *     var phidgets = require('phidgets');
 *     var ps = new phidgets.PhidgetStepper();
 *
 *     ps.addListener("opened", onReady);
 *
 *     function onReady() {
 *
 *        // Engage motor and set desired properties
 *        ps.engageMotor(0, true);
 *        ps.setAcceleration(0, ps.maximumAcceleration);
 *        ps.setTargetVelocity(0, ps.maximumVelocity);
 *
 *        // Makes the motor move
 *        ps.setTargetPosition(0, 200);
 *
 *        // Triggered when the target position is reached
 *        ps.once("target", function(e, data) {
 *          ps.setTargetPosition(0, 0);
 *        });
 *
 *     }
 *
 *     ps.open();
 *
 * The `PhidgetStepper` object adds 5 events to the basic ones inherited by all Phidgets.
 * They are:
 *
 *  * `position`: triggered each time the motor's position changes
 *  * `start`: triggered when a motor starts moving
 *  * `stop`: triggered when a motor stops moving
 *  * `target`: triggered when the motor has reached its target position
 *  * `input`: triggered when a digital input changes (not available on all boards)
 *
 * This object extends the `Phidget` object which itself extends Node.js'
 * [`events.EventEmitter` object](https://nodejs.org/api/events.html#events_class_events_eventemitter).
 * See that object's documentation for more inherited methods and properties.
 *
 * @class PhidgetStepper
 * @constructor
 * @extends Phidget
 */
var PhidgetStepper = function() {
  PhidgetStepper.super_.call(this, 'PhidgetStepper');

  var self = this;

  /**
   * The duration (in milliseconds) between update notifications (must be multiple of 8).
   * The shorter the interval is, the more frequent the updates will be sent by the
   * device.
   *
   * @property updateInterval {int}
   * @default 16
   */
  Object.defineProperty(this, 'updateInterval', {
    enumerable: true,
    get: function () {
      return (self._updateInterval);
    },
    set: function (value) {

      if (value % 8 !== 0) { value = 16; }

      self._updateInterval = parseInt(value);
      if (self.ready) {
        self._sendPck(self._makePckString('DataRate'), self._updateInterval, true);
      }
    }
  });

  /**
   * [Read-only] The minimum acceleration value that can be set on outputs
   * @property minimumAcceleration {Number}
   */
  Object.defineProperty(this, 'minimumAcceleration', {
    enumerable: true,
    get: function () {
      return (self._accelerationMin);
    }
  });

  /**
   * [Read-only] The maximum acceleration value that can be set on outputs
   * @property maximumAcceleration {Number}
   */
  Object.defineProperty(this, 'maximumAcceleration', {
    enumerable: true,
    get: function () {
      return (self._accelerationMax);
    }
  });

  /**
   * [Read-only] The minimum position that an output can travel to.
   * @property minimumPosition {Number}
   */
  Object.defineProperty(this, 'minimumPosition', {
    enumerable: true,
    get: function () {
      return (self._positionMin);
    }
  });

  /**
   * [Read-only] The maximum position that an output can travel to.
   * @property maximumPosition {Number}
   */
  Object.defineProperty(this, 'maximumPosition', {
    enumerable: true,
    get: function () {
      return (self._positionMax);
    }
  });

  /**
   * [Read-only] The minimum velocity that an output can be set to.
   * @property minimumVelocity {Number}
   */
  Object.defineProperty(this, 'minimumVelocity', {
    enumerable: true,
    get: function () {
      return (self._velocityMin);
    }
  });

  /**
   * [Read-only] The maximum velocity that an output can be set to.
   * @property maximumVelocity {Number}
   */
  Object.defineProperty(this, 'maximumVelocity', {
    enumerable: true,
    get: function () {
      return (self._velocityMax);
    }
  });

  /**
   * [Read-only] The minimum current that an output can be set to. Current limits are not
   * supported by all stepper controllers.
   *
   * @property minimumCurrent {Number}
   */
  Object.defineProperty(this, 'minimumCurrent', {
    enumerable: true,
    get: function () {
      return (self._currentMin);
    }
  });

  /**
   * [Read-only] The maximum current that an output can be set to. Current limits are not
   * supported by all stepper controllers.
   *
   * @property maximumCurrent {Number}
   */
  Object.defineProperty(this, 'maximumCurrent', {
    enumerable: true,
    get: function () {
      return (self._currentMax);
    }
  });

  /**
   * [read-only] An object containing information about the motor outputs of the device.
   * Here are a few examples of how to retrieve information in that object:
   *
   *     PhidgetStepper.outputs[5].currentPosition  // Motor 5's current position
   *     PhidgetStepper.outputs[5].stopped          // Is motor 5 stopped?
   *     PhidgetStepper.outputs.count               // Total number of outputs on the device
   *
   * @property outputs {Object}
   *
   * @property outputs.count {int} The total number of outputs on the device.
   *
   * @property outputs[int].position {Number} The position of the motor hooked up to that
   * output. This value remains between sessions. It is used when calculating the movement
   * needed to reach the target position. It can be manually set with `setPosition()`.
   *
   * @property outputs[int].targetPosition {Number} The last set target position.
   *
   * @property outputs[int].acceleration {Number} The last set acceleration value (also
   * used as deceleration value). This property should be set as part of initialization
   * because otherwise, it will remain unknown.
   *
   * @property outputs[int].currentLimit {Number} The last set current limit. Current
   * limit is not supported by all stepper controllers.
   *
   * @property outputs[int].current {Number} The actual current draw for the motor
   * connected to that output. Current sense is not supported by all stepper controllers.
   *
   * @property outputs[int].targetVelocity {Number} The desired velocity (speed) for the
   * motor on that output. Sometimes referred to as the "velocity limit".
   *
   * @property outputs[int].velocity {Number} The actual current velocity for the motor
   * on that output.
   *
   * @property outputs[int].engaged {Boolean} The engaged state. This is whether or not
   * the motor connected to the output is currently powered.
   *
   * @property outputs[int].stopped {Boolean} Whether the motor connected to that output
   * is currently stopped. If this is true, it indicates that the motor is not moving, and
   * there are no outstanding commands.
   *
   */
  this.outputs = {};

  /**
   * [read-only] An object containing information about the digital inputs of the
   * PhidgetStepper board. If `PhidgetStepper.inputs.count` equals 0, it simply means that
   * your board does not have any digital inputs.
   *
   *     PhidgetStepper.inputs[0].value  // Digital input 0's boolean value
   *     PhidgetStepper.outputs.count    // Number of digital inputs on the device
   *
   * @property inputs {Object}
   *
   * @property inputs.count {int} The total number of digital inputs on the device.
   *
   * @property outputs[int].value {Boolean} The current boolean value of the input.
   */
  this.inputs = {};

  /** @private */
  this._updateInterval = 8;

  /** @private */
  this._positionMin = undefined;

  /** @private */
  this._positionMax = undefined;

  /** @private */
  this._accelerationMin = undefined;

  /** @private */
  this._accelerationMax = undefined;

  /** @private */
  this._velocityMin = undefined;

  /** @private */
  this._velocityMax = undefined;

  /** @private */
  this._currentMin = undefined;

  /** @private */
  this._currentMax = undefined;

};

util.inherits(PhidgetStepper, Phidget);

/**
 * Starts or stops power from being sent to the motor connected to a specific output (or
 * array of outputs). By default, outputs do not power connected motors. Before moving
 * the motor, you must therefore engage the motor first.
 *
 * To reduce the motor's power consumption, you can disengage it once it's reached its
 * target position. If you are concerned about keeping accurate track of position, the
 * motor should not be disengaged until the motor is stopped.
 *
 * @method engageMotor
 *
 * @param index {int|Array} The motor's index number (or an array of motor numbers)
 * @param value {Boolean} The boolean status to use.
 *
 * @returns {PhidgetStepper} Returns the PhidgetStepper object to allow method chaining.
 *
 * @chainable
 */
PhidgetStepper.prototype.engageMotor = function(index, value) {

  var self = this;
  if (self.ready !== true) { return self; }

  index = [].concat(index);
  value = (value === true);

  for (var i = 0; i < index.length; i++) {

    var pos = parseInt(index[i]);
    if (!self.outputs[pos]) { self.outputs[pos] = {}; }
    self.outputs[pos].engaged = value;
    self._sendPck(self._makePckString('Engaged', pos), value ? 1 : 0, true);

  }

  return self;

};

/**
 * Sets the acceleration for the motor connected to the specified output. The motor will
 * both accelerate and decelarate at this rate. For the 1062 board, this is specified in
 * half-steps.
 *
 * The minimum and maximum acceleration values can be viewed in the `minimumAcceleration`
 * and `maximumAcceleration` properties.
 *
 * The acceleration should be explicitely set as part of initialization because otherwise
 * it will remain unknown.
 *
 * @method setAcceleration
 *
 * @param index {int|Array} The output's index number (or an array of output numbers)
 * @param [value] {Number} The desired acceleration specified in half-steps. If not
 * specified (or invalid), the maximum acceleration will be used.
 *
 * @returns {PhidgetStepper} Returns the PhidgetStepper to allow method chaining.
 *
 * @chainable
 */
PhidgetStepper.prototype.setAcceleration = function(index, value) {

  var self = this;
  if (self.ready !== true) { return self; }

  index = [].concat(index);
  value = parseFloat(value) || self._accelerationMax;

  for (var i = 0; i < index.length; i++) {

    var pos = parseInt(index[i]);
    if (!self.outputs[pos]) { self.outputs[pos] = {}; }
    self.outputs[pos].acceleration = value;
    self._sendPck(self._makePckString('Acceleration', pos), value, true);

  }

  return self;

};

/**
 * Sets the target speed (velocity) for the motor connected to the specified output (or
 * array of outputs). If the `targetVelocity` is set to 0, the motor will not move.
 *
 * Note that this is not necessarily the speed that the motor is being turned at. The
 * motor is accelerated towards the target velocity and then decelerated as it approaches
 * the target position. If the target position is close enough, it may never reach the
 * target velocity.
 *
 * @method setTargetVelocity
 *
 * @param index {int|Array} The output's index number (or an array of output numbers)
 * @param [value] {Number} The desired velocity specified in half-steps. If not specified
 * (or invalid), the maximum velocity will be used.
 *
 * @returns {PhidgetStepper} Returns the PhidgetStepper to allow method chaining.
 * @chainable
 */
PhidgetStepper.prototype.setTargetVelocity = function(index, value) {

  var self = this;
  if (self.ready !== true) { return self; }

  index = [].concat(index);
  value = parseFloat(value);
  if (isNaN(value)) { value =  self._velocityMax; }

  for (var i = 0; i < index.length; i++) {

    var pos = parseInt(index[i]);
    if (!self.outputs[pos]) { self.outputs[pos] = {}; }
    self.outputs[pos].targetVelocity = value;
    self._sendPck(self._makePckString('VelocityLimit', pos), value, true);

  }

  return self;

};

/**
 * Sets the current position of the motor connected to the specified output (or array of
 * outputs). Setting the position does not actually move the motor, it merely sets the
 * reference that will be used when moving to a target position.
 *
 * @method setPosition
 *
 * @param index {int|Array} The output's index number (or an array of output numbers).
 *
 * @param [value] {Number} The desired velocity specified in half-steps. If not specified
 * (or invalid), the maximum velocity will be used.
 *
 * @returns {PhidgetStepper} Returns the PhidgetStepper to allow method chaining.
 *
 * @chainable
 */
PhidgetStepper.prototype.setPosition = function(index, value) {

  var self = this;
  if (self.ready !== true) { return self; }

  index = [].concat(index);
  if (index < 0 || index >= self.outputs.count) {
    throw new Error("No such output.");
  }

  value = parseFloat(value);
  if (isNaN(value)) { value =  self._velocityMax; }

  for (var i = 0; i < index.length; i++) {

    var pos = parseInt(index[i]);
    if (!self.outputs[pos]) { self.outputs[pos] = {}; }
    self.outputs[pos].position = value;
    self._sendPck(self._makePckString('CurrentPosition', pos), value, true);

  }

  return self;

};

/**
 * Sets a new target position for the motor connected to the specified output. The motor
 * will immediately start moving towards this position.
 *
 * Note that calling `setTargetPosition()` will override a previous call to
 * `setTargetPosition()` and the motor will begin tracking to the new position
 * immediately. The velocity of the motor will be ramped appropriately.
 *
 * @method setTargetPosition
 * @param index {int|Array} The output's index number (or an array of output numbers)
 * @param [value=1000] {Number} The target position specified in half-steps.
 * @returns {PhidgetStepper} Returns the PhidgetStepper to allow method chaining.
 * @chainable
 */
PhidgetStepper.prototype.setTargetPosition = function(index, value) {

  var self = this;
  if (self.ready !== true) { return self; }

  index = [].concat(index);
  value = parseFloat(value);
  if (isNaN(value)) { value =  1000; }

  for (var i = 0; i < index.length; i++) {

    var pos = parseInt(index[i]);
    if (!self.outputs[pos]) { self.outputs[pos] = {}; }
    self.outputs[pos].targetPosition = value;
    self._sendPck(self._makePckString('TargetPosition', pos), value, true);

  }

  return self;

};

/**
 * Sets the upper current limit for the motor connected to the specified output. Note that
 * not all stepper controllers support current limiting.
 *
 * @method setCurrentLimit
 * @param index {int|Array} The output's index number (or an array of output numbers)
 * @param value {Number} The target position specified in half-steps.
 * @returns {PhidgetStepper} Returns the PhidgetStepper to allow method chaining.
 * @chainable
 */
PhidgetStepper.prototype.setCurrentLimit = function(index, value) {

  var self = this;
  if (self.ready !== true) { return self; }

  index = [].concat(index);
  value = parseFloat(value);

  for (var i = 0; i < index.length; i++) {

    var pos = parseInt(index[i]);
    if (!self.outputs[pos]) { self.outputs[pos] = {}; }
    self.outputs[pos].currentLimit = value;
    self._sendPck(self._makePckString('CurrentLimit', pos), value, true);

  }

  return self;

};

PhidgetStepper.prototype._setPhidgetSpecificInitialState = function () {};

/** @private See overridden method for details. */
PhidgetStepper.prototype._parsePhidgetSpecificData = function (data) {

  var self = this;

  if (data.keyword === 'AccelerationMin') {
    self._accelerationMin = parseFloat(data.value);
  } else if (data.keyword === 'AccelerationMax') {
    self._accelerationMax = parseFloat(data.value);
  } else if (data.keyword === 'CurrentMin') {
    self._currentMin = parseFloat(data.value);
  } else if (data.keyword === 'CurrentMax') {
    self._currentMax = parseFloat(data.value);
  } else if (data.keyword === 'PositionMin') {
    self._positionMin = parseFloat(data.value);
  } else if (data.keyword === 'PositionMax') {
    self._positionMax = parseFloat(data.value);
  } else if (data.keyword === 'VelocityMin') {
    self._velocityMin = parseFloat(data.value);
  } else if (data.keyword === 'VelocityMax') {
    self._velocityMax = parseFloat(data.value);

  } else if (data.keyword === 'NumberOfInputs') {
    self.inputs.count = parseInt(data.value);

  } else if (data.keyword === 'Input') {

    if (!self.inputs[data.index]) { self.inputs[data.index] = {}; }
    self.inputs[data.index].value = (data.value === '1');

    /**
     * Event emitted when digital input data is received.
     *
     * @event input
     * @param {PhidgetStepper} emitter The actual PhidgetStepper object that emitted the
     * event.
     * @param {Object} data An object containing the input data and related information
     * @param {int} data.index The input's index number
     * @param {Boolean} data.value The input's received value
     */
    self.ready && self.emit(
        "input",
        self,
        {
          "index": data.index,
          "value": self.inputs[data.index].value
        }
    );

  } else if (data.keyword === 'NumberOfMotors') {
    self.outputs.count = parseInt(data.value);

    // Reset reference position and (by the same token) target position. Also set 'engage'
    // status to false.
    for (var i = 0; i < data.value; i++) {
      self._sendPck(self._makePckString('CurrentPosition', i), 0, true);
      self._sendPck(self._makePckString('TargetPosition', i), 0, true);
      self._sendPck(self._makePckString('Engaged', i), 0, true);
    }

  } else if (data.keyword === 'CurrentPosition') {
    if (!self.outputs[data.index]) { self.outputs[data.index] = {}; }
    self.outputs[data.index].position = parseInt(data.value);

    /**
     * Event emitted to report that the position of a motor connected to one of the
     * board's outputs has changed.
     *
     * @event position
     * @param {PhidgetStepper} emitter The actual PhidgetStepper object that emitted the
     * event.
     * @param {Object} data An object containing information about the event.
     * @param {int} data.index The output's index number.
     * @param {Number} data.index The motor's new position.
     */
    self.ready && self.emit(
        "position",
        self,
        {
          "index": data.index,
          "position": self.outputs[data.index].position
        }
    );

  } else if (data.keyword === 'TargetPosition') {
    if (!self.outputs[data.index]) { self.outputs[data.index] = {}; }
    self.outputs[data.index].targetPosition = parseInt(data.value);

  } else if (data.keyword === 'Acceleration') {
    if (!self.outputs[data.index]) { self.outputs[data.index] = {}; }
    self.outputs[data.index].acceleration = parseFloat(data.value);

  } else if (data.keyword === 'CurrentLimit') {
    if (!self.outputs[data.index]) { self.outputs[data.index] = {}; }
    self.outputs[data.index].currentLimit = parseFloat(data.value); // value larger than int's maximum

  } else if (data.keyword === 'Current') {
    if (!self.outputs[data.index]) { self.outputs[data.index] = {}; }
    self.outputs[data.index].current = parseFloat(data.value);
    // dispatch event ?!

  } else if (data.keyword === 'VelocityLimit') {
    if (!self.outputs[data.index]) { self.outputs[data.index] = {}; }
    self.outputs[data.index].targetVelocity = parseFloat(data.value);

  } else if (data.keyword === 'Velocity') {
    if (!self.outputs[data.index]) { self.outputs[data.index] = {}; }
    self.outputs[data.index].velocity = parseFloat(data.value);
    // dispatch event ?!

  } else if (data.keyword === 'Engaged') {
    if (!self.outputs[data.index]) { self.outputs[data.index] = {}; }
    self.outputs[data.index].engaged = (parseInt(data.value) === 1);

  } else if (data.keyword === 'Stopped') {
    if (!self.outputs[data.index]) { self.outputs[data.index] = {}; }

    // The board continuously reports that the board is NOT stopped. To trigger the event
    // the last time only, we must check if the value is different
    if (self.outputs[data.index].stopped === (parseInt(data.value) === 1)) {
      return;
    }

    self.outputs[data.index].stopped = (parseInt(data.value) === 1);

    // The order is important. "target" event must be dispatched before start and stop
    // events.
    if (
        self.outputs[data.index].stopped &&
        self.outputs[data.index].position ===
        self.outputs[data.index].targetPosition
    ) {

      /**
       * Event emitted when a motor connected to one of the board's outputs has reached
       * its target position.
       *
       * @event target
       * @param {PhidgetStepper} emitter The actual PhidgetStepper object that emitted the
       * event.
       * @param {Object} data An object containing information about the event.
       * @param {int} data.index The output's index number.
       * @param {int} data.position The motor's position
       */
      self.ready && self.emit(
          "target",
          self,
          {
            "index": data.index,
            "position": self.outputs[data.index].position
          }
      );

    }

    /**
     * Event emitted when a motor connected to one of the board's outputs starts moving.
     *
     * @event start
     * @param {PhidgetStepper} emitter The actual PhidgetStepper object that emitted the
     * event.
     * @param {Object} data An object containing information about the event.
     * @param {int} data.index The output's index number.
     */
    /**
     * Event emitted when a motor connected to one of the board's outputs stops moving.
     *
     * @event stop
     * @param {PhidgetStepper} emitter The actual PhidgetStepper object that emitted the
     * event.
     * @param {Object} data An object containing information about the event.
     * @param {int} data.index The output's index number.
     */
    self.ready && self.emit(
        self.outputs[data.index].stopped ? "stop" : "start",
        self,
        {
          "index": data.index,
          "position": self.outputs[data.index].position
        }
    );

  }

};

module.exports.PhidgetStepper = PhidgetStepper;









/****************************************************************************************/
/************************************  PhidgetRFID  *************************************/
/****************************************************************************************/

/**
 * The `PhidgetRFID` class allows you to use a PhidgetRFID board to read and write (if the board
 * supports if) RFID tags. The PhidgetRFID board supports 3 protocols:
 *
 *  - EM4100/EM4102 40-bit
 *  - ISO11785 FDX-B encoding, Animal ID
 *  - PhidgetsTAG Protocol 24 character ASCII
 *
 * Please note that the antenna must be activated for the `PhidgetRFID` to report tag reads. Here's
 * an example of how to use this object to read from a tag:
 *
 *     var phidgets = require("Phidgets");
 *
 *     var pRFID = new phidgets.PhidgetRFID()
 *         .on('opened', function(emitter, data) {
 *             emitter.antenna = true;
 *             console.log("Device ready. Antenna activated.");
 *         })
 *         .open();
 *
 * This object extends the `Phidget` object which itself extends Node.js'
 * [`events.EventEmitter` object](https://nodejs.org/api/events.html#events_class_events_eventemitter).
 * See that object's documentation for inherited methods.
 *
 * @class PhidgetRFID
 * @constructor
 * @extends Phidget
*/
var PhidgetRFID = function() {

 PhidgetRFID.super_.call(this, "PhidgetRFID");

 var self = this;

  // Define some "static" properties
  Object.defineProperties(this, {

    /**
     * [read-only] Array of all protocols supported by the device.
     *
     * @property SUPPORTED_PROTOCOLS
     * @type Array
     * @static
     */
    SUPPORTED_PROTOCOLS: {
      value: [undefined, "EM4100", "ISO11785 FDX-B", "PhidgetTag"],
      writable: false,
      enumerable: true,
      configurable: false
    }

  });

 /**
  * [read-only] An object containing information about the digital outputs of the device.
  * Output 0 is also labeled on the board as "+5V". Output 1 is also labeled on the board
  * as "LED". This is not to be confused with the onboard LED. To control the onboard
  * LED, please use the `PhidgetRFID.led` property.
  *
  * Here is how to retrieve an output's value or the total number of outputs:
  *
  *     PhidgetRFID.outputs[1].value     // Output 1 current value
  *     PhidgetRFID.outputs.count        // Total number of outputs on the device
  *
  * @property outputs {Object}
  * @property outputs.count {int} The total number of outputs on the device.
  * @property outputs[int].value {int} The current value of the specified output.
  */
 this.outputs = {};

 /**
  * [read-only] Status of the reader. The two possible statuses are:
  *
  *   - waiting: no tag is being detected because none are present or because the antenna is off
  *   - detecting: a tag is currently being detected
  *
  * @property status {String}
  */
 this.status = "waiting";

 /**
  * [read-only] An object containing information about the last tag that was read.
  *
  * @property tag {Object}
  * @property tag.value {int} The tag's value.
  * @property tag.protocol {int} The tag's protocol.
  * @property tag.detectedAt {Date} A `Date` object representing the moment when the tag was read.
  * @property tag.lostAt {Date} A `Date` object representing the moment when the previously read
  * tag was lost.
  */
 this.tag = {
   value: 0,
   protocol: 0,
   detectedAt: undefined,
   lostAt: undefined
 };

 /**
  * The status of the onboard LED (not to be confused with the LED output). Setting this
  * property to `true` will turn on the onboard LED while setting it to `false` will turn it
  * off.
  *
  * @property led {Boolean}
  */
 Object.defineProperty(this, 'led', {
   enumerable: true,
   get: function () {
     return (self._led);
   },
   set: function(value) {
     self._sendPck(self._makePckString('LEDOn'), value ? 1 : 0, true);
     self._led = !!value;
   }
 });

 /**
  * The activity status of the RFID antenna. The antenna must be activated in order for
  * the device to work properly. Setting this property to `true` activates the antenna
  * while setting it to `false` deactivates it.
  *
  * @property antenna {Boolean}
  */
 Object.defineProperty(this, 'antenna', {
   enumerable: true,
   get: function () {
     return (self._antenna);
   },
   set: function(value) {
     self._sendPck(self._makePckString('AntennaOn'), value ? 1 : 0, true);
     self._antenna = !!value;
   }

 });

 this._led = false;
 this._antenna = false;

};

util.inherits(PhidgetRFID, Phidget);


PhidgetRFID.prototype._parsePhidgetSpecificData = function (data) {

 var self = this;

 if (data.keyword === "NumberOfOutputs") {

   self.outputs.count = parseInt(data.value);

 } else if (data.keyword === "LastTag") {

   self.tag.protocol = self.SUPPORTED_PROTOCOLS[data.value.split("/", 2)[0]];
   self.tag.value = data.value.split("/", 2)[1];
   self.tag.detectedAt = undefined;
   self.tag.lostAt = undefined;

 } else if (data.keyword === "Tag2") {

   self.tag.protocol = self.SUPPORTED_PROTOCOLS[data.value.split("/", 2)[0]];
   self.tag.value = data.value.split("/", 2)[1];
   self.tag.detectedAt = new Date();
   self.tag.lostAt = undefined;

   /**
    * Event emitted when a tag has been detected.
    *
    * @event detected
    * @param {PhidgetRFID} emitter The actual PhidgetRFID object that emitted the event.
    * @param {Object} tag
    * @param {Number} tag.protocol The tag's protocol. Supported protocols are: `ISO11785 FDX-B`,
    * `EM4100` and `PhidgetTag`.
    * @param {String} tag.value The tag's value.
    * @param {Date} tag.detectedAt The date and time when the tag was detected.
    * @param {Date} tag.lostAt The date and time when the tag was lost.
    */
   self.ready && self.emit("detected", self, self.tag);


 } else if (data.keyword === "TagLoss2") {

   self.tag.protocol = self.SUPPORTED_PROTOCOLS[data.value.split("/", 2)[0]];
   self.tag.value = data.value.split("/", 2)[1];
   self.tag.lostAt = new Date();

   /**
    * Event emitted when a previously detected tag is now lost
    *
    * @event lost
    * @param {PhidgetRFID} emitter The actual PhidgetRFID object that emitted the event.
    * @param {Object} tag
    * @param {Number} tag.protocol The tag's protocol. Supported protocols are: `ISO11785 FDX-B`,
    * `EM4100` and `PhidgetTag`.
    * @param {String} tag.value The tag's value.
    * @param {Date} tag.detectedAt The date and time when the tag was detected.
    * @param {Date} tag.lostAt The date and time when the tag was lost.
    */
   self.ready && self.emit("lost", self, self.tag);

 } else if (data.keyword === "TagState") {

   self.status = (data.value === '1') ? "detecting" : "waiting";

   /**
    * Event emitted when the device's status changes.
    *
    * @event status
    * @param {PhidgetRFID} emitter The actual PhidgetRFID object that emitted the event.
    * @param {String} status Status of the device (`detecting` or `waiting`).
    */
   self.emit(
     "status",
     self,
     self.status
   );

 } else if (data.keyword === "AntennaOn") {

   self._antenna = (data.value === '1');

   /**
    * Event emitted when the antenna's status changes.
    *
    * @event antenna
    * @param {PhidgetRFID} emitter The actual PhidgetRFID object that emitted the event.
    * @param {Boolean} status Status of the antenna. `true` means it is now "on" and `false` means
    * it is now "off".
    */
   self.emit(
     "antenna",
     self,
     self._led
   );

 } else if (data.keyword === "LEDOn") {

   self._led = (data.value === '1');

   /**
    * Event emitted when the onboard LED's status changes.
    *
    * @event led
    * @param {PhidgetRFID} emitter The actual PhidgetRFID object that emitted the event.
    * @param {Boolean} status Status of the LED. `true` means it is now "on" and `false` means it is
    * now "off".
    */
   self.emit(
     "led",
     self,
     self._led
   );

 } else if (data.keyword === "Output") {

   if (!self.outputs[data.index]) { self.outputs[data.index] = {}; }
   self.outputs[data.index].value = (data.value === '1');

   /**
    * Event emitted when an output's status has changed.
    *
    * @event output
    * @param {PhidgetRFID} emitter The actual PhidgetRFID object that emitted the event.
    * @param {Object} data An object containing the output data and related information
    * @param {int} data.index The output's index number
    * @param {int} data.value The output's new value
    */
   self.emit(
     "output",
     self,
     { "index": data.index, "value": self.outputs[data.index].value }
   );

 }

};

/**
 * Sets the specified output to active (`true`) or inactive (`false`). This method should only
 * be used after the board is 'opened'. Calling it before will fail silently.
 *
 * @method setOutput
 * @param index {int|Array} The output number to set (or array of output numbers)
 * @param [value=false] {Boolean} The value you wish to set the output to.
 * @returns {PhidgetRFID} Returns the `PhidgetRFID` object to allow method chaining.
 * @chainable
 */
PhidgetRFID.prototype.setOutput = function(index, value) {

  // var self = this;
  if (this.ready !== true) { return this; }

  index = [].concat(index);
  value = (value === true);
  var vOut = (value === true) ? 1 : 0;

  for (var i = 0; i < index.length; i++) {
    var pos = parseInt(index[i]);
    if (!this.outputs[pos]) { this.outputs[pos] = {}; }
    this.outputs[pos].value = value;
    this._sendPck(this._makePckString('Output', pos), vOut, true);
  }

  return this;

};

/**
 * Writes a tag. Please note that not all devices have write capacity. This method will be silently
 *
 * @method write
 * @param tag {String} The value that should be written to the card.
 * @param protocol {Number=1} An integer identifying the protocol to be used. 1 is EM4100, 2 is
 * ISO11785 FDX-B and 3 is PhidgetTag. Default is EM4100.
 * @param lock {Boolean=false} Whether the card should be prevented from being written again.
 * Default is false.
 *
 * @returns {PhidgetRFID} Returns the `PhidgetRFID` object to allow method chaining.
 * @chainable
 */
PhidgetRFID.prototype.write = function(tag, protocol, lock) {

  var intLock = (lock === true) ? 1 : 0;
  protocol = parseInt(protocol);
  if ( !(protocol >= 1 && protocol <= 3) ) { protocol = 1; }

  var write = protocol + "/" + intLock + "/" + tag;
  this._sendPck(this._makePckString('WriteTag'), write, true);

  return this;

};

module.exports.PhidgetRFID = PhidgetRFID;









/****************************************************************************************/
/*****************************  PhidgetTemperatureSensor  *******************************/
/****************************************************************************************/

/**
 * The `PhidgetTemperatureSensor` class allows you receive data from PhidgetTemperatureSensor
 * boards.
 *
 * As of this writing, this class has only been tested with a
 * [1048_0 - PhidgetTemperatureSensor 4-input](http://www.phidgets.com/products.php?product_id=1048),
 * but should be compatible with any PhidgetTemperatureSensor board.
 * 
 * ```JavaScript
 * var PhidgetTemperatureSensor = require('phidgets').PhidgetTemperatureSensor;
 * 
 * var pts = new PhidgetTemperatureSensor();
 * 
 * function onReady() {
 * 
 *    var inputIndex = 0;
 *    
 *    // set the ThermocoupleType to K
 *    pts.setThermocoupleType(inputIndex, PhidgetTemperatureSensor.THERMOCOUPLE_TYPES.TYPE_K);
 * 
 *    // receive temperature events when the temperature changes by at least 2 degrees Celsius
 *    // (default is 0.5)
 *    pts.setTemperatureChangeTrigger(inputIndex, 2);
 *    
 *    pts.on('temperature', function (emitter, data) {
 *      if (data.index === inputIndex) {
 *        console.log('Temperature: ' + data.value);
 *      }
 *    });
 * 
 * }
 * 
 * pts.addListener('opened', onReady);
 * 
 * pts.open();
 * ```
 * 
 * This object extends the `Phidget` object which itself extends Node.js'
 * [`events.EventEmitter` object](https://nodejs.org/api/events.html#events_class_events_eventemitter).
 * See that object's documentation for more inherited methods and properties.
 *
 * @class PhidgetTemperatureSensor
 * @constructor
 * @extends Phidget
 * @author Andrew Berger <andrew@andrewberger.net>
 */
var PhidgetTemperatureSensor = function() {
  PhidgetTemperatureSensor.super_.call(this, 'PhidgetTemperatureSensor');

  var self = this;

  /**
   * [read-only] The last known ambient temperature of the sensor (where the inputs connect to the
   * board), in degrees celsius.
   * 
   * @property ambientTemperature {Number}
   * @instance
   */
  Object.defineProperty(this, 'ambientTemperature', {
    enumerable: true,
    get: function() {
      return self._ambientTemperature;
    }
  });

  /**
   * [read-only] The highest possible ambient temperature value which can be returned by the sensor,
   * in degrees celsius.
   * 
   * @property ambientTemperatureMax {Number}
   * @instance
   */
  Object.defineProperty(this, 'ambientTemperatureMax', {
    enumerable: true,
    get: function() {
      return self._ambientTemperatureMax;
    }
  });

  /**
   * [read-only] The lowest possible ambient temperature value which can be returned by the sensor,
   * in degrees celsius.
   * 
   * @property ambientTemperatureMin {Number}
   * @instance
   */
  Object.defineProperty(this, 'ambientTemperatureMin', {
    enumerable: true,
    get: function() {
      return self._ambientTemperatureMin;
    }
  });

  /**
   * [read-only] An object containing data about each input on the device.
   *
   * @property inputs {Object}
   * 
   * @property inputs.count {int} The number of inputs available on the device.
   * 
   * @property inputs[int].temperature {Number} The input's last known temperature in degrees
   * celsius.
   * 
   * @property inputs[int].temperatureMax {Number} The highest temperature possible for the given
   * input, based on thermocouple type.
   * 
   * @property inputs[int].temperatureMin {Number} The lowest temperature possible for the given
   * input, based on thermocouple type.
   * 
   * @property inputs[int].trigger {Number} The input's temperature change trigger, in
   * degrees celsius.
   * 
   * @property inputs[int].potential {Number} The input's last known voltage.
   * 
   * @property inputs[int].potentialMax {Number} The highest voltage possible for the given input,
   * based on thermocouple type.
   * 
   * @property inputs[int].potentialMin {Number} The lowest voltage possible for the given input,
   * based on thermocouple type.
   * 
   * @property inputs[int].thermocoupleType {int} The type of thermocouple attached to the given
   * input. Value corresponds to a value from PhidgetTemperatureSensor.THERMOCOUPLE_TYPES.
   */
  this.inputs = {};

  /** @private */
  this._ambientTemperature = undefined;

  /** @private */
  this._ambientTemperatureMax = undefined;

  /** @private */
  this._ambientTemperatureMin = undefined;

};

util.inherits(PhidgetTemperatureSensor, Phidget);


/**
 * [read-only] An enum of supported thermocouple types. Support for other thermocouple types, and
 * voltage sources other than thermocouples in the valid range (between potentialMin and
 * potentialMax) can be achieved using potential.
 * 
 * @property THERMOCOUPLE_TYPES {Object}
 * @property THERMOCOUPLE_TYPES.TYPE_K {int} Integer value represent a K-type thermocouple.
 * @property THERMOCOUPLE_TYPES.TYPE_J {int} Integer value represent a J-type thermocouple.
 * @property THERMOCOUPLE_TYPES.TYPE_E {int} Integer value represent a E-type thermocouple.
 * @property THERMOCOUPLE_TYPES.TYPE_T {int} Integer value represent a T-type thermocouple.
 */
Object.defineProperty(PhidgetTemperatureSensor, 'THERMOCOUPLE_TYPES', {
  enumerable: true,
  writable: false,
  value: {
    TYPE_K: 1,
    TYPE_J: 2,
    TYPE_E: 3,
    TYPE_T: 4
  }
});

/** @private */
Object.defineProperty(PhidgetTemperatureSensor, '_thermocoupleTypesArray', {
  writable: false,
  value: [
    PhidgetTemperatureSensor.THERMOCOUPLE_TYPES.TYPE_J,
    PhidgetTemperatureSensor.THERMOCOUPLE_TYPES.TYPE_K,
    PhidgetTemperatureSensor.THERMOCOUPLE_TYPES.TYPE_E,
    PhidgetTemperatureSensor.THERMOCOUPLE_TYPES.TYPE_T
  ]
});

/**
 * Sets the change trigger for an input. This is the amount by which the sensed temperature must
 * change between temperature change events. By default this is set to 0.5. Setting trigger
 * to 0 will cause all temperature updates to fire events. This is helpful for applications that are
 * implementing their own filtering.
 * 
 * @method setTemperatureChangeTrigger
 * @param index {int|Array} The input's number (or an array of input numbers)
 * @param value {Number} The TemperatureChangeTigger value to set
 * @returns {PhidgetTemperatureSensor} Returns the PhidgetTemperatureSensor to allow method
 * chaining.
 * @chainable
 */
PhidgetTemperatureSensor.prototype.setTemperatureChangeTrigger = function(index, value) {

  var self = this;
  if (self.ready !== true) { return self; }

  value = Number(value);

  if (isNaN(value) || value < 0) {
    throw new Error('Trigger must be a floating-point number greater than or equal to 0');
  }

  index = [].concat(index);

  for (var i = 0; i < index.length; i++) {
    var pos = Number(index[i]);
    if (!self.inputs[pos]) { self.inputs[pos] = {}; }
    self.inputs[pos].temperatureChangeTrigger = value;
    self._sendPck(self._makePckString('Trigger', pos), value, true);
  }

  return self;
};


/**
 * Sets the thermocouple type for an input. The possible values are 'J', 'K', 'E', and 'T', or one
 * of the values in the PhidgetTemperatureSensor.THERMOCOUPLE_TYPES enum. Support for other
 * thermocouple types, and voltage sources other than thermocouples in the valid range (between
 * potentialMin and potentialMax) can be achieved using potential.
 * 
 * @method setThermocoupleType
 * @param index {int|Array} The input's index (or an array of input indices)
 * @param value {string|Number} The ThermocoupleType to set
 * @returns {PhidgetTemperatureSensor} Returns the PhidgetTemperatureSensor to allow method
 * chaining.
 * @chainable
 */
PhidgetTemperatureSensor.prototype.setThermocoupleType = function(index, value) {

  var self = this;
  if (self.ready !== true) { return self; }

  if (typeof value === 'string') {
    var propName = 'TYPE_' + value.toUpperCase();
    if (!PhidgetTemperatureSensor.THERMOCOUPLE_TYPES.hasOwnProperty(propName)) {
      throw new Error('Unsupported thermocouple type: `' + value + '`');
    }

    value = PhidgetTemperatureSensor.THERMOCOUPLE_TYPES[propName];
  } else {
    value = Number(value);

    if (isNaN(value) || (PhidgetTemperatureSensor._thermocoupleTypesArray.indexOf(value) < 0)) {
      throw new Error('ThermocoupleType must be one of PhidgetTemperatureSensor.THERMOCOUPLE_TYPES');
    }
  }

  index = [].concat(index);

  for (var i = 0; i < index.length; i++) {
    var pos = Number(index[i]);
    if (!self.inputs[pos]) { self.inputs[pos] = {}; }
    self.inputs[pos].thermocoupleType = value;
    self._sendPck(self._makePckString('ThermocoupleType', pos), value, true);
  }

  return self;
};

/** @private See overridden method for details. */
PhidgetTemperatureSensor.prototype._parsePhidgetSpecificData = function (data) {
  
  var self = this;

  // keywords which should result in an emitted event
  var emittableEvents = [

    // @TODO is this event necessary?
    /**
     * The sensor's ambient temperature has changed.
     *
     * @event ambientTemperature
     * @param {PhidgetTemperatureSensor} emitter The actual PhidgetTemperatureSensor object that
     * emitted the event.
     * @param {Object} data An object containing the sensor data and related information
     * @param {Number} data.value The sensor's new ambient temperature.
     */
    'AmbientTemperature',

    /**
     * The calculated temperature of the given input has changed by more than
     * temperatureChangeTrigger. This value is dependent on the sensor's ambient temperature, the
     * input's thermocouple type, and the input's potential.
     *
     * @event temperature
     * @param {PhidgetTemperatureSensor} emitter The actual PhidgetTemperatureSensor object that
     * emitted the event.
     * @param {Object} data An object containing the sensor data and related information
     * @param {int} data.index The input's index number
     * @param {Number} data.value The input's new temperature.
     */
    'Temperature',

    // @TODO it may be useful to return the last known AmbientTemperature with this event, as its
    // most likely use is in calculating temperature from a voltage source other than a J K E or T thermocouple.
    /**
     * The potential (voltage) of the given input has changed.
     *
     * @event potential
     * @param {PhidgetTemperatureSensor} emitter The actual PhidgetTemperatureSensor object that
     * emitted the event.
     * @param {Object} data An object containing the sensor data and related information
     * @param {int} data.index The input's index number
     * @param {Number} data.value The input's new potential.
     */
    'Potential'
  ];

  // keywords which relate to a specific input, and have a useful data.index value
  var inputSpecificEvents = [
    'Temperature',
    'TemperatureMax',
    'TemperatureMin',
    'Potential',
    'PotentialMax',
    'PotentialMin',
    'ThermocoupleType',
    'Trigger'
  ];

  // emitted event names and properties on each input must be camelCase
  var camelizedKeyword = data.keyword.charAt(0).toLowerCase() + data.keyword.slice(1);

  // the value emitted by an emittableEvent
  var eventData = {
    value: Number(data.value)
  };

  if (data.keyword === 'AmbientTemperature') {
    self._ambientTemperature = eventData.value;

  } else if (data.keyword === 'AmbientTemperatureMax') {
    self._ambientTemperatureMax = eventData.value;

  } else if (data.keyword === 'AmbientTemperatureMin') {
    self._ambientTemperatureMin = eventData.value;

  } else if (data.keyword === 'NumberOfSensors') {
    self.inputs.count = eventData.value;

  } else if (inputSpecificEvents.indexOf(data.keyword) >= 0) {
    if (!self.inputs[data.index]) { self.inputs[data.index] = {}; }

    eventData.index = data.index;

    self.inputs[data.index][camelizedKeyword] = eventData.value;

  } else {
    // unhandled event
    return;
  }

  if (self.ready && (emittableEvents.indexOf(data.keyword) >= 0)) {
    self.emit(camelizedKeyword, self, eventData);
  }
};

module.exports.PhidgetTemperatureSensor = PhidgetTemperatureSensor;