summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Glushkov <david.glushkov@sntiq.com>2026-06-05 19:50:35 +0200
committerJiri Kosina <jkosina@suse.com>2026-06-29 10:52:45 +0200
commit2346d6997b70dae1f8302fd029aa422e28112eaa (patch)
treebae0c0a37a8c5cb2f10f6b30758b447e904b99aa
parentb7556c8e713c88596046a906c7c4385218d44736 (diff)
HID: steelseries: Add MSI Raider A18 HX A9WJG RGB support
The MSI Raider A18 HX A9WJG exposes two internal SteelSeries USB HID devices for RGB lighting: KLC (1038:1122) for the keyboard and ALC (1038:1161) for the lightbar/logo zones. Add DMI-gated support for these devices and expose them as multicolor LED class devices. The driver sends the same HID class SET_REPORT control transfer as the tested userspace implementation for this machine and writes a uniform RGB value to all known keyboard keys or ALC zones. The ALC payload uses sparse LED IDs on this chassis: 0x00, 0x01, 0x02 and 0x03 are physical zones, while 0x04 and 0x05 do not appear to map to physical LEDs. Unused payload LED ID slots are initialized to 0xff so they are ignored by the controller instead of defaulting to LED ID 0x00. Limit RGB support to USB interface 0 and the tested DMI system because the KLC product ID is shared across MSI laptop designs and the key layout mapping is model-specific. If the DMI or interface check does not match, keep the device bound as a regular HID device instead of failing probe. Also make the existing Arctis 9 vendor usage-page check defensive by returning false for report descriptors shorter than three bytes before inspecting hdev->rdesc[0..2]. Tested on MSI Raider A18 HX A9WJG. Both internal SteelSeries ALC (1038:1161) and KLC (1038:1122) HID devices bind on interface 0 and create steelseries::lightbar and steelseries::kbd_backlight. Setting multi_intensity and brightness changes the keyboard and lightbar colors. Reported-by: kernel test robot <lkp@intel.com> Closes: https://lore.kernel.org/oe-kbuild-all/202606010709.X0QYNjFZ-lkp@intel.com/ Signed-off-by: David Glushkov <david.glushkov@sntiq.com> Signed-off-by: Jiri Kosina <jkosina@suse.com>
-rw-r--r--drivers/hid/hid-ids.h2
-rw-r--r--drivers/hid/hid-steelseries.c313
2 files changed, 306 insertions, 9 deletions
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 1059922baaac..b70f719b3b07 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1379,6 +1379,8 @@
#define USB_DEVICE_ID_STEELSERIES_SRWS1 0x1410
#define USB_DEVICE_ID_STEELSERIES_ARCTIS_1 0x12b6
#define USB_DEVICE_ID_STEELSERIES_ARCTIS_9 0x12c2
+#define USB_DEVICE_ID_STEELSERIES_MSI_KLC 0x1122
+#define USB_DEVICE_ID_STEELSERIES_MSI_ALC 0x1161
#define USB_VENDOR_ID_SUN 0x0430
#define USB_DEVICE_ID_RARITAN_KVM_DONGLE 0xcdab
diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index f98435631aa1..73f77dd07110 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -10,16 +10,30 @@
*/
#include <linux/device.h>
+#include <linux/dmi.h>
#include <linux/hid.h>
#include <linux/module.h>
#include <linux/usb.h>
#include <linux/leds.h>
+#include <linux/led-class-multicolor.h>
+#include <linux/slab.h>
#include "hid-ids.h"
#define STEELSERIES_SRWS1 BIT(0)
#define STEELSERIES_ARCTIS_1 BIT(1)
#define STEELSERIES_ARCTIS_9 BIT(2)
+#define STEELSERIES_MSI_RGB BIT(3)
+
+#define STEELSERIES_MSI_RGB_WVALUE 0x0300 /* Feature report, ID 0 */
+#define STEELSERIES_MSI_RGB_REPORT_LEN 524
+#define STEELSERIES_MSI_RGB_OPCODE 0x0c
+#define STEELSERIES_MSI_RGB_KLC_MODE 0x66
+#define STEELSERIES_MSI_RGB_ALC_MODE 0x06
+
+#define STEELSERIES_HAS_LEDS_MULTICOLOR \
+ (IS_BUILTIN(CONFIG_LEDS_CLASS_MULTICOLOR) || \
+ (IS_MODULE(CONFIG_LEDS_CLASS_MULTICOLOR) && IS_MODULE(CONFIG_HID_STEELSERIES)))
struct steelseries_device {
struct hid_device *hdev;
@@ -34,6 +48,14 @@ struct steelseries_device {
uint8_t battery_capacity;
bool headset_connected;
bool battery_charging;
+ bool battery_registered;
+
+#if STEELSERIES_HAS_LEDS_MULTICOLOR
+ struct led_classdev_mc mc_cdev;
+ struct mc_subled subled_info[3];
+ struct mutex rgb_lock; /* protects rgb_buf */
+ u8 *rgb_buf;
+#endif
};
#if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
@@ -510,6 +532,8 @@ static int steelseries_headset_battery_register(struct steelseries_device *sd)
power_supply_powers(sd->battery, &sd->hdev->dev);
INIT_DELAYED_WORK(&sd->battery_work, steelseries_headset_battery_timer_tick);
+ /* Pairs with smp_load_acquire() in raw_event and remove paths */
+ smp_store_release(&sd->battery_registered, true);
steelseries_headset_fetch_battery(sd->hdev);
if (sd->quirks & STEELSERIES_ARCTIS_9) {
@@ -523,11 +547,230 @@ static int steelseries_headset_battery_register(struct steelseries_device *sd)
static bool steelseries_is_vendor_usage_page(struct hid_device *hdev, uint8_t usage_page)
{
+ if (hdev->rsize < 3)
+ return false;
+
return hdev->rdesc[0] == 0x06 &&
hdev->rdesc[1] == usage_page &&
hdev->rdesc[2] == 0xff;
}
+static const struct dmi_system_id steelseries_msi_rgb_dmi_table[] = {
+ {
+ .matches = {
+ DMI_MATCH(DMI_SYS_VENDOR, "Micro-Star International Co., Ltd."),
+ DMI_MATCH(DMI_PRODUCT_NAME, "Raider A18 HX A9WJG"),
+ DMI_MATCH(DMI_BOARD_NAME, "MS-182L"),
+ },
+ },
+ { }
+};
+
+static struct usb_interface *steelseries_hid_to_usb_intf(struct hid_device *hdev)
+{
+ if (!hid_is_usb(hdev))
+ return NULL;
+
+ return to_usb_interface(hdev->dev.parent);
+}
+
+static bool steelseries_msi_rgb_is_interface0(struct hid_device *hdev)
+{
+ struct usb_interface *intf = steelseries_hid_to_usb_intf(hdev);
+ struct usb_device *udev;
+
+ if (!intf)
+ return false;
+
+ udev = interface_to_usbdev(intf);
+
+ return intf == usb_ifnum_to_if(udev, 0);
+}
+
+#if STEELSERIES_HAS_LEDS_MULTICOLOR
+
+static struct usb_device *steelseries_hid_to_usb_dev(struct hid_device *hdev)
+{
+ struct usb_interface *intf = steelseries_hid_to_usb_intf(hdev);
+
+ if (!intf)
+ return NULL;
+
+ return interface_to_usbdev(intf);
+}
+
+static int steelseries_msi_rgb_set_blocking(struct led_classdev *led_cdev,
+ enum led_brightness brightness)
+{
+ struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(led_cdev);
+ struct steelseries_device *sd = container_of(mc_cdev,
+ struct steelseries_device,
+ mc_cdev);
+ struct hid_device *hdev = sd->hdev;
+ struct usb_device *udev = steelseries_hid_to_usb_dev(hdev);
+ int i, ret;
+ u8 r, g, b;
+
+ static const u8 keys[] = {
+ 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
+ 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13,
+ 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b,
+ 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23,
+ 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b,
+ 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x33, 0x34,
+ 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c,
+ 0x3d, 0x3e, 0x3f, 0x40, 0x41, 0x42, 0x43, 0x44,
+ 0x45, 0x46, 0x47, 0x49, 0x4b, 0x4c, 0x4e, 0x4f,
+ 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57,
+ 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
+ 0x60, 0x61, 0x62, 0x63, 0x64, 0x66, 0xe0, 0xe1,
+ 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xf0
+ };
+ static const u8 alc_zones[] = { 0x00, 0x01, 0x02, 0x03 };
+
+ if (!udev)
+ return -ENODEV;
+
+ mutex_lock(&sd->rgb_lock);
+
+ led_mc_calc_color_components(mc_cdev, brightness);
+
+ r = mc_cdev->subled_info[0].brightness;
+ g = mc_cdev->subled_info[1].brightness;
+ b = mc_cdev->subled_info[2].brightness;
+
+ /*
+ * Report layout (524 bytes):
+ * Byte 0: Opcode (0x0c)
+ * Byte 1: 0x00
+ * Byte 2: Mode (0x66 for Keyboard, 0x06 for Lightbar)
+ * Byte 3: 0x00
+ * Bytes 4+: 4-byte chunks per LED (Index, R, G, B)
+ */
+ memset(sd->rgb_buf, 0, STEELSERIES_MSI_RGB_REPORT_LEN);
+ sd->rgb_buf[0] = STEELSERIES_MSI_RGB_OPCODE;
+ sd->rgb_buf[1] = 0x00;
+ sd->rgb_buf[3] = 0x00;
+
+ for (i = 0; i < (STEELSERIES_MSI_RGB_REPORT_LEN - 4) / 4; i++)
+ sd->rgb_buf[4 + i * 4] = 0xff;
+
+ if (hdev->product == USB_DEVICE_ID_STEELSERIES_MSI_KLC) {
+ sd->rgb_buf[2] = STEELSERIES_MSI_RGB_KLC_MODE;
+ for (i = 0; i < ARRAY_SIZE(keys); i++) {
+ sd->rgb_buf[4 + i * 4] = keys[i];
+ sd->rgb_buf[5 + i * 4] = r;
+ sd->rgb_buf[6 + i * 4] = g;
+ sd->rgb_buf[7 + i * 4] = b;
+ }
+ } else {
+ sd->rgb_buf[2] = STEELSERIES_MSI_RGB_ALC_MODE;
+ for (i = 0; i < ARRAY_SIZE(alc_zones); i++) {
+ sd->rgb_buf[4 + i * 4] = alc_zones[i];
+ sd->rgb_buf[5 + i * 4] = r;
+ sd->rgb_buf[6 + i * 4] = g;
+ sd->rgb_buf[7 + i * 4] = b;
+ }
+ }
+
+ /*
+ * Send the vendor report verbatim with usb_control_msg(): byte 0 is a
+ * protocol opcode (0x0c), not a HID report ID, and the controller
+ * expects it under report ID 0 (wValue 0x0300). hid_hw_raw_request()
+ * would write the report number into byte 0, so the direct control
+ * transfer is used to keep the payload byte-identical to the tested
+ * userspace implementation.
+ */
+ ret = hid_hw_power(hdev, PM_HINT_FULLON);
+ if (ret < 0)
+ goto out_unlock;
+
+ ret = usb_control_msg(udev, usb_sndctrlpipe(udev, 0),
+ HID_REQ_SET_REPORT,
+ USB_DIR_OUT | USB_TYPE_CLASS | USB_RECIP_INTERFACE,
+ STEELSERIES_MSI_RGB_WVALUE, 0,
+ sd->rgb_buf, STEELSERIES_MSI_RGB_REPORT_LEN,
+ USB_CTRL_SET_TIMEOUT);
+
+ hid_hw_power(hdev, PM_HINT_NORMAL);
+
+out_unlock:
+ mutex_unlock(&sd->rgb_lock);
+ return ret < 0 ? ret : 0;
+}
+
+static void steelseries_msi_rgb_free_buf(void *data)
+{
+ kfree(data);
+}
+
+static int steelseries_msi_rgb_register(struct steelseries_device *sd)
+{
+ struct hid_device *hdev = sd->hdev;
+ struct led_classdev *led_cdev;
+ int ret;
+
+ sd->rgb_buf = kzalloc(STEELSERIES_MSI_RGB_REPORT_LEN, GFP_KERNEL);
+ if (!sd->rgb_buf)
+ return -ENOMEM;
+
+ ret = devm_add_action_or_reset(&hdev->dev,
+ steelseries_msi_rgb_free_buf,
+ sd->rgb_buf);
+ if (ret) {
+ sd->rgb_buf = NULL;
+ return ret;
+ }
+
+ ret = devm_mutex_init(&hdev->dev, &sd->rgb_lock);
+ if (ret) {
+ devm_remove_action(&hdev->dev, steelseries_msi_rgb_free_buf,
+ sd->rgb_buf);
+ kfree(sd->rgb_buf);
+ sd->rgb_buf = NULL;
+ return ret;
+ }
+
+ sd->subled_info[0].color_index = LED_COLOR_ID_RED;
+ sd->subled_info[1].color_index = LED_COLOR_ID_GREEN;
+ sd->subled_info[2].color_index = LED_COLOR_ID_BLUE;
+ sd->subled_info[0].intensity = 255;
+ sd->subled_info[1].intensity = 255;
+ sd->subled_info[2].intensity = 255;
+ sd->subled_info[0].channel = 0;
+ sd->subled_info[1].channel = 1;
+ sd->subled_info[2].channel = 2;
+
+ sd->mc_cdev.subled_info = sd->subled_info;
+ sd->mc_cdev.num_colors = 3;
+
+ led_cdev = &sd->mc_cdev.led_cdev;
+ if (hdev->product == USB_DEVICE_ID_STEELSERIES_MSI_KLC)
+ led_cdev->name = "steelseries::kbd_backlight";
+ else
+ led_cdev->name = "steelseries::lightbar";
+
+ led_cdev->max_brightness = 255;
+ led_cdev->brightness_set_blocking = steelseries_msi_rgb_set_blocking;
+
+ ret = devm_led_classdev_multicolor_register(&hdev->dev, &sd->mc_cdev);
+ if (ret) {
+ devm_remove_action(&hdev->dev, steelseries_msi_rgb_free_buf,
+ sd->rgb_buf);
+ kfree(sd->rgb_buf);
+ sd->rgb_buf = NULL;
+ return ret;
+ }
+
+ return 0;
+}
+#else
+static int steelseries_msi_rgb_register(struct steelseries_device *sd)
+{
+ return -ENODEV;
+}
+#endif
+
static int steelseries_probe(struct hid_device *hdev, const struct hid_device_id *id)
{
struct steelseries_device *sd;
@@ -549,6 +792,14 @@ static int steelseries_probe(struct hid_device *hdev, const struct hid_device_id
sd->hdev = hdev;
sd->quirks = id->driver_data;
+ if (sd->quirks & STEELSERIES_MSI_RGB) {
+ if (!dmi_check_system(steelseries_msi_rgb_dmi_table) ||
+ !steelseries_msi_rgb_is_interface0(hdev)) {
+ hid_dbg(hdev, "MSI RGB quirk not applicable, using generic HID path\n");
+ sd->quirks &= ~STEELSERIES_MSI_RGB;
+ }
+ }
+
ret = hid_parse(hdev);
if (ret)
return ret;
@@ -565,12 +816,28 @@ static int steelseries_probe(struct hid_device *hdev, const struct hid_device_id
ret = hid_hw_open(hdev);
if (ret)
- return ret;
+ goto err_stop;
+
+ if (sd->quirks & STEELSERIES_MSI_RGB) {
+ ret = steelseries_msi_rgb_register(sd);
+ if (ret) {
+ hid_warn(hdev,
+ "Failed to register MSI RGB LEDs: %d, continuing without RGB support\n",
+ ret);
+ sd->quirks &= ~STEELSERIES_MSI_RGB;
+ }
+ return 0;
+ }
- if (steelseries_headset_battery_register(sd) < 0)
+ if ((sd->quirks & (STEELSERIES_ARCTIS_1 | STEELSERIES_ARCTIS_9)) &&
+ steelseries_headset_battery_register(sd) < 0)
hid_err(sd->hdev,
"Failed to register battery for headset\n");
+ return 0;
+
+err_stop:
+ hid_hw_stop(hdev);
return ret;
}
@@ -588,12 +855,16 @@ static void steelseries_remove(struct hid_device *hdev)
}
sd = hid_get_drvdata(hdev);
+ if (!sd)
+ return;
spin_lock_irqsave(&sd->lock, flags);
sd->removed = true;
spin_unlock_irqrestore(&sd->lock, flags);
- cancel_delayed_work_sync(&sd->battery_work);
+ /* Pairs with smp_store_release() in steelseries_headset_battery_register() */
+ if (smp_load_acquire(&sd->battery_registered))
+ cancel_delayed_work_sync(&sd->battery_work);
hid_hw_close(hdev);
hid_hw_stop(hdev);
@@ -624,20 +895,34 @@ static uint8_t steelseries_headset_map_capacity(uint8_t capacity, uint8_t min_in
return (capacity - min_in) * 100 / (max_in - min_in);
}
+static bool steelseries_is_headset(struct hid_device *hdev)
+{
+ return hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1 ||
+ hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_9;
+}
+
static int steelseries_headset_raw_event(struct hid_device *hdev,
struct hid_report *report, u8 *read_buf,
int size)
{
- struct steelseries_device *sd = hid_get_drvdata(hdev);
- int capacity = sd->battery_capacity;
- bool connected = sd->headset_connected;
- bool charging = sd->battery_charging;
+ struct steelseries_device *sd;
+ int capacity;
+ bool connected;
+ bool charging;
unsigned long flags;
- /* Not a headset */
- if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
+ if (!steelseries_is_headset(hdev))
return 0;
+ sd = hid_get_drvdata(hdev);
+ /* Pairs with smp_store_release() in steelseries_headset_battery_register() */
+ if (!sd || !smp_load_acquire(&sd->battery_registered))
+ return 0;
+
+ capacity = sd->battery_capacity;
+ connected = sd->headset_connected;
+ charging = sd->battery_charging;
+
if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1) {
hid_dbg(sd->hdev,
"Parsing raw event for Arctis 1 headset (%*ph)\n", size, read_buf);
@@ -732,6 +1017,16 @@ static const struct hid_device_id steelseries_devices[] = {
HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_9),
.driver_data = STEELSERIES_ARCTIS_9 },
+#if STEELSERIES_HAS_LEDS_MULTICOLOR
+ { /* MSI Raider A18 KLC */
+ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_MSI_KLC),
+ .driver_data = STEELSERIES_MSI_RGB },
+
+ { /* MSI Raider A18 ALC */
+ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_MSI_ALC),
+ .driver_data = STEELSERIES_MSI_RGB },
+#endif
+
{ }
};
MODULE_DEVICE_TABLE(hid, steelseries_devices);