blob: e5d909d175a83c30bb9c7e87c1eb041ca434ee0d [file] [log] [blame]
// 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',
'device/serial/serial_serialization.mojom',
'mojo/public/js/core',
'mojo/public/js/router',
'stash_client',
], function(serviceProvider,
dataReceiver,
dataSender,
serialMojom,
serialization,
core,
routerModule,
stashClient) {
/**
* 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.SerialService.proxyClass(
new routerModule.Router(
serviceProvider.connectToService(serialMojom.SerialService.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 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],
};
}
// Update client-side options |clientOptions| from the user-provided
// |options|.
function updateClientOptions(clientOptions, options) {
if ('name' in options)
clientOptions.name = options.name;
if ('receiveTimeout' in options)
clientOptions.receiveTimeout = options.receiveTimeout;
if ('sendTimeout' in options)
clientOptions.sendTimeout = options.sendTimeout;
if ('bufferSize' in options)
clientOptions.bufferSize = options.bufferSize;
if ('persistent' in options)
clientOptions.persistent = options.persistent;
};
function Connection(connection, router, receivePipe, receiveClientPipe,
sendPipe, id, options) {
var state = new serialization.ConnectionState();
state.connectionId = id;
updateClientOptions(state, options);
var receiver = new dataReceiver.DataReceiver(
receivePipe, receiveClientPipe, state.bufferSize,
serialMojom.ReceiveError.DISCONNECTED);
var sender = new dataSender.DataSender(sendPipe, state.bufferSize,
serialMojom.SendError.DISCONNECTED);
this.init_(state,
connection,
router,
receiver,
sender,
null,
serialMojom.ReceiveError.NONE);
connections_.set(id, this);
this.startReceive_();
}
// Initializes this Connection from the provided args.
Connection.prototype.init_ = function(state,
connection,
router,
receiver,
sender,
queuedReceiveData,
queuedReceiveError) {
this.state_ = state;
// 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.
if (queuedReceiveError != serialMojom.ReceiveError.NONE)
this.queuedReceiveError_ = {error: queuedReceiveError};
if (queuedReceiveData) {
this.queuedReceiveData_ = new ArrayBuffer(queuedReceiveData.length);
new Int8Array(this.queuedReceiveData_).set(queuedReceiveData);
}
this.router_ = router;
this.remoteConnection_ = connection;
this.receivePipe_ = receiver;
this.sendPipe_ = sender;
this.sendInProgress_ = false;
};
Connection.create = function(path, options) {
options = options || {};
var serviceOptions = getServiceOptions(options);
var pipe = core.createMessagePipe();
var sendPipe = core.createMessagePipe();
var receivePipe = core.createMessagePipe();
var receivePipeClient = core.createMessagePipe();
service.connect(path,
serviceOptions,
pipe.handle0,
sendPipe.handle0,
receivePipe.handle0,
receivePipeClient.handle0);
var router = new routerModule.Router(pipe.handle1);
var connection = new serialMojom.Connection.proxyClass(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);
core.close(receivePipeClient.handle1);
throw e;
}).then(function(results) {
var info = results[0];
var id = results[1];
var serialConnectionClient = new Connection(connection,
router,
receivePipe.handle1,
receivePipeClient.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_);
connections_.delete(this.state_.connectionId);
return true;
};
Connection.prototype.getClientInfo_ = function() {
return {
connectionId: this.state_.connectionId,
paused: this.state_.paused,
persistent: this.state_.persistent,
name: this.state_.name,
receiveTimeout: this.state_.receiveTimeout,
sendTimeout: this.state_.sendTimeout,
bufferSize: this.state_.bufferSize,
};
};
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.setOptions = function(options) {
updateClientOptions(this.state_, 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.state_.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.state_.sendTimeout) {
this.sendTimeoutId_ = setTimeout(function() {
this.sendPipe_.cancel(serialMojom.SendError.TIMEOUT);
}.bind(this), this.state_.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.state_.paused) {
this.queuedReceiveData_ = data;
return;
}
if (this.onData) {
this.onData(data);
}
if (!this.state_.paused) {
this.startReceive_();
}
};
Connection.prototype.onReceiveError_ = function(e) {
clearTimeout(this.receiveTimeoutId_);
this.receiveInProgress_ = false;
if (this.state_.paused) {
this.queuedReceiveError_ = e;
return;
}
var error = e.error;
this.state_.paused = true;
if (this.onError)
this.onError(RECEIVE_ERROR_FROM_MOJO[error]);
};
Connection.prototype.startReceiveTimeoutTimer_ = function() {
clearTimeout(this.receiveTimeoutId_);
if (this.state_.receiveTimeout && !this.state_.paused) {
this.receiveTimeoutId_ = setTimeout(this.onReceiveTimeout_.bind(this),
this.state_.receiveTimeout);
}
};
Connection.prototype.onReceiveTimeout_ = function() {
if (this.onError)
this.onError('timeout');
this.startReceiveTimeoutTimer_();
};
Connection.prototype.serialize = function() {
connections_.delete(this.state_.connectionId);
this.onData = null;
this.onError = null;
var handle = this.router_.connector_.handle_;
this.router_.connector_.handle_ = null;
this.router_.close();
clearTimeout(this.receiveTimeoutId_);
clearTimeout(this.sendTimeoutId_);
// Serializing receivePipe_ will cancel an in-progress receive, which would
// pause the connection, so save it ahead of time.
var paused = this.state_.paused;
return Promise.all([
this.receivePipe_.serialize(),
this.sendPipe_.serialize(),
]).then(function(serializedComponents) {
var queuedReceiveError = serialMojom.ReceiveError.NONE;
if (this.queuedReceiveError_)
queuedReceiveError = this.queuedReceiveError_.error;
this.state_.paused = paused;
var serialized = new serialization.SerializedConnection();
serialized.state = this.state_;
serialized.queuedReceiveError = queuedReceiveError;
serialized.queuedReceiveData =
this.queuedReceiveData_ ? new Int8Array(this.queuedReceiveData_) :
null;
serialized.connection = handle;
serialized.receiver = serializedComponents[0];
serialized.sender = serializedComponents[1];
return serialized;
}.bind(this));
};
Connection.deserialize = function(serialized) {
var serialConnection = $Object.create(Connection.prototype);
var router = new routerModule.Router(serialized.connection);
var connection = new serialMojom.Connection.proxyClass(router);
var receiver = dataReceiver.DataReceiver.deserialize(serialized.receiver);
var sender = dataSender.DataSender.deserialize(serialized.sender);
// Ensure that paused and persistent are booleans.
serialized.state.paused = !!serialized.state.paused;
serialized.state.persistent = !!serialized.state.persistent;
serialConnection.init_(serialized.state,
connection,
router,
receiver,
sender,
serialized.queuedReceiveData,
serialized.queuedReceiveError);
serialConnection.awaitingResume_ = true;
var connectionId = serialized.state.connectionId;
connections_.set(connectionId, serialConnection);
if (connectionId >= nextConnectionId_)
nextConnectionId_ = connectionId + 1;
return serialConnection;
};
// Resume receives on a deserialized connection.
Connection.prototype.resumeReceives = function() {
if (!this.awaitingResume_)
return;
this.awaitingResume_ = false;
if (!this.state_.paused)
this.startReceive_();
};
// All accesses to connections_ and nextConnectionId_ other than those
// involved in deserialization should ensure that
// connectionDeserializationComplete_ has resolved first.
var connectionDeserializationComplete_ = stashClient.retrieve(
'serial', serialization.SerializedConnection).then(function(decoded) {
if (!decoded)
return;
return Promise.all($Array.map(decoded, Connection.deserialize));
});
// The map of connection ID to connection object.
var connections_ = new Map();
// The next connection ID to be allocated.
var nextConnectionId_ = 0;
function getConnections() {
return connectionDeserializationComplete_.then(function() {
return new Map(connections_);
});
}
function getConnection(id) {
return getConnections().then(function(connections) {
if (!connections.has(id))
throw new Error('Serial connection not found.');
return connections.get(id);
});
}
function allocateConnectionId() {
return connectionDeserializationComplete_.then(function() {
return nextConnectionId_++;
});
}
stashClient.registerClient(
'serial', serialization.SerializedConnection, function() {
return connectionDeserializationComplete_.then(function() {
var clientPromises = [];
for (var connection of connections_.values()) {
if (connection.state_.persistent)
clientPromises.push(connection.serialize());
else
connection.close();
}
return Promise.all($Array.map(clientPromises, function(promise) {
return promise.then(function(serialization) {
return {
serialization: serialization,
monitorHandles: !serialization.paused,
};
});
}));
});
});
return {
getDevices: getDevices,
createConnection: Connection.create,
getConnection: getConnection,
getConnections: getConnections,
// For testing.
Connection: Connection,
};
});