
TLDR; This is a multi part series of using your Elgato Streamdeck with Java. This is the second part, which deals with event handling and creating a stateful application on top of a stateless StreamDeck.
You can read part 1 here.
In this post we will implement a stateful application, even though the StreamDeck does not know any state, but we want an application that has subscreens and back buttons, so we need to put that into our Java app.
Handling events
First we need some architecture of dealing with events, when there is interaction with the stream deck. Pressing a button might sent us into a sub view or trigger an action or return into a parent view.
So we probably need events and listeners similar to this:
@FunctionalInterface
public interface StreamDeckEventListener {
void onStreamDeckEvent(StreamDeckEvent e);
}
public interface StreamDeckEvent {}
record ButtonPressed(int pos) implements StreamDeckEvent {}
Let’s start with a button that changes its color when pressed. Let it be red by default and the alternate between orange and white.
In order to do so, we also need a StreamDeck
class, that continously reads
from the device and triggers events as needed. So basically we have
something like
public class StreamDeck {
private final HidDevice hidDevice;
private StreamDeckEventListener listener;
public StreamDeck(HidDevice device) {
this.hidDevice = hidDevice;
}
public void setListener(StreamDeckEventListener listener) {
this.listener = listener;
}
static final byte[] BUTTON_PRESSED = new byte[] { 1, 0, 8, 0};
public void listen() {
while (true) {
byte[] data = new byte[20];
hidDevice.read(data);
if (listener != null) {
// check if the first bytes match
// not super efficient as it copies the byte array
// good enough for me
if (Arrays.compare(BUTTON_PRESSED,
new byte[] {data[0], data[1], data[2], data[3]}) == 0) {
for (int i = 0; i < 8; i++) {
if (data[4 + i] == 1) {
listener.onStreamDeckEvent(new ButtonPressed(i));
}
}
}
}
}
}
}
Now the listen()
method contains some code to read, if a button has been
pressed and sends an event to the listener. The main code would look like
this:
StreamDeck streamDeck = new StreamDeck(hidDevice);
streamDeck.setListener(e -> {
if (e instanceof ButtonPressed buttonPressed) {
System.out.println("Button " + buttonPressed.pos() + " pressed");
}
});
streamDeck.listen();
And with that we now can implement listeners for our events and try to create our color changing button.
final StreamDeck streamDeck = new StreamDeck(hidDevice);
final AtomicReference<Color> color = new AtomicReference<>(Color.PINK);
streamDeck.setButtonColor(0, color.get());
streamDeck.setListener(e -> {
if (e instanceof StreamDeck.ButtonPressed buttonPressed && buttonPressed.pos() == 0) {
if (color.get().equals(Color.ORANGE)) {
color.set(Color.WHITE);
} else {
color.set(Color.ORANGE);
}
streamDeck.setButtonColor(0, color.get());
}
});
The StreamDeck.setButtonColor()
is known from the last blog post, as this
requires sending a feature report:
public void setButtonColor(int pos, Color color) {
final byte[] report = new byte[] { 0x06, (byte) pos,
(byte) color.getRed(), (byte) color.getGreen(), (byte) color.getBlue() };
hidDevice.sendFeatureReport(report, (byte) 0x03);
}
Ok, first part done. We can react on events, and manage a bit of state in our app. Let’s go further.
Modeling Screens, Buttons
So, what if we encapsulate the full state of our as an entity. So the
combination of reacting on all buttons, encoders and the LCD. In my
following examples I named this a Screen
. This screen is basically a
listener for stream deck events. When we move from one screen to another, we
need that screen to listen for events instead. Let’s have an example of two
screens:
- First screen, loaded on startup, shows four buttons, colored red, green, yellow and blue.
- Each press on a button loads shows a new screen where the first button allows to return on the first screen, and the second button has the same color than the previous one.
final StreamDeck streamDeck = new StreamDeck(hidDevice);
Mainscreen mainScreeen = new MainScreen(streamdeck);
streamDeck.setListener(mainScreen);
mainScreen.render();
streamDeck.listen();
Now time to write the main screen
public static class MainScreen implements StreamDeck.StreamDeckEventListener {
private final StreamDeck streamDeck;
public MainScreen(StreamDeck streamDeck) {
this.streamDeck = streamDeck;
}
public void render() {
this.streamDeck.setButtonColor(0, Color.RED);
this.streamDeck.setButtonColor(1, Color.GREEN);
this.streamDeck.setButtonColor(2, Color.YELLOW);
this.streamDeck.setButtonColor(3, Color.BLACK);
}
@Override
public void onStreamDeckEvent(StreamDeck.StreamDeckEvent e) {
}
}
This renders four buttons, but leaves the event handling empty for now, as we don’t have anything yet to act on.
Now we have a bit of a catch-22 issue. We need the four color screens to refer to the main screen to return to it, but we also need the main screen to refer to the four color screens to jump to it.
We do the most lazy thing possible, lazy initialization. We initialize our four screens first, and then create the main screen, and add a setter to configure the ability to go back.
final StreamDeck streamDeck = new StreamDeck(hidDevice);
GoBackScreen[] goBackScreens = new GoBackScreen[4];
goBackScreens[0] = new GoBackScreen(streamDeck, Color.RED);
goBackScreens[1] = new GoBackScreen(streamDeck, Color.GREEN);
goBackScreens[2] = new GoBackScreen(streamDeck, Color.YELLOW);
goBackScreens[3] = new GoBackScreen(streamDeck, Color.BLUE);
MainScreen mainScreen = new MainScreen(streamDeck, goBackScreens);
for (int i = 0; i < 4; i++) {
goBackScreens[i].setReturnScreen(mainScreen);
}
streamDeck.setListener(mainScreen);
mainScreen.render();
streamDeck.listen();
Our GoBackScreen
features now has the possibility to “go back”, simply by
rerendering the main screen.
public static class GoBackScreen implements StreamDeck.StreamDeckEventListener {
private final StreamDeck streamDeck;
private final Color color;
private MainScreen mainScreen;
public GoBackScreen(StreamDeck streamDeck, Color color) {
this.streamDeck = streamDeck;
this.color = color;
}
public void setReturnScreen(MainScreen mainScreen) {
this.mainScreen = mainScreen;
}
public void render() {
streamDeck.setButtonText(0, "Back", "Roboto", 24, Color.PINK);
streamDeck.setButtonColor(1, this.color);
}
@Override
public void onStreamDeckEvent(StreamDeck.StreamDeckEvent e) {
if (e instanceof StreamDeck.ButtonPressed buttonPressed && buttonPressed.pos() == 0) {
streamDeck.setListener(mainScreen);
mainScreen.render();
}
}
}
The main screen now needs to do the same and needs to render the other screens if a button is pressed
public static class MainScreen implements StreamDeck.StreamDeckEventListener {
private final StreamDeck streamDeck;
private final GoBackScreen[] goBackScreens;
public MainScreen(StreamDeck streamDeck, GoBackScreen[] goBackScreens) {
this.streamDeck = streamDeck;
this.goBackScreens = goBackScreens;
}
public void render() {
this.streamDeck.setButtonColor(0, Color.RED);
this.streamDeck.setButtonColor(1, Color.GREEN);
this.streamDeck.setButtonColor(2, Color.YELLOW);
this.streamDeck.setButtonColor(3, Color.BLUE);
}
@Override
public void onStreamDeckEvent(StreamDeck.StreamDeckEvent e) {
if (e instanceof StreamDeck.ButtonPressed buttonPressed && buttonPressed.pos() < 4) {
streamDeck.setListener(goBackScreens[buttonPressed.pos()]);
goBackScreens[buttonPressed.pos()].render();
}
}
}
When trying this example, there is one left over task to do: Resetting the
whole streamdeck when an event is triggered, because currently only the
buttons that are explicitely configured in the screens are set. This can be
done by setting the other buttons to Color.BLACK
and you are done.
Lots of code in here, but how does it look in practice?
There is one more thing, and we will cover this in the next post. The code
here is blocking and single threaded, so if you are executing a long running
action, you should run this outside of the listener, as otherwise we need
the while
loop above to react on key inputs. We will add logic for that in
the next post as well.
Before we finish this post, let’s add one more event type: turning the knob to change the brightness of StreamDeck itself. First, we need an event for turning a knob and the code to deal with it.
record EncoderPressed(int pos) implements StreamDeckEvent {}
record EncoderRotated(int pos, int value) implements StreamDeckEvent {}
Now we need to emit this in our StreamDeck
static final byte[] BUTTON_PRESSED = new byte[] { 1, 0, 8, 0};
static final byte[] ENCODER_PRESSED = new byte[] { 1, 3, 5, 0, 0};
static final byte[] ENCODER_ROTATED = new byte[] { 1, 3, 5, 0, 1};
and we also need to extend the while
loop parsing the read USB data
} else if (Arrays.compare(ENCODER_ROTATED,
new byte[] {data[0], data[1], data[2], data[3], data[4]} ) == 0) {
for (int i = 0; i < 4; i++) {
if (data[5 + i] != 0) {
listener.onStreamDeckEvent(new EncoderRotated(i, data[5 + i]));
}
}
}
Also there is a built-in way to set the StreamDeck brightness like this:
public void setBrightness(int brightness) {
final byte BRIGHTNESS_REPORT_ID = 0x03;
if (brightness < 0 || brightness > 100) {
throw new IllegalArgumentException("Brightness is out of bounds! Must be between 0-100.");
}
hidDevice.sendFeatureReport(getBrightnessBuffer((byte) brightness), BRIGHTNESS_REPORT_ID);
}
private static byte[] getBrightnessBuffer(byte brightness) {
return new byte[]{
0x08, brightness, 0x23, (byte) 0xB8, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
(byte) 0xA5, 0x49, (byte) 0xCD, 0x02, (byte) 0xFE, 0x7F, 0x00, 0x00,
};
}
With this, we can add a knob to control and a button to display the
brightness to our MainScreen
:
@Override
public void onStreamDeckEvent(StreamDeck.StreamDeckEvent e) {
// ...
if (e instanceof StreamDeck.EncoderRotated encoderRotated && encoderRotated.pos() == 0) {
this.brightness = Math.min(100, Math.max(0, brightness + encoderRotated.value()));
this.streamDeck.setBrightness(brightness);
this.streamDeck.setButtonText(7, brightness + "%", "Roboto", 24, Color.YELLOW);
}
}
This is what it looks like now:
And that’s it, we have a knob to control brightness - we will use such a knob also to control the level of the blinds in the next post, along with the brightness of Elgato lights.
Resources
- hid4java
- lsusb
- python-elgato-streamdeck
- node-elgato-stream-deck: interesting implementation as you can control your Streamdeck from within the browser via webHID.
Final remarks
If you made it down here, wooow! Thanks for sticking with me. You can follow or contact me on Bluesky, Mastodon, GitHub, LinkedIn or reach me via Email (just to tell me, you read this whole thing :-).
If there is anything to correct, drop me a note, and I am happy to fix and append a note to this post!
Same applies for questions. If you have question, go ahead and ask!