Skip to content

Commit

Permalink
Streams rewrite
Browse files Browse the repository at this point in the history
This is a major change.  To `read()`, the poller and the bindings layer, there isn’t a great way to do this piecemeal. The goal here is to make bindings a plugin layer, our c++ bindings isolated and tested, and make serialport a valid streams object.

 - Move all bindings into platform specific files
 - Provide mock bindings as a tested first class bindings layer
 - Separate out the binary bindings from the SerialPort JS so it can be required without bindings
 - Make Bindings manage .isOpen and FD as file descriptors are an implementation detail
 - Make Bindings an instantiated object so they can isolate their own state, have setup code, etc.
 - Since all integration tests now require an arduino ditch the "integration-light" tests and test with both native and mock bindings

TODO

 - Figure out how we’re going to do _read
 - ensure docs are up to date with sp methods
 - ensure bindings are documented
 - redo parsers as transform streams
 - update parser documentation
 - performance test
 - 🎉

We're currently providing `push` to the binding constructor and asking the binding to provide a `_read` implementation. Considering a `read(function(data){});` interface instead. 

The c++ bindings use .open to create some global state around write queues and read pollers. Now that the stream will only have a single call to write out at any time this is probably not necessary. For Reads on the other hands it might absolutely be necessary. In any case, hiding this state in a “global scope” is bad practice.
  • Loading branch information
