Skip to content

Instantly share code, notes, and snippets.

@maietta
Created March 10, 2025 07:02
Show Gist options
  • Save maietta/0c7729c7ee640bba2bdb616ba2462b9f to your computer and use it in GitHub Desktop.
Save maietta/0c7729c7ee640bba2bdb616ba2462b9f to your computer and use it in GitHub Desktop.
Stream Deck Plus LCD testing in TYpeScript & Bun
import { openStreamDeck, listStreamDecks, StreamDeck } from '@elgato-stream-deck/node';
import sharp from 'sharp';
import HID from 'node-hid';
import usb, { LibUSBException } from 'usb';
async function main() {
try {
console.log('Looking for Stream Deck Plus devices...');
// Find the Stream Deck Plus device
const device = usb.findByIds(0x0fd9, 0x0084);
if (!device) {
throw new Error('No Stream Deck Plus devices found');
}
console.log('Found device:', device);
try {
device.open();
console.log('Opened device');
// Claim interface 0
const iface = device.interface(0);
try {
if (iface.isKernelDriverActive()) {
console.log('Detaching kernel driver...');
iface.detachKernelDriver();
}
} catch (err) {
console.log('No kernel driver active');
}
iface.claim();
console.log('Claimed interface');
// Get endpoints
const outEndpoint = iface.endpoints.find(ep => ep.direction === 'out');
if (!outEndpoint) {
throw new Error('Could not find OUT endpoint');
}
console.log('Found OUT endpoint:', outEndpoint);
// Set maximum brightness using control transfer
const brightnessReport = Buffer.alloc(32);
brightnessReport[0] = 0x03; // Feature report ID
brightnessReport[1] = 0x08; // Set brightness command
brightnessReport[2] = 100; // Brightness value (0-100)
try {
await new Promise((resolve, reject) => {
device.controlTransfer(
0x21, // bmRequestType (Host-to-device, Class, Interface)
0x09, // bRequest (SET_REPORT)
0x0300, // wValue (Feature report 0x03)
0x0000, // wIndex
brightnessReport,
(error?: LibUSBException, data?: Buffer | number) => {
if (error) reject(error);
else resolve(true);
}
);
});
console.log('Set brightness to maximum');
} catch (err) {
console.error('Error setting brightness:', err);
}
// Create a red raw image for the LCD display
const width = 200;
const height = 100;
const redImage = await sharp({
create: {
width,
height,
channels: 3,
background: { r: 255, g: 0, b: 0 }
}
})
.raw()
.toBuffer();
// Try each LCD display (0-3 for Stream Deck Plus)
for (let i = 0; i < 4; i++) {
try {
console.log(`\nSending red image to LCD ${i}...`);
// Reset key stream
const resetData = Buffer.alloc(32);
resetData[0] = 0x02; // Report ID
resetData[1] = 0x0B; // Reset command
try {
await new Promise((resolve, reject) => {
(outEndpoint as any).transferOut(resetData, (error?: LibUSBException) => {
if (error) reject(error);
else resolve(true);
});
});
console.log('Reset command sent successfully');
} catch (err) {
console.error('Error sending reset command:', err);
continue;
}
await new Promise(resolve => setTimeout(resolve, 50));
// LCD header
const header = Buffer.alloc(32);
header[0] = 0x02; // Report ID
header[1] = 0x07; // Set LCD image
header[2] = i & 0xff; // LCD index
header[3] = 0x00; // Padding
header[4] = 0x00; // Padding
header[5] = 0x00; // Padding
header[6] = 0x00; // Padding
header[7] = 0x00; // Padding
header[8] = redImage.length & 0xff; // Image size (little endian)
header[9] = (redImage.length >> 8) & 0xff;
header[10] = (redImage.length >> 16) & 0xff;
header[11] = (redImage.length >> 24) & 0xff;
header[12] = 0x00; // Raw image format
header[13] = 0x00;
header[14] = 0x00;
header[15] = 0x00;
// Send header
try {
await new Promise((resolve, reject) => {
(outEndpoint as any).transferOut(header, (error?: LibUSBException) => {
if (error) reject(error);
else resolve(true);
});
});
console.log('Header sent successfully');
} catch (err) {
console.error('Error sending header:', err);
continue;
}
await new Promise(resolve => setTimeout(resolve, 50));
// Send image data in chunks
const CHUNK_SIZE = 1024;
const numChunks = Math.ceil(redImage.length / CHUNK_SIZE);
let chunksSent = 0;
for (let chunk = 0; chunk < numChunks; chunk++) {
const start = chunk * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, redImage.length);
const chunkData = redImage.slice(start, end);
// Create chunk packet
const packet = Buffer.alloc(CHUNK_SIZE);
chunkData.copy(packet);
// Send chunk
try {
await new Promise((resolve, reject) => {
(outEndpoint as any).transferOut(packet, (error?: LibUSBException) => {
if (error) reject(error);
else resolve(true);
});
});
chunksSent++;
if (chunksSent % 10 === 0) {
console.log(`Sent ${chunksSent}/${numChunks} chunks...`);
}
} catch (err) {
console.error(`Error sending chunk ${chunk}:`, err);
break;
}
await new Promise(resolve => setTimeout(resolve, 25));
}
console.log(`Successfully sent ${chunksSent}/${numChunks} chunks to LCD ${i}`);
} catch (err) {
console.error(`Error setting image on LCD ${i}:`, err);
}
}
console.log('\nTest complete. Press Ctrl+C to exit.');
// Keep the process running
process.on('SIGINT', () => {
try {
iface.release(() => {
device.close();
console.log('Device closed');
process.exit();
});
} catch (err) {
console.error('Error closing device:', err);
process.exit(1);
}
});
} catch (err) {
console.error('Error:', err);
if (device) {
try {
device.close();
} catch (closeErr) {
console.error('Error closing device:', closeErr);
}
}
}
} catch (err) {
console.error('Error:', err);
}
}
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment