define(["lib/underscore", "common/util/check", "common/network/dispatcher",
"lib/jquery", "common/util/binding_list", "common/util/log"],
function(_, check, Dispatcher,
$, BindingList, Log) {
"use strict";
var logger = Log("Device");
// connect to the proxy that lets us talk to the device app
var proxyUrl = "http://proxy.lens2.lens/index.html";
var dispatcherToProxy = (function() {
var iframe = $("<iframe />", {src: proxyUrl, style: "display: none"});
iframe.appendTo("body");
var dispatcherToProxy = Dispatcher(Dispatcher._PostMessageTransport(iframe[0]));
return dispatcherToProxy;
})();
// check validator for ConnectionOptions
var dataBitsValidator = check.Match.Enum("seven", "eight");
var parityBitValidator = check.Match.Enum("no", "odd", "even");
var stopBitsValidator = check.Match.Enum("one", "two");
var connectionOptionsValidator = {
persistent: check.Match.Optional(Boolean),
name: check.Match.Optional(String),
bufferSize: check.Match.Optional(check.Match.Integer),
bitrate: check.Match.Optional(check.Match.Integer),
dataBits: check.Match.Optional(dataBitsValidator),
parityBit: check.Match.Optional(parityBitValidator),
stopBits: check.Match.Optional(stopBitsValidator),
ctsFlowControl: check.Match.Optional(Boolean),
receiveTimeout: check.Match.Optional(check.Match.Integer),
sendTimeout: check.Match.Optional(check.Match.Integer)
};
// this is a proxy for the "chrome" object that exposes the chrome.serial
// and chrome.usb APIs
var chromeProxy = {
serial: {
getDevices: function(callback) {
check(callback, Function);
dispatcherToProxy.callRPC("getDevices", {}, callback);
},
connect: function(path, options, callback) {
if(_.isUndefined(callback) && !_.isUndefined(options)) {
options = undefined;
callback = options;
}
check(path, String);
check(options, check.Match.Optional(connectionOptionsValidator));
check(callback, Function);
dispatcherToProxy.callRPC("connect", [path, options], callback);
},
update: function(path, options, callback) {
check(path, String);
check(options, connectionOptionsValidator);
check(callback, check.Match.Optional(Function));
dispatcherToProxy.callRPC("update", [path, options], callback);
},
disconnect: function(connectionId, callback) {
check(connectionId, check.Match.Integer);
check(callback, check.Match.Optional(Function));
dispatcherToProxy.callRPC("disconnect", connectionId, callback);
},
setPaused: function(connectionId, paused, callback) {
check(connectionId, check.Match.Integer);
check(paused, Boolean);
check(callback, check.Match.Optional(Function));
dispatcherToProxy.callRPC("setPaused", [connectionId, paused], callback);
},
getInfo: function(connectionId, callback) {
check(connectionId, check.Match.Integer);
check(callback, Function);
dispatcherToProxy.callRPC("getInfo", connectionId, callback);
},
getConnections: function(callback) {
check(callback, Function);
dispatcherToProxy.callRPC("getConnections", {}, callback);
},
send: function(connectionId, data, callback) {
check(connectionId, check.Match.Integer);
check(data, ArrayBuffer);
check(callback, check.Match.Optional(Function));
var dataArray = _.toArray(new Uint8Array(data));
dispatcherToProxy.callRPC("send", [connectionId, dataArray], callback);
},
flush: function(connectionId, callback) {
check(connectionId, check.Match.Integer);
check(callback, check.Match.Optional(Function));
dispatcherToProxy.callRPC("flush", connectionId, callback);
},
getControlSignals: function(connectionId, callback) {
check(connectionId, check.Match.Integer);
check(callback, check.Match.Optional(Function));
dispatcherToProxy.callRPC("getControlSignals", connectionId, callback);
},
setControlSignals: function(connectionId, signals, callback) {
check(connectionId, check.Match.Integer);
check(signals, {
dtr: check.Match.Optional(Boolean),
rts: check.Match.Optional(Boolean)
});
dispatcherToProxy.callRPC("setControlSignals", [connectionId, signals], callback);
},
getUSBSerialInfo: function(callback) {
check(callback, Function);
$.getJSON("http://lens2.lens/cgi-bin/usb_serial.rb", callback);
},
findUSBSerialDevice: function(attributes, callback) {
check(attributes, Object);
check(callback, Function);
chromeProxy.serial.getUSBSerialInfo(function(data) {
callback(_.find(_.keys(data), function(path) {
var devAttrs = data[path];
return _.all(attributes, function(value, key) {
return devAttrs[key] === value;
});
}));
});
},
onReceive: (function() {
var handlers = BindingList();
dispatcherToProxy.subscribe("onReceive", function(data) {
// deserialize from an array to a real ArrayBuffer
var readInfo = {
connectionId: data.connectionId,
data: (new Uint8Array(data.data)).buffer
};
handlers.callAll(readInfo);
});
return {
addListener: function(callback) {
return handlers.add(callback);
}
};
})(),
onReceiveError: (function() {
var handlers = BindingList();
dispatcherToProxy.subscribe("onReceiveError", function(data) {
handlers.callAll(data);
});
return {
addListener: function(callback) {
return handlers.add(callback);
}
};
})()
}
};
// driver name -> driver factory
var drivers = {};
// instantiates a driver and checks that it has open and close methods.
// returns the driver if these checks pass, throws an error otherwise.
var checkDriver = function(factory) {
if(!_.isFunction(factory)) {
throw "Invalid driver factory: Factory is not a function.";
}
var driver = factory();
if(!driver) {
throw "Invalid driver factory: Factory did not return an object.";
}
if(!(_.isFunction(driver.open) && _.isFunction(driver.close))) {
throw "Invalid driver factory: Factory did not return an object with open and close methods.";
}
return driver;
};
// states a driver can be in.
var STATE_CLOSED = 0;
var STATE_OPENING = 1;
var STATE_OPEN = 2;
var STATE_CLOSING = 3;
var stateNames = {};
stateNames[STATE_CLOSED] = "closed";
stateNames[STATE_OPENING] = "opening";
stateNames[STATE_OPEN] = "open";
stateNames[STATE_CLOSING] = "closing";
/**
* Creates a new device connection. Does not open the connection.
* @param {String} driver Name of the driver to use.
*
* @class Device
* Represents a connection to a serial device.
*
* @memberOf Lens
*/
var Device = function(driverName) {
check(driverName, String);
// create the driver
var driverFactory = drivers[driverName];
if(!driverFactory) {
throw "No such driver: " + driverName;
}
var driver = checkDriver(driverFactory);
var state = STATE_CLOSED;
/** @alias Lens.Device.prototype */
var self = {};
/**
* Opens a connection to the device.
*
* @param {Array} args Arguments to pass to the driver
* @param {Function} callback Callback invoked when the connection is
* open.
*/
self.open = function(args, clientCallback) {
// allow clients to pass just a callback
if(!clientCallback && _.isFunction(args)) {
clientCallback = args;
args = [];
}
// sanity checks
check(args, check.Match.Optional(Array));
check(clientCallback, check.Match.Optional(Function));
if(state !== STATE_CLOSED) {
throw "Device is currently " + stateNames[state] +
". open can only be called on closed devices.";
}
// call the driver's open method
state = STATE_OPENING;
var callback = function(success) {
if(success) {
state = STATE_OPEN;
if(clientCallback) {
clientCallback();
}
}
else {
state = STATE_CLOSED;
logger.error("Call to open() failed");
}
};
driver.open(callback, args, chromeProxy);
};
/**
* Closes the connection to the device.
*
* @param {Array} args Arguments to pass to the driver
* @param {Function} callback Callback invoked when the connection is
* closed.
*/
self.close = function(args, clientCallback) {
// allow clients to pass just a callback
if(!clientCallback && _.isFunction(args)) {
clientCallback = args;
args = [];
}
// sanity checks
check(args, check.Match.Optional(Array));
check(clientCallback, check.Match.Optional(Function));
if(state !== STATE_OPEN) {
throw "Device is currently " + stateNames[state] +
". close can only be called on open devices.";
}
// call the driver's open method
state = STATE_CLOSING;
var callback = function(success) {
if(success) {
state = STATE_CLOSED;
if(clientCallback) {
clientCallback();
}
}
else {
state = STATE_OPEN;
logger.error("Call to close() failed");
}
};
driver.close(callback, args, chromeProxy);
};
/**
* Return true if this device supports the given method.
*
* @param {String} method Name of the method
* @return {Boolean}
*/
self.supports = function(method) {
if((method === "open") || (method === "close") ||
(method === "getState") || (method === "setState") ||
(method === "write") || (method === "read") ||
(method === "onReceive")) {
return !!driver[method];
}
return false;
};
/**
* Returns the state of the driver.
*
* @return {String} one of "open", "opening", "closed", or "closing".
*/
self.getDriverState = function() {
return stateNames[state];
};
var proxyMethodToDriver = function(method) {
return function(args, clientCallback) {
// allow clients to pass just a callback
if(!clientCallback && _.isFunction(args)) {
clientCallback = args;
args = [];
}
// sanity checks
check(args, check.Match.Optional(Array));
check(clientCallback, check.Match.Optional(Function));
if(!clientCallback) {
clientCallback = function(){};
}
if(state !== STATE_OPEN) {
throw "Device is currently " + stateNames[state] + ". " +
method + " can only be called on open devices.";
}
if(!self.supports(method)) {
throw "Device does not support method " + method;
}
// call the driver method
driver[method](clientCallback, args, chromeProxy);
};
};
/**
* Gets the state of this device, as determined by the driver.
*
* @memberOf Lens.Device.prototype
* @method getState
* @param {Array} [args] Array of arguments.
* @param {Function} [callback] Function that will receive the state as
* an argument.
*/
self.getState = proxyMethodToDriver("getState");
/**
* Sets the state of this device, as determined by the driver.
*
* @memberOf Lens.Device.prototype
* @method setState
* @param {Array} [args] Array of arguments.
* @param {Function} [callback] Function that will be called when the
* operation is complete.
*/
self.setState = proxyMethodToDriver("setState");
/**
* Writes to this device. The format of the data is determined by the
* driver.
*
* @memberOf Lens.Device.prototype
* @method write
* @param {Array} [args] Array of arguments.
* @param {Function} [callback] Function that will be called when the
* operation is complete.
*/
self.write = proxyMethodToDriver("write");
/**
* Reads from this device. The format of the data is determined by the
* driver.
*
* @memberOf Lens.Device.prototype
* @method read
* @param {Array} [args] Array of arguments.
* @param {Function} [callback] Function that will be called with the
* result of the read operation.
*/
self.read = proxyMethodToDriver("read");
/**
* Registers a callback to be called when new data is received from a
* device.
*
* @memberOf Lens.Device.prototype
* @method onReceive
* @param {Array} [args] Array of arguments.
* @param {Function} [callback] Function that will be called when data
* arrives.
*/
self.onReceive = proxyMethodToDriver("onReceive");
// close on unload
$(window).unload(function() {
if(state === STATE_OPEN) {
self.close();
}
});
Object.freeze(self);
return self;
};
_.extend(Device, /** @lends Lens.Device */ {
/**
* Registers a new driver that can be used to communicate with a
* serial device. Drivers are created by specifying a factory that
* returns an instance of the driver.
*
* An instance of the driver *must* have the properties `open` and
* `close`. If *may* have one or more of the properties `getState`,
* `setState`, `write`, `read`, and `onReceive`, depending on the most
* logical way for clients to interact with the device.
*
* Each of these driver methods receive three arguments: first, a
* callback that should be called with the result of the operation;
* second, an array of arguments passed to the driver by the
* client; and third, a proxy for the `chrome` object that gives
* access to the `chrome.serial` API.
*
* The factory should be lightweight. Any heavy-lifting should be done
* in the `open` method; drivers are sometimes instantiated and never
* used
*
* See {@tutorial Devices} for more information how to create device
* drivers.
*
* @param {String} name Name of this driver
* @param {Function} factory Factory that returns an instance of this
* driver.
*/
registerDriver: function(name, factory) {
check(name, String);
// create a test driver to make sure the factory works OK
checkDriver(factory);
drivers[name] = factory;
},
/**
* A helper for registering drivers that connect to a USB-serial device
* with an identifiable property. open and close methods are
* automatically written for you, and your factory will receive the
* connection id from the chrome.serial API as well as the chrome proxy
* object. The factory should then return an object containing one or
* more of the properties `getState`, `setState`, `write`, `read`, and
* `onReceive`, depending on the most logical way for clients to
* interact with the device.
*
* It may also have a `close` property. This close function will be
* called before the built-in close handler that actually closes the
* connection to the device. This function must still call the callback
* with either true or false. False indicates that the close operation
* should be aborted.
*
* @param {String} name Name of the driver
* @param {Object} opts Configuration options
* @param {Object} opts.id Identifying properties of the USB-serial
* device.
* @param {Number} opts.bitrate Bitrate of the device
*/
registerUSBSerialDriver: function(name, opts, factory) {
check(name, String);
check(opts, {
id: Object,
bitrate: check.Match.Optional(check.Match.Integer)
});
var connectionOptions = {};
if(opts.bitrate) {
connectionOptions.bitrate = opts.bitrate;
}
Device.registerDriver(name, function() {
var connectionId;
var closeHandler;
var driver = {
open: function(callback, args, chrome) {
// find device by serial number
chrome.serial.findUSBSerialDevice(opts.id, function(path) {
// if we can't find it, it means it's not plugged in.
if(!path) {
logger.error("Error when opening device " + name + ": device not attached");
connectionId = undefined;
callback(false);
return;
}
// if we can find it, try to connect to it
chrome.serial.connect(path, connectionOptions, function(openInfo) {
// if we didn't get a connection id, there was an
// error opening the device.
if(!openInfo) {
logger.error("Error when opening device " + name + ": could not open device");
connectionId = undefined;
callback(false);
}
// everything worked; store the connection id.
connectionId = openInfo.connectionId;
logger.info("Connected to device " + name + " with connection Id " + connectionId);
// copy the methods from the client's driver
// to this driver
var clientDriver = factory(connectionId, chrome);
driver.getState = clientDriver.getState;
driver.setState = clientDriver.setState;
driver.write = clientDriver.write;
driver.read = clientDriver.read;
driver.onReceive = clientDriver.onReceive;
closeHandler = clientDriver.close;
callback(true);
});
});
},
close: function(callback, args, chrome) {
if(_.isFunction(closeHandler)) {
closeHandler(function(success) {
if(success) {
chrome.serial.disconnect(connectionId, function() {
callback(true);
});
}
else {
callback(false);
}
}, args, chrome);
}
else {
chrome.serial.disconnect(connectionId, function() {
callback(true);
});
}
}
};
return driver;
});
}
});
Object.freeze(Device);
Lens._addMember(Device, "Device");
return Device;
});