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; });