├── .gitignore ├── Makefile ├── README.md └── resources ├── 98-steelseries-init.py ├── 98-steelseries.rules ├── preinstall-check.sh └── wine-init.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | # DESTDIR=./ 163 | etc/ 164 | .work/ 165 | /.temp/ 166 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | DESTDIR=/ 3 | 4 | RESOURCE_DIR=$(CURDIR)/resources 5 | WINE_INIT=$(RESOURCE_DIR)/wine-init.sh 6 | 7 | UDEV_RULES=98-steelseries.rules 98-steelseries-init.py 8 | UDEV_RULES_DIR=$(DESTDIR)etc/udev/rules.d 9 | 10 | WORKDIR=$(CURDIR)/.temp 11 | ENGINE_DOWNLOAD_URI=https://steelseries.com/gg/downloads/gg/latest/windows 12 | ENGINE_EXE=$(WORKDIR)/SteelSeriesSetup.exe 13 | 14 | INSTALL_FILES=$(patsubst %,$(UDEV_RULES_DIR)/%,$(UDEV_RULES)) 15 | 16 | SUDO=sudo 17 | 18 | $(UDEV_RULES_DIR)/%: $(RESOURCE_DIR)/% 19 | ${SUDO} mkdir -p "$(dir $@)" 20 | ${SUDO} cp -f "$<" "$@" 21 | 22 | $(ENGINE_EXE): 23 | mkdir -p "$(dir $@)" 24 | curl "${ENGINE_DOWNLOAD_URI}" -L --output "$(ENGINE_EXE)" 25 | chmod +x "$(ENGINE_EXE)" 26 | 27 | download: $(ENGINE_EXE) 28 | 29 | install: $(INSTALL_FILES) $(ENGINE_EXE) 30 | bash "$(RESOURCE_DIR)/preinstall-check.sh" 31 | echo "Configuring wine" 32 | bash "$(WINE_INIT)" 33 | wine "$(ENGINE_EXE)" 34 | 35 | .DEFAULT_GOAL = install -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unofficial SteelSeries Keyboard Support 2 | 3 | Original post and code written by Michael Lelli here: https://gist.github.com/ToadKing/26c28809b8174ad0e06bfba309cf3ff3 4 | 5 | There are a bunch of Linux gamers out there, some new to the Linux game and some who have been around since the days of rebuilding kernels to get your sound and wireless working. While SteelSeries Engine does not have official Linux support, with some setup you can get parts of SteelSeries Engine running and have some functionality working well. 6 | 7 | 8 | 9 | Note that is is all unofficial and not supported by SteelSeries. After this post you will be on your own and there will be bugs. 10 | 11 | ## Prerequisites 12 | 13 | * **Wine** - At least version 4.12 is recommended since that version [fixes a bug with some HID reports](https://bugs.winehq.org/show_bug.cgi?id=47013). Without that version, some devices won't work correctly. 14 | * **udev** - Unless you're using an obscure/old distro you probably already have this. 15 | * **Python 3** - Python 2 might also work but I have not tested it. It's time to upgrade anyway. 16 | * **gnu make** - Install setup scripts and udev rules 17 | * Your favorite distro of **Linux**. 18 | 19 | 20 | ## Setup (automatic) 21 | 22 | Clone this repo, setup the above prerequisites, and run `make install` 23 | 24 | ## Setup (manual) 25 | 26 | ### udev Rules 27 | To configure the firmware on SteelSeries devices, you will need to send it HID reports through the **hidraw** kernel driver. However by default the device files the driver creates are only readable and writable by the root user for security purposes. Rather than just give read and write permission to everything, we are going to make a udev rule to only allow read and write access to the devices we need. 28 | 29 | Start by creating this udev rule file in `/etc/udev/rules.d/98-steelseries.rules`: 30 | 31 | ACTION=="add", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="1038" RUN+="/etc/udev/rules.d/steelseries-perms.py '%E{DEVNAME}'" 32 | 33 | This sets up a rule for any hidraw device that is added with the SteelSeries USB Vendor ID. Rather than set the permissions here, we instead forward it the device file path to a python script. 34 | 35 | The python script at `/etc/udev/rules.d/steelseries-perms.py` should be this: 36 | 37 | #!/usr/bin/env python3 38 | 39 | import ctypes 40 | import fcntl 41 | import os 42 | import struct 43 | import sys 44 | 45 | # from linux headers hidraw.h, hid.h, and ioctl.h 46 | _IOC_NRBITS = 8 47 | _IOC_TYPEBITS = 8 48 | _IOC_SIZEBITS = 14 49 | 50 | _IOC_NRSHIFT = 0 51 | _IOC_TYPESHIFT = _IOC_NRSHIFT + _IOC_NRBITS 52 | _IOC_SIZESHIFT = _IOC_TYPESHIFT + _IOC_TYPEBITS 53 | _IOC_DIRSHIFT = _IOC_SIZESHIFT + _IOC_SIZEBITS 54 | 55 | _IOC_READ = 2 56 | 57 | def _IOC(dir, type, nr, size): 58 | return (dir << _IOC_DIRSHIFT) | \ 59 | (ord(type) << _IOC_TYPESHIFT) | \ 60 | (nr << _IOC_NRSHIFT) | \ 61 | (size << _IOC_SIZESHIFT) 62 | 63 | def _IOR(type, nr, size): 64 | return _IOC(_IOC_READ, type, nr, size) 65 | 66 | HID_MAX_DESCRIPTOR_SIZE = 4096 67 | 68 | class hidraw_report_descriptor(ctypes.Structure): 69 | _fields_ = [ 70 | ('size', ctypes.c_uint), 71 | ('value', ctypes.c_uint8 * HID_MAX_DESCRIPTOR_SIZE), 72 | ] 73 | 74 | HIDIOCGRDESCSIZE = _IOR('H', 0x01, ctypes.sizeof(ctypes.c_int)) 75 | HIDIOCGRDESC = _IOR('H', 0x02, ctypes.sizeof(hidraw_report_descriptor)) 76 | 77 | hidraw = sys.argv[1] 78 | 79 | with open(hidraw, 'wb') as fd: 80 | size = ctypes.c_uint() 81 | fcntl.ioctl(fd, HIDIOCGRDESCSIZE, size, True) 82 | descriptor = hidraw_report_descriptor() 83 | descriptor.size = size 84 | fcntl.ioctl(fd, HIDIOCGRDESC, descriptor, True) 85 | 86 | descriptor = bytes(descriptor.value)[0:int.from_bytes(size, byteorder=sys.byteorder)] 87 | 88 | # walk through the descriptor until we find the usage page 89 | usagePage = 0 90 | i = 0 91 | while i < len(descriptor): 92 | b0 = descriptor[i] 93 | bTag = (b0 >> 4) & 0x0F 94 | bType = (b0 >> 2) & 0x03 95 | bSize = b0 & 0x03 96 | 97 | if bSize != 0: 98 | bSize = 2 ** (bSize - 1) 99 | 100 | if b0 == 0b11111110: 101 | # long types shouldn't be the usage page, skip them 102 | i += 3 + descriptor[i+1] 103 | continue 104 | 105 | if bType == 1 and bTag == 0: 106 | # usage page, grab it 107 | format = '' 108 | if bSize == 1: 109 | format = 'B' 110 | elif bSize == 2: 111 | format = 'H' 112 | elif bSize == 4: 113 | format = 'I' 114 | else: 115 | raise Exception('usage page is length {}???'.format(bSize)) 116 | usagePage = struct.unpack_from(format, descriptor, i + 1)[0] 117 | break 118 | 119 | i += 1 + bSize 120 | 121 | # set read/write permissions for vendor and consumer usage pages 122 | # some devices don't use the vendor page, allow the interfaces they do use 123 | if usagePage == 0x000C or usagePage >= 0xFF00: 124 | os.chmod(hidraw, 0o666) 125 | 126 | This python script does the following actions: 127 | 128 | 1. Reads the HID Descriptor of the device. 129 | 2. Does a simple parsing of the HID Descriptor to get the [usage page](https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf) of the descriptor. 130 | 3. We check to see if the device has a vendor-defined usage page or a consumer usage page. These two usage pages are what most SteelSeries USB devices use for configuring the device. 131 | 4. If the usage page matches one of those, set read and write permissions for the file. 132 | 133 | Once you create this file, make sure the execute bit on the file is set so the udev rule can execute it properly. Once that's done you can either reset your udev rules and replug your SteelSeries devices or simply reboot your computer. Once you do you should see that some hidraw device files have read and write permissions for everybody: 134 | 135 | $ ls -l /dev/hidraw* 136 | crw-rw-rw- 1 root root 237, 0 Jul 13 17:58 /dev/hidraw0 137 | crw------- 1 root root 237, 1 Jul 13 17:58 /dev/hidraw1 138 | crw-rw-rw- 1 root root 237, 10 Jul 13 17:58 /dev/hidraw10 139 | crw-rw-rw- 1 root root 237, 11 Jul 13 17:58 /dev/hidraw11 140 | crw-rw-rw- 1 root root 237, 12 Jul 13 17:58 /dev/hidraw12 141 | crw-rw-rw- 1 root root 237, 13 Jul 13 17:58 /dev/hidraw13 142 | crw-rw-rw- 1 root root 237, 2 Jul 13 17:58 /dev/hidraw2 143 | crw------- 1 root root 237, 3 Jul 13 17:58 /dev/hidraw3 144 | crw------- 1 root root 237, 4 Jul 13 17:58 /dev/hidraw4 145 | crw-rw-rw- 1 root root 237, 5 Jul 13 17:58 /dev/hidraw5 146 | crw------- 1 root root 237, 6 Jul 13 17:58 /dev/hidraw6 147 | crw-rw-rw- 1 root root 237, 7 Jul 13 17:58 /dev/hidraw7 148 | crw-rw-rw- 1 root root 237, 8 Jul 13 17:58 /dev/hidraw8 149 | crw------- 1 root root 237, 9 Jul 13 17:58 /dev/hidraw9 150 | 151 | (Notice that some filea have "crw-rw-rw-" permissions. That means they have read and write support for everyone.) 152 | 153 | ### Wine Setup 154 | 155 | You must make a small change to your Wine prefix to get SteelSeries Engine to work correctly. By default, Wine makes fake plug-and-play devices from SDL devices. However, for SteelSeries Engine we will need full proper plug-and-play support. To do that, we will have to set the **HKEY_LOCAL_MACHINE\\System\\CurrentControlSet\\Services\\WineBus** registry key. Value **Enable SDL** must be 0. This can be set with this command: 156 | 157 | wine reg add HKEY_LOCAL_MACHINE\\System\\CurrentControlSet\\Services\\WineBus /v Enable\ SDL /t Reg_Dword /d 0 158 | 159 | Optionally, if you have a device with an OLED screen and want proper Engine Apps support you will need to supply "Arial Bold" and "Arial Black" font files. Different distros can install these files in different places and under different names, but you will want to put them under `drive_c/windows/Fonts/` in your Wine prefix and use the filenames `arialbd.ttf` and `ariblk.ttf` respectively. 160 | 161 | # Change these paths to your respective files. 162 | cp /usr/share/fonts/TTF/arialbd.ttf ~/.wine/drive_c/windows/Fonts/arialbd.ttf 163 | cp /usr/share/fonts/TTF/ariblk.ttf ~/.wine/drive_c/windows/Fonts/ariblk.ttf 164 | 165 | If you don't have the Arial fonts, you could try an equivalent replacement like Liberation Sans, although this is unsupported and untested. 166 | 167 | ### Installing SteelSeries Engine 168 | 169 | Installation works very similarly to Windows: Simply run the installer exe and follow the steps. Note that during installation the driver installation process might crash or hang due to missing functionality in Wine. To work around this, simply kill the `win_driver_installer.exe` process if it hangs. The installer will continue along after it's killed. 170 | 171 | ## What Works 172 | 173 | * Configuring non-key binding settings on most devices 174 | * Some Engine Apps, like PrismSync, ImageSync, and even Discord! (Discord requires having Discord running on a local Linux client, not a web browser.) 175 | 176 | ## What Doesn't 177 | 178 | * The taskbar icon doesn't work. This is probably a Wine bug. If you want to stop SteelSeries Engine you must kill the process or shutdown wine with `wineserver -k`. 179 | * Device hotplugging does not work. If you unplug a device you will need to restart Engine for it to show up again. 180 | * App detection does not work. 181 | * Features requiring driver support. Depending on the device this includes some or all button bindings and macros. 182 | * Devices that have onboard macro support, like the Rival 700 and the new Apex 7 and Apex Pro, will have functional macros, but not other key bindings like launch application. 183 | * Devices with software-driver virtual surround will not have configurable surround sound. 184 | * Arctis 3 support does not work. 185 | * No devices are tested on Wine at all, and there could be random bugs anywhere. 186 | 187 | ## What Else 188 | 189 | This support is unofficial and bugs will be abundant. While we won't provide official Linux support at this time, we will be open to talk with devs who are looking to fix bugs in Wine or other Linux software to better support SteelSeries products. Please feel free to drop us a line at the tech blog email at the bottom of the page if you want to get in touch. -------------------------------------------------------------------------------- /resources/98-steelseries-init.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Source: https://gist.github.com/ToadKing/26c28809b8174ad0e06bfba309cf3ff3 3 | 4 | import ctypes 5 | import fcntl 6 | import os 7 | import struct 8 | import sys 9 | 10 | # from linux headers hidraw.h, hid.h, and ioctl.h 11 | _IOC_NRBITS = 8 12 | _IOC_TYPEBITS = 8 13 | _IOC_SIZEBITS = 14 14 | 15 | _IOC_NRSHIFT = 0 16 | _IOC_TYPESHIFT = _IOC_NRSHIFT + _IOC_NRBITS 17 | _IOC_SIZESHIFT = _IOC_TYPESHIFT + _IOC_TYPEBITS 18 | _IOC_DIRSHIFT = _IOC_SIZESHIFT + _IOC_SIZEBITS 19 | 20 | _IOC_READ = 2 21 | 22 | def _IOC(dir, type, nr, size): 23 | return (dir << _IOC_DIRSHIFT) | \ 24 | (ord(type) << _IOC_TYPESHIFT) | \ 25 | (nr << _IOC_NRSHIFT) | \ 26 | (size << _IOC_SIZESHIFT) 27 | 28 | def _IOR(type, nr, size): 29 | return _IOC(_IOC_READ, type, nr, size) 30 | 31 | HID_MAX_DESCRIPTOR_SIZE = 4096 32 | 33 | class hidraw_report_descriptor(ctypes.Structure): 34 | _fields_ = [ 35 | ('size', ctypes.c_uint), 36 | ('value', ctypes.c_uint8 * HID_MAX_DESCRIPTOR_SIZE), 37 | ] 38 | 39 | HIDIOCGRDESCSIZE = _IOR('H', 0x01, ctypes.sizeof(ctypes.c_int)) 40 | HIDIOCGRDESC = _IOR('H', 0x02, ctypes.sizeof(hidraw_report_descriptor)) 41 | 42 | hidraw = sys.argv[1] 43 | 44 | with open(hidraw, 'wb') as fd: 45 | size = ctypes.c_uint() 46 | fcntl.ioctl(fd, HIDIOCGRDESCSIZE, size, True) 47 | descriptor = hidraw_report_descriptor() 48 | descriptor.size = size 49 | fcntl.ioctl(fd, HIDIOCGRDESC, descriptor, True) 50 | 51 | descriptor = bytes(descriptor.value)[0:int.from_bytes(size, byteorder=sys.byteorder)] 52 | 53 | # walk through the descriptor until we find the usage page 54 | usagePage = 0 55 | i = 0 56 | while i < len(descriptor): 57 | b0 = descriptor[i] 58 | bTag = (b0 >> 4) & 0x0F 59 | bType = (b0 >> 2) & 0x03 60 | bSize = b0 & 0x03 61 | 62 | if bSize != 0: 63 | bSize = 2 ** (bSize - 1) 64 | 65 | if b0 == 0b11111110: 66 | # long types shouldn't be the usage page, skip them 67 | i += 3 + descriptor[i+1] 68 | continue 69 | 70 | if bType == 1 and bTag == 0: 71 | # usage page, grab it 72 | format = '' 73 | if bSize == 1: 74 | format = 'B' 75 | elif bSize == 2: 76 | format = 'H' 77 | elif bSize == 4: 78 | format = 'I' 79 | else: 80 | raise Exception('usage page is length {}???'.format(bSize)) 81 | usagePage = struct.unpack_from(format, descriptor, i + 1)[0] 82 | break 83 | 84 | i += 1 + bSize 85 | 86 | # set read/write permissions for vendor and consumer usage pages 87 | # some devices don't use the vendor page, allow the interfaces they do use 88 | if usagePage == 0x000C or usagePage >= 0xFF00: 89 | os.chmod(hidraw, 0o666) -------------------------------------------------------------------------------- /resources/98-steelseries.rules: -------------------------------------------------------------------------------- 1 | ACTION=="add", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="1038" RUN+="/etc/udev/rules.d/98-steelseries-init.py '%E{DEVNAME}'" -------------------------------------------------------------------------------- /resources/preinstall-check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | info() { 4 | echo "$1" 5 | } 6 | 7 | error() { 8 | echo "Error: " $@ >&2 9 | } 10 | 11 | REQUIRED_COMMANDS="python3 wine make bash fc-list curl" 12 | MISSING_COMMANDS=() 13 | for cmd in $REQUIRED_COMMANDS; do 14 | if [ -z "$(command -v "$cmd")" ]; then 15 | MISSING_COMMANDS+=("$cmd") 16 | fi 17 | done 18 | 19 | if [ ${#MISSING_COMMANDS[@]} -gt 0 ]; then 20 | error "Missing required prerequisites: ${MISSING_COMMANDS[@]}" 21 | exit 1 22 | fi -------------------------------------------------------------------------------- /resources/wine-init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Source: https://gist.github.com/ToadKing/26c28809b8174ad0e06bfba309cf3ff3 4 | 5 | WINE_DIR=~/.wine 6 | FONTS="arialbd.ttf ariblk.ttf" 7 | 8 | get_font(){ 9 | fc-list | grep -m 1 -i "$1" | awk -F: '{print $1}' 10 | } 11 | 12 | # Enable full plug-and-play support 13 | echo "Configuring wine registry for plug-and-play support" 14 | wine reg add HKEY_LOCAL_MACHINE\\System\\CurrentControlSet\\Services\\WineBus /v Enable\ SDL /t Reg_Dword /d 0 /f 15 | 16 | WINE_FONT_DIR=$(realpath -m "${WINE_DIR}/drive_c/windows/Fonts") 17 | if [ ! -d "${WINE_FONT_DIR}" ]; then 18 | echo "Creating directory ${WINE_FONT_DIR}" 19 | mkdir -p "${WINE_FONT_DIR}" 20 | fi 21 | 22 | # Install fonts for oled devices 23 | for font in $FONTS; do 24 | install_path=$(realpath -m "${WINE_FONT_DIR}/${font}") 25 | if [ -r "${install_path}" ]; then 26 | # Nothing to do 27 | continue 28 | fi 29 | 30 | font_path=$(get_font "$font") 31 | if [ -z "$font_path" ]; then 32 | echo "Error: Font $font not found in system fonts!" >&2 33 | exit 1 34 | else 35 | echo "Copying system font ${font_path} to ${install_path}" 36 | cp -f "${font_path}" "${install_path}" 37 | fi 38 | done 39 | 40 | --------------------------------------------------------------------------------