diff options
-rw-r--r-- | CMakeLists.txt | 30 | ||||
-rw-r--r-- | LICENSE | 2 | ||||
-rw-r--r-- | README.adoc | 16 | ||||
-rw-r--r-- | razer-bw-te-ctl.c | 427 |
4 files changed, 472 insertions, 3 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 7ddb1cb..9ce894d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,8 +40,32 @@ if (WITH_LIBUSB) target_include_directories (elksmart-comm PUBLIC ${libusb_INCLUDE_DIRS}) target_link_directories (elksmart-comm PUBLIC ${libusb_LIBRARY_DIRS}) target_link_libraries (elksmart-comm ${libusb_LIBRARIES}) + + list (APPEND targets razer-bw-te-ctl) + add_executable (razer-bw-te-ctl razer-bw-te-ctl.c) + target_include_directories (razer-bw-te-ctl PUBLIC ${libusb_INCLUDE_DIRS}) + target_link_directories (razer-bw-te-ctl PUBLIC ${libusb_LIBRARY_DIRS}) + target_link_libraries (razer-bw-te-ctl ${libusb_LIBRARIES}) +endif () + +# Generate documentation from help output +find_program (HELP2MAN_EXECUTABLE help2man) +if (NOT HELP2MAN_EXECUTABLE) + message (FATAL_ERROR "help2man not found") endif () +foreach (target ${targets}) + set (page_output "${PROJECT_BINARY_DIR}/${target}.1") + list (APPEND project_MAN_PAGES "${page_output}") + add_custom_command (OUTPUT ${page_output} + COMMAND ${HELP2MAN_EXECUTABLE} -N + "${PROJECT_BINARY_DIR}/${target}" -o ${page_output} + DEPENDS ${target} + COMMENT "Generating man page for ${target}" VERBATIM) +endforeach () + +add_custom_target (docs ALL DEPENDS ${project_MAN_PAGES}) + # The files to be installed include (GNUInstallDirs) @@ -55,6 +79,12 @@ install (TARGETS ${targets} DESTINATION ${CMAKE_INSTALL_BINDIR} SETUID) install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR}) +foreach (page ${project_MAN_PAGES}) + string (REGEX MATCH "\\.([0-9])$" manpage_suffix "${page}") + install (FILES "${page}" + DESTINATION "${CMAKE_INSTALL_MANDIR}/man${CMAKE_MATCH_1}") +endforeach () + # CPack set (CPACK_PACKAGE_VENDOR "Premysl Eric Janouch") set (CPACK_PACKAGE_CONTACT "Přemysl Eric Janouch <p@janouch.name>") @@ -1,4 +1,4 @@ -Copyright (c) 2024, Přemysl Eric Janouch <p@janouch.name> +Copyright (c) 2013, 2024, Přemysl Eric Janouch <p@janouch.name> Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. diff --git a/README.adoc b/README.adoc index bb27c27..b715932 100644 --- a/README.adoc +++ b/README.adoc @@ -15,14 +15,26 @@ and may not run at the same time, as it would contend for device access. partially reimplementing the Ocrustar mobile app. While it does not build for Windows, this is incidental. + - _razer-bw-te-ctl_ makes it possible to change the configuration of your Razer + BlackWidow Tournament Edition keybooard. ++ +Make sure to let the Windows Razer Synapse tool upgrade the firmware to the +newest version before running the program. There might be some issues otherwise +due to protocol changes, although I don't really deem it very probable. + Packages -------- Regular releases are sporadic. git master should be stable enough. +You can get a package with the latest development version +as a https://git.janouch.name/p/nixexprs[Nix derivation]. + Building -------- -Build dependencies: CMake, pkg-config, liberty (included) + -Runtime dependencies: libusb-1.0 (elksmart-comm), hidapi (eizoctl) +Build dependencies: + CMake, pkg-config, liberty (included), help2man + +Runtime dependencies: + libusb-1.0 (elksmart-comm, razer-bw-te-ctl), hidapi (eizoctl) $ git clone --recursive https://git.janouch.name/p/usb-drivers.git $ mkdir desktop-tools/build diff --git a/razer-bw-te-ctl.c b/razer-bw-te-ctl.c new file mode 100644 index 0000000..3081d93 --- /dev/null +++ b/razer-bw-te-ctl.c @@ -0,0 +1,427 @@ +/* + * razer-bw-te-ctl.c: Razer BlackWidow Tournament Edition control utility + * + * Everything has been reverse-engineered via Wireshark/usbmon and VirtualBox. + * + * Copyright (c) 2013, Přemysl Eric Janouch <p@janouch.name> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted. + * + * 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" +#undef PROGRAM_NAME +#define PROGRAM_NAME "razer-bw-te-ctl" + +// --- 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_RAZER 0x1532 +#define USB_PRODUCT_RAZER_BW_TE 0x011c + +#define USB_GET_REPORT 0x01 +#define USB_SET_REPORT 0x09 + +#define BW_CTL_IFACE 0 + +/** Razer logo backlight mode. */ +enum bw_led_mode +{ + LED_BRIGHTNESS = 0, + LED_BLINK, + LED_PULSATE +}; + +/** Overall device configuration. */ +struct bw_config +{ + enum bw_led_mode led_mode; + unsigned led_brightness; + unsigned macro_led_on : 1; + unsigned macro_led_blinking : 1; + unsigned gaming_mode : 1; +}; + +/** Send a command to the mouse via SET_REPORT. */ +static int +bw_send_command (libusb_device_handle *device, + unsigned char *data, uint16_t length) +{ + unsigned char packet[90] = { 0x00 }; + assert (length <= sizeof packet - 5); + memcpy (packet + 5, data, length); + + unsigned char checksum = 0; + while (length--) + checksum ^= data[length]; + packet[sizeof packet - 2] = checksum; + + // XXX wIndex should actually be 0x0002 but that doesn't work + int result = libusb_control_transfer (device, LIBUSB_ENDPOINT_OUT + | LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE, + USB_SET_REPORT, 0x0300, 0x0000, packet, sizeof packet, 0); + return result < 0 ? result : 0; +} + +/** Set Razer logo backlight mode. */ +static int +bw_set_led_mode (libusb_device_handle *device, enum bw_led_mode mode) +{ + unsigned char cmd[] = { 0x03, 0x03, 0x02, 0x01, 0x04, mode }; + return bw_send_command (device, cmd, sizeof cmd); +} + +/** Set Razer logo backlight brightness. */ +static int +bw_set_led_brightness (libusb_device_handle *device, unsigned char brightness) +{ + unsigned char cmd[] = { 0x03, 0x03, 0x03, 0x01, 0x04, brightness }; + return bw_send_command (device, cmd, sizeof cmd); +} + +/** Set the on/off state of the macro LED. */ +static int +bw_set_macro_led (libusb_device_handle *device, bool on) +{ + unsigned char cmd[] = { 0x03, 0x03, 0x00, 0x00, 0x07, on }; + return bw_send_command (device, cmd, sizeof cmd); +} + +/** Set whether the macro LED should blink. */ +static int +bw_set_macro_led_blinking (libusb_device_handle *device, bool blinking) +{ + unsigned char cmd[] = { 0x03, 0x03, 0x02, 0x00, 0x07, blinking }; + return bw_send_command (device, cmd, sizeof cmd); +} + +/** Set the gaming mode (whether the Windows key should be ignored). */ +static int +bw_set_gaming_mode (libusb_device_handle *device, bool on) +{ + unsigned char cmd[] = { 0x03, 0x03, 0x00, 0x01, 0x08, on }; + return bw_send_command (device, cmd, sizeof cmd); +} + +// --- Control utility --------------------------------------------------------- + +struct options +{ + unsigned set_led_mode : 1; + unsigned set_led_brightness : 1; + unsigned set_macro_led_on : 1; + unsigned set_macro_led_blinking : 1; + unsigned set_gaming_mode : 1; +}; + +static void +show_usage (const char *program_name) +{ + printf ("Usage: %s [OPTION]...\n", program_name); + printf ("Configure Razer BlackWidow Tournament Edition devices.\n\n"); + printf (" -h, --help Show this help\n"); + printf (" --version Show program version and exit\n"); + printf (" --led-mode X Set the mode of the Razer logo LED" + " (can be 'normal', 'blink' or 'pulsate')\n"); + printf (" --led-brightness X Set Razer logo LED brightness" + " (from 0 to 255)\n"); + printf (" --macro-led X Set the macro LED mode" + " ('off', 'on' or 'blink')\n"); + printf (" --gaming-mode BOOL Set whether the Windows key is ignored\n"); + printf ("\n"); +} + +static void +parse_options (int argc, char *argv[], + struct options *options, struct bw_config *new_config) +{ + static struct option long_opts[] = + { + { "help", no_argument, 0, 'h' }, + { "version", no_argument, 0, 'V' }, + { "led-mode", required_argument, 0, 'l' }, + { "led-brightness", required_argument, 0, 'L' }, + { "macro-led", required_argument, 0, 'm' }, + { "gaming-mode", required_argument, 0, 'g' }, + { 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 (PROGRAM_NAME " " PROGRAM_VERSION "\n"); + exit (EXIT_SUCCESS); + case 'l': + if (!strcasecmp (optarg, "normal")) + new_config->led_mode = LED_BRIGHTNESS; + else if (!strcasecmp (optarg, "blink")) + new_config->led_mode = LED_BLINK; + else if (!strcasecmp (optarg, "pulsate")) + new_config->led_mode = LED_PULSATE; + else + { + fprintf (stderr, "Error: invalid LED mode: %s\n", optarg); + exit (EXIT_FAILURE); + } + options->set_led_mode = true; + break; + case 'L': + { + char *end; + long bri = strtol (optarg, &end, 10); + if (!*optarg || *end || bri < 0 || bri > 255) + { + fprintf (stderr, "Error: invalid LED brightness value\n"); + exit (EXIT_FAILURE); + } + options->set_led_brightness = true; + break; + } + case 'm': + if (!strcasecmp (optarg, "off")) + { + new_config->macro_led_on = false; + new_config->macro_led_blinking = false; + } + else if (!strcasecmp (optarg, "blink")) + { + new_config->macro_led_on = true; + new_config->macro_led_blinking = true; + } + else if (!strcasecmp (optarg, "on")) + { + new_config->macro_led_on = true; + new_config->macro_led_blinking = false; + } + else + { + fprintf (stderr, "Error: invalid macro LED mode: %s\n", optarg); + exit (EXIT_FAILURE); + } + options->set_macro_led_blinking = true; + options->set_macro_led_on = true; + break; + case 'g': + if (!strcasecmp (optarg, "true") + || !strcasecmp (optarg, "on") + || !strcasecmp (optarg, "yes")) + new_config->gaming_mode = true; + else if (!strcasecmp (optarg, "false") + || !strcasecmp (optarg, "off") + || !strcasecmp (optarg, "no")) + new_config->gaming_mode = false; + else + { + fprintf (stderr, "Error: invalid gaming mode" + " setting: %s\n", optarg); + exit (EXIT_FAILURE); + } + options->set_gaming_mode = 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 bw_config *new_config) +{ + int result; + + if (options->set_led_mode) + if ((result = bw_set_led_mode + (device, new_config->led_mode))) + return result; + if (options->set_led_brightness) + if ((result = bw_set_led_brightness + (device, new_config->led_brightness))) + return result; + + if (options->set_macro_led_on) + if ((result = bw_set_macro_led + (device, new_config->macro_led_on))) + return result; + if (options->set_macro_led_blinking) + if ((result = bw_set_macro_led_blinking + (device, new_config->macro_led_blinking))) + return result; + + if (options->set_gaming_mode) + if ((result = bw_set_gaming_mode + (device, new_config->gaming_mode))) + 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 bw_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_RAZER, USB_PRODUCT_RAZER_BW_TE, &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, BW_CTL_IFACE); + switch (result) + { + case 0: + case LIBUSB_ERROR_NOT_SUPPORTED: + break; + case 1: + reattach_driver = true; + result = libusb_detach_kernel_driver (device, BW_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, BW_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, BW_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, BW_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; +} + |