Skip to content

Commit

Permalink
Add packet length parser (serialport#2197)
Browse files Browse the repository at this point in the history
* Add packet length parser
  • Loading branch information
doggkruse authored and bailli committed Aug 10, 2021
1 parent 7a5605b commit d032c0f
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 0 deletions.
3 changes: 3 additions & 0 deletions packages/parser-packet-length/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.DS_Store
*.test.js
CHANGELOG.md
5 changes: 5 additions & 0 deletions packages/parser-packet-length/CHANGELOG.md
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.

28 changes: 28 additions & 0 deletions packages/parser-packet-length/README.md
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,
}))
79 changes: 79 additions & 0 deletions packages/parser-packet-length/lib/index.js
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
94 changes: 94 additions & 0 deletions packages/parser-packet-length/lib/index.test.js
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)
})
})
16 changes: 16 additions & 0 deletions packages/parser-packet-length/package.json
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"
}
}

0 comments on commit d032c0f

Please sign in to comment.