Compare commits

...
Author SHA1 Message Date
Adam Honse 5c931a61b4 Add support for Keychron K10 V2 in QMKKeychronController 2026-06-24 23:33:23 -05:00
Wolfieee Wolf ad7534d00d fix(razer): apply Tartarus V2 keypad LED layout in OpenRGB 2026-06-24 22:01:20 -05:00
Richard Harris c81f8699f1 Add support for Govee H6022 Smart Table Lamp 2 2026-06-24 21:52:47 -05:00
Adam Honse 2af03eb0dc Return false if no PawnIO bus was detected so that the warning dialog actually displays 2026-06-24 18:30:37 -05:00
Adam Honse 5064ba3f1d Update warning messages and remove references to help.openrgb.org. 2026-06-24 15:02:20 -05:00
Adam Honse c5abe47859 Create a common type for storing QMK LED info and use it in both VialRGB and Keychron controllers 2026-06-24 11:55:26 -05:00
Nikhil Jeikar P M 8fdd0e40f8 Adding support for MSI PRO X870E-S EVO WIFI 2026-06-24 16:14:26 +05:30
HorrorTroll a6a07ba0d7 Added support for Colorful iGame GeForce RTX 5060 Ultra W OC when using OC mode 2026-06-23 23:50:18 -05:00
Ken Sanislo 614fef4189 G515 LS TKL and G502 X PLUS support, via Logitech HID++ 2.0 controller with feature discovery 2026-06-23 19:33:18 -05:00
Adam Honse d3e7dba2b7 Add supported feature check in QMKKeychronController 2026-06-23 19:22:50 -05:00
Stefanie Jane 39343c3e9e Fix buffer overflow in KeyboardLayoutManager::GetKeyMap() 2026-06-22 20:49:53 -05:00
Adam Honse 73865fec59 Improve matrix generation for QMK Keychron controller, clean up modes, implement manual save 2026-06-22 18:42:03 -05:00
Adam Honse b508818f23 Update PawnIO Modules to Release 0.2.9 and re-add Intel Skylake-X IMC SMBus support 2026-06-21 22:17:49 -05:00
lin bef8ae0a2f Add support for Gigabyte AORUS GeForce RTX 5090 D V2 MASTER ICE 2026-06-22 10:33:29 +08:00
Adam Honse cce2d41500 Cleanups to QMKKeychronController and create common QMK VIA protocol header 2026-06-21 21:14:46 -05:00
Amadej Kastelic 175f3ed338 feat: add Keychron Q1 HE keyboard support and restructure Keychron controllers 2026-06-21 21:14:46 -05:00
Adam Honse c2cc7c6163 Move priority setup to ResourceManager and make it configurable 2026-06-21 21:14:46 -05:00
RedBlackAka 8fee041fad Lower process priority 2026-06-21 21:14:46 -05:00
Wolfieee Wolf dfc1f8524a fix(razer): add BlackWidow X Chroma keyboard layout 2026-06-21 18:30:31 -05:00
Wolfieee Wolf 752bd2e35a fix(intel): number Arc A770 LE per-LED dropdown labels 2026-06-21 18:26:31 -05:00
I-Kerosin-I 01fe0b4630 Add support for Gigabyte GeForce RTX 4070 GAMING OC V2 with AD103 2026-06-21 18:05:32 -05:00
Daniel Clark e7f8bc0136 Fix X870 Gaming WIFI6 and X870 Aorus Tachyon ICE layouts 2026-06-17 06:45:01 -07:00
Rodrigo Henrique 370918e07f Add Gigabyte GeForce RTX 5070 Eagle OC ICE 2026-06-17 07:35:31 -05:00
trjohnson19 133966677b Add support for ASUS TUF RTX5070 Ti OC BTF White 2026-06-16 07:52:28 -05:00
kStor2poche 44f84e4781 Add Gigabyte Radeon RX 6800 XT GAMING OC. Closes #5657 2026-06-16 07:41:56 -05:00
Roy Biggins a8354ee046 Add Gigabyte Aorus Waterforce X II 360 AIO controller (Castor3) 2026-06-16 00:02:41 -05:00
Luca Cesarano 4550e3073a Fix NZXT Kraken Elite V2 effects 2026-06-15 23:58:57 -05:00
Adam Honse abe7a49092 Updates to DRAM controllers to avoid use of SMBus Quick Write and add fallback paths if SMBus Block Write is unavailable, in preparation for X299 SMBus support 2026-06-15 23:54:33 -05:00
Adam Honse 82a486bfb6 Update SMBus tools page to add more SMBus operation commands 2026-06-15 23:54:33 -05:00
Victor Hugo Garzo Prado ad06e733f8 Add missing push_back to insert Corsair AIO leds into leds vector 2026-06-15 23:40:05 -05:00
Adam Honse 4306603a28 Remove Skylake-X IMC SMBus PawnIO module for now as it does not support the necessary features yet 2026-06-09 22:58:41 -05:00
Jerry Fan 554f834a90 Fix typo of custled in RealtekARGBController 2026-06-09 08:16:39 -05:00
manuelulreich 7236bec61f [New Device] Add razer blackwidow v4 low profile tenkeyless hyperspeed 2026-06-09 07:29:23 -05:00
XPRAMT c799f1bfe9 Improve ColorWheel drag behavior 2026-06-09 07:19:14 -05:00
Adam Honse f15c3c1420 Add Redragon M921 2026-06-07 14:08:34 -05:00
Adam Honse aa67045330 Fix duplicate entries in SteelSeriesControllerDetect 2026-06-07 13:28:46 -05:00
Alexey Kopytko 736640f38d Add support for Zotac RTX 5090 SOLID OC 2026-06-07 13:21:29 -05:00
David Glushkov e9caf663bc Fix MSILaptopController ALC zone mapping and packet initialization 2026-06-07 13:09:19 -05:00
Daniel Clark 8afbea53de Update RGBFusion2USB Driver. 2026-06-07 01:13:03 -05:00
metroite c3d496ad5b Initial commit for SteelSeries Apex Pro TKL Gen 3 2026-06-07 01:09:30 -05:00
ilyaKhalidov88 06501f528d Add support for ASUS ROG Astral 5090 2026-06-07 00:46:27 -05:00
Jerry Fan f6a678c1a4 Add support for RealtekARGBController 2026-06-05 01:03:29 -05:00
Andy Herbert 30690d0ebd New device- MSI MAG272CQR monitor 2026-06-04 11:54:51 -05:00
Kyle Finter 7a830cddf9 Added new Govee devices H612F and H607C 2026-06-04 07:27:02 +00:00
Adam Honse 3dc1365d39 Add PowerShell implementation of build-msi for building MSI on Windows, add additional information about PawnIO and the OpenRGB system service to the installer. 2026-06-02 22:07:17 -05:00
Adam Honse b03cd94ae8 Fix incorrect firmware packet format and add timeouts to HID reads in ThermaltakeRiingController 2026-06-02 01:07:33 -05:00
Jefferson González bbf348d273 Add HyperX Eve 1800 keyboard support 2026-06-02 00:15:19 -05:00
David Glushkov dce48eb415 Add MSI Raider A18 HX A9WJG SteelSeries RGB 2026-06-02 00:05:18 -05:00
Ox HaK decf14635d Add Fnatic Streak65 support 2026-06-01 08:27:17 -05:00
Richard Harris db4f49bbba Implement Direct mode support for Roccat Vulcan TKL (non-Pro) 2026-05-31 12:38:57 -05:00
HorrorTroll 26824fcb6e Added support for Colorful iGame GeForce RTX 5060 Ultra W OC 2026-05-31 15:12:36 +07:00
Jerry Fan 8afad91b33 Add support for RealtekBridgeController 2026-05-29 07:46:22 -05:00
Adam Honse 69de0b13c4 Update PawnIO modules to 0.2.6 and enable Intel Skylake IMC SMBus module 2026-05-28 23:48:42 -05:00
alstruit 967153bc10 Add support for ASUS ROG Gladius III Core 2026-05-25 12:08:43 -05:00
JAO1988 971d9be1bd Added Colorful iGame RTX 5060TI 16GB Ultra W DUO Variant 2026-05-23 23:10:36 -05:00
Mark Willis 7a3faef886 Initial commit for ASUS TUF GeForce RTX 5070 Ti Gaming White OC 2026-05-22 12:25:59 -05:00
Purrple Kitten f67030fcc7 Add support for the Gigabyte AORUS RTX 5080 MASTER ICE 2026-05-18 07:30:37 -05:00
Alex Jawhari 7353e99449 Fix AMD GPU i2c bus PCI ID detection on Linux (sysfs path traversal) 2026-05-17 23:58:21 -05:00
JAO1988 91d9597c89 Added Support for ASUS ROG ASTRAL GeForce RTX 5080 WHITE 2026-05-17 23:47:55 -05:00
Adam Honse 2eb569912b XPGSummonerKeyboardController - Remove fake modes 2026-05-13 11:49:05 -05:00
Ken Sanislo 9410d9c066 Add support for Thrustmaster Sol series joystick RGB LEDs 2026-05-09 00:21:40 -05:00
Adam Honse 29d8788e25 Allow value 0x27 at address 0x00 for Galax GPU controllers at address 0x23 2026-05-08 20:05:40 -05:00
Elvis Pranskevichus 0304c50bc6 Add support for Gigabyte AORUS RX 7900 XTX ELITE 24G 2026-05-08 00:12:43 -05:00
124 changed files with 26182 additions and 4808 deletions
@@ -22,6 +22,7 @@
#define AURA_ROG_GLADIUS_II_WIRELESS_1_PID 0x189E
#define AURA_ROG_GLADIUS_II_WIRELESS_2_PID 0x18A0
#define AURA_ROG_GLADIUS_III_PID 0x197B
#define AURA_ROG_GLADIUS_III_CORE_PID 0x1C8D
#define AURA_ROG_GLADIUS_III_WIRELESS_USB_PID 0x197D
#define AURA_ROG_GLADIUS_III_WIRELESS_2_4_PID 0x197F
#define AURA_ROG_GLADIUS_III_WIRELESS_BT_PID 0x1981
@@ -217,6 +218,20 @@ static std::map<int,mouse_type> aura_mouse_devices =
{ AURA_MOUSE_MODE_STATIC, AURA_MOUSE_MODE_BREATHING, AURA_MOUSE_MODE_SPECTRUM, AURA_MOUSE_MODE_WAVE, AURA_MOUSE_MODE_REACTIVE, AURA_MOUSE_MODE_COMET, AURA_MOUSE_MODE_BATTERY }
}
},
{
AURA_ROG_GLADIUS_III_CORE_PID, // ROG Gladius III Core
{
0,
0,
0,
100,
false,
1,
false,
{ AURA_MOUSE_ZONE_LOGO, AURA_MOUSE_ZONE_SCROLL },
{ AURA_MOUSE_MODE_STATIC, AURA_MOUSE_MODE_BREATHING, AURA_MOUSE_MODE_SPECTRUM, AURA_MOUSE_MODE_NONE, AURA_MOUSE_MODE_REACTIVE }
}
},
{
AURA_ROG_GLADIUS_III_WIRELESS_USB_PID, // ROG Gladius III Wireless USB
{
@@ -394,6 +394,7 @@ REGISTER_HID_DETECTOR_IP("ASUS ROG Gladius II Origin COD",
REGISTER_HID_DETECTOR_IP("ASUS ROG Gladius II Wireless", DetectAsusAuraUSBMice, AURA_USB_VID, AURA_ROG_GLADIUS_II_WIRELESS_1_PID, 1, 0xFF13);
REGISTER_HID_DETECTOR_IP("ASUS ROG Gladius II Wireless", DetectAsusAuraUSBMice, AURA_USB_VID, AURA_ROG_GLADIUS_II_WIRELESS_2_PID, 2, 0xFF01);
REGISTER_HID_DETECTOR_IP("ASUS ROG Gladius III", DetectAsusAuraUSBMice, AURA_USB_VID, AURA_ROG_GLADIUS_III_PID, 0, 0xFF01);
REGISTER_HID_DETECTOR_IP("ASUS ROG Gladius III Core", DetectAsusAuraUSBMice, AURA_USB_VID, AURA_ROG_GLADIUS_III_CORE_PID, 0, 0xFF01);
REGISTER_HID_DETECTOR_IP("ASUS ROG Gladius III Wireless USB", DetectAsusAuraUSBMice, AURA_USB_VID, AURA_ROG_GLADIUS_III_WIRELESS_USB_PID, 0, 0xFF01);
REGISTER_HID_DETECTOR_IP("ASUS ROG Gladius III Wireless 2.4Ghz", DetectAsusAuraUSBMice, AURA_USB_VID, AURA_ROG_GLADIUS_III_WIRELESS_2_4_PID, 0, 0xFF01);
REGISTER_HID_DETECTOR_IP("ASUS ROG Gladius III Wireless Bluetooth", DetectAsusAuraUSBMice, AURA_USB_VID, AURA_ROG_GLADIUS_III_WIRELESS_BT_PID, 0, 0xFF01);
@@ -1,4 +1,4 @@
/*---------------------------------------------------------*\
/*---------------------------------------------------------*\
| ColorfulGPUControllerDetect.cpp |
| |
| Detector for Colorful GPU |
@@ -48,7 +48,7 @@ REGISTER_I2C_PCI_DETECTOR("iGame GeForce RTX 3060 Advanced OC 12G L-V", Detec
REGISTER_I2C_PCI_DETECTOR("iGame GeForce RTX 3060 Ultra W OC 12G L-V", DetectColorfulGPUControllers, NVIDIA_VEN, NVIDIA_RTX3060_LHR_DEV, COLORFUL_SUB_VEN, COLORFUL_IGAME_RTX_3060_ULTRAW_OC_12G, 0x61);
REGISTER_I2C_PCI_DETECTOR("iGame GeForce RTX 3060 Ultra W OC 12G L-V", DetectColorfulGPUControllers, NVIDIA_VEN, NVIDIA_RTX3060_GA106_DEV, COLORFUL_SUB_VEN, COLORFUL_IGAME_RTX_3060_ULTRAW_OC_12G, 0x61);
REGISTER_I2C_PCI_DETECTOR("iGame GeForce RTX 3060 Ultra W OC 12G L-V", DetectColorfulGPUControllers, NVIDIA_VEN, NVIDIA_RTX3060_LHR_DEV, COLORFUL_SUB_VEN, COLORFUL_IGAME_RTX_3060_ULTRAW_OC_12G_2, 0x61);
REGISTER_I2C_PCI_DETECTOR("iGame GeForce RTX 3060 Ti Ultra W OC LHR-V", DetectColorfulGPUControllers, NVIDIA_VEN, NVIDIA_RTX3060TI_LHR_DEV, COLORFUL_SUB_VEN, COLORFUL_IGAME_RTX_3060_ULTRAW_OC_12G, 0x61);
REGISTER_I2C_PCI_DETECTOR("iGame GeForce RTX 3060 Ti Ultra W OC LHR-V", DetectColorfulGPUControllers, NVIDIA_VEN, NVIDIA_RTX3060TI_LHR_DEV, COLORFUL_SUB_VEN, COLORFUL_IGAME_RTX_3060_ULTRAW_OC_12G, 0x61);
REGISTER_I2C_PCI_DETECTOR("iGame GeForce RTX 3060 Ti Ultra W OC LHR-V", DetectColorfulGPUControllers, NVIDIA_VEN, NVIDIA_RTX3060TI_LHR_DEV, COLORFUL_SUB_VEN, COLORFUL_IGAME_RTX_3060_ULTRAW_OC_12G_2, 0x61);
REGISTER_I2C_PCI_DETECTOR("iGame GeForce RTX 3060 Ti Advanced OC-V", DetectColorfulGPUControllers, NVIDIA_VEN, NVIDIA_RTX3060TI_LHR_DEV, COLORFUL_SUB_VEN, COLORFUL_IGAME_RTX_3070_ADVANCED_OCV, 0x61);
REGISTER_I2C_PCI_DETECTOR("iGame GeForce RTX 3060 Ti Advanced OC-V", DetectColorfulGPUControllers, NVIDIA_VEN, NVIDIA_RTX3060TI_LHR_DEV, COLORFUL_SUB_VEN, COLORFUL_IGAME_RTX_3060TI_ADVANCED_OC, 0x61);
@@ -72,6 +72,9 @@ REGISTER_I2C_PCI_DETECTOR("iGame GeForce RTX 4080 Ultra W OC-V", Detec
REGISTER_I2C_PCI_DETECTOR("iGame GeForce RTX 4080 Ultra W OC-V", DetectColorfulGPUControllers, NVIDIA_VEN, NVIDIA_RTX4080_DEV, COLORFUL_SUB_VEN, COLORFUL_IGAME_RTX_4080_ULTRAW_OCV2, 0x61);
REGISTER_I2C_PCI_DETECTOR("iGame GeForce RTX 4090 Advanced OC-V", DetectColorfulGPUControllers, NVIDIA_VEN, NVIDIA_RTX4090_DEV, COLORFUL_SUB_VEN, COLORFUL_IGAME_RTX_4090_ADVANCED_OCV, 0x61);
REGISTER_I2C_PCI_DETECTOR("iGame GeForce RTX 4090 Advanced OC-V", DetectColorfulGPUControllers, NVIDIA_VEN, NVIDIA_RTX4090_DEV, COLORFUL_SUB_VEN, COLORFUL_IGAME_RTX_4090_ADVANCED_OCV2, 0x61);
REGISTER_I2C_PCI_DETECTOR("iGame GeForce RTX 5060 Ti Ultra W DUO OC", DetectColorfulGPUControllers, NVIDIA_VEN, NVIDIA_RTX5060TI_DEV, COLORFUL_SUB_VEN, COLORFUL_IGAME_RTX_5060TI_ULTRAW_DUO_OC, 0x61);
REGISTER_I2C_PCI_DETECTOR("iGame GeForce RTX 5060 Ultra W OC", DetectColorfulGPUControllers, NVIDIA_VEN, NVIDIA_RTX5060_DEV, COLORFUL_SUB_VEN, COLORFUL_IGAME_RTX_5060_ULTRAW_OC, 0x61);
REGISTER_I2C_PCI_DETECTOR("iGame GeForce RTX 5060 Ultra W OC", DetectColorfulGPUControllers, NVIDIA_VEN, NVIDIA_RTX5060_DEV, COLORFUL_SUB_VEN, COLORFUL_IGAME_RTX_5060_ULTRAW_OC_2, 0x61);
REGISTER_I2C_PCI_DETECTOR("iGame GeForce RTX 5060 Ti Ultra W DUO OC", DetectColorfulGPUControllers, NVIDIA_VEN, NVIDIA_RTX5060TI_DEV, COLORFUL_SUB_VEN, COLORFUL_IGAME_RTX_5060TI_ULTRAW_DUO_OC, 0x61);
REGISTER_I2C_PCI_DETECTOR("iGame GeForce RTX 5060 Ti Ultra W DUO OC", DetectColorfulGPUControllers, NVIDIA_VEN, NVIDIA_RTX5060TI_DEV, COLORFUL_SUB_VEN, COLORFUL_IGAME_RTX_5060TI_ULTRAW_DUO_OC_2, 0x61);
REGISTER_I2C_PCI_DETECTOR("iGame GeForce RTX 5070 Ultra W OC-V", DetectColorfulGPUControllers, NVIDIA_VEN, NVIDIA_RTX5070_DEV, COLORFUL_SUB_VEN, COLORFUL_IGAME_RTX_5070_ULTRAW_OCV, 0x61);
REGISTER_I2C_PCI_DETECTOR("iGame GeForce RTX 5070 Ultra W OC-V", DetectColorfulGPUControllers, NVIDIA_VEN, NVIDIA_RTX5070_DEV, COLORFUL_SUB_VEN, COLORFUL_IGAME_RTX_5070_ULTRAW_OCV2, 0x61);
@@ -26,6 +26,7 @@ CorsairDRAMController::CorsairDRAMController(i2c_smbus_interface *bus, corsair_d
this->bus = bus;
this->dev = dev;
device_index = 0;
direct_mode = true;
pid = 0;
vid = 0;
protocol_version = 0;
@@ -132,13 +133,79 @@ void CorsairDRAMController::SetColorsPerLED(RGBColor* colors)
| bytes, use a second block write to the second |
| block write address for the remaining data |
\*-------------------------------------------------*/
bus->i2c_smbus_write_block_data(dev, CORSAIR_DRAM_REG_COLOR_BUFFER_BLOCK_1, 32, direct_packet);
s32 ret = bus->i2c_smbus_write_block_data(dev, CORSAIR_DRAM_REG_COLOR_BUFFER_BLOCK_1, 32, direct_packet);
if(direct_packet_size > 32)
if((ret >= 0) && (direct_packet_size > 32))
{
bus->i2c_smbus_write_block_data(dev, CORSAIR_DRAM_REG_COLOR_BUFFER_BLOCK_2, direct_packet_size - 32, direct_packet + 32);
}
/*-------------------------------------------------*\
| Corsair DRAM supports an alternate means of |
| writing block data without using SMBus block |
| operations. If block operations are not |
| available, fall back to this scheme. |
| |
| Blocks are split up into word data writes. |
| Some data bytes are packed into the lower nibble |
| of the register address byte. |
\*-------------------------------------------------*/
if(ret < 0)
{
unsigned int block_index = 0;
bool even_frame = true;
bool first_frame = true;
while(block_index < direct_packet_size)
{
unsigned char reg_value_0 = 0xA0;
unsigned char reg_value_1 = 0x00;
unsigned short word_value_0 = 0;
unsigned short word_value_1 = 0;
if(block_index == 0)
{
reg_value_0 = 0x90;
}
word_value_0 = (direct_packet[block_index]);
block_index++;
if(block_index < direct_packet_size)
{
word_value_0 |= (direct_packet[block_index] << 8);
block_index++;
}
if(block_index < direct_packet_size)
{
reg_value_1 = 0xA0;
reg_value_0 |= (direct_packet[block_index] & 0x0F);
reg_value_1 |= (direct_packet[block_index] & 0xF0) >> 4;
block_index++;
}
if(block_index < direct_packet_size)
{
word_value_1 = (direct_packet[block_index]);
block_index++;
}
if(block_index < direct_packet_size)
{
word_value_1 |= (direct_packet[block_index] << 8);
block_index++;
}
bus->i2c_smbus_write_word_data(dev, reg_value_0, word_value_0);
if(reg_value_1 > 0)
{
bus->i2c_smbus_write_word_data(dev, reg_value_1, word_value_1);
}
}
}
/*-------------------------------------------------*\
| Remember to delete the data buffer |
\*-------------------------------------------------*/
@@ -24,18 +24,15 @@ using namespace std::chrono_literals;
bool TestForCorsairDRAMController(i2c_smbus_interface *bus, unsigned char address)
{
int res = bus->i2c_smbus_write_quick(address, I2C_SMBUS_WRITE);
LOG_DEBUG("[%s] Trying address %02X", CORSAIR_DRAM_NAME, address);
int res = bus->i2c_smbus_read_byte_data(address, 0x43);
if(res < 0)
{
LOG_DEBUG("[%s] Failed: res was %04X", CORSAIR_DRAM_NAME, res);
return false;
}
res = bus->i2c_smbus_read_byte_data(address, 0x43);
if(!(res == 0x1A || res == 0x1B || res == 0x1C))
{
LOG_DEBUG("[%s] Failed: expected 0x1A, 0x1B, or 0x1C, got %04X", CORSAIR_DRAM_NAME, res);
@@ -96,7 +96,16 @@ void RGBController_CorsairICueLink::SetupZones()
lcd_zone.leds_min = controller->GetEndpoints()[lcd_idx]->led_channels;
lcd_zone.leds_max = lcd_zone.leds_min;
lcd_zone.leds_count = lcd_zone.leds_min;
zones.push_back(lcd_zone);
for(unsigned int led_idx = 0; led_idx < lcd_zone.leds_count; led_idx++)
{
led new_led;
new_led.name = "LED " + std::to_string(led_idx + 1);
leds.push_back(new_led);
}
}
}
}
@@ -134,7 +134,14 @@ void CrucialController::CrucialRegisterWriteBlock(crucial_register reg, unsigned
bus->i2c_smbus_write_word_data(dev, 0x00, ((reg << 8) & 0xFF00) | ((reg >> 8) & 0x00FF));
//Write Crucial block data
bus->i2c_smbus_write_block_data(dev, 0x03, sz, data);
if(bus->i2c_smbus_write_block_data(dev, 0x03, sz, data) == -1)
{
//Fall back to individual byte operations if the block operation fails
for(unsigned int block_byte = 0; block_byte < sz; block_byte++)
{
bus->i2c_smbus_write_byte_data(dev, 0x01, data[block_byte]);
}
}
}
void CrucialController::SendEffectColor
@@ -27,12 +27,6 @@ using namespace std::chrono_literals;
static const unsigned char crucial_addresses[] =
{
/*-----------------------------------------------------*\
| These addresses have been disabled due to conflict |
| with ASUS Aura DRAM. Since the detection scheme is |
| the same, Aura RAM will be detected as Crucial. |
| We need to improve the Crucial detection scheme. |
\*-----------------------------------------------------*/
0x39,
0x3A,
0x3B,
@@ -81,7 +75,7 @@ unsigned char CrucialRegisterRead(i2c_smbus_interface* bus, crucial_dev_id dev,
* TestForCrucialController *
* *
* Tests the given address to see if an Crucial controller exists there. First does a*
* quick write to test for a response, and if so does a simple read at 0xA0 to test *
* byte read to test for a response, and if so does a simple read at 0xA0 to test *
* for incrementing values 0...F which was observed at this location during data dump *
* *
\******************************************************************************************/
@@ -90,19 +84,19 @@ bool TestForCrucialController(i2c_smbus_interface* bus, unsigned char address)
{
bool pass = false;
int res = bus->i2c_smbus_write_quick(address, I2C_SMBUS_WRITE);
int res = bus->i2c_smbus_read_byte(address);
if (res >= 0)
if(res >= 0)
{
pass = true;
LOG_DEBUG("[%s] Detected an I2C device at address %02X", CRUCIAL_CONTROLLER_NAME, address);
for (int i = 0xA0; i < 0xB0; i++)
for(int i = 0xA0; i < 0xB0; i++)
{
res = bus->i2c_smbus_read_byte_data(address, i);
if (res != (i - 0xA0))
if(res != (i - 0xA0))
{
LOG_VERBOSE("[%s] Detection failed testing register %02X. Expected %02X, got %02X.", CRUCIAL_CONTROLLER_NAME, i, (i - 0xA0), res);
@@ -178,7 +172,7 @@ void DetectCrucialControllers(std::vector<i2c_smbus_interface*> &busses)
{
for(unsigned int slot = 0; slot < 4; slot++)
{
int res = busses[bus]->i2c_smbus_write_quick(0x27, I2C_SMBUS_WRITE);
int res = busses[bus]->i2c_smbus_read_byte(0x27);
if(res < 0)
{
@@ -192,7 +186,7 @@ void DetectCrucialControllers(std::vector<i2c_smbus_interface*> &busses)
if(address_list_idx < CRUCIAL_ADDRESS_COUNT)
{
res = busses[bus]->i2c_smbus_write_quick(crucial_addresses[address_list_idx], I2C_SMBUS_WRITE);
res = busses[bus]->i2c_smbus_read_byte(crucial_addresses[address_list_idx]);
}
else
{
@@ -112,7 +112,7 @@ static void ENERegisterWrite(i2c_smbus_interface* bus, ene_dev_id dev, ene_regis
* TestForENESMBusController *
* *
* Tests the given address to see if an ENE controller exists there. First does a *
* quick write to test for a response, and if so does a simple read at 0xA0 to test *
* byte read to test for a response, and if so does a simple read at 0xA0 to test *
* for incrementing values 0...F which was observed at this location during data dump *
* *
* Also tests for the string "Micron" in the ENE register space. Crucial (Micron) *
@@ -202,7 +202,7 @@ void DetectENESMBusDRAMControllers(std::vector<i2c_smbus_interface*> &busses)
for (unsigned int slot = 0; slot < 8; slot++)
{
int res = busses[bus]->i2c_smbus_write_quick(0x77, I2C_SMBUS_WRITE);
int res = busses[bus]->i2c_smbus_read_byte(0x77);
if(res < 0)
{
@@ -219,7 +219,7 @@ void DetectENESMBusDRAMControllers(std::vector<i2c_smbus_interface*> &busses)
{
LOG_DEBUG("[ENE SMBus DRAM] Testing address %02X to see if there is a device there", ene_ram_addresses[address_list_idx]);
res = busses[bus]->i2c_smbus_write_quick(ene_ram_addresses[address_list_idx], I2C_SMBUS_WRITE);
res = busses[bus]->i2c_smbus_read_byte(ene_ram_addresses[address_list_idx]);
}
else
{
@@ -461,6 +461,8 @@ REGISTER_I2C_PCI_DETECTOR("ASUS ROG MATRIX PLATINUM GeForce RTX 4090",
REGISTER_I2C_PCI_DETECTOR("ASUS TUF GeForce RTX 5060 Gaming OC", DetectENESMBusGPUControllers, NVIDIA_VEN, NVIDIA_RTX5060_DEV, ASUS_SUB_VEN, ASUS_TUF_RTX_5060_O8G_GAMING, 0x67);
REGISTER_I2C_PCI_DETECTOR("ASUS TUF GeForce RTX 5070 Gaming OC", DetectENESMBusGPUControllers, NVIDIA_VEN, NVIDIA_RTX5070_DEV, ASUS_SUB_VEN, ASUS_TUF_RTX_5070_O12G_GAMING, 0x67);
REGISTER_I2C_PCI_DETECTOR("ASUS TUF GeForce RTX 5070 Ti Gaming OC", DetectENESMBusGPUControllers, NVIDIA_VEN, NVIDIA_RTX5070TI_DEV, ASUS_SUB_VEN, ASUS_TUF_RTX_5070TI_O16G_GAMING, 0x67);
REGISTER_I2C_PCI_DETECTOR("ASUS TUF GeForce RTX 5070 Ti Gaming BTF White OC", DetectENESMBusGPUControllers, NVIDIA_VEN, NVIDIA_RTX5070TI_DEV, ASUS_SUB_VEN, ASUS_TUF_RTX_5070TI_O16G_GAMING_BTF_WHITE, 0x67);
REGISTER_I2C_PCI_DETECTOR("ASUS TUF GeForce RTX 5070 Ti Gaming White OC", DetectENESMBusGPUControllers, NVIDIA_VEN, NVIDIA_RTX5070TI_DEV, ASUS_SUB_VEN, ASUS_TUF_RTX_5070TI_O16G_GAMING_WHITE, 0x67);
REGISTER_I2C_PCI_DETECTOR("ASUS ROG STRIX GeForce RTX 5070 Ti Gaming OC", DetectENESMBusGPUControllers, NVIDIA_VEN, NVIDIA_RTX5070TI_DEV, ASUS_SUB_VEN, ASUS_ROG_STRIX_RTX_5070TI_O16G_GAMING_OC, 0x67);
REGISTER_I2C_PCI_DETECTOR("ASUS TUF GeForce RTX 5080 Gaming OC", DetectENESMBusGPUControllers, NVIDIA_VEN, NVIDIA_RTX5080_DEV, ASUS_SUB_VEN, ASUS_TUF_RTX_5080_O16G_GAMING, 0x67);
REGISTER_I2C_PCI_DETECTOR("ASUS TUF GeForce RTX 5090 Gaming OC", DetectENESMBusGPUControllers, NVIDIA_VEN, NVIDIA_RTX5090_DEV, ASUS_SUB_VEN, ASUS_TUF_RTX_5090_O32G_GAMING, 0x67);
@@ -468,7 +470,9 @@ REGISTER_I2C_PCI_DETECTOR("ASUS TUF GeForce RTX 5090 Gaming",
REGISTER_I2C_PCI_DETECTOR("ASUS ROG ASTRAL GeForce RTX 5080 OC", DetectENESMBusGPUControllers, NVIDIA_VEN, NVIDIA_RTX5080_DEV, ASUS_SUB_VEN, ASUS_ROG_ASTRAL_RTX_5080_O16G_GAMING, 0x67);
REGISTER_I2C_PCI_DETECTOR("ASUS ROG ASTRAL GeForce RTX 5080 OC WHITE", DetectENESMBusGPUControllers, NVIDIA_VEN, NVIDIA_RTX5080_DEV, ASUS_SUB_VEN, ASUS_ROG_ASTRAL_RTX_5080_O16G_GAMING_WHITE, 0x67);
REGISTER_I2C_PCI_DETECTOR("ASUS ROG ASTRAL GeForce RTX 5080", DetectENESMBusGPUControllers, NVIDIA_VEN, NVIDIA_RTX5080_DEV, ASUS_SUB_VEN, ASUS_ROG_ASTRAL_RTX_5080_16G_GAMING, 0x67);
REGISTER_I2C_PCI_DETECTOR("ASUS ROG ASTRAL GeForce RTX 5080 WHITE", DetectENESMBusGPUControllers, NVIDIA_VEN, NVIDIA_RTX5080_DEV, ASUS_SUB_VEN, ASUS_ROG_ASTRAL_RTX_5080_16G_GAMING_WHITE, 0x67);
REGISTER_I2C_PCI_DETECTOR("ASUS ROG ASTRAL GeForce RTX 5090 OC", DetectENESMBusGPUControllers, NVIDIA_VEN, NVIDIA_RTX5090_DEV, ASUS_SUB_VEN, ASUS_ROG_ASTRAL_RTX_5090_O32G_GAMING, 0x67);
REGISTER_I2C_PCI_DETECTOR("ASUS ROG ASTRAL GeForce RTX 5090", DetectENESMBusGPUControllers, NVIDIA_VEN, NVIDIA_RTX5090_DEV, ASUS_SUB_VEN, ASUS_ROG_ASTRAL_RTX_5090_O32G_GAMING_2, 0x67);
REGISTER_I2C_PCI_DETECTOR("ASUS ROG ASTRAL GeForce RTX 5090 OC BTF", DetectENESMBusGPUControllers, NVIDIA_VEN, NVIDIA_RTX5090_DEV, ASUS_SUB_VEN, ASUS_ROG_ASTRAL_RTX_5090_O32G_GAMING_BTF, 0x67);
REGISTER_I2C_PCI_DETECTOR("ASUS ROG ASTRAL GeForce RTX 5090 OC WHITE", DetectENESMBusGPUControllers, NVIDIA_VEN, NVIDIA_RTX5090_DEV, ASUS_SUB_VEN, ASUS_ROG_ASTRAL_RTX_5090_O32G_GAMING_WHITE, 0x67);
REGISTER_I2C_PCI_DETECTOR("ASUS ROG ASTRAL LC GeForce RTX 5090 OC", DetectENESMBusGPUControllers, NVIDIA_VEN, NVIDIA_RTX5090_DEV, ASUS_SUB_VEN, ASUS_ROG_ASTRAL_LC_RTX_5090_O32G_GAMING, 0x67);
@@ -61,5 +61,12 @@ void ENESMBusInterface_i2c_smbus::ENERegisterWriteBlock(ene_dev_id dev, ene_regi
bus->i2c_smbus_write_word_data(dev, 0x00, ((reg << 8) & 0xFF00) | ((reg >> 8) & 0x00FF));
//Write ENE block data
bus->i2c_smbus_write_block_data(dev, 0x03, sz, data);
if(bus->i2c_smbus_write_block_data(dev, 0x03, sz, data) == -1)
{
//Fall back to individual byte operations if the block operation fails
for(unsigned int block_byte = 0; block_byte < sz; block_byte++)
{
bus->i2c_smbus_write_byte_data(dev, 0x01, data[block_byte]);
}
}
}
@@ -44,7 +44,7 @@ FnaticStreakController::~FnaticStreakController()
std::string FnaticStreakController::GetDeviceLocation()
{
return("HID " + location);
return("HID: " + location);
}
std::string FnaticStreakController::GetNameString()
@@ -230,16 +230,41 @@ void FnaticStreakController::SetLEDsDirect(std::vector<led> leds, std::vector<RG
void FnaticStreakController::SendRGBToDevice()
{
unsigned int total_leds = GetLEDCount();
unsigned int data_size = total_leds * 3;
if(keyboard_type == FNATIC_STREAK_TYPE_65)
{
unsigned char data[64];
memset(data, 0x00, sizeof(data));
/*-----------------------------------------------------*\
| For direct/software control, use command 0x0f |
| This bypasses the profile and allows immediate update |
| The 0x03 subcommand indicates per-key color data |
\*-----------------------------------------------------*/
unsigned char prefix[] = { 0x0f, 0x03 };
SendRequest(prefix, sizeof(prefix), color_buf, data_size);
data[0] = 0x0F;
data[1] = 0x15;
data[7] = 0x0F;
data[8] = 0x03;
for(unsigned int i = 9; i < 25; i++)
{
data[i] = 0xFF;
}
data[25] = color_buf[0];
data[26] = color_buf[1];
data[27] = color_buf[2];
hid_write(dev, data, sizeof(data));
}
else
{
unsigned int total_leds = GetLEDCount();
unsigned int data_size = total_leds * 3;
/*-----------------------------------------------------*\
| For direct/software control, use command 0x0f |
| This bypasses the profile and allows immediate update |
| The 0x03 subcommand indicates per-key color data |
\*-----------------------------------------------------*/
unsigned char prefix[] = { 0x0f, 0x03 };
SendRequest(prefix, sizeof(prefix), color_buf, data_size);
}
}
void FnaticStreakController::SetPulse(unsigned char color_mode, unsigned char r, unsigned char g, unsigned char b, unsigned char speed)
@@ -51,7 +51,8 @@
enum FnaticStreakType
{
FNATIC_STREAK_TYPE_FULL,
FNATIC_STREAK_TYPE_MINI
FNATIC_STREAK_TYPE_MINI,
FNATIC_STREAK_TYPE_65
};
class FnaticStreakController
@@ -26,6 +26,7 @@
#define FNATIC_STREAK_PID 0x0101
#define FNATIC_MINISTREAK_PID 0x0102
#define FNATIC_STREAK65_PID 0x0105
void DetectFnaticStreakKeyboard(hid_device_info* info, const std::string& name)
{
@@ -39,6 +40,10 @@ void DetectFnaticStreakKeyboard(hid_device_info* info, const std::string& name)
{
kb_type = FNATIC_STREAK_TYPE_MINI;
}
else if(info->product_id == FNATIC_STREAK65_PID)
{
kb_type = FNATIC_STREAK_TYPE_65;
}
else
{
kb_type = FNATIC_STREAK_TYPE_FULL;
@@ -53,3 +58,4 @@ void DetectFnaticStreakKeyboard(hid_device_info* info, const std::string& name)
REGISTER_HID_DETECTOR_I("Fnatic Streak", DetectFnaticStreakKeyboard, FNATIC_VID, FNATIC_STREAK_PID, 1);
REGISTER_HID_DETECTOR_I("Fnatic miniStreak", DetectFnaticStreakKeyboard, FNATIC_VID, FNATIC_MINISTREAK_PID, 1);
REGISTER_HID_DETECTOR_I("Fnatic Streak65", DetectFnaticStreakKeyboard, FNATIC_VID, FNATIC_STREAK65_PID, 1);
@@ -37,19 +37,22 @@ bool TestForGalaxGPUController(i2c_smbus_interface* bus, unsigned char address)
\*-----------------------------------------------------------------*/
case 0x32:
res = bus->i2c_smbus_read_byte_data(address, 0x00);
if(res == 0x27 || res == 0x26) {
if(res == 0x27 || res == 0x26)
{
res = bus->i2c_smbus_read_byte_data(address, 0x01);
if (res == 0x10 || res == 0x20)
if(res == 0x10 || res == 0x20)
{
pass = true;
}
}
break;
/*-----------------------------------------------------------------*\
| V1 Controller - RTX 3080 |
| V1 Controller |
\*-----------------------------------------------------------------*/
case 0x23:
res = bus->i2c_smbus_read_byte_data(address, 0x00);
if(res == 0x30)
if(res == 0x27 || res == 0x30)
{
pass = true;
}
@@ -60,8 +63,10 @@ bool TestForGalaxGPUController(i2c_smbus_interface* bus, unsigned char address)
\*-----------------------------------------------------------------*/
case 0x51:
res = bus->i2c_smbus_read_byte_data(address, 0x00);
if (res == 0x80)
if(res == 0x80)
{
pass = true;
}
break;
}
@@ -0,0 +1,238 @@
/*---------------------------------------------------------*\
| GigabyteCastor3Controller.cpp |
| |
| Driver for Gigabyte Aorus Waterforce X II 360 AIO |
| (Castor3 USB HID controller) |
| |
| RGB ring control only — LCD not implemented |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include <cstring>
#include "GigabyteCastor3Controller.h"
#include "StringUtils.h"
GigabyteCastor3Controller::GigabyteCastor3Controller(hid_device* dev_handle, const char* path)
{
dev = dev_handle;
location = path;
/*---------------------------------------------------------*\
| Query firmware version via de 00 |
| Response: de [version_byte] |
| Observed: de 02 -> version 2 |
\*---------------------------------------------------------*/
unsigned char de_payload[] = { 0xDE, 0x00 };
SendPacket(de_payload, 2);
unsigned char response[CASTOR3_IN_SIZE];
int bytes_read = hid_read_timeout(dev, response, CASTOR3_IN_SIZE, CASTOR3_IN_TIMEOUT_MS);
if(bytes_read > 1)
{
/*-----------------------------------------------------*\
| Strip report ID 0x99 if present on IN packets |
\*-----------------------------------------------------*/
int offset = 0;
if(response[0] == CASTOR3_REPORT_ID)
{
offset = 1;
}
if(response[offset] == 0xDE && bytes_read > offset + 1)
{
firmware_version = std::to_string(response[offset + 1]);
}
}
}
GigabyteCastor3Controller::~GigabyteCastor3Controller()
{
hid_close(dev);
}
std::string GigabyteCastor3Controller::GetDeviceLocation()
{
return("HID: " + location);
}
std::string GigabyteCastor3Controller::GetSerialString()
{
wchar_t serial_string[128];
int ret = hid_get_serial_number_string(dev, serial_string, 128);
if(ret != 0)
{
return("");
}
return(StringUtils::wstring_to_string(serial_string));
}
std::string GigabyteCastor3Controller::GetFirmwareVersion()
{
return firmware_version;
}
void GigabyteCastor3Controller::SetEffect
(
unsigned char style,
unsigned char speed,
unsigned char brightness,
unsigned char b4,
unsigned char b5,
castor3_color_type color_type,
std::vector<RGBColor> colors
)
{
/*---------------------------------------------------------*\
| Wire protocol for LED effect apply |
| (from castor3.py led_set_effect, confirmed from |
| led_profile.pcapng + .cled.dat cross-reference) |
| |
| Step 1: c9 [Style] [Speed*20] [Bright] [b4] [b5] |
| Step 2: cd [R] [G] [B] (single-color only) |
| -OR- b0..b3 palette registers (palette effects) |
| Step 3: b6 (commit) |
\*---------------------------------------------------------*/
/*---------------------------------------------------------*\
| Step 1: c9 — effect select + parameters |
\*---------------------------------------------------------*/
unsigned char c9_payload[] =
{
0xC9,
style,
(unsigned char)(speed * 20),
brightness,
b4,
b5
};
SendPacket(c9_payload, sizeof(c9_payload));
/*---------------------------------------------------------*\
| Step 2a: cd — primary color for single-color effects |
| Effects: static, pulse, flash, dflash, gradient, off |
\*---------------------------------------------------------*/
if(color_type == CASTOR3_COLORS_SINGLE)
{
unsigned char r = 0xFF, g = 0x66, b = 0x00;
if(colors.size() > 0)
{
r = RGBGetRValue(colors[0]);
g = RGBGetGValue(colors[0]);
b = RGBGetBValue(colors[0]);
}
unsigned char cd_payload[] = { 0xCD, r, g, b };
SendPacket(cd_payload, sizeof(cd_payload));
}
/*---------------------------------------------------------*\
| Step 2b: b0..b3 — palette registers for multi-color |
| Each register carries 2 colors (6 bytes RGB + 1 pad): |
| bN [Style] [R1 G1 B1] [R2 G2 B2] 0x00 |
| b0=colors 1+2, b1=colors 3+4, b2=colors 5+6, b3=7+8 |
| |
| Effects: colorshift(8), tricolor(3), spin(3), switch(2) |
\*---------------------------------------------------------*/
else if(color_type == CASTOR3_COLORS_PALETTE)
{
/*-----------------------------------------------------*\
| Build palette: pad to 8 colors by repeating last |
\*-----------------------------------------------------*/
RGBColor palette[8];
for(int i = 0; i < 8; i++)
{
if(i < (int)colors.size())
{
palette[i] = colors[i];
}
else if(colors.size() > 0)
{
palette[i] = colors[colors.size() - 1];
}
else
{
palette[i] = ToRGBColor(0xFF, 0x00, 0x00);
}
}
unsigned char regs[] = { 0xB0, 0xB1, 0xB2, 0xB3 };
for(int i = 0; i < 4; i++)
{
RGBColor c1 = palette[i * 2];
RGBColor c2 = palette[i * 2 + 1];
unsigned char bN_payload[] =
{
regs[i],
style,
(unsigned char)RGBGetRValue(c1), (unsigned char)RGBGetGValue(c1), (unsigned char)RGBGetBValue(c1),
(unsigned char)RGBGetRValue(c2), (unsigned char)RGBGetGValue(c2), (unsigned char)RGBGetBValue(c2),
0x00
};
SendPacket(bN_payload, sizeof(bN_payload));
}
}
/*---------------------------------------------------------*\
| Step 3: b6 — commit/apply |
\*---------------------------------------------------------*/
unsigned char b6_payload[] = { 0xB6 };
SendPacket(b6_payload, sizeof(b6_payload));
}
void GigabyteCastor3Controller::SetOff()
{
/*---------------------------------------------------------*\
| Off = Style 0x01 (Static) with Speed=0 |
| From castor3.py: off uses style 0x01, speed_wire=0 |
\*---------------------------------------------------------*/
std::vector<RGBColor> black;
black.push_back(ToRGBColor(0x00, 0x00, 0x00));
unsigned char c9_payload[] =
{
0xC9,
CASTOR3_STYLE_STATIC,
0x00, /* speed_wire = 0 for off */
CASTOR3_BRIGHTNESS_DEFAULT,
0x02, /* b4 */
0x01 /* b5 */
};
SendPacket(c9_payload, sizeof(c9_payload));
unsigned char cd_payload[] = { 0xCD, 0x00, 0x00, 0x00 };
SendPacket(cd_payload, sizeof(cd_payload));
unsigned char b6_payload[] = { 0xB6 };
SendPacket(b6_payload, sizeof(b6_payload));
}
void GigabyteCastor3Controller::SendPacket(const unsigned char* payload, unsigned int payload_len)
{
/*---------------------------------------------------------*\
| Castor3 HID OUT transport: |
| - Byte 0: Report ID (0x99) |
| - Bytes 1..6143: payload, zero-padded |
| Total write size: 6144 bytes |
\*---------------------------------------------------------*/
unsigned char buf[CASTOR3_OUT_TOTAL];
memset(buf, 0x00, CASTOR3_OUT_TOTAL);
buf[0] = CASTOR3_REPORT_ID;
if(payload_len > CASTOR3_OUT_PAYLOAD)
{
payload_len = CASTOR3_OUT_PAYLOAD;
}
memcpy(&buf[1], payload, payload_len);
hid_write(dev, buf, CASTOR3_OUT_TOTAL);
}
@@ -0,0 +1,123 @@
/*---------------------------------------------------------*\
| GigabyteCastor3Controller.h |
| |
| Driver for Gigabyte Aorus Waterforce X II 360 AIO |
| (Castor3 USB HID controller) |
| |
| RGB ring control only — LCD not implemented |
| |
| Protocol reference: castor3.py reverse engineering |
| VID=0x0414 PID=0x7A5E Report ID=0x99 |
| OUT size: 6144 (report ID + 6143 payload) |
| IN size: 255 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#pragma once
#include <string>
#include <vector>
#include <hidapi.h>
#include "RGBController.h"
/*---------------------------------------------------------*\
| Castor3 USB IDs |
\*---------------------------------------------------------*/
#define CASTOR3_VID 0x0414
#define CASTOR3_PID 0x7A5E
/*---------------------------------------------------------*\
| Castor3 HID transport |
\*---------------------------------------------------------*/
#define CASTOR3_REPORT_ID 0x99
#define CASTOR3_OUT_PAYLOAD 6143 /* payload after report ID */
#define CASTOR3_OUT_TOTAL 6144 /* report ID + payload */
#define CASTOR3_IN_SIZE 255
#define CASTOR3_IN_TIMEOUT_MS 2000
/*---------------------------------------------------------*\
| LED effect style IDs (c9 byte[1]) |
| From castor3.py LED_EFFECTS, cross-referenced with |
| .cled.dat profile files and led_profile.pcapng |
\*---------------------------------------------------------*/
#define CASTOR3_STYLE_STATIC 0x01
#define CASTOR3_STYLE_PULSE 0x02
#define CASTOR3_STYLE_CYCLE 0x03
#define CASTOR3_STYLE_FLASH 0x04
#define CASTOR3_STYLE_DFLASH 0x05
#define CASTOR3_STYLE_GRADIENT 0x06
#define CASTOR3_STYLE_COLORSHIFT 0x07
#define CASTOR3_STYLE_WAVE 0x08
#define CASTOR3_STYLE_RAINBOW 0x0A
#define CASTOR3_STYLE_TRICOLOR 0x0B
#define CASTOR3_STYLE_SPIN 0x0C
#define CASTOR3_STYLE_SWITCH 0x0D
/*---------------------------------------------------------*\
| LED speed range: 1-5 (wire value = speed * 20) |
| LED brightness range: 1-10 (direct on wire) |
\*---------------------------------------------------------*/
#define CASTOR3_SPEED_MIN 1
#define CASTOR3_SPEED_MAX 5
#define CASTOR3_SPEED_DEFAULT 5
#define CASTOR3_BRIGHTNESS_MIN 1
#define CASTOR3_BRIGHTNESS_MAX 10
#define CASTOR3_BRIGHTNESS_DEFAULT 10
/*---------------------------------------------------------*\
| Color type for effects |
\*---------------------------------------------------------*/
enum castor3_color_type
{
CASTOR3_COLORS_NONE = 0, /* cycle, wave, rainbow */
CASTOR3_COLORS_SINGLE = 1, /* static, pulse, flash... */
CASTOR3_COLORS_PALETTE = 2, /* colorshift, tricolor... */
};
class GigabyteCastor3Controller
{
public:
GigabyteCastor3Controller(hid_device* dev_handle, const char* path);
~GigabyteCastor3Controller();
std::string GetDeviceLocation();
std::string GetSerialString();
std::string GetFirmwareVersion();
/*---------------------------------------------------------*\
| LED effect application |
| |
| Wire sequence per effect apply: |
| c9 [Style] [Speed*20] [Bright] [b4] [b5] |
| cd [R] [G] [B] — single-color only |
| b0 [Style] [R1G1B1] [R2G2B2] 00 — palette colors 1+2 |
| b1 [Style] [R3G3B3] [R4G4B4] 00 — colors 3+4 |
| b2 [Style] [R5G5B5] [R6G6B6] 00 — colors 5+6 |
| b3 [Style] [R7G7B7] [R8G8B8] 00 — colors 7+8 |
| b6 — commit/apply |
\*---------------------------------------------------------*/
void SetEffect(unsigned char style,
unsigned char speed,
unsigned char brightness,
unsigned char b4,
unsigned char b5,
castor3_color_type color_type,
std::vector<RGBColor> colors);
void SetOff();
private:
hid_device* dev;
std::string location;
std::string firmware_version;
void SendPacket(const unsigned char* payload, unsigned int payload_len);
};
@@ -0,0 +1,39 @@
/*---------------------------------------------------------*\
| GigabyteCastor3ControllerDetect.cpp |
| |
| Detector for Gigabyte Aorus Waterforce X II 360 AIO |
| (Castor3 USB HID controller) |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include <hidapi.h>
#include "Detector.h"
#include "GigabyteCastor3Controller.h"
#include "RGBController_GigabyteCastor3.h"
#define GIGABYTE_CASTOR3_VID 0x0414
#define GIGABYTE_CASTOR3_PID 0x7A5E
void DetectGigabyteCastor3Controllers(hid_device_info* info, const std::string& name)
{
hid_device* dev = hid_open_path(info->path);
if(dev)
{
GigabyteCastor3Controller* controller = new GigabyteCastor3Controller(dev, info->path);
RGBController_GigabyteCastor3* rgb_controller = new RGBController_GigabyteCastor3(controller);
rgb_controller->name = name;
ResourceManager::get()->RegisterRGBController(rgb_controller);
}
}
REGISTER_HID_DETECTOR_IPU("Gigabyte Aorus Waterforce X II 360",
DetectGigabyteCastor3Controllers,
GIGABYTE_CASTOR3_VID,
GIGABYTE_CASTOR3_PID,
0, /* interface 0 */
0x0000, /* usage_page */
0x0002); /* usage */
@@ -0,0 +1,446 @@
/*---------------------------------------------------------*\
| RGBController_GigabyteCastor3.cpp |
| |
| RGBController for Gigabyte Aorus Waterforce X II 360 |
| AIO Cooler (Castor3 controller) |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include "RGBController_GigabyteCastor3.h"
/**------------------------------------------------------------------*\
@name Gigabyte Aorus Waterforce X II 360
@category Cooler
@type USB
@save :o:
@direct :o:
@effects :white_check_mark:
@detectors DetectGigabyteCastor3Controllers
@comment Controls the LED ring on the pump head of the
Gigabyte Aorus Waterforce X II 360 AIO cooler.
Uses the Castor3 USB HID controller (VID 0x0414, PID 0x7A5E).
The LED ring and fans share the same controller — they are
not independently addressable and follow the same effect.
LCD display control is not implemented.
No direct/per-LED mode available — all modes are hardware
effects with a single color or palette.
\*-------------------------------------------------------------------*/
RGBController_GigabyteCastor3::RGBController_GigabyteCastor3(GigabyteCastor3Controller* controller_ptr)
{
controller = controller_ptr;
name = "Gigabyte Aorus Waterforce X II 360";
vendor = "Gigabyte";
type = DEVICE_TYPE_COOLER;
description = "Gigabyte Aorus Waterforce X II 360 AIO Cooler";
location = controller->GetDeviceLocation();
serial = controller->GetSerialString();
version = controller->GetFirmwareVersion();
/*---------------------------------------------------------*\
| All modes from castor3.py LED_EFFECTS (12 effects + off) |
| |
| The Castor3 has no direct/per-LED mode. All effects are |
| hardware-driven, either single-color, palette, or |
| firmware-color (no user color). |
| |
| Speed: 1-5 mapped to MODE_FLAG_HAS_SPEED |
| Brightness: 1-10 mapped to MODE_FLAG_HAS_BRIGHTNESS |
| |
| mode.value stores the style byte for the c9 command. |
| mode.speed stores 1-5 (scaled to speed*20 on wire). |
| mode.brightness stores 1-10 (direct on wire). |
\*---------------------------------------------------------*/
/*---------------------------------------------------------*\
| Off |
| Wire: Style=0x01 (Static) with speed_wire=0 |
\*---------------------------------------------------------*/
mode Off;
Off.name = "Off";
Off.value = 0xFF; /* sentinel — handled specially */
Off.flags = 0;
Off.color_mode = MODE_COLORS_NONE;
modes.push_back(Off);
/*---------------------------------------------------------*\
| Static — single color, no speed |
\*---------------------------------------------------------*/
mode Static;
Static.name = "Static";
Static.value = CASTOR3_STYLE_STATIC;
Static.flags = MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS;
Static.colors_min = 1;
Static.colors_max = 1;
Static.color_mode = MODE_COLORS_MODE_SPECIFIC;
Static.brightness_min = CASTOR3_BRIGHTNESS_MIN;
Static.brightness_max = CASTOR3_BRIGHTNESS_MAX;
Static.brightness = CASTOR3_BRIGHTNESS_DEFAULT;
Static.colors.resize(1);
modes.push_back(Static);
/*---------------------------------------------------------*\
| Pulse — single color, speed + brightness |
\*---------------------------------------------------------*/
mode Pulse;
Pulse.name = "Pulse";
Pulse.value = CASTOR3_STYLE_PULSE;
Pulse.flags = MODE_FLAG_HAS_SPEED | MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS;
Pulse.speed_min = CASTOR3_SPEED_MIN;
Pulse.speed_max = CASTOR3_SPEED_MAX;
Pulse.speed = CASTOR3_SPEED_DEFAULT;
Pulse.colors_min = 1;
Pulse.colors_max = 1;
Pulse.color_mode = MODE_COLORS_MODE_SPECIFIC;
Pulse.brightness_min = CASTOR3_BRIGHTNESS_MIN;
Pulse.brightness_max = CASTOR3_BRIGHTNESS_MAX;
Pulse.brightness = CASTOR3_BRIGHTNESS_DEFAULT;
Pulse.colors.resize(1);
modes.push_back(Pulse);
/*---------------------------------------------------------*\
| Flash — single color, speed + brightness |
\*---------------------------------------------------------*/
mode Flash;
Flash.name = "Flash";
Flash.value = CASTOR3_STYLE_FLASH;
Flash.flags = MODE_FLAG_HAS_SPEED | MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS;
Flash.speed_min = CASTOR3_SPEED_MIN;
Flash.speed_max = CASTOR3_SPEED_MAX;
Flash.speed = CASTOR3_SPEED_DEFAULT;
Flash.colors_min = 1;
Flash.colors_max = 1;
Flash.color_mode = MODE_COLORS_MODE_SPECIFIC;
Flash.brightness_min = CASTOR3_BRIGHTNESS_MIN;
Flash.brightness_max = CASTOR3_BRIGHTNESS_MAX;
Flash.brightness = CASTOR3_BRIGHTNESS_DEFAULT;
Flash.colors.resize(1);
modes.push_back(Flash);
/*---------------------------------------------------------*\
| Double Flash — single color, speed + brightness |
\*---------------------------------------------------------*/
mode DFlash;
DFlash.name = "Double Flash";
DFlash.value = CASTOR3_STYLE_DFLASH;
DFlash.flags = MODE_FLAG_HAS_SPEED | MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS;
DFlash.speed_min = CASTOR3_SPEED_MIN;
DFlash.speed_max = CASTOR3_SPEED_MAX;
DFlash.speed = CASTOR3_SPEED_DEFAULT;
DFlash.colors_min = 1;
DFlash.colors_max = 1;
DFlash.color_mode = MODE_COLORS_MODE_SPECIFIC;
DFlash.brightness_min = CASTOR3_BRIGHTNESS_MIN;
DFlash.brightness_max = CASTOR3_BRIGHTNESS_MAX;
DFlash.brightness = CASTOR3_BRIGHTNESS_DEFAULT;
DFlash.colors.resize(1);
modes.push_back(DFlash);
/*---------------------------------------------------------*\
| Cycle — firmware colors, speed only |
\*---------------------------------------------------------*/
mode Cycle;
Cycle.name = "Spectrum Cycle";
Cycle.value = CASTOR3_STYLE_CYCLE;
Cycle.flags = MODE_FLAG_HAS_SPEED | MODE_FLAG_HAS_BRIGHTNESS;
Cycle.speed_min = CASTOR3_SPEED_MIN;
Cycle.speed_max = CASTOR3_SPEED_MAX;
Cycle.speed = CASTOR3_SPEED_DEFAULT;
Cycle.color_mode = MODE_COLORS_NONE;
Cycle.brightness_min = CASTOR3_BRIGHTNESS_MIN;
Cycle.brightness_max = CASTOR3_BRIGHTNESS_MAX;
Cycle.brightness = CASTOR3_BRIGHTNESS_DEFAULT;
modes.push_back(Cycle);
/*---------------------------------------------------------*\
| Gradient — single color, speed + brightness |
| Note: b4 = brightness on wire (special case) |
\*---------------------------------------------------------*/
mode Gradient;
Gradient.name = "Gradient";
Gradient.value = CASTOR3_STYLE_GRADIENT;
Gradient.flags = MODE_FLAG_HAS_SPEED | MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS;
Gradient.speed_min = CASTOR3_SPEED_MIN;
Gradient.speed_max = CASTOR3_SPEED_MAX;
Gradient.speed = CASTOR3_SPEED_DEFAULT;
Gradient.colors_min = 1;
Gradient.colors_max = 1;
Gradient.color_mode = MODE_COLORS_MODE_SPECIFIC;
Gradient.brightness_min = CASTOR3_BRIGHTNESS_MIN;
Gradient.brightness_max = CASTOR3_BRIGHTNESS_MAX;
Gradient.brightness = CASTOR3_BRIGHTNESS_DEFAULT;
Gradient.colors.resize(1);
modes.push_back(Gradient);
/*---------------------------------------------------------*\
| Color Shift — 8-color palette, speed + brightness |
\*---------------------------------------------------------*/
mode ColorShift;
ColorShift.name = "Color Shift";
ColorShift.value = CASTOR3_STYLE_COLORSHIFT;
ColorShift.flags = MODE_FLAG_HAS_SPEED | MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS;
ColorShift.speed_min = CASTOR3_SPEED_MIN;
ColorShift.speed_max = CASTOR3_SPEED_MAX;
ColorShift.speed = CASTOR3_SPEED_DEFAULT;
ColorShift.colors_min = 1;
ColorShift.colors_max = 8;
ColorShift.color_mode = MODE_COLORS_MODE_SPECIFIC;
ColorShift.brightness_min = CASTOR3_BRIGHTNESS_MIN;
ColorShift.brightness_max = CASTOR3_BRIGHTNESS_MAX;
ColorShift.brightness = CASTOR3_BRIGHTNESS_DEFAULT;
ColorShift.colors.resize(8);
ColorShift.colors[0] = ToRGBColor(0xFF, 0x00, 0x00);
ColorShift.colors[1] = ToRGBColor(0xFF, 0x72, 0x00);
ColorShift.colors[2] = ToRGBColor(0xFF, 0xFF, 0x00);
ColorShift.colors[3] = ToRGBColor(0x00, 0xFF, 0x00);
ColorShift.colors[4] = ToRGBColor(0x00, 0xFF, 0xFF);
ColorShift.colors[5] = ToRGBColor(0x00, 0x00, 0xFF);
ColorShift.colors[6] = ToRGBColor(0xFF, 0x00, 0xFF);
ColorShift.colors[7] = ToRGBColor(0xFF, 0x80, 0x80);
modes.push_back(ColorShift);
/*---------------------------------------------------------*\
| Wave — firmware colors, speed only |
\*---------------------------------------------------------*/
mode Wave;
Wave.name = "Wave";
Wave.value = CASTOR3_STYLE_WAVE;
Wave.flags = MODE_FLAG_HAS_SPEED | MODE_FLAG_HAS_BRIGHTNESS;
Wave.speed_min = CASTOR3_SPEED_MIN;
Wave.speed_max = CASTOR3_SPEED_MAX;
Wave.speed = CASTOR3_SPEED_DEFAULT;
Wave.color_mode = MODE_COLORS_NONE;
Wave.brightness_min = CASTOR3_BRIGHTNESS_MIN;
Wave.brightness_max = CASTOR3_BRIGHTNESS_MAX;
Wave.brightness = CASTOR3_BRIGHTNESS_DEFAULT;
modes.push_back(Wave);
/*---------------------------------------------------------*\
| Rainbow — firmware colors, speed only |
\*---------------------------------------------------------*/
mode Rainbow;
Rainbow.name = "Rainbow";
Rainbow.value = CASTOR3_STYLE_RAINBOW;
Rainbow.flags = MODE_FLAG_HAS_SPEED | MODE_FLAG_HAS_BRIGHTNESS;
Rainbow.speed_min = CASTOR3_SPEED_MIN;
Rainbow.speed_max = CASTOR3_SPEED_MAX;
Rainbow.speed = CASTOR3_SPEED_DEFAULT;
Rainbow.color_mode = MODE_COLORS_NONE;
Rainbow.brightness_min = CASTOR3_BRIGHTNESS_MIN;
Rainbow.brightness_max = CASTOR3_BRIGHTNESS_MAX;
Rainbow.brightness = CASTOR3_BRIGHTNESS_DEFAULT;
modes.push_back(Rainbow);
/*---------------------------------------------------------*\
| Tri-Color — 3-color palette, speed + brightness |
\*---------------------------------------------------------*/
mode TriColor;
TriColor.name = "Tri-Color";
TriColor.value = CASTOR3_STYLE_TRICOLOR;
TriColor.flags = MODE_FLAG_HAS_SPEED | MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS;
TriColor.speed_min = CASTOR3_SPEED_MIN;
TriColor.speed_max = CASTOR3_SPEED_MAX;
TriColor.speed = CASTOR3_SPEED_DEFAULT;
TriColor.colors_min = 1;
TriColor.colors_max = 3;
TriColor.color_mode = MODE_COLORS_MODE_SPECIFIC;
TriColor.brightness_min = CASTOR3_BRIGHTNESS_MIN;
TriColor.brightness_max = CASTOR3_BRIGHTNESS_MAX;
TriColor.brightness = CASTOR3_BRIGHTNESS_DEFAULT;
TriColor.colors.resize(3);
TriColor.colors[0] = ToRGBColor(0x00, 0x00, 0xFF);
TriColor.colors[1] = ToRGBColor(0x7D, 0x00, 0xFF);
TriColor.colors[2] = ToRGBColor(0xFF, 0x00, 0xFF);
modes.push_back(TriColor);
/*---------------------------------------------------------*\
| Spin — 3-color palette, speed + brightness |
\*---------------------------------------------------------*/
mode Spin;
Spin.name = "Spin";
Spin.value = CASTOR3_STYLE_SPIN;
Spin.flags = MODE_FLAG_HAS_SPEED | MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS;
Spin.speed_min = CASTOR3_SPEED_MIN;
Spin.speed_max = CASTOR3_SPEED_MAX;
Spin.speed = CASTOR3_SPEED_DEFAULT;
Spin.colors_min = 1;
Spin.colors_max = 3;
Spin.color_mode = MODE_COLORS_MODE_SPECIFIC;
Spin.brightness_min = CASTOR3_BRIGHTNESS_MIN;
Spin.brightness_max = CASTOR3_BRIGHTNESS_MAX;
Spin.brightness = CASTOR3_BRIGHTNESS_DEFAULT;
Spin.colors.resize(3);
Spin.colors[0] = ToRGBColor(0xFF, 0x00, 0xFE);
Spin.colors[1] = ToRGBColor(0x00, 0xFF, 0xFB);
Spin.colors[2] = ToRGBColor(0xFF, 0xFF, 0x00);
modes.push_back(Spin);
/*---------------------------------------------------------*\
| Switch — 2-color palette, speed + brightness |
\*---------------------------------------------------------*/
mode Switch;
Switch.name = "Switch";
Switch.value = CASTOR3_STYLE_SWITCH;
Switch.flags = MODE_FLAG_HAS_SPEED | MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS;
Switch.speed_min = CASTOR3_SPEED_MIN;
Switch.speed_max = CASTOR3_SPEED_MAX;
Switch.speed = CASTOR3_SPEED_DEFAULT;
Switch.colors_min = 1;
Switch.colors_max = 2;
Switch.color_mode = MODE_COLORS_MODE_SPECIFIC;
Switch.brightness_min = CASTOR3_BRIGHTNESS_MIN;
Switch.brightness_max = CASTOR3_BRIGHTNESS_MAX;
Switch.brightness = CASTOR3_BRIGHTNESS_DEFAULT;
Switch.colors.resize(2);
Switch.colors[0] = ToRGBColor(0xFF, 0x00, 0xFE);
Switch.colors[1] = ToRGBColor(0x00, 0xFF, 0xFB);
modes.push_back(Switch);
SetupZones();
}
RGBController_GigabyteCastor3::~RGBController_GigabyteCastor3()
{
delete controller;
}
void RGBController_GigabyteCastor3::SetupZones()
{
/*---------------------------------------------------------*\
| Single zone: LED ring + fans (not independently |
| addressable — fans follow the pump ring effect) |
| |
| This is a SINGLE zone because the protocol does not |
| expose per-LED addressing. All LEDs display the same |
| hardware effect simultaneously. |
\*---------------------------------------------------------*/
zone led_ring;
led_ring.name = "LED Ring";
led_ring.type = ZONE_TYPE_SINGLE;
led_ring.leds_min = 1;
led_ring.leds_max = 1;
led_ring.leds_count = 1;
led_ring.matrix_map = NULL;
zones.push_back(led_ring);
led new_led;
new_led.name = "LED Ring";
leds.push_back(new_led);
SetupColors();
}
void RGBController_GigabyteCastor3::ResizeZone(int /*zone*/, int /*new_size*/)
{
/*---------------------------------------------------------*\
| Fixed zone size — not resizable |
\*---------------------------------------------------------*/
}
void RGBController_GigabyteCastor3::DeviceUpdateLEDs()
{
DeviceUpdateMode();
}
void RGBController_GigabyteCastor3::UpdateZoneLEDs(int /*zone*/)
{
DeviceUpdateLEDs();
}
void RGBController_GigabyteCastor3::UpdateSingleLED(int /*led*/)
{
DeviceUpdateLEDs();
}
void RGBController_GigabyteCastor3::DeviceUpdateMode()
{
unsigned char style = (unsigned char)modes[active_mode].value;
/*---------------------------------------------------------*\
| Handle Off mode (sentinel value 0xFF) |
\*---------------------------------------------------------*/
if(style == 0xFF)
{
controller->SetOff();
return;
}
/*---------------------------------------------------------*\
| Determine speed, brightness, b4, b5, and color type |
| from the effect lookup table matching castor3.py |
\*---------------------------------------------------------*/
unsigned char speed = CASTOR3_SPEED_DEFAULT;
unsigned char brightness = CASTOR3_BRIGHTNESS_DEFAULT;
if(modes[active_mode].flags & MODE_FLAG_HAS_SPEED)
{
speed = (unsigned char)modes[active_mode].speed;
}
if(modes[active_mode].flags & MODE_FLAG_HAS_BRIGHTNESS)
{
brightness = (unsigned char)modes[active_mode].brightness;
}
/*---------------------------------------------------------*\
| Look up b4, b5, and color_type per effect |
| These match the LED_EFFECTS dict in castor3.py |
\*---------------------------------------------------------*/
unsigned char b4 = 0x02;
unsigned char b5 = 0x01;
castor3_color_type color_type = CASTOR3_COLORS_NONE;
switch(style)
{
case CASTOR3_STYLE_STATIC:
case CASTOR3_STYLE_PULSE:
case CASTOR3_STYLE_FLASH:
case CASTOR3_STYLE_DFLASH:
b4 = 0x02;
b5 = 0x01;
color_type = CASTOR3_COLORS_SINGLE;
break;
case CASTOR3_STYLE_GRADIENT:
/*-------------------------------------------------*\
| Gradient: b4 = brightness on wire (special case) |
\*-------------------------------------------------*/
b4 = brightness;
b5 = 0x01;
color_type = CASTOR3_COLORS_SINGLE;
break;
case CASTOR3_STYLE_CYCLE:
case CASTOR3_STYLE_WAVE:
case CASTOR3_STYLE_RAINBOW:
b4 = 0x02;
b5 = 0x01;
color_type = CASTOR3_COLORS_NONE;
break;
case CASTOR3_STYLE_COLORSHIFT:
b4 = 0x08; /* ClrCount=8 */
b5 = 0x02; /* CmbIndex+1=2 */
color_type = CASTOR3_COLORS_PALETTE;
break;
case CASTOR3_STYLE_TRICOLOR:
case CASTOR3_STYLE_SPIN:
b4 = 0x02;
b5 = 0x01;
color_type = CASTOR3_COLORS_PALETTE;
break;
case CASTOR3_STYLE_SWITCH:
b4 = 0x02;
b5 = 0x01;
color_type = CASTOR3_COLORS_PALETTE;
break;
}
controller->SetEffect(style, speed, brightness, b4, b5,
color_type, modes[active_mode].colors);
}
@@ -0,0 +1,33 @@
/*---------------------------------------------------------*\
| RGBController_GigabyteCastor3.h |
| |
| RGBController for Gigabyte Aorus Waterforce X II 360 |
| AIO Cooler (Castor3 controller) |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#pragma once
#include "RGBController.h"
#include "GigabyteCastor3Controller.h"
class RGBController_GigabyteCastor3 : public RGBController
{
public:
RGBController_GigabyteCastor3(GigabyteCastor3Controller* controller_ptr);
~RGBController_GigabyteCastor3();
void SetupZones();
void ResizeZone(int zone, int new_size);
void DeviceUpdateLEDs();
void UpdateZoneLEDs(int zone);
void UpdateSingleLED(int led);
void DeviceUpdateMode();
private:
GigabyteCastor3Controller* controller;
};
@@ -68,9 +68,14 @@ void RGBFusion2BlackwellGPUController::SetMode(uint8_t type, uint8_t zone, uint8
if(zone_config.numberOfColors > 0)
{
int currentPos = 12;
if(gpu_layout == RGB_FUSION2_BLACKWELL_GPU_AORUS_MASTER_5080_LAYOUT)
switch(gpu_layout)
{
currentPos = 11;
case RGB_FUSION2_BLACKWELL_GPU_AORUS_MASTER_5080_LAYOUT:
case RGB_FUSION2_BLACKWELL_GPU_AORUS_MASTER_5090D_V2_ICE_LAYOUT:
currentPos = 11;
break;
default:
break;
}
for(uint8_t i = 0; i < zone_config.numberOfColors; i++)
@@ -90,6 +95,16 @@ void RGBFusion2BlackwellGPUController::SetZone(uint8_t zone, uint8_t mode, fusio
if(mode == RGB_FUSION2_BLACKWELL_GPU_MODE_BREATHING)
zone_config.brightness = RGB_FUSION2_BLACKWELL_GPU_BRIGHTNESS_MAX;
switch(gpu_layout)
{
case RGB_FUSION2_BLACKWELL_GPU_AORUS_MASTER_5090D_V2_ICE_LAYOUT:
if(mode == RGB_FUSION2_BLACKWELL_GPU_MODE_DIRECT)
mode = RGB_FUSION2_BLACKWELL_GPU_MODE_STATIC;
break;
default:
break;
}
uint8_t type = RGB_FUSION2_BLACKWELL_GPU_REG_COLOR;
if(mode != RGB_FUSION2_BLACKWELL_GPU_MODE_DIRECT)
type = RGB_FUSION2_BLACKWELL_GPU_REG_MODE;
@@ -62,11 +62,12 @@ enum
enum
{
RGB_FUSION2_BLACKWELL_GPU_SINGLE_ZONE = 0,
RGB_FUSION2_BLACKWELL_GPU_GAMING_LAYOUT = 1,
RGB_FUSION2_BLACKWELL_GPU_WATERFORCE_LAYOUT = 2,
RGB_FUSION2_BLACKWELL_GPU_AORUS_WATERFORCE_LAYOUT = 3,
RGB_FUSION2_BLACKWELL_GPU_AORUS_MASTER_5080_LAYOUT = 4,
RGB_FUSION2_BLACKWELL_GPU_SINGLE_ZONE = 0,
RGB_FUSION2_BLACKWELL_GPU_GAMING_LAYOUT = 1,
RGB_FUSION2_BLACKWELL_GPU_WATERFORCE_LAYOUT = 2,
RGB_FUSION2_BLACKWELL_GPU_AORUS_WATERFORCE_LAYOUT = 3,
RGB_FUSION2_BLACKWELL_GPU_AORUS_MASTER_5080_LAYOUT = 4,
RGB_FUSION2_BLACKWELL_GPU_AORUS_MASTER_5090D_V2_ICE_LAYOUT = 5,
};
class RGBFusion2BlackwellGPUController
@@ -1,4 +1,4 @@
/*---------------------------------------------------------*\
/*---------------------------------------------------------*\
| GigabyteRGBFusion2BlackwellGPUControllerDetect.cpp |
| |
| Detector for Gigabyte RGB Fusion 2 Blackwell GPU |
@@ -181,29 +181,48 @@ void DetectGigabyteRGBFusion2BlackwellAorusMaster5080LayoutGPUControllers(i2c_sm
DetectGigabyteRGBFusion2BlackwellGPUControllers(bus, i2c_addr, name, RGB_FUSION2_BLACKWELL_GPU_AORUS_MASTER_5080_LAYOUT);
} /* DetectGigabyteRGBFusion2BlackwellAorusMaster5080LayoutGPUControllers() */
/*******************************************************************************************\
* *
* DetectGigabyteRGBFusion2BlackwellAorusMaster5090DV2IceLayoutGPUControllers *
* *
* Detect GigabyteRGB Fusion2 controllers with AORUS master 5090 D V2 ICE layout on *
* enumerated I2C busses. *
* *
* bus - pointer to i2c_smbus_interface where RGB Fusion2 device is connected *
* dev - I2C address of RGB Fusion2 device *
* *
\*******************************************************************************************/
void DetectGigabyteRGBFusion2BlackwellAorusMaster5090DV2IceLayoutGPUControllers(i2c_smbus_interface* bus, uint8_t i2c_addr, const std::string& name)
{
DetectGigabyteRGBFusion2BlackwellGPUControllers(bus, i2c_addr, name, RGB_FUSION2_BLACKWELL_GPU_AORUS_MASTER_5090D_V2_ICE_LAYOUT);
} /* DetectGigabyteRGBFusion2BlackwellAorusMaster5090DV2IceLayoutGPUControllers() */
/*-----------------------------------------*\
| Nvidia GPUs |
\*-----------------------------------------*/
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 5060 Ti Gaming OC", DetectGigabyteRGBFusion2BlackwellSingleZoneGPUControllers, NVIDIA_VEN, NVIDIA_RTX5060TI_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX5060TI_GAMING_OC_16G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 5070 Aero OC", DetectGigabyteRGBFusion2BlackwellSingleZoneGPUControllers, NVIDIA_VEN, NVIDIA_RTX5070_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX5070_AERO_OC_12G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 5070 Eagle OC", DetectGigabyteRGBFusion2BlackwellSingleZoneGPUControllers, NVIDIA_VEN, NVIDIA_RTX5070_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX5070_EAGLE_OC_12G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 5070 Gaming OC", DetectGigabyteRGBFusion2BlackwellGamingLayoutGPUControllers, NVIDIA_VEN, NVIDIA_RTX5070_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX5070_GAMING_OC_12G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 5070 Ti Eagle OC", DetectGigabyteRGBFusion2BlackwellSingleZoneGPUControllers, NVIDIA_VEN, NVIDIA_RTX5070TI_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX5070TI_EAGLE_OC_16G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 5070 Ti Eagle OC ICE", DetectGigabyteRGBFusion2BlackwellSingleZoneGPUControllers, NVIDIA_VEN, NVIDIA_RTX5070TI_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX5070TI_EAGLE_OC_ICE_16G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 5070 Ti Aero OC", DetectGigabyteRGBFusion2BlackwellSingleZoneGPUControllers, NVIDIA_VEN, NVIDIA_RTX5070TI_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX5070TI_AERO_OC_16G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 5070 Ti Gaming OC", DetectGigabyteRGBFusion2BlackwellGamingLayoutGPUControllers, NVIDIA_VEN, NVIDIA_RTX5070TI_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX5070TI_GAMING_OC_16G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 5080 Aero OC", DetectGigabyteRGBFusion2BlackwellSingleZoneGPUControllers, NVIDIA_VEN, NVIDIA_RTX5080_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX5080_AERO_OC_16G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 5080 Gaming OC", DetectGigabyteRGBFusion2BlackwellGamingLayoutGPUControllers, NVIDIA_VEN, NVIDIA_RTX5080_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX5080_GAMING_OC_16G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte AORUS GeForce RTX 5080 MASTER", DetectGigabyteRGBFusion2BlackwellAorusMaster5080LayoutGPUControllers, NVIDIA_VEN, NVIDIA_RTX5080_DEV, GIGABYTE_SUB_VEN, GIGABYTE_AORUS_RTX5080_MASTER_16G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte AORUS GeForce RTX 5080 XTREME WATERFORCE", DetectGigabyteRGBFusion2BlackwellAorusWaterforceLayoutGPUControllers, NVIDIA_VEN, NVIDIA_RTX5080_DEV, GIGABYTE_SUB_VEN, GIGABYTE_AORUS_RTX5080_XTREME_WATERFORCE_16G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 5080 XTREME WATERFORCE", DetectGigabyteRGBFusion2BlackwellWaterforceLayoutGPUControllers, NVIDIA_VEN, NVIDIA_RTX5080_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX5080_XTREME_WATERFORCE_16G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 5090 Gaming OC", DetectGigabyteRGBFusion2BlackwellGamingLayoutGPUControllers, NVIDIA_VEN, NVIDIA_RTX5090_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX5090_GAMING_OC_32G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 5090 XTREME WATERFORCE", DetectGigabyteRGBFusion2BlackwellWaterforceLayoutGPUControllers, NVIDIA_VEN, NVIDIA_RTX5090_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX5090_XTREME_WATERFORCE_32G_SUB_DEV1, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 5090 XTREME WATERFORCE", DetectGigabyteRGBFusion2BlackwellWaterforceLayoutGPUControllers, NVIDIA_VEN, NVIDIA_RTX5090_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX5090_XTREME_WATERFORCE_32G_SUB_DEV2, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte AORUS GeForce RTX 5090 MASTER", DetectGigabyteRGBFusion2BlackwellGamingLayoutGPUControllers, NVIDIA_VEN, NVIDIA_RTX5090_DEV, GIGABYTE_SUB_VEN, GIGABYTE_AORUS_RTX5090_MASTER_32G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte AORUS GeForce RTX 5090 MASTER ICE", DetectGigabyteRGBFusion2BlackwellGamingLayoutGPUControllers, NVIDIA_VEN, NVIDIA_RTX5090_DEV, GIGABYTE_SUB_VEN, GIGABYTE_AORUS_RTX5090_MASTER_ICE_32G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 5060 Ti Gaming OC", DetectGigabyteRGBFusion2BlackwellSingleZoneGPUControllers, NVIDIA_VEN, NVIDIA_RTX5060TI_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX5060TI_GAMING_OC_16G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 5070 Aero OC", DetectGigabyteRGBFusion2BlackwellSingleZoneGPUControllers, NVIDIA_VEN, NVIDIA_RTX5070_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX5070_AERO_OC_12G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 5070 Eagle OC", DetectGigabyteRGBFusion2BlackwellSingleZoneGPUControllers, NVIDIA_VEN, NVIDIA_RTX5070_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX5070_EAGLE_OC_12G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 5070 Eagle OC ICE", DetectGigabyteRGBFusion2BlackwellSingleZoneGPUControllers, NVIDIA_VEN, NVIDIA_RTX5070_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX5070_EAGLE_OC_ICE_12G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 5070 Gaming OC", DetectGigabyteRGBFusion2BlackwellGamingLayoutGPUControllers, NVIDIA_VEN, NVIDIA_RTX5070_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX5070_GAMING_OC_12G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 5070 Ti Eagle OC", DetectGigabyteRGBFusion2BlackwellSingleZoneGPUControllers, NVIDIA_VEN, NVIDIA_RTX5070TI_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX5070TI_EAGLE_OC_16G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 5070 Ti Eagle OC ICE", DetectGigabyteRGBFusion2BlackwellSingleZoneGPUControllers, NVIDIA_VEN, NVIDIA_RTX5070TI_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX5070TI_EAGLE_OC_ICE_16G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 5070 Ti Aero OC", DetectGigabyteRGBFusion2BlackwellSingleZoneGPUControllers, NVIDIA_VEN, NVIDIA_RTX5070TI_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX5070TI_AERO_OC_16G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 5070 Ti Gaming OC", DetectGigabyteRGBFusion2BlackwellGamingLayoutGPUControllers, NVIDIA_VEN, NVIDIA_RTX5070TI_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX5070TI_GAMING_OC_16G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 5080 Aero OC", DetectGigabyteRGBFusion2BlackwellSingleZoneGPUControllers, NVIDIA_VEN, NVIDIA_RTX5080_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX5080_AERO_OC_16G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 5080 Gaming OC", DetectGigabyteRGBFusion2BlackwellGamingLayoutGPUControllers, NVIDIA_VEN, NVIDIA_RTX5080_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX5080_GAMING_OC_16G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte AORUS GeForce RTX 5080 MASTER", DetectGigabyteRGBFusion2BlackwellAorusMaster5080LayoutGPUControllers, NVIDIA_VEN, NVIDIA_RTX5080_DEV, GIGABYTE_SUB_VEN, GIGABYTE_AORUS_RTX5080_MASTER_16G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte AORUS GeForce RTX 5080 XTREME WATERFORCE", DetectGigabyteRGBFusion2BlackwellAorusWaterforceLayoutGPUControllers, NVIDIA_VEN, NVIDIA_RTX5080_DEV, GIGABYTE_SUB_VEN, GIGABYTE_AORUS_RTX5080_XTREME_WATERFORCE_16G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 5080 XTREME WATERFORCE", DetectGigabyteRGBFusion2BlackwellWaterforceLayoutGPUControllers, NVIDIA_VEN, NVIDIA_RTX5080_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX5080_XTREME_WATERFORCE_16G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte AORUS GeForce RTX 5080 MASTER ICE", DetectGigabyteRGBFusion2BlackwellAorusMaster5080LayoutGPUControllers, NVIDIA_VEN, NVIDIA_RTX5080_DEV, GIGABYTE_SUB_VEN, GIGABYTE_AORUS_RTX5080_MASTER_ICE_16G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 5090 Gaming OC", DetectGigabyteRGBFusion2BlackwellGamingLayoutGPUControllers, NVIDIA_VEN, NVIDIA_RTX5090_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX5090_GAMING_OC_32G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 5090 XTREME WATERFORCE", DetectGigabyteRGBFusion2BlackwellWaterforceLayoutGPUControllers, NVIDIA_VEN, NVIDIA_RTX5090_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX5090_XTREME_WATERFORCE_32G_SUB_DEV1, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 5090 XTREME WATERFORCE", DetectGigabyteRGBFusion2BlackwellWaterforceLayoutGPUControllers, NVIDIA_VEN, NVIDIA_RTX5090_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX5090_XTREME_WATERFORCE_32G_SUB_DEV2, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte AORUS GeForce RTX 5090 MASTER", DetectGigabyteRGBFusion2BlackwellGamingLayoutGPUControllers, NVIDIA_VEN, NVIDIA_RTX5090_DEV, GIGABYTE_SUB_VEN, GIGABYTE_AORUS_RTX5090_MASTER_32G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte AORUS GeForce RTX 5090 MASTER ICE", DetectGigabyteRGBFusion2BlackwellGamingLayoutGPUControllers, NVIDIA_VEN, NVIDIA_RTX5090_DEV, GIGABYTE_SUB_VEN, GIGABYTE_AORUS_RTX5090_MASTER_ICE_32G_SUB_DEV, 0x75);
REGISTER_I2C_PCI_DETECTOR("Gigabyte AORUS GeForce RTX 5090 D V2 MASTER ICE", DetectGigabyteRGBFusion2BlackwellAorusMaster5090DV2IceLayoutGPUControllers, NVIDIA_VEN, NVIDIA_RTX5090D_V2_DEV, GIGABYTE_SUB_VEN, GIGABYTE_AORUS_RTX5090D_V2_MASTER_ICE_24G_SUB_DEV, 0x75);
/*-----------------------------------------*\
| AMD GPUs |
@@ -400,6 +400,52 @@ void RGBController_RGBFusion2BlackwellGPU::SetupZones()
leds.push_back(top_led);
}
else if(gpu_layout == RGB_FUSION2_BLACKWELL_GPU_AORUS_MASTER_5090D_V2_ICE_LAYOUT)
{
const char * fan_names[] = { "Right Fan", "Left Fan", "Middle Fan" };
for(int i = 0; i < 3; i++)
{
zone fan_zone;
fan_zone.name = fan_names[i];
fan_zone.type = ZONE_TYPE_SINGLE;
fan_zone.leds_min = 1;
fan_zone.leds_max = 1;
fan_zone.leds_count = 1;
fan_zone.matrix_map = NULL;
zones.push_back(fan_zone);
led new_led;
new_led.name = fan_names[i];
leds.push_back(new_led);
}
zone backplate;
backplate.name = "Backplate";
backplate.type = ZONE_TYPE_SINGLE;
backplate.leds_min = 1;
backplate.leds_max = 1;
backplate.leds_count = 1;
backplate.matrix_map = NULL;
zones.push_back(backplate);
led bp_led;
bp_led.name = "Backplate";
leds.push_back(bp_led);
zone side_logo;
side_logo.name = "Side Logo";
side_logo.type = ZONE_TYPE_SINGLE;
side_logo.leds_min = 1;
side_logo.leds_max = 1;
side_logo.leds_count = 1;
side_logo.matrix_map = NULL;
zones.push_back(side_logo);
led sl_led;
sl_led.name = "Side Logo";
leds.push_back(sl_led);
}
SetupColors();
}
@@ -446,6 +492,10 @@ void RGBController_RGBFusion2BlackwellGPU::DeviceUpdateLEDs()
gpu_zones = 6; // Zones 4 and 5 refer to ui zone 4
break;
case RGB_FUSION2_BLACKWELL_GPU_AORUS_MASTER_5090D_V2_ICE_LAYOUT:
gpu_zones = 6;
break;
default:
LOG_TRACE("[%s] Invalid GPU layout (%d) when updating LEDs.", name.c_str(), gpu_layout);
return; // should not happen
@@ -472,7 +522,8 @@ void RGBController_RGBFusion2BlackwellGPU::DeviceUpdateLEDs()
}
ui_zone_idx = zone_idx - 1; // Map: HW zone 1->UI zone 0, HW zone 2->UI zone 1, HW zone 3->UI zone 2, HW zone 4->UI zone 3
}
else if(gpu_layout == RGB_FUSION2_BLACKWELL_GPU_AORUS_MASTER_5080_LAYOUT)
else if(gpu_layout == RGB_FUSION2_BLACKWELL_GPU_AORUS_MASTER_5080_LAYOUT ||
gpu_layout == RGB_FUSION2_BLACKWELL_GPU_AORUS_MASTER_5090D_V2_ICE_LAYOUT)
{
if(zone_idx == 5)
{
@@ -152,6 +152,7 @@ REGISTER_I2C_PCI_DETECTOR("Gigabyte AORUS GeForce RTX 3090 XTREME WATERFORCE",
REGISTER_I2C_PCI_DETECTOR("Gigabyte AORUS GeForce RTX 3090 XTREME WATERFORCE WB", DetectGigabyteRGBFusion2GPUControllers, NVIDIA_VEN, NVIDIA_RTX3090_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX3090_XTREME_WATERFORCE_WB_SUB_DEV, 0x64);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 4070 GAMING OC", DetectGigabyteRGBFusion2GPUControllers, NVIDIA_VEN, NVIDIA_RTX4070_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX4070_GAMING_OC_12G, 0x71);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 4070 GAMING OC", DetectGigabyteRGBFusion2GPUControllers, NVIDIA_VEN, NVIDIA_RTX4070_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX4070_GAMING_OC_12G_V2, 0x71);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 4070 GAMING OC", DetectGigabyteRGBFusion2GPUControllers, NVIDIA_VEN, NVIDIA_RTX4070_AD103_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX4070_GAMING_OC_12G_V2, 0x71);
REGISTER_I2C_PCI_DETECTOR("Gigabyte Geforce RTX 4070 Aero OC", DetectGigabyteRGBFusion2GPUControllers, NVIDIA_VEN, NVIDIA_RTX4070_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX4070_AERO_OC_12G_SUB_DEV, 0x71);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 4070 SUPER GAMING OC", DetectGigabyteRGBFusion2GPUControllers, NVIDIA_VEN, NVIDIA_RTX4070S_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX4070S_GAMING_OC_12G, 0x71);
REGISTER_I2C_PCI_DETECTOR("Gigabyte GeForce RTX 4070 SUPER Aero OC", DetectGigabyteRGBFusion2GPUControllers, NVIDIA_VEN, NVIDIA_RTX4070S_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RTX4070S_AERO_OC_12G, 0x71);
@@ -188,12 +189,14 @@ REGISTER_I2C_PCI_DETECTOR("Gigabyte Radeon RX 7600 GAMING OC 8G",
REGISTER_I2C_PCI_DETECTOR("Gigabyte Radeon RX 7600 GAMING OC 8G", DetectGigabyteRGBFusion2GPUControllers, AMD_GPU_VEN, AMD_NAVI33_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RX7600_GAMING_OC_8G_SUB_DEV2, 0x55);
REGISTER_I2C_PCI_DETECTOR("Gigabyte Radeon RX 7600 XT GAMING OC 16G", DetectGigabyteRGBFusion2GPUControllers, AMD_GPU_VEN, AMD_NAVI33_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RX7600XT_GAMING_OC_16G_SUB_DEV, 0x55);
REGISTER_I2C_PCI_DETECTOR("Gigabyte Radeon RX 6700 XT GAMING OC 12G", DetectGigabyteRGBFusion2GPUControllers, AMD_GPU_VEN, AMD_NAVI22_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RX6700XT_GAMING_OC_12G_SUB_DEV, 0x62);
REGISTER_I2C_PCI_DETECTOR("Gigabyte Radeon RX 6800 XT GAMING OC", DetectGigabyteRGBFusion2GPUControllers, AMD_GPU_VEN, AMD_NAVI21_DEV1, GIGABYTE_SUB_VEN, GIGABYTE_RX6800XT_GAMING_OC_SUB_DEV, 0x62);
REGISTER_I2C_PCI_DETECTOR("Gigabyte Radeon RX 6900 XT GAMING OC", DetectGigabyteRGBFusion2GPUControllers, AMD_GPU_VEN, AMD_NAVI21_DEV1, GIGABYTE_SUB_VEN, GIGABYTE_RX6900XT_GAMING_OC_SUB_DEV, 0x62);
REGISTER_I2C_PCI_DETECTOR("Gigabyte Radeon RX 7700 XT GAMING OC", DetectGigabyteRGBFusion2GPUControllers, AMD_GPU_VEN, AMD_NAVI32_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RX7700XT_GAMING_OC_SUB_DEV, 0x62);
REGISTER_I2C_PCI_DETECTOR("Gigabyte Radeon RX 7800 XT GAMING OC", DetectGigabyteRGBFusion2GPUControllers, AMD_GPU_VEN, AMD_NAVI32_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RX7800XT_GAMING_OC_16G_SUB_DEV, 0x62);
REGISTER_I2C_PCI_DETECTOR("Gigabyte Radeon RX 7900 GRE GAMING OC", DetectGigabyteRGBFusion2GPUControllers, AMD_GPU_VEN, AMD_NAVI31_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RX7900GRE_GAMING_OC_16G_SUB_DEV, 0x62);
REGISTER_I2C_PCI_DETECTOR("Gigabyte Radeon RX 7900 XT GAMING OC", DetectGigabyteRGBFusion2GPUControllers, AMD_GPU_VEN, AMD_NAVI31_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RX7900XT_GAMING_OC_20G_SUB_DEV, 0x62);
REGISTER_I2C_PCI_DETECTOR("Gigabyte Radeon RX 7900 XTX GAMING OC", DetectGigabyteRGBFusion2GPUControllers, AMD_GPU_VEN, AMD_NAVI31_DEV, GIGABYTE_SUB_VEN, GIGABYTE_RX7900XTX_GAMING_OC_24G_SUB_DEV, 0x62);
REGISTER_I2C_PCI_DETECTOR("Gigabyte AORUS Radeon RX 7900 XTX ELITE 24G", DetectGigabyteRGBFusion2GPUControllers, AMD_GPU_VEN, AMD_NAVI31_DEV, GIGABYTE_SUB_VEN, GIGABYTE_AORUS_RX7900XTX_ELITE_24G_SUB_DEV, 0x71);
REGISTER_I2C_PCI_DETECTOR("Gigabyte AORUS RX 6750 XT ELITE 12G", DetectGigabyteRGBFusion2GPUControllers, AMD_GPU_VEN, AMD_NAVI22_DEV, GIGABYTE_SUB_VEN, GIGABYTE_AORUS_RX_6750_XT_ELITE_12G_SUB_DEV, 0x70);
REGISTER_I2C_PCI_DETECTOR("Gigabyte AORUS RX 6900 XT EXTREME WATERFORCE WB", DetectGigabyteRGBFusion2GPUControllers, AMD_GPU_VEN, AMD_NAVI21_DEV2, GIGABYTE_SUB_VEN, GIGABYTE_RX6900XT_XTREME_WATERFORCE_WB_SUB_DEV, 0x70);
REGISTER_I2C_PCI_DETECTOR("Gigabyte AORUS Radeon RX 9070 XT Elite", DetectGigabyteRGBFusion2GPUControllers, AMD_GPU_VEN, AMD_NAVI48_DEV, GIGABYTE_SUB_VEN, GIGABYTE_AORUS_RX9070XT_ELITE_16G_SUB_DEV, 0x73);
File diff suppressed because it is too large Load Diff
@@ -16,7 +16,7 @@
#include <map>
#include "RGBController.h"
#define GB_FUSION2_ZONES_MAX 8
#define GB_FUSION2_ZONES_MAX 12
/*--------------------------------------------------------*\
| Base LED mappings found on all controllers. |
@@ -89,6 +89,23 @@ const FwdLedHeaders LedLookup =
\*-------------------------------------------------*/
};
/*--------------------------------------------------------*\
| The layout_id masks for supported effects. |
\*--------------------------------------------------------*/
enum GB_LID_EFFECTS_MASKS : uint32_t
{
GB_EFF_BREATH = 0x001,
GB_EFF_BEAT = 0x002,
GB_EFF_CYCLE = 0x004,
GB_EFF_FLASH = 0x008,
GB_EFF_RANDOM = 0x010,
GB_EFF_WAVE = 0x020,
GB_EFF_DFLASH = 0x040,
GB_EFF_WAVE1 = 0x080,
GB_EFF_WAVE2 = 0x100,
GB_EFF_CORE_MASK = 0x1FF
};
typedef struct
{
GB_FUSION2_LED_IDX idx;
@@ -95,10 +95,9 @@ bool RGBFusion2USBController::RefreshHardwareInfo()
return false;
}
IT8297Report report;
std::memcpy(&report, buffer, sizeof(IT8297Report));
report_loaded = true;
device_num = report.device_num;
description = std::string(report.str_product, 28);
if(std::string::iterator nul = std::find(description.begin(), description.end(), '\0');
nul != description.end())
@@ -318,7 +317,7 @@ bool RGBFusion2USBController::SetCalibration(const EncodedCalibration& cal, bool
cal_data.mainboard = desired.c.rgb_cali;
cal_data.spare[0] = desired.c.c_spare0;
cal_data.spare[1] = desired.c.c_spare1;
if(product_id == 0x5711)
{
cal_data.dled[2] = desired.c.d_strip_c2;
@@ -362,7 +361,6 @@ void RGBFusion2USBController::SetLedCount(unsigned int c0, unsigned int c1, unsi
\*---------------------------------------------------------*/
bool RGBFusion2USBController::SetStripBuiltinEffectState(int hdr, bool enable)
{
static bool first_call = true;
int bitmask = 0;
if(hdr == -1)
@@ -392,16 +390,16 @@ bool RGBFusion2USBController::SetStripBuiltinEffectState(int hdr, bool enable)
}
}
int new_effect_disabled = enable
? (effect_disabled & ~bitmask)
: (effect_disabled | bitmask);
int base_mask = (effect_disabled < 0) ? 0 : effect_disabled;
int new_effect_disabled = enable
? (base_mask & ~bitmask)
: (base_mask | bitmask);
if(!first_call && new_effect_disabled == effect_disabled)
// Skip redundant writes only after we have synchronized at least once
if(effect_disabled >= 0 && new_effect_disabled == effect_disabled)
{
return true;
}
first_call = false;
effect_disabled = new_effect_disabled;
int res = SendCCReport(0x32, effect_disabled);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
@@ -439,7 +437,7 @@ bool RGBFusion2USBController::EnableLampArray(bool enable)
{
return SendCCReport(0x48, enable ? 1 : 0);
}
std::string RGBFusion2USBController::GetDeviceName()
{
return(name);
@@ -703,3 +701,115 @@ void RGBFusion2USBController::ResetController()
}
ApplyEffect(true);
}
/*---------------------------------------------------------*\
| Check controller for gen2 ARGB support |
| Checks for supported device number |
| Then checks for supported controllers |
| Then checks for supported feature bit (SaveLEDState) |
| Finally checks for strip detection value. |
\*---------------------------------------------------------*/
bool RGBFusion2USBController::SupportsGen2() const
{
bool supports_gen2 = false;
supports_gen2 = (device_num == 0x00)
&& (product_id == 0x5702 || product_id==0x5711 || product_id==0x8950)
&& (report.support_cmd_flag & 0x01)
&& (report.strip_detect == 0x01);
return supports_gen2;
}
std::vector<Gen2StripInfo> RGBFusion2USBController::ExportGen2Strips() const
{
size_t count = (product_id == 0x5711) ? 4u : 2u;
std::vector<Gen2StripInfo> out;
out.reserve(count);
for(size_t i = 0; i < count; ++i)
{
out.push_back(g2_strip_info[i]);
}
return out;
}
/*---------------------------------------------------------*\
| Scan Headers for Gen2 Devices |
\*---------------------------------------------------------*/
bool RGBFusion2USBController::ScanGen2Strips()
{
for(unsigned i = 0; i < 4; ++i)
{
if(g2_strip_info[i].LedsOfStrip.capacity() < 15)
{
g2_strip_info[i].LedsOfStrip.reserve(15);
}
g2_strip_info[i].numStrip = 0;
g2_strip_info[i].totalLeds = 0;
g2_strip_info[i].LedsOfStrip.resize(0);
}
const unsigned int hdr_lim = (product_id == 0x5711) ? 4u : 2u;
for(unsigned int slot = 0; slot < hdr_lim; ++slot)
{
static constexpr uint8_t delta[4] = {4, 5, 0, 1};
uint8_t scan_cmd = static_cast<uint8_t>(GEN2_LED_BASE_SCAN + delta[slot]);
uint8_t info_cmd = static_cast<uint8_t>(scan_cmd + 2);
if(!SendCCReport(scan_cmd, 0x00, 0x00))
{
return false;
}
std::this_thread::sleep_for(std::chrono::milliseconds(700));
if(!SendCCReport(info_cmd, 0x00, 0x00))
{
return false;
}
unsigned char feature_buf[64] = {0};
feature_buf[0] = report_id;
int recv_len = hid_get_feature_report(dev, feature_buf, sizeof(feature_buf));
if(recv_len < 64)
{
return false;
}
int seg_count = static_cast<int>(feature_buf[1]);
if(seg_count < 0)
{
seg_count = 0;
}
if(seg_count > 15)
{
seg_count = 15;
}
Gen2StripInfo& dst = g2_strip_info[slot];
dst.numStrip = static_cast<uint8_t>(seg_count);
dst.LedsOfStrip.resize(static_cast<size_t>(seg_count));
uint32_t total_leds = 0;
const int counts_base = 2;
for(int k = 0; k < seg_count; ++k)
{
const int off = counts_base + (k * 2);
uint16_t lo = static_cast<uint16_t>(feature_buf[off + 0]);
uint16_t hi = static_cast<uint16_t>(feature_buf[off + 1]);
uint16_t cnt = static_cast<uint16_t>(lo | (hi << 8));
dst.LedsOfStrip[static_cast<size_t>(k)] = cnt;
total_leds += cnt;
}
dst.totalLeds = total_leds;
SetLedCount(0, 0, 0, 0);
std::this_thread::sleep_for(std::chrono::milliseconds(20));
SaveLEDState(false);
std::this_thread::sleep_for(std::chrono::milliseconds(20));
}
return true;
}
@@ -21,6 +21,11 @@
#define FUSION2_USB_BUFFER_SIZE 64
/*--------------------------------------------------------*\
| Gen2 scan/info opcode base |
\*--------------------------------------------------------*/
#define GEN2_LED_BASE_SCAN 0x38
/*---------------------------------------------------------*\
| Effects mode list |
\*---------------------------------------------------------*/
@@ -83,6 +88,16 @@ struct EncodedCalibration
std::string mainboard;
};
/*---------------------------------------------------------*\
| High level struct to contain Gen2 Header Data |
\*---------------------------------------------------------*/
struct Gen2StripInfo
{
uint8_t numStrip = 0;
std::vector<uint16_t> LedsOfStrip;
uint32_t totalLeds = 0;
};
#pragma pack(push, 1)
/*---------------------------------------------------------*\
@@ -321,6 +336,8 @@ public:
void SetLEDEffect(int led, int mode, unsigned int speed, unsigned char brightness, bool random, uint32_t* color);
bool SetStripBuiltinEffectState(int hdr, bool enable);
void SetStripColors(unsigned int hdr, RGBColor * colors, unsigned int num_colors, int single_led = -1);
bool SupportsGen2() const;
bool ScanGen2Strips();
EncodedCalibration GetCalibration(bool refresh_from_hw = false);
std::string GetDeviceName();
@@ -330,6 +347,8 @@ public:
std::string GetFWVersion();
uint16_t GetProductID();
std::string GetSerial();
std::vector<Gen2StripInfo> ExportGen2Strips() const;
Gen2StripInfo g2_strip_info[4];
private:
std::string DecodeCalibrationBuffer(uint32_t value) const;
@@ -345,7 +364,7 @@ private:
int SendPacket(unsigned char* packet);
hid_device* dev;
int device_num;
uint8_t device_num;
uint16_t product_id;
uint32_t effect_zone_mask = 0;
int mode;
@@ -356,7 +375,7 @@ private:
std::string location;
std::string version;
std::string chip_id;
int effect_disabled = 0;
int effect_disabled = -1;
int report_id = 0xCC;
bool report_loaded = false;
bool cali_loaded = false;
@@ -25,7 +25,7 @@
@effects :white_check_mark:
@detectors DetectGigabyteRGBFusion2USBControllers
@comment The Fusion 2 USB controller applies to most AMD and
Intel mainboards from the x570 and z390 chipsets onwards.
Intel mainboards from the X570 and z390 chipsets onwards.
\*-------------------------------------------------------------------*/
RGBController_RGBFusion2USB::RGBController_RGBFusion2USB(RGBFusion2USBController* controller_ptr, std::string detector)
@@ -39,8 +39,10 @@ RGBController_RGBFusion2USB::RGBController_RGBFusion2USB(RGBFusion2USBController
version = controller->GetFWVersion();
location = controller->GetDeviceLocation();
serial = controller->GetSerial();
product_id = controller->GetProductID();
device_num = controller->GetDeviceNum();
mode Direct;
Direct.name = "Direct";
Direct.value = 0xFFFF;
@@ -206,6 +208,13 @@ RGBController_RGBFusion2USB::RGBController_RGBFusion2USB(RGBFusion2USBController
RGBController_RGBFusion2USB::~RGBController_RGBFusion2USB()
{
// Free any zones we allocated for the per-instance layout
for(gb_fusion2_zone* z : allocated_zones)
{
delete z;
}
allocated_zones.clear();
delete controller;
}
@@ -214,94 +223,125 @@ RGBController_RGBFusion2USB::~RGBController_RGBFusion2USB()
\*---------------------------------------------------------*/
void RGBController_RGBFusion2USB::Init_Controller()
{
const std::string SectionCustom = "CustomLayout";
const gb_fusion2_device* src_layout = gb_fusion2_device_list[device_index];
const std::string SectionGen2 = "Gigabyte-Gen2-ARGB";
const std::string SectionCustomBase = "CustomLayout";
const std::string SectionCustom = SectionCustomBase + std::to_string(device_num);
const std::string SectionCalibration = "Calibration";
RvrseLedHeaders ReverseLedLookup = reverse_map(LedLookup);
SettingsManager* settings_manager = ResourceManager::get()->GetSettingsManager();
nlohmann::json device_settings = settings_manager->GetSettings(detector_name);
/*---------------------------------------------------------*\
| Create the custom layout from the generic_device |
| Checks for Gen2 support and adds flag to json. |
\*---------------------------------------------------------*/
gb_fusion2_device* layout = const_cast<gb_fusion2_device*>(gb_fusion2_device_list[device_index]);
if(controller->SupportsGen2())
{
if(!device_settings.contains(SectionGen2))
{
device_settings[SectionGen2]["Enabled"] = false;
settings_manager->SetSettings(detector_name, device_settings);
settings_manager->SaveSettings();
}
supports_gen2 = device_settings[SectionGen2]["Enabled"];
if(supports_gen2)
{
controller->ScanGen2Strips();
}
}
/*---------------------------------------------------------*\
| Create the custom layout from the generic layout |
\*---------------------------------------------------------*/
switch(product_id)
{
case 0x8950:
src_layout = gb_fusion2_device_list[device_index + 1];
break;
case 0x5711:
src_layout = gb_fusion2_device_list[device_index + 2];
break;
default:
break;
}
if(!device_settings.contains(SectionCustom))
{
device_settings[SectionCustom]["Enabled"] = false;
device_settings[SectionCustom]["Data"] = BuildCustomLayoutJson(layout, ReverseLedLookup);
device_settings[SectionCustom]["Data"] = BuildCustomLayoutJson(src_layout, ReverseLedLookup);
settings_manager->SetSettings(detector_name, device_settings);
settings_manager->SaveSettings();
}
bool custom_layout = device_settings[SectionCustom]["Enabled"];
if(custom_layout)
{
LoadCustomLayoutFromJson(device_settings[SectionCustom]["Data"], LedLookup, layout);
}
EncodedCalibration hw_cal = controller->GetCalibration(false);
if(!device_settings.contains(SectionCalibration))
if(device_num == 0)
{
device_settings[SectionCalibration]["Enabled"] = false;
device_settings[SectionCalibration]["Data"] = WriteCalJsonFrom(hw_cal);
settings_manager->SetSettings(detector_name, device_settings);
settings_manager->SaveSettings();
}
else
{
nlohmann::json& cal_sec = device_settings[SectionCalibration];
bool cal_enable = cal_sec.value("Enabled", false);
if(!cal_sec.contains("Data") || !cal_sec["Data"].is_object())
if(!device_settings.contains(SectionCalibration))
{
cal_sec["Data"] = WriteCalJsonFrom(hw_cal);
device_settings[SectionCalibration]["Enabled"] = false;
device_settings[SectionCalibration]["Data"] = WriteCalJsonFrom(hw_cal);
settings_manager->SetSettings(detector_name, device_settings);
settings_manager->SaveSettings();
}
else
{
nlohmann::json& cdata = cal_sec["Data"];
FillMissingWith(cdata, hw_cal);
nlohmann::json& cal_sec = device_settings[SectionCalibration];
bool cal_enable = cal_sec.value("Enabled", false);
if(!cal_enable)
if(!cal_sec.contains("Data") || !cal_sec["Data"].is_object())
{
cal_sec["Data"] = WriteCalJsonFrom(hw_cal);
settings_manager->SetSettings(detector_name, device_settings);
settings_manager->SaveSettings();
}
}
if(cal_enable)
{
const nlohmann::json& cdata = cal_sec["Data"];
EncodedCalibration desired;
desired.dled[0] = GET_JSON_VAL_ELSE_OFF(cdata, "HDR_D_LED1");
desired.dled[1] = GET_JSON_VAL_ELSE_OFF(cdata, "HDR_D_LED2");
desired.mainboard = GET_JSON_VAL_ELSE_OFF(cdata, "Mainboard");
desired.spare[0] = GET_JSON_VAL_ELSE_OFF(cdata, "Spare0");
desired.spare[1] = GET_JSON_VAL_ELSE_OFF(cdata, "Spare1");
if(controller->GetProductID() == 0x5711)
{
desired.dled[2] = GET_JSON_VAL_ELSE_OFF(cdata, "HDR_D_LED3");
desired.dled[3] = GET_JSON_VAL_ELSE_OFF(cdata, "HDR_D_LED4");
desired.spare[2] = GET_JSON_VAL_ELSE_OFF(cdata, "Spare2");
desired.spare[3] = GET_JSON_VAL_ELSE_OFF(cdata, "Spare3");
}
else
{
desired.dled[2] = "OFF";
desired.dled[3] = "OFF";
desired.spare[2] = "OFF";
desired.spare[3] = "OFF";
nlohmann::json& cdata = cal_sec["Data"];
FillMissingWith(cdata, hw_cal);
if(!cal_enable)
{
cal_sec["Data"] = WriteCalJsonFrom(hw_cal);
settings_manager->SetSettings(detector_name, device_settings);
settings_manager->SaveSettings();
}
}
if(cal_enable)
{
const nlohmann::json& cdata = cal_sec["Data"];
EncodedCalibration desired;
desired.dled[0] = GET_JSON_VAL_ELSE_OFF(cdata, "HDR_D_LED1");
desired.dled[1] = GET_JSON_VAL_ELSE_OFF(cdata, "HDR_D_LED2");
desired.mainboard = GET_JSON_VAL_ELSE_OFF(cdata, "Mainboard");
desired.spare[0] = GET_JSON_VAL_ELSE_OFF(cdata, "Spare0");
desired.spare[1] = GET_JSON_VAL_ELSE_OFF(cdata, "Spare1");
if(controller->GetProductID() == 0x5711)
{
desired.dled[2] = GET_JSON_VAL_ELSE_OFF(cdata, "HDR_D_LED3");
desired.dled[3] = GET_JSON_VAL_ELSE_OFF(cdata, "HDR_D_LED4");
desired.spare[2] = GET_JSON_VAL_ELSE_OFF(cdata, "Spare2");
desired.spare[3] = GET_JSON_VAL_ELSE_OFF(cdata, "Spare3");
}
else
{
desired.dled[2] = "OFF";
desired.dled[3] = "OFF";
desired.spare[2] = "OFF";
desired.spare[3] = "OFF";
}
controller->SetCalibration(desired, false);
}
controller->SetCalibration(desired, false);
}
}
/*---------------------------------------------------------------------*\
| When no match found the first entry (generic_device) will be used |
| otherwise look up channel map based on device name |
@@ -315,35 +355,81 @@ void RGBController_RGBFusion2USB::Init_Controller()
\*-----------------------------------------------------------------*/
for(unsigned int i = 0; i < GB_FUSION2_DEVICE_COUNT; i++)
{
if(gb_fusion2_device_list[i]->name == name)
if(gb_fusion2_device_list[i]->name == name &&
gb_fusion2_device_list[i]->device_num == device_num)
{
/*---------------------------------------------------------*\
| Set device ID |
\*---------------------------------------------------------*/
device_index = i;
layout = const_cast<gb_fusion2_device*>(gb_fusion2_device_list[i]);
src_layout = gb_fusion2_device_list[i];
break;
}
}
}
/*---------------------------------------------------------------------*\
| Creates per instance copy of layouts. |
\*---------------------------------------------------------------------*/
instance_layout.zones = &instance_zones;
instance_layout.layout_id = src_layout->layout_id;
instance_layout.device_num = src_layout->device_num;
instance_layout.name = src_layout->name;
for(uint8_t zi = 0; zi < GB_FUSION2_ZONES_MAX; ++zi)
{
(*instance_layout.zones)[zi] = (*src_layout->zones)[zi];
}
if(custom_layout)
{
LoadCustomLayoutFromJson(device_settings[SectionCustom]["Data"], LedLookup, &instance_layout);
}
/*---------------------------------------------------------------------*\
| Culls the mode support based on layout_id. |
\*---------------------------------------------------------------------*/
const uint32_t effect_mask = instance_layout.layout_id & GB_EFF_CORE_MASK;
modes.erase(std::remove_if(modes.begin(), modes.end(),
[effect_mask](const mode& m)
{
if(m.value == 0xFFFF /* Direct */) { return false; }
if(m.value == EFFECT_STATIC) { return false; }
uint32_t bit = 0u;
switch(m.value)
{
case EFFECT_PULSE: bit = GB_EFF_BREATH; break;
case EFFECT_COLORCYCLE: bit = GB_EFF_CYCLE; break;
case EFFECT_BLINKING: bit = GB_EFF_FLASH; break;
case EFFECT_RANDOM: bit = GB_EFF_RANDOM; break;
case EFFECT_WAVE: bit = GB_EFF_WAVE; break;
case EFFECT_DFLASH: bit = GB_EFF_DFLASH; break;
case EFFECT_WAVE1: bit = GB_EFF_WAVE1; break;
case EFFECT_WAVE2: bit = GB_EFF_WAVE2; break;
case EFFECT_WAVE3: bit = GB_EFF_WAVE1; break;
case EFFECT_WAVE4: bit = GB_EFF_WAVE2; break;
default: bit = 0u; break;
}
return (bit == 0u) || ((effect_mask & bit) == 0u);
}),
modes.end());
/*---------------------------------------------------------*\
| Iterate through layout and process each zone |
\*---------------------------------------------------------*/
for(uint8_t zone_idx = 0; zone_idx < GB_FUSION2_ZONES_MAX; zone_idx++)
{
if(!layout->zones[0][zone_idx])
if(!(*instance_layout.zones)[zone_idx])
{
continue;
}
const gb_fusion2_zone* zone_at_idx = layout->zones[0][zone_idx];
const gb_fusion2_zone* zone_at_idx = (*instance_layout.zones)[zone_idx];
zone new_zone;
new_zone.name = zone_at_idx->name;
new_zone.leds_min = zone_at_idx->leds_min;
new_zone.leds_max = zone_at_idx->leds_max;
new_zone.leds_count = new_zone.leds_min;
new_zone.type = (new_zone.leds_min == new_zone.leds_max) ? ZONE_TYPE_SINGLE : ZONE_TYPE_LINEAR;
new_zone.type = ((new_zone.leds_min == 1) && (new_zone.leds_max == 1)) ? ZONE_TYPE_SINGLE : ZONE_TYPE_LINEAR;
new_zone.matrix_map = NULL;
zones.emplace_back(new_zone);
}
@@ -363,17 +449,68 @@ void RGBController_RGBFusion2USB::SetupZones()
| Set up zones (Fixed so as to not spam the controller) |
\*---------------------------------------------------------*/
std::vector<Gen2StripInfo> strips = controller->ExportGen2Strips();
for(uint8_t zone_idx = 0; zone_idx < GB_FUSION2_ZONES_MAX; zone_idx++)
{
const gb_fusion2_zone* zone_at_idx = gb_fusion2_device_list[device_index]->zones[0][zone_idx];
const gb_fusion2_zone* zone_at_idx = (*instance_layout.zones)[zone_idx];
if(!zone_at_idx)
{
continue;
}
bool single_zone = (zone_at_idx->leds_min == zone_at_idx->leds_max);
bool single_zone = ((zone_at_idx->leds_min == 1) && (zone_at_idx->leds_max == 1));
if(!single_zone)
{
if(supports_gen2)
{
int slot = 0;
switch(zone_at_idx->idx)
{
case LED4:
case HDR_D_LED2:
slot = 1;
break;
case HDR_D_LED3:
slot = 2;
break;
case HDR_D_LED4:
slot = 3;
break;
default:
break;
}
if(slot >= 0 && static_cast<size_t>(slot) < strips.size())
{
const Gen2StripInfo& info = strips[static_cast<size_t>(slot)];
if(info.totalLeds > 0u)
{
zones[zone_idx].leds_count = static_cast<unsigned int>(info.totalLeds);
zones[zone_idx].leds_min = static_cast<unsigned int>(info.totalLeds);
zones[zone_idx].leds_max = static_cast<unsigned int>(info.totalLeds);
zones[zone_idx].segments.clear();
zones[zone_idx].segments.reserve(info.LedsOfStrip.size());
unsigned int offset = 0;
for(size_t si = 0; si < info.LedsOfStrip.size(); ++si)
{
const uint16_t cnt = info.LedsOfStrip[si];
if(cnt == 0) continue;
segment seg;
seg.name = std::string("Segment ") + std::to_string(si);
seg.type = ZONE_TYPE_LINEAR;
seg.start_idx = offset;
seg.leds_count = static_cast<unsigned int>(cnt);
zones[zone_idx].segments.push_back(seg);
offset += static_cast<unsigned int>(cnt);
}
}
}
}
switch(zone_at_idx->idx)
{
case LED4:
@@ -826,6 +963,7 @@ void RGBController_RGBFusion2USB::LoadCustomLayoutFromJson(
&& new_zone->idx <= GB_FUSION2_LED_IDX::LED11)
{
layout->zones[0][zone_idx] = new_zone;
allocated_zones.push_back(new_zone);
}
else
{
@@ -62,17 +62,26 @@ private:
std::string detector_name;
RGBFusion2USBController* controller;
int device_num;
uint8_t device_num;
RGBColor null_color = 0;
bool supports_gen2 = 0;
/*---------------------------------------------------------*\
| The intial value of device_index should point to the |
| layout for the generic_device |
\*---------------------------------------------------------*/
uint32_t device_index = 0;
uint16_t product_id = 0;
uint32_t effects_mask = 0;
void Init_Controller();
int GetLED_Zone(int led_idx);
/*---------------------------------------------------------*\
| Per instance layout lookup tables. |
\*---------------------------------------------------------*/
gb_fusion2_device instance_layout{};
gb_fusion2_layout instance_zones{};
std::vector<gb_fusion2_zone*> allocated_zones;
nlohmann::json WriteCalJsonFrom(
const EncodedCalibration& src);
void FillMissingWith(
@@ -16,10 +16,21 @@
using namespace std::chrono_literals;
static std::map<std::string, unsigned int> govee_led_counts
struct GoveeHardwareInfo
{
{ "H619A", 20 },
{ "H70B1", 20 },
unsigned int led_count;
unsigned int matrix_row_len; // 0 = linear
};
const unsigned int GOVEE_FALLBACK_LED_COUNT = 20;
static std::map<std::string, GoveeHardwareInfo> govee_hardware_info
{
{ "H6022", { 132, 12 } }, // Govee Smart Table Lamp 2
{ "H612F", { 12, 0 } }, // Govee Strip Light S (3m)
{ "H619A", { 20, 0 } }, // Govee RGBIC Led Strip Lights
{ "H70B1", { 20, 0 } }, // Govee LED Curtain Lights
{ "H607C", { 174, 0 } }, // Govee Floor Lamp 2
};
RGBController_Govee::RGBController_Govee(GoveeController* controller_ptr)
@@ -67,39 +78,64 @@ RGBController_Govee::~RGBController_Govee()
void RGBController_Govee::SetupZones()
{
unsigned int led_count = 0;
std::map<std::string, unsigned int>::iterator it = govee_led_counts.find(controller->GetSku());
if(it != govee_led_counts.end())
GoveeHardwareInfo hw = { GOVEE_FALLBACK_LED_COUNT, 0 };
bool resizable = true;
std::map<std::string, GoveeHardwareInfo>::iterator it = govee_hardware_info.find(controller->GetSku());
if(it != govee_hardware_info.end())
{
led_count = it->second;
}
/*-----------------------------------------------------*\
| Fallback so Direct mode is usable even if SKU isn't |
| in the table |
\*-----------------------------------------------------*/
if(led_count == 0)
{
led_count = 20; /* safe default; user can resize in UI */
hw = it->second;
resizable = false;
}
zone strip;
strip.name = "Govee Strip";
strip.type = ZONE_TYPE_LINEAR;
strip.leds_count = led_count;
/*-----------------------------------------------------*\
| Only make resizable for unknown SKUs |
\*-----------------------------------------------------*/
if(govee_led_counts.find(controller->GetSku()) == govee_led_counts.end())
strip.leds_count = hw.led_count;
strip.leds_min = resizable ? 0 : hw.led_count;
strip.leds_max = resizable ? 255 : hw.led_count;
if(hw.matrix_row_len == 0)
{
strip.leds_min = 1;
strip.leds_max = 255;
strip.name = "Govee Strip";
strip.type = ZONE_TYPE_LINEAR;
strip.matrix_map = NULL;
}
else
{
strip.leds_min = led_count;
strip.leds_max = led_count;
strip.name = "Govee Matrix";
strip.type = ZONE_TYPE_MATRIX;
unsigned int width = hw.matrix_row_len;
unsigned int height = hw.led_count / width;
strip.matrix_map = new matrix_map_type;
strip.matrix_map->height = height;
strip.matrix_map->width = width;
strip.matrix_map->map = new unsigned int[hw.led_count];
/*-----------------------------------------------------*\
| On H6022, LEDs indexed bottom to top, alternating |
| clockwise and counterclockwise for each row. |
\*-----------------------------------------------------*/
for(unsigned int y = 0; y < height; y++)
{
/*-------------------------------------------------*\
| LEDs numbered bottom to top, opposite of matrix |
| clockwise and counterclockwise for each row. |
\*-------------------------------------------------*/
unsigned int led_y = (height - 1) - y;
for(unsigned int x = 0; x < width; x++)
{
/*---------------------------------------------*\
| LED is right-to-left for even rows, including |
| first one |
\*---------------------------------------------*/
unsigned int led_x = led_y & 1 ? x : (width - 1) - x;
strip.matrix_map->map[y * width + x] = led_y * width + led_x;
}
}
}
strip.matrix_map = NULL;
zones.push_back(strip);
for(std::size_t led_idx = 0; led_idx < strip.leds_count; led_idx++)
@@ -114,6 +150,11 @@ void RGBController_Govee::SetupZones()
void RGBController_Govee::ResizeZone(int zone, int new_size)
{
if(zones[zone].type == ZONE_TYPE_MATRIX)
{
return;
}
if(zone < 0 || zone >= (int)zones.size() || new_size <= 0)
{
return;
@@ -30,28 +30,9 @@ using namespace std::chrono_literals;
bool TestForHyperXDRAMController(i2c_smbus_interface* bus, unsigned char address)
{
bool pass = false;
int res = bus->i2c_smbus_read_byte(address);
int res = bus->i2c_smbus_write_quick(address, I2C_SMBUS_WRITE);
LOG_DEBUG("[%s] Writing at address %02X, res=%02X", HYPERX_CONTROLLER_NAME, address, res);
if (res >= 0)
{
pass = true;
for (int i = 0xA0; i < 0xB0; i++)
{
res = bus->i2c_smbus_read_byte_data(address, i);
if (res != i)
{
pass = false;
}
}
}
return(pass);
return(res >= 0);
} /* TestForHyperXDRAMController() */
@@ -0,0 +1,121 @@
/*---------------------------------------------------------*\
| HyperXEve1800Controller.cpp |
| |
| Driver for HyperX Eve 1800 keyboard |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include <cstring>
#include "HyperXEve1800Controller.h"
#include "StringUtils.h"
HyperXEve1800Controller::HyperXEve1800Controller(hid_device* dev_handle, const char* path, std::string dev_name)
{
dev = dev_handle;
location = path;
name = dev_name;
}
HyperXEve1800Controller::~HyperXEve1800Controller()
{
hid_close(dev);
}
std::string HyperXEve1800Controller::GetDeviceLocation()
{
return("HID: " + location);
}
std::string HyperXEve1800Controller::GetNameString()
{
return(name);
}
std::string HyperXEve1800Controller::GetSerialString()
{
wchar_t serial_string[128];
int ret = hid_get_serial_number_string(dev, serial_string, 128);
if(ret != 0)
{
return("");
}
return(StringUtils::wstring_to_string(serial_string));
}
void HyperXEve1800Controller::SetBrightness(unsigned int brightness)
{
unsigned char buf[65];
memset(buf, 0x00, sizeof(buf));
buf[0x00] = 0x40;
buf[0x01] = 0x01;
buf[0x02] = 0x00;
buf[0x03] = 0x00;
buf[0x04] = brightness & 0xFF;
hid_write(dev, buf, 65);
}
void HyperXEve1800Controller::SetLEDsDirect(std::vector<RGBColor> colors)
{
SendDirectInitialization();
if(colors.empty())
{
RGBColor temp = 0x00000000;
SendDirectColorPacket(&temp, 1);
}
else
{
SendDirectColorPacket(&colors[0], (unsigned int)colors.size());
}
}
void HyperXEve1800Controller::SendDirectInitialization()
{
unsigned char buf[65];
memset(buf, 0x00, sizeof(buf));
buf[0x00] = 0x44;
buf[0x01] = 0x01;
buf[0x02] = 0x04;
buf[0x03] = 0x00;
hid_write(dev, buf, 65);
}
void HyperXEve1800Controller::SendDirectColorPacket(RGBColor* color_data, unsigned int color_count)
{
unsigned char buf[65];
memset(buf, 0x00, sizeof(buf));
buf[0x00] = 0x44;
buf[0x01] = 0x02;
buf[0x02] = 0x00;
buf[0x03] = 0x00;
/*-----------------------------------------------------*\
| The Eve 1800 exposes 10 lighting zones, but this |
| report format can carry up to 20 RGB triplets. |
\*-----------------------------------------------------*/
if(color_count > 20)
{
color_count = 20;
}
for(unsigned int color_idx = 0; color_idx < color_count; color_idx++)
{
buf[4 + (color_idx * 3)] = RGBGetRValue(color_data[color_idx]);
buf[4 + (color_idx * 3) + 1] = RGBGetGValue(color_data[color_idx]);
buf[4 + (color_idx * 3) + 2] = RGBGetBValue(color_data[color_idx]);
}
hid_write(dev, buf, 65);
}
@@ -0,0 +1,36 @@
/*---------------------------------------------------------*\
| HyperXEve1800Controller.h |
| |
| Driver for HyperX Eve 1800 keyboard |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#pragma once
#include <string>
#include <hidapi.h>
#include "RGBController.h"
class HyperXEve1800Controller
{
public:
HyperXEve1800Controller(hid_device* dev_handle, const char* path, std::string dev_name);
~HyperXEve1800Controller();
std::string GetDeviceLocation();
std::string GetNameString();
std::string GetSerialString();
void SetBrightness(unsigned int brightness);
void SetLEDsDirect(std::vector<RGBColor> colors);
private:
hid_device* dev;
std::string location;
std::string name;
void SendDirectInitialization();
void SendDirectColorPacket(RGBColor* color_data, unsigned int color_count);
};
@@ -0,0 +1,127 @@
/*---------------------------------------------------------*\
| RGBController_HyperXEve1800.cpp |
| |
| RGBController for HyperX Eve 1800 keyboard |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include "RGBController_HyperXEve1800.h"
using namespace std::chrono_literals;
#define HYPERX_EVE_1800_BRIGHTNESS_MIN 0x00
#define HYPERX_EVE_1800_BRIGHTNESS_MAX 0xFF
#define HYPERX_EVE_1800_ZONE_COUNT 10
/**------------------------------------------------------------------*\
@name HyperX Eve 1800
@category Keyboard
@type USB
@save :x:
@direct :white_check_mark:
@effects :x:
@detectors DetectHyperXEve1800
@comment
\*-------------------------------------------------------------------*/
RGBController_HyperXEve1800::RGBController_HyperXEve1800(HyperXEve1800Controller* controller_ptr)
{
controller = controller_ptr;
name = controller->GetNameString();
vendor = "HyperX";
type = DEVICE_TYPE_KEYBOARD;
description = "HyperX Eve 1800 Keyboard Device";
location = controller->GetDeviceLocation();
serial = controller->GetSerialString();
mode Direct;
Direct.name = "Direct";
Direct.value = 0xFFFF;
Direct.flags = MODE_FLAG_HAS_PER_LED_COLOR | MODE_FLAG_HAS_BRIGHTNESS;
Direct.brightness_min = HYPERX_EVE_1800_BRIGHTNESS_MIN;
Direct.brightness_max = HYPERX_EVE_1800_BRIGHTNESS_MAX;
Direct.brightness = HYPERX_EVE_1800_BRIGHTNESS_MAX;
Direct.color_mode = MODE_COLORS_PER_LED;
modes.push_back(Direct);
SetupZones();
keepalive_thread_run = 1;
keepalive_thread = new std::thread(&RGBController_HyperXEve1800::KeepaliveThread, this);
}
RGBController_HyperXEve1800::~RGBController_HyperXEve1800()
{
keepalive_thread_run = 0;
keepalive_thread->join();
delete keepalive_thread;
delete controller;
}
void RGBController_HyperXEve1800::SetupZones()
{
zone new_zone;
new_zone.name = "Keyboard";
new_zone.type = ZONE_TYPE_LINEAR;
new_zone.leds_min = HYPERX_EVE_1800_ZONE_COUNT;
new_zone.leds_max = HYPERX_EVE_1800_ZONE_COUNT;
new_zone.leds_count = HYPERX_EVE_1800_ZONE_COUNT;
new_zone.matrix_map = NULL;
zones.push_back(new_zone);
for(unsigned int led_idx = 0; led_idx < zones[0].leds_count; led_idx++)
{
led new_led;
new_led.name = "Zone ";
new_led.name.append(std::to_string(led_idx + 1));
leds.push_back(new_led);
}
SetupColors();
}
void RGBController_HyperXEve1800::ResizeZone(int /*zone*/, int /*new_size*/)
{
}
void RGBController_HyperXEve1800::DeviceUpdateLEDs()
{
controller->SetLEDsDirect(colors);
last_update_time = std::chrono::steady_clock::now();
}
void RGBController_HyperXEve1800::UpdateZoneLEDs(int /*zone*/)
{
DeviceUpdateLEDs();
}
void RGBController_HyperXEve1800::UpdateSingleLED(int /*led*/)
{
DeviceUpdateLEDs();
}
void RGBController_HyperXEve1800::DeviceUpdateMode()
{
controller->SetBrightness(modes[active_mode].brightness);
DeviceUpdateLEDs();
}
void RGBController_HyperXEve1800::KeepaliveThread()
{
while(keepalive_thread_run.load())
{
if(active_mode == 0)
{
if((std::chrono::steady_clock::now() - last_update_time) > std::chrono::milliseconds(50))
{
UpdateLEDs();
}
}
std::this_thread::sleep_for(10ms);
}
}
@@ -0,0 +1,39 @@
/*---------------------------------------------------------*\
| RGBController_HyperXEve1800.h |
| |
| RGBController for HyperX Eve 1800 keyboard |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#pragma once
#include <chrono>
#include "RGBController.h"
#include "HyperXEve1800Controller.h"
class RGBController_HyperXEve1800 : public RGBController
{
public:
RGBController_HyperXEve1800(HyperXEve1800Controller* controller_ptr);
~RGBController_HyperXEve1800();
void SetupZones();
void ResizeZone(int zone, int new_size);
void DeviceUpdateLEDs();
void UpdateZoneLEDs(int zone);
void UpdateSingleLED(int led);
void DeviceUpdateMode();
void KeepaliveThread();
private:
HyperXEve1800Controller* controller;
std::thread* keepalive_thread;
std::atomic<bool> keepalive_thread_run;
std::chrono::time_point<std::chrono::steady_clock> last_update_time;
};
@@ -15,6 +15,7 @@
#include "HyperXAlloyOriginsController.h"
#include "HyperXAlloyOriginsCoreController.h"
#include "HyperXAlloyOrigins60and65Controller.h"
#include "HyperXEve1800Controller.h"
#include "HyperXOrigins2_65Controller.h"
#include "RGBController_HyperXAlloyElite.h"
#include "RGBController_HyperXAlloyElite2.h"
@@ -22,6 +23,7 @@
#include "RGBController_HyperXAlloyOrigins.h"
#include "RGBController_HyperXAlloyOriginsCore.h"
#include "RGBController_HyperXAlloyOrigins60and65.h"
#include "RGBController_HyperXEve1800.h"
#include "RGBController_HyperXOrigins2_65.h"
/*-----------------------------------------------------*\
@@ -46,6 +48,7 @@
#define HYPERX_ALLOY_ORIGINS_65_HP_PID 0x038F
#define HYPERX_ALLOY_ORIGINS_CORE_HP_PID 0x098F
#define HYPERX_ALLOY_ORIGINS_HP_PID 0x0591
#define HYPERX_EVE_1800_HP_PID 0x08C2
#define HYPERX_ORIGINS_2_65_HP_PID 0x0CC2
AlloyOrigins60and65MappingLayoutType GetAlloyOrigins60and65MappingLayoutType(int pid)
@@ -156,6 +159,19 @@ void DetectHyperXOrigins2_65(hid_device_info* info, const std::string& name)
}
}
void DetectHyperXEve1800(hid_device_info* info, const std::string& name)
{
hid_device* dev = hid_open_path(info->path);
if(dev)
{
HyperXEve1800Controller* controller = new HyperXEve1800Controller(dev, info->path, name);
RGBController_HyperXEve1800* rgb_controller = new RGBController_HyperXEve1800(controller);
ResourceManager::get()->RegisterRGBController(rgb_controller);
}
}
REGISTER_HID_DETECTOR_IP("HyperX Alloy Elite RGB", DetectHyperXAlloyElite, HYPERX_KEYBOARD_VID, HYPERX_ALLOY_ELITE_PID, 2, 0xFF01);
REGISTER_HID_DETECTOR_IP("HyperX Alloy FPS RGB", DetectHyperXAlloyFPS, HYPERX_KEYBOARD_VID, HYPERX_ALLOY_FPS_RGB_PID, 2, 0xFF01);
REGISTER_HID_DETECTOR_I("HyperX Alloy Origins Core", DetectHyperXAlloyOriginsCore, HYPERX_KEYBOARD_VID, HYPERX_ALLOY_ORIGINS_CORE_PID, 2);
@@ -163,6 +179,7 @@ REGISTER_HID_DETECTOR_I("HyperX Alloy Origins Core", DetectHyperXAlloyOrigi
REGISTER_HID_DETECTOR_I("HyperX Alloy Origins Core (HP)", DetectHyperXAlloyOriginsCore, HP_KEYBOARD_VID, HYPERX_ALLOY_ORIGINS_CORE_HP_PID, 2);
REGISTER_HID_DETECTOR_I("HyperX Origins 2 65 (HP)", DetectHyperXOrigins2_65, HP_KEYBOARD_VID, HYPERX_ORIGINS_2_65_HP_PID, 3);
REGISTER_HID_DETECTOR_I("HyperX Eve 1800 (HP)", DetectHyperXEve1800, HP_KEYBOARD_VID, HYPERX_EVE_1800_HP_PID, 2);
#ifdef _WIN32
REGISTER_HID_DETECTOR_I("HyperX Alloy Origins", DetectHyperXAlloyOrigins, HYPERX_KEYBOARD_VID, HYPERX_ALLOY_ORIGINS_PID, 3);
@@ -118,7 +118,7 @@ void RGBController_IntelArcA770LE::SetupZones()
for(unsigned int led_idx = 0; led_idx < 16; led_idx++)
{
led fan_1_led;
fan_1_led.name = "Fan 1 LED";
fan_1_led.name = "Fan 1 LED " + std::to_string(led_idx + 1);
fan_1_led.value = fan_1_leds[led_idx];
leds.push_back(fan_1_led);
}
@@ -126,7 +126,7 @@ void RGBController_IntelArcA770LE::SetupZones()
for(unsigned int led_idx = 0; led_idx < 16; led_idx++)
{
led fan_2_led;
fan_2_led.name = "Fan 2 LED";
fan_2_led.name = "Fan 2 LED " + std::to_string(led_idx + 1);
fan_2_led.value = fan_2_leds[led_idx];
leds.push_back(fan_2_led);
}
@@ -134,7 +134,7 @@ void RGBController_IntelArcA770LE::SetupZones()
for(unsigned int led_idx = 0; led_idx < 8; led_idx++)
{
led back_led;
back_led.name = "Back LED";
back_led.name = "Back LED " + std::to_string(led_idx + 1);
back_led.value = back_leds[led_idx];
leds.push_back(back_led);
}
@@ -142,7 +142,7 @@ void RGBController_IntelArcA770LE::SetupZones()
for(unsigned int led_idx = 0; led_idx < 50; led_idx++)
{
led ring_led;
ring_led.name = "Ring LED";
ring_led.name = "Ring LED " + std::to_string(led_idx + 1);
ring_led.value = ring_leds[led_idx];
leds.push_back(ring_led);
}
@@ -150,7 +150,7 @@ void RGBController_IntelArcA770LE::SetupZones()
for(unsigned int led_idx = 0; led_idx < 1; led_idx++)
{
led logo_led;
logo_led.name = "Logo LED";
logo_led.name = "Logo LED " + std::to_string(led_idx + 1);
logo_led.value = logo_leds[led_idx];
leds.push_back(logo_led);
}
@@ -10,6 +10,7 @@
#include <thread>
#include <hidapi.h>
#include "Detector.h"
#include "ResourceManager.h"
#include "LogManager.h"
#include "LogitechProtocolCommon.h"
#include "LogitechG203LController.h"
@@ -40,6 +41,8 @@
#include "RGBController_LogitechLightspeed.h"
#include "RGBController_LogitechGPowerPlay.h" // Linux-only
#include "RGBController_LogitechX56.h"
#include "LogitechHIDPP20Controller.h"
#include "RGBController_LogitechHIDPP20.h"
using namespace std::chrono_literals;
@@ -84,6 +87,9 @@ using namespace std::chrono_literals;
#define LOGITECH_G502_HERO_PID 0xC08B
#define LOGITECH_G502_LIGHTSPEED_PID 0xC08D
#define LOGITECH_G502_X_PLUS_PID 0xC095
#define LOGITECH_G515_LS_TKL_PID 0xC355
#define LOGITECH_G522_LIGHTSPEED_USB_PID 0x0B19
#define LOGITECH_G522_LIGHTSPEED_DONGLE_PID 0x0B18
#define LOGITECH_G600_PID 0xC24A
#define LOGITECH_G703_LIGHTSPEED_PID 0xC087
#define LOGITECH_G703_HERO_LIGHTSPEED_PID 0xC090
@@ -135,6 +141,8 @@ using namespace std::chrono_literals;
#define LOGITECH_G903_LIGHTSPEED_VIRTUAL_HERO_PID 0x4087
#define LOGITECH_G_PRO_WIRELESS_VIRTUAL_PID 0x4079
#define LOGITECH_POWERPLAY_MAT_VIRTUAL_PID 0x405F
#define LOGITECH_G502_X_PLUS_LIGHTSPEED_VIRTUAL_PID 0x4099
#define LOGITECH_G515_LS_TKL_LIGHTSPEED_VIRTUAL_PID 0x40B4
/*-----------------------------------------------------*\
| Joystick product IDs |
@@ -650,6 +658,220 @@ void DetectLogitechX56(hid_device_info* info, const std::string& name)
}
}
/*------------------------------------------------------------------------------*\
| Unified HID++ 2.0 Detection |
| Probes IRoot (feature 0x0000) to determine if the device speaks HID++ 2.0. |
| If it does and has RGB features, the unified controller handles it. |
| If not, the device is released for legacy controllers. |
\*------------------------------------------------------------------------------*/
void DetectLogitechHIDPP20(hid_device_info* info, const std::string& /*name*/)
{
hid_device* dev = hid_open_path(info->path);
if(!dev)
{
return;
}
LogitechHIDPP20Controller* controller = new LogitechHIDPP20Controller(
dev, info->path, LOGITECH_DEFAULT_DEVICE_INDEX, false, nullptr,
info->usage_page);
if(controller->Probe())
{
controller->Initialize();
const HIDPP20DeviceCapabilities& caps = controller->GetCapabilities();
if(caps.has_zone_effects || caps.has_perkey)
{
/*-------------------------------------------------*\
| Device has RGB features — create and register |
| RGBController for the UI. |
\*-------------------------------------------------*/
RGBController_LogitechHIDPP20* rgb_controller = new RGBController_LogitechHIDPP20(controller);
LOG_INFO("[%s] Registering RGB controller", caps.device_name.c_str());
ResourceManager::get()->RegisterRGBController(rgb_controller);
/*--------------------------------------------------*\
| Start reader + power threads immediately so we |
| detect connection events and handle power mgmt |
| from the start — not deferred to DeviceUpdateMode. |
\*--------------------------------------------------*/
if(caps.has_power_mgmt || caps.idx_wireless_status != 0)
{
controller->StartPowerManager();
if(!caps.has_power_mgmt && caps.idx_wireless_status != 0)
{
controller->StartEventWatcher();
}
}
}
else if(controller->HasBridge())
{
/*--------------------------------------------------*\
| Centurion dongle with no sub-device — keep the |
| controller alive and start reader thread to watch |
| for sub-device connection events. |
\*--------------------------------------------------*/
LOG_INFO("[%s] Dongle registered, watching for sub-device",
caps.device_name.c_str());
controller->SetRegisterCallback([](RGBController* rgb)
{
ResourceManager::get()->RegisterRGBController(rgb);
});
controller->StartEventWatcher();
}
else
{
/*--------------------------------------------------*\
| Device probed successfully but has no RGB and no |
| bridge — nothing to do (e.g., headset without RGB) |
\*--------------------------------------------------*/
LOG_INFO("[%s] No RGB features, skipping", caps.device_name.c_str());
delete controller;
}
}
else
{
/*--------------------------------------------------*\
| Probe failed. Could be an offline paired device, |
| a stale pairing slot, or a receiver itself. |
| |
| Only skip if the name explicitly says "Receiver". |
| Everything else gets a watcher — devices can come |
| back at any time (power cycle, dongle swap, etc.) |
\*--------------------------------------------------*/
std::string hid_name;
if(info->product_string)
{
std::wstring ws(info->product_string);
hid_name = std::string(ws.begin(), ws.end());
}
if(hid_name.find("Receiver") != std::string::npos ||
hid_name.find("receiver") != std::string::npos)
{
delete controller;
}
else
{
LOG_INFO("[HID++2.0 %s] Probe failed — watching for device (name='%s')",
info->path, hid_name.c_str());
controller->SetRegisterCallback([](RGBController* rgb)
{
ResourceManager::get()->RegisterRGBController(rgb);
});
controller->StartProbeWatcher();
}
}
}
#if defined(_WIN32) || defined(__APPLE__)
/*-------------------------------------------------------------------------------------------------------------------------------------------------*\
| Unified HID++ 2.0 Lightspeed Receiver Detection (Windows / macOS) |
| |
| On Linux, hid-logitech-dj splits receiver traffic into per-slot virtual child hidraw nodes with their own 0x40XX PIDs, so Linux detection can |
| use DetectLogitechHIDPP20 directly against the virtual PIDs. Windows and macOS have no DJ driver — the receiver appears as a single HID device |
| and we must probe each paired slot by hand, addressing it via the HID++ device_index header byte. |
| |
| Iterates device indices 0x01..0x06. c547 is dual-pair, but the loop covers Unifying-style receivers and any future wider-pair variants. Each |
| responding slot gets its own hid_device handle and a shared std::mutex so sibling slots serialize HID writes — matching the pattern in the |
| legacy LogitechLightspeedController (see CreateLogitechLightspeedDevice around line 860). |
| |
| TODO (untested on Windows): runtime reader-thread coordination. Each slot controller starts its own reader thread; on a shared receiver both |
| threads will see both slots' incoming packets. The reader needs to drop frames whose device_index doesn't match its own, or dispatch across |
| sibling controllers. Safe during probe (mutex serializes writes, reads are direct); becomes an issue post-StartPowerManager. |
\*-------------------------------------------------------------------------------------------------------------------------------------------------*/
void DetectLogitechHIDPP20LightspeedReceiver(hid_device_info* info, const std::string& /*name*/)
{
std::shared_ptr<std::mutex> receiver_mutex = std::make_shared<std::mutex>();
for(uint8_t idx = 0x01; idx <= 0x06; idx++)
{
hid_device* dev = hid_open_path(info->path);
if(!dev)
{
continue;
}
LogitechHIDPP20Controller* controller = new LogitechHIDPP20Controller(
dev, info->path, idx, true, receiver_mutex, info->usage_page);
if(!controller->Probe())
{
/*--------------------------------------------------*\
| Slot is empty, stale, or not HID++ 2.0. Destructor |
| closes the per-slot handle we opened above. |
\*--------------------------------------------------*/
delete controller;
continue;
}
controller->Initialize();
const HIDPP20DeviceCapabilities& caps = controller->GetCapabilities();
if(caps.has_zone_effects || caps.has_perkey)
{
RGBController_LogitechHIDPP20* rgb_controller = new RGBController_LogitechHIDPP20(controller);
LOG_INFO("[%s slot=%u] Registering RGB controller", caps.device_name.c_str(), idx);
ResourceManager::get()->RegisterRGBController(rgb_controller);
if(caps.has_power_mgmt || caps.idx_wireless_status != 0)
{
controller->StartPowerManager();
if(!caps.has_power_mgmt && caps.idx_wireless_status != 0)
{
controller->StartEventWatcher();
}
}
}
else
{
LOG_INFO("[%s slot=%u] No RGB features, skipping", caps.device_name.c_str(), idx);
delete controller;
}
}
}
#endif
/*-------------------------------------------------------------------------------------------------------------------------------------------------*\
| Unified HID++ 2.0 Devices |
| PID-specific registrations for devices tested with the unified controller. |
| Wired paths use the device's own USB PID; wireless paths on Linux use the hid-logitech-dj virtual child PIDs (0x40XX range). |
\*-------------------------------------------------------------------------------------------------------------------------------------------------*/
REGISTER_HID_DETECTOR_IPU("Logitech HID++ 2.0 G502 X Plus (wired)", DetectLogitechHIDPP20, LOGITECH_VID, LOGITECH_G502_X_PLUS_PID, 2, 0xFF00, 2);
REGISTER_HID_DETECTOR_IPU("Logitech HID++ 2.0 G515 LS TKL (wired)", DetectLogitechHIDPP20, LOGITECH_VID, LOGITECH_G515_LS_TKL_PID, 2, 0xFF00, 2);
#ifdef __linux__
REGISTER_HID_DETECTOR_IPU("Logitech HID++ 2.0 G502 X Plus (wireless)", DetectLogitechHIDPP20, LOGITECH_VID, LOGITECH_G502_X_PLUS_LIGHTSPEED_VIRTUAL_PID, 2, 0xFF00, 2);
REGISTER_HID_DETECTOR_IPU("Logitech HID++ 2.0 G515 LS TKL (wireless)", DetectLogitechHIDPP20, LOGITECH_VID, LOGITECH_G515_LS_TKL_LIGHTSPEED_VIRTUAL_PID, 2, 0xFF00, 2);
#endif
#if defined(_WIN32) || defined(__APPLE__)
REGISTER_HID_DETECTOR_IPU("Logitech HID++ 2.0 Lightspeed Receiver (C547)", DetectLogitechHIDPP20LightspeedReceiver, LOGITECH_VID, 0xC547, 2, 0xFF00, 2);
#endif
/*-------------------------------------------------------------------------------------------------------------------------------------------------*\
| Centurion-transport devices (63-byte reports on usage page 0xFFA0, 0x50 addressed or 0x51 direct). |
| Centurion receivers are not DJ-style Lightspeed receivers and are not split by hid-logitech-dj — the dongle PID enumerates as a single hidraw |
| on all platforms. The controller's DiscoverTransport parses the report descriptor to pick the 0x50/0x51 variant and runs a 0x00..0xFF address |
| sweep for the 0x50 (addressed) variant, so the detector only needs VID/PID + usage page 0xFFA0. |
\*-------------------------------------------------------------------------------------------------------------------------------------------------*/
REGISTER_HID_DETECTOR_P("Logitech HID++ 2.0 G522 Lightspeed (wired)", DetectLogitechHIDPP20, LOGITECH_VID, LOGITECH_G522_LIGHTSPEED_USB_PID, 0xFFA0);
REGISTER_HID_DETECTOR_P("Logitech HID++ 2.0 G522 Lightspeed (dongle)", DetectLogitechHIDPP20, LOGITECH_VID, LOGITECH_G522_LIGHTSPEED_DONGLE_PID, 0xFFA0);
/*-------------------------------------------------------------------------------------------------------------------------------------------------*\
| Keyboards |
\*-------------------------------------------------------------------------------------------------------------------------------------------------*/
@@ -683,11 +905,11 @@ REGISTER_HID_DETECTOR_IP ("Logitech G600 Gaming Mouse", Dete
REGISTER_HID_DETECTOR_IP ("Logitech G Pro Gaming Mouse", DetectLogitechMouseGPRO, LOGITECH_VID, LOGITECH_G_PRO_PID, 1, 0xFF00);
REGISTER_HID_DETECTOR_IP ("Logitech G Pro HERO Gaming Mouse", DetectLogitechMouseGPRO, LOGITECH_VID, LOGITECH_G_PRO_HERO_PID, 1, 0xFF00);
/*-------------------------------------------------------------------------------------------------------------------------------------------------*\
| Speakers |
| Speakers |
\*-------------------------------------------------------------------------------------------------------------------------------------------------*/
REGISTER_HID_DETECTOR_IPU("Logitech G560 Lightsync Speaker", DetectLogitechG560, LOGITECH_VID, LOGITECH_G560_PID, 2, 0xFF43, 514);
/*-------------------------------------------------------------------------------------------------------------------------------------------------*\
| Headsets |
| Headsets |
\*-------------------------------------------------------------------------------------------------------------------------------------------------*/
REGISTER_HID_DETECTOR_IPU("Logitech G933 Lightsync Headset", DetectLogitechG933, LOGITECH_VID, LOGITECH_G933_PID, 3, 0xFF43, 514);
/*-------------------------------------------------------------------------------------------------------------------------------------------------*\
@@ -890,12 +1112,12 @@ void DetectLogitechWireless(hid_device_info* info, const std::string& /*name*/)
}
}
/*-------------------------------------------------------------------------------------------------------------------------------------------------*\
| Lightspeed Devices (Linux Wireless) |
| |
| DUMMY_DEVICE_DETECTOR("Logitech G Lightspeed Receiver", DetectLogitechWireless, 0x046D, 0xC539 ) |
/*--------------------------------------------------------------------------------------------------------------------------------------------------*\
| Lightspeed Devices (Linux Wireless) |
| |
| DUMMY_DEVICE_DETECTOR("Logitech G Lightspeed Receiver", DetectLogitechWireless, 0x046D, 0xC539 ) |
| DUMMY_DEVICE_DETECTOR("Logitech Powerplay Mat Receiver", DetectLogitechWireless, 0x046D, 0xC53A ) |
\*-------------------------------------------------------------------------------------------------------------------------------------------------*/
\*--------------------------------------------------------------------------------------------------------------------------------------------------*/
REGISTER_HID_DETECTOR_IPU("Logitech G403 Wireless Gaming Mouse", DetectLogitechWireless, LOGITECH_VID, LOGITECH_G403_LIGHTSPEED_VIRTUAL_PID, 2, 0xFF00, 2);
REGISTER_HID_DETECTOR_IPU("Logitech G502 Wireless Gaming Mouse", DetectLogitechWireless, LOGITECH_VID, LOGITECH_G502_LIGHTSPEED_VIRTUAL_PID, 2, 0xFF00, 2);
REGISTER_HID_DETECTOR_IPU("Logitech G703 Wireless Gaming Mouse", DetectLogitechWireless, LOGITECH_VID, LOGITECH_G703_LIGHTSPEED_VIRTUAL_PID, 2, 0xFF00, 2);
@@ -913,7 +1135,6 @@ REGISTER_HID_DETECTOR_IPU("Logitech Powerplay Mat",
| G502 changed to PU to accomodate old and new firmware. Other devices may require similar update #4627 |
\*-------------------------------------------------------------------------------------------------------------------------------------------------*/
REGISTER_HID_DETECTOR_PU("Logitech G502 Wireless Gaming Mouse (wired)", DetectLogitechWired, LOGITECH_VID, LOGITECH_G502_LIGHTSPEED_PID, 0xFF00, 2);
REGISTER_HID_DETECTOR_IPU("Logitech G502 X Plus (wired)", DetectLogitechWired, LOGITECH_VID, LOGITECH_G502_X_PLUS_PID, 2, 0xFF00, 2);
REGISTER_HID_DETECTOR_IPU("Logitech G502 Proteus Spectrum Gaming Mouse", DetectLogitechWired, LOGITECH_VID, LOGITECH_G502_PROTEUS_SPECTRUM_PID, 1, 0xFF00, 2);
REGISTER_HID_DETECTOR_IPU("Logitech G502 HERO Gaming Mouse", DetectLogitechWired, LOGITECH_VID, LOGITECH_G502_HERO_PID, 1, 0xFF00, 2);
REGISTER_HID_DETECTOR_IPU("Logitech G403 Prodigy Gaming Mouse", DetectLogitechWired, LOGITECH_VID, LOGITECH_G403_PID, 1, 0xFF00, 2);
@@ -0,0 +1,874 @@
/*---------------------------------------------------------*\
| LogitechHIDPP20Controller.h |
| |
| Unified Logitech HID++ 2.0 controller |
| |
| Uses feature discovery (IRoot 0x0000) to dynamically |
| determine device capabilities and adapt to any HID++ |
| 2.0 device with RGB lighting support. |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#pragma once
#include <string>
#include <vector>
#include <map>
#include <mutex>
#include <memory>
#include <thread>
#include <atomic>
#include <functional>
#include <chrono>
#include <deque>
#include <condition_variable>
#include <hidapi.h>
#include "RGBController.h"
#include "LogitechProtocolCommon.h"
/*-----------------------------------------------------*\
| HID++ 2.0 Feature Page IDs |
\*-----------------------------------------------------*/
#define HIDPP20_FEAT_IROOT 0x0000
#define HIDPP20_FEAT_FEATURE_SET 0x0001
#define HIDPP20_FEAT_DEVICE_NAME_TYPE 0x0005
#define HIDPP20_FEAT_ONBOARD_PROFILES 0x8100
#define HIDPP20_FEAT_PROFILE_MANAGEMENT 0x8101
#define HIDPP20_FEAT_FIRMWARE_INFO 0x0003
#define HIDPP20_FEAT_CENTPPBRIDGE 0x0003 /* same ID, different meaning on Centurion */
#define HIDPP20_FEAT_COLOR_LED_EFFECTS 0x8070
#define HIDPP20_FEAT_RGB_EFFECTS 0x8071
#define HIDPP20_FEAT_PER_KEY_LIGHTING_V1 0x8080
#define HIDPP20_FEAT_PER_KEY_LIGHTING_V2 0x8081
#define HIDPP20_FEAT_KEYBOARD_LAYOUT 0x4540
#define HIDPP20_FEAT_DISABLE_KEYS_BY_USAGE 0x4522
#define HIDPP20_FEAT_CENTURION_RGB 0x0600
#define HIDPP20_FEAT_HEADSET_RGB_HOSTMODE 0x0620
#define HIDPP20_FEAT_CENTURION_DEVICE_INFO 0x0100
#define HIDPP20_FEAT_CENTURION_DEVICE_NAME 0x0101
#define HIDPP20_FEAT_UNIFIED_BATTERY 0x1004
#define HIDPP20_FEAT_WIRELESS_STATUS 0x1D4B
/*-----------------------------------------------------*\
| HID++ 2.0 Function IDs (byte 3 high nibble) |
| Function ID is shifted left 4 bits, low nibble = swID |
\*-----------------------------------------------------*/
/*-----------------------------------------------------*\
| HID++ Software ID — identifies our responses. |
| Must avoid: 0x00 (firmware), 0x01 (vendor app), |
| 0x02-0x0F (Solaar cycles these). |
| There are only 16 values (4-bit field), and all are |
| claimed. We pick a fixed value and will coordinate |
| with Solaar to exclude it from its cycle. |
\*-----------------------------------------------------*/
#define HIDPP20_SW_ID 0x07
/*-----------------------------------------------------*\
| Feature 0x8071 functions |
\*-----------------------------------------------------*/
#define FN_8071_GET_INFO 0x00
#define FN_8071_SET_EFFECT 0x10
#define FN_8071_SET_PATTERN 0x20
#define FN_8071_NV_CONFIG 0x30
#define FN_8071_BIN_INFO 0x40
#define FN_8071_SW_CONTROL 0x50
#define FN_8071_SYNC 0x60
#define FN_8071_PWR_CONFIG 0x70
#define FN_8071_PWR_MODE 0x80
/*-----------------------------------------------------*\
| Feature 0x8081 functions |
\*-----------------------------------------------------*/
#define FN_8081_GET_INFO 0x00
#define FN_8081_SET_INDIVIDUAL 0x10
#define FN_8081_SET_CONSECUTIVE 0x20
#define FN_8081_SET_DELTA_5BIT 0x30
#define FN_8081_SET_DELTA_4BIT 0x40
#define FN_8081_SET_RANGE 0x50
#define FN_8081_SET_SINGLE_VALUE 0x60
#define FN_8081_FRAME_END 0x70
/*-----------------------------------------------------*\
| Feature 0x0620 functions (headset RGB hostmode) |
\*-----------------------------------------------------*/
#define FN_0620_GET_INFO 0x00
#define FN_0620_GET_RGB_ZONE_INFO 0x10
#define FN_0620_SET_INDIVIDUAL_RGB_ZONES 0x20
#define FN_0620_SET_CONSECUTIVE_RGB_ZONES 0x30
#define FN_0620_SET_RANGE_RGB_ZONES 0x40
#define FN_0620_SET_RGB_ZONES_SINGLE_VALUE 0x50
#define FN_0620_FRAME_END 0x60
#define FN_0620_GET_HOST_MODE_STATE 0x70
#define FN_0620_SET_HOST_MODE_STATE 0x80
/*-----------------------------------------------------*\
| Feature 0x8100 functions |
\*-----------------------------------------------------*/
#define FN_8100_SET_ONBOARD_MODE 0x10
#define FN_8100_GET_ONBOARD_MODE 0x20
/*-----------------------------------------------------*\
| Feature 0x8101 functions |
\*-----------------------------------------------------*/
#define FN_8101_GET_SET_MODE 0x60
#define FN_8101_LOAD 0x80 // load(partition, sector, size)
#define FN_8101_READBUFFER 0xC0 // readBuffer(offset)
/*-----------------------------------------------------*\
| Zone cluster effect entry |
\*-----------------------------------------------------*/
struct HIDPP20Effect
{
uint8_t index;
uint16_t effect_id;
uint16_t capabilities;
uint16_t default_period;
};
/*-----------------------------------------------------*\
| Zone cluster info from 0x8071 GetRgbClusterInfo |
\*-----------------------------------------------------*/
struct HIDPP20ZoneCluster
{
uint8_t index;
uint16_t location;
uint8_t effect_count;
std::vector<HIDPP20Effect> effects;
};
/*-----------------------------------------------------*\
| Per-model device quirks — behavioral differences that |
| can't be detected via feature probing. |
\*-----------------------------------------------------*/
enum HIDPP20DeviceQuirks : uint32_t
{
HIDPP20_QUIRK_NONE = 0,
HIDPP20_QUIRK_FADE_ACCEPTS_WRITES = (1 << 0), // firmware accepts host frames during sleep fade without waking
};
struct HIDPP20DeviceQuirkEntry
{
uint16_t pid_wireless;
uint16_t pid_wired;
uint32_t quirks;
};
/*---------------------------------------------------------*\
| Default: suppress frames while SLEEPING. Safe everywhere |
| — it cannot wake a device that treats writes as activity. |
| Devices listed here opt out of suppression because their |
| firmware accepts writes during the fade without |
| cancelling sleep. |
\*---------------------------------------------------------*/
static constexpr HIDPP20DeviceQuirkEntry HIDPP20_DEVICE_QUIRK_TABLE[] =
{
{ 0x40B4, 0xC355, HIDPP20_QUIRK_FADE_ACCEPTS_WRITES }, // G515 LS TKL
};
/*-----------------------------------------------------*\
| Device capabilities discovered via feature probing |
\*-----------------------------------------------------*/
struct HIDPP20DeviceCapabilities
{
std::string device_name;
uint8_t device_type;
std::string firmware_version;
std::string serial_number;
std::string unit_id; // stable hardware ID (from FirmwareInfo fn0)
uint16_t pid_wireless; // wireless virtual PID (from FirmwareInfo fn0)
uint16_t pid_wired; // wired USB PID (from FirmwareInfo fn0)
uint32_t quirks; // resolved from HIDPP20_DEVICE_QUIRK_TABLE after PID discovery
/*--------------------------------------------------*\
| Complete feature map (feature_id → runtime index) |
| Built once by EnumerateFeatures, used by all |
| subsequent GetFeatureIndex lookups (no wire). |
\*--------------------------------------------------*/
std::map<uint16_t, uint8_t> feature_map;
std::map<uint16_t, uint8_t> feature_versions; /* feature_id -> version byte */
bool feature_map_complete; // true after bulk enumeration
/*-------------------------------------------------*\
| Feature indices (0 = not supported) |
\*-------------------------------------------------*/
uint8_t idx_onboard_profiles;
uint8_t idx_profile_management;
uint8_t idx_rgb_effects;
uint8_t idx_headset_rgb_hostmode; /* 0x0620 — Centurion headset RGB */
uint8_t idx_perkey_v2;
uint8_t idx_perkey_v1;
uint8_t idx_wireless_status;
uint8_t idx_disable_keys_by_usage; /* 0x4522 — keyboard-family handshake */
uint16_t rgb_feature_page;
/*--------------------------------------------------*\
| Resolved function IDs for the RGB effects feature |
| Varies between 0x8070, 0x8071, 0x0600 |
\*--------------------------------------------------*/
uint8_t fn_set_effect;
uint8_t fn_sw_control;
uint8_t fn_pwr_config;
uint8_t fn_pwr_mode;
bool has_power_mgmt;
bool sw_control_simple;
/*--------------------------------------------------*\
| Persistent NV settings read from RGBEffects fn3 |
| (FN_8071_NV_CONFIG). Capability 0x0020 is the |
| sleep ramp / off-ramp transition (enabled + |
| ramp_seconds). |
\*--------------------------------------------------*/
bool nv_sleep_ramp_known;
bool nv_sleep_ramp_enabled;
uint8_t nv_sleep_ramp_seconds;
/*---------------------------------------------------*\
| Device-firmware effect cards (0x8071 fn0 |
| GetEffectSpecificInfo). Populated by |
| DiscoverEffectCards at feature-discovery time. |
| |
| has_effect_cards — probe returned a valid response |
| for firmware card 0 page 1 (no InvalidArgument). |
| effect_card_template[0..1] — device-wide constant |
| bytes read from that response at data[10..11]. |
| Echoed back into prep1 of DoObservedPerKeyPrep |
| at SetEffectByIndex params[6..7]. |
| |
| Gate for the observed per-key prep is now |
| has_effect_cards — replaces the earlier |
| "effects.size() < 5" heuristic, which was a proxy |
| that correlated with "has cards" on the devices we |
| happened to know but had no principled meaning. |
\*---------------------------------------------------*/
bool has_effect_cards;
uint8_t effect_card_template[2];
/*-------------------------------------------------*\
| Discovered zone and LED data |
\*-------------------------------------------------*/
std::vector<HIDPP20ZoneCluster> zone_clusters;
std::vector<uint16_t> perkey_zone_ids;
std::vector<uint8_t> headset_rgb_hostmode_zone_ids; /* 0x0620 fn1 result */
bool has_perkey;
bool has_zone_effects;
bool is_headset_rgb_hostmode; /* 0x0620 path selected */
bool has_numpad;
uint8_t keyboard_layout_code;
};
/*------------------------------------------------------*\
| Transport type — determines wire framing |
\*------------------------------------------------------*/
enum HIDPP20TransportType
{
HIDPP20_TRANSPORT_STANDARD, // 0xFF00/0xFF43: report IDs 0x10/0x11, 7/20 bytes
HIDPP20_TRANSPORT_CENTURION // 0xFFA0: report ID 0x51 or 0x50, 64 bytes,
// with CPL framing and CentPPBridge sub-device routing
};
/*------------------------------------------------------*\
| Transport layer — abstracts wire format differences |
| |
| Standard HID++ and Centurion both carry the same |
| feature/function/data payload, but with different |
| report framing. This struct holds transport state |
| so SendMessage/ReadMessage can adapt. |
\*------------------------------------------------------*/
struct HIDPP20Transport
{
HIDPP20TransportType type;
uint16_t usage_page; // 0xFF00, 0xFF43, or 0xFFA0
uint8_t report_id; // 0x10/0x11 for standard, 0x51/0x50 for Centurion
bool addressed; // Centurion 0x50 has device address byte
uint8_t device_address; // Centurion 0x50: device address (e.g., 0x23)
uint8_t bridge_feat_idx; // CentPPBridge feature index on parent (0 if N/A)
uint8_t sub_device_id; // CentPPBridge sub-device ID (typically 0)
uint16_t bridge_mtu; // CentPPBridge MTU from getConnectionInfo:
// 0 = no sub-device, sendFragment will fail
// >0 = sub-device present, payload size in bytes
};
/*-----------------------------------------------------*\
| Power management state machine |
| Matches Solaar's RGBPowerManager states |
\*-----------------------------------------------------*/
enum HIDPP20PowerState
{
HIDPP20_POWER_ACTIVE = 0,
HIDPP20_POWER_DIMMING = 1,
HIDPP20_POWER_IDLE = 2,
HIDPP20_POWER_SLEEPING = 3,
};
/*-----------------------------------------------------*\
| Parsed HID++ message for the response queue |
\*-----------------------------------------------------*/
struct HIDPP20RawMessage
{
uint8_t feat;
uint8_t func;
uint8_t data[60];
int result;
};
/*------------------------------------------------------*\
| Per-key write tracking. SendPerKeyData is fire-and- |
| forget at the wire layer; we track which zones each |
| outstanding write covers so PerKeyFrameEnd can match |
| ACKs back by FIFO order and report which zones the |
| firmware actually committed. |
\*------------------------------------------------------*/
struct OutstandingPerKeyWrite
{
uint8_t function; // FN_8081_* (high nibble carries the type)
std::vector<uint8_t> zone_ids; // zones covered by this packet
};
/*-----------------------------------------------------*\
| Result of a per-key frame commit. The caller uses |
| these to update its delta-tracking state: |
| - frame_end_acked: did the FrameEnd packet ACK? |
| - acked_zones: zones whose write packet ACKed |
| - attempted_zones: every zone written this frame |
\*-----------------------------------------------------*/
struct PerKeyFrameResult
{
bool frame_end_acked;
std::vector<uint8_t> acked_zones;
std::vector<uint8_t> attempted_zones;
};
/*-------------------------------------------------------*\
| Retry policy for SendAcked. |
| |
| Controls the send/read/retry loop for a single HID++ |
| request. Three canned policies cover all use cases: |
| |
| Reliable: probe/discovery/set/get (~6s worst case) |
| FrameEnd: per-key commit gate (~230ms worst) |
| Streaming: per-key write inside the animation loop |
| (~80ms worst) |
| |
| backoff_ms[i] is the delay applied BEFORE the i-th |
| attempt. backoff_ms[0] is normally 0 (no delay before |
| the first send). The schedule mirrors the firmware's |
| own event burst pattern (63→125→250→500→1000→2000ms, |
| "catch at least one of N"). |
\*-------------------------------------------------------*/
struct HIDPP20RetryPolicy
{
const uint16_t* backoff_ms; // schedule[i] = delay before attempt i
uint8_t attempts; // length of backoff_ms (>=1)
uint16_t read_window_ms; // per-attempt read budget
bool flush_before; // flush response queue at call start
bool retry_on_busy; // BUSY (0x08) -> retry the send
const char* name; // for logging ("reliable", etc.)
};
/*------------------------------------------------------*\
| Canned backoff schedules. |
\*------------------------------------------------------*/
static constexpr uint16_t HIDPP20_BACKOFF_RELIABLE[] =
{ 0, 63, 125, 250, 500, 1000, 2000 };
static constexpr uint16_t HIDPP20_BACKOFF_PROBE[] =
{ 0, 100 };
/*------------------------------------------------------*\
| SW-control reclaim backoff. Used by ReconnectDevice |
| to retry the claim+push sequence after a wireless |
| reconnect, racing the firmware boot animation. The |
| vendor app typically lands control in ~50ms; this |
| schedule fits |
| 6 attempts inside ~620ms so the animation never gets |
| a chance to become visible. |
\*------------------------------------------------------*/
static constexpr uint16_t HIDPP20_RECLAIM_BACKOFF_MS[] =
{ 0, 20, 40, 80, 160, 320 };
/*------------------------------------------------------*\
| FrameEnd BUSY retry backoff. Used by PerKeyFrameEnd |
| when the firmware returns HID++ error 0x08 (BUSY) |
| because it's still draining the per-key write queue. |
| First retry is fast (~2 USB round trips) for the |
| common case where BUSY was transient; subsequent |
| retries give actual drain headroom. Total worst case |
| ~180ms, fits inside the PerKeyFrameEnd 300ms deadline |
| with margin for the eventual ACK to land. |
\*------------------------------------------------------*/
static constexpr uint16_t HIDPP20_FRAME_END_BUSY_BACKOFF_MS[] =
{ 30, 60, 90 };
/*-----------------------------------------------------*\
| Deep-sleep detection threshold. After StartSleep() |
| commands the firmware fade, the device eventually |
| enters deep sleep and returns BUSY to every FrameEnd. |
| Once this many consecutive FrameEnd attempts exhaust |
| all BUSY retries while power_state == SLEEPING, we |
| suppress further frame sends until Wake() fires. |
\*-----------------------------------------------------*/
static constexpr int HIDPP20_DEEP_SLEEP_FAILURE_THRESHOLD = 5;
/*------------------------------------------------------*\
| Per-key frame retry backoff. Used when a full |
| DeviceUpdateLEDs pass completes with some zones |
| unacked (partial commit). The retry re-runs a whole |
| frame from the power thread, so the backoff is |
| between full frames, not individual packets. |
| |
| First value aligned to the power thread's 50ms poll |
| cadence — anything shorter rounds up anyway, and |
| matching the tick makes latency predictable. |
| |
| Worst case: 5 retries, cumulative ~1550ms. This |
| covers the reconnect-transient window where the G502 |
| firmware silently drops per-key writes for several |
| hundred ms after the wireless link re-establishes. |
\*------------------------------------------------------*/
static constexpr uint16_t HIDPP20_REPAINT_RETRY_BACKOFF_MS[] =
{ 50, 100, 200, 400, 800 };
static constexpr HIDPP20RetryPolicy HIDPP20_POLICY_RELIABLE = {
HIDPP20_BACKOFF_RELIABLE,
sizeof(HIDPP20_BACKOFF_RELIABLE) / sizeof(uint16_t),
300, // read window
true, // flush_before
true, // retry_on_busy
"reliable"
};
/*------------------------------------------------------*\
| Probe policy: tight budget for is-this-HID++ checks |
| during initial discovery. ~500ms worst case for dead |
| devices, vs ~6s for reliable. One retry handles a |
| transient hiccup on the first IRoot call (e.g. on a |
| busy mouse), but we bail fast on truly non-responsive |
| or non-HID++ hidraws so probe latency stays bounded. |
\*------------------------------------------------------*/
static constexpr HIDPP20RetryPolicy HIDPP20_POLICY_PROBE = {
HIDPP20_BACKOFF_PROBE,
sizeof(HIDPP20_BACKOFF_PROBE) / sizeof(uint16_t),
200, // read window
true, // flush_before
true, // retry_on_busy
"probe"
};
class LogitechHIDPP20Controller
{
public:
LogitechHIDPP20Controller(hid_device* dev, const char* path,
uint8_t device_index, bool wireless,
std::shared_ptr<std::mutex> mutex_ptr,
uint16_t usage_page = 0xFF00);
~LogitechHIDPP20Controller();
/*-------------------------------------------------*\
| Lifecycle |
\*-------------------------------------------------*/
bool Probe();
void Initialize();
void Shutdown();
/*-------------------------------------------------*\
| Accessors |
\*-------------------------------------------------*/
const HIDPP20DeviceCapabilities& GetCapabilities() const;
std::string GetDeviceLocation();
std::string GetSerialString();
uint32_t GetInitGeneration() const;
/*--------------------------------------------------*\
| Per-key lighting (0x8081) |
| |
| Per-key writes are fire-and-forget at the wire |
| layer. SendPerKeyData enqueues an outstanding |
| write entry; PerKeyFrameEnd drains the response |
| queue, matches ACKs by FIFO, and returns which |
| zones the firmware actually committed plus |
| whether the FrameEnd itself ACKed. |
\*--------------------------------------------------*/
void SetPerKeyColors(const std::vector<std::pair<uint16_t, RGBColor>>& zone_colors);
void SetAllPerKeyColor(RGBColor color);
void SendPerKeyData(uint8_t perkey_idx, uint8_t function,
const uint8_t* data, size_t len,
const std::vector<uint8_t>& zone_ids);
PerKeyFrameResult PerKeyFrameEnd();
/*-------------------------------------------------*\
| Zone effects (0x8071 / 0x8070) |
\*-------------------------------------------------*/
void SetZoneEffect(uint8_t cluster_idx, uint8_t effect_idx,
uint16_t effect_id,
unsigned char r, unsigned char g, unsigned char b,
uint16_t period, unsigned char brightness,
unsigned char direction, bool persist);
/*---------------------------------------------------*\
| Headset RGB hostmode (0x0620). |
| Sticky-claim model: SetHostMode() claims once via |
| fn8, then each write is fn5 (single-value) or fn2 |
| (individual) + fn6 FrameEnd[0x01]. 0x02 persist was |
| tested and does not work on G522 firmware. |
\*---------------------------------------------------*/
void SetHeadsetRGBHostmodeColors(const std::vector<RGBColor>& zone_colors);
/*-------------------------------------------------*\
| SW control management |
\*-------------------------------------------------*/
int SetSWControl(uint8_t mode, uint8_t flags);
void SetRGBPowerMode(uint8_t mode);
void SetHostMode();
bool ClaimSWControlIfNeeded();
void UpgradeSwControlAfterFirstPaint();
/*-------------------------------------------------*\
| Keyboard-family handshake (0x4522 fn3 + fn1). |
| G815 / G915 / G Pro send this before any mode |
| write. Feature-gated no-op on devices (G502 / |
| G515) that don't enumerate 0x4522. |
\*-------------------------------------------------*/
void DoDisableKeysByUsageHandshake();
/*--------------------------------------------------*\
| Keyboard-family per-key takeover prep. |
| Per-cluster SetEffectByIndex(effectIdx=0=Off, |
| persist=1) + primer key via SetIndividualRgbZones |
| + FrameEnd. Matches G815 / G915 InitializeDirect. |
| Gated on 0x4522 + per-key V2 presence. |
\*--------------------------------------------------*/
void DoKeyboardFamilyPerKeyPrep();
/*--------------------------------------------------*\
| Wake-repaint flag. Set by Wake() before calling |
| request_repaint_fn so the repaint callback knows |
| to invalidate sent_colors (force a full per-key |
| push) without triggering the claim/prep sequence. |
\*--------------------------------------------------*/
bool ConsumeWakeFullRepaint();
bool NeedsPrepSequence() const { return sw_control_needs_upgrade_to_5; }
/*--------------------------------------------------*\
| Per-key retry scheduling. Called by the RGB |
| controller's DeviceUpdateLEDs on partial-commit |
| frames; the power thread polls and fires |
| request_repaint_fn when a retry deadline expires. |
\*--------------------------------------------------*/
void ScheduleRetryPaint();
void CancelRetryPaint();
void TickRetryPaintIfPending();
/*--------------------------------------------------*\
| Observed per-key prep sequence. Two |
| SetEffectByIndex calls cloned byte-for-byte from a |
| wire capture on a G502 X PLUS. |
| Used in place of the Static-pass-through prep when |
| the device's RGBEffects enumeration matches the |
| G502 shape — see DeviceUpdateLEDs for gating. |
\*--------------------------------------------------*/
void DoObservedPerKeyPrep();
/*--------------------------------------------------*\
| Power management (idle/dim/sleep/wake) |
\*--------------------------------------------------*/
void StartPowerManager();
void StopPowerManager();
void StartEventWatcher();
void StartProbeWatcher();
bool HasBridge() const;
void SetRepaintCallback(std::function<void()> repaint);
void SetReapplyActiveModeCallback(std::function<bool()> cb);
void SetRegisterCallback(std::function<void(RGBController*)> cb);
HIDPP20PowerState GetPowerState() const;
int GetDimBrightness() const;
bool IsOnline() const;
bool IsDeepSleep() const;
void SetWireless(bool w) { wireless = w; }
bool QueryWirelessStatus();
bool QueryExternalPower();
void FlushResponseQueue();
private:
hid_device* dev;
std::string location;
uint8_t device_index;
bool wireless;
std::shared_ptr<std::mutex> mutex;
HIDPP20DeviceCapabilities caps;
HIDPP20Transport transport;
bool initialized;
bool sw_control_claimed;
bool sw_control_needs_upgrade_to_5;
uint32_t frame_counter;
/*--------------------------------------------------*\
| Retry-paint state (partial-commit recovery). |
| retry_paint_deadline_ zero = no retry pending. |
| retry_paint_attempt_ indexes into |
| HIDPP20_REPAINT_RETRY_BACKOFF_MS; once it reaches |
| the array length, we give up for this sequence. |
| Atomic so both the paint thread (RGB controller) |
| and the power thread can access without locks. |
\*--------------------------------------------------*/
std::atomic<std::chrono::steady_clock::time_point> retry_paint_deadline_;
std::atomic<uint8_t> retry_paint_attempt_;
std::atomic<bool> wake_full_repaint_pending_;
uint32_t init_generation;
std::string log_tag;
/*---------------------------------------------------*\
| Transport-layer I/O |
| |
| SendMessage/ReadMessage handle wire framing based |
| on transport.type. Upper layers pass feature index, |
| function ID, and payload — the transport layer |
| wraps them in the correct report format. |
\*---------------------------------------------------*/
int SendMessage(uint8_t feat_idx, uint8_t function,
const uint8_t* data, size_t len);
int ReadMessage(uint8_t* feat_idx_out, uint8_t* function_out,
uint8_t* data_out, size_t data_max,
int timeout_ms = LOGITECH_PROTOCOL_TIMEOUT);
/*--------------------------------------------------*\
| Standard HID++ transport (0xFF00/0xFF43) |
\*--------------------------------------------------*/
int SendStandard(uint8_t feat_idx, uint8_t function,
const uint8_t* data, size_t len);
int ReadStandardDirect(uint8_t* feat_idx_out, uint8_t* function_out,
uint8_t* data_out, size_t data_max,
int timeout_ms);
/*--------------------------------------------------*\
| Centurion transport (0xFFA0) |
| Wraps messages in CPL framing, routes through |
| CentPPBridge for sub-device access. |
\*--------------------------------------------------*/
int SendCenturion(uint8_t feat_idx, uint8_t function,
const uint8_t* data, size_t len);
int ReadCenturionDirect(uint8_t* feat_idx_out, uint8_t* function_out,
uint8_t* data_out, size_t data_max,
int timeout_ms);
/*--------------------------------------------------*\
| Reader thread dispatch layer |
| ReadHIDDirect: raw HID read (used by reader thread |
| and during Probe before reader starts). |
| ReadFromQueue: waits on response queue filled by |
| the reader thread. |
\*--------------------------------------------------*/
int ReadHIDDirect(uint8_t* feat_idx_out, uint8_t* function_out,
uint8_t* data_out, size_t data_max,
int timeout_ms);
int ReadFromQueue(uint8_t* feat_idx_out, uint8_t* function_out,
uint8_t* data_out, size_t data_max,
int timeout_ms);
/*---------------------------------------------------*\
| High-level helpers |
| SendAndReceive is retained as a thin wrapper |
| around SendAcked with the reliable policy, for |
| call-site stability. |
\*---------------------------------------------------*/
int SendAndReceive(uint8_t feat_idx, uint8_t function,
const uint8_t* send_data, size_t send_len,
uint8_t* recv_data, size_t recv_max);
/*---------------------------------------------------*\
| Unified send-and-ack primitive with retry policy. |
| All command paths converge here. Returns: |
| >0 : bytes copied into recv_data |
| 0 : timeout / BUSY exhaustion |
| -1 : non-BUSY HID++ error |
| -2 : wire error (SendMessage failed) |
| If hidpp20_error_out is non-null and return is -1, |
| the HID++ error code is stored there. |
\*---------------------------------------------------*/
int SendAcked(uint8_t feat_idx, uint8_t function,
const uint8_t* send_data, size_t send_len,
uint8_t* recv_data, size_t recv_max,
const HIDPP20RetryPolicy& policy = HIDPP20_POLICY_RELIABLE,
uint8_t* hidpp20_error_out = nullptr);
/*--------------------------------------------------*\
| Compatibility shim: same as SendAcked but writes |
| the response into a blankFAPmessage. Used by |
| callers that inherited the SendLong+ReadResponse |
| interface and inspect response.data[] downstream. |
\*--------------------------------------------------*/
int SendAckedIntoFAP(uint8_t feat_idx, uint8_t function,
const uint8_t* send_data, size_t send_len,
blankFAPmessage& response,
const HIDPP20RetryPolicy& policy = HIDPP20_POLICY_RELIABLE);
/*-------------------------------------------------*\
| Feature discovery |
\*-------------------------------------------------*/
uint8_t GetFeatureIndex(uint16_t feature_page,
const HIDPP20RetryPolicy& policy = HIDPP20_POLICY_RELIABLE);
uint8_t GetFeatureVersion(uint16_t feature_page) const;
void DiscoverTransport();
void DiscoverDeviceName();
void DiscoverDeviceType();
void EnumerateFeatures(uint8_t feature_set_idx);
void DiscoverFirmwareInfo();
void DiscoverRGBEffects();
void DiscoverEffectCards();
void DiscoverHeadsetRGBHostmode();
void DiscoverPerKeyZones();
void DiscoverKeyboardLayout();
/*-------------------------------------------------*\
| Power management internals |
\*-------------------------------------------------*/
void ReaderThreadFunc();
void PowerThreadFunc();
void DispatchEvent(uint8_t feat, uint8_t func, const uint8_t* data);
void OnUserActivity(uint8_t activity_type);
void StartDimRamp();
void DimRampStep();
void StartSleep();
void Wake();
void ReadFirmwareTimers();
void ReadNvSleepRampConfig();
void WritePowerConfig(uint16_t idle_s, uint16_t sleep_s);
void ReadActiveProfileSector();
void ReprobeSubDevice();
void ReconnectDevice();
void FullReprobe();
void RediscoverFeatures();
/*----------------------------------------------------*\
| Platform-specific. ScanForDevice walks the OS-level |
| HID enumeration to find the same physical device on |
| a new path (USB<->wireless transitions). Linux uses |
| sysfs; Windows and macOS use hidapi + serial_number |
| matching. Bodies live in |
| LogitechHIDPP20Controller_Linux.cpp and |
| LogitechHIDPP20Controller_Windows_MacOS.cpp. |
\*----------------------------------------------------*/
bool ScanForDevice(bool force = false);
/*----------------------------------------------------*\
| Platform-specific. Returns the friendly name for a |
| Centurion sub-device at the given hidapi path, or "" |
| if no name is available. Linux reads HID_NAME from |
| sysfs; Windows uses hid_device_info::product_string. |
\*----------------------------------------------------*/
std::string GetCenturionSubDeviceName(const std::string& path);
void SwapHIDHandle(hid_device* new_dev, const std::string& new_path);
/*-------------------------------------------------*\
| Reader thread + response queue |
\*-------------------------------------------------*/
std::thread* reader_thread;
std::atomic<bool> reader_running;
std::mutex response_mutex;
std::condition_variable response_cv;
std::deque<HIDPP20RawMessage> response_queue;
/*-------------------------------------------------*\
| Power thread (state machine + command sender) |
\*-------------------------------------------------*/
std::thread* power_thread;
std::atomic<bool> power_thread_running;
std::atomic<int> pending_activity; // -1=none, 0=idle, 1+=active
std::atomic<int> pending_connection; // 0=none, +1=connected, -1=disconnected
std::atomic<int> pending_path_check; // HID++1.0 DJ notification → force-scan retries remaining (0=idle)
std::atomic<bool> device_online; // false when device is unreachable
std::atomic<int> consecutive_timeouts; // reset on successful response
std::atomic<bool> watcher_mode; // true when retrying failed probe
/*-------------------------------------------------*\
| Power management state |
\*-------------------------------------------------*/
HIDPP20PowerState power_state;
std::mutex power_mutex;
std::atomic<bool> deep_sleep; // true once device stops responding after StartSleep()
std::atomic<int> consecutive_frame_end_failures; // FrameEnd BUSY exhaustions while SLEEPING
/*-------------------------------------------------*\
| Dim ramp state |
| dim_brightness_pct is applied by DeviceUpdateLEDs |
| to scale colors before pushing to device. |
| This is our own host-side animation, independent |
| of any firmware dim/sleep timers. |
\*-------------------------------------------------*/
static const int DIM_STEPS = 25;
static const int DIM_INTERVAL_MS = 200;
static const int DIM_TARGET_PCT = 50;
std::atomic<int> dim_brightness_pct; // 100=full, 50=dimmed
int dim_step;
std::chrono::steady_clock::time_point next_dim_time;
/*-------------------------------------------------*\
| Sleep timer |
\*-------------------------------------------------*/
std::chrono::steady_clock::time_point sleep_deadline;
/*--------------------------------------------------*\
| Last idle-settings re-read timestamp. Drives the |
| 500ms poll in PowerThreadFunc that re-reads the |
| LogitechHIDPP20IdleSettings JSON key so updates |
| from the plugin (or manual edits) apply within |
| about half a second without any callback plumbing. |
\*--------------------------------------------------*/
std::chrono::steady_clock::time_point last_idle_poll;
/*-------------------------------------------------*\
| Effective idle/sleep timers used by the state |
| machine. Populated from the firmware snapshot by |
| default, then possibly overridden by profile |
| values in ApplyPowerSavingProfile. |
\*-------------------------------------------------*/
uint16_t idle_timeout_s;
uint16_t sleep_timeout_s;
/*--------------------------------------------------*\
| Firmware-configured timer snapshot. Read at init |
| (and on reconnect) by ReadFirmwareTimers and |
| never overwritten by profile application, so |
| that the unconfigured fallback path and transition |
| back from a user profile both have a clean set |
| of defaults to return to. |
\*--------------------------------------------------*/
uint16_t fw_idle_timeout_s = 60;
uint16_t fw_sleep_timeout_s = 300;
uint16_t written_idle_s = 0; // last value written to device RAM (0 = not written yet)
uint16_t written_sleep_s = 0;
/*--------------------------------------------------*\
| Host-side idle/dim/sleep state. |
| Populated from LogitechHIDPP20IdleSettings on each |
| ApplyPowerSavingProfile() invocation. |
\*--------------------------------------------------*/
bool ps_dim_enabled = false;
int ps_dim_target_pct = DIM_TARGET_PCT;
bool ps_sleep_enabled = false;
bool ps_on_external_power = false;
bool ps_last_logged_external = false;
int ps_last_logged_pct = -1;
int ps_last_logged_idle = -1;
int ps_last_logged_sleep = -1;
uint16_t last_power_raw = 0xFFFF; // dedup for QueryExternalPower TRACE
uint8_t idx_unified_battery = 0;
void ApplyPowerSavingProfile();
bool IsCurrentlyWireless() const;
/*-------------------------------------------------*\
| Per-key write tracking (per active frame) |
| Populated by SendPerKeyData, drained by |
| PerKeyFrameEnd. Single-threaded — only the RGB |
| controller thread touches per-key state. |
\*-------------------------------------------------*/
std::vector<OutstandingPerKeyWrite> outstanding_writes;
/*-------------------------------------------------*\
| Callbacks |
\*-------------------------------------------------*/
std::function<void()> request_repaint_fn;
std::function<bool()> reapply_active_mode_fn;
std::function<void(RGBController*)> register_controller_fn;
};
@@ -0,0 +1,433 @@
/*---------------------------------------------------------*\
| LogitechHIDPP20Controller_Linux.cpp |
| |
| Linux-specific path-migration and device-name lookup |
| for the unified Logitech HID++ 2.0 controller. |
| |
| Uses sysfs (/sys/class/hidraw) to find the same |
| physical device on a new hidraw path after a USB <-> |
| wireless transition, and to read Centurion sub-device |
| friendly names from HID_NAME uevent fields. |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include <cstring>
#include <cctype>
#include <cstdio>
#include <chrono>
#include <vector>
#include <string>
#include <fstream>
#include <dirent.h>
#include "LogitechHIDPP20Controller.h"
#include "LogManager.h"
#define LOG_TAG log_tag.c_str()
std::string LogitechHIDPP20Controller::GetCenturionSubDeviceName(const std::string& path)
{
/*--------------------------------------------------------*\
| Centurion sub-device friendly name comes from sysfs |
| HID_NAME=, same field Solaar reads. The `path` arg is a |
| hidraw dev path like /dev/hidraw5; extract the hidrawN |
| basename and read /sys/class/hidraw/<basename>/device/ |
| uevent. |
\*--------------------------------------------------------*/
std::string sysfs_name;
size_t pos = path.rfind("hidraw");
if(pos == std::string::npos)
{
return sysfs_name;
}
std::string uevent_path = "/sys/class/hidraw/" + path.substr(pos) + "/device/uevent";
FILE* f = fopen(uevent_path.c_str(), "r");
if(!f)
{
return sysfs_name;
}
char line[256];
while(fgets(line, sizeof(line), f))
{
if(strncmp(line, "HID_NAME=", 9) == 0)
{
sysfs_name = line + 9;
while(!sysfs_name.empty() && (sysfs_name.back() == '\n' || sysfs_name.back() == '\r'))
{
sysfs_name.pop_back();
}
break;
}
}
fclose(f);
return sysfs_name;
}
bool LogitechHIDPP20Controller::ScanForDevice(bool force)
{
/*---------------------------------------------------------*\
| Scan sysfs for a hidraw with matching unitId. Called by |
| the power thread either periodically (normal reactive |
| mode — only when device_online==false, USB/wireless |
| transition or physical unplug) or on demand from the |
| reader thread after a HID++1.0 Device Connection |
| notification arrives (force=true, bypasses the online |
| gate). Finds the device on its new connection path. |
| |
| Reactive-only: we never migrate while the current path |
| still works. On devices like the G502 X PLUS that expose |
| both a USB-direct hidraw AND a wireless-via-dongle hidraw |
| simultaneously (when paired to the receiver and plugged |
| in at the same time), eager migration would bounce us off |
| a working path onto one the firmware has actively muted, |
| breaking control. The device signals which path is |
| active by returning errors/going silent on the inactive |
| path — our reader thread picks that up as a hid_read |
| failure and flips device_online=false, at which point the |
| power thread calls this function to pick the new path. |
| |
| Linux implementation: walks /sys/class/hidraw and matches |
| on HID_UNIQ. The Windows counterpart (same class method, |
| different .cpp file) uses hid_enumerate + serial_number. |
| |
| Match criteria: |
| - device_online == false (caller should already ensure) |
| - HID_UNIQ matches our unitId |
| - Logitech VID (046D) |
| - HID++ interface (usage_page 0xFF00, usage 2) |
| - Different from our current path (device moved) |
\*---------------------------------------------------------*/
if(caps.unit_id.empty() || caps.unit_id == "00000000")
{
return false;
}
/*---------------------------------------------------------*\
| Online guard: never migrate off a working path unless |
| the caller explicitly forces a re-check. The normal |
| periodic scan path stays gated on device_online==false |
| so it's a no-op when everything is healthy. |
| |
| The reader thread bypasses this guard (force=true) after |
| seeing a HID++1.0 Device Connection Status notification |
| from the Lightspeed receiver, which fires BEFORE the |
| firmware fully switches its data flow from wireless to |
| USB. At that moment device_online is still true (the |
| current path hasn't failed yet), but we want the scan to |
| run so we can migrate to the new path proactively. |
\*---------------------------------------------------------*/
if(!force && device_online.load())
{
return false;
}
/*---------------------------------------------------------*\
| Normalize our unitId to lowercase hex without dashes for |
| comparison. Sysfs HID_UNIQ varies between drivers: |
| - Lightspeed virtual: "0d-12-5d-47" (dashes) |
| - USB direct: "0D125D47" (no dashes) |
| We strip dashes and lowercase both sides for matching. |
\*---------------------------------------------------------*/
std::string target_norm;
for(char c : caps.unit_id)
{
if(c != '-')
{
target_norm += (char)tolower(c);
}
}
/*---------------------------------------------------------*\
| Read our current PID from sysfs. Only migrate to paths |
| with a DIFFERENT PID — same PID means same receiver, |
| just a different pairing slot (e.g., stale pairing). |
\*---------------------------------------------------------*/
unsigned int current_pid = 0;
{
std::string cur_hidraw = location.substr(location.rfind('/') + 1);
std::string cur_uevent = "/sys/class/hidraw/" + cur_hidraw + "/device/uevent";
std::ifstream cur_file(cur_uevent);
std::string line;
while(std::getline(cur_file, line))
{
if(line.compare(0, 7, "HID_ID=") == 0)
{
size_t lc = line.rfind(':');
if(lc != std::string::npos)
{
sscanf(line.c_str() + lc + 1, "%x", &current_pid);
}
break;
}
}
}
/*----------------------------------------------------------*\
| Collect ALL sysfs hidraws with matching unit_id + Logitech |
| VID + different PID. With multiple Lightspeed dongles in |
| the system, several hidraws can share the same HID_UNIQ: |
| one is the live virt-slot on the connected dongle, others |
| are stale virt-slots on dongles where our device is not |
| actually present. We have to probe each one to find out |
| which slot is real — sysfs alone can't tell them apart. |
\*----------------------------------------------------------*/
struct Candidate
{
std::string dev_path;
unsigned int pid;
};
std::vector<Candidate> candidates;
DIR* dir = opendir("/sys/class/hidraw");
if(!dir)
{
return false;
}
struct dirent* entry;
while((entry = readdir(dir)) != nullptr)
{
if(strncmp(entry->d_name, "hidraw", 6) != 0)
{
continue;
}
std::string uevent_path = "/sys/class/hidraw/" +
std::string(entry->d_name) + "/device/uevent";
std::ifstream uevent(uevent_path);
if(!uevent.is_open())
{
continue;
}
std::string line;
std::string hid_uniq;
std::string hid_id;
while(std::getline(uevent, line))
{
if(line.compare(0, 9, "HID_UNIQ=") == 0)
{
hid_uniq = line.substr(9);
}
else if(line.compare(0, 7, "HID_ID=") == 0)
{
hid_id = line.substr(7);
}
}
std::string uniq_norm;
for(char c : hid_uniq)
{
if(c != '-')
{
uniq_norm += (char)tolower(c);
}
}
if(uniq_norm != target_norm)
{
continue;
}
if(hid_id.find("0000046D") == std::string::npos &&
hid_id.find("0000046d") == std::string::npos)
{
continue;
}
std::string dev_path = "/dev/" + std::string(entry->d_name);
if(dev_path == location)
{
continue;
}
size_t last_colon = hid_id.rfind(':');
if(last_colon == std::string::npos)
{
continue;
}
unsigned int pid = 0;
sscanf(hid_id.c_str() + last_colon + 1, "%x", &pid);
if(pid == current_pid)
{
continue;
}
candidates.push_back({dev_path, pid});
}
closedir(dir);
if(candidates.empty())
{
return false;
}
/*----------------------------------------------------------*\
| Probe each candidate before committing. Two-dongle systems |
| can expose multiple sysfs hidraws with the same HID_UNIQ: |
| one is the live slot, others are stale pairings that |
| respond with short-format UNKNOWN_DEVICE errors. Issue a |
| cheap IRoot GetFeature (feat 0x0001) to distinguish them. |
| A live slot returns a long-form response with feat_idx=0 |
| and feat_byte matching the sub-index we asked for. A stale |
| slot returns r[10 xx 8F 00 00 08 ...] (short error, code |
| 0x08 UNKNOWN_DEVICE). We accept the first candidate that |
| passes; the overall pending_path_check retry loop will |
| naturally re-probe all candidates on subsequent scans if |
| none pass on the current pass (e.g., mid-transition). |
\*----------------------------------------------------------*/
std::string found_path;
hid_device* found_dev = nullptr;
for(size_t c = 0; c < candidates.size(); c++)
{
const Candidate& cand = candidates[c];
LOG_DEBUG("%s Scan: migration candidate at %s (pid=0x%04X, current=%s pid=0x%04X)",
LOG_TAG, cand.dev_path.c_str(), cand.pid,
location.c_str(), current_pid);
/*-----------------------------------------------------*\
| Multi-dongle caveat: hid_enumerate returns entries |
| for ALL dongles with this PID (046D:4099 Lightspeed |
| can appear several times in a single machine). Match |
| strictly on the sysfs candidate's dev_path so each |
| candidate resolves to its OWN dongle's HID++ |
| interface — not the first match we stumble across. |
\*-----------------------------------------------------*/
hid_device_info* devs = hid_enumerate(0x046D, (uint16_t)cand.pid);
std::string hidpp20_path;
for(hid_device_info* d = devs; d != nullptr; d = d->next)
{
LOG_TRACE("%s Scan: enumerate PID=0x%04X path=%s page=0x%04X usage=%d",
LOG_TAG, cand.pid, d->path, d->usage_page, d->usage);
if(std::string(d->path) == cand.dev_path &&
d->usage_page == 0xFF00 && d->usage == 2 &&
std::string(d->path) != location)
{
hidpp20_path = d->path;
break;
}
}
hid_free_enumeration(devs);
if(hidpp20_path.empty())
{
LOG_DEBUG("%s Scan: %s no matching LogitechHID++ interface in enum",
LOG_TAG, cand.dev_path.c_str());
continue;
}
hid_device* test_dev = hid_open_path(hidpp20_path.c_str());
if(!test_dev)
{
LOG_DEBUG("%s Scan: %s failed to open", LOG_TAG, hidpp20_path.c_str());
continue;
}
/*------------------------------------------------------*\
| Probe: HID++2.0 IRoot GetFeature for feat 0x0001 |
| (IFeatureSet). Wire: w[10 FF 0000 000100] |
| - report_id 0x10 (short) |
| - device_index 0xFF |
| - feat_idx 0x00 (IRoot) |
| - address 0x00 (func 0 GetFeature, sw_id 0) |
| - payload 00 01 00 (feat_id 0x0001) |
| |
| A live device returns r[11 xx 00 0X ...] — long form, |
| feat_idx 0x00, the address byte we sent back, and the |
| feature index in the payload. |
| A stale slot returns r[10 xx 8F 00 00 08 ...] — short |
| error form. Reject anything that starts with 0x10. |
\*------------------------------------------------------*/
uint8_t probe[7] = {0x10, 0xFF, 0x00, 0x00, 0x00, 0x01, 0x00};
uint8_t reply[20] = {};
int write_rc = hid_write(test_dev, probe, sizeof(probe));
bool probe_ok = false;
if(write_rc >= 0)
{
/*-----------------------------------------------------*\
| Drain up to ~100ms, looking for a long-form response |
| matching our probe. Skip stray reports from unrelated |
| firmware events that might be queued on the hidraw. |
\*-----------------------------------------------------*/
std::chrono::steady_clock::time_point deadline = std::chrono::steady_clock::now() +
std::chrono::milliseconds(100);
while(std::chrono::steady_clock::now() < deadline)
{
int read_rc = hid_read_timeout(test_dev, reply, sizeof(reply), 50);
if(read_rc <= 0)
{
continue;
}
if(reply[0] == 0x11 && reply[2] == 0x00 && reply[3] == 0x00)
{
probe_ok = true;
break;
}
if(reply[0] == 0x10 && reply[2] == 0x8F)
{
LOG_DEBUG("%s Scan: %s probe rejected — err=0x%02X (stale slot)",
LOG_TAG, hidpp20_path.c_str(), reply[5]);
break;
}
}
}
if(!probe_ok)
{
hid_close(test_dev);
continue;
}
LOG_DEBUG("%s Scan: %s probe accepted (feat_idx=0x%02X)",
LOG_TAG, hidpp20_path.c_str(), reply[4]);
found_path = hidpp20_path;
found_dev = test_dev;
break;
}
if(!found_dev)
{
return false;
}
LOG_INFO("%s Device migrated: %s -> %s", LOG_TAG, location.c_str(), found_path.c_str());
SwapHIDHandle(found_dev, found_path);
return true;
}
@@ -0,0 +1,302 @@
/*---------------------------------------------------------*\
| LogitechHIDPP20Controller_Windows_MacOS.cpp |
| |
| Path-migration and device-name lookup for the unified |
| Logitech HID++ 2.0 controller on Windows and macOS. |
| |
| Uses hidapi's hid_enumerate + hid_device_info fields |
| (serial_number, product_string, product_id, usage_page) |
| on platforms with no /sys/class/hidraw equivalent. |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include <algorithm>
#include <chrono>
#include <cstring>
#include <string>
#include <vector>
#include "LogitechHIDPP20Controller.h"
#include "LogManager.h"
#include "StringUtils.h"
#define LOG_TAG log_tag.c_str()
std::string LogitechHIDPP20Controller::GetCenturionSubDeviceName(const std::string& path)
{
/*---------------------------------------------------------*\
| On Windows, hidapi's hid_device_info carries a |
| product_string field (wchar_t*). Enumerate all Logitech |
| devices and find the one whose path matches `path`. |
| |
| Caveat: Windows hidapi typically returns the parent |
| product string on every (interface, usage_page, usage) |
| entry that maps to the same USB device, so Centurion |
| sub-devices may share a name with the parent dongle. |
| That's a less specific name than Linux's HID_NAME, but |
| still better than "Logitech Centurion Device". |
\*---------------------------------------------------------*/
std::string friendly;
hid_device_info* devs = hid_enumerate(0x046D, 0x0000);
for(hid_device_info* d = devs; d != nullptr; d = d->next)
{
if(d->path == nullptr)
{
continue;
}
if(std::string(d->path) != path)
{
continue;
}
friendly = StringUtils::wchar_to_string(d->product_string);
break;
}
hid_free_enumeration(devs);
return friendly;
}
bool LogitechHIDPP20Controller::ScanForDevice(bool force)
{
/*---------------------------------------------------------*\
| Scan hidapi for a Logitech device with matching unitId. |
| Called by the power thread either periodically (normal |
| reactive mode — only when device_online==false, USB/ |
| wireless transition or physical unplug) or on demand |
| from the reader thread after a HID++1.0 Device |
| Connection notification arrives (force=true, bypasses |
| the online gate). Finds the device on its new |
| connection path. |
| |
| Reactive-only: we never migrate while the current path |
| still works (same rationale as Linux — see the Linux |
| companion file for the full explanation). |
| |
| Windows implementation: walks hid_enumerate(046D, *) and |
| matches on hid_device_info::serial_number, which for |
| Logitech devices generally corresponds to the same |
| stable identity as the HID++-reported unit_id. If |
| serial_number matching yields nothing (hidapi may not |
| populate it for some Logitech devices on Windows), we |
| fall through to IRoot probing every candidate on the |
| matching usage_page/usage so we can still find the |
| device albeit more slowly. |
\*---------------------------------------------------------*/
if(caps.unit_id.empty() || caps.unit_id == "00000000")
{
return false;
}
if(!force && device_online.load())
{
return false;
}
std::string target_norm = StringUtils::normalize_hex_id(caps.unit_id);
/*---------------------------------------------------------*\
| Candidate = {path, pid, has_serial_match}. Serial matches |
| sort first so we try the cheapest/most-likely candidate. |
\*---------------------------------------------------------*/
struct Candidate
{
std::string dev_path;
unsigned int pid;
bool serial_match;
};
std::vector<Candidate> candidates;
unsigned int current_pid = 0;
hid_device_info* devs = hid_enumerate(0x046D, 0x0000);
/*---------------------------------------------------------*\
| First pass: find the entry matching our current path and |
| record its product_id so we can skip same-PID candidates. |
\*---------------------------------------------------------*/
for(hid_device_info* d = devs; d != nullptr; d = d->next)
{
if(d->path != nullptr && std::string(d->path) == location)
{
current_pid = d->product_id;
break;
}
}
/*---------------------------------------------------------*\
| Second pass: collect candidates that (a) aren't our |
| current path, (b) speak the HID++ interface, and (c) have |
| a plausible identity match — either the serial_number |
| matches our unit_id, or at minimum their PID differs from |
| ours so they can't be another slot on the same dongle. |
\*---------------------------------------------------------*/
for(hid_device_info* d = devs; d != nullptr; d = d->next)
{
if(d->path == nullptr)
{
continue;
}
if(std::string(d->path) == location)
{
continue;
}
/*-----------------------------------------------------*\
| Only HID++ interface (usage_page 0xFF00, usage 2). |
| Note: on Windows hidapi may or may not populate |
| usage / usage_page consistently. If either is zero, |
| fall through — we'll probe unconditionally in that |
| case rather than dropping a potential candidate. |
\*-----------------------------------------------------*/
bool usage_known = (d->usage_page != 0 || d->usage != 0);
if(usage_known && (d->usage_page != 0xFF00 || d->usage != 2))
{
continue;
}
/*-----------------------------------------------------*\
| Compare serial_number (wchar_t*) against our unit_id. |
| Normalize both and do an exact-match comparison. |
| Empty/missing serial is allowed — falls through as a |
| serial_match=false candidate so the probe path can |
| still find it via a different-PID filter. |
\*-----------------------------------------------------*/
bool serial_match = false;
if(d->serial_number != nullptr && d->serial_number[0] != L'\0')
{
std::string sn = StringUtils::wchar_to_string(d->serial_number);
std::string sn_norm = StringUtils::normalize_hex_id(sn);
if(!sn_norm.empty() && sn_norm == target_norm)
{
serial_match = true;
}
}
/*-----------------------------------------------------*\
| If we have no serial match AND this path shares the |
| current PID, skip. Same PID with no identity evidence |
| is probably a sibling slot on the same dongle, not a |
| valid migration target. |
\*-----------------------------------------------------*/
if(!serial_match && d->product_id == current_pid && current_pid != 0)
{
continue;
}
Candidate c;
c.dev_path = d->path;
c.pid = d->product_id;
c.serial_match = serial_match;
candidates.push_back(c);
}
hid_free_enumeration(devs);
if(candidates.empty())
{
return false;
}
/*---------------------------------------------------------*\
| Sort so serial-matched candidates get probed first. |
\*---------------------------------------------------------*/
std::stable_sort(candidates.begin(), candidates.end(),
[](const Candidate& a, const Candidate& b)
{
return a.serial_match && !b.serial_match;
});
/*---------------------------------------------------------*\
| Probe each candidate with IRoot GetFeature feat 0x0001. |
| A live slot returns a long-form (0x11) response; a stale |
| slot returns a short-form (0x10) error. See the Linux |
| companion file for the wire-level explanation. |
\*---------------------------------------------------------*/
std::string found_path;
hid_device* found_dev = nullptr;
for(size_t c = 0; c < candidates.size(); c++)
{
const Candidate& cand = candidates[c];
LOG_DEBUG("%s Scan: migration candidate at %s (pid=0x%04X, serial_match=%d)",
LOG_TAG, cand.dev_path.c_str(), cand.pid,
cand.serial_match ? 1 : 0);
hid_device* test_dev = hid_open_path(cand.dev_path.c_str());
if(!test_dev)
{
LOG_DEBUG("%s Scan: %s failed to open",
LOG_TAG, cand.dev_path.c_str());
continue;
}
uint8_t probe[7] = {0x10, 0xFF, 0x00, 0x00, 0x00, 0x01, 0x00};
uint8_t reply[20] = {};
int write_rc = hid_write(test_dev, probe, sizeof(probe));
bool probe_ok = false;
if(write_rc >= 0)
{
std::chrono::steady_clock::time_point deadline = std::chrono::steady_clock::now() +
std::chrono::milliseconds(100);
while(std::chrono::steady_clock::now() < deadline)
{
int read_rc = hid_read_timeout(test_dev, reply, sizeof(reply), 50);
if(read_rc <= 0)
{
continue;
}
if(reply[0] == 0x11 && reply[2] == 0x00 && reply[3] == 0x00)
{
probe_ok = true;
break;
}
if(reply[0] == 0x10 && reply[2] == 0x8F)
{
LOG_DEBUG("%s Scan: %s probe rejected — err=0x%02X (stale slot)",
LOG_TAG, cand.dev_path.c_str(), reply[5]);
break;
}
}
}
if(!probe_ok)
{
hid_close(test_dev);
continue;
}
LOG_DEBUG("%s Scan: %s probe accepted (feat_idx=0x%02X)",
LOG_TAG, cand.dev_path.c_str(), reply[4]);
found_path = cand.dev_path;
found_dev = test_dev;
break;
}
if(!found_dev)
{
return false;
}
LOG_INFO("%s Device migrated: %s -> %s",
LOG_TAG, location.c_str(), found_path.c_str());
SwapHIDHandle(found_dev, found_path);
return true;
}
@@ -0,0 +1,146 @@
/*---------------------------------------------------------*\
| LogitechHIDPP20IdleSettings.cpp |
| |
| Host-side idle/dim/sleep settings storage helper for |
| Logitech HID++ 2.0 devices. Loads the configuration |
| block from SettingsManager JSON, caches it, and writes |
| changes back through SettingsManager::SetSettings(). |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include "LogitechHIDPP20IdleSettings.h"
#include "ResourceManager.h"
#include "SettingsManager.h"
static const char* SETTINGS_KEY = "LogitechHIDPP20IdleSettings";
LogitechHIDPP20IdleSettings* LogitechHIDPP20IdleSettings::instance()
{
static LogitechHIDPP20IdleSettings inst;
return &inst;
}
/*---------------------------------------------------------*\
| Minimum idle / sleep values the controller will honor. |
| Matches what Logitech firmware typically clamps to on |
| HID++ 2.0 devices, and guarantees the skip-dim path's |
| sleep_delay math always produces a positive window. |
\*---------------------------------------------------------*/
static const int MIN_IDLE_S = 15;
static const int MIN_SLEEP_S = 45;
static const int MIN_DIM_WINDOW = 30; // sleep must exceed idle by at least this
static void ClampProfile(LogitechHIDPP20IdleProfile& p)
{
if(p.dim_brightness < 0) p.dim_brightness = 0;
if(p.dim_brightness > 100) p.dim_brightness = 100;
if(p.idle_timeout_s < MIN_IDLE_S)
{
p.idle_timeout_s = MIN_IDLE_S;
}
if(p.sleep_timeout_s < MIN_SLEEP_S)
{
p.sleep_timeout_s = MIN_SLEEP_S;
}
if(p.sleep_timeout_s < p.idle_timeout_s + MIN_DIM_WINDOW)
{
p.sleep_timeout_s = p.idle_timeout_s + MIN_DIM_WINDOW;
}
}
static LogitechHIDPP20IdleProfile ProfileFromJson(const json& j)
{
LogitechHIDPP20IdleProfile p;
if(j.contains("dim_when_idle")) p.dim_when_idle = j["dim_when_idle"];
if(j.contains("dim_brightness")) p.dim_brightness = j["dim_brightness"];
if(j.contains("idle_timeout_s")) p.idle_timeout_s = j["idle_timeout_s"];
if(j.contains("allow_sleep")) p.allow_sleep = j["allow_sleep"];
if(j.contains("sleep_timeout_s")) p.sleep_timeout_s = j["sleep_timeout_s"];
ClampProfile(p);
return p;
}
static json ProfileToJson(const LogitechHIDPP20IdleProfile& p)
{
json j;
j["dim_when_idle"] = p.dim_when_idle;
j["dim_brightness"] = p.dim_brightness;
j["idle_timeout_s"] = p.idle_timeout_s;
j["allow_sleep"] = p.allow_sleep;
j["sleep_timeout_s"] = p.sleep_timeout_s;
return j;
}
void LogitechHIDPP20IdleSettings::load()
{
json settings = ResourceManager::get()->GetSettingsManager()->GetSettings(SETTINGS_KEY);
/*---------------------------------------------------------*\
| Empty / missing key means the plugin is not in use. |
| Reset both profiles to defaults with configured=false so |
| the controller defers to firmware. |
\*---------------------------------------------------------*/
if(!settings.is_object() || settings.empty())
{
configured = false;
on_battery = LogitechHIDPP20IdleProfile{};
plugged_in = LogitechHIDPP20IdleProfile{};
return;
}
configured = true;
if(settings.contains("on_battery"))
{
on_battery = ProfileFromJson(settings["on_battery"]);
}
else
{
on_battery = LogitechHIDPP20IdleProfile{};
}
if(settings.contains("plugged_in"))
{
plugged_in = ProfileFromJson(settings["plugged_in"]);
}
else
{
plugged_in = LogitechHIDPP20IdleProfile{};
}
}
void LogitechHIDPP20IdleSettings::save()
{
json settings;
settings["on_battery"] = ProfileToJson(on_battery);
settings["plugged_in"] = ProfileToJson(plugged_in);
SettingsManager* mgr = ResourceManager::get()->GetSettingsManager();
mgr->SetSettings(SETTINGS_KEY, settings);
mgr->SaveSettings();
configured = true;
}
void LogitechHIDPP20IdleSettings::setOnBattery(const LogitechHIDPP20IdleProfile& p)
{
on_battery = p;
configured = true;
}
void LogitechHIDPP20IdleSettings::setPluggedIn(const LogitechHIDPP20IdleProfile& p)
{
plugged_in = p;
configured = true;
}
@@ -0,0 +1,47 @@
/*---------------------------------------------------------*\
| LogitechHIDPP20IdleSettings.h |
| |
| Host-side idle/dim/sleep configuration for Logitech |
| HID++ 2.0 devices. Two profiles (on_battery, plugged_in)|
| selected at runtime based on the device's external- |
| power flag. `configured == false` means the JSON key |
| is absent entirely — the controller defers to firmware. |
| Qt-free so the controller can consume it directly. |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#pragma once
struct LogitechHIDPP20IdleProfile
{
bool dim_when_idle = false;
int dim_brightness = 50;
int idle_timeout_s = 60;
bool allow_sleep = false;
int sleep_timeout_s = 300;
};
class LogitechHIDPP20IdleSettings
{
public:
static LogitechHIDPP20IdleSettings* instance();
void load();
void save();
bool isConfigured() const { return configured; }
const LogitechHIDPP20IdleProfile& onBattery() const { return on_battery; }
const LogitechHIDPP20IdleProfile& pluggedIn() const { return plugged_in; }
void setOnBattery(const LogitechHIDPP20IdleProfile& p);
void setPluggedIn(const LogitechHIDPP20IdleProfile& p);
private:
LogitechHIDPP20IdleSettings() = default;
bool configured = false;
LogitechHIDPP20IdleProfile on_battery;
LogitechHIDPP20IdleProfile plugged_in;
};
@@ -0,0 +1,72 @@
/*---------------------------------------------------------*\
| RGBController_LogitechHIDPP20.h |
| |
| RGBController for unified Logitech HID++ 2.0 devices |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#pragma once
#include "RGBController.h"
#include "LogitechHIDPP20Controller.h"
class RGBController_LogitechHIDPP20 : public RGBController
{
public:
RGBController_LogitechHIDPP20(LogitechHIDPP20Controller* controller_ptr);
~RGBController_LogitechHIDPP20();
void SetupZones();
void ResizeZone(int zone, int new_size);
void DeviceUpdateLEDs();
void UpdateZoneLEDs(int zone);
void UpdateSingleLED(int led);
void DeviceUpdateMode();
void DeviceSaveMode();
bool ReapplyActiveMode();
private:
LogitechHIDPP20Controller* controller;
/*---------------------------------------------------------*\
| Repaint callback handler. Registered with the controller |
| as request_repaint_fn and invoked from the power thread |
| for dim/wake when no animation is driving updates. |
\*---------------------------------------------------------*/
void OnRepaintRequest();
/*---------------------------------------------------------*\
| When true, the next DeviceUpdateMode cycle sends its |
| SetZoneEffect calls with persist=true instead of the |
| default ephemeral write. Used by DeviceSaveMode to replay |
| the active mode as a NVM-committed effect on 0x8070 |
| devices, which default to non-persistent live writes. |
\*---------------------------------------------------------*/
bool save_pending = false;
/*---------------------------------------------------------*\
| Maps OpenRGB LED index -> HID++ per-key zone ID |
\*---------------------------------------------------------*/
std::vector<uint16_t> led_to_zone_id;
/*---------------------------------------------------------*\
| Reverse map: zone_id -> LED index (-1 if no LED). |
| Indexed 0..255 (zone IDs are bytes). Built once in |
| SetupZones to avoid scanning led_to_zone_id at commit |
| time, which would be O(N) per acked zone. |
\*---------------------------------------------------------*/
std::vector<int> zone_id_to_led_idx;
/*---------------------------------------------------------*\
| Last successfully committed colors for delta updates. |
| An entry of HIDPP20_UNCOMMITTED (0xFF000000) marks an LED |
| whose last write didn't ACK and which therefore needs to |
| be re-pushed in the next frame regardless of color delta. |
| The high byte (0xFF) is impossible for any value produced |
| by ToRGBColor() so it never collides with a real color. |
\*---------------------------------------------------------*/
std::vector<RGBColor> sent_colors;
uint32_t last_init_gen = 0;
};
@@ -0,0 +1,95 @@
/*---------------------------------------------------------*\
| MSILaptopController.cpp |
| |
| Driver for MSI laptop SteelSeries KLC/ALC RGB devices |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include <cstring>
#include "MSILaptopController.h"
#include "StringUtils.h"
#define MSI_LAPTOP_REPORT_ID 0x00
#define MSI_LAPTOP_COMMAND 0x0C
#define MSI_LAPTOP_KLC_PACKET_ID 0x66
#define MSI_LAPTOP_ALC_PACKET_ID 0x06
#define MSI_LAPTOP_PACKET_SIZE 525
#define MSI_LAPTOP_PAYLOAD_OFFSET 5
MSILaptopController::MSILaptopController(hid_device* dev_handle, const char* path, std::string dev_name, msi_laptop_device device_type)
{
dev = dev_handle;
location = path;
name = dev_name;
type = device_type;
}
MSILaptopController::~MSILaptopController()
{
hid_close(dev);
}
std::string MSILaptopController::GetDeviceLocation()
{
return("HID: " + location);
}
std::string MSILaptopController::GetDeviceName()
{
return(name);
}
std::string MSILaptopController::GetSerialString()
{
wchar_t serial_string[128];
int ret = hid_get_serial_number_string(dev, serial_string, 128);
if(ret != 0)
{
return("");
}
return(StringUtils::wstring_to_string(serial_string));
}
msi_laptop_device MSILaptopController::GetDeviceType()
{
return(type);
}
void MSILaptopController::SetLEDs(std::vector<led> leds, std::vector<RGBColor> colors)
{
unsigned char buf[MSI_LAPTOP_PACKET_SIZE];
unsigned int led_count = (leds.size() < colors.size()) ? leds.size() : colors.size();
memset(buf, 0x00, sizeof(buf));
buf[0x00] = MSI_LAPTOP_REPORT_ID;
buf[0x01] = MSI_LAPTOP_COMMAND;
buf[0x03] = (type == MSI_LAPTOP_KLC) ? MSI_LAPTOP_KLC_PACKET_ID : MSI_LAPTOP_ALC_PACKET_ID;
// Fill unused LED IDs with 0xFF so they are ignored by the controller
for(int i = 0; i < (MSI_LAPTOP_PACKET_SIZE - MSI_LAPTOP_PAYLOAD_OFFSET) / 4; i++)
{
buf[MSI_LAPTOP_PAYLOAD_OFFSET + (i * 4)] = 0xFF;
}
for(unsigned int led_idx = 0; led_idx < led_count; led_idx++)
{
unsigned int offset = MSI_LAPTOP_PAYLOAD_OFFSET + (led_idx * 4);
if((offset + 3) >= sizeof(buf))
{
break;
}
buf[offset + 0] = leds[led_idx].value;
buf[offset + 1] = RGBGetRValue(colors[led_idx]);
buf[offset + 2] = RGBGetGValue(colors[led_idx]);
buf[offset + 3] = RGBGetBValue(colors[led_idx]);
}
hid_send_feature_report(dev, buf, sizeof(buf));
}
@@ -0,0 +1,67 @@
/*---------------------------------------------------------*\
| MSILaptopController.h |
| |
| Driver for MSI laptop SteelSeries KLC/ALC RGB devices |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#pragma once
#include <string>
#include <vector>
#include <hidapi.h>
#include "RGBController.h"
typedef enum
{
MSI_LAPTOP_KLC,
MSI_LAPTOP_ALC,
} msi_laptop_device;
typedef struct
{
const char* name;
unsigned char id;
} msi_laptop_led;
struct MSILaptopModel
{
const char* sys_vendor;
const char* product_name;
/* Keyboard layout */
const msi_laptop_led* klc_leds;
unsigned int klc_leds_count;
unsigned int klc_matrix_height;
unsigned int klc_matrix_width;
const unsigned int* klc_matrix_map;
/* Lightbar layout */
const msi_laptop_led* alc_leds;
unsigned int alc_leds_count;
unsigned int alc_lightbar_leds;
};
class MSILaptopController
{
public:
MSILaptopController(hid_device* dev_handle, const char* path, std::string dev_name, msi_laptop_device device_type);
~MSILaptopController();
std::string GetDeviceLocation();
std::string GetDeviceName();
std::string GetSerialString();
msi_laptop_device GetDeviceType();
void SetLEDs(std::vector<led> leds, std::vector<RGBColor> colors);
private:
hid_device* dev;
std::string location;
std::string name;
msi_laptop_device type;
};
@@ -0,0 +1,235 @@
/*---------------------------------------------------------*\
| MSILaptopControllerDetect.cpp |
| |
| Detector for MSI laptop SteelSeries RGB devices |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include "Detector.h"
#include "RGBControllerKeyNames.h"
#include "RGBController_MSILaptop.h"
#include "MSILaptopController.h"
#include "dmiinfo.h"
#define MSI_LAPTOP_ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
#define STEELSERIES_VID 0x1038
#define STEELSERIES_MSI_RAIDER_A18_KLC_PID 0x1122
#define STEELSERIES_MSI_RAIDER_A18_ALC_PID 0x1161
#define NA 0xFFFFFFFF
#define MSI_LAPTOP_KLC_MATRIX_HEIGHT 6
#define MSI_LAPTOP_KLC_MATRIX_WIDTH 23
#define MSI_LAPTOP_ALC_LIGHTBAR_LEDS 3
static const msi_laptop_led msi_raider_a18_klc_leds[] =
{
{ KEY_EN_A, 0x04 },
{ KEY_EN_B, 0x05 },
{ KEY_EN_C, 0x06 },
{ KEY_EN_D, 0x07 },
{ KEY_EN_E, 0x08 },
{ KEY_EN_F, 0x09 },
{ KEY_EN_G, 0x0A },
{ KEY_EN_H, 0x0B },
{ KEY_EN_I, 0x0C },
{ KEY_EN_J, 0x0D },
{ KEY_EN_K, 0x0E },
{ KEY_EN_L, 0x0F },
{ KEY_EN_M, 0x10 },
{ KEY_EN_N, 0x11 },
{ KEY_EN_O, 0x12 },
{ KEY_EN_P, 0x13 },
{ KEY_EN_Q, 0x14 },
{ KEY_EN_R, 0x15 },
{ KEY_EN_S, 0x16 },
{ KEY_EN_T, 0x17 },
{ KEY_EN_U, 0x18 },
{ KEY_EN_V, 0x19 },
{ KEY_EN_W, 0x1A },
{ KEY_EN_X, 0x1B },
{ KEY_EN_Y, 0x1C },
{ KEY_EN_Z, 0x1D },
{ KEY_EN_1, 0x1E },
{ KEY_EN_2, 0x1F },
{ KEY_EN_3, 0x20 },
{ KEY_EN_4, 0x21 },
{ KEY_EN_5, 0x22 },
{ KEY_EN_6, 0x23 },
{ KEY_EN_7, 0x24 },
{ KEY_EN_8, 0x25 },
{ KEY_EN_9, 0x26 },
{ KEY_EN_0, 0x27 },
{ KEY_EN_ESCAPE, 0x29 },
{ KEY_EN_TAB, 0x2B },
{ KEY_EN_SPACE, 0x2C },
{ KEY_EN_MINUS, 0x2D },
{ KEY_EN_EQUALS, 0x2E },
{ KEY_EN_LEFT_BRACKET, 0x2F },
{ KEY_EN_RIGHT_BRACKET, 0x30 },
{ KEY_EN_SEMICOLON, 0x33 },
{ KEY_EN_QUOTE, 0x34 },
{ KEY_EN_BACK_TICK, 0x35 },
{ KEY_EN_COMMA, 0x36 },
{ KEY_EN_PERIOD, 0x37 },
{ KEY_EN_FORWARD_SLASH, 0x38 },
{ KEY_EN_CAPS_LOCK, 0x39 },
{ KEY_EN_F1, 0x3A },
{ KEY_EN_F2, 0x3B },
{ KEY_EN_F3, 0x3C },
{ KEY_EN_F4, 0x3D },
{ KEY_EN_F5, 0x3E },
{ KEY_EN_F6, 0x3F },
{ KEY_EN_F7, 0x40 },
{ KEY_EN_F8, 0x41 },
{ KEY_EN_F9, 0x42 },
{ KEY_EN_F10, 0x43 },
{ KEY_EN_F11, 0x44 },
{ KEY_EN_F12, 0x45 },
{ KEY_EN_PRINT_SCREEN, 0x46 },
{ KEY_EN_SCROLL_LOCK, 0x47 },
{ KEY_EN_INSERT, 0x49 },
{ "Home/Page Up", 0x4B },
{ KEY_EN_DELETE, 0x4C },
{ KEY_EN_PAGE_DOWN, 0x4E },
{ KEY_EN_RIGHT_ARROW, 0x4F },
{ KEY_EN_LEFT_ARROW, 0x50 },
{ KEY_EN_DOWN_ARROW, 0x51 },
{ KEY_EN_UP_ARROW, 0x52 },
{ KEY_EN_NUMPAD_LOCK, 0x53 },
{ KEY_EN_NUMPAD_DIVIDE, 0x54 },
{ KEY_EN_NUMPAD_TIMES, 0x55 },
{ KEY_EN_NUMPAD_MINUS, 0x56 },
{ KEY_EN_NUMPAD_PLUS, 0x57 },
{ KEY_EN_NUMPAD_ENTER, 0x58 },
{ KEY_EN_NUMPAD_1, 0x59 },
{ KEY_EN_NUMPAD_2, 0x5A },
{ KEY_EN_NUMPAD_3, 0x5B },
{ KEY_EN_NUMPAD_4, 0x5C },
{ KEY_EN_NUMPAD_5, 0x5D },
{ KEY_EN_NUMPAD_6, 0x5E },
{ KEY_EN_NUMPAD_7, 0x5F },
{ KEY_EN_NUMPAD_8, 0x60 },
{ KEY_EN_NUMPAD_9, 0x61 },
{ KEY_EN_NUMPAD_0, 0x62 },
{ KEY_EN_NUMPAD_PERIOD, 0x63 },
{ KEY_EN_POWER, 0x66 },
{ KEY_EN_LEFT_CONTROL, 0xE0 },
{ KEY_EN_LEFT_SHIFT, 0xE1 },
{ KEY_EN_LEFT_ALT, 0xE2 },
{ KEY_EN_LEFT_WINDOWS, 0xE3 },
{ KEY_EN_RIGHT_WINDOWS, 0xE4 },
{ KEY_EN_RIGHT_FUNCTION, 0xF0 },
{ KEY_EN_ANSI_ENTER, 0x28 },
{ KEY_EN_BACKSPACE, 0x2A },
{ KEY_EN_BACK_SLASH, 0x31 },
{ KEY_EN_ISO_BACK_SLASH, 0x64 },
{ KEY_EN_RIGHT_SHIFT, 0xE5 },
{ KEY_EN_RIGHT_ALT, 0xE6 },
};
static unsigned int msi_raider_a18_klc_matrix_map[MSI_LAPTOP_KLC_MATRIX_HEIGHT][MSI_LAPTOP_KLC_MATRIX_WIDTH] =
{
{ 36, NA, 50, 51, 52, 53, NA, 54, 55, 56, 57, NA, 58, 59, 60, 61, 62, 63, NA, NA, NA, NA, 89 },
{ 45, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 39, 40, 97, NA, 64, 65, NA, 72, 73, 74, 75, NA },
{ 37, 16, 22, 4, 17, 19, 24, 20, 8, 14, 15, 41, 42, 98, NA, 66, 67, NA, 84, 85, 86, 76, NA },
{ 49, 0, 18, 3, 5, 6, 7, 9, 10, 11, 43, 44, 96, NA, NA, NA, NA, NA, 81, 82, 83, NA, NA },
{ 91, 25, 23, 2, 21, 1, 13, 12, 46, 47, 48,100, NA, NA, NA, NA, 71, NA, 78, 79, 80, 77, NA },
{ 90, 93, 92, NA, NA, NA, 38, NA, NA, NA,101, 94, 95, NA, 69, 70, 68, NA, 87, NA, 88, NA, NA },
};
/*---------------------------------------------------------*\
| Note on Raider A18 HX ALC (Lightbar) LED mappings: |
| Initial reverse engineering packet captures showed 6 zones|
| being updated (indexes 0x00 to 0x05). |
| However, indexes 0x04 (L4) and 0x05 (L5) are "dummy" zones|
| on this specific model and do not correspond to physical |
| LEDs. To avoid user confusion, they are omitted from the |
| UI profile here. |
| The SetLEDs function handles zero-padding the unused |
| payload slots with 0xFF so that omitting these zones |
| doesn't accidentally overwrite the 0x00 index (L1). |
\*---------------------------------------------------------*/
static const msi_laptop_led msi_raider_a18_alc_leds[] =
{
{ "Lightbar 1", 0x00 },
{ "Lightbar 2", 0x01 },
{ "Lightbar 3", 0x02 },
{ "Logo", 0x03 },
};
static const MSILaptopModel msi_laptop_models[] =
{
{
"Micro-Star International Co., Ltd.",
"Raider A18 HX A9WJG",
/* Keyboard layout */
msi_raider_a18_klc_leds,
MSI_LAPTOP_ARRAY_SIZE(msi_raider_a18_klc_leds),
MSI_LAPTOP_KLC_MATRIX_HEIGHT,
MSI_LAPTOP_KLC_MATRIX_WIDTH,
(const unsigned int*)msi_raider_a18_klc_matrix_map,
/* Lightbar layout */
msi_raider_a18_alc_leds,
MSI_LAPTOP_ARRAY_SIZE(msi_raider_a18_alc_leds),
MSI_LAPTOP_ALC_LIGHTBAR_LEDS,
},
};
static const MSILaptopModel* GetMSILaptopModelDMI()
{
DMIInfo dmi;
for(unsigned int i = 0; i < MSI_LAPTOP_ARRAY_SIZE(msi_laptop_models); i++)
{
if((dmi.getManufacturer() == msi_laptop_models[i].sys_vendor) &&
(dmi.getProductName() == msi_laptop_models[i].product_name))
{
return &msi_laptop_models[i];
}
}
return nullptr;
}
void DetectMSILaptop(hid_device_info* info, const std::string& name)
{
const MSILaptopModel* model = GetMSILaptopModelDMI();
if(!model)
{
return;
}
msi_laptop_device device_type;
if(info->product_id == STEELSERIES_MSI_RAIDER_A18_KLC_PID)
{
device_type = MSI_LAPTOP_KLC;
}
else if(info->product_id == STEELSERIES_MSI_RAIDER_A18_ALC_PID)
{
device_type = MSI_LAPTOP_ALC;
}
else
{
return;
}
hid_device* dev = hid_open_path(info->path);
if(dev)
{
MSILaptopController* controller = new MSILaptopController(dev, info->path, name, device_type);
RGBController_MSILaptop* rgb_controller = new RGBController_MSILaptop(controller, model);
ResourceManager::get()->RegisterRGBController(rgb_controller);
}
}
REGISTER_HID_DETECTOR("MSI Laptop Keyboard", DetectMSILaptop, STEELSERIES_VID, STEELSERIES_MSI_RAIDER_A18_KLC_PID);
REGISTER_HID_DETECTOR_I("MSI Laptop Lightbar", DetectMSILaptop, STEELSERIES_VID, STEELSERIES_MSI_RAIDER_A18_ALC_PID, 0);
@@ -0,0 +1,141 @@
/*---------------------------------------------------------*\
| RGBController_MSILaptop.cpp |
| |
| RGBController for MSI laptop SteelSeries RGB devices |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include "RGBControllerKeyNames.h"
#include "RGBController_MSILaptop.h"
#define NA 0xFFFFFFFF
/**------------------------------------------------------------------*\
@name MSI Laptop SteelSeries RGB
@category Keyboard
@type USB
@save :x:
@direct :white_check_mark:
@effects :x:
@detectors DetectMSILaptop
@comment
\*-------------------------------------------------------------------*/
RGBController_MSILaptop::RGBController_MSILaptop(MSILaptopController* controller_ptr, const MSILaptopModel* model_ptr)
{
controller = controller_ptr;
model = model_ptr;
name = controller->GetDeviceName();
vendor = "SteelSeries";
description = std::string(model->sys_vendor) + " " + std::string(model->product_name) + " RGB Device";
location = controller->GetDeviceLocation();
serial = controller->GetSerialString();
type = (controller->GetDeviceType() == MSI_LAPTOP_KLC) ? DEVICE_TYPE_KEYBOARD : DEVICE_TYPE_LEDSTRIP;
mode Direct;
Direct.name = "Direct";
Direct.flags = MODE_FLAG_HAS_PER_LED_COLOR;
Direct.color_mode = MODE_COLORS_PER_LED;
modes.push_back(Direct);
SetupZones();
}
RGBController_MSILaptop::~RGBController_MSILaptop()
{
for(unsigned int zone_idx = 0; zone_idx < zones.size(); zone_idx++)
{
if(zones[zone_idx].matrix_map != NULL)
{
delete zones[zone_idx].matrix_map;
}
}
delete controller;
}
void RGBController_MSILaptop::SetupZones()
{
if(controller->GetDeviceType() == MSI_LAPTOP_KLC)
{
zone keyboard_zone;
keyboard_zone.name = ZONE_EN_KEYBOARD;
keyboard_zone.type = ZONE_TYPE_MATRIX;
keyboard_zone.leds_min = model->klc_leds_count;
keyboard_zone.leds_max = model->klc_leds_count;
keyboard_zone.leds_count = model->klc_leds_count;
keyboard_zone.matrix_map = new matrix_map_type;
keyboard_zone.matrix_map->height = model->klc_matrix_height;
keyboard_zone.matrix_map->width = model->klc_matrix_width;
keyboard_zone.matrix_map->map = (unsigned int *)model->klc_matrix_map;
zones.push_back(keyboard_zone);
for(unsigned int led_idx = 0; led_idx < model->klc_leds_count; led_idx++)
{
led new_led;
new_led.name = model->klc_leds[led_idx].name;
new_led.value = model->klc_leds[led_idx].id;
leds.push_back(new_led);
}
}
else
{
zone lightbar_zone;
lightbar_zone.name = "Lightbar";
lightbar_zone.type = ZONE_TYPE_LINEAR;
lightbar_zone.start_idx = 0;
lightbar_zone.leds_min = model->alc_lightbar_leds;
lightbar_zone.leds_max = model->alc_lightbar_leds;
lightbar_zone.leds_count = model->alc_lightbar_leds;
lightbar_zone.matrix_map = NULL;
zones.push_back(lightbar_zone);
zone logo_zone;
logo_zone.name = "Logo";
logo_zone.type = ZONE_TYPE_SINGLE;
logo_zone.start_idx = model->alc_lightbar_leds;
logo_zone.leds_min = 1;
logo_zone.leds_max = 1;
logo_zone.leds_count = 1;
logo_zone.matrix_map = NULL;
zones.push_back(logo_zone);
for(unsigned int led_idx = 0; led_idx < model->alc_leds_count; led_idx++)
{
led new_led;
new_led.name = model->alc_leds[led_idx].name;
new_led.value = model->alc_leds[led_idx].id;
leds.push_back(new_led);
}
}
SetupColors();
}
void RGBController_MSILaptop::ResizeZone(int /*zone*/, int /*new_size*/)
{
}
void RGBController_MSILaptop::DeviceUpdateLEDs()
{
controller->SetLEDs(leds, colors);
}
void RGBController_MSILaptop::UpdateZoneLEDs(int /*zone*/)
{
DeviceUpdateLEDs();
}
void RGBController_MSILaptop::UpdateSingleLED(int /*led*/)
{
DeviceUpdateLEDs();
}
void RGBController_MSILaptop::DeviceUpdateMode()
{
DeviceUpdateLEDs();
}
@@ -0,0 +1,34 @@
/*---------------------------------------------------------*\
| RGBController_MSILaptop.h |
| |
| RGBController for MSI laptop SteelSeries RGB devices |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#pragma once
#include "RGBController.h"
#include "MSILaptopController.h"
class RGBController_MSILaptop : public RGBController
{
public:
RGBController_MSILaptop(MSILaptopController* controller_ptr, const MSILaptopModel* model_ptr);
~RGBController_MSILaptop();
void SetupZones();
void ResizeZone(int zone, int new_size);
void DeviceUpdateLEDs();
void UpdateZoneLEDs(int zone);
void UpdateSingleLED(int led);
void DeviceUpdateMode();
private:
MSILaptopController* controller;
const MSILaptopModel* model;
};
@@ -0,0 +1,121 @@
/*---------------------------------------------------------*\
| RGBController_MSIMonitor.cpp |
| |
| RGBController for MSI monitor (gaming controller) |
| |
| Andy Herbert 2026 June 4 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include <string.h>
#include "MSIMonitorController.h"
#include "StringUtils.h"
MSIMonitorController::MSIMonitorController(hid_device* dev_handle, const hid_device_info& info, std::string dev_name)
{
dev = dev_handle;
location = info.path;
name = dev_name;
}
MSIMonitorController::~MSIMonitorController()
{
hid_close(dev);
}
std::string MSIMonitorController::GetDeviceLocation()
{
return("HID: " + location);
}
std::string MSIMonitorController::GetNameString()
{
return(name);
}
std::string MSIMonitorController::GetSerialString()
{
wchar_t serial_string[128];
int ret = hid_get_serial_number_string(dev, serial_string, 128);
if(ret != 0)
{
return("");
}
return(StringUtils::wstring_to_string(serial_string));
}
void MSIMonitorController::Set(uint8_t mode_value, const std::vector<RGBColor> colors, uint8_t last_bit)
{
/*---------------------------------------------------------*\
| Prepare color data |
\*---------------------------------------------------------*/
uint8_t data[MSI_MONITOR_PACKET_SIZE];
memset(data, 0x00, MSI_MONITOR_PACKET_SIZE);
unsigned int offset = 0;
data[offset++] = 0x71;
data[offset++] = 0x01;
for(int i = 0; i < 3; i++)
{
data[offset++] = 0x00;
}
data[offset++] = 0x01;
data[offset++] = 0x64;
for(int i = 0; i < 5; i++)
{
data[offset++] = 0x00;
}
/*---------------------------------------------------------*\
| put mode_value |
\*---------------------------------------------------------*/
data[offset++] = mode_value;
for(int i = 0; i < 3; i++)
{
data[offset++] = 0x00;
}
data[offset++] = 0x01;
data[offset++] = 0x64;
for(int i = 0; i < 5; i++)
{
data[offset++] = 0x00;
}
/*-----------------------------------------------------------------------*\
| this data looks like placeholder for additional LEDs in other monitors |
\*-----------------------------------------------------------------------*/
for(int i = 0; i < 9; i++)
{
data[offset++] = 0xff;
data[offset++] = 0x00;
data[offset++] = 0x00;
}
//RGB values begin
for(const RGBColor color: colors)
{
data[offset++] = RGBGetRValue(color);
data[offset++] = RGBGetGValue(color);
data[offset++] = RGBGetBValue(color);
}
/*-------------------------------------------------------------------------------*\
| last bit is probably a write to device bit- 0x01 saves to device, 0x00 doesn't. |
| For direct mode, bit is set to 0x00, otherwise lights will flicker |
\*-------------------------------------------------------------------------------*/
data[offset++] = last_bit;
/*---------------------------------------------------------*\
| Send the data (1 packet) |
\*---------------------------------------------------------*/
hid_send_feature_report(dev, data, MSI_MONITOR_PACKET_SIZE);
}
@@ -0,0 +1,53 @@
/*---------------------------------------------------------*\
| RGBController_MSIMonitor.cpp |
| |
| RGBController for MSI monitor (gaming controller) |
| |
| Andy Herbert 2026 June 1 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#pragma once
#include <string>
#include <hidapi.h>
#include "RGBController.h"
#define MSI_MONITOR_LEDS 9
#define MSI_MONITOR_PACKET_SIZE 78
enum
{
MSI_MONITOR_OFF_MODE_VALUE = 0x00,
MSI_MONITOR_STATIC_MODE_VALUE = 0x01,
MSI_MONITOR_BREATHING_MODE_VALUE = 0x02,
MSI_MONITOR_FLASHING_MODE_VALUE = 0x03,
MSI_MONITOR_LIGHTNING_MODE_VALUE = 0x05,
MSI_MONITOR_MARQUEE_MODE_VALUE = 0x06,
MSI_MONITOR_METEOR_MODE_VALUE = 0x08,
MSI_MONITOR_RAINBOW_MODE_VALUE = 0x1A,
MSI_MONITOR_RANDOM_MODE_VALUE = 0x1F
};
class MSIMonitorController
{
public:
MSIMonitorController(hid_device *dev_handle, const hid_device_info &info, std::string dev_name);
~MSIMonitorController();
std::string GetDeviceLocation();
std::string GetFirmwareVersion();
std::string GetNameString();
std::string GetSerialString();
void Set(uint8_t mode_value, const std::vector<RGBColor> colors, uint8_t last_bit);
private:
hid_device *dev;
std::string description;
std::string location;
std::string name;
std::string version;
};
@@ -0,0 +1,38 @@
/*---------------------------------------------------------*\
| MSIKeyboardControllerDetect.cpp |
| |
| Detector for MSI monitor (MSI Gaming Controller) |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include "Detector.h"
#include "MSIMonitorController.h"
#include "RGBController_MSIMonitor.h"
#define MSI_USB_VID 0x1462
#define MSI_USB_PID 0x3FA4
/*----------------------------------------------------------*\
| |
| DetectMSIMonitorController |
| |
| Detect MSI monitor and maybe others |
| |
\*----------------------------------------------------------*/
static void DetectMSIMonitorController(hid_device_info* info, const std::string& name)
{
hid_device* dev = hid_open_path(info->path);
if(dev)
{
MSIMonitorController* controller = new MSIMonitorController(dev, *info, name);
RGBController_MSIMonitor* rgb_controller = new RGBController_MSIMonitor(controller);
ResourceManager::get()->RegisterRGBController(rgb_controller);
}
}
REGISTER_HID_DETECTOR_IPU("MSI Monitor (Gaming Controller)", DetectMSIMonitorController, MSI_USB_VID, MSI_USB_PID, 0, 0x01, 0);
@@ -0,0 +1,171 @@
/*---------------------------------------------------------*\
| RGBController_MSIMonitor.cpp |
| |
| RGBController for MSI monitor (gaming controller) |
| |
| Andy Herbert 2026 June 1 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include <chrono>
#include <thread>
#include "RGBController_MSIMonitor.h"
using namespace std::chrono_literals;
/**------------------------------------------------------------------*\
@name MSIMonitor
@category Accessory
@type USB
@save :robot:
@direct :white_check_mark:
@effects :white_check_mark:
@detectors DetectMSIMonitorController
@comment Developed with MSI MAG272CQR
\*-------------------------------------------------------------------*/
RGBController_MSIMonitor::RGBController_MSIMonitor(MSIMonitorController* controller_ptr)
{
controller = controller_ptr;
name = controller->GetNameString();
vendor = "MSI";
type = DEVICE_TYPE_MONITOR;
description = "MSI Monitor (Gaming Controller)";
location = controller->GetDeviceLocation();
serial = controller->GetSerialString();
mode Direct;
Direct.name = "Direct";
Direct.value = MSI_MONITOR_STATIC_MODE_VALUE;
Direct.flags = MODE_FLAG_HAS_PER_LED_COLOR;
Direct.color_mode = MODE_COLORS_PER_LED;
modes.push_back(Direct);
mode Static;
Static.name = "Static";
Static.value = MSI_MONITOR_STATIC_MODE_VALUE;
Static.flags = MODE_FLAG_HAS_PER_LED_COLOR;
Static.color_mode = MODE_COLORS_PER_LED;
modes.push_back(Static);
mode Breathing;
Breathing.name = "Breathing";
Breathing.value = MSI_MONITOR_BREATHING_MODE_VALUE;
Breathing.flags = MODE_FLAG_HAS_PER_LED_COLOR;
Breathing.color_mode = MODE_COLORS_PER_LED;
modes.push_back(Breathing);
mode Flashing;
Flashing.name = "Flashing";
Flashing.value = MSI_MONITOR_FLASHING_MODE_VALUE;
Flashing.flags = MODE_FLAG_HAS_PER_LED_COLOR;
Flashing.color_mode = MODE_COLORS_PER_LED;
modes.push_back(Flashing);
mode Lightning;
Lightning.name = "Lightning";
Lightning.value = MSI_MONITOR_LIGHTNING_MODE_VALUE;
Lightning.flags = MODE_FLAG_HAS_PER_LED_COLOR;
Lightning.color_mode = MODE_COLORS_PER_LED;
modes.push_back(Lightning);
mode Marquee;
Marquee.name = "Marquee";
Marquee.value = MSI_MONITOR_MARQUEE_MODE_VALUE;
Marquee.flags = MODE_FLAG_AUTOMATIC_SAVE;
Marquee.color_mode = MODE_COLORS_NONE;
modes.push_back(Marquee);
mode Meteor;
Meteor.name = "Meteor";
Meteor.value = MSI_MONITOR_METEOR_MODE_VALUE;
Meteor.flags = MODE_FLAG_AUTOMATIC_SAVE;
Meteor.color_mode = MODE_COLORS_NONE;
modes.push_back(Meteor);
mode Rainbow;
Rainbow.name = "Rainbow";
Rainbow.value = MSI_MONITOR_RAINBOW_MODE_VALUE;
Rainbow.flags = MODE_FLAG_AUTOMATIC_SAVE;
Rainbow.color_mode = MODE_COLORS_NONE;
modes.push_back(Rainbow);
mode Random;
Random.name = "Random";
Random.value = MSI_MONITOR_RANDOM_MODE_VALUE;
Random.flags = MODE_FLAG_AUTOMATIC_SAVE;
Random.color_mode = MODE_COLORS_RANDOM;
modes.push_back(Random);
mode Off;
Off.name = "Off";
Off.value = MSI_MONITOR_OFF_MODE_VALUE;
Off.flags = MODE_FLAG_AUTOMATIC_SAVE;
Off.color_mode = MODE_COLORS_NONE;
modes.push_back(Off);
SetupZones();
}
RGBController_MSIMonitor::~RGBController_MSIMonitor()
{
keepalive_thread_run = 0;
keepalive_thread->join();
delete keepalive_thread;
delete controller;
}
void RGBController_MSIMonitor::SetupZones()
{
zone new_zone;
new_zone.name = "Rear";
new_zone.type = ZONE_TYPE_LINEAR;
new_zone.leds_min = 9;
new_zone.leds_max = 9;
new_zone.leds_count = 9;
new_zone.matrix_map = nullptr;
zones.emplace_back(new_zone);
for(unsigned int i = 0 ; i < 9; i ++)
{
led new_led;
new_led.name = "LED " + std::to_string(i + 1);
leds.push_back(new_led);
}
SetupColors();
}
void RGBController_MSIMonitor::ResizeZone(int /*zone*/, int /*new_size*/)
{
/*---------------------------------------------------------*\
| This device does not support resizing zones |
\*---------------------------------------------------------*/
}
void RGBController_MSIMonitor::DeviceUpdateLEDs()
{
last_update_time = std::chrono::steady_clock::now();
controller->Set(modes[active_mode].value, colors, active_mode == 0 ? 0x00 : 0x01);
}
void RGBController_MSIMonitor::UpdateZoneLEDs(int /*zone*/)
{
DeviceUpdateLEDs();
}
void RGBController_MSIMonitor::UpdateSingleLED(int /*led*/)
{
DeviceUpdateLEDs();
}
void RGBController_MSIMonitor::DeviceUpdateMode()
{
controller->Set(modes[active_mode].value, colors, 0x01);
}
@@ -0,0 +1,38 @@
/*---------------------------------------------------------*\
| RGBController_MSIMonitor.cpp |
| |
| RGBController for MSI monitor (gaming controller) |
| |
| Andy Herbert 2026 May 16 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#pragma once
#include <chrono>
#include "RGBController.h"
#include "MSIMonitorController.h"
class RGBController_MSIMonitor : public RGBController
{
public:
RGBController_MSIMonitor(MSIMonitorController* controller_ptr);
~RGBController_MSIMonitor();
void SetupZones();
void ResizeZone(int zone, int new_size);
void DeviceUpdateLEDs();
void UpdateZoneLEDs(int zone);
void UpdateSingleLED(int led);
void DeviceUpdateMode();
private:
MSIMonitorController* controller;
std::thread* keepalive_thread;
std::atomic<bool> keepalive_thread_run;
std::chrono::time_point<std::chrono::steady_clock> last_update_time;
void KeepaliveThread();
};
@@ -71,6 +71,7 @@ static const std::string board_names[] =
"MSI PRO B850M-VC WIFI6E (MS-7E71)",
"MSI MAG B850 TOMAHAWK WIFI (MS-7E53)",
"MSI MEG Z890 UNIFY-X (MS-7E20)",
"MSI PRO X870E-S EVO WIFI (MS-7E86)",
};
static const mystic_light_761_config board_configs[] =
@@ -99,6 +100,7 @@ static const mystic_light_761_config board_configs[] =
{ &(board_names[21]), 0, 0, 0, 1, &zone_set1, MSIMysticLight761Controller::DIRECT_MODE_ZONE_BASED }, // MSI PRO B850M-VC WIFI6E (MS-7E71)
{ &(board_names[22]), 0, 0, 0, 1, &zone_set1, MSIMysticLight761Controller::DIRECT_MODE_ZONE_BASED }, // MSI MAG B850 TOMAHAWK WIFI (MS-7E53)
{ &(board_names[23]), 0, 0, 0, 1, &zone_set1, MSIMysticLight761Controller::DIRECT_MODE_ZONE_BASED }, // MSI MEG Z890 UNIFY-X (MS-7E20)
{ &(board_names[24]), 0, 0, 0, 1, &zone_set1, MSIMysticLight761Controller::DIRECT_MODE_ZONE_BASED }, // MSI PRO X870E-S EVO WIFI (MS-7E86)
};
enum MSI_ZONE setup_map [] =
@@ -17,12 +17,13 @@
#include "NZXTHue2Controller.h"
#include "StringUtils.h"
NZXTHue2Controller::NZXTHue2Controller(hid_device* dev_handle, unsigned int rgb_channels, unsigned int fan_channels, const char* path, std::string dev_name)
NZXTHue2Controller::NZXTHue2Controller(hid_device* dev_handle, unsigned int rgb_channels, unsigned int fan_channels, const char* path, std::string dev_name, bool use_2023_effects_val)
{
dev = dev_handle;
location = path;
name = dev_name;
use_2023_effects = use_2023_effects_val;
num_fan_channels = fan_channels;
num_rgb_channels = rgb_channels;
@@ -330,7 +331,14 @@ void NZXTHue2Controller::SetChannelEffect
/*-----------------------------------------------------*\
| Send effect packet |
\*-----------------------------------------------------*/
SendEffect(channel, mode, speed, direction, num_colors, &color_data[0]);
if(use_2023_effects)
{
SendEffect2023(channel, mode, speed, direction, num_colors, &color_data[0]);
}
else
{
SendEffect(channel, mode, speed, direction, num_colors, &color_data[0]);
}
}
void NZXTHue2Controller::SetChannelLEDs
@@ -357,14 +365,15 @@ void NZXTHue2Controller::SetChannelLEDs
/*-----------------------------------------------------*\
| Send first group of color data |
\*-----------------------------------------------------*/
SendDirect(channel, 0, 20, &color_data[0]);
unsigned char first_color_count = (num_colors > 20) ? 20 : (unsigned char)num_colors;
SendDirect(channel, 0, first_color_count, &color_data[0]);
/*-----------------------------------------------------*\
| Send second group of color data if necessary |
\*-----------------------------------------------------*/
if(num_colors > 20)
{
SendDirect(channel, 1, 20, &color_data[60]);
SendDirect(channel, 1, (unsigned char)(num_colors - 20), &color_data[60]);
}
/*-----------------------------------------------------*\
@@ -502,6 +511,99 @@ void NZXTHue2Controller::SendEffect
//hid_read(dev, usb_buf, 64);
}
void NZXTHue2Controller::SendEffect2023
(
unsigned char channel,
unsigned char mode,
unsigned char speed,
bool direction,
unsigned char color_count,
unsigned char* color_data
)
{
unsigned char usb_buf[64];
unsigned char speed_data[2] = { 0x32, 0x00 };
unsigned char mode_modifier = 0x00;
memset(usb_buf, 0x00, sizeof(usb_buf));
if(speed > HUE_2_SPEED_FASTEST)
{
speed = HUE_2_SPEED_NORMAL;
}
switch(mode)
{
case HUE_2_MODE_FADING:
{
const unsigned char values[5][2] = { {0x50, 0x00}, {0x3C, 0x00}, {0x28, 0x00}, {0x14, 0x00}, {0x0A, 0x00} };
speed_data[0] = values[speed][0];
speed_data[1] = values[speed][1];
mode_modifier = 0x08;
}
break;
case HUE_2_MODE_SPECTRUM:
case HUE_2_MODE_RAINBOW_FLOW:
case HUE_2_MODE_SUPER_RAINBOW:
case HUE_2_MODE_RAINBOW_PULSE:
{
const unsigned char values[5][2] = { {0x5E, 0x01}, {0x2C, 0x01}, {0xFA, 0x00}, {0x96, 0x00}, {0x50, 0x00} };
speed_data[0] = values[speed][0];
speed_data[1] = values[speed][1];
}
break;
case HUE_2_MODE_ALTERNATING:
{
const unsigned char values[5][2] = { {0x40, 0x06}, {0x14, 0x05}, {0xE8, 0x03}, {0x20, 0x03}, {0x58, 0x02} };
speed_data[0] = values[speed][0];
speed_data[1] = values[speed][1];
}
break;
case HUE_2_MODE_PULSING:
case HUE_2_MODE_STARRY_NIGHT:
{
const unsigned char values[5][2] = { {0x19, 0x00}, {0x14, 0x00}, {0x0F, 0x00}, {0x07, 0x00}, {0x04, 0x00} };
speed_data[0] = values[speed][0];
speed_data[1] = values[speed][1];
mode_modifier = (mode == HUE_2_MODE_PULSING) ? 0x08 : 0x00;
}
break;
case HUE_2_MODE_BREATHING:
{
const unsigned char values[5][2] = { {0x28, 0x00}, {0x1E, 0x00}, {0x14, 0x00}, {0x0A, 0x00}, {0x04, 0x00} };
speed_data[0] = values[speed][0];
speed_data[1] = values[speed][1];
mode_modifier = 0x08;
}
break;
default:
break;
}
usb_buf[0x00] = 0x2A;
usb_buf[0x01] = 0x04;
usb_buf[0x02] = (unsigned char)(1 << channel);
usb_buf[0x03] = usb_buf[0x02];
usb_buf[0x04] = mode;
usb_buf[0x05] = speed_data[0];
usb_buf[0x06] = speed_data[1];
memcpy(&usb_buf[0x07], color_data, color_count * 3);
usb_buf[0x37] = direction ? 0x02 : 0x00;
usb_buf[0x38] = color_count;
usb_buf[0x39] = mode_modifier;
usb_buf[0x3A] = 0x08;
usb_buf[0x3B] = 0x03;
hid_write(dev, usb_buf, 64);
}
void NZXTHue2Controller::SendFirmwareRequest()
{
unsigned char usb_buf[64];
@@ -66,7 +66,7 @@ enum
class NZXTHue2Controller
{
public:
NZXTHue2Controller(hid_device* dev_handle, unsigned int rgb_channels, unsigned int fan_channels, const char* path, std::string dev_name);
NZXTHue2Controller(hid_device* dev_handle, unsigned int rgb_channels, unsigned int fan_channels, const char* path, std::string dev_name, bool use_2023_effects = false);
~NZXTHue2Controller();
std::string GetFirmwareVersion();
@@ -129,6 +129,7 @@ private:
char firmware_version[16];
std::string location;
std::string name;
bool use_2023_effects;
unsigned int num_fan_channels;
unsigned int num_rgb_channels;
@@ -155,5 +156,15 @@ private:
unsigned char* color_data
);
void SendEffect2023
(
unsigned char channel,
unsigned char mode,
unsigned char speed,
bool direction,
unsigned char color_count,
unsigned char* color_data
);
void SendFirmwareRequest();
};
@@ -38,13 +38,13 @@
#define NZXT_SMART_DEVICE_V2_1_PID 0x200D
#define NZXT_SMART_DEVICE_V2_2_PID 0x200F
static void spawn_hue(hid_device_info* info, const std::string& name, int rgb_channels, int fan_channels)
static void spawn_hue(hid_device_info* info, const std::string& name, int rgb_channels, int fan_channels, bool use_2023_effects = false)
{
hid_device* dev = hid_open_path(info->path);
if(dev)
{
NZXTHue2Controller* controller = new NZXTHue2Controller(dev, rgb_channels, fan_channels, info->path, name);
NZXTHue2Controller* controller = new NZXTHue2Controller(dev, rgb_channels, fan_channels, info->path, name, use_2023_effects);
RGBController_NZXTHue2* rgb_controller = new RGBController_NZXTHue2(controller);
ResourceManager::get()->RegisterRGBController(rgb_controller);
@@ -83,7 +83,7 @@ void DetectNZXTKrakenX3(hid_device_info* info, const std::string& name)
void DetectNZXTKrakenElite(hid_device_info* info, const std::string& name)
{
spawn_hue(info, name, 2, 2);
spawn_hue(info, name, 2, 2, true);
}
void DetectNZXTFanController(hid_device_info* info, const std::string& name)
+22
View File
@@ -0,0 +1,22 @@
/*---------------------------------------------------------*\
| QMKCommon.h |
| |
| Common QMK definitions |
| |
| Adam Honse <calcprogrammer1@gmail.com) 24 Jun 2026 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#pragma once
typedef struct
{
unsigned char x; /* X position in RGB matrix (0-224) */
unsigned char y; /* Y position in RGB matrix (0-64) */
unsigned char flags; /* LED flags */
unsigned char row; /* Row in key matrix */
unsigned char col; /* Column in key matrix */
bool valid; /* Is this LED valid? */
} qmk_rgb_matrix_led_info;
@@ -0,0 +1,512 @@
/*---------------------------------------------------------*\
| QMKKeychronController.cpp |
| |
| Driver for Keychron QMK-based keyboards |
| |
| Amadej Kastelic 21 Jun 2026 |
| Adam Honse <calcprogrammer1@gmail.com> 22 Jun 2026 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include <string.h>
#include "hsv.h"
#include "QMKKeychronController.h"
#include "QMKViaCommands.h"
#include "StringUtils.h"
#include "LogManager.h"
using namespace std::chrono_literals;
QMKKeychronController::QMKKeychronController(hid_device* dev_handle, const char *path)
{
/*-----------------------------------------------------*\
| Initialize controller fields |
\*-----------------------------------------------------*/
dev = dev_handle;
location = path;
kc_protocol_version = 0;
supported_features = 0;
via_protocol_version = 0;
/*-----------------------------------------------------*\
| Read product string |
\*-----------------------------------------------------*/
wchar_t product_string[256];
int ret = hid_get_product_string(dev, product_string, 256);
if(ret != 0)
{
name = "";
}
else
{
name = StringUtils::wstring_to_string(product_string);
}
/*-----------------------------------------------------*\
| Read vendor string |
\*-----------------------------------------------------*/
wchar_t vendor_string[256];
ret = hid_get_manufacturer_string(dev, vendor_string, 256);
if(ret != 0)
{
vendor = "";
}
else
{
vendor = StringUtils::wstring_to_string(vendor_string);
}
/*-----------------------------------------------------*\
| Read serial string |
\*-----------------------------------------------------*/
wchar_t serial_string[256];
ret = hid_get_serial_number_string(dev, serial_string, 256);
if(ret != 0)
{
serial = "";
}
else
{
serial = StringUtils::wstring_to_string(serial_string);
}
/*-----------------------------------------------------*\
| Get VIA protocol version |
\*-----------------------------------------------------*/
CmdGetViaProtocolVersion(&via_protocol_version);
/*-----------------------------------------------------*\
| Get Keychron protocol version |
\*-----------------------------------------------------*/
CmdGetKeychronProtocolVersion(&kc_protocol_version);
/*-----------------------------------------------------*\
| Get supported Keychron features |
\*-----------------------------------------------------*/
CmdGetSupportFeature(&supported_features);
if(!GetSupported())
{
return;
}
/*-----------------------------------------------------*\
| Get Keychron RGB protocol version |
\*-----------------------------------------------------*/
CmdGetKeychronRGBProtocolVersion(&kc_rgb_protocol_version);
/*-----------------------------------------------------*\
| Get count of LEDs |
\*-----------------------------------------------------*/
CmdGetNumberLEDs(&number_leds);
led_info.resize(number_leds);
keycodes.resize(number_leds);
for(std::size_t led_idx = 0; led_idx < led_info.size(); led_idx++)
{
led_info[led_idx].valid = false;
}
/*-----------------------------------------------------*\
| Get info and keycode for all LEDs |
\*-----------------------------------------------------*/
for(unsigned char row = 0; row < 32; row++)
{
std::vector<unsigned char> row_leds = CmdGetLEDIndexByRow(row);
for(unsigned char col = 0; col < row_leds.size(); col++)
{
if(row_leds[col] != 0xFF && row_leds[col] < led_info.size() && led_info[row_leds[col]].valid == false)
{
led_info[row_leds[col]].valid = true;
led_info[row_leds[col]].col = col;
led_info[row_leds[col]].row = row;
}
}
}
for(unsigned short led_index = 0; led_index < number_leds; led_index++)
{
keycodes[led_index] = CmdGetKeycode(0, led_info[led_index].row, led_info[led_index].col);
}
}
QMKKeychronController::~QMKKeychronController()
{
hid_close(dev);
}
std::string QMKKeychronController::GetLocation()
{
return("HID: " + location);
}
std::string QMKKeychronController::GetName()
{
return(name);
}
std::string QMKKeychronController::GetSerial()
{
return(serial);
}
std::string QMKKeychronController::GetVendor()
{
return(vendor);
}
std::string QMKKeychronController::GetVersion()
{
/*-----------------------------------------------------*\
| Format multi-line version text |
\*-----------------------------------------------------*/
return("VIA: " + std::to_string(via_protocol_version) + "\r\n" +
"Keychron: " + std::to_string(kc_protocol_version) + "\r\n" +
"Keychron RGB: " + std::to_string(kc_rgb_protocol_version));
}
bool QMKKeychronController::GetSupported()
{
return(supported_features & KC_FEATURE_KEYCHRON_RGB);
}
unsigned short QMKKeychronController::GetKeycode(unsigned short led_index)
{
return(keycodes[led_index]);
}
unsigned short QMKKeychronController::GetLEDCount()
{
return(number_leds);
}
qmk_rgb_matrix_led_info QMKKeychronController::GetLEDInfo(unsigned short led_index)
{
return(led_info[led_index]);
}
void QMKKeychronController::SaveMode()
{
CmdSaveMode();
}
void QMKKeychronController::SendLEDs(unsigned short number_leds, RGBColor* color_data)
{
unsigned short led_start_index = 0;
unsigned char number_packet_leds = 9;
while(led_start_index < number_leds)
{
if((number_leds - led_start_index) < 9)
{
number_packet_leds = (number_leds - led_start_index);
}
CmdSendLEDs(led_start_index, number_packet_leds, &color_data[led_start_index]);
led_start_index += number_packet_leds;
}
}
void QMKKeychronController::SetMode(unsigned short mode, unsigned char speed, unsigned char hue, unsigned char sat, unsigned char val)
{
if(mode == 0xFFFF)
{
CmdSetRGBMatrixMode(KEYCHRON_QHE_PER_KEY_RGB_EFFECT);
CmdSetPerKeyRGBType(KEYCHRON_PER_KEY_RGB_SOLID);
}
else
{
CmdSetRGBMatrixMode((unsigned char)mode);
CmdSetColorHS(hue, sat);
CmdSetBrightness(val);
CmdSetSpeed(speed);
}
}
unsigned short QMKKeychronController::CmdGetKeycode
(
unsigned char layer,
unsigned char row,
unsigned char col
)
{
unsigned char args[3];
unsigned char response[5];
unsigned short keycode;
args[0] = layer;
args[1] = row;
args[2] = col;
ViaSendCommand(QMK_VIA_CMD_VIA_DYNAMIC_KEYMAP_GET_KEYCODE, args, sizeof(args), response, sizeof(response));
keycode = ( response[3] << 8 )| response[4];
return(keycode);
}
void QMKKeychronController::CmdGetKeychronProtocolVersion
(
unsigned char* kc_protocol_version
)
{
ViaSendCommand(KC_GET_PROTOCOL_VERSION, NULL, 0, (unsigned char*)kc_protocol_version, sizeof(unsigned char));
}
void QMKKeychronController::CmdGetKeychronRGBProtocolVersion(unsigned short* kc_rgb_protocol_version)
{
ViaSendCommandSub(KC_KEYCHRON_RGB, KEYCHRON_RGB_PROTOCOL_VER, NULL, 0, (unsigned char*)kc_rgb_protocol_version, sizeof(unsigned short));
/*-----------------------------------------------------*\
| The RGB protocol version byte order is reversed |
\*-----------------------------------------------------*/
*kc_rgb_protocol_version = ((*kc_rgb_protocol_version & 0x00FF) << 8) | ((*kc_rgb_protocol_version & 0xFF00) >> 8);
}
std::vector<unsigned char> QMKKeychronController::CmdGetLEDIndexByRow(unsigned char row)
{
unsigned char args[4];
unsigned char response[KEYCHRON_QHE_PACKET_SIZE - 2];
args[0] = row;
args[1] = 0xFF;
args[2] = 0xFF;
args[3] = 0xFF;
int bytes_read = ViaSendCommandSub(KC_KEYCHRON_RGB, KEYCHRON_RGB_LED_IDX, args, sizeof(args), response, sizeof(response));
std::vector<unsigned char> result;
if(bytes_read > 0)
{
for(int i = 1; i < bytes_read; i++)
{
result.push_back(response[i] == 0xFF ? -1 : response[i]);
}
}
return result;
}
void QMKKeychronController::CmdGetNumberLEDs
(
unsigned short* number_leds
)
{
ViaSendCommandSub(KC_KEYCHRON_RGB, KEYCHRON_RGB_LED_COUNT, NULL, 0, (unsigned char*)number_leds, sizeof(unsigned short));
/*-----------------------------------------------------*\
| The LED count byte order is reversed |
\*-----------------------------------------------------*/
*number_leds = ((*number_leds & 0x00FF) << 8) | ((*number_leds & 0xFF00) >> 8);
}
void QMKKeychronController::CmdGetSupportFeature(unsigned short* supported_features)
{
ViaSendCommand(KC_GET_SUPPORT_FEATURE, NULL, 0, (unsigned char*)supported_features, sizeof(unsigned short));
/*-----------------------------------------------------*\
| The supported features byte order is reversed |
\*-----------------------------------------------------*/
*supported_features = ((*supported_features & 0x00FF) << 8) | ((*supported_features & 0xFF00) >> 8);
}
void QMKKeychronController::CmdGetViaProtocolVersion
(
unsigned short* via_protocol_version
)
{
ViaSendCommand(QMK_VIA_CMD_GET_PROTOCOL_VERSION, NULL, 0, (unsigned char*)via_protocol_version, sizeof(unsigned short));
/*-----------------------------------------------------*\
| The protocol version byte order is reversed |
\*-----------------------------------------------------*/
*via_protocol_version = ((*via_protocol_version & 0x00FF) << 8) | ((*via_protocol_version & 0xFF00) >> 8);
}
void QMKKeychronController::CmdSaveMode()
{
ViaSendCommandSub(KC_KEYCHRON_RGB, KEYCHRON_RGB_SAVE, NULL, 0, NULL, 0);
}
void QMKKeychronController::CmdSendLEDs(unsigned char start_index, unsigned char number_leds, RGBColor* color_data)
{
unsigned char args[KEYCHRON_QHE_PACKET_SIZE - 2];
args[0] = start_index;
args[1] = number_leds;
if(number_leds > 9)
{
number_leds = 9;
}
for(unsigned char led_index = 0; led_index < number_leds; led_index++)
{
/*-------------------------------------------------*\
| VialRGB sends direct packets in HSV for some |
| inexplicable reason, so do the RGB to HSV |
| conversion before sending |
\*-------------------------------------------------*/
hsv_t hsv_color;
rgb2hsv(color_data[led_index], &hsv_color);
args[2 + (led_index * 3)] = (unsigned char)((float)hsv_color.hue * (256.0f / 360.0f));
args[3 + (led_index * 3)] = hsv_color.saturation;
args[4 + (led_index * 3)] = hsv_color.value;
}
ViaSendCommandSub(KC_KEYCHRON_RGB, KEYCHRON_RGB_PER_KEY_SET_COLOR, args, sizeof(args), NULL, 0);
}
void QMKKeychronController::CmdSetBrightness(unsigned char brightness)
{
unsigned char args[3];
args[0] = QMK_VIA_RGB_MATRIX_CHANNEL;
args[1] = QMK_VIA_RGB_MATRIX_BRIGHTNESS;
args[2] = brightness;
ViaSendCommand(QMK_VIA_CMD_CUSTOM_SET_VALUE, args, sizeof(args), NULL, 0);
}
void QMKKeychronController::CmdSetColorHS(unsigned char h, unsigned char s)
{
unsigned char args[4];
args[0] = QMK_VIA_RGB_MATRIX_CHANNEL;
args[1] = QMK_VIA_RGB_MATRIX_COLOR;
args[2] = h;
args[3] = s;
ViaSendCommand(QMK_VIA_CMD_CUSTOM_SET_VALUE, args, sizeof(args), NULL, 0);
}
void QMKKeychronController::CmdSetPerKeyRGBType(unsigned char type)
{
unsigned char args[1];
args[0] = type;
ViaSendCommandSub(KC_KEYCHRON_RGB, KEYCHRON_RGB_PER_KEY_SET_TYPE, args, sizeof(args), NULL, 0);
}
void QMKKeychronController::CmdSetRGBMatrixMode(unsigned char mode)
{
unsigned char args[3];
args[0] = QMK_VIA_RGB_MATRIX_CHANNEL;
args[1] = QMK_VIA_RGB_MATRIX_EFFECT;
args[2] = mode;
ViaSendCommand(QMK_VIA_CMD_CUSTOM_SET_VALUE, args, sizeof(args), NULL, 0);
}
void QMKKeychronController::CmdSetSpeed(unsigned char speed)
{
unsigned char args[3];
args[0] = QMK_VIA_RGB_MATRIX_CHANNEL;
args[1] = QMK_VIA_RGB_MATRIX_EFFECT_SPEED;
args[2] = speed;
ViaSendCommand(QMK_VIA_CMD_CUSTOM_SET_VALUE, args, sizeof(args), NULL, 0);
}
int QMKKeychronController::ViaSendCommand
(
unsigned char cmd,
unsigned char* data_in,
unsigned char data_in_size,
unsigned char* data_out,
unsigned char data_out_size
)
{
/*-----------------------------------------------------*\
| Standard VIA command with no sub-command |
| |
| Byte 0: Command |
| Byte 1+: Data |
\*-----------------------------------------------------*/
unsigned char usb_buf[KEYCHRON_QHE_PACKET_SIZE + 1];
memset(usb_buf, 0, sizeof(usb_buf));
/*-----------------------------------------------------*\
| Write command, offsetting by 1 for HID report ID |
\*-----------------------------------------------------*/
usb_buf[1] = cmd;
memcpy(&usb_buf[2], data_in, data_in_size);
hid_write(dev, usb_buf, sizeof(usb_buf));
/*-----------------------------------------------------*\
| Read response |
\*-----------------------------------------------------*/
int bytes_received = hid_read_timeout(dev, usb_buf, sizeof(usb_buf) - 1, 1000);
if(usb_buf[0] != cmd)
{
return(-1);
}
memcpy(data_out, &usb_buf[1], data_out_size);
return(bytes_received - 1);
}
int QMKKeychronController::ViaSendCommandSub
(
unsigned char cmd,
unsigned char subcmd,
unsigned char* data_in,
unsigned char data_in_size,
unsigned char* data_out,
unsigned char data_out_size
)
{
/*-----------------------------------------------------*\
| Standard VIA command with sub-command |
| |
| Byte 0: Command |
| Byte 1: Sub-Command |
| Byte 2+: Data |
\*-----------------------------------------------------*/
unsigned char usb_buf[KEYCHRON_QHE_PACKET_SIZE + 1];
memset(usb_buf, 0, sizeof(usb_buf));
/*-----------------------------------------------------*\
| Write command, offsetting by 1 for HID report ID |
\*-----------------------------------------------------*/
usb_buf[1] = cmd;
usb_buf[2] = subcmd;
memcpy(&usb_buf[3], data_in, data_in_size);
hid_write(dev, usb_buf, sizeof(usb_buf));
/*-----------------------------------------------------*\
| Read response |
\*-----------------------------------------------------*/
int bytes_received = hid_read_timeout(dev, usb_buf, sizeof(usb_buf) - 1, 1000);
if(usb_buf[0] != cmd || usb_buf[1] != subcmd)
{
return(-1);
}
memcpy(data_out, &usb_buf[2], data_out_size);
return(bytes_received - 2);
}
@@ -0,0 +1,206 @@
/*---------------------------------------------------------*\
| QMKKeychronController.h |
| |
| Driver for Keychron QMK-based keyboards |
| |
| Amadej Kastelic 21 Jun 2026 |
| Adam Honse <calcprogrammer1@gmail.com> 22 Jun 2026 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#pragma once
#include <string>
#include <hidapi.h>
#include "QMKCommon.h"
#include "RGBController.h"
/*---------------------------------------------------------*\
| Keychron vendor ID |
\*---------------------------------------------------------*/
#define KEYCHRON_VID 0x3434
/*---------------------------------------------------------*\
| Product IDs |
\*---------------------------------------------------------*/
#define KEYCHRON_K10_V2_PID 0x0DA0
#define KEYCHRON_Q1_HE_PID 0x0B10
#define KEYCHRON_Q2_PID 0x0111
/*---------------------------------------------------------*\
| QMK raw HID usage page/usage |
\*---------------------------------------------------------*/
#define KEYCHRON_QMK_USAGE_PAGE 0xFF60
#define KEYCHRON_QMK_USAGE 0x61
/*---------------------------------------------------------*\
| HID packet constants |
\*---------------------------------------------------------*/
#define KEYCHRON_QHE_PACKET_SIZE 32
#define KEYCHRON_QHE_HID_READ_TIMEOUT 1000
/*---------------------------------------------------------*\
| Keychron-specific VIA protocol extension |
\*---------------------------------------------------------*/
enum
{
KC_GET_PROTOCOL_VERSION = 0xA0,
KC_GET_FIRMWARE_VERSION = 0xA1,
KC_GET_SUPPORT_FEATURE = 0xA2,
KC_GET_DEFAULT_LAYER = 0xA3,
KC_MISC_CMD_GROUP = 0xA7,
KC_KEYCHRON_RGB = 0xA8,
KC_ANALOG_MATRIX = 0xA9,
KC_WIRELESS_DFU = 0xAA,
KC_FACTORY_TEST = 0xAB
};
enum KeychronKCRGBCommand
{
KEYCHRON_RGB_PROTOCOL_VER = 0x01,
KEYCHRON_RGB_SAVE = 0x02,
KEYCHRON_RGB_GET_INDICATORS = 0x03,
KEYCHRON_RGB_SET_INDICATORS = 0x04,
KEYCHRON_RGB_LED_COUNT = 0x05,
KEYCHRON_RGB_LED_IDX = 0x06,
KEYCHRON_RGB_PER_KEY_GET_TYPE = 0x07,
KEYCHRON_RGB_PER_KEY_SET_TYPE = 0x08,
KEYCHRON_RGB_PER_KEY_GET_COLOR = 0x09,
KEYCHRON_RGB_PER_KEY_SET_COLOR = 0x0A,
KEYCHRON_RGB_MIXED_GET_INFO = 0x0B,
KEYCHRON_RGB_MIXED_GET_REGIONS = 0x0C,
KEYCHRON_RGB_MIXED_SET_REGIONS = 0x0D,
KEYCHRON_RGB_MIXED_GET_EFFECTS = 0x0E,
KEYCHRON_RGB_MIXED_SET_EFFECTS = 0x0F,
};
enum
{
KC_FEATURE_DEFAULT_LAYER = ( 1 << 0 ),
KC_FEATURE_BLUETOOTH = ( 1 << 1 ),
KC_FEATURE_P24G = ( 1 << 2 ),
KC_FEATURE_ANALOG_MATRIX = ( 1 << 3 ),
KC_FEATURE_STATE_NOTIFY = ( 1 << 4 ),
KC_FEATURE_DYNAMIC_DEBOUNCE = ( 1 << 5 ),
KC_FEATURE_SNAP_CLICK = ( 1 << 6 ),
KC_FEATURE_KEYCHRON_RGB = ( 1 << 7 ),
KC_FEATURE_QUICK_START = ( 1 << 8 ),
KC_FEATURE_NKRO = ( 1 << 9 ),
};
/*---------------------------------------------------------*\
| Per-key RGB animation types (PER_KEY_RGB_SET_TYPE value) |
\*---------------------------------------------------------*/
enum KeychronPerKeyRgbType
{
KEYCHRON_PER_KEY_RGB_SOLID = 0,
KEYCHRON_PER_KEY_RGB_BREATHING = 1,
KEYCHRON_PER_KEY_RGB_REACTIVE_SIMPLE = 2,
KEYCHRON_PER_KEY_RGB_REACTIVE_WIDE = 3,
KEYCHRON_PER_KEY_RGB_REACTIVE_SPLASH = 4,
};
/*---------------------------------------------------------*\
| VIA backlight value IDs |
\*---------------------------------------------------------*/
#define KEYCHRON_VIA_BACKLIGHT_TYPE_RGB_MATRIX 0x03
enum KeychronVIABacklightValueID
{
KEYCHRON_VIA_BACKLIGHT_BRIGHTNESS = 0x01,
KEYCHRON_VIA_BACKLIGHT_EFFECT = 0x02,
KEYCHRON_VIA_BACKLIGHT_SPEED = 0x03,
KEYCHRON_VIA_BACKLIGHT_COLOR = 0x04,
};
/*---------------------------------------------------------*\
| Q1 HE effect IDs |
| |
| Determined by the number of standard effects enabled |
| in the Q1 HE firmware (info.json animations) + |
| 2 custom effects (PER_KEY_RGB, MIXED_RGB). |
| May need adjustment for other firmware versions. |
\*---------------------------------------------------------*/
#define KEYCHRON_QHE_PER_KEY_RGB_EFFECT 23
/*---------------------------------------------------------*\
| Brightness and speed ranges |
\*---------------------------------------------------------*/
#define KEYCHRON_QHE_MIN_BRIGHTNESS 0x00
#define KEYCHRON_QHE_MAX_BRIGHTNESS 0xFF
#define KEYCHRON_QHE_MIN_SPEED 0x00
#define KEYCHRON_QHE_MAX_SPEED 0xFF
class QMKKeychronController
{
public:
QMKKeychronController(hid_device* dev_handle, const char *path);
~QMKKeychronController();
std::string GetLocation();
std::string GetName();
std::string GetSerial();
std::string GetVendor();
std::string GetVersion();
bool GetSupported();
unsigned short GetKeycode(unsigned short led_index);
unsigned short GetLEDCount();
qmk_rgb_matrix_led_info GetLEDInfo(unsigned short led_index);
void SendLEDs(unsigned short number_leds, RGBColor* color_data);
void SetMode(unsigned short mode, unsigned char speed, unsigned char hue, unsigned char sat, unsigned char val);
void SaveMode();
private:
hid_device* dev;
unsigned char kc_protocol_version;
unsigned short kc_rgb_protocol_version;
std::vector<unsigned short> keycodes;
std::vector<qmk_rgb_matrix_led_info> led_info;
std::string location;
std::string name;
unsigned short number_leds;
std::string serial;
unsigned short supported_features;
std::string vendor;
unsigned short via_protocol_version;
unsigned short CmdGetKeycode(unsigned char layer, unsigned char row, unsigned char col);
void CmdGetKeychronProtocolVersion(unsigned char* kc_protocol_version);
void CmdGetKeychronRGBProtocolVersion(unsigned short* kc_rgb_protocol_version);
std::vector<unsigned char> CmdGetLEDIndexByRow(unsigned char row);
void CmdGetNumberLEDs(unsigned short* number_leds);
void CmdGetSupportFeature(unsigned short* supported_features);
void CmdGetViaProtocolVersion(unsigned short* via_protocol_version);
void CmdSaveMode();
void CmdSendLEDs(unsigned char start_index, unsigned char number_leds, RGBColor* color_data);
void CmdSetBrightness(unsigned char brightness);
void CmdSetColorHS(unsigned char h, unsigned char s);
void CmdSetPerKeyRGBType(unsigned char type);
void CmdSetRGBMatrixMode(unsigned char mode);
void CmdSetSpeed(unsigned char speed);
int ViaSendCommand
(
unsigned char cmd,
unsigned char* data_in,
unsigned char data_in_size,
unsigned char* data_out,
unsigned char data_out_size
);
int ViaSendCommandSub
(
unsigned char cmd,
unsigned char subcmd,
unsigned char* data_in,
unsigned char data_in_size,
unsigned char* data_out,
unsigned char data_out_size
);
};
@@ -0,0 +1,39 @@
/*---------------------------------------------------------*\
| QMKKeychronControllerDetect.cpp |
| |
| Detector for Keychron QMK-based keyboards |
| |
| Amadej Kastelic 21 Jun 2026 |
| Adam Honse <calcprogrammer1@gmail.com> 22 Jun 2026 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include "Detector.h"
#include "QMKKeychronController.h"
#include "RGBController_QMKKeychron.h"
void DetectQMKKeychronController(hid_device_info *info, const std::string&)
{
hid_device *dev = hid_open_path(info->path);
if(dev)
{
QMKKeychronController* controller = new QMKKeychronController(dev, info->path);
if(controller->GetSupported())
{
RGBController_QMKKeychron* rgb_controller = new RGBController_QMKKeychron(controller);
ResourceManager::get()->RegisterRGBController(rgb_controller);
}
else
{
delete controller;
}
}
}
REGISTER_HID_DETECTOR_IPU("Keychron K10 V2", DetectQMKKeychronController, KEYCHRON_VID, KEYCHRON_K10_V2_PID, 1, KEYCHRON_QMK_USAGE_PAGE, KEYCHRON_QMK_USAGE);
REGISTER_HID_DETECTOR_IPU("Keychron Q1 HE", DetectQMKKeychronController, KEYCHRON_VID, KEYCHRON_Q1_HE_PID, 1, KEYCHRON_QMK_USAGE_PAGE, KEYCHRON_QMK_USAGE);
REGISTER_HID_DETECTOR_IPU("Keychron Q2", DetectQMKKeychronController, KEYCHRON_VID, KEYCHRON_Q2_PID, 1, KEYCHRON_QMK_USAGE_PAGE, KEYCHRON_QMK_USAGE);
@@ -0,0 +1,239 @@
/*---------------------------------------------------------*\
| RGBController_QMKKeychron.cpp |
| |
| RGBController for Keychron QMK-based keyboards |
| |
| Amadej Kastelic 21 Jun 2026 |
| Adam Honse <calcprogrammer1@gmail.com> 22 Jun 2026 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include <cstring>
#include "hsv.h"
#include "RGBController_QMKKeychron.h"
#include "QMKKeycodes.h"
#include "QMKKeychronController.h"
/**------------------------------------------------------------------*\
@name QMK Keychron
@category Keyboard
@type USB
@save :white_check_mark:
@direct :white_check_mark:
@effects :white_check_mark:
@detectors DetectQMKKeychronController
@comment
\*-------------------------------------------------------------------*/
typedef struct
{
std::string name;
int value;
int flags;
} kc_effect;
static const kc_effect kc_effects[] =
{
{ "Direct", 0xFFFF, MODE_FLAG_HAS_PER_LED_COLOR },
{ "Solid Color", 1, MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_MANUAL_SAVE },
{ "Breathing", 2, MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_SPEED | MODE_FLAG_MANUAL_SAVE },
{ "Band Spiral", 3, MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_SPEED | MODE_FLAG_MANUAL_SAVE },
{ "Cycle All", 4, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_SPEED | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_MANUAL_SAVE },
{ "Cycle Left Right", 5, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_SPEED | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_MANUAL_SAVE },
{ "Cycle Up Down", 6, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_SPEED | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_MANUAL_SAVE },
{ "Rainbow Moving Chevron", 7, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_SPEED | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_MANUAL_SAVE },
{ "Cycle Out In", 8, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_SPEED | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_MANUAL_SAVE },
{ "Cycle Out In Dual", 9, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_SPEED | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_MANUAL_SAVE },
{ "Cycle Pinwheel", 10, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_SPEED | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_MANUAL_SAVE },
{ "Cycle Spiral", 11, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_SPEED | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_MANUAL_SAVE },
{ "Dual Beacon", 12, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_SPEED | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_MANUAL_SAVE },
{ "Rainbow Beacon", 13, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_SPEED | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_MANUAL_SAVE },
{ "Jellybean Raindrops", 14, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_MANUAL_SAVE },
{ "Pixel Rain", 15, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_MANUAL_SAVE },
{ "Typing Heatmap", 16, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_MANUAL_SAVE },
{ "Digital Rain", 17, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_SPEED | MODE_FLAG_MANUAL_SAVE },
{ "Solid Reactive Simple", 18, MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_SPEED | MODE_FLAG_MANUAL_SAVE },
{ "Solid Reactive Multiwide", 19, MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_SPEED | MODE_FLAG_MANUAL_SAVE },
{ "Solid Reactive Multinexus", 20, MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_SPEED | MODE_FLAG_MANUAL_SAVE },
{ "Splash", 21, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_SPEED | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_MANUAL_SAVE },
{ "Solid Splash", 22, MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_SPEED | MODE_FLAG_MANUAL_SAVE },
};
RGBController_QMKKeychron::RGBController_QMKKeychron(QMKKeychronController* controller_ptr)
{
controller = controller_ptr;
name = controller->GetName();
vendor = controller->GetVendor();
type = DEVICE_TYPE_KEYBOARD;
description = "QMK Keychron Device";
location = controller->GetLocation();
serial = controller->GetSerial();
version = controller->GetVersion();
for(const kc_effect& effect : kc_effects)
{
mode m;
m.name = effect.name;
m.value = effect.value;
m.flags = effect.flags;
if(m.flags & MODE_FLAG_HAS_MODE_SPECIFIC_COLOR)
{
m.color_mode = MODE_COLORS_MODE_SPECIFIC;
m.colors_min = 1;
m.colors_max = 1;
m.colors.resize(1);
}
else if(m.flags & MODE_FLAG_HAS_PER_LED_COLOR)
{
m.color_mode = MODE_COLORS_PER_LED;
m.colors_min = 0;
m.colors_max = 0;
m.colors.resize(0);
}
else
{
m.color_mode = MODE_COLORS_NONE;
m.colors_min = 0;
m.colors_max = 0;
m.colors.resize(0);
}
if(m.flags & MODE_FLAG_HAS_SPEED)
{
m.speed_min = KEYCHRON_QHE_MIN_SPEED;
m.speed_max = KEYCHRON_QHE_MAX_SPEED;
m.speed = KEYCHRON_QHE_MAX_SPEED / 2;
}
if(m.flags & MODE_FLAG_HAS_BRIGHTNESS)
{
m.brightness_min = KEYCHRON_QHE_MIN_BRIGHTNESS;
m.brightness_max = KEYCHRON_QHE_MAX_BRIGHTNESS;
m.brightness = KEYCHRON_QHE_MAX_BRIGHTNESS;
}
modes.push_back(m);
}
SetupZones();
}
RGBController_QMKKeychron::~RGBController_QMKKeychron()
{
delete controller;
}
void RGBController_QMKKeychron::SetupZones()
{
/*-----------------------------------------------------*\
| Build matrix map |
\*-----------------------------------------------------*/
unsigned char max_col = 0;
unsigned char max_row = 0;
for(unsigned short led_index = 0; led_index < controller->GetLEDCount(); led_index++)
{
qmk_rgb_matrix_led_info info = controller->GetLEDInfo(led_index);
if(info.col > max_col)
{
max_col = info.col;
}
if(info.row > max_row)
{
max_row = info.row;
}
}
unsigned char height = max_row + 1;
unsigned char width = max_col + 1;
unsigned int* matrix_map = new unsigned int[width * height];
memset(matrix_map, 0xFF, (sizeof(unsigned int) * (width * height)));
for(unsigned short led_index = 0; led_index < controller->GetLEDCount(); led_index++)
{
qmk_rgb_matrix_led_info info = controller->GetLEDInfo(led_index);
matrix_map[(width * info.row) + info.col] = (unsigned int)led_index;
}
/*-----------------------------------------------------*\
| Create keyboard zone |
\*-----------------------------------------------------*/
zone keyboard;
keyboard.name = "Keyboard";
keyboard.type = ZONE_TYPE_MATRIX;
keyboard.leds_min = controller->GetLEDCount();
keyboard.leds_max = controller->GetLEDCount();
keyboard.leds_count = controller->GetLEDCount();
keyboard.matrix_map = new matrix_map_type;
keyboard.matrix_map->height = height;
keyboard.matrix_map->width = width;
keyboard.matrix_map->map = matrix_map;
zones.push_back(keyboard);
/*-----------------------------------------------------*\
| Create keyboard LEDs |
\*-----------------------------------------------------*/
for(unsigned short led_idx = 0; led_idx < controller->GetLEDCount(); led_idx++)
{
led new_led;
new_led.name = qmk_keynames[controller->GetKeycode(led_idx)];
leds.push_back(new_led);
}
SetupColors();
}
void RGBController_QMKKeychron::ResizeZone(int /*zone*/, int /*new_size*/)
{
}
void RGBController_QMKKeychron::DeviceUpdateLEDs()
{
controller->SendLEDs((unsigned short)colors.size(), colors.data());
}
void RGBController_QMKKeychron::UpdateZoneLEDs(int /*zone*/)
{
DeviceUpdateLEDs();
}
void RGBController_QMKKeychron::UpdateSingleLED(int /*led*/)
{
DeviceUpdateLEDs();
}
void RGBController_QMKKeychron::DeviceUpdateMode()
{
unsigned char hue = 0;
unsigned char sat = 255;
unsigned char val = modes[active_mode].brightness;
if(modes[active_mode].color_mode == MODE_COLORS_MODE_SPECIFIC)
{
hsv_t hsv_color;
rgb2hsv(modes[active_mode].colors[0], &hsv_color);
hue = (unsigned char)((float)hsv_color.hue * (256.0f / 360.0f));
sat = hsv_color.saturation;
val = hsv_color.value;
}
controller->SetMode(modes[active_mode].value, modes[active_mode].speed, hue, sat, val);
}
void RGBController_QMKKeychron::DeviceSaveMode()
{
controller->SaveMode();
}
@@ -0,0 +1,36 @@
/*---------------------------------------------------------*\
| RGBController_QMKKeychron.h |
| |
| RGBController for Keychron QMK-based keyboards |
| |
| Amadej Kastelic 21 Jun 2026 |
| Adam Honse <calcprogrammer1@gmail.com> 22 Jun 2026 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#pragma once
#include "RGBController.h"
#include "QMKKeychronController.h"
class RGBController_QMKKeychron : public RGBController
{
public:
RGBController_QMKKeychron(QMKKeychronController* controller_ptr);
~RGBController_QMKKeychron();
void SetupZones();
void ResizeZone(int zone, int new_size);
void DeviceUpdateLEDs();
void UpdateZoneLEDs(int zone);
void UpdateSingleLED(int led);
void DeviceUpdateMode();
void DeviceSaveMode();
private:
QMKKeychronController* controller;
};
@@ -0,0 +1,71 @@
/*---------------------------------------------------------*\
| QMKViaCommands.h |
| |
| List of QMK VIA command values |
| |
| Adam Honse <calcprogrammer1@gmail.com) 21 Jun 2026 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#pragma once
enum
{
QMK_VIA_CMD_GET_PROTOCOL_VERSION = 0x01,
QMK_VIA_CMD_GET_KEYBOARD_VALUE = 0x02,
QMK_VIA_CMD_SET_KEYBOARD_VALUE = 0x03,
QMK_VIA_CMD_VIA_DYNAMIC_KEYMAP_GET_KEYCODE = 0x04,
QMK_VIA_CMD_VIA_DYNAMIC_KEYMAP_SET_KEYCODE = 0x05,
QMK_VIA_CMD_VIA_DYNAMIC_KEYMAP_RESET = 0x06,
QMK_VIA_CMD_CUSTOM_SET_VALUE = 0x07,
QMK_VIA_CMD_CUSTOM_GET_VALUE = 0x08,
QMK_VIA_CMD_CUSTOM_SAVE = 0x09,
QMK_VIA_CMD_EEPROM_RESET = 0x0A,
};
enum
{
QMK_VIA_CUSTOM_CHANNEL = 0,
QMK_VIA_BACKLIGHT_CHANNEL = 1,
QMK_VIA_RGBLIGHT_CHANNEL = 2,
QMK_VIA_RGB_MATRIX_CHANNEL = 3,
QMK_VIA_AUDIO_CHANNEL = 4,
QMK_VIA_LED_MATRIX_CHANNEL = 5,
};
enum
{
QMK_VIA_BACKLIGHT_BRIGHTNESS = 1,
QMK_VIA_BACKLIGHT_EFFECT = 2,
};
enum
{
QMK_VIA_RGBLIGHT_BRIGHTNESS = 1,
QMK_VIA_RGBLIGHT_EFFECT = 2,
QMK_VIA_RGBLIGHT_EFFECT_SPEED = 3,
QMK_VIA_RGBLIGHT_COLOR = 4,
};
enum
{
QMK_VIA_RGB_MATRIX_BRIGHTNESS = 1,
QMK_VIA_RGB_MATRIX_EFFECT = 2,
QMK_VIA_RGB_MATRIX_EFFECT_SPEED = 3,
QMK_VIA_RGB_MATRIX_COLOR = 4,
};
enum
{
QMK_VIA_LED_MATRIX_BRIGHTNESS = 1,
QMK_VIA_LED_MATRIX_EFFECT = 2,
QMK_VIA_LED_MATRIX_EFFECT_SPEED = 3,
};
enum
{
QMK_VIA_AUDIO_ENABLE = 1,
QMK_VIA_AUDIO_CLICKY_ENABLE = 2,
};
@@ -186,7 +186,7 @@ unsigned short QMKVialRGBController::GetLEDCount()
return(number_leds);
}
vialrgb_led_info QMKVialRGBController::GetLEDInfo(unsigned short led_index)
qmk_rgb_matrix_led_info QMKVialRGBController::GetLEDInfo(unsigned short led_index)
{
return(led_info[led_index]);
}
@@ -257,12 +257,12 @@ unsigned short QMKVialRGBController::CmdGetKeycode
return(keycode);
}
vialrgb_led_info QMKVialRGBController::CmdGetLEDInfo
qmk_rgb_matrix_led_info QMKVialRGBController::CmdGetLEDInfo
(
unsigned short led_index
)
{
vialrgb_led_info data;
qmk_rgb_matrix_led_info data;
SendCheckCommand(CMD_LIGHTING_GET_VALUE, VIALRGB_GET_LED_INFO, (unsigned char*)&led_index, sizeof(led_index), (unsigned char*)&data, sizeof(data));
@@ -13,6 +13,7 @@
#include <hidapi.h>
#include "ResourceManager.h"
#include "QMKCommon.h"
#include "RGBController.h"
#define MSG_LEN 32
@@ -98,34 +99,25 @@ enum
VIALRGB_EFFECT_SKIP = 0xFFFF
};
typedef struct
{
unsigned char x;
unsigned char y;
unsigned char flags;
unsigned char row;
unsigned char col;
} vialrgb_led_info;
class QMKVialRGBController
{
public:
QMKVialRGBController(hid_device *dev_handle, const char *path);
~QMKVialRGBController();
std::string GetLocation();
std::string GetName();
std::string GetSerial();
std::string GetVendor();
std::string GetVersion();
std::string GetLocation();
std::string GetName();
std::string GetSerial();
std::string GetVendor();
std::string GetVersion();
bool GetSupported();
bool GetSupported();
unsigned short GetEffect(std::size_t effect_idx);
std::size_t GetEffectCount();
unsigned short GetKeycode(unsigned short led_index);
unsigned short GetLEDCount();
vialrgb_led_info GetLEDInfo(unsigned short led_index);
unsigned short GetEffect(std::size_t effect_idx);
std::size_t GetEffectCount();
unsigned short GetKeycode(unsigned short led_index);
unsigned short GetLEDCount();
qmk_rgb_matrix_led_info GetLEDInfo(unsigned short led_index);
void GetMode
(
@@ -152,22 +144,22 @@ public:
);
private:
hid_device* dev;
unsigned long long keyboard_uid;
std::vector<unsigned short> keycodes;
std::vector<vialrgb_led_info> led_info;
std::string location;
unsigned char maximum_brightness;
std::string name;
unsigned short number_leds;
std::string serial;
bool supported;
std::vector<unsigned short> supported_effects;
std::string vendor;
unsigned short via_protocol_version;
unsigned int vial_protocol_version;
unsigned short vialrgb_protocol_version;
unsigned char vialrgb_flag;
hid_device* dev;
unsigned long long keyboard_uid;
std::vector<unsigned short> keycodes;
std::vector<qmk_rgb_matrix_led_info> led_info;
std::string location;
unsigned char maximum_brightness;
std::string name;
unsigned short number_leds;
std::string serial;
bool supported;
std::vector<unsigned short> supported_effects;
std::string vendor;
unsigned short via_protocol_version;
unsigned int vial_protocol_version;
unsigned short vialrgb_protocol_version;
unsigned char vialrgb_flag;
unsigned short CmdGetKeycode
(
@@ -176,7 +168,7 @@ private:
unsigned char col
);
vialrgb_led_info CmdGetLEDInfo
qmk_rgb_matrix_led_info CmdGetLEDInfo
(
unsigned short led_index
);
@@ -171,7 +171,7 @@ void RGBController_QMKVialRGB::SetupZones()
for(unsigned short led_index = 0; led_index < controller->GetLEDCount(); led_index++)
{
vialrgb_led_info info = controller->GetLEDInfo(led_index);
qmk_rgb_matrix_led_info info = controller->GetLEDInfo(led_index);
if(info.col > max_col)
{
@@ -193,7 +193,7 @@ void RGBController_QMKVialRGB::SetupZones()
for(unsigned short led_index = 0; led_index < controller->GetLEDCount(); led_index++)
{
vialrgb_led_info info = controller->GetLEDInfo(led_index);
qmk_rgb_matrix_led_info info = controller->GetLEDInfo(led_index);
matrix_map[(width * info.row) + info.col] = (unsigned int)led_index;
}
@@ -175,7 +175,8 @@ void RGBController_Razer::SetupZones()
\*---------------------------------------------------------*/
if(new_zone.type == ZONE_TYPE_MATRIX)
{
if(new_zone.name == ZONE_EN_KEYBOARD && device_list[device_index]->layout != NULL)
if(device_list[device_index]->layout != NULL &&
(new_zone.name == ZONE_EN_KEYBOARD || new_zone.name == "Keypad"))
{
/*---------------------------------------------------------*\
| Dynamically generate a keyboard layout |
@@ -212,7 +213,8 @@ void RGBController_Razer::SetupZones()
new_map->width = device_list[device_index]->zones[zone_id]->cols;
new_map->map = new unsigned int[new_map->height * new_map->width];
if(device_list[device_index]->layout->base_size != KEYBOARD_SIZE::KEYBOARD_SIZE_EMPTY)
if(device_list[device_index]->layout->base_size != KEYBOARD_SIZE::KEYBOARD_SIZE_EMPTY ||
!device_list[device_index]->layout->edit_keys.empty())
{
/*---------------------------------------------------------*\
| Minor adjustments to keyboard layout |
@@ -443,6 +443,8 @@ bool RazerController::SupportsWave()
case RAZER_BLACKWIDOW_V4_X_PID:
case RAZER_BLACKWIDOW_V4_TKL_WIRED_PID:
case RAZER_BLACKWIDOW_V4_TKL_WIRELESS_PID:
case RAZER_BLACKWIDOW_V4_LOWPROFILE_TKL_WIRED_PID:
case RAZER_BLACKWIDOW_V4_LOWPROFILE_TKL_WIRELESS_PID:
case RAZER_BLACKWIDOW_X_CHROMA_PID:
case RAZER_BLACKWIDOW_X_CHROMA_TE_PID:
case RAZER_BLADE_2016_PID:
@@ -244,6 +244,8 @@ REGISTER_HID_DETECTOR_IPU("Razer Blackwidow V4 75% (Wired)", Det
REGISTER_HID_DETECTOR_IPU("Razer Blackwidow V4 X", DetectRazerControllers, RAZER_VID, RAZER_BLACKWIDOW_V4_X_PID, 0x02, 0x01, 0x02);
REGISTER_HID_DETECTOR_IPU("Razer Blackwidow V4 TKL (Wired)", DetectRazerControllers, RAZER_VID, RAZER_BLACKWIDOW_V4_TKL_WIRED_PID, 0x03, 0x0C, 0x01);
REGISTER_HID_DETECTOR_IPU("Razer Blackwidow V4 TKL (Wireless)", DetectRazerControllers, RAZER_VID, RAZER_BLACKWIDOW_V4_TKL_WIRELESS_PID, 0x02, 0x01, 0x02);
REGISTER_HID_DETECTOR_IPU("Razer Blackwidow V4 Low Profile TKL (Wired)", DetectRazerControllers, RAZER_VID, RAZER_BLACKWIDOW_V4_LOWPROFILE_TKL_WIRED_PID, 0x03, 0x0C, 0x01);
REGISTER_HID_DETECTOR_IPU("Razer Blackwidow V4 Low Profile TKL (Wireless)", DetectRazerControllers, RAZER_VID, RAZER_BLACKWIDOW_V4_LOWPROFILE_TKL_WIRELESS_PID, 0x02, 0x01, 0x02);
REGISTER_HID_DETECTOR_IPU("Razer Blackwidow X Chroma", DetectRazerControllers, RAZER_VID, RAZER_BLACKWIDOW_X_CHROMA_PID, 0x02, 0x01, 0x02);
REGISTER_HID_DETECTOR_IPU("Razer Blackwidow X Chroma Tournament Edition", DetectRazerControllers, RAZER_VID, RAZER_BLACKWIDOW_X_CHROMA_TE_PID, 0x02, 0x01, 0x02);
REGISTER_HID_DETECTOR_IPU("Razer Cynosa Chroma", DetectRazerControllers, RAZER_VID, RAZER_CYNOSA_CHROMA_PID, 0x02, 0x01, 0x02);
+98 -2
View File
@@ -77,6 +77,30 @@ keyboard_keymap_overlay_values razer_blackwidow_2019_layout
}
};
keyboard_keymap_overlay_values razer_blackwidow_x_chroma_layout
{
KEYBOARD_SIZE::KEYBOARD_SIZE_FULL,
{
{ /* ANSI Value set not used */ },
{
/* Add more regional layout fixes here */
}
},
{
/*-------------------------------------------------------------------------------------------------------------------------------------*\
| Edit Keys |
| Zone, Row, Column, Value, Name, Alternate Name, OpCode |
\*-------------------------------------------------------------------------------------------------------------------------------------*/
{ 0, 0, 0, 0, KEY_EN_UNUSED, KEY_EN_UNUSED, KEYBOARD_OPCODE_INSERT_SHIFT_RIGHT, }, // Move 'Esc' 1 right (Shifts row)
{ 0, 1, 0, 0, KEY_EN_UNUSED, KEY_EN_UNUSED, KEYBOARD_OPCODE_INSERT_SHIFT_RIGHT, }, // Move Backtick 1 right (Shifts row)
{ 0, 2, 0, 0, KEY_EN_UNUSED, KEY_EN_UNUSED, KEYBOARD_OPCODE_INSERT_SHIFT_RIGHT, }, // Move Tab 1 right (Shifts row)
{ 0, 3, 0, 0, KEY_EN_UNUSED, KEY_EN_UNUSED, KEYBOARD_OPCODE_INSERT_SHIFT_RIGHT, }, // Move Caps 1 right (Shifts row)
{ 0, 4, 0, 0, KEY_EN_UNUSED, KEY_EN_UNUSED, KEYBOARD_OPCODE_INSERT_SHIFT_RIGHT, }, // Move LFT_SHFT 1 right (Shifts row)
{ 0, 5, 0, 0, KEY_EN_UNUSED, KEY_EN_UNUSED, KEYBOARD_OPCODE_INSERT_SHIFT_RIGHT, }, // Move LFT_CTRL 1 right (Shifts row)
{ 0, 0, 20, 0, "Logo", KEY_EN_UNUSED, KEYBOARD_OPCODE_SWAP_ONLY, }, // Insert 'Logo' key
}
};
keyboard_keymap_overlay_values razer_blackwidow_chroma_layout
{
KEYBOARD_SIZE::KEYBOARD_SIZE_FULL,
@@ -2056,6 +2080,76 @@ static const razer_device blackwidow_v4_tkl_wireless_device =
&razer_blackwidow_v4_tkl_layout
};
/*-------------------------------------------------------------*\
| Razer Blackwidow V4 Low Profile TKL (Wired) 1532:02D4 |
| |
| Zone "Keyboard" |
| Matrix |
| 6 Rows, 18 Columns |
\*-------------------------------------------------------------*/
static const razer_zone blackwidow_v4_lowprofile_tkl_wired_zone =
{
ZONE_EN_KEYBOARD,
ZONE_TYPE_MATRIX,
6,
18
};
static const razer_device blackwidow_v4_lowprofile_tkl_wired_device =
{
"Razer Blackwidow V4 Low Profile TKL (Wired)",
RAZER_BLACKWIDOW_V4_LOWPROFILE_TKL_WIRED_PID,
DEVICE_TYPE_KEYBOARD,
RAZER_MATRIX_TYPE_EXTENDED,
0x1F,
6,
18,
{
&blackwidow_v4_tkl_wired_zone,
NULL,
NULL,
NULL,
NULL,
NULL
},
&razer_blackwidow_v4_tkl_layout //same layout as v4 (just low profile)
};
/*-------------------------------------------------------------*\
| Razer Blackwidow V4 Low Profile TKL (Wireless) 1532:02D2 |
| |
| Zone "Keyboard" |
| Matrix |
| 6 Rows, 18 Columns |
\*-------------------------------------------------------------*/
static const razer_zone blackwidow_v4_lowprofile_tkl_wireless_zone =
{
ZONE_EN_KEYBOARD,
ZONE_TYPE_MATRIX,
6,
18
};
static const razer_device blackwidow_v4_lowprofile_tkl_wireless_device =
{
"Razer Blackwidow V4 Low Profile TKL (Wireless)",
RAZER_BLACKWIDOW_V4_LOWPROFILE_TKL_WIRELESS_PID,
DEVICE_TYPE_KEYBOARD,
RAZER_MATRIX_TYPE_EXTENDED,
0x9F,
6,
18,
{
&blackwidow_v4_lowprofile_tkl_wireless_zone,
NULL,
NULL,
NULL,
NULL,
NULL
},
&razer_blackwidow_v4_tkl_layout //same layout as v4 (just low profile)
};
/*-------------------------------------------------------------*\
| Razer Blackwidow X Chroma 1532:0216 |
| |
@@ -2088,7 +2182,7 @@ static const razer_device blackwidow_x_chroma_device =
NULL,
NULL
},
NULL
&razer_blackwidow_x_chroma_layout
};
/*-------------------------------------------------------------*\
@@ -7429,7 +7523,7 @@ static const razer_device tartarus_pro_device =
};
/*-------------------------------------------------------------*\
| Razer Tartarus V2 1532:0208 |
| Razer Tartarus V2 1532:022B |
| |
| Zone "Keypad" |
| Matrix |
@@ -9329,6 +9423,8 @@ const razer_device* razer_device_list[] =
&blackwidow_v4_x_device,
&blackwidow_v4_tkl_wired_device,
&blackwidow_v4_tkl_wireless_device,
&blackwidow_v4_lowprofile_tkl_wired_device,
&blackwidow_v4_lowprofile_tkl_wireless_device,
&blackwidow_x_chroma_device,
&blackwidow_x_chroma_te_device,
&cynosa_chroma_device,
@@ -59,6 +59,8 @@
#define RAZER_BLACKWIDOW_V4_75_WIRED_PID 0x02A5
#define RAZER_BLACKWIDOW_V4_TKL_WIRED_PID 0x02D7
#define RAZER_BLACKWIDOW_V4_TKL_WIRELESS_PID 0x02D5
#define RAZER_BLACKWIDOW_V4_LOWPROFILE_TKL_WIRED_PID 0x02D4
#define RAZER_BLACKWIDOW_V4_LOWPROFILE_TKL_WIRELESS_PID 0x02D2
#define RAZER_BLACKWIDOW_X_CHROMA_PID 0x0216
#define RAZER_BLACKWIDOW_X_CHROMA_TE_PID 0x021A
#define RAZER_BLADE_2016_PID 0x020F
@@ -0,0 +1,523 @@
/*---------------------------------------------------------*\
| RGBController_RealtekARGB.cpp |
| |
| RGBController for Realtek USB ARGB ICs |
| |
| Jerry Fan (JerryFan0612) 13 Mar 2026 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include "RGBController_RealtekARGB.h"
/**------------------------------------------------------------------*\
@name Realtek ARGB Device
@category LEDStrip
@type USB
@save :x:
@direct :rotating_light:
@effects :white_check_mark:
@detectors RealtekARGBControllerDetect
@comment
\*-------------------------------------------------------------------*/
RGBController_RealtekARGB::RGBController_RealtekARGB(RealtekARGBController* controller_ptr)
{
controller = controller_ptr;
name = controller_ptr->get_dev_name();
vendor = controller_ptr->get_manu_name();
location = controller_ptr->get_dev_loc();
serial = controller_ptr->get_sn();
version = controller_ptr->get_fw_ver();
description = vendor + "ARGB Device";
type = DEVICE_TYPE_LEDSTRIP;
std::fill(std::begin(ready_to_reboot), std::end(ready_to_reboot), false);
SetupModes();
SetupZones();
}
void RGBController_RealtekARGB::SetupModes()
{
int brightness = controller->get_argb_brightness(0) >> 8;
mode Direct;
Direct.name = "Direct";
Direct.value = REALTEK_ARGB_EFF_NULL;
Direct.flags = MODE_FLAG_HAS_PER_LED_COLOR | MODE_FLAG_HAS_BRIGHTNESS;
Direct.color_mode = MODE_COLORS_PER_LED;
Direct.brightness_min = 0;
Direct.brightness_max = 255;
Direct.brightness = brightness;
modes.push_back(Direct);
mode Static;
Static.name = "Static";
Static.value = REALTEK_ARGB_EFF_ALWAYS_ON;
Static.flags = MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS;
Static.colors_min = 1;
Static.colors_max = 1;
Static.color_mode = MODE_COLORS_MODE_SPECIFIC;
Static.colors.resize(1);
Static.brightness_min = 0;
Static.brightness_max = 255;
Static.brightness = brightness;
modes.push_back(Static);
mode Blink;
Blink.name = "Blink";
Blink.value = REALTEK_ARGB_EFF_BLINK;
Blink.flags = MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED;
Blink.speed_min = REALTEK_ARGB_SPEED_MIN;
Blink.speed_max = REALTEK_ARGB_SPEED_MAX;
Blink.speed = REALTEK_ARGB_SPEED_NORMAL;
Blink.colors_min = 1;
Blink.colors_max = 2;
Blink.color_mode = MODE_COLORS_MODE_SPECIFIC;
Blink.colors.resize(2);
Blink.brightness_min = 0;
Blink.brightness_max = 255;
Blink.brightness = brightness;
modes.push_back(Blink);
mode Breathing;
Breathing.name = "Breathing";
Breathing.value = REALTEK_ARGB_EFF_BREATH;
Breathing.flags = MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED;
Breathing.speed_min = REALTEK_ARGB_SPEED_MIN;
Breathing.speed_max = REALTEK_ARGB_SPEED_MAX;
Breathing.speed = REALTEK_ARGB_SPEED_NORMAL;
Breathing.colors_min = 1;
Breathing.colors_max = 2;
Breathing.color_mode = MODE_COLORS_MODE_SPECIFIC;
Breathing.colors.resize(2);
Breathing.brightness_min = 0;
Breathing.brightness_max = 255;
Breathing.brightness = brightness;
modes.push_back(Breathing);
mode Spectrum;
Spectrum.name = "Spectrum";
Spectrum.value = REALTEK_ARGB_EFF_SPECTRUM;
Spectrum.flags = MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED;
Spectrum.speed_min = REALTEK_ARGB_SPEED_MIN;
Spectrum.speed_max = REALTEK_ARGB_SPEED_MAX;
Spectrum.speed = REALTEK_ARGB_SPEED_NORMAL;
Spectrum.color_mode = MODE_COLORS_NONE;
Spectrum.brightness_min = 0;
Spectrum.brightness_max = 255;
Spectrum.brightness = brightness;
modes.push_back(Spectrum);
mode Scroll;
Scroll.name = "Scroll";
Scroll.value = REALTEK_ARGB_EFF_SCROLL;
Scroll.flags = MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED;
Scroll.speed_min = REALTEK_ARGB_SPEED_MIN;
Scroll.speed_max = REALTEK_ARGB_SPEED_MAX;
Scroll.speed = REALTEK_ARGB_SPEED_NORMAL;
Scroll.colors_min = 1;
Scroll.colors_max = 2;
Scroll.color_mode = MODE_COLORS_MODE_SPECIFIC;
Scroll.colors.resize(2);
Scroll.brightness_min = 0;
Scroll.brightness_max = 255;
Scroll.brightness = brightness;
modes.push_back(Scroll);
mode RainbowScroll;
RainbowScroll.name = "Rainbow Scroll";
RainbowScroll.value = REALTEK_ARGB_EFF_RAINBOW_SCROLL;
RainbowScroll.flags = MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED;
RainbowScroll.speed_min = REALTEK_ARGB_SPEED_MIN;
RainbowScroll.speed_max = REALTEK_ARGB_SPEED_MAX;
RainbowScroll.speed = REALTEK_ARGB_SPEED_NORMAL;
RainbowScroll.color_mode = MODE_COLORS_NONE;
RainbowScroll.brightness_min = 0;
RainbowScroll.brightness_max = 255;
RainbowScroll.brightness = brightness;
modes.push_back(RainbowScroll);
mode RunningWater;
RunningWater.name = "Running Water";
RunningWater.value = REALTEK_ARGB_EFF_RUNNING_WATER;
RunningWater.flags = MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED;
RunningWater.speed_min = REALTEK_ARGB_SPEED_MIN;
RunningWater.speed_max = REALTEK_ARGB_SPEED_MAX;
RunningWater.speed = REALTEK_ARGB_SPEED_NORMAL;
RunningWater.colors_min = 1;
RunningWater.colors_max = 2;
RunningWater.color_mode = MODE_COLORS_MODE_SPECIFIC;
RunningWater.colors.resize(2);
RunningWater.brightness_min = 0;
RunningWater.brightness_max = 255;
RunningWater.brightness = brightness;
modes.push_back(RunningWater);
mode Sliding;
Sliding.name = "Sliding";
Sliding.value = REALTEK_ARGB_EFF_SLIDING;
Sliding.flags = MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED | MODE_FLAG_HAS_DIRECTION_LR;
Sliding.speed_min = REALTEK_ARGB_SPEED_MIN;
Sliding.speed_max = REALTEK_ARGB_SPEED_MAX;
Sliding.speed = REALTEK_ARGB_SPEED_NORMAL;
Sliding.colors_min = 1;
Sliding.colors_max = 2;
Sliding.color_mode = MODE_COLORS_MODE_SPECIFIC;
Sliding.colors.resize(2);
Sliding.brightness_min = 0;
Sliding.brightness_max = 255;
Sliding.brightness = brightness;
modes.push_back(Sliding);
mode WideSliding;
WideSliding.name = "Wide Sliding";
WideSliding.value = REALTEK_ARGB_EFF_WIDE_SLIDING;
WideSliding.flags = MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED | MODE_FLAG_HAS_DIRECTION_LR;
WideSliding.speed_min = REALTEK_ARGB_SPEED_MIN;
WideSliding.speed_max = REALTEK_ARGB_SPEED_MAX;
WideSliding.speed = REALTEK_ARGB_SPEED_NORMAL;
WideSliding.colors_min = 1;
WideSliding.colors_max = 2;
WideSliding.color_mode = MODE_COLORS_MODE_SPECIFIC;
WideSliding.colors.resize(2);
WideSliding.brightness_min = 0;
WideSliding.brightness_max = 255;
WideSliding.brightness = brightness;
modes.push_back(WideSliding);
mode RainbowSliding;
RainbowSliding.name = "Rainbow Sliding";
RainbowSliding.value = REALTEK_ARGB_EFF_RAINBOW_SLIDING;
RainbowSliding.flags = MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED | MODE_FLAG_HAS_DIRECTION_LR;
RainbowSliding.speed_min = REALTEK_ARGB_SPEED_MIN;
RainbowSliding.speed_max = REALTEK_ARGB_SPEED_MAX;
RainbowSliding.speed = REALTEK_ARGB_SPEED_NORMAL;
RainbowSliding.color_mode = MODE_COLORS_NONE;
RainbowSliding.brightness_min = 0;
RainbowSliding.brightness_max = 255;
RainbowSliding.brightness = brightness;
modes.push_back(RainbowSliding);
mode RainbowFadeSliding;
RainbowFadeSliding.name = "Rainbow Fade Sliding";
RainbowFadeSliding.value = REALTEK_ARGB_EFF_RAINBOW_FADE_SLIDING;
RainbowFadeSliding.flags = MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED;
RainbowFadeSliding.speed_min = REALTEK_ARGB_SPEED_MIN;
RainbowFadeSliding.speed_max = REALTEK_ARGB_SPEED_MAX;
RainbowFadeSliding.speed = REALTEK_ARGB_SPEED_NORMAL;
RainbowFadeSliding.color_mode = MODE_COLORS_NONE;
RainbowFadeSliding.brightness_min = 0;
RainbowFadeSliding.brightness_max = 255;
RainbowFadeSliding.brightness = brightness;
modes.push_back(RainbowFadeSliding);
mode NewtonCradle;
NewtonCradle.name = "Newton Cradle";
NewtonCradle.value = REALTEK_ARGB_EFF_NEWTON_CRADLE;
NewtonCradle.flags = MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED;
NewtonCradle.speed_min = REALTEK_ARGB_SPEED_MIN;
NewtonCradle.speed_max = REALTEK_ARGB_SPEED_MAX;
NewtonCradle.speed = REALTEK_ARGB_SPEED_NORMAL;
NewtonCradle.colors_min = 1;
NewtonCradle.colors_max = 2;
NewtonCradle.color_mode = MODE_COLORS_MODE_SPECIFIC;
NewtonCradle.colors.resize(2);
NewtonCradle.brightness_min = 0;
NewtonCradle.brightness_max = 255;
NewtonCradle.brightness = brightness;
modes.push_back(NewtonCradle);
mode Meteor;
Meteor.name = "Meteor";
Meteor.value = REALTEK_ARGB_EFF_METEOR;
Meteor.flags = MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED;
Meteor.speed_min = REALTEK_ARGB_SPEED_MIN;
Meteor.speed_max = REALTEK_ARGB_SPEED_MAX;
Meteor.speed = REALTEK_ARGB_SPEED_NORMAL;
Meteor.colors_min = 1;
Meteor.colors_max = 2;
Meteor.color_mode = MODE_COLORS_MODE_SPECIFIC;
Meteor.colors.resize(2);
Meteor.brightness_min = 0;
Meteor.brightness_max = 255;
Meteor.brightness = brightness;
modes.push_back(Meteor);
mode ZigZag;
ZigZag.name = "ZigZag";
ZigZag.value = REALTEK_ARGB_EFF_ZIGZAG;
ZigZag.flags = MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED | MODE_FLAG_HAS_RANDOM_COLOR;
ZigZag.speed_min = REALTEK_ARGB_SPEED_MIN;
ZigZag.speed_max = REALTEK_ARGB_SPEED_MAX;
ZigZag.speed = REALTEK_ARGB_SPEED_NORMAL;
ZigZag.colors_min = 1;
ZigZag.colors_max = 2;
ZigZag.color_mode = MODE_COLORS_MODE_SPECIFIC;
ZigZag.colors.resize(2);
ZigZag.brightness_min = 0;
ZigZag.brightness_max = 255;
ZigZag.brightness = brightness;
modes.push_back(ZigZag);
mode StarryNight;
StarryNight.name = "Starry Night";
StarryNight.value = REALTEK_ARGB_EFF_STARRY_NIGHT;
StarryNight.flags = MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED | MODE_FLAG_HAS_RANDOM_COLOR;
StarryNight.speed_min = REALTEK_ARGB_SPEED_MIN;
StarryNight.speed_max = REALTEK_ARGB_SPEED_MAX;
StarryNight.speed = REALTEK_ARGB_SPEED_NORMAL;
StarryNight.colors_min = 1;
StarryNight.colors_max = 1;
StarryNight.color_mode = MODE_COLORS_MODE_SPECIFIC;
StarryNight.colors.resize(1);
StarryNight.brightness_min = 0;
StarryNight.brightness_max = 255;
StarryNight.brightness = brightness;
modes.push_back(StarryNight);
mode Stack;
Stack.name = "Stack";
Stack.value = REALTEK_ARGB_EFF_STACK;
Stack.flags = MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS;
Stack.colors_min = 1;
Stack.colors_max = 2;
Stack.color_mode = MODE_COLORS_MODE_SPECIFIC;
Stack.colors.resize(2);
Stack.brightness_min = 0;
Stack.brightness_max = 255;
Stack.brightness = brightness;
modes.push_back(Stack);
}
RGBController_RealtekARGB::~RGBController_RealtekARGB()
{
delete controller;
}
void RGBController_RealtekARGB::SetupZones()
{
/*-------------------------------------------------*\
| Only set LED count on the first run |
\*-------------------------------------------------*/
bool first_run = false;
int idx = 0;
int argb_num = 0;
int fix_grps = 0;
int num_fixgrp = 0;
int argb_num_fixgrp = 0;
if(zones.size() == 0)
{
first_run = true;
}
/*-------------------------------------------------*\
| Clear any existing color/LED configuration |
\*-------------------------------------------------*/
leds.clear();
colors.clear();
valid_grp.clear();
fix_grps = controller->get_fix_grps();
for(int grp_num = 0; grp_num < REALTEK_ARGB_NUM_ARGB_GRP; grp_num++)
{
if(controller->get_zone_enable(grp_num))
{
valid_grp.push_back(grp_num);
if(fix_grps & (0x1 << grp_num))
{
num_fixgrp++;
argb_num_fixgrp += controller->get_argb_num(grp_num);
}
}
}
/*-------------------------------------------------*\
| Set zones and leds |
\*-------------------------------------------------*/
zones.resize(valid_grp.size());
for(int grp_num : valid_grp)
{
argb_num = controller->get_argb_num(grp_num);
zones[idx].name = "strip " + std::to_string(idx + 1);
zones[idx].type = ZONE_TYPE_LINEAR;
if(fix_grps & (0x1 << grp_num))
{
zones[idx].leds_count = argb_num;
zones[idx].leds_min = argb_num;
zones[idx].leds_max = argb_num;
}
else
{
if(first_run)
{
zones[idx].leds_count = 0;
}
else
{
zones[idx].leds_count = argb_num;
}
zones[idx].leds_min = 0;
zones[idx].leds_max = (REALTEK_ARGB_MAX - argb_num_fixgrp) / ((int)valid_grp.size() - num_fixgrp);
}
zones[idx].matrix_map = NULL;
for(unsigned int led_idx = 0; led_idx < zones[idx].leds_count; led_idx++)
{
led myled;
myled.name = zones[idx].name + " led_";
myled.name.append(std::to_string(led_idx + 1));
leds.push_back(myled);
}
idx++;
}
SetupColors();
}
void RGBController_RealtekARGB::ResizeZone(int zone, int new_size)
{
int fix_grps = controller->get_fix_grps();
int orig_num = controller->get_argb_num(valid_grp[zone]);
bool need_reboot = true;
if((size_t) zone >= zones.size())
{
return;
}
if((new_size == orig_num) && zones[zone].leds_count)
{
return;
}
if(((unsigned int)new_size >= zones[zone].leds_min) && ((unsigned int)new_size <= zones[zone].leds_max))
{
int total_argb_num = 0;
std::vector<RGBColor> rtk_colors(orig_num, 0);
for(int grp_num = 0; grp_num < REALTEK_ARGB_NUM_ARGB_GRP; grp_num++)
{
if(grp_num == valid_grp[zone])
{
total_argb_num += new_size;
}
else
{
total_argb_num += controller->get_argb_num(grp_num);
}
}
if(total_argb_num > REALTEK_ARGB_MAX)
{
return;
}
controller->set_argb_direct(valid_grp[zone], rtk_colors, 0xFF);
controller->set_argb_num(valid_grp[zone], new_size);
ready_to_reboot[valid_grp[zone]] = true;
for(int grp_num = 0; grp_num < REALTEK_ARGB_NUM_ARGB_GRP; grp_num++)
{
if(!ready_to_reboot[grp_num] && !(fix_grps & (0x1 << grp_num)))
{
need_reboot = false;
break;
}
}
if(need_reboot)
{
controller->device_reboot();
controller->device_rescan_trigger();
}
SetupZones();
}
}
void RGBController_RealtekARGB::DeviceUpdateLEDs()
{
for(std::size_t zone_idx = 0; zone_idx < zones.size(); zone_idx++)
{
if(zones[zone_idx].leds_count > 0)
{
UpdateZoneLEDs((int)zone_idx);
}
}
}
void RGBController_RealtekARGB::UpdateZoneLEDs(int zone)
{
unsigned short brightness = 0xFF;
mode& curr_mode = modes[active_mode];
std::vector<RGBColor> color_buf;
if(curr_mode.color_mode == MODE_COLORS_PER_LED &&
curr_mode.value == REALTEK_ARGB_EFF_NULL) //direct mode
{
if(curr_mode.flags & MODE_FLAG_HAS_BRIGHTNESS)
{
brightness = curr_mode.brightness;
}
color_buf.resize(zones[zone].leds_count);
for(unsigned int i = 0; i < zones[zone].leds_count; i++)
color_buf[i] = zones[zone].colors[i];
controller->set_argb_direct(valid_grp[zone], color_buf, brightness);
}
else
{
UpdateSingleLED(zone);
}
}
void RGBController_RealtekARGB::UpdateSingleLED(int zone)
{
mode& curr_mode = modes[active_mode];
std::vector<RGBColor> rtk_colors = curr_mode.colors;
struct RealtekARGBControllerSetEffParam param;
param.speed = REALTEK_ARGB_SPEED_NORMAL;
param.brightness = 0xFF;
param.dir = 0;
param.random_color = 0;
if(curr_mode.flags & MODE_FLAG_HAS_SPEED)
{
param.speed = curr_mode.speed;
}
if(curr_mode.flags & MODE_FLAG_HAS_BRIGHTNESS)
{
param.brightness = curr_mode.brightness;
}
if(curr_mode.flags & MODE_FLAG_HAS_DIRECTION_LR)
{
if(curr_mode.direction == MODE_DIRECTION_RIGHT)
{
param.dir = 1;
}
}
if(curr_mode.flags & MODE_FLAG_HAS_RANDOM_COLOR)
{
if(curr_mode.color_mode == MODE_COLORS_RANDOM)
{
param.random_color = 1;
}
}
if(curr_mode.color_mode == MODE_COLORS_PER_LED)
{
rtk_colors = colors;
}
else if(curr_mode.color_mode == MODE_COLORS_NONE)
{
rtk_colors.clear();
}
controller->set_argb_effect(valid_grp[zone], curr_mode.value, rtk_colors, &param);
}
void RGBController_RealtekARGB::DeviceUpdateMode()
{
if(modes[active_mode].value != REALTEK_ARGB_EFF_NULL)
{
DeviceUpdateLEDs();
}
}
@@ -0,0 +1,36 @@
/*---------------------------------------------------------*\
| RGBController_RealtekARGB.h |
| |
| RGBController for Realtek USB ARGB ICs |
| |
| Jerry Fan (JerryFan0612) 13 Mar 2026 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#pragma once
#include "RGBController.h"
#include "RealtekARGBController.h"
class RGBController_RealtekARGB : public RGBController
{
public:
RGBController_RealtekARGB(RealtekARGBController* controller_ptr);
~RGBController_RealtekARGB();
void SetupModes();
void SetupZones();
void ResizeZone(int zone, int new_size);
void DeviceUpdateLEDs();
void UpdateZoneLEDs(int zone);
void UpdateSingleLED(int zone);
void DeviceUpdateMode();
private:
RealtekARGBController* controller;
std::vector<int> valid_grp;
bool ready_to_reboot[REALTEK_ARGB_NUM_ARGB_GRP] = {false};
};
@@ -0,0 +1,715 @@
/*---------------------------------------------------------*\
| RealtekARGBController.cpp |
| |
| Controller for Realtek USB ARGB ICs |
| |
| Jerry Fan (JerryFan0612) 13 Mar 2026 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include "RealtekARGBController.h"
#include "MathUtils.h"
#include "StringUtils.h"
using namespace std::chrono_literals;
static const unsigned char hid_set_packet[] =
{
0x56, 0x53, 0x42, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0xE3,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static const unsigned char hid_get_packet[] =
{
0x56, 0x53, 0x42, 0x43, 0x78, 0x56, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x10, 0xE2,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static const unsigned char hid_end_packet[] =
{
0x58, 0x53, 0x42, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
RealtekARGBController::RealtekARGBController(hid_device* dev, hid_device_info* info)
{
hdev = dev;
hidinfo = info;
for(int i = 0; i < REALTEK_ARGB_NUM_ARGB_GRP; i++)
{
argbctl_data[i] = (unsigned char*)calloc(REALTEK_ARGB_CTL_DATA_SIZE, 1);
memset(argbctl_data[i], 0, REALTEK_ARGB_CTL_DATA_SIZE);
prev_bright[i] = 0xFFFF;
}
device_init();
keepalive_thread_run = false;
keepalive_thread = NULL;
}
RealtekARGBController::~RealtekARGBController()
{
/*-----------------------------------------------------*\
| Close keepalive thread |
\*-----------------------------------------------------*/
if(keepalive_thread != NULL)
{
keepalive_thread_run = false;
keepalive_thread->join();
delete keepalive_thread;
}
if(hdev)
{
for(int i = 0; i < REALTEK_ARGB_NUM_ARGB_GRP; i++)
{
int ret = set_appctl(i, REALTEK_ARGB_LED_CTL_FW);
if(!ret)
{
appctl[i] = REALTEK_ARGB_LED_CTL_FW;
}
}
hid_close(hdev);
hdev = NULL;
}
memset(argbctl_hdr, 0, REALTEK_ARGB_CTL_HDR_SIZE);
for(int i = 0; i < REALTEK_ARGB_NUM_ARGB_GRP; i++)
free(argbctl_data[i]);
}
void RealtekARGBController::KeepaliveThreadFunction()
{
/*-----------------------------------------------------------------*\
| One shot thread to rescan device |
\*-----------------------------------------------------------------*/
while(keepalive_thread_run.load())
{
std::this_thread::sleep_for(2s);
device_rescan();
keepalive_thread_run = false;
}
}
int RealtekARGBController::usb_hid_ioctl(unsigned char* usb_buf, unsigned char* data, int data_len,
unsigned int offset, unsigned char is_in)
{
int id;
int ret = 0;
int buf_len = usb_hid_get_report(data_len, &id);
if(hdev == NULL)
{
free(usb_buf);
return -1;
}
if(!is_in && data_len)
{
usb_buf[0x04] = data[0];
}
memcpy(&usb_buf[0x08], &data_len, sizeof(data_len));
memcpy(&usb_buf[0x1B], &data_len, sizeof(data_len));
ret = hid_send_feature_report(hdev, usb_buf, sizeof(hid_get_packet));
memset(usb_buf, 0x00, buf_len);
memcpy(usb_buf, data, data_len);
usb_buf[0x00] = id;
if(is_in)
{
ret = hid_get_feature_report(hdev, usb_buf, buf_len);
memcpy(data, usb_buf + offset, data_len);
}
else
{
ret = hid_send_feature_report(hdev, usb_buf, buf_len);
}
memset(usb_buf, 0x00, buf_len);
memcpy(usb_buf, hid_end_packet, sizeof(hid_end_packet));
ret = hid_get_feature_report(hdev, usb_buf, sizeof(hid_end_packet));
free(usb_buf);
return (ret > 0) ? 0 : ret;
}
int RealtekARGBController::usb_hid_get_report(int data_len, int* id)
{
int retlen = 0;
if(data_len <= REALTEK_ARGB_HID_DATALEN_CH2)
{
*id = REALTEK_ARGB_HID_ID_DATA_CH2;
retlen = REALTEK_ARGB_HID_DATALEN_CH2;
}
else if(data_len <= REALTEK_ARGB_HID_DATALEN_CH3)
{
*id = REALTEK_ARGB_HID_ID_DATA_CH3;
retlen = REALTEK_ARGB_HID_DATALEN_CH3;
}
else if(data_len <= REALTEK_ARGB_HID_DATALEN_CH4)
{
*id = REALTEK_ARGB_HID_ID_DATA_CH4;
retlen = REALTEK_ARGB_HID_DATALEN_CH4;
}
else if(data_len <= REALTEK_ARGB_HID_DATALEN_CH5)
{
*id = REALTEK_ARGB_HID_ID_DATA_CH5;
retlen = REALTEK_ARGB_HID_DATALEN_CH5;
}
else
{
*id = REALTEK_ARGB_HID_ID_DATA_CH1;
retlen = REALTEK_ARGB_HID_DATALEN_CH1;
}
return retlen;
}
void RealtekARGBController::device_init()
{
set_write_unlock();
get_argbctl_hdr();
get_argbctl_data();
for(int i = 0; i < REALTEK_ARGB_NUM_ARGB_GRP; i++)
{
int ret = set_appctl(i, REALTEK_ARGB_LED_CTL_FW);
if(!ret)
{
appctl[i] = REALTEK_ARGB_LED_CTL_FW;
}
}
}
int RealtekARGBController::set_write_unlock()
{
int ret = 0;
int buf_len = REALTEK_ARGB_HID_DATALEN_CH1;
int data_len = 96;
unsigned int addr = 0xAC004000;
unsigned char* data = (unsigned char*)calloc(data_len, 1);
unsigned char* usb_buf = (unsigned char*)calloc(buf_len, 1);// will release in usb_hid_ioctl function
memset(usb_buf, 0x00, buf_len);
memcpy(usb_buf, hid_get_packet, sizeof(hid_get_packet));
usb_buf[0x13] = 0x92;
memcpy(&usb_buf[0x17], &addr, sizeof(addr));
ret = usb_hid_ioctl(usb_buf, data, data_len, 0, true);
free(data);
return ret;
}
unsigned char RealtekARGBController::get_support_openrgb()
{
int buf_len = REALTEK_ARGB_HID_DATALEN_CH1;
unsigned char is_support = 0;
unsigned char* usb_buf = (unsigned char*)calloc(buf_len, 1);// will release in usb_hid_ioctl function
memset(usb_buf, 0x00, buf_len);
memcpy(usb_buf, hid_get_packet, sizeof(hid_get_packet));
usb_buf[0x13] = 0xCC;
usb_buf[0x14] = 0x04;
usb_buf[0x15] = REALTEK_ARGB_SYNC_METHOD_OPENRGB;
if(usb_hid_ioctl(usb_buf, &is_support, sizeof(is_support), 0, true))
{
is_support = 0;
}
return is_support;
}
std::string RealtekARGBController::get_manu_name()
{
return StringUtils::wchar_to_char(hidinfo->manufacturer_string);
}
std::string RealtekARGBController::get_product_name()
{
return StringUtils::wchar_to_char(hidinfo->product_string);
}
std::string RealtekARGBController::get_sn()
{
return StringUtils::wchar_to_char(hidinfo->serial_number);
}
std::string RealtekARGBController::get_dev_loc()
{
return hidinfo->path;
}
std::string RealtekARGBController::get_fw_ver()
{
std::string ver = "";
struct RealtekARGBControllerFWVersion fw_ver;
int buf_len = REALTEK_ARGB_HID_DATALEN_CH1;
unsigned char* usb_buf = (unsigned char*)calloc(buf_len, 1);// will release in usb_hid_ioctl function
memset(usb_buf, 0x00, buf_len);
memcpy(usb_buf, hid_get_packet, sizeof(hid_get_packet));
usb_buf[0x13] = 0xA5;
if(!usb_hid_ioctl(usb_buf, (unsigned char*)&fw_ver, sizeof(fw_ver), 0, true))
{
ver += std::to_string(fw_ver.fw_major_ver) + "." +
std::to_string(fw_ver.fw_minor_ver) + "." +
std::to_string(fw_ver.fw_extra_ver) + "." +
std::to_string(fw_ver.fw_build_date);
}
return ver;
}
std::string RealtekARGBController::get_ic_uuid()
{
unsigned int uuid = 0;
int buf_len = REALTEK_ARGB_HID_DATALEN_CH1;
unsigned char* usb_buf = (unsigned char*)calloc(buf_len, 1);// will release in usb_hid_ioctl function
memset(usb_buf, 0x00, buf_len);
memcpy(usb_buf, hid_get_packet, sizeof(hid_get_packet));
usb_buf[0x13] = 0xC3;
usb_hid_ioctl(usb_buf, (unsigned char*)&uuid, sizeof(uuid), 0, true);
return StringUtils::u32int_to_hexString(uuid);
}
std::string RealtekARGBController::get_dev_name()
{
bool got_custled = false;
std::string devname = get_product_name();
if(custled[0])
{
got_custled = true;
}
else
{
if(!get_custled(custled, sizeof(custled)))
{
got_custled = true;
}
}
if(got_custled)
{
if(custled[6] == REALTEK_ARGB_CUST_DEVNAME_MANU_UUID)
{
devname = get_manu_name() + get_ic_uuid();
}
}
return devname;
}
int RealtekARGBController::get_fix_grps()
{
bool got_custled = false;
static int fix_grps = 0;
if(custled[0])
{
got_custled = true;
}
else
{
if(!get_custled(custled, sizeof(custled)))
{
got_custled = true;
}
}
if(got_custled)
{
fix_grps = custled[4];
}
return fix_grps;
}
bool RealtekARGBController::get_zone_enable(int grp_num)
{
bool got_custled = false;
bool en = false;
if(custled[0])
{
got_custled = true;
}
else
{
if(!get_custled(custled, sizeof(custled)))
{
got_custled = true;
}
}
if(got_custled)
{
en = (custled[5] & (0x1 << grp_num)) ? true : false;
}
return en;
}
int RealtekARGBController::get_argb_num(int grp_num)
{
int num_rgb = 0;
memcpy(&num_rgb, &argbctl_hdr[32 + grp_num * 2], 2);
return num_rgb;
}
int RealtekARGBController::get_argb_brightness(int grp_num)
{
int bright = 0;
memcpy(&bright, &argbctl_hdr[42 + grp_num * 2], 2);
return bright;
}
int RealtekARGBController::set_argb_brightness(int grp_num, unsigned short bright)
{
memcpy(&argbctl_hdr[42 + grp_num * sizeof(bright)], &bright, sizeof(bright));
set_argbctl_hdr();
return 0;
}
int RealtekARGBController::set_appctl(unsigned char grp_num, unsigned char ctl_sts)
{
int buf_len = REALTEK_ARGB_HID_DATALEN_CH1;
unsigned char* usb_buf = (unsigned char*)calloc(buf_len, 1);// will release in usb_hid_ioctl function
memset(usb_buf, 0x00, buf_len);
memcpy(usb_buf, hid_set_packet, sizeof(hid_set_packet));
usb_buf[0x13] = 0x4C;
usb_buf[0x14] = 0x01;
usb_buf[0x15] = ctl_sts;
usb_buf[0x16] = grp_num;
return usb_hid_ioctl(usb_buf, &ctl_sts, sizeof(ctl_sts), 0, false);
}
int RealtekARGBController::get_custled(unsigned char* cust, unsigned int cust_len)
{
int ret = 0;
int buf_len = REALTEK_ARGB_HID_DATALEN_CH1;
unsigned int addr = 0xAC004000;
unsigned int offset = 0x800;
unsigned char* usb_buf = (unsigned char*)calloc(buf_len + offset, 1);// will release in usb_hid_ioctl function
unsigned char* data = (unsigned char*)calloc(buf_len, 1);
memset(data, 0x00, buf_len);
memset(usb_buf, 0x00, buf_len + offset);
memcpy(usb_buf, hid_get_packet, sizeof(hid_get_packet));
usb_buf[0x13] = 0x92;
memcpy(&usb_buf[0x17], &addr, sizeof(addr));
ret = usb_hid_ioctl(usb_buf, data, buf_len, offset, true);
if(!ret)
{
memcpy(cust, &data[70], cust_len);
}
free(data);
return ret;
}
int RealtekARGBController::get_argbctl_hdr()
{
int buf_len = REALTEK_ARGB_HID_DATALEN_CH1;
unsigned char* usb_buf = (unsigned char*)calloc(buf_len, 1);// will release in usb_hid_ioctl function
memset(usb_buf, 0x00, buf_len);
memcpy(usb_buf, hid_get_packet, sizeof(hid_get_packet));
usb_buf[0x13] = 0xCC;
usb_buf[0x14] = 0x02;
return usb_hid_ioctl(usb_buf, argbctl_hdr, REALTEK_ARGB_CTL_HDR_SIZE, 0, true);
}
int RealtekARGBController::set_argbctl_hdr()
{
int buf_len = REALTEK_ARGB_HID_DATALEN_CH1;
unsigned char* usb_buf = (unsigned char*)calloc(buf_len, 1);// will release in usb_hid_send/usb_hid_get function
memset(usb_buf, 0x00, buf_len);
memcpy(usb_buf, hid_set_packet, sizeof(hid_set_packet));
usb_buf[0x13] = 0x4C;
usb_buf[0x14] = 0x02;
return usb_hid_ioctl(usb_buf, argbctl_hdr, REALTEK_ARGB_CTL_HDR_SIZE, 0, false);
}
int RealtekARGBController::get_argbctl_data()
{
int ret = 0;
int buf_len = REALTEK_ARGB_HID_DATALEN_CH1;
int offset = REALTEK_ARGB_CTL_HDR_SIZE;
unsigned char* usb_buf;
for(int i = 0; i < REALTEK_ARGB_NUM_ARGB_GRP; i++)
{
usb_buf = (unsigned char*)calloc(buf_len, 1);// will release in usb_hid_send/usb_hid_get function
memset(usb_buf, 0x00, buf_len);
memcpy(usb_buf, hid_get_packet, sizeof(hid_get_packet));
usb_buf[0x13] = 0xCC;
usb_buf[0x14] = 0x02;
memcpy(&usb_buf[0x17], &offset, sizeof(offset));
ret = usb_hid_ioctl(usb_buf, argbctl_data[i], REALTEK_ARGB_CTL_DATA_SIZE, 0, true);
offset += REALTEK_ARGB_CTL_DATA_SIZE;
}
return ret;
}
int RealtekARGBController::set_argbctl_data(unsigned char grp_num)
{
int buf_len = REALTEK_ARGB_HID_DATALEN_CH1;
int offset = REALTEK_ARGB_CTL_HDR_SIZE + REALTEK_ARGB_CTL_DATA_SIZE * grp_num;
unsigned char* usb_buf;
usb_buf = (unsigned char*)calloc(buf_len, 1);// will release in usb_hid_send/usb_hid_get function
memset(usb_buf, 0x00, buf_len);
memcpy(usb_buf, hid_set_packet, sizeof(hid_set_packet));
usb_buf[0x13] = 0x4C;
usb_buf[0x14] = 0x02;
memcpy(&usb_buf[0x17], &offset, sizeof(offset));
return usb_hid_ioctl(usb_buf, argbctl_data[grp_num], REALTEK_ARGB_CTL_DATA_SIZE, 0, false);
}
int RealtekARGBController::set_eff_id(unsigned char grp_num, unsigned short effid)
{
memcpy(argbctl_data[grp_num], &effid, sizeof(effid));
return 0;
}
int RealtekARGBController::set_p_color(unsigned char grp_num, RGBColor color)
{
memcpy(argbctl_data[grp_num] + 8, &color, sizeof(color));
return 0;
}
int RealtekARGBController::set_s_color(unsigned char grp_num, RGBColor color)
{
memcpy(argbctl_data[grp_num] + 12, &color, sizeof(color));
return 0;
}
int RealtekARGBController::set_cycle(unsigned char grp_num, unsigned short cycle)
{
memcpy(argbctl_data[grp_num] + 2, &cycle, sizeof(cycle));
return 0;
}
int RealtekARGBController::set_ramp(unsigned char grp_num, unsigned short ramp)
{
memcpy(argbctl_data[grp_num] + 4, &ramp, sizeof(ramp));
return 0;
}
int RealtekARGBController::set_stable(unsigned char grp_num, unsigned short stable)
{
memcpy(argbctl_data[grp_num] + 6, &stable, sizeof(stable));
return 0;
}
int RealtekARGBController::set_subcmd(unsigned char grp_num, int subcmd)
{
memcpy(argbctl_data[grp_num] + 16, &subcmd, sizeof(subcmd));
return 0;
}
int RealtekARGBController::set_direct(unsigned char* color, int color_num, unsigned char grp_num)
{
int buf_len = REALTEK_ARGB_HID_DATALEN_CH1;
int data_len = color_num * REALTEK_ARGB_COLOR_DEPTH;
unsigned char* usb_buf = (unsigned char*)calloc(buf_len, 1);// will release in usb_hid_ioctl function
memset(usb_buf, 0x00, buf_len);
memcpy(usb_buf, hid_set_packet, sizeof(hid_set_packet));
usb_buf[0x13] = 0x4C;
usb_buf[0x14] = 0x03;
usb_buf[0x15] = REALTEK_ARGB_SYNC_METHOD_OPENRGB;
usb_buf[0x16] = grp_num;
memcpy(&usb_buf[0x17], &color_num, sizeof(color_num));
return usb_hid_ioctl(usb_buf, color, data_len, 0, false);
}
int RealtekARGBController::get_flash_argbctl_hdr(unsigned char* data)
{
int buf_len = REALTEK_ARGB_HID_DATALEN_CH1;
unsigned int addr = 0xAC000000;
unsigned char* usb_buf = (unsigned char*)calloc(buf_len, 1);// will release in usb_hid_send/usb_hid_get function
memset(usb_buf, 0x00, buf_len);
memcpy(usb_buf, hid_get_packet, sizeof(hid_get_packet));
usb_buf[0x13] = 0x92;
memcpy(&usb_buf[0x17], &addr, sizeof(addr));
return usb_hid_ioctl(usb_buf, data, buf_len, 0, true);
}
int RealtekARGBController::set_flash_argbctl_hdr(unsigned int offset, unsigned int data_len)
{
int ret = 0;
int buf_len = REALTEK_ARGB_HID_DATALEN_CH1;
unsigned int addr = 0xAC000000;
unsigned char* usb_buf = (unsigned char*)calloc(buf_len, 1);// will release in usb_hid_send/usb_hid_get function
unsigned char* data = (unsigned char*)calloc(buf_len, 1);
memset(data, 0, buf_len);
ret = get_flash_argbctl_hdr(data);
if(!ret)
{
memset(usb_buf, 0x00, buf_len);
memcpy(usb_buf, hid_set_packet, sizeof(hid_set_packet));
usb_buf[0x13] = 0x10;
memcpy(&usb_buf[0x17], &addr, sizeof(addr));
ret = usb_hid_ioctl(usb_buf, (unsigned char*)&addr, sizeof(addr), 0, false);
usb_buf = (unsigned char*)calloc(buf_len, 1);// will release in usb_hid_send/usb_hid_get function
memset(usb_buf, 0x00, buf_len);
memcpy(usb_buf, hid_set_packet, sizeof(hid_set_packet));
usb_buf[0x13] = 0x11;
memcpy(data + offset, argbctl_hdr + offset, data_len);
ret = usb_hid_ioctl(usb_buf, data, buf_len, 0, false);
}
free(data);
return ret;
}
int RealtekARGBController::set_argb_direct(int grp_num, std::vector<RGBColor> color_buf, unsigned short brightness)
{
int ret = -1;
size_t color_num = color_buf.size();
size_t buf_len = color_num * REALTEK_ARGB_COLOR_DEPTH;
unsigned char* buf;
std::lock_guard<std::mutex> lock(my_mutex);
if(color_num == 0)
{
goto exit;
}
if(appctl[grp_num] != REALTEK_ARGB_LED_CTL_APP)
{
ret = set_appctl(grp_num, REALTEK_ARGB_LED_CTL_APP);
if(ret)
{
goto exit;
}
else
{
appctl[grp_num] = REALTEK_ARGB_LED_CTL_APP;
}
}
if(prev_bright[grp_num] != brightness)
{
prev_bright[grp_num] = brightness;
ret = set_argb_brightness(grp_num, brightness << 8);
if(ret)
{
goto exit;
}
}
buf = (uint8_t*)malloc(buf_len);
memset(buf, 0, buf_len);
for(size_t i = 0; i < color_num; i++)
{
buf[i * REALTEK_ARGB_COLOR_DEPTH + 0] = RGBGetRValue(color_buf[i]);
buf[i * REALTEK_ARGB_COLOR_DEPTH + 1] = RGBGetGValue(color_buf[i]);
buf[i * REALTEK_ARGB_COLOR_DEPTH + 2] = RGBGetBValue(color_buf[i]);
}
ret = set_direct(buf, (int)color_num, grp_num);
free(buf);
exit:
return ret;
}
int RealtekARGBController::set_argb_effect(int grp_num, uint8_t mode, std::vector<RGBColor> color_buf, struct RealtekARGBControllerSetEffParam* param)
{
int ret = -1;
int cycle = MathUtils::IntInterpolate(REALTEK_ARGB_CYCLE_MAX, REALTEK_ARGB_CYCLE_MIN, 0, REALTEK_ARGB_SPEED_MAX, param->speed);
std::lock_guard<std::mutex> lock(my_mutex);
if(mode == REALTEK_ARGB_EFF_ALWAYS_ON || mode == REALTEK_ARGB_EFF_STACK)
{
cycle = 0;
}
if(color_buf.size() >= 1)
{
set_p_color(grp_num, color_buf[0]);
}
if(color_buf.size() >= 2)
{
set_s_color(grp_num, color_buf[1]);
}
set_eff_id(grp_num, mode);
set_cycle(grp_num, cycle);
set_ramp(grp_num, 0);
set_stable(grp_num, 0);
set_subcmd(grp_num, (param->dir || param->random_color) ? 0x1 : 0x0);
set_argb_brightness(grp_num, param->brightness << 8);
set_argbctl_data(grp_num);
ret = set_appctl(grp_num, REALTEK_ARGB_LED_CTL_FW);
if(!ret)
{
appctl[grp_num] = REALTEK_ARGB_LED_CTL_FW;
}
return ret;
}
int RealtekARGBController::set_argb_num(int grp_num, unsigned short new_num)
{
memcpy(&argbctl_hdr[32 + grp_num * sizeof(new_num)], &new_num, sizeof(new_num));
set_flash_argbctl_hdr(32 + grp_num * sizeof(new_num), sizeof(new_num));
return 0;
}
int RealtekARGBController::device_reboot()
{
std::lock_guard<std::mutex> lock(my_mutex);
int ret = 0;
int buf_len = REALTEK_ARGB_HID_DATALEN_CH1;
int reboot_type = 1;
unsigned char* usb_buf = (unsigned char*)calloc(buf_len, 1);// will release in usb_hid_send/usb_hid_get function
memset(usb_buf, 0x00, buf_len);
memcpy(usb_buf, hid_set_packet, sizeof(hid_set_packet));
usb_buf[0x13] = 0x30;
usb_buf[0x14] = reboot_type;
ret = usb_hid_ioctl(usb_buf, (unsigned char*)&reboot_type, sizeof(reboot_type), 0, false);
hid_close(hdev);
hdev = NULL;
return ret;
}
void RealtekARGBController::device_rescan_trigger()
{
keepalive_thread_run = true;
keepalive_thread = new std::thread(&RealtekARGBController::KeepaliveThreadFunction, this);
}
void RealtekARGBController::device_rescan()
{
hid_device_info* info_full = hid_enumerate(REALTEK_ARGB_VID, REALTEK_ARGB_PID);
hid_device_info* info_temp = info_full;
while(info_temp)
{
if(info_temp->vendor_id == REALTEK_ARGB_VID &&
info_temp->product_id == REALTEK_ARGB_PID &&
info_temp->usage == REALTEK_ARGB_HID2SCSI_USAGE &&
info_temp->usage_page == REALTEK_ARGB_HID2SCSI_PG)
{
hid_device* dev_temp = hid_open_path(info_temp->path);
if(dev_temp)
{
hdev = dev_temp;
if(get_support_openrgb())
{
device_init();
break;
}
else
{
hid_close(hdev);
hdev = NULL;
}
}
}
info_temp = info_temp->next;
}
hid_free_enumeration(info_full);
}
@@ -0,0 +1,182 @@
/*---------------------------------------------------------*\
| RealtekARGBController.h |
| |
| Controller for Realtek USB ARGB ICs |
| |
| Jerry Fan (JerryFan0612) 13 Mar 2026 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#pragma once
#include <stdlib.h>
#include <stdint.h>
#include <cmath>
#include <cstring>
#include <vector>
#include <thread>
#include <hidapi.h>
#include "RGBController.h"
#define REALTEK_ARGB_VID 0x0BDA
#define REALTEK_ARGB_PID 0x9209
#define REALTEK_ARGB_HID2SCSI_PG 0xFF00
#define REALTEK_ARGB_HID2SCSI_USAGE 0x0001
#define REALTEK_ARGB_NUM_ARGB_GRP 5
#define REALTEK_ARGB_BRINTF_TYPE_HID 1
#define REALTEK_ARGB_SYNC_METHOD_OPENRGB 6
#define REALTEK_ARGB_COLOR_DEPTH 3
#define REALTEK_ARGB_MAX 400
#define REALTEK_ARGB_CTL_HDR_SIZE 80
#define REALTEK_ARGB_CTL_DATA_SIZE 96
#define REALTEK_ARGB_HID_DATALEN_CH1 4096
#define REALTEK_ARGB_HID_DATALEN_CH2 64
#define REALTEK_ARGB_HID_DATALEN_CH3 512
#define REALTEK_ARGB_HID_DATALEN_CH4 1200
#define REALTEK_ARGB_HID_DATALEN_CH5 2048
#define REALTEK_ARGB_HID_ID_DATA_CH1 0x57
#define REALTEK_ARGB_HID_ID_DATA_CH2 0x59
#define REALTEK_ARGB_HID_ID_DATA_CH3 0x5A
#define REALTEK_ARGB_HID_ID_DATA_CH4 0x5B
#define REALTEK_ARGB_HID_ID_DATA_CH5 0x5C
enum REALTEK_ARGB_LED_CTL
{
REALTEK_ARGB_LED_CTL_FW,
REALTEK_ARGB_LED_CTL_APP,
REALTEK_ARGB_LED_CTL_LAMP,
};
enum REALTEK_ARGB_EFFECT_ID
{
REALTEK_ARGB_EFF_NULL,
REALTEK_ARGB_EFF_ALWAYS_ON,
REALTEK_ARGB_EFF_BLINK,
REALTEK_ARGB_EFF_BREATH,
REALTEK_ARGB_EFF_SPECTRUM,
REALTEK_ARGB_EFF_SCROLL,
REALTEK_ARGB_EFF_RAINBOW_SCROLL,
REALTEK_ARGB_EFF_RUNNING_WATER,
REALTEK_ARGB_EFF_SLIDING,
REALTEK_ARGB_EFF_NEWTON_CRADLE,
REALTEK_ARGB_EFF_METEOR,
REALTEK_ARGB_EFF_RAINBOW_SLIDING,
REALTEK_ARGB_EFF_RAINBOW_FADE_SLIDING,
REALTEK_ARGB_EFF_WIDE_SLIDING,
REALTEK_ARGB_EFF_DOT_MATRIX, // reserved: Not supported by OpenRGB
REALTEK_ARGB_EFF_DOT_MATRIX_BREATH, // reserved: Not supported by OpenRGB
REALTEK_ARGB_EFF_ZIGZAG,
REALTEK_ARGB_EFF_STARRY_NIGHT,
REALTEK_ARGB_EFF_STACK,
};
enum REALTEK_ARGB_SPEED
{
REALTEK_ARGB_SPEED_MIN = 1,
REALTEK_ARGB_SPEED_NORMAL = 50,
REALTEK_ARGB_SPEED_MAX = 100,
};
enum REALTEK_ARGB_CYCLE_MS
{
REALTEK_ARGB_CYCLE_MIN = 200,
REALTEK_ARGB_CYCLE_NORMAL = 2000,
REALTEK_ARGB_CYCLE_MAX = 10000,
};
enum REALTEK_ARGB_CUST_DEVNAME
{
REALTEK_ARGB_CUST_DEVNAME_NULL = 0x0,
REALTEK_ARGB_CUST_DEVNAME_MANU_UUID = 0x1,
};
struct RealtekARGBControllerSetEffParam
{
unsigned int speed;
unsigned short brightness;
unsigned char dir;
unsigned char random_color;
};
struct RealtekARGBControllerFWVersion
{
unsigned int fw_major_ver;
unsigned int fw_minor_ver;
unsigned int fw_extra_ver;
unsigned int fw_build_ver;
unsigned int fw_build_date;
};
class RealtekARGBController
{
public:
RealtekARGBController(hid_device* dev, hid_device_info* info);
~RealtekARGBController();
unsigned char get_support_openrgb();
std::string get_manu_name();
std::string get_product_name();
std::string get_sn();
std::string get_dev_loc();
std::string get_fw_ver();
std::string get_ic_uuid();
std::string get_dev_name();
int get_fix_grps();
bool get_zone_enable(int grp_num);
int get_argb_num(int grp_num);
int get_argb_brightness(int grp_num);
int set_argb_brightness(int grp_num, unsigned short bright);
int set_argb_direct(int grp_num, std::vector<RGBColor> color_buf, unsigned short brightness);
int set_argb_effect(int grp_num, uint8_t mode, std::vector<RGBColor> color_buf, struct RealtekARGBControllerSetEffParam* param);
int set_argb_num(int grp_num, unsigned short new_num);
int device_reboot();
void device_rescan_trigger();
private:
hid_device* hdev;
std::mutex my_mutex;
unsigned char argbctl_hdr[REALTEK_ARGB_CTL_HDR_SIZE] = {0};
unsigned char* argbctl_data[REALTEK_ARGB_NUM_ARGB_GRP] = {0};
hid_device_info* hidinfo = NULL;
int appctl[REALTEK_ARGB_NUM_ARGB_GRP] = {REALTEK_ARGB_LED_CTL_FW};
unsigned short prev_bright[REALTEK_ARGB_NUM_ARGB_GRP] = {0};
std::atomic<bool> keepalive_thread_run;
std::thread* keepalive_thread;
unsigned char custled[16] = {0};
void KeepaliveThreadFunction();
int usb_hid_ioctl(unsigned char* usb_buf, unsigned char* data, int data_len,
unsigned int offset, unsigned char is_in);
int usb_hid_get_report(int data_len, int* id);
void device_init();
int set_write_unlock();
int set_appctl(unsigned char grp_num, unsigned char ctl_sts);
int set_argbctl_data(unsigned char* data, int data_len, int offset);
int get_custled(unsigned char* cust, unsigned int cust_len);
int get_argbctl_hdr();
int set_argbctl_hdr();
int get_argbctl_data();
int set_argbctl_data(unsigned char grp_num);
int set_eff_id(unsigned char grp_num, unsigned short effid);
int set_p_color(unsigned char grp_num, RGBColor color);
int set_s_color(unsigned char grp_num, RGBColor color);
int set_cycle(unsigned char grp_num, unsigned short cycle);
int set_ramp(unsigned char grp_num, unsigned short ramp);
int set_stable(unsigned char grp_num, unsigned short stable);
int set_subcmd(unsigned char grp_num, int subcmd);
int set_direct(unsigned char* color, int color_num, unsigned char grp_num);
int get_flash_argbctl_hdr(unsigned char* data);
int set_flash_argbctl_hdr(unsigned int offset, unsigned int data_len);
void device_rescan();
};
@@ -0,0 +1,52 @@
/*---------------------------------------------------------*\
| RealtekARGBControllerDetect.cpp |
| |
| Detector for Realtek USB ARGB ICs |
| |
| Jerry Fan (JerryFan0612) 13 Mar 2026 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include "Detector.h"
#include "RGBController.h"
#include "RGBController_RealtekARGB.h"
/******************************************************************************************\
* *
* DetectRealtekARGBControllers *
* *
* Tests the USB address to see if an Realtek ARGB controller exists there *
* *
\******************************************************************************************/
void DetectRealtekARGBControllers(hid_device_info* info, const std::string& /*name*/)
{
RealtekARGBController* controller = NULL;
RGBController_RealtekARGB* rgb_controller = NULL;
hid_device* dev = hid_open_path(info->path);
if(dev)
{
controller = new RealtekARGBController(dev, info);
if(controller->get_support_openrgb())
{
rgb_controller = new RGBController_RealtekARGB(controller);
if(rgb_controller->type != DEVICE_TYPE_UNKNOWN)
{
ResourceManager::get()->RegisterRGBController(rgb_controller);
}
else
{
delete rgb_controller;
}
}
else
{
delete controller;
}
}
return;
}
REGISTER_HID_DETECTOR_PU("RTL9209", DetectRealtekARGBControllers, REALTEK_ARGB_VID, REALTEK_ARGB_PID, REALTEK_ARGB_HID2SCSI_PG, REALTEK_ARGB_HID2SCSI_USAGE);
@@ -0,0 +1,303 @@
/*---------------------------------------------------------*\
| RGBController_RealtekBridge.cpp |
| |
| Controller for Realtek USB to SSD Bridge ICs |
| |
| Jerry Fan (JerryFan0612) 13 Mar 2026 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include "RGBController_RealtekBridge.h"
/**------------------------------------------------------------------*\
@name Realtek Bridge Device
@category Storage
@type USB
@save :x:
@direct :rotating_light:
@effects :white_check_mark:
@detectors RealtekBridgeControllerDetect
@comment
\*-------------------------------------------------------------------*/
RGBController_RealtekBridge::RGBController_RealtekBridge(RealtekBridgeController* controller_ptr)
{
controller = controller_ptr;
name = controller_ptr->get_product_name();
vendor = controller_ptr->get_manu_name();
location = controller_ptr->get_dev_loc();
serial = controller_ptr->get_sn();
version = controller_ptr->get_fw_ver();
description = vendor + "Storage Device";
type = DEVICE_TYPE_STORAGE;
SetupModes();
SetupZones();
}
RGBController_RealtekBridge::~RGBController_RealtekBridge()
{
delete controller;
}
void RGBController_RealtekBridge::SetupModes()
{
int brightness = controller->get_argb_brightness() >> 8;
mode Direct;
Direct.name = "Direct";
Direct.value = REALTEK_BRIDGE_LED_EFF_NONE;
Direct.flags = MODE_FLAG_HAS_PER_LED_COLOR | MODE_FLAG_HAS_BRIGHTNESS;
Direct.color_mode = MODE_COLORS_PER_LED;
Direct.brightness_min = 0;
Direct.brightness_max = 255;
Direct.brightness = brightness;
modes.push_back(Direct);
mode Static;
Static.name = "Static";
Static.value = REALTEK_BRIDGE_LED_EFF_ALWAYS;
Static.flags = MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS;
Static.colors_min = 1;
Static.colors_max = 1;
Static.color_mode = MODE_COLORS_MODE_SPECIFIC;
Static.colors.resize(1);
Static.brightness_min = 0;
Static.brightness_max = 255;
Static.brightness = brightness;
modes.push_back(Static);
mode Blink;
Blink.name = "Blink";
Blink.value = REALTEK_BRIDGE_LED_EFF_BLINK;
Blink.flags = MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED;
Blink.speed_min = REALTEK_BRIDGE_SPEED_MIN;
Blink.speed_max = REALTEK_BRIDGE_SPEED_MAX;
Blink.speed = REALTEK_BRIDGE_SPEED_NORMAL;
Blink.colors_min = 1;
Blink.colors_max = 1;
Blink.color_mode = MODE_COLORS_MODE_SPECIFIC;
Blink.colors.resize(1);
Blink.brightness_min = 0;
Blink.brightness_max = 255;
Blink.brightness = brightness;
modes.push_back(Blink);
mode Breathing;
Breathing.name = "Breathing";
Breathing.value = REALTEK_BRIDGE_LED_EFF_BREATHE;
Breathing.flags = MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED;
Breathing.speed_min = REALTEK_BRIDGE_SPEED_MIN;
Breathing.speed_max = REALTEK_BRIDGE_SPEED_MAX;
Breathing.speed = REALTEK_BRIDGE_SPEED_NORMAL;
Breathing.colors_min = 1;
Breathing.colors_max = 1;
Breathing.color_mode = MODE_COLORS_MODE_SPECIFIC;
Breathing.colors.resize(1);
Breathing.brightness_min = 0;
Breathing.brightness_max = 255;
Breathing.brightness = brightness;
modes.push_back(Breathing);
mode Spectrum;
Spectrum.name = "Spectrum";
Spectrum.value = REALTEK_BRIDGE_LED_EFF_SPECTRUM;
Spectrum.flags = MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED;
Spectrum.speed_min = REALTEK_BRIDGE_SPEED_MIN;
Spectrum.speed_max = REALTEK_BRIDGE_SPEED_MAX;
Spectrum.speed = REALTEK_BRIDGE_SPEED_NORMAL;
Spectrum.color_mode = MODE_COLORS_NONE;
Spectrum.brightness_min = 0;
Spectrum.brightness_max = 255;
Spectrum.brightness = brightness;
modes.push_back(Spectrum);
mode Scroll;
Scroll.name = "Scroll";
Scroll.value = REALTEK_BRIDGE_LED_EFF_SCROLL;
Scroll.flags = MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED;
Scroll.speed_min = REALTEK_BRIDGE_SPEED_MIN;
Scroll.speed_max = REALTEK_BRIDGE_SPEED_MAX;
Scroll.speed = REALTEK_BRIDGE_SPEED_NORMAL;
Scroll.colors_min = 1;
Scroll.colors_max = 1;
Scroll.color_mode = MODE_COLORS_MODE_SPECIFIC;
Scroll.colors.resize(1);
Scroll.brightness_min = 0;
Scroll.brightness_max = 255;
Scroll.brightness = brightness;
modes.push_back(Scroll);
mode RainbowScroll;
RainbowScroll.name = "Rainbow Scroll";
RainbowScroll.value = REALTEK_BRIDGE_LED_EFF_RAINBOW_SCROLL;
RainbowScroll.flags = MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED;
RainbowScroll.speed_min = REALTEK_BRIDGE_SPEED_MIN;
RainbowScroll.speed_max = REALTEK_BRIDGE_SPEED_MAX;
RainbowScroll.speed = REALTEK_BRIDGE_SPEED_NORMAL;
RainbowScroll.color_mode = MODE_COLORS_NONE;
RainbowScroll.brightness_min = 0;
RainbowScroll.brightness_max = 255;
RainbowScroll.brightness = brightness;
modes.push_back(RainbowScroll);
mode RunningWater;
RunningWater.name = "Running Water";
RunningWater.value = REALTEK_BRIDGE_LED_EFF_RUNNING_WATER;
RunningWater.flags = MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED;
RunningWater.speed_min = REALTEK_BRIDGE_SPEED_MIN;
RunningWater.speed_max = REALTEK_BRIDGE_SPEED_MAX;
RunningWater.speed = REALTEK_BRIDGE_SPEED_NORMAL;
RunningWater.colors_min = 1;
RunningWater.colors_max = 1;
RunningWater.color_mode = MODE_COLORS_MODE_SPECIFIC;
RunningWater.colors.resize(1);
RunningWater.brightness_min = 0;
RunningWater.brightness_max = 255;
RunningWater.brightness = brightness;
modes.push_back(RunningWater);
mode Sliding;
Sliding.name = "Sliding";
Sliding.value = REALTEK_BRIDGE_LED_EFF_SLIDING;
Sliding.flags = MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED;
Sliding.speed_min = REALTEK_BRIDGE_SPEED_MIN;
Sliding.speed_max = REALTEK_BRIDGE_SPEED_MAX;
Sliding.speed = REALTEK_BRIDGE_SPEED_NORMAL;
Sliding.color_mode = MODE_COLORS_NONE;
Sliding.brightness_min = 0;
Sliding.brightness_max = 255;
Sliding.brightness = brightness;
modes.push_back(Sliding);
mode NewtonCradle;
NewtonCradle.name = "Newton Cradle";
NewtonCradle.value = REALTEK_BRIDGE_LED_EFF_NEWTON_CRADLE;
NewtonCradle.flags = MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED;
NewtonCradle.speed_min = REALTEK_BRIDGE_SPEED_MIN;
NewtonCradle.speed_max = REALTEK_BRIDGE_SPEED_MAX;
NewtonCradle.speed = REALTEK_BRIDGE_SPEED_NORMAL;
NewtonCradle.color_mode = MODE_COLORS_NONE;
NewtonCradle.brightness_min = 0;
NewtonCradle.brightness_max = 255;
NewtonCradle.brightness = brightness;
modes.push_back(NewtonCradle);
mode Meteor;
Meteor.name = "Meteor";
Meteor.value = REALTEK_BRIDGE_LED_EFF_METEOR;
Meteor.flags = MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED;
Meteor.speed_min = REALTEK_BRIDGE_SPEED_MIN;
Meteor.speed_max = REALTEK_BRIDGE_SPEED_MAX;
Meteor.speed = REALTEK_BRIDGE_SPEED_NORMAL;
Meteor.colors_min = 1;
Meteor.colors_max = 1;
Meteor.color_mode = MODE_COLORS_MODE_SPECIFIC;
Meteor.colors.resize(1);
Meteor.brightness_min = 0;
Meteor.brightness_max = 255;
Meteor.brightness = brightness;
modes.push_back(Meteor);
}
void RGBController_RealtekBridge::SetupZones()
{
zone argb_zone;
argb_zone.name = "strip";
argb_zone.type = ZONE_TYPE_LINEAR;
argb_zone.leds_min = controller->get_argb_num();
argb_zone.leds_max = controller->get_argb_num();
argb_zone.leds_count = controller->get_argb_num();
argb_zone.matrix_map = NULL;
zones.push_back(argb_zone);
for(unsigned int led_idx = 0; led_idx < argb_zone.leds_count; led_idx++)
{
led StripLED;
StripLED.name = "led ";
StripLED.name.append(std::to_string(led_idx + 1));
leds.push_back(StripLED);
}
SetupColors();
}
void RGBController_RealtekBridge::ResizeZone(int /*zone*/, int /*new_size*/)
{
/*---------------------------------------------------------*\
| This device does not support resizing zones |
\*---------------------------------------------------------*/
}
void RGBController_RealtekBridge::DeviceUpdateLEDs()
{
UpdateZoneLEDs(0);
}
void RGBController_RealtekBridge::UpdateZoneLEDs(int /*zone*/)
{
unsigned short brightness = 0xFF;
mode& curr_mode = modes[active_mode];
if(curr_mode.color_mode == MODE_COLORS_PER_LED &&
curr_mode.value == REALTEK_BRIDGE_LED_EFF_NONE) //direct mode
{
if(curr_mode.flags & MODE_FLAG_HAS_BRIGHTNESS)
{
brightness = curr_mode.brightness;
}
controller->set_argb_direct(colors, brightness);
}
else
{
UpdateSingleLED(0);
}
}
void RGBController_RealtekBridge::UpdateSingleLED(int /*led*/)
{
unsigned char speed = REALTEK_BRIDGE_SPEED_NORMAL;
unsigned char dir = 0;
unsigned short brightness = 0xFF;
mode& curr_mode = modes[active_mode];
std::vector<RGBColor> rtk_colors = curr_mode.colors;
if(curr_mode.flags & MODE_FLAG_HAS_SPEED)
{
speed = curr_mode.speed;
}
if(curr_mode.flags & MODE_FLAG_HAS_BRIGHTNESS)
{
brightness = curr_mode.brightness;
}
if(curr_mode.flags & MODE_FLAG_HAS_DIRECTION_LR)
{
if(curr_mode.direction == MODE_DIRECTION_RIGHT)
{
dir = 1;
}
}
if(curr_mode.color_mode == MODE_COLORS_PER_LED)
{
rtk_colors = colors;
}
else if(curr_mode.color_mode == MODE_COLORS_NONE)
{
rtk_colors.clear();
}
controller->set_argb_effect(curr_mode.value, rtk_colors, speed, brightness);
}
void RGBController_RealtekBridge::DeviceUpdateMode()
{
if(modes[active_mode].value != REALTEK_BRIDGE_LED_EFF_NONE)
{
DeviceUpdateLEDs();
}
}
@@ -0,0 +1,34 @@
/*---------------------------------------------------------*\
| RGBController_RealtekBridge.h |
| |
| Controller for Realtek USB to SSD Bridge ICs |
| |
| Jerry Fan (JerryFan0612) 13 Mar 2026 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#pragma once
#include "RGBController.h"
#include "RealtekBridgeController.h"
class RGBController_RealtekBridge : public RGBController
{
public:
RGBController_RealtekBridge(RealtekBridgeController* controller_ptr);
~RGBController_RealtekBridge();
void SetupModes();
void SetupZones();
void ResizeZone(int zone, int new_size);
void DeviceUpdateLEDs();
void UpdateZoneLEDs(int zone);
void UpdateSingleLED(int led);
void DeviceUpdateMode();
private:
RealtekBridgeController* controller;
};
@@ -0,0 +1,802 @@
/*---------------------------------------------------------*\
| RealtekBridgeController.cpp |
| |
| Controller for Realtek USB to SSD Bridge ICs |
| |
| Jerry Fan (JerryFan0612) 13 Mar 2026 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include "RealtekBridgeController.h"
#include "hsv.h"
#include <StringUtils.h>
#include <MathUtils.h>
static const unsigned char hid_set_packet[] =
{
0x56, 0x53, 0x42, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0xE3,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static const unsigned char hid_get_packet[] =
{
0x56, 0x53, 0x42, 0x43, 0x78, 0x56, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x10, 0xE2,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static const unsigned char hid_end_packet[] =
{
0x58, 0x53, 0x42, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
RealtekBridgeController::RealtekBridgeController(hid_device* dev, hid_device_info* info)
{
hdev = dev;
hidinfo = info;
brctl_data = (unsigned char*)calloc(REALTEK_BRIDGE_CTL_DATA_SIZE, 1);
memset(brctl_data, 0, REALTEK_BRIDGE_CTL_DATA_SIZE);
set_write_unlock();
get_brctl_hdr();
get_brctl_data();
set_appctl(true);
}
RealtekBridgeController::~RealtekBridgeController()
{
if(hdev)
{
set_appctl(false);
hid_close(hdev);
hdev = NULL;
}
memset(brctl_hdr, 0, REALTEK_BRIDGE_CTL_HDR_SIZE);
free(brctl_data);
}
int RealtekBridgeController::usb_hid_ioctl(unsigned char* usb_buf, unsigned char* data, int data_len,
unsigned int offset, unsigned char is_in)
{
int id;
int ret = 0;
int buf_len = usb_hid_get_report(data_len, &id);
if(!is_in && data_len)
{
usb_buf[0x04] = data[0];
}
memcpy(&usb_buf[0x08], &data_len, sizeof(data_len));
memcpy(&usb_buf[0x1B], &data_len, sizeof(data_len));
ret = hid_send_feature_report(hdev, usb_buf, sizeof(hid_get_packet));
memset(usb_buf, 0x00, buf_len);
memcpy(usb_buf, data, data_len);
usb_buf[0x00] = id;
if(is_in)
{
ret = hid_get_feature_report(hdev, usb_buf, buf_len);
memcpy(data, usb_buf + offset, data_len);
}
else
{
ret = hid_send_feature_report(hdev, usb_buf, buf_len);
}
memset(usb_buf, 0x00, buf_len);
memcpy(usb_buf, hid_end_packet, sizeof(hid_end_packet));
ret = hid_get_feature_report(hdev, usb_buf, sizeof(hid_end_packet));
free(usb_buf);
return (ret > 0) ? 0 : ret;
}
int RealtekBridgeController::usb_hid_get_report(int data_len, int* id)
{
int retlen = 0;
if(data_len <= REALTEK_BRIDGE_HID_DATALEN_CH2)
{
*id = REALTEK_BRIDGE_HID_ID_DATA_CH2;
retlen = REALTEK_BRIDGE_HID_DATALEN_CH2;
}
else if(data_len <= REALTEK_BRIDGE_HID_DATALEN_CH3)
{
*id = REALTEK_BRIDGE_HID_ID_DATA_CH3;
retlen = REALTEK_BRIDGE_HID_DATALEN_CH3;
}
else if(data_len <= REALTEK_BRIDGE_HID_DATALEN_CH4)
{
*id = REALTEK_BRIDGE_HID_ID_DATA_CH4;
retlen = REALTEK_BRIDGE_HID_DATALEN_CH4;
}
else if(data_len <= REALTEK_BRIDGE_HID_DATALEN_CH5)
{
*id = REALTEK_BRIDGE_HID_ID_DATA_CH5;
retlen = REALTEK_BRIDGE_HID_DATALEN_CH5;
}
else
{
*id = REALTEK_BRIDGE_HID_ID_DATA_CH1;
retlen = REALTEK_BRIDGE_HID_DATALEN_CH1;
}
return retlen;
}
int RealtekBridgeController::set_write_unlock()
{
int ret = 0;
int buf_len = REALTEK_BRIDGE_HID_DATALEN_CH1;
int data_len = 96;
unsigned int addr = 0xAC004000;
unsigned char* data = (unsigned char*)calloc(data_len, 1);
unsigned char* usb_buf = (unsigned char*)calloc(buf_len, 1);// will release in usb_hid_ioctl function
memset(usb_buf, 0x00, buf_len);
memcpy(usb_buf, hid_get_packet, sizeof(hid_get_packet));
usb_buf[0x13] = 0x92;
memcpy(&usb_buf[0x17], &addr, sizeof(addr));
ret = usb_hid_ioctl(usb_buf, data, data_len, 0, true);
free(data);
return ret;
}
int RealtekBridgeController::get_brctl_hdr()
{
int buf_len = REALTEK_BRIDGE_HID_DATALEN_CH1;
unsigned char* usb_buf = (unsigned char*)calloc(buf_len, 1);// will release in usb_hid_ioctl function
memset(usb_buf, 0x00, buf_len);
memcpy(usb_buf, hid_get_packet, sizeof(hid_get_packet));
usb_buf[0x13] = 0xCC;
usb_buf[0x14] = 0x02;
return usb_hid_ioctl(usb_buf, brctl_hdr, REALTEK_BRIDGE_CTL_HDR_SIZE, 0, true);
}
int RealtekBridgeController::set_brctl_hdr()
{
int buf_len = REALTEK_BRIDGE_HID_DATALEN_CH1;
unsigned char* usb_buf = (unsigned char*)calloc(buf_len, 1);// will release in usb_hid_ioctl function
memset(usb_buf, 0x00, buf_len);
memcpy(usb_buf, hid_set_packet, sizeof(hid_set_packet));
usb_buf[0x13] = 0x4C;
usb_buf[0x14] = 0x02;
return usb_hid_ioctl(usb_buf, brctl_hdr, REALTEK_BRIDGE_CTL_HDR_SIZE, 0, false);
}
int RealtekBridgeController::get_brctl_data()
{
int buf_len = REALTEK_BRIDGE_HID_DATALEN_CH1;
int offset = REALTEK_BRIDGE_CTL_HDR_SIZE;
unsigned char* usb_buf = (unsigned char*)calloc(buf_len, 1);// will release in usb_hid_ioctl function
memset(usb_buf, 0x00, buf_len);
memcpy(usb_buf, hid_get_packet, sizeof(hid_get_packet));
usb_buf[0x13] = 0xCC;
usb_buf[0x14] = 0x02;
memcpy(&usb_buf[0x17], &offset, sizeof(offset));
return usb_hid_ioctl(usb_buf, brctl_data, REALTEK_BRIDGE_CTL_DATA_SIZE, 0, true);
}
int RealtekBridgeController::set_brctl_data()
{
int buf_len = REALTEK_BRIDGE_HID_DATALEN_CH1;
int offset = REALTEK_BRIDGE_CTL_HDR_SIZE;
unsigned char* usb_buf = (unsigned char*)calloc(buf_len, 1);// will release in usb_hid_ioctl function
memset(usb_buf, 0x00, buf_len);
memcpy(usb_buf, hid_set_packet, sizeof(hid_set_packet));
usb_buf[0x13] = 0x4C;
usb_buf[0x14] = 0x02;
memcpy(&usb_buf[0x17], &offset, sizeof(offset));
return usb_hid_ioctl(usb_buf, brctl_data, REALTEK_BRIDGE_CTL_DATA_SIZE, 0, false);
}
unsigned char RealtekBridgeController::get_support_openrgb()
{
int buf_len = REALTEK_BRIDGE_HID_DATALEN_CH1;
unsigned char is_support = 0;
unsigned char* usb_buf = (unsigned char*)calloc(buf_len, 1);// will release in usb_hid_ioctl function
memset(usb_buf, 0x00, buf_len);
memcpy(usb_buf, hid_get_packet, sizeof(hid_get_packet));
usb_buf[0x13] = 0xCC;
usb_buf[0x14] = 0x04;
usb_buf[0x15] = REALTEK_BRIDGE_SYNC_METHOD_OPENRGB;
if(usb_hid_ioctl(usb_buf, &is_support, sizeof(is_support), 0, true))
{
is_support = 0;
}
return is_support;
}
std::string RealtekBridgeController::get_manu_name()
{
return StringUtils::wchar_to_char(hidinfo->manufacturer_string);
}
std::string RealtekBridgeController::get_product_name()
{
return StringUtils::wchar_to_char(hidinfo->product_string);
}
std::string RealtekBridgeController::get_sn()
{
return StringUtils::wchar_to_char(hidinfo->serial_number);
}
std::string RealtekBridgeController::get_dev_loc()
{
return hidinfo->path;
}
std::string RealtekBridgeController::get_fw_ver()
{
struct RealtekBridgeControllerFWVersion fw_ver;
std::string ver = "";
int buf_len = REALTEK_BRIDGE_HID_DATALEN_CH1;
unsigned char* usb_buf = (unsigned char*)calloc(buf_len, 1);// will release in usb_hid_ioctl function
memset(usb_buf, 0x00, buf_len);
memcpy(usb_buf, hid_get_packet, sizeof(hid_get_packet));
usb_buf[0x13] = 0xA5;
if(!usb_hid_ioctl(usb_buf, (unsigned char*)&fw_ver, sizeof(fw_ver), 0, true))
{
ver += std::to_string(fw_ver.fw_major_ver) + "." +
std::to_string(fw_ver.fw_minor_ver) + "." +
std::to_string(fw_ver.fw_extra_ver) + "." +
std::to_string(fw_ver.fw_build_date);
}
return ver;
}
int RealtekBridgeController::get_argb_num()
{
int num_rgb = 0;
memcpy(&num_rgb, &brctl_hdr[24], sizeof(num_rgb));
return num_rgb;
}
int RealtekBridgeController::get_argb_brightness()
{
int bright = 0;
memcpy(&bright, &brctl_hdr[28], 2);
return bright;
}
int RealtekBridgeController::set_argb_brightness(unsigned short bright)
{
memcpy(&brctl_hdr[28], &bright, sizeof(bright));
set_brctl_hdr();
return 0;
}
int RealtekBridgeController::set_appctl(unsigned char ctl_sts)
{
int buf_len = REALTEK_BRIDGE_HID_DATALEN_CH1;
unsigned char* usb_buf = (unsigned char*)calloc(buf_len, 1);// will release in usb_hid_ioctl function
memset(usb_buf, 0x00, buf_len);
memcpy(usb_buf, hid_set_packet, sizeof(hid_set_packet));
usb_buf[0x13] = 0x4C;
usb_buf[0x14] = 0x01;
usb_buf[0x15] = ctl_sts;
return usb_hid_ioctl(usb_buf, &ctl_sts, sizeof(ctl_sts), 0, false);
}
int RealtekBridgeController::set_direct(unsigned char* color, int color_num)
{
int buf_len = REALTEK_BRIDGE_HID_DATALEN_CH1;
int data_len = color_num * REALTEK_BRIDGE_COLOR_DEPTH;
unsigned char* usb_buf = (unsigned char*)calloc(buf_len, 1);// will release in usb_hid_ioctl function
memset(usb_buf, 0x00, buf_len);
memcpy(usb_buf, hid_set_packet, sizeof(hid_set_packet));
usb_buf[0x13] = 0x4C;
usb_buf[0x14] = 0x03;
usb_buf[0x15] = REALTEK_BRIDGE_SYNC_METHOD_OPENRGB;
memcpy(&usb_buf[0x17], &color_num, sizeof(color_num));
return usb_hid_ioctl(usb_buf, color, data_len, 0, false);
}
int RealtekBridgeController::eff_set_always_on(RGBColor rgb)
{
int i;
int j;
int row = 1;
int maxrow = REALTEK_BRIDGE_MAX_APPCTL_ROW;
int single_size = maxrow * REALTEK_BRIDGE_COLOR_DEPTH + 32;
memset(brctl_data, 0, REALTEK_BRIDGE_CTL_DATA_SIZE);
for(i = 0; i < get_argb_num(); i++)
{
brctl_data[i * single_size + 0] = 1;
brctl_data[i * single_size + 1] = row;
for(j = 0; j < row; j++)
{
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 32] = RGBGetRValue(rgb);
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 33] = RGBGetGValue(rgb);
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 34] = RGBGetBValue(rgb);
}
}
return set_brctl_data();
}
int RealtekBridgeController::eff_set_blink(RGBColor rgb, int cycle)
{
int i;
int j;
int row = 2;
int maxrow = REALTEK_BRIDGE_MAX_APPCTL_ROW;
int single_size = maxrow * REALTEK_BRIDGE_COLOR_DEPTH + 32;
memset(brctl_data, 0, REALTEK_BRIDGE_CTL_DATA_SIZE);
for(i = 0; i < get_argb_num(); i++)
{
brctl_data[i * single_size + 0] = 2;
brctl_data[i * single_size + 1] = row;
memcpy(&brctl_data[i * single_size + 2], &cycle, 2);
brctl_data[i * single_size + 8] = 1;
for(j = 0; j < row; j++)
{
if((j & 0x1) == 0)
{
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 32] = RGBGetRValue(rgb);
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 33] = RGBGetGValue(rgb);
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 34] = RGBGetBValue(rgb);
}
}
}
return set_brctl_data();
}
int RealtekBridgeController::eff_set_breathe(RGBColor rgb, int cycle)
{
int i;
int j;
int row = 2;
int maxrow = REALTEK_BRIDGE_MAX_APPCTL_ROW;
int single_size = maxrow * REALTEK_BRIDGE_COLOR_DEPTH + 32;
memset(brctl_data, 0, REALTEK_BRIDGE_CTL_DATA_SIZE);
for(i = 0; i < get_argb_num(); i++)
{
brctl_data[i * single_size + 0] = 4;
brctl_data[i * single_size + 1] = row;
memcpy(&brctl_data[i * single_size + 2], &cycle, 2);
brctl_data[i * single_size + 8] = 2;
for(j = 0; j < row; j++)
{
if((j & 0x1) == 0)
{
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 32] = RGBGetRValue(rgb);
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 33] = RGBGetGValue(rgb);
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 34] = RGBGetBValue(rgb);
}
}
}
return set_brctl_data();
}
int RealtekBridgeController::eff_set_spectrum(int cycle)
{
int i;
int j;
int row = 6;
int maxrow = REALTEK_BRIDGE_MAX_APPCTL_ROW;
int single_size = maxrow * REALTEK_BRIDGE_COLOR_DEPTH + 32;
static const RGBColor rainbow_table[6] = {0x0000FF, 0x0050FF, 0x0080FF, 0x00FF00, 0xFF0000, 0xFF0080};
memset(brctl_data, 0, REALTEK_BRIDGE_CTL_DATA_SIZE);
for(i = 0; i < get_argb_num(); i++)
{
brctl_data[i * single_size + 0] = 4;
brctl_data[i * single_size + 1] = row;
memcpy(&brctl_data[i * single_size + 2], &cycle, 2);
brctl_data[i * single_size + 8] = 3;
for(j = 0; j < row; j++)
{
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 32] = RGBGetRValue(rainbow_table[j]);
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 33] = RGBGetGValue(rainbow_table[j]);
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 34] = RGBGetBValue(rainbow_table[j]);
}
}
return set_brctl_data();
}
int RealtekBridgeController::eff_set_scroll(RGBColor rgb, int cycle)
{
int i;
int j;
int start_idx = -1;
int row = get_argb_num() * 2 - 2;
int maxrow = REALTEK_BRIDGE_MAX_APPCTL_ROW;
int single_size = maxrow * REALTEK_BRIDGE_COLOR_DEPTH + 32;
memset(brctl_data, 0, REALTEK_BRIDGE_CTL_DATA_SIZE);
for(i = 0; i < get_argb_num(); i++)
{
brctl_data[i * single_size + 0] = 2;
brctl_data[i * single_size + 1] = row;
memcpy(&brctl_data[i * single_size + 2], &cycle, 2);
brctl_data[i * single_size + 8] = 4;
if(start_idx == -1)
{
start_idx = i;
}
j = i - start_idx;
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 32] = RGBGetRValue(rgb);
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 33] = RGBGetGValue(rgb);
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 34] = RGBGetBValue(rgb);
if(i != start_idx)
{
j = row - (i - start_idx);
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 32] = RGBGetRValue(rgb);
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 33] = RGBGetGValue(rgb);
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 34] = RGBGetBValue(rgb);
}
}
return set_brctl_data();
}
int RealtekBridgeController::eff_set_rainbow_scroll(int cycle)
{
int i;
int j;
int start_idx = -1;
int row = get_argb_num() * 2 - 2;
int maxrow = REALTEK_BRIDGE_MAX_APPCTL_ROW;
int single_size = maxrow * REALTEK_BRIDGE_COLOR_DEPTH + 32;
RGBColor slide_table[REALTEK_BRIDGE_MAX_ARGB_NUM] = {0};
hsv_t hsv_color;
hsv_color.saturation = 255;
hsv_color.value = 255;
for(i = 0; i < get_argb_num(); i++)
{
hsv_color.hue = (i % get_argb_num()) * (360 / get_argb_num());
slide_table[i] = hsv2rgb(&hsv_color);
}
memset(brctl_data, 0, REALTEK_BRIDGE_CTL_DATA_SIZE);
for(i = 0; i < get_argb_num(); i++)
{
brctl_data[i * single_size + 0] = 2;
brctl_data[i * single_size + 1] = row;
memcpy(&brctl_data[i * single_size + 2], &cycle, 2);
brctl_data[i * single_size + 8] = 0x85;
if(start_idx == -1)
{
start_idx = i;
}
j = i - start_idx;
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 32] = RGBGetRValue(slide_table[i]);
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 33] = RGBGetGValue(slide_table[i]);
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 34] = RGBGetBValue(slide_table[i]);
if(i != start_idx)
{
j = row - (i - start_idx);
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 32] = RGBGetRValue(slide_table[i]);
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 33] = RGBGetGValue(slide_table[i]);
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 34] = RGBGetBValue(slide_table[i]);
}
}
return set_brctl_data();
}
int RealtekBridgeController::eff_set_running_water(RGBColor rgb, int cycle)
{
int i;
int j;
int start_idx = -1;
int row = get_argb_num() * 2;
int maxrow = REALTEK_BRIDGE_MAX_APPCTL_ROW;
int single_size = maxrow * REALTEK_BRIDGE_COLOR_DEPTH + 32;
memset(brctl_data, 0, REALTEK_BRIDGE_CTL_DATA_SIZE);
for(i = 0; i < get_argb_num(); i++)
{
brctl_data[i * single_size + 0] = 2;
brctl_data[i * single_size + 1] = row;
memcpy(&brctl_data[i * single_size + 2], &cycle, 2);
brctl_data[i * single_size + 8] = 5;
if(start_idx == -1)
{
start_idx = i;
}
for(j = i - start_idx; j < row / 2; j++)
{
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 32] = RGBGetRValue(rgb);
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 33] = RGBGetGValue(rgb);
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 34] = RGBGetBValue(rgb);
}
}
return set_brctl_data();
}
int RealtekBridgeController::eff_set_sliding(int cycle)
{
int i;
int j;
int start_idx = -1;
int row = get_argb_num();
int maxrow = REALTEK_BRIDGE_MAX_APPCTL_ROW;
int single_size = maxrow * REALTEK_BRIDGE_COLOR_DEPTH + 32;
RGBColor slide_table[REALTEK_BRIDGE_MAX_ARGB_NUM] = {0};
RGBColor rgb = 0;
hsv_t hsv_color;
hsv_color.saturation = 255;
hsv_color.value = 255;
for(i = 0; i < get_argb_num(); i++)
{
hsv_color.hue = (i % get_argb_num()) * (360 / get_argb_num());
slide_table[i] = hsv2rgb(&hsv_color);
}
memset(brctl_data, 0, REALTEK_BRIDGE_CTL_DATA_SIZE);
for(i = 0; i < get_argb_num(); i++)
{
brctl_data[i * single_size + 0] = 4;
brctl_data[i * single_size + 1] = row;
memcpy(&brctl_data[i * single_size + 2], &cycle, 2);
brctl_data[i * single_size + 8] = 0x83;
if(start_idx == -1)
{
start_idx = i;
}
for(j = 0; j < row; j++)
{
rgb = slide_table[(i - start_idx + j) % row];
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 32] = RGBGetRValue(rgb);
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 33] = RGBGetGValue(rgb);
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 34] = RGBGetBValue(rgb);
}
}
return set_brctl_data();
}
int RealtekBridgeController::eff_set_newton_cradle(int cycle)
{
int i;
int j;
int start_idx = -1;
int row;
int maxrow = REALTEK_BRIDGE_MAX_APPCTL_ROW;
int single_size = maxrow * REALTEK_BRIDGE_COLOR_DEPTH + 32;
RGBColor rgb = 0;
static const int const_ball_num = 3;
static const RGBColor ball_color[] = {0x0080FF, 0x0000FF, 0x00FF00, 0xFF0000};
static const int mid = get_argb_num() >> 1;
if(get_argb_num() <= const_ball_num)
{
return 0;
}
row = (get_argb_num() - const_ball_num) * 2;
memset(brctl_data, 0, REALTEK_BRIDGE_CTL_DATA_SIZE);
for(i = 0; i < get_argb_num(); i++)
{
brctl_data[i * single_size + 0] = 2;
brctl_data[i * single_size + 1] = row;
memcpy(&brctl_data[i * single_size + 2], &cycle, 2);
brctl_data[i * single_size + 8] = 0x84;
if(start_idx == -1)
{
start_idx = i;
}
for(j = 0; j < row; j++)
{
rgb = 0;
if(i - start_idx > mid - 2 &&
i - start_idx < mid + 2)
{
if(j < mid - 1 || j > row - mid)
{
if(i - start_idx == mid - 1)
{
rgb = ball_color[1];
}
else if(i - start_idx == mid)
{
rgb = ball_color[2];
}
else
{
rgb = ball_color[3];
}
}
else
{
if(i - start_idx == mid - 1)
{
rgb = ball_color[0];
}
else if(i - start_idx == mid)
{
rgb = ball_color[1];
}
else
{
rgb = ball_color[2];
}
}
}
else if(i - start_idx < mid)
{
if(i - start_idx == j || i - start_idx + j == row - 1)
{
rgb = ball_color[0];
}
}
else if(i - start_idx > mid + 1)
{
if(i - start_idx - j == const_ball_num ||
i - start_idx + j == row - 1 + const_ball_num)
{
rgb = ball_color[3];
}
}
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 32] = RGBGetRValue(rgb);
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 33] = RGBGetGValue(rgb);
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 34] = RGBGetBValue(rgb);
}
}
return set_brctl_data();
}
int RealtekBridgeController::eff_set_meteor(RGBColor rgb, int cycle)
{
int i;
int j;
int row = 2;
int maxrow = REALTEK_BRIDGE_MAX_APPCTL_ROW;
int single_size = maxrow * REALTEK_BRIDGE_COLOR_DEPTH + 32;
int stable_ms = cycle / row;
int latency = stable_ms / get_argb_num();
memset(brctl_data, 0, REALTEK_BRIDGE_CTL_DATA_SIZE);
for(i = 0; i < get_argb_num(); i++)
{
brctl_data[i * single_size + 0] = 4;
brctl_data[i * single_size + 1] = row;
brctl_data[i * single_size + 6] = stable_ms / 100;
brctl_data[i * single_size + 7] = stable_ms / 100;
brctl_data[i * single_size + 8] = 6;
memcpy(&brctl_data[i * single_size + 9], &latency, 2);
j = 0;
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 32] = RGBGetRValue(rgb);
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 33] = RGBGetGValue(rgb);
brctl_data[i * single_size + j * REALTEK_BRIDGE_COLOR_DEPTH + 34] = RGBGetBValue(rgb);
}
return set_brctl_data();
}
int RealtekBridgeController::set_argb_direct(std::vector<RGBColor> color_buf, unsigned short brightness)
{
int ret = -1;
size_t color_num = color_buf.size();
size_t buf_len = color_num * REALTEK_BRIDGE_COLOR_DEPTH;
static unsigned short prev_bright = 0xFFFF;
unsigned char* buf;
if(color_num <= 0)
{
goto exit;
}
ret = set_appctl(true);
if(ret)
{
goto exit;
}
if(prev_bright != brightness)
{
prev_bright = brightness;
ret = set_argb_brightness(brightness << 8);
if(ret)
{
goto exit;
}
}
buf = (unsigned char*)malloc(buf_len);
memset(buf, 0, buf_len);
for(int i = 0; i < (int)color_num; i++)
{
buf[i * REALTEK_BRIDGE_COLOR_DEPTH + 0] = RGBGetRValue(color_buf[i]);
buf[i * REALTEK_BRIDGE_COLOR_DEPTH + 1] = RGBGetGValue(color_buf[i]);
buf[i * REALTEK_BRIDGE_COLOR_DEPTH + 2] = RGBGetBValue(color_buf[i]);
}
ret = set_direct(buf, (int)color_num);
free(buf);
if(ret)
{
goto exit;
}
exit:
return ret;
}
int RealtekBridgeController::set_argb_effect(unsigned char mode, std::vector<RGBColor> color_buf, int speed, unsigned short brightness)
{
int ret = -1;
int cycle = MathUtils::IntInterpolate(REALTEK_BRIDGE_CYCLE_MAX, REALTEK_BRIDGE_CYCLE_MIN, 0, REALTEK_BRIDGE_SPEED_MAX, speed);
RGBColor rgb = 0;
static unsigned short prev_bright = 0xFFFF;
if(color_buf.size() >= 1)
{
rgb = color_buf[0];
}
if(prev_bright != brightness)
{
prev_bright = brightness;
ret = set_argb_brightness(brightness << 8);
if(ret)
{
goto exit;
}
}
switch(mode)
{
case REALTEK_BRIDGE_LED_EFF_ALWAYS:
ret = eff_set_always_on(rgb);
break;
case REALTEK_BRIDGE_LED_EFF_BLINK:
ret = eff_set_blink(rgb, cycle);
break;
case REALTEK_BRIDGE_LED_EFF_BREATHE:
ret = eff_set_breathe(rgb, cycle);
break;
case REALTEK_BRIDGE_LED_EFF_SPECTRUM:
ret = eff_set_spectrum(cycle);
break;
case REALTEK_BRIDGE_LED_EFF_SCROLL:
ret = eff_set_scroll(rgb, cycle);
break;
case REALTEK_BRIDGE_LED_EFF_RAINBOW_SCROLL:
ret = eff_set_rainbow_scroll(cycle);
break;
case REALTEK_BRIDGE_LED_EFF_RUNNING_WATER:
ret = eff_set_running_water(rgb, cycle);
break;
case REALTEK_BRIDGE_LED_EFF_SLIDING:
ret = eff_set_sliding(cycle);
break;
case REALTEK_BRIDGE_LED_EFF_NEWTON_CRADLE:
ret = eff_set_newton_cradle(cycle);
break;
case REALTEK_BRIDGE_LED_EFF_METEOR:
ret = eff_set_meteor(rgb, cycle);
break;
default:
break;
}
if(!ret)
{
ret = set_appctl(false);
}
exit:
return ret;
}
@@ -0,0 +1,126 @@
/*---------------------------------------------------------*\
| RealtekBridgeController.h |
| |
| Controller for Realtek USB to SSD Bridge ICs |
| |
| Jerry Fan (JerryFan0612) 13 Mar 2026 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#pragma once
#include <stdlib.h>
#include <stdint.h>
#include <cmath>
#include <cstring>
#include <vector>
#include <hidapi.h>
#include "RGBController.h"
#define REALTEK_BRIDGE_SYNC_METHOD_OPENRGB 6
#define REALTEK_BRIDGE_COLOR_DEPTH 3
#define REALTEK_BRIDGE_CTL_HDR_SIZE 64
#define REALTEK_BRIDGE_CTL_DATA_SIZE 3220
#define REALTEK_BRIDGE_MAX_ARGB_NUM 20
#define REALTEK_BRIDGE_MAX_APPCTL_ROW 43
#define REALTEK_BRIDGE_HID_DATALEN_CH1 4096
#define REALTEK_BRIDGE_HID_DATALEN_CH2 64
#define REALTEK_BRIDGE_HID_DATALEN_CH3 512
#define REALTEK_BRIDGE_HID_DATALEN_CH4 1200
#define REALTEK_BRIDGE_HID_DATALEN_CH5 2048
#define REALTEK_BRIDGE_HID_ID_DATA_CH1 0x57
#define REALTEK_BRIDGE_HID_ID_DATA_CH2 0x59
#define REALTEK_BRIDGE_HID_ID_DATA_CH3 0x5A
#define REALTEK_BRIDGE_HID_ID_DATA_CH4 0x5B
#define REALTEK_BRIDGE_HID_ID_DATA_CH5 0x5C
enum REALTEK_BRIDGE_LED_EFF
{
REALTEK_BRIDGE_LED_EFF_NONE,
REALTEK_BRIDGE_LED_EFF_ALWAYS,
REALTEK_BRIDGE_LED_EFF_BLINK,
REALTEK_BRIDGE_LED_EFF_BREATHE,
REALTEK_BRIDGE_LED_EFF_SPECTRUM,
REALTEK_BRIDGE_LED_EFF_SCROLL,
REALTEK_BRIDGE_LED_EFF_RAINBOW_SCROLL,
REALTEK_BRIDGE_LED_EFF_RUNNING_WATER,
REALTEK_BRIDGE_LED_EFF_SLIDING,
REALTEK_BRIDGE_LED_EFF_NEWTON_CRADLE,
REALTEK_BRIDGE_LED_EFF_METEOR,
REALTEK_BRIDGE_NUMBER_OF_LED_EFF_MODE,
};
enum REALTEK_BRIDGE_SPEED
{
REALTEK_BRIDGE_SPEED_MIN = 1,
REALTEK_BRIDGE_SPEED_NORMAL = 50,
REALTEK_BRIDGE_SPEED_MAX = 100,
};
enum REALTEK_BRIDGE_CYCLE_MS
{
REALTEK_BRIDGE_CYCLE_MIN = 200,
REALTEK_BRIDGE_CYCLE_NORMAL = 2000,
REALTEK_BRIDGE_CYCLE_MAX = 10000,
};
struct RealtekBridgeControllerFWVersion
{
unsigned int fw_major_ver;
unsigned int fw_minor_ver;
unsigned int fw_extra_ver;
unsigned int fw_build_ver;
unsigned int fw_build_date;
};
class RealtekBridgeController
{
public:
RealtekBridgeController(hid_device* dev, hid_device_info* info);
~RealtekBridgeController();
unsigned char get_support_openrgb();
std::string get_manu_name();
std::string get_product_name();
std::string get_sn();
std::string get_dev_loc();
std::string get_fw_ver();
int get_argb_num();
int get_argb_brightness();
int set_argb_brightness(unsigned short bright);
int set_argb_direct(std::vector<RGBColor> color_buf, unsigned short brightness);
int set_argb_effect(unsigned char mode, std::vector<RGBColor> color_buf, int speed, unsigned short brightness);
private:
hid_device* hdev;
hid_device_info* hidinfo = NULL;
unsigned char brctl_hdr[REALTEK_BRIDGE_CTL_HDR_SIZE] = {0};
unsigned char* brctl_data = NULL;
int usb_hid_ioctl(unsigned char* usb_buf, unsigned char* data, int data_len,
unsigned int offset, unsigned char is_in);
int usb_hid_get_report(int data_len, int* id);
int set_write_unlock();
int get_brctl_hdr();
int set_brctl_hdr();
int get_brctl_data();
int set_brctl_data();
int set_appctl(unsigned char ctl_sts);
int set_direct(unsigned char* color, int color_num);
int eff_set_always_on(RGBColor rgb);
int eff_set_blink(RGBColor rgb, int cycle);
int eff_set_breathe(RGBColor rgb, int cycle);
int eff_set_spectrum(int cycle);
int eff_set_scroll(RGBColor rgb, int cycle);
int eff_set_rainbow_scroll(int cycle);
int eff_set_running_water(RGBColor rgb, int cycle);
int eff_set_sliding(int cycle);
int eff_set_newton_cradle(int cycle);
int eff_set_meteor(RGBColor rgb, int cycle);
};
@@ -0,0 +1,61 @@
/*---------------------------------------------------------*\
| RealtekBridgeControllerDetect.cpp |
| |
| Controller for Realtek USB to SSD Bridge ICs |
| |
| Jerry Fan (JerryFan0612) 13 Mar 2026 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include "Detector.h"
#include "RGBController_RealtekBridge.h"
#define REALTEK_BRIDGE_VID 0x0BDA
#define REALTEK_BRIDGE_PID0 0x9220
#define REALTEK_BRIDGE_PID1 0x9201
#define REALTEK_BRIDGE_PID2 0x9210
#define REALTEK_HID2SCSI_PG 0xFF00
#define REALTEK_HID2SCSI_USAGE 0x0001
/******************************************************************************************\
* *
* DetectRealtekBridgeControllers *
* *
* Tests the USB address to see if an Realtek Bridge controller exists there *
* *
\******************************************************************************************/
void DetectRealtekBridgeControllers(hid_device_info* info, const std::string& /*name*/)
{
RealtekBridgeController* controller = NULL;
RGBController_RealtekBridge* rgb_controller = NULL;
hid_device* dev = hid_open_path(info->path);
if(dev)
{
controller = new RealtekBridgeController(dev, info);
if(controller->get_support_openrgb())
{
rgb_controller = new RGBController_RealtekBridge(controller);
if(rgb_controller->type != DEVICE_TYPE_UNKNOWN)
{
ResourceManager::get()->RegisterRGBController(rgb_controller);
}
else
{
delete rgb_controller;
}
}
else
{
delete controller;
}
}
return;
}
REGISTER_HID_DETECTOR_PU("RTL9220", DetectRealtekBridgeControllers, REALTEK_BRIDGE_VID, REALTEK_BRIDGE_PID0, REALTEK_HID2SCSI_PG, REALTEK_HID2SCSI_USAGE);
REGISTER_HID_DETECTOR_PU("RTL9201", DetectRealtekBridgeControllers, REALTEK_BRIDGE_VID, REALTEK_BRIDGE_PID1, REALTEK_HID2SCSI_PG, REALTEK_HID2SCSI_USAGE);
REGISTER_HID_DETECTOR_PU("RTL9210", DetectRealtekBridgeControllers, REALTEK_BRIDGE_VID, REALTEK_BRIDGE_PID2, REALTEK_HID2SCSI_PG, REALTEK_HID2SCSI_USAGE);
@@ -28,6 +28,7 @@
#define REDRAGON_M801_PID 0xFC58
#define REDRAGON_M810_PID 0xFA7E
#define REDRAGON_M987_PID 0xFC69
#define REDRAGON_M921_PID 0xFC40
/******************************************************************************************\
* *
@@ -62,3 +63,4 @@ REGISTER_HID_DETECTOR_IP("Redragon M808 Storm", DetectRedragonMice, RE
REGISTER_HID_DETECTOR_IP("Redragon M801 Sniper", DetectRedragonMice, REDRAGON_MOUSE_VID, REDRAGON_M801_PID, 2, REDRAGON_MOUSE_USAGE_PAGE);
REGISTER_HID_DETECTOR_IP("Redragon M810 Taipan", DetectRedragonMice, REDRAGON_MOUSE_VID, REDRAGON_M810_PID, 2, REDRAGON_MOUSE_USAGE_PAGE);
REGISTER_HID_DETECTOR_IP("Redragon M987 Reaping", DetectRedragonMice, REDRAGON_MOUSE_VID, REDRAGON_M987_PID, 2, REDRAGON_MOUSE_USAGE_PAGE);
REGISTER_HID_DETECTOR_IP("Redragon M921 Azzinoth", DetectRedragonMice, REDRAGON_MOUSE_VID, REDRAGON_M921_PID, 2, REDRAGON_MOUSE_USAGE_PAGE);
@@ -86,9 +86,14 @@ device_info RoccatVulcanKeyboardController::InitDeviceInfo()
buf[0] = report_id;
hid_get_feature_report(dev_ctrl, buf, packet_length);
char version[5];
snprintf(version, 5, "%d.%02d", buf[2] / 100, buf[2] % 100);
/*-------------------------------------------------------------*\
| buf[2] is version e.g. 103 means v1.03 |
\*-------------------------------------------------------------*/
dev_info.version_major = buf[2] / 100;
dev_info.version_minor = buf[2] % 100;
char version[5];
snprintf(version, 5, "%d.%02d", dev_info.version_major, dev_info.version_minor);
dev_info.version = version;
if(device_pid == ROCCAT_MAGMA_PID || device_pid == ROCCAT_MAGMA_MINI_PID)
@@ -116,6 +121,11 @@ device_info RoccatVulcanKeyboardController::GetDeviceInfo()
return dev_info;
}
bool RoccatVulcanKeyboardController::IsBigEndianDirectMode()
{
return (dev_info.version_major >= 1 && dev_info.version_minor >= 16);
}
void RoccatVulcanKeyboardController::EnableDirect(bool on_off_switch)
{
uint8_t* buf;
@@ -218,8 +228,16 @@ void RoccatVulcanKeyboardController::SendColors(std::vector<led_color> colors)
}
else
{
bufs[0][3] = packet_length & 0xFF;
bufs[0][4] = (packet_length >> 8) & 0xFF;
if(IsBigEndianDirectMode())
{
bufs[0][3] = (packet_length >> 8) & 0xFF;
bufs[0][4] = packet_length & 0xFF;
}
else
{
bufs[0][3] = packet_length & 0xFF;
bufs[0][4] = (packet_length >> 8) & 0xFF;
}
}
unsigned int data_length_packet = 64 - header_length_first;
@@ -61,6 +61,8 @@ enum
struct device_info
{
std::string version;
int version_major;
int version_minor;
int layout_type;
};
@@ -89,6 +91,7 @@ public:
void AwaitResponse(int ms);
void ClearResponses();
uint16_t device_pid;
private:
@@ -97,4 +100,6 @@ private:
device_info dev_info;
std::string location;
std::string name;
bool IsBigEndianDirectMode();
};
@@ -120,13 +120,13 @@
#define STEELSERIES_APEX_PRO_TKL_2023_PID 0x1628
#define STEELSERIES_APEX_PRO_TKL_2023_WL_PID_1 0x1630
#define STEELSERIES_APEX_PRO_TKL_2023_WL_PID_2 0x1632
#define STEELSERIES_APEX_PRO_TKL_GEN3_PID 0x1642
#define STEELSERIES_APEX_PRO_TKL_GEN3_WL_PID_1 0x1644
#define STEELSERIES_APEX_PRO_TKL_GEN3_WL_PID_2 0x1646
#define STEELSERIES_APEX_M750_PID 0x0616
#define STEELSERIES_APEX_OG_PID 0x1202
#define STEELSERIES_APEX_350_PID 0x1206
#define STEELSERIES_APEX_PRO3_PID 0x1640
#define STEELSERIES_APEX_PRO_TKL_GEN3_PID 0x1642
#define STEELSERIES_APEX_PRO_TKL_GEN3_WL_PID_1 0x1644
#define STEELSERIES_APEX_PRO_TKL_GEN3_WL_PID_2 0x1646
void DetectSteelSeriesAerox3(hid_device_info* info, const std::string& name)
{
@@ -516,10 +516,10 @@ REGISTER_HID_DETECTOR_I ("SteelSeries Apex Pro TKL", Dete
REGISTER_HID_DETECTOR_I ("SteelSeries Apex Pro TKL 2023 Wired", DetectSteelSeriesApex, STEELSERIES_VID, STEELSERIES_APEX_PRO_TKL_2023_PID, 1 );
REGISTER_HID_DETECTOR_IPU("SteelSeries Apex Pro TKL 2023 Wireless", DetectSteelSeriesApex, STEELSERIES_VID, STEELSERIES_APEX_PRO_TKL_2023_WL_PID_1, 3, 0xFFC0, 1);
REGISTER_HID_DETECTOR_IPU("SteelSeries Apex Pro TKL 2023 Wireless", DetectSteelSeriesApex, STEELSERIES_VID, STEELSERIES_APEX_PRO_TKL_2023_WL_PID_2, 3, 0xFFC0, 1);
REGISTER_HID_DETECTOR_I ("SteelSeries Apex Pro TKL Gen 3 Wired", DetectSteelSeriesApex, STEELSERIES_VID, STEELSERIES_APEX_PRO_TKL_GEN3_PID, 1 );
REGISTER_HID_DETECTOR_IPU("SteelSeries Apex Pro TKL Gen 3 Wireless", DetectSteelSeriesApex, STEELSERIES_VID, STEELSERIES_APEX_PRO_TKL_GEN3_WL_PID_1, 3, 0xFFC0, 1);
REGISTER_HID_DETECTOR_IPU("SteelSeries Apex Pro TKL Gen 3 Wireless", DetectSteelSeriesApex, STEELSERIES_VID, STEELSERIES_APEX_PRO_TKL_GEN3_WL_PID_2, 3, 0xFFC0, 1);
REGISTER_HID_DETECTOR_I ("SteelSeries Apex M750", DetectSteelSeriesApexM, STEELSERIES_VID, STEELSERIES_APEX_M750_PID, 2 );
REGISTER_HID_DETECTOR_I ("SteelSeries Apex (OG)/Apex Fnatic", DetectSteelSeriesApexOld, STEELSERIES_VID, STEELSERIES_APEX_OG_PID, 0 );
REGISTER_HID_DETECTOR_I ("SteelSeries Apex 350", DetectSteelSeriesApexOld, STEELSERIES_VID, STEELSERIES_APEX_350_PID, 0 );
REGISTER_HID_DETECTOR_I ("SteelSeries Apex Pro 3", DetectSteelSeriesApex, STEELSERIES_VID, STEELSERIES_APEX_PRO3_PID, 1 );
REGISTER_HID_DETECTOR_I ("SteelSeries Apex Pro TKL Gen 3", DetectSteelSeriesApex, STEELSERIES_VID, STEELSERIES_APEX_PRO_TKL_GEN3_PID, 1 );
REGISTER_HID_DETECTOR_IPU("SteelSeries Apex Pro TKL Gen 3 Wireless", DetectSteelSeriesApex, STEELSERIES_VID, STEELSERIES_APEX_PRO_TKL_GEN3_WL_PID_1, 3, 0xFFC0, 1);
REGISTER_HID_DETECTOR_IPU("SteelSeries Apex Pro TKL Gen 3 Wireless", DetectSteelSeriesApex, STEELSERIES_VID, STEELSERIES_APEX_PRO_TKL_GEN3_WL_PID_2, 3, 0xFFC0, 1);
@@ -46,7 +46,7 @@ std::string ThermaltakeRiingController::GetSerialString()
std::string ThermaltakeRiingController::GetFirmwareVersion()
{
unsigned char usb_buf[64];
unsigned char usb_buf[65];
/*-----------------------------------------------------*\
| Zero out buffer |
@@ -56,14 +56,15 @@ std::string ThermaltakeRiingController::GetFirmwareVersion()
/*-----------------------------------------------------*\
| Set up Get Firmware Version packet |
\*-----------------------------------------------------*/
usb_buf[0x00] = 0x33;
usb_buf[0x01] = 0x50;
usb_buf[0x00] = 0x00;
usb_buf[0x01] = 0x33;
usb_buf[0x02] = 0x50;
/*-----------------------------------------------------*\
| Send packet |
\*-----------------------------------------------------*/
hid_write(dev, usb_buf, 64);
hid_read(dev, usb_buf, 64);
hid_write(dev, usb_buf, 65);
hid_read_timeout(dev, usb_buf, 65, 100);
std::string ret_str = std::to_string(usb_buf[2]) + "." + std::to_string(usb_buf[3]) + "." + std::to_string(usb_buf[4]);
@@ -117,7 +118,7 @@ void ThermaltakeRiingController::SendInit()
| Send packet |
\*-----------------------------------------------------*/
hid_write(dev, usb_buf, 65);
hid_read(dev, usb_buf, 65);
hid_read_timeout(dev, usb_buf, 65, 100);
}
void ThermaltakeRiingController::SendRGB
@@ -154,5 +155,5 @@ void ThermaltakeRiingController::SendRGB
| Send packet |
\*-----------------------------------------------------*/
hid_write(dev, usb_buf, 65);
hid_read(dev, usb_buf, 65);
hid_read_timeout(dev, usb_buf, 65, 100);
}
@@ -0,0 +1,353 @@
/*---------------------------------------------------------*\
| RGBController_ThrustmasterSol.cpp |
| |
| RGBController for Thrustmaster Sol series joysticks |
| |
| Ken Sanislo 02 Apr 2026 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include "RGBController_ThrustmasterSol.h"
/**------------------------------------------------------------------*\
@name Thrustmaster Sol
@category Gamepad
@type USB
@save :white_check_mark:
@direct :white_check_mark:
@effects :x:
@detectors DetectThrustmasterSolControllers
@comment Thrustmaster Sol series joystick RGB LED control. Supports
Sol-R, Sol F16, and Sol F18 variants. Only Sol-R has been tested.
Uses vendor-specific USB interface 1 (not HID).
\*-------------------------------------------------------------------*/
#define NA 0xFFFFFFFF
static unsigned int logo_matrix_map[2][2] =
{
{ 0, 3 },
{ 1, 2 },
};
static unsigned int ring_matrix_map[3][5] =
{
{ NA, 7, 0, 1, NA },
{ 6, NA, NA, NA, 2 },
{ NA, 5, 4, 3, NA },
};
static unsigned int right_buttons_matrix_map[2][2] =
{
{ 0, 1 },
{ 2, 3 },
};
static unsigned int left_buttons_matrix_map[2][2] =
{
{ 0, 1 },
{ 2, 3 },
};
RGBController_ThrustmasterSol::RGBController_ThrustmasterSol(ThrustmasterSolController* controller_ptr)
{
controller = controller_ptr;
vendor = "Thrustmaster";
type = DEVICE_TYPE_GAMEPAD;
description = "Thrustmaster Sol Series Joystick";
location = controller->GetDeviceLocation();
serial = controller->GetSerialString();
mode Direct;
Direct.name = "Direct";
Direct.value = 0;
Direct.flags = MODE_FLAG_HAS_PER_LED_COLOR | MODE_FLAG_MANUAL_SAVE;
Direct.color_mode = MODE_COLORS_PER_LED;
modes.push_back(Direct);
SetupZones();
/*---------------------------------------------------------*\
| Read current EEPROM colors from device so the UI starts |
| with the actual hardware state rather than all-black |
\*---------------------------------------------------------*/
std::vector<unsigned int> hw_zones;
std::vector<RGBColor> hw_colors;
controller->ReadColors(hw_zones, hw_colors);
for(unsigned int i = 0; i < hw_zones.size(); i++)
{
for(unsigned int j = 0; j < leds.size(); j++)
{
if(leds[j].value == hw_zones[i])
{
colors[j] = hw_colors[i];
break;
}
}
}
}
RGBController_ThrustmasterSol::~RGBController_ThrustmasterSol()
{
delete controller;
}
void RGBController_ThrustmasterSol::SetupZones()
{
/*---------------------------------------------------------*\
| Thumbstick zone (grip, 1 LED) |
\*---------------------------------------------------------*/
zone thumbstick_zone;
thumbstick_zone.name = "Thumbstick";
thumbstick_zone.type = ZONE_TYPE_SINGLE;
thumbstick_zone.leds_min = 1;
thumbstick_zone.leds_max = 1;
thumbstick_zone.leds_count = 1;
thumbstick_zone.matrix_map = NULL;
zones.push_back(thumbstick_zone);
led thumbstick_led;
thumbstick_led.name = "Thumbstick";
thumbstick_led.value = THRUSTMASTER_SOL_GRIP_FLAG | 0x00;
leds.push_back(thumbstick_led);
/*---------------------------------------------------------*\
| TM Logo zone (3 LEDs) |
\*---------------------------------------------------------*/
zone logo_zone;
logo_zone.name = "TM Logo";
logo_zone.type = ZONE_TYPE_MATRIX;
logo_zone.leds_min = 4;
logo_zone.leds_max = 4;
logo_zone.leds_count = 4;
logo_zone.matrix_map = new matrix_map_type;
logo_zone.matrix_map->height = 2;
logo_zone.matrix_map->width = 2;
logo_zone.matrix_map->map = (unsigned int *)&logo_matrix_map;
zones.push_back(logo_zone);
led logo_top_left;
logo_top_left.name = "TM Logo Top Left";
logo_top_left.value = 0x01;
leds.push_back(logo_top_left);
led logo_bottom_left;
logo_bottom_left.name = "TM Logo Bottom Left";
logo_bottom_left.value = 0x02;
leds.push_back(logo_bottom_left);
led logo_bottom_right;
logo_bottom_right.name = "TM Logo Bottom Right";
logo_bottom_right.value = 0x03;
leds.push_back(logo_bottom_right);
led logo_top_right;
logo_top_right.name = "TM Logo Top Right";
logo_top_right.value = 0x00;
leds.push_back(logo_top_right);
/*---------------------------------------------------------*\
| Left Buttons zone (4 LEDs: buttons 5-8) |
\*---------------------------------------------------------*/
zone left_buttons_zone;
left_buttons_zone.name = "Left Buttons";
left_buttons_zone.type = ZONE_TYPE_MATRIX;
left_buttons_zone.leds_min = 4;
left_buttons_zone.leds_max = 4;
left_buttons_zone.leds_count = 4;
left_buttons_zone.matrix_map = new matrix_map_type;
left_buttons_zone.matrix_map->height = 2;
left_buttons_zone.matrix_map->width = 2;
left_buttons_zone.matrix_map->map = (unsigned int *)&left_buttons_matrix_map;
zones.push_back(left_buttons_zone);
led btn5;
btn5.name = "Button 5";
btn5.value = 0x11;
leds.push_back(btn5);
led btn6;
btn6.name = "Button 6";
btn6.value = 0x10;
leds.push_back(btn6);
led btn7;
btn7.name = "Button 7";
btn7.value = 0x12;
leds.push_back(btn7);
led btn8;
btn8.name = "Button 8";
btn8.value = 0x13;
leds.push_back(btn8);
/*---------------------------------------------------------*\
| Base Ring zone (8 LEDs, clockwise from top) |
\*---------------------------------------------------------*/
zone ring_zone;
ring_zone.name = "Base Ring";
ring_zone.type = ZONE_TYPE_MATRIX;
ring_zone.leds_min = 8;
ring_zone.leds_max = 8;
ring_zone.leds_count = 8;
ring_zone.matrix_map = new matrix_map_type;
ring_zone.matrix_map->height = 3;
ring_zone.matrix_map->width = 5;
ring_zone.matrix_map->map = (unsigned int *)&ring_matrix_map;
zones.push_back(ring_zone);
led ring_upper;
ring_upper.name = "Upper";
ring_upper.value = 0x04;
leds.push_back(ring_upper);
led ring_upper_right;
ring_upper_right.name = "Upper Right";
ring_upper_right.value = 0x05;
leds.push_back(ring_upper_right);
led ring_right;
ring_right.name = "Right";
ring_right.value = 0x06;
leds.push_back(ring_right);
led ring_bottom_right;
ring_bottom_right.name = "Bottom Right";
ring_bottom_right.value = 0x0B;
leds.push_back(ring_bottom_right);
led ring_bottom;
ring_bottom.name = "Bottom";
ring_bottom.value = 0x0C;
leds.push_back(ring_bottom);
led ring_bottom_left;
ring_bottom_left.name = "Bottom Left";
ring_bottom_left.value = 0x0D;
leds.push_back(ring_bottom_left);
led ring_left;
ring_left.name = "Left";
ring_left.value = 0x0E;
leds.push_back(ring_left);
led ring_upper_left;
ring_upper_left.name = "Upper Left";
ring_upper_left.value = 0x0F;
leds.push_back(ring_upper_left);
/*---------------------------------------------------------*\
| Right Buttons zone (4 LEDs: buttons 16-19) |
\*---------------------------------------------------------*/
zone right_buttons_zone;
right_buttons_zone.name = "Right Buttons";
right_buttons_zone.type = ZONE_TYPE_MATRIX;
right_buttons_zone.leds_min = 4;
right_buttons_zone.leds_max = 4;
right_buttons_zone.leds_count = 4;
right_buttons_zone.matrix_map = new matrix_map_type;
right_buttons_zone.matrix_map->height = 2;
right_buttons_zone.matrix_map->width = 2;
right_buttons_zone.matrix_map->map = (unsigned int *)&right_buttons_matrix_map;
zones.push_back(right_buttons_zone);
led btn17;
btn17.name = "Button 17";
btn17.value = 0x07;
leds.push_back(btn17);
led btn16;
btn16.name = "Button 16";
btn16.value = 0x08;
leds.push_back(btn16);
led btn19;
btn19.name = "Button 19";
btn19.value = 0x0A;
leds.push_back(btn19);
led btn18;
btn18.name = "Button 18";
btn18.value = 0x09;
leds.push_back(btn18);
SetupColors();
}
void RGBController_ThrustmasterSol::ResizeZone(int /*zone*/, int /*new_size*/)
{
/*---------------------------------------------------------*\
| This device does not support resizing zones |
\*---------------------------------------------------------*/
}
void RGBController_ThrustmasterSol::DeviceUpdateLEDs()
{
unsigned int led_zones[THRUSTMASTER_SOL_R_ZONE_COUNT];
RGBColor led_colors[THRUSTMASTER_SOL_R_ZONE_COUNT];
for(unsigned int led_idx = 0; led_idx < leds.size(); led_idx++)
{
led_zones[led_idx] = leds[led_idx].value;
led_colors[led_idx] = colors[led_idx];
}
controller->SetLEDColors(led_zones, led_colors, static_cast<unsigned int>(leds.size()));
}
void RGBController_ThrustmasterSol::UpdateZoneLEDs(int zone)
{
unsigned int start_idx = 0;
unsigned int zone_size = 0;
for(unsigned int z_idx = 0; z_idx < zones.size(); z_idx++)
{
if(z_idx == (unsigned int)zone)
{
zone_size = zones[z_idx].leds_count;
break;
}
start_idx += zones[z_idx].leds_count;
}
unsigned int led_zones[THRUSTMASTER_SOL_R_ZONE_COUNT];
RGBColor led_colors[THRUSTMASTER_SOL_R_ZONE_COUNT];
for(unsigned int led_idx = 0; led_idx < zone_size; led_idx++)
{
unsigned int current_idx = start_idx + led_idx;
led_zones[led_idx] = leds[current_idx].value;
led_colors[led_idx] = colors[current_idx];
}
controller->SetLEDColors(led_zones, led_colors, zone_size);
}
void RGBController_ThrustmasterSol::UpdateSingleLED(int led)
{
controller->SetLEDColor(leds[led].value, colors[led]);
}
void RGBController_ThrustmasterSol::DeviceUpdateMode()
{
DeviceUpdateLEDs();
}
void RGBController_ThrustmasterSol::DeviceSaveMode()
{
unsigned int led_zones[THRUSTMASTER_SOL_R_ZONE_COUNT];
RGBColor led_colors[THRUSTMASTER_SOL_R_ZONE_COUNT];
for(unsigned int led_idx = 0; led_idx < leds.size(); led_idx++)
{
led_zones[led_idx] = leds[led_idx].value;
led_colors[led_idx] = colors[led_idx];
}
controller->SaveColors(led_zones, led_colors, static_cast<unsigned int>(leds.size()));
}
@@ -0,0 +1,35 @@
/*---------------------------------------------------------*\
| RGBController_ThrustmasterSol.h |
| |
| RGBController for Thrustmaster Sol series joysticks |
| |
| Ken Sanislo 02 Apr 2026 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#pragma once
#include "RGBController.h"
#include "ThrustmasterSolController.h"
class RGBController_ThrustmasterSol : public RGBController
{
public:
RGBController_ThrustmasterSol(ThrustmasterSolController* controller_ptr);
~RGBController_ThrustmasterSol();
void SetupZones();
void ResizeZone(int zone, int new_size);
void DeviceUpdateLEDs();
void UpdateZoneLEDs(int zone);
void UpdateSingleLED(int led);
void DeviceUpdateMode();
void DeviceSaveMode();
private:
ThrustmasterSolController* controller;
};
@@ -0,0 +1,315 @@
/*---------------------------------------------------------*\
| ThrustmasterSolController.cpp |
| |
| Driver for Thrustmaster Sol series joysticks |
| |
| Ken Sanislo 02 Apr 2026 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include <cstring>
#include <thread>
#include <chrono>
#include "ThrustmasterSolController.h"
#include "StringUtils.h"
ThrustmasterSolController::ThrustmasterSolController(libusb_device_handle* dev_handle,
const char* path,
unsigned short pid)
{
dev = dev_handle;
this->pid = pid;
location = "USB: ";
location += path;
/*---------------------------------------------------------*\
| Get serial number string descriptor |
\*---------------------------------------------------------*/
libusb_device_descriptor desc;
libusb_get_device_descriptor(libusb_get_device(dev_handle), &desc);
if(desc.iSerialNumber != 0)
{
unsigned char serial_str[256];
int ret = libusb_get_string_descriptor_ascii(dev_handle,
desc.iSerialNumber,
serial_str,
sizeof(serial_str));
if(ret > 0)
{
serial = std::string(reinterpret_cast<char*>(serial_str), ret);
}
}
}
ThrustmasterSolController::~ThrustmasterSolController()
{
if(dev != nullptr)
{
libusb_release_interface(dev, THRUSTMASTER_SOL_INTERFACE);
libusb_close(dev);
}
}
std::string ThrustmasterSolController::GetDeviceLocation()
{
return(location);
}
std::string ThrustmasterSolController::GetSerialString()
{
return(serial);
}
unsigned short ThrustmasterSolController::GetPID()
{
return(pid);
}
void ThrustmasterSolController::SendPacket(unsigned char* packet, unsigned int size)
{
if(dev == nullptr)
{
return;
}
int actual_length = 0;
libusb_interrupt_transfer(dev,
THRUSTMASTER_SOL_ENDPOINT_OUT,
packet,
size,
&actual_length,
1000);
}
void ThrustmasterSolController::BuildAndSendPackets(unsigned int* zones,
RGBColor* colors,
unsigned int count,
bool persistent)
{
unsigned char persist_flag = persistent ? THRUSTMASTER_SOL_PERSISTENT
: THRUSTMASTER_SOL_VOLATILE;
/*---------------------------------------------------------*\
| Separate grip zones from base zones. Zone 0x00 exists on |
| both the base (logo LED) and the grip (thumbstick LED). |
| The GRIP_FLAG bit selects the grip report type. |
\*---------------------------------------------------------*/
unsigned char base_zones[THRUSTMASTER_SOL_R_ZONE_COUNT];
RGBColor base_colors[THRUSTMASTER_SOL_R_ZONE_COUNT];
unsigned int base_count = 0;
for(unsigned int i = 0; i < count; i++)
{
if(zones[i] & THRUSTMASTER_SOL_GRIP_FLAG)
{
/*-------------------------------------------------*\
| Send grip packet for thumbstick immediately |
\*-------------------------------------------------*/
unsigned char zone_id = zones[i] & 0xFF;
unsigned char packet[THRUSTMASTER_SOL_PACKET_SIZE];
memset(packet, 0x00, THRUSTMASTER_SOL_PACKET_SIZE);
packet[0] = THRUSTMASTER_SOL_REPORT_GRIP_LO;
packet[1] = THRUSTMASTER_SOL_REPORT_GRIP_HI;
packet[2] = persist_flag | 0x01;
packet[3] = THRUSTMASTER_SOL_MARKER;
packet[4] = zone_id;
packet[5] = RGBGetRValue(colors[i]);
packet[6] = RGBGetGValue(colors[i]);
packet[7] = RGBGetBValue(colors[i]);
SendPacket(packet, THRUSTMASTER_SOL_PACKET_SIZE);
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
else
{
base_zones[base_count] = zones[i] & 0xFF;
base_colors[base_count] = colors[i];
base_count++;
}
}
/*---------------------------------------------------------*\
| Send base zone packets, batching up to 15 entries each |
\*---------------------------------------------------------*/
for(unsigned int offset = 0; offset < base_count; offset += THRUSTMASTER_SOL_MAX_ENTRIES)
{
unsigned int entries = base_count - offset;
if(entries > THRUSTMASTER_SOL_MAX_ENTRIES)
{
entries = THRUSTMASTER_SOL_MAX_ENTRIES;
}
unsigned char packet[THRUSTMASTER_SOL_PACKET_SIZE];
memset(packet, 0x00, THRUSTMASTER_SOL_PACKET_SIZE);
packet[0] = THRUSTMASTER_SOL_REPORT_BASE_LO;
packet[1] = THRUSTMASTER_SOL_REPORT_BASE_HI;
packet[2] = persist_flag | (unsigned char)entries;
packet[3] = THRUSTMASTER_SOL_MARKER;
for(unsigned int j = 0; j < entries; j++)
{
unsigned int idx = offset + j;
packet[4 + j * 4] = base_zones[idx];
packet[4 + j * 4 + 1] = RGBGetRValue(base_colors[idx]);
packet[4 + j * 4 + 2] = RGBGetGValue(base_colors[idx]);
packet[4 + j * 4 + 3] = RGBGetBValue(base_colors[idx]);
}
SendPacket(packet, THRUSTMASTER_SOL_PACKET_SIZE);
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
}
void ThrustmasterSolController::SetLEDColor(unsigned int zone, RGBColor color)
{
BuildAndSendPackets(&zone, &color, 1, false);
}
void ThrustmasterSolController::SetLEDColors(unsigned int* zones,
RGBColor* colors,
unsigned int count)
{
BuildAndSendPackets(zones, colors, count, false);
}
void ThrustmasterSolController::SaveColors(unsigned int* zones,
RGBColor* colors,
unsigned int count)
{
BuildAndSendPackets(zones, colors, count, true);
}
void ThrustmasterSolController::ReadColors(std::vector<unsigned int>& zones,
std::vector<RGBColor>& colors)
{
if(dev == nullptr)
{
return;
}
zones.clear();
colors.clear();
/*---------------------------------------------------------*\
| Read base zones (report 0x0002). May require multiple |
| queries if more than 14 zones are returned per response. |
| Zone IDs are returned without the grip flag, so they |
| match the base LED values (including zone 0x00 = logo). |
\*---------------------------------------------------------*/
unsigned char start = 0;
for(int page = 0; page < 4; page++)
{
unsigned char query[THRUSTMASTER_SOL_PACKET_SIZE];
memset(query, 0x00, THRUSTMASTER_SOL_PACKET_SIZE);
query[0] = THRUSTMASTER_SOL_READ_BASE_LO;
query[1] = THRUSTMASTER_SOL_READ_BASE_HI;
query[2] = start;
SendPacket(query, THRUSTMASTER_SOL_PACKET_SIZE);
unsigned char resp[THRUSTMASTER_SOL_PACKET_SIZE];
int actual = 0;
int ret = libusb_interrupt_transfer(dev,
THRUSTMASTER_SOL_ENDPOINT_IN,
resp,
THRUSTMASTER_SOL_PACKET_SIZE,
&actual,
1000);
if(ret != LIBUSB_SUCCESS || actual < 4)
{
break;
}
unsigned int n = resp[2] & 0x0F;
if(n == 0)
{
break;
}
unsigned char last = start;
for(unsigned int i = 0; i < n; i++)
{
unsigned int off = 4 + i * 4;
if(off + 3 >= (unsigned int)actual)
{
break;
}
unsigned char zid = resp[off];
unsigned char r = resp[off + 1];
unsigned char g = resp[off + 2];
unsigned char b = resp[off + 3];
zones.push_back(zid);
colors.push_back(ToRGBColor(r, g, b));
last = zid;
}
start = last + 1;
if(n < 14 || start > 0x20)
{
break;
}
}
/*---------------------------------------------------------*\
| Read grip zones (report 0x8002). Grip zone IDs are |
| returned with the GRIP_FLAG set so they match the |
| thumbstick LED value. They are added as new entries, |
| not overwriting base entries (zone 0x00 exists on both). |
\*---------------------------------------------------------*/
unsigned char grip_query[THRUSTMASTER_SOL_PACKET_SIZE];
memset(grip_query, 0x00, THRUSTMASTER_SOL_PACKET_SIZE);
grip_query[0] = THRUSTMASTER_SOL_READ_GRIP_LO;
grip_query[1] = THRUSTMASTER_SOL_READ_GRIP_HI;
SendPacket(grip_query, THRUSTMASTER_SOL_PACKET_SIZE);
unsigned char grip_resp[THRUSTMASTER_SOL_PACKET_SIZE];
int grip_actual = 0;
int grip_ret = libusb_interrupt_transfer(dev,
THRUSTMASTER_SOL_ENDPOINT_IN,
grip_resp,
THRUSTMASTER_SOL_PACKET_SIZE,
&grip_actual,
1000);
if(grip_ret == LIBUSB_SUCCESS && grip_actual >= 8)
{
unsigned int grip_n = grip_resp[2] & 0x0F;
for(unsigned int i = 0; i < grip_n; i++)
{
unsigned int off = 4 + i * 4;
if(off + 3 >= (unsigned int)grip_actual)
{
break;
}
unsigned char zid = grip_resp[off];
unsigned char r = grip_resp[off + 1];
unsigned char g = grip_resp[off + 2];
unsigned char b = grip_resp[off + 3];
zones.push_back(THRUSTMASTER_SOL_GRIP_FLAG | zid);
colors.push_back(ToRGBColor(r, g, b));
}
}
}
@@ -0,0 +1,116 @@
/*---------------------------------------------------------*\
| ThrustmasterSolController.h |
| |
| Driver for Thrustmaster Sol series joysticks |
| |
| Ken Sanislo 02 Apr 2026 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#pragma once
#include <string>
#include <vector>
#include "RGBController.h"
#ifdef _WIN32
#include "dependencies/libusb-1.0.27/include/libusb.h"
#else
#include <libusb.h>
#endif
/*-----------------------------------------------------*\
| Thrustmaster vendor ID |
\*-----------------------------------------------------*/
#define THRUSTMASTER_VID 0x044F
/*-----------------------------------------------------*\
| Thrustmaster Sol series product IDs |
\*-----------------------------------------------------*/
#define THRUSTMASTER_SOL_R_RIGHT_PID 0x0422
#define THRUSTMASTER_SOL_R_LEFT_PID 0x042A
#define THRUSTMASTER_SOL_F16_RIGHT_PID 0x0420
#define THRUSTMASTER_SOL_F18_RIGHT_PID 0x0421
#define THRUSTMASTER_SOL_F16_LEFT_PID 0x0428
#define THRUSTMASTER_SOL_F18_LEFT_PID 0x0429
/*-----------------------------------------------------*\
| Thrustmaster Sol USB constants |
\*-----------------------------------------------------*/
#define THRUSTMASTER_SOL_INTERFACE 1
#define THRUSTMASTER_SOL_ENDPOINT_OUT 0x02
#define THRUSTMASTER_SOL_ENDPOINT_IN 0x82
#define THRUSTMASTER_SOL_PACKET_SIZE 64
#define THRUSTMASTER_SOL_MAX_ENTRIES 15
/*-----------------------------------------------------*\
| Thrustmaster Sol report type identifiers |
\*-----------------------------------------------------*/
#define THRUSTMASTER_SOL_REPORT_BASE_LO 0x01
#define THRUSTMASTER_SOL_REPORT_BASE_HI 0x08
#define THRUSTMASTER_SOL_REPORT_GRIP_LO 0x01
#define THRUSTMASTER_SOL_REPORT_GRIP_HI 0x88
/*-----------------------------------------------------*\
| Thrustmaster Sol persistence flags |
\*-----------------------------------------------------*/
#define THRUSTMASTER_SOL_VOLATILE 0x00
#define THRUSTMASTER_SOL_PERSISTENT 0x80
/*-----------------------------------------------------*\
| Thrustmaster Sol read query report types |
\*-----------------------------------------------------*/
#define THRUSTMASTER_SOL_READ_BASE_LO 0x02
#define THRUSTMASTER_SOL_READ_BASE_HI 0x00
#define THRUSTMASTER_SOL_READ_GRIP_LO 0x02
#define THRUSTMASTER_SOL_READ_GRIP_HI 0x80
/*-----------------------------------------------------*\
| Thrustmaster Sol zone constants |
| |
| Zone 0x00 is shared: on the base it is a logo LED, |
| on the grip it is the thumbstick LED. The grip flag |
| (bit 8) selects the grip report type for zone 0x00. |
\*-----------------------------------------------------*/
#define THRUSTMASTER_SOL_ZONE_THUMBSTICK 0x00
#define THRUSTMASTER_SOL_GRIP_FLAG 0x100
#define THRUSTMASTER_SOL_MARKER 0xFF
/*-----------------------------------------------------*\
| Thrustmaster Sol zone count for Sol-R |
| 19 base zones + thumbstick + logo zone 0x00 = 21 |
\*-----------------------------------------------------*/
#define THRUSTMASTER_SOL_R_ZONE_COUNT 21
class ThrustmasterSolController
{
public:
ThrustmasterSolController(libusb_device_handle* dev_handle,
const char* path,
unsigned short pid);
~ThrustmasterSolController();
std::string GetDeviceLocation();
std::string GetSerialString();
unsigned short GetPID();
void SetLEDColor(unsigned int zone, RGBColor color);
void SetLEDColors(unsigned int* zones, RGBColor* colors,
unsigned int count);
void SaveColors(unsigned int* zones, RGBColor* colors,
unsigned int count);
void ReadColors(std::vector<unsigned int>& zones,
std::vector<RGBColor>& colors);
private:
libusb_device_handle* dev;
std::string location;
std::string serial;
unsigned short pid;
void SendPacket(unsigned char* packet, unsigned int size);
void BuildAndSendPackets(unsigned int* zones, RGBColor* colors,
unsigned int count, bool persistent);
};
@@ -0,0 +1,125 @@
/*---------------------------------------------------------*\
| ThrustmasterSolControllerDetect.cpp |
| |
| Detector for Thrustmaster Sol series joysticks |
| |
| Ken Sanislo 02 Apr 2026 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include "Detector.h"
#include "ThrustmasterSolController.h"
#include "RGBController_ThrustmasterSol.h"
typedef struct
{
unsigned short usb_vid;
unsigned short usb_pid;
const char* name;
} thrustmaster_sol_device;
#define THRUSTMASTER_SOL_NUM_DEVICES (sizeof(device_list) / sizeof(device_list[0]))
static const thrustmaster_sol_device device_list[] =
{
/*-----------------------------------------------------------------------------------------------------*\
| Sol-R variants (tested) |
\*-----------------------------------------------------------------------------------------------------*/
{ THRUSTMASTER_VID, THRUSTMASTER_SOL_R_RIGHT_PID, "Thrustmaster Sol-R Right" },
{ THRUSTMASTER_VID, THRUSTMASTER_SOL_R_LEFT_PID, "Thrustmaster Sol-R Left" },
/*-----------------------------------------------------------------------------------------------------*\
| Sol F16/F18 variants (untested, same base protocol) |
\*-----------------------------------------------------------------------------------------------------*/
{ THRUSTMASTER_VID, THRUSTMASTER_SOL_F16_RIGHT_PID, "Thrustmaster Sol F16 Right" },
{ THRUSTMASTER_VID, THRUSTMASTER_SOL_F18_RIGHT_PID, "Thrustmaster Sol F18 Right" },
{ THRUSTMASTER_VID, THRUSTMASTER_SOL_F16_LEFT_PID, "Thrustmaster Sol F16 Left" },
{ THRUSTMASTER_VID, THRUSTMASTER_SOL_F18_LEFT_PID, "Thrustmaster Sol F18 Left" },
};
/******************************************************************************************\
* *
* DetectThrustmasterSolControllers *
* *
* Detect Thrustmaster Sol series joysticks via libusb. *
* LED control is on USB interface 1 (vendor-specific class 255, not HID). *
* Interface 0 (joystick HID) is not touched. *
* *
\******************************************************************************************/
void DetectThrustmasterSolControllers()
{
libusb_init(NULL);
#ifdef _WIN32
libusb_set_option(NULL, LIBUSB_OPTION_USE_USBDK);
#endif
libusb_device** devs;
ssize_t num_devs = libusb_get_device_list(NULL, &devs);
if(num_devs <= 0)
{
return;
}
for(ssize_t i = 0; i < num_devs; i++)
{
libusb_device_descriptor desc;
if(libusb_get_device_descriptor(devs[i], &desc) != 0)
{
continue;
}
for(std::size_t d = 0; d < THRUSTMASTER_SOL_NUM_DEVICES; d++)
{
if(desc.idVendor == device_list[d].usb_vid &&
desc.idProduct == device_list[d].usb_pid)
{
libusb_device_handle* handle = NULL;
if(libusb_open(devs[i], &handle) != LIBUSB_SUCCESS)
{
continue;
}
libusb_set_auto_detach_kernel_driver(handle, 1);
if(libusb_claim_interface(handle, THRUSTMASTER_SOL_INTERFACE) != LIBUSB_SUCCESS)
{
libusb_close(handle);
continue;
}
uint8_t bus = libusb_get_bus_number(devs[i]);
uint8_t address = libusb_get_device_address(devs[i]);
char path[32];
snprintf(path, sizeof(path), "%d-%d", bus, address);
ThrustmasterSolController* controller = new ThrustmasterSolController(handle, path, desc.idProduct);
RGBController_ThrustmasterSol* rgb_controller = new RGBController_ThrustmasterSol(controller);
rgb_controller->name = device_list[d].name;
ResourceManager::get()->RegisterRGBController(rgb_controller);
break;
}
}
}
libusb_free_device_list(devs, 1);
}
REGISTER_DETECTOR("Thrustmaster Sol", DetectThrustmasterSolControllers);
/*---------------------------------------------------------------------------------------------------------*\
| Entries for dynamic UDEV rules |
| |
| DUMMY_DEVICE_DETECTOR("Thrustmaster Sol", DetectThrustmasterSolControllers, 0x044F, 0x0420 ) |
| DUMMY_DEVICE_DETECTOR("Thrustmaster Sol", DetectThrustmasterSolControllers, 0x044F, 0x0421 ) |
| DUMMY_DEVICE_DETECTOR("Thrustmaster Sol", DetectThrustmasterSolControllers, 0x044F, 0x0422 ) |
| DUMMY_DEVICE_DETECTOR("Thrustmaster Sol", DetectThrustmasterSolControllers, 0x044F, 0x0428 ) |
| DUMMY_DEVICE_DETECTOR("Thrustmaster Sol", DetectThrustmasterSolControllers, 0x044F, 0x0429 ) |
| DUMMY_DEVICE_DETECTOR("Thrustmaster Sol", DetectThrustmasterSolControllers, 0x044F, 0x042A ) |
\*---------------------------------------------------------------------------------------------------------*/
@@ -187,50 +187,21 @@ static const char *led_names[] =
\*---------------------------------------------------------*/
RGBController_XPGSummoner::RGBController_XPGSummoner(XPGSummonerController *controller_ptr)
{
controller = controller_ptr;
name = controller->GetNameString();
vendor = "XPG";
description = "XPG Summoner Keyboard Device";
location = controller->GetLocationString();
serial = controller->GetSerialString();
type = DEVICE_TYPE_KEYBOARD;
controller = controller_ptr;
name = controller->GetNameString();
vendor = "XPG";
description = "XPG Summoner Keyboard Device";
location = controller->GetLocationString();
serial = controller->GetSerialString();
type = DEVICE_TYPE_KEYBOARD;
mode Direct;
Direct.name = "Direct";
Direct.value = XPG_SUMMONER_MODE_DIRECT;
Direct.flags = MODE_FLAG_HAS_PER_LED_COLOR;
Direct.color_mode = MODE_COLORS_PER_LED;
Direct.name = "Direct";
Direct.value = XPG_SUMMONER_MODE_DIRECT;
Direct.flags = MODE_FLAG_HAS_PER_LED_COLOR;
Direct.color_mode = MODE_COLORS_PER_LED;
modes.push_back(Direct);
mode Static;
Static.name = "Static";
Static.value = XPG_SUMMONER_MODE_STATIC;
Static.flags = MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS;
Static.color_mode = MODE_COLORS_MODE_SPECIFIC;
Static.colors_min = 1;
Static.colors_max = 1;
Static.brightness_min = 5;
Static.brightness_max = 64;
Static.colors.resize(1);
modes.push_back(Static);
mode Stars;
Stars.name = "Stars";
Stars.value = XPG_SUMMONER_MODE_STARS;
Stars.flags = MODE_FLAG_HAS_MODE_SPECIFIC_COLOR;
Stars.colors_min = 1;
Stars.colors_max = 1;
Stars.color_mode = MODE_COLORS_MODE_SPECIFIC;
Stars.colors.resize(1);
modes.push_back(Stars);
mode Off;
Off.name = "Off";
Off.value = XPG_SUMMONER_MODE_OFF;
Off.flags = 0;
Off.color_mode = MODE_COLORS_NONE;
modes.push_back(Off);
SetupZones();
}
@@ -321,7 +292,9 @@ void RGBController_XPGSummoner::DeviceUpdateLEDs()
for(std::size_t led_idx = 0; led_idx < leds.size(); led_idx++)
{
if(leds[led_idx].value == NA)
{
continue;
}
if(modes[active_mode].color_mode == MODE_COLORS_PER_LED)
{
std::size_t real_idx = leds[led_idx].value;
@@ -330,76 +303,6 @@ void RGBController_XPGSummoner::DeviceUpdateLEDs()
frame_buf[(real_idx * 4) + 2] = RGBGetGValue(colors[led_idx]);
frame_buf[(real_idx * 4) + 3] = RGBGetBValue(colors[led_idx]);
}
else if(modes[active_mode].color_mode == MODE_COLORS_MODE_SPECIFIC && modes[active_mode].value == XPG_SUMMONER_MODE_STATIC)
{
std::size_t real_idx = leds[led_idx].value;
frame_buf[(real_idx * 4) + 0] = modes[active_mode].brightness;
frame_buf[(real_idx * 4) + 1] = RGBGetRValue(modes[active_mode].colors[0]);
frame_buf[(real_idx * 4) + 2] = RGBGetGValue(modes[active_mode].colors[0]);
frame_buf[(real_idx * 4) + 3] = RGBGetBValue(modes[active_mode].colors[0]);
}
else if(modes[active_mode].color_mode == MODE_COLORS_MODE_SPECIFIC && modes[active_mode].value == XPG_SUMMONER_MODE_STARS)
{
for(std::size_t i = 0; i < leds.size(); ++i)
{
std::size_t real_idx = leds[i].value;
frame_buf[(real_idx * 4) + 0] = 0;
frame_buf[(real_idx * 4) + 1] = 0;
frame_buf[(real_idx * 4) + 2] = 0;
frame_buf[(real_idx * 4) + 3] = 0;
}
controller->SendColors(frame_buf, sizeof(frame_buf));
/*---------------------------------------------------*\
| Select a random central LED in the physical matrix |
\*---------------------------------------------------*/
int rows = 6;
int cols = 21;
int center_row = rand() % rows;
int center_col = rand() % cols;
unsigned int center_led = matrix_map[center_row][center_col];
if(center_led != NA)
{
frame_buf[(center_led * 4) + 0] = brightness;
frame_buf[(center_led * 4) + 1] = RGBGetRValue(modes[active_mode].colors[0]);
frame_buf[(center_led * 4) + 2] = RGBGetGValue(modes[active_mode].colors[0]);
frame_buf[(center_led * 4) + 3] = RGBGetBValue(modes[active_mode].colors[0]);
}
/*---------------------------------------------------*\
| Neighbors (fade effect) |
\*---------------------------------------------------*/
unsigned char fade_brightness = 0x32;
int neighbor_offsets[4][2] = { {0, -1}, {0, +1}, {-1, 0}, {+1, 0} };
for(int k = 0; k < 4; ++k)
{
int n_row = center_row + neighbor_offsets[k][0];
int n_col = center_col + neighbor_offsets[k][1];
if(n_row >= 0 && n_row < rows && n_col >= 0 && n_col < cols)
{
unsigned int neighbor_led = matrix_map[n_row][n_col];
if(neighbor_led != NA)
{
frame_buf[(neighbor_led * 4) + 0] = fade_brightness;
frame_buf[(neighbor_led * 4) + 1] = RGBGetRValue(modes[active_mode].colors[0]);
frame_buf[(neighbor_led * 4) + 2] = RGBGetGValue(modes[active_mode].colors[0]);
frame_buf[(neighbor_led * 4) + 3] = RGBGetBValue(modes[active_mode].colors[0]);
}
}
}
controller->SendColors(frame_buf, sizeof(frame_buf));
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
else if(modes[active_mode].color_mode == MODE_COLORS_NONE)
{
std::size_t real_idx = leds[led_idx].value;
frame_buf[(real_idx * 4) + 0] = 0;
frame_buf[(real_idx * 4) + 1] = 0;
frame_buf[(real_idx * 4) + 2] = 0;
frame_buf[(real_idx * 4) + 3] = 0;
}
}
controller->SendColors(frame_buf, sizeof(frame_buf));
}
@@ -27,10 +27,7 @@
enum
{
XPG_SUMMONER_MODE_OFF = 0x00,
XPG_SUMMONER_MODE_DIRECT = 0x01,
XPG_SUMMONER_MODE_STATIC = 0x02,
XPG_SUMMONER_MODE_STARS = 0x03
XPG_SUMMONER_MODE_DIRECT = 0x01
};
class XPGSummonerController
@@ -10,6 +10,8 @@
\*---------------------------------------------------------*/
#include "RGBController_ZotacBlackwellGPU.h"
#include "LogManager.h"
#include "pci_ids.h"
/**------------------------------------------------------------------*\
@name ZOTAC RTX 50 series GPU
@@ -20,21 +22,60 @@
@effects :tools:
@detectors DetectZotacBlackwellGPUControllersPCI
@comment
Supports ZOTAC Blackwell (RTX 50 series) GPUs with 3 zones:
Logo, Side Bar, and Infinity Mirror.
Supports ZOTAC Blackwell (RTX 50 series) GPUs. The zone layout
varies per card and is resolved from a static table keyed on
PCI device and sub-device IDs.
The controller uses individual SMBus byte writes (registers
0x20-0x2F) with a 3ms delay between each transaction.
To add new cards, add PCI ID entries in `pci_ids/pci_ids.h`
and detection entries in
To add new cards, add PCI ID entries in `pci_ids/pci_ids.h`,
a zone config entry in `device_zone_configs` below, and a
`REGISTER_I2C_PCI_DETECTOR` line in
`Controllers/ZotacBlackwellGPUController/ZotacBlackwellGPUControllerDetect.cpp`.
\*-------------------------------------------------------------------*/
RGBController_ZotacBlackwellGPU::RGBController_ZotacBlackwellGPU(ZotacBlackwellGPUController* controller_ptr)
const RGBController_ZotacBlackwellGPU::DeviceZoneConfig RGBController_ZotacBlackwellGPU::device_zone_configs[] =
{
{ NVIDIA_RTX5080_DEV, ZOTAC_RTX5080_AMP_EXTREME_SUB_DEV, { "Logo", "Side Bar", "Infinity Mirror" }, 3 },
{ NVIDIA_RTX5090_DEV, ZOTAC_RTX5090_SOLID_OC_SUB_DEV, { "ZOTAC Gaming", "Logo" }, 2 },
{ 0, 0, { nullptr }, 0 }
};
const RGBController_ZotacBlackwellGPU::DeviceZoneConfig* RGBController_ZotacBlackwellGPU::FindZoneConfig(uint16_t device, uint16_t subdevice)
{
for(const DeviceZoneConfig* cfg = device_zone_configs; cfg->zone_count != 0; cfg++)
{
if(cfg->device == device && cfg->subdevice == subdevice)
{
return cfg;
}
}
return nullptr;
}
RGBController_ZotacBlackwellGPU::RGBController_ZotacBlackwellGPU(ZotacBlackwellGPUController* controller_ptr,
uint16_t device, uint16_t subdevice)
{
controller = controller_ptr;
const DeviceZoneConfig* cfg = FindZoneConfig(device, subdevice);
if(cfg == nullptr)
{
LOG_ERROR("[%s] Unrecognized PCI device/subdevice: %04X/%04X. Falling back to three generic zones.",
controller->GetName().c_str(), device, subdevice);
zone_names.push_back("Zone 0");
zone_names.push_back("Zone 1");
zone_names.push_back("Zone 2");
}
else
{
for(uint8_t z = 0; z < cfg->zone_count; z++)
{
zone_names.push_back(cfg->zones[z]);
}
}
name = controller->GetName();
vendor = "ZOTAC";
description = "ZOTAC RTX 50 series RGB GPU Device (" + controller->GetVersion() + ")";
@@ -252,52 +293,25 @@ RGBController_ZotacBlackwellGPU::~RGBController_ZotacBlackwellGPU()
void RGBController_ZotacBlackwellGPU::SetupZones()
{
/*---------------------------------------------------------*\
| Zone 0: Logo |
| One single-LED zone per name from the device_zone_configs |
| table. The zone's index is its position here, which is |
| written verbatim to the zone register (0x21) on update. |
\*---------------------------------------------------------*/
zone logo_zone;
logo_zone.name = "Logo";
logo_zone.type = ZONE_TYPE_SINGLE;
logo_zone.leds_min = 1;
logo_zone.leds_max = 1;
logo_zone.leds_count = 1;
logo_zone.matrix_map = NULL;
zones.push_back(logo_zone);
for(const std::string& zone_name : zone_names)
{
zone new_zone;
new_zone.name = zone_name;
new_zone.type = ZONE_TYPE_SINGLE;
new_zone.leds_min = 1;
new_zone.leds_max = 1;
new_zone.leds_count = 1;
new_zone.matrix_map = NULL;
zones.push_back(new_zone);
led logo_led;
logo_led.name = "Logo LED";
leds.push_back(logo_led);
/*---------------------------------------------------------*\
| Zone 1: Side Bar |
\*---------------------------------------------------------*/
zone sidebar_zone;
sidebar_zone.name = "Side Bar";
sidebar_zone.type = ZONE_TYPE_SINGLE;
sidebar_zone.leds_min = 1;
sidebar_zone.leds_max = 1;
sidebar_zone.leds_count = 1;
sidebar_zone.matrix_map = NULL;
zones.push_back(sidebar_zone);
led sidebar_led;
sidebar_led.name = "Side Bar LED";
leds.push_back(sidebar_led);
/*---------------------------------------------------------*\
| Zone 2: Infinity Mirror |
\*---------------------------------------------------------*/
zone infinity_zone;
infinity_zone.name = "Infinity Mirror";
infinity_zone.type = ZONE_TYPE_SINGLE;
infinity_zone.leds_min = 1;
infinity_zone.leds_max = 1;
infinity_zone.leds_count = 1;
infinity_zone.matrix_map = NULL;
zones.push_back(infinity_zone);
led infinity_led;
infinity_led.name = "Infinity Mirror LED";
leds.push_back(infinity_led);
led new_led;
new_led.name = zone_name + " LED";
leds.push_back(new_led);
}
SetupColors();
}
@@ -362,7 +376,7 @@ void RGBController_ZotacBlackwellGPU::DeviceUpdateZone(int zone)
void RGBController_ZotacBlackwellGPU::DeviceUpdateMode()
{
for(unsigned int zone_idx = 0; zone_idx < ZOTAC_BLACKWELL_GPU_NUM_ZONES; zone_idx++)
for(unsigned int zone_idx = 0; zone_idx < zones.size(); zone_idx++)
{
DeviceUpdateZone(zone_idx);
}
@@ -11,13 +11,17 @@
#pragma once
#include <stdint.h>
#include <string>
#include <vector>
#include "RGBController.h"
#include "ZotacBlackwellGPUController.h"
class RGBController_ZotacBlackwellGPU : public RGBController
{
public:
RGBController_ZotacBlackwellGPU(ZotacBlackwellGPUController* controller_ptr);
RGBController_ZotacBlackwellGPU(ZotacBlackwellGPUController* controller_ptr,
uint16_t device, uint16_t subdevice);
~RGBController_ZotacBlackwellGPU();
void SetupZones();
@@ -33,4 +37,17 @@ public:
private:
ZotacBlackwellGPUController* controller;
std::vector<std::string> zone_names;
struct DeviceZoneConfig
{
uint16_t device;
uint16_t subdevice;
const char* zones[4];
uint8_t zone_count;
};
static const DeviceZoneConfig device_zone_configs[];
static const DeviceZoneConfig* FindZoneConfig(uint16_t device, uint16_t subdevice);
};

Some files were not shown because too many files have changed in this diff Show More