diff mbox series

HID: corsair-void: Add Corsair Void headset family driver

Message ID 20240813153819.840275-3-stuart.a.hayhurst@gmail.com
State Superseded
Headers show
Series HID: corsair-void: Add Corsair Void headset family driver | expand

Commit Message

Stuart Aug. 13, 2024, 3:38 p.m. UTC
Introduce a driver for the Corsair Void family of headsets, supporting:
 - Battery reporting (power_supply)
 - Sidetone setting support
 - Physical microphone location reporting
 - Headset and receiver firmware version reporting
 - Built-in alert triggering
 - USB wireless_status

Currently, the sysfs documentation uses the date I implemented each attribute, but if that's
supposed to be the date it was submitted, I can correct that.

Also, the sysfs attributes currently report -ENODEV in some cases if the headset is wired,
or if the wireless headset is disconnected (such as send_alert).
Should I leave it like this, or deregister the attributes, so those cases are never triggered?

I'm also not sure about the block of comments at the start describing the (guessed) format of the
packets, let me know if this should go somewhere else.

Apologies if I've messed anything up, I've only sent fairly small patches until now :)

Signed-off-by: Stuart Hayhurst <stuart.a.hayhurst@gmail.com>
---
 .../ABI/testing/sysfs-driver-hid-corsair-void |  38 +
 drivers/hid/Kconfig                           |  10 +
 drivers/hid/Makefile                          |   1 +
 drivers/hid/hid-corsair-void.c                | 851 ++++++++++++++++++
 4 files changed, 900 insertions(+)
 create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-corsair-void
 create mode 100644 drivers/hid/hid-corsair-void.c

Comments

kernel test robot Aug. 15, 2024, 4:19 a.m. UTC | #1
Hi Stuart,

kernel test robot noticed the following build warnings:

