├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── examples ├── opc-client.js └── opc-server.js ├── index.js ├── lib ├── opc-client-stream.js └── opc-parse-stream.js ├── package.json ├── test ├── opc-client-stream.js └── opc-parse-stream.js └── wallaby.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | - 0.12 5 | - iojs 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Martin Schuhfuss 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # openpixelcontrol-stream ![build-status](https://api.travis-ci.org/raspberry-node/node-openpixelcontrol.svg) 2 | 3 | stream-based implementation of the [openpixelcontrol][]-protocol. 4 | Provides a protocol-parser and a client-implementation. 5 | 6 | ## installation 7 | 8 | npm install openpixelcontrol-stream 9 | 10 | ## usage example 11 | 12 | ### openpixelcontrol server 13 | 14 | This will run an openpixelcontrol server on the default port (7890) and send 15 | received data to the `rpi-ws281x-native` module for output to a strip of 16 | ws2812-leds. 17 | 18 | ```javascript 19 | var ParseStream = require('openpixelcontrol-stream').OpcParseStream, 20 | net = require('net'), 21 | ws281x = require('rpi-ws281x-native'); 22 | 23 | 24 | var server = net.createServer(function(conn) { 25 | var parser = new ParseStream({ 26 | channel: 1, 27 | dataFormat: ParseStream.DataFormat.UINT32_ARRAY 28 | }); 29 | 30 | parser.on('setpixelcolors', function(data) { 31 | ws281x.render(data); 32 | }); 33 | 34 | conn.pipe(parser); 35 | }); 36 | 37 | ws281x.init(100); 38 | server.listen(7890); 39 | ``` 40 | 41 | 42 | ### openpixelcontrol client 43 | 44 | A basic client connecting to an openpixelcontrol-server and running an 45 | animation there. 46 | 47 | ```javascript 48 | var ClientStream = require('openpixelcontrol-stream').OpcClientStream, 49 | net = require('net'); 50 | 51 | var NUM_LEDS = 100, 52 | OPC_CHANNEL = 0; 53 | 54 | var client = new ClientStream(); 55 | 56 | // connect to openpixelcontrol-server at `192.168.1.42:7890` 57 | var socket = net.createConnection(7890, '192.168.1.42', function() { 58 | client.pipe(socket); 59 | 60 | run(); 61 | }); 62 | 63 | function run() { 64 | // create a typed-array for color-data 65 | var data = new Uint32Array(NUM_LEDS); 66 | 67 | // setup an animation-loop at 10FPS 68 | setInterval(function () { 69 | // ... update colors in `data` ... 70 | 71 | client.setPixelColors(OPC_CHANNEL, data); 72 | }, 100); 73 | } 74 | 75 | ``` 76 | 77 | [openpixelcontrol]: http://openpixelcontrol.org/ 78 | -------------------------------------------------------------------------------- /examples/opc-client.js: -------------------------------------------------------------------------------- 1 | var ClientStream = require('../lib/opc-client-stream'), 2 | net = require('net'); 3 | 4 | var opts = require('nomnom') 5 | .options({ 6 | host: { 7 | position: 0, 8 | required: true, 9 | help: 'host' 10 | }, 11 | port: { 12 | abbr: 'p', 13 | default: 7890, 14 | help: 'port number' 15 | }, 16 | channel: { 17 | abbr: 'c', 18 | default: 0, 19 | help: 'OPC channel' 20 | } 21 | }).parse(); 22 | 23 | 24 | var socket = net.createConnection(opts.port, opts.host, function() { 25 | var client = new ClientStream(); 26 | 27 | client.pipe(socket); 28 | 29 | var offset = 0, count = 27; 30 | var data = new Uint32Array(count); 31 | setInterval(function () { 32 | for (var i = 0; i < count; i++) { 33 | data[i] = colorwheel((offset + i) % 256); 34 | } 35 | 36 | client.setPixelColors(opts.channel, data); 37 | offset = (offset + 1) % 256; 38 | }, 100); 39 | }); 40 | 41 | 42 | // rainbow-colors, taken from http://goo.gl/Cs3H0v 43 | function colorwheel(pos) { 44 | pos = 255 - pos; 45 | if (pos < 85) { return rgb2Int(255 - pos * 3, 0, pos * 3); } 46 | else if (pos < 170) { pos -= 85; return rgb2Int(0, pos * 3, 255 - pos * 3); } 47 | else { pos -= 170; return rgb2Int(pos * 3, 255 - pos * 3, 0); } 48 | } 49 | 50 | function rgb2Int(r, g, b) { 51 | return ((r & 0xff) << 16) + ((g & 0xff) << 8) + (b & 0xff); 52 | } -------------------------------------------------------------------------------- /examples/opc-server.js: -------------------------------------------------------------------------------- 1 | var ParseStream = require('../lib/opc-parse-stream'), 2 | net = require('net'), 3 | ws281x = require('rpi-ws281x-native'); 4 | 5 | 6 | var server = net.createServer(function(socket) { 7 | var parser = new ParseStream({ 8 | channel: 1, 9 | dataFormat: ParseStream.DataFormat.UINT32_ARRAY 10 | }); 11 | 12 | parser.on('setpixelcolors', function(data) { 13 | ws281x.render(data); 14 | }); 15 | 16 | socket.pipe(parser); 17 | }); 18 | 19 | ws281x.init(100); 20 | server.listen(7890); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | OpcParseStream: require('./lib/opc-parse-stream'), 3 | OpcClientStream: require('./lib/opc-client-stream') 4 | }; -------------------------------------------------------------------------------- /lib/opc-client-stream.js: -------------------------------------------------------------------------------- 1 | var ReadableStream = require('stream').Readable, 2 | util = require('util'); 3 | 4 | 5 | /** 6 | * streaming client-interface for the openpixelcontrol-protocol. 7 | * For protocol-details see http://openpixelcontrol.org/ 8 | * 9 | * 10 | * 11 | * @constructor 12 | */ 13 | function OpcClientStream(options) { 14 | ReadableStream.call(this); 15 | 16 | this.stream = this; 17 | } 18 | util.inherits(OpcClientStream, ReadableStream); 19 | 20 | /** 21 | * @enum {number} 22 | */ 23 | OpcClientStream.Command = { 24 | SETPIXELCOLORS: 0x00, 25 | SYSEX: 0xff 26 | }; 27 | 28 | 29 | /** 30 | * @param {number} channel 31 | * @param {OpcClientStream.Command} command 32 | * @param {Buffer} data 33 | * 34 | * @return {Buffer} 35 | */ 36 | OpcClientStream.createMessage = function(channel, command, data) { 37 | var msg = new Buffer(4 + data.length); 38 | 39 | msg.writeUInt8(channel, 0, true); 40 | msg.writeUInt8(command, 1, true); 41 | msg.writeUInt16BE(data.length, 2, true); 42 | 43 | data.copy(msg, 4); 44 | 45 | return msg; 46 | }; 47 | 48 | 49 | /** 50 | * send a setPixelColors-message. 51 | * 52 | * @param {number} channel 53 | * @param {Buffer | Uint32Array} data pixel-data. Either a Buffer with a 54 | * single byte per color-channel ([r0, g0, b0, r1, g1, b1, ...]) or 55 | * an Uint32Array with an uint32 per pixel (format 0x00rrggbb) 56 | */ 57 | OpcClientStream.prototype.setPixelColors = function(channel, data) { 58 | if(data instanceof Uint32Array) { 59 | var tmp = new Buffer(3*data.length); 60 | for(var i=0; i>16) & 0xff, 3*i); 64 | tmp.writeUInt8((rgb>>8) & 0xff, 3*i+1); 65 | tmp.writeUInt8(rgb & 0xff, 3*i+2); 66 | } 67 | 68 | data = tmp; 69 | } 70 | 71 | var msg = OpcClientStream.createMessage( 72 | channel, OpcClientStream.Command.SETPIXELCOLORS, data); 73 | 74 | this.push(msg); 75 | }; 76 | 77 | 78 | /** 79 | * send a sysex-message. 80 | * 81 | * @param {number} channel channel-number 82 | * @param {number} systemId 16-bit integer indicating the systemId to be sent 83 | * @param {Buffer} data the data to be sent. 84 | */ 85 | OpcClientStream.prototype.sysex = function(channel, systemId, data) { 86 | var buffer = new Buffer(2 + data.length), msg; 87 | 88 | buffer.writeUInt16BE(systemId, 0, true); 89 | data.copy(buffer, 2); 90 | 91 | msg = OpcClientStream.createMessage( 92 | channel, OpcClientStream.Command.SYSEX, buffer); 93 | 94 | this.push(msg); 95 | }; 96 | 97 | /** 98 | * required to implement for readable-streams. 99 | * Doesn't do anything as data is only pushed from other API-methods 100 | * @private 101 | */ 102 | OpcClientStream.prototype._read = function() {}; 103 | 104 | module.exports = OpcClientStream; -------------------------------------------------------------------------------- /lib/opc-parse-stream.js: -------------------------------------------------------------------------------- 1 | var util = require('util'), 2 | WritableStream = require('stream').Writable; 3 | 4 | /** 5 | * A writable stream to act as a sink for the binary OpenPixelControl-protocol. 6 | * For protocol-details see http://openpixelcontrol.org/ 7 | * 8 | * Channel and System-Id (for sysex-messages) can be configured using 9 | * `options.channel` and `options.systemId`. 10 | * 11 | * There are two options for the internal data-format that can be configured 12 | * with `options.dataFormat`: 13 | * 14 | * - `OpcParseStream.DataFormat.BUFFER` (default): uses buffer-instances with 15 | * 3 byte per pixel (RGB-order). These are a bit smaller and more efficient 16 | * than using Uint32Arrays, as the data can be directly forwarded from the 17 | * network-packets. 18 | * 19 | * - `OpcParseStream.DataFormat.UINT32_ARRAY`: uses an unsined 32-bit integer 20 | * per pixel in an Uint32Array. Data format is `0x00rrggbb`. 21 | * 22 | * 23 | * There are two ways how this can be used this in your project: 24 | * 25 | * - using events: The OpcParseStream will emit 'setpixelcolors' and 26 | * 'sysex'-events whenever data has been received, the data-format for 27 | * pixel-data depends on the `options.dataFormat` setting. 28 | * 29 | * - extend: you can also inherit from this prototype and override the 30 | * `setPixelColors()` and `sysex()` method to handle the data. The 31 | * default-implementation provided here just emits the events mentioned 32 | * above. 33 | * 34 | * @param {OpcParseStream.Options} options 35 | * @extends {WritableStream} 36 | * @constructor 37 | */ 38 | function OpcParseStream(options) { 39 | WritableStream.call(this); 40 | 41 | options = options || {}; 42 | 43 | this._pushback = null; 44 | 45 | /** 46 | * @type {OpcParseStream.DataFormat} 47 | */ 48 | this.dataFormat = options.dataFormat || OpcParseStream.DataFormat.BUFFER; 49 | 50 | /** 51 | * @type {number} 52 | */ 53 | this.channel = ~~options.channel || 0; 54 | 55 | /** 56 | * @type {number} 57 | */ 58 | this.systemId = ~~options.systemId || 0xffff; 59 | } 60 | util.inherits(OpcParseStream, WritableStream); 61 | 62 | 63 | /** 64 | * @typedef {{ 65 | * channel: number, 66 | * systemId: number, 67 | * dataFormat: OpcParseStream.DataFormat 68 | * }} 69 | */ 70 | OpcParseStream.Options; 71 | 72 | 73 | /** 74 | * @enum {string} 75 | */ 76 | OpcParseStream.DataFormat = { 77 | BUFFER: 'buffer', 78 | UINT32_ARRAY: 'uint32array' 79 | }; 80 | 81 | /** 82 | * @enum {number} 83 | */ 84 | OpcParseStream.Commands = { 85 | SETPIXELCOLORS: 0x00, 86 | SYSEX: 0xff 87 | }; 88 | 89 | 90 | /** 91 | * handle a setpixelcolors-command. 92 | * 93 | * @param {Uint32Array|Buffer} data pixel-data, type depends on 94 | * options.dataFormat 95 | */ 96 | OpcParseStream.prototype.setPixelColors = function(data) { 97 | this.emit('setpixelcolors', data); 98 | }; 99 | 100 | 101 | /** 102 | * handle a parsed sysex-message. 103 | * 104 | * @param {number} commandId 105 | * @param {Buffer} data 106 | */ 107 | OpcParseStream.prototype.sysex = function(commandId, data) { 108 | this.emit('sysex', commandId, data); 109 | }; 110 | 111 | 112 | 113 | // ---- PRIVATE 114 | /** 115 | * @param {Buffer} buffer 116 | * @param {String} encoding 117 | * @param {function(Error?)} callback 118 | * @private 119 | */ 120 | OpcParseStream.prototype._write = function(buffer, encoding, callback) { 121 | var channel, command, length, data; 122 | 123 | if(encoding !== 'buffer') { 124 | // FIXME: add tests for error-handling 125 | return callback(new Error('expected data as Buffer')); 126 | } 127 | 128 | if(this._pushback) { 129 | buffer = Buffer.concat([this._pushback, buffer]); 130 | this._pushback = null; 131 | } 132 | 133 | while(buffer.length > 0) { 134 | if(buffer.length < 4) { 135 | this._pushback = buffer; 136 | 137 | return callback(); 138 | } 139 | 140 | channel = buffer.readUInt8(0, true); 141 | command = buffer.readUInt8(1, true); 142 | length = buffer.readUInt16BE(2, true); 143 | 144 | // FIXME: handle edge-cases 145 | if(buffer.length < 4+length) { 146 | this._pushback = buffer; 147 | 148 | return callback(); 149 | } 150 | 151 | if(buffer.length > 4+length) { 152 | this._pushback = buffer.slice(4+length); 153 | } 154 | 155 | data = buffer.slice(4, 4+length); 156 | 157 | this._handleOpcMessage(channel, command, data); 158 | buffer = buffer.slice(4+length); 159 | } 160 | 161 | callback(); 162 | }; 163 | 164 | 165 | /** 166 | * @private 167 | */ 168 | OpcParseStream.prototype._handleOpcMessage = function(channel, command, data) { 169 | // skip non-broadcast messages for other channels 170 | if(channel > 0 && channel !== this.channel) { 171 | return; 172 | } 173 | 174 | if(command === OpcParseStream.Commands.SETPIXELCOLORS) { 175 | this._handleSetPixelColorsMessage(data); 176 | } 177 | 178 | if(command === OpcParseStream.Commands.SYSEX) { 179 | // sysex-messages with less than 4bytes are considered invalid. 180 | // However, we don't throw an error here and simply ignore the packet. 181 | if(data.length < 4) { 182 | return; 183 | } 184 | 185 | this._handleSysExMessage(data); 186 | } 187 | }; 188 | 189 | 190 | /** 191 | * @param {Buffer} data 192 | * @fires OpcParseStream#setpixelcolors 193 | * @private 194 | */ 195 | OpcParseStream.prototype._handleSetPixelColorsMessage = function(data) { 196 | if(this.dataFormat === OpcParseStream.DataFormat.BUFFER) { 197 | this.setPixelColors(data); 198 | } else { 199 | var pixelData = new Uint32Array(data.length/3); 200 | 201 | for(var i=0; i", 20 | "license": "MIT", 21 | "repository": { 22 | "type": "git", 23 | "url": "git@github.com:raspberry-node/node-openpixelcontrol.git" 24 | }, 25 | "keywords": [ 26 | "opc", 27 | "openpixelcontrol", 28 | "lighting" 29 | ], 30 | "bugs": { 31 | "url": "https://github.com/raspberry-node/node-openpixelcontrol/issues" 32 | }, 33 | "homepage": "https://github.com/raspberry-node/node-openpixelcontrol" 34 | } 35 | -------------------------------------------------------------------------------- /test/opc-client-stream.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js'), 2 | sinon = require('sinon'), 3 | assert = require('assert'); 4 | 5 | var ReadableStream = require('stream').Readable, 6 | WritableStream = require('stream').Writable; 7 | 8 | 9 | describe('opc-client-stream.js', function() { 10 | var OpcClientStream = require('../lib/opc-client-stream'); 11 | 12 | it('exists and inherits from stream.Readable', function() { 13 | 14 | 15 | expect(OpcClientStream).to.be.a('function'); 16 | expect(new OpcClientStream()).to.be.a(ReadableStream); 17 | }); 18 | 19 | describe('protocol', function() { 20 | var clientStream, outputSpy; 21 | 22 | beforeEach(function () { 23 | outputSpy = sinon.spy(); 24 | clientStream = new OpcClientStream(); 25 | 26 | var outputStream = (function () { 27 | var s = new WritableStream(); 28 | s._write = function (buffer, encoding, callback) { 29 | outputSpy(buffer, encoding); 30 | callback(); 31 | }; 32 | 33 | return s; 34 | }()); 35 | 36 | clientStream.pipe(outputStream); 37 | }); 38 | 39 | describe('setpixelcolors', function() { 40 | it('sends setpixelcolors-messages', function (done) { 41 | clientStream.setPixelColors(7, new Buffer([0xaa, 0xbb, 0xcc])); 42 | 43 | process.nextTick(function () { 44 | expect(outputSpy.called).to.be(true); 45 | 46 | sinon.assert.calledWith( 47 | outputSpy, 48 | // buffer 49 | sinon.match(function (buf) { 50 | return buf.readUInt8(0) === 0x07 // channel 51 | && buf.readUInt8(1) === 0x00 // command 52 | && buf.readUInt16BE(2) === 0x0003 // length 53 | && buf.readUInt8(4) === 0xaa 54 | && buf.readUInt8(5) === 0xbb 55 | && buf.readUInt8(6) === 0xcc 56 | ; 57 | }), 58 | // encoding 59 | sinon.match('buffer') 60 | ); 61 | done(); 62 | }); 63 | }); 64 | 65 | it('handles Uint32Array-data', function(done) { 66 | var pixelData = new Uint32Array(2); 67 | pixelData.set([0xaabbcc, 0x112233]); 68 | 69 | clientStream.setPixelColors(7, pixelData); 70 | 71 | process.nextTick(function () { 72 | expect(outputSpy.called).to.be(true); 73 | 74 | sinon.assert.calledWith( 75 | outputSpy, 76 | // buffer 77 | sinon.match(function (buf) { 78 | return buf.readUInt8(0) === 0x07 // channel 79 | && buf.readUInt8(1) === 0x00 // command 80 | && buf.readUInt16BE(2) === 0x0006 // length 81 | && buf.readUInt8(4) === 0xaa 82 | && buf.readUInt8(5) === 0xbb 83 | && buf.readUInt8(6) === 0xcc 84 | && buf.readUInt8(7) === 0x11 85 | && buf.readUInt8(8) === 0x22 86 | && buf.readUInt8(9) === 0x33 87 | ; 88 | }), 89 | // encoding 90 | sinon.match('buffer') 91 | ); 92 | done(); 93 | }); 94 | }); 95 | }); 96 | 97 | describe('sysex', function() { 98 | it('sends sysex-messages', function(done) { 99 | clientStream.sysex(7, 0x2342, new Buffer([0xaa, 0xbb, 0xcc])); 100 | 101 | process.nextTick(function() { 102 | expect(outputSpy.called).to.be(true); 103 | 104 | sinon.assert.calledWith( 105 | outputSpy, 106 | // buffer 107 | sinon.match(function(buf) { 108 | return buf.readUInt8(0) === 0x07 // channel 109 | && buf.readUInt8(1) === 0xff // command 110 | && buf.readUInt16BE(2) === 0x0005 // length 111 | && buf.readUInt16BE(4) === 0x2342 // systemId 112 | && buf.readUInt8(6) === 0xaa // payload... 113 | && buf.readUInt8(7) === 0xbb 114 | && buf.readUInt8(8) === 0xcc 115 | ; 116 | }, 'stuuff'), 117 | // encoding 118 | sinon.match('buffer') 119 | ); 120 | done(); 121 | }); 122 | }) 123 | }); 124 | }); 125 | }); -------------------------------------------------------------------------------- /test/opc-parse-stream.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js'), 2 | sinon = require('sinon'), 3 | assert = require('assert'); 4 | 5 | describe('opc-parser', function() { 6 | var OpcParseStream = require('../lib/opc-parse-stream'); 7 | 8 | function createOpcMessage(channel, command, data) { 9 | assert(~~channel === channel); 10 | assert(~~command === command); 11 | assert(Buffer.isBuffer(data)); 12 | 13 | var msg = new Buffer(4 + data.length); 14 | msg.writeUInt8(channel, 0, true); 15 | msg.writeUInt8(command, 1, true); 16 | msg.writeUInt16BE(data.length, 2, true); 17 | 18 | data.copy(msg, 4); 19 | 20 | return msg; 21 | } 22 | 23 | 24 | describe('basics', function() { 25 | it('should exist', function() { 26 | expect(OpcParseStream).to.be.a('function'); 27 | }); 28 | 29 | it('should inherit from WritableStream', function() { 30 | var stream = require('stream'); 31 | 32 | expect(new OpcParseStream()).to.be.a(stream.Writable); 33 | }); 34 | }); 35 | 36 | describe('data handling', function() { 37 | var renderer, parser; 38 | 39 | beforeEach(function() { 40 | renderer = sinon.spy(); 41 | parser = new OpcParseStream({ 42 | channel: 7, 43 | systemId: 0x0042, 44 | dataFormat: OpcParseStream.DataFormat.UINT32_ARRAY 45 | }); 46 | 47 | parser.on('setpixelcolors', renderer); 48 | }); 49 | 50 | describe('streaming - handle partial messages', function() { 51 | it('should wait for the complete message', function() { 52 | // fragmented message for 3 LEDs: 0xffffff, 0x0000ff, 0x00ff00 53 | var fragments = [ 54 | // channel, command, length MSB 55 | new Buffer([0x00, OpcParseStream.Commands.SETPIXELCOLORS, 0x00]), 56 | // length LSB, data[0-1] 57 | new Buffer([0x09, 0xff, 0xff]), 58 | // data[2-4] 59 | new Buffer([0xff, 0x00, 0x00]), 60 | // data[5-9] 61 | new Buffer([0xff, 0x00, 0xff, 0x00]) 62 | ]; 63 | 64 | parser.write(fragments[0]); 65 | for(var i=1; i<4; i++) { 66 | expect(renderer.called).to.be(false); 67 | parser.write(fragments[i]); 68 | } 69 | 70 | expect(renderer.called).to.be(true); 71 | }); 72 | 73 | it('should hold data of a upcoming message', function() { 74 | // two different messages, split unevenly so the rest of 75 | // the first has to be kept back 76 | var fragments = [ 77 | new Buffer([0x00, 0x00, 0x00, 0x03, 0xaa, 0xbb, 0xcc, 78 | 0x07, 0x00]), 79 | new Buffer([ 0x00, 0x03, 0xaa, 0xcc, 0xbb]), 80 | new Buffer([0x00, 0x00, 0x00, 0x03, 0xcc, 0xbb, 0xaa]) 81 | ]; 82 | 83 | parser.write(fragments[0]); 84 | sinon.assert.calledOnce(renderer); 85 | 86 | parser.write(fragments[1]); 87 | sinon.assert.calledTwice(renderer); 88 | 89 | parser.write(fragments[2]); 90 | sinon.assert.calledThrice(renderer); 91 | var renderCall = renderer.getCall(2); 92 | 93 | sinon.assert.calledWith( 94 | renderCall, 95 | sinon.match(function(arr) { return arr[0] = 0xccbbaa; })) 96 | }); 97 | }); 98 | 99 | describe('streaming - multiple messages at once', function() { 100 | it('should handle multiple messages', function() { 101 | var msg = Buffer.concat([ 102 | createOpcMessage(0x00, OpcParseStream.Commands.SETPIXELCOLORS, 103 | new Buffer([0xff,0xee,0xdd])), 104 | createOpcMessage(0x00, OpcParseStream.Commands.SETPIXELCOLORS, 105 | new Buffer([0x11,0x22,0x33])) 106 | ]); 107 | 108 | parser.write(msg); 109 | expect(renderer.callCount).to.be(2); 110 | 111 | sinon.assert.calledWith( 112 | renderer.getCall(1), 113 | sinon.match.instanceOf(Uint32Array) 114 | .and(sinon.match.has('length', 1)) 115 | .and(sinon.match(function(arr) { 116 | return arr[0] === 0x112233; 117 | }, 'correct color-values')) 118 | ); 119 | }); 120 | }); 121 | 122 | describe('protocol - setPixelColors', function() { 123 | it('should accept broadcast setpixelcolor-messages', function() { 124 | var colorData = new Buffer([0xff, 0x00, 0x00, 0x00, 0xff, 0x00]); 125 | 126 | parser.write(createOpcMessage(0, OpcParseStream.Commands.SETPIXELCOLORS, colorData)); 127 | expect(renderer.called).to.be(true); 128 | 129 | sinon.assert.calledWith( 130 | renderer, 131 | 132 | sinon.match.instanceOf(Uint32Array) 133 | .and(sinon.match.has('length', 2)) 134 | .and(sinon.match(function(arr) { 135 | return arr[0] === 0xff0000 136 | && arr[1] === 0x00ff00; 137 | }, 'correct color-values')) 138 | ); 139 | }); 140 | 141 | it('should accept targeted setpixelcolor-messages', function() { 142 | var colorData = new Buffer([0xff, 0x00, 0x00, 0x00, 0xff, 0x00]); 143 | parser.write(createOpcMessage(7, OpcParseStream.Commands.SETPIXELCOLORS, colorData)); 144 | 145 | expect(renderer.called).to.be(true); 146 | }); 147 | 148 | it('should ignore values for other channels', function() { 149 | var colorData = new Buffer([0xff, 0xff, 0xff]); 150 | 151 | parser.write(createOpcMessage(4, OpcParseStream.Commands.SETPIXELCOLORS, colorData)); 152 | expect(renderer.called).to.be(false); 153 | }); 154 | 155 | it('should ignore sysex-messages', function() { 156 | parser.write(createOpcMessage(0, OpcParseStream.Commands.SYSEX, 157 | new Buffer([0x00, 0x42, 0x00, 0x00]))); 158 | 159 | expect(renderer.called).to.be(false); 160 | }); 161 | }); 162 | 163 | describe('protocol - sysex', function() { 164 | var sysexHandler; 165 | 166 | beforeEach(function() { 167 | sysexHandler = sinon.spy(); 168 | parser.on('sysex', sysexHandler); 169 | }); 170 | 171 | it('should skip too short messages', function() { 172 | var msg = new Buffer([0x00, 0x42, 0x11]); 173 | parser.write(createOpcMessage(0, OpcParseStream.Commands.SYSEX, msg)); 174 | 175 | expect(sysexHandler.called).to.be(false); 176 | }); 177 | 178 | it('should skip messages with wrong messageId', function() { 179 | var msg = new Buffer([0x00, 0x01, 0x11, 0x22]); 180 | parser.write(createOpcMessage(0, OpcParseStream.Commands.SYSEX, msg)); 181 | 182 | expect(sysexHandler.called).to.be(false); 183 | }); 184 | 185 | it('should handle sysex-messages', function() { 186 | // sysex-message: systemId:0x0042, commandId:Uint16BE, content 187 | var msg = new Buffer([0x00, 0x42, 0x11, 0x22, 0xaa, 0xbb]); 188 | parser.write(createOpcMessage(0, OpcParseStream.Commands.SYSEX, msg)); 189 | 190 | expect(sysexHandler.called).to.be(true); 191 | sinon.assert.calledWith( 192 | sysexHandler, 193 | 194 | sinon.match(0x1122), 195 | sinon.match.instanceOf(Buffer) 196 | .and(sinon.match.has('length', 2)) 197 | .and(sinon.match(function(arr) { 198 | return arr[0] === 0xaa 199 | && arr[1] === 0xbb; 200 | }, 'correct sysex-payload')) 201 | ); 202 | }); 203 | }); 204 | }); 205 | 206 | describe('data-format', function() { 207 | it('should handle buffer-format', function() { 208 | var renderer = sinon.spy(); 209 | var parser = new OpcParseStream(); 210 | 211 | parser.on('setpixelcolors', renderer); 212 | 213 | var colorData = new Buffer([0xff, 0x00, 0x00, 0x00, 0xcc, 0x00]); 214 | parser.write(createOpcMessage(0, OpcParseStream.Commands.SETPIXELCOLORS, colorData)); 215 | expect(renderer.called).to.be(true); 216 | 217 | sinon.assert.calledWith( 218 | renderer, 219 | 220 | sinon.match.instanceOf(Buffer) 221 | .and(sinon.match.has('length', 6)) 222 | .and(sinon.match(function(arr) { 223 | return arr.readUInt8(0) === 0xff 224 | && arr.readUInt8(4) === 0xcc; 225 | }, 'correct color-values')) 226 | ); 227 | }); 228 | }); 229 | }); -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | return { 3 | files: [ 4 | 'lib/*.js' 5 | ], 6 | 7 | tests: [ 8 | 'test/*.js' 9 | ], 10 | 11 | testFramework: 'mocha@2.1.0', 12 | env: { 13 | // use 'node' type to use node.js or io.js 14 | type: 'node' 15 | 16 | // if runner property is not set, then wallaby.js embedded node/io.js version is used 17 | // you can specifically set the node version by specifying 'node' (or any other command) 18 | // that resolves your default node version or just specify the path 19 | // your node installation, like 20 | 21 | // runner: 'node' 22 | // or 23 | // runner: 'path to the desired node version' 24 | 25 | //params: { 26 | // runner: '--harmony --harmony_arrow_functions', 27 | // env: 'PARAM1=true;PARAM2=false' 28 | //} 29 | }, 30 | 31 | bootstrap: function (wallaby) { 32 | var mocha = wallaby.testFramework; 33 | 34 | } 35 | }; 36 | }; --------------------------------------------------------------------------------