• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
5define('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