One control panel for SteelSeries peripherals on Linux.
Three good open-source backends already cover disjoint pieces of the hardware, but they do not talk to each other and share no idea of a "profile". Periphub is the layer above them: a per-key effect engine, profiles that span every device at once, and automatic switching — driven by a single background service that owns the hardware.
| Layer | Backend | What it actually gives you |
|---|---|---|
| Lighting | OpenRGB (SDK over TCP) | per-key control — 111 LEDs on an Apex Pro, Direct mode, 6×22 matrix |
| Headset | HeadsetControl | sidetone, battery, earcup lighting, auto power-off, 10-band equaliser |
| Mouse | rivalcfg + ratbagd / libratbag | Rival 5 DPI, polling, buttons and lighting; ratbag for other supported models |
Not affiliated with SteelSeries. Periphub combines Linux backends with a native, experimental Apex Pro OmniPoint driver. See OmniPoint support and limits before changing actuation settings.
Gear — a native Engine workspace with separate device windows. Connected devices have colored artwork; disconnected or unknown devices appear in gray.
Apex Pro Actuation — select individual keys or groups, edit thresholds and Rapid Trigger in a local draft, then explicitly apply a complete configuration.
Lighting, mouse, headset, profiles, automation and diagnostics
Screenshots show the actual Qt application with English demonstration profiles. Hardware status and RGB frames are read-only queries; the displayed keyboard and mouse configurations are examples, not device readback. See capture details.
Read the detailed change notes and UI implementation notes.
- A native Engine workspace with independent device windows, a full-size ANSI keyboard illustration, and profiles, automation and diagnostics grouped under one Periphub tab.
- Experimental Apex Pro actuation and Rapid Trigger — 61 adjustable keys, 0.1–4.0 mm thresholds, per-key RT drafts, mandatory backups and explicit full-map application. Opening or switching a profile does not send these settings.
- Rival 5 configuration — DPI presets, polling rate, buttons and ten lighting zones through an optional isolated rivalcfg worker.
- 17 per-key lighting effects (plus
off) rendered on the CPU and pushed in one SDK packet — including effects no backend offers: a ripple spreading from the pressed key, a typing heatmap, a system monitor across the function row, a clock, a battery gauge. - Profiles that cover every layer at once — one file sets lighting, sidetone, equaliser and DPI together.
- Automatic profile switching when a matching program starts.
- Low-battery notifications for the headset, with configurable thresholds.
- One owner of the hardware. The service holds every device; the window
and the CLI are clients over a
AF_UNIXsocket, so two writers can never fight over a device. - Honest diagnostics —
periphub doctorsays what is missing and why.
Periphub separates four things that are easy to confuse, and shows all four:
| Meaning | |
|---|---|
| detected | the device is on the USB bus — read from sysfs, so it stays visible even when a backend is down |
| connected | there is a usable link. For a wireless base station this is not the same as detected: the dock is plugged in while the headset is switched off |
| support level | FULL / PARTIAL / DETECTED_ONLY / UNAVAILABLE |
| capabilities | the individual functions that can actually be driven |
Verified on real hardware:
| Device | USB id | Support | Notes |
|---|---|---|---|
| SteelSeries Apex Pro | 1038:1610 |
PARTIAL |
111 LEDs, reactive effects; experimental 61-key OmniPoint; see docs |
| SteelSeries Arctis Nova Pro Wireless | 1038:12e0 |
FULL |
sidetone, battery, lighting, power, equaliser |
| SteelSeries Rival 5 | 1038:183c |
PARTIAL |
optional rivalcfg backend: DPI presets, polling, buttons, 10-zone RGB; no configuration readback |
Any other SteelSeries device is detected, classified by its USB interface
classes and listed as DETECTED_ONLY rather than hidden or reported as an
error. Devices from other vendors are ignored.
periphub devicesFedora:
sudo dnf install openrgb headsetcontrol libratbag-ratbagd python3-pyside6On other distributions install the equivalents: OpenRGB, HeadsetControl,
libratbag/ratbagd, PySide6 and python3-evdev (needed for reactive effects).
There is no build step. Rival 5 control uses an optional isolated dependency
environment and a scoped udev rule; see Rival 5 setup and usage.
The udev rules shipped by headsetcontrol and openrgb-udev-rules give access
without root. After installing them, re-plug the devices or:
sudo udevadm control --reload-rules && sudo udevadm trigger --subsystem-match=hidraw --action=changeRead docs/OPENRGB.md before the first run. Without a
detector whitelist, OpenRGB can hang on unrelated motherboard RGB controllers,
and the service will wait for it.
git clone https://github.com/Filipluke/periphub.git ~/Projects/periphubmkdir -p ~/.local/bin && ln -sf ~/Projects/periphub/scripts/periphub ~/.local/bin/periphub && ln -sf ~/Projects/periphub/scripts/periphub-gui ~/.local/bin/periphub-guiCheck the environment before starting anything:
periphub doctorRun the service in the foreground once to see what it does:
periphub daemonThen install it as a user service, started with the graphical session:
mkdir -p ~/.config/systemd/user && cp ~/Projects/periphub/systemd/periphub.service ~/.config/systemd/user/ && systemctl --user daemon-reload && systemctl --user enable --now periphubThe unit expects the checkout in ~/Projects/periphub — it sets PYTHONPATH
there and refuses to start if the directory is missing. If you cloned somewhere
else, edit the two %h/Projects/periphub paths in your copy of the unit.
Open the window:
periphub guiClosing the window does not stop hardware control. Effects, profiles and automatic switching keep running, because the service owns them. The window itself hides to the system tray — click the tray icon to bring it back, or use Quit in its menu to close the window for good. Starting Periphub again while it is in the tray raises the existing window instead of opening a second one.
To keep the tray icon ready after every login, start it hidden:
periphub gui --trayTo actually stop hardware control:
systemctl --user stop periphubThe .desktop file declares Icon=periphub, so install the icon together with
the entry — otherwise the menu shows an empty frame.
install -Dm644 ~/Projects/periphub/assets/periphub.svg ~/.local/share/icons/hicolor/scalable/apps/periphub.svgdesktop-file-install --dir="$HOME/.local/share/applications" ~/Projects/periphub/periphub.desktop && update-desktop-database "$HOME/.local/share/applications" && gtk-update-icon-cache -f -t ~/.local/share/icons/hicolor 2>/dev/null; kbuildsycoca6 2>/dev/nullperiphub statusperiphub devicesperiphub effectsperiphub effect reactive_ripple --color FF3000 --reactive-color FFFFFF --speed 1.4periphub headset --sidetone 40 --eq-profile action --inactive-time 30The inactive timeout selected in the Headset screen or with
--inactive-time is saved in the base profile. The service reapplies it after
a restart and when returning from an automatically selected profile. An
automatic profile may still provide its own timeout while it is active. Older
base profiles with a missing, null, or blank value are migrated to Never
on first start.
periphub profile list && periphub profile apply gamingperiphub profile create tournament --effect gaming --color 00FF80 --processes "cs2,steam_app_"periphub auto offThe window, the command line and the service all ship in English and Polish. A fresh installation starts in English; the system locale is deliberately not used to guess — running a Polish desktop does not mean you want a Polish interface. Pick the language in About Periphub; the service stores it, the command line reads the same setting, and the window applies it after it is reopened.
Default profile names are language-neutral identifiers (default, gaming,
work, monitor, night) because they are used in commands and file names;
only their descriptions are translated.
Translations are flat JSON catalogues, one file per language, with no build step:
periphub/locale/en.json
periphub/locale/pl.json
Adding a language means copying en.json, translating the values, adding the
code to LANGUAGES in periphub/i18n.py and — if the language needs it — a
plural rule next to the existing ones. python3 tools/test_i18n.py then checks
catalogue parity, missing keys and placeholder mismatches for you. Plural forms are handled by a rule per
language rather than by hand-written conditionals, so 1 LED / 2 LEDs and
1 dioda / 2 diody / 111 diod both come out right. A missing key falls back to
English and then to the key itself, so a partial translation degrades quietly
instead of showing blanks.
Technical identifiers — effect names, IPC operations, backend and brand names — are never translated.
A graphite Engine shell with a Gear inventory and a separate Periphub tab for overview, profiles, automation and diagnostics. Clicking a device opens its own nonmodal window; closing and reopening it preserves the local editor draft.
| Screen | What it shows |
|---|---|
| Devices | a card per detected device, the active configuration, and system load used by informational effects |
| Lighting | live keyboard visualisation and an effect editor whose form is built from the effect |
| Headset | battery, sidetone, power, earcup lighting, equaliser as a curve |
| Mouse | explicit controls for DPI presets, polling, buttons and lighting; profile values clearly separated from hardware readback |
| Profiles | profile manager — no raw JSON in the normal workflow |
| Rules | which program triggers which profile, and whether it matches right now |
| Status | layers as status rows, performance metrics, diagnostics to the clipboard |
Theme rule: shared surfaces live in theme.py; measured Engine tokens and
scoped QSS live in engine_theme.py. Qt properties select control variants.
Original vector artwork and a few explicit banner/type treatments paint locally.
OpenRGB reports LED positions in a 6×22 matrix and their names, but not key
widths — in the matrix, Space takes one cell just like a letter.
keycaps.py adds geometry only (a name → row, x, width, height table in 1u
units), matched against the LED names the device actually reports. The backend
stays untouched.
Layout confirmed by measurement: full-size ANSI Apex Pro, 104 keys.
Every effect class declares params — the parameters it really reads. The form
is built from that list, so there is no speed slider on a static effect and no
colour picker on the system monitor.
Ten draggable points on a dB grid. Bands are numbered 1–10, not labelled with
frequencies — HeadsetControl does not expose the centre frequencies anywhere:
not in -o json, not in its README, not in the strings of the binary. The band
count and range come from the tool's own validation messages
(Device only supports 10 bands., bands ranging from -10 to 10), so they are
measured rather than assumed.
The band order is an assumption: the graphic-equaliser convention, band 1 lowest. Checking it by ear takes half a minute — play something with obvious bass and run:
periphub headset --eq-curve "10,10,0,0,0,0,0,0,-10,-10"Bassy and dull means the order is right; thin and glassy means it is reversed,
and reversing the lists in EQ_PRESETS (periphub/backends/headset.py) is the
whole fix.
periphub headset --eq-profile flat| Preset | For |
|---|---|
flat |
reference, equaliser off |
neutral |
everyday listening, gentle cleanup |
fps |
shooters: trimmed low end, localisation band pushed |
action |
vehicles and explosions: impact plus brightened detail |
balanced |
general purpose: full low end, clean mids, open top |
music |
gentle V, vocals stay in place |
movie |
cinematic low end, clearer dialogue |
voice |
calls and meetings |
bass |
strong low end without muddying the mids |
Profiles carry a preset, so switching a profile also changes the sound.
| Name | Needs | Description |
|---|---|---|
static |
— | one colour |
breathing |
— | fade in and out |
rainbow |
— | whole keyboard through the spectrum |
rainbow_wave |
key map | spectrum travelling across the matrix |
wave |
key map | brightness wave in one colour |
gradient |
key map | static blend between two colours |
rainbow_wave_reactive |
key map + keys | default — rainbow plus a white ripple from the pressed key |
starlight |
— | random keys flare and fade |
fire |
key map | warm noise rising from the bottom row |
matrix_rain |
key map | falling columns |
reactive_key |
keys | the pressed key flares |
reactive_ripple |
key map + keys | ring spreading from the pressed key |
heatmap |
keys | typing heatmap |
system_monitor |
key map | CPU on F1–F12, GPU on the number row, RAM on the numpad, battery on the arrows |
battery |
— | whole keyboard as a battery gauge |
gaming |
— | WASD and action keys lit |
clock |
— | hours and minutes on the keys |
off |
— | dark |
Effects that react to keystrokes need read access to keyboard input events; spatial effects need a key map from the device. Both are reported per effect in the interface rather than failing silently.
┌──────────────┐
periphub gui ───▶│ │──▶ OpenRGB SDK (TCP 127.0.0.1:6742)
│ SERVICE │──▶ headsetcontrol (process, JSON)
periphub cli ───▶│ AF_UNIX IPC │──▶ ratbagctl / ratbagd (D-Bus)
│ │──▶ evdev (key events)
└──────────────┘──▶ sysfs (device discovery)
The service is the only owner of the hardware. Threads: render, headset, mouse,
appwatch, IPC. The socket lives in $XDG_RUNTIME_DIR/periphub.sock, mode
0600. Clients never touch a device directly, which is what keeps a closed
window from stopping the lighting.
The OpenRGB SDK client is written from scratch on the standard library
(periphub/backends/openrgb_sdk.py): the openrgb CLI runs a full detection
pass on every invocation and cannot address individual LEDs at animation rate.
Protocol 4 requires parsing zone segments; without that the buffer cursor
drifts and the LED list comes out empty.
Device discovery reads sysfs rather than asking the backends, because a
backend answers "what can I drive", not "what is plugged in". USB interfaces
are deduplicated by construction: interface directories (1-9:1.0) have no
idVendor file, so filtering on it yields exactly one entry per physical
device — a keyboard reporting five HID interfaces is one card, not five.
periphub/
daemon.py the service: threads, IPC operations, hardware ownership
discovery.py physical device detection from sysfs
effects.py per-key effect engine
config.py settings and profiles
i18n.py language runtime
locale/ translation catalogues
backends/ OpenRGB SDK client, HeadsetControl, libratbag, rivalcfg worker
gui/ PySide6 window (theme, widgets, keycaps, equaliser, pages)
tools/ QA and test scripts, no runtime dependency
for test in tools/test_*.py; do
if [ "$test" = "tools/test_mouse_protocol.py" ]; then
~/.local/share/periphub/mouse-venv/bin/python "$test" || exit 1
else
QT_QPA_PLATFORM=offscreen python3 "$test" || exit 1
fi
doneThe suites cover device discovery against fake sysfs trees, translation
catalogue consistency, mouse protocol/GUI/profile handling, and headset timeout persistence with an isolated
configuration and mocked hardware. No running service or physical device is
modified. The current suite has 102 cases across 15 scripts, including
keyboard packet codecs, backup failures, explicit-apply guards and independent
window lifecycles. The mouse protocol suite requires the optional backend
environment described in Rival 5 setup.
- Rival 5 configuration cannot be read back from hardware. The optional rivalcfg backend controls DPI, polling, buttons and lighting, but values in Periphub are configured/last-applied values. Custom RGB colours are restored by the daemon after reconnect. Selective rainbow and macro sequences are not exposed. See Rival 5 documentation.
- Apex Pro actuation and Rapid Trigger are experimental session writes. Engine → Apex Pro → Actuation edits 61 keys and 40 thresholds. Separate, acknowledged full-map sends preserve the existing guarded transport and backup. Live setting readback and flash persistence are not implemented. Protection Mode remains physically unverified. See current evidence.
- Headset battery readings are intermittently wrong. Measured: the first
call can return
Wrong id for device status packetwhile the next one reports the level immediately. Every read is therefore retried (headset_battery_retries). - ChatMix, voice prompts and microphone control appear in
headsetcontrol --help-all, but the Arctis Nova Pro Wireless does not report them among its capabilities — the interface greys those controls out instead of sending commands into the void. - HeadsetControl cannot read the equaliser curve back from the device. The
last curve applied is remembered in
~/.config/periphub/config.jsonso the window has something to show. - Per-application profiles match on processes, not on the focused window. Reading the active window on KDE/Wayland needs a KWin script.
- The service and the client can disagree on language. Each process reads the setting on start, so text produced by the service (device reasons, backend errors) follows the service's language until it is restarted, even if a client was started with another one. On a normal installation both are the same.
~/.config/periphub/config.json settings
~/.config/periphub/profiles/*.json profiles
~/.local/state/periphub/periphub.log log
$XDG_RUNTIME_DIR/periphub.sock IPC socket
MIT — see LICENSE.
Engine → Apex Pro → Actuation edits all 61 adjustable keys at 0.1–4.0 mm in 0.1 mm steps, with Ctrl/Shift multi-selection, groups and profile drafts. Apply is explicit and session-only, guarded by firmware checks and an onboard backup. Qualitative actuation/RT behavior on A was physically confirmed by the user; precise travel, all-key coverage and live readback remain unverified. This is not complete Engine parity. Workflow, evidence and rollback.
The native Qt shell now follows the GG 116 instance captured from the Windows VM:
80px navigation, Gear banners, configuration sidebar, seven keyboard tabs and
fixed-size horizontally scrollable editor. The Apex Pro illustration is original
QPainter artwork using the measured 104-key ANSI layout, with per-key selection,
rectangle selection, groups and live RGB preview. Each device editor is a real,
reusable window. Periphub-specific tools share one top tab. Bundled Open Sans
provides consistent typography without a system font installation; the license
and provenance are in assets/fonts/open-sans/. Actuation and RT edit drafts;
Save Draft and Apply to Device are separate. Unsupported GG features are visibly
disabled. The existing RGB, mouse, headset, IPC and profile backends are retained.
Measured reference, visual comparisons, testing and rollback. Visual parity is not complete: vector materials, fonts, unsupported content and explicit draft/apply controls differ. No SteelSeries source/assets were copied.







