]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
HID: hid-oxp: Add Second Generation RGB Control
authorDerek J. Clark <derekjohn.clark@gmail.com>
Sun, 19 Apr 2026 04:26:21 +0000 (21:26 -0700)
committerJiri Kosina <jkosina@suse.com>
Tue, 12 May 2026 15:55:57 +0000 (17:55 +0200)
Adds support for the second generation of RGB Control for OneXPlayer
devices. The interface mirrors the first generation, with some
differences to how messages are formatted.

Some devices have both a GEN1 MCU for RGB control and a GEN2 MCU for
button mapping. To avoid conflicts, quirk these devices to skip RGB
setup for the GEN2_USAGE_PAGE.

Reviewed-by: Zhouwang Huang <honjow311@gmail.com>
Tested-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
Signed-off-by: Jiri Kosina <jkosina@suse.com>
drivers/hid/Kconfig
drivers/hid/hid-ids.h
drivers/hid/hid-oxp.c

index db6e1e0bca6ea6781d5dc54f23101887610be2b9..f60837e4ed52524d477047a3dd3929e1fd3a2005 100644 (file)
@@ -901,6 +901,7 @@ config HID_OXP
        depends on USB_HID
        depends on LEDS_CLASS
        depends on LEDS_CLASS_MULTICOLOR
+       depends on DMI
        help
          Say Y here if you would like to enable support for OneXPlayer handheld
          devices that come with RGB LED rings around the joysticks and macro buttons.
index 589e4db940c7c7b575889baef69392ee0a8e5ebf..93939e489d83c84dbdb0ee40ced03d4f765dd377 100644 (file)
 #define USB_VENDOR_ID_CRSC                     0x1a2c
 #define USB_DEVICE_ID_ONEXPLAYER_GEN1          0xb001
 
+#define USB_VENDOR_ID_WCH                      0x1a86
+#define USB_DEVICE_ID_ONEXPLAYER_GEN2          0xfe00
+
 #define USB_VENDOR_ID_ONTRAK           0x0a07
 #define USB_DEVICE_ID_ONTRAK_ADU100    0x0064
 
index f72bc74a7e6e1b7246b544ce66da819ebee3087c..835de2118e3c5fca35835d3f8e77d3e7a7a0e6ca 100644 (file)
@@ -10,6 +10,7 @@
 #include <linux/delay.h>
 #include <linux/dev_printk.h>
 #include <linux/device.h>
+#include <linux/dmi.h>
 #include <linux/hid.h>
 #include <linux/jiffies.h>
 #include <linux/kstrtox.h>
 #define OXP_PACKET_SIZE 64
 
 #define GEN1_MESSAGE_ID        0xff
+#define GEN2_MESSAGE_ID        0x3f
 
 #define GEN1_USAGE_PAGE        0xff01
