#include <linux/bitfield.h>
#include <linux/bitops.h>
#include <linux/device.h>
#include <linux/hid.h>
#include <linux/module.h>
#include <linux/power_supply.h>
#include <linux/usb.h>
#include <linux/workqueue.h>
#include <asm/byteorder.h>
#include "hid-ids.h"
#define CORSAIR_VOID_DEVICE(id, type) { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, (id)), \
.driver_data = (type) }
#define CORSAIR_VOID_WIRELESS_DEVICE(id) CORSAIR_VOID_DEVICE((id), CORSAIR_VOID_WIRELESS)
#define CORSAIR_VOID_WIRED_DEVICE(id) CORSAIR_VOID_DEVICE((id), CORSAIR_VOID_WIRED)
#define CORSAIR_VOID_STATUS_REQUEST_ID 0xC9
#define CORSAIR_VOID_NOTIF_REQUEST_ID 0xCA
#define CORSAIR_VOID_SIDETONE_REQUEST_ID 0xFF
#define CORSAIR_VOID_STATUS_REPORT_ID 0x64
#define CORSAIR_VOID_FIRMWARE_REPORT_ID 0x66
#define CORSAIR_VOID_USB_SIDETONE_REQUEST 0x1
#define CORSAIR_VOID_USB_SIDETONE_REQUEST_TYPE 0x21
#define CORSAIR_VOID_USB_SIDETONE_VALUE 0x200
#define CORSAIR_VOID_USB_SIDETONE_INDEX 0xB00
#define CORSAIR_VOID_MIC_MASK GENMASK(7, 7)
#define CORSAIR_VOID_CAPACITY_MASK GENMASK(6, 0)
#define CORSAIR_VOID_WIRELESS_CONNECTED 177
#define CORSAIR_VOID_SIDETONE_MAX_WIRELESS 55
#define CORSAIR_VOID_SIDETONE_MAX_WIRED 4096
enum {
CORSAIR_VOID_WIRELESS,
CORSAIR_VOID_WIRED,
};
enum {
CORSAIR_VOID_BATTERY_NORMAL = 1,
CORSAIR_VOID_BATTERY_LOW = 2,
CORSAIR_VOID_BATTERY_CRITICAL = 3,
CORSAIR_VOID_BATTERY_CHARGED = 4,
CORSAIR_VOID_BATTERY_CHARGING = 5,
};
enum {
CORSAIR_VOID_ADD_BATTERY = 0,
CORSAIR_VOID_REMOVE_BATTERY = 1,
CORSAIR_VOID_UPDATE_BATTERY = 2,
};
static enum power_supply_property corsair_void_battery_props[] = {
POWER_SUPPLY_PROP_STATUS,
POWER_SUPPLY_PROP_PRESENT,
POWER_SUPPLY_PROP_CAPACITY,
POWER_SUPPLY_PROP_CAPACITY_LEVEL,
POWER_SUPPLY_PROP_SCOPE,
POWER_SUPPLY_PROP_MODEL_NAME,
POWER_SUPPLY_PROP_MANUFACTURER,
};
struct corsair_void_battery_data {
int status;
bool present;
int capacity;
int capacity_level;
};
struct corsair_void_drvdata {
struct hid_device *hid_dev;
struct device *dev;
char *name;
bool is_wired;
unsigned int sidetone_max;
struct corsair_void_battery_data battery_data;
bool mic_up;
bool connected;
int fw_receiver_major;
int fw_receiver_minor;
int fw_headset_major;
int fw_headset_minor;
struct power_supply *battery;
struct power_supply_desc battery_desc;
struct delayed_work delayed_status_work;
struct delayed_work delayed_firmware_work;
unsigned long battery_work_flags;
struct work_struct battery_work;
};
static void corsair_void_set_wireless_status(struct corsair_void_drvdata *drvdata)
{
struct usb_interface *usb_if = to_usb_interface(drvdata->dev->parent);
if (drvdata->is_wired)
return;
usb_set_wireless_status(usb_if, drvdata->connected ?
USB_WIRELESS_STATUS_CONNECTED :
USB_WIRELESS_STATUS_DISCONNECTED);
}
static void corsair_void_set_unknown_batt(struct corsair_void_drvdata *drvdata)
{
struct corsair_void_battery_data *battery_data = &drvdata->battery_data;
battery_data->status = POWER_SUPPLY_STATUS_UNKNOWN;
battery_data->present = false;
battery_data->capacity = 0;
battery_data->capacity_level = POWER_SUPPLY_CAPACITY_LEVEL_UNKNOWN;
}
static void corsair_void_set_unknown_wireless_data(struct corsair_void_drvdata *drvdata)
{
drvdata->fw_headset_major = 0;
drvdata->fw_headset_minor = 0;
drvdata->connected = false;
drvdata->mic_up = false;
corsair_void_set_wireless_status(drvdata);
}
static void corsair_void_process_receiver(struct corsair_void_drvdata *drvdata,
int raw_battery_capacity,
int raw_connection_status,
int raw_battery_status)
{
struct corsair_void_battery_data *battery_data = &drvdata->battery_data;
struct corsair_void_battery_data orig_battery_data;
orig_battery_data = *battery_data;
if (raw_connection_status != CORSAIR_VOID_WIRELESS_CONNECTED)
goto unknown_battery;
if (raw_battery_status == 0)
goto unknown_battery;
battery_data->present = true;
battery_data->capacity_level = POWER_SUPPLY_CAPACITY_LEVEL_NORMAL;
switch (raw_battery_status) {
case CORSAIR_VOID_BATTERY_NORMAL:
case CORSAIR_VOID_BATTERY_LOW:
case CORSAIR_VOID_BATTERY_CRITICAL:
battery_data->status = POWER_SUPPLY_STATUS_DISCHARGING;
if (raw_battery_status == CORSAIR_VOID_BATTERY_LOW)
battery_data->capacity_level = POWER_SUPPLY_CAPACITY_LEVEL_LOW;
else if (raw_battery_status == CORSAIR_VOID_BATTERY_CRITICAL)
battery_data->capacity_level = POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL;
break;
case CORSAIR_VOID_BATTERY_CHARGED:
battery_data->status = POWER_SUPPLY_STATUS_FULL;
break;
case CORSAIR_VOID_BATTERY_CHARGING:
battery_data->status = POWER_SUPPLY_STATUS_CHARGING;
break;
default:
hid_warn(drvdata->hid_dev, "unknown battery status '%d'",
raw_battery_status);
goto unknown_battery;
break;
}
battery_data->capacity = raw_battery_capacity;
corsair_void_set_wireless_status(drvdata);
goto success;
unknown_battery:
corsair_void_set_unknown_batt(drvdata);
success:
if (memcmp(&orig_battery_data, battery_data, sizeof(*battery_data))) {
set_bit(CORSAIR_VOID_UPDATE_BATTERY,
&drvdata->battery_work_flags);
schedule_work(&drvdata->battery_work);
}
}
static int corsair_void_battery_get_property(struct power_supply *psy,
enum power_supply_property prop,
union power_supply_propval *val)
{
struct corsair_void_drvdata *drvdata = power_supply_get_drvdata(psy);
switch (prop) {
case POWER_SUPPLY_PROP_SCOPE:
val->intval = POWER_SUPPLY_SCOPE_DEVICE;
break;
case POWER_SUPPLY_PROP_MODEL_NAME:
if (!strncmp(drvdata->hid_dev->name, "Corsair ", 8))
val->strval = drvdata->hid_dev->name + 8;
else
val->strval = drvdata->hid_dev->name;
break;
case POWER_SUPPLY_PROP_MANUFACTURER:
val->strval = "Corsair";
break;
case POWER_SUPPLY_PROP_STATUS:
val->intval = drvdata->battery_data.status;
break;
case POWER_SUPPLY_PROP_PRESENT:
val->intval = drvdata->battery_data.present;
break;
case POWER_SUPPLY_PROP_CAPACITY:
val->intval = drvdata->battery_data.capacity;
break;
case POWER_SUPPLY_PROP_CAPACITY_LEVEL:
val->intval = drvdata->battery_data.capacity_level;
break;
default:
return -EINVAL;
}
return 0;
}
static ssize_t microphone_up_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct corsair_void_drvdata *drvdata = dev_get_drvdata(dev);
if (!drvdata->connected)
return -ENODEV;
return sysfs_emit(buf, "%d\n", drvdata->mic_up);
}
static ssize_t fw_version_receiver_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
struct corsair_void_drvdata *drvdata = dev_get_drvdata(dev);
if (drvdata->fw_receiver_major == 0 && drvdata->fw_receiver_minor == 0)
return -ENODATA;
return sysfs_emit(buf, "%d.%02d\n", drvdata->fw_receiver_major,
drvdata->fw_receiver_minor);
}
static ssize_t fw_version_headset_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
struct corsair_void_drvdata *drvdata = dev_get_drvdata(dev);
if (drvdata->fw_headset_major == 0 && drvdata->fw_headset_minor == 0)
return -ENODATA;
return sysfs_emit(buf, "%d.%02d\n", drvdata->fw_headset_major,
drvdata->fw_headset_minor);
}
static ssize_t sidetone_max_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
struct corsair_void_drvdata *drvdata = dev_get_drvdata(dev);
return sysfs_emit(buf, "%d\n", drvdata->sidetone_max);
}
static ssize_t send_alert_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct corsair_void_drvdata *drvdata = dev_get_drvdata(dev);
struct hid_device *hid_dev = drvdata->hid_dev;
unsigned char alert_id;
unsigned char *send_buf __free(kfree) = NULL;
int ret;
if (!drvdata->connected || drvdata->is_wired)
return -ENODEV;
if (kstrtou8(buf, 10, &alert_id) || alert_id >= 2)
return -EINVAL;
send_buf = kmalloc(3, GFP_KERNEL);
if (!send_buf)
return -ENOMEM;
send_buf[0] = CORSAIR_VOID_NOTIF_REQUEST_ID;
send_buf[1] = 0x02;
send_buf[2] = alert_id;
ret = hid_hw_raw_request(hid_dev, CORSAIR_VOID_NOTIF_REQUEST_ID,
send_buf, 3, HID_OUTPUT_REPORT,
HID_REQ_SET_REPORT);
if (ret < 0)
hid_warn(hid_dev, "failed to send alert request (reason: %d)",
ret);
else
ret = count;
return ret;
}
static int corsair_void_set_sidetone_wired(struct device *dev, const char *buf,
unsigned int sidetone)
{
struct usb_interface *usb_if = to_usb_interface(dev->parent);
struct usb_device *usb_dev = interface_to_usbdev(usb_if);
__le16 sidetone_le = cpu_to_le16(sidetone);
return usb_control_msg_send(usb_dev, 0,
CORSAIR_VOID_USB_SIDETONE_REQUEST,
CORSAIR_VOID_USB_SIDETONE_REQUEST_TYPE,
CORSAIR_VOID_USB_SIDETONE_VALUE,
CORSAIR_VOID_USB_SIDETONE_INDEX,
&sidetone_le, 2, USB_CTRL_SET_TIMEOUT,
GFP_KERNEL);
}
static int corsair_void_set_sidetone_wireless(struct device *dev,
const char *buf,
unsigned char sidetone)
{
struct corsair_void_drvdata *drvdata = dev_get_drvdata(dev);
struct hid_device *hid_dev = drvdata->hid_dev;
unsigned char *send_buf __free(kfree) = NULL;
send_buf = kmalloc(12, GFP_KERNEL);
if (!send_buf)
return -ENOMEM;
send_buf[0] = CORSAIR_VOID_SIDETONE_REQUEST_ID;
send_buf[1] = 0x0B;
send_buf[2] = 0x00;
send_buf[3] = 0xFF;
send_buf[4] = 0x04;
send_buf[5] = 0x0E;
send_buf[6] = 0xFF;
send_buf[7] = 0x05;
send_buf[8] = 0x01;
send_buf[9] = 0x04;
send_buf[10] = 0x00;
send_buf[11] = sidetone + 200;
return hid_hw_raw_request(hid_dev, CORSAIR_VOID_SIDETONE_REQUEST_ID,
send_buf, 12, HID_FEATURE_REPORT,
HID_REQ_SET_REPORT);
}
static ssize_t set_sidetone_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct corsair_void_drvdata *drvdata = dev_get_drvdata(dev);
struct hid_device *hid_dev = drvdata->hid_dev;
unsigned int sidetone;
int ret;
if (!drvdata->connected)
return -ENODEV;
if (kstrtouint(buf, 10, &sidetone) || sidetone > drvdata->sidetone_max)
return -EINVAL;
if (drvdata->is_wired)
ret = corsair_void_set_sidetone_wired(dev, buf, sidetone);
else
ret = corsair_void_set_sidetone_wireless(dev, buf, sidetone);
if (ret < 0)
hid_warn(hid_dev, "failed to send sidetone (reason: %d)", ret);
else
ret = count;
return ret;
}
static int corsair_void_request_status(struct hid_device *hid_dev, int id)
{
unsigned char *send_buf __free(kfree) = NULL;
send_buf = kmalloc(2, GFP_KERNEL);
if (!send_buf)
return -ENOMEM;
send_buf[0] = CORSAIR_VOID_STATUS_REQUEST_ID;
send_buf[1] = id;
return hid_hw_raw_request(hid_dev, CORSAIR_VOID_STATUS_REQUEST_ID,
send_buf, 2, HID_OUTPUT_REPORT,
HID_REQ_SET_REPORT);
}
static void corsair_void_status_work_handler(struct work_struct *work)
{
struct corsair_void_drvdata *drvdata;
struct delayed_work *delayed_work;
int battery_ret;
delayed_work = to_delayed_work(work);
drvdata = container_of(delayed_work, struct corsair_void_drvdata,
delayed_status_work);
battery_ret = corsair_void_request_status(drvdata->hid_dev,
CORSAIR_VOID_STATUS_REPORT_ID);
if (battery_ret < 0) {
hid_warn(drvdata->hid_dev,
"failed to request battery (reason: %d)", battery_ret);
}
}
static void corsair_void_firmware_work_handler(struct work_struct *work)
{
struct corsair_void_drvdata *drvdata;
struct delayed_work *delayed_work;
int firmware_ret;
delayed_work = to_delayed_work(work);
drvdata = container_of(delayed_work, struct corsair_void_drvdata,
delayed_firmware_work);
firmware_ret = corsair_void_request_status(drvdata->hid_dev,
CORSAIR_VOID_FIRMWARE_REPORT_ID);
if (firmware_ret < 0) {
hid_warn(drvdata->hid_dev,
"failed to request firmware (reason: %d)", firmware_ret);
}
}
static void corsair_void_add_battery(struct corsair_void_drvdata *drvdata)
{
struct power_supply_config psy_cfg = {};
struct power_supply *new_supply;
if (drvdata->battery)
return;
psy_cfg.drv_data = drvdata;
new_supply = power_supply_register(drvdata->dev,
&drvdata->battery_desc,
&psy_cfg);
if (IS_ERR(new_supply)) {
hid_err(drvdata->hid_dev,
"failed to register battery '%s' (reason: %ld)\n",
drvdata->battery_desc.name,
PTR_ERR(new_supply));
return;
}
if (power_supply_powers(new_supply, drvdata->dev)) {
power_supply_unregister(new_supply);
return;
}
drvdata->battery = new_supply;
}
static void corsair_void_battery_work_handler(struct work_struct *work)
{
struct corsair_void_drvdata *drvdata = container_of(work,
struct corsair_void_drvdata, battery_work);
bool add_battery = test_and_clear_bit(CORSAIR_VOID_ADD_BATTERY,
&drvdata->battery_work_flags);
bool remove_battery = test_and_clear_bit(CORSAIR_VOID_REMOVE_BATTERY,
&drvdata->battery_work_flags);
bool update_battery = test_and_clear_bit(CORSAIR_VOID_UPDATE_BATTERY,
&drvdata->battery_work_flags);
if (add_battery && !remove_battery) {
corsair_void_add_battery(drvdata);
} else if (remove_battery && !add_battery && drvdata->battery) {
power_supply_unregister(drvdata->battery);
drvdata->battery = NULL;
}
if (update_battery && drvdata->battery)
power_supply_changed(drvdata->battery);
}
static void corsair_void_headset_connected(struct corsair_void_drvdata *drvdata)
{
set_bit(CORSAIR_VOID_ADD_BATTERY, &drvdata->battery_work_flags);
schedule_work(&drvdata->battery_work);
schedule_delayed_work(&drvdata->delayed_firmware_work,
msecs_to_jiffies(100));
}
static void corsair_void_headset_disconnected(struct corsair_void_drvdata *drvdata)
{
set_bit(CORSAIR_VOID_REMOVE_BATTERY, &drvdata->battery_work_flags);
schedule_work(&drvdata->battery_work);
corsair_void_set_unknown_wireless_data(drvdata);
corsair_void_set_unknown_batt(drvdata);
}
static DEVICE_ATTR_RO(fw_version_receiver);
static DEVICE_ATTR_RO(fw_version_headset);
static DEVICE_ATTR_RO(microphone_up);
static DEVICE_ATTR_RO(sidetone_max);
static DEVICE_ATTR_WO(send_alert);
static DEVICE_ATTR_WO(set_sidetone);
static struct attribute *corsair_void_attrs[] = {
&dev_attr_fw_version_receiver.attr,
&dev_attr_fw_version_headset.attr,
&dev_attr_microphone_up.attr,
&dev_attr_send_alert.attr,
&dev_attr_set_sidetone.attr,
&dev_attr_sidetone_max.attr,
NULL,
};
static const struct attribute_group corsair_void_attr_group = {
.attrs = corsair_void_attrs,
};
static int corsair_void_probe(struct hid_device *hid_dev,
const struct hid_device_id *hid_id)
{
int ret;
struct corsair_void_drvdata *drvdata;
char *name;
if (!hid_is_usb(hid_dev))
return -EINVAL;
drvdata = devm_kzalloc(&hid_dev->dev, sizeof(*drvdata),
GFP_KERNEL);
if (!drvdata)
return -ENOMEM;
hid_set_drvdata(hid_dev, drvdata);
dev_set_drvdata(&hid_dev->dev, drvdata);
drvdata->dev = &hid_dev->dev;
drvdata->hid_dev = hid_dev;
drvdata->is_wired = hid_id->driver_data == CORSAIR_VOID_WIRED;
drvdata->sidetone_max = CORSAIR_VOID_SIDETONE_MAX_WIRELESS;
if (drvdata->is_wired)
drvdata->sidetone_max = CORSAIR_VOID_SIDETONE_MAX_WIRED;
corsair_void_set_unknown_wireless_data(drvdata);
corsair_void_set_unknown_batt(drvdata);
drvdata->fw_receiver_major = 0;
drvdata->fw_receiver_minor = 0;
ret = hid_parse(hid_dev);
if (ret) {
hid_err(hid_dev, "parse failed (reason: %d)\n", ret);
return ret;
}
name = devm_kasprintf(drvdata->dev, GFP_KERNEL,
"corsair-void-%d-battery", hid_dev->id);
if (!name)
return -ENOMEM;
drvdata->battery_desc.name = name;
drvdata->battery_desc.type = POWER_SUPPLY_TYPE_BATTERY;
drvdata->battery_desc.properties = corsair_void_battery_props;
drvdata->battery_desc.num_properties = ARRAY_SIZE(corsair_void_battery_props);
drvdata->battery_desc.get_property = corsair_void_battery_get_property;
drvdata->battery = NULL;
INIT_WORK(&drvdata->battery_work, corsair_void_battery_work_handler);
ret = sysfs_create_group(&hid_dev->dev.kobj, &corsair_void_attr_group);
if (ret)
return ret;
ret = hid_hw_start(hid_dev, HID_CONNECT_DEFAULT);
if (ret) {
hid_err(hid_dev, "hid_hw_start failed (reason: %d)\n", ret);
goto failed_after_sysfs;
}
INIT_DELAYED_WORK(&drvdata->delayed_status_work,
corsair_void_status_work_handler);
schedule_delayed_work(&drvdata->delayed_status_work,
msecs_to_jiffies(100));
INIT_DELAYED_WORK(&drvdata->delayed_firmware_work,
corsair_void_firmware_work_handler);
schedule_delayed_work(&drvdata->delayed_firmware_work,
msecs_to_jiffies(100));
return 0;
failed_after_sysfs:
sysfs_remove_group(&hid_dev->dev.kobj, &corsair_void_attr_group);
return ret;
}
static void corsair_void_remove(struct hid_device *hid_dev)
{
struct corsair_void_drvdata *drvdata = hid_get_drvdata(hid_dev);
hid_hw_stop(hid_dev);
cancel_work_sync(&drvdata->battery_work);
if (drvdata->battery)
power_supply_unregister(drvdata->battery);
cancel_delayed_work_sync(&drvdata->delayed_status_work);
cancel_delayed_work_sync(&drvdata->delayed_firmware_work);
sysfs_remove_group(&hid_dev->dev.kobj, &corsair_void_attr_group);
}
static int corsair_void_raw_event(struct hid_device *hid_dev,
struct hid_report *hid_report,
u8 *data, int size)
{
struct corsair_void_drvdata *drvdata = hid_get_drvdata(hid_dev);
bool was_connected = drvdata->connected;
if (hid_report->id == CORSAIR_VOID_STATUS_REPORT_ID) {
drvdata->mic_up = FIELD_GET(CORSAIR_VOID_MIC_MASK, data[2]);
drvdata->connected = (data[3] == CORSAIR_VOID_WIRELESS_CONNECTED) ||
drvdata->is_wired;
corsair_void_process_receiver(drvdata,
FIELD_GET(CORSAIR_VOID_CAPACITY_MASK, data[2]),
data[3], data[4]);
} else if (hid_report->id == CORSAIR_VOID_FIRMWARE_REPORT_ID) {
drvdata->fw_receiver_major = data[1];
drvdata->fw_receiver_minor = data[2];
drvdata->fw_headset_major = data[3];
drvdata->fw_headset_minor = data[4];
}
if ((was_connected != drvdata->connected) && !drvdata->is_wired) {
if (drvdata->connected)
corsair_void_headset_connected(drvdata);
else
corsair_void_headset_disconnected(drvdata);
}
return 0;
}
static const struct hid_device_id corsair_void_devices[] = {
CORSAIR_VOID_WIRELESS_DEVICE(0x0a0c),
CORSAIR_VOID_WIRELESS_DEVICE(0x0a2b),
CORSAIR_VOID_WIRELESS_DEVICE(0x1b23),
CORSAIR_VOID_WIRELESS_DEVICE(0x1b25),
CORSAIR_VOID_WIRELESS_DEVICE(0x1b27),
CORSAIR_VOID_WIRED_DEVICE(0x0a0f),
CORSAIR_VOID_WIRED_DEVICE(0x1b1c),
CORSAIR_VOID_WIRED_DEVICE(0x1b29),
CORSAIR_VOID_WIRED_DEVICE(0x1b2a),
CORSAIR_VOID_WIRED_DEVICE(0x0a30),
CORSAIR_VOID_WIRED_DEVICE(0x0a31),
CORSAIR_VOID_WIRELESS_DEVICE(0x0a14),
CORSAIR_VOID_WIRELESS_DEVICE(0x0a16),
CORSAIR_VOID_WIRELESS_DEVICE(0x0a1a),
CORSAIR_VOID_WIRED_DEVICE(0x0a17),
CORSAIR_VOID_WIRED_DEVICE(0x0a1d),
CORSAIR_VOID_WIRED_DEVICE(0x0a18),
CORSAIR_VOID_WIRED_DEVICE(0x0a1e),
CORSAIR_VOID_WIRED_DEVICE(0x0a1f),
CORSAIR_VOID_WIRELESS_DEVICE(0x0a51),
CORSAIR_VOID_WIRELESS_DEVICE(0x0a55),
CORSAIR_VOID_WIRELESS_DEVICE(0x0a75),
CORSAIR_VOID_WIRED_DEVICE(0x0a52),
CORSAIR_VOID_WIRED_DEVICE(0x0a56),
CORSAIR_VOID_WIRED_DEVICE(0x0a53),
CORSAIR_VOID_WIRED_DEVICE(0x0a57),
{}
};
MODULE_DEVICE_TABLE(hid, corsair_void_devices);
static struct hid_driver corsair_void_driver = {
.name = "hid-corsair-void",
.id_table = corsair_void_devices,
.probe = corsair_void_probe,
.remove = corsair_void_remove,
.raw_event = corsair_void_raw_event,
};
module_hid_driver(corsair_void_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Stuart Hayhurst <[email protected]>");
MODULE_DESCRIPTION("HID driver for Corsair Void headsets");