diff --git a/packages/parser-slip-encoder/lib/decoder.js b/packages/parser-slip-encoder/lib/decoder.js new file mode 100644 index 000000000..c387214ff --- /dev/null +++ b/packages/parser-slip-encoder/lib/decoder.js @@ -0,0 +1,94 @@ +const { Transform } = require('stream') + +/** +* A transform stream that decodes slip encoded data. +* @extends Transform +* @summary Runs in O(n) time, stripping out slip encoding and emitting decoded data. Optionally, +* custom slip escape and delimiters can be provided. +* @example +// Receive slip encoded data from a serialport and log decoded data +const SerialPort = require('serialport') +const { SlipDecoder } = require('@serialport/parser-slip-encoder') +const port = new SerialPort('/dev/tty-usbserial1') +const parser = port.pipe(new SlipDecoder()) +parser.on('data', console.log) +*/ +class SlipDecoder extends Transform { + constructor(options = {}) { + super(options) + + const opts = { + START: undefined, + ESC: 0xdb, + END: 0xc0, + + ESC_START: undefined, + ESC_END: 0xdc, + ESC_ESC: 0xdd, + + ...options, + } + this.opts = opts + + this.buffer = Buffer.alloc(0) + this.escape = false + this.start = false + } + + _transform(chunk, encoding, cb) { + for (let ndx = 0; ndx < chunk.length; ndx++) { + let byte = chunk[ndx] + + if (byte === this.opts.START) { + this.start = true + continue + } else if (undefined == this.opts.START) { + this.start = true + } + + if (this.escape) { + if (byte === this.opts.ESC_START) { + byte = this.opts.START + } else if (byte === this.opts.ESC_ESC) { + byte = this.opts.ESC + } else if (byte === this.opts.ESC_END) { + byte = this.opts.END + } else { + this.escape = false + this.push(this.buffer) + this.buffer = Buffer.alloc(0) + } + } else { + if (byte === this.opts.ESC) { + this.escape = true + continue + } + + if (byte === this.opts.END) { + this.push(this.buffer) + this.buffer = Buffer.alloc(0) + + this.escape = false + this.start = false + continue + } + } + + this.escape = false + + if (true === this.start) { + this.buffer = Buffer.concat([this.buffer, Buffer.from([byte])]) + } + } + + cb() + } + + _flush(cb) { + this.push(this.buffer) + this.buffer = Buffer.alloc(0) + cb() + } +} + +module.exports = SlipDecoder diff --git a/packages/parser-slip-encoder/lib/encoder.js b/packages/parser-slip-encoder/lib/encoder.js new file mode 100644 index 000000000..0a8953a54 --- /dev/null +++ b/packages/parser-slip-encoder/lib/encoder.js @@ -0,0 +1,89 @@ +const { Transform } = require('stream') + +/** +* A transform stream that emits SLIP-encoded data for each incoming packet. +* @extends Transform +* @summary Runs in O(n) time, adding a 0xC0 character at the end of each +* received packet and escaping characters, according to RFC 1055. Adds another +* 0xC0 character at the beginning if the `bluetoothQuirk` option is truthy (as +* per the Bluetooth Core Specification 4.0, Volume 4, Part D, Chapter 3 "SLIP Layer"). +* Optionally, custom slip escape and delimiters can be provided. +* @example +// Read lines from a text file, then SLIP-encode each and send them to a serial port +const SerialPort = require('serialport') +const { SlipEncoder } = require('@serialport/parser-slip-encoder') +const Readline = require('parser-readline') +const fileReader = require('fs').createReadStream('/tmp/some-file.txt'); +const port = new SerialPort('/dev/tty-usbserial1') +const lineParser = fileReader.pipe(new Readline({ delimiter: '\r\n' })); +const encoder = fileReader.pipe(new SlipEncoder({ bluetoothQuirk: false })); +encoder.pipe(port); +*/ +class SlipEncoder extends Transform { + constructor(options = {}) { + super(options) + + const opts = { + START: undefined, + ESC: 0xdb, + END: 0xc0, + + ESC_START: undefined, + ESC_END: 0xdc, + ESC_ESC: 0xdd, + + ...options, + } + this.opts = opts + + if (options.bluetoothQuirk) { + this._bluetoothQuirk = true + } + } + + _transform(chunk, encoding, cb) { + const chunkLength = chunk.length + + if (this._bluetoothQuirk && chunkLength === 0) { + // Edge case: push no data. Bluetooth-quirky SLIP parsers don't like + // lots of 0xC0s together. + return cb() + } + + // Allocate memory for the worst-case scenario: all bytes are escaped, + // plus start and end separators. + const encoded = Buffer.alloc(chunkLength * 2 + 2) + let j = 0 + + if (this._bluetoothQuirk == true) { + encoded[j++] = this.opts.END + } + + if (this.opts.START !== undefined) { + encoded[j++] = this.opts.START + } + + for (let i = 0; i < chunkLength; i++) { + let byte = chunk[i] + + if (byte === this.opts.START) { + encoded[j++] = this.opts.ESC + byte = this.opts.ESC_START + } else if (byte === this.opts.END) { + encoded[j++] = this.opts.ESC + byte = this.opts.ESC_END + } else if (byte === this.opts.ESC) { + encoded[j++] = this.opts.ESC + byte = this.opts.ESC_ESC + } + + encoded[j++] = byte + } + + encoded[j++] = this.opts.END + + cb(null, encoded.slice(0, j)) + } +} + +module.exports = SlipEncoder diff --git a/packages/parser-slip-encoder/lib/index.js b/packages/parser-slip-encoder/lib/index.js index 0127ec51a..c0a69f199 100644 --- a/packages/parser-slip-encoder/lib/index.js +++ b/packages/parser-slip-encoder/lib/index.js @@ -1,73 +1,4 @@ -const { Transform } = require('stream') - -const END = 0xc0 -const ESC = 0xdb -const ESC_END = 0xdc -const ESC_ESC = 0xdd - -/** -* A transform stream that emits SLIP-encoded data for each incoming packet. -* @extends Transform -* @summary Runs in O(n) time, adding a 0xC0 character at the end of each -* received packet and escaping characters, according to RFC 1055. Adds another -* 0xC0 character at the beginning if the `bluetoothQuirk` option is truthy (as -* per the Bluetooth Core Specification 4.0, Volume 4, Part D, Chapter 3 "SLIP Layer"). -* Runs in O(n) time. -* @example -// Read lines from a text file, then SLIP-encode each and send them to a serial port -const SerialPort = require('serialport') -const SlipEncoder = require('@serialport/parser-slip-encoder') -const Readline = require('parser-readline') -const fileReader = require('fs').createReadStream('/tmp/some-file.txt'); -const port = new SerialPort('/dev/tty-usbserial1') -const lineParser = fileReader.pipe(new Readline({ delimiter: '\r\n' })); -const encoder = fileReader.pipe(new SlipEncoder({ bluetoothQuirk: false })); -encoder.pipe(port); -*/ -class SlipEncoderParser extends Transform { - constructor(options = {}) { - super(options) - - if (options.bluetoothQuirk) { - this._bluetoothQuirk = true - } - } - - _transform(chunk, encoding, cb) { - const chunkLength = chunk.length - - if (this._bluetoothQuirk && chunkLength === 0) { - // Edge case: push no data. Bluetooth-quirky SLIP parsers don't like - // lots of 0xC0s together. - return cb() - } - - // Allocate memory for the worst-case scenario: all bytes are escaped, - // plus start and end separators. - const encoded = Buffer.alloc(chunkLength * 2 + 2) - let j = 0 - - if (this._bluetoothQuirk) { - encoded[j++] = END - } - - for (let i = 0; i < chunkLength; i++) { - let byte = chunk[i] - if (byte === END) { - encoded[j++] = ESC - byte = ESC_END - } else if (byte === ESC) { - encoded[j++] = ESC - byte = ESC_ESC - } - - encoded[j++] = byte - } - - encoded[j++] = END - - cb(null, encoded.slice(0, j)) - } +module.exports = { + SlipEncoder: require('./encoder'), + SlipDecoder: require('./decoder'), } - -module.exports = SlipEncoderParser diff --git a/packages/parser-slip-encoder/lib/index.test.js b/packages/parser-slip-encoder/lib/index.test.js index 75a6e8f2e..f22378d35 100644 --- a/packages/parser-slip-encoder/lib/index.test.js +++ b/packages/parser-slip-encoder/lib/index.test.js @@ -2,9 +2,9 @@ const sinon = require('sinon') -const SlipEncoder = require('../') +const { SlipEncoder, SlipDecoder } = require('../') -describe('SlipEncoderParser', () => { +describe('SlipEncoder', () => { it('Adds one delimiter to one-byte messages', () => { const spy = sinon.spy() const encoder = new SlipEncoder() @@ -86,6 +86,38 @@ describe('SlipEncoderParser', () => { assert.deepEqual(spy.getCall(5).args[0], Buffer.from([0xff, 0xc0])) }) + it('Escapes characters with custom escapes', () => { + const spy = sinon.spy() + const slip = { + START: 0xab, + ESC: 0xcd, + END: 0xbc, + + ESC_START: 0xac, + ESC_ESC: 0xce, + ESC_END: 0xbd, + } + const encoder = new SlipEncoder(slip) + encoder.on('data', spy) + + encoder.write(Buffer.from([0x01])) + encoder.write(Buffer.from([0xbc])) + encoder.write(Buffer.from([0xcd])) + encoder.write(Buffer.from([0xdc])) + encoder.write(Buffer.from([0xdd])) + encoder.write(Buffer.from([0xff])) + encoder.write(Buffer.from([0xab])) + + assert.equal(spy.callCount, 7) + assert.deepEqual(spy.getCall(0).args[0], Buffer.from([0xab, 0x01, 0xbc])) + assert.deepEqual(spy.getCall(1).args[0], Buffer.from([0xab, 0xcd, 0xbd, 0xbc])) + assert.deepEqual(spy.getCall(2).args[0], Buffer.from([0xab, 0xcd, 0xce, 0xbc])) + assert.deepEqual(spy.getCall(3).args[0], Buffer.from([0xab, 0xdc, 0xbc])) + assert.deepEqual(spy.getCall(4).args[0], Buffer.from([0xab, 0xdd, 0xbc])) + assert.deepEqual(spy.getCall(5).args[0], Buffer.from([0xab, 0xff, 0xbc])) + assert.deepEqual(spy.getCall(6).args[0], Buffer.from([0xab, 0xcd, 0xac, 0xbc])) + }) + it('Escapes characters with the bluetooth quirk', () => { const spy = sinon.spy() const encoder = new SlipEncoder({ bluetoothQuirk: true }) @@ -107,3 +139,185 @@ describe('SlipEncoderParser', () => { assert.deepEqual(spy.getCall(5).args[0], Buffer.from([0xc0, 0xff, 0xc0])) }) }) + +describe('SlipDecoder', () => { + it('Decodes one-byte messages', () => { + const spy = sinon.spy() + const decoder = new SlipDecoder() + decoder.on('data', spy) + + decoder.write(Buffer.from([0x01, 0xc0])) + decoder.write(Buffer.from([0x80, 0xc0])) + decoder.write(Buffer.from([0xff, 0xc0])) + decoder.write(Buffer.from([0xa5, 0xc0])) + + assert.equal(spy.callCount, 4) + assert.deepEqual(spy.getCall(0).args[0], Buffer.from([0x01])) + assert.deepEqual(spy.getCall(1).args[0], Buffer.from([0x80])) + assert.deepEqual(spy.getCall(2).args[0], Buffer.from([0xff])) + assert.deepEqual(spy.getCall(3).args[0], Buffer.from([0xa5])) + }) + + it('No data event on zero-byte messages', () => { + const spy = sinon.spy() + const decoder = new SlipDecoder() + decoder.on('data', spy) + + decoder.write(Buffer.from([0xc0])) + + assert.equal(spy.callCount, 0) + }) + + it('Decodes Escaped characters', () => { + const spy = sinon.spy() + const decoder = new SlipDecoder() + decoder.on('data', spy) + + decoder.write(Buffer.from([0x01, 0xc0])) + decoder.write(Buffer.from([0xdb, 0xdc, 0xc0])) + decoder.write(Buffer.from([0xdb, 0xdd, 0xc0])) + decoder.write(Buffer.from([0xdc, 0xc0])) + decoder.write(Buffer.from([0xdd, 0xc0])) + decoder.write(Buffer.from([0xff, 0xc0])) + + assert.equal(spy.callCount, 6) + assert.deepEqual(spy.getCall(0).args[0], Buffer.from([0x01])) + assert.deepEqual(spy.getCall(1).args[0], Buffer.from([0xc0])) + assert.deepEqual(spy.getCall(2).args[0], Buffer.from([0xdb])) + assert.deepEqual(spy.getCall(3).args[0], Buffer.from([0xdc])) + assert.deepEqual(spy.getCall(4).args[0], Buffer.from([0xdd])) + assert.deepEqual(spy.getCall(5).args[0], Buffer.from([0xff])) + }) + + it('Decodes Escaped characters with custom escapes', () => { + const spy = sinon.spy() + const slip = { + START: 0xab, + ESC: 0xcd, + END: 0xbc, + + ESC_START: 0xac, + ESC_ESC: 0xce, + ESC_END: 0xbd, + } + const decoder = new SlipDecoder(slip) + decoder.on('data', spy) + + decoder.write(Buffer.from([0xab, 0x01, 0xbc])) + decoder.write(Buffer.from([0xab, 0xcd, 0xbd, 0xbc])) + decoder.write(Buffer.from([0xab, 0xcd, 0xce, 0xbc])) + decoder.write(Buffer.from([0xab, 0xdc, 0xbc])) + decoder.write(Buffer.from([0xab, 0xdd, 0xbc])) + decoder.write(Buffer.from([0xab, 0xff, 0xbc])) + decoder.write(Buffer.from([0xab, 0xcd, 0xac, 0xbc])) + + assert.equal(spy.callCount, 7) + assert.deepEqual(spy.getCall(0).args[0], Buffer.from([0x01])) + assert.deepEqual(spy.getCall(1).args[0], Buffer.from([0xbc])) + assert.deepEqual(spy.getCall(2).args[0], Buffer.from([0xcd])) + assert.deepEqual(spy.getCall(3).args[0], Buffer.from([0xdc])) + assert.deepEqual(spy.getCall(4).args[0], Buffer.from([0xdd])) + assert.deepEqual(spy.getCall(5).args[0], Buffer.from([0xff])) + assert.deepEqual(spy.getCall(6).args[0], Buffer.from([0xab])) + }) + + it('Decodes invalid escape', () => { + const spy = sinon.spy() + const slip = { + START: 0xab, + ESC: 0xcd, + END: 0xbc, + + ESC_START: 0xac, + ESC_ESC: 0xce, + ESC_END: 0xbd, + } + const decoder = new SlipDecoder(slip) + decoder.on('data', spy) + + decoder.write(Buffer.from([0xab, 0x01, 0xcd, 0x02, 0xbc])) + + assert.equal(spy.callCount, 2) + assert.deepEqual(spy.getCall(0).args[0], Buffer.from([0x01])) + assert.deepEqual(spy.getCall(1).args[0], Buffer.from([0x02])) + }) + + it('Decodes data split across multiple chunks', () => { + const spy = sinon.spy() + const slip = { + START: 0xab, + ESC: 0xcd, + END: 0xbc, + + ESC_START: 0xac, + ESC_ESC: 0xce, + ESC_END: 0xbd, + } + const decoder = new SlipDecoder(slip) + decoder.on('data', spy) + + decoder.write(Buffer.from([0xab, 0x01])) + decoder.write(Buffer.from([0xcd, 0xbd, 0xbc])) + + assert.equal(spy.callCount, 1) + assert.deepEqual(spy.getCall(0).args[0], Buffer.from([0x01, 0xbc])) + }) + + it('Decodes data split across multiple chunks on escape', () => { + const spy = sinon.spy() + const slip = { + START: 0xab, + ESC: 0xcd, + END: 0xbc, + + ESC_START: 0xac, + ESC_ESC: 0xce, + ESC_END: 0xbd, + } + const decoder = new SlipDecoder(slip) + decoder.on('data', spy) + + decoder.write(Buffer.from([0xab, 0x01, 0xcd])) + decoder.write(Buffer.from([0xbd, 0xbc])) + + assert.equal(spy.callCount, 1) + assert.deepEqual(spy.getCall(0).args[0], Buffer.from([0x01, 0xbc])) + }) + + it('Data before start is dropped', () => { + const spy = sinon.spy() + const slip = { + START: 0xab, + END: 0xbc, + } + const decoder = new SlipDecoder(slip) + decoder.on('data', spy) + + decoder.write(Buffer.from([0x01, 0x02, 0xdb])) + decoder.write(Buffer.from([0xab, 0x03, 0x04, 0xbc])) + + assert.equal(spy.callCount, 1) + assert.deepEqual(spy.getCall(0).args[0], Buffer.from([0x03, 0x04])) + }) + + it('Flushes pending data on stream end', () => { + const spy = sinon.spy() + const slip = { + START: 0xab, + ESC: 0xcd, + END: 0xbc, + + ESC_START: 0xac, + ESC_ESC: 0xce, + ESC_END: 0xbd, + } + const decoder = new SlipDecoder(slip) + decoder.on('data', spy) + + decoder.write(Buffer.from([0xab, 0x01, 0xcd])) + decoder.end() + + assert.equal(spy.callCount, 1) + assert.deepEqual(spy.getCall(0).args[0], Buffer.from([0x01])) + }) +})