elgato-stream-deck
Advanced tools
Comparing version 1.0.0 to 1.1.0
@@ -0,1 +1,12 @@ | ||
<a name="1.1.0"></a> | ||
# [1.1.0](https://github.com/Lange/node-elgato-stream-deck/compare/v1.0.0...v1.1.0) (2017-05-18) | ||
### Features | ||
* add `write` method ([0085d87](https://github.com/Lange/node-elgato-stream-deck/commit/0085d87)) | ||
* add `fillColor`, `fillImage`, and `fillImageFromFile` methods ([5fe46ef](https://github.com/Lange/node-elgato-stream-deck/commit/5fe46ef)) | ||
<a name="1.0.0"></a> | ||
@@ -2,0 +13,0 @@ # 1.0.0 (2017-05-17) |
138
index.js
@@ -8,36 +8,138 @@ 'use strict'; | ||
const HID = require('node-hid'); | ||
const sharp = require('sharp'); | ||
const NUM_KEYS = 15; | ||
const PAGE_PACKET_SIZE = 8191; | ||
const NUM_FIRST_PAGE_PIXELS = 2583; | ||
const NUM_SECOND_PAGE_PIXELS = 2601; | ||
const ICON_SIZE = 72; | ||
const NUM_TOTAL_PIXELS = NUM_FIRST_PAGE_PIXELS + NUM_SECOND_PAGE_PIXELS; | ||
const keyState = new Array(NUM_KEYS).fill(false); | ||
const emitter = new EventEmitter(); | ||
const devices = HID.devices(); | ||
const streamDecks = devices.filter(device => { | ||
const connectedStreamDecks = devices.filter(device => { | ||
return device.product === 'Stream Deck' && device.manufacturer === 'Elgato Systems'; | ||
}); | ||
if (streamDecks.length > 1) { | ||
if (connectedStreamDecks.length > 1) { | ||
throw new Error('More than one Stream Deck is connected. This is unsupported at this time.'); | ||
} | ||
const streamDeck = new HID.HID(streamDecks[0].path); | ||
class StreamDeck extends EventEmitter { | ||
constructor(device) { | ||
super(); | ||
this.device = device; | ||
streamDeck.on('data', data => { | ||
for (let i = 0; i < NUM_KEYS; i++) { | ||
const keyPressed = Boolean(data[i + 1]); | ||
if (keyPressed !== keyState[i]) { | ||
if (keyPressed) { | ||
emitter.emit('down', i); | ||
} else { | ||
emitter.emit('up', i); | ||
this.device.on('data', data => { | ||
// The first byte is a report ID, the last byte appears to be padding | ||
// strip these out for now. | ||
data = data.slice(1, data.length - 1); | ||
for (let i = 0; i < NUM_KEYS; i++) { | ||
const keyPressed = Boolean(data[i]); | ||
if (keyPressed !== keyState[i]) { | ||
if (keyPressed) { | ||
this.emit('down', i); | ||
} else { | ||
this.emit('up', i); | ||
} | ||
} | ||
keyState[i] = keyPressed; | ||
} | ||
}); | ||
this.device.on('error', err => { | ||
this.emit('error', err); | ||
}); | ||
} | ||
write(buffer) { | ||
return this.device.write(StreamDeck.bufferToIntArray(buffer)); | ||
} | ||
fillColor(keyIndex, r, g, b) { | ||
const pixel = Buffer.from([b, g, r]); | ||
this._writePage1(keyIndex, Buffer.alloc(NUM_FIRST_PAGE_PIXELS * 3, pixel)); | ||
this._writePage2(keyIndex, Buffer.alloc(NUM_SECOND_PAGE_PIXELS * 3, pixel)); | ||
} | ||
fillImage(keyIndex, imageBuffer) { | ||
if (imageBuffer.length !== 15552) { | ||
throw new Error(`Expected image buffer of length 15552, got length ${imageBuffer.length}`); | ||
} | ||
keyState[i] = keyPressed; | ||
let pixels = []; | ||
for (let r = 0; r < ICON_SIZE; r++) { | ||
const row = []; | ||
const start = r * 3 * ICON_SIZE; | ||
for (let i = start; i < start + (ICON_SIZE * 3); i += 3) { | ||
const r = imageBuffer.readUInt8(i); | ||
const g = imageBuffer.readUInt8(i + 1); | ||
const b = imageBuffer.readUInt8(i + 2); | ||
row.push(b, g, r); | ||
} | ||
pixels = pixels.concat(row.reverse()); | ||
} | ||
const firstPagePixels = pixels.slice(0, NUM_FIRST_PAGE_PIXELS * 3); | ||
const secondPagePixels = pixels.slice(NUM_FIRST_PAGE_PIXELS * 3, NUM_TOTAL_PIXELS * 3); | ||
this._writePage1(keyIndex, Buffer.from(firstPagePixels)); | ||
this._writePage2(keyIndex, Buffer.from(secondPagePixels)); | ||
} | ||
}); | ||
streamDeck.on('error', err => { | ||
emitter.emit('error', err); | ||
}); | ||
fillImageFromFile(keyIndex, filePath) { | ||
return sharp(filePath) | ||
.flatten() // Eliminate alpha channel, if any. | ||
.resize(this.ICON_SIZE, this.ICON_SIZE) | ||
.raw() | ||
.toBuffer() | ||
.then(buffer => { | ||
return this.fillImage(keyIndex, buffer); | ||
}); | ||
} | ||
module.exports = emitter; | ||
_writePage1(keyIndex, buffer) { | ||
const header = Buffer.from([ | ||
0x02, 0x01, 0x01, 0x00, 0x00, keyIndex + 1, 0x00, 0x00, | ||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||
0x42, 0x4d, 0xf6, 0x3c, 0x00, 0x00, 0x00, 0x00, | ||
0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x28, 0x00, | ||
0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x48, 0x00, | ||
0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, | ||
0x00, 0x00, 0xc0, 0x3c, 0x00, 0x00, 0xc4, 0x0e, | ||
0x00, 0x00, 0xc4, 0x0e, 0x00, 0x00, 0x00, 0x00, | ||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00 | ||
]); | ||
const packetWithHeader = Buffer.concat([header, buffer]); | ||
const numZeroesToFill = PAGE_PACKET_SIZE - packetWithHeader.length; | ||
const packet = Buffer.concat([packetWithHeader, Buffer.alloc(numZeroesToFill)]); | ||
return this.write(packet); | ||
} | ||
_writePage2(keyIndex, buffer) { | ||
const header = Buffer.from([ | ||
0x02, 0x01, 0x02, 0x00, 0x01, keyIndex + 1, 0x00, 0x00, | ||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 | ||
]); | ||
const packetWithHeader = Buffer.concat([header, buffer]); | ||
const numZeroesToFill = PAGE_PACKET_SIZE - packetWithHeader.length; | ||
const packet = Buffer.concat([packetWithHeader, Buffer.alloc(numZeroesToFill)]); | ||
return this.write(packet); | ||
} | ||
get ICON_SIZE() { | ||
return ICON_SIZE; | ||
} | ||
static bufferToIntArray(buffer) { | ||
const array = []; | ||
for (const pair of buffer.entries()) { | ||
array.push(pair[1]); | ||
} | ||
return array; | ||
} | ||
} | ||
module.exports = new StreamDeck(new HID.HID(connectedStreamDecks[0].path)); |
{ | ||
"name": "elgato-stream-deck", | ||
"version": "1.0.0", | ||
"version": "1.1.0", | ||
"description": "A npm module for interfacing with the Elgato Stream Deck", | ||
@@ -12,2 +12,3 @@ "main": "index.js", | ||
"eslint-config-xo": "^0.18.2", | ||
"sharp": "^0.17.3", | ||
"weallbehave": "^1.2.0", | ||
@@ -39,4 +40,17 @@ "weallcontribute": "^1.0.8" | ||
"email": "email@alexvan.camp", | ||
"url": "https://alexvan.camp/" | ||
"url": "https://alexvan.camp/", | ||
"twitter": "vancamp" | ||
}, | ||
"contributors": [ | ||
{ | ||
"name": "Richard Fox", | ||
"twitter": "ProbablePrime" | ||
}, | ||
{ | ||
"name": "Chris Hanel", | ||
"twitter": "chrishanel", | ||
"email": "chrishanel@gmail.com", | ||
"url": "http://www.chrishanel.com/" | ||
} | ||
], | ||
"license": "MIT", | ||
@@ -43,0 +57,0 @@ "bugs": { |
@@ -1,3 +0,5 @@ | ||
# elgato-stream-deck [![npm version](https://img.shields.io/npm/v/elgato-stream-deck.svg)](https://npm.im/elgato-stream-deck) [![license](https://img.shields.io/npm/l/elgato-stream-deck.svg)](https://npm.im/elgato-stream-deck) [![Travis](https://img.shields.io/travis/lange/elgato-stream-deck.svg)](https://travis-ci.org/lange/elgato-stream-deck) | ||
# elgato-stream-deck [![npm version](https://img.shields.io/npm/v/elgato-stream-deck.svg)](https://npm.im/elgato-stream-deck) [![license](https://img.shields.io/npm/l/elgato-stream-deck.svg)](https://npm.im/elgato-stream-deck) [![Travis](https://travis-ci.org/Lange/node-elgato-stream-deck.svg?branch=master)](https://travis-ci.org/Lange/node-elgato-stream-deck) [![Join the chat at https://gitter.im/node-elgato-stream-deck/Lobby](https://badges.gitter.im/node-elgato-stream-deck/Lobby.svg)](https://gitter.im/node-elgato-stream-deck/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) | ||
![alt text](media/streamdeck_ui.png "elgato-stream-deck") | ||
[`elgato-stream-deck`](https://github.com/lange/elgato-stream-deck) is a Node.js library for interfacing | ||
@@ -14,4 +16,9 @@ with the [Elgato Stream Deck](https://www.elgato.com/en/gaming/stream-deck). | ||
* [Features](#features) | ||
* [PLanned Features](#planned-features) | ||
* [Planned Features](#planned-features) | ||
* [Contributing](#contributing) | ||
* [API](#api) | ||
* [`write`](#write) | ||
* [`fillColor`](#fill-color) | ||
* [`fillImageFromFile`](#fill-image-from-file) | ||
* [`fillImage`](#fill-image) | ||
* [Events](#events) | ||
@@ -25,3 +32,5 @@ * [`down`](#down) | ||
```javascript | ||
const path = require('path'); | ||
const streamDeck = require('elgato-stream-deck') | ||
streamDeck.on('down', keyIndex => { | ||
@@ -38,2 +47,12 @@ console.log('key %d down', keyIndex); | ||
}); | ||
// Fill the second button from the left in the first row with an image of the GitHub logo. | ||
// This is asynchronous and returns a promise. | ||
streamDeck.fillImageFromFile(3, path.resolve(__dirname, 'github_logo.png')).then(() => { | ||
console.log('Successfully wrote a GitHub logo to key 3.'); | ||
}); | ||
// Fill the first button form the left in the first row with a solid red color. This is synchronous. | ||
streamDeck.fillColor(4, 255, 0, 0); | ||
console.log('Successfully wrote a red square to key 4.'); | ||
``` | ||
@@ -44,2 +63,3 @@ | ||
* Key `down` and key `up` events | ||
* Fill keys with images or solid RGB colors | ||
@@ -49,3 +69,2 @@ ### Planned Features | ||
* Key combinations | ||
* Send new images to keys | ||
* Support "pages" feature from the official Elgato Stream Deck software | ||
@@ -61,2 +80,69 @@ | ||
### API | ||
#### <a name="write"></a> `> streamDeck.write(buffer) -> undefined` | ||
Synchronously writes an arbitrary [`Buffer`](https://nodejs.org/api/buffer.html) instance to the Stream Deck. | ||
Throws if an error is encountered during the write operation. | ||
##### Example | ||
```javascript | ||
// Writes 16 bytes of zero to the Stream Deck. | ||
streamDeck.write(Buffer.alloc(16)); | ||
``` | ||
#### <a name="fill-color"></a> `> streamDeck.fillColor(keyIndex, r, g, b) -> undefined` | ||
Synchronously sets the given `keyIndex`'s screen to a solid RGB color. | ||
##### Example | ||
```javascript | ||
// Turn key 4 (the top left key) solid red. | ||
streamDeck.fillColor(4, 255, 0, 0); | ||
``` | ||
#### <a name="fill-image-from-file"></a> `> streamDeck.fillImageFromFile(keyIndex, filePath) -> Promise` | ||
Asynchronously reads an image from `filePath` and sets the given `keyIndex`'s screen to that image. | ||
Automatically scales the image to 72x72 and strips out the alpha channel. | ||
If necessary, the image will be center-cropped to fit into a square. | ||
##### Example | ||
```javascript | ||
// Fill the second button from the left in the first row with an image of the GitHub logo. | ||
streamDeck.fillImageFromFile(3, path.resolve(__dirname, 'github_logo.png')) | ||
.then(() => { | ||
console.log('Successfully wrote a GitHub logo to key 3.'); | ||
}) | ||
.catch(err => { | ||
console.error(err); | ||
}); | ||
``` | ||
#### <a name="fill-image"></a> `> streamDeck.fillImage(keyIndex, buffer) -> undefined` | ||
Synchronously writes a buffer of 72x72 RGB image data to the given `keyIndex`'s screen. | ||
The buffer must be exactly 15552 bytes in length. Any other length will result in an error being thrown. | ||
##### Example | ||
```javascript | ||
// Fill the third button from the left in the first row with an image of the GitHub logo. | ||
const sharp = require('sharp'); // See http://sharp.dimens.io/en/stable/ for full docs on this great library! | ||
sharp(path.resolve(__dirname, 'github_logo.png')) | ||
.flatten() // Eliminate alpha channel, if any. | ||
.resize(streamDeck.ICON_SIZE, streamDeck.ICON_SIZE) // Scale down to the right size, cropping if necessary. | ||
.raw() // Give us uncompressed RGB | ||
.toBuffer() | ||
.then(buffer => { | ||
return streamDeck.fillImage(2, buffer); | ||
}) | ||
.catch(err => { | ||
console.error(err); | ||
}); | ||
``` | ||
### Events | ||
@@ -66,3 +152,3 @@ | ||
Fired whenever a key is pressed. `keyIndex` is the 0-15 numerical index of that key. | ||
Fired whenever a key is pressed. `keyIndex` is the 0-14 numerical index of that key. | ||
@@ -79,3 +165,3 @@ ##### Example | ||
Fired whenever a key is released. `keyIndex` is the 0-15 numerical index of that key. | ||
Fired whenever a key is released. `keyIndex` is the 0-14 numerical index of that key. | ||
@@ -82,0 +168,0 @@ ##### Example |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
13342
122
180
5