Deno Stream Deck

The Stream Deck is a rather pricey keyboard. Each key is a programmable LCD at ~72×72 pixels depending on device. Like all Elgato products, the Stream Deck is designed for broadcasting but can be repurposed for general use.

The official Stream Deck software is a drag & drop UI with extensions. For non-techy users it’s well designed with strong functionality.

To much ire Elgato have made little effort to support Apple Silicon. On macOS running on Rosetta the software is janky, bloated, and resource heavy. To Elgato’s credit their devices — I own a few — have developer documentation and APIs not locked behind proprietary apps. If their software isn’t up to par, I’ll write my own! In JavaScript, of course!

my Stream Deck attached to a Raspberry Pi running custom software

So I did. It’s running on a Raspberry Pi (zero 2 w). My intent is to go wireless. The Stream Deck housing is big enough to fit the Pi and a USB battery pack inside (I hope).

Human Interface

Stream Decks are USB HIDs (human interface devices).

There is a low-level multi-platform dynamic library to interface with them. Node.js has a popular bindings package. One option I could have used.

There is also an experimental WebHID API spec in the works. Only chromium browsers have some support for it. I was hoping Deno would have an unstable API ready but that’s on low priority due to difficulties in reliable testing.

I like Deno too much to fall back to Node. I figured I could write similar Deno bindings for the HID dynamic library. Deno FFI (foreign function interface) is the ticket. I stumbled around cross referencing the C header file and types table in the Deno docs. Decoding strings was a bit of guess work but I got there.

I’ve published my library as Deno USB HID API.

A basic search for connected devices is coded like:

const vendorId = 0x0fd9; // Elgato
hid.enumerate(vendorId).forEach((hidInfo) => {
  // e.g. "Stream Deck MK.2"
  console.log(hidInfo.product);
});

The bindings are extremely fast but prone to segfaults if you do something unexpected. For example: trying to close a device pointer that isn’t open. There is no try catching these errors. You need to be aware of what the dynamic library functions tolerate.

Stream Deck Library

I’ve published another library called Deno Stream Deck. This is a foundation for writing custom Stream Deck software. It uses my generic HID library to:

  • safely manage a device connection
  • set key brightness levels
  • write key image data
  • read key input asynchronously
  • dispatch events

Usage example:

import {DeckType, StreamDeck} from 'https://deno.land/x/deno_streamdeck/mod.ts';

const deck = new StreamDeck(DeckType.MK2);

deck.addEventListener('keyup', ({detail: {key}}) => {
  const [x, y] = deck.getKeyXY(key);
  console.log(`Key press at index ${key} coords ${x}-${y}`);
});

I owe a lot of gratitude to the Python Stream Deck library for the specifics of writing data to the device. That project was a big inspiration. I’ve basically written the same thing in JavaScript because I don’t like coding python! But also because it’s fun to hack around and learn things by doing it yourself.

Next Step

Another reason for using Deno is the fantastic WebSocket API support. My Stream Deck software architecture is based around web sockets. The Raspberry Pi is running a web server. Client plugins can connect, register their own keypad, and listen to events. The server has a little website that mirrors the Stream Deck on a canvas element. Key states can be monitored and simulated via the browser.

The nice thing about the server/client setup is that the Stream Deck can be wireless. It’s not permanently tethered to a single desktop computer like the official Elgato software. I can have separate clients running on independent machines. So when my MacBook Air wakes up, for example, it reconnects and new controls for it appear on the Stream Deck.

I’ve written a client to control my lights — as shown in the photo earlier. That plugin interacts with Home Assistant via another web socket. Web sockets are cool.

Anyway, a lot of work in progress! For now “real work” has taken priority. At least I’m not working in the dark! Although with energy prices today I’m opting to.

More to share on this in the coming months I hope.

Buy me a coffee! Support me on Ko-fi