Skip to content

Commit 4b984e8

Browse files
committed
write binding tests and cleanup (#808)
- Fix bug where write callback was being called multiple times when write operations blocked - [windows] refactored write code to be less complex - [unix] refactored write code to be less complex - added arduino required integration tests
1 parent 17b0133 commit 4b984e8

File tree

10 files changed

+195
-65
lines changed

10 files changed

+195
-65
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ The `buffer` parameter accepts a [`Buffer` ](http://nodejs.org/api/buffer.html)
364364

365365
**_callback (optional)_**
366366

367-
Called once the write operation returns. The callback should be a function that looks like: `function (error, bytesWritten) { ... }`
367+
Called once the write operation returns. The callback should be a function that looks like: `function (error) { ... }`
368368

369369
**Note:** The write operation is non-blocking. When it returns, data may still have not actually been written to the serial port. See `drain()`.
370370

lib/serialport.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -262,13 +262,13 @@ SerialPort.prototype.write = function(buffer, callback) {
262262
if (!Buffer.isBuffer(buffer)) {
263263
buffer = new Buffer(buffer);
264264
}
265-
debug('write data: ' + JSON.stringify(buffer));
266-
SerialPortBinding.write(this.fd, buffer, function(err, result) {
265+
debug('write ' + buffer.length + ' bytes of data');
266+
SerialPortBinding.write(this.fd, buffer, function(err) {
267267
if (err) {
268268
debug('SerialPortBinding.write had an error', err);
269269
return this._error(err, callback);
270270
}
271-
if (callback) { callback(null, result) }
271+
if (callback) { callback(null) }
272272
}.bind(this));
273273
};
274274

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "serialport",
3-
"version": "3.1.2",
3+
"version": "3.2.0-beta1",
44
"description": "Node.js package to access serial ports. Welcome your robotic javascript overlords. Better yet, program them!",
55
"author": {
66
"name": "Chris Williams",
@@ -10,7 +10,7 @@
1010
"binary": {
1111
"module_name": "serialport",
1212
"module_path": "build/{configuration}/",
13-
"host": "https://github.com/voodootikigod/node-serialport/releases/download/3.1.2"
13+
"host": "https://github.com/voodootikigod/node-serialport/releases/download/3.2.0-beta1"
1414
},
1515
"main": "./lib/serialport",
1616
"repository": {
@@ -92,6 +92,7 @@
9292
"rebuild-all": "npm rebuild && node-gyp rebuild",
9393
"gyp-rebuild": "node-gyp rebuild",
9494
"stress": "mocha --no-timeouts test/arduinoTest/stress.js",
95+
"integration": "mocha test/arduinoTest/integration.js test/integration-lite.js",
9596
"grunt": "grunt",
9697
"test": "grunt --verbose",
9798
"valgrind": "valgrind --leak-check=full --show-possibly-lost=no node --expose-gc --trace-gc node_modules/.bin/grunt test"

src/serialport.cpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -289,15 +289,12 @@ void EIO_AfterWrite(uv_work_t* req) {
289289
QueuedWrite* queuedWrite = static_cast<QueuedWrite*>(req->data);
290290
WriteBaton* data = static_cast<WriteBaton*>(queuedWrite->baton);
291291

292-
v8::Local<v8::Value> argv[2];
292+
v8::Local<v8::Value> argv[1];
293293
if (data->errorString[0]) {
294294
argv[0] = v8::Exception::Error(Nan::New<v8::String>(data->errorString).ToLocalChecked());
295-
argv[1] = Nan::Undefined();
296295
} else {
297-
argv[0] = Nan::Undefined();
298-
argv[1] = Nan::New<v8::Int32>(data->result);
296+
argv[0] = Nan::Null();
299297
}
300-
data->callback->Call(2, argv);
301298

302299
if (data->offset < data->bufferLength && !data->errorString[0]) {
303300
// We're not done with this baton, so throw it right back onto the queue.
@@ -308,6 +305,7 @@ void EIO_AfterWrite(uv_work_t* req) {
308305
return;
309306
}
310307

308+
// throwing errors instead of returning them at this point is rude
311309
int fd = data->fd;
312310
_WriteQueue *q = qForFD(fd);
313311
if (!q) {
@@ -321,6 +319,8 @@ void EIO_AfterWrite(uv_work_t* req) {
321319
// remove this one from the list
322320
queuedWrite->remove();
323321

322+
data->callback->Call(1, argv);
323+
324324
// If there are any left, start a new thread to write the next one.
325325
if (!write_queue.empty()) {
326326
// Always pull the next work item from the head of the queue

src/serialport_unix.cpp

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -333,31 +333,33 @@ int setup(int fd, OpenBaton *data) {
333333
void EIO_Write(uv_work_t* req) {
334334
QueuedWrite* queuedWrite = static_cast<QueuedWrite*>(req->data);
335335
WriteBaton* data = static_cast<WriteBaton*>(queuedWrite->baton);
336+
int bytesWritten = 0;
336337

337-
data->result = 0;
338-
errno = 0;
339-
340-
// We carefully *DON'T* break out of this loop.
341338
do {
342-
if ((data->result = write(data->fd, data->bufferData + data->offset, data->bufferLength - data->offset)) == -1) {
343-
if (errno == EAGAIN || errno == EWOULDBLOCK)
344-
return;
345-
346-
// The write call might be interrupted, if it is we just try again immediately.
347-
if (errno != EINTR) {
348-
snprintf(data->errorString, sizeof(data->errorString), "Error: %s, calling write", strerror(errno));
349-
return;
350-
}
339+
errno = 0; // probably don't need this
340+
bytesWritten = write(data->fd, data->bufferData + data->offset, data->bufferLength - data->offset);
341+
if (-1 != bytesWritten) {
342+
// there wasn't an error, do the math on what we actually wrote and keep writing until finished
343+
data->offset += bytesWritten;
344+
continue;
345+
}
351346

352-
// try again...
347+
// The write call was interrupted before anything was written, try again immediately.
348+
if (errno == EINTR) {
349+
// why try again right away instead of in another event loop?
353350
continue;
354-
} else {
355-
// there wasn't an error, do the math on what we actually wrote...
356-
data->offset += data->result;
357351
}
358352

359-
// if we get there, we really don't want to loop
360-
// break;
353+
// Try again in another event loop
354+
if (errno == EAGAIN || errno == EWOULDBLOCK){
355+
return;
356+
}
357+
358+
// EBAD would mean we're "disconnected"
359+
360+
// a real error so lets bail
361+
snprintf(data->errorString, sizeof(data->errorString), "Error: %s, calling write", strerror(errno));
362+
return;
361363
} while (data->bufferLength > data->offset);
362364
}
363365

src/serialport_win.cpp

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,12 @@ void EIO_Open(uv_work_t* req) {
5757
HANDLE file = CreateFile(
5858
data->path,
5959
GENERIC_READ | GENERIC_WRITE,
60-
0,
60+
0, // dwShareMode 0 Prevents other processes from opening if they request delete, read, or write access
6161
NULL,
6262
OPEN_EXISTING,
63-
FILE_FLAG_OVERLAPPED,
64-
NULL);
63+
FILE_FLAG_OVERLAPPED, // allows for reading and writing at the same time and sets the handle for asynchronous I/O
64+
NULL
65+
);
6566
if (file == INVALID_HANDLE_VALUE) {
6667
DWORD errorCode = GetLastError();
6768
char temp[100];
@@ -368,35 +369,31 @@ void EIO_Write(uv_work_t* req) {
368369
ov.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
369370

370371
// Start write operation - synchrounous or asynchronous
371-
DWORD bytesWrittenSync = 0;
372-
if (!WriteFile((HANDLE)data->fd, data->bufferData, static_cast<DWORD>(data->bufferLength), &bytesWrittenSync, &ov)) {
372+
DWORD bytesWritten = 0;
373+
if (!WriteFile((HANDLE)data->fd, data->bufferData, static_cast<DWORD>(data->bufferLength), &bytesWritten, &ov)) {
373374
DWORD lastError = GetLastError();
374375
if (lastError != ERROR_IO_PENDING) {
375376
// Write operation error
376377
ErrorCodeToString("Writing to COM port (WriteFile)", lastError, data->errorString);
378+
CloseHandle(ov.hEvent);
377379
return;
378-
} else {
379-
// Write operation is asynchronous and is pending
380-
// We MUST wait for operation completion before deallocation of OVERLAPPED struct
381-
// or write data buffer
382-
383-
// Wait for async write operation completion or timeout
384-
DWORD bytesWrittenAsync = 0;
385-
if (!GetOverlappedResult((HANDLE)data->fd, &ov, &bytesWrittenAsync, TRUE)) {
386-
// Write operation error
387-
DWORD lastError = GetLastError();
388-
ErrorCodeToString("Writing to COM port (GetOverlappedResult)", lastError, data->errorString);
389-
return;
390-
} else {
391-
// Write operation completed asynchronously
392-
data->result = bytesWrittenAsync;
393-
}
394380
}
395-
} else {
396-
// Write operation completed synchronously
397-
data->result = bytesWrittenSync;
398-
}
381+
// Write operation is completing asynchronously
382+
// We MUST wait for the operation completion before deallocation of OVERLAPPED struct
383+
// or write data buffer
399384

385+
// block for async write operation completion
386+
bytesWritten = 0;
387+
if (!GetOverlappedResult((HANDLE)data->fd, &ov, &bytesWritten, TRUE)) {
388+
// Write operation error
389+
DWORD lastError = GetLastError();
390+
ErrorCodeToString("Writing to COM port (GetOverlappedResult)", lastError, data->errorString);
391+
CloseHandle(ov.hEvent);
392+
return;
393+
}
394+
}
395+
// Write operation completed synchronously
396+
data->result = bytesWritten;
400397
data->offset += data->result;
401398
CloseHandle(ov.hEvent);
402399
} while (data->bufferLength > data->offset);
Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
#define SET_BAUD_57600 130
2+
#define SET_BAUD_9600 131
3+
14
void setup() {
25
pinMode(LED_BUILTIN, OUTPUT);
36
Serial.begin(9600);
@@ -7,14 +10,19 @@ void setup() {
710
void loop() {
811
while (Serial.available()) {
912
int byte = Serial.read();
10-
if (byte == 130) {
11-
Serial.begin(57600);
12-
Serial.write("set to 57600");
13-
} else if (byte == 131) {
14-
Serial.begin(9600);
15-
Serial.write("set to 9600");
16-
} else {
17-
Serial.write(byte);
13+
switch (byte) {
14+
case SET_BAUD_57600:
15+
Serial.begin(57600);
16+
Serial.write("set to 57600");
17+
break;
18+
case SET_BAUD_9600:
19+
Serial.begin(9600);
20+
Serial.write("set to 9600");
21+
break;
22+
default:
23+
Serial.write(byte);
24+
break;
1825
}
1926
}
2027
}
28+

test/arduinoTest/integration.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
'use strict';
2+
var crypto = require('crypto');
3+
var assert = require('chai').assert;
4+
var serialPort = require('../../');
5+
var SerialPort = serialPort.SerialPort;
6+
7+
var platform;
8+
switch (process.platform) {
9+
case 'win32':
10+
platform = 'win32';
11+
break;
12+
case 'darwin':
13+
platform = 'darwin';
14+
break;
15+
default:
16+
platform = 'unix';
17+
}
18+
19+
var testPort = process.env.TEST_PORT;
20+
21+
if (!testPort) {
22+
throw 'These test require an arduino loaded with the arduinoEcho program on a serialport set to the TEST_PORT env var';
23+
}
24+
25+
describe('SerialPort Integration tests', function() {
26+
it('.list', function(done) {
27+
serialPort.list(function(err, ports) {
28+
var foundPort = false;
29+
ports.forEach(function(port) {
30+
if (port.comName === testPort){
31+
foundPort = true;
32+
}
33+
});
34+
assert.isTrue(foundPort);
35+
done();
36+
});
37+
});
38+
39+
// Be careful to close the ports when you're done with them
40+
// Ports are exclusively locked in windows and maybe other platforms eventually
41+
42+
describe('#update', function() {
43+
if (platform === 'win32') {
44+
return it("Isn't supported on windows yet");
45+
}
46+
47+
it('allows changing the baud rate of an open port', function(done) {
48+
var port = new SerialPort(testPort, function() {
49+
port.update({baudRate: 57600}, function(err) {
50+
assert.isNull(err);
51+
port.close(done);
52+
});
53+
});
54+
});
55+
it('can still read and write after a baud change');
56+
});
57+
58+
describe('#read and #write', function() {
59+
it('writes 5k and reads it back', function(done) {
60+
this.timeout(20000);
61+
// 5k of random ascii
62+
var output = new Buffer(crypto.randomBytes(5000).toString('ascii'));
63+
var expectedInput = Buffer.concat([new Buffer('READY'), output]);
64+
var port = new SerialPort(testPort);
65+
66+
// this will trigger from the "READY" the arduino sends when it's... ready
67+
port.once('data', function(){
68+
port.write(output);
69+
});
70+
71+
var input = new Buffer(0);
72+
port.on('data', function(data) {
73+
input = Buffer.concat([input, data]);
74+
if (input.length === expectedInput.length){
75+
assert.deepEqual(expectedInput, input);
76+
port.close(done);
77+
}
78+
});
79+
});
80+
});
81+
});

test/bindings.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,42 @@ describe('SerialPortBinding', function() {
178178
});
179179
});
180180
});
181+
describe('#write', function() {
182+
if (!testPort) {
183+
it('Cannot be tested as we have no test ports on ' + platform);
184+
return;
185+
}
186+
187+
beforeEach(function(done) {
188+
SerialPortBinding.open(testPort, defaultPortOpenOptions, function(err, fd) {
189+
assert.isNull(err);
190+
assert.isNumber(fd);
191+
this.fd = fd;
192+
done();
193+
}.bind(this));
194+
});
195+
196+
afterEach(function(done) {
197+
var fd = this.fd;
198+
this.fd = null;
199+
SerialPortBinding.close(fd, done);
200+
});
201+
202+
it('calls the write callback once after a small write', function(done){
203+
var data = new Buffer('simple write of 24 bytes');
204+
SerialPortBinding.write(this.fd, data, function(err){
205+
assert.isNull(err);
206+
done();
207+
});
208+
});
209+
210+
it('calls the write callback once after a 5k write', function(done){
211+
this.timeout(20000);
212+
var data = new Buffer(1024 * 5);
213+
SerialPortBinding.write(this.fd, data, function(err){
214+
assert.isNull(err);
215+
done();
216+
});
217+
});
218+
});
181219
});

0 commit comments

Comments
 (0)