[auto build test WARNING on hid/for-next]
[also build test WARNING on linus/master v6.11-rc3 next-20240814]
[If your patch is applied to the wrong git tree, kindly drop us a note.
And when submitting patch, we suggest to use '--base' as documented in
https://git-scm.com/docs/git-format-patch#_base_tree_information]

url:    https://github.com/intel-lab-lkp/linux/commits/Stuart-Hayhurst/HID-corsair-void-Add-Corsair-Void-headset-family-driver/20240815-004208
base:   https://git.kernel.org/pub/scm/linux/kernel/git/hid/hid.git for-next
patch link:    https://lore.kernel.org/r/20240813153819.840275-3-stuart.a.hayhurst%40gmail.com
patch subject: [PATCH] HID: corsair-void: Add Corsair Void headset family driver
config: sh-allmodconfig (https://download.01.org/0day-ci/archive/20240815/202408151231.kUWzsw88-lkp@intel.com/config)
compiler: sh4-linux-gcc (GCC) 14.1.0
reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20240815/202408151231.kUWzsw88-lkp@intel.com/reproduce)

If you fix the issue in a separate patch/commit (i.e. not just a new version of
the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <lkp@intel.com>
| Closes: https://lore.kernel.org/oe-kbuild-all/202408151231.kUWzsw88-lkp@intel.com/

All warnings (new ones prefixed by >>):

   drivers/hid/hid-corsair-void.c: In function 'corsair_void_probe':
>> drivers/hid/hid-corsair-void.c:646:36: warning: variable 'psy_cfg' set but not used [-Wunused-but-set-variable]
     646 |         struct power_supply_config psy_cfg;
         |                                    ^~~~~~~


vim +/psy_cfg +646 drivers/hid/hid-corsair-void.c

   640	
   641	static int corsair_void_probe(struct hid_device *hid_dev,
   642				      const struct hid_device_id *hid_id)
   643	{
   644		int ret = 0;
   645		struct corsair_void_drvdata *drvdata;
 > 646		struct power_supply_config psy_cfg;
   647		char *name;
   648		int name_length;
   649	
   650		if (!hid_is_usb(hid_dev))
   651			return -EINVAL;
   652	
   653		drvdata = devm_kzalloc(&hid_dev->dev, sizeof(struct corsair_void_drvdata),
   654				       GFP_KERNEL);
   655		if (!drvdata)
   656			return -ENOMEM;
   657	
   658		hid_set_drvdata(hid_dev, drvdata);
   659		psy_cfg.drv_data = drvdata;
   660		dev_set_drvdata(&hid_dev->dev, drvdata);
   661	
   662		drvdata->dev = &hid_dev->dev;
   663		drvdata->hid_dev = hid_dev;
   664		drvdata->is_wired = hid_id->driver_data == CORSAIR_VOID_WIRED;
   665	
   666		drvdata->sidetone_max = CORSAIR_VOID_SIDETONE_MAX_WIRELESS;
   667		if (drvdata->is_wired)
   668			drvdata->sidetone_max = CORSAIR_VOID_SIDETONE_MAX_WIRED;
   669	
   670		/* Set initial values for no wireless headset attached */
   671		/* If a headset is attached, it'll be prompted later */
   672		corsair_void_set_unknown_wireless_data(drvdata);
   673		corsair_void_set_unknown_batt(drvdata);
   674	
   675		/* Receiver version won't be reset after init */
   676		/* Headset version already set via set_unknown_wireless_data */
   677		drvdata->fw_receiver_major = 0;
   678		drvdata->fw_receiver_minor = 0;
   679	
   680		ret = hid_parse(hid_dev);
   681		if (ret) {
   682			hid_err(hid_dev, "parse failed (reason: %d)\n", ret);
   683			return ret;
   684		}
   685	
   686		name_length = snprintf(NULL, 0, "corsair-void-%d-battery", hid_dev->id);
   687		name = devm_kzalloc(drvdata->dev, name_length + 1, GFP_KERNEL);
   688		if (!name)
   689			return -ENOMEM;
   690		snprintf(name, name_length + 1, "corsair-void-%d-battery", hid_dev->id);
   691	
   692		drvdata->battery_desc.name = name;
   693		drvdata->battery_desc.type = POWER_SUPPLY_TYPE_BATTERY;
   694		drvdata->battery_desc.properties = corsair_void_battery_props;
   695		drvdata->battery_desc.num_properties = ARRAY_SIZE(corsair_void_battery_props);
   696		drvdata->battery_desc.get_property = corsair_void_battery_get_property;
   697	
   698		drvdata->battery = NULL;
   699		INIT_WORK(&drvdata->battery_remove_work,
   700			  corsair_void_battery_remove_work_handler);
   701		INIT_WORK(&drvdata->battery_add_work,
   702			  corsair_void_battery_add_work_handler);
   703		ret = devm_mutex_init(drvdata->dev, &drvdata->battery_mutex);
   704		if (ret)
   705			return ret;
   706	
   707		ret = sysfs_create_group(&hid_dev->dev.kobj, &corsair_void_attr_group);
   708		if (ret)
   709			return ret;
   710	
   711		ret = hid_hw_start(hid_dev, HID_CONNECT_DEFAULT);
   712		if (ret) {
   713			hid_err(hid_dev, "hid_hw_start failed (reason: %d)\n", ret);
   714			goto failed_after_sysfs;
   715		}
   716	
   717		/* Any failures after here should go to failed_after_hid_start */
   718	
   719		/* Refresh battery data, in case wireless headset is already connected */
   720		INIT_DELAYED_WORK(&drvdata->delayed_status_work,
   721				  corsair_void_status_work_handler);
   722		schedule_delayed_work(&drvdata->delayed_status_work,
   723				      msecs_to_jiffies(100));
   724	
   725		/* Refresh firmware versions */
   726		INIT_DELAYED_WORK(&drvdata->delayed_firmware_work,
   727				  corsair_void_firmware_work_handler);
   728		schedule_delayed_work(&drvdata->delayed_firmware_work,
   729				      msecs_to_jiffies(100));
   730	
   731		goto success;
   732
Markus Elfring Aug. 15, 2024, 7:50 a.m. UTC | #2
> Should I leave it like this, or deregister the attributes, so those cases are never triggered?
>
> I'm also not sure about the block of comments at the start describing the (guessed) format of the
> packets, let me know if this should go somewhere else.
>
> Apologies if I've messed anything up, I've only sent fairly small patches until now :)

Such information should not belong to the change description.
It may be specified behind the marker line.
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/process/submitting-patches.rst?h=v6.11-rc3#n711


…
> +++ b/drivers/hid/hid-corsair-void.c
> @@ -0,0 +1,851 @@> +static void corsair_void_process_receiver(struct corsair_void_drvdata *drvdata,
> +					  int raw_battery_capacity,
> +					  int raw_connection_status,
> +					  int raw_battery_status)
> +{> +	/* Set battery status */
> +	switch (raw_battery_status) {
> +	case 1:
> +	case 2:
> +	case 3: /* Battery normal / low / critical */
…

Will any enumeration values become more helpful here?


…
> +static int corsair_void_request_status(struct hid_device *hid_dev, int id)
> +{
> +	unsigned char *send_buf;
> +	int ret;
> +
> +	send_buf = kzalloc(12, GFP_KERNEL);
…

Please improve the size determination (or explanation).


…
> +static int corsair_void_probe(struct hid_device *hid_dev,
> +			      const struct hid_device_id *hid_id)
> +{> +/*failed_after_hid_start:
> +	hid_hw_stop(hid_dev);*/
> +failed_after_sysfs:
…

I guess that you would not like to preserve code which was commented out.

Regards,
Markus
Stuart Aug. 16, 2024, 12:57 a.m. UTC | #3
> Such information should not belong to the change description.
> It may be specified behind the marker line.
> https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/process/submitting-patches.rst?h=v6.11-rc3#n711

I was just beginning to think I actually had the format right, thanks
for the link

> Will any enumeration values become more helpful here?

Yes that's clearer, done locally, it'll be in the v2

> Please improve the size determination (or explanation).

That should be 2 bytes, thanks. It doesn't really need to be kzalloc()
then if the whole thing is set, the same goes for the other calls to
kzalloc().
I'll change those to kmalloc() unless there's a reason not to.

> I guess that you would not like to preserve code which was commented out.

I did mean to leave that there for the v1 only to ask if it was
worthwhile leaving, but I forgot to ask about it.
There's a comment ~16 lines back saying to use that exit point for any
failures after that point. It was a reminder while things were moving
around a lot, but I can drop it now if it's not worth keeping.

Thanks for the review

Stuart
kernel test robot Aug. 16, 2024, 2:49 p.m. UTC | #4
Hi Stuart,

kernel test robot noticed the following build warnings:

[auto build test WARNING on hid/for-next]
[also build test WARNING on linus/master v6.11-rc3 next-20240816]
[If your patch is applied to the wrong git tree, kindly drop us a note.
And when submitting patch, we suggest to use '--base' as documented in
https://git-scm.com/docs/git-format-patch#_base_tree_information]

url:    https://github.com/intel-lab-lkp/linux/commits/Stuart-Hayhurst/HID-corsair-void-Add-Corsair-Void-headset-family-driver/20240815-004208
base:   https://git.kernel.org/pub/scm/linux/kernel/git/hid/hid.git for-next
patch link:    https://lore.kernel.org/r/20240813153819.840275-3-stuart.a.hayhurst%40gmail.com
patch subject: [PATCH] HID: corsair-void: Add Corsair Void headset family driver
config: parisc-randconfig-r131-20240816 (https://download.01.org/0day-ci/archive/20240816/202408162236.nuV8tt6o-lkp@intel.com/config)
compiler: hppa-linux-gcc (GCC) 14.1.0
reproduce: (https://download.01.org/0day-ci/archive/20240816/202408162236.nuV8tt6o-lkp@intel.com/reproduce)

If you fix the issue in a separate patch/commit (i.e. not just a new version of
the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <lkp@intel.com>
| Closes: https://lore.kernel.org/oe-kbuild-all/202408162236.nuV8tt6o-lkp@intel.com/

sparse warnings: (new ones prefixed by >>)
>> drivers/hid/hid-corsair-void.c:405:23: sparse: sparse: incorrect type in assignment (different base types) @@     expected unsigned short [usertype] send_sidetone @@     got restricted __le16 [usertype] @@
   drivers/hid/hid-corsair-void.c:405:23: sparse:     expected unsigned short [usertype] send_sidetone
   drivers/hid/hid-corsair-void.c:405:23: sparse:     got restricted __le16 [usertype]

vim +405 drivers/hid/hid-corsair-void.c

   395	
   396	static int corsair_void_send_sidetone_wired(struct device *dev, const char *buf,
   397						    unsigned int sidetone)
   398	{
   399		struct usb_interface *usb_if = to_usb_interface(dev->parent);
   400		struct usb_device *usb_dev = interface_to_usbdev(usb_if);
   401		u16 send_sidetone;
   402		int ret = 0;
   403	
   404		/* Packet format to set sidetone for wired headsets */
 > 405		send_sidetone = cpu_to_le16(sidetone);
   406		ret = usb_control_msg_send(usb_dev, 0,
   407					   CORSAIR_VOID_USB_SIDETONE_REQUEST,
   408					   CORSAIR_VOID_USB_SIDETONE_REQUEST_TYPE,
   409					   CORSAIR_VOID_USB_SIDETONE_VALUE,
   410					   CORSAIR_VOID_USB_SIDETONE_INDEX,
   411					   &send_sidetone, 2, USB_CTRL_SET_TIMEOUT,
   412					   GFP_KERNEL);
   413	
   414		return ret;
   415	}
   416
Markus Elfring Aug. 18, 2024, 1:12 p.m. UTC | #5
How do you think about to distinguish properties any further for available
device attributes?
https://elixir.bootlin.com/linux/v6.11-rc3/source/Documentation/driver-api/driver-model/device.rst#L38


…
> +++ b/drivers/hid/hid-corsair-void.c
> @@ -0,0 +1,851 @@> +static DEVICE_ATTR(fw_version_receiver, 0444, corsair_void_report_firmware, NULL);
> +static DEVICE_ATTR(fw_version_headset, 0444, corsair_void_report_firmware, NULL);

* Are these really changeable?

* Can the macro “DEVICE_ATTR_RO” be applied?


…
> +MODULE_AUTHOR("Stuart Hayhurst");

Would you like to add an email address here?

Regards,
Markus
Stuart Aug. 18, 2024, 11:17 p.m. UTC | #6
> How do you think about to distinguish properties any further for available
> device attributes?
> https://elixir.bootlin.com/linux/v6.11-rc3/source/Documentation/driver-api/driver-model/device.rst#L38

Sorry I'm not sure I follow you're saying

> * Are these really changeable?

The receiver firmware version wouldn't change during the device's
lifetime unless someone wrote a firmware updater for Linux, even then
it would probably reconnect
The headset firmware version could theoretically change if you changed
which headset is paired, but I don't have 2 headsets of the same model
to test this

> * Can the macro “DEVICE_ATTR_RO” be applied?

Done, also applied DEVICE_ATTR_WO

> > +MODULE_AUTHOR("Stuart Hayhurst");
> Would you like to add an email address here?

Done. Thanks again for the review, I'll submit a v2 with the changes so far.

Stuart

On Sun, Aug 18, 2024 at 2:12 PM Markus Elfring <Markus.Elfring@web.de> wrote:
>
> How do you think about to distinguish properties any further for available
> device attributes?
> https://elixir.bootlin.com/linux/v6.11-rc3/source/Documentation/driver-api/driver-model/device.rst#L38
>
>
> …
> > +++ b/drivers/hid/hid-corsair-void.c
> > @@ -0,0 +1,851 @@
> …
> > +static DEVICE_ATTR(fw_version_receiver, 0444, corsair_void_report_firmware, NULL);
> > +static DEVICE_ATTR(fw_version_headset, 0444, corsair_void_report_firmware, NULL);
>
> * Are these really changeable?
>
> * Can the macro “DEVICE_ATTR_RO” be applied?
>
>
> …
> > +MODULE_AUTHOR("Stuart Hayhurst");
>
> Would you like to add an email address here?
>
> Regards,
> Markus
Markus Elfring Aug. 19, 2024, 5:53 a.m. UTC | #7
>> How do you think about to distinguish properties any further for available
>> device attributes?
>> https://elixir.bootlin.com/linux/v6.11-rc3/source/Documentation/driver-api/driver-model/device.rst#L38
>
> Sorry I'm not sure I follow you're saying

I became curious about system fine-tuning possibilities.


…
>> * Can the macro “DEVICE_ATTR_RO” be applied?
>
> Done, also applied DEVICE_ATTR_WO
…


How will applications of macro variants evolve further?

Regards,
Markus
Stuart Aug. 19, 2024, 5:16 p.m. UTC | #8
> How will applications of macro variants evolve further?

Sorry to get you to clarify again, but I'm not sure what you're asking.

Stuart

On Mon, Aug 19, 2024 at 6:53 AM Markus Elfring <Markus.Elfring@web.de> wrote:
>
> >> How do you think about to distinguish properties any further for available
> >> device attributes?
> >> https://elixir.bootlin.com/linux/v6.11-rc3/source/Documentation/driver-api/driver-model/device.rst#L38
> >
> > Sorry I'm not sure I follow you're saying
>
> I became curious about system fine-tuning possibilities.
>
>
> …
> >> * Can the macro “DEVICE_ATTR_RO” be applied?
> >
> > Done, also applied DEVICE_ATTR_WO
> …
>
>
> How will applications of macro variants evolve further?
>
> Regards,
> Markus
diff mbox series

Patch

diff --git a/Documentation/ABI/testing/sysfs-driver-hid-corsair-void b/Documentation/ABI/testing/sysfs-driver-hid-corsair-void
new file mode 100644
index 000000000000..fc93806aa382
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-driver-hid-corsair-void
@@ -0,0 +1,38 @@ 
+What:		/sys/bus/hid/drivers/hid-corsair-void/<dev>/fw_version_headset
+Date:		January 2024
+KernelVersion:	6.12
+Contact:	Stuart Hayhurst <stuart.a.hayhurst@gmail.com>
+Description:	(R) The firmware version of the headset
+			* Returns -ENODATA if no version was reported
+
+What:		/sys/bus/hid/drivers/hid-corsair-void/<dev>/fw_version_receiver
+Date:		January 2024
+KernelVersion:	6.12
+Contact:	Stuart Hayhurst <stuart.a.hayhurst@gmail.com>
+Description:	(R) The firmware version of the receiver
+
+What:		/sys/bus/hid/drivers/hid-corsair-void/<dev>/microphone_up
+Date:		July 2023
+KernelVersion:	6.12
+Contact:	Stuart Hayhurst <stuart.a.hayhurst@gmail.com>
+Description:	(R) Get the physical position of the microphone
+			* 1 -> Microphone up
+			* 0 -> Microphone down
+
+What:		/sys/bus/hid/drivers/hid-corsair-void/<dev>/send_alert
+Date:		July 2023
+KernelVersion:	6.12
+Contact:	Stuart Hayhurst <stuart.a.hayhurst@gmail.com>
+Description:	(W) Play a built-in notification from the headset (0 / 1)
+
+What:		/sys/bus/hid/drivers/hid-corsair-void/<dev>/set_sidetone
+Date:		December 2023
+KernelVersion:	6.12
+Contact:	Stuart Hayhurst <stuart.a.hayhurst@gmail.com>
+Description:	(W) Set the sidetone volume (0 - sidetone_max)
+
+What:		/sys/bus/hid/drivers/hid-corsair-void/<dev>/sidetone_max
+Date:		July 2024
+KernelVersion:	6.12
+Contact:	Stuart Hayhurst <stuart.a.hayhurst@gmail.com>
+Description:	(R) Report the maximum sidetone volume
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 08446c89eff6..7dcf911f3367 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -221,6 +221,16 @@  config HID_CORSAIR
 	- Vengeance K90
 	- Scimitar PRO RGB
 
+config HID_CORSAIR_VOID
+	tristate "Corsair Void headsets"
+	depends on USB_HID
+	select POWER_SUPPLY
+	help
+	Support for Corsair Void headsets.
+
+	Supported devices:
+	- Corsair Void headsets
+
 config HID_COUGAR
 	tristate "Cougar devices"
 	help
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index e40f1ddebbb7..037d7e3b6c3e 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -39,6 +39,7 @@  obj-$(CONFIG_HID_CHERRY)	+= hid-cherry.o
 obj-$(CONFIG_HID_CHICONY)	+= hid-chicony.o
 obj-$(CONFIG_HID_CMEDIA)	+= hid-cmedia.o
 obj-$(CONFIG_HID_CORSAIR)	+= hid-corsair.o
+obj-$(CONFIG_HID_CORSAIR_VOID)	+= hid-corsair-void.o
 obj-$(CONFIG_HID_COUGAR)	+= hid-cougar.o
 obj-$(CONFIG_HID_CP2112)	+= hid-cp2112.o
 obj-$(CONFIG_HID_CYPRESS)	+= hid-cypress.o
diff --git a/drivers/hid/hid-corsair-void.c b/drivers/hid/hid-corsair-void.c
new file mode 100644
index 000000000000..5628c0ce54d7
--- /dev/null
+++ b/drivers/hid/hid-corsair-void.c
@@ -0,0 +1,851 @@ 
+// 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 <linux/bitfield.h>
+#include <linux/bitops.h>
+#include <linux/cleanup.h>
+#include <linux/hid.h>
+#include <linux/module.h>
+#include <linux/mutex.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,
+};
+
+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;
+	int battery_struct_size = sizeof(struct corsair_void_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 1:
+	case 2:
+	case 3: /* Battery normal / low / critical */
+		battery_data->status = POWER_SUPPLY_STATUS_DISCHARGING;
+		if (raw_battery_status == 2)
+			battery_data->capacity_level = POWER_SUPPLY_CAPACITY_LEVEL_LOW;
+		else if (raw_battery_status == 3)
+			battery_data->capacity_level = POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL;
+
+		break;
+	case 4: /* Battery fully charged */
+		battery_data->status = POWER_SUPPLY_STATUS_FULL;
+		break;
+	case 5: /* 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, battery_struct_size)) {
+		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 corsair_void_report_mic_up(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 corsair_void_report_firmware(struct device *dev,
+					    struct device_attribute *attr,
+					    char *buf)
+{
+	struct corsair_void_drvdata *drvdata = dev_get_drvdata(dev);
+	int major, minor;
+
+	if (!strcmp(attr->attr.name, "fw_version_receiver")) {
+		major = drvdata->fw_receiver_major;
+		minor = drvdata->fw_receiver_minor;
+	} else {
+		major = drvdata->fw_headset_major;
+		minor = drvdata->fw_headset_minor;
+	}
+
+	if (major == 0 && minor == 0)
+		return -ENODATA;
+
+	return sysfs_emit(buf, "%d.%02d\n", major, minor);
+}
+
+static ssize_t corsair_void_report_sidetone_max(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 corsair_void_send_alert(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;
+	int ret;
+
+	if (!drvdata->connected)
+		return -ENODEV;
+
+	if (drvdata->is_wired)
+		return -ENODEV;
+
+	if (kstrtou8(buf, 10, &alert_id))
+		return -EINVAL;
+
+	/* Only accept 0 or 1 for alert ID */
+	if (alert_id >= 2)
+		return -EINVAL;
+
+	send_buf = kzalloc(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;
+	}
+
+	kfree(send_buf);
+	return ret;
+}
+
+static int corsair_void_send_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);
+	u16 send_sidetone;
+	int ret = 0;
+
+	/* Packet format to set sidetone for wired headsets */
+	send_sidetone = cpu_to_le16(sidetone);
+	ret = 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,
+				   &send_sidetone, 2, USB_CTRL_SET_TIMEOUT,
+				   GFP_KERNEL);
+
+	return ret;
+}
+
+static int corsair_void_send_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;
+	int ret = 0;
+
+	send_buf = kzalloc(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;
+
+	ret = hid_hw_raw_request(hid_dev, CORSAIR_VOID_SIDETONE_REQUEST_ID,
+				 send_buf, 12, HID_FEATURE_REPORT,
+				 HID_REQ_SET_REPORT);
+
+	kfree(send_buf);
+	return ret;
+}
+
+static ssize_t corsair_void_send_sidetone(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))
+		return -EINVAL;
+
+	/* sidetone must be between 0 and drvdata->sidetone_max inclusive */
+	if (sidetone > drvdata->sidetone_max)
+		return -EINVAL;
+
+	if (drvdata->is_wired)
+		ret = corsair_void_send_sidetone_wired(dev, buf, sidetone);
+	else
+		ret = corsair_void_send_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;
+	int ret;
+
+	send_buf = kzalloc(12, 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 */
+	ret = hid_hw_raw_request(hid_dev, CORSAIR_VOID_STATUS_REQUEST_ID,
+			  send_buf, 2, HID_OUTPUT_REPORT, HID_REQ_SET_REPORT);
+	if (ret < 0) {
+		switch (id) {
+		case CORSAIR_VOID_STATUS_REPORT_ID:
+			hid_warn(hid_dev, "failed to request battery (reason: %d)",
+				 ret);
+			break;
+		case CORSAIR_VOID_FIRMWARE_REPORT_ID:
+			hid_warn(hid_dev, "failed to request firmware (reason: %d)",
+				 ret);
+			break;
+		default:
+			hid_warn(hid_dev, "failed to send report %d (reason: %d)",
+				 id, ret);
+			break;
+		}
+	} else {
+		ret = 0;
+	}
+
+	kfree(send_buf);
+	return ret;
+}
+
+/*
+ * 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;
+
+	delayed_work = container_of(work, struct delayed_work, work);
+	drvdata = container_of(delayed_work, struct corsair_void_drvdata,
+			       delayed_status_work);
+
+	corsair_void_request_status(drvdata->hid_dev,
+				    CORSAIR_VOID_STATUS_REPORT_ID);
+}
+
+static void corsair_void_firmware_work_handler(struct work_struct *work)
+{
+	struct corsair_void_drvdata *drvdata;
+	struct delayed_work *delayed_work;
+
+	delayed_work = container_of(work, struct delayed_work, work);
+	drvdata = container_of(delayed_work, struct corsair_void_drvdata,
+			       delayed_firmware_work);
+
+	corsair_void_request_status(drvdata->hid_dev,
+				    CORSAIR_VOID_FIRMWARE_REPORT_ID);
+}
+
+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;
+
+	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;
+	drvdata->battery = power_supply_register(drvdata->dev,
+						 &drvdata->battery_desc,
+						 &psy_cfg);
+
+	if (IS_ERR(drvdata->battery)) {
+		hid_err(drvdata->hid_dev,
+			"failed to register battery '%s' (reason: %ld)\n",
+			drvdata->battery_desc.name,
+			PTR_ERR(drvdata->battery));
+		drvdata->battery = NULL;
+		return;
+	}
+
+	if (power_supply_powers(drvdata->battery, drvdata->dev)) {
+		power_supply_unregister(drvdata->battery);
+		drvdata->battery = NULL;
+		return;
+	}
+}
+
+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(fw_version_receiver, 0444, corsair_void_report_firmware, NULL);
+static DEVICE_ATTR(fw_version_headset, 0444, corsair_void_report_firmware, NULL);
+static DEVICE_ATTR(microphone_up, 0444, corsair_void_report_mic_up, NULL);
+static DEVICE_ATTR(sidetone_max, 0444, corsair_void_report_sidetone_max, NULL);
+
+/* Write-only alert, as it only plays a sound (nothing to report back) */
+static DEVICE_ATTR(send_alert, 0200, NULL, corsair_void_send_alert);
+/* Write-only alert, as sidetone volume can't be queried */
+static DEVICE_ATTR(set_sidetone, 0200, NULL, corsair_void_send_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 = 0;
+	struct corsair_void_drvdata *drvdata;
+	struct power_supply_config psy_cfg;
+	char *name;
+	int name_length;
+
+	if (!hid_is_usb(hid_dev))
+		return -EINVAL;
+
+	drvdata = devm_kzalloc(&hid_dev->dev, sizeof(struct corsair_void_drvdata),
+			       GFP_KERNEL);
+	if (!drvdata)
+		return -ENOMEM;
+
+	hid_set_drvdata(hid_dev, drvdata);
+	psy_cfg.drv_data = 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_length = snprintf(NULL, 0, "corsair-void-%d-battery", hid_dev->id);
+	name = devm_kzalloc(drvdata->dev, name_length + 1, GFP_KERNEL);
+	if (!name)
+		return -ENOMEM;
+	snprintf(name, name_length + 1, "corsair-void-%d-battery", hid_dev->id);
+
+	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;
+
+	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;
+	}
+
+	/* Any failures after here should go to failed_after_hid_start */
+
+	/* 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));
+
+	goto success;
+
+/*failed_after_hid_start:
+	hid_hw_stop(hid_dev);*/
+failed_after_sysfs:
+	sysfs_remove_group(&hid_dev->dev.kobj, &corsair_void_attr_group);
+success:
+	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");