diff options
| author | David Glushkov <david.glushkov@sntiq.com> | 2026-06-05 19:50:35 +0200 |
|---|---|---|
| committer | Jiri Kosina <jkosina@suse.com> | 2026-06-29 10:52:45 +0200 |
| commit | 2346d6997b70dae1f8302fd029aa422e28112eaa (patch) | |
| tree | bae0c0a37a8c5cb2f10f6b30758b447e904b99aa | |
| parent | b7556c8e713c88596046a906c7c4385218d44736 (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.h | 2 | ||||
| -rw-r--r-- | drivers/hid/hid-steelseries.c | 313 |
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); |
