Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

18 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Periphub

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 ratbagd / libratbag DPI and polling rate — for models that library knows

Not affiliated with SteelSeries and not a SteelSeries Engine clone. It is a front end for other people's backends, plus the parts none of them provide.


Screenshots

Devices Lighting
Devices — what is physically plugged in, and how much of it Periphub can drive Lighting — live keyboard preview and a form built from the selected effect
Headset Profiles
Headset — battery, sidetone, power behaviour, equaliser as a draggable curve Profiles — one set of settings for every device at once

What Periphub adds on top of the backends

  • 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_UNIX socket, so two writers can never fight over a device.
  • Honest diagnosticsperiphub doctor says what is missing and why.

Supported hardware

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 FULL 111 LEDs, full-size ANSI layout, reactive effects
SteelSeries Arctis Nova Pro Wireless 1038:12e0 FULL sidetone, battery, lighting, power, equaliser
SteelSeries Rival 5 1038:183c DETECTED_ONLY works as a pointing device; no advanced control (see Limitations)

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 devices

Requirements

Fedora:

sudo dnf install openrgb headsetcontrol libratbag-ratbagd python3-pyside6

On other distributions install the equivalents: OpenRGB, HeadsetControl, libratbag/ratbagd, PySide6 and python3-evdev (needed for reactive effects). Nothing else is required — there is no build step and no third-party Python dependency beyond PySide6.

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=change

Read 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.


Installation

git clone https://github.com/Filipluke/periphub.git ~/Projects/periphub
mkdir -p ~/.local/bin && ln -sf ~/Projects/periphub/scripts/periphub ~/.local/bin/periphub && ln -sf ~/Projects/periphub/scripts/periphub-gui ~/.local/bin/periphub-gui

Check the environment before starting anything:

periphub doctor

Run the service in the foreground once to see what it does:

periphub daemon

Then 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 periphub

The 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 gui

Closing 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 --tray

To actually stop hardware control:

systemctl --user stop periphub

Desktop entry

The .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.svg
desktop-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/null

Command line

periphub status
periphub devices
periphub effects
periphub effect reactive_ripple --color FF3000 --reactive-color FFFFFF --speed 1.4
periphub headset --sidetone 40 --eq-profile action --inactive-time 30
periphub profile list && periphub profile apply gaming
periphub profile create tournament --effect gaming --color 00FF80 --processes "cs2,steam_app_"
periphub auto off

Language

The 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.


Graphical interface

A dark, single-accent theme. Navigation is a left sidebar, not tabs.

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 separates "works as a pointing device" from "advanced settings unavailable"
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: no component sets its own colours through setStyleSheet. Variants are declared as Qt properties (role, variant, tone) and the look lives in one stylesheet in theme.py. The only exceptions are the elements whose colour is the content: the colour swatch and the keyboard preview.

Keyboard visualisation

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.

Effect form

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.

Equaliser

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

Sound presets

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.


Effects

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.


Architecture

                    ┌──────────────┐
   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.

Layout

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
  gui/           PySide6 window (theme, widgets, keycaps, equaliser, pages)
tools/           QA and test scripts, no runtime dependency

Tests

python3 tools/test_discovery.py

Twelve device-discovery cases run against fake sysfs trees — no hardware and no service required, including cases that are hard to stage on a desk: an unknown model, a switched-off headset, a backend that is down.


Limitations — measured, not guessed

  • SteelSeries Rival 5 (1038:183c) has no advanced support. It is in neither the libratbag 0.18 database (ratbagctl listNo devices available) nor the OpenRGB detectors. Periphub deliberately does not ship a guessed .device file — guessing a protocol means sending random HID reports to hardware. The mouse works normally as a pointing device and the interface says exactly that instead of pretending.
  • Rapid Trigger / OmniPoint on the Apex Pro is not available. The adjustable actuation point uses a proprietary SteelSeries protocol that none of the installed backends implement. Settings written once from Engine on Windows stay in the keyboard's memory and remain active under Linux.
  • Headset battery readings are intermittently wrong. Measured: the first call can return Wrong id for device status packet while 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.json so 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.

Files

~/.config/periphub/config.json        settings
~/.config/periphub/profiles/*.json    profiles
~/.local/state/periphub/periphub.log  log
$XDG_RUNTIME_DIR/periphub.sock        IPC socket

Licence

MIT — see LICENSE.

About

One control panel for SteelSeries peripherals on Linux: per-key lighting effects, headset equaliser, cross-device profiles and automatic switching, driven by a single background service.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages