From d032c0f8c4b39b773e7833cf80cedc2f62355afe Mon Sep 17 00:00:00 2001 From: doggkruse <6468053+doggkruse@users.noreply.github.com> Date: Mon, 22 Feb 2021 18:04:07 -0800 Subject: [PATCH] Add packet length parser (#2197) * Add packet length parser --- packages/parser-packet-length/.npmignore | 3 + packages/parser-packet-length/CHANGELOG.md | 5 + packages/parser-packet-length/README.md | 28 ++++++ packages/parser-packet-length/lib/index.js | 79 ++++++++++++++++ .../parser-packet-length/lib/index.test.js | 94 +++++++++++++++++++ packages/parser-packet-length/package.json | 16 ++++ 6 files changed, 225 insertions(+) create mode 100644 packages/parser-packet-length/.npmignore create mode 100644 packages/parser-packet-length/CHANGELOG.md create mode 100644 packages/parser-packet-length/README.md create mode 100644 packages/parser-packet-length/lib/index.js create mode 100644 packages/parser-packet-length/lib/index.test.js create mode 100644 packages/parser-packet-length/package.json diff --git a/packages/parser-packet-length/.npmignore b/packages/parser-packet-length/.npmignore new file mode 100644 index 0000000000..a1623b1ea8 --- /dev/null +++ b/packages/parser-packet-length/.npmignore @@ -0,0 +1,3 @@ +.DS_Store +*.test.js +CHANGELOG.md diff --git a/packages/parser-packet-length/CHANGELOG.md b/packages/parser-packet-length/CHANGELOG.md new file mode 100644 index 0000000000..767840ada8 --- /dev/null +++ b/packages/parser-packet-length/CHANGELOG.md @@ -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. + diff --git a/packages/parser-packet-length/README.md b/packages/parser-packet-length/README.md new file mode 100644 index 0000000000..a826538fe1 --- /dev/null +++ b/packages/parser-packet-length/README.md @@ -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, +})) diff --git a/packages/parser-packet-length/lib/index.js b/packages/parser-packet-length/lib/index.js new file mode 100644 index 0000000000..bc3d0cd56a --- /dev/null +++ b/packages/parser-packet-length/lib/index.js @@ -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 diff --git a/packages/parser-packet-length/lib/index.test.js b/packages/parser-packet-length/lib/index.test.js new file mode 100644 index 0000000000..2897dcb790 --- /dev/null +++ b/packages/parser-packet-length/lib/index.test.js @@ -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) + }) +}) diff --git a/packages/parser-packet-length/package.json b/packages/parser-packet-length/package.json new file mode 100644 index 0000000000..88371eb35f --- /dev/null +++ b/packages/parser-packet-length/package.json @@ -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" + } +}