+#define GEN2_USAGE_PAGE        0xff00
 
 enum oxp_function_index {
        OXP_FID_GEN1_RGB_SET =          0x07,
        OXP_FID_GEN1_RGB_REPLY =        0x0f,
+       OXP_FID_GEN2_STATUS_EVENT =     0xb8,
 };
 
 static struct oxp_hid_cfg {
@@ -122,6 +126,22 @@ struct oxp_gen_1_rgb_report {
        u8 blue;
 } __packed;
 
+struct oxp_gen_2_rgb_report {
+       u8 report_id;
+       u8 header_id;
+       u8 padding_2;
+       u8 message_id;
+       u8 padding_4[2];
+       u8 enabled;
+       u8 speed;
+       u8 brightness;
+       u8 red;
+       u8 green;
+       u8 blue;
+       u8 padding_12[3];
+       u8 effect;
+} __packed;
+
 static u16 get_usage_page(struct hid_device *hdev)
 {
        return hdev->collection[0].usage >> 16;
@@ -162,6 +182,44 @@ static int oxp_hid_raw_event_gen_1(struct hid_device *hdev,
        return 0;
 }
 
+static int oxp_hid_raw_event_gen_2(struct hid_device *hdev,
+                                  struct hid_report *report, u8 *data,
+                                  int size)
+{
+       struct led_classdev_mc *led_mc = drvdata.led_mc;
+       struct oxp_gen_2_rgb_report *rgb_rep;
+
+       if (data[0] != OXP_FID_GEN2_STATUS_EVENT)
+               return 0;
+
+       if (data[3] != OXP_GET_PROPERTY)
+               return 0;
+
+       rgb_rep = (struct oxp_gen_2_rgb_report *)data;
+       /* Ensure we save monocolor as the list value */
+       drvdata.rgb_effect = rgb_rep->effect == OXP_EFFECT_MONO_TRUE ?
+                            OXP_EFFECT_MONO_LIST :
+                            rgb_rep->effect;
+       drvdata.rgb_speed = rgb_rep->speed;
+       drvdata.rgb_en = rgb_rep->enabled == 0 ? OXP_FEAT_DISABLED :
+                                                OXP_FEAT_ENABLED;
+       drvdata.rgb_brightness = rgb_rep->brightness;
+       led_mc->led_cdev.brightness = rgb_rep->brightness / 4 *
+                                     led_mc->led_cdev.max_brightness;
+       /* If monocolor had less than 100% brightness on the previous boot,
+        * there will be no reliable way to determine the real intensity.
+        * Since intensity scaling is used with a hardware brightness set at max,
+        * our brightness will always look like 100%. Use the last set value to
+        * prevent successive boots from lowering the brightness further.
+        * Brightness will be "wrong" but the effect will remain the same visually.
+        */
+       led_mc->subled_info[0].intensity = rgb_rep->red;
+       led_mc->subled_info[1].intensity = rgb_rep->green;
+       led_mc->subled_info[2].intensity = rgb_rep->blue;
+
+       return 0;
+}
+
 static int oxp_hid_raw_event(struct hid_device *hdev, struct hid_report *report,
                             u8 *data, int size)
 {
@@ -172,6 +230,8 @@ static int oxp_hid_raw_event(struct hid_device *hdev, struct hid_report *report,
        switch (up) {
        case GEN1_USAGE_PAGE:
                return oxp_hid_raw_event_gen_1(hdev, report, data, size);
+       case GEN2_USAGE_PAGE:
+               return oxp_hid_raw_event_gen_2(hdev, report, data, size);
        default:
                break;
        }
@@ -217,6 +277,18 @@ static int oxp_gen_1_property_out(enum oxp_function_index fid, u8 *data,
        return mcu_property_out(header, header_size, data, data_size, NULL, 0);
 }
 
+static int oxp_gen_2_property_out(enum oxp_function_index fid, u8 *data,
+                                 u8 data_size)
+{
+       u8 header[] = { fid, GEN2_MESSAGE_ID, 0x01 };
+       u8 footer[] = { GEN2_MESSAGE_ID, fid };
+       size_t header_size = ARRAY_SIZE(header);
+       size_t footer_size = ARRAY_SIZE(footer);
+
+       return mcu_property_out(header, header_size, data, data_size, footer,
+                               footer_size);
+}
+
 static int oxp_rgb_status_store(u8 enabled, u8 speed, u8 brightness)
 {
        u16 up = get_usage_page(drvdata.hdev);
@@ -231,6 +303,11 @@ static int oxp_rgb_status_store(u8 enabled, u8 speed, u8 brightness)
                if (drvdata.rgb_effect == OXP_EFFECT_MONO_LIST)
                        data[3] = 0x04;
                return oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, 4);
+       case GEN2_USAGE_PAGE:
+               data = (u8[6]) { OXP_SET_PROPERTY, 0x00, 0x02, enabled, speed, brightness };
+               if (drvdata.rgb_effect == OXP_EFFECT_MONO_LIST)
+                       data[5] = 0x04;
+               return oxp_gen_2_property_out(OXP_FID_GEN2_STATUS_EVENT, data, 6);
        default:
                return -ENODEV;
        }
@@ -245,6 +322,9 @@ static ssize_t oxp_rgb_status_show(void)
        case GEN1_USAGE_PAGE:
                data = (u8[1]) { OXP_GET_PROPERTY };
                return oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, 1);
+       case GEN2_USAGE_PAGE:
+               data = (u8[3]) { OXP_GET_PROPERTY, 0x00, 0x02 };
+               return oxp_gen_2_property_out(OXP_FID_GEN2_STATUS_EVENT, data, 3);
        default:
                return -ENODEV;
        }
@@ -275,6 +355,16 @@ static int oxp_rgb_color_set(void)
                        data[3 * i + 3] = blue;
                }
                return oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, size);
+       case GEN2_USAGE_PAGE:
+               size = 57;
+               data = (u8[57]) { OXP_EFFECT_MONO_TRUE, 0x00, 0x02 };
+
+               for (i = 1; i < size / 3; i++) {
+                       data[3 * i] = red;
+                       data[3 * i + 1] = green;
+                       data[3 * i + 2] = blue;
+               }
+               return oxp_gen_2_property_out(OXP_FID_GEN2_STATUS_EVENT, data, size);
        default:
                return -ENODEV;
        }
@@ -311,6 +401,10 @@ static int oxp_rgb_effect_set(u8 effect)
                        data = (u8[1]) { effect };
                        ret = oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, 1);
                        break;
+               case GEN2_USAGE_PAGE:
+                       data = (u8[3]) { effect, 0x00, 0x02 };
+                       ret = oxp_gen_2_property_out(OXP_FID_GEN2_STATUS_EVENT, data, 3);
+                       break;
                default:
                        ret = -ENODEV;
                }
@@ -559,6 +653,56 @@ static struct led_classdev_mc oxp_cdev_rgb = {
        .subled_info = oxp_rgb_subled_info,
 };
 
+struct quirk_entry {
+       bool hybrid_mcu;
+};
+
+static struct quirk_entry quirk_hybrid_mcu = {
+       .hybrid_mcu = true,
+};
+
+static const struct dmi_system_id oxp_hybrid_mcu_list[] = {
+       {
+               .ident = "OneXPlayer Apex",
+               .matches = {
+                       DMI_MATCH(DMI_SYS_VENDOR, "ONE-NETBOOK"),
+                       DMI_MATCH(DMI_PRODUCT_NAME, "ONEXPLAYER APEX"),
+               },
+               .driver_data = &quirk_hybrid_mcu,
+       },
+       {
+               .ident = "OneXPlayer G1 AMD",
+               .matches = {
+                       DMI_MATCH(DMI_SYS_VENDOR, "ONE-NETBOOK"),
+                       DMI_MATCH(DMI_PRODUCT_NAME, "ONEXPLAYER G1 A"),
+               },
+               .driver_data = &quirk_hybrid_mcu,
+       },
+       {
+               .ident = "OneXPlayer G1 Intel",
+               .matches = {
+                       DMI_MATCH(DMI_SYS_VENDOR, "ONE-NETBOOK"),
+                       DMI_MATCH(DMI_PRODUCT_NAME, "ONEXPLAYER G1 i"),
+               },
+               .driver_data = &quirk_hybrid_mcu,
+       },
+       {},
+};
+
+static bool oxp_hybrid_mcu_device(void)
+{
+       const struct dmi_system_id *dmi_id;
+       struct quirk_entry *quirks;
+
+       dmi_id = dmi_first_match(oxp_hybrid_mcu_list);
+       if (!dmi_id)
+               return false;
+
+       quirks = dmi_id->driver_data;
+
+       return quirks->hybrid_mcu;
+}
+
 static int oxp_cfg_probe(struct hid_device *hdev, u16 up)
 {
        int ret;
@@ -566,6 +710,10 @@ static int oxp_cfg_probe(struct hid_device *hdev, u16 up)
        hid_set_drvdata(hdev, &drvdata);
        mutex_init(&drvdata.cfg_mutex);
        drvdata.hdev = hdev;
+
+       if (up == GEN2_USAGE_PAGE && oxp_hybrid_mcu_device())
+               goto skip_rgb;
+
        drvdata.led_mc = &oxp_cdev_rgb;
 
        INIT_DELAYED_WORK(&drvdata.oxp_rgb_queue, oxp_rgb_queue_fn);
@@ -585,6 +733,7 @@ static int oxp_cfg_probe(struct hid_device *hdev, u16 up)
                dev_warn(drvdata.led_mc->led_cdev.dev,
                         "Failed to query RGB initial state: %i\n", ret);
 
+skip_rgb:
        return 0;
 }
 
@@ -613,6 +762,7 @@ static int oxp_hid_probe(struct hid_device *hdev,
 
        switch (up) {
        case GEN1_USAGE_PAGE:
+       case GEN2_USAGE_PAGE:
                ret = oxp_cfg_probe(hdev, up);
                if (ret) {
                        hid_hw_close(hdev);
@@ -634,6 +784,7 @@ static void oxp_hid_remove(struct hid_device *hdev)
 
 static const struct hid_device_id oxp_devices[] = {
        { HID_USB_DEVICE(USB_VENDOR_ID_CRSC, USB_DEVICE_ID_ONEXPLAYER_GEN1) },
+       { HID_USB_DEVICE(USB_VENDOR_ID_WCH, USB_DEVICE_ID_ONEXPLAYER_GEN2) },
        {}
 };