Writing Plugins for the Elgato Stream Deck (NodeJS)

Objective in Brief

This article will show you how to write a simple plugin for the Elgato Stream Deck from scratch with NodeJS. It also shows how you can integrate a secondary language into this, for those for whom JS is not their primary language.

Introduction

Although streaming is on my list of things to do in 2021, I purchased my Stream Deck XL a few weeks ago as much to see how i could use it for developing tools than entering into the world of Twitch en all.

A shout out of Chrissy LeMaire and her post My setup after a year of livestreaming which gave me the original impetus to try out one of these button riddled marvels.

Elgato provide a readily accessible SDK for writing plugins for their device and I’ve spent the past week getting to grips with it.

NOTE FOR WINDOWS USERS:
The code below makes use of shell scripts with language specific shebangs. You will need to adjust these to use a suitable equivalent and update the codepath property in manifest.js accordingly. However, the code will remain exactly the same.

Plugin Background

Architecture

Essentially, you write plugins for the Stream Deck App, as it acts as a proxy to the device itself. Indeed, if you do not have the Stream Deck app open, your stream deck won’t operate beyond displaying the screensaver image. The document below illustrates this:

Plugins can utilise native Javascript in HTML documents, or alternatively a compiled command line tool, which can be written in a language of your choice.

Operation

Plugins are represented in the Stream Deck application on the right hand side of the window. The example below is a specific Custom group, including our Catme! plugin.

A plugin can contain one more more actions that can be used. To use one with the Stream Deck, you simply drag the action onto a specific key in the app. At this point, it is now also visible on your stream deck device and you can interact with it accordingly.

Plugins allow you to define the actions that occur, not only when a button is pressed, but also when other activities happen, such as an application you are monitoring is launched.

The Catme! Plugin

I decided that a good use of a plugin for a device costing over two hundred euros would be one that displayed a random cat image each time a button was pressed. Go figure… πŸ™‚

Our Configuration

The basic software and hardware configuration the cat plugin has been developed on consists of:

  • Mac OSX, Big Sur – See note below for Windows systems and shell scripts.
  • PowerShell 7.1.0
  • NodeJS v15.50 (more about this below)
  • Elgato Stream Deck XL

Create New Plugin Directory

Plugins are stored in a specific directory under the generic Stream Deck plugin folder. On OSX systems, this is located by default at $HOME/Library/Application Support/com.elgato.StreamDeck/Plugins.

Making sure the Stream Deck application is closed, create a folder, com.example.timsapp.sdPlugin in the above directory. You’ll probably have noticed that folder names are in reverse DNS name format. This is intended for the purpose that may arise where you want your plugin to be available..

Create a Manifest File

Copy the text below, paste it into your editor of choice and save it in the folder you just renamed, saving the file as manifest.json, overwriting the existing file.

{
  "Actions": [
    {
      "Icon": "actionIcon",
      "Name": "Catme!",
      "States": [
        {
          "Image": "image",
          "TitleAlignment": "middle",
          "FontSize": "16"
        }
      ],
      "SupportedInMultiActions": false,
      "Tooltip": "This is a test application",
      "UUID": "com.example.timsapp.action"
    }
  ],
  "SDKVersion": 2,
  "Author": "Tim",
  "CodePath": "plugin",
  "Description": "This is a test application",
  "Name": "Tims App",
  "URL": "http://52.212.255.70",
  "Version": "1.4",
  "OS": [
    {
      "Platform": "mac",
      "MinimumVersion": "10.11"
    },
    {
      "Platform": "windows",
      "MinimumVersion": "10"
    }
  ],
  "Software": {
    "MinimumVersion": "4.1"
  }
}

You can find full documentation on the manifest file, manifest.json, here. However, the most important property to note at this time, with regards to the above, is “CodePath”. This refers to the filename of the plugins custom program that will be executed when the Stream Deck app starts up. Note that it’s location is relative to the directory of the plugin.

Process Initialisation Data

External plugins are invoked on loading of the Stream Deck app. The file specified in the CodePath property of manifest.json is launched (plugin in our case), passing argument parameters that are required for the intial registration of the plugin. This makes them readily accessible in any language that can process shell level arguments, such as PowerShell (via the $args special variable).

Here is an example of the format of data that gets sent on every initialisation.

