Initial commit for SteelSeriesApex3TKLController to resolve #1902

This commit is contained in:
Paul Konstantin Gerke 2022-11-25 18:33:02 +11:00 committed by Adam Honse
parent 88136152fc
commit df1c0a0dad
6 changed files with 460 additions and 0 deletions

View file

@ -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<int>(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<int>(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<APEX3_TKL_MODES>(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;
}
}

View file

@ -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();
};

View file

@ -0,0 +1,123 @@
/*-----------------------------------------*\
| SteelSeriesApex3TKLController.cpp |
| |
| Paul K. Gerke - 27.10.2022 |
| Dr_no (Chris M) |
\*-----------------------------------------*/
#include "SteelSeriesApex3TKLController.h"
#include <cstring>
#include <LogManager.h>
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<RGBColor> 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);
}

View file

@ -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 <string>
#include <hidapi/hidapi.h>
#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<RGBColor> 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...?
*/

View file

@ -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 );