A general-purpose guide for wiring physical StreamDock keys to anything on a Linux machine — volume controls, launching apps, toggling services, running scripts — using Claude, ChatGPT, or any other coding LLM to write the glue code for you.
You do not need to be a Python expert. This guide gives you (1) a copy-paste prompt that tells the LLM everything it needs to know about the SDK, and (2) generic worked examples you can adapt.
The StreamDock Python SDK talks to the device over USB-HID. Your program:
- Enumerates and opens the device.
- Draws an image/icon on each physical key.
- Registers a callback that fires when a key is pressed or released.
- In that callback, runs whatever you want — a shell command, an API call, a script.
That's the whole loop. Everything below is just filling in step 4.
# Clone the SDK
git clone https://github.com/MiraboxSpace/StreamDock-Device-SDK.git
cd StreamDock-Device-SDK/Python-SDK
# System libraries (Debian/Ubuntu) — install libusb BEFORE hidapi
sudo apt install -y libudev-dev libusb-1.0-0-dev libhidapi-libusb0
# Python deps (a virtualenv is strongly recommended; Python 3.10+ preferred)
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtLet your user run the device without sudo. Copy the udev rules shipped in the repo
and reload them:
sudo cp 99-streamdock.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules
sudo udevadm triggerUnplug and replug the device after this. To confirm the device is seen:
lsusb | grep -i mirabox # or look for the StreamDock vendor entryPaste the block below into Claude (or any coding LLM), then add your one-line request at the end, e.g. "Make key 1 mute/unmute the system audio." The prompt front-loads the API surface so the model doesn't hallucinate method names.
I'm writing a Python script for a Mirabox StreamDock macro keypad on Linux using the official StreamDock-Device-SDK. Help me write a single self-contained script. Here is the SDK API you must use (do not invent other methods): from StreamDock.DeviceManager import DeviceManager from StreamDock.InputTypes import EventType manager = DeviceManager() devices = manager.enumerate() # list of device objects device.open(); device.init() # open + initialize before use device.image_keys() # iterable of valid key indices (skips knobs) device.set_key_image(index, "path.png")# draw a PNG/JPEG icon on a key device.set_key_gif(index, "anim.gif") # animated key icon; then device.start_gif_loop() device.clearIcon(index) # clear one key device.clearAllIcon() # clear every key device.refresh() # push pending image changes to the screen device.set_brightness(percent) # 0-100 screen/key brightness device.set_key_callback(callback) # register key handler device.close() # release the device on exit The key callback signature is: def key_callback(device, event): if event.event_type == EventType.BUTTON: pressed = event.state == 1 # 1 = press, 0 = release key_index = event.key.value elif event.event_type == EventType.KNOB_ROTATE: direction = event.direction.value # for devices with knobs elif event.event_type == EventType.KNOB_PRESS: pressed = event.state == 1 elif event.event_type == EventType.SWIPE: direction = event.direction.value Requirements for the script you write: - Enumerate, open, and init the device; print firmware + serial. - Run device.listen() in a daemon thread so hotplug (unplug/replug) is handled, and re-apply my key setup via an on_device_added callback. - Draw an icon on each key I use and refresh(). - In the callback, ACT ON PRESS ONLY (event.state == 1) to avoid double-firing. - Run external commands with subprocess and never let an exception kill the loop — wrap each action in try/except and print errors. - Keep the main thread alive with a sleep loop; on Ctrl+C, clear icons and close() the device cleanly (closing on exit prevents segfaults). - Use absolute paths for icons and make them configurable at the top of the file. My device has <N> keys. The desktop environment is <e.g. GNOME on Wayland / KDE / X11>. Audio is managed by <PipeWire / PulseAudio>. Now: <describe what you want each key to do>.
Fill in the angle-bracket fields. The audio/desktop details matter because the command
a key runs differs between PipeWire (wpctl), PulseAudio (pactl), X11 (xdotool), and
Wayland.
These are intentionally simple and vendor-neutral. Mix and match them inside the callback.
Most modern distros (Fedora, Ubuntu 22.10+, recent Debian) use PipeWire, which ships
wpctl. PulseAudio equivalents are shown in comments.
import subprocess
SINK = "@DEFAULT_AUDIO_SINK@"
def vol_up():
subprocess.run(["wpctl", "set-volume", SINK, "5%+"]) # pactl: set-sink-volume @DEFAULT_SINK@ +5%
subprocess.run(["wpctl", "set-volume", SINK, "1.0", "--limit", "1.0"]) # cap at 100%
def vol_down():
subprocess.run(["wpctl", "set-volume", SINK, "5%-"]) # pactl: set-sink-volume @DEFAULT_SINK@ -5%
def toggle_mute():
subprocess.run(["wpctl", "set-mute", SINK, "toggle"]) # pactl: set-sink-mute @DEFAULT_SINK@ toggleWire them to keys 0, 1, 2:
ACTIONS = {
0: vol_down,
1: toggle_mute,
2: vol_up,
}
def key_callback(device, event):
if event.event_type != EventType.BUTTON or event.state != 1:
return # press only
action = ACTIONS.get(event.key.value)
if action:
try:
action()
except Exception as e:
print(f"action failed: {e}", flush=True)def key_callback(device, event):
if event.event_type == EventType.KNOB_ROTATE:
step = "5%+" if event.direction.value == "right" else "5%-"
subprocess.run(["wpctl", "set-volume", SINK, step])
elif event.event_type == EventType.KNOB_PRESS and event.state == 1:
subprocess.run(["wpctl", "set-mute", SINK, "toggle"])playerctl controls any MPRIS-compatible player (Spotify, VLC, browsers, mpv):
def key_callback(device, event):
if event.event_type != EventType.BUTTON or event.state != 1:
return
cmd = {
0: ["playerctl", "previous"],
1: ["playerctl", "play-pause"],
2: ["playerctl", "next"],
}.get(event.key.value)
if cmd:
subprocess.run(cmd)def launch(*cmd):
# Detach so the app keeps running independently of this script.
subprocess.Popen(cmd, start_new_session=True,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
ACTIONS = {
0: lambda: launch("firefox"),
1: lambda: launch("code"), # VS Code
2: lambda: launch("gnome-terminal"),
}def toggle_service(name):
active = subprocess.run(["systemctl", "is-active", "--quiet", name]).returncode == 0
subprocess.run(["systemctl", "restart" if not active else "stop", name])
ACTIONS = {
0: lambda: toggle_service("docker"),
1: lambda: subprocess.run(["/home/me/scripts/backup.sh"]),
}(Service control needs privileges — run the script as the right user, or grant a specific
sudoers rule for just that command rather than running the whole script as root.)
This is the smallest complete script. Hand it to the LLM as a starting point, or run it
as-is and swap in your own ACTIONS.
#!/usr/bin/env python3
import subprocess
import threading
import time
from StreamDock.DeviceManager import DeviceManager
from StreamDock.InputTypes import EventType
ICON_DIR = "/absolute/path/to/icons" # put your PNGs here
SINK = "@DEFAULT_AUDIO_SINK@"
ACTIONS = {
0: lambda: subprocess.run(["wpctl", "set-volume", SINK, "5%-"]),
1: lambda: subprocess.run(["wpctl", "set-mute", SINK, "toggle"]),
2: lambda: subprocess.run(["wpctl", "set-volume", SINK, "5%+"]),
}
ICONS = {0: "vol_down.png", 1: "mute.png", 2: "vol_up.png"}
def draw_keys(device):
for index, name in ICONS.items():
device.set_key_image(index, f"{ICON_DIR}/{name}")
device.refresh()
def key_callback(device, event):
if event.event_type != EventType.BUTTON or event.state != 1:
return
action = ACTIONS.get(event.key.value)
if not action:
return
try:
action()
except Exception as e:
print(f"key {event.key.value} action failed: {e}", flush=True)
def setup(device):
device.open()
device.init()
print(f"firmware={device.firmware_version} serial={device.serial_number}")
draw_keys(device)
device.set_key_callback(key_callback)
def main():
manager = DeviceManager()
devices = manager.enumerate()
threading.Thread(
target=manager.listen,
kwargs={"on_device_added": setup, "auto_open": False},
daemon=True,
).start()
for device in devices:
setup(device)
print("Running. Ctrl+C to quit.")
try:
while True:
time.sleep(0.1)
except KeyboardInterrupt:
pass
finally:
for device in reversed(list(manager.streamdocks)):
try:
device.set_key_callback(None)
time.sleep(0.1)
device.clearAllIcon()
device.refresh()
device.close()
except Exception as e:
print(f"close error: {e}")
if __name__ == "__main__":
main()Once your script works, run it on login without a terminal. Create
~/.config/systemd/user/streamdock.service:
[Unit]
Description=StreamDock macro keys
After=graphical-session.target
[Service]
ExecStart=/path/to/StreamDock-Device-SDK/Python-SDK/.venv/bin/python /path/to/your_script.py
Restart=on-failure
RestartSec=2
# So wpctl/playerctl reach the right audio + display session:
Environment=XDG_RUNTIME_DIR=%t
[Install]
WantedBy=default.targetEnable it:
systemctl --user daemon-reload
systemctl --user enable --now streamdock.service
systemctl --user status streamdock.service # check it's running
journalctl --user -u streamdock.service -f # watch logs / your print() output- Tell it your exact key count and layout. "16-key, 4x4" vs "6-key" changes how it
loops over
image_keys()and assigns actions. - Tell it your audio/display stack (PipeWire vs PulseAudio, Wayland vs X11). The SDK code is identical; only the command each key runs changes.
- Ask for press-only handling. Without it, every key fires twice (press + release).
- Ask it to never crash the loop. Each action wrapped in try/except keeps one bad command from killing the whole keypad.
- Iterate one key at a time. Get key 0 doing something trivial (
print), confirm the index matches the physical button, then build out from there — physical-to-index mapping is the most common surprise. - For icons, ask the LLM to generate simple labeled PNGs with Pillow if you don't have artwork, e.g. a colored square with centered text.
- SDK repo: https://github.com/MiraboxSpace/StreamDock-Device-SDK
- Python SDK README (full API, per-device features, hotplug):
Python-SDK/README.md - Working sample covering every device type:
Python-SDK/src/main.py wpctl(PipeWire): part ofwireplumberpactl(PulseAudio): part ofpulseaudio-utilsplayerctl(MPRIS media control): https://github.com/altdesktop/playerctl
License: the SDK is MIT-licensed; this guide assumes the same.