diff --git a/Controllers/SteelSeriesController/RGBController_SteelSeriesApex3TKL.cpp b/Controllers/SteelSeriesController/RGBController_SteelSeriesApex3TKL.cpp new file mode 100644 index 00000000..85c4829a --- /dev/null +++ b/Controllers/SteelSeriesController/RGBController_SteelSeriesApex3TKL.cpp @@ -0,0 +1,127 @@ +/*-----------------------------------------*\ +| RGBController_SteelSeriesApex3TKL.cpp | +| | +| Paul K. Gerke - 27.10.2022 | +| Dr_no (Chris M) | +\*-----------------------------------------*/ + +#include "RGBController_SteelSeriesApex3TKL.h" + +/**------------------------------------------------------------------*\ + @name Steel Series Apex 3 TKL Keyboard + @category Keyboard + @type USB + @save :x: + @direct :white_check_mark: + @effects :white_check_mark: + @detectors DetectSteelSeriesApex3TKL + @comment +\*-------------------------------------------------------------------*/ + +RGBController_SteelSeriesApex3TKL::RGBController_SteelSeriesApex3TKL(SteelSeriesApex3TKLController* controller_ptr) +{ + controller = controller_ptr; + + name = "Apex 3 TKL"; + vendor = "SteelSeries"; + type = DEVICE_TYPE_KEYBOARD; + description = "SteelSeries Apex 3 TKL device"; + location = controller->GetDeviceLocation(); + serial = controller->GetSerialString(); + current_brightness = controller->GetBrightness(); + + mode direct; + direct.name = "Direct"; + direct.value = static_cast(APEX3_TKL_MODES::DIRECT); + direct.flags = MODE_FLAG_HAS_PER_LED_COLOR | MODE_FLAG_HAS_BRIGHTNESS; + direct.color_mode = MODE_COLORS_PER_LED; + direct.brightness_min = STEELSERIES_APEX3TKL_BRIGHTNESS_MIN; + direct.brightness_max = STEELSERIES_APEX3TKL_BRIGHTNESS_MAX; + direct.brightness = current_brightness; + modes.push_back(direct); + + mode rainbow; + rainbow.name = "Rainbow Wave"; + rainbow.value = static_cast(APEX3_TKL_MODES::RAINBOW); + rainbow.flags = MODE_FLAG_HAS_BRIGHTNESS; + rainbow.color_mode = MODE_COLORS_NONE; + rainbow.brightness_min = STEELSERIES_APEX3TKL_BRIGHTNESS_MIN; + rainbow.brightness_max = STEELSERIES_APEX3TKL_BRIGHTNESS_MAX; + rainbow.brightness = current_brightness; + modes.push_back(rainbow); + + SetupZones(); +} + +RGBController_SteelSeriesApex3TKL::~RGBController_SteelSeriesApex3TKL() +{ + delete controller; +} + + +void RGBController_SteelSeriesApex3TKL::SetupZones() +{ + zone curr_zone; + curr_zone.name = "Keyboard"; + curr_zone.type = ZONE_TYPE_LINEAR; + curr_zone.leds_min = 8; + curr_zone.leds_max = 8; + curr_zone.leds_count = 8; + curr_zone.matrix_map = NULL; + zones.push_back(curr_zone); + + for(unsigned int i = 0; i < curr_zone.leds_count; i++) + { + led zone_led; + zone_led.name = "LED " + std::to_string(i); + leds.push_back(zone_led); + } + + SetupColors(); +} + +void RGBController_SteelSeriesApex3TKL::ResizeZone(int /*zone*/, int /*new_size*/) +{ + /*---------------------------------------------------------*\ + | This device does not support resizing zones | + \*---------------------------------------------------------*/ +} + +void RGBController_SteelSeriesApex3TKL::DeviceUpdateLEDs() +{ + controller->SetColor(colors); + UpdateBrightness(); +} + +void RGBController_SteelSeriesApex3TKL::UpdateZoneLEDs(int /*zone*/) +{ + DeviceUpdateLEDs(); +} + +void RGBController_SteelSeriesApex3TKL::UpdateSingleLED(int /*led*/) +{ + DeviceUpdateLEDs(); +} + +void RGBController_SteelSeriesApex3TKL::DeviceUpdateMode() +{ + switch (static_cast(active_mode)) { + case APEX3_TKL_MODES::DIRECT: + DeviceUpdateLEDs(); + break; + case APEX3_TKL_MODES::RAINBOW: + controller->SetRainbowMode(); + break; + } + UpdateBrightness(); +} + +void RGBController_SteelSeriesApex3TKL::UpdateBrightness() +{ + // Taken from Aerox3 Controller to prevent OpenRGB from automatically + // overriding user-set brightness values via Mod+F11/F12 + if (current_brightness != modes[active_mode].brightness) { + controller->SetBrightness(modes[active_mode].brightness); + current_brightness = modes[active_mode].brightness; + } +} diff --git a/Controllers/SteelSeriesController/RGBController_SteelSeriesApex3TKL.h b/Controllers/SteelSeriesController/RGBController_SteelSeriesApex3TKL.h new file mode 100644 index 00000000..5167dd09 --- /dev/null +++ b/Controllers/SteelSeriesController/RGBController_SteelSeriesApex3TKL.h @@ -0,0 +1,38 @@ +/*-----------------------------------------*\ +| RGBController_SteelSeriesApex3TKL.h | +| | +| Paul K. Gerke - 27.10.2022 | +| Dr_no (Chris M) | +\*-----------------------------------------*/ + +#pragma once + +#include "RGBController.h" +#include "SteelSeriesApex3TKLController.h" + +enum class APEX3_TKL_MODES +{ + DIRECT = 0, + RAINBOW = 1 +}; + +class RGBController_SteelSeriesApex3TKL : public RGBController +{ +public: + RGBController_SteelSeriesApex3TKL(SteelSeriesApex3TKLController* apex_tzone_ptr); + ~RGBController_SteelSeriesApex3TKL(); + + void SetupZones(); + void ResizeZone(int zone, int new_size); + + void DeviceUpdateLEDs(); + void UpdateZoneLEDs(int zone); + void UpdateSingleLED(int led); + + void DeviceUpdateMode(); +private: + SteelSeriesApex3TKLController* controller; + unsigned int current_brightness; + + void UpdateBrightness(); +}; diff --git a/Controllers/SteelSeriesController/SteelSeriesApex3TKLController.cpp b/Controllers/SteelSeriesController/SteelSeriesApex3TKLController.cpp new file mode 100644 index 00000000..067d0a17 --- /dev/null +++ b/Controllers/SteelSeriesController/SteelSeriesApex3TKLController.cpp @@ -0,0 +1,123 @@ +/*-----------------------------------------*\ +| SteelSeriesApex3TKLController.cpp | +| | +| Paul K. Gerke - 27.10.2022 | +| Dr_no (Chris M) | +\*-----------------------------------------*/ + +#include "SteelSeriesApex3TKLController.h" +#include +#include + +SteelSeriesApex3TKLController::SteelSeriesApex3TKLController + ( + hid_device* dev_handle, + steelseries_type proto_type, + const char* path, + const char* device_name + ) +{ + dev = dev_handle; + location = path; + proto = proto_type; + strcpy(this->device_name, device_name); +} + +SteelSeriesApex3TKLController::~SteelSeriesApex3TKLController() +{ + hid_close(dev); +} + +std::string SteelSeriesApex3TKLController::GetDeviceLocation() +{ + return("HID: " + location); +} + +char* SteelSeriesApex3TKLController::GetDeviceName() +{ + return device_name; +} + +std::string SteelSeriesApex3TKLController::GetSerialString() +{ + wchar_t serial_string[128]; + int ret = hid_get_serial_number_string(dev, serial_string, 128); + + if (ret != 0) + { + return(""); + } + + std::wstring return_wstring = serial_string; + std::string return_string(return_wstring.begin(), return_wstring.end()); + + return(return_string); +} + +steelseries_type SteelSeriesApex3TKLController::GetKeyboardType() +{ + return proto; +} + +void SteelSeriesApex3TKLController::SetBrightness(unsigned char brightness) +{ + unsigned char buf[STEELSERIES_APEX3TKL_WRITE_PACKET_SIZE]; + memset(buf, 0x00, STEELSERIES_APEX3TKL_WRITE_PACKET_SIZE); + + buf[0x01] = 0x23; + buf[0x02] = brightness; + hid_write(dev, buf, STEELSERIES_APEX3TKL_WRITE_PACKET_SIZE); +} + +unsigned char SteelSeriesApex3TKLController::GetBrightness() +{ + unsigned char buf[STEELSERIES_APEX3TKL_WRITE_PACKET_SIZE]; + memset(buf, 0x00, STEELSERIES_APEX3TKL_WRITE_PACKET_SIZE); + + buf[0x01] = 0xA3; + hid_write(dev, buf, STEELSERIES_APEX3TKL_WRITE_PACKET_SIZE); + + int read_back = hid_read_timeout(dev, buf, STEELSERIES_APEX3TKL_WRITE_PACKET_SIZE, 100); + if (read_back >= 2 && buf[0x00] == 0xA3) + { + return(buf[0x01]); + } + LOG_ERROR("[%s] Failed load brightness; defaulting to max", device_name); + return(STEELSERIES_APEX3TKL_BRIGHTNESS_MAX); +} + +void SteelSeriesApex3TKLController::SetRainbowMode() +{ + unsigned char buf[STEELSERIES_APEX3TKL_WRITE_PACKET_SIZE]; + + /*-----------------------------------------------------*\ + | Zero out buffer, set up packet and send | + \*-----------------------------------------------------*/ + memset(buf, 0x00, STEELSERIES_APEX3TKL_WRITE_PACKET_SIZE); + buf[0] = 0x00; + buf[1] = 0x22; + buf[2] = 0xFF; + buf[3] = 0x00; + buf[4] = 0x00; + hid_write(dev, buf, STEELSERIES_APEX3TKL_WRITE_PACKET_SIZE); +} + +void SteelSeriesApex3TKLController::SetColor(std::vector colors) +{ + unsigned char buf[STEELSERIES_APEX3TKL_WRITE_PACKET_SIZE]; + memset(buf, 0x00, STEELSERIES_APEX3TKL_WRITE_PACKET_SIZE); + + buf[0x01] = 0x21; + buf[0x02] = 0xFF; + + for(unsigned int i = 0; i < colors.size(); i++) + { + uint8_t index = i * 3; + + buf[index + 3] = RGBGetRValue(colors[i]);; + buf[index + 4] = RGBGetGValue(colors[i]);; + buf[index + 5] = RGBGetBValue(colors[i]);; + } + + hid_write(dev, buf, STEELSERIES_APEX3TKL_WRITE_PACKET_SIZE); +} diff --git a/Controllers/SteelSeriesController/SteelSeriesApex3TKLController.h b/Controllers/SteelSeriesController/SteelSeriesApex3TKLController.h new file mode 100644 index 00000000..8d6b9196 --- /dev/null +++ b/Controllers/SteelSeriesController/SteelSeriesApex3TKLController.h @@ -0,0 +1,152 @@ +/*-----------------------------------------*\ +| SteelSeriesApex3TKLController.h | +| | +| Paul K. Gerke - 27.10.2022 | +| Dr_no (Chris M) | +| | +| Paul K: I attached my notes on reverse | +| engineering the protocol to the end of | +| this file | +\*-----------------------------------------*/ + +#include +#include + +#include "SteelSeriesGeneric.h" +#include "SteelSeriesApexBaseController.h" + +#pragma once + +#define STEELSERIES_APEX3TKL_WRITE_PACKET_SIZE 65 +#define STEELSERIES_APEX3TKL_BRIGHTNESS_MIN 0x00 +#define STEELSERIES_APEX3TKL_BRIGHTNESS_MAX 0x10 + +class SteelSeriesApex3TKLController +{ +public: + SteelSeriesApex3TKLController + ( + hid_device* dev_handle, + steelseries_type proto_type, + const char* path, + const char* device_name + ); + + ~SteelSeriesApex3TKLController(); + + std::string GetDeviceLocation(); + char* GetDeviceName(); + std::string GetSerialString(); + steelseries_type GetKeyboardType(); + + unsigned char GetBrightness(); + void SetBrightness(unsigned char brightness); + void SetColor(std::vector colors); + void SetRainbowMode(); +private: + char device_name[32]; + hid_device* dev; + std::string location; + steelseries_type proto; +}; + +/* + +# Reverse engineering protocol of APEX 3 TKL + +Paul K. Gerke - 2022-10-27 + +I used a Windows 10 VM on my Linux host and virtually attached my +SteelSeries Apex3 TKL keyboard to the Windows VM using VirtualBox. On Windows, +I used Wireshark 4.x to listen to the USB-HID traffic between the SteelSeries +keyboard and the SteelSeries GG Software version 26.0.0. + +## General keyboard behavior overview + +- The keyboard does not seem to have persistent memory: Cycling the usb + connection resets to keyboard to a "rainbow" pattern mode. SteelSeries GG + has to be running to set color patterns, indicating that the PC software does + most of the heavy lifting. +- The keyboard has 8 colorzones from left to right. Similar to the ApexTZone + controller, but that controller did not work for me. +- The keyboard has a "rainbow" pattern mode and "constant color" settings for + each color zone. +- The brightness settings of the keyboard can somehow be read back by the + SteelSeries GG Software from the keyboard. Changing the brightness with + Mod+F11/F12 changes the slider in the SteelSeries GG software. Also, when + changing the brightness and opening the configuration dialog, the slider + is updated to the value set via Mod+F11/F12. I found some messages + relating to this, but could not identify the full mechanism how to query this + data. + +## Message protocol + +The HID-Messages contain a data-packet that is sent with HID-Requests to the +keyboard to change the keyboard's mode. The first byte seems to be a command-ID. +The full message-length is 64 byes (or 65 for the OpenRGB controller which +uses the first byte as deviceId selector). + +Values not explicitly mentioned below are zero-bytes. + +Command IDs: + +- 0x21: Set color of the 8 color regions. Message format: + + 0x21 0xFF R1 G1 B1 R2 G2 B2 ... [up to rgb values for zone 8] + + The 0xFF value seems to be constant - purpose unknown. + + The next sequence of bytes consist of 8 x 3 byte-RGB data for each of the + three regions. Value range from 0 up to 255 for each color value (as you + might expect). + +- 0x23: Set overall brightness of all color channels. Seems to act as a factor + for the individual RGB values, but can be individually adjusted by the user + through the Mod+F11/F12 key combinations - making it interesting to maybe + decouple overall-brightness from the overall color settings. Message format: + + 0x23 [brightness] + + Valid brightness values are in the range from 0x00 (dark) to 0x10 (full + brightness); + +- 0x22: Set the keyboard to "rainbow animation" mode. Message + + Message format: + + 0x22 0xFF + + As for 0x21, 0xFF seems to be constant, purpose unknown. + +Other observed commands/messages of unknown purpose were: + +- 0x6c 0x00 0x01 0x01 + - I am half sure that this triggers some sort of URB_INTERRUPT from USB + device 1.2.2 reporting back some data. No idea what data though. It seems + to be pretty 0x6c followed by only zeroes. +- 0xa3 !!! Query brightness??? +- 0x90 + - I guess "get firmware version" or similar... or some other settings. Again + a complex answer is echoed from device 1.2.2 starting with 0x90 and a + few bytes (~8-10) of data. No idea what this is. + +## Reading back the set brightness from the keyboard + +I was able to _which_ messages contain the information, but not how to get them. +On windows, when adjusting the overall brightness using the Mod+F11/F12 key +combination or sometimes when using the SteelSeries GG software, the keyboard +sent a URB_INTERRUPT from the device labeled as "1.2.5" with HID-data that was +formatted as follows: + +0x01 [brightness] + +0x01 seems to be the data-type(?) and then brightness is a value in the range +from 0x00 (dark) to 0x10 (full brightness). + +However, I was not able to capture these interrupts on my Linux machine when +pressing the brightness-buttons. + +Also I was not able to find a message that tells the keyboard to report this +data to the PC. Somehow it only works on Windows. To be continued...? + +*/ diff --git a/Controllers/SteelSeriesController/SteelSeriesControllerDetect.cpp b/Controllers/SteelSeriesController/SteelSeriesControllerDetect.cpp index 59ba990c..3d5ef41b 100644 --- a/Controllers/SteelSeriesController/SteelSeriesControllerDetect.cpp +++ b/Controllers/SteelSeriesController/SteelSeriesControllerDetect.cpp @@ -5,6 +5,7 @@ #include "SteelSeriesQCKMatController.h" #include "SteelSeriesApexController.h" #include "SteelSeriesApexTZoneController.h" +#include "SteelSeriesApex3TKLController.h" #include "SteelSeriesOldApexController.h" #include "SteelSeriesApexMController.h" #include "SteelSeriesSenseiController.h" @@ -18,6 +19,7 @@ #include "RGBController_SteelSeriesQCKMat.h" #include "RGBController_SteelSeriesApex.h" #include "RGBController_SteelSeriesApexTZone.h" +#include "RGBController_SteelSeriesApex3TKL.h" #include "RGBController_SteelSeriesOldApex.h" #include "RGBController_SteelSeriesSensei.h" #include "RGBController_SteelSeriesArctis5.h" @@ -77,6 +79,7 @@ | Keyboard product IDs | \*-----------------------------------------------------*/ #define STEELSERIES_APEX_3_PID 0x161A +#define STEELSERIES_APEX_3_TKL_PID 0x1622 #define STEELSERIES_APEX_5_PID 0x161C #define STEELSERIES_APEX_7_PID 0x1612 #define STEELSERIES_APEX_7_TKL_PID 0x1618 @@ -122,6 +125,18 @@ void DetectSteelSeriesApexTZone(hid_device_info* info, const std::string& name) } } +void DetectSteelSeriesApex3TKL(hid_device_info* info, const std::string& name) +{ + hid_device* dev = hid_open_path(info->path); + if(dev) + { + SteelSeriesApex3TKLController* controller = new SteelSeriesApex3TKLController(dev, APEX_TZONE, info->path, name.c_str()); + RGBController_SteelSeriesApex3TKL* rgb_controller = new RGBController_SteelSeriesApex3TKL(controller); + rgb_controller->name = name; + ResourceManager::get()->RegisterRGBController(rgb_controller); + } +} + void DetectSteelSeriesApex(hid_device_info* info, const std::string& name) { hid_device* dev = hid_open_path(info->path); @@ -331,6 +346,7 @@ REGISTER_HID_DETECTOR_I("SteelSeries QCK Prism Cloth 4XL", | Keyboards | \*-------------------------------------------------------------------------------------------------------------------------------------------------------------------------*/ REGISTER_HID_DETECTOR_I("SteelSeries Apex 3", DetectSteelSeriesApexTZone, STEELSERIES_VID, STEELSERIES_APEX_3_PID, 3 ); +REGISTER_HID_DETECTOR_I("SteelSeries Apex 3 TKL", DetectSteelSeriesApex3TKL, STEELSERIES_VID, STEELSERIES_APEX_3_TKL_PID, 1 ); REGISTER_HID_DETECTOR_I("SteelSeries Apex 5", DetectSteelSeriesApex, STEELSERIES_VID, STEELSERIES_APEX_5_PID, 1 ); REGISTER_HID_DETECTOR_I("SteelSeries Apex 7", DetectSteelSeriesApex, STEELSERIES_VID, STEELSERIES_APEX_7_PID, 1 ); REGISTER_HID_DETECTOR_I("SteelSeries Apex 7 TKL", DetectSteelSeriesApex, STEELSERIES_VID, STEELSERIES_APEX_7_TKL_PID, 1 ); diff --git a/OpenRGB.pro b/OpenRGB.pro index 02afe505..c8d0df19 100644 --- a/OpenRGB.pro +++ b/OpenRGB.pro @@ -587,6 +587,7 @@ HEADERS += Controllers/SteelSeriesController/color32.h \ Controllers/SteelSeriesController/SteelSeriesAerox3Controller.h \ Controllers/SteelSeriesController/SteelSeriesAerox9Controller.h \ + Controllers/SteelSeriesController/SteelSeriesApex3TKLController.h \ Controllers/SteelSeriesController/SteelSeriesApexBaseController.h \ Controllers/SteelSeriesController/SteelSeriesApexController.h \ Controllers/SteelSeriesController/SteelSeriesApexRegions.h \ @@ -602,6 +603,7 @@ HEADERS += Controllers/SteelSeriesController/SteelSeriesSiberiaController.h \ Controllers/SteelSeriesController/SteelSeriesQCKMatController.h \ Controllers/SteelSeriesController/RGBController_SteelSeriesApex.h \ + Controllers/SteelSeriesController/RGBController_SteelSeriesApex3TKL.h \ Controllers/SteelSeriesController/RGBController_SteelSeriesApexTZone.h \ Controllers/SteelSeriesController/RGBController_SteelSeriesArctis5.h \ Controllers/SteelSeriesController/RGBController_SteelSeriesOldApex.h \ @@ -1178,6 +1180,7 @@ SOURCES += Controllers/SRGBmodsController/RGBController_SRGBmodsPico.cpp \ Controllers/SteelSeriesController/SteelSeriesAerox3Controller.cpp \ Controllers/SteelSeriesController/SteelSeriesAerox9Controller.cpp \ + Controllers/SteelSeriesController/SteelSeriesApex3TKLController.cpp \ Controllers/SteelSeriesController/SteelSeriesApexController.cpp \ Controllers/SteelSeriesController/SteelSeriesApexMController.cpp \ Controllers/SteelSeriesController/SteelSeriesApexTZoneController.cpp \ @@ -1191,6 +1194,7 @@ SOURCES += Controllers/SteelSeriesController/SteelSeriesSiberiaController.cpp \ Controllers/SteelSeriesController/SteelSeriesQCKMatController.cpp \ Controllers/SteelSeriesController/RGBController_SteelSeriesApex.cpp \ + Controllers/SteelSeriesController/RGBController_SteelSeriesApex3TKL.cpp \ Controllers/SteelSeriesController/RGBController_SteelSeriesApexTZone.cpp \ Controllers/SteelSeriesController/RGBController_SteelSeriesArctis5.cpp \ Controllers/SteelSeriesController/RGBController_SteelSeriesOldApex.cpp \