blob: b0b34849f2b3784d7b40824b533deeb1a3a34fed [file]
// 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);