├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── rollup.config.ts ├── src ├── index.ts └── lib │ ├── Device.ts │ ├── DeviceData.ts │ ├── Service.ts │ └── SocketDeviceData.ts ├── test ├── node.js └── pixel-pusher.test.ts ├── tools ├── gh-pages-publish.ts └── semantic-release-prepare.ts ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | #root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | max_line_length = 100 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | .DS_Store 5 | *.log 6 | .vscode 7 | .idea 8 | dist 9 | compiled 10 | .awcache 11 | .rpt2_cache 12 | docs 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - ~/.npm 5 | notifications: 6 | email: false 7 | node_js: 8 | - '10' 9 | - '11' 10 | - '8' 11 | - '6' 12 | script: 13 | - npm run test:prod && npm run build 14 | after_success: 15 | - npm run travis-deploy-once "npm run report-coverage" 16 | - if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" ]; then npm run travis-deploy-once "npm run deploy-docs"; fi 17 | - if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" ]; then npm run travis-deploy-once "npm run semantic-release"; fi 18 | branches: 19 | except: 20 | - /^v\d+\.\d+\.\d+$/ 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 jmswrnr 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-pixel-pusher 2 | 3 | A zero-dependency Node.js Pixel Pusher library designed for a fast full-update refresh rate. 4 | 5 | ## Simple Example 6 | 7 | Fill with green rectangle. 8 | Using [node-canvas](https://github.com/Automattic/node-canvas) 9 | 10 | ```js 11 | const PixelPusher = require('node-pixel-pusher'); 12 | const nodeCanvas = require('canvas'); 13 | 14 | const MAX_FPS = 30; 15 | 16 | const service = new PixelPusher.Service(); 17 | 18 | service.on('discover', device => { 19 | console.log('Discovered device', device.deviceData); 20 | 21 | const width = device.deviceData.pixelsPerStrip; 22 | const height = device.deviceData.numberStrips; 23 | const canvas = nodeCanvas.createCanvas(width, height); 24 | const ctx = canvas.getContext('2d'); 25 | 26 | console.log(`Starting render at ${MAX_FPS} FPS`); 27 | 28 | device.startRendering(() => { 29 | ctx.fillStyle = 'green'; 30 | ctx.fillRect(0, 0, width, height); 31 | const ImageData = ctx.getImageData(0, 0, width, height); 32 | device.setRGBABuffer(ImageData.data); 33 | }, MAX_FPS); 34 | }); 35 | ``` 36 | 37 | ## Animated Example 38 | 39 | Animate horizontal scan line. 40 | Using [node-canvas](https://github.com/Automattic/node-canvas) 41 | 42 | ```js 43 | const PixelPusher = require('node-pixel-pusher'); 44 | const nodeCanvas = require('canvas'); 45 | 46 | let service = new PixelPusher.Service(); 47 | service.on('discover', device => { 48 | console.log('Discovered device', device.deviceData); 49 | 50 | const width = device.deviceData.pixelsPerStrip; 51 | const height = device.deviceData.numberStrips; 52 | const canvas = nodeCanvas.createCanvas(width, height); 53 | const rectWidth = Math.max(1, Math.min(16, Math.floor(width / 4))) 54 | const ctx = canvas.getContext('2d'); 55 | ctx.fillStyle = 'white'; 56 | let pos = 0; 57 | const maxFPS = 30; 58 | 59 | console.log(`Starting render at ${maxFPS} FPS`); 60 | 61 | device.startRendering(() => { 62 | ctx.clearRect(0, 0, width, height); 63 | ctx.fillRect(pos, 0, rectWidth, height); 64 | let ImageData = ctx.getImageData(0, 0, width, height); 65 | device.setRGBABuffer(ImageData.data); 66 | 67 | pos = (pos+1) % (width - rectWidth); 68 | }, maxFPS); 69 | 70 | setTimeout(() => { 71 | console.log('Stopping render') 72 | device.stopRendering(); 73 | }, 5 * 1000); 74 | }); 75 | 76 | ``` 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-pixel-pusher", 3 | "version": "1.0.4", 4 | "description": "A zero-dependency Node.js Pixel Pusher library designed to achieve highest full update refresh rate.", 5 | "keywords": [ 6 | "pixel", "pusher", "led", "pixelpusher" 7 | ], 8 | "main": "dist/index.umd.js", 9 | "module": "dist/index.es5.js", 10 | "typings": "dist/types/index.d.ts", 11 | "files": [ 12 | "dist" 13 | ], 14 | "author": "jmswrnr ", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/jmswrnr/node-pixel-pusher" 18 | }, 19 | "license": "MIT", 20 | "engines": { 21 | "node": ">=6.0.0" 22 | }, 23 | "scripts": { 24 | "lint": "tslint --project tsconfig.json -t codeFrame 'src/**/*.ts' 'test/**/*.ts'", 25 | "prebuild": "rimraf dist", 26 | "build": "tsc --module commonjs && rollup -c rollup.config.ts && typedoc --out docs --target es6 --theme minimal --mode file src", 27 | "start": "rollup -c rollup.config.ts -w", 28 | "test": "jest --coverage", 29 | "test:watch": "jest --coverage --watch", 30 | "test:prod": "npm run lint && npm run test -- --no-cache", 31 | "deploy-docs": "ts-node tools/gh-pages-publish", 32 | "report-coverage": "cat ./coverage/lcov.info | coveralls", 33 | "commit": "git-cz", 34 | "semantic-release": "semantic-release", 35 | "semantic-release-prepare": "ts-node tools/semantic-release-prepare", 36 | "precommit": "lint-staged", 37 | "travis-deploy-once": "travis-deploy-once" 38 | }, 39 | "lint-staged": { 40 | "{src,test}/**/*.{ts,js}": [ 41 | "prettier --write", 42 | "git add" 43 | ] 44 | }, 45 | "config": { 46 | "commitizen": { 47 | "path": "node_modules/cz-conventional-changelog" 48 | } 49 | }, 50 | "jest": { 51 | "transform": { 52 | ".(ts|tsx)": "ts-jest" 53 | }, 54 | "testEnvironment": "node", 55 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$", 56 | "moduleFileExtensions": [ 57 | "ts", 58 | "tsx", 59 | "js" 60 | ], 61 | "coveragePathIgnorePatterns": [ 62 | "/node_modules/", 63 | "/test/" 64 | ], 65 | "coverageThreshold": { 66 | "global": { 67 | "branches": 90, 68 | "functions": 95, 69 | "lines": 95, 70 | "statements": 95 71 | } 72 | }, 73 | "collectCoverageFrom": [ 74 | "src/*.{js,ts}" 75 | ] 76 | }, 77 | "prettier": { 78 | "semi": true, 79 | "singleQuote": true 80 | }, 81 | "commitlint": { 82 | "extends": [ 83 | "@commitlint/config-conventional" 84 | ] 85 | }, 86 | "devDependencies": { 87 | "@commitlint/cli": "^8.2.0", 88 | "@commitlint/config-conventional": "^7.1.2", 89 | "@types/jest": "^23.3.2", 90 | "@types/node": "^10.11.0", 91 | "canvas": "^2.6.0", 92 | "colors": "^1.3.2", 93 | "commitizen": "^3.0.0", 94 | "coveralls": "^3.0.2", 95 | "cross-env": "^5.2.0", 96 | "cz-conventional-changelog": "^2.1.0", 97 | "husky": "^1.0.1", 98 | "jest": "^25.0.0", 99 | "jest-config": "^25.0.0", 100 | "lint-staged": "^8.0.0", 101 | "lodash.camelcase": "^4.3.0", 102 | "prettier": "^1.14.3", 103 | "prompt": "^1.0.0", 104 | "replace-in-file": "^3.4.2", 105 | "rimraf": "^2.6.2", 106 | "rollup": "^0.67.0", 107 | "rollup-plugin-commonjs": "^9.1.8", 108 | "rollup-plugin-json": "^3.1.0", 109 | "rollup-plugin-node-resolve": "^3.4.0", 110 | "rollup-plugin-sourcemaps": "^0.4.2", 111 | "rollup-plugin-typescript2": "^0.24.3", 112 | "semantic-release": "^15.9.16", 113 | "shelljs": "^0.8.3", 114 | "travis-deploy-once": "^5.0.9", 115 | "ts-jest": "^23.10.2", 116 | "ts-node": "^7.0.1", 117 | "tslint": "^5.11.0", 118 | "tslint-config-prettier": "^1.15.0", 119 | "tslint-config-standard": "^8.0.1", 120 | "typedoc": "^0.15.0", 121 | "typescript": "^3.0.3" 122 | }, 123 | "dependencies": {} 124 | } 125 | -------------------------------------------------------------------------------- /rollup.config.ts: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve' 2 | import commonjs from 'rollup-plugin-commonjs' 3 | import sourceMaps from 'rollup-plugin-sourcemaps' 4 | import camelCase from 'lodash.camelcase' 5 | import typescript from 'rollup-plugin-typescript2' 6 | import json from 'rollup-plugin-json' 7 | 8 | const pkg = require('./package.json') 9 | 10 | const libraryName = 'pixel-pusher' 11 | 12 | export default { 13 | input: `src/index.ts`, 14 | output: [ 15 | { file: pkg.main, name: camelCase(libraryName), format: 'umd', sourcemap: true }, 16 | { file: pkg.module, format: 'es', sourcemap: true }, 17 | ], 18 | // Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash') 19 | external: [], 20 | watch: { 21 | include: 'src/**', 22 | }, 23 | plugins: [ 24 | // Allow json resolution 25 | json(), 26 | // Compile TypeScript files 27 | typescript({ useTsconfigDeclarationDir: true }), 28 | // Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs) 29 | commonjs(), 30 | // Allow node_modules resolution, so you can use 'external' to control 31 | // which external modules to include in the bundle 32 | // https://github.com/rollup/rollup-plugin-node-resolve#usage 33 | resolve(), 34 | 35 | // Resolve source maps to the original source 36 | sourceMaps(), 37 | ], 38 | } 39 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { Service } from './lib/Service'; 2 | export { default as Device } from './lib/Device'; 3 | export { default as SocketDeviceData } from './lib/SocketDeviceData'; 4 | export { default as DeviceData } from './lib/DeviceData'; 5 | -------------------------------------------------------------------------------- /src/lib/Device.ts: -------------------------------------------------------------------------------- 1 | import DeviceData from './DeviceData'; 2 | 3 | const SEQUENCE_DENOTATION_LENGTH = 4; 4 | const STRIP_DENOTATION_LENGTH = 1; 5 | 6 | const NS_PER_SEC = 1e9; 7 | const MS_PER_NS = 1e-6; 8 | 9 | export default class Device { 10 | public deviceData: DeviceData; 11 | 12 | public readonly stripPacketData: { 13 | packetIndex: number; 14 | dataOffset: number; 15 | rgbaOffset: number; 16 | }[]; 17 | 18 | private packetBuffer: Buffer[]; 19 | private packetIndex: number = 0; 20 | private packetSequenceNumber: number = 1; 21 | private packetsPerRefresh: number = 1; 22 | private minimumUpdatePeriod: number = 0; 23 | 24 | public readonly stripDataSize: number = 0; 25 | private renderfn: () => boolean | void = () => {}; 26 | private shouldRender: boolean = false; 27 | 28 | constructor(deviceData: DeviceData) { 29 | this.deviceData = deviceData; 30 | 31 | this.packetBuffer = []; 32 | this.stripPacketData = []; 33 | 34 | this.stripDataSize = this.deviceData.pixelsPerStrip * 3; 35 | 36 | this.packetsPerRefresh = Math.ceil(this.deviceData.numberStrips / this.deviceData.stripsPerPkt); 37 | 38 | for (let p = 0; p < this.packetsPerRefresh; p++) { 39 | let stripOffset = this.deviceData.stripsPerPkt * p; 40 | 41 | let stripCount = Math.min( 42 | this.deviceData.stripsPerPkt, 43 | this.deviceData.numberStrips - stripOffset 44 | ); 45 | 46 | let packetSize = 47 | SEQUENCE_DENOTATION_LENGTH + stripCount * (STRIP_DENOTATION_LENGTH + this.stripDataSize); 48 | 49 | let packet = (this.packetBuffer[p] = Buffer.alloc(packetSize)); 50 | packet.fill(0x00); 51 | 52 | let pos = 4; 53 | let slen = stripOffset + stripCount; 54 | for (let s = stripOffset; s < slen; s++) { 55 | packet.writeUInt8(s, pos); 56 | // increment after writing UInt8 57 | pos += 1; 58 | this.stripPacketData.push({ 59 | packetIndex: p, 60 | dataOffset: pos, 61 | rgbaOffset: s * this.deviceData.pixelsPerStrip * 4 62 | }); 63 | // increment to skip strip data 64 | pos += this.stripDataSize; 65 | } 66 | } 67 | 68 | this.fullRefreshTick = this.fullRefreshTick.bind(this); 69 | } 70 | 71 | setRGBABuffer(data: Buffer | Uint8ClampedArray) { 72 | if (data.length !== this.deviceData.numberStrips * this.deviceData.pixelsPerStrip * 4) { 73 | console.log('Invalid buffer size'); 74 | return; 75 | } 76 | 77 | for (let y = 0; y < this.deviceData.numberStrips; y++) { 78 | let meta = this.stripPacketData[y]; 79 | let target = this.packetBuffer[meta.packetIndex]; 80 | 81 | for (var i = 0, j = 0; i < this.stripDataSize; i += 3, j += 4) { 82 | target[meta.dataOffset + i] = data[meta.rgbaOffset + j]; 83 | target[meta.dataOffset + i + 1] = data[meta.rgbaOffset + j + 1]; 84 | target[meta.dataOffset + i + 2] = data[meta.rgbaOffset + j + 2]; 85 | } 86 | } 87 | } 88 | 89 | sendPacket(packet: Buffer, deviceData: DeviceData): void {} 90 | 91 | fullRefreshTick() { 92 | let tickStartTime = process.hrtime(); 93 | let sendPacket = true; 94 | 95 | if (!this.shouldRender) { 96 | // Stop render loop 97 | return; 98 | } 99 | 100 | if (this.packetIndex === 0) { 101 | // Request render data 102 | if (this.renderfn() === false) { 103 | sendPacket = false; 104 | } 105 | } 106 | 107 | if (sendPacket) { 108 | let packet = this.packetBuffer[this.packetIndex]; 109 | packet.writeUInt32LE(this.packetSequenceNumber++, 0); 110 | this.sendPacket(packet, this.deviceData); 111 | this.packetIndex = (this.packetIndex + 1) % this.packetsPerRefresh; 112 | } 113 | 114 | const tickHRTime = process.hrtime(tickStartTime); 115 | const tickMS = (tickHRTime[0] * NS_PER_SEC + tickHRTime[1]) * MS_PER_NS; 116 | 117 | setTimeout( 118 | this.fullRefreshTick, 119 | Math.max( 120 | // Don't go below update limit 121 | this.minimumUpdatePeriod, 122 | // Skip to next frame render if we're not sending packets 123 | (sendPacket 124 | ? this.deviceData.updatePeriod 125 | : this.deviceData.updatePeriod * this.packetsPerRefresh) - 126 | // Remove time used by render/packet sending 127 | tickMS 128 | ) 129 | ); 130 | } 131 | 132 | setMaxFPS(maxFps: number) { 133 | this.minimumUpdatePeriod = 1000 / maxFps / this.packetsPerRefresh; 134 | } 135 | 136 | startRendering(renderfn: () => boolean | void, maxFps?: number) { 137 | let shouldStartRender = !this.shouldRender; 138 | 139 | if (maxFps) { 140 | this.setMaxFPS(maxFps); 141 | } 142 | this.renderfn = renderfn; 143 | 144 | if (shouldStartRender) { 145 | this.shouldRender = true; 146 | this.fullRefreshTick(); 147 | } 148 | } 149 | 150 | stopRendering() { 151 | this.shouldRender = false; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/lib/DeviceData.ts: -------------------------------------------------------------------------------- 1 | import SocketDeviceData from './SocketDeviceData'; 2 | 3 | export default interface DeviceData extends SocketDeviceData { 4 | numberStrips: number; 5 | stripsPerPkt: number; 6 | pixelsPerStrip: number; 7 | updatePeriod: number; 8 | powerTotal: number; 9 | deltaSequence: number; 10 | controllerNo: number; 11 | groupNo: number; 12 | myPort: number; 13 | // Extra Params 14 | artnetUniverse?: number; 15 | artnetChannel?: number; 16 | // 17 | stripFlags?: number[]; 18 | // 19 | pusherFlags?: number; 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/Service.ts: -------------------------------------------------------------------------------- 1 | import * as dgram from 'dgram'; 2 | import DeviceData from './DeviceData'; 3 | import Device from './Device'; 4 | import { AddressInfo } from 'net'; 5 | 6 | import { EventEmitter } from 'events'; 7 | 8 | const LISTENER_SOCKET_PORT: number = 7331; 9 | 10 | export declare interface Service { 11 | on(event: 'discover', listener: (controller: Device) => void): this; 12 | on(event: string, listener: Function): this; 13 | } 14 | 15 | export class Service extends EventEmitter { 16 | private socket: dgram.Socket; 17 | private devices: { [mac: string]: Device }; 18 | 19 | constructor() { 20 | super(); 21 | this.devices = {}; 22 | this.socket = dgram.createSocket('udp4'); 23 | this.socket.on('message', this.onMessage.bind(this)); 24 | this.socket.on('listening', () => { 25 | const address: AddressInfo = this.socket.address() as AddressInfo; 26 | console.log('Socket listening for PixelPusher on udp://*:' + address.port); 27 | }); 28 | this.socket.on('error', (err: Error) => { 29 | console.log('Error opening socket to detect PixelPusher', err); 30 | }); 31 | this.socket.bind(LISTENER_SOCKET_PORT); 32 | } 33 | 34 | close() { 35 | this.socket.close(); 36 | } 37 | 38 | onMessage(msg: Buffer, rinfo: dgram.RemoteInfo) { 39 | if (msg.length < 48) { 40 | return; 41 | } 42 | 43 | let mac: string = msg 44 | .slice(0, 6) 45 | .toString('hex') 46 | .match(/.{2}/g)! 47 | .join(':'); 48 | 49 | if (!mac) { 50 | return; 51 | } 52 | 53 | let controller = this.devices[mac]; 54 | if (controller && controller.deviceData.deviceType === 2) { 55 | // Already received message from this device 56 | 57 | let cycleTime = msg.readUInt32LE(28) / 1000; 58 | let delta = msg.readUInt32LE(36); 59 | 60 | if (delta > 5) { 61 | cycleTime += 5; 62 | } else if (delta === 0 && cycleTime > 1) { 63 | cycleTime -= 1; 64 | } 65 | 66 | controller.deviceData.updatePeriod = cycleTime; 67 | controller.deviceData.powerTotal = msg.readUInt32LE(32); 68 | controller.deviceData.deltaSequence = delta; 69 | 70 | return; 71 | } 72 | 73 | let ipAddress: string = msg 74 | .slice(6, 10) 75 | .toString('hex') 76 | .match(/.{2}/g)! 77 | .map(function(x) { 78 | return parseInt(x, 16); 79 | }) 80 | .join('.'); 81 | 82 | if (!ipAddress) { 83 | return; 84 | } 85 | 86 | let deviceType: number = msg[10]; 87 | 88 | if (deviceType !== 2) { 89 | // Must be PixelPusher device type 90 | return; 91 | } 92 | 93 | console.log(`PixelPusher Device discovered at ${ipAddress} [${mac}]`); 94 | 95 | let deviceData: DeviceData = { 96 | macAddress: mac, 97 | ipAddress: ipAddress, 98 | deviceType: msg[10], 99 | protocolVrsn: msg[11], 100 | vendorID: msg.readUInt16LE(12), 101 | productID: msg.readUInt16LE(14), 102 | hardwareRev: msg.readUInt16LE(16), 103 | softwareRev: msg.readUInt16LE(18), 104 | linkSpeed: msg.readUInt32LE(20), 105 | 106 | numberStrips: msg[24], 107 | stripsPerPkt: msg[25], 108 | pixelsPerStrip: msg.readUInt16LE(26), 109 | updatePeriod: msg.readUInt32LE(28) / 1000, // usec -> ms 110 | powerTotal: msg.readUInt32LE(32), 111 | deltaSequence: msg.readUInt32LE(36), 112 | controllerNo: msg.readInt32LE(40), 113 | groupNo: msg.readInt32LE(44), 114 | 115 | myPort: 9761 116 | }; 117 | 118 | if (msg.length >= 54) { 119 | deviceData.artnetUniverse = msg.readUInt16LE(48); 120 | deviceData.artnetChannel = msg.readUInt16LE(50); 121 | deviceData.myPort = msg.readUInt16LE(52); 122 | } 123 | 124 | if (msg.length >= 62) { 125 | deviceData.stripFlags = msg 126 | .slice(54, 62) 127 | .toString('hex') 128 | .match(/.{2}/g)! 129 | .map(function(x) { 130 | return parseInt(x, 16); 131 | }); 132 | } 133 | 134 | if (msg.length >= 66) { 135 | deviceData.pusherFlags = msg.readInt32LE(62); 136 | } 137 | 138 | let newDevice: Device = new Device(deviceData); 139 | 140 | newDevice.sendPacket = (packet: Buffer, deviceData: DeviceData) => { 141 | this.socket.send(packet, 0, packet.length, deviceData.myPort, deviceData.ipAddress); 142 | }; 143 | this.devices[mac] = newDevice; 144 | 145 | this.emit('discover', newDevice); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/lib/SocketDeviceData.ts: -------------------------------------------------------------------------------- 1 | import * as dgram from 'dgram'; 2 | 3 | export default interface SocketDeviceData { 4 | macAddress: string; 5 | ipAddress: string; 6 | deviceType: number; 7 | protocolVrsn: number; 8 | vendorID: number; 9 | productID: number; 10 | hardwareRev: number; 11 | softwareRev: number; 12 | linkSpeed: number; 13 | } 14 | -------------------------------------------------------------------------------- /test/node.js: -------------------------------------------------------------------------------- 1 | const PixelPusher = require('../dist/index.umd'); 2 | const nodeCanvas = require('canvas'); 3 | 4 | let service = new PixelPusher.Service(); 5 | service.on('discover', device => { 6 | console.log('Discovered device', device.deviceData); 7 | 8 | const width = device.deviceData.pixelsPerStrip; 9 | const height = device.deviceData.numberStrips; 10 | const canvas = nodeCanvas.createCanvas(width, height); 11 | const rectWidth = Math.max(1, Math.min(16, Math.floor(width / 4))); 12 | const ctx = canvas.getContext('2d'); 13 | ctx.fillStyle = 'white'; 14 | let pos = 0; 15 | const maxFPS = 30; 16 | 17 | console.log(`Starting render at ${maxFPS} FPS`); 18 | 19 | device.startRendering(() => { 20 | ctx.clearRect(0, 0, width, height); 21 | ctx.fillRect(pos, 0, rectWidth, height); 22 | let ImageData = ctx.getImageData(0, 0, width, height); 23 | device.setRGBABuffer(ImageData.data); 24 | 25 | pos = (pos + 1) % (width - rectWidth); 26 | }, maxFPS); 27 | 28 | setTimeout(() => { 29 | console.log('Stopping render'); 30 | device.stopRendering(); 31 | }, 5 * 1000); 32 | }); 33 | -------------------------------------------------------------------------------- /test/pixel-pusher.test.ts: -------------------------------------------------------------------------------- 1 | import * as PixelPusher from '../src/index'; 2 | 3 | import { createCanvas } from 'canvas'; 4 | 5 | /** 6 | * Discover test 7 | */ 8 | describe('Discover and render', () => { 9 | it( 10 | 'Canvas render test', 11 | done => { 12 | let s: PixelPusher.Service = new PixelPusher.Service(); 13 | s.on('discover', (device: PixelPusher.Device) => { 14 | console.log('Discovered Controller', device.deviceData); 15 | 16 | const width = device.deviceData.pixelsPerStrip; 17 | const height = device.deviceData.numberStrips; 18 | const canvas = createCanvas(width, height); 19 | const rectWidth = Math.max(1, Math.min(16, Math.floor(width / 4))); 20 | const ctx = canvas.getContext('2d'); 21 | ctx.fillStyle = 'white'; 22 | let pos = 0; 23 | const maxFPS = 30; 24 | 25 | console.log(`Starting render at ${maxFPS} FPS`); 26 | 27 | device.startRendering(() => { 28 | ctx.clearRect(0, 0, width, height); 29 | ctx.fillRect(pos, 0, rectWidth, height); 30 | let ImageData = ctx.getImageData(0, 0, width, height); 31 | device.setRGBABuffer(ImageData.data); 32 | 33 | pos = (pos + 1) % (width - rectWidth); 34 | }, maxFPS); 35 | 36 | setTimeout(() => { 37 | console.log('Stopping render'); 38 | device.stopRendering(); 39 | }, 5 * 1000); 40 | 41 | setTimeout(() => { 42 | console.log('Finishing test'); 43 | done(); 44 | }, 10 * 1000); 45 | }); 46 | }, 47 | 30 * 1000 48 | ); 49 | }); 50 | -------------------------------------------------------------------------------- /tools/gh-pages-publish.ts: -------------------------------------------------------------------------------- 1 | const { cd, exec, echo, touch } = require("shelljs") 2 | const { readFileSync } = require("fs") 3 | const url = require("url") 4 | 5 | let repoUrl 6 | let pkg = JSON.parse(readFileSync("package.json") as any) 7 | if (typeof pkg.repository === "object") { 8 | if (!pkg.repository.hasOwnProperty("url")) { 9 | throw new Error("URL does not exist in repository section") 10 | } 11 | repoUrl = pkg.repository.url 12 | } else { 13 | repoUrl = pkg.repository 14 | } 15 | 16 | let parsedUrl = url.parse(repoUrl) 17 | let repository = (parsedUrl.host || "") + (parsedUrl.path || "") 18 | let ghToken = process.env.GH_TOKEN 19 | 20 | echo("Deploying docs!!!") 21 | cd("docs") 22 | touch(".nojekyll") 23 | exec("git init") 24 | exec("git add .") 25 | exec('git config user.name "jmswrnr"') 26 | exec('git config user.email "jmswrnr@users.noreply.github.com"') 27 | exec('git commit -m "docs(docs): update gh-pages"') 28 | exec( 29 | `git push --force --quiet "https://${ghToken}@${repository}" master:gh-pages` 30 | ) 31 | echo("Docs deployed!!") 32 | -------------------------------------------------------------------------------- /tools/semantic-release-prepare.ts: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | const { fork } = require("child_process") 3 | const colors = require("colors") 4 | 5 | const { readFileSync, writeFileSync } = require("fs") 6 | const pkg = JSON.parse( 7 | readFileSync(path.resolve(__dirname, "..", "package.json")) 8 | ) 9 | 10 | pkg.scripts.prepush = "npm run test:prod && npm run build" 11 | pkg.scripts.commitmsg = "commitlint -E HUSKY_GIT_PARAMS" 12 | 13 | writeFileSync( 14 | path.resolve(__dirname, "..", "package.json"), 15 | JSON.stringify(pkg, null, 2) 16 | ) 17 | 18 | // Call husky to set up the hooks 19 | fork(path.resolve(__dirname, "..", "node_modules", "husky", "lib", "installer", 'bin'), ['install']) 20 | 21 | console.log() 22 | console.log(colors.green("Done!!")) 23 | console.log() 24 | 25 | if (pkg.repository.url.trim()) { 26 | console.log(colors.cyan("Now run:")) 27 | console.log(colors.cyan(" npm install -g semantic-release-cli")) 28 | console.log(colors.cyan(" semantic-release-cli setup")) 29 | console.log() 30 | console.log( 31 | colors.cyan('Important! Answer NO to "Generate travis.yml" question') 32 | ) 33 | console.log() 34 | console.log( 35 | colors.gray( 36 | 'Note: Make sure "repository.url" in your package.json is correct before' 37 | ) 38 | ) 39 | } else { 40 | console.log( 41 | colors.red( 42 | 'First you need to set the "repository.url" property in package.json' 43 | ) 44 | ) 45 | console.log(colors.cyan("Then run:")) 46 | console.log(colors.cyan(" npm install -g semantic-release-cli")) 47 | console.log(colors.cyan(" semantic-release-cli setup")) 48 | console.log() 49 | console.log( 50 | colors.cyan('Important! Answer NO to "Generate travis.yml" question') 51 | ) 52 | } 53 | 54 | console.log() 55 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es5", 5 | "module":"es2015", 6 | "lib": ["es2015", "es2016", "es2017", "dom"], 7 | "strict": true, 8 | "sourceMap": true, 9 | "declaration": true, 10 | "allowSyntheticDefaultImports": true, 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true, 13 | "declarationDir": "dist/types", 14 | "outDir": "dist/lib", 15 | "typeRoots": [ 16 | "node_modules/@types" 17 | ] 18 | }, 19 | "include": [ 20 | "src" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-config-standard", 4 | "tslint-config-prettier" 5 | ] 6 | } 7 | --------------------------------------------------------------------------------