-port
28196
-pluginUUID
77E0F22FBDF149124519C52E62BE4D2E
-registerEvent
registerPlugin
-info
{"application":{"language":"en","platform":"mac","version":"4.9.2.13193"},"devicePixelRatio":2,"devices":[{"id":"B32CC8E223C16EAB84570A06958A4860","name":"Stream Deck XL","size":{"columns":8,"rows":4},"type":2}],"plugin":{"version":"1.4"}}

You can find more specifics in the SDK documentation, but for the next stage, plugin registration, you will just need the port, pluginUUID, and registerEvent values.

Plugin Registration

After passing the initial details above, the Stream Deck app then handles the rest of plugin interactions exclusively via async communication over a websocket (including plugin registration). In addition to the creation of a websocket, the client plugin also needs to register handlers for the websocket events, so that interactions with the deck can be processed.

PowerShell and WebSockets Strife

The initial choice i wanted to do was to write the add-on completely in PowerShell. I had started out with it and successfully been able to obtain the arguments from the streamdeck app.

Unfortunately, the many hours I spent after trying to find a simple way to do the triumvirtate of websockets, asynchronous tasks and callbacks in PowerShell, my desire to keep it as simple as possible and minimise technical debt, led me away from using it in its entirety.

Instead, I went for an approach that still includes PowerShell in the mix, using NodeJS to handle registration, and any communication over the websocket, with the rest left to PowerShell to do. I’ve barely touched Javscript, but was able to put together something prettty quickly.

Create the NodeJS Plugin

We’re going to first ensure that two necessary packages are installed and available for our plug.

  • ws
  • shelljs

Then, we’ll create the file containing code which carries out the following:

  • Loads in the modules we installed above
  • Assign variables to the applicable elements in the arguments array.
  • Creates a websocket
  • Defines code for the ‘Open’ event being triggered to register the plugin
  • Defines code for the ‘Message’ event being triggered, detecting if the action was keyDown
  • If the action was keyDown, we fire off the PowerShell script, catme
  • Once the above is finished, start the setImage function, which instructs the app to reset the key image to that as defined in the manifest.

First, start a terminal session, and enter the following to change to the new plugin folder and install two packages we need. Leave your terminal session open.

cd "$HOME/Library/Application Support/com.elgato.StreamDeck/Plugins/com.example.timsapp.sdPlugin"
npm install ws
npm install shelljs

Next, start up your text editor of choice, and paste the following text in, saving it as a plugin

#! /usr/local/bin/node

var WS = require('ws');
var shell = require('shelljs');
var myArgs = process.argv.slice(2);
var port = myArgs[1];
var uuid = myArgs[3];
var registerEvent = myArgs[5];
var ws = new WS('ws://127.0.0.1:' + port);

var DestinationEnum = Object.freeze({
    HARDWARE_AND_SOFTWARE: 0,
    HARDWARE_ONLY: 1,
    SOFTWARE_ONLY: 2
});

function setImage(context, data) {
    var json = {
        event: 'setImage',
        context: context,
        payload: {
            image: data,
            target: DestinationEnum.HARDWARE_AND_SOFTWARE,
            state: 0
        }
    };
    ws.send(JSON.stringify(json));
}

ws.on('open', function () {
    var json = {
        event: registerEvent,
        uuid: uuid
    };
    ws.send(JSON.stringify(json));
});

ws.on('message', function (evt) {
    var jsonObj = JSON.parse(evt);
    if (jsonObj.event && jsonObj.event === 'keyDown') {
        shell.exec('./catme');
        setImage(jsonObj.context, null);
    }
});

Mark the plugin file as executable

Next up, we need change the file plugin so that the operating system sees it as an executable.
Going back to the terminal you left open, enter the following

chmod plugin +x

Register with thecatapi.com

You’ll need an api key in order to connect to the api service of thecatapi.

  • In your browser, go to https://thecatapi.com and signup
  • In due course you will have an api key that can be used in the following script

Create the PowerShell Script

I still wanted to show how simple it is to introduce PowerShell into the equation for your Elgato plugins. In this case, I’m performing the actions that connect to thecatapi.com and obtain a random image.

  • Again, go to your text editor of choice, and paste the code below.
  • Paste your api key between the quotes for x-api-key.
  • Save the file as catme
#! /usr/local/bin/pwsh
$headers = @{
  'x-api-key' = '<< insert your api key here>>'
}

$uri = 'https://api.thecatapi.com/v1/images/search'
$resp = Invoke-RestMethod -Uri $uri -Headers $headers

Invoke-WebRequest -Uri $resp.url -Headers $headers -OutFile ./image.png

