diff --git a/Controllers/SteelSeriesController/SteelSeriesApexBaseController.cpp b/Controllers/SteelSeriesController/SteelSeriesApexBaseController.cpp index a7d53ee12..6c991b9c4 100644 --- a/Controllers/SteelSeriesController/SteelSeriesApexBaseController.cpp +++ b/Controllers/SteelSeriesController/SteelSeriesApexBaseController.cpp @@ -10,6 +10,7 @@ \*---------------------------------------------------------*/ #include "SteelSeriesApexBaseController.h" +#include 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); } } } diff --git a/Controllers/SteelSeriesController/SteelSeriesApexBaseController.h b/Controllers/SteelSeriesController/SteelSeriesApexBaseController.h index f9d622b41..eab323e89 100644 --- a/Controllers/SteelSeriesController/SteelSeriesApexBaseController.h +++ b/Controllers/SteelSeriesController/SteelSeriesApexBaseController.h @@ -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; }; diff --git a/Controllers/SteelSeriesController/SteelSeriesApexController/SteelSeriesApexController.cpp b/Controllers/SteelSeriesController/SteelSeriesApexController/SteelSeriesApexController.cpp index 7f0a31733..d4b602906 100644 --- a/Controllers/SteelSeriesController/SteelSeriesApexController/SteelSeriesApexController.cpp +++ b/Controllers/SteelSeriesController/SteelSeriesApexController/SteelSeriesApexController.cpp @@ -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 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 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"; } diff --git a/Controllers/SteelSeriesController/SteelSeriesApexController/SteelSeriesApexController.h b/Controllers/SteelSeriesController/SteelSeriesApexController/SteelSeriesApexController.h index 2ea12dd4e..dfeb2347c 100644 --- a/Controllers/SteelSeriesController/SteelSeriesApexController/SteelSeriesApexController.h +++ b/Controllers/SteelSeriesController/SteelSeriesApexController/SteelSeriesApexController.h @@ -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; }; diff --git a/Controllers/SteelSeriesController/SteelSeriesApexRegions.h b/Controllers/SteelSeriesController/SteelSeriesApexRegions.h index cb65ba8b3..2998fe452 100644 --- a/Controllers/SteelSeriesController/SteelSeriesApexRegions.h +++ b/Controllers/SteelSeriesController/SteelSeriesApexRegions.h @@ -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 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 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& 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& 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& input, std::string& sku, unsigned | >> APEX PRO TKL Gen 2 / 2023 | | | | "64856", // US TKL | +| "64861", // Japanese TKL | | | | >> APEX PRO TKL Wireless Gen 2 / 2023 | | |