/**
* Copyright (c) 2011-2016 Jeff Hoefs <soundanalogous@gmail.com>
* Released under the MIT license. See LICENSE file for details.
*/
JSUTILS.namespace('BO.IOBoard');
BO.IOBoard = (function() {
var IOBoard;
// Private static constants:
// Message command bytes (128-255/0x80-0xFF)
var DIGITAL_MESSAGE = 0x90,
REPORT_ANALOG = 0xC0,
REPORT_DIGITAL = 0xD0,
ANALOG_MESSAGE = 0xE0,
SET_PIN_MODE = 0xF4,
SET_PIN_VALUE = 0xF5,
REPORT_VERSION = 0xF9,
SYSEX_RESET = 0xFF,
START_SYSEX = 0xF0,
END_SYSEX = 0xF7;
// Extended command set using sysex (0-127/0x00-0x7F)
var ANALOG_MAPPING_QUERY = 0x69,
ANALOG_MAPPING_RESPONSE = 0x6A,
CAPABILITY_QUERY = 0x6B,
CAPABILITY_RESPONSE = 0x6C,
PIN_STATE_QUERY = 0x6D,
PIN_STATE_RESPONSE = 0x6E,
EXTENDED_ANALOG = 0x6F,
SERVO_CONFIG = 0x70,
STRING_DATA = 0x71,
REPORT_FIRMWARE = 0x79,
SAMPLING_INTERVAL = 0x7A;
var MIN_SAMPLING_INTERVAL = 1,
MAX_SAMPLING_INTERVAL = 100,
MULTI_CLIENT = "multiClient";
// Dependencies
var Pin = BO.Pin,
EventDispatcher = JSUTILS.EventDispatcher,
PinEvent = BO.PinEvent,
IOBoardEvent = BO.IOBoardEvent;
/**
* Creates an interface to the I/O board. The IOBoard object brokers
* the communication between your application and the physical I/O board.
* Currently you can only connect to a single I/O board per computer.
* However you could connect to multiple I/O boards if they are attached to
* multiple computers on your network. In that case you would create a
* separate IOBoard instance for each board you are connecting to in your
* network.
*
* @class IOBoard
* @constructor
* @uses JSUTILS.EventDispatcher
* @param {String} host The host address of the web server.
* @param {Number} port The port to connect to on the web server.
* Default = false.
* @param {String} protocol [optional] The websockt protocol definition
* (if necessary).
*/
IOBoard = function(host, port, protocol) {
"use strict";
this.name = "IOBoard";
// Private properties
this._inputDataBuffer = [];
this._digitalPort = [];
this._numPorts = 0;
this._numDigitialIOPins = 0;
this._analogPinMapping = [];
this._digitalPinMapping = [];
this._i2cPins = [];
this._ioPins = [];
this._totalPins = 0;
this._totalAnalogPins = 0;
this._samplingInterval = 19; // Default sampling interval
this._isReady = false;
this._firmwareName = "";
this._firmwareVersion = 0;
this._protocolVersion = 0;
this._isMultiClientEnabled = false;
this._isConfigured = false;
this._capabilityQueryResponseReceived = false;
this._debugMode = BO.enableDebugging;
this._numPinStateRequests = 0;
this._boardCapabilities = Object.create(null);
this._evtDispatcher = new EventDispatcher(this);
// bind event handlers to this
this.initialVersionResultHandler = this.onInitialVersionResult.bind(this);
this.sendOutHandler = this.sendOut.bind(this);
this.socketConnectionHandler = this.onSocketConnection.bind(this);
this.socketMessageHandler = this.onSocketMessage.bind(this);
this.socketClosedHandler = this.onSocketClosed.bind(this);
this._socket = new BO.WSocketWrapper(host, port, protocol);
this._socket.addEventListener(BO.WSocketEvent.CONNECTED, this.socketConnectionHandler);
this._socket.addEventListener(BO.WSocketEvent.MESSAGE, this.socketMessageHandler);
this._socket.addEventListener(BO.WSocketEvent.CLOSE, this.socketClosedHandler);
};
IOBoard.prototype = {
constructor: IOBoard,
// Private methods:
/**
* A websocket connection has been established.
* @private
* @method onSocketConnection
*/
onSocketConnection: function(event) {
this.debug("debug: Socket Status: (open)");
this.dispatchEvent(new IOBoardEvent(IOBoardEvent.CONNECTED));
this.begin();
},
/**
* A websocket message has been received.
* @param {Object} event The message property is an array of one or
* more stringified bytes from the board or a config string from
* the server.
* @private
* @method onSocketMessage
*/
onSocketMessage: function(event) {
var message = event.message,
data = [],
len;
if (typeof message === "string") {
data = message.split(",");
} else {
data = message;
}
len = data.length;
for (var i = 0; i < len; i++) {
this.parseInputMessage(data[i]);
}
},
/**
* Determine if the incoming data is a config message or a byte.
* @param {String} data A string representing a config message or
* an 8-bit unsigned integer.
* @private
* @method parseInputMessage
*/
parseInputMessage: function(data) {
var pattern = /config/,
message = "";
// Check for config messages from the server
if (data.match && data.match(pattern)) {
// to do: update servers to send a JSON string
// then parse the string here
message = data.substr(data.indexOf(':') + 2);
this.processStatusMessage(message);
} else {
// We have data from the IOBoard
this.processInput(parseInt(data, 10));
}
},
/**
* Report that the websocket connection has been closed.
* @private
* @method onSocketClosed
*/
onSocketClosed: function(event) {
this.debug("debug: Socket Status: " + this._socket.readyState + " (Closed)");
this.dispatchEvent(new IOBoardEvent(IOBoardEvent.DISCONNECTED));
},
/**
* Request the firmware version from the IOBoard.
* @private
* @method begin
*/
begin: function() {
this.addEventListener(IOBoardEvent.FIRMWARE_VERSION, this.initialVersionResultHandler);
this.reportVersion();
this.reportFirmware();
},
/**
* On startup, Firmata reports its version. Make sure the version is
* 2.3 or greater before proceeding. If the Firmata version is < 2.3
* report this to the user (to do: throw appropriate error?).
*
* @private
* @method onInitialVersionResult
*/
onInitialVersionResult: function(event) {
var version = event.version * 10,
name = event.name,
self = this;
this.removeEventListener(IOBoardEvent.FIRMWARE_VERSION, this.initialVersionResultHandler);
this.debug("debug: Firmware name = " + name + ", Firmware version = " + event.version);
// Make sure the user has uploaded a version of Firmata implementing protocol version
// 2.3.0 or higher
if (this._protocolVersion >= 23) {
if (!this._isMultiClientEnabled) {
// reset IOBoard to its default state
this.systemReset();
// Delay to allow systemReset function to execute in StandardFirmata
setTimeout(function() {
self.queryCapabilities();
self.checkForQueryResponse();
}, 200);
} else {
this.queryCapabilities();
this.checkForQueryResponse();
}
} else {
var err = "error: You must upload StandardFirmata version 2.3 or greater from Arduino version 1.0 or higher";
console.log(err);
}
},
/**
* Check if a capability response was received. If not, assume that
* a custom sketch was loaded to the IOBoard and fire a READY event.
* @private
* @method checkForQueryResponse
*/
checkForQueryResponse: function() {
var self = this;
// If after 200ms a capability query response is not received,
// assume that the user is running a custom sketch that does
// not implement a capability query response.
// 200ms is sufficient for an Arduino Mega (current longest
// response time). Need to revisit when Arduino Due support is
// added to Firmata.
setTimeout(function() {
if (self._capabilityQueryResponseReceived === false) {
self.startup();
}
}, 200);
},
/**
* Process a status message from the websocket server
* @private
* @method processStatusMessage
*/
processStatusMessage: function(message) {
if (message === MULTI_CLIENT) {
this.debug("debug: Multi-client mode enabled");
this._isMultiClientEnabled = true;
}
},
/**
* Process input data from the IOBoard.
* @param {Number} inputData Number as an 8-bit unsigned integer
* @private
* @method processInput
*/
processInput: function(inputData) {
var len;
this._inputDataBuffer.push(inputData);
len = this._inputDataBuffer.length;
if (this._inputDataBuffer[0] >= 128 && this._inputDataBuffer[0] != START_SYSEX) {
if (len === 3) {
this.processMultiByteCommand(this._inputDataBuffer);
// Clear buffer
this._inputDataBuffer = [];
}
} else if (this._inputDataBuffer[0] === START_SYSEX && this._inputDataBuffer[len - 1] === END_SYSEX) {
this.processSysexCommand(this._inputDataBuffer);
// Clear buffer
this._inputDataBuffer = [];
} else if (inputData >= 128 && this._inputDataBuffer[0] < 128) {
// If for some reason we got a new command and there is already data
// in the buffer, reset the buffer
console.log("warning: Malformed input data... resetting buffer");
this._inputDataBuffer = [];
if (inputData !== END_SYSEX) {
this._inputDataBuffer.push(inputData);
}
}
},
/**
* Incoming data is either multibyte or sysex. Route multibyte
* data to the appropriate method.
*
* @private
* @method processMultiByteCommand
*/
processMultiByteCommand: function(commandData) {
var command = commandData[0],
channel;
if (command < 0xF0) {
command = command & 0xF0;
channel = commandData[0] & 0x0F;
}
switch (command) {
case DIGITAL_MESSAGE:
this.processDigitalMessage(channel, commandData[1], commandData[2]); //(LSB, MSB)
break;
case REPORT_VERSION:
this._protocolVersion = commandData[2] + commandData[1] * 10;
this.dispatchEvent(new IOBoardEvent(IOBoardEvent.PROTOCOL_VERSION), {
version: this._protocolVersion
});
break;
case ANALOG_MESSAGE:
this.processAnalogMessage(channel, commandData[1], commandData[2]);
break;
}
},
/**
* Processing incoming digital data. Parse the port number and value
* to determine if any digital input data has changed. Dispatch an
* event if the value has changed.
*
* @param {Number} port Digital data is sent per port. This does not
* align with the concept of a microcontroller port, but is a
* collection of 8 pins on the microcontroller.
*
* @param {Number} bits0_6 Bits 0 - 6 of the port value.
* @param {Number} bits7_13 Bits 7 - 13 of the port value.
* @private
* @method processDigitalMessage
*/
processDigitalMessage: function(port, bits0_6, bits7_13) {
var offset = port * 8,
lastPin = offset + 8,
portVal = bits0_6 | (bits7_13 << 7),
pinVal,
pin = {};
if (lastPin >= this._totalPins) {
lastPin = this._totalPins;
}
var j = 0;
for (var i = offset; i < lastPin; i++) {
pin = this.getDigitalPin(i);
// Ignore data send on Firmata startup
if (pin === undefined) {
return;
}
if (pin.getType() === Pin.DIN || pin.getType() === Pin.INPUT_PULLUP) {
pinVal = (portVal >> j) & 0x01;
if (pinVal != pin.value) {
pin.value = pinVal;
this.dispatchEvent(new IOBoardEvent(IOBoardEvent.DIGITAL_DATA), {
pin: pin
});
}
}
j++;
}
},
/**
* Process incoming analog data. The value is mapped from
* 0 - pin.analogReadResolution to a floating point value between
* 0.0 - 1.0.
*
* @private
* @method processAnalogMessage
*/
processAnalogMessage: function(channel, bits0_6, bits7_13) {
var analogPin = this.getAnalogPin(channel);
// NOTE: Is there a better way to handle this? This issue is on
// browser refresh the IOBoard board is still sending analog data
// if analog reporting was set before the refresh. Analog reporting
// won't be disabled by systemReset systemReset() is called. There
// is not a way to call that method fast enough so the following
// code is needed. An alternative would be to set a flag that
// prevents critical operations before systemReset has completed.
if (analogPin === undefined) {
return;
}
// scale according to the analog read resolution set for the pin
analogPin.value = this.getValueFromTwo7bitBytes(bits0_6, bits7_13) / analogPin.analogReadResolution;
if (analogPin.value != analogPin.lastValue) {
this.dispatchEvent(new IOBoardEvent(IOBoardEvent.ANALOG_DATA), {
pin: analogPin
});
}
},
/**
* Route the incoming sysex data to the appropriate method.
* @private
* @method processSysexCommand
*/
processSysexCommand: function(sysexData) {
// Remove the first and last element from the array
// since these are the START_SYSEX and END_SYSEX
sysexData.shift();
sysexData.pop();
var command = sysexData[0];
switch (command) {
case REPORT_FIRMWARE:
this.processQueryFirmwareResult(sysexData);
break;
case STRING_DATA:
this.processSysExString(sysexData);
break;
case CAPABILITY_RESPONSE:
this.processCapabilitiesResponse(sysexData);
break;
case PIN_STATE_RESPONSE:
this.processPinStateResponse(sysexData);
break;
case ANALOG_MAPPING_RESPONSE:
this.processAnalogMappingResponse(sysexData);
break;
default:
// Custom sysEx message
this.dispatchEvent(new IOBoardEvent(IOBoardEvent.SYSEX_MESSAGE), {
message: sysexData
});
break;
}
},
/**
* Construct the firmware name and version from incoming ascii data.
* @private
* @method processQueryFirmwareResult
*/
processQueryFirmwareResult: function(msg) {
var data;
for (var i = 3, len = msg.length; i < len; i += 2) {
data = msg[i];
data += msg[i + 1];
this._firmwareName += String.fromCharCode(data);
}
this._firmwareVersion = msg[1] + msg[2] / 10;
this.dispatchEvent(new IOBoardEvent(IOBoardEvent.FIRMWARE_VERSION), {
name: this._firmwareName,
version: this._firmwareVersion
});
},
/**
* Construct a String from an incoming ascii data.
* @private
* @method processSysExString
*/
processSysExString: function(msg) {
var str = "",
data,
len = msg.length;
for (var i = 1; i < len; i += 2) {
data = msg[i];
data += msg[i + 1];
str += String.fromCharCode(data);
}
this.dispatchEvent(new IOBoardEvent(IOBoardEvent.STRING_MESSAGE), {
message: str
});
},
/**
* Auto configure using capabilities response.
* This creates a configuration for any board in the Firmata boards.h
* file.
*
* @private
* @method processCapabilitiesResponse
*/
processCapabilitiesResponse: function(msg) {
// If running in multi-client mode and this client is already
// configured, ignore capabilities response
if (this._isConfigured) {
return;
}
var pinCapabilities = {},
byteCounter = 1, // Skip 1st byte because it's the command
pinCounter = 0,
analogPinCounter = 0,
mode,
resolution,
len = msg.length,
type,
pin;
this._numDigitialIOPins = 0;
this._capabilityQueryResponseReceived = true;
// Create default configuration
while (byteCounter <= len) {
// 127 denotes end of pin's modes
if (msg[byteCounter] == 127) {
// Is digital pin mapping even necessary anymore?
this._digitalPinMapping[pinCounter] = pinCounter;
type = undefined;
// Assign default types
if (pinCapabilities[Pin.DOUT]) {
// Map digital pins
type = Pin.DOUT;
}
if (pinCapabilities[Pin.AIN]) {
type = Pin.AIN;
// Map analog input pins
this._analogPinMapping[analogPinCounter++] = pinCounter;
}
pin = new Pin(pinCounter, type);
pin.setCapabilities(pinCapabilities);
this.managePinListener(pin);
this._ioPins[pinCounter] = pin;
// Store the 2 i2c pin numbers if they exist
// To Do: allow for more than 2 i2c pins on a board?
// How to identify SDA-SCL pairs in that case?
if (pin.getCapabilities()[Pin.I2C]) {
this._i2cPins.push(pin.number);
}
if (pinCapabilities[Pin.DOUT] || pinCapabilities[Pin.DIN]) {
this._numDigitialIOPins++;
}
pinCapabilities = {};
pinCounter++;
byteCounter++;
} else {
// Create capabilities object (mode: resolution) for each
// mode supported by each pin
mode = msg[byteCounter];
resolution = msg[byteCounter + 1];
if (typeof mode !== "undefined") {
this._boardCapabilities[mode] = true;
}
pinCapabilities[mode] = resolution;
byteCounter += 2;
}
}
// use the number of digital I/O pins rather than the total number of pins
// to calculate the tnumber of ports
this._numPorts = Math.ceil(this._numDigitialIOPins / 8);
this.debug("debug: Num ports = " + this._numPorts);
// Initialize port values
for (var j = 0; j < this._numPorts; j++) {
this._digitalPort[j] = 0;
}
this._totalPins = pinCounter;
this._totalAnalogPins = analogPinCounter;
this.debug("debug: Num pins = " + this._totalPins);
// Map the analog pins to the board pins
// This will map the IOBoard analog pin numbers (printed on IOBoard)
// to their digital pin number equivalents
this.queryAnalogMapping();
},
/**
* Map map analog pins to board pin numbers. Need to do this because
* the capability query does not provide the correct order of analog
* pins.
*
* @private
* @method processAnalogMappingResponse
*/
processAnalogMappingResponse: function(msg) {
// If running in multi-client mode and this client is
// already configured ignore analog mapping response
if (this._isConfigured) {
return;
}
var len = msg.length;
for (var i = 1; i < len; i++) {
if (msg[i] != 127) {
this._analogPinMapping[msg[i]] = i - 1;
this.getPin(i - 1).setAnalogNumber(msg[i]);
}
}
if (!this._isMultiClientEnabled) {
this.startup();
} else {
this.startupInMultiClientMode();
}
},
/**
* Single client mode is the default mode.
* Checking the "Enable multi-client" box in the Breakout Server UI to
* enable multi-client mode.
*
* @private
* @method startupInMultiClientMode
*/
startupInMultiClientMode: function() {
var len = this.getPinCount();
// Populate pins with the current IOBoard state
for (var i = 0; i < len; i++) {
this.queryPinState(this.getDigitalPin(i));
}
// Wait for the pin states to finish updating
setTimeout(this.startup.bind(this), 500);
this._isConfigured = true;
},
/**
* The IOBoard is configured and ready to send and accept commands.
* @private
* @method startup
*/
startup: function() {
this.debug("debug: IOBoard ready");
this._isReady = true;
this.enableDigitalPins();
this.dispatchEvent(new IOBoardEvent(IOBoardEvent.READY));
},
/**
* Resets the board to its default state without physically resetting
* the board.
*
* @private
* @method systemReset
*/
systemReset: function() {
this.debug("debug: System reset");
this.send(SYSEX_RESET);
},
/**
* Reads the current configuration of the requested pin. The following
* values are returned: 1: pin number, 2: pin type (0: DIN, 1: DOUT,
* 2: AIN, 3: AOUT / PWM, 4: SERVO, 5: SHIFT, 6: I2C), 3: pin state.
* The pin state for output modes is the value previously written
* to the pin. For input modes (AIN, DIN, etc) the state is typically
* zero (it is not the value that was written to the pin). For digital
* inputs the state is the status of the pullup resistor.
*
* @private
* @method processPinStateResponse
*/
processPinStateResponse: function(msg) {
// Ignore requests that were not made by this client
if (this._numPinStateRequests <= 0) {
return;
}
var len = msg.length,
pinNumber = msg[1],
pinType = msg[2],
pinState,
pin = this._ioPins[pinNumber];
if (len > 4) {
pinState = this.getValueFromTwo7bitBytes(msg[3], msg[4]);
} else if (len > 3) {
pinState = msg[3];
}
// update the pin type if it has changed
// typically this only happens when multiple clients are connecting
// to a single IOBoard. Each client (aside from the initial client)
// needs to get the current pin type
if (pin.getType() != pinType) {
pin.setType(pinType);
this.managePinListener(pin);
}
pin.setState(pinState);
this._numPinStateRequests--;
if (this._numPinStateRequests < 0) {
// should never happen, but just in case...
this._numPinStateRequests = 0;
}
this.dispatchEvent(new IOBoardEvent(IOBoardEvent.PIN_STATE_RESPONSE), {
pin: pin
});
},
/**
* Convert char to decimal value.
*
* @private
* @method toDec
*/
toDec: function(ch) {
ch = ch.substring(0, 1);
var decVal = ch.charCodeAt(0);
return decVal;
},
/**
* Called when ever a pin value is set via pin.value = someValue.
* Sends digital or analog output pin and output values to the IOBoard.
*
* @private
* @method sendOut
* @param {Event} event A reference to the event object (Pin in this
* case).
*/
sendOut: function(event) {
var type = event.target.getType(),
pinNum = event.target.number,
value = event.target.value;
switch (type) {
case Pin.DOUT:
this.sendDigitalData(pinNum, value);
break;
case Pin.AOUT:
this.sendAnalogData(pinNum, value);
break;
case Pin.SERVO:
this.sendServoData(pinNum, value);
break;
}
},
/**
* Ensure that event listeners are properly managed for pin objects
* as the pin type is changed during the execution of the program.
*
* @private
* @method managePinListener
*/
managePinListener: function(pin) {
if (pin.getType() == Pin.DOUT || pin.getType() == Pin.AOUT || pin.getType() == Pin.SERVO) {
if (!pin.hasEventListener(PinEvent.CHANGE)) {
pin.addEventListener(PinEvent.CHANGE, this.sendOutHandler);
}
} else {
if (pin.hasEventListener(PinEvent.CHANGE)) {
try {
pin.removeEventListener(PinEvent.CHANGE, this.sendOutHandler);
} catch (e) {
// Pin had reference to other handler, ignore
this.debug("debug: Caught pin removeEventListener exception");
}
}
}
},
/**
* Sends an analog value up to 14 bits on an analog pin number between
* 0 and 15. The value passed to this method should be in the range of
* 0.0 to 1.0. It is multiplied by the analog write (PWM) resolution
* set for the pin.
*
* @param {Number} pin The analog pin number.
* param {Number} value The value to send (0.0 to 1.0).
* @private
* @method sendAnalogData
*/
sendAnalogData: function(pin, value) {
var pwmResolution = this.getDigitalPin(pin).analogWriteResolution;
value *= pwmResolution;
value = (value < 0) ? 0 : value;
value = (value > pwmResolution) ? pwmResolution : value;
if (pin > 15 || value > Math.pow(2, 14)) {
this.sendExtendedAnalogData(pin, value);
} else {
this.send([ANALOG_MESSAGE | (pin & 0x0F), value & 0x007F, (value >> 7) & 0x007F]);
}
},
/**
* Sends an analog value > 14 bits and/or send a value for a pin number
* greater than 15.
* @param {Number} pin The analog pin number (up to 128).
* @param {Number} value The value to send (up to 16 bits).
* @private
* @method sendExtendedAnalogData
*/
sendExtendedAnalogData: function(pin, value) {
var analogData = [];
// If > 16 bits
if (value > Math.pow(2, 16)) {
var err = "error: Extended Analog values > 16 bits are not currently supported by StandardFirmata";
console.log(err);
throw err;
}
analogData[0] = START_SYSEX;
analogData[1] = EXTENDED_ANALOG;
analogData[2] = pin;
analogData[3] = value & 0x007F;
analogData[4] = (value >> 7) & 0x007F; // Up to 14 bits
// If > 14 bits
if (value >= Math.pow(2, 14)) {
analogData[5] = (value >> 14) & 0x007F;
}
analogData.push(END_SYSEX);
this.send(analogData);
},
/**
* Add the pin value to the appropriate digital port and send the
* updated digital port value.
*
* @param {Number} pin The digital pin number.
* @param {Number} value The value of the digital pin (0 or 1).
* @private
* @method sendDigitalData
*/
sendDigitalData: function(pin, value) {
var portNum = Math.floor(pin / 8);
if (value == Pin.HIGH) {
// Set the bit
this._digitalPort[portNum] |= (value << (pin % 8));
} else if (value == Pin.LOW) {
// Clear the bit
this._digitalPort[portNum] &= ~(1 << (pin % 8));
} else {
console.log("warning: Invalid value passed to sendDigital, value must be 0 or 1.");
return; // Invalid value
}
this.sendDigitalPort(portNum, this._digitalPort[portNum]);
},
/**
* Send the servo angle.
* @param {Number} pin The digital pin number the servo is attached to.
* @param {Number} value The angle to rotate to (0.0 to 1.0 mapped to 0 - 180).
* @private
* @method sendServoData
*/
sendServoData: function(pin, value) {
var servoPin = this.getDigitalPin(pin);
if (servoPin.getType() == Pin.SERVO && servoPin.lastValue != value) {
this.sendAnalogData(pin, value);
}
},
/**
* Query the cababilities and current state any board running Firmata.
*
* @private
* @method queryCapabilities
*/
queryCapabilities: function() {
this.send([START_SYSEX, CAPABILITY_QUERY, END_SYSEX]);
},
/**
* Query which pins correspond to the analog channels
*
* @private
* @method queryAnalogMapping
*/
queryAnalogMapping: function() {
this.send([START_SYSEX, ANALOG_MAPPING_QUERY, END_SYSEX]);
},
/**
* Call this method to enable or disable analog input for the specified
* pin.
*
* @private
* @method setAnalogPinReporting
* @param {Number} pin The pin connected to the analog input
* @param {Number} mode Pin.ON to enable input or Pin.OFF to disable
* input for the specified pin.
*/
setAnalogPinReporting: function(pin, mode) {
this.send([REPORT_ANALOG | pin, mode]);
this.getAnalogPin(pin).setType(Pin.AIN);
},
/**
* for debugging
* @private
*/
debug: function(str) {
if (this._debugMode) {
console.log(str);
}
},
// Getters and setters:
/**
* Get or set the sampling interval (how often to run the main loop on
* the IOBoard). Normally the sampling interval should not be changed.
* Default = 19 (ms).
*
* @property samplingInterval
* @type Number
*/
get samplingInterval() {
return this._samplingInterval;
},
set samplingInterval(interval) {
if (interval >= MIN_SAMPLING_INTERVAL && interval <= MAX_SAMPLING_INTERVAL) {
this._samplingInterval = interval;
this.send([START_SYSEX, SAMPLING_INTERVAL, interval & 0x007F, (interval >> 7) & 0x007F, END_SYSEX]);
} else {
console.log("warning: Sampling interval must be between " + MIN_SAMPLING_INTERVAL + " and " + MAX_SAMPLING_INTERVAL);
}
},
/**
* Set to true when the IOBoard is ready. This can be used in place of
* listening for the IOBoardEvent.READY event when creating an app with
* a draw loop (such as when using processing.js or three.js);
*
* @property isReady
* @type Boolean
*/
get isReady() {
return this._isReady;
},
// Public methods:
/**
* A utility method to assemble a single value from the 2 bytes returned
* from the IOBoard (since data is passed in 7 bit Bytes rather than
* 8 bit it must be reassembled. This is to be used as a protected
* method and should not be needed in any application level code.
*
* @private
* @method getValueFromTwo7bitBytes
* @param {Number} lsb The least-significant byte of the 2 values to
* be concatentated
* @param {Number} msb The most-significant byte of the 2 values to be
* concatenated
* @return {Number} The result of merging the 2 bytes
*/
getValueFromTwo7bitBytes: function(lsb, msb) {
return (msb << 7) | lsb;
},
/**
* @method getSocket
* @return {WSocketWrapper} A reference to the WebSocket
*/
getSocket: function() {
return this._socket;
},
/**
* Request the Firmata protocol version implemented in the firmware (sketch)
* running on the IOBoard.
* Listen for the IOBoard.PROTOCOL_VERSION event to be notified of when
* the Firmata version is returned from the IOBoard.
* @method reportVersion
*/
reportVersion: function() {
this.send(REPORT_VERSION);
},
/**
* Request the name and version of the firmware (the sketch) running on the IOBoard.
* Listen for the IOBoard.FIRMWARE_VERSION event to be notified of when
* the name is returned from the IOBoard. The version number is also
* returned.
* @method reportFirmware
*/
reportFirmware: function() {
this.send([START_SYSEX, REPORT_FIRMWARE, END_SYSEX]);
},
/**
* Disables digital pin reporting for all digital pins.
* @method disableDigitalPins
*/
disableDigitalPins: function() {
for (var i = 0; i < this._numPorts; i++) {
this.sendDigitalPortReporting(i, Pin.OFF);
}
},
/**
* Enables digital pin reporting for all digital pins. You must call
* this before you can receive digital pin data from the IOBoard.
* @method enableDigitalPins
*/
enableDigitalPins: function() {
for (var i = 0; i < this._numPorts; i++) {
this.sendDigitalPortReporting(i, Pin.ON);
}
},
/**
* Enable or disable reporting of all digital pins for the specified
* port.
* @method sendDigitalPortReporting
* @param {Number} mode Either Pin.On or Pin.OFF
*/
sendDigitalPortReporting: function(port, mode) {
this.send([(REPORT_DIGITAL | port), mode]);
},
/**
* Call this method to enable analog input for the specified pin.
* @method enableAnalogPin
* @param {Number} pin The pin connected to the analog input
*/
enableAnalogPin: function(pin) {
this.setAnalogPinReporting(pin, Pin.ON);
},
/**
* Call this method to disable analog input for the specified pin.
* @method disableAnalogPin
* @param {Number} pin The pin connected to the analog input
*/
disableAnalogPin: function(pin) {
this.setAnalogPinReporting(pin, Pin.OFF);
},
/**
* Set the specified digital pin mode.
*
* @method setDigitalPinMode
* @param {Number} pin The number of the pin. When using and analog
* pin as a digital pin, refer the datasheet for your board to obtain
* the digital pin equivalent of the analog pin number. For example on
* an Arduino UNO, analog pin 0 = digital pin 14.
* @param {Number} mode Pin.DIN, Pin.INPUT_PULLUP, Pin.DOUT, Pin.PWM, Pin.SERVO,
* Pin.SHIFT, or Pin.I2c
* @param {Boolean} silent [optional] Set to true to not send
* SET_PIN_MODE command. Default = false.
*/
setDigitalPinMode: function(pinNumber, mode, silent) {
this.getDigitalPin(pinNumber).setType(mode);
this.managePinListener(this.getDigitalPin(pinNumber));
// sometimes we want to set up a pin without sending the set pin
// mode command because the firmware handles the pin mode
if (!silent || silent !== true) {
this.send([SET_PIN_MODE, pinNumber, mode]);
}
},
/**
* Set the value of the specified pin
*
* @method setDigitalPinValue
* @param {Number} pin The number of the digital pin.
* @param {Number} value Pin.HIGH or Pin.LOW
*/
setDigitalPinValue: function(pinNumber, value) {
var portNum = Math.floor(pinNumber / 8);
// set digital port value in case user mixes setDigitalPinValue
// and sendDigitalData in the same application
if (value == Pin.HIGH) {
// Set the bit
this._digitalPort[portNum] |= (value << (pinNumber % 8));
} else if (value == Pin.LOW) {
// Clear the bit
this._digitalPort[portNum] &= ~(1 << (pinNumber % 8));
}
this.send([SET_PIN_VALUE, pinNumber, value]);
},
/**
* Enable the internal pull-up resistor for the specified pin number.
* @method enablePullUp
* @param {Number} pinNum The number of the input pin to enable the
* pull-up resistor.
*/
enablePullUp: function(pinNum) {
if (this._boardCapabilities[Pin.INPUT_PULLUP]) {
this.setDigitalPinMode(pinNum, Pin.INPUT_PULLUP);
} else {
this.sendDigitalData(pinNum, Pin.HIGH);
}
},
/**
* @method getFirmwareName
* @deprecated use getFirmwareVersion instead
* @return {String} The name of the firmware running on the IOBoard.
*/
getFirmwareName: function() {
// To Do: It seams that Firmata is reporting the Firmware
// name malformed.
return this._firmwareName;
},
/**
* @method getFirmwareVersion
* @return {String} The version of the firmware running on the
* IOBoard.
*/
getFirmwareVersion: function() {
return this._firmwareVersion;
},
/**
* @method getProtocolVersion
* @return {String} The version of Firmata protocol implemented by the firmware
* running on the IOBoard.
*/
getProtocolVersion: function() {
return this._protocolVersion;
},
/**
* Returns the capabilities for each pin on the IOBoard. The array is
* indexed by pin number (beginning at pin 0). Each array element
* contains an object with a property for each modes (input, output,
* pwm, servo, i2c, etc) supported by the pin. The mode value is the
* resolution in bits.
*
* @method getPinCapabilities
* @return {Array} The capabilities of the Pins on the IOBoard.
*/
getPinCapabilities: function() {
var capabilities = [],
len,
pinElements,
pinCapabilities,
hasCapabilities;
var modeNames = {
0: "input",
1: "output",
2: "analog",
3: "pwm",
4: "servo",
5: "shift",
6: "i2c",
7: "onewire",
8: "stepper",
9: "encoder",
10: "serial",
11: "pullup"
};
len = this._ioPins.length;
for (var i = 0; i < len; i++) {
pinElements = {};
pinCapabilities = this._ioPins[i].getCapabilities();
hasCapabilities = false;
for (var mode in pinCapabilities) {
if (pinCapabilities.hasOwnProperty(mode)) {
hasCapabilities = true;
if (mode >= 0) {
pinElements[modeNames[mode]] = this._ioPins[i].getCapabilities()[mode];
}
}
}
if (!hasCapabilities) {
capabilities[i] = {
"not available": "0"
};
} else {
capabilities[i] = pinElements;
}
}
return capabilities;
},
/**
* Reads the current state of the requested pin. Listen for the
* IOBoardEvent.PIN_STATE_RESPONSE event to get the response.
* The response contains a reference to the pin object with its
* state updated to match the current state of the pin on the IOBoard.
*
* You should not typically need to call this method since the pin
* states are maintained client-side. Use the getAnalogPin or
* getDigitalPin to get the current state of a pin or getPins to
* get an array of all Pin objects for the IOBoard.
*
* Cases for queryPinState are to update the pin state after a period
* of inactivity. For example if multiple client applications are
* using the same IOBoard (so multiple JavaScript apps connected to
* the same Arduino). When a new client connection is made,
* queryPinState is called automatically to copy the IOBoard pin state
* to the client. If for some reason you needed to copy the state of a
* single or multiple Pins again, you could call queryPinState in your
* application. In most cases however you should never need to call
* this method.
*
* @method queryPinState
* @param {Pin} pin The pin object to query the pin state for.
*/
queryPinState: function(pin) {
// To Do: Ensure that pin is a Pin object
var pinNumber = pin.number;
this.send([START_SYSEX, PIN_STATE_QUERY, pinNumber, END_SYSEX]);
this._numPinStateRequests++;
},
/**
* Send the digital values for a port. Making this private for now.
*
* @private
* @method sendDigitalPort
* @param {Number} portNumber The number of the port
* @param {Number} portData A byte representing the state of the 8 pins
* for the specified port
*/
sendDigitalPort: function(portNumber, portData) {
this.send([DIGITAL_MESSAGE | (portNumber & 0x0F), portData & 0x7F, portData >> 7]);
},
/**
* Send a string message to the IOBoard. This is useful if you have a
* custom sketch running on the IOBoard rather than StandardFirmata
* and want to communicate with your javascript message via string
* messages that you then parse in javascript.
* You can receive string messages as well.
*
* <p>To test, load the EchoString.pde example from Firmata->Examples
* menu in the IOBoard Application, then use sendString("your string
* message") to have it echoed back to your javascript application.</p>
*
* @method sendString
* @param {String} str The string message to send to the IOBoard
*/
sendString: function(str) {
// Convert chars to decimal values
var decValues = [];
for (var i = 0, len = str.length; i < len; i++) {
decValues.push(this.toDec(str[i]) & 0x007F);
decValues.push((this.toDec(str[i]) >> 7) & 0x007F);
}
// Data > 7 bits in length must be split into 2 bytes and
// packed into an array before passing to the sendSysex
// method
this.sendSysex(STRING_DATA, decValues);
},
/**
* Send a sysEx message to the IOBoard. This is useful for sending
* custom sysEx data to the IOBoard, for example if you are not using
* StandardFirmata. You would likely use it in a class rather than
* calling it from your main application.
*
* @private
* @method sendSysex
* @param {Number} command The sysEx command value (see firmata.org)
* @param {Number[]} data A packet of data representing the sysEx
* message to be sent
* @see <a href="http://firmata.org/wiki/Protocol#Sysex_Message_Format">Firmata Sysex Message Format"</a>
*/
sendSysex: function(command, data) {
var sysexData = [];
sysexData[0] = START_SYSEX;
sysexData[1] = command;
// This would be problematic since the sysEx message format does
// not enforce splitting all bytes after the command byte
//for (var i=0, len=data.length; i<len; i++) {
// sysexData.push(data[i] & 0x007F);
// sysexData.push((data[i] >> 7) & 0x007F);
//}
for (var i = 0, len = data.length; i < len; i++) {
sysexData.push(data[i]);
}
sysexData.push(END_SYSEX);
this.send(sysexData);
},
/**
* Call to associate a pin with a connected servo motor. See the
* documentation for your servo motor for the minimum and maximum
* pulse width. If you can't find it, then the default values should
* be close enough so call sendServoAttach(pin) omitting the min and
* max values.
*
* @method sendServoAttach
* @param {Number} pin The pin the server is connected to.
* @param {Number} minPulse [optional] The minimum pulse width for the
* servo. Default = 544.
* @param {Number} maxPulse [optional] The maximum pulse width for the
* servo. Default = 2400.
*/
sendServoAttach: function(pin, minPulse, maxPulse) {
var servoPin,
servoData = [];
minPulse = minPulse || 544; // Default value = 544
maxPulse = maxPulse || 2400; // Default value = 2400
servoData[0] = START_SYSEX;
servoData[1] = SERVO_CONFIG;
servoData[2] = pin;
servoData[3] = minPulse % 128;
servoData[4] = minPulse >> 7;
servoData[5] = maxPulse % 128;
servoData[6] = maxPulse >> 7;
servoData[7] = END_SYSEX;
this.send(servoData);
servoPin = this.getDigitalPin(pin);
servoPin.setType(Pin.SERVO);
this.managePinListener(servoPin);
},
/**
* @private
* @method getPin
* @return {Pin} An unmapped reference to the Pin object.
*/
getPin: function(pinNumber) {
return this._ioPins[pinNumber];
},
/**
* @method getAnalogPin
* @return {Pin} A reference to the Pin object (mapped to the IOBoard
* board analog pin).
*/
getAnalogPin: function(pinNumber) {
return this._ioPins[this._analogPinMapping[pinNumber]];
},
/**
* @method getDigitalPin
* @return {Pin} A reference to the Pin object (mapped to the IOBoard
* board digital pin).
*/
getDigitalPin: function(pinNumber) {
return this._ioPins[this._digitalPinMapping[pinNumber]];
},
/**
* @method getPins
* @return {Pin[]} An array containing all pins on the IOBoard
*/
getPins: function() {
return this._ioPins;
},
/**
* Use this method to obtain the digital pin number equivalent
* for an analog pin.
*
* @example
* // set analog pin A3 on an Arduino Uno to digital input
* board.setDigitalPinMode(board.analogToDigital(3), Pin.DIN);
*
* <p>board.analogToDigital(3) returns 17 which is the digital
* equivalent of the analog pin</p>
*
* @method analogToDigital
* @return {Number} The digital pin number equivalent for the specified
* analog pin number.
*/
analogToDigital: function(analogPinNumber) {
return this.getAnalogPin(analogPinNumber).number;
},
/**
* @method getPinCount
* @return {Number} Total number of pins
*/
getPinCount: function() {
return this._totalPins;
},
/**
* @method getAnalogPinCount
* @return {Number} The total number of analog pins supported by this
* IOBoard
*/
getAnalogPinCount: function() {
return this._totalAnalogPins;
},
/**
* Returns undefined if the board does not have i2c pins.
* @private
* @method getI2cPins
* @return {Number[]} The pin numbers of the i2c pins if the board has
* i2c.
*/
getI2cPins: function() {
return this._i2cPins;
},
/**
* Call this method to print the capabilities for all pins to
* the console.
* @method reportPinCapabilities
*/
reportPinCapabilities: function() {
var capabilities = this.getPinCapabilities(),
len = capabilities.length,
resolution;
for (var i = 0; i < len; i++) {
console.log("Pin " + i + ":");
for (var mode in capabilities[i]) {
if (capabilities[i].hasOwnProperty(mode)) {
resolution = capabilities[i][mode];
console.log("\t" + mode + " (" + resolution + (resolution > 1 ? " bits)" : " bit)"));
}
}
}
},
/**
* A wrapper for the send method of the WebSocket
* I'm not sure there is a case for the user to call this method
* So I'm making this private for now.
*
* @private
* @method send
* @param {Number[]} message Message data to be sent to the IOBoard
*/
send: function(message) {
this._socket.sendString(message);
},
/**
* A wrapper for the close method of the WebSocket. Making this
* private until a use case arises.
*
* @private
* @method close
*/
close: function() {
this._socket.close();
},
// Implement EventDispatcher
/**
* @param {String} type The event type
* @param {Function} listener The function to be called when the event
* is fired
*/
addEventListener: function(type, listener) {
this._evtDispatcher.addEventListener(type, listener);
},
/**
* @param {String} type The event type
* @param {Function} listener The function to be called when the event
* is fired
*/
removeEventListener: function(type, listener) {
this._evtDispatcher.removeEventListener(type, listener);
},
/**
* @param {String} type The event type
* return {boolean} True is listener exists for this type, false if not.
*/
hasEventListener: function(type) {
return this._evtDispatcher.hasEventListener(type);
},
/**
* @param {Event} type The Event object
* @param {Object} optionalParams Optional parameters to assign to the
* event object.
* return {boolean} True if dispatch is successful, false if not.
*/
dispatchEvent: function(event, optionalParams) {
return this._evtDispatcher.dispatchEvent(event, optionalParams);
}
};
/**
* @method reportCapabilities
* @deprecated use reportPinCapabilities instead
*/
IOBoard.prototype.reportCapabilities = IOBoard.prototype.reportPinCapabilities;
// Document events
/**
* The ioBoardReady event is dispatched when the board is ready to
* send and receive commands.
* @type BO.IOBoardEvent.READY
* @event ioBoardReady
* @param {IOBoard} target A reference to the IOBoard
*/
/**
* The ioBoardConnected event is dispatched when the websocket
* connection is established.
* @type BO.IOBoardEvent.CONNECTED
* @event ioBoardConnected
* @param {IOBoard} target A reference to the IOBoard
*/
/**
* The ioBoardDisconnected event is dispatched when the websocket
* connection is closed.
* @type BO.IOBoardEvent.DISCONNECTED
* @event ioBoardDisconnected
* @param {IOBoard} target A reference to the IOBoard
*/
/**
* The stringMessage event is dispatched when a string is received
* from the IOBoard.
* @type BO.IOBoardEvent.STRING_MESSAGE
* @event stringMessage
* @param {IOBoard} target A reference to the IOBoard
* @param {String} message The string message received from the IOBoard
*/
/**
* The sysexMessage event is dispatched when a sysEx message is
* received from the IOBoard.
* @type BO.IOBoardEvent.SYSEX_MESSAGE
* @event sysexMessage
* @param {IOBoard} target A reference to the IOBoard
* @param {Array} message The sysEx data
*/
/**
* The protocolVersion event is dispatched when the Firmata protocol version
* is received from the IOBoard.
* @type BO.IOBoardEvent.PROTOCOL_VERSION
* @event protocolVersion
* @param {IOBoard} target A reference to the IOBoard
* @param {Number} version The protocol version (where Firmata 2.3 = 23)
*/
/**
* The firmwareName event is dispatched when the firmware name is
* received from the IOBoard.
* @type BO.IOBoardEvent.FIRMWARE_NAME
* @deprecated use FIRMWARE_VERION instead
* @event firmwareName
* @param {IOBoard} target A reference to the IOBoard
* @param {String} name The name of the firmware running on the IOBoard
* @param {Number} version The firmware version (where Firmata 2.3 = 23)
*/
/**
* The firmwareVersion event is dispatched when the firmware name and version
* is received from the IOBoard.
* @type BO.IOBoardEvent.FIRMWARE_VERSION
* @event firmwareVersion
* @param {IOBoard} target A reference to the IOBoard
* @param {Number} version The firmware version (where Firmata 2.3 = 23)
*/
/**
* The pinStateResponse event is dispatched when the results of
* a pin state query (via a call to: queryPinState()) is received.
* @type BO.IOBoardEvent.PIN_STATE_RESPONSE
* @event pinStateResponse
* @param {IOBoard} target A reference to the IOBoard
* @param {BO.Pin} pin A reference to the pin object.
*/
/**
* The analogData event is dispatched when analog data is received
* from the IOBoard. Use thie event to be notified when any analog
* pin value changes. Use Pin.CHANGE to be notified when a specific
* pin value changes.
* @type BO.IOBoardEvent.ANALOG_DATA
* @event analogData
* @param {IOBoard} target A reference to the IOBoard
* @param {BO.Pin} pin A reference to the pin object.
*/
/**
* The digitalData event is dispatched when digital data is received
* from the IOBoard. Use this event to be notified when any digital
* pin value changes. Use Pin.CHANGE to be notified when a specific
* pin value changes.
* @type BO.IOBoardEvent.DIGITAL_DATA
* @event digitalData
* @param {IOBoard} target A reference to the IOBoard
* @param {BO.Pin} pin A reference to the pin object.
*/
return IOBoard;
}());