reconbot committed Aug 22, 2016
1 parent cf822fc commit 4690c57
Show file tree
Hide file tree
Showing 22 changed files with 1,531 additions and 1,323 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ module.exports = {
},
rules: {
"brace-style": [2, "1tbs", {"allowSingleLine": true} ],
"complexity": [2, 12],
"complexity": [2, 13],
"curly": 2,
"eqeqeq": 2,
"indent": [2, 2, {
Expand Down
425 changes: 337 additions & 88 deletions README.md

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions lib/bindings-auto-detect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use strict';

switch (process.platform) {
case 'win32':
module.exports = require('./bindings-win32');
break;
case 'darwin':
module.exports = require('./bindings-darwin');
break;
default:
module.exports = require('./bindings-unix');
}
87 changes: 87 additions & 0 deletions lib/bindings-darwin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
'use strict';

var binding = require('bindings')('serialport.node');

function DarwinBinding(opt) {
if (typeof opt.disconnect !== 'function') {
throw new TypeError('options.disconnect is not a function');
}
this.disconnect = opt.disconnect;
this.fd = null;
};

DarwinBinding.prototype.open = function(path, options, cb) {
binding.open(path, options, function(err, fd) {
if (err) {
return cb(err);
}
this.fd = fd;
cb(null);
}.bind(this));
};

DarwinBinding.prototype.close = function(cb) {
if (!this.isOpen) {
return cb(new Error('Already closed'));
}

binding.close(this.fd, function(err) {
if (err) {
return cb(err);
}
this.fd = null;
cb(null);
}.bind(this));
};

DarwinBinding.prototype.set = function(opt, cb) {
if (typeof opt !== 'object') {
throw new TypeError('options is not an object');
}

if (!this.isOpen) {
return cb(new Error('Port is not open'));
}
binding.set(this.fd, opt, cb);
};

DarwinBinding.prototype.write = function(buffer, cb) {
if (!Buffer.isBuffer(buffer)) {
throw new TypeError('buffer is not a Buffer');
}

if (!this.isOpen) {
return cb(new Error('Port is not open'));
}

binding.write(this.fd, buffer, cb);
};

var commonMethods = [
'drain',
'flush',
'update',
'read'
];

commonMethods.map(function(methodName) {
DarwinBinding.prototype[methodName] = function() {
var args = Array.prototype.slice.apply(arguments);
if (!this.isOpen) {
var cb = args.pop();
return cb(new Error('Port is not open'));
}
args.unshift(this.fd);
binding[methodName].apply(binding, args);
};
});

Object.defineProperty(DarwinBinding.prototype, 'isOpen', {
enumerable: true,
get: function() {
return this.fd !== null;
}
});

DarwinBinding.list = binding.list;
module.exports = DarwinBinding;
188 changes: 188 additions & 0 deletions lib/bindings-mock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
'use strict';

var util = require('util');
var processNextTick = require('process-nextick-args');

function MissingPortError(message) {
this.message = message || 'Port does not exist - please call hardware.createPort(path) first';
this.name = 'MissingPortError';
Error.captureStackTrace(this, MissingPortError);
}
util.inherits(MissingPortError, Error);

var ports = {};

function MockBindings(opt) {
if (typeof opt.disconnect !== 'function') {
throw new TypeError('options.disconnect is not a function');
}
this.disconnectedCallback = opt.disconnect;
this.isOpen = false;
};

MockBindings.reset = function() {
ports = {};
};

MockBindings.createPort = function(path, options) {
var echo = (options || {}).echo;
ports[path] = {
data: new Buffer(0),
lastWrite: null,
echo: echo,
info: {
comName: path,
manufacturer: 'The J5 Robotics Company',
serialNumber: undefined,
pnpId: undefined,
locationId: undefined,
vendorId: undefined,
productId: undefined
}
};
};

MockBindings.list = function(cb) {
var info = Object.keys(ports).map(function(path) {
return ports[path].info;
});
processNextTick(cb, null, info);
};

MockBindings.prototype.emitData = function(data) {
if (!this.port) {
return;
}
this.port.data = Buffer.concat([this.port.data, data]);
if (this.pendingRead) {
processNextTick(this.finishRead.bind(this));
}
};

MockBindings.prototype.disconnect = function() {
var err = new Error('disconnected');
this.disconnectedCallback(err);
};

MockBindings.prototype.open = function(path, opt, cb) {
var port = this.port = ports[path];
if (!port) {
return cb(new MissingPortError(path));
}

if (port.openOpt && port.openOpt.lock) {
return cb(new Error('port is locked cannot open'));
}
port.openOpt = opt;
processNextTick(function() {
this.isOpen = true;
processNextTick(function() {
cb(null);
if (port.echo) {
processNextTick(function() {
this.emitData(new Buffer('READY'));
}.bind(this));
}
}.bind(this));
}.bind(this));
};

MockBindings.prototype.close = function(cb) {
var port = this.port;
if (!port) {
return processNextTick(cb, new Error('port is already closed'));
}
processNextTick(function() {
delete port.openOpt;

// reset data on close
port.data = new Buffer(0);

delete this.port;
this.isOpen = false;
processNextTick(cb, null);
}.bind(this));
};

MockBindings.prototype.update = function(opt, cb) {
if (typeof opt !== 'object') {
throw new TypeError('options is not an object');
}

if (!opt.baudRate) {
throw new Error('Missing baudRate');
}

if (!this.port) {
return processNextTick(cb, new MissingPortError());
}
this.port.openOpt.baudRate = opt.baudRate;
processNextTick(cb, null);
};

MockBindings.prototype.set = function(opt, cb) {
if (typeof opt !== 'object') {
throw new TypeError('options is not an object');
}

if (!this.port) {
return processNextTick(cb, new MissingPortError());
}
processNextTick(cb, null);
};

MockBindings.prototype.write = function(buffer, cb) {
if (!Buffer.isBuffer(buffer)) {
throw new TypeError('buffer is not a Buffer');
}

var port = this.port;
if (!port) {
return processNextTick(cb, new MissingPortError());
}

port.lastWrite = new Buffer(buffer); // copy
processNextTick(cb, null);

if (port.echo) {
processNextTick(this.emitData.bind(this), port.lastWrite);
}
};

MockBindings.prototype.read = function(cb) {
var port = this.port;
if (!port) {
return processNextTick(cb, new MissingPortError());
}
if (this.pendingRead) {
return processNextTick(cb, new Error('Already reading'));
}
var data = port.data;
port.data = new Buffer(0);
if (data.length > 0) {
return processNextTick(cb, null, data);
}
this.pendingRead = cb;
};

MockBindings.prototype.finishRead = function() {
var cb = this.pendingRead;
delete this.pendingRead;
processNextTick(this.read.bind(this), cb);
};

MockBindings.prototype.flush = function(cb) {
if (!this.port) {
return processNextTick(cb, new MissingPortError());
}
processNextTick(cb, null);
};

MockBindings.prototype.drain = function(cb) {
if (!this.port) {
return processNextTick(cb, new MissingPortError());
}
processNextTick(cb, null);
};

module.exports = MockBindings;
17 changes: 17 additions & 0 deletions lib/bindings-unix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use strict';

var bindings = require('bindings')('serialport.node');
var listLinux = require('./list-linux');

module.exports = {
close: bindings.close,
drain: bindings.drain,
flush: bindings.flush,
list: listLinux,
open: bindings.open,
set: bindings.set,
update: bindings.update,
write: bindings.write,
read: bindings.read,
dataAvailable: bindings.dataAvailable
};
16 changes: 16 additions & 0 deletions lib/bindings-win32.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use strict';

var bindings = require('bindings')('serialport.node');

module.exports = {
close: bindings.close,
drain: bindings.drain,
flush: bindings.flush,
list: bindings.list,
open: bindings.open,
set: bindings.set,
update: bindings.update,
write: bindings.write,
read: bindings.read,
dataAvailable: bindings.dataAvailable
};
27 changes: 0 additions & 27 deletions lib/bindings.js

This file was deleted.

12 changes: 12 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use strict';

/**
* @module serialport
* @copyright Chris Williams <chris@iterativedesigns.com>
*/

var SerialPort = require('./serialport');
var Binding = require('./bindings-auto-detect');

SerialPort.Binding = Binding;
module.exports = SerialPort;
15 changes: 15 additions & 0 deletions lib/read-unix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use strict';
// var fs = require('fs');

// module.export = function read(size) {
// var push = this.push.bind(this);
// var fd = this.fd;
// };


// bindings.ondata(fd, function(chunk) {
// // if push() returns false, then stop reading from source
// if (!this.push(chunk)) {
// this._source.readStop();
// }
// });
Loading

0 comments on commit 4690c57

Please sign in to comment.