mirror of
https://github.com/torvalds/linux.git
synced 2026-09-22 20:54:03 +02:00
-----BEGIN PGP SIGNATURE----- iQIzBAABCAAdFiEEL65usyKPHcrRDEicpmLzj2vtYEkFAmqyY7EACgkQpmLzj2vt YEnprxAAofmpQUorlic97WALU2CamXN/hdzLGlra2RPW6eJyxQUlwqR8xcUptx3e 14OM5X4WDHfFD1lRFx7/flzn2J5h5CLXUWnNUp5DR7W0GLQSnSwOiuawfb5/Enkd aqhgsqC2nfEjxQ3iqdsY1MTASN/MEjolNU66tP31DQH5D+CGY6eZtivNwFHaHjJI Ps4/vr2nuddSCsXaWRKwdBQ9wMr8g+F7dIeI8vqAJlWg5IdN0tu3ClAuHUqMr6X3 zMHJ7xkkrRg7S40xeQsajDiPGRRWPkC6G3lrJ1OSFOP1IEMKT5dLLg5HB2wLwfzI Xzw4CJZiSIaNKyMQLQJcik8wZZbc60A2ulDscbpk9PLlr9nweBc+7wzVZ0mOQ6Lo rD2iYG7ROG684Jt5HVd9SPrIgjCDnlOXDBT9JLyPCw38eEysEHOdSlwA0CSJyR21 cD0UmfIUgFDXxR/omcPbZDVng8683eRR3W/Tj2XPBOyKZYF/JtnIsrbowIN2dmGe PKnsrtp2LW5/xiO/lTfEsiiHL8WoxtXj/Mwoq/NuZm4TvZ/C3nd3ZiEaL5UQeYYc AyTFOqBe4DoLauyS2M50gitvDpSBFS9i5xrjCukalpJGfTUlX8lQjU7kdH7O5rbh VlMjlYqPNSyd4Yo/nveqIS3ELrKGs5z3oDSxwSB05h1nCgn3dgo= =k0Hr -----END PGP SIGNATURE----- Merge tag 'hid-for-linus-2026092202' of git://git.kernel.org/pub/scm/linux/kernel/git/hid/hid Pull HID fixes from Jiri Kosina: - new device IDs/quirks (Logitech G502X, Elecom M-XT4DRBK, Steelseries Arctis 7, Asus Rog Z13 Folio, Lenovo Yoga Slim Gen 11) - fixes for various code issues found by LLMs * tag 'hid-for-linus-2026092202' of git://git.kernel.org/pub/scm/linux/kernel/git/hid/hid: selftests/hid: add unnumbered variant to the hid_bpf tests HID: bpf: fix __hid_bpf_hw_check_params report length selftests/hid: add define for commonly used buf size HID: amd_sfh: Validate PCI BAR size before mapping HID: elecom: fix bus type for M-XGL20DLBK HID: elecom: Add support for ELECOM M-XT4DRBK (018E) HID: corsair-void: Fix firmware event packet description HID: i2c-hid: Add i2c-hid-quirk-bad-input-size quirk for 0911:5288 device HID: hid-oxp: use cancel_delayed_work_sync() in remove HID: i2c-hid: add reset quirk for Lenovo Yoga Slim 7x Gen 11 keyboard HID: roccat: fix locking in roccat_connect() and roccat_disconnect() HID: steelseries: Add support for Arctis 7 (2018) HID: logitech-hidpp: Add support for G502 X Lightspeed USB mouse HID: fix semantic patch and improve its performance HID: alps: fix use-after-free on input2 registration failure HID: alps: unregister DualPoint Stick input device on remove HID: winwing: fix use-after-free in force feedback teardown HID: multitouch: Add report ID mismatch quirk for ASUS ROG Z13 Folio HID: wacom: fix OOB read in wacom_wac_pen_serial_enforce() HID: quirks: add ALWAYS_POLL quirk for SDINNOVATION gaming keyboard
763 lines
20 KiB
C
763 lines
20 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
/*
|
|
* HID driver for Steelseries arctis headsets
|
|
*
|
|
* Copyright (c) 2023 Bastien Nocera
|
|
* Copyright (c) 2026 Sriman Achanta
|
|
*/
|
|
|
|
#include <linux/device.h>
|
|
#include <linux/hid.h>
|
|
#include <linux/kref.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/module.h>
|
|
#include <linux/power_supply.h>
|
|
#include <linux/spinlock.h>
|
|
#include <linux/usb.h>
|
|
#include <linux/workqueue.h>
|
|
|
|
#include "hid-ids.h"
|
|
|
|
#define SS_CAP_BATTERY BIT(0)
|
|
|
|
struct steelseries_device;
|
|
|
|
struct steelseries_device_info {
|
|
unsigned long capabilities;
|
|
|
|
u8 sync_interface;
|
|
u8 async_interface;
|
|
|
|
int (*request_status)(struct hid_device *hdev);
|
|
void (*parse_status)(struct steelseries_device *sd, u8 *data, int size);
|
|
};
|
|
|
|
struct steelseries_device {
|
|
struct kref refcnt;
|
|
|
|
struct hid_device *hdev;
|
|
const struct steelseries_device_info *info;
|
|
|
|
struct delayed_work status_work;
|
|
|
|
struct power_supply_desc battery_desc;
|
|
struct power_supply *battery;
|
|
bool headset_connected;
|
|
u8 battery_capacity;
|
|
bool battery_charging;
|
|
|
|
spinlock_t lock;
|
|
bool removed;
|
|
};
|
|
|
|
static void steelseries_device_release(struct kref *ref)
|
|
{
|
|
struct steelseries_device *sd =
|
|
container_of(ref, struct steelseries_device, refcnt);
|
|
|
|
kfree(sd);
|
|
}
|
|
|
|
/*
|
|
* Headset report helpers
|
|
*/
|
|
|
|
static int steelseries_send_report(struct hid_device *hdev, const u8 *data,
|
|
int len, enum hid_report_type type)
|
|
{
|
|
u8 *buf;
|
|
int ret;
|
|
|
|
buf = kmemdup(data, len, GFP_KERNEL);
|
|
if (!buf)
|
|
return -ENOMEM;
|
|
|
|
ret = hid_hw_raw_request(hdev, data[0], buf, len, type,
|
|
HID_REQ_SET_REPORT);
|
|
kfree(buf);
|
|
|
|
if (ret < 0)
|
|
return ret;
|
|
if (ret < len)
|
|
return -EIO;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static inline int steelseries_send_output_report(struct hid_device *hdev,
|
|
const u8 *data, int len)
|
|
{
|
|
return steelseries_send_report(hdev, data, len, HID_OUTPUT_REPORT);
|
|
}
|
|
|
|
/*
|
|
* Headset status request functions
|
|
*/
|
|
|
|
static int steelseries_arctis_1_request_status(struct hid_device *hdev)
|
|
{
|
|
const u8 data[] = { 0x06, 0x12 };
|
|
|
|
return steelseries_send_output_report(hdev, data, sizeof(data));
|
|
}
|
|
|
|
static int steelseries_arctis_7_2018_request_status(struct hid_device *hdev)
|
|
{
|
|
const u8 connection[] = { 0x06, 0x14 };
|
|
const u8 battery[] = { 0x06, 0x18 };
|
|
int ret;
|
|
|
|
ret = steelseries_send_output_report(hdev, connection, sizeof(connection));
|
|
if (ret)
|
|
return ret;
|
|
|
|
return steelseries_send_output_report(hdev, battery, sizeof(battery));
|
|
}
|
|
|
|
static int steelseries_arctis_9_request_status(struct hid_device *hdev)
|
|
{
|
|
const u8 data[] = { 0x00, 0x20 };
|
|
|
|
return steelseries_send_output_report(hdev, data, sizeof(data));
|
|
}
|
|
|
|
static int steelseries_arctis_nova_request_status(struct hid_device *hdev)
|
|
{
|
|
const u8 data[] = { 0x00, 0xb0 };
|
|
|
|
return steelseries_send_output_report(hdev, data, sizeof(data));
|
|
}
|
|
|
|
/*
|
|
* Headset battery helpers
|
|
*/
|
|
|
|
static int battery_capacity_to_level(int capacity)
|
|
{
|
|
if (capacity >= 50)
|
|
return POWER_SUPPLY_CAPACITY_LEVEL_NORMAL;
|
|
if (capacity >= 20)
|
|
return POWER_SUPPLY_CAPACITY_LEVEL_LOW;
|
|
return POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL;
|
|
}
|
|
|
|
static u8 steelseries_map_capacity(u8 capacity, u8 min_in, u8 max_in)
|
|
{
|
|
if (capacity >= max_in)
|
|
return 100;
|
|
if (capacity <= min_in)
|
|
return 0;
|
|
return (capacity - min_in) * 100 / (max_in - min_in);
|
|
}
|
|
|
|
/*
|
|
* Headset status parse functions
|
|
*/
|
|
|
|
static void steelseries_arctis_1_parse_status(struct steelseries_device *sd,
|
|
u8 *data, int size)
|
|
{
|
|
/* Only the battery status report echoes the request header. */
|
|
if (size < 8 || data[0] != 0x06 || data[1] != 0x12)
|
|
return;
|
|
|
|
sd->headset_connected = (data[2] != 0x01);
|
|
sd->battery_capacity = data[3];
|
|
}
|
|
|
|
static void steelseries_arctis_7_2018_parse_status(struct steelseries_device *sd,
|
|
u8 *data, int size)
|
|
{
|
|
if (size < 3 || data[0] != 0x06)
|
|
return;
|
|
|
|
switch (data[1]) {
|
|
case 0x14:
|
|
/* 0x03 means that the headset is connected to the transmitter. */
|
|
sd->headset_connected = data[2] == 0x03;
|
|
break;
|
|
case 0x18:
|
|
if (!sd->headset_connected)
|
|
break;
|
|
|
|
/* The Arctis 7 sometimes overreports battery. Cap to 100. */
|
|
sd->battery_capacity = steelseries_map_capacity(data[2], 0, 100);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void steelseries_arctis_9_parse_status(struct steelseries_device *sd,
|
|
u8 *data, int size)
|
|
{
|
|
if (size < 5)
|
|
return;
|
|
|
|
if (data[0] == 0xaa && data[1] == 0x01) {
|
|
sd->headset_connected = true;
|
|
sd->battery_charging = (data[4] == 0x01);
|
|
sd->battery_capacity = steelseries_map_capacity(data[3], 0x64, 0x9a);
|
|
} else {
|
|
/* Device off: 0x55 (no status) or 0x03 (stale status). */
|
|
sd->headset_connected = false;
|
|
sd->battery_charging = false;
|
|
}
|
|
}
|
|
|
|
static void steelseries_arctis_nova_parse_status(struct steelseries_device *sd,
|
|
u8 *data, int size)
|
|
{
|
|
if (size < 2)
|
|
return;
|
|
|
|
switch (data[0]) {
|
|
case 0xb0:
|
|
if (size < 4)
|
|
return;
|
|
sd->headset_connected = (data[1] == 0x03);
|
|
sd->battery_capacity = data[2];
|
|
sd->battery_charging = (data[3] == 0x01);
|
|
break;
|
|
case 0xb7:
|
|
sd->battery_capacity = data[1];
|
|
break;
|
|
case 0xb9:
|
|
sd->headset_connected = (data[1] == 0x03);
|
|
break;
|
|
case 0xbb:
|
|
sd->battery_charging = (data[1] == 0x01);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void steelseries_arctis_nova_7_parse_status(struct steelseries_device *sd,
|
|
u8 *data, int size)
|
|
{
|
|
if (size < 2)
|
|
return;
|
|
|
|
switch (data[0]) {
|
|
case 0xb0:
|
|
if (size < 4)
|
|
return;
|
|
sd->headset_connected = (data[1] == 0x03);
|
|
sd->battery_capacity = steelseries_map_capacity(data[2], 0, 4);
|
|
sd->battery_charging = (data[3] == 0x01);
|
|
break;
|
|
case 0xb7:
|
|
sd->battery_capacity = steelseries_map_capacity(data[1], 0, 4);
|
|
break;
|
|
case 0xb9:
|
|
sd->headset_connected = (data[1] == 0x03);
|
|
break;
|
|
case 0xbb:
|
|
sd->battery_charging = (data[1] == 0x01);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Device info definitions
|
|
*/
|
|
|
|
static const struct steelseries_device_info arctis_1_info = {
|
|
.sync_interface = 3,
|
|
.capabilities = SS_CAP_BATTERY,
|
|
.request_status = steelseries_arctis_1_request_status,
|
|
.parse_status = steelseries_arctis_1_parse_status,
|
|
};
|
|
|
|
static const struct steelseries_device_info arctis_7_2018_info = {
|
|
.sync_interface = 5,
|
|
.capabilities = SS_CAP_BATTERY,
|
|
.request_status = steelseries_arctis_7_2018_request_status,
|
|
.parse_status = steelseries_arctis_7_2018_parse_status,
|
|
};
|
|
|
|
static const struct steelseries_device_info arctis_9_info = {
|
|
.sync_interface = 0,
|
|
.capabilities = SS_CAP_BATTERY,
|
|
.request_status = steelseries_arctis_9_request_status,
|
|
.parse_status = steelseries_arctis_9_parse_status,
|
|
};
|
|
|
|
static const struct steelseries_device_info arctis_nova_info = {
|
|
.sync_interface = 3,
|
|
.async_interface = 5,
|
|
.capabilities = SS_CAP_BATTERY,
|
|
.request_status = steelseries_arctis_nova_request_status,
|
|
.parse_status = steelseries_arctis_nova_parse_status,
|
|
};
|
|
|
|
static const struct steelseries_device_info arctis_nova_7_info = {
|
|
.sync_interface = 3,
|
|
.async_interface = 5,
|
|
.capabilities = SS_CAP_BATTERY,
|
|
.request_status = steelseries_arctis_nova_request_status,
|
|
.parse_status = steelseries_arctis_nova_7_parse_status,
|
|
};
|
|
|
|
/*
|
|
* Headset wireless status and battery infrastructure
|
|
*/
|
|
|
|
#define STEELSERIES_HEADSET_STATUS_TIMEOUT_MS 3000
|
|
|
|
static void
|
|
steelseries_headset_set_wireless_status(struct hid_device *hdev,
|
|
bool connected)
|
|
{
|
|
struct usb_interface *intf;
|
|
|
|
if (!hid_is_usb(hdev))
|
|
return;
|
|
|
|
intf = to_usb_interface(hdev->dev.parent);
|
|
usb_set_wireless_status(intf, connected ?
|
|
USB_WIRELESS_STATUS_CONNECTED :
|
|
USB_WIRELESS_STATUS_DISCONNECTED);
|
|
}
|
|
|
|
#define STEELSERIES_PREFIX "SteelSeries "
|
|
|
|
static int steelseries_battery_get_property(struct power_supply *psy,
|
|
enum power_supply_property psp,
|
|
union power_supply_propval *val)
|
|
{
|
|
struct steelseries_device *sd = power_supply_get_drvdata(psy);
|
|
size_t prefix_len;
|
|
int ret = 0;
|
|
|
|
switch (psp) {
|
|
case POWER_SUPPLY_PROP_MODEL_NAME:
|
|
val->strval = sd->hdev->name;
|
|
while ((prefix_len = str_has_prefix(val->strval, STEELSERIES_PREFIX)))
|
|
val->strval += prefix_len;
|
|
break;
|
|
case POWER_SUPPLY_PROP_MANUFACTURER:
|
|
val->strval = "SteelSeries";
|
|
break;
|
|
case POWER_SUPPLY_PROP_PRESENT:
|
|
val->intval = 1;
|
|
break;
|
|
case POWER_SUPPLY_PROP_STATUS:
|
|
if (!sd->headset_connected)
|
|
val->intval = POWER_SUPPLY_STATUS_UNKNOWN;
|
|
else if (sd->battery_charging)
|
|
val->intval = sd->battery_capacity >= 100 ?
|
|
POWER_SUPPLY_STATUS_FULL :
|
|
POWER_SUPPLY_STATUS_CHARGING;
|
|
else
|
|
val->intval = POWER_SUPPLY_STATUS_DISCHARGING;
|
|
break;
|
|
case POWER_SUPPLY_PROP_SCOPE:
|
|
val->intval = POWER_SUPPLY_SCOPE_DEVICE;
|
|
break;
|
|
case POWER_SUPPLY_PROP_CAPACITY:
|
|
val->intval = sd->battery_capacity;
|
|
break;
|
|
case POWER_SUPPLY_PROP_CAPACITY_LEVEL:
|
|
val->intval = battery_capacity_to_level(sd->battery_capacity);
|
|
break;
|
|
default:
|
|
ret = -EINVAL;
|
|
break;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static enum power_supply_property steelseries_battery_props[] = {
|
|
POWER_SUPPLY_PROP_MODEL_NAME,
|
|
POWER_SUPPLY_PROP_MANUFACTURER,
|
|
POWER_SUPPLY_PROP_PRESENT,
|
|
POWER_SUPPLY_PROP_STATUS,
|
|
POWER_SUPPLY_PROP_SCOPE,
|
|
POWER_SUPPLY_PROP_CAPACITY,
|
|
POWER_SUPPLY_PROP_CAPACITY_LEVEL,
|
|
};
|
|
|
|
/*
|
|
* Delayed work handlers for status polling
|
|
*/
|
|
|
|
static void steelseries_status_timer_work_handler(struct work_struct *work)
|
|
{
|
|
struct steelseries_device *sd = container_of(
|
|
work, struct steelseries_device, status_work.work);
|
|
unsigned long flags;
|
|
|
|
sd->info->request_status(sd->hdev);
|
|
|
|
spin_lock_irqsave(&sd->lock, flags);
|
|
/* Async devices push status events themselves; only poll once. */
|
|
if (!sd->removed && !sd->info->async_interface)
|
|
schedule_delayed_work(&sd->status_work,
|
|
msecs_to_jiffies(STEELSERIES_HEADSET_STATUS_TIMEOUT_MS));
|
|
spin_unlock_irqrestore(&sd->lock, flags);
|
|
}
|
|
|
|
static int steelseries_battery_register(struct steelseries_device *sd)
|
|
{
|
|
struct power_supply_config battery_cfg = { .drv_data = sd, };
|
|
struct power_supply *battery;
|
|
int ret;
|
|
|
|
sd->battery_desc.type = POWER_SUPPLY_TYPE_BATTERY;
|
|
sd->battery_desc.properties = steelseries_battery_props;
|
|
sd->battery_desc.num_properties = ARRAY_SIZE(steelseries_battery_props);
|
|
sd->battery_desc.get_property = steelseries_battery_get_property;
|
|
sd->battery_desc.use_for_apm = 0;
|
|
sd->battery_desc.name = devm_kasprintf(&sd->hdev->dev, GFP_KERNEL,
|
|
"steelseries_headset_battery_%s",
|
|
sd->hdev->uniq[0] ? sd->hdev->uniq :
|
|
dev_name(&sd->hdev->dev));
|
|
if (!sd->battery_desc.name)
|
|
return -ENOMEM;
|
|
|
|
/* avoid the warning of 0% battery while waiting for the first info */
|
|
sd->battery_capacity = 100;
|
|
sd->battery_charging = false;
|
|
sd->headset_connected = false;
|
|
steelseries_headset_set_wireless_status(sd->hdev, false);
|
|
|
|
battery = power_supply_register(&sd->hdev->dev,
|
|
&sd->battery_desc, &battery_cfg);
|
|
if (IS_ERR(battery)) {
|
|
ret = PTR_ERR(battery);
|
|
hid_err(sd->hdev,
|
|
"%s:power_supply_register failed with error %d\n",
|
|
__func__, ret);
|
|
return ret;
|
|
}
|
|
power_supply_powers(battery, &sd->hdev->dev);
|
|
|
|
/* Assign on success only, so a concurrent raw_event never sees an ERR_PTR. */
|
|
sd->battery = battery;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static struct hid_driver steelseries_arctis_driver;
|
|
|
|
static struct steelseries_device *
|
|
steelseries_get_sibling_sd(struct hid_device *hdev, int interface_num)
|
|
{
|
|
struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
|
|
struct usb_device *usb_dev = interface_to_usbdev(intf);
|
|
struct usb_interface *sibling_intf;
|
|
struct hid_device *sibling_hdev;
|
|
struct steelseries_device *sd = NULL;
|
|
|
|
sibling_intf = usb_ifnum_to_if(usb_dev, interface_num);
|
|
if (!sibling_intf)
|
|
return NULL;
|
|
|
|
/*
|
|
* usb_get_intfdata() only yields a hid_device when usbhid is bound;
|
|
* gate on the descriptor class so a non-HID sibling (e.g. a crafted
|
|
* device exposing storage or audio here) is never treated as one.
|
|
*/
|
|
if (sibling_intf->cur_altsetting->desc.bInterfaceClass != USB_INTERFACE_CLASS_HID)
|
|
return NULL;
|
|
|
|
/*
|
|
* Take the sibling's device lock across the intfdata read and the
|
|
* kref_get so a concurrent unbind cannot free the hid_device underneath
|
|
* us; usbhid leaves intfdata dangling on disconnect, so dev.driver is
|
|
* the reliable "still bound" test under this lock. Use device_trylock()
|
|
* to stay off the lockdep chain of the interface being probed and let
|
|
* the caller retry via -EPROBE_DEFER if the sibling is momentarily busy.
|
|
*/
|
|
if (!device_trylock(&sibling_intf->dev))
|
|
return NULL;
|
|
if (sibling_intf->dev.driver) {
|
|
sibling_hdev = usb_get_intfdata(sibling_intf);
|
|
if (sibling_hdev &&
|
|
sibling_hdev->driver == &steelseries_arctis_driver) {
|
|
sd = hid_get_drvdata(sibling_hdev);
|
|
if (sd)
|
|
kref_get(&sd->refcnt);
|
|
}
|
|
}
|
|
device_unlock(&sibling_intf->dev);
|
|
|
|
return sd;
|
|
}
|
|
|
|
static int steelseries_arctis_probe(struct hid_device *hdev,
|
|
const struct hid_device_id *id)
|
|
{
|
|
const struct steelseries_device_info *info =
|
|
(const struct steelseries_device_info *)id->driver_data;
|
|
struct steelseries_device *sd;
|
|
struct usb_interface *intf;
|
|
u8 interface_num;
|
|
int ret;
|
|
|
|
if (hid_is_usb(hdev)) {
|
|
intf = to_usb_interface(hdev->dev.parent);
|
|
interface_num = intf->cur_altsetting->desc.bInterfaceNumber;
|
|
} else {
|
|
return -ENODEV;
|
|
}
|
|
|
|
ret = hid_parse(hdev);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/* Let hid-generic handle non-vendor or unknown interfaces */
|
|
if (interface_num != info->sync_interface &&
|
|
(!info->async_interface || interface_num != info->async_interface))
|
|
return hid_hw_start(hdev, HID_CONNECT_DEFAULT);
|
|
|
|
if (interface_num == info->sync_interface) {
|
|
sd = kzalloc_obj(*sd);
|
|
if (!sd)
|
|
return -ENOMEM;
|
|
|
|
kref_init(&sd->refcnt);
|
|
sd->hdev = hdev;
|
|
sd->info = info;
|
|
spin_lock_init(&sd->lock);
|
|
INIT_DELAYED_WORK(&sd->status_work, steelseries_status_timer_work_handler);
|
|
|
|
ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
|
|
if (ret)
|
|
goto err_free;
|
|
|
|
ret = hid_hw_open(hdev);
|
|
if (ret)
|
|
goto err_stop;
|
|
|
|
if (info->capabilities & SS_CAP_BATTERY) {
|
|
ret = steelseries_battery_register(sd);
|
|
if (ret < 0)
|
|
hid_warn(hdev, "Failed to register battery: %d\n", ret);
|
|
}
|
|
|
|
/*
|
|
* Publish drvdata only once fully initialised: the async sibling
|
|
* attaches by reading it, so it must never observe a half-built or
|
|
* failed instance. A failed probe never gets here, so the error
|
|
* path below has nothing to unpublish.
|
|
*/
|
|
hid_set_drvdata(hdev, sd);
|
|
schedule_delayed_work(&sd->status_work, msecs_to_jiffies(100));
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* The async interface shares the steelseries_device created by the
|
|
* sync interface. Defer until the sync interface has probed and
|
|
* published its drvdata.
|
|
*/
|
|
if (info->async_interface && interface_num == info->async_interface) {
|
|
sd = steelseries_get_sibling_sd(hdev, info->sync_interface);
|
|
if (!sd)
|
|
return -EPROBE_DEFER;
|
|
|
|
hid_set_drvdata(hdev, sd);
|
|
|
|
ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
|
|
if (ret) {
|
|
kref_put(&sd->refcnt, steelseries_device_release);
|
|
return ret;
|
|
}
|
|
|
|
ret = hid_hw_open(hdev);
|
|
if (ret) {
|
|
hid_hw_stop(hdev);
|
|
kref_put(&sd->refcnt, steelseries_device_release);
|
|
return ret;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
return -ENODEV;
|
|
|
|
err_stop:
|
|
hid_hw_stop(hdev);
|
|
err_free:
|
|
/* drvdata is unpublished until full success, so no sibling can hold sd. */
|
|
kref_put(&sd->refcnt, steelseries_device_release);
|
|
return ret;
|
|
}
|
|
|
|
static void steelseries_arctis_remove(struct hid_device *hdev)
|
|
{
|
|
struct steelseries_device *sd;
|
|
struct power_supply *battery;
|
|
unsigned long flags;
|
|
struct usb_interface *intf;
|
|
u8 interface_num;
|
|
|
|
if (hid_is_usb(hdev)) {
|
|
intf = to_usb_interface(hdev->dev.parent);
|
|
interface_num = intf->cur_altsetting->desc.bInterfaceNumber;
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
sd = hid_get_drvdata(hdev);
|
|
|
|
if (!sd) {
|
|
hid_hw_stop(hdev);
|
|
return;
|
|
}
|
|
|
|
if (interface_num == sd->info->sync_interface) {
|
|
spin_lock_irqsave(&sd->lock, flags);
|
|
sd->removed = true;
|
|
battery = sd->battery;
|
|
sd->battery = NULL;
|
|
spin_unlock_irqrestore(&sd->lock, flags);
|
|
|
|
cancel_delayed_work_sync(&sd->status_work);
|
|
|
|
if (battery)
|
|
power_supply_unregister(battery);
|
|
}
|
|
|
|
hid_hw_close(hdev);
|
|
hid_hw_stop(hdev);
|
|
|
|
kref_put(&sd->refcnt, steelseries_device_release);
|
|
}
|
|
|
|
static int steelseries_arctis_raw_event(struct hid_device *hdev,
|
|
struct hid_report *report, u8 *data, int size)
|
|
{
|
|
struct steelseries_device *sd = hid_get_drvdata(hdev);
|
|
u8 old_capacity;
|
|
bool old_connected;
|
|
bool old_charging;
|
|
bool is_async_interface;
|
|
unsigned long flags;
|
|
|
|
if (!sd)
|
|
return 0;
|
|
|
|
is_async_interface = (hdev != sd->hdev);
|
|
|
|
spin_lock_irqsave(&sd->lock, flags);
|
|
|
|
if (sd->removed) {
|
|
spin_unlock_irqrestore(&sd->lock, flags);
|
|
return 0;
|
|
}
|
|
|
|
old_capacity = sd->battery_capacity;
|
|
old_connected = sd->headset_connected;
|
|
old_charging = sd->battery_charging;
|
|
|
|
sd->info->parse_status(sd, data, size);
|
|
|
|
if (sd->headset_connected != old_connected) {
|
|
hid_dbg(hdev,
|
|
"Connected status changed from %sconnected to %sconnected\n",
|
|
old_connected ? "" : "not ",
|
|
sd->headset_connected ? "" : "not ");
|
|
|
|
if (sd->headset_connected && !old_connected &&
|
|
sd->info->async_interface && is_async_interface)
|
|
schedule_delayed_work(&sd->status_work, 0);
|
|
|
|
if (sd->battery) {
|
|
steelseries_headset_set_wireless_status(sd->hdev,
|
|
sd->headset_connected);
|
|
power_supply_changed(sd->battery);
|
|
}
|
|
}
|
|
|
|
if (sd->battery_capacity != old_capacity) {
|
|
hid_dbg(hdev, "Battery capacity changed from %d%% to %d%%\n",
|
|
old_capacity, sd->battery_capacity);
|
|
if (sd->battery)
|
|
power_supply_changed(sd->battery);
|
|
}
|
|
|
|
if (sd->battery_charging != old_charging) {
|
|
hid_dbg(hdev,
|
|
"Battery charging status changed from %scharging to %scharging\n",
|
|
old_charging ? "" : "not ",
|
|
sd->battery_charging ? "" : "not ");
|
|
if (sd->battery)
|
|
power_supply_changed(sd->battery);
|
|
}
|
|
|
|
spin_unlock_irqrestore(&sd->lock, flags);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct hid_device_id steelseries_arctis_devices[] = {
|
|
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
|
|
USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X),
|
|
.driver_data = (unsigned long)&arctis_1_info },
|
|
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
|
|
USB_DEVICE_ID_STEELSERIES_ARCTIS_7_2018),
|
|
.driver_data = (unsigned long)&arctis_7_2018_info },
|
|
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
|
|
USB_DEVICE_ID_STEELSERIES_ARCTIS_9),
|
|
.driver_data = (unsigned long)&arctis_9_info },
|
|
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
|
|
USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5_X),
|
|
.driver_data = (unsigned long)&arctis_nova_info },
|
|
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
|
|
USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7),
|
|
.driver_data = (unsigned long)&arctis_nova_7_info },
|
|
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
|
|
USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X),
|
|
.driver_data = (unsigned long)&arctis_nova_7_info },
|
|
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
|
|
USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_2),
|
|
.driver_data = (unsigned long)&arctis_nova_7_info },
|
|
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
|
|
USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO),
|
|
.driver_data = (unsigned long)&arctis_nova_7_info },
|
|
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
|
|
USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_WOW),
|
|
.driver_data = (unsigned long)&arctis_nova_7_info },
|
|
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
|
|
USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_2026),
|
|
.driver_data = (unsigned long)&arctis_nova_info },
|
|
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
|
|
USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_P_2026),
|
|
.driver_data = (unsigned long)&arctis_nova_info },
|
|
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
|
|
USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_2026),
|
|
.driver_data = (unsigned long)&arctis_nova_info },
|
|
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
|
|
USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO_2026),
|
|
.driver_data = (unsigned long)&arctis_nova_info },
|
|
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
|
|
USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_GEN2),
|
|
.driver_data = (unsigned long)&arctis_nova_info },
|
|
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
|
|
USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2),
|
|
.driver_data = (unsigned long)&arctis_nova_info },
|
|
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
|
|
USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2_2),
|
|
.driver_data = (unsigned long)&arctis_nova_info },
|
|
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
|
|
USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2_3),
|
|
.driver_data = (unsigned long)&arctis_nova_info },
|
|
{}
|
|
};
|
|
MODULE_DEVICE_TABLE(hid, steelseries_arctis_devices);
|
|
|
|
static struct hid_driver steelseries_arctis_driver = {
|
|
.name = "hid-steelseries-arctis",
|
|
.id_table = steelseries_arctis_devices,
|
|
.probe = steelseries_arctis_probe,
|
|
.remove = steelseries_arctis_remove,
|
|
.raw_event = steelseries_arctis_raw_event,
|
|
};
|
|
|
|
module_hid_driver(steelseries_arctis_driver);
|
|
MODULE_DESCRIPTION("HID driver for Steelseries arctis headsets");
|
|
MODULE_LICENSE("GPL");
|
|
MODULE_AUTHOR("Christian Mayer <git@mayer-bgk.de>");
|
|
MODULE_AUTHOR("Bastien Nocera <hadess@hadess.net>");
|
|
MODULE_AUTHOR("Sriman Achanta <srimanachanta@gmail.com>");
|