forked from serialport/node-serialport
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add packet length parser (serialport#2197)
* Add packet length parser
- Loading branch information
Showing
6 changed files
with
225 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
.DS_Store | ||
*.test.js | ||
CHANGELOG.md |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# Change Log | ||
|
||
All notable changes to this project will be documented in this file. | ||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
--- | ||
title: Packet Delimiter Length Parser | ||
--- | ||
```typescript | ||
new PacketLengthParser((options?) | ||
``` | ||
A transform stream that emits data after a delimiter and number of bytes is received. The length in bytes of the packet follows the delimiter at a specified byte offset. To use the `PacketLength` parser, provide a delimiter (defaults to 0xaa), packetOverhead (defaults to 2), number of length bytes (defaults to 1) and the lengthOffset (defaults to 1). Data is emitted as a buffer. | ||
Arguments | ||
- `options.delimiter?: UInt8` delimiter to use | ||
- `options.packetOverhead?: UInt8` overhead of packet (including length, delimiter and any checksum / packet footer) | ||
- `options.lengthBytes?: UInt8` number of bytes containing length | ||
- `options.lengthOffset?: UInt8` offset of length field | ||
- `options.maxLen?: UInt8` maximum valid length for a packet | ||
## Example | ||
```js | ||
// Parse length encoded packets received on the serial port in the form: | ||
// [delimiter][0][len 0][len 1][cargo 0]...[cargo n][footer 0] | ||
const SerialPort = require('serialport') | ||
const PacketLengthParser = require('@serialport/packet-length-parser') | ||
const port = new SerialPort('/dev/tty-usbserial1') | ||
const parser = port.pipe(new PacketLengthParser({ | ||
delimiter: 0xbc, | ||
packetOverhead: 5, | ||
lengthBytes: 2, | ||
lengthOffset: 2, | ||
})) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
const { Transform } = require('stream') | ||
|
||
/** | ||
* A transform stream that decodes packets with a delimiter and length of payload | ||
* specified within the data stream. | ||
* @extends Transform | ||
* @summary Decodes packets of the general form: | ||
* [delimiter][len][payload0] ... [payload0 + len] | ||
* | ||
* The length field can be up to 4 bytes and can be at any offset within the packet | ||
* [delimiter][header0][header1][len0][len1[payload0] ... [payload0 + len] | ||
* | ||
* The offset and number of bytes of the length field need to be provided in options | ||
* if not 1 byte immediately following the delimiter. | ||
* @example | ||
// Parse length encoded packets received on the serial port | ||
const SerialPort = require('serialport') | ||
const PacketLengthParser = require('@serialport/packet-length-parser') | ||
const port = new SerialPort('/dev/tty-usbserial1') | ||
const parser = port.pipe(new PacketLengthParser({ | ||
delimiter: 0xbc, | ||
packetOverhead: 5, | ||
lengthBytes: 2, | ||
lengthOffset: 2, | ||
})) | ||
*/ | ||
class PacketLengthParser extends Transform { | ||
constructor(options = {}) { | ||
super(options) | ||
|
||
const opts = { | ||
delimiter: 0xaa, | ||
packetOverhead: 2, | ||
lengthBytes: 1, | ||
lengthOffset: 1, | ||
maxLen: 0xff, | ||
|
||
...options, | ||
} | ||
this.opts = opts | ||
|
||
this.buffer = Buffer.alloc(0) | ||
this.start = false | ||
} | ||
|
||
_transform(chunk, encoding, cb) { | ||
for (let ndx = 0; ndx < chunk.length; ndx++) { | ||
const byte = chunk[ndx] | ||
|
||
if (byte === this.opts.delimiter) { | ||
this.start = true | ||
} | ||
|
||
if (true === this.start) { | ||
this.buffer = Buffer.concat([this.buffer, Buffer.from([byte])]) | ||
|
||
if (this.buffer.length >= this.opts.lengthOffset + this.opts.lengthBytes) { | ||
const len = this.buffer.readUIntLE(this.opts.lengthOffset, this.opts.lengthBytes) | ||
|
||
if (this.buffer.length == len + this.opts.packetOverhead || len > this.opts.maxLen) { | ||
this.push(this.buffer) | ||
this.buffer = Buffer.alloc(0) | ||
this.start = false | ||
} | ||
} | ||
} | ||
} | ||
|
||
cb() | ||
} | ||
|
||
_flush(cb) { | ||
this.push(this.buffer) | ||
this.buffer = Buffer.alloc(0) | ||
cb() | ||
} | ||
} | ||
|
||
module.exports = PacketLengthParser |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
/* eslint-disable no-new */ | ||
|
||
const sinon = require('sinon') | ||
const PacketLengthParser = require('../') | ||
|
||
describe('DelimiterParser', () => { | ||
it('transforms data to packets of correct length starting with delimiter', () => { | ||
const spy = sinon.spy() | ||
const parser = new PacketLengthParser() | ||
parser.on('data', spy) | ||
parser.write(Buffer.from('\xaa\x0dI love robots\xaa\x13Each ')) | ||
parser.write(Buffer.from('and Every One\n')) | ||
parser.write(Buffer.from([0xaa, 0x24])) | ||
parser.write(Buffer.from('even you!')) | ||
|
||
assert.deepEqual(spy.getCall(0).args[0], Buffer.concat([Buffer.from([0xaa, 0x0d]), Buffer.from('I love robots')])) | ||
assert.deepEqual(spy.getCall(1).args[0], Buffer.concat([Buffer.from([0xaa, 0x13]), Buffer.from('Each and Every One\n')])) | ||
assert(spy.calledTwice) | ||
}) | ||
|
||
it('transforms data to packets of correct length starting with delimiter when length is offset', () => { | ||
const spy = sinon.spy() | ||
const parser = new PacketLengthParser({ lengthOffset: 2, packetOverhead: 3 }) | ||
parser.on('data', spy) | ||
parser.write(Buffer.from('\xaa\x01\x0dI love robots\xaa\x02\x13Each ')) | ||
parser.write(Buffer.from('and Every One\n')) | ||
parser.write(Buffer.from([0xaa, 0x03, 0x24])) | ||
parser.write(Buffer.from('even you!')) | ||
|
||
assert.deepEqual(spy.getCall(0).args[0], Buffer.concat([Buffer.from([0xaa, 0x01, 0x0d]), Buffer.from('I love robots')])) | ||
assert.deepEqual(spy.getCall(1).args[0], Buffer.concat([Buffer.from([0xaa, 0x02, 0x13]), Buffer.from('Each and Every One\n')])) | ||
assert(spy.calledTwice) | ||
}) | ||
|
||
it('transforms data to packets of correct length starting with delimiter when length is offset and multibyte', () => { | ||
const spy = sinon.spy() | ||
const parser = new PacketLengthParser({ lengthOffset: 2, packetOverhead: 4, lengthBytes: 2 }) | ||
parser.on('data', spy) | ||
parser.write(Buffer.from('\xaa\x01\x0d\x00I love robots\xaa\x02\x13\x00Each ')) | ||
parser.write(Buffer.from('and Every One\n')) | ||
parser.write(Buffer.from([0xaa, 0x03, 0x24, 0x00])) | ||
parser.write(Buffer.from('even you!')) | ||
|
||
assert.deepEqual(spy.getCall(0).args[0], Buffer.concat([Buffer.from([0xaa, 0x01, 0x0d, 0x00]), Buffer.from('I love robots')])) | ||
assert.deepEqual(spy.getCall(1).args[0], Buffer.concat([Buffer.from([0xaa, 0x02, 0x13, 0x00]), Buffer.from('Each and Every One\n')])) | ||
assert(spy.calledTwice) | ||
}) | ||
|
||
it('flushes remaining data when the stream ends', () => { | ||
const spy = sinon.spy() | ||
const parser = new PacketLengthParser({ lengthOffset: 2, packetOverhead: 4, lengthBytes: 2 }) | ||
parser.on('data', spy) | ||
parser.write(Buffer.from('\xaa\x01\x0d\x00I love robots\xaa\x02\x13\x00Each ')) | ||
parser.write(Buffer.from('and Every One\n')) | ||
parser.write(Buffer.from([0xaa, 0x03, 0x24, 0x00])) | ||
parser.write(Buffer.from('even you!')) | ||
parser.end() | ||
|
||
assert.deepEqual(spy.getCall(0).args[0], Buffer.concat([Buffer.from([0xaa, 0x01, 0x0d, 0x00]), Buffer.from('I love robots')])) | ||
assert.deepEqual(spy.getCall(1).args[0], Buffer.concat([Buffer.from([0xaa, 0x02, 0x13, 0x00]), Buffer.from('Each and Every One\n')])) | ||
assert.deepEqual(spy.getCall(2).args[0], Buffer.concat([Buffer.from([0xaa, 0x03, 0x24, 0x00]), Buffer.from('even you!')])) | ||
assert(spy.calledThrice) | ||
}) | ||
|
||
it('Emits data when early when invalid length encountered', () => { | ||
const spy = sinon.spy() | ||
const parser = new PacketLengthParser({ lengthOffset: 2, packetOverhead: 4, lengthBytes: 2, maxLen: 0x0d }) | ||
parser.on('data', spy) | ||
parser.write(Buffer.from('\xaa\x01\x0d\x00I love robots\xaa\x02\x13\x00Each ')) | ||
parser.write(Buffer.from('and Every One\n')) | ||
parser.write(Buffer.from([0xaa, 0x03, 0x09, 0x00])) | ||
parser.write(Buffer.from('even you!')) | ||
|
||
assert.deepEqual(spy.getCall(0).args[0], Buffer.concat([Buffer.from([0xaa, 0x01, 0x0d, 0x00]), Buffer.from('I love robots')])) | ||
assert.deepEqual(spy.getCall(1).args[0], Buffer.from([0xaa, 0x02, 0x13, 0x00])) | ||
assert.deepEqual(spy.getCall(2).args[0], Buffer.concat([Buffer.from([0xaa, 0x03, 0x09, 0x00]), Buffer.from('even you!')])) | ||
assert(spy.calledThrice) | ||
}) | ||
|
||
it('works if a multibyte length crosses a chunk boundary', () => { | ||
const spy = sinon.spy() | ||
const parser = new PacketLengthParser({ lengthOffset: 2, packetOverhead: 4, lengthBytes: 2 }) | ||
parser.on('data', spy) | ||
parser.write(Buffer.from('\xaa\x01\x0d')) | ||
parser.write(Buffer.from('\x00I love robots\xaa\x02\x13\x00Each ')) | ||
parser.write(Buffer.from('and Every One\n')) | ||
parser.write(Buffer.from([0xaa, 0x03, 0x24, 0x00])) | ||
parser.write(Buffer.from('even you!')) | ||
|
||
assert.deepEqual(spy.getCall(0).args[0], Buffer.concat([Buffer.from([0xaa, 0x01, 0x0d, 0x00]), Buffer.from('I love robots')])) | ||
assert.deepEqual(spy.getCall(1).args[0], Buffer.concat([Buffer.from([0xaa, 0x02, 0x13, 0x00]), Buffer.from('Each and Every One\n')])) | ||
assert(spy.calledTwice) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
{ | ||
"name": "@serialport/parser-packet-length", | ||
"main": "lib", | ||
"version": "9.0.1", | ||
"engines": { | ||
"node": ">=8.6.0" | ||
}, | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"license": "MIT", | ||
"repository": { | ||
"type": "git", | ||
"url": "git://github.com/serialport/node-serialport.git" | ||
} | ||
} |