Home | History | Annotate | Download | only in resources
      1 // Copyright 2014 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 define('serial_service', [
      6     'content/public/renderer/service_provider',
      7     'data_receiver',
      8     'data_sender',
      9     'device/serial/serial.mojom',
     10     'mojo/public/js/bindings/core',
     11     'mojo/public/js/bindings/router',
     12 ], function(serviceProvider,
     13             dataReceiver,
     14             dataSender,
     15             serialMojom,
     16             core,
     17             routerModule) {
     18   /**
     19    * A Javascript client for the serial service and connection Mojo services.
     20    *
     21    * This provides a thick client around the Mojo services, exposing a JS-style
     22    * interface to serial connections and information about serial devices. This
     23    * converts parameters and result between the Apps serial API types and the
     24    * Mojo types.
     25    */
     26 
     27   var service = new serialMojom.SerialServiceProxy(new routerModule.Router(
     28       serviceProvider.connectToService(serialMojom.SerialServiceProxy.NAME_)));
     29 
     30   function getDevices() {
     31     return service.getDevices().then(function(response) {
     32       return $Array.map(response.devices, function(device) {
     33         var result = {path: device.path};
     34         if (device.has_vendor_id)
     35           result.vendorId = device.vendor_id;
     36         if (device.has_product_id)
     37           result.productId = device.product_id;
     38         if (device.display_name)
     39           result.displayName = device.display_name;
     40         return result;
     41       });
     42     });
     43   }
     44 
     45   var DEFAULT_CLIENT_OPTIONS = {
     46     persistent: false,
     47     name: '',
     48     receiveTimeout: 0,
     49     sendTimeout: 0,
     50     bufferSize: 4096,
     51   };
     52 
     53   var DATA_BITS_TO_MOJO = {
     54     undefined: serialMojom.DataBits.NONE,
     55     'seven': serialMojom.DataBits.SEVEN,
     56     'eight': serialMojom.DataBits.EIGHT,
     57   };
     58   var STOP_BITS_TO_MOJO = {
     59     undefined: serialMojom.StopBits.NONE,
     60     'one': serialMojom.StopBits.ONE,
     61     'two': serialMojom.StopBits.TWO,
     62   };
     63   var PARITY_BIT_TO_MOJO = {
     64     undefined: serialMojom.ParityBit.NONE,
     65     'no': serialMojom.ParityBit.NO,
     66     'odd': serialMojom.ParityBit.ODD,
     67     'even': serialMojom.ParityBit.EVEN,
     68   };
     69   var SEND_ERROR_TO_MOJO = {
     70     undefined: serialMojom.SendError.NONE,
     71     'disconnected': serialMojom.SendError.DISCONNECTED,
     72     'pending': serialMojom.SendError.PENDING,
     73     'timeout': serialMojom.SendError.TIMEOUT,
     74     'system_error': serialMojom.SendError.SYSTEM_ERROR,
     75   };
     76   var RECEIVE_ERROR_TO_MOJO = {
     77     undefined: serialMojom.ReceiveError.NONE,
     78     'disconnected': serialMojom.ReceiveError.DISCONNECTED,
     79     'device_lost': serialMojom.ReceiveError.DEVICE_LOST,
     80     'timeout': serialMojom.ReceiveError.TIMEOUT,
     81     'system_error': serialMojom.ReceiveError.SYSTEM_ERROR,
     82   };
     83 
     84   function invertMap(input) {
     85     var output = {};
     86     for (var key in input) {
     87       if (key == 'undefined')
     88         output[input[key]] = undefined;
     89       else
     90         output[input[key]] = key;
     91     }
     92     return output;
     93   }
     94   var DATA_BITS_FROM_MOJO = invertMap(DATA_BITS_TO_MOJO);
     95   var STOP_BITS_FROM_MOJO = invertMap(STOP_BITS_TO_MOJO);
     96   var PARITY_BIT_FROM_MOJO = invertMap(PARITY_BIT_TO_MOJO);
     97   var SEND_ERROR_FROM_MOJO = invertMap(SEND_ERROR_TO_MOJO);
     98   var RECEIVE_ERROR_FROM_MOJO = invertMap(RECEIVE_ERROR_TO_MOJO);
     99 
    100   function getServiceOptions(options) {
    101     var out = {};
    102     if (options.dataBits)
    103       out.data_bits = DATA_BITS_TO_MOJO[options.dataBits];
    104     if (options.stopBits)
    105       out.stop_bits = STOP_BITS_TO_MOJO[options.stopBits];
    106     if (options.parityBit)
    107       out.parity_bit = PARITY_BIT_TO_MOJO[options.parityBit];
    108     if ('ctsFlowControl' in options) {
    109       out.has_cts_flow_control = true;
    110       out.cts_flow_control = options.ctsFlowControl;
    111     }
    112     if ('bitrate' in options)
    113       out.bitrate = options.bitrate;
    114     return out;
    115   }
    116 
    117   function convertServiceInfo(result) {
    118     if (!result.info)
    119       throw new Error('Failed to get ConnectionInfo.');
    120     return {
    121       ctsFlowControl: !!result.info.cts_flow_control,
    122       bitrate: result.info.bitrate || undefined,
    123       dataBits: DATA_BITS_FROM_MOJO[result.info.data_bits],
    124       stopBits: STOP_BITS_FROM_MOJO[result.info.stop_bits],
    125       parityBit: PARITY_BIT_FROM_MOJO[result.info.parity_bit],
    126     };
    127   }
    128 
    129   function Connection(
    130       remoteConnection, router, receivePipe, sendPipe, id, options) {
    131     this.remoteConnection_ = remoteConnection;
    132     this.router_ = router;
    133     this.options_ = {};
    134     for (var key in DEFAULT_CLIENT_OPTIONS) {
    135       this.options_[key] = DEFAULT_CLIENT_OPTIONS[key];
    136     }
    137     this.setClientOptions_(options);
    138     this.receivePipe_ =
    139         new dataReceiver.DataReceiver(receivePipe,
    140                                       this.options_.bufferSize,
    141                                       serialMojom.ReceiveError.DISCONNECTED);
    142     this.sendPipe_ = new dataSender.DataSender(
    143         sendPipe, this.options_.bufferSize, serialMojom.SendError.DISCONNECTED);
    144     this.id_ = id;
    145     getConnections().then(function(connections) {
    146       connections[this.id_] = this;
    147     }.bind(this));
    148     this.paused_ = false;
    149     this.sendInProgress_ = false;
    150 
    151     // queuedReceiveData_ or queuedReceiveError will store the receive result or
    152     // error, respectively, if a receive completes or fails while this
    153     // connection is paused. At most one of the the two may be non-null: a
    154     // receive completed while paused will only set one of them, no further
    155     // receives will be performed while paused and a queued result is dispatched
    156     // before any further receives are initiated when unpausing.
    157     this.queuedReceiveData_ = null;
    158     this.queuedReceiveError = null;
    159 
    160     this.startReceive_();
    161   }
    162 
    163   Connection.create = function(path, options) {
    164     options = options || {};
    165     var serviceOptions = getServiceOptions(options);
    166     var pipe = core.createMessagePipe();
    167     var sendPipe = core.createMessagePipe();
    168     var receivePipe = core.createMessagePipe();
    169     service.connect(path,
    170                     serviceOptions,
    171                     pipe.handle0,
    172                     sendPipe.handle0,
    173                     receivePipe.handle0);
    174     var router = new routerModule.Router(pipe.handle1);
    175     var connection = new serialMojom.ConnectionProxy(router);
    176     return connection.getInfo().then(convertServiceInfo).then(function(info) {
    177       return Promise.all([info, allocateConnectionId()]);
    178     }).catch(function(e) {
    179       router.close();
    180       core.close(sendPipe.handle1);
    181       core.close(receivePipe.handle1);
    182       throw e;
    183     }).then(function(results) {
    184       var info = results[0];
    185       var id = results[1];
    186       var serialConnectionClient = new Connection(connection,
    187                                                   router,
    188                                                   receivePipe.handle1,
    189                                                   sendPipe.handle1,
    190                                                   id,
    191                                                   options);
    192       var clientInfo = serialConnectionClient.getClientInfo_();
    193       for (var key in clientInfo) {
    194         info[key] = clientInfo[key];
    195       }
    196       return {
    197         connection: serialConnectionClient,
    198         info: info,
    199       };
    200     });
    201   };
    202 
    203   Connection.prototype.close = function() {
    204     this.router_.close();
    205     this.receivePipe_.close();
    206     this.sendPipe_.close();
    207     clearTimeout(this.receiveTimeoutId_);
    208     clearTimeout(this.sendTimeoutId_);
    209     return getConnections().then(function(connections) {
    210       delete connections[this.id_];
    211       return true;
    212     }.bind(this));
    213   };
    214 
    215   Connection.prototype.getClientInfo_ = function() {
    216     var info = {
    217       connectionId: this.id_,
    218       paused: this.paused_,
    219     };
    220     for (var key in this.options_) {
    221       info[key] = this.options_[key];
    222     }
    223     return info;
    224   };
    225 
    226   Connection.prototype.getInfo = function() {
    227     var info = this.getClientInfo_();
    228     return this.remoteConnection_.getInfo().then(convertServiceInfo).then(
    229         function(result) {
    230       for (var key in result) {
    231         info[key] = result[key];
    232       }
    233       return info;
    234     }).catch(function() {
    235       return info;
    236     });
    237   };
    238 
    239   Connection.prototype.setClientOptions_ = function(options) {
    240     if ('name' in options)
    241       this.options_.name = options.name;
    242     if ('receiveTimeout' in options)
    243       this.options_.receiveTimeout = options.receiveTimeout;
    244     if ('sendTimeout' in options)
    245       this.options_.sendTimeout = options.sendTimeout;
    246     if ('bufferSize' in options)
    247       this.options_.bufferSize = options.bufferSize;
    248   };
    249 
    250   Connection.prototype.setOptions = function(options) {
    251     this.setClientOptions_(options);
    252     var serviceOptions = getServiceOptions(options);
    253     if ($Object.keys(serviceOptions).length == 0)
    254       return true;
    255     return this.remoteConnection_.setOptions(serviceOptions).then(
    256         function(result) {
    257       return !!result.success;
    258     }).catch(function() {
    259       return false;
    260     });
    261   };
    262 
    263   Connection.prototype.getControlSignals = function() {
    264     return this.remoteConnection_.getControlSignals().then(function(result) {
    265       if (!result.signals)
    266         throw new Error('Failed to get control signals.');
    267       var signals = result.signals;
    268       return {
    269         dcd: !!signals.dcd,
    270         cts: !!signals.cts,
    271         ri: !!signals.ri,
    272         dsr: !!signals.dsr,
    273       };
    274     });
    275   };
    276 
    277   Connection.prototype.setControlSignals = function(signals) {
    278     var controlSignals = {};
    279     if ('dtr' in signals) {
    280       controlSignals.has_dtr = true;
    281       controlSignals.dtr = signals.dtr;
    282     }
    283     if ('rts' in signals) {
    284       controlSignals.has_rts = true;
    285       controlSignals.rts = signals.rts;
    286     }
    287     return this.remoteConnection_.setControlSignals(controlSignals).then(
    288         function(result) {
    289       return !!result.success;
    290     });
    291   };
    292 
    293   Connection.prototype.flush = function() {
    294     return this.remoteConnection_.flush().then(function(result) {
    295       return !!result.success;
    296     });
    297   };
    298 
    299   Connection.prototype.setPaused = function(paused) {
    300     this.paused_ = paused;
    301     if (paused) {
    302       clearTimeout(this.receiveTimeoutId_);
    303       this.receiveTimeoutId_ = null;
    304     } else if (!this.receiveInProgress_) {
    305       this.startReceive_();
    306     }
    307   };
    308 
    309   Connection.prototype.send = function(data) {
    310     if (this.sendInProgress_)
    311       return Promise.resolve({bytesSent: 0, error: 'pending'});
    312 
    313     if (this.options_.sendTimeout) {
    314       this.sendTimeoutId_ = setTimeout(function() {
    315         this.sendPipe_.cancel(serialMojom.SendError.TIMEOUT);
    316       }.bind(this), this.options_.sendTimeout);
    317     }
    318     this.sendInProgress_ = true;
    319     return this.sendPipe_.send(data).then(function(bytesSent) {
    320       return {bytesSent: bytesSent};
    321     }).catch(function(e) {
    322       return {
    323         bytesSent: e.bytesSent,
    324         error: SEND_ERROR_FROM_MOJO[e.error],
    325       };
    326     }).then(function(result) {
    327       if (this.sendTimeoutId_)
    328         clearTimeout(this.sendTimeoutId_);
    329       this.sendTimeoutId_ = null;
    330       this.sendInProgress_ = false;
    331       return result;
    332     }.bind(this));
    333   };
    334 
    335   Connection.prototype.startReceive_ = function() {
    336     this.receiveInProgress_ = true;
    337     var receivePromise = null;
    338     // If we have a queued receive result, dispatch it immediately instead of
    339     // starting a new receive.
    340     if (this.queuedReceiveData_) {
    341       receivePromise = Promise.resolve(this.queuedReceiveData_);
    342       this.queuedReceiveData_ = null;
    343     } else if (this.queuedReceiveError) {
    344       receivePromise = Promise.reject(this.queuedReceiveError);
    345       this.queuedReceiveError = null;
    346     } else {
    347       receivePromise = this.receivePipe_.receive();
    348     }
    349     receivePromise.then(this.onDataReceived_.bind(this)).catch(
    350         this.onReceiveError_.bind(this));
    351     this.startReceiveTimeoutTimer_();
    352   };
    353 
    354   Connection.prototype.onDataReceived_ = function(data) {
    355     this.startReceiveTimeoutTimer_();
    356     this.receiveInProgress_ = false;
    357     if (this.paused_) {
    358       this.queuedReceiveData_ = data;
    359       return;
    360     }
    361     if (this.onData) {
    362       this.onData(data);
    363     }
    364     if (!this.paused_) {
    365       this.startReceive_();
    366     }
    367   };
    368 
    369   Connection.prototype.onReceiveError_ = function(e) {
    370     clearTimeout(this.receiveTimeoutId_);
    371     this.receiveInProgress_ = false;
    372     if (this.paused_) {
    373       this.queuedReceiveError = e;
    374       return;
    375     }
    376     var error = e.error;
    377     this.paused_ = true;
    378     if (this.onError)
    379       this.onError(RECEIVE_ERROR_FROM_MOJO[error]);
    380   };
    381 
    382   Connection.prototype.startReceiveTimeoutTimer_ = function() {
    383     clearTimeout(this.receiveTimeoutId_);
    384     if (this.options_.receiveTimeout && !this.paused_) {
    385       this.receiveTimeoutId_ = setTimeout(this.onReceiveTimeout_.bind(this),
    386                                           this.options_.receiveTimeout);
    387     }
    388   };
    389 
    390   Connection.prototype.onReceiveTimeout_ = function() {
    391     if (this.onError)
    392       this.onError('timeout');
    393     this.startReceiveTimeoutTimer_();
    394   };
    395 
    396   var connections_ = {};
    397   var nextConnectionId_ = 0;
    398 
    399   // Wrap all access to |connections_| through getConnections to avoid adding
    400   // any synchronous dependencies on it. This will likely be important when
    401   // supporting persistent connections by stashing them.
    402   function getConnections() {
    403     return Promise.resolve(connections_);
    404   }
    405 
    406   function getConnection(id) {
    407     return getConnections().then(function(connections) {
    408       if (!connections[id])
    409         throw new Error('Serial connection not found.');
    410       return connections[id];
    411     });
    412   }
    413 
    414   function allocateConnectionId() {
    415     return Promise.resolve(nextConnectionId_++);
    416   }
    417 
    418   return {
    419     getDevices: getDevices,
    420     createConnection: Connection.create,
    421     getConnection: getConnection,
    422     getConnections: getConnections,
    423     // For testing.
    424     Connection: Connection,
    425   };
    426 });
    427