Merge branch 'for-7.4/pulsar' into for-linus

This commit is contained in:
Jiri Kosina 2026-10-21 21:38:35 +02:00
commit 1c32a54daf
6 changed files with 797 additions and 303 deletions

View File

@ -11639,6 +11639,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>

View File

@ -527,15 +527,6 @@ config HID_KYE
- MousePen i608X tablet
- EasyPen M610X tablet
config HID_KYSONA
tristate "Kysona devices"
depends on USB_HID
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.
config HID_UCLOGIC
tristate "UC-Logic"
depends on USB_HID
@ -1359,6 +1350,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

View File

@ -74,7 +74,6 @@ obj-$(CONFIG_HID_JABRA) += hid-jabra.o
obj-$(CONFIG_HID_KENSINGTON) += hid-kensington.o
obj-$(CONFIG_HID_KEYTOUCH) += hid-keytouch.o
obj-$(CONFIG_HID_KYE) += hid-kye.o
obj-$(CONFIG_HID_KYSONA) += hid-kysona.o
obj-$(CONFIG_HID_LCPOWER) += hid-lcpower.o
obj-$(CONFIG_HID_LENOVO) += hid-lenovo.o
obj-$(CONFIG_HID_LENOVO_GO) += hid-lenovo-go.o
@ -117,6 +116,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

View File

@ -253,6 +253,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
@ -1224,6 +1230,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

View File

@ -1,293 +0,0 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* USB HID driver for Kysona
* Kysona M600 mice.
*
* Copyright (c) 2024 Lode Willems <me@lodewillems.com>
*/
#include <linux/device.h>
#include <linux/hid.h>
#include <linux/usb.h>
#include "hid-ids.h"
#define BATTERY_TIMEOUT_MS 5000
#define ONLINE_REPORT_ID 3
#define BATTERY_REPORT_ID 4
struct kysona_drvdata {
struct hid_device *hdev;
bool online;
struct power_supply_desc battery_desc;
struct power_supply *battery;
u8 battery_capacity;
bool battery_charging;
u16 battery_voltage;
struct delayed_work battery_work;
};
static enum power_supply_property kysona_battery_props[] = {
POWER_SUPPLY_PROP_STATUS,
POWER_SUPPLY_PROP_PRESENT,
POWER_SUPPLY_PROP_CAPACITY,
POWER_SUPPLY_PROP_SCOPE,
POWER_SUPPLY_PROP_MODEL_NAME,
POWER_SUPPLY_PROP_VOLTAGE_NOW,
POWER_SUPPLY_PROP_ONLINE
};
static int kysona_battery_get_property(struct power_supply *psy,
enum power_supply_property psp,
union power_supply_propval *val)
{
struct kysona_drvdata *drv_data = power_supply_get_drvdata(psy);
int ret = 0;
switch (psp) {
case POWER_SUPPLY_PROP_PRESENT:
val->intval = 1;
break;
case POWER_SUPPLY_PROP_ONLINE:
val->intval = drv_data->online;
break;
case POWER_SUPPLY_PROP_STATUS:
if (drv_data->online)
val->intval = drv_data->battery_charging ?
POWER_SUPPLY_STATUS_CHARGING :
POWER_SUPPLY_STATUS_DISCHARGING;
else
val->intval = POWER_SUPPLY_STATUS_UNKNOWN;
break;
case POWER_SUPPLY_PROP_SCOPE:
val->intval = POWER_SUPPLY_SCOPE_DEVICE;
break;
case POWER_SUPPLY_PROP_CAPACITY:
val->intval = drv_data->battery_capacity;
break;
case POWER_SUPPLY_PROP_VOLTAGE_NOW:
/* hardware reports voltage in mV. sysfs expects uV */
val->intval = drv_data->battery_voltage * 1000;
break;
case POWER_SUPPLY_PROP_MODEL_NAME:
val->strval = drv_data->hdev->name;
break;
default:
ret = -EINVAL;
break;
}
return ret;
}
static const char kysona_online_request[] = {
0x08, ONLINE_REPORT_ID, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4a
};
static const char kysona_battery_request[] = {
0x08, BATTERY_REPORT_ID, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49
};
static int kysona_m600_fetch_online(struct hid_device *hdev)
{
u8 *write_buf;
int ret;
/* Request online information */
write_buf = kmemdup(kysona_online_request, sizeof(kysona_online_request), GFP_KERNEL);
if (!write_buf)
return -ENOMEM;
ret = hid_hw_raw_request(hdev, kysona_online_request[0],
write_buf, sizeof(kysona_online_request),
HID_OUTPUT_REPORT, HID_REQ_SET_REPORT);
if (ret < (int)sizeof(kysona_online_request)) {
hid_err(hdev, "hid_hw_raw_request() failed with %d\n", ret);
ret = -ENODATA;
}
kfree(write_buf);
return ret;
}
static void kysona_fetch_online(struct hid_device *hdev)
{
int ret = kysona_m600_fetch_online(hdev);
if (ret < 0)
hid_dbg(hdev,
"Online query failed (err: %d)\n", ret);
}
static int kysona_m600_fetch_battery(struct hid_device *hdev)
{
u8 *write_buf;
int ret;
/* Request battery information */
write_buf = kmemdup(kysona_battery_request, sizeof(kysona_battery_request), GFP_KERNEL);
if (!write_buf)
return -ENOMEM;
ret = hid_hw_raw_request(hdev, kysona_battery_request[0],
write_buf, sizeof(kysona_battery_request),
HID_OUTPUT_REPORT, HID_REQ_SET_REPORT);
if (ret < (int)sizeof(kysona_battery_request)) {
hid_err(hdev, "hid_hw_raw_request() failed with %d\n", ret);
ret = -ENODATA;
}
kfree(write_buf);
return ret;
}
static void kysona_fetch_battery(struct hid_device *hdev)
{
int ret = kysona_m600_fetch_battery(hdev);
if (ret < 0)
hid_dbg(hdev,
"Battery query failed (err: %d)\n", ret);
}
static void kysona_battery_timer_tick(struct work_struct *work)
{
struct kysona_drvdata *drv_data = container_of(work,
struct kysona_drvdata, battery_work.work);
struct hid_device *hdev = drv_data->hdev;
kysona_fetch_online(hdev);
kysona_fetch_battery(hdev);
schedule_delayed_work(&drv_data->battery_work,
msecs_to_jiffies(BATTERY_TIMEOUT_MS));
}
static int kysona_battery_probe(struct hid_device *hdev)
{
struct kysona_drvdata *drv_data = hid_get_drvdata(hdev);
struct power_supply_config pscfg = { .drv_data = drv_data };
int ret = 0;
drv_data->online = false;
drv_data->battery_capacity = 100;
drv_data->battery_voltage = 4200;
drv_data->battery_desc.properties = kysona_battery_props;
drv_data->battery_desc.num_properties = ARRAY_SIZE(kysona_battery_props);
drv_data->battery_desc.get_property = kysona_battery_get_property;
drv_data->battery_desc.type = POWER_SUPPLY_TYPE_BATTERY;
drv_data->battery_desc.use_for_apm = 0;
drv_data->battery_desc.name = devm_kasprintf(&hdev->dev, GFP_KERNEL,
"kysona-%s-battery",
strlen(hdev->uniq) ?
hdev->uniq : dev_name(&hdev->dev));
if (!drv_data->battery_desc.name)
return -ENOMEM;
drv_data->battery = devm_power_supply_register(&hdev->dev,
&drv_data->battery_desc, &pscfg);
if (IS_ERR(drv_data->battery)) {
ret = PTR_ERR(drv_data->battery);
drv_data->battery = NULL;
hid_err(hdev, "Unable to register battery device\n");
return ret;
}
power_supply_powers(drv_data->battery, &hdev->dev);
INIT_DELAYED_WORK(&drv_data->battery_work, kysona_battery_timer_tick);
kysona_fetch_online(hdev);
kysona_fetch_battery(hdev);
schedule_delayed_work(&drv_data->battery_work,
msecs_to_jiffies(BATTERY_TIMEOUT_MS));
return ret;
}
static int kysona_probe(struct hid_device *hdev, const struct hid_device_id *id)
{
int ret;
struct kysona_drvdata *drv_data;
struct usb_interface *usbif;
if (!hid_is_usb(hdev))
return -EINVAL;
usbif = to_usb_interface(hdev->dev.parent);
drv_data = devm_kzalloc(&hdev->dev, sizeof(*drv_data), GFP_KERNEL);
if (!drv_data)
return -ENOMEM;
hid_set_drvdata(hdev, drv_data);
drv_data->hdev = hdev;
ret = hid_parse(hdev);
if (ret)
return ret;
ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
if (ret)
return ret;
if (usbif->cur_altsetting->desc.bInterfaceNumber == 1) {
if (kysona_battery_probe(hdev) < 0)
hid_err(hdev, "Kysona hid battery_probe failed: %d\n", ret);
}
return 0;
}
static int kysona_raw_event(struct hid_device *hdev,
struct hid_report *report, u8 *data, int size)
{
struct kysona_drvdata *drv_data = hid_get_drvdata(hdev);
if (size == sizeof(kysona_online_request) &&
data[0] == 8 && data[1] == ONLINE_REPORT_ID) {
drv_data->online = data[6];
}
if (size == sizeof(kysona_battery_request) &&
data[0] == 8 && data[1] == BATTERY_REPORT_ID) {
drv_data->battery_capacity = data[6];
drv_data->battery_charging = data[7];
drv_data->battery_voltage = (data[8] << 8) | data[9];
}
return 0;
}
static void kysona_remove(struct hid_device *hdev)
{
struct kysona_drvdata *drv_data = hid_get_drvdata(hdev);
if (drv_data->battery)
cancel_delayed_work_sync(&drv_data->battery_work);
hid_hw_stop(hdev);
}
static const struct hid_device_id kysona_devices[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_KYSONA, USB_DEVICE_ID_KYSONA_M600_DONGLE) },
{ HID_USB_DEVICE(USB_VENDOR_ID_KYSONA, USB_DEVICE_ID_KYSONA_M600_WIRED) },
{ HID_USB_DEVICE(USB_VENDOR_ID_KYSONA, USB_DEVICE_ID_KYSONA_M600V2_DONGLE) },
{ HID_USB_DEVICE(USB_VENDOR_ID_VXE, USB_DEVICE_ID_VXE_DRAGONFLY_R1_PRO_DONGLE) },
{ HID_USB_DEVICE(USB_VENDOR_ID_VXE, USB_DEVICE_ID_VXE_DRAGONFLY_R1_PRO_WIRED) },
{ }
};
MODULE_DEVICE_TABLE(hid, kysona_devices);
static struct hid_driver kysona_driver = {
.name = "kysona",
.id_table = kysona_devices,
.probe = kysona_probe,
.raw_event = kysona_raw_event,
.remove = kysona_remove
};
module_hid_driver(kysona_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("HID driver for Kysona devices");
MODULE_AUTHOR("Lode Willems <me@lodewillems.com>");

767
drivers/hid/hid-pulsar.c Normal file
View File

@ -0,0 +1,767 @@
// 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) {
const int status = read_status(drvdata);
if (status < 0) {
ret = status;
hid_err(drvdata->hdev,
"%s: failed to read status: %d\n",
__func__, ret);
goto unlock;
}
battery->last_status = now;
if (!status) {
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_KYSONA, USB_DEVICE_ID_KYSONA_M600V2_DONGLE),
.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);