The Linux Kernel Mailing List
 help / color / mirror / Atom feed
From: Benjamin Tissoires <bentiss@kernel.org>
To: Nikolas Koesling <nikolas@koesling.info>
Cc: Jiri Kosina <jikos@kernel.org>, Lode Willems <me@lodewillems.com>,
	 linux-input@vger.kernel.org, linux-kernel@vger.kernel.org,
	Leo <leo@managarm.org>
Subject: Re: [PATCH v6] HID: pulsar: add driver for Pulsar gaming mice
Date: Tue, 25 Aug 2026 12:12:49 +0200	[thread overview]
Message-ID: <ao1pMvF1_yXKX4D_@beelink> (raw)
In-Reply-To: <20260816105423.47305-1-nikolas@koesling.info>

On Aug 16 2026, Nikolas Koesling wrote:
> 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>

I'm a little bit worried that we are at version 6 when a maintainer
made you a remark at version 3 that wasn't addressed nor replied:

About HID_KYSONA:
"I guess that we want to just ditch that driver altogether then?"

Having 2 drivers for the same device is bad. Simply bad. If hid-kysona
is not required, it should be dropped. Please, don't make 2 drivers for
the same device.

Cheers,
Benjamin

> ---
> 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
> 
> Changes in v4:
> - Call hid_hw_stop after cancel_work_sync
> - Resolve use after free (mutex)
> 
> Changes in v5:
> - Add bus number to battery name
> - Fix battery cache during first 60s uptime
> 
> Changes in v6:
> - Fix ATK vendor IDs (swap wired/dongle)
> ---
>  MAINTAINERS              |   6 +
>  drivers/hid/Kconfig      |  15 +
>  drivers/hid/Makefile     |   1 +
>  drivers/hid/hid-ids.h    |  11 +
>  drivers/hid/hid-pulsar.c | 763 +++++++++++++++++++++++++++++++++++++++
>  5 files changed, 796 insertions(+)
>  create mode 100644 drivers/hid/hid-pulsar.c
> 
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 8014b9f8253e..2e51a79366af 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -11552,6 +11552,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 f9bcaeb66385..17c3a0d0c94b 100644
> --- a/drivers/hid/Kconfig
> +++ b/drivers/hid/Kconfig
> @@ -504,12 +504,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
> @@ -1312,6 +1315,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 23e6e3dd0c56..fbc6cc8fc589 100644
> --- a/drivers/hid/Makefile
> +++ b/drivers/hid/Makefile
> @@ -115,6 +115,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_RAKK)		+= hid-rakk.o
>  obj-$(CONFIG_HID_RAPOO) += hid-rapoo.o
> diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
> index 1059922baaac..e3c4b116396b 100644
> --- a/drivers/hid/hid-ids.h
> +++ b/drivers/hid/hid-ids.h
> @@ -251,6 +251,12 @@
>  #define USB_VENDOR_ID_ATMEL_V_USB	0x16c0
>  #define USB_DEVICE_ID_ATMEL_V_USB	0x05df
>  
> +#define USB_VENDOR_ID_ATK	0x3554
> +#define USB_DEVICE_ID_ATK_VXE_R1_SE_WIRED	0xF58F
> +
> +#define USB_VENDOR_ID_ATK_ALT		0x373B
> +#define USB_DEVICE_ID_ATK_VXE_R1_SE_DONGLE	0x1085
> +
>  #define USB_VENDOR_ID_AUREAL		0x0755
>  #define USB_DEVICE_ID_AUREAL_W01RN	0x2626
>  
> @@ -1206,6 +1212,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
> diff --git a/drivers/hid/hid-pulsar.c b/drivers/hid/hid-pulsar.c
> new file mode 100644
> index 000000000000..a6ff6e436d40
> --- /dev/null
> +++ b/drivers/hid/hid-pulsar.c
> @@ -0,0 +1,763 @@
> +// 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;
> +	bool refresh;		/* cache invalid, read regardless of age */
> +	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 = battery->refresh ||
> +	    (now - battery->last_status >= MAX_UNAVAIL_AGE_NS);
> +	need_power = battery->available &&
> +	    (battery->refresh ||
> +	     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;
> +			battery->refresh = 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;
> +	battery->refresh = false;
> +
> +	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.refresh = true;
> +	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;
> +	drvdata->battery.refresh = true;
> +
> +	snprintf(drvdata->battery.name, sizeof(drvdata->battery.name),
> +		 "pulsar_%s_battery", dev_name(&usbdev->dev));
> +
> +	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:
> +	hid_hw_stop(hdev);
> +	cancel_work_sync(&drvdata->power_uevent_work);
> +	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);
> +
> +	/*
> +	 * Drain init_work before the device is torn down.
> +	 * It cannot requeue itself because exec_cmd() fails with -ENODEV.
> +	 */
> +	cancel_delayed_work_sync(&drvdata->init_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);
> +	cancel_work_sync(&drvdata->power_uevent_work);
> +}
> +
> +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_WIRED),
> +	  .driver_data = TYPE_ATK },
> +	{ HID_USB_DEVICE(USB_VENDOR_ID_ATK_ALT, USB_DEVICE_ID_ATK_VXE_R1_SE_DONGLE),
> +	  .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);
> 
> base-commit: db2ddb87143519e20a95aa36c60b36107b736a58
> -- 
> 2.55.0
> 
> 

  reply	other threads:[~2026-08-25 10:12 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-16 10:54 [PATCH v6] HID: pulsar: add driver for Pulsar gaming mice Nikolas Koesling
2026-08-25 10:12 ` Benjamin Tissoires [this message]
2026-08-25 16:16   ` Nikolas Koesling

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=ao1pMvF1_yXKX4D_@beelink \
    --to=bentiss@kernel.org \
    --cc=jikos@kernel.org \
    --cc=leo@managarm.org \
    --cc=linux-input@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=me@lodewillems.com \
    --cc=nikolas@koesling.info \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox