Skip to content
FiliplukePublic

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

1 star

Watchers

0 watching

Forks

Latest commit

 

History

19 Commits

Folders and files

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


Screenshots

Gear — a native Engine workspace with separate device windows. Connected devices have colored artwork; disconnected or unknown devices appear in gray.

Engine Gear with connected Apex Pro, Arctis Nova Pro Wireless and Rival 5

Apex Pro Actuation — select individual keys or groups, edit thresholds and Rapid Trigger in a local draft, then explicitly apply a complete configuration.

Apex Pro editor with WASD selected, actuation thresholds and per-key Rapid Trigger

Lighting, mouse, headset, profiles, automation and diagnostics
Lighting Rival 5 settings
Illumination — per-key RGB preview and effect controls Rival 5 — explicit selection of DPI, polling, button and lighting changes
Headset Profiles
Headset — battery, sidetone, power and equalizer Profiles — saved configurations in the Periphub tab
Automation Status
Automation — process-based profile switching Status — backend availability and performance

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.


What Periphub adds on top of the backends

  • 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_UNIX socket, so two writers can never fight over a device.
  • Honest diagnostics — periphub 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 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 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). 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=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

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

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

Tests

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
done

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


Limitations — measured, not guessed

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

Apex Pro per-key OmniPoint (2026-10-10)

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.

GG Engine presentation (2026-10-11)

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.

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

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages