Commit 638a7d91 authored by sammc@chromium.org's avatar sammc@chromium.org

Implement more of chrome.serial on the Mojo SerialConnection.

This implements connect, disconnect, getInfo, update, getControlSignals,
setControlSignals, flush, setPaused and getConnections.

BUG=389016

Review URL: https://codereview.chromium.org/423403002

Cr-Commit-Position: refs/heads/master@{#289578}
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@289578 0039d316-1c4b-4281-b951-d872f2087c98
parent 3ba20389
...@@ -166,6 +166,7 @@ test("unittests") { ...@@ -166,6 +166,7 @@ test("unittests") {
"//base/test:test_support", "//base/test:test_support",
"//content/test:test_support", "//content/test:test_support",
"//device/serial", "//device/serial",
"//device/serial:test_util",
"//extensions/common", "//extensions/common",
"//extensions/renderer", "//extensions/renderer",
"//extensions/strings", "//extensions/strings",
......
...@@ -825,6 +825,7 @@ ...@@ -825,6 +825,7 @@
'../components/components.gyp:keyed_service_content', '../components/components.gyp:keyed_service_content',
'../content/content_shell_and_tests.gyp:test_support_content', '../content/content_shell_and_tests.gyp:test_support_content',
'../device/serial/serial.gyp:device_serial', '../device/serial/serial.gyp:device_serial',
'../device/serial/serial.gyp:device_serial_test_util',
'../mojo/mojo_base.gyp:mojo_environment_chromium', '../mojo/mojo_base.gyp:mojo_environment_chromium',
'../mojo/mojo_base.gyp:mojo_cpp_bindings', '../mojo/mojo_base.gyp:mojo_cpp_bindings',
'../mojo/mojo_base.gyp:mojo_js_bindings_lib', '../mojo/mojo_base.gyp:mojo_js_bindings_lib',
......
...@@ -2,6 +2,15 @@ ...@@ -2,6 +2,15 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
/**
* Custom bindings for the Serial API.
*
* The bindings are implemented by asynchronously delegating to the
* serial_service module. The functions that apply to a particular connection
* are delegated to the appropriate method on the Connection object specified by
* the ID parameter.
*/
var binding = require('binding').Binding.create('serial'); var binding = require('binding').Binding.create('serial');
function createAsyncProxy(targetPromise, functionNames) { function createAsyncProxy(targetPromise, functionNames) {
...@@ -12,19 +21,64 @@ function createAsyncProxy(targetPromise, functionNames) { ...@@ -12,19 +21,64 @@ function createAsyncProxy(targetPromise, functionNames) {
return targetPromise.then(function(target) { return targetPromise.then(function(target) {
return $Function.apply(target[name], target, args); return $Function.apply(target[name], target, args);
}); });
} };
}); });
return functionProxies; return functionProxies;
} }
var serialService = createAsyncProxy(requireAsync('serial_service'), [ var serialService = createAsyncProxy(requireAsync('serial_service'), [
'getDevices', 'getDevices',
'createConnection',
'getConnection',
'getConnections',
]); ]);
function forwardToConnection(methodName) {
return function(connectionId) {
var args = $Array.slice(arguments, 1);
return serialService.getConnection(connectionId).then(function(connection) {
return $Function.apply(connection[methodName], connection, args);
});
};
}
binding.registerCustomHook(function(bindingsAPI) { binding.registerCustomHook(function(bindingsAPI) {
var apiFunctions = bindingsAPI.apiFunctions; var apiFunctions = bindingsAPI.apiFunctions;
apiFunctions.setHandleRequestWithPromise('getDevices', apiFunctions.setHandleRequestWithPromise('getDevices',
serialService.getDevices); serialService.getDevices);
apiFunctions.setHandleRequestWithPromise('connect', function(path, options) {
return serialService.createConnection(path, options).then(function(result) {
return result.info;
}).catch (function(e) {
throw new Error('Failed to connect to the port.');
});
});
apiFunctions.setHandleRequestWithPromise(
'disconnect', forwardToConnection('close'));
apiFunctions.setHandleRequestWithPromise(
'getInfo', forwardToConnection('getInfo'));
apiFunctions.setHandleRequestWithPromise(
'update', forwardToConnection('setOptions'));
apiFunctions.setHandleRequestWithPromise(
'getControlSignals', forwardToConnection('getControlSignals'));
apiFunctions.setHandleRequestWithPromise(
'setControlSignals', forwardToConnection('setControlSignals'));
apiFunctions.setHandleRequestWithPromise(
'flush', forwardToConnection('flush'));
apiFunctions.setHandleRequestWithPromise(
'setPaused', forwardToConnection('setPaused'));
apiFunctions.setHandleRequestWithPromise('getConnections', function() {
return serialService.getConnections().then(function(connections) {
var promises = [];
for (var id in connections) {
promises.push(connections[id].getInfo());
}
return Promise.all(promises);
});
});
}); });
exports.binding = binding.generate(); exports.binding = binding.generate();
...@@ -5,21 +5,20 @@ ...@@ -5,21 +5,20 @@
define('serial_service', [ define('serial_service', [
'content/public/renderer/service_provider', 'content/public/renderer/service_provider',
'device/serial/serial.mojom', 'device/serial/serial.mojom',
'mojo/public/js/bindings/core',
'mojo/public/js/bindings/router', 'mojo/public/js/bindings/router',
], function(serviceProvider, serialMojom, routerModule) { ], function(serviceProvider, 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.
*/
function defineService(proxy, handle) { var service = new serialMojom.SerialServiceProxy(new routerModule.Router(
if (!handle) serviceProvider.connectToService(serialMojom.SerialServiceProxy.NAME_)));
handle = serviceProvider.connectToService(proxy.NAME_);
var router = new routerModule.Router(handle);
var service = new proxy(router);
return {
service: service,
router: router,
};
}
var service = defineService(serialMojom.SerialServiceProxy).service;
function getDevices() { function getDevices() {
return service.getDevices().then(function(response) { return service.getDevices().then(function(response) {
...@@ -36,7 +35,240 @@ define('serial_service', [ ...@@ -36,7 +35,240 @@ define('serial_service', [
}); });
} }
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,
};
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);
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, id, options) {
this.remoteConnection_ = remoteConnection;
this.router_ = router;
this.id_ = id;
getConnections().then(function(connections) {
connections[this.id_] = this;
}.bind(this));
this.paused_ = false;
this.options_ = {};
for (var key in DEFAULT_CLIENT_OPTIONS) {
this.options_[key] = DEFAULT_CLIENT_OPTIONS[key];
}
this.setClientOptions_(options);
}
Connection.create = function(path, options) {
options = options || {};
var serviceOptions = getServiceOptions(options);
var pipe = core.createMessagePipe();
service.connect(path, serviceOptions, pipe.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();
throw e;
}).then(function(results) {
var info = results[0];
var id = results[1];
var serialConnectionClient = new Connection(
connection, router, 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();
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;
};
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 { return {
getDevices: getDevices, getDevices: getDevices,
createConnection: Connection.create,
getConnection: getConnection,
getConnections: getConnections,
}; };
}); });
...@@ -2,10 +2,62 @@ ...@@ -2,10 +2,62 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
/**
* Unit tests for the JS serial service client.
*
* These test that configuration and data are correctly transmitted between the
* client and the service.
*/
var test = require('test').binding; var test = require('test').binding;
var serial = require('serial').binding; var serial = require('serial').binding;
var unittestBindings = require('test_environment_specific_bindings'); var unittestBindings = require('test_environment_specific_bindings');
var connectionId = null;
function connect(callback, options) {
options = options || {
name: 'test connection',
bufferSize: 8192,
receiveTimeout: 12345,
sendTimeout: 6789,
persistent: true,
}
serial.connect('device', options, test.callbackPass(function(connectionInfo) {
connectionId = connectionInfo.connectionId;
callback(connectionInfo);
}));
}
function disconnect() {
serial.disconnect(connectionId, test.callbackPass(function(success) {
test.assertTrue(success);
connectionId = null;
}));
}
function checkClientConnectionInfo(connectionInfo) {
test.assertFalse(connectionInfo.persistent);
test.assertEq('test connection', connectionInfo.name);
test.assertEq(12345, connectionInfo.receiveTimeout);
test.assertEq(6789, connectionInfo.sendTimeout);
test.assertEq(8192, connectionInfo.bufferSize);
test.assertFalse(connectionInfo.paused);
}
function checkServiceConnectionInfo(connectionInfo) {
test.assertEq(9600, connectionInfo.bitrate);
test.assertEq('eight', connectionInfo.dataBits);
test.assertEq('no', connectionInfo.parityBit);
test.assertEq('one', connectionInfo.stopBits);
test.assertFalse(connectionInfo.ctsFlowControl);
}
function checkConnectionInfo(connectionInfo) {
checkClientConnectionInfo(connectionInfo);
checkServiceConnectionInfo(connectionInfo);
}
unittestBindings.exportTests([ unittestBindings.exportTests([
function testGetDevices() { function testGetDevices() {
serial.getDevices(test.callbackPass(function(devices) { serial.getDevices(test.callbackPass(function(devices) {
...@@ -21,4 +73,240 @@ unittestBindings.exportTests([ ...@@ -21,4 +73,240 @@ unittestBindings.exportTests([
test.assertEq('', devices[2].path); test.assertEq('', devices[2].path);
})); }));
}, },
function testConnectFail() {
serial.connect('device',
test.callbackFail('Failed to connect to the port.'));
},
function testGetInfoFailOnConnect() {
serial.connect('device',
test.callbackFail('Failed to connect to the port.'));
},
function testConnectInvalidBitrate() {
serial.connect('device', {bitrate: -1}, test.callbackFail(
'Failed to connect to the port.'));
},
function testConnect() {
connect(function(connectionInfo) {
disconnect();
checkConnectionInfo(connectionInfo);
});
},
function testConnectDefaultOptions() {
connect(function(connectionInfo) {
disconnect();
test.assertEq(9600, connectionInfo.bitrate);
test.assertEq('eight', connectionInfo.dataBits);
test.assertEq('no', connectionInfo.parityBit);
test.assertEq('one', connectionInfo.stopBits);
test.assertFalse(connectionInfo.ctsFlowControl);
test.assertFalse(connectionInfo.persistent);
test.assertEq('', connectionInfo.name);
test.assertEq(0, connectionInfo.receiveTimeout);
test.assertEq(0, connectionInfo.sendTimeout);
test.assertEq(4096, connectionInfo.bufferSize);
}, {});
},
function testGetInfo() {
connect(function() {
serial.getInfo(connectionId,
test.callbackPass(function(connectionInfo) {
disconnect();
checkConnectionInfo(connectionInfo);
}));
});
},
function testGetInfoFailToGetPortInfo() {
connect(function() {
serial.getInfo(connectionId,
test.callbackPass(function(connectionInfo) {
disconnect();
checkClientConnectionInfo(connectionInfo);
test.assertFalse('bitrate' in connectionInfo);
test.assertFalse('dataBits' in connectionInfo);
test.assertFalse('parityBit' in connectionInfo);
test.assertFalse('stopBit' in connectionInfo);
test.assertFalse('ctsFlowControl' in connectionInfo);
}));
});
},
function testGetConnections() {
connect(function() {
serial.getConnections(test.callbackPass(function(connections) {
disconnect();
test.assertEq(1, connections.length);
checkConnectionInfo(connections[0]);
}));
});
},
function testGetControlSignals() {
connect(function() {
var calls = 0;
function checkControlSignals(signals) {
if (calls == 15) {
disconnect();
} else {
serial.getControlSignals(
connectionId,
test.callbackPass(checkControlSignals));
}
test.assertEq(!!(calls & 1), signals.dcd);
test.assertEq(!!(calls & 2), signals.cts);
test.assertEq(!!(calls & 4), signals.ri);
test.assertEq(!!(calls & 8), signals.dsr);
calls++;
}
serial.getControlSignals(connectionId,
test.callbackPass(checkControlSignals));
});
},
function testSetControlSignals() {
connect(function() {
var signalsValues = [
{},
{dtr: false},
{dtr: true},
{rts: false},
{dtr: false, rts: false},
{dtr: true, rts: false},
{rts: true},
{dtr: false, rts: true},
{dtr: true, rts: true},
];
var calls = 0;
function setControlSignals(success) {
if (calls == signalsValues.length) {
disconnect();
} else {
serial.setControlSignals(connectionId,
signalsValues[calls++],
test.callbackPass(setControlSignals));
}
test.assertTrue(success);
}
setControlSignals(true);
});
},
function testUpdate() {
connect(function() {
var optionsValues = [
{}, // SetPortOptions is called once during connection.
{bitrate: 57600},
{dataBits: 'seven'},
{dataBits: 'eight'},
{parityBit: 'no'},
{parityBit: 'odd'},
{parityBit: 'even'},
{stopBits: 'one'},
{stopBits: 'two'},
{ctsFlowControl: false},
{ctsFlowControl: true},
{bufferSize: 1},
{sendTimeout: 0},
{receiveTimeout: 0},
{persistent: false},
{name: 'name'},
];
var calls = 0;
function checkInfo(info) {
for (var key in optionsValues[calls]) {
test.assertEq(optionsValues[calls][key], info[key]);
}
setOptions();
}
function setOptions() {
if (++calls == optionsValues.length) {
disconnect();
} else {
serial.update(connectionId,
optionsValues[calls],
test.callbackPass(function(success) {
serial.getInfo(connectionId, test.callbackPass(checkInfo));
test.assertTrue(success);
}));
}
}
setOptions();
});
},
function testUpdateInvalidBitrate() {
connect(function() {
serial.update(connectionId,
{bitrate: -1},
test.callbackPass(function(success) {
disconnect();
test.assertFalse(success);
}));
});
},
function testFlush() {
connect(function() {
serial.flush(connectionId,
test.callbackPass(function(success) {
disconnect();
test.assertTrue(success);
}));
});
},
function testSetPaused() {
connect(function() {
serial.setPaused(connectionId, true, test.callbackPass(function() {
serial.getInfo(connectionId, test.callbackPass(function(info) {
serial.setPaused(connectionId, false, test.callbackPass(function() {
serial.getInfo(connectionId, test.callbackPass(function(info) {
test.assertFalse(info.paused);
disconnect();
}));
}));
test.assertTrue(info.paused);
}));
}));
});
},
function testDisconnectUnknownConnectionId() {
serial.disconnect(-1, test.callbackFail('Serial connection not found.'));
},
function testGetInfoUnknownConnectionId() {
serial.getInfo(-1, test.callbackFail('Serial connection not found.'));
},
function testUpdateUnknownConnectionId() {
serial.update(-1, {}, test.callbackFail('Serial connection not found.'));
},
function testSetControlSignalsUnknownConnectionId() {
serial.setControlSignals(-1, {}, test.callbackFail(
'Serial connection not found.'));
},
function testGetControlSignalsUnknownConnectionId() {
serial.getControlSignals(-1, test.callbackFail(
'Serial connection not found.'));
},
function testFlushUnknownConnectionId() {
serial.flush(-1, test.callbackFail('Serial connection not found.'));
},
function testSetPausedUnknownConnectionId() {
serial.setPaused(
-1, true, test.callbackFail('Serial connection not found.'));
serial.setPaused(
-1, false, test.callbackFail('Serial connection not found.'));
},
], test.runTests, exports); ], test.runTests, exports);
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment