// SPDX-License-Identifier: GPL-2.0-or-later /* * HID driver for Corsair Void headsets * * Copyright (C) 2023-2024 Stuart Hayhurst */ /* -------------------------------------------------------------------------- */ /* Receiver report information: (ID 100) */ /* -------------------------------------------------------------------------- */ /* * When queried, the receiver reponds with 5 bytes to describe the battery * The power button, mute button and moving the mic also trigger this report * This includes power button + mic + connection + battery status and capacity * The information below may not be perfect, it's been gathered through guesses * * 0: REPORT ID * 100 for the battery packet * * 1: POWER BUTTON + (?) * Largest bit is 1 when power button pressed * * 2: BATTERY CAPACITY + MIC STATUS * Battery capacity: * Seems to report ~54 higher than reality when charging * Capped at 100, charging or not * Microphone status: * Largest bit is set to 1 when the mic is physically up * No bits change when the mic is muted, only when physically moved * This report is sent every time the mic is moved, no polling required * * 3: CONNECTION STATUS * 16: Wired headset * 38: Initialising * 49: Lost connection * 51: Disconnected, searching * 52: Disconnected, not searching * 177: Normal * * 4: BATTERY STATUS * 0: Disconnected * 1: Normal * 2: Low * 3: Critical - sent during shutdown * 4: Fully charged * 5: Charging */ /* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */ /* Receiver report information: (ID 102) */ /* -------------------------------------------------------------------------- */ /* * When queried, the recevier responds with 4 bytes to describe the firmware * The first 2 bytes are for the receiver, the second 2 are the headset * The headset firmware version will be 0 if no headset is connected * * 0: Recevier firmware major version * Major version of the receiver's firmware * * 1: Recevier firmware minor version * Minor version of the receiver's firmware * * 2: Headset firmware major version * Major version of the headset's firmware * * 3: Headset firmware minor version * Minor version of the headset's firmware */ /* -------------------------------------------------------------------------- */ #include #include #include #include #include #include #include #include #include #include #include #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, }; 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 mutex battery_mutex; struct delayed_work delayed_status_work; struct delayed_work delayed_firmware_work; struct work_struct battery_remove_work; struct work_struct battery_add_work; }; /* * Functions to process receiver data */ 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; } /* Reset data that may change between wireless connections */ static void corsair_void_set_unknown_wireless_data(struct corsair_void_drvdata *drvdata) { /* Only 0 out headset, receiver is always known if relevant */ 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; /* Save initial battery data, to compare later */ orig_battery_data = *battery_data; /* Headset not connected, or it's wired */ if (raw_connection_status != CORSAIR_VOID_WIRELESS_CONNECTED) goto unknown_battery; /* Battery information unavailable */ if (raw_battery_status == 0) goto unknown_battery; /* Battery must be connected then */ battery_data->present = true; battery_data->capacity_level = POWER_SUPPLY_CAPACITY_LEVEL_NORMAL; /* Set battery status */ 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: /* Inform power supply if battery values changed */ if (memcmp(&orig_battery_data, battery_data, sizeof(*battery_data))) { scoped_guard(mutex, &drvdata->battery_mutex) { if (drvdata->battery) { power_supply_changed(drvdata->battery); } } } } /* * Functions to report stored data */ 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); } /* * Functions to send data to headset */ 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; /* Only accept 0 or 1 for alert ID */ if (kstrtou8(buf, 10, &alert_id) || alert_id >= 2) return -EINVAL; send_buf = kmalloc(3, GFP_KERNEL); if (!send_buf) return -ENOMEM; /* Packet format to send alert with ID alert_id */ 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); /* Packet format to set sidetone for wired headsets */ __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; /* Packet format to set sidetone for wireless headsets */ 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; /* sidetone must be between 0 and drvdata->sidetone_max inclusive */ 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; /* Packet format to request data item (status / firmware) refresh */ send_buf[0] = CORSAIR_VOID_STATUS_REQUEST_ID; send_buf[1] = id; /* Send request for data refresh */ return hid_hw_raw_request(hid_dev, CORSAIR_VOID_STATUS_REQUEST_ID, send_buf, 2, HID_OUTPUT_REPORT, HID_REQ_SET_REPORT); } /* * Headset connect / disconnect handlers and work handlers */ 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 = container_of(work, struct 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 = container_of(work, struct 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_battery_remove_work_handler(struct work_struct *work) { struct corsair_void_drvdata *drvdata; drvdata = container_of(work, struct corsair_void_drvdata, battery_remove_work); scoped_guard(mutex, &drvdata->battery_mutex) { if (drvdata->battery) { power_supply_unregister(drvdata->battery); drvdata->battery = NULL; } } } static void corsair_void_battery_add_work_handler(struct work_struct *work) { struct corsair_void_drvdata *drvdata; struct power_supply_config psy_cfg; struct power_supply *new_supply; drvdata = container_of(work, struct corsair_void_drvdata, battery_add_work); guard(mutex)(&drvdata->battery_mutex); 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_headset_connected(struct corsair_void_drvdata *drvdata) { schedule_work(&drvdata->battery_add_work); schedule_delayed_work(&drvdata->delayed_firmware_work, msecs_to_jiffies(100)); } static void corsair_void_headset_disconnected(struct corsair_void_drvdata *drvdata) { schedule_work(&drvdata->battery_remove_work); corsair_void_set_unknown_wireless_data(drvdata); corsair_void_set_unknown_batt(drvdata); } /* * Driver setup, probing and HID event handling */ 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; /* Set initial values for no wireless headset attached */ /* If a headset is attached, it'll be prompted later */ corsair_void_set_unknown_wireless_data(drvdata); corsair_void_set_unknown_batt(drvdata); /* Receiver version won't be reset after init */ /* Headset version already set via set_unknown_wireless_data */ 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_remove_work, corsair_void_battery_remove_work_handler); INIT_WORK(&drvdata->battery_add_work, corsair_void_battery_add_work_handler); ret = devm_mutex_init(drvdata->dev, &drvdata->battery_mutex); if (ret) return ret; ret = sysfs_create_group(&hid_dev->dev.kobj, &corsair_void_attr_group); if (ret) return ret; /* Any failures after here will need to call hid_hw_stop */ 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; } /* Refresh battery data, in case wireless headset is already connected */ INIT_DELAYED_WORK(&drvdata->delayed_status_work, corsair_void_status_work_handler); schedule_delayed_work(&drvdata->delayed_status_work, msecs_to_jiffies(100)); /* Refresh firmware versions */ 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_remove_work); cancel_work_sync(&drvdata->battery_add_work); if (drvdata->battery) power_supply_unregister(drvdata->battery); 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; /* Description of packets are documented at the top of this file */ 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]; } /* Handle wireless headset connect / disconnect */ 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 */ 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 USB */ CORSAIR_VOID_WIRED_DEVICE(0x0a0f), CORSAIR_VOID_WIRED_DEVICE(0x1b1c), CORSAIR_VOID_WIRED_DEVICE(0x1b29), CORSAIR_VOID_WIRED_DEVICE(0x1b2a), /* Corsair Void Surround */ CORSAIR_VOID_WIRED_DEVICE(0x0a30), CORSAIR_VOID_WIRED_DEVICE(0x0a31), /* Corsair Void Pro Wireless */ CORSAIR_VOID_WIRELESS_DEVICE(0x0a14), CORSAIR_VOID_WIRELESS_DEVICE(0x0a16), CORSAIR_VOID_WIRELESS_DEVICE(0x0a1a), /* Corsair Void Pro USB */ CORSAIR_VOID_WIRED_DEVICE(0x0a17), CORSAIR_VOID_WIRED_DEVICE(0x0a1d), /* Corsair Void Pro Surround */ CORSAIR_VOID_WIRED_DEVICE(0x0a18), CORSAIR_VOID_WIRED_DEVICE(0x0a1e), CORSAIR_VOID_WIRED_DEVICE(0x0a1f), /* Corsair Void Elite Wireless */ CORSAIR_VOID_WIRELESS_DEVICE(0x0a51), CORSAIR_VOID_WIRELESS_DEVICE(0x0a55), CORSAIR_VOID_WIRELESS_DEVICE(0x0a75), /* Corsair Void Elite USB */ CORSAIR_VOID_WIRED_DEVICE(0x0a52), CORSAIR_VOID_WIRED_DEVICE(0x0a56), /* Corsair Void Elite Surround */ 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 "); MODULE_DESCRIPTION("HID driver for Corsair Void headsets");