summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt18
-rw-r--r--config.h.in9
-rw-r--r--sensei-raw-ctl.c578
3 files changed, 605 insertions, 0 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..e34375c
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,18 @@
+cmake_minimum_required (VERSION 2.8.4)
+project (sensei-raw-ctl C)
+set (project_VERSION "1.0")
+
+find_package (PkgConfig REQUIRED)
+pkg_check_modules (dependencies REQUIRED libusb-1.0)
+include_directories (${dependencies_INCLUDE_DIRS})
+
+configure_file (${CMAKE_CURRENT_SOURCE_DIR}/config.h.in
+ ${CMAKE_CURRENT_BINARY_DIR}/config.h)
+include_directories (${CMAKE_CURRENT_BINARY_DIR})
+
+add_executable (${CMAKE_PROJECT_NAME} ${CMAKE_PROJECT_NAME}.c)
+target_link_libraries (${CMAKE_PROJECT_NAME} ${dependencies_LIBRARIES})
+
+include (GNUInstallDirs)
+install (TARGETS ${CMAKE_PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR})
+
diff --git a/config.h.in b/config.h.in
new file mode 100644
index 0000000..d00c838
--- /dev/null
+++ b/config.h.in
@@ -0,0 +1,9 @@
+#ifndef CONFIG_H
+#define CONFIG_H
+
+#define PROJECT_NAME "${CMAKE_PROJECT_NAME}"
+#define PROJECT_VERSION "${project_VERSION}"
+
+#endif /* ! CONFIG_H */
+
+
diff --git a/sensei-raw-ctl.c b/sensei-raw-ctl.c
new file mode 100644
index 0000000..8e0c18b
--- /dev/null
+++ b/sensei-raw-ctl.c
@@ -0,0 +1,578 @@
+/*
+ * sensei-raw-ctl.c: SteelSeries Sensei Raw control utility
+ *
+ * Everything has been reverse-engineered via Wireshark/usbmon and VirtualBox.
+ * Device configuration has been discovered by accident.
+ *
+ * The code might be a bit long but does very little; most of it is UI.
+ *
+ * Copyright (c) 2013, Přemysl Janouch <p.janouch@gmail.com>
+ * All rights reserved.
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+ * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+ * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ *
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdbool.h>
+#include <assert.h>
+
+#include <getopt.h>
+#include <strings.h>
+#include <libusb.h>
+
+#include "config.h"
+
+// --- Utilities ---------------------------------------------------------------
+
+/** Search for a device with given vendor and product ID. */
+static libusb_device_handle *
+find_device (int vendor, int product, int *error)
+{
+ libusb_device **list;
+ libusb_device *found = NULL;
+ libusb_device_handle *handle = NULL;
+ int err = 0;
+
+ ssize_t cnt = libusb_get_device_list (NULL, &list);
+ if (cnt < 0)
+ goto out;
+
+ ssize_t i = 0;
+ for (i = 0; i < cnt; i++)
+ {
+ libusb_device *device = list[i];
+
+ struct libusb_device_descriptor desc;
+ if (libusb_get_device_descriptor (device, &desc))
+ continue;
+
+ if (desc.idVendor == vendor && desc.idProduct == product)
+ {
+ found = device;
+ break;
+ }
+ }
+
+ if (found)
+ {
+ err = libusb_open (found, &handle);
+ if (err)
+ goto out_free;
+ }
+
+out_free:
+ libusb_free_device_list(list, 1);
+out:
+ if (error != NULL && err != 0)
+ *error = err;
+ return handle;
+}
+
+// --- Device configuration ----------------------------------------------------
+
+#define USB_VENDOR_STEELSERIES 0x1038
+#define USB_PRODUCT_STEELSERIES_SENSEI 0x1369
+
+#define GET_REPORT 0x01
+#define SET_REPORT 0x09
+
+#define SENSEI_CTL_IFACE 0
+
+#define SENSEI_CPI_MIN 0x01
+#define SENSEI_CPI_MAX 0x3f
+#define SENSEI_CPI_STEP 90
+
+/** Backlight pulsation. */
+enum sensei_pulsation
+{
+ PULSATION_STEADY = 1,
+ PULSATION_SLOW,
+ PULSATION_MEDIUM,
+ PULSATION_FAST
+};
+
+/** Device mode. */
+/* Just guessing the names, could be anything */
+enum sensei_mode
+{
+ MODE_COMPATIBILITY = 1,
+ MODE_NORMAL
+};
+
+/** Backlight intensity. */
+enum sensei_intensity
+{
+ INTENSITY_OFF = 1,
+ INTENSITY_LOW,
+ INTENSITY_MEDIUM,
+ INTENSITY_HIGH
+};
+
+/** Polling frequency. */
+enum sensei_polling
+{
+ POLLING_1000_HZ = 1,
+ POLLING_500_HZ,
+ POLLING_250_HZ,
+ POLLING_125_HZ
+};
+
+/** Overall device configuration. */
+struct sensei_config
+{
+ enum sensei_mode mode;
+ int cpi_off;
+ int cpi_on;
+ enum sensei_pulsation pulsation;
+ enum sensei_intensity intensity;
+ enum sensei_polling polling;
+};
+
+/** Send a command to the mouse via SET_REPORT. */
+static int
+sensei_send_command (libusb_device_handle *device,
+ unsigned char *data, uint16_t length)
+{
+ int result = libusb_control_transfer (device, LIBUSB_ENDPOINT_OUT
+ | LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE,
+ SET_REPORT, 0x0200, 0x0000, data, length, 0);
+ return result < 0 ? result : 0;
+}
+
+/** Set the operating mode of the mouse. */
+static int
+sensei_set_mode (libusb_device_handle *device,
+ enum sensei_mode mode)
+{
+ unsigned char cmd[32] = { 0x02, 0x00, mode };
+ return sensei_send_command (device, cmd, sizeof cmd);
+}
+
+/** Set backlight intensity. */
+static int
+sensei_set_intensity (libusb_device_handle *device,
+ enum sensei_intensity intensity)
+{
+ unsigned char cmd[32] = { 0x05, 0x01, intensity };
+ return sensei_send_command (device, cmd, sizeof cmd);
+}
+
+/** Set pulsation speed. */
+static int
+sensei_set_pulsation (libusb_device_handle *device,
+ enum sensei_pulsation pulsation)
+{
+ unsigned char cmd[32] = { 0x07, 0x01, pulsation };
+ return sensei_send_command (device, cmd, sizeof cmd);
+}
+
+/** Set sensitivity in CPI. */
+static int
+sensei_set_cpi (libusb_device_handle *device,
+ int cpi, bool led_status)
+{
+ assert (cpi >= SENSEI_CPI_MIN && cpi <= SENSEI_CPI_MAX);
+ unsigned char cmd[32] = { 0x03, led_status ? 2 : 1, cpi };
+ return sensei_send_command (device, cmd, sizeof cmd);
+}
+
+/** Set the polling frequency. */
+static int
+sensei_set_polling (libusb_device_handle *device,
+ enum sensei_polling polling)
+{
+ unsigned char cmd[32] = { 0x04, 0x00, polling };
+ return sensei_send_command (device, cmd, sizeof cmd);
+}
+
+/** Save the current configuration to ROM. */
+static int
+sensei_save_to_rom (libusb_device_handle *device)
+{
+ unsigned char cmd[32] = { 0x09, 0x00, 0x00 };
+ return sensei_send_command (device, cmd, sizeof cmd);
+}
+
+/** Read device configuration. */
+static int
+sensei_load_config (libusb_device_handle *device,
+ struct sensei_config *config)
+{
+ unsigned char data[256];
+
+ int result = libusb_control_transfer (device, LIBUSB_ENDPOINT_IN
+ | LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE,
+ GET_REPORT, 0x0300, 0x0000, data, sizeof data, 0);
+ if (result < 0)
+ return result;
+
+ config->intensity = data[102];
+ config->pulsation = data[103];
+ config->cpi_off = data[107];
+ config->cpi_on = data[108];
+ config->polling = data[128];
+ return 0;
+}
+
+// --- Control utility ---------------------------------------------------------
+
+static void
+sensei_display_config (const struct sensei_config *config)
+{
+ printf ("Backlight intensity: ");
+ switch (config->intensity)
+ {
+ case INTENSITY_OFF: printf ("off\n"); break;
+ case INTENSITY_LOW: printf ("low\n"); break;
+ case INTENSITY_MEDIUM: printf ("medium\n"); break;
+ case INTENSITY_HIGH: printf ("high\n"); break;
+ default: printf ("unknown\n");
+ }
+
+ printf ("Backlight pulsation: ");
+ switch (config->pulsation)
+ {
+ case PULSATION_STEADY: printf ("steady\n"); break;
+ case PULSATION_SLOW: printf ("slow\n"); break;
+ case PULSATION_MEDIUM: printf ("medium\n"); break;
+ case PULSATION_FAST: printf ("fast\n"); break;
+ default: printf ("unknown\n");
+ }
+
+ printf ("Speed in CPI (LED is off): %d\n", 90 * config->cpi_off);
+ printf ("Speed in CPI (LED is on): %d\n", 90 * config->cpi_on);
+
+ printf ("Polling frequency: ");
+ switch (config->polling)
+ {
+ case POLLING_1000_HZ: printf ("1000Hz\n"); break;
+ case POLLING_500_HZ: printf ("500Hz\n"); break;
+ case POLLING_250_HZ: printf ("250Hz\n"); break;
+ case POLLING_125_HZ: printf ("125Hz\n"); break;
+ default: printf ("unknown\n");
+ }
+}
+
+struct options
+{
+ unsigned show_config : 1;
+ unsigned save_to_rom : 1;
+ unsigned set_pulsation : 1;
+ unsigned set_mode : 1;
+ unsigned set_intensity : 1;
+ unsigned set_polling : 1;
+ unsigned set_cpi_off : 1;
+ unsigned set_cpi_on : 1;
+};
+
+static void
+show_usage (const char *program_name)
+{
+ printf ("Usage: %s [OPTION]... - configure"
+ " SteelSeries Sensei Raw devices\n\n", program_name);
+ printf (" -h, --help Show this help\n");
+ printf (" --version Show program version and exit\n");
+ printf (" --show Show current mouse settings and exit\n");
+ printf (" --mode X Set the mode of the mouse"
+ " (can be either 'compat' or 'normal')\n");
+ printf (" --polling X Set polling to X Hz (1000, 500, 250, 125)\n");
+ printf (" --cpi-on X Set CPI with the LED on to X\n");
+ printf (" --cpi-off X Set CPI with the LED off to X\n");
+ printf (" --pulsation X Set the pulsation mode"
+ " (steady, slow, medium, fast)\n");
+ printf (" --intensity X Set the backlight intensity"
+ " (off, low, medium, high)\n");
+ printf (" --save Save the current configuration to ROM\n");
+ printf ("\n");
+}
+
+static int
+encode_cpi (const char *str)
+{
+ char *end;
+ long cpi = strtol (str, &end, 10);
+ if (!*str || *end || cpi < 0)
+ {
+ fprintf (stderr, "Error: invalid CPI value\n");
+ exit (EXIT_FAILURE);
+ }
+
+ cpi /= SENSEI_CPI_STEP;
+ if (cpi < 1)
+ {
+ fprintf (stderr, "Notice: CPI too low, using %d\n",
+ SENSEI_CPI_MIN * SENSEI_CPI_STEP);
+ cpi = 1;
+ }
+ if (cpi > SENSEI_CPI_MAX)
+ {
+ fprintf (stderr, "Notice: CPI too high, using %d\n",
+ SENSEI_CPI_MAX * SENSEI_CPI_STEP);
+ cpi = SENSEI_CPI_MAX;
+ }
+ return cpi;
+}
+
+static void
+parse_options (int argc, char *argv[],
+ struct options *options, struct sensei_config *new_config)
+{
+ static struct option long_opts[] =
+ {
+ { "help", no_argument, 0, 'h' },
+ { "version", no_argument, 0, 'V' },
+ { "show", no_argument, 0, 's' },
+ { "save", no_argument, 0, 'S' },
+ { "mode", required_argument, 0, 'm' },
+ { "polling", required_argument, 0, 'p' },
+ { "cpi-on", required_argument, 0, 'c' },
+ { "cpi-off", required_argument, 0, 'C' },
+ { "pulsation", required_argument, 0, 'P' },
+ { "intensity", required_argument, 0, 'i' },
+ { 0, 0, 0, 0 }
+ };
+
+ if (argc == 1)
+ {
+ show_usage (argv[0]);
+ exit (EXIT_FAILURE);
+ }
+
+ int c;
+ while ((c = getopt_long (argc, argv, "h", long_opts, NULL)) != -1)
+ {
+ switch (c)
+ {
+ case 'h':
+ show_usage (argv[0]);
+ exit (EXIT_SUCCESS);
+ case 'V':
+ printf (PROJECT_NAME " " PROJECT_VERSION "\n");
+ exit (EXIT_SUCCESS);
+ case 's':
+ options->show_config = true;
+ break;
+ case 'S':
+ options->save_to_rom = true;
+ break;
+ case 'm':
+ if (!strcasecmp (optarg, "compat"))
+ new_config->mode = MODE_COMPATIBILITY;
+ else if (!strcasecmp (optarg, "normal"))
+ new_config->mode = MODE_NORMAL;
+ else
+ {
+ fprintf (stderr, "Error: invalid mode: %s\n", optarg);
+ exit (EXIT_FAILURE);
+ }
+ options->set_mode = true;
+ break;
+ case 'p':
+ if (!strcmp (optarg, "1000"))
+ new_config->polling = POLLING_1000_HZ;
+ else if (!strcmp (optarg, "500"))
+ new_config->polling = POLLING_500_HZ;
+ else if (!strcmp (optarg, "250"))
+ new_config->polling = POLLING_250_HZ;
+ else if (!strcmp (optarg, "125"))
+ new_config->polling = POLLING_125_HZ;
+ else
+ {
+ fprintf (stderr, "Error: invalid polling frequency: %s\n", optarg);
+ exit (EXIT_FAILURE);
+ }
+ options->set_polling = true;
+ break;
+ case 'c':
+ new_config->cpi_on = encode_cpi (optarg);
+ options->set_cpi_on = true;
+ break;
+ case 'C':
+ new_config->cpi_off = encode_cpi (optarg);
+ options->set_cpi_off = true;
+ break;
+ case 'P':
+ if (!strcasecmp (optarg, "steady"))
+ new_config->pulsation = PULSATION_STEADY;
+ else if (!strcasecmp (optarg, "slow"))
+ new_config->pulsation = PULSATION_SLOW;
+ else if (!strcasecmp (optarg, "medium"))
+ new_config->pulsation = PULSATION_MEDIUM;
+ else if (!strcasecmp (optarg, "fast"))
+ new_config->pulsation = PULSATION_FAST;
+ else
+ {
+ fprintf (stderr, "Error: invalid backlight pulsation: %s\n", optarg);
+ exit (EXIT_FAILURE);
+ }
+ options->set_pulsation = true;
+ break;
+ case 'i':
+ if (!strcasecmp (optarg, "off"))
+ new_config->intensity = INTENSITY_OFF;
+ else if (!strcasecmp (optarg, "low"))
+ new_config->intensity = INTENSITY_LOW;
+ else if (!strcasecmp (optarg, "medium"))
+ new_config->intensity = INTENSITY_MEDIUM;
+ else if (!strcasecmp (optarg, "high"))
+ new_config->intensity = INTENSITY_HIGH;
+ else
+ {
+ fprintf (stderr, "Error: invalid backlight intensity: %s\n", optarg);
+ exit (EXIT_FAILURE);
+ }
+ options->set_intensity = true;
+ break;
+ case '?':
+ exit (EXIT_FAILURE);
+ }
+ }
+
+ if (optind < argc)
+ {
+ fprintf (stderr, "Error: extra parameters\n");
+ exit (EXIT_FAILURE);
+ }
+}
+
+static int
+apply_options (libusb_device_handle *device,
+ struct options *options, struct sensei_config *new_config)
+{
+ int result;
+ if (options->show_config)
+ {
+ struct sensei_config config;
+ if ((result = sensei_load_config (device, &config)))
+ return result;
+ sensei_display_config (&config);
+ return 0;
+ }
+
+ if (options->set_mode)
+ if ((result = sensei_set_mode (device, new_config->mode)))
+ return result;
+ if (options->set_polling)
+ if ((result = sensei_set_polling (device, new_config->polling)))
+ return result;
+ if (options->set_intensity)
+ if ((result = sensei_set_intensity (device, new_config->intensity)))
+ return result;
+ if (options->set_pulsation)
+ if ((result = sensei_set_pulsation (device, new_config->pulsation)))
+ return result;
+
+ if (options->set_cpi_off)
+ if ((result = sensei_set_cpi (device, new_config->cpi_off, false)))
+ return result;
+ if (options->set_cpi_on)
+ if ((result = sensei_set_cpi (device, new_config->cpi_on, true)))
+ return result;
+
+ if (options->save_to_rom)
+ if ((result = sensei_save_to_rom (device)))
+ return result;
+
+ return 0;
+}
+
+#define ERROR(label, ...) \
+ do { \
+ fprintf (stderr, "Error: " __VA_ARGS__); \
+ status = 1; \
+ goto label; \
+ } while (0)
+
+int
+main (int argc, char *argv[])
+{
+ struct options options = { 0 };
+ struct sensei_config new_config = { 0 };
+
+ parse_options (argc, argv, &options, &new_config);
+
+ int result, status = 0;
+
+ result = libusb_init (NULL);
+ if (result)
+ ERROR (error_0, "libusb initialisation failed: %s\n",
+ libusb_error_name (result));
+
+ result = 0;
+ libusb_device_handle *device = find_device
+ (USB_VENDOR_STEELSERIES, USB_PRODUCT_STEELSERIES_SENSEI, &result);
+ if (!device)
+ {
+ if (result)
+ ERROR (error_1, "couldn't open device: %s\n",
+ libusb_error_name (result));
+ else
+ ERROR (error_1, "no suitable device found\n");
+ }
+
+ bool reattach_driver = false;
+
+ result = libusb_kernel_driver_active (device, SENSEI_CTL_IFACE);
+ switch (result)
+ {
+ case 0:
+ case LIBUSB_ERROR_NOT_SUPPORTED:
+ break;
+ case 1:
+ reattach_driver = true;
+ result = libusb_detach_kernel_driver (device, SENSEI_CTL_IFACE);
+ if (result)
+ ERROR (error_2, "couldn't detach kernel driver: %s\n",
+ libusb_error_name (result));
+ break;
+ default:
+ ERROR (error_2, "coudn't detect kernel driver presence: %s\n",
+ libusb_error_name (result));
+ }
+
+ result = libusb_claim_interface (device, SENSEI_CTL_IFACE);
+ if (result)
+ ERROR (error_3, "couldn't claim interface: %s\n",
+ libusb_error_name (result));
+
+ result = apply_options (device, &options, &new_config);
+ if (result)
+ ERROR (error_4, "operation failed: %s\n",
+ libusb_error_name (result));
+
+error_4:
+ result = libusb_release_interface (device, SENSEI_CTL_IFACE);
+ if (result)
+ ERROR (error_3, "couldn't release interface: %s\n",
+ libusb_error_name (result));
+
+error_3:
+ if (reattach_driver)
+ {
+ result = libusb_attach_kernel_driver (device, SENSEI_CTL_IFACE);
+ if (result)
+ ERROR (error_2, "couldn't reattach kernel driver: %s\n",
+ libusb_error_name (result));
+ }
+
+error_2:
+ libusb_close (device);
+error_1:
+ libusb_exit (NULL);
+error_0:
+ return status;
+}
+