Code Summary

  • We defined the header for the webrequest, containing the api key to be used for accessing the web service
  • The webrequest is executed and the output save/overwritten as image.png

Mark the powershell file as executable

Next up, we need change the file plugin so that the operating system sees it as an executable.
Going back to the terminal you left open, enter the following

chmod catme +x

Add an Instance of Catme!

We’re nearly there. All that’s left for us to do now is to add an instance of the plugin onto the deck.

Open the Stream Deck app, expand Custom on the right hand side of the window, and drag Catme! to a spare location on your deck.

At this point, we have carried out all the steps for making a working plugin.

Cat Away!

Now, you’re free to see as many pics of cats as long as you want. Just keep hitting that button. πŸ™‚

You’ll notice on first run that the image on the tile is just a blue background with the question mark. This is simply because no image file exists at that point. Once you start pressing away, but images will appear. As an extra benefit, the last image you had on your deck before you quit the stream deck app will be the one you see appear on reload.

Conclusion

In this blog, we’ve gone through the following process of creating a real barebones plugin for the Elgato Deck. Although not necessary (all of the code can be done in NodeJS), we’ve shown how a secondary language can be used simply

We’ve :

  • Created a simple NodeJS script container handlers for the websocket connection and events
  • Demonstrated how to create a websocket and process its events easily.
  • Shown how we can launch secondary scripts from our plugin.
  • Written a PowerShell script which pulls down a cat image from the web and saves/overwrites it as image.png in the plugin directory.
Share

Using PowerShell to make Azure Automation Graphical Runbooks – Part 2

The first article in this series introduced the Azure Automation Graphical SDK, targeting its use to produce a programmatic version of an existing graphical runbook, Get-DiceThrow. It then began covering the classes used by the SDK, referencing the portal elements and associated ones.

This second article continues the above, focusing on the largest and most challenging part of developing in this SDK, Activities.

Activities

An Activity refers to an action that is represented by an individual item on the graphic runook. Multiple Activity classes are available to be used. In the runbook we are developing, these are CommandActivity and WorkflowScriptActivity.

Workflow Script Activities

The WorkflowScriptActivity was briefly covered in the first article, when Workflows were outlined. As mentioned above, a workflow script activity is handled by the WorkflowScriptActivity class.

When an instance of this class is made, the following properties are available. Of these, only the first four are native to the class, with the other properties being inherited.

  • Begin
  • Process
  • End
  • CheckpointAfter
  • LoopExitCondition
  • EffectiveLoopExitCondition
  • LoopDelay
  • Name
  • EntityId
  • PositionX
  • PositionY
  • Description

Of note is that the Begin, Process and End properties match the options available in PowerShell advanced functions, providing the opportunity to perform pre-processing, record by record, and post-processing operations respectively.

In the script, Name, Process, PositionX, and PositionY are set. Name is visible on the graphical workbook diagram, as the text label on the activity. ProcessΒ is the actual script being used.Β PositionX and PositionY dictate the location on the canvas of the activity.

 

Command Activities

Not mentioned yet are the command activities. A command activity is represented by the CommandActivity class.

On its own the CommandActivity class has a minimal number of properties that are available. From the screenshot below, only Name and Description are represented directly in this class.

4 - Command Activities

However, when a CommandActivity class is instantiated, other inhertied properties are available, making the following

  • CommandType
  • InvocationActivityType
  • ParameterSetName
  • Parameters
  • CustomParameters
  • CheckpointAfter
  • LoopExitCondition
  • EffectiveLoopExitCondition
  • LoopDelay
  • Name
  • EntityId
  • PositionX
  • PositionY
  • Description

Name, PositionX, and PositionY are implemented for the same use as that mentioned in the workflow script activity mentioned above. Also of note is EntityID, which is a unique identifier for the activity.

For the runbook we are developing CommandType, ParameterSetName, and Parameters to be set.

The first of these, CommandType, refers to one of the available Activity Types in the SDK.

Activity Types

Activity types detail the properties that make the action ‘tick’. That is, it describes what the activity does.

The CommandActivityType class has three properties that we set in the Runbook :

CommandName is the actual command. e.g. Write-Output

ModuleName refers to the module in which the command can be found. Write-Output for example uses Microsoft.PowerShell.Utility

5 - cmdlets

The next article in this series will cover the use of Parameters, and ParameterSets in an activity type, and the classes used by them.

Thanks for reading,

Tim

Share