| // 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); |