Linux Input/HID development
 help / color / mirror / Atom feed
* [PATCH] HID: sony: use input_dev from sc struct in sony_init_ff()
From: Rosalie Wanders @ 2026-04-11 15:53 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Rosalie Wanders, linux-input, linux-kernel

This commit makes sony_init_ff() use the input_dev from the sc struct,
this simplifies the sony_init_ff() function.

Signed-off-by: Rosalie Wanders <rosalie@mailbox.org>
---
 drivers/hid/hid-sony.c | 17 ++++-------------
 1 file changed, 4 insertions(+), 13 deletions(-)

diff --git a/drivers/hid/hid-sony.c b/drivers/hid/hid-sony.c
index 6a860b9ef677..ad394ac57fa5 100644
--- a/drivers/hid/hid-sony.c
+++ b/drivers/hid/hid-sony.c
@@ -1853,18 +1853,8 @@ static int sony_play_effect(struct input_dev *dev, void *data,
 
 static int sony_init_ff(struct sony_sc *sc)
 {
-	struct hid_input *hidinput;
-	struct input_dev *input_dev;
-
-	if (list_empty(&sc->hdev->inputs)) {
-		hid_err(sc->hdev, "no inputs found\n");
-		return -ENODEV;
-	}
-	hidinput = list_entry(sc->hdev->inputs.next, struct hid_input, list);
-	input_dev = hidinput->input;
-
-	input_set_capability(input_dev, EV_FF, FF_RUMBLE);
-	return input_ff_create_memless(input_dev, NULL, sony_play_effect);
+	input_set_capability(sc->input_dev, EV_FF, FF_RUMBLE);
+	return input_ff_create_memless(sc->input_dev, NULL, sony_play_effect);
 }
 
 #else
@@ -2151,6 +2141,8 @@ static int sony_input_configured(struct hid_device *hdev,
 	int append_dev_id;
 	int ret;
 
+	sc->input_dev = hidinput->input;
+
 	ret = sony_set_device_id(sc);
 	if (ret < 0) {
 		hid_err(hdev, "failed to allocate the device id\n");
@@ -2311,7 +2303,6 @@ static int sony_input_configured(struct hid_device *hdev,
 			goto err_close;
 	}
 
-	sc->input_dev = hidinput->input;
 	return 0;
 err_close:
 	hid_hw_close(hdev);
-- 
2.53.0


^ permalink raw reply related

* [PATCH] HID: magicmouse: enable battery polling for 2024 Magic Trackpad
From: Dmitri Ollari @ 2026-04-11 16:38 UTC (permalink / raw)
  To: linux-input; +Cc: jikos, Dmitri Ollari


[-- Attachment #1.1: Type: text/plain, Size: 6501 bytes --]

The 2024 Magic Trackpad USB-C (PID 0x0324) does not report battery
strength via HID descriptor fields over Bluetooth. Instead it requires
an explicit HID_REQ_GET_REPORT request to retrieve the battery level.

This patch makes the following changes:

1. Replace the battery_timer (timer_list) with battery_work (delayed_work)
   so that HID_REQ_GET_REPORT can be issued from a sleepable context.
   Timers run in atomic context and cannot block, which caused deadlocks
   on the Bluetooth transport path.

2. Extend the fetch guard and probe scheduling block to include the 2024
   Magic Trackpad USB-C when connected over Bluetooth (vendor 0x004C,
   product 0x0324 via BT_VENDOR_ID_APPLE).

3. Schedule battery_work immediately at probe (delay=0) instead of
   issuing a direct magicmouse_fetch_battery() call. The direct call
   bypassed the cold-start correction logic and could publish a stale
   value before the work handler had a chance to validate it.

4. Add a cold-start
 double-poll: the device may return a stale battery
   value (e.g. 4%) on the very first GET_REPORT after power-on. On the
   first successful poll battery_validated is set and a second poll is
   scheduled 3 seconds later to obtain the real value. Subsequent polls
   use the normal 60-second interval.

5. Remove the early-return guard that skipped polling when
   battery_capacity equalled battery_max. This prevented the second
   corrective poll from firing when the first stale response happened
   to equal 100.

Signed-off-by: Dmitri Ollari <dmitri.ollari@protonmail.com>
---
 hid-magicmouse.c | 55 +++++++++++++++++++++++++++++-------------------
 1 file changed, 33 insertions(+), 22 deletions(-)

diff --git a/hid-magicmouse.c b/hid-magicmouse.c
index 9eadf32..bc9c467 100644
--- a/hid-magicmouse.c
+++ b/hid-magicmouse.c
@@ -123,7 +123,10 @@ MODULE_PARM_DESC(report_undeciphered, "Report undeciphered multi-touch state fie
  * @tracking_ids: Mapping of current touch 
input data to @touches.
  * @hdev: Pointer to the underlying HID device.
  * @work: Workqueue to handle initialization retry for quirky devices.
- * @battery_timer: Timer for obtaining battery level information.
+ * @battery_work: Delayed work for periodic battery level polling.
+ * @battery_validated: Set after the first successful poll; gates the
+ *	second poll that corrects the stale value the device may report
+ *	on cold start.
  */
 struct magicmouse_sc {
 	struct input_dev *input;
@@ -148,7 +151,8 @@ struct magicmouse_sc {
 
 	struct hid_device *hdev;
 	struct delayed_work work;
-	struct timer_list battery_timer;
+	struct delayed_work battery_work;
+	bool battery_validated;
 };
 
 static int magicmouse_firm_touch(struct magicmouse_sc *msc)
@@ -820,7 +824,8 @@ static int magicmouse_fetch_battery(struct hid_device *hdev)
 
 	if (!hdev->battery ||
 	    (!is_usb_magicmouse2(hdev->vendor, hdev->product) &&
-	     !is_usb_magictrackpad2(hdev->vendor, hdev->p
roduct)))
+	     !is_usb_magictrackpad2(hdev->vendor, hdev->product) &&
+	     hdev->product != USB_DEVICE_ID_APPLE_MAGICTRACKPAD2_USBC)) /* 2024 Magic Trackpad USB-C over Bluetooth */
 		return -1;
 
 	report_enum = &hdev->report_enum[hdev->battery_report_type];
@@ -829,9 +834,6 @@ static int magicmouse_fetch_battery(struct hid_device *hdev)
 	if (!report || report->maxfield < 1)
 		return -1;
 
-	if (hdev->battery_capacity == hdev->battery_max)
-		return -1;
-
 	hid_hw_request(hdev, report, HID_REQ_GET_REPORT);
 	return 0;
 #else
@@ -839,14 +841,23 @@ static int magicmouse_fetch_battery(struct hid_device *hdev)
 #endif
 }
 
-static void magicmouse_battery_timer_tick(struct timer_list *t)
+static void magicmouse_battery_work(struct work_struct *work)
 {
-	struct magicmouse_sc *msc = timer_container_of(msc, t, battery_timer);
+	struct magicmouse_sc *msc = container_of(work, struct magicmouse_sc, battery_work.work);
 	struct hid_device *hdev = msc->hdev;
 
 	i
f (magicmouse_fetch_battery(hdev) == 0) {
-		mod_timer(&msc->battery_timer,
-			  jiffies + secs_to_jiffies(USB_BATTERY_TIMEOUT_SEC));
+		if (!msc->battery_validated) {
+			/* The device may return a stale value (e.g. 4%) on the
+			 * first GET_REPORT after cold start. Schedule a second
+			 * poll shortly after to get the real value, then settle
+			 * into the normal 60s interval.
+			 */
+			msc->battery_validated = true;
+			schedule_delayed_work(&msc->battery_work, secs_to_jiffies(3));
+		} else {
+			schedule_delayed_work(&msc->battery_work, secs_to_jiffies(USB_BATTERY_TIMEOUT_SEC));
+		}
 	}
 }
 
@@ -866,6 +877,7 @@ static int magicmouse_probe(struct hid_device *hdev,
 	msc->scroll_accel = SCROLL_ACCEL_DEFAULT;
 	msc->hdev = hdev;
 	INIT_DEFERRABLE_WORK(&msc->work, magicmouse_enable_mt_work);
+	INIT_DELAYED_WORK(&msc->battery_work, magicmouse_battery_work);
 
 	msc->quirks = id->driver_data;
 	hid_set_drvdata(hdev, msc);
@@ -883,11 +895,13 @@ static int
 magicmouse_probe(struct hid_device *hdev,
 	}
 
 	if (is_usb_magicmouse2(id->vendor, id->product) ||
-	    is_usb_magictrackpad2(id->vendor, id->product)) {
-		timer_setup(&msc->battery_timer, magicmouse_battery_timer_tick, 0);
-		mod_timer(&msc->battery_timer,
-			  jiffies + secs_to_jiffies(USB_BATTERY_TIMEOUT_SEC));
-		magicmouse_fetch_battery(hdev);
+	    is_usb_magictrackpad2(id->vendor, id->product) ||
+	    id->product == USB_DEVICE_ID_APPLE_MAGICTRACKPAD2_USBC) {
+		/* Schedule immediately so battery_work runs ASAP, sets battery_validated,
+		 * then reschedules every 60s. Avoids direct fetch which bypasses
+		 * battery_validated and would publish a stale startup value.
+		 */
+		schedule_delayed_work(&msc->battery_work, 0);
 	}
 
 	if (is_usb_magicmouse2(id->vendor, id->product) ||
@@ -955,10 +969,8 @@ static int magicmouse_probe(struct hid_device *hdev,
 
 	return 0;
 err_stop_hw:
-	if (is_usb_magicmouse2(id->vendor, id->product) ||
-	    is_usb_magi
ctrackpad2(id->vendor, id->product))
-		timer_delete_sync(&msc->battery_timer);
-
+	/* Clean up battery work on error */
+	cancel_delayed_work_sync(&msc->battery_work);
 	hid_hw_stop(hdev);
 	return ret;
 }
@@ -969,9 +981,8 @@ static void magicmouse_remove(struct hid_device *hdev)
 
 	if (msc) {
 		cancel_delayed_work_sync(&msc->work);
-		if (is_usb_magicmouse2(hdev->vendor, hdev->product) ||
-		    is_usb_magictrackpad2(hdev->vendor, hdev->product))
-			timer_delete_sync(&msc->battery_timer);
+		/* Cancel battery polling on device removal */
+		cancel_delayed_work_sync(&msc->battery_work);
 	}
 
 	hid_hw_stop(hdev);
-- 
2.53.0


[-- Attachment #1.2: publickey - dmitri.ollari@protonmail.com - 0xF53BC391.asc --]
[-- Type: application/pgp-keys, Size: 722 bytes --]

[-- Attachment #2: OpenPGP digital signature --]
[-- Type: application/pgp-signature, Size: 322 bytes --]

^ permalink raw reply related

* Re: [git pull] Input updates for v7.0-rc7
From: pr-tracker-bot @ 2026-04-11 19:58 UTC (permalink / raw)
  To: Dmitry Torokhov; +Cc: Linus Torvalds, linux-kernel, linux-input
In-Reply-To: <adnMg9E_ZAjxcv1k@google.com>

The pull request you sent on Fri, 10 Apr 2026 21:23:07 -0700:

> git://git.kernel.org/pub/scm/linux/kernel/git/dtor/input.git tags/input-for-v7.0-rc7

has been merged into torvalds/linux.git:
https://git.kernel.org/torvalds/c/778322a06e217e768ba3dc550a6f599f73ed781d

Thank you!

-- 
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/prtracker.html

^ permalink raw reply

* HID: core: regression in 6.19.12 - short report handling floods dmesg for working devices
From: Anj D @ 2026-04-12  0:24 UTC (permalink / raw)
  To: linux-input@vger.kernel.org
  Cc: benjamin.tissoires@redhat.com, lee@kernel.org, jikos@kernel.org

Since 6.19.12, my dmesg is flooded with the following - 

hid-generic 0003:051D:0002.0007: Event data for report 22 was too short (4 vs 2)

This message fires continuously at polling rate (~every 320ms), with the
rate limiter suppressing batches and then re-emitting, producing significant
dmesg noise on any system with this device attached.

The affected hardware is an APC UPS (USB VID/PID 051D:0002)

Bisected to commit 0a3fe972a7cb1404f693d6f1711f32bc1d244b1c introduced in 6.19.12.
Reverting this single commit on 6.19.12 restores the previous behaviour.

The APC UPS firmware has always sent HID report 22 with 2 bytes when 
the descriptor declares 4 bytes. The previous memset() silently padded 
the short data. The new early-return code logs the mismatch on every 
poll, flooding dmesg. The UPS functions correctly otherwise.

Suggestion: Since we previously only reported these events when HID_DEBUG was enabled.  
Perhaps we can continue to do the same?  i.e.

--- a/drivers/hid/hid-core.c
+++ b/drivers/hid/hid-core.c
@@ -2057,7 +2057,7 @@
		rsize = max_buffer_size;

	if (csize < rsize) {
-		hid_warn_ratelimited(hid, "Event data for report %d was too short (%d vs %d)\n",
+		dbg_hid("Event data for report %d was too short (%d vs %d)\n",
				     report->id, rsize, csize);
		ret = -EINVAL;
		goto out;


Or if it we feel it is necessary to report the event, do so only once per device?

Best regards,
Anj Duvnjak

^ permalink raw reply

* [PATCH] HID: sony: add missing size validation for SMK-Link remotes
From: Rosalie Wanders @ 2026-04-12  1:08 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Rosalie Wanders, linux-input, linux-kernel

This commit adds the missing size validation for SMK-Link remotes in
sony_raw_event(), this prevents a malicious device from allowing
hid-sony to read out of bounds of the provided buffer.

I do not own these devices so the size check only forces that the buffer
is large enough for nsg_mrxu_parse_report().

Signed-off-by: Rosalie Wanders <rosalie@mailbox.org>
---
 drivers/hid/hid-sony.c | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/drivers/hid/hid-sony.c b/drivers/hid/hid-sony.c
index ad394ac57fa5..2260d55a17b0 100644
--- a/drivers/hid/hid-sony.c
+++ b/drivers/hid/hid-sony.c
@@ -1169,10 +1169,9 @@ static int sony_raw_event(struct hid_device *hdev, struct hid_report *report,
 		sixaxis_parse_report(sc, rd, size);
 	} else if ((sc->quirks & MOTION_CONTROLLER_BT) && rd[0] == 0x01 && size == 49) {
 		sixaxis_parse_report(sc, rd, size);
-	} else if ((sc->quirks & NAVIGATION_CONTROLLER) && rd[0] == 0x01 &&
-			size == 49) {
+	} else if ((sc->quirks & NAVIGATION_CONTROLLER) && rd[0] == 0x01 && size == 49) {
 		sixaxis_parse_report(sc, rd, size);
-	} else if ((sc->quirks & NSG_MRXU_REMOTE) && rd[0] == 0x02) {
+	} else if ((sc->quirks & NSG_MRXU_REMOTE) && rd[0] == 0x02 && size >= 12) {
 		nsg_mrxu_parse_report(sc, rd, size);
 		return 1;
 	} else if ((sc->quirks & RB4_GUITAR_PS4_USB) && rd[0] == 0x01 && size == 64) {
-- 
2.53.0


^ permalink raw reply related

* [PATCH] HID: sony: add missing size validation for Rock Band 3 Pro instruments
From: Rosalie Wanders @ 2026-04-12  1:12 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Rosalie Wanders, linux-input, linux-kernel

This commit adds the missing size validation for Rock Band 3 PS3 Pro
instruments in sony_raw_event(), this prevents a malicious device from
allowing hid-sony to read out of bounds of the provided buffer.

Signed-off-by: Rosalie Wanders <rosalie@mailbox.org>
---
 drivers/hid/hid-sony.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/drivers/hid/hid-sony.c b/drivers/hid/hid-sony.c
index 2260d55a17b0..e75246d29e16 100644
--- a/drivers/hid/hid-sony.c
+++ b/drivers/hid/hid-sony.c
@@ -1188,7 +1188,7 @@ static int sony_raw_event(struct hid_device *hdev, struct hid_report *report,
 	/* Rock Band 3 PS3 Pro instruments set rd[24] to 0xE0 when they're
 	 * sending full reports, and 0x02 when only sending navigation.
 	 */
-	if ((sc->quirks & RB3_PRO_INSTRUMENT) && rd[24] == 0x02) {
+	if ((sc->quirks & RB3_PRO_INSTRUMENT) && size >= 25 && rd[24] == 0x02) {
 		/* Only attempt to enable full report every 8 seconds */
 		if (time_after(jiffies, sc->rb3_pro_poke_jiffies)) {
 			sc->rb3_pro_poke_jiffies = jiffies + secs_to_jiffies(8);
-- 
2.53.0


^ permalink raw reply related

* Re: [PATCH v2] HID: pulsar: add driver for Pulsar gaming mice
From: kernel test robot @ 2026-04-12  4:53 UTC (permalink / raw)
  To: Nikolas Koesling, Jiri Kosina, Benjamin Tissoires
  Cc: oe-kbuild-all, linux-input, linux-kernel, Leo
In-Reply-To: <20260401185708.286359-1-nikolas@koesling.info>

Hi Nikolas,

kernel test robot noticed the following build warnings:

[auto build test WARNING on v7.0-rc7]
[cannot apply to hid/for-next linus/master next-20260410]
[If your patch is applied to the wrong git tree, kindly drop us a note.
And when submitting patch, we suggest to use '--base' as documented in
https://git-scm.com/docs/git-format-patch#_base_tree_information]

url:    https://github.com/intel-lab-lkp/linux/commits/Nikolas-Koesling/HID-pulsar-add-driver-for-Pulsar-gaming-mice/20260411-084544
base:   v7.0-rc7
patch link:    https://lore.kernel.org/r/20260401185708.286359-1-nikolas%40koesling.info
patch subject: [PATCH v2] HID: pulsar: add driver for Pulsar gaming mice
config: alpha-allyesconfig (https://download.01.org/0day-ci/archive/20260412/202604121253.ZYbxnPmo-lkp@intel.com/config)
compiler: alpha-linux-gcc (GCC) 15.2.0
reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20260412/202604121253.ZYbxnPmo-lkp@intel.com/reproduce)

If you fix the issue in a separate patch/commit (i.e. not just a new version of
the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <lkp@intel.com>
| Closes: https://lore.kernel.org/oe-kbuild-all/202604121253.ZYbxnPmo-lkp@intel.com/

All warnings (new ones prefixed by >>):

   drivers/hid/hid-pulsar.c: In function 'pulsar_init_work':
>> drivers/hid/hid-pulsar.c:601:27: warning: '%s' directive output may be truncated writing up to 127 bytes into a region of size 32 [-Wformat-truncation=]
     601 |                          "%s", hdev->name);
         |                           ^~
   drivers/hid/hid-pulsar.c:600:17: note: 'snprintf' output between 1 and 128 bytes into a destination of size 32
     600 |                 snprintf(drvdata->battery.model, sizeof(drvdata->battery.model),
         |                 ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     601 |                          "%s", hdev->name);
         |                          ~~~~~~~~~~~~~~~~~
   drivers/hid/hid-pulsar.c:586:59: warning: '%s' directive output may be truncated writing up to 127 bytes into a region of size 32 [-Wformat-truncation=]
     586 |                          sizeof(drvdata->battery.model), "%s", hdev->name);
         |                                                           ^~
   drivers/hid/hid-pulsar.c:585:17: note: 'snprintf' output between 1 and 128 bytes into a destination of size 32
     585 |                 snprintf(drvdata->battery.model,
         |                 ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     586 |                          sizeof(drvdata->battery.model), "%s", hdev->name);
         |                          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


vim +601 drivers/hid/hid-pulsar.c

   553	
   554	static void pulsar_init_work(struct work_struct *work)
   555	{
   556		struct pulsar_data *drvdata;
   557		struct hid_device *hdev;
   558		struct power_supply_config psy_cfg;
   559		int ret;
   560		u8 data[DEV_INFO_LEN];
   561	
   562		drvdata = container_of(work, struct pulsar_data, init_work.work);
   563		hdev = drvdata->hdev;
   564	
   565		ret = read_device_info(drvdata, data);
   566		if (ret == -ETIMEDOUT) {
   567			if (drvdata->init_retries--) {
   568				hid_dbg(hdev,
   569					"device info read timed out, retrying (%u left)\n",
   570					drvdata->init_retries);
   571				schedule_delayed_work(&drvdata->init_work,
   572						      msecs_to_jiffies
   573						      (INIT_DELAY_MSEC));
   574				return;
   575			}
   576			hid_err(hdev, "device info read timed out, giving up\n");
   577			return;
   578		}
   579		if (ret < 0) {
   580			if (drvdata->type == TYPE_PULSAR) {
   581				hid_err(hdev, "failed to read device info: %d\n", ret);
   582				return;
   583			}
   584			hid_dbg(hdev, "failed to read device info: %d\n", ret);
   585			snprintf(drvdata->battery.model,
   586				 sizeof(drvdata->battery.model), "%s", hdev->name);
   587			goto register_battery;
   588		}
   589	
   590		hid_dbg(hdev, "device info: %*ph (%d)\n", DEV_INFO_LEN, data, ret);
   591	
   592		switch (drvdata->type) {
   593		case TYPE_PULSAR:
   594			model_pulsar(data, drvdata);
   595			break;
   596		case TYPE_ATK:
   597			model_atk(data, drvdata);
   598			break;
   599		default:
   600			snprintf(drvdata->battery.model, sizeof(drvdata->battery.model),
 > 601				 "%s", hdev->name);
   602		}
   603	
   604	register_battery:
   605		init_power_supply_desc(drvdata);
   606	
   607		psy_cfg = (struct power_supply_config) {.drv_data = drvdata };
   608		drvdata->battery.ps =
   609		    devm_power_supply_register(&hdev->dev, &drvdata->battery.desc,
   610					       &psy_cfg);
   611		if (IS_ERR(drvdata->battery.ps)) {
   612			hid_err(hdev, "failed to register battery: %ld\n",
   613				PTR_ERR(drvdata->battery.ps));
   614			drvdata->battery.ps = NULL;
   615			return;
   616		}
   617	
   618		atomic_set(&drvdata->device_verified, 1);
   619		hid_info(hdev, "device verified, battery registered\n");
   620	}
   621	

-- 
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki

^ permalink raw reply

* [PATCH v3] HID: pulsar: add driver for Pulsar gaming mice
From: Nikolas Koesling @ 2026-04-12  7:53 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Lode Willems, linux-input, linux-kernel, Leo

Add a HID driver for Pulsar wireless gaming mice (X2 V2, X2H, X2A,
Xlite V3). The driver exposes battery level, voltage, and charging
status through the power supply framework. It supports wired, 1kHz,
and 4kHz wireless dongle connections.

The driver also supports Kysona M600 ATK, VXE R1 SE+ and
VXE Dragonfly R1 Pro, which use the same protocol for reading
battery status and availability.

The protocol used by this driver is based on findings from
python-pulsar-mouse-tool by Andrew Rabert (MIT License):
https://github.com/andrewrabert/python-pulsar-mouse-tool

ATK vendor and device IDs were provided by Leo <leo@managarm.org>.
VXE and Kysona vendor and device IDS are from hid-kysona.c by
        Lode Willems <me@lodewillems.com>

Tested-by: Leo <leo@managarm.org>
Signed-off-by: Nikolas Koesling <nikolas@koesling.info>
---
Changes in v2:
- Add support for Kysona M600, ATK VXE R1 SE+, and VXE Dragonfly R1 Pro
- Add device type enum to distinguish vendors and generate proper
  battery names per vendor/model
- Add mutual exclusion with HID_KYSONA in Kconfig
- Add ATK and VXE vendor/device IDs to hid-ids.h
- Refactor model name generation: extract model_pulsar() and add
  model_atk() for vendor-specific battery naming
- Fall back to hdev->name for battery model when device info read
  fails on non-Pulsar devices (downgrade error to debug log)
- Remove POWER_SUPPLY_PROP_MANUFACTURER property
- Pass device type via driver_data in hid_device_id table

Changes in v3:
- Increase size of battery model name to hid device name size
---
 MAINTAINERS              |   6 +
 drivers/hid/Kconfig      |  15 +
 drivers/hid/Makefile     |   1 +
 drivers/hid/hid-ids.h    |  15 +
 drivers/hid/hid-pulsar.c | 754 +++++++++++++++++++++++++++++++++++++++
 5 files changed, 791 insertions(+)
 create mode 100644 drivers/hid/hid-pulsar.c

diff --git a/MAINTAINERS b/MAINTAINERS
index c3fe46d7c4bc..207216632918 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -11352,6 +11352,12 @@ L:	linux-input@vger.kernel.org
 S:	Supported
 F:	drivers/hid/hid-playstation.c
 
+HID PULSAR DRIVER
+M:	Nikolas Koesling <nikolas@koesling.info>
+L:	linux-input@vger.kernel.org
+S:	Maintained
+F:	drivers/hid/hid-pulsar.c
+
 HID SENSOR HUB DRIVERS
 M:	Jiri Kosina <jikos@kernel.org>
 M:	Jonathan Cameron <jic23@kernel.org>
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index c1d9f7c6a5f2..333d165554ee 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -511,12 +511,15 @@ config HID_KYE
 config HID_KYSONA
 	tristate "Kysona devices"
 	depends on USB_HID
+	depends on !HID_PULSAR
 	help
 	Support for Kysona mice.
 
 	Say Y here if you have a Kysona M600 mouse
 	and want to be able to read its battery capacity.
 
+	Note: The Kysona M600 is also supported by HID_PULSAR.
+
 config HID_UCLOGIC
 	tristate "UC-Logic"
 	depends on USB_HID
@@ -1280,6 +1283,18 @@ config HID_UNIVERSAL_PIDFF
 
 	  Supports Moza Racing, Cammus, VRS, FFBeast and more.
 
+config HID_PULSAR
+	tristate "Pulsar gaming mouse support"
+	depends on USB_HID
+	select POWER_SUPPLY
+	help
+	  Support for Pulsar gaming mice (X2 V2, X2H, X2A, Xlite V3)
+	  connected via 1kHz/4kHz USB dongle or wired.
+	  Provides battery level, voltage, and charging status
+	  monitoring via the power supply framework.
+
+	  Additional supported devices: Kysona M600, ATK VXE R1 SE+
+
 config HID_WACOM
 	tristate "Wacom Intuos/Graphire tablet support (USB)"
 	depends on USB_HID
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index e01838239ae6..67ad39b47df1 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -112,6 +112,7 @@ hid-picolcd-$(CONFIG_DEBUG_FS)		+= hid-picolcd_debugfs.o
 obj-$(CONFIG_HID_PLANTRONICS)	+= hid-plantronics.o
 obj-$(CONFIG_HID_PLAYSTATION)	+= hid-playstation.o
 obj-$(CONFIG_HID_PRIMAX)	+= hid-primax.o
+obj-$(CONFIG_HID_PULSAR)	+= hid-pulsar.o
 obj-$(CONFIG_HID_PXRC)		+= hid-pxrc.o
 obj-$(CONFIG_HID_RAPOO) += hid-rapoo.o
 obj-$(CONFIG_HID_RAZER)	+= hid-razer.o
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index afcee13bad61..5ce542150d61 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -248,6 +248,12 @@
 #define USB_VENDOR_ID_ATMEL_V_USB	0x16c0
 #define USB_DEVICE_ID_ATMEL_V_USB	0x05df
 
+#define USB_VENDOR_ID_ATK		0x373B
+#define USB_DEVICE_ID_ATK_VXE_R1_SE_WIRED	0xF58F
+
+#define USB_VENDOR_ID_ATK_ALT		0x3554
+#define USB_DEVICE_ID_ATK_VXE_R1_SE_DONGLE	0x1085
+
 #define USB_VENDOR_ID_AUREAL		0x0755
 #define USB_DEVICE_ID_AUREAL_W01RN	0x2626
 
@@ -1169,6 +1175,11 @@
 #define USB_VENDOR_ID_PRODIGE		0x05af
 #define USB_DEVICE_ID_PRODIGE_CORDLESS	0x3062
 
+#define USB_VENDOR_ID_PULSAR		0x3554
+#define USB_DEVICE_ID_PULSAR_WIRED	0xf507
+#define USB_DEVICE_ID_PULSAR_1KHZ	0xf508
+#define USB_DEVICE_ID_PULSAR_4KHZ	0xf509
+
 #define I2C_VENDOR_ID_QTEC              0x6243
 
 #define USB_VENDOR_ID_QUANTA		0x0408
@@ -1471,6 +1482,10 @@
 #define USB_VENDOR_ID_VTL		0x0306
 #define USB_DEVICE_ID_VTL_MULTITOUCH_FF3F	0xff3f
 
+#define USB_VENDOR_ID_VXE				0x3554
+#define USB_DEVICE_ID_VXE_DRAGONFLY_R1_PRO_DONGLE	0xf58a
+#define USB_DEVICE_ID_VXE_DRAGONFLY_R1_PRO_WIRED	0xf58c
+
 #define USB_VENDOR_ID_WACOM		0x056a
 #define USB_DEVICE_ID_WACOM_GRAPHIRE_BLUETOOTH	0x81
 #define USB_DEVICE_ID_WACOM_INTUOS4_BLUETOOTH   0x00BD
diff --git a/drivers/hid/hid-pulsar.c b/drivers/hid/hid-pulsar.c
new file mode 100644
index 000000000000..f95b7a33d9e5
--- /dev/null
+++ b/drivers/hid/hid-pulsar.c
@@ -0,0 +1,754 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * HID driver for pulsar mice
+ *
+ * Supported pulsar devices:
+ *	- Pulsar
+ *		- X2 V2
+ *		- X2H
+ *		- X2A
+ *		- Xlite V3
+ *	- Kysona
+ *		-M600
+ *	- ATK
+ *		- VXE R1 SE+
+ *	- VXE
+ *		- Dragonfly R1 Pro
+ *
+ * Copyright (c) 2026 Nikolas Koesling
+ */
+
+#include <linux/hid.h>
+#include <linux/usb.h>
+#include <linux/power_supply.h>
+#include "hid-ids.h"
+
+/* ----- driver settings ----- */
+#define CMD_TIMEOUT_MSEC 100
+#define MAX_BATTERY_AGE_NS 60000000000ULL	/* 60s */
+#define MAX_UNAVAIL_AGE_NS 5000000000ULL	/* 5s */
+#define INIT_RETRIES 1
+#define INIT_DELAY_MSEC 1000
+
+/* ----- constants ----- */
+#define USB_INTERFACE 1
+#define USB_PAYLOAD_LEN 17
+#define CMD_HID_REPORT_ID 0x08
+#define CHECKSUM_MAGIC 0x55
+#define DEV_INFO_LEN 4
+#define CON_1K 0x00
+#define CON_4K 0x01
+#define CON_WIRED 0x02
+
+/* ----- device commands ----- */
+enum pulsar_cmd {
+	CMD_NONE = 0,
+	CMD_INFO = 0x01,
+	CMD_STATUS = 0x03,
+	CMD_POWER = 0x04,
+	CMD_EVENT = 0x0a,	/* recv only */
+};
+
+#define EVENT_PWR 0x40		/* power status change */
+#define EVENT_PWR_CHK 0xf9
+
+/* ----- device types ----- */
+enum dev_type {
+	TYPE_UNKNOWN,
+	TYPE_PULSAR,
+	TYPE_KYSONA,
+	TYPE_ATK,
+	TYPE_VXE,
+};
+
+/* ----- structs ----- */
+struct pulsar_battery {
+	struct power_supply *ps;
+	struct power_supply_desc desc;
+	char name[48];
+	char model[MAX(32, sizeof((struct hid_device){}).name)];
+	u8 level;		/* percent */
+	u16 voltage;		/* millivolts */
+	bool conn;
+	bool available;
+	u64 last_read;
+	u64 last_status;
+};
+
+struct pulsar_data {
+	struct hid_device *hdev;
+
+	enum dev_type type;
+
+	spinlock_t raw_event_lock;	/* protects response_buf, pending_event */
+	struct mutex lock_cmd;		/* serializes device command execution */
+	struct rw_semaphore lock_bat;	/* protects battery state */
+
+	struct completion response_ready;
+	u8 response_buf[USB_PAYLOAD_LEN];
+	u8 pending_event;
+	struct work_struct power_uevent_work;
+	struct delayed_work init_work;
+	unsigned int init_retries;
+	atomic_t device_verified;
+	atomic_t stopping;
+
+	struct pulsar_battery battery;
+};
+
+static u8 calc_checksum(const u8 *data, size_t len)
+{
+	u8 sum = 0;
+
+	for (size_t i = 0; i < len - 1; i++)
+		sum += data[i];
+
+	return (u8)CHECKSUM_MAGIC - sum;
+}
+
+static int send_cmd(struct hid_device *hdev, const u8 *buf, size_t len)
+{
+	int ret;
+	u8 *dmabuf;
+
+	hid_dbg(hdev, "send command: %*ph\n", (int)len, buf);
+
+	dmabuf = kmemdup(buf, len, GFP_KERNEL);
+	if (!dmabuf)
+		return -ENOMEM;
+
+	/* device listens only to control transfers */
+	ret = hid_hw_raw_request(hdev, dmabuf[0], dmabuf, len,
+				 HID_OUTPUT_REPORT, HID_REQ_SET_REPORT);
+
+	kfree(dmabuf);
+
+	if (ret < 0)
+		return ret;
+	if (ret != len)
+		return -EIO;
+
+	return 0;
+}
+
+static int exec_cmd(struct pulsar_data *drvdata, const u8 *payload,
+		    u8 *response, unsigned int timeout_msec)
+{
+	struct hid_device *hdev = drvdata->hdev;
+	unsigned long flags;
+	int ret;
+	unsigned long timeout;
+	u8 checksum;
+
+	if (atomic_read(&drvdata->stopping))
+		return -ENODEV;
+
+	mutex_lock(&drvdata->lock_cmd);
+
+	if (atomic_read(&drvdata->stopping)) {
+		ret = -ENODEV;
+		goto out;
+	}
+
+	spin_lock_irqsave(&drvdata->raw_event_lock, flags);
+	reinit_completion(&drvdata->response_ready);
+	drvdata->pending_event = payload[1];
+	spin_unlock_irqrestore(&drvdata->raw_event_lock, flags);
+
+	ret = send_cmd(hdev, payload, USB_PAYLOAD_LEN);
+
+	if (ret < 0) {
+		spin_lock_irqsave(&drvdata->raw_event_lock, flags);
+		drvdata->pending_event = CMD_NONE;
+		spin_unlock_irqrestore(&drvdata->raw_event_lock, flags);
+		hid_err(hdev, "failed to send command 0x%02x: %d\n",
+			payload[1], ret);
+		goto out;
+	}
+
+	timeout = wait_for_completion_timeout(&drvdata->response_ready,
+					      msecs_to_jiffies(timeout_msec));
+
+	if (timeout == 0) {
+		spin_lock_irqsave(&drvdata->raw_event_lock, flags);
+		drvdata->pending_event = CMD_NONE;
+		spin_unlock_irqrestore(&drvdata->raw_event_lock, flags);
+		ret = -ETIMEDOUT;
+		goto out;
+	}
+
+	spin_lock_irqsave(&drvdata->raw_event_lock, flags);
+	memcpy(response, drvdata->response_buf, USB_PAYLOAD_LEN);
+	spin_unlock_irqrestore(&drvdata->raw_event_lock, flags);
+
+	/* validate checksum */
+	checksum = calc_checksum(response, USB_PAYLOAD_LEN);
+
+	if (response[USB_PAYLOAD_LEN - 1] != checksum) {
+		hid_err(hdev,
+			"invalid checksum in response: 0x%02x (expected 0x%02x)\n",
+			response[USB_PAYLOAD_LEN - 1], checksum);
+		ret = -EIO;
+		goto out;
+	}
+
+	ret = 0;
+out:
+	mutex_unlock(&drvdata->lock_cmd);
+	return ret;
+}
+
+static inline void finalize_payload(u8 *payload, u8 cmd)
+{
+	payload[0] = CMD_HID_REPORT_ID;
+	payload[1] = cmd;
+	payload[USB_PAYLOAD_LEN - 1] = calc_checksum(payload, USB_PAYLOAD_LEN);
+}
+
+static int read_status(struct pulsar_data *drvdata)
+{
+	int ret;
+	u8 payload[USB_PAYLOAD_LEN] = { 0 };
+	u8 response[USB_PAYLOAD_LEN];
+
+	finalize_payload(payload, CMD_STATUS);
+
+	ret = exec_cmd(drvdata, payload, response, CMD_TIMEOUT_MSEC);
+	if (ret < 0)
+		return ret;
+	if (response[6] > 0x01)
+		return -EIO;
+
+	return (int)response[6];	/* 1: available, 0: not available */
+}
+
+static int read_device_info(struct pulsar_data *drvdata, u8 *data)
+{
+	int ret;
+	u8 payload[USB_PAYLOAD_LEN] = { 0 };
+	u8 response[USB_PAYLOAD_LEN];
+
+	payload[5] = DEV_INFO_LEN * 2;
+	get_random_bytes(payload + 6, DEV_INFO_LEN);
+	finalize_payload(payload, CMD_INFO);
+
+	ret = exec_cmd(drvdata, payload, response, CMD_TIMEOUT_MSEC);
+	if (ret < 0)
+		return ret;
+
+	if (data)
+		memcpy(data, response + 6 + DEV_INFO_LEN, DEV_INFO_LEN);
+
+	response[8 + DEV_INFO_LEN] = 0;
+	response[9 + DEV_INFO_LEN] = 0;
+
+	/*
+	 * Verify challenge-response. Response layout from offset 6:
+	 *   [0..3] encoded response   [4..7] device info (ID + conn type)
+	 *
+	 * resp[i] = challenge[i] * (i+1) + challenge[(i+1) % 4] + device_id[i]
+	 *
+	 * bytes 6..7 are zeroed for verification.
+	 */
+	for (int i = 0; i < DEV_INFO_LEN; i++) {
+		u8 expect = response[6 + DEV_INFO_LEN + i];
+		u8 actual = response[6 + i] - (i + 1) * payload[6 + i] -
+		    payload[6 + (i + 1) % DEV_INFO_LEN];
+
+		if (expect != actual) {
+			hid_warn(drvdata->hdev,
+				 "device info[%d] mismatch: %02x != %02x\n",
+				 i, expect, actual);
+			return -EIO;
+		}
+	}
+
+	return 0;
+}
+
+static int read_power(struct pulsar_data *drvdata)
+{
+	u64 now;
+	bool need_status, need_power;
+	int ret = 0;
+	u8 payload[USB_PAYLOAD_LEN] = { 0 };
+	u8 response[USB_PAYLOAD_LEN];
+	struct pulsar_battery *battery = &drvdata->battery;
+
+	now = ktime_get_ns();
+
+	down_write(&drvdata->lock_bat);
+
+	need_status = (now - battery->last_status >= MAX_UNAVAIL_AGE_NS);
+	need_power = battery->available &&
+	    (now - battery->last_read >= MAX_BATTERY_AGE_NS);
+
+	if (!need_status && !need_power)
+		goto unlock;
+
+	if (need_status) {
+		ret = read_status(drvdata);
+		if (ret < 0) {
+			hid_err(drvdata->hdev,
+				"%s: failed to read status: %d\n",
+				__func__, ret);
+			goto unlock;
+		}
+
+		battery->last_status = now;
+
+		if (!ret) {
+			battery->available = false;
+			goto unlock;
+		}
+
+		/* device just became available, force power read */
+		if (!battery->available)
+			need_power = true;
+	}
+
+	if (!need_power)
+		goto unlock;
+
+	finalize_payload(payload, CMD_POWER);
+
+	ret = exec_cmd(drvdata, payload, response, CMD_TIMEOUT_MSEC);
+	if (ret < 0) {
+		hid_err(drvdata->hdev, "%s: failed to read power: %d\n",
+			__func__, ret);
+		goto unlock;
+	}
+
+	if (response[6] > 100 || response[7] > 0x01) {
+		ret = -EIO;
+		goto unlock;
+	}
+
+	battery->available = true;
+	battery->level = response[6];
+	battery->conn = response[7] == 1;
+	battery->voltage = (response[8] << 8) | response[9];
+	battery->last_read = now;
+
+	hid_dbg(drvdata->hdev, "%s: level=%d, conn=%d, voltage=%d\n",
+		__func__, battery->level, battery->conn, battery->voltage);
+
+unlock:
+	up_write(&drvdata->lock_bat);
+	return ret;
+}
+
+static int battery_get_property(struct power_supply *psy,
+				enum power_supply_property psp,
+				union power_supply_propval *val)
+{
+	struct pulsar_data *drvdata;
+	int ret;
+
+	drvdata = power_supply_get_drvdata(psy);
+
+	ret = read_power(drvdata);
+	if (ret)
+		return ret;
+
+	down_read(&drvdata->lock_bat);
+
+	switch (psp) {
+	case POWER_SUPPLY_PROP_STATUS:
+		if (!drvdata->battery.available)
+			val->intval = POWER_SUPPLY_STATUS_UNKNOWN;
+		else if (drvdata->battery.conn && drvdata->battery.level < 100)
+			val->intval = POWER_SUPPLY_STATUS_CHARGING;
+		else if (drvdata->battery.conn && drvdata->battery.level >= 100)
+			val->intval = POWER_SUPPLY_STATUS_FULL;
+		else
+			val->intval = POWER_SUPPLY_STATUS_DISCHARGING;
+		break;
+	case POWER_SUPPLY_PROP_CAPACITY:
+		val->intval = drvdata->battery.level;
+		break;
+	case POWER_SUPPLY_PROP_VOLTAGE_NOW:
+		val->intval = drvdata->battery.voltage * 1000;
+		break;
+	case POWER_SUPPLY_PROP_PRESENT:
+	case POWER_SUPPLY_PROP_ONLINE:
+		val->intval = drvdata->battery.available;
+		break;
+	case POWER_SUPPLY_PROP_SCOPE:
+		val->intval = POWER_SUPPLY_SCOPE_DEVICE;
+		break;
+	case POWER_SUPPLY_PROP_MODEL_NAME:
+		val->strval = drvdata->battery.model;
+		break;
+	default:
+		ret = -EINVAL;
+	}
+
+	up_read(&drvdata->lock_bat);
+	return ret;
+}
+
+static void power_uevent_work_handler(struct work_struct *work)
+{
+	struct pulsar_data *drvdata;
+	int ret;
+
+	drvdata = container_of(work, struct pulsar_data, power_uevent_work);
+
+	if (atomic_read(&drvdata->stopping))
+		return;
+
+	down_write(&drvdata->lock_bat);
+	drvdata->battery.last_read = 0;
+	drvdata->battery.last_status = 0;
+	up_write(&drvdata->lock_bat);
+
+	ret = read_power(drvdata);
+	if (ret < 0) {
+		hid_err(drvdata->hdev, "%s: failed to read power: %d\n",
+			__func__, ret);
+		return;
+	}
+
+	power_supply_changed(drvdata->battery.ps);
+}
+
+static int pulsar_raw_event(struct hid_device *hdev,
+			    struct hid_report *report, u8 *data, int size)
+{
+	struct pulsar_data *drvdata;
+
+	drvdata = hid_get_drvdata(hdev);
+	if (!drvdata)
+		return 0;
+
+	hid_dbg(hdev, "received raw event: %*ph\n", size, data);
+
+	if (size != USB_PAYLOAD_LEN || data[0] != CMD_HID_REPORT_ID)
+		return 0;
+
+	if (data[1] != CMD_EVENT) {
+		spin_lock(&drvdata->raw_event_lock);
+		if (drvdata->pending_event != data[1]) {
+			spin_unlock(&drvdata->raw_event_lock);
+			return 0;
+		}
+		memcpy(drvdata->response_buf, data, size);
+		drvdata->pending_event = CMD_NONE;
+		complete(&drvdata->response_ready);
+		spin_unlock(&drvdata->raw_event_lock);
+		return 1;
+	}
+
+	if (!atomic_read(&drvdata->device_verified))
+		return 0;
+
+	if (data[6] == EVENT_PWR && data[USB_PAYLOAD_LEN - 1] == EVENT_PWR_CHK) {
+		schedule_work(&drvdata->power_uevent_work);
+		hid_dbg(hdev, "received power event\n");
+		return 1;
+	}
+
+	return 0;
+}
+
+static const enum power_supply_property pulsar_battery_props[] = {
+	POWER_SUPPLY_PROP_STATUS, POWER_SUPPLY_PROP_CAPACITY,
+	POWER_SUPPLY_PROP_VOLTAGE_NOW, POWER_SUPPLY_PROP_ONLINE,
+	POWER_SUPPLY_PROP_MODEL_NAME, POWER_SUPPLY_PROP_SCOPE,
+	POWER_SUPPLY_PROP_PRESENT
+};
+
+static void init_power_supply_desc(struct pulsar_data *drvdata)
+{
+	drvdata->battery.desc.name = drvdata->battery.name;
+	drvdata->battery.desc.type = POWER_SUPPLY_TYPE_BATTERY;
+	drvdata->battery.desc.properties = pulsar_battery_props;
+	drvdata->battery.desc.num_properties = ARRAY_SIZE(pulsar_battery_props);
+	drvdata->battery.desc.get_property = battery_get_property;
+}
+
+static void model_pulsar(u8 *device_id, struct pulsar_data *drvdata)
+{
+	u16 model_id;
+	const char *con_type = "unknown";
+
+	model_id = device_id[0] << 8 | device_id[1];
+
+	switch (device_id[2]) {
+	case CON_1K:
+		con_type = "1kHz";
+		break;
+	case CON_4K:
+		con_type = "4kHz";
+		break;
+	case CON_WIRED:
+		con_type = "wired";
+		break;
+	}
+
+	switch (model_id) {
+	case 0x060a:
+	case 0x060b:
+	case 0x0612:
+	case 0x0613:
+	case 0x0614:
+	case 0x0615:
+		snprintf(drvdata->battery.model, sizeof(drvdata->battery.model),
+			 "Pulsar X2 V2 (%s)", con_type);
+		break;
+	case 0x060c:
+	case 0x060d:
+		snprintf(drvdata->battery.model, sizeof(drvdata->battery.model),
+			 "Pulsar X2H (%s)", con_type);
+		break;
+	case 0x0607:
+	case 0x060e:
+	case 0x060f:
+	case 0x0610:
+	case 0x0611:
+		snprintf(drvdata->battery.model, sizeof(drvdata->battery.model),
+			 "Pulsar Xlite V3 (%s)", con_type);
+		break;
+	case 0x0608:
+	case 0x0609:
+		snprintf(drvdata->battery.model, sizeof(drvdata->battery.model),
+			 "Pulsar X2A (%s)", con_type);
+		break;
+	default:
+		snprintf(drvdata->battery.model, sizeof(drvdata->battery.model),
+			 "Pulsar unknown (%s)", con_type);
+	}
+}
+
+static void model_atk(u8 *device_id, struct pulsar_data *drvdata)
+{
+	u16 model_id;
+	const char *con_type = "unknown";
+
+	model_id = device_id[0] << 8 | device_id[1];
+
+	switch (device_id[2]) {
+	case CON_1K:
+		con_type = "1kHz";
+		break;
+	case CON_4K:
+		con_type = "4kHz";
+		break;
+	case CON_WIRED:
+		con_type = "wired";
+		break;
+	}
+
+	switch (model_id) {
+	case 0x0220:
+		snprintf(drvdata->battery.model, sizeof(drvdata->battery.model),
+			 "ATK VXE R1 SE+ (%s)", con_type);
+		break;
+	default:
+		snprintf(drvdata->battery.model, sizeof(drvdata->battery.model),
+			 "Unknown ATK (%s)", con_type);
+	}
+}
+
+static void pulsar_init_work(struct work_struct *work)
+{
+	struct pulsar_data *drvdata;
+	struct hid_device *hdev;
+	struct power_supply_config psy_cfg;
+	int ret;
+	u8 data[DEV_INFO_LEN];
+
+	drvdata = container_of(work, struct pulsar_data, init_work.work);
+	hdev = drvdata->hdev;
+
+	ret = read_device_info(drvdata, data);
+	if (ret == -ETIMEDOUT) {
+		if (drvdata->init_retries--) {
+			hid_dbg(hdev,
+				"device info read timed out, retrying (%u left)\n",
+				drvdata->init_retries);
+			schedule_delayed_work(&drvdata->init_work,
+					      msecs_to_jiffies
+					      (INIT_DELAY_MSEC));
+			return;
+		}
+		hid_err(hdev, "device info read timed out, giving up\n");
+		return;
+	}
+	if (ret < 0) {
+		if (drvdata->type == TYPE_PULSAR) {
+			hid_err(hdev, "failed to read device info: %d\n", ret);
+			return;
+		}
+		hid_dbg(hdev, "failed to read device info: %d\n", ret);
+		snprintf(drvdata->battery.model,
+			 sizeof(drvdata->battery.model), "%s", hdev->name);
+		goto register_battery;
+	}
+
+	hid_dbg(hdev, "device info: %*ph (%d)\n", DEV_INFO_LEN, data, ret);
+
+	switch (drvdata->type) {
+	case TYPE_PULSAR:
+		model_pulsar(data, drvdata);
+		break;
+	case TYPE_ATK:
+		model_atk(data, drvdata);
+		break;
+	default:
+		snprintf(drvdata->battery.model, sizeof(drvdata->battery.model),
+			 "%s", hdev->name);
+	}
+
+register_battery:
+	init_power_supply_desc(drvdata);
+
+	psy_cfg = (struct power_supply_config) {.drv_data = drvdata };
+	drvdata->battery.ps =
+	    devm_power_supply_register(&hdev->dev, &drvdata->battery.desc,
+				       &psy_cfg);
+	if (IS_ERR(drvdata->battery.ps)) {
+		hid_err(hdev, "failed to register battery: %ld\n",
+			PTR_ERR(drvdata->battery.ps));
+		drvdata->battery.ps = NULL;
+		return;
+	}
+
+	atomic_set(&drvdata->device_verified, 1);
+	hid_info(hdev, "device verified, battery registered\n");
+}
+
+static int pulsar_probe(struct hid_device *hdev, const struct hid_device_id *id)
+{
+	int ret;
+	struct usb_interface *intf;
+	struct usb_device *usbdev;
+	struct pulsar_data *drvdata;
+	struct hid_report *report_in;
+	struct hid_report *report_out;
+
+	if (!hid_is_usb(hdev))
+		return -ENODEV;
+
+	ret = hid_parse(hdev);
+	if (ret < 0) {
+		hid_err(hdev, "hid_parse failed: %d\n", ret);
+		return ret;
+	}
+
+	intf = to_usb_interface(hdev->dev.parent);
+	report_in =
+	    hdev->report_enum[HID_INPUT_REPORT].report_id_hash[CMD_HID_REPORT_ID];
+	report_out =
+	    hdev->report_enum[HID_OUTPUT_REPORT].report_id_hash[CMD_HID_REPORT_ID];
+
+	if (!report_in || !report_out ||
+	    hid_report_len(report_in) != USB_PAYLOAD_LEN ||
+	    hid_report_len(report_out) != USB_PAYLOAD_LEN ||
+	    intf->cur_altsetting->desc.bInterfaceNumber != USB_INTERFACE)
+		return hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+
+	drvdata = devm_kzalloc(&hdev->dev, sizeof(*drvdata), GFP_KERNEL);
+	if (!drvdata)
+		return -ENOMEM;
+
+	drvdata->hdev = hdev;
+	drvdata->type = id->driver_data;
+
+	mutex_init(&drvdata->lock_cmd);
+	init_rwsem(&drvdata->lock_bat);
+
+	usbdev = interface_to_usbdev(intf);
+
+	spin_lock_init(&drvdata->raw_event_lock);
+	hid_set_drvdata(hdev, drvdata);
+	init_completion(&drvdata->response_ready);
+	INIT_WORK(&drvdata->power_uevent_work, power_uevent_work_handler);
+	INIT_DELAYED_WORK(&drvdata->init_work, pulsar_init_work);
+	drvdata->init_retries = INIT_RETRIES;
+
+	snprintf(drvdata->battery.name, sizeof(drvdata->battery.name),
+		 "pulsar_%s_battery", usbdev->devpath);
+
+	ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+	if (ret < 0) {
+		hid_err(hdev, "hw start failed\n");
+		return ret;
+	}
+
+	ret = hid_hw_open(hdev);
+	if (ret < 0) {
+		hid_err(hdev, "hw open failed\n");
+		goto err_open;
+	}
+
+	schedule_delayed_work(&drvdata->init_work, 0);
+
+	return 0;
+
+err_open:
+	cancel_work_sync(&drvdata->power_uevent_work);
+	hid_hw_stop(hdev);
+	return ret;
+}
+
+static void pulsar_remove(struct hid_device *hdev)
+{
+	struct pulsar_data *drvdata;
+
+	drvdata = hid_get_drvdata(hdev);
+	if (!drvdata) {
+		hid_hw_stop(hdev);
+		return;
+	}
+
+	atomic_set(&drvdata->stopping, 1);
+	cancel_delayed_work_sync(&drvdata->init_work);
+	cancel_work_sync(&drvdata->power_uevent_work);
+
+	/* wait for active device i/o (exec_cmd) */
+	mutex_lock(&drvdata->lock_cmd);
+	hid_hw_close(hdev);
+	mutex_unlock(&drvdata->lock_cmd);
+
+	hid_hw_stop(hdev);
+	mutex_destroy(&drvdata->lock_cmd);
+}
+
+static const struct hid_device_id pulsar_table[] = {
+	{ HID_USB_DEVICE(USB_VENDOR_ID_PULSAR, USB_DEVICE_ID_PULSAR_WIRED),
+	  .driver_data = TYPE_PULSAR },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_PULSAR, USB_DEVICE_ID_PULSAR_1KHZ),
+	  .driver_data = TYPE_PULSAR },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_PULSAR, USB_DEVICE_ID_PULSAR_4KHZ),
+	  .driver_data = TYPE_PULSAR },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_KYSONA, USB_DEVICE_ID_KYSONA_M600_DONGLE),
+	  .driver_data = TYPE_KYSONA },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_KYSONA, USB_DEVICE_ID_KYSONA_M600_WIRED),
+	  .driver_data = TYPE_KYSONA },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_ATK, USB_DEVICE_ID_ATK_VXE_R1_SE_DONGLE),
+	  .driver_data = TYPE_ATK },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_ATK_ALT, USB_DEVICE_ID_ATK_VXE_R1_SE_WIRED),
+	  .driver_data = TYPE_ATK },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_VXE, USB_DEVICE_ID_VXE_DRAGONFLY_R1_PRO_DONGLE),
+	  .driver_data = TYPE_VXE },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_VXE, USB_DEVICE_ID_VXE_DRAGONFLY_R1_PRO_WIRED),
+	  .driver_data = TYPE_VXE },
+	{ }
+};
+
+static struct hid_driver pulsar_driver = {
+	.name = "pulsar",
+	.id_table = pulsar_table,
+	.probe = pulsar_probe,
+	.remove = pulsar_remove,
+	.raw_event = pulsar_raw_event,
+};
+
+module_hid_driver(pulsar_driver);
+
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("HID driver for pulsar mice");
+MODULE_AUTHOR("Nikolas Koesling");
+MODULE_DEVICE_TABLE(hid, pulsar_table);
-- 
2.53.0


^ permalink raw reply related

* Re: [PATCH v2 0/5] iio: buffer: fix timestamp alignment (in rare case)
From: Jonathan Cameron @ 2026-04-12 14:20 UTC (permalink / raw)
  To: Nuno Sá
  Cc: David Lechner, Jiri Kosina, Srinivas Pandruvada, Nuno Sá,
	Andy Shevchenko, Lars Möllendorf, Lars-Peter Clausen,
	Greg Kroah-Hartman, Jonathan Cameron, Lixu Zhang, Francesco Lavra,
	linux-input, linux-iio, linux-kernel
In-Reply-To: <00213b587ae4f9bde11ec928081abb60ddbed09a.camel@gmail.com>

On Mon, 09 Mar 2026 14:15:41 +0000
Nuno Sá <noname.nuno@gmail.com> wrote:

> On Sat, 2026-03-07 at 19:44 -0600, David Lechner wrote:
> > In [1], it was pointed out that the iio_push_to_buffers_with_timestamp()
> > function is not putting the timestamp at the correct offset in the scan
> > buffer in rare cases where the largest scan element size is larger than
> > sizeof(int64_t).
> > 
> > [1]: https://lore.kernel.org/linux-iio/20260215162351.79f40b32@jic23-huawei/
> > 
> > This only affected one driver, namely hid-sensor-rotation since it is
> > the only driver that meets the condition. To fix things up, first we
> > fix the hid-sensor-rotation driver in a way that preserves compatibility
> > with the broken timestamp alignment. Then we are free to fix the core
> > IIO code without affecting any users.
> > 
> > The first patch depends on [2] which is now in iio/fixes-togreg. It
> > should be OK to apply the first patch there and let the rest of the
> > patches go through iio/togreg (the later patches are just preventing
> > future bugs).
> > 
> > [2]:
> > https://lore.kernel.org/linux-iio/20260228-iio-fix-repeat-alignment-v2-0-d58bfaa2920d@baylibre.com/
> > 
> > Signed-off-by: David Lechner <dlechner@baylibre.com>
> > ---  
> 
> LGTM,
> 
> Reviewed-by: Nuno Sá <nuno.sa@analog.com>
Applied 2-5 to the testing branch of iio.git.
Next cycle material so won't be in next until I can rebase on rc1.

Thanks,

Jonathan

> 
> > Changes in v2:
> > - Don't say "HACK" in comments.
> > - Cache timestamp offset instead of largest scan element size.
> > - New patch to ensure size/alignment is always power of 2 bytes.
> > - Link to v1:
> > https://lore.kernel.org/r/20260301-iio-fix-timestamp-alignment-v1-0-1a54980bfb90@baylibre.com
> > 
> > ---
> > David Lechner (5):
> >       iio: orientation: hid-sensor-rotation: add timestamp hack to not break userspace
> >       iio: buffer: check return value of iio_compute_scan_bytes()
> >       iio: buffer: cache timestamp offset in scan buffer
> >       iio: buffer: ensure repeat alignment is a power of two
> >       iio: buffer: fix timestamp alignment when quaternion in scan
> > 
> >  drivers/iio/industrialio-buffer.c             | 46 ++++++++++++++++++++-------
> >  drivers/iio/orientation/hid-sensor-rotation.c | 22 +++++++++++--
> >  include/linux/iio/buffer.h                    | 12 +++++--
> >  include/linux/iio/iio.h                       |  3 ++
> >  4 files changed, 66 insertions(+), 17 deletions(-)
> > ---
> > base-commit: 6f25a6105c41a7d6b12986dbe80ded396a5667f8
> > change-id: 20260228-iio-fix-timestamp-alignment-89ade1af458b
> > prerequisite-message-id: <20260228-iio-fix-repeat-alignment-v2-0-d58bfaa2920d@baylibre.com>
> > prerequisite-patch-id: e155a526d57c5759a2fcfbfca7f544cb419addfd
> > prerequisite-patch-id: 6c69eaad0dd2ae69bd2745e7d387f739fc1a9ba0
> > 
> > Best regards,
> > --  
> > David Lechner <dlechner@baylibre.com>  
> 


^ permalink raw reply

* Re: [PATCH v2] iio: orientation: hid-sensor-rotation: use ext_scan_type
From: Jonathan Cameron @ 2026-04-12 14:26 UTC (permalink / raw)
  To: David Lechner
  Cc: Jiri Kosina, Srinivas Pandruvada, Nuno Sá, Andy Shevchenko,
	linux-input, linux-iio, linux-kernel
In-Reply-To: <20260301-iio-hid-sensor-rotation-cleanup-v2-1-245c6ad59afc@baylibre.com>

On Sun, 01 Mar 2026 17:46:48 -0600
David Lechner <dlechner@baylibre.com> wrote:

> Make use of ext_scan_type to handle the dynamic realbits size of the
> quaternion data. This lets us implement it using static data rather than
> having to duplicate the channel info for each driver instance.
> 
> Signed-off-by: David Lechner <dlechner@baylibre.com>
> ---
I'm going to apply this now, but would welcome any additional feedback
from Srinivas or others.

Note, given this is next cycle material now I'll only push the tree out
as testing until I can rebase on rc1.

Thanks,

Jonathan

> This is something I noticed we could do while looking at an unrelated
> bug. I've tested this using the same script from [1] and confirmed that
> that the scan type didn't change. Before and after are both:
> 
> $ cat in_rot_quaternion_type
> le:s16/32X4>>0  
> 
> [1]: https://lore.kernel.org/linux-iio/20260301-iio-fix-timestamp-alignment-v1-1-1a54980bfb90@baylibre.com/
> ---
> Changes in v2:
> - Dropped DEV_ROT_SCAN_TYPE_8BIT.
> - Tested using /dev/uhid.
> - Link to v1: https://lore.kernel.org/r/20260214-iio-hid-sensor-rotation-cleanup-v1-1-3aec9a533c0f@baylibre.com
> ---
>  drivers/iio/orientation/hid-sensor-rotation.c | 71 ++++++++++++++++-----------
>  1 file changed, 43 insertions(+), 28 deletions(-)
> 
> diff --git a/drivers/iio/orientation/hid-sensor-rotation.c b/drivers/iio/orientation/hid-sensor-rotation.c
> index e759f91a710a..3cfd0b323514 100644
> --- a/drivers/iio/orientation/hid-sensor-rotation.c
> +++ b/drivers/iio/orientation/hid-sensor-rotation.c
> @@ -34,6 +34,27 @@ static const u32 rotation_sensitivity_addresses[] = {
>  	HID_USAGE_SENSOR_ORIENT_QUATERNION,
>  };
>  
> +enum {
> +	DEV_ROT_SCAN_TYPE_16BIT,
> +	DEV_ROT_SCAN_TYPE_32BIT,
> +};
> +
> +static const struct iio_scan_type dev_rot_scan_types[] = {
> +	[DEV_ROT_SCAN_TYPE_16BIT] = {
> +		.sign = 's',
> +		.realbits = 16,
> +		/* Storage bits has to stay 32 to not break userspace. */
> +		.storagebits = 32,
> +		.repeat = 4,
> +	},
> +	[DEV_ROT_SCAN_TYPE_32BIT] = {
> +		.sign = 's',
> +		.realbits = 32,
> +		.storagebits = 32,
> +		.repeat = 4,
> +	},
> +};
> +
>  /* Channel definitions */
>  static const struct iio_chan_spec dev_rot_channels[] = {
>  	{
> @@ -45,23 +66,14 @@ static const struct iio_chan_spec dev_rot_channels[] = {
>  					BIT(IIO_CHAN_INFO_OFFSET) |
>  					BIT(IIO_CHAN_INFO_SCALE) |
>  					BIT(IIO_CHAN_INFO_HYSTERESIS),
> -		.scan_index = 0
> +		.scan_index = 0,
> +		.has_ext_scan_type = 1,
> +		.ext_scan_type = dev_rot_scan_types,
> +		.num_ext_scan_type = ARRAY_SIZE(dev_rot_scan_types),
>  	},
>  	IIO_CHAN_SOFT_TIMESTAMP(1)
>  };
>  
> -/* Adjust channel real bits based on report descriptor */
> -static void dev_rot_adjust_channel_bit_mask(struct iio_chan_spec *chan,
> -						int size)
> -{
> -	chan->scan_type.sign = 's';
> -	/* Real storage bits will change based on the report desc. */
> -	chan->scan_type.realbits = size * 8;
> -	/* Maximum size of a sample to capture is u32 */
> -	chan->scan_type.storagebits = sizeof(u32) * 8;
> -	chan->scan_type.repeat = 4;
> -}
> -
>  /* Channel read_raw handler */
>  static int dev_rot_read_raw(struct iio_dev *indio_dev,
>  				struct iio_chan_spec const *chan,
> @@ -136,9 +148,25 @@ static int dev_rot_write_raw(struct iio_dev *indio_dev,
>  	return ret;
>  }
>  
> +static int dev_rot_get_current_scan_type(const struct iio_dev *indio_dev,
> +					 const struct iio_chan_spec *chan)
> +{
> +	struct dev_rot_state *rot_state = iio_priv(indio_dev);
> +
> +	switch (rot_state->quaternion.size / 4) {
> +	case sizeof(s16):
> +		return DEV_ROT_SCAN_TYPE_16BIT;
> +	case sizeof(s32):
> +		return DEV_ROT_SCAN_TYPE_32BIT;
> +	default:
> +		return -EINVAL;
> +	}
> +}
> +
>  static const struct iio_info dev_rot_info = {
>  	.read_raw_multi = &dev_rot_read_raw,
>  	.write_raw = &dev_rot_write_raw,
> +	.get_current_scan_type = &dev_rot_get_current_scan_type,
>  };
>  
>  /* Callback handler to send event after all samples are received and captured */
> @@ -196,7 +224,6 @@ static int dev_rot_capture_sample(struct hid_sensor_hub_device *hsdev,
>  /* Parse report which is specific to an usage id*/
>  static int dev_rot_parse_report(struct platform_device *pdev,
>  				struct hid_sensor_hub_device *hsdev,
> -				struct iio_chan_spec *channels,
>  				unsigned usage_id,
>  				struct dev_rot_state *st)
>  {
> @@ -210,9 +237,6 @@ static int dev_rot_parse_report(struct platform_device *pdev,
>  	if (ret)
>  		return ret;
>  
> -	dev_rot_adjust_channel_bit_mask(&channels[0],
> -		st->quaternion.size / 4);
> -
>  	dev_dbg(&pdev->dev, "dev_rot %x:%x\n", st->quaternion.index,
>  		st->quaternion.report_id);
>  
> @@ -271,22 +295,13 @@ static int hid_dev_rot_probe(struct platform_device *pdev)
>  		return ret;
>  	}
>  
> -	indio_dev->channels = devm_kmemdup(&pdev->dev, dev_rot_channels,
> -					   sizeof(dev_rot_channels),
> -					   GFP_KERNEL);
> -	if (!indio_dev->channels) {
> -		dev_err(&pdev->dev, "failed to duplicate channels\n");
> -		return -ENOMEM;
> -	}
> -
> -	ret = dev_rot_parse_report(pdev, hsdev,
> -				   (struct iio_chan_spec *)indio_dev->channels,
> -					hsdev->usage, rot_state);
> +	ret = dev_rot_parse_report(pdev, hsdev, hsdev->usage, rot_state);
>  	if (ret) {
>  		dev_err(&pdev->dev, "failed to setup attributes\n");
>  		return ret;
>  	}
>  
> +	indio_dev->channels = dev_rot_channels;
>  	indio_dev->num_channels = ARRAY_SIZE(dev_rot_channels);
>  	indio_dev->info = &dev_rot_info;
>  	indio_dev->name = name;
> 
> ---
> base-commit: 3fa5e5702a82d259897bd7e209469bc06368bf31
> change-id: 20260214-iio-hid-sensor-rotation-cleanup-84e8410926ef
> 
> Best regards,


^ permalink raw reply

* [PATCH v3 0/5] Add OneXPlayer Configuration HID Driver
From: Derek J. Clark @ 2026-04-12 21:34 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Pierre-Loup A . Griffais, Lambert Fan, Zhouwang Huang,
	Derek J . Clark, linux-input, linux-doc, linux-kernel

Adds an HID driver for OneXPlayer HID configuration devices. There are
currently 2 generations of OneXPlayer HID protocol. The first (OneXPlayer
F1 series) only provides an RGB control interface over HID. The Second
(X1 mini series, G1 series, AOKZOE A1X) also includes a hardware level
button mapping interface, vibration intensity settings, and the ability
to switch output between xinput and a debug mode that can be used to debug
the button mapping. Some devices (G1 Series, APEX) use a hybrid of Gen1
RGB control and Gen 2 controller settings. To ensure there is no conflicts
when the driver is loaded, we skip creating the RGB interface for Gen 2
devices if there is a DMI match.

I'll also add a note that Gen 1 devices also have an interface for
setting the key map and debug mode, but that is done entirely over a
serial TTY device so it is not able to be added to this driver. There
are also some "Gen 0" devices (OneXPlayer 2 Series) also use it, but
the TTY interface also handles the RGB control so no support is
provided by this driver for those interfaces.

Signed-off-by: Derel J. Clark <derekjohn.clark@gmail.com>

Derek J. Clark (5):
  HID: hid-oxp: Add OneXPlayer configuration driver
  HID: hid-oxp: Add Second Generation RGB Control
  HID: hid-oxp: Add Second Generation Gamepad Mode Switch
  HID: hid-oxp: Add Button Mapping Interface
  HID: hid-oxp: Add Vibration Intensity Attributes

 MAINTAINERS           |    6 +
 drivers/hid/Kconfig   |   13 +
 drivers/hid/Makefile  |    1 +
 drivers/hid/hid-ids.h |    6 +
 drivers/hid/hid-oxp.c | 1575 +++++++++++++++++++++++++++++++++++++++++
 5 files changed, 1601 insertions(+)
 create mode 100644 drivers/hid/hid-oxp.c

-- 
2.53.0


^ permalink raw reply

* [PATCH v3 1/5] HID: hid-oxp: Add OneXPlayer configuration driver
From: Derek J. Clark @ 2026-04-12 21:34 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Pierre-Loup A . Griffais, Lambert Fan, Zhouwang Huang,
	Derek J . Clark, linux-input, linux-doc, linux-kernel
In-Reply-To: <20260412213444.2231505-1-derekjohn.clark@gmail.com>

Adds OneXPlayer HID configuration driver. In this initial driver patch,
add the RGB interface for the first generation of HID based RGB control.

This interface provides the following attributes:
- brightness: provided by the LED core, this works in a fairly unique
  way on this device. The hardware accepts 5 brightness values (0-4),
  which affects the brightness of the multicolor and animated effects
  built into the MCU firmware. For monocolor settings, the device
  expects the hardware brightness value to be pushed to maximum, then we
  apply brightness adjustments mathematically based on % (0-100). This
  leads to some odd conversion as we need the brightness slider to reach
  the full range, but it has no affect when incrementing between the
  division points for other effects.
- multi-intensity: provided by the LED core for red, green, and blue.
- effect: Allows the MCU to set 19 individual effects.
- effect_index: Lists the 19 valid effect names for the interface.
- enabled: Allows the MCU to toggle the RGB interface on/off.
- enabled_index: Lists the valid states for enabled.
- speed: Allows the MCU to set the animation rate for the various
  effects.
- speed_range: Lists the valid range of speed (0-9).

The MCU also has a few odd quirks that make sending multiple synchronous
events challenging. It will essentially freeze if it receives another
message before it has finished processing the last command. It also will
not reply if you wait on it using a completion. To get around this, we
do a 200ms sleep inside a work queue thread and debounce all but the most
recent message using a 50ms mod_delayed_work. This will cache the last
write, queue the work, then return so userspace can release its write
thread. The work queue is only used for brightness/multi-intensity as
that is the path likely to receive rapid successive writes.

Reviewed-by: Zhouwang Huang <honjow311@gmail.com>
Tested-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
 MAINTAINERS           |   6 +
 drivers/hid/Kconfig   |  12 +
 drivers/hid/Makefile  |   1 +
 drivers/hid/hid-ids.h |   3 +
 drivers/hid/hid-oxp.c | 651 ++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 673 insertions(+)
 create mode 100644 drivers/hid/hid-oxp.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 6f6517bf4f97..dae814192fa4 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -19707,6 +19707,12 @@ S:	Maintained
 F:	drivers/mtd/nand/onenand/
 F:	include/linux/mtd/onenand*.h
 
+ONEXPLAYER HID DRIVER
+M:	Derek J. Clark <derekjohn.clark@gmail.com>
+L:	linux-input@vger.kernel.org
+S:	Maintained
+F:	drivers/hid/hid-oxp.c
+
 ONEXPLAYER PLATFORM EC DRIVER
 M:	Antheas Kapenekakis <lkml@antheas.dev>
 M:	Derek John Clark <derekjohn.clark@gmail.com>
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 3c034cd32fa8..2deaec9f467d 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -919,6 +919,18 @@ config HID_ORTEK
 	   - Ortek WKB-2000
 	   - Skycable wireless presenter
 
+config HID_OXP
+	tristate "OneXPlayer handheld controller configuration support"
+	depends on USB_HID
+	depends on LEDS_CLASS
+	depends on LEDS_CLASS_MULTICOLOR
+	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.
+
+	  To compile this driver as a module, choose M here: the module will
+	  be called hid-oxp.
+
 config HID_PANTHERLORD
 	tristate "Pantherlord/GreenAsia game controller"
 	help
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 03ef72ec4499..bda8a24c9257 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -99,6 +99,7 @@ obj-$(CONFIG_HID_NTI)			+= hid-nti.o
 obj-$(CONFIG_HID_NTRIG)		+= hid-ntrig.o
 obj-$(CONFIG_HID_NVIDIA_SHIELD)	+= hid-nvidia-shield.o
 obj-$(CONFIG_HID_ORTEK)		+= hid-ortek.o
+obj-$(CONFIG_HID_OXP)		+= hid-oxp.o
 obj-$(CONFIG_HID_PRODIKEYS)	+= hid-prodikeys.o
 obj-$(CONFIG_HID_PANTHERLORD)	+= hid-pl.o
 obj-$(CONFIG_HID_PENMOUNT)	+= hid-penmount.o
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 5bad81222c6e..dcc5a3a70eaf 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1131,6 +1131,9 @@
 #define USB_VENDOR_ID_NVIDIA				0x0955
 #define USB_DEVICE_ID_NVIDIA_THUNDERSTRIKE_CONTROLLER	0x7214
 
+#define USB_VENDOR_ID_CRSC			0x1a2c
+#define USB_DEVICE_ID_ONEXPLAYER_GEN1		0xb001
+
 #define USB_VENDOR_ID_ONTRAK		0x0a07
 #define USB_DEVICE_ID_ONTRAK_ADU100	0x0064
 
diff --git a/drivers/hid/hid-oxp.c b/drivers/hid/hid-oxp.c
new file mode 100644
index 000000000000..c4219ecd8d71
--- /dev/null
+++ b/drivers/hid/hid-oxp.c
@@ -0,0 +1,651 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ *  HID driver for OneXPlayer gamepad configuration devices.
+ *
+ *  Copyright (c) 2026 Valve Corporation
+ */
+
+#include <linux/array_size.h>
+#include <linux/cleanup.h>
+#include <linux/delay.h>
+#include <linux/dev_printk.h>
+#include <linux/device.h>
+#include <linux/hid.h>
+#include <linux/jiffies.h>
+#include <linux/kstrtox.h>
+#include <linux/led-class-multicolor.h>
+#include <linux/mutex.h>
+#include <linux/sysfs.h>
+#include <linux/types.h>
+#include <linux/workqueue.h>
+
+#include "hid-ids.h"
+
+#define OXP_PACKET_SIZE 64
+
+#define GEN1_MESSAGE_ID	0xff
+
+#define GEN1_USAGE_PAGE	0xff01
+
+enum oxp_function_index {
+	OXP_FID_GEN1_RGB_SET =		0x07,
+	OXP_FID_GEN1_RGB_REPLY =	0x0f,
+};
+
+static struct oxp_hid_cfg {
+	struct led_classdev_mc *led_mc;
+	struct hid_device *hdev;
+	struct mutex cfg_mutex; /*ensure single synchronous output report*/
+	u8 rgb_brightness;
+	u8 rgb_effect;
+	u8 rgb_speed;
+	u8 rgb_en;
+} drvdata;
+
+enum oxp_feature_en_index {
+	OXP_FEAT_DISABLED,
+	OXP_FEAT_ENABLED,
+};
+
+static const char *const oxp_feature_en_text[] = {
+	[OXP_FEAT_DISABLED] = "false",
+	[OXP_FEAT_ENABLED] = "true",
+};
+
+enum oxp_rgb_effect_index {
+	OXP_UNKNOWN,
+	OXP_EFFECT_AURORA,
+	OXP_EFFECT_BIRTHDAY,
+	OXP_EFFECT_FLOWING,
+	OXP_EFFECT_CHROMA_1,
+	OXP_EFFECT_NEON,
+	OXP_EFFECT_CHROMA_2,
+	OXP_EFFECT_DREAMY,
+	OXP_EFFECT_WARM,
+	OXP_EFFECT_CYBERPUNK,
+	OXP_EFFECT_SEA,
+	OXP_EFFECT_SUNSET,
+	OXP_EFFECT_COLORFUL,
+	OXP_EFFECT_MONSTER,
+	OXP_EFFECT_GREEN,
+	OXP_EFFECT_BLUE,
+	OXP_EFFECT_YELLOW,
+	OXP_EFFECT_TEAL,
+	OXP_EFFECT_PURPLE,
+	OXP_EFFECT_FOGGY,
+	OXP_EFFECT_MONO_LIST, /* placeholder for effect_index_show */
+};
+
+/* These belong to rgb_effect_index, but we want to hide them from
+ * rgb_effect_text
+ */
+
+#define OXP_GET_PROPERTY 0xfc
+#define OXP_SET_PROPERTY 0xfd
+#define OXP_EFFECT_MONO_TRUE 0xfe /* actual index for monocolor */
+
+static const char *const oxp_rgb_effect_text[] = {
+	[OXP_UNKNOWN] = "unknown",
+	[OXP_EFFECT_AURORA] = "aurora",
+	[OXP_EFFECT_BIRTHDAY] = "birthday_cake",
+	[OXP_EFFECT_FLOWING] = "flowing_light",
+	[OXP_EFFECT_CHROMA_1] = "chroma_popping",
+	[OXP_EFFECT_NEON] = "neon",
+	[OXP_EFFECT_CHROMA_2] = "chroma_breathing",
+	[OXP_EFFECT_DREAMY] = "dreamy",
+	[OXP_EFFECT_WARM] = "warm_sun",
+	[OXP_EFFECT_CYBERPUNK] = "cyberpunk",
+	[OXP_EFFECT_SEA] = "sea_foam",
+	[OXP_EFFECT_SUNSET] = "sunset_afterglow",
+	[OXP_EFFECT_COLORFUL] = "colorful",
+	[OXP_EFFECT_MONSTER] = "monster_woke",
+	[OXP_EFFECT_GREEN] = "green_breathing",
+	[OXP_EFFECT_BLUE] = "blue_breathing",
+	[OXP_EFFECT_YELLOW] = "yellow_breathing",
+	[OXP_EFFECT_TEAL] = "teal_breathing",
+	[OXP_EFFECT_PURPLE] = "purple_breathing",
+	[OXP_EFFECT_FOGGY] = "foggy_haze",
+	[OXP_EFFECT_MONO_LIST] = "monocolor",
+};
+
+struct oxp_gen_1_rgb_report {
+	u8 report_id;
+	u8 message_id;
+	u8 padding_2[2];
+	u8 effect;
+	u8 enabled;
+	u8 speed;
+	u8 brightness;
+	u8 red;
+	u8 green;
+	u8 blue;
+} __packed;
+
+static u16 get_usage_page(struct hid_device *hdev)
+{
+	return hdev->collection[0].usage >> 16;
+}
+
+static int oxp_hid_raw_event_gen_1(struct hid_device *hdev,
+				   struct hid_report *report, u8 *data,
+				   int size)
+{
+	struct led_classdev_mc *led_mc = drvdata.led_mc;
+	struct oxp_gen_1_rgb_report *rgb_rep;
+
+	if (data[1] != OXP_FID_GEN1_RGB_REPLY)
+		return 0;
+
+	rgb_rep = (struct oxp_gen_1_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)
+{
+	u16 up = get_usage_page(hdev);
+
+	dev_dbg(&hdev->dev, "raw event data: [%*ph]\n", OXP_PACKET_SIZE, data);
+
+	switch (up) {
+	case GEN1_USAGE_PAGE:
+		return oxp_hid_raw_event_gen_1(hdev, report, data, size);
+	default:
+		break;
+	}
+
+	return 0;
+}
+
+static int mcu_property_out(u8 *header, size_t header_size, u8 *data,
+			    size_t data_size, u8 *footer, size_t footer_size)
+{
+	unsigned char *dmabuf __free(kfree) = kzalloc(OXP_PACKET_SIZE, GFP_KERNEL);
+	int ret;
+
+	if (!dmabuf)
+		return -ENOMEM;
+
+	if (header_size + data_size + footer_size > OXP_PACKET_SIZE)
+		return -EINVAL;
+
+	guard(mutex)(&drvdata.cfg_mutex);
+	memcpy(dmabuf, header, header_size);
+	memcpy(dmabuf + header_size, data, data_size);
+	if (footer_size)
+		memcpy(dmabuf + OXP_PACKET_SIZE - footer_size, footer, footer_size);
+
+	dev_dbg(&drvdata.hdev->dev, "raw data: [%*ph]\n", OXP_PACKET_SIZE, dmabuf);
+
+	ret = hid_hw_output_report(drvdata.hdev, dmabuf, OXP_PACKET_SIZE);
+	if (ret < 0)
+		return ret;
+
+	/* MCU takes 200ms to be ready for another command. */
+	msleep(200);
+	return ret == OXP_PACKET_SIZE ? 0 : -EIO;
+}
+
+static int oxp_gen_1_property_out(enum oxp_function_index fid, u8 *data,
+				  u8 data_size)
+{
+	u8 header[] = { fid, GEN1_MESSAGE_ID };
+	size_t header_size = ARRAY_SIZE(header);
+
+	return mcu_property_out(header, header_size, data, data_size, NULL, 0);
+}
+
+static int oxp_rgb_status_store(u8 enabled, u8 speed, u8 brightness)
+{
+	u16 up = get_usage_page(drvdata.hdev);
+	u8 *data;
+
+	/* Always default to max brightness and use intensity scaling when in
+	 * monocolor mode.
+	 */
+	switch (up) {
+	case GEN1_USAGE_PAGE:
+		data = (u8[4]) { OXP_SET_PROPERTY, enabled, speed, 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);
+	default:
+		return -ENODEV;
+	}
+}
+
+static ssize_t oxp_rgb_status_show(void)
+{
+	u16 up = get_usage_page(drvdata.hdev);
+	u8 *data;
+
+	switch (up) {
+	case GEN1_USAGE_PAGE:
+		data = (u8[1]) { OXP_GET_PROPERTY };
+		return oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, 1);
+	default:
+		return -ENODEV;
+	}
+}
+
+static int oxp_rgb_color_set(void)
+{
+	u8 max_br = drvdata.led_mc->led_cdev.max_brightness;
+	u8 br = drvdata.led_mc->led_cdev.brightness;
+	u16 up = get_usage_page(drvdata.hdev);
+	u8 green, red, blue;
+	size_t size;
+	u8 *data;
+	int i;
+
+	red = br * drvdata.led_mc->subled_info[0].intensity / max_br;
+	green = br * drvdata.led_mc->subled_info[1].intensity / max_br;
+	blue = br * drvdata.led_mc->subled_info[2].intensity / max_br;
+
+	switch (up) {
+	case GEN1_USAGE_PAGE:
+		size = 55;
+		data = (u8[55]) { OXP_EFFECT_MONO_TRUE };
+
+		for (i = 0; i < (size - 1) / 3; i++) {
+			data[3 * i + 1] = red;
+			data[3 * i + 2] = green;
+			data[3 * i + 3] = blue;
+		}
+		return oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, size);
+	default:
+		return -ENODEV;
+	}
+}
+
+static int oxp_rgb_effect_set(u8 effect)
+{
+	u16 up = get_usage_page(drvdata.hdev);
+	u8 *data;
+	int ret;
+
+	switch (effect) {
+	case OXP_EFFECT_AURORA:
+	case OXP_EFFECT_BIRTHDAY:
+	case OXP_EFFECT_FLOWING:
+	case OXP_EFFECT_CHROMA_1:
+	case OXP_EFFECT_NEON:
+	case OXP_EFFECT_CHROMA_2:
+	case OXP_EFFECT_DREAMY:
+	case OXP_EFFECT_WARM:
+	case OXP_EFFECT_CYBERPUNK:
+	case OXP_EFFECT_SEA:
+	case OXP_EFFECT_SUNSET:
+	case OXP_EFFECT_COLORFUL:
+	case OXP_EFFECT_MONSTER:
+	case OXP_EFFECT_GREEN:
+	case OXP_EFFECT_BLUE:
+	case OXP_EFFECT_YELLOW:
+	case OXP_EFFECT_TEAL:
+	case OXP_EFFECT_PURPLE:
+	case OXP_EFFECT_FOGGY:
+		switch (up) {
+		case GEN1_USAGE_PAGE:
+			data = (u8[1]) { effect };
+			ret = oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, 1);
+			break;
+		default:
+			ret = -ENODEV;
+		}
+		break;
+	case OXP_EFFECT_MONO_LIST:
+		ret = oxp_rgb_color_set();
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	if (ret)
+		return ret;
+
+	drvdata.rgb_effect = effect;
+
+	return 0;
+}
+
+static ssize_t enabled_store(struct device *dev, struct device_attribute *attr,
+			     const char *buf, size_t count)
+{
+	int ret;
+	u8 val;
+
+	ret = sysfs_match_string(oxp_feature_en_text, buf);
+	if (ret < 0)
+		return ret;
+	val = ret;
+
+	ret = oxp_rgb_status_store(val, drvdata.rgb_speed,
+				   drvdata.rgb_brightness);
+	if (ret)
+		return ret;
+
+	drvdata.rgb_en = val;
+	return count;
+}
+
+static ssize_t enabled_show(struct device *dev, struct device_attribute *attr,
+			    char *buf)
+{
+	int ret;
+
+	ret = oxp_rgb_status_show();
+	if (ret)
+		return ret;
+
+	if (drvdata.rgb_en >= ARRAY_SIZE(oxp_feature_en_text))
+		return -EINVAL;
+
+	return sysfs_emit(buf, "%s\n", oxp_feature_en_text[drvdata.rgb_en]);
+}
+static DEVICE_ATTR_RW(enabled);
+
+static ssize_t enabled_index_show(struct device *dev,
+				  struct device_attribute *attr, char *buf)
+{
+	size_t count = 0;
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(oxp_feature_en_text); i++)
+		count += sysfs_emit_at(buf, count, "%s ", oxp_feature_en_text[i]);
+
+	if (count)
+		buf[count - 1] = '\n';
+
+	return count;
+}
+static DEVICE_ATTR_RO(enabled_index);
+
+static ssize_t effect_store(struct device *dev, struct device_attribute *attr,
+			    const char *buf, size_t count)
+{
+	int ret;
+	u8 val;
+
+	ret = sysfs_match_string(oxp_rgb_effect_text, buf);
+	if (ret < 0)
+		return ret;
+
+	val = ret;
+
+	ret = oxp_rgb_status_store(drvdata.rgb_en, drvdata.rgb_speed,
+				   drvdata.rgb_brightness);
+	if (ret)
+		return ret;
+
+	ret = oxp_rgb_effect_set(val);
+	if (ret)
+		return ret;
+
+	return count;
+}
+
+static ssize_t effect_show(struct device *dev, struct device_attribute *attr,
+			   char *buf)
+{
+	int ret;
+
+	ret = oxp_rgb_status_show();
+	if (ret)
+		return ret;
+
+	if (drvdata.rgb_effect >= ARRAY_SIZE(oxp_rgb_effect_text))
+		return -EINVAL;
+
+	return sysfs_emit(buf, "%s\n", oxp_rgb_effect_text[drvdata.rgb_effect]);
+}
+
+static DEVICE_ATTR_RW(effect);
+
+static ssize_t effect_index_show(struct device *dev,
+				 struct device_attribute *attr, char *buf)
+{
+	size_t count = 0;
+	unsigned int i;
+
+	for (i = 1; i < ARRAY_SIZE(oxp_rgb_effect_text); i++)
+		count += sysfs_emit_at(buf, count, "%s ", oxp_rgb_effect_text[i]);
+
+	if (count)
+		buf[count - 1] = '\n';
+
+	return count;
+}
+static DEVICE_ATTR_RO(effect_index);
+
+static ssize_t speed_store(struct device *dev, struct device_attribute *attr,
+			   const char *buf, size_t count)
+{
+	int ret;
+	u8 val;
+
+	ret = kstrtou8(buf, 10, &val);
+	if (ret)
+		return ret;
+
+	if (val > 9)
+		return -EINVAL;
+
+	ret = oxp_rgb_status_store(drvdata.rgb_en, val, drvdata.rgb_brightness);
+	if (ret)
+		return ret;
+
+	drvdata.rgb_speed = val;
+	return count;
+}
+
+static ssize_t speed_show(struct device *dev, struct device_attribute *attr,
+			  char *buf)
+{
+	int ret;
+
+	ret = oxp_rgb_status_show();
+	if (ret)
+		return ret;
+
+	if (drvdata.rgb_speed > 9)
+		return -EINVAL;
+
+	return sysfs_emit(buf, "%hhu\n", drvdata.rgb_speed);
+}
+static DEVICE_ATTR_RW(speed);
+
+static ssize_t speed_range_show(struct device *dev,
+				struct device_attribute *attr, char *buf)
+{
+	return sysfs_emit(buf, "0-9\n");
+}
+static DEVICE_ATTR_RO(speed_range);
+
+static void oxp_rgb_queue_fn(struct work_struct *work)
+{
+	unsigned int max_brightness = drvdata.led_mc->led_cdev.max_brightness;
+	unsigned int brightness = drvdata.led_mc->led_cdev.brightness;
+	u8 val = 4 * brightness / max_brightness;
+	int ret;
+
+	if (drvdata.rgb_brightness != val) {
+		ret = oxp_rgb_status_store(drvdata.rgb_en, drvdata.rgb_speed, val);
+		if (ret)
+			dev_err(drvdata.led_mc->led_cdev.dev,
+				"Error: Failed to write RGB Status: %i\n", ret);
+
+		drvdata.rgb_brightness = val;
+	}
+
+	if (drvdata.rgb_effect != OXP_EFFECT_MONO_LIST)
+		return;
+
+	ret = oxp_rgb_effect_set(drvdata.rgb_effect);
+	if (ret)
+		dev_err(drvdata.led_mc->led_cdev.dev, "Error: Failed to write RGB color: %i\n",
+			ret);
+}
+
+static DECLARE_DELAYED_WORK(oxp_rgb_queue, oxp_rgb_queue_fn);
+
+static void oxp_rgb_brightness_set(struct led_classdev *led_cdev,
+				   enum led_brightness brightness)
+{
+	led_cdev->brightness = brightness;
+	mod_delayed_work(system_wq, &oxp_rgb_queue, msecs_to_jiffies(50));
+}
+
+static struct attribute *oxp_rgb_attrs[] = {
+	&dev_attr_effect.attr,
+	&dev_attr_effect_index.attr,
+	&dev_attr_enabled.attr,
+	&dev_attr_enabled_index.attr,
+	&dev_attr_speed.attr,
+	&dev_attr_speed_range.attr,
+	NULL,
+};
+
+static const struct attribute_group oxp_rgb_attr_group = {
+	.attrs = oxp_rgb_attrs,
+};
+
+static struct mc_subled oxp_rgb_subled_info[] = {
+	{
+		.color_index = LED_COLOR_ID_RED,
+		.intensity = 0x24,
+		.channel = 0x1,
+	},
+	{
+		.color_index = LED_COLOR_ID_GREEN,
+		.intensity = 0x22,
+		.channel = 0x2,
+	},
+	{
+		.color_index = LED_COLOR_ID_BLUE,
+		.intensity = 0x99,
+		.channel = 0x3,
+	},
+};
+
+static struct led_classdev_mc oxp_cdev_rgb = {
+	.led_cdev = {
+		.name = "oxp:rgb:joystick_rings",
+		.color = LED_COLOR_ID_RGB,
+		.brightness = 0x64,
+		.max_brightness = 0x64,
+		.brightness_set = oxp_rgb_brightness_set,
+	},
+	.num_colors = ARRAY_SIZE(oxp_rgb_subled_info),
+	.subled_info = oxp_rgb_subled_info,
+};
+
+static int oxp_cfg_probe(struct hid_device *hdev, u16 up)
+{
+	int ret;
+
+	hid_set_drvdata(hdev, &drvdata);
+	mutex_init(&drvdata.cfg_mutex);
+	drvdata.hdev = hdev;
+	drvdata.led_mc = &oxp_cdev_rgb;
+
+	ret = devm_led_classdev_multicolor_register(&hdev->dev, &oxp_cdev_rgb);
+	if (ret)
+		return dev_err_probe(&hdev->dev, ret,
+				     "Failed to create RGB device\n");
+
+	ret = devm_device_add_group(drvdata.led_mc->led_cdev.dev,
+				    &oxp_rgb_attr_group);
+	if (ret)
+		return dev_err_probe(drvdata.led_mc->led_cdev.dev, ret,
+				     "Failed to create RGB configuration attributes\n");
+
+	ret = oxp_rgb_status_show();
+	if (ret)
+		dev_warn(drvdata.led_mc->led_cdev.dev,
+			 "Failed to query RGB initial state: %i\n", ret);
+
+	return 0;
+}
+
+static int oxp_hid_probe(struct hid_device *hdev,
+			 const struct hid_device_id *id)
+{
+	int ret;
+	u16 up;
+
+	ret = hid_parse(hdev);
+	if (ret)
+		return dev_err_probe(&hdev->dev, ret, "Failed to parse HID device\n");
+
+	ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+	if (ret)
+		return dev_err_probe(&hdev->dev, ret, "Failed to start HID device\n");
+
+	ret = hid_hw_open(hdev);
+	if (ret) {
+		hid_hw_stop(hdev);
+		return dev_err_probe(&hdev->dev, ret, "Failed to open HID device\n");
+	}
+
+	up = get_usage_page(hdev);
+	dev_dbg(&hdev->dev, "Got usage page %04x\n", up);
+
+	switch (up) {
+	case GEN1_USAGE_PAGE:
+		ret = oxp_cfg_probe(hdev, up);
+		if (ret) {
+			hid_hw_close(hdev);
+			hid_hw_stop(hdev);
+		}
+
+		return ret;
+	default:
+		return 0;
+	}
+}
+
+static void oxp_hid_remove(struct hid_device *hdev)
+{
+	hid_hw_close(hdev);
+	hid_hw_stop(hdev);
+}
+
+static const struct hid_device_id oxp_devices[] = {
+	{ HID_USB_DEVICE(USB_VENDOR_ID_CRSC, USB_DEVICE_ID_ONEXPLAYER_GEN1) },
+	{}
+};
+
+MODULE_DEVICE_TABLE(hid, oxp_devices);
+static struct hid_driver hid_oxp = {
+	.name = "hid-oxp",
+	.id_table = oxp_devices,
+	.probe = oxp_hid_probe,
+	.remove = oxp_hid_remove,
+	.raw_event = oxp_hid_raw_event,
+};
+module_hid_driver(hid_oxp);
+
+MODULE_AUTHOR("Derek J. Clark <derekjohn.clark@gmail.com>");
+MODULE_DESCRIPTION("Driver for OneXPlayer HID Interfaces");
+MODULE_LICENSE("GPL");
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 2/5] HID: hid-oxp: Add Second Generation RGB Control
From: Derek J. Clark @ 2026-04-12 21:34 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Pierre-Loup A . Griffais, Lambert Fan, Zhouwang Huang,
	Derek J . Clark, linux-input, linux-doc, linux-kernel
In-Reply-To: <20260412213444.2231505-1-derekjohn.clark@gmail.com>

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>
---
v2:
  - Add DMI quirks table.
---
 drivers/hid/Kconfig   |   1 +
 drivers/hid/hid-ids.h |   3 +
 drivers/hid/hid-oxp.c | 151 ++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 155 insertions(+)

diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 2deaec9f467d..b779088b80b6 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -924,6 +924,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.
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index dcc5a3a70eaf..0d1ff879e959 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1134,6 +1134,9 @@
 #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
 
diff --git a/drivers/hid/hid-oxp.c b/drivers/hid/hid-oxp.c
index c4219ecd8d71..25214356163e 100644
--- a/drivers/hid/hid-oxp.c
+++ b/drivers/hid/hid-oxp.c
@@ -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>
@@ -24,12 +25,15 @@
 #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 {
@@ -121,6 +125,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;
@@ -161,6 +181,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)
 {
@@ -171,6 +229,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;
 	}
@@ -216,6 +276,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);
@@ -230,6 +302,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;
 	}
@@ -244,6 +321,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;
 	}
@@ -274,6 +354,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;
 	}
@@ -310,6 +400,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;
 		}
@@ -560,6 +654,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;
@@ -567,6 +711,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;
 
 	ret = devm_led_classdev_multicolor_register(&hdev->dev, &oxp_cdev_rgb);
@@ -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);
@@ -633,6 +783,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) },
 	{}
 };
 
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 3/5] HID: hid-oxp: Add Second Generation Gamepad Mode Switch
From: Derek J. Clark @ 2026-04-12 21:34 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Pierre-Loup A . Griffais, Lambert Fan, Zhouwang Huang,
	Derek J . Clark, linux-input, linux-doc, linux-kernel
In-Reply-To: <20260412213444.2231505-1-derekjohn.clark@gmail.com>

Adds "gamepad_mode" attribute to second generation OneXPlayer
configuration HID devices. This attribute initiates a mode shift in the
device MCU that puts it into a state where all events are routed to an
hidraw interface instead of the xpad evdev interface. This allows for
debugging the hardware input mapping added in the next patch.

Reviewed-by: Zhouwang Huang <honjow311@gmail.com>
Tested-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v2:
  - Rename to gamepad_mode & show relevant gamepad modes instead of
    using a debug enable/disable paradigm, to match other drivers.
---
 drivers/hid/hid-oxp.c | 130 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 130 insertions(+)

diff --git a/drivers/hid/hid-oxp.c b/drivers/hid/hid-oxp.c
index 25214356163e..c62952537d98 100644
--- a/drivers/hid/hid-oxp.c
+++ b/drivers/hid/hid-oxp.c
@@ -33,6 +33,7 @@
 enum oxp_function_index {
 	OXP_FID_GEN1_RGB_SET =		0x07,
 	OXP_FID_GEN1_RGB_REPLY =	0x0f,
+	OXP_FID_GEN2_TOGGLE_MODE =	0xb2,
 	OXP_FID_GEN2_STATUS_EVENT =	0xb8,
 };
 
@@ -41,11 +42,22 @@ static struct oxp_hid_cfg {
 	struct hid_device *hdev;
 	struct mutex cfg_mutex; /*ensure single synchronous output report*/
 	u8 rgb_brightness;
+	u8 gamepad_mode;
 	u8 rgb_effect;
 	u8 rgb_speed;
 	u8 rgb_en;
 } drvdata;
 
+enum oxp_gamepad_mode_index {
+	OXP_GP_MODE_XINPUT = 0x00,
+	OXP_GP_MODE_DEBUG = 0x03,
+};
+
+static const char *const oxp_gamepad_mode_text[] = {
+	[OXP_GP_MODE_XINPUT] = "xinput",
+	[OXP_GP_MODE_DEBUG] = "debug",
+};
+
 enum oxp_feature_en_index {
 	OXP_FEAT_DISABLED,
 	OXP_FEAT_ENABLED,
@@ -181,6 +193,32 @@ static int oxp_hid_raw_event_gen_1(struct hid_device *hdev,
 	return 0;
 }
 
+static int oxp_gen_2_property_out(enum oxp_function_index fid, u8 *data, u8 data_size);
+
+static void oxp_mcu_init_fn(struct work_struct *work)
+{
+	u8 gp_mode_data[3] = { OXP_GP_MODE_DEBUG, 0x01, 0x02 };
+	int ret;
+
+	/* Cycle the gamepad mode */
+	ret = oxp_gen_2_property_out(OXP_FID_GEN2_TOGGLE_MODE, gp_mode_data, 3);
+	if (ret)
+		dev_err(&drvdata.hdev->dev,
+			"Error: Failed to set gamepad mode: %i\n", ret);
+
+	/* Remainder only applies for xinput mode */
+	if (drvdata.gamepad_mode == OXP_GP_MODE_DEBUG)
+		return;
+
+	gp_mode_data[0] = OXP_GP_MODE_XINPUT;
+	ret = oxp_gen_2_property_out(OXP_FID_GEN2_TOGGLE_MODE, gp_mode_data, 3);
+	if (ret)
+		dev_err(&drvdata.hdev->dev,
+			"Error: Failed to set gamepad mode: %i\n", ret);
+}
+
+static DECLARE_DELAYED_WORK(oxp_mcu_init, oxp_mcu_init_fn);
+
 static int oxp_hid_raw_event_gen_2(struct hid_device *hdev,
 				   struct hid_report *report, u8 *data,
 				   int size)
@@ -191,6 +229,14 @@ static int oxp_hid_raw_event_gen_2(struct hid_device *hdev,
 	if (data[0] != OXP_FID_GEN2_STATUS_EVENT)
 		return 0;
 
+	/* Sent ~6s after resume event, indicating the MCU has fully reset.
+	 * Re-apply our settings after this has been received.
+	 */
+	if (data[3] == OXP_EFFECT_MONO_TRUE) {
+		mod_delayed_work(system_wq, &oxp_mcu_init, msecs_to_jiffies(50));
+		return 0;
+	}
+
 	if (data[3] != OXP_GET_PROPERTY)
 		return 0;
 
@@ -288,6 +334,77 @@ static int oxp_gen_2_property_out(enum oxp_function_index fid, u8 *data,
 				footer_size);
 }
 
+static ssize_t gamepad_mode_store(struct device *dev,
+				  struct device_attribute *attr, const char *buf,
+				  size_t count)
+{
+	u16 up = get_usage_page(drvdata.hdev);
+	u8 data[3] = { 0x00, 0x01, 0x02 };
+	int ret = -EINVAL;
+	int i;
+
+	if (up != GEN2_USAGE_PAGE)
+		return ret;
+
+	for (i = 0; i < ARRAY_SIZE(oxp_gamepad_mode_text); i++) {
+		if (oxp_gamepad_mode_text[i] && sysfs_streq(buf, oxp_gamepad_mode_text[i])) {
+			ret = i;
+			break;
+		}
+	}
+	if (ret < 0)
+		return ret;
+
+	data[0] = ret;
+
+	ret = oxp_gen_2_property_out(OXP_FID_GEN2_TOGGLE_MODE, data, 3);
+	if (ret)
+		return ret;
+
+	drvdata.gamepad_mode = data[0];
+
+	return count;
+}
+
+static ssize_t gamepad_mode_show(struct device *dev,
+				 struct device_attribute *attr, char *buf)
+{
+	return sysfs_emit(buf, "%s\n", oxp_gamepad_mode_text[drvdata.gamepad_mode]);
+}
+static DEVICE_ATTR_RW(gamepad_mode);
+
+static ssize_t gamepad_mode_index_show(struct device *dev,
+				       struct device_attribute *attr,
+				       char *buf)
+{
+	ssize_t count = 0;
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(oxp_gamepad_mode_text); i++) {
+		if (!oxp_gamepad_mode_text[i] ||
+		    oxp_gamepad_mode_text[i][0] == '\0')
+			continue;
+
+		count += sysfs_emit_at(buf, count, "%s ", oxp_gamepad_mode_text[i]);
+	}
+
+	if (count)
+		buf[count - 1] = '\n';
+
+	return count;
+}
+static DEVICE_ATTR_RO(gamepad_mode_index);
+
+static struct attribute *oxp_cfg_attrs[] = {
+	&dev_attr_gamepad_mode.attr,
+	&dev_attr_gamepad_mode_index.attr,
+	NULL,
+};
+
+static const struct attribute_group oxp_cfg_attrs_group = {
+	.attrs = oxp_cfg_attrs,
+};
+
 static int oxp_rgb_status_store(u8 enabled, u8 speed, u8 brightness)
 {
 	u16 up = get_usage_page(drvdata.hdev);
@@ -733,7 +850,20 @@ 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);
 
+	/* Below features are only implemented in gen 2 */
+	if (up != GEN2_USAGE_PAGE)
+		return 0;
+
 skip_rgb:
+	mod_delayed_work(system_wq, &oxp_mcu_init, msecs_to_jiffies(50));
+
+	drvdata.gamepad_mode = OXP_GP_MODE_XINPUT;
+
+	ret = devm_device_add_group(&hdev->dev, &oxp_cfg_attrs_group);
+	if (ret)
+		return dev_err_probe(&hdev->dev, ret,
+				     "Failed to attach configuration attributes\n");
+
 	return 0;
 }
 
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 5/5] HID: hid-oxp: Add Vibration Intensity Attribute
From: Derek J. Clark @ 2026-04-12 21:34 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Pierre-Loup A . Griffais, Lambert Fan, Zhouwang Huang,
	Derek J . Clark, linux-input, linux-doc, linux-kernel
In-Reply-To: <20260412213444.2231505-1-derekjohn.clark@gmail.com>

Adds attribute for setting the rumble intensity level. This setting must
be re-applied after the gamepad mode is set as doing so resets this to
the default value.

Reviewed-by: Zhouwang Huang <honjow311@gmail.com>
Tested-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
 drivers/hid/hid-oxp.c | 78 +++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 78 insertions(+)

diff --git a/drivers/hid/hid-oxp.c b/drivers/hid/hid-oxp.c
index 959ec1a90d22..a4e9d41bd3a7 100644
--- a/drivers/hid/hid-oxp.c
+++ b/drivers/hid/hid-oxp.c
@@ -34,6 +34,7 @@ enum oxp_function_index {
 	OXP_FID_GEN1_RGB_SET =		0x07,
 	OXP_FID_GEN1_RGB_REPLY =	0x0f,
 	OXP_FID_GEN2_TOGGLE_MODE =	0xb2,
+	OXP_FID_GEN2_RUMBLE_SET =	0xb3,
 	OXP_FID_GEN2_KEY_STATE =	0xb4,
 	OXP_FID_GEN2_STATUS_EVENT =	0xb8,
 };
@@ -178,6 +179,7 @@ static struct oxp_hid_cfg {
 	struct mutex cfg_mutex; /*ensure single synchronous output report*/
 	u8 rgb_brightness;
 	u8 gamepad_mode;
+	u8 rumble_intensity;
 	u8 rgb_effect;
 	u8 rgb_speed;
 	u8 rgb_en;
@@ -263,6 +265,11 @@ static const char *const oxp_rgb_effect_text[] = {
 	[OXP_EFFECT_MONO_LIST] = "monocolor",
 };
 
+enum oxp_rumble_side_index {
+	OXP_RUMBLE_LEFT = 0x00,
+	OXP_RUMBLE_RIGHT,
+};
+
 struct oxp_gen_1_rgb_report {
 	u8 report_id;
 	u8 message_id;
@@ -338,6 +345,7 @@ static int oxp_hid_raw_event_gen_1(struct hid_device *hdev,
 
 static int oxp_gen_2_property_out(enum oxp_function_index fid, u8 *data, u8 data_size);
 static int oxp_set_buttons(void);
+static int oxp_rumble_intensity_set(u8 intensity);
 
 static void oxp_mcu_init_fn(struct work_struct *work)
 {
@@ -365,6 +373,12 @@ static void oxp_mcu_init_fn(struct work_struct *work)
 	if (ret)
 		dev_err(&drvdata.hdev->dev,
 			"Error: Failed to set gamepad mode: %i\n", ret);
+
+	/* Set vibration level */
+	ret = oxp_rumble_intensity_set(drvdata.rumble_intensity);
+	if (ret)
+		dev_err(&drvdata.hdev->dev,
+			"Error: Failed to set rumble intensity: %i\n", ret);
 }
 
 static DECLARE_DELAYED_WORK(oxp_mcu_init, oxp_mcu_init_fn);
@@ -513,6 +527,14 @@ static ssize_t gamepad_mode_store(struct device *dev,
 
 	drvdata.gamepad_mode = data[0];
 
+	if (drvdata.gamepad_mode == OXP_GP_MODE_DEBUG)
+		return count;
+
+	/* Re-apply rumble settings as switching gamepad mode will override */
+	ret = oxp_rumble_intensity_set(drvdata.rumble_intensity);
+	if (ret)
+		return ret;
+
 	return count;
 }
 
@@ -858,6 +880,59 @@ static ssize_t button_mapping_options_show(struct device *dev,
 }
 static DEVICE_ATTR_RO(button_mapping_options);
 
+static int oxp_rumble_intensity_set(u8 intensity)
+{
+	u8 header[15] = { 0x02, 0x38, 0x02, 0xe3, 0x39, 0xe3, 0x39, 0xe3,
+			  0x39, 0x01, intensity, 0x05, 0xe3, 0x39, 0xe3 };
+	u8 footer[9] = { 0x39, 0xe3, 0x39, 0xe3, 0xe3, 0x02, 0x04, 0x39, 0x39 };
+	size_t footer_size = ARRAY_SIZE(footer);
+	size_t header_size = ARRAY_SIZE(header);
+	u8 data[59] = { 0x0 };
+	size_t data_size = ARRAY_SIZE(data);
+
+	memcpy(data, header, header_size);
+	memcpy(data + data_size - footer_size, footer, footer_size);
+
+	return oxp_gen_2_property_out(OXP_FID_GEN2_RUMBLE_SET, data, data_size);
+}
+
+static ssize_t rumble_intensity_store(struct device *dev,
+				      struct device_attribute *attr, const char *buf,
+				      size_t count)
+{
+	int ret;
+	u8 val;
+
+	ret = kstrtou8(buf, 10, &val);
+	if (ret)
+		return ret;
+
+	if (val < 0 || val > 5)
+		return -EINVAL;
+
+	ret = oxp_rumble_intensity_set(val);
+	if (ret)
+		return ret;
+
+	drvdata.rumble_intensity = val;
+
+	return count;
+}
+
+static ssize_t rumble_intensity_show(struct device *dev,
+				     struct device_attribute *attr, char *buf)
+{
+	return sysfs_emit(buf, "%i\n", drvdata.rumble_intensity);
+}
+static DEVICE_ATTR_RW(rumble_intensity);
+
+static ssize_t rumble_intensity_range_show(struct device *dev,
+					   struct device_attribute *attr, char *buf)
+{
+	return sysfs_emit(buf, "0-5\n");
+}
+static DEVICE_ATTR_RO(rumble_intensity_range);
+
 #define OXP_DEVICE_ATTR_RW(_name, _group)                                     \
 	static ssize_t _name##_store(struct device *dev,                      \
 				     struct device_attribute *attr,           \
@@ -949,6 +1024,8 @@ static struct attribute *oxp_cfg_attrs[] = {
 	&dev_attr_gamepad_mode.attr,
 	&dev_attr_gamepad_mode_index.attr,
 	&dev_attr_reset_buttons.attr,
+	&dev_attr_rumble_intensity.attr,
+	&dev_attr_rumble_intensity_range.attr,
 	NULL,
 };
 
@@ -1422,6 +1499,7 @@ static int oxp_cfg_probe(struct hid_device *hdev, u16 up)
 	drvdata.bmap_2 = bmap_2;
 	oxp_reset_buttons();
 	drvdata.gamepad_mode = OXP_GP_MODE_XINPUT;
+	drvdata.rumble_intensity = 5;
 	mod_delayed_work(system_wq, &oxp_mcu_init, msecs_to_jiffies(50));
 
 	ret = devm_device_add_group(&hdev->dev, &oxp_cfg_attrs_group);
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 4/5] HID: hid-oxp: Add Button Mapping Interface
From: Derek J. Clark @ 2026-04-12 21:34 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Pierre-Loup A . Griffais, Lambert Fan, Zhouwang Huang,
	Derek J . Clark, linux-input, linux-doc, linux-kernel
In-Reply-To: <20260412213444.2231505-1-derekjohn.clark@gmail.com>

Adds button mapping interface for second generation OneXPlayer
configuration HID interfaces. This interface allows the MCU to swap
button mappings at the hardware level. The current state cannot be
retrieved, and the mappings may have been modified in Windows prior, so
we reset the button mapping at init and expose an attribute to allow
userspace to do this again at any time.

The interface requires two pages of button mapping data to be sent
before the settings will take place. Since the MCU requires a 200ms
delay after each message (total 400ms for these attributes) use the same
debounce work queue method we used for RGB. This will allow for
userspace or udev rules to rapidly map all buttons. The values will
be cached before the final write is finally sent to the device.

Reviewed-by: Zhouwang Huang <honjow311@gmail.com>
Tested-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v3:
  - Ensure default button map is properly init during probe.
v2:
  - Add detection of post-suspend MCU init to trigger setting the button
    map again.
---
 drivers/hid/hid-oxp.c | 569 +++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 567 insertions(+), 2 deletions(-)

diff --git a/drivers/hid/hid-oxp.c b/drivers/hid/hid-oxp.c
index c62952537d98..959ec1a90d22 100644
--- a/drivers/hid/hid-oxp.c
+++ b/drivers/hid/hid-oxp.c
@@ -34,10 +34,145 @@ enum oxp_function_index {
 	OXP_FID_GEN1_RGB_SET =		0x07,
 	OXP_FID_GEN1_RGB_REPLY =	0x0f,
 	OXP_FID_GEN2_TOGGLE_MODE =	0xb2,
+	OXP_FID_GEN2_KEY_STATE =	0xb4,
 	OXP_FID_GEN2_STATUS_EVENT =	0xb8,
 };
 
+#define OXP_MAPPING_GAMEPAD	0x01
+#define OXP_MAPPING_KEYBOARD	0x02
+
+struct oxp_button_data {
+	u8 mode;
+	u8 index;
+	u8 key_id;
+	u8 padding[2];
+} __packed;
+
+struct oxp_button_entry {
+	struct oxp_button_data data;
+	const char *name;
+};
+
+static const struct oxp_button_entry oxp_button_table[] = {
+	/* Gamepad Buttons */
+	{ { OXP_MAPPING_GAMEPAD, 0x01 }, "BTN_A" },
+	{ { OXP_MAPPING_GAMEPAD, 0x02 }, "BTN_B" },
+	{ { OXP_MAPPING_GAMEPAD, 0x03 }, "BTN_X" },
+	{ { OXP_MAPPING_GAMEPAD, 0x04 }, "BTN_Y" },
+	{ { OXP_MAPPING_GAMEPAD, 0x05 }, "BTN_LB" },
+	{ { OXP_MAPPING_GAMEPAD, 0x06 }, "BTN_RB" },
+	{ { OXP_MAPPING_GAMEPAD, 0x07 }, "BTN_LT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x08 }, "BTN_RT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x09 }, "BTN_START" },
+	{ { OXP_MAPPING_GAMEPAD, 0x0a }, "BTN_SELECT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x0b }, "BTN_L3" },
+	{ { OXP_MAPPING_GAMEPAD, 0x0c }, "BTN_R3" },
+	{ { OXP_MAPPING_GAMEPAD, 0x0d }, "DPAD_UP" },
+	{ { OXP_MAPPING_GAMEPAD, 0x0e }, "DPAD_DOWN" },
+	{ { OXP_MAPPING_GAMEPAD, 0x0f }, "DPAD_LEFT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x10 }, "DPAD_RIGHT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x11 }, "JOY_L_UP" },
+	{ { OXP_MAPPING_GAMEPAD, 0x12 }, "JOY_L_UP_RIGHT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x13 }, "JOY_L_RIGHT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x14 }, "JOY_L_DOWN_RIGHT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x15 }, "JOY_L_DOWN" },
+	{ { OXP_MAPPING_GAMEPAD, 0x16 }, "JOY_L_DOWN_LEFT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x17 }, "JOY_L_LEFT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x18 }, "JOY_L_UP_LEFT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x19 }, "JOY_R_UP" },
+	{ { OXP_MAPPING_GAMEPAD, 0x1a }, "JOY_R_UP_RIGHT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x1b }, "JOY_R_RIGHT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x1c }, "JOY_R_DOWN_RIGHT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x1d }, "JOY_R_DOWN" },
+	{ { OXP_MAPPING_GAMEPAD, 0x1e }, "JOY_R_DOWN_LEFT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x1f }, "JOY_R_LEFT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x20 }, "JOY_R_UP_LEFT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x22 }, "BTN_GUIDE" },
+	/* Keyboard Keys */
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x5a }, "KEY_F1" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x5b }, "KEY_F2" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x5c }, "KEY_F3" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x5d }, "KEY_F4" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x5e }, "KEY_F5" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x5f }, "KEY_F6" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x60 }, "KEY_F7" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x61 }, "KEY_F8" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x62 }, "KEY_F9" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x63 }, "KEY_F10" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x64 }, "KEY_F11" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x65 }, "KEY_F12" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x66 }, "KEY_F13" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x67 }, "KEY_F14" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x68 }, "KEY_F15" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x69 }, "KEY_F16" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x6a }, "KEY_F17" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x6b }, "KEY_F18" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x6c }, "KEY_F19" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x6d }, "KEY_F20" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x6e }, "KEY_F21" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x6f }, "KEY_F22" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x70 }, "KEY_F23" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x71 }, "KEY_F24" },
+};
+
+enum oxp_joybutton_index {
+	BUTTON_A =	0x01,
+	BUTTON_B,
+	BUTTON_X,
+	BUTTON_Y,
+	BUTTON_LB,
+	BUTTON_RB,
+	BUTTON_LT,
+	BUTTON_RT,
+	BUTTON_START,
+	BUTTON_SELECT,
+	BUTTON_L3,
+	BUTTON_R3,
+	BUTTON_DUP,
+	BUTTON_DDOWN,
+	BUTTON_DLEFT,
+	BUTTON_DRIGHT,
+	BUTTON_M1 =	0x22,
+	BUTTON_M2,
+	/* These are unused currently, reserved for future devices */
+	BUTTON_M3,
+	BUTTON_M4,
+	BUTTON_M5,
+	BUTTON_M6,
+};
+
+struct oxp_button_idx {
+	enum oxp_joybutton_index button_idx;
+	u8 mapping_idx;
+} __packed;
+
+struct oxp_bmap_page_1 {
+	struct oxp_button_idx btn_a;
+	struct oxp_button_idx btn_b;
+	struct oxp_button_idx btn_x;
+	struct oxp_button_idx btn_y;
+	struct oxp_button_idx btn_lb;
+	struct oxp_button_idx btn_rb;
+	struct oxp_button_idx btn_lt;
+	struct oxp_button_idx btn_rt;
+	struct oxp_button_idx btn_start;
+} __packed;
+
+struct oxp_bmap_page_2 {
+	struct oxp_button_idx btn_select;
+	struct oxp_button_idx btn_l3;
+	struct oxp_button_idx btn_r3;
+	struct oxp_button_idx btn_dup;
+	struct oxp_button_idx btn_ddown;
+	struct oxp_button_idx btn_dleft;
+	struct oxp_button_idx btn_dright;
+	struct oxp_button_idx btn_m1;
+	struct oxp_button_idx btn_m2;
+} __packed;
+
 static struct oxp_hid_cfg {
+	struct oxp_bmap_page_1 *bmap_1;
+	struct oxp_bmap_page_2 *bmap_2;
 	struct led_classdev_mc *led_mc;
 	struct hid_device *hdev;
 	struct mutex cfg_mutex; /*ensure single synchronous output report*/
@@ -48,6 +183,10 @@ static struct oxp_hid_cfg {
 	u8 rgb_en;
 } drvdata;
 
+#define OXP_FILL_PAGE_SLOT(page, btn)            \
+	{ .button_idx = (page)->btn.button_idx,  \
+	  .mapping_idx = (page)->btn.mapping_idx }
+
 enum oxp_gamepad_mode_index {
 	OXP_GP_MODE_XINPUT = 0x00,
 	OXP_GP_MODE_DEBUG = 0x03,
@@ -153,6 +292,10 @@ struct oxp_gen_2_rgb_report {
 	u8 effect;
 } __packed;
 
+struct oxp_attr {
+	u8 index;
+};
+
 static u16 get_usage_page(struct hid_device *hdev)
 {
 	return hdev->collection[0].usage >> 16;
@@ -194,12 +337,19 @@ static int oxp_hid_raw_event_gen_1(struct hid_device *hdev,
 }
 
 static int oxp_gen_2_property_out(enum oxp_function_index fid, u8 *data, u8 data_size);
+static int oxp_set_buttons(void);
 
 static void oxp_mcu_init_fn(struct work_struct *work)
 {
 	u8 gp_mode_data[3] = { OXP_GP_MODE_DEBUG, 0x01, 0x02 };
 	int ret;
 
+	/* Re-apply the button mapping */
+	ret = oxp_set_buttons();
+	if (ret)
+		dev_err(&drvdata.hdev->dev,
+			"Error: Failed to set button mapping: %i\n", ret);
+
 	/* Cycle the gamepad mode */
 	ret = oxp_gen_2_property_out(OXP_FID_GEN2_TOGGLE_MODE, gp_mode_data, 3);
 	if (ret)
@@ -395,9 +545,410 @@ static ssize_t gamepad_mode_index_show(struct device *dev,
 }
 static DEVICE_ATTR_RO(gamepad_mode_index);
 
+static void oxp_set_defaults_bmap_1(struct oxp_bmap_page_1 *bmap)
+{
+	bmap->btn_a.button_idx = BUTTON_A;
+	bmap->btn_a.mapping_idx = 0;
+	bmap->btn_b.button_idx = BUTTON_B;
+	bmap->btn_b.mapping_idx = 1;
+	bmap->btn_x.button_idx = BUTTON_X;
+	bmap->btn_x.mapping_idx = 2;
+	bmap->btn_y.button_idx = BUTTON_Y;
+	bmap->btn_y.mapping_idx = 3;
+	bmap->btn_lb.button_idx = BUTTON_LB;
+	bmap->btn_lb.mapping_idx = 4;
+	bmap->btn_rb.button_idx = BUTTON_RB;
+	bmap->btn_rb.mapping_idx = 5;
+	bmap->btn_lt.button_idx = BUTTON_LT;
+	bmap->btn_lt.mapping_idx = 6;
+	bmap->btn_rt.button_idx = BUTTON_RT;
+	bmap->btn_rt.mapping_idx = 7;
+	bmap->btn_start.button_idx = BUTTON_START;
+	bmap->btn_start.mapping_idx = 8;
+}
+
+static void oxp_set_defaults_bmap_2(struct oxp_bmap_page_2 *bmap)
+{
+	bmap->btn_select.button_idx = BUTTON_SELECT;
+	bmap->btn_select.mapping_idx = 9;
+	bmap->btn_l3.button_idx = BUTTON_L3;
+	bmap->btn_l3.mapping_idx = 10;
+	bmap->btn_r3.button_idx = BUTTON_R3;
+	bmap->btn_r3.mapping_idx = 11;
+	bmap->btn_dup.button_idx = BUTTON_DUP;
+	bmap->btn_dup.mapping_idx = 12;
+	bmap->btn_ddown.button_idx = BUTTON_DDOWN;
+	bmap->btn_ddown.mapping_idx = 13;
+	bmap->btn_dleft.button_idx = BUTTON_DLEFT;
+	bmap->btn_dleft.mapping_idx = 14;
+	bmap->btn_dright.button_idx = BUTTON_DRIGHT;
+	bmap->btn_dright.mapping_idx = 15;
+	bmap->btn_m1.button_idx = BUTTON_M1;
+	bmap->btn_m1.mapping_idx = 48; /* KEY_F15 */
+	bmap->btn_m2.button_idx = BUTTON_M2;
+	bmap->btn_m2.mapping_idx = 49; /* KEY_F16 */
+}
+
+static void oxp_page_fill_data(char *buf, const struct oxp_button_idx *buttons,
+			       size_t len)
+{
+	size_t offset_increment = sizeof(u8) + sizeof(struct oxp_button_idx);
+	size_t offset = 5;
+	unsigned int i;
+
+	for (i = 0; i < len; i++, offset += offset_increment) {
+		buf[offset] = (u8)buttons[i].button_idx;
+		memcpy(buf + offset + 1,
+		       &oxp_button_table[buttons[i].mapping_idx].data,
+		       sizeof(struct oxp_button_data));
+	}
+}
+
+static int oxp_set_buttons(void)
+{
+	u8 page_1[59] = { 0x02, 0x38, 0x20, 0x01, 0x01 };
+	u8 page_2[59] = { 0x02, 0x38, 0x20, 0x02, 0x01 };
+	u16 up = get_usage_page(drvdata.hdev);
+	int ret;
+
+	if (up != GEN2_USAGE_PAGE)
+		return -EINVAL;
+
+	const struct oxp_button_idx p1[] = {
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_a),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_b),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_x),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_y),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_lb),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_rb),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_lt),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_rt),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_start),
+	};
+
+	const struct oxp_button_idx p2[] = {
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_select),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_l3),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_r3),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_dup),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_ddown),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_dleft),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_dright),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_m1),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_m2),
+	};
+
+	oxp_page_fill_data(page_1, p1, ARRAY_SIZE(p1));
+	oxp_page_fill_data(page_2, p2, ARRAY_SIZE(p2));
+
+	ret = oxp_gen_2_property_out(OXP_FID_GEN2_KEY_STATE, page_1, ARRAY_SIZE(page_1));
+	if (ret)
+		return ret;
+
+	return oxp_gen_2_property_out(OXP_FID_GEN2_KEY_STATE, page_2, ARRAY_SIZE(page_2));
+}
+
+static void oxp_reset_buttons(void)
+{
+	oxp_set_defaults_bmap_1(drvdata.bmap_1);
+	oxp_set_defaults_bmap_2(drvdata.bmap_2);
+}
+
+static ssize_t reset_buttons_store(struct device *dev,
+				   struct device_attribute *attr, const char *buf,
+				   size_t count)
+{
+	int val, ret;
+
+	ret = kstrtoint(buf, 10, &val);
+	if (ret)
+		return ret;
+
+	if (val != 1)
+		return -EINVAL;
+
+	oxp_reset_buttons();
+	ret = oxp_set_buttons();
+	if (ret)
+		return ret;
+
+	return count;
+}
+static DEVICE_ATTR_WO(reset_buttons);
+
+static void oxp_btn_queue_fn(struct work_struct *work)
+{
+	int ret;
+
+	ret = oxp_set_buttons();
+	if (ret)
+		dev_err(&drvdata.hdev->dev,
+			"Error: Failed to write button mapping: %i\n", ret);
+}
+
+static DECLARE_DELAYED_WORK(oxp_btn_queue, oxp_btn_queue_fn);
+
+static int oxp_button_idx_from_str(const char *buf)
+{
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(oxp_button_table); i++)
+		if (sysfs_streq(buf, oxp_button_table[i].name))
+			return i;
+
+	return -EINVAL;
+}
+
+static ssize_t map_button_store(struct device *dev,
+				struct device_attribute *attr, const char *buf,
+				size_t count, u8 index)
+{
+	int idx;
+
+	idx = oxp_button_idx_from_str(buf);
+	if (idx < 0)
+		return idx;
+
+	switch (index) {
+	case BUTTON_A:
+		drvdata.bmap_1->btn_a.mapping_idx = idx;
+		break;
+	case BUTTON_B:
+		drvdata.bmap_1->btn_b.mapping_idx = idx;
+		break;
+	case BUTTON_X:
+		drvdata.bmap_1->btn_x.mapping_idx = idx;
+		break;
+	case BUTTON_Y:
+		drvdata.bmap_1->btn_y.mapping_idx = idx;
+		break;
+	case BUTTON_LB:
+		drvdata.bmap_1->btn_lb.mapping_idx = idx;
+		break;
+	case BUTTON_RB:
+		drvdata.bmap_1->btn_rb.mapping_idx = idx;
+		break;
+	case BUTTON_LT:
+		drvdata.bmap_1->btn_lt.mapping_idx = idx;
+		break;
+	case BUTTON_RT:
+		drvdata.bmap_1->btn_rt.mapping_idx = idx;
+		break;
+	case BUTTON_START:
+		drvdata.bmap_1->btn_start.mapping_idx = idx;
+		break;
+	case BUTTON_SELECT:
+		drvdata.bmap_2->btn_select.mapping_idx = idx;
+		break;
+	case BUTTON_L3:
+		drvdata.bmap_2->btn_l3.mapping_idx = idx;
+		break;
+	case BUTTON_R3:
+		drvdata.bmap_2->btn_r3.mapping_idx = idx;
+		break;
+	case BUTTON_DUP:
+		drvdata.bmap_2->btn_dup.mapping_idx = idx;
+		break;
+	case BUTTON_DDOWN:
+		drvdata.bmap_2->btn_ddown.mapping_idx = idx;
+		break;
+	case BUTTON_DLEFT:
+		drvdata.bmap_2->btn_dleft.mapping_idx = idx;
+		break;
+	case BUTTON_DRIGHT:
+		drvdata.bmap_2->btn_dright.mapping_idx = idx;
+		break;
+	case BUTTON_M1:
+		drvdata.bmap_2->btn_m1.mapping_idx = idx;
+		break;
+	case BUTTON_M2:
+		drvdata.bmap_2->btn_m2.mapping_idx = idx;
+		break;
+	default:
+		return -EINVAL;
+	}
+	mod_delayed_work(system_wq, &oxp_btn_queue, msecs_to_jiffies(50));
+	return count;
+}
+
+static ssize_t map_button_show(struct device *dev,
+			       struct device_attribute *attr, char *buf,
+			       u8 index)
+{
+	u8 i;
+
+	switch (index) {
+	case BUTTON_A:
+		i = drvdata.bmap_1->btn_a.mapping_idx;
+		break;
+	case BUTTON_B:
+		i = drvdata.bmap_1->btn_b.mapping_idx;
+		break;
+	case BUTTON_X:
+		i = drvdata.bmap_1->btn_x.mapping_idx;
+		break;
+	case BUTTON_Y:
+		i = drvdata.bmap_1->btn_y.mapping_idx;
+		break;
+	case BUTTON_LB:
+		i = drvdata.bmap_1->btn_lb.mapping_idx;
+		break;
+	case BUTTON_RB:
+		i = drvdata.bmap_1->btn_rb.mapping_idx;
+		break;
+	case BUTTON_LT:
+		i = drvdata.bmap_1->btn_lt.mapping_idx;
+		break;
+	case BUTTON_RT:
+		i = drvdata.bmap_1->btn_rt.mapping_idx;
+		break;
+	case BUTTON_START:
+		i = drvdata.bmap_1->btn_start.mapping_idx;
+		break;
+	case BUTTON_SELECT:
+		i = drvdata.bmap_2->btn_select.mapping_idx;
+		break;
+	case BUTTON_L3:
+		i = drvdata.bmap_2->btn_l3.mapping_idx;
+		break;
+	case BUTTON_R3:
+		i = drvdata.bmap_2->btn_r3.mapping_idx;
+		break;
+	case BUTTON_DUP:
+		i = drvdata.bmap_2->btn_dup.mapping_idx;
+		break;
+	case BUTTON_DDOWN:
+		i = drvdata.bmap_2->btn_ddown.mapping_idx;
+		break;
+	case BUTTON_DLEFT:
+		i = drvdata.bmap_2->btn_dleft.mapping_idx;
+		break;
+	case BUTTON_DRIGHT:
+		i = drvdata.bmap_2->btn_dright.mapping_idx;
+		break;
+	case BUTTON_M1:
+		i = drvdata.bmap_2->btn_m1.mapping_idx;
+		break;
+	case BUTTON_M2:
+		i = drvdata.bmap_2->btn_m2.mapping_idx;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	if (i >= ARRAY_SIZE(oxp_button_table))
+		return -EINVAL;
+
+	return sysfs_emit(buf, "%s\n", oxp_button_table[i].name);
+}
+
+static ssize_t button_mapping_options_show(struct device *dev,
+					   struct device_attribute *attr, char *buf)
+{
+	ssize_t count = 0;
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(oxp_button_table); i++)
+		count += sysfs_emit_at(buf, count, "%s ", oxp_button_table[i].name);
+
+	if (count)
+		buf[count - 1] = '\n';
+
+	return count;
+}
+static DEVICE_ATTR_RO(button_mapping_options);
+
+#define OXP_DEVICE_ATTR_RW(_name, _group)                                     \
+	static ssize_t _name##_store(struct device *dev,                      \
+				     struct device_attribute *attr,           \
+				     const char *buf, size_t count)           \
+	{                                                                     \
+		return _group##_store(dev, attr, buf, count, _name.index);    \
+	}                                                                     \
+	static ssize_t _name##_show(struct device *dev,                       \
+				    struct device_attribute *attr, char *buf) \
+	{                                                                     \
+		return _group##_show(dev, attr, buf, _name.index);            \
+	}                                                                     \
+	static DEVICE_ATTR_RW(_name)
+
+static struct oxp_attr button_a = { BUTTON_A };
+OXP_DEVICE_ATTR_RW(button_a, map_button);
+
+static struct oxp_attr button_b = { BUTTON_B };
+OXP_DEVICE_ATTR_RW(button_b, map_button);
+
+static struct oxp_attr button_x = { BUTTON_X };
+OXP_DEVICE_ATTR_RW(button_x, map_button);
+
+static struct oxp_attr button_y = { BUTTON_Y };
+OXP_DEVICE_ATTR_RW(button_y, map_button);
+
+static struct oxp_attr button_lb = { BUTTON_LB };
+OXP_DEVICE_ATTR_RW(button_lb, map_button);
+
+static struct oxp_attr button_rb = { BUTTON_RB };
+OXP_DEVICE_ATTR_RW(button_rb, map_button);
+
+static struct oxp_attr button_lt = { BUTTON_LT };
+OXP_DEVICE_ATTR_RW(button_lt, map_button);
+
+static struct oxp_attr button_rt = { BUTTON_RT };
+OXP_DEVICE_ATTR_RW(button_rt, map_button);
+
+static struct oxp_attr button_start = { BUTTON_START };
+OXP_DEVICE_ATTR_RW(button_start, map_button);
+
+static struct oxp_attr button_select = { BUTTON_SELECT };
+OXP_DEVICE_ATTR_RW(button_select, map_button);
+
+static struct oxp_attr button_l3 = { BUTTON_L3 };
+OXP_DEVICE_ATTR_RW(button_l3, map_button);
+
+static struct oxp_attr button_r3 = { BUTTON_R3 };
+OXP_DEVICE_ATTR_RW(button_r3, map_button);
+
+static struct oxp_attr button_d_up = { BUTTON_DUP };
+OXP_DEVICE_ATTR_RW(button_d_up, map_button);
+
+static struct oxp_attr button_d_down = { BUTTON_DDOWN };
+OXP_DEVICE_ATTR_RW(button_d_down, map_button);
+
+static struct oxp_attr button_d_left = { BUTTON_DLEFT };
+OXP_DEVICE_ATTR_RW(button_d_left, map_button);
+
+static struct oxp_attr button_d_right = { BUTTON_DRIGHT };
+OXP_DEVICE_ATTR_RW(button_d_right, map_button);
+
+static struct oxp_attr button_m1 = { BUTTON_M1 };
+OXP_DEVICE_ATTR_RW(button_m1, map_button);
+
+static struct oxp_attr button_m2 = { BUTTON_M2 };
+OXP_DEVICE_ATTR_RW(button_m2, map_button);
+
 static struct attribute *oxp_cfg_attrs[] = {
+	&dev_attr_button_a.attr,
+	&dev_attr_button_b.attr,
+	&dev_attr_button_d_down.attr,
+	&dev_attr_button_d_left.attr,
+	&dev_attr_button_d_right.attr,
+	&dev_attr_button_d_up.attr,
+	&dev_attr_button_l3.attr,
+	&dev_attr_button_lb.attr,
+	&dev_attr_button_lt.attr,
+	&dev_attr_button_m1.attr,
+	&dev_attr_button_m2.attr,
+	&dev_attr_button_mapping_options.attr,
+	&dev_attr_button_r3.attr,
+	&dev_attr_button_rb.attr,
+	&dev_attr_button_rt.attr,
+	&dev_attr_button_select.attr,
+	&dev_attr_button_start.attr,
+	&dev_attr_button_x.attr,
+	&dev_attr_button_y.attr,
 	&dev_attr_gamepad_mode.attr,
 	&dev_attr_gamepad_mode_index.attr,
+	&dev_attr_reset_buttons.attr,
 	NULL,
 };
 
@@ -823,6 +1374,8 @@ static bool oxp_hybrid_mcu_device(void)
 
 static int oxp_cfg_probe(struct hid_device *hdev, u16 up)
 {
+	struct oxp_bmap_page_1 *bmap_1;
+	struct oxp_bmap_page_2 *bmap_2;
 	int ret;
 
 	hid_set_drvdata(hdev, &drvdata);
@@ -855,9 +1408,21 @@ static int oxp_cfg_probe(struct hid_device *hdev, u16 up)
 		return 0;
 
 skip_rgb:
-	mod_delayed_work(system_wq, &oxp_mcu_init, msecs_to_jiffies(50));
-
+	bmap_1 = devm_kzalloc(&hdev->dev, sizeof(struct oxp_bmap_page_1), GFP_KERNEL);
+	if (!bmap_1)
+		return dev_err_probe(&hdev->dev, -ENOMEM,
+				     "Unable to allocate button map page 1\n");
+
+	bmap_2 = devm_kzalloc(&hdev->dev, sizeof(struct oxp_bmap_page_2), GFP_KERNEL);
+	if (!bmap_2)
+		return dev_err_probe(&hdev->dev, -ENOMEM,
+				     "Unable to allocate button map page 2\n");
+
+	drvdata.bmap_1 = bmap_1;
+	drvdata.bmap_2 = bmap_2;
+	oxp_reset_buttons();
 	drvdata.gamepad_mode = OXP_GP_MODE_XINPUT;
+	mod_delayed_work(system_wq, &oxp_mcu_init, msecs_to_jiffies(50));
 
 	ret = devm_device_add_group(&hdev->dev, &oxp_cfg_attrs_group);
 	if (ret)
-- 
2.53.0


^ permalink raw reply related

* Re: [PATCH] Input: edt-ft5x06 - Support label device property for input name
From: Dmitry Torokhov @ 2026-04-13  3:58 UTC (permalink / raw)
  To: webgeek1234; +Cc: linux-input, linux-kernel
In-Reply-To: <20260409-ft5x06-label-v1-1-21e8a9ae9a60@gmail.com>

Hi Aaron,

On Thu, Apr 09, 2026 at 06:16:08PM -0500, Aaron Kling via B4 Relay wrote:
> From: Aaron Kling <webgeek1234@gmail.com>
> 
> The AYN Thor uses a ft5426 and a ft5452 for each screen respectively and
> these currently get the same input name, making them indistinguishable
> from userspace. Support setting a label in kernel dt to make these
> report uniquely.
> 
> Signed-off-by: Aaron Kling <webgeek1234@gmail.com>
> ---
>  drivers/input/touchscreen/edt-ft5x06.c | 4 +++-
>  1 file changed, 3 insertions(+), 1 deletion(-)
> 
> diff --git a/drivers/input/touchscreen/edt-ft5x06.c b/drivers/input/touchscreen/edt-ft5x06.c
> index ba8ff65f7ea671..c36497571b1aa1 100644
> --- a/drivers/input/touchscreen/edt-ft5x06.c
> +++ b/drivers/input/touchscreen/edt-ft5x06.c
> @@ -1285,7 +1285,9 @@ static int edt_ft5x06_ts_probe(struct i2c_client *client)
>  		"Model \"%s\", Rev. \"%s\", %dx%d sensors\n",
>  		tsdata->name, tsdata->fw_version, tsdata->num_x, tsdata->num_y);
>  
> -	input->name = tsdata->name;
> +	if (device_property_read_string(&client->dev, "label", &input->name))
> +		input->name = tsdata->name;
> +

You should be able to differentiate them by their sysfs path.

Thanks.

-- 
Dmitry

^ permalink raw reply

* Re: [PATCH] Input: edt-ft5x06 - Support label device property for input name
From: Aaron Kling @ 2026-04-13  4:02 UTC (permalink / raw)
  To: Dmitry Torokhov; +Cc: linux-input, linux-kernel
In-Reply-To: <adxpdaSXYAa9dLPD@google.com>

On Sun, Apr 12, 2026 at 10:58 PM Dmitry Torokhov
<dmitry.torokhov@gmail.com> wrote:
>
> Hi Aaron,
>
> On Thu, Apr 09, 2026 at 06:16:08PM -0500, Aaron Kling via B4 Relay wrote:
> > From: Aaron Kling <webgeek1234@gmail.com>
> >
> > The AYN Thor uses a ft5426 and a ft5452 for each screen respectively and
> > these currently get the same input name, making them indistinguishable
> > from userspace. Support setting a label in kernel dt to make these
> > report uniquely.
> >
> > Signed-off-by: Aaron Kling <webgeek1234@gmail.com>
> > ---
> >  drivers/input/touchscreen/edt-ft5x06.c | 4 +++-
> >  1 file changed, 3 insertions(+), 1 deletion(-)
> >
> > diff --git a/drivers/input/touchscreen/edt-ft5x06.c b/drivers/input/touchscreen/edt-ft5x06.c
> > index ba8ff65f7ea671..c36497571b1aa1 100644
> > --- a/drivers/input/touchscreen/edt-ft5x06.c
> > +++ b/drivers/input/touchscreen/edt-ft5x06.c
> > @@ -1285,7 +1285,9 @@ static int edt_ft5x06_ts_probe(struct i2c_client *client)
> >               "Model \"%s\", Rev. \"%s\", %dx%d sensors\n",
> >               tsdata->name, tsdata->fw_version, tsdata->num_x, tsdata->num_y);
> >
> > -     input->name = tsdata->name;
> > +     if (device_property_read_string(&client->dev, "label", &input->name))
> > +             input->name = tsdata->name;
> > +
>
> You should be able to differentiate them by their sysfs path.

My target use case is Android, which to my knowledge can only use
vid:pid or device name to differentiate input devices. The sysfs path
does not help this case.

Aaron

^ permalink raw reply

* Re: [PATCH] Input: edt-ft5x06 - Support label device property for input name
From: Dmitry Torokhov @ 2026-04-13  4:06 UTC (permalink / raw)
  To: Aaron Kling; +Cc: linux-input, linux-kernel
In-Reply-To: <CALHNRZ_m-hWpmp5OgOjHEc-QHRAkmTGaJX=O_K-X6EwY-5ToFQ@mail.gmail.com>

On Sun, Apr 12, 2026 at 11:02:59PM -0500, Aaron Kling wrote:
> On Sun, Apr 12, 2026 at 10:58 PM Dmitry Torokhov
> <dmitry.torokhov@gmail.com> wrote:
> >
> > Hi Aaron,
> >
> > On Thu, Apr 09, 2026 at 06:16:08PM -0500, Aaron Kling via B4 Relay wrote:
> > > From: Aaron Kling <webgeek1234@gmail.com>
> > >
> > > The AYN Thor uses a ft5426 and a ft5452 for each screen respectively and
> > > these currently get the same input name, making them indistinguishable
> > > from userspace. Support setting a label in kernel dt to make these
> > > report uniquely.
> > >
> > > Signed-off-by: Aaron Kling <webgeek1234@gmail.com>
> > > ---
> > >  drivers/input/touchscreen/edt-ft5x06.c | 4 +++-
> > >  1 file changed, 3 insertions(+), 1 deletion(-)
> > >
> > > diff --git a/drivers/input/touchscreen/edt-ft5x06.c b/drivers/input/touchscreen/edt-ft5x06.c
> > > index ba8ff65f7ea671..c36497571b1aa1 100644
> > > --- a/drivers/input/touchscreen/edt-ft5x06.c
> > > +++ b/drivers/input/touchscreen/edt-ft5x06.c
> > > @@ -1285,7 +1285,9 @@ static int edt_ft5x06_ts_probe(struct i2c_client *client)
> > >               "Model \"%s\", Rev. \"%s\", %dx%d sensors\n",
> > >               tsdata->name, tsdata->fw_version, tsdata->num_x, tsdata->num_y);
> > >
> > > -     input->name = tsdata->name;
> > > +     if (device_property_read_string(&client->dev, "label", &input->name))
> > > +             input->name = tsdata->name;
> > > +
> >
> > You should be able to differentiate them by their sysfs path.
> 
> My target use case is Android, which to my knowledge can only use
> vid:pid or device name to differentiate input devices. The sysfs path
> does not help this case.

Please work with Android team to add missing functionality.

Thanks.

-- 
Dmitry

^ permalink raw reply

* Re: [PATCH] HID: core: clamp report_size in s32ton() to avoid undefined shift
From: Jiri Kosina @ 2026-04-13  9:40 UTC (permalink / raw)
  To: Greg Kroah-Hartman; +Cc: linux-input, linux-kernel, stable, Benjamin Tissoires
In-Reply-To: <2026040609-equation-ascent-2b3d@gregkh>

On Mon, 6 Apr 2026, Greg Kroah-Hartman wrote:

> s32ton() shifts by n-1 where n is the field's report_size, a value that
> comes directly from a HID device.  The HID parser bounds report_size
> only to <= 256, so a broken HID device can supply a report descriptor
> with a wide field that triggers shift exponents up to 256 on a 32-bit
> type when an output report is built via hid_output_field() or
> hid_set_field().
> 
> Commit ec61b41918587 ("HID: core: fix shift-out-of-bounds in
> hid_report_raw_event") added the same n > 32 clamp to the function
> snto32(), but s32ton() was never given the same fix as I guess syzbot
> hadn't figured out how to fuzz a device the same way.
> 
> Fix this up by just clamping the max value of n, just like snto32()
> does.
> 
> Cc: stable <stable@kernel.org>
> Cc: Jiri Kosina <jikos@kernel.org>
> Cc: Benjamin Tissoires <bentiss@kernel.org>
> Cc: linux-input@vger.kernel.org
> Assisted-by: gregkh_clanker_t1000
> Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>

Applied, thanks.

-- 
Jiri Kosina
SUSE Labs


^ permalink raw reply

* [PATCH v2] HID: multitouch: Fix Yoga Book 9 14IAH10 touchscreen misclassification
From: Dave Carey @ 2026-04-13 12:58 UTC (permalink / raw)
  To: linux-input; +Cc: jikos, bentiss, Dave Carey
In-Reply-To: <20260402182937.388847-1-carvsdriver@gmail.com>

The Lenovo Yoga Book 9 14IAH10 (83KJ) (17EF:6161) firmware includes a
HID_DG_TOUCHPAD application collection designed for the Windows inbox HID
driver's Win8 PTP touchpad mode.  On Linux the HID_DG_TOUCHSCREEN
collections provide the correct direct-touch interface.  The presence of
the touchpad collection causes hid-multitouch to misclassify the
touchscreen nodes as indirect buttonpads, leaving them non-functional.

Within the touchpad collection:
- HID_UP_BUTTON usages trigger the touchscreen-with-buttons heuristic
  that sets INPUT_MT_POINTER on the touchscreen applications.
- The HID_DG_TOUCHPAD application itself sets INPUT_MT_POINTER via
  mt_allocate_application(), propagating to all touchscreen nodes.
- A HID_DG_BUTTONTYPE feature (report 0x51) returns MT_BUTTONTYPE_CLICKPAD,
  setting td->is_buttonpad = true for the entire device.

Additionally, the firmware resets if any USB control request arrives while
the CDC-ACM interface is initialising (~1.18 s after enumeration).
The Win8 compliance blob (0xff00:0xc5) and Contact Count Max feature
reports in the touchscreen collections trigger GET_REPORT calls at probe
that hit this window.  Surface Switch (0x57) and Button Switch (0x58)
feature reports are sent by mt_set_modes() on every input-device open and
close, repeatedly hitting this window throughout device lifetime.

The firmware also leaves a persistent ghost contact in its contact buffer
(contact ID 2, fixed coordinates, tip always asserted) on every enumeration.
This ghost occupies a multitouch slot and prevents KWin from seeing a clean
finger-lift, causing stuck touch state.  The ghost is cleared when Input
Mode is set via HID_REQ_SET_REPORT at probe.

Fix using a report descriptor fixup in mt_report_fixup() and a class
definition update:

1. Remove the entire HID_DG_TOUCHPAD application collection.  Parsing
   HID short items from its header to the matching End Collection and
   closing the gap with memmove eliminates all three BUTTONPAD
   heuristics and the feature reports within the collection.

2. Neutralize the Win8 compliance blob feature reports remaining in the
   touchscreen collections by changing Usage Page 0xff00 to 0x0f00,
   preventing the case 0xff0000c5 branch in mt_feature_mapping() from
   issuing GET_REPORT.

3. Neutralize the Contact Count Max feature reports by changing usage
   0x55 to 0x00; set maxcontacts = 10 in the class definition so the
   driver uses the correct contact limit without querying the device.

4. Neutralize Surface Switch (0x57) and Button Switch (0x58) feature
   report usages in the Device Configuration collection so mt_set_modes()
   does not issue HID_REQ_SET_REPORT for these on every input-device
   open/close.  Input Mode (0x52) is intentionally left intact: the single
   HID_REQ_SET_REPORT at probe flushes the firmware's contact buffer and
   clears the persistent ghost contact.  By probe time the cdc-acm driver
   has already satisfied the CDC-ACM init watchdog (~130 ms), so this
   request arrives safely after the reset window has closed.

5. Add MT_QUIRK_NOT_SEEN_MEANS_UP to the MT_CLS_YOGABOOK9I class so that
   contacts not present in a frame are released via INPUT_MT_DROP_UNUSED,
   preventing stale multitouch slots from lingering if the firmware omits
   a contact from a report.

Signed-off-by: Dave Carey <carvsdriver@gmail.com>
Tested-by: Dave Carey <carvsdriver@gmail.com>
---
Re: Benjamin's question about the Windows driver — the device uses
Windows' generic inbox drivers: usbser.sys binds interface 0 (CDC ACM)
and the generic HID class driver handles the HID interfaces.  The
HID_DG_TOUCHPAD collection is in the descriptor for Windows' PTP inbox
touchpad path, not for a custom driver.  On Windows the system routes
input through the touchpad application; on Linux hid-multitouch sees
both the touchscreen and touchpad applications and gets confused by the
touchpad one.  The descriptor fixup removes it from Linux's view.

Changes in v2:
  - Replace per-callsite MT_QUIRK_YOGABOOK9I guards with a single
    mt_yogabook9_fixup() function called from mt_report_fixup().
  - Drop the HID_DG_TOUCHPAD application collection entirely via memmove,
    eliminating all three BUTTONPAD heuristics at source rather than
    suppressing their effects at each callsite.
  - Neutralize Win8 compliance blob GET_REPORT triggers by changing
    Usage Page 0xff00 to 0x0f00 in the descriptor.
  - Neutralize Contact Count Max GET_REPORT trigger (usage 0x55 -> 0x00);
    set maxcontacts = 10 in the class definition.
  - Neutralize Surface Switch (0x57) and Button Switch (0x58) SET_REPORT
    triggers; retain Input Mode (0x52) so the single probe-time SET_REPORT
    flushes the firmware contact buffer and clears a persistent ghost contact.
  - Add MT_QUIRK_NOT_SEEN_MEANS_UP to the MT_CLS_YOGABOOK9I class.

 drivers/hid/hid-multitouch.c | 146 ++++++++++++++++++++++++++++++++++-
 1 file changed, 145 insertions(+), 1 deletion(-)

diff --git a/drivers/hid/hid-multitouch.c b/drivers/hid/hid-multitouch.c
index e82a3c4e5..ec04dbafb 100644
--- a/drivers/hid/hid-multitouch.c
+++ b/drivers/hid/hid-multitouch.c
@@ -443,11 +443,13 @@ static const struct mt_class mt_classes[] = {
 			MT_QUIRK_CONTACT_CNT_ACCURATE,
 	},
 		{ .name = MT_CLS_YOGABOOK9I,
-		.quirks = MT_QUIRK_ALWAYS_VALID |
+		.quirks = MT_QUIRK_NOT_SEEN_MEANS_UP |
+			MT_QUIRK_ALWAYS_VALID |
 			MT_QUIRK_FORCE_MULTI_INPUT |
 			MT_QUIRK_SEPARATE_APP_REPORT |
 			MT_QUIRK_HOVERING |
 			MT_QUIRK_YOGABOOK9I,
+		.maxcontacts = 10,
 		.export_all_inputs = true
 	},
 	{ .name = MT_CLS_EGALAX_P80H84,
@@ -1566,6 +1568,144 @@ static int mt_event(struct hid_device *hid, struct hid_field *field,
 	return 0;
 }
 
+/*
+ * Yoga Book 9 14IAH10 descriptor fixup.
+ *
+ * The device includes a HID_DG_TOUCHPAD application collection designed for
+ * the Windows inbox HID driver's Win8 PTP touchpad mode.  On Linux we want
+ * only the HID_DG_TOUCHSCREEN collections.  The touchpad collection (and the
+ * HID_DG_BUTTONTYPE and Win8 compliance blob features it contains) must be
+ * removed so hid-multitouch does not misclassify the touchscreen nodes as
+ * indirect buttonpads.
+ *
+ * The firmware also resets if any USB control request is received while the
+ * CDC-ACM interface is initialising (~1.18 s after enumeration).  Dropping
+ * the Win8 blob and Contact Count Max feature reports prevents the
+ * GET_REPORT calls that hid-multitouch issues at probe.
+ */
+static void mt_yogabook9_fixup(struct hid_device *hdev, __u8 *rdesc,
+			       unsigned int *size)
+{
+	/* Usage Page (Digitizer), Usage (Touch Pad), Collection (Application) */
+	static const __u8 tp_app_hdr[] = { 0x05, 0x0d, 0x09, 0x05, 0xa1, 0x01 };
+	/* Vendor Usage Page 0xff00 (Win8 compliance blob header) */
+	static const __u8 win8_page[] = { 0x06, 0x00, 0xff };
+	/* Usage (Contact Count Max = 0x55) */
+	static const __u8 ccmax_usage[] = { 0x09, 0x55 };
+	unsigned int i;
+
+	/*
+	 * Step 1: find and remove the Touch Pad application collection.
+	 * Walk HID short items from the collection header to its matching
+	 * End Collection, then close the gap with memmove.
+	 */
+	for (i = 0; i + sizeof(tp_app_hdr) <= *size; i++) {
+		if (memcmp(rdesc + i, tp_app_hdr, sizeof(tp_app_hdr)) == 0) {
+			__u8 *start = rdesc + i;
+			__u8 *coll_end = NULL;
+			__u8 *p = start;
+			unsigned int drop;
+			int depth = 0;
+
+			while (p < rdesc + *size) {
+				__u8 b = *p;
+				int ds = b & 3;
+				int item_len;
+
+				if (b == 0xfe) { /* long item */
+					if (p + 2 >= rdesc + *size)
+						break;
+					item_len = p[1] + 3;
+				} else {
+					item_len = (ds == 3) ? 5 : ds + 1;
+				}
+				if (p + item_len > rdesc + *size)
+					break;
+
+				if ((b & 0xfc) == 0xa0)
+					depth++; /* Collection */
+				else if (b == 0xc0) {
+					depth--; /* End Collection */
+					if (depth == 0) {
+						coll_end = p;
+						break;
+					}
+				}
+				p += item_len;
+			}
+
+			if (!coll_end) {
+				hid_err(hdev,
+					"Yoga Book 9: Touch Pad End Collection not found\n");
+				break;
+			}
+
+			drop = coll_end - start + 1;
+			memmove(start, coll_end + 1, rdesc + *size - coll_end - 1);
+			*size -= drop;
+			hid_dbg(hdev,
+				"Yoga Book 9: dropped Touch Pad collection (%u bytes)\n",
+				drop);
+			break;
+		}
+	}
+
+	/*
+	 * Step 2: neutralize Win8 compliance blob feature reports remaining
+	 * in the touchscreen collections.  Change Usage Page 0xff00 to 0x0f00
+	 * so the case 0xff0000c5 branch in mt_feature_mapping() is not reached
+	 * and no GET_REPORT is issued.
+	 */
+	for (i = 0; i + sizeof(win8_page) <= *size; i++) {
+		if (memcmp(rdesc + i, win8_page, sizeof(win8_page)) == 0) {
+			rdesc[i + 2] = 0x0f; /* 0xff00 -> 0x0f00 */
+			hid_dbg(hdev,
+				"Yoga Book 9: neutralized Win8 blob at offset %u\n",
+				i);
+		}
+	}
+
+	/*
+	 * Step 3: neutralize Contact Count Max feature reports.  Change usage
+	 * 0x55 (HID_DG_CONTACTMAX) to 0x00 so mt_feature_mapping() does not
+	 * issue GET_REPORT.  The class maxcontacts field provides the value.
+	 */
+	for (i = 0; i + sizeof(ccmax_usage) <= *size; i++) {
+		if (memcmp(rdesc + i, ccmax_usage, sizeof(ccmax_usage)) == 0) {
+			rdesc[i + 1] = 0x00;
+			hid_dbg(hdev,
+				"Yoga Book 9: neutralized ContactMax at offset %u\n",
+				i);
+		}
+	}
+
+	/*
+	 * Step 4: neutralize Surface Switch (0x57) and Button Switch (0x58)
+	 * feature report usages in the Device Configuration collection.
+	 * mt_set_modes() issues HID_REQ_SET_REPORT for these on every
+	 * input-device open/close; those repeated control requests hit the
+	 * firmware's CDC-ACM init window and trigger resets.
+	 *
+	 * Input Mode (0x52) is intentionally left intact.  mt_set_modes()
+	 * sends it once at probe to set the device into touchscreen mode,
+	 * which flushes the firmware's contact buffer and clears a persistent
+	 * ghost contact (cid 2, fixed coordinates) that otherwise appears on
+	 * every enumeration.  By probe time cdc_acm has already satisfied the
+	 * CDC-ACM init watchdog (~130 ms), so the single SET_REPORT for Input
+	 * Mode arrives safely after the reset window has closed.
+	 */
+	for (i = 0; i + 2 <= *size; i++) {
+		if (rdesc[i] == 0x09 &&
+		    (rdesc[i + 1] == 0x57 ||
+		     rdesc[i + 1] == 0x58)) {
+			hid_dbg(hdev,
+				"Yoga Book 9: neutralized set-modes usage 0x%02x at offset %u\n",
+				rdesc[i + 1], i);
+			rdesc[i + 1] = 0x00;
+		}
+	}
+}
+
 static const __u8 *mt_report_fixup(struct hid_device *hdev, __u8 *rdesc,
 			     unsigned int *size)
 {
@@ -1595,6 +1735,10 @@ got: %x\n",
 		}
 	}
 
+	if (hdev->vendor == USB_VENDOR_ID_LENOVO &&
+	    hdev->product == USB_DEVICE_ID_LENOVO_YOGABOOK9I)
+		mt_yogabook9_fixup(hdev, rdesc, size);
+
 	return rdesc;
 }
 

base-commit: 705c735d0ef7701cf9ded290545345a8c9b8bd7e
-- 
2.53.0


^ permalink raw reply related

* Re: [PATCH] HID: logitech-dj: fix wrong detection of bad DJ_SHORT output report
From: Lee Jones @ 2026-04-13 13:15 UTC (permalink / raw)
  To: Jiri Kosina
  Cc: Benjamin Tissoires, Filipe Laíns, linux-input, linux-kernel
In-Reply-To: <2qo95np2-n977-9r09-p016-880q98025q44@xreary.bet>

On Fri, 10 Apr 2026, Jiri Kosina wrote:

> On Fri, 10 Apr 2026, Benjamin Tissoires wrote:
> 
> > commit b6a57912854e ("HID: logitech-dj: Prevent REPORT_ID_DJ_SHORT
> > related user initiated OOB write") assumed that all HID devices attached
> > to the logitech-dj driver was having an output report of DJ_SHORT.
> > 
> > However, on the receiver itself, we have 2 other HID device we attach
> > here: the mouse emulation and the keyboard emulation. For those devices
> > the value of rep is NULL and we are triggered a segfault here.
> > 
> > This is doubly required because logitech-dj also handles non DJ devices
> > that might not have the DJ collection.
> > 
> > Fixes: b6a57912854e ("HID: logitech-dj: Prevent REPORT_ID_DJ_SHORT related user initiated OOB write")
> > Signed-off-by: Benjamin Tissoires <bentiss@kernel.org>
> 
> Thanks a lot Benjamin, your CI be praised!

Thanks Benjamin.  I appreciate your work.

If it's not too late:

  Reviewed-by: Lee Jones <lee@kernel.org>

-- 
Lee Jones [李琼斯]

^ permalink raw reply

* Re: [PATCH 0/2] MIPS RB532 GPIO descriptor conversion
From: Thomas Bogendoerfer @ 2026-04-13 13:43 UTC (permalink / raw)
  To: Linus Walleij
  Cc: Dmitry Torokhov, Bartosz Golaszewski, Miquel Raynal,
	Richard Weinberger, Vignesh Raghavendra, linux-mips, linux-input,
	linux-gpio, linux-mtd
In-Reply-To: <20260328-mips-input-rb532-button-v1-0-98e201621501@kernel.org>

On Sat, Mar 28, 2026 at 04:55:46PM +0100, Linus Walleij wrote:
> This moves the MIPS Mikrotik RouterBoard RB532 over to using
> GPIO descriptors by augmenting the two remaining drivers using
> GPIOs to use software nodes and device properties.
> 
> This is part of the pull to get rid of the legacy GPIO API
> inside the kernel.
> 
> It would be nice if someone can test of this actually works,
> I've only compile-tested it.
> 
> If we can agree on this method to move forward with this machine
> it would be nice if the MIPS maintainer could merge the end
> result with ACKs from the input and MTD maintainers.
> 
> Signed-off-by: Linus Walleij <linusw@kernel.org>
> ---
> Linus Walleij (2):
>       MIPS/input: Move RB532 button to GPIO descriptors
>       MIPS/mtd: Handle READY GPIO in generic NAND platform data
> 
>  arch/mips/rb532/devices.c         | 83 ++++++++++++++++++++++++++++-----------
>  drivers/input/misc/rb532_button.c | 35 ++++++++++++++---
>  drivers/mtd/nand/raw/plat_nand.c  | 24 ++++++++++-
>  3 files changed, 113 insertions(+), 29 deletions(-)

series applied to mips-next

Thomas.

-- 
Crap can work. Given enough thrust pigs will fly, but it's not necessarily a
good idea.                                                [ RFC1925, 2.3 ]

^ permalink raw reply

* Re: [PATCH] HID: apple: Add Niz keyboard dongle to non-apple keyboards list
From: utzcoz @ 2026-04-13 15:27 UTC (permalink / raw)
  To: Jiri Kosina; +Cc: Benjamin Tissoires, linux-input, linux-kernel
In-Reply-To: <0rqo1ors-45s9-n22r-6qss-2q3q96n5r6rr@xreary.bet>

Hi Jiri,

Thanks for your review.

Sorry for sending this email again with plain text mode as the
previous one has the html content part.

> doesn't really sound too well established identity to me,

Yeah. It's my id for code contribution, not my real name, and you can
find me as a real person at https://github.com/utzcoz.

>  know that the kernel documentation is now a little bit more liberal  about not having to use real names

Sorry about it, and it's my contribution to Linux Kernel to resolve my
personal issue, and I didn't notice this rule.

> but "well established identities"  being fine

I also use this id to contribute code to many large open-source
projects like AOSP, Chromium(although small contributions),
Could you think of it as the "well established identities"?

Thanks for your reviewing and reminder again.

^ permalink raw reply

* Re: [PATCH v5 4/4] Input: charlieplex_keypad: add GPIO charlieplex keypad
From: Hugo Villeneuve @ 2026-04-13 16:20 UTC (permalink / raw)
  To: Andy Shevchenko
  Cc: robin, andy, geert, robh, krzk+dt, conor+dt, dmitry.torokhov,
	hvilleneuve, mkorpershoek, matthias.bgg,
	angelogioacchino.delregno, lee, alexander.sverdlin, marek.vasut,
	akurz, devicetree, linux-kernel, linux-input, linux-arm-kernel,
	linux-mediatek
In-Reply-To: <abPXX1eWoq7C7J1R@ashevche-desk.local>

Hi Dmitry,

On Fri, 13 Mar 2026 11:22:39 +0200
Andy Shevchenko <andriy.shevchenko@intel.com> wrote:

> On Thu, Mar 12, 2026 at 02:00:58PM -0400, Hugo Villeneuve wrote:
> > 
> > Add support for GPIO-based charlieplex keypad, allowing to control
> > N^2-N keys using N GPIO lines.
> > 
> > Reuse matrix keypad keymap to simplify, even if there is no concept
> > of rows and columns in this type of keyboard.
> 
> LGTM,
> Reviewed-by: Andy Shevchenko <andriy.shevchenko@intel.com>

I was just wondering if this will go into v7.1, as I am not seing the
patch series in your input/next tree/branch for the moment? Let me know
if you need me to rebase it on v7.0.

-- 
Hugo Villeneuve

^ permalink raw reply


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox