Apex Pro Gen 3 TKL wired, add media light and regional layout support

This commit is contained in:
Joseph E
2026-04-09 23:11:52 +00:00
committed by Adam Honse
parent 583a737cb5
commit c171bfbf3b
5 changed files with 172 additions and 70 deletions

View File

@@ -10,6 +10,7 @@
\*---------------------------------------------------------*/
#include "SteelSeriesApexBaseController.h"
#include <algorithm>
SteelSeriesApexBaseController::SteelSeriesApexBaseController(hid_device* dev_handle, const char* path, std::string dev_name)
{
@@ -34,17 +35,15 @@ std::string SteelSeriesApexBaseController::GetName()
}
/*---------------------------------------------------------*\
| The serial number of the keyboard is acquired by sending |
| an output report to address 0xFF and reading the result. |
| The HID capability table is not used. The serial number |
| also contains the model number which can be used to |
| determine the physical layout of different region |
| keyboards throughout the product stack. |
| Gen 1 Apex Pro stores the unit serial number in firmware. |
| The first 5 digits determine the region of the keyboard. |
| This is not the case for Gen 3, call to this function |
| will be ignored. |
\*---------------------------------------------------------*/
std::string SteelSeriesApexBaseController::GetSerial()
{
std::string return_string = "";
if(proto_type == APEX)
if(proto_type == APEX && kbd_quirk == APEX_GEN1)
{
unsigned char obuf[STEELSERIES_PACKET_OUT_SIZE];
unsigned char ibuf[STEELSERIES_PACKET_IN_SIZE];
@@ -70,6 +69,27 @@ std::string SteelSeriesApexBaseController::GetSerial()
return(return_string);
}
std::string ExtractVersion(std::string version_string)
{
/*---------------------------------------------*\
| Find 2 periods in string, if found we can |
| form a X.Y.Z revision. |
\*---------------------------------------------*/
std::size_t majorp = version_string.find('.');
if(majorp != std::string::npos)
{
std::size_t minorp = version_string.find('.', majorp+1);
if(minorp != std::string::npos)
{
std::string major = version_string.substr(0, majorp);
std::string minor = version_string.substr(majorp+1, (minorp-majorp-1));
std::string build = version_string.substr(minorp+1);
return major + "." + minor + "." + build;
}
}
return "";
}
std::string SteelSeriesApexBaseController::GetVersion()
{
std::string return_string = "Unsupported protocol";
@@ -77,11 +97,9 @@ std::string SteelSeriesApexBaseController::GetVersion()
if(proto_type == APEX)
{
/*-------------------------------------------------*\
| For the Apex Pro there are two firmware versions |
| which can be acquired, KBD and LED. We know |
| where both are located, we do not know which is |
| what. For now we'll make an assumption and fix |
| if proven wrong. |
| Gen 1 & 2 Apex Pro report KBD and LED firmware |
| Gen 3 only reports the KBD firmware, ignoring |
| requests to read the LED version |
\*-------------------------------------------------*/
unsigned char obuf[STEELSERIES_PACKET_OUT_SIZE];
unsigned char ibuf[STEELSERIES_PACKET_IN_SIZE];
@@ -96,50 +114,36 @@ std::string SteelSeriesApexBaseController::GetVersion()
if(result > 0)
{
std::string fwver(ibuf, ibuf+STEELSERIES_PACKET_IN_SIZE);
fwver = fwver.c_str();
fwver.erase(std::remove(fwver.begin(), fwver.end(), '\0'), fwver.end());
/*---------------------------------------------*\
| Find 2 periods in string, if found we can |
| form a X.Y.Z revision. |
| Apex Pro Gen 3 needs the first char dropped |
\*---------------------------------------------*/
std::size_t majorp = fwver.find('.');
if(majorp != std::string::npos)
if(kbd_quirk == APEX_GEN3)
{
std::size_t minorp = fwver.find('.', majorp+1);
if(minorp != std::string::npos)
{
std::string major = fwver.substr(0, majorp);
std::string minor = fwver.substr(majorp+1, (minorp-majorp-1));
std::string build = fwver.substr(minorp+1);
return_string = "KBD: " + major + "." + minor + "." + build;
}
fwver.erase(0,1);
}
return_string = "KBD: " + ExtractVersion(fwver);
}
/*-------------------------------------------------*\
| Clear and reuse buffer |
\*-------------------------------------------------*/
memset(ibuf, 0x00, sizeof(ibuf));
obuf[0x02] = 0x01;
hid_write(dev, obuf, STEELSERIES_PACKET_OUT_SIZE);
result = hid_read_timeout(dev, ibuf, STEELSERIES_PACKET_IN_SIZE, 10);
if(result > 0)
if(kbd_quirk != APEX_GEN3)
{
std::string fwver(ibuf, ibuf+STEELSERIES_PACKET_IN_SIZE);
fwver = fwver.c_str();
memset(ibuf, 0x00, sizeof(ibuf));
obuf[0x02] = 0x01;
hid_write(dev, obuf, STEELSERIES_PACKET_OUT_SIZE);
result = hid_read_timeout(dev, ibuf, STEELSERIES_PACKET_IN_SIZE, 10);
std::size_t majorp = fwver.find('.');
if(majorp != std::string::npos)
if(result > 0)
{
std::size_t minorp = fwver.find('.', majorp+1);
if(minorp != std::string::npos)
{
std::string major = fwver.substr(0, majorp);
std::string minor = fwver.substr(majorp+1, (minorp-majorp-1));
std::string build = fwver.substr(minorp+1);
return_string = return_string + " / LED: " + major + "." + minor + "." + build;
}
std::string fwver(ibuf, ibuf+STEELSERIES_PACKET_IN_SIZE);
fwver.erase(std::remove(fwver.begin(), fwver.end(), '\0'), fwver.end());
fwver = fwver.c_str();
return_string = return_string + " / LED: " + ExtractVersion(fwver);
}
}
}

View File

@@ -19,6 +19,20 @@
#define STEELSERIES_PACKET_IN_SIZE 64
#define STEELSERIES_PACKET_OUT_SIZE STEELSERIES_PACKET_IN_SIZE + 1
/*-------------------------------------------------*\
| Gen 1: 2019-22 models (all FW) & 2023 FW < 1.19.7 |
| Gen 2: 2023 models with FW >= 1.19.7 |
| Gen 3: 2025+ and may feature Gen 3 in the name |
\*-------------------------------------------------*/
typedef enum
{
APEX_GEN1 = 0x00,
APEX_GEN2 = 0x01,
APEX_GEN3 = 0x02,
} protocol_quirk;
class SteelSeriesApexBaseController
{
public:
@@ -41,4 +55,5 @@ protected:
unsigned char active_mode;
std::string location;
std::string name;
protocol_quirk kbd_quirk;
};

View File

@@ -33,18 +33,31 @@ static unsigned int keys[] = {0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x
0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xF0, 0x31, 0x87,
0x88, 0x89, 0x8A, 0x8B, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, //100
0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, 0x60, 0x61, 0x62,
0x63 };
0x63, 0xFB };
SteelSeriesApexController::SteelSeriesApexController(hid_device* dev_handle, steelseries_type type, const char* path, std::string dev_name) : SteelSeriesApexBaseController(dev_handle, path, dev_name)
{
proto_type = type;
use_new_protocol = false;
kbd_quirk = APEX_GEN1;
SendInitialization();
}
SteelSeriesApexController::~SteelSeriesApexController()
{
/*-----------------------------------------------------*\
| Gen 3 models must be explicitly cleared for on-board |
| config selection to apply without power cycling after |
| OpenRGB shuts down. |
\*-----------------------------------------------------*/
if(kbd_quirk == APEX_GEN3)
{
unsigned char obuf[STEELSERIES_PACKET_OUT_SIZE];
memset(obuf, 0x00, sizeof(obuf));
obuf[0x00] = 0;
obuf[0x01] = APEX_GEN3_PACKET_CLEAR_LIGHTING;
hid_write(dev, obuf, STEELSERIES_PACKET_OUT_SIZE);
}
hid_close(dev);
}
@@ -70,7 +83,7 @@ void SteelSeriesApexController::SetLEDsDirect(std::vector<RGBColor> colors)
num_keys = sizeof(keys) / sizeof(*keys);
if(use_new_protocol)
if(kbd_quirk >= APEX_GEN2)
{
struct hid_device_info* info = hid_get_device_info(dev);
@@ -101,7 +114,7 @@ void SteelSeriesApexController::SetLEDsDirect(std::vector<RGBColor> colors)
\*-----------------------------------------------------*/
buf[0x00] = 0;
buf[0x01] = packet_id;
buf[0x02] = (use_new_protocol) ? (unsigned char)colors.size() : num_keys;
buf[0x02] = kbd_quirk ? (unsigned char)colors.size() : num_keys;
/*-----------------------------------------------------*\
| Fill in color data |
@@ -151,7 +164,7 @@ void SteelSeriesApexController::SendInitialization()
unsigned short pid = (info) ? info->product_id : 0;
/*-----------------------------------------------------*\
| Firmware check |
| Firmware check for TKL 2023 |
\*-----------------------------------------------------*/
if(pid == 0x1628)
{
@@ -192,17 +205,17 @@ void SteelSeriesApexController::SendInitialization()
\*-----------------------------------------*/
if(major > 1)
{
use_new_protocol = true;
kbd_quirk = APEX_GEN2;
}
else if(major == 1)
{
if(minor > 19)
{
use_new_protocol = true;
kbd_quirk = APEX_GEN2;
}
else if(minor == 19 && patch >= 7)
{
use_new_protocol = true;
kbd_quirk = APEX_GEN2;
}
}
}
@@ -217,13 +230,13 @@ void SteelSeriesApexController::SendInitialization()
|| pid == 0x162C || pid == 0x162D
|| pid == 0x1642 || pid == 0x1644 || pid == 0x1646)
{
use_new_protocol = true;
kbd_quirk = APEX_GEN3;
}
/*-----------------------------------------------------*\
| Send Initialization packet on new protocol. |
\*-----------------------------------------------------*/
if(use_new_protocol)
if(kbd_quirk >= APEX_GEN2)
{
memset(buf, 0x00, sizeof(buf));
buf[0x00] = 0x00;
@@ -240,8 +253,64 @@ void SteelSeriesApexController::SendInitialization()
std::string SteelSeriesApexController::GetSerial()
{
if(use_new_protocol)
/*-------------------------------------------------*\
| Gen 3 doesn't expose the serial number in |
| firmware. A region code is instead set by the |
| user and subsequently read back. This region code |
| is used by all 5 on-board configs. |
| For consistency with other Apex keyboards, this |
| region code in combination with the PID is mapped |
| to an approximate product number as the region |
| patch logic from that point is identical. |
| The product number used may not be an exact match |
| for the keyboard but should reflect the form |
| factor, region and RGB layout |
\*-------------------------------------------------*/
if(kbd_quirk >= APEX_GEN2)
{
unsigned char obuf[STEELSERIES_PACKET_OUT_SIZE];
unsigned char ibuf[STEELSERIES_PACKET_IN_SIZE];
int result;
struct hid_device_info* info = hid_get_device_info(dev);
unsigned short pid = (info) ? info->product_id : 0;
memset(obuf, 0x00, sizeof(obuf));
if(pid == 0x1642)
{
obuf[0x00] = 0;
obuf[0x01] = 0xF5;
hid_write(dev, obuf, STEELSERIES_PACKET_OUT_SIZE);
result = hid_read_timeout(dev, ibuf, STEELSERIES_PACKET_IN_SIZE, 2);
if(result > 3 && ibuf[0] == 0xF5)
{
switch(ibuf[2])
{
case 0x1:
return "64740";
break;
case 0x3:
return "64741";
break;
case 0x4:
return "64743";
break;
case 0x6:
return "64744";
break;
case 0xA:
return "64742";
break;
case 0xD:
return "64745";
break;
default:
break;
}
}
}
return "64865";
}

View File

@@ -28,6 +28,7 @@ enum
APEX_2023_PACKET_ID_DIRECT_WIRELESS = 0x61, /* New Wireless Direct mode */
APEX_2023_PACKET_ID_INIT = 0x4B, /* New Initialization */
APEX_2023_PACKET_LENGTH = 643,
APEX_GEN3_PACKET_CLEAR_LIGHTING = 0x41,
};
class SteelSeriesApexController : public SteelSeriesApexBaseController
@@ -55,5 +56,4 @@ private:
void SendInitialization();
bool use_new_protocol;
};

View File

@@ -20,17 +20,16 @@
#define NA 0xFFFFFFFF
/*----------------------------------------------------------------------*\
| As of firmware 4.1.0 there are in total 111 possible standard keys |
| which are shared across the Apex Pro / 7 / TKL / 5 and their regional |
| SKUs in addition to the 6 media keys. No SKU has all 111, however |
| regardless of the physial layout, the one configuration can be used |
| across all SKUs with missing keys simply having no effect. |
| The complication comes in the visualisation as different key layouts |
| change the LED positions, additionally some labels / scancodes are |
| overloaded based on the language which clashes with the OpenRGB |
| defaults. In order to account for this a base SKU (ANSI) is assumed |
| which is transformed into a regional SKU when device detection returns |
| a known SKU number from the first 5 characters of the serial number. |
| Steelseries keyboards share a library of 111 standard keys plus extra |
| ambients across all physical form factors. No keyboard has every key |
| fitted but the same format packet can be sent regardless of model and |
| the firmware will pick the keys applicable to it, ignoring the rest. |
| The complication comes in commuicating this to the OpenRGB GUI as |
| different key layouts change the LED positions, additionally some |
| labels / scancodes are overloaded based on the language which clashes |
| with the OpenRGB defaults. In order to account for this a base SKU |
| (ANSI) is assumed which is transformed into a regional SKU when device |
| detection returns a known product number acquired from the keyboard |
\*----------------------------------------------------------------------*/
#define MATRIX_HEIGHT 6
@@ -50,6 +49,10 @@
static const int matrix_mapsize = MATRIX_HEIGHT * MATRIX_WIDTH;
/*-------------------------------------------------------*\
| These map to the values defined in the keys array at |
| SteelSeriesApexController |
\*-------------------------------------------------------*/
static const char* led_names[] =
{
KEY_EN_A,
@@ -163,6 +166,7 @@ static const char* led_names[] =
KEY_EN_NUMPAD_9,
KEY_EN_NUMPAD_0,
KEY_EN_NUMPAD_PERIOD,
KEY_EN_MEDIA_PLAY_PAUSE
};
struct matrix_region_patch
@@ -244,7 +248,7 @@ static const std::vector<matrix_region_patch> apex_tkl_us_region_patch =
{
{0, 15, NA},
{0, 16, NA},
{0, 17, NA},
{0, 17, 111},
{1, 18, NA},
{1, 19, NA},
{1, 20, NA},
@@ -367,6 +371,9 @@ static const std::map<std::string, sku_patch> patch_lookup =
{ "64660", { {}, {}, {} }},
{ "64740", { apex_tkl_us_region_patch, {}, {} }},
{ "64742", { apex_tkl_us_region_patch, apex_iso_region_patch, apex_nor_keyname_lookup }},
{ "64743", { apex_tkl_us_region_patch, apex_iso_region_patch, apex_uk_keyname_lookup }},
{ "64745", { apex_tkl_us_region_patch, apex_jp_region_patch, apex_jp_keyname_lookup }},
{ "64871", { apex_tkl_us_region_patch, {}, {} }},
{ "64913", { apex_mini_us_region_patch, {}, {} }},
@@ -488,7 +495,7 @@ static void SetSkuLedNames (std::vector<led>& input, std::string& sku, unsigned
| SKU codes for all known Apex Pro / 7 / 5 & TKL variant |
| keyboards as at Janauary 2022. Generated by cross-checking |
| store listings aginst Steelseries website. |
| Updated 2025 for Apex 9 and Pro Gen 2 & Gen 3 |
| Updated 2026 for Apex 9 and Pro Gen 2 & Gen 3 |
| The product Pro Mini seem to belong to Gen 2 |
| |
| -- APEX PRO Gen 3 -- |
@@ -497,11 +504,17 @@ static void SetSkuLedNames (std::vector<led>& input, std::string& sku, unsigned
| |
| >> APEX PRO TKL Gen 3 |
| |
| "64740", // US TKL |
| "64740", // US TKL Black |
| "64741", // UK TKL Black |
| "64742", // Nordic TKL Black |
| "64743", // German TKL Black |
| "64744", // French TKL Black |
| "64745", // Japanese TKL Black |
| |
| >> APEX PRO TKL Wireless Gen 3 |
| |
| "64871", // US TKL Wireless |
| "64871", // US TKL Black Wireless |
| "64876", // Japanese TKL Black Wireless |
| |
| >> APEX PRO Mini Gen 3 |
| |
@@ -512,6 +525,7 @@ static void SetSkuLedNames (std::vector<led>& input, std::string& sku, unsigned
| >> APEX PRO TKL Gen 2 / 2023 |
| |
| "64856", // US TKL |
| "64861", // Japanese TKL |
| |
| >> APEX PRO TKL Wireless Gen 2 / 2023 |
| |