// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. define('serial_service', [ 'content/public/renderer/service_provider', 'data_receiver', 'data_sender', 'device/serial/serial.mojom', 'mojo/public/js/bindings/core', 'mojo/public/js/bindings/router', ], function(serviceProvider, dataReceiver, dataSender, serialMojom, core, routerModule) { /** * A Javascript client for the serial service and connection Mojo services. * * This provides a thick client around the Mojo services, exposing a JS-style * interface to serial connections and information about serial devices. This * converts parameters and result between the Apps serial API types and the * Mojo types. */ var service = new serialMojom.SerialServiceProxy(new routerModule.Router( serviceProvider.connectToService(serialMojom.SerialServiceProxy.NAME_))); function getDevices() { return service.getDevices().then(function(response) { return $Array.map(response.devices, function(device) { var result = {path: device.path}; if (device.has_vendor_id) result.vendorId = device.vendor_id; if (device.has_product_id) result.productId = device.product_id; if (device.display_name) result.displayName = device.display_name; return result; }); }); } var DEFAULT_CLIENT_OPTIONS = { persistent: false, name: '', receiveTimeout: 0, sendTimeout: 0, bufferSize: 4096, }; var DATA_BITS_TO_MOJO = { undefined: serialMojom.DataBits.NONE, 'seven': serialMojom.DataBits.SEVEN, 'eight': serialMojom.DataBits.EIGHT, }; var STOP_BITS_TO_MOJO = { undefined: serialMojom.StopBits.NONE, 'one': serialMojom.StopBits.ONE, 'two': serialMojom.StopBits.TWO, }; var PARITY_BIT_TO_MOJO = { undefined: serialMojom.ParityBit.NONE, 'no': serialMojom.ParityBit.NO, 'odd': serialMojom.ParityBit.ODD, 'even': serialMojom.ParityBit.EVEN, }; var SEND_ERROR_TO_MOJO = { undefined: serialMojom.SendError.NONE, 'disconnected': serialMojom.SendError.DISCONNECTED, 'pending': serialMojom.SendError.PENDING, 'timeout': serialMojom.SendError.TIMEOUT, 'system_error': serialMojom.SendError.SYSTEM_ERROR, }; var RECEIVE_ERROR_TO_MOJO = { undefined: serialMojom.ReceiveError.NONE, 'disconnected': serialMojom.ReceiveError.DISCONNECTED, 'device_lost': serialMojom.ReceiveError.DEVICE_LOST, 'timeout': serialMojom.ReceiveError.TIMEOUT, 'system_error': serialMojom.ReceiveError.SYSTEM_ERROR, }; function invertMap(input) { var output = {}; for (var key in input) { if (key == 'undefined') output[input[key]] = undefined; else output[input[key]] = key; } return output; } var DATA_BITS_FROM_MOJO = invertMap(DATA_BITS_TO_MOJO); var STOP_BITS_FROM_MOJO = invertMap(STOP_BITS_TO_MOJO); var PARITY_BIT_FROM_MOJO = invertMap(PARITY_BIT_TO_MOJO); var SEND_ERROR_FROM_MOJO = invertMap(SEND_ERROR_TO_MOJO); var RECEIVE_ERROR_FROM_MOJO = invertMap(RECEIVE_ERROR_TO_MOJO); function getServiceOptions(options) { var out = {}; if (options.dataBits) out.data_bits = DATA_BITS_TO_MOJO[options.dataBits]; if (options.stopBits) out.stop_bits = STOP_BITS_TO_MOJO[options.stopBits]; if (options.parityBit) out.parity_bit = PARITY_BIT_TO_MOJO[options.parityBit]; if ('ctsFlowControl' in options) { out.has_cts_flow_control = true; out.cts_flow_control = options.ctsFlowControl; } if ('bitrate' in options) out.bitrate = options.bitrate; return out; } function convertServiceInfo(result) { if (!result.info) throw new Error('Failed to get ConnectionInfo.'); return { ctsFlowControl: !!result.info.cts_flow_control, bitrate: result.info.bitrate || undefined, dataBits: DATA_BITS_FROM_MOJO[result.info.data_bits], stopBits: STOP_BITS_FROM_MOJO[result.info.stop_bits], parityBit: PARITY_BIT_FROM_MOJO[result.info.parity_bit], }; } function Connection( remoteConnection, router, receivePipe, sendPipe, id, options) { this.remoteConnection_ = remoteConnection; this.router_ = router; this.options_ = {}; for (var key in DEFAULT_CLIENT_OPTIONS) { this.options_[key] = DEFAULT_CLIENT_OPTIONS[key]; } this.setClientOptions_(options); this.receivePipe_ = new dataReceiver.DataReceiver(receivePipe, this.options_.bufferSize, serialMojom.ReceiveError.DISCONNECTED); this.sendPipe_ = new dataSender.DataSender( sendPipe, this.options_.bufferSize, serialMojom.SendError.DISCONNECTED); this.id_ = id; getConnections().then(function(connections) { connections[this.id_] = this; }.bind(this)); this.paused_ = false; this.sendInProgress_ = false; // queuedReceiveData_ or queuedReceiveError will store the receive result or // error, respectively, if a receive completes or fails while this // connection is paused. At most one of the the two may be non-null: a // receive completed while paused will only set one of them, no further // receives will be performed while paused and a queued result is dispatched // before any further receives are initiated when unpausing. this.queuedReceiveData_ = null; this.queuedReceiveError = null; this.startReceive_(); } Connection.create = function(path, options) { options = options || {}; var serviceOptions = getServiceOptions(options); var pipe = core.createMessagePipe(); var sendPipe = core.createMessagePipe(); var receivePipe = core.createMessagePipe(); service.connect(path, serviceOptions, pipe.handle0, sendPipe.handle0, receivePipe.handle0); var router = new routerModule.Router(pipe.handle1); var connection = new serialMojom.ConnectionProxy(router); return connection.getInfo().then(convertServiceInfo).then(function(info) { return Promise.all([info, allocateConnectionId()]); }).catch(function(e) { router.close(); core.close(sendPipe.handle1); core.close(receivePipe.handle1); throw e; }).then(function(results) { var info = results[0]; var id = results[1]; var serialConnectionClient = new Connection(connection, router, receivePipe.handle1, sendPipe.handle1, id, options); var clientInfo = serialConnectionClient.getClientInfo_(); for (var key in clientInfo) { info[key] = clientInfo[key]; } return { connection: serialConnectionClient, info: info, }; }); }; Connection.prototype.close = function() { this.router_.close(); this.receivePipe_.close(); this.sendPipe_.close(); clearTimeout(this.receiveTimeoutId_); clearTimeout(this.sendTimeoutId_); return getConnections().then(function(connections) { delete connections[this.id_]; return true; }.bind(this)); }; Connection.prototype.getClientInfo_ = function() { var info = { connectionId: this.id_, paused: this.paused_, }; for (var key in this.options_) { info[key] = this.options_[key]; } return info; }; Connection.prototype.getInfo = function() { var info = this.getClientInfo_(); return this.remoteConnection_.getInfo().then(convertServiceInfo).then( function(result) { for (var key in result) { info[key] = result[key]; } return info; }).catch(function() { return info; }); }; Connection.prototype.setClientOptions_ = function(options) { if ('name' in options) this.options_.name = options.name; if ('receiveTimeout' in options) this.options_.receiveTimeout = options.receiveTimeout; if ('sendTimeout' in options) this.options_.sendTimeout = options.sendTimeout; if ('bufferSize' in options) this.options_.bufferSize = options.bufferSize; }; Connection.prototype.setOptions = function(options) { this.setClientOptions_(options); var serviceOptions = getServiceOptions(options); if ($Object.keys(serviceOptions).length == 0) return true; return this.remoteConnection_.setOptions(serviceOptions).then( function(result) { return !!result.success; }).catch(function() { return false; }); }; Connection.prototype.getControlSignals = function() { return this.remoteConnection_.getControlSignals().then(function(result) { if (!result.signals) throw new Error('Failed to get control signals.'); var signals = result.signals; return { dcd: !!signals.dcd, cts: !!signals.cts, ri: !!signals.ri, dsr: !!signals.dsr, }; }); }; Connection.prototype.setControlSignals = function(signals) { var controlSignals = {}; if ('dtr' in signals) { controlSignals.has_dtr = true; controlSignals.dtr = signals.dtr; } if ('rts' in signals) { controlSignals.has_rts = true; controlSignals.rts = signals.rts; } return this.remoteConnection_.setControlSignals(controlSignals).then( function(result) { return !!result.success; }); }; Connection.prototype.flush = function() { return this.remoteConnection_.flush().then(function(result) { return !!result.success; }); }; Connection.prototype.setPaused = function(paused) { this.paused_ = paused; if (paused) { clearTimeout(this.receiveTimeoutId_); this.receiveTimeoutId_ = null; } else if (!this.receiveInProgress_) { this.startReceive_(); } }; Connection.prototype.send = function(data) { if (this.sendInProgress_) return Promise.resolve({bytesSent: 0, error: 'pending'}); if (this.options_.sendTimeout) { this.sendTimeoutId_ = setTimeout(function() { this.sendPipe_.cancel(serialMojom.SendError.TIMEOUT); }.bind(this), this.options_.sendTimeout); } this.sendInProgress_ = true; return this.sendPipe_.send(data).then(function(bytesSent) { return {bytesSent: bytesSent}; }).catch(function(e) { return { bytesSent: e.bytesSent, error: SEND_ERROR_FROM_MOJO[e.error], }; }).then(function(result) { if (this.sendTimeoutId_) clearTimeout(this.sendTimeoutId_); this.sendTimeoutId_ = null; this.sendInProgress_ = false; return result; }.bind(this)); }; Connection.prototype.startReceive_ = function() { this.receiveInProgress_ = true; var receivePromise = null; // If we have a queued receive result, dispatch it immediately instead of // starting a new receive. if (this.queuedReceiveData_) { receivePromise = Promise.resolve(this.queuedReceiveData_); this.queuedReceiveData_ = null; } else if (this.queuedReceiveError) { receivePromise = Promise.reject(this.queuedReceiveError); this.queuedReceiveError = null; } else { receivePromise = this.receivePipe_.receive(); } receivePromise.then(this.onDataReceived_.bind(this)).catch( this.onReceiveError_.bind(this)); this.startReceiveTimeoutTimer_(); }; Connection.prototype.onDataReceived_ = function(data) { this.startReceiveTimeoutTimer_(); this.receiveInProgress_ = false; if (this.paused_) { this.queuedReceiveData_ = data; return; } if (this.onData) { this.onData(data); } if (!this.paused_) { this.startReceive_(); } }; Connection.prototype.onReceiveError_ = function(e) { clearTimeout(this.receiveTimeoutId_); this.receiveInProgress_ = false; if (this.paused_) { this.queuedReceiveError = e; return; } var error = e.error; this.paused_ = true; if (this.onError) this.onError(RECEIVE_ERROR_FROM_MOJO[error]); }; Connection.prototype.startReceiveTimeoutTimer_ = function() { clearTimeout(this.receiveTimeoutId_); if (this.options_.receiveTimeout && !this.paused_) { this.receiveTimeoutId_ = setTimeout(this.onReceiveTimeout_.bind(this), this.options_.receiveTimeout); } }; Connection.prototype.onReceiveTimeout_ = function() { if (this.onError) this.onError('timeout'); this.startReceiveTimeoutTimer_(); }; var connections_ = {}; var nextConnectionId_ = 0; // Wrap all access to |connections_| through getConnections to avoid adding // any synchronous dependencies on it. This will likely be important when // supporting persistent connections by stashing them. function getConnections() { return Promise.resolve(connections_); } function getConnection(id) { return getConnections().then(function(connections) { if (!connections[id]) throw new Error('Serial connection not found.'); return connections[id]; }); } function allocateConnectionId() { return Promise.resolve(nextConnectionId_++); } return { getDevices: getDevices, createConnection: Connection.create, getConnection: getConnection, getConnections: getConnections, // For testing. Connection: Connection, }; });