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