* [PATCH v2 0/6] HID: steam: Add 2026 Steam Controller support
@ 2026-08-06 2:26 Vicki Pfau
2026-08-06 2:26 ` [PATCH v2 1/6] HID: steam: Unregister battery on partial registration error Vicki Pfau
` (5 more replies)
0 siblings, 6 replies; 11+ messages in thread
From: Vicki Pfau @ 2026-08-06 2:26 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau
This is the second half of a series that modernizes the hid-steam
controller. It adds support for the new 2026 Steam Controller, with feature
parity compared to the 2015 model and the Steam Deck drivers, as well as
some smaller cleanup. The biggest cleanup is cleaning up the somewhat lax
locking practices, but a few more minor things exposed to userspace include
logging improvements and zeroing inputs when switching from gamepad mode to
lizard mode.
This fixes up a few things from the last version, including some potential
deadlocks and adding a missing battery unregistration that made it through
the last series.
Vicki Pfau (6):
HID: steam: Unregister battery on partial registration error
HID: steam: Initial 2026 Steam Controller support
HID: steam: Fix wording of connect/disconnect logs
HID: steam: Don't set feature reports when disconnecting
HID: steam: Clean up locking
HID: steam: Zero out inputs when disabling gamepad mode
drivers/hid/hid-ids.h | 4 +
drivers/hid/hid-steam.c | 1008 ++++++++++++++++++++++++++++++++-------
2 files changed, 831 insertions(+), 181 deletions(-)
--
2.54.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH v2 1/6] HID: steam: Unregister battery on partial registration error
2026-08-06 2:26 [PATCH v2 0/6] HID: steam: Add 2026 Steam Controller support Vicki Pfau
@ 2026-08-06 2:26 ` Vicki Pfau
2026-08-06 2:41 ` sashiko-bot
2026-08-06 2:26 ` [PATCH v2 2/6] HID: steam: Initial 2026 Steam Controller support Vicki Pfau
` (4 subsequent siblings)
5 siblings, 1 reply; 11+ messages in thread
From: Vicki Pfau @ 2026-08-06 2:26 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau
In a previous refactor the code for unregistering a battery if later
registration steps failed was accidentally left out. As a result, a
lingering power_supply object could get left over after the steam object
was torn down.
Signed-off-by: Vicki Pfau <vi@endrift.com>
---
drivers/hid/hid-steam.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
index 6199f67f3c4c..967f12c7a599 100644
--- a/drivers/hid/hid-steam.c
+++ b/drivers/hid/hid-steam.c
@@ -1199,6 +1199,7 @@ static int steam_register(struct steam_device *steam)
steam_register_sensors_fail:
steam_input_unregister(steam);
steam_register_input_fail:
+ steam_battery_unregister(steam);
return ret;
}
--
2.54.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v2 2/6] HID: steam: Initial 2026 Steam Controller support
2026-08-06 2:26 [PATCH v2 0/6] HID: steam: Add 2026 Steam Controller support Vicki Pfau
2026-08-06 2:26 ` [PATCH v2 1/6] HID: steam: Unregister battery on partial registration error Vicki Pfau
@ 2026-08-06 2:26 ` Vicki Pfau
2026-08-06 2:49 ` sashiko-bot
2026-08-06 2:26 ` [PATCH v2 3/6] HID: steam: Fix wording of connect/disconnect logs Vicki Pfau
` (3 subsequent siblings)
5 siblings, 1 reply; 11+ messages in thread
From: Vicki Pfau @ 2026-08-06 2:26 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau
This brings support for the 2026 Steam Controller, comparably featureful to
the existing support for the Steam Deck.
Signed-off-by: Vicki Pfau <vi@endrift.com>
---
drivers/hid/hid-ids.h | 4 +
drivers/hid/hid-steam.c | 846 +++++++++++++++++++++++++++++++++-------
2 files changed, 710 insertions(+), 140 deletions(-)
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 1059922baaac..b3559e70eec8 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1374,6 +1374,10 @@
#define USB_DEVICE_ID_STEAM_CONTROLLER 0x1102
#define USB_DEVICE_ID_STEAM_CONTROLLER_WIRELESS 0x1142
#define USB_DEVICE_ID_STEAM_DECK 0x1205
+#define USB_DEVICE_ID_STEAM_CONTROLLER_IBEX 0x1302
+#define USB_DEVICE_ID_STEAM_CONTROLLER_IBEX_BLE 0x1303
+#define USB_DEVICE_ID_STEAM_CONTROLLER_PROTEUS 0x1304
+#define USB_DEVICE_ID_STEAM_CONTROLLER_NEREID 0x1305
#define USB_VENDOR_ID_STEELSERIES 0x1038
#define USB_DEVICE_ID_STEELSERIES_SRWS1 0x1410
diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
index 967f12c7a599..6476fbdc5888 100644
--- a/drivers/hid/hid-steam.c
+++ b/drivers/hid/hid-steam.c
@@ -44,6 +44,7 @@
#include <linux/delay.h>
#include <linux/power_supply.h>
#include <linux/unaligned.h>
+#include <linux/usb.h>
#include "hid-ids.h"
MODULE_DESCRIPTION("HID driver for Valve Steam Controller");
@@ -58,6 +59,8 @@ static LIST_HEAD(steam_devices);
#define STEAM_QUIRK_WIRELESS BIT(0)
#define STEAM_QUIRK_DECK BIT(1)
+#define STEAM_QUIRK_IBEX BIT(2)
+#define STEAM_QUIRK_BLE BIT(3)
/* Touch pads are 40 mm in diameter and 65535 units */
#define STEAM_PAD_RESOLUTION 1638
@@ -310,11 +313,83 @@ enum {
TRACKPAD_GESTURE_KEYBOARD,
};
+/* Report identifiers (Ibex only) */
+enum {
+ /* Feature */
+ REPORT_ID_FEATURES_CONTROLLER = 1,
+ REPORT_ID_FEATURES_DONGLE = 2,
+
+ /* Input */
+ REPORT_ID_INPUT = 0x42,
+ REPORT_ID_BATTERY = 0x43,
+ REPORT_ID_INPUT2 = 0x45,
+ REPORT_ID_WIRELESS_EVENT = 0x79,
+
+ /* Output */
+ REPORT_ID_HAPTIC_RUMBLE = 0x80,
+ REPORT_ID_HAPTIC_PULSE = 0x81,
+ REPORT_ID_HAPTIC_COMMAND = 0x82,
+ REPORT_ID_HAPTIC_LFO_TONE = 0x83,
+ REPORT_ID_HAPTIC_LOG_SWEEP = 0x84,
+ REPORT_ID_HAPTIC_SCRIPT = 0x85,
+};
+
+/* Ibex charge state */
+enum {
+ CHARGE_STATE_DISCHARGING = 1,
+ CHARGE_STATE_CHARGING = 2,
+ CHARGE_STATE_CHARGING_DONE = 4,
+};
+
+/* Ibex wireless events */
+enum {
+ WIRELESS_EVENT_DISCONNECT = 1,
+ WIRELESS_EVENT_CONNECT = 2,
+ WIRELESS_EVENT_PAIR = 3,
+};
+
struct steam_controller_attribute {
unsigned char tag;
__le32 value;
} __packed;
+struct steam_ibex_battery_status {
+ u8 charge_state;
+ u8 battery_level;
+ __le16 battery_voltage;
+ __le16 system_voltage;
+ __le16 input_voltage;
+ __le16 battery_current;
+ __le16 input_current;
+ __le16 temperature;
+};
+
+struct steam_ibex_haptic_rumble {
+ u8 type;
+ __le16 intensity;
+ struct {
+ __le16 speed;
+ u8 gain;
+ } __packed left, right;
+} __packed;
+static_assert(sizeof(struct steam_ibex_haptic_rumble) == 9);
+
+struct steam_ibex_haptic_pulse {
+ u8 side;
+ __le16 on_us;
+ __le16 off_us;
+ __le16 repeat_count;
+} __packed;
+static_assert(sizeof(struct steam_ibex_haptic_pulse) == 7);
+
+struct steam_ibex_output_report {
+ u8 id;
+ union {
+ struct steam_ibex_haptic_rumble rumble;
+ struct steam_ibex_haptic_pulse pulse;
+ };
+} __packed;
+
/* Pad identifiers for the deck */
#define STEAM_PAD_LEFT 0
#define STEAM_PAD_RIGHT 1
@@ -338,7 +413,10 @@ struct steam_device {
struct power_supply_desc battery_desc;
struct power_supply __rcu *battery;
u8 battery_charge;
- u16 voltage;
+ u8 battery_status;
+ u16 battery_temp;
+ u16 battery_voltage;
+ u16 battery_current;
struct delayed_work mode_switch;
bool did_mode_switch;
bool gamepad_mode;
@@ -350,12 +428,14 @@ struct steam_device {
unsigned int sensor_update_rate_us;
};
-static int steam_recv_report(struct steam_device *steam,
- u8 *data, int size)
+static int steam_recv_report_id(struct steam_device *steam,
+ u8 *data, int size, u8 report_id)
{
struct hid_report *r;
u8 *buf;
+ unsigned int retries = 50;
int ret;
+ u32 len;
/*
* All reports start with a two byte header.
@@ -364,13 +444,14 @@ static int steam_recv_report(struct steam_device *steam,
if (size < 2)
return -EINVAL;
- r = steam->hdev->report_enum[HID_FEATURE_REPORT].report_id_hash[0];
+ r = steam->hdev->report_enum[HID_FEATURE_REPORT].report_id_hash[report_id];
if (!r) {
- hid_err(steam->hdev, "No HID_FEATURE_REPORT submitted - nothing to read\n");
+ hid_err(steam->hdev, "No HID_FEATURE_REPORT present for ID %u\n", report_id);
return -EINVAL;
}
- if (hid_report_len(r) < 64)
+ len = hid_report_len(r);
+ if (len < 64)
return -EINVAL;
buf = hid_alloc_report_buf(r, GFP_KERNEL);
@@ -378,22 +459,36 @@ static int steam_recv_report(struct steam_device *steam,
return -ENOMEM;
/*
- * The report ID is always 0, so strip the first byte from the output.
+ * The report ID is consistent, so strip the first byte from the output.
* hid_report_len() is not counting the report ID, so +1 to the length
* or else we get a EOVERFLOW. We are safe from a buffer overflow
* because hid_alloc_report_buf() allocates +7 bytes.
*/
- ret = hid_hw_raw_request(steam->hdev, 0x00,
- buf, hid_report_len(r) + 1,
- HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
+ if (!(steam->quirks & STEAM_QUIRK_IBEX))
+ len += 1;
+
+ /*
+ * Sometimes the wireless controller fails with EPIPE
+ * when sending a feature report.
+ * Doing a HID_REQ_GET_REPORT and waiting for a while
+ * seems to fix that.
+ */
+ do {
+ ret = hid_hw_raw_request(steam->hdev, report_id,
+ buf, len,
+ HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
+ if (ret != -EPIPE)
+ break;
+ msleep(20);
+ } while (--retries);
if (ret > 0) {
/* Remove the report ID from the return buffer */
ret--;
size = min(size, ret);
memcpy(data, buf + 1, size);
}
- kfree(buf);
+ kfree(buf);
if (ret < 0)
hid_err(steam->hdev, "%s: error %d\n", __func__, ret);
else
@@ -413,41 +508,60 @@ static int steam_recv_report(struct steam_device *steam,
return size;
}
-static int steam_send_report(struct steam_device *steam,
- u8 *cmd, int size)
+static int steam_recv_report(struct steam_device *steam,
+ u8 *data, int size)
+{
+ u8 report_id;
+
+ if (steam->quirks & STEAM_QUIRK_IBEX)
+ report_id = REPORT_ID_FEATURES_CONTROLLER;
+ else
+ report_id = 0;
+
+ return steam_recv_report_id(steam, data, size, report_id);
+}
+
+static int steam_send_report_id(struct steam_device *steam,
+ u8 *cmd, int size, u8 report_id)
{
struct hid_report *r;
u8 *buf;
unsigned int retries = 50;
int ret;
+ u32 len;
- r = steam->hdev->report_enum[HID_FEATURE_REPORT].report_id_hash[0];
+ r = steam->hdev->report_enum[HID_FEATURE_REPORT].report_id_hash[report_id];
if (!r) {
- hid_err(steam->hdev, "No HID_FEATURE_REPORT submitted - nothing to read\n");
+ hid_err(steam->hdev, "No HID_FEATURE_REPORT present for ID %u\n", report_id);
return -EINVAL;
}
- if (hid_report_len(r) < 64)
+ len = hid_report_len(r);
+ if (len < 64)
return -EINVAL;
buf = hid_alloc_report_buf(r, GFP_KERNEL);
if (!buf)
return -ENOMEM;
- /* The report ID is always 0 */
+ /* The report ID is always consistent */
+ buf[0] = report_id;
memcpy(buf + 1, cmd, size);
hid_dbg(steam->hdev, "Sending report %*ph\n", size, cmd);
+ if (!(steam->quirks & STEAM_QUIRK_IBEX))
+ len += 1;
+
/*
* Sometimes the wireless controller fails with EPIPE
* when sending a feature report.
- * Doing a HID_REQ_GET_REPORT and waiting for a while
+ * Doing a HID_REQ_SET_REPORT and waiting for a while
* seems to fix that.
*/
do {
- ret = hid_hw_raw_request(steam->hdev, 0,
- buf, max(size, 64) + 1,
+ ret = hid_hw_raw_request(steam->hdev, report_id,
+ buf, max(size + 1, len),
HID_FEATURE_REPORT, HID_REQ_SET_REPORT);
if (ret != -EPIPE)
break;
@@ -461,6 +575,19 @@ static int steam_send_report(struct steam_device *steam,
return ret;
}
+static int steam_send_report(struct steam_device *steam,
+ u8 *cmd, int size)
+{
+ u8 report_id;
+
+ if (steam->quirks & STEAM_QUIRK_IBEX)
+ report_id = REPORT_ID_FEATURES_CONTROLLER;
+ else
+ report_id = 0;
+
+ return steam_send_report_id(steam, cmd, size, report_id);
+}
+
static inline int steam_send_report_byte(struct steam_device *steam, u8 cmd)
{
return steam_send_report(steam, &cmd, 1);
@@ -473,7 +600,6 @@ static int steam_write_settings(struct steam_device *steam,
u8 reg;
u16 val;
u8 cmd[64] = {ID_SET_SETTINGS_VALUES, 0x00};
- int ret;
va_list args;
va_start(args, steam);
@@ -489,16 +615,7 @@ static int steam_write_settings(struct steam_device *steam,
}
va_end(args);
- ret = steam_send_report(steam, cmd, 2 + cmd[1]);
- if (ret < 0)
- return ret;
-
- /*
- * Sometimes a lingering report for this command can
- * get read back instead of the last set report if
- * this isn't explicitly queried
- */
- return steam_recv_report(steam, cmd, 2 + cmd[1]);
+ return steam_send_report(steam, cmd, 2 + cmd[1]);
}
static int steam_exchange_report(struct steam_device *steam, u8 *cmd, int csize,
@@ -591,15 +708,32 @@ static int steam_get_attributes(struct steam_device *steam)
return 0;
}
-/*
- * This command requests the wireless adaptor to post an event
- * with the connection status. Useful if this driver is loaded when
- * the controller is already connected.
- */
-static inline int steam_request_conn_status(struct steam_device *steam)
+static int steam_get_conn_status(struct steam_device *steam)
{
+ int ret = 0;
+ u8 cmd[] = {ID_DONGLE_GET_WIRELESS_STATE};
+ u8 reply[3] = {};
+ u8 report_id;
+
+ if (steam->quirks & STEAM_QUIRK_IBEX)
+ report_id = REPORT_ID_FEATURES_DONGLE;
+ else
+ report_id = 0;
+
guard(mutex)(&steam->report_mutex);
- return steam_send_report_byte(steam, ID_DONGLE_GET_WIRELESS_STATE);
+ ret = steam_send_report_id(steam, cmd, sizeof(cmd), report_id);
+ if (ret < 0)
+ return ret;
+ ret = steam_recv_report_id(steam, reply, sizeof(reply), report_id);
+ if (ret < 0)
+ return ret;
+ if (reply[0] != ID_DONGLE_GET_WIRELESS_STATE || reply[1] < 1) {
+ hid_err(steam->hdev, "%s: invalid reply (%*ph)\n", __func__,
+ (int)sizeof(reply), reply);
+ return -EIO;
+ }
+
+ return reply[2];
}
/*
@@ -612,24 +746,42 @@ static inline int steam_haptic_pulse(struct steam_device *steam, u8 pad,
u16 duration, u16 interval, u16 count, u8 gain)
{
int ret;
- u8 report[10] = {ID_TRIGGER_HAPTIC_PULSE, 8};
/* Left and right are swapped on this report for legacy reasons */
if (pad < STEAM_PAD_BOTH)
pad ^= 1;
- report[2] = pad;
- report[3] = duration & 0xFF;
- report[4] = duration >> 8;
- report[5] = interval & 0xFF;
- report[6] = interval >> 8;
- report[7] = count & 0xFF;
- report[8] = count >> 8;
- report[9] = gain;
+ if (steam->quirks & STEAM_QUIRK_IBEX) {
+ struct steam_ibex_output_report *report =
+ kzalloc(sizeof(struct steam_ibex_output_report), GFP_KERNEL);
+
+ if (!report)
+ return -ENOMEM;
+
+ report->id = REPORT_ID_HAPTIC_PULSE;
+ report->pulse.side = pad;
+ put_unaligned_le16(duration, &report->pulse.on_us);
+ put_unaligned_le16(interval, &report->pulse.off_us);
+ put_unaligned_le16(count, &report->pulse.repeat_count);
+
+ ret = hid_hw_output_report(steam->hdev, (u8 *)report, 8);
+
+ kfree(report);
+ } else {
+ u8 report[10] = {ID_TRIGGER_HAPTIC_PULSE, 8, pad};
+
+ report[3] = duration & 0xFF;
+ report[4] = duration >> 8;
+ report[5] = interval & 0xFF;
+ report[6] = interval >> 8;
+ report[7] = count & 0xFF;
+ report[8] = count >> 8;
+ report[9] = gain;
+
+ guard(mutex)(&steam->report_mutex);
+ ret = steam_send_report(steam, report, 10);
+ }
- mutex_lock(&steam->report_mutex);
- ret = steam_send_report(steam, report, sizeof(report));
- mutex_unlock(&steam->report_mutex);
return ret;
}
@@ -638,20 +790,39 @@ static inline int steam_haptic_rumble(struct steam_device *steam,
u8 left_gain, u8 right_gain)
{
int ret;
- u8 report[11] = {ID_TRIGGER_RUMBLE_CMD, 9};
- report[3] = intensity & 0xFF;
- report[4] = intensity >> 8;
- report[5] = left_speed & 0xFF;
- report[6] = left_speed >> 8;
- report[7] = right_speed & 0xFF;
- report[8] = right_speed >> 8;
- report[9] = left_gain;
- report[10] = right_gain;
+ if (steam->quirks & STEAM_QUIRK_IBEX) {
+ struct steam_ibex_output_report *report =
+ kzalloc(sizeof(struct steam_ibex_output_report), GFP_KERNEL);
- mutex_lock(&steam->report_mutex);
- ret = steam_send_report(steam, report, sizeof(report));
- mutex_unlock(&steam->report_mutex);
+ if (!report)
+ return -ENOMEM;
+
+ report->id = REPORT_ID_HAPTIC_RUMBLE;
+ put_unaligned_le16(intensity, &report->rumble.intensity);
+ put_unaligned_le16(left_speed, &report->rumble.left.speed);
+ report->rumble.left.gain = left_gain;
+ put_unaligned_le16(right_speed, &report->rumble.right.speed);
+ report->rumble.right.gain = right_gain;
+
+ ret = hid_hw_output_report(steam->hdev, (u8 *)report, 10);
+
+ kfree(report);
+ } else {
+ u8 report[11] = {ID_TRIGGER_RUMBLE_CMD, 9};
+
+ report[3] = intensity & 0xFF;
+ report[4] = intensity >> 8;
+ report[5] = left_speed & 0xFF;
+ report[6] = left_speed >> 8;
+ report[7] = right_speed & 0xFF;
+ report[8] = right_speed >> 8;
+ report[9] = left_gain;
+ report[10] = right_gain;
+
+ guard(mutex)(&steam->report_mutex);
+ ret = steam_send_report(steam, report, sizeof(report));
+ }
return ret;
}
@@ -713,20 +884,18 @@ static void steam_set_lizard_mode(struct steam_device *steam, bool enable)
/* disable esc, enter, cursor */
steam_send_report_byte(steam, ID_CLEAR_DIGITAL_MAPPINGS);
- if (steam->quirks & STEAM_QUIRK_DECK) {
+ if (steam->quirks & (STEAM_QUIRK_DECK | STEAM_QUIRK_IBEX))
steam_write_settings(steam,
- SETTING_LEFT_TRACKPAD_MODE, TRACKPAD_NONE, /* disable mouse */
- SETTING_RIGHT_TRACKPAD_MODE, TRACKPAD_NONE, /* disable mouse */
- SETTING_LEFT_TRACKPAD_CLICK_PRESSURE, 0xFFFF, /* disable haptic click */
- SETTING_RIGHT_TRACKPAD_CLICK_PRESSURE, 0xFFFF, /* disable haptic click */
- SETTING_STEAM_WATCHDOG_ENABLE, 0, /* disable watchdog that tests if Steam is active */
+ /* disable lizard mode */
+ SETTING_LIZARD_MODE, 0,
+ /* disable watchdog that tests if Steam is active */
+ SETTING_STEAM_WATCHDOG_ENABLE, 0,
0);
- } else {
+ else
steam_write_settings(steam,
SETTING_LEFT_TRACKPAD_MODE, TRACKPAD_NONE, /* disable mouse */
SETTING_RIGHT_TRACKPAD_MODE, TRACKPAD_NONE, /* disable mouse */
0);
- }
}
mutex_unlock(&steam->report_mutex);
}
@@ -742,7 +911,7 @@ static int steam_input_open(struct input_dev *dev)
* Controller. On the Steam Deck, this is toggled manually by holding
* the options button instead, handled by steam_mode_switch_cb.
*/
- if (!(steam->quirks & STEAM_QUIRK_DECK)) {
+ if (!(steam->quirks & (STEAM_QUIRK_DECK | STEAM_QUIRK_IBEX))) {
spin_lock_irqsave(&steam->lock, flags);
set_lizard_mode = !steam->client_opened && lizard_mode;
spin_unlock_irqrestore(&steam->lock, flags);
@@ -759,7 +928,7 @@ static void steam_input_close(struct input_dev *dev)
unsigned long flags;
bool set_lizard_mode;
- if (!(steam->quirks & STEAM_QUIRK_DECK)) {
+ if (!(steam->quirks & (STEAM_QUIRK_DECK | STEAM_QUIRK_IBEX))) {
spin_lock_irqsave(&steam->lock, flags);
set_lizard_mode = !steam->client_opened && lizard_mode;
spin_unlock_irqrestore(&steam->lock, flags);
@@ -817,25 +986,40 @@ static int steam_battery_get_property(struct power_supply *psy,
{
struct steam_device *steam = power_supply_get_drvdata(psy);
unsigned long flags;
- s16 volts;
+ u16 volts;
+ u16 curr;
+ u16 temp;
u8 batt;
+ u8 status;
int ret = 0;
spin_lock_irqsave(&steam->lock, flags);
- volts = steam->voltage;
+ volts = steam->battery_voltage;
+ curr = steam->battery_current;
batt = steam->battery_charge;
+ temp = steam->battery_temp;
+ status = steam->battery_status;
spin_unlock_irqrestore(&steam->lock, flags);
switch (psp) {
case POWER_SUPPLY_PROP_PRESENT:
val->intval = 1;
break;
+ case POWER_SUPPLY_PROP_STATUS:
+ val->intval = status;
+ break;
case POWER_SUPPLY_PROP_SCOPE:
val->intval = POWER_SUPPLY_SCOPE_DEVICE;
break;
case POWER_SUPPLY_PROP_VOLTAGE_NOW:
val->intval = volts * 1000; /* mV -> uV */
break;
+ case POWER_SUPPLY_PROP_CURRENT_NOW:
+ val->intval = curr * 1000; /* mA -> uA */
+ break;
+ case POWER_SUPPLY_PROP_TEMP:
+ val->intval = temp / 100; /* thousandths °C -> tenths °C */
+ break;
case POWER_SUPPLY_PROP_CAPACITY:
val->intval = batt;
break;
@@ -846,6 +1030,16 @@ static int steam_battery_get_property(struct power_supply *psy,
return ret;
}
+static enum power_supply_property steam_ibex_battery_props[] = {
+ POWER_SUPPLY_PROP_PRESENT,
+ POWER_SUPPLY_PROP_STATUS,
+ POWER_SUPPLY_PROP_SCOPE,
+ POWER_SUPPLY_PROP_VOLTAGE_NOW,
+ POWER_SUPPLY_PROP_CURRENT_NOW,
+ POWER_SUPPLY_PROP_CAPACITY,
+ POWER_SUPPLY_PROP_TEMP,
+};
+
static int steam_battery_register(struct steam_device *steam)
{
struct power_supply *battery;
@@ -854,19 +1048,35 @@ static int steam_battery_register(struct steam_device *steam)
int ret;
steam->battery_desc.type = POWER_SUPPLY_TYPE_BATTERY;
- steam->battery_desc.properties = steam_battery_props;
- steam->battery_desc.num_properties = ARRAY_SIZE(steam_battery_props);
+ if (steam->quirks & STEAM_QUIRK_IBEX) {
+ steam->battery_desc.properties = steam_ibex_battery_props;
+ steam->battery_desc.num_properties = ARRAY_SIZE(steam_ibex_battery_props);
+ /*
+ * Ibex needs a shorter name as it has a temperature and the
+ * thermal zone name length limit is 20 characters. It's more
+ * ambiguous sounding, so let's only use it when needed.
+ */
+ steam->battery_desc.name = devm_kasprintf(&steam->hdev->dev,
+ GFP_KERNEL, "steam-%s",
+ steam->serial_no);
+ } else {
+ steam->battery_desc.properties = steam_battery_props;
+ steam->battery_desc.num_properties = ARRAY_SIZE(steam_battery_props);
+ steam->battery_desc.name = devm_kasprintf(&steam->hdev->dev,
+ GFP_KERNEL, "steam-controller-%s-battery",
+ steam->serial_no);
+ }
steam->battery_desc.get_property = steam_battery_get_property;
- steam->battery_desc.name = devm_kasprintf(&steam->hdev->dev,
- GFP_KERNEL, "steam-controller-%s-battery",
- steam->serial_no);
if (!steam->battery_desc.name)
return -ENOMEM;
/* avoid the warning of 0% battery while waiting for the first info */
spin_lock_irqsave(&steam->lock, flags);
- steam->voltage = 3000;
+ steam->battery_voltage = 3000;
steam->battery_charge = 100;
+ steam->battery_current = 0;
+ steam->battery_temp = 20000;
+ steam->battery_status = POWER_SUPPLY_STATUS_UNKNOWN;
spin_unlock_irqrestore(&steam->lock, flags);
battery = power_supply_register(&steam->hdev->dev,
@@ -938,7 +1148,7 @@ static int steam_input_register(struct steam_device *steam)
input_set_capability(input, EV_KEY, BTN_THUMB2);
input_set_capability(input, EV_KEY, BTN_GRIPL);
input_set_capability(input, EV_KEY, BTN_GRIPR);
- if (steam->quirks & STEAM_QUIRK_DECK) {
+ if (steam->quirks & (STEAM_QUIRK_DECK | STEAM_QUIRK_IBEX)) {
input_set_capability(input, EV_KEY, BTN_BASE);
input_set_capability(input, EV_KEY, BTN_GRIPL2);
input_set_capability(input, EV_KEY, BTN_GRIPR2);
@@ -952,7 +1162,7 @@ static int steam_input_register(struct steam_device *steam)
input_set_abs_params(input, ABS_HAT0Y, -32767, 32767,
STEAM_PAD_FUZZ, 0);
- if (steam->quirks & STEAM_QUIRK_DECK) {
+ if (steam->quirks & (STEAM_QUIRK_DECK | STEAM_QUIRK_IBEX)) {
input_set_abs_params(input, ABS_HAT2Y, 0, 32767, 0, 0);
input_set_abs_params(input, ABS_HAT2X, 0, 32767, 0, 0);
@@ -992,7 +1202,7 @@ static int steam_input_register(struct steam_device *steam)
input_abs_set_res(input, ABS_HAT0Y, STEAM_PAD_RESOLUTION);
#ifdef CONFIG_STEAM_FF
- if (steam->quirks & STEAM_QUIRK_DECK) {
+ if (steam->quirks & (STEAM_QUIRK_DECK | STEAM_QUIRK_IBEX)) {
input_set_capability(input, EV_FF, FF_RUMBLE);
ret = input_ff_create_memless(input, NULL, steam_play_effect);
if (ret)
@@ -1017,6 +1227,7 @@ static int steam_sensors_register(struct steam_device *steam)
struct hid_device *hdev = steam->hdev;
struct input_dev *sensors;
int ret;
+ bool needs_open_close;
rcu_read_lock();
sensors = rcu_dereference(steam->sensors);
@@ -1032,7 +1243,27 @@ static int steam_sensors_register(struct steam_device *steam)
input_set_drvdata(sensors, steam);
sensors->dev.parent = &hdev->dev;
- if (!(steam->quirks & STEAM_QUIRK_DECK)) {
+
+ /*
+ * The open/close calls are needed in these specific cases:
+ *
+ * - Steam Controller (2015): Always
+ * - Steam Deck: Never
+ * - Steam Controller (2026): Only when wireless/BLE
+ */
+ if (steam->quirks & STEAM_QUIRK_DECK)
+ needs_open_close = false;
+ else if (steam->quirks & (STEAM_QUIRK_WIRELESS | STEAM_QUIRK_BLE))
+ /* Both wireless Steam Controller setups */
+ needs_open_close = true;
+ else if (steam->quirks & STEAM_QUIRK_IBEX)
+ /* Wired Steam Controller (2026) */
+ needs_open_close = false;
+ else
+ /* Wired Steam Controller (2015) */
+ needs_open_close = true;
+
+ if (needs_open_close) {
sensors->open = steam_sensor_open;
sensors->close = steam_sensor_close;
}
@@ -1139,9 +1370,6 @@ static void steam_battery_unregister(struct steam_device *steam)
static int steam_register(struct steam_device *steam)
{
int ret;
- unsigned long client_opened;
- unsigned long flags;
- bool do_add;
/*
* This function can be called several times in a row with the
@@ -1149,51 +1377,42 @@ static int steam_register(struct steam_device *steam)
* another client send a get_connection_status command, for example.
* The battery and serial number are set just once per device.
*/
- if (!steam->serial_no[0]) {
- /*
- * Unlikely, but getting the serial could fail, and it is not so
- * important, so make up a serial number and go on.
- */
- if (steam_get_serial(steam) < 0)
- strscpy(steam->serial_no, "XXXXXXXXXX",
- sizeof(steam->serial_no));
-
- ret = steam_get_attributes(steam);
- if (ret < 0)
- hid_err(steam->hdev,
- "%s:steam_get_attributes failed with error %d\n",
- __func__, ret);
+ if (steam->serial_no[0])
+ return 0;
- hid_info(steam->hdev, "Steam Controller '%s' connected",
- steam->serial_no);
+ /*
+ * Unlikely, but getting the serial could fail, and it is not so
+ * important, so make up a serial number and go on.
+ */
+ if (steam_get_serial(steam) < 0)
+ strscpy(steam->serial_no, "XXXXXXXXXX",
+ sizeof(steam->serial_no));
- /* ignore battery errors, we can live without it */
- if (steam->quirks & STEAM_QUIRK_WIRELESS)
- steam_battery_register(steam);
+ ret = steam_get_attributes(steam);
+ if (ret < 0)
+ hid_err(steam->hdev,
+ "%s:steam_get_attributes failed with error %d\n",
+ __func__, ret);
- do_add = true;
- }
+ hid_info(steam->hdev, "Steam Controller '%s' connected",
+ steam->serial_no);
- spin_lock_irqsave(&steam->lock, flags);
- client_opened = steam->client_opened;
- spin_unlock_irqrestore(&steam->lock, flags);
+ /* ignore battery errors, we can live without it */
+ if (steam->quirks & (STEAM_QUIRK_WIRELESS | STEAM_QUIRK_IBEX))
+ steam_battery_register(steam);
- if (!client_opened) {
- steam_set_lizard_mode(steam, lizard_mode);
- ret = steam_input_register(steam);
- if (ret != 0)
- goto steam_register_input_fail;
- ret = steam_sensors_register(steam);
- if (ret != 0)
- goto steam_register_sensors_fail;
- }
+ steam_set_lizard_mode(steam, lizard_mode);
+ ret = steam_input_register(steam);
+ if (ret != 0)
+ goto steam_register_input_fail;
+ ret = steam_sensors_register(steam);
+ if (ret != 0)
+ goto steam_register_sensors_fail;
- if (do_add) {
- mutex_lock(&steam_devices_lock);
- if (list_empty(&steam->list))
- list_add(&steam->list, &steam_devices);
- mutex_unlock(&steam_devices_lock);
- }
+ mutex_lock(&steam_devices_lock);
+ if (list_empty(&steam->list))
+ list_add(&steam->list, &steam_devices);
+ mutex_unlock(&steam_devices_lock);
return 0;
steam_register_sensors_fail:
@@ -1277,21 +1496,46 @@ static void steam_mode_switch_cb(struct work_struct *work)
}
}
-static bool steam_is_valve_interface(struct hid_device *hdev)
+static bool steam_is_valve_interface(struct hid_device *hdev, int quirks)
{
struct hid_report_enum *rep_enum;
/*
- * The wired device creates 3 interfaces:
+ * The 2015 wired controller creates 3 interfaces:
* 0: emulated mouse.
* 1: emulated keyboard.
* 2: the real game pad.
- * The wireless device creates 5 interfaces:
+ * The 2015 wireless adapter creates 5 interfaces:
* 0: emulated keyboard.
- * 1-4: slots where up to 4 real game pads will be connected to.
- * We know which one is the real gamepad interface because they are the
- * only ones with a feature report.
+ * 1-4: slots where up to 4 real controllers will be connected to.
+ * The Steam Deck creates 5 interfaces:
+ * 0: emulated mouse.
+ * 1: emulated keyboard.
+ * 2: the real game pad.
+ * 3-4: internal device comms (not HID).
+ * The 2026 wired controller creates 1 unified interface.
+ * The 2026 wireless puck creates 7 interfaces:
+ * 0-1: internal device comms (not HID).
+ * 2-5: slots where up to 4 real controllers will be connected to.
+ * 6: basic pogo pin interface.
+ *
+ * We know which one is the real controller interface for the pre-2026
+ * controllers because they are the only ones with a feature report.
+ *
+ * The puck's pogo pin interface should be ignored as it's stripped
+ * down. It has one collection with usage page FF00 with usage ID 2.
*/
+ if (quirks & STEAM_QUIRK_IBEX) {
+ /* There is only one BLE HID interface */
+ if (quirks & STEAM_QUIRK_BLE)
+ return true;
+
+ if (hdev->maxcollection < 1)
+ return true;
+
+ return hdev->collection[0].usage != 0xFF000002;
+ }
+
rep_enum = &hdev->report_enum[HID_FEATURE_REPORT];
return !list_empty(&rep_enum->report_list);
}
@@ -1351,6 +1595,14 @@ static int steam_client_ll_raw_request(struct hid_device *hdev,
report_type, reqtype);
}
+static int steam_client_ll_output_report(struct hid_device *hdev,
+ u8 *buf, size_t count)
+{
+ struct steam_device *steam = hdev->driver_data;
+
+ return hid_hw_output_report(steam->hdev, buf, count);
+}
+
static const struct hid_ll_driver steam_client_ll_driver = {
.parse = steam_client_ll_parse,
.start = steam_client_ll_start,
@@ -1358,6 +1610,7 @@ static const struct hid_ll_driver steam_client_ll_driver = {
.open = steam_client_ll_open,
.close = steam_client_ll_close,
.raw_request = steam_client_ll_raw_request,
+ .output_report = steam_client_ll_output_report,
};
static struct hid_device *steam_create_client_hid(struct hid_device *hdev)
@@ -1412,7 +1665,7 @@ static int steam_probe(struct hid_device *hdev,
* The non-valve interfaces (mouse and keyboard emulation) are
* connected without changes.
*/
- if (!steam_is_valve_interface(hdev))
+ if (!steam_is_valve_interface(hdev, id->driver_data))
return hid_hw_start(hdev, HID_CONNECT_DEFAULT);
steam = devm_kzalloc(&hdev->dev, sizeof(*steam), GFP_KERNEL);
@@ -1451,14 +1704,21 @@ static int steam_probe(struct hid_device *hdev,
goto err_hw_stop;
}
+ steam->connected = true;
+
if (steam->quirks & STEAM_QUIRK_WIRELESS) {
hid_info(hdev, "Steam wireless receiver connected");
/* If using a wireless adaptor ask for connection status */
steam->connected = false;
- steam_request_conn_status(steam);
- } else {
- /* A wired connection is always present */
- steam->connected = true;
+ ret = steam_get_conn_status(steam);
+ if (ret < 0)
+ hid_err(hdev,
+ "%s:steam_get_conn_status failed with error %d\n",
+ __func__, ret);
+ else if (ret == WIRELESS_EVENT_CONNECT)
+ steam->connected = true;
+ }
+ if (steam->connected) {
ret = steam_register(steam);
if (ret) {
hid_err(hdev,
@@ -1969,7 +2229,7 @@ static void steam_do_battery_event(struct steam_device *steam,
battery = rcu_dereference(steam->battery);
if (likely(battery)) {
spin_lock_irqsave(&steam->lock, flags);
- steam->voltage = volts;
+ steam->battery_voltage = volts;
steam->battery_charge = batt;
spin_unlock_irqrestore(&steam->lock, flags);
power_supply_changed(battery);
@@ -1977,6 +2237,216 @@ static void steam_do_battery_event(struct steam_device *steam,
rcu_read_unlock();
}
+/*
+ * The size for this message payload is 53 in REPORT_ID_INPUT and 45 in REPORT_ID_INPUT2.
+ * The values are:
+ * (* values only in REPORT_ID_INPUT)
+ * Offset| Type | Mapped to |Meaning
+ * -------+-------+-----------+--------------------------
+ * 1 | u8 | -- | sequence number
+ * 2-5 | u32 | see below | buttons
+ * 6-7 | s16 | ABS_HAT2Y | left trigger (uncalibrated)
+ * 8-9 | s16 | ABS_HAT2X | right trigger (uncalibrated)
+ * 10-11 | s16 | ABS_X | left joystick X
+ * 12-13 | s16 | ABS_Y | left joystick Y
+ * 14-15 | s16 | ABS_RX | right joystick X
+ * 16-17 | s16 | ABS_RY | right joystick Y
+ * 18-19 | s16 | ABS_HAT0X | left-pad X value
+ * 20-21 | s16 | ABS_HAT0Y | left-pad Y value
+ * 22-23 | u16 | -- | left pad pressure
+ * 24-25 | s16 | ABS_HAT1X | right-pad X value
+ * 26-27 | s16 | ABS_HAT1Y | right-pad Y value
+ * 28-29 | u16 | -- | right pad pressure
+ * 30-33 | u32 | IMU MSC_TIMESTAMP | IMU timestamp
+ * 34-35 | s16 | IMU ABS_X | accelerometer X value
+ * 36-37 | s16 | IMU ABS_Z | accelerometer Y value
+ * 38-39 | s16 | IMU ABS_Y | accelerometer Z value
+ * 40-41 | s16 | IMU ABS_RX | gyro X value
+ * 42-43 | s16 | IMU ABS_RZ | gyro Y value
+ * 44-45 | s16 | IMU ABS_RY | gyro Z value
+ * 46-47 | s16 | -- | * quaternion W value
+ * 48-49 | s16 | -- | * quaternion X value
+ * 50-51 | s16 | -- | * quaternion Y value
+ * 52-53 | s16 | -- | * quaternion Z value
+ *
+ * The buttons are:
+ * Bit | Mapped to | Description
+ * ------+------------+--------------------------------
+ * 2.0 | BTN_A | button A
+ * 2.1 | BTN_B | button B
+ * 2.2 | BTN_X | button X
+ * 2.3 | BTN_Y | button Y
+ * 2.4 | BTN_BASE | quick access button
+ * 2.5 | BTN_THUMBR | right joystick clicked
+ * 2.6 | BTN_START | menu
+ * 2.7 | BTN_GRIPR | right top grip button
+ * 3.0 | BTN_GRIPR2 | right bottom grip button
+ * 3.1 | BTN_TR | right shoulder
+ * 3.2 | BTN_DPAD_DOWN | left-pad down
+ * 3.3 | BTN_DPAD_RIGHT | left-pad right
+ * 3.4 | BTN_DPAD_LEFT | left-pad left
+ * 3.5 | BTN_DPAD_UP | left-pad up
+ * 3.6 | BTN_SELECT | view
+ * 3.7 | BTN_THUMBL | left joystick clicked
+ * 4.0 | BTN_MODE | steam logo
+ * 4.1 | BTN_GRIPL | left top grip button
+ * 4.2 | BTN_GRIPL2 | left bottom grip button
+ * 4.3 | BTN_TL | left shoulder
+ * 4.4 | -- | right joystick touched
+ * 4.5 | -- | right pad touched
+ * 4.6 | BTN_THUMB2 | right pad pressed
+ * 4.7 | BTN_TR2 | right trigger fully pressed
+ * 5.0 | -- | left joystick touched
+ * 5.1 | -- | left pad touched
+ * 5.2 | BTN_THUMB | left pad pressed
+ * 5.3 | BTN_TL2 | left trigger fully pressed
+ * 5.4 | -- | right grip touch
+ * 5.5 | -- | left grip touch
+ * 5.6 | -- | unmapped
+ * 5.7 | -- | unmapped
+ */
+
+static const struct steam_button_mapping steam_ibex_button_mappings[] = {
+ { BTN_A, 2, 0 },
+ { BTN_B, 2, 1 },
+ { BTN_X, 2, 2 },
+ { BTN_Y, 2, 3 },
+ { BTN_BASE, 2, 4 },
+ { BTN_THUMBR, 2, 5 },
+ { BTN_START, 2, 6 },
+ { BTN_GRIPR, 2, 7 },
+ { BTN_GRIPR2, 3, 0 },
+ { BTN_TR, 3, 1 },
+ { BTN_DPAD_DOWN, 3, 2 },
+ { BTN_DPAD_RIGHT, 3, 3 },
+ { BTN_DPAD_LEFT, 3, 4 },
+ { BTN_DPAD_UP, 3, 5 },
+ { BTN_SELECT, 3, 6 },
+ { BTN_THUMBL, 3, 7 },
+ { BTN_MODE, 4, 0 },
+ { BTN_GRIPL, 4, 1 },
+ { BTN_GRIPL2, 4, 2 },
+ { BTN_TL, 4, 3 },
+ { BTN_THUMB2, 4, 6 },
+ { BTN_TR2, 4, 7 },
+ { BTN_THUMB, 5, 2 },
+ { BTN_TL2, 5, 3 },
+ { /* sentinel */ },
+};
+
+static const struct steam_axis_mapping steam_ibex_axis_mappings[] = {
+ { ABS_X, 1, 10 },
+ { ABS_Y, -1, 12 },
+ { ABS_RX, 1, 14 },
+ { ABS_RY, -1, 16 },
+ { ABS_HAT2Y, 1, 6 },
+ { ABS_HAT2X, 1, 8 },
+ { /* sentinel */ },
+};
+
+static const struct steam_axis_mapping steam_ibex_imu_mappings[] = {
+ { ABS_X, 1, 34 },
+ { ABS_Z, -1, 36 },
+ { ABS_Y, 1, 38 },
+ { ABS_RX, 1, 40 },
+ { ABS_RZ, -1, 42 },
+ { ABS_RY, 1, 44 },
+ { /* sentinel */ },
+};
+
+static void steam_do_ibex_input_event(struct steam_device *steam,
+ struct input_dev *input, const u8 *data)
+{
+ bool start_pressed;
+ bool lpad_touched, rpad_touched;
+
+ start_pressed = data[2] & BIT(6);
+
+ if (!start_pressed && steam->did_mode_switch) {
+ steam->did_mode_switch = false;
+ cancel_delayed_work(&steam->mode_switch);
+ } else if (!steam->client_opened && start_pressed && !steam->did_mode_switch) {
+ steam->did_mode_switch = true;
+ schedule_delayed_work(&steam->mode_switch, 45 * HZ / 100);
+ }
+
+ if (!steam->gamepad_mode && lizard_mode)
+ return;
+
+ lpad_touched = data[5] & BIT(1);
+ rpad_touched = data[4] & BIT(5);
+
+ if (lpad_touched) {
+ input_report_abs(input, ABS_HAT0X, steam_le16(data + 18));
+ input_report_abs(input, ABS_HAT0Y, steam_le16(data + 20));
+ } else {
+ input_report_abs(input, ABS_HAT0X, 0);
+ input_report_abs(input, ABS_HAT0Y, 0);
+ }
+
+ if (rpad_touched) {
+ input_report_abs(input, ABS_HAT1X, steam_le16(data + 24));
+ input_report_abs(input, ABS_HAT1Y, steam_le16(data + 26));
+ } else {
+ input_report_abs(input, ABS_HAT1X, 0);
+ input_report_abs(input, ABS_HAT1Y, 0);
+ }
+ steam_map_buttons(input, steam_ibex_button_mappings, data);
+ steam_map_axes(input, steam_ibex_axis_mappings, data);
+
+ input_sync(input);
+}
+
+static void steam_do_ibex_sensors_event(struct steam_device *steam,
+ struct input_dev *sensors, const u8 *data)
+{
+ u32 timestamp;
+
+ if (!steam->gamepad_mode && lizard_mode)
+ return;
+
+ timestamp = (u32) get_unaligned_le32((__le32 *)&data[30]);
+ input_event(sensors, EV_MSC, MSC_TIMESTAMP, timestamp);
+ steam_map_axes(sensors, steam_ibex_imu_mappings, data);
+
+ input_sync(sensors);
+}
+
+static void steam_do_ibex_battery_event(struct steam_device *steam,
+ struct power_supply *battery,
+ const struct steam_ibex_battery_status *data)
+{
+ unsigned long flags;
+
+ /* Creating the battery may have failed */
+ guard(rcu)();
+ battery = rcu_dereference(steam->battery);
+ if (!likely(battery))
+ return;
+
+ spin_lock_irqsave(&steam->lock, flags);
+ steam->battery_voltage = get_unaligned_le16(&data->battery_voltage);
+ steam->battery_current = get_unaligned_le16(&data->battery_current);
+ steam->battery_temp = get_unaligned_le16(&data->temperature);
+ steam->battery_charge = data->battery_level;
+ switch (data->charge_state) {
+ case CHARGE_STATE_CHARGING:
+ steam->battery_status = POWER_SUPPLY_STATUS_CHARGING;
+ break;
+ case CHARGE_STATE_DISCHARGING:
+ steam->battery_status = POWER_SUPPLY_STATUS_DISCHARGING;
+ break;
+ case CHARGE_STATE_CHARGING_DONE:
+ steam->battery_status = POWER_SUPPLY_STATUS_FULL;
+ break;
+ default:
+ steam->battery_status = POWER_SUPPLY_STATUS_UNKNOWN;
+ break;
+ }
+ spin_unlock_irqrestore(&steam->lock, flags);
+ power_supply_changed(battery);
+}
+
static int steam_raw_event(struct hid_device *hdev,
struct hid_report *report, u8 *data,
int size)
@@ -1990,8 +2460,84 @@ static int steam_raw_event(struct hid_device *hdev,
return 0;
if (steam->client_opened)
- hid_input_report(steam->client_hdev, HID_FEATURE_REPORT,
- data, size, 0);
+ hid_input_report(steam->client_hdev, report->type, data, size, 0);
+
+ /* Ibex uses a different report format */
+ if (steam->quirks & STEAM_QUIRK_IBEX) {
+ if (report->type != HID_INPUT_REPORT)
+ return 0;
+
+ switch (report->id) {
+ case REPORT_ID_INPUT:
+ if (size != 54)
+ return 0;
+ if (steam->client_opened)
+ return 0;
+ rcu_read_lock();
+ input = rcu_dereference(steam->input);
+ if (likely(input)) {
+ steam_do_ibex_input_event(steam, input, data);
+ } else {
+ dbg_hid("%s: input data without connect event\n",
+ __func__);
+ steam_do_connect_event(steam, true);
+ }
+ sensors = rcu_dereference(steam->sensors);
+ if (likely(sensors))
+ steam_do_ibex_sensors_event(steam, sensors, data);
+ rcu_read_unlock();
+ break;
+ case REPORT_ID_INPUT2:
+ if (size != 46)
+ return 0;
+ if (steam->client_opened)
+ return 0;
+ rcu_read_lock();
+ input = rcu_dereference(steam->input);
+ if (likely(input)) {
+ steam_do_ibex_input_event(steam, input, data);
+ } else {
+ dbg_hid("%s: input data without connect event\n",
+ __func__);
+ steam_do_connect_event(steam, true);
+ }
+ sensors = rcu_dereference(steam->sensors);
+ if (likely(sensors))
+ steam_do_ibex_sensors_event(steam, sensors, data);
+ rcu_read_unlock();
+ break;
+ case REPORT_ID_BATTERY:
+ if (size != 15)
+ return 0;
+ rcu_read_lock();
+ battery = rcu_dereference(steam->battery);
+ if (likely(battery)) {
+ steam_do_ibex_battery_event(steam, battery,
+ (const struct steam_ibex_battery_status *)&data[1]);
+ } else {
+ dbg_hid("%s: battery data without connect event\n",
+ __func__);
+ steam_do_connect_event(steam, true);
+ }
+ rcu_read_unlock();
+ break;
+ case REPORT_ID_WIRELESS_EVENT:
+ if (size != 2)
+ return 0;
+ switch (data[1]) {
+ case WIRELESS_EVENT_DISCONNECT:
+ steam_do_connect_event(steam, false);
+ break;
+ case WIRELESS_EVENT_CONNECT:
+ steam_do_connect_event(steam, true);
+ break;
+ }
+ break;
+ }
+
+ return 0;
+ }
+
/*
* All messages are size=64, all values little-endian.
* The format is:
@@ -2114,6 +2660,26 @@ static const struct hid_device_id steam_controllers[] = {
USB_DEVICE_ID_STEAM_DECK),
.driver_data = STEAM_QUIRK_DECK
},
+ { /* Steam Controller (2026) wired */
+ HID_USB_DEVICE(USB_VENDOR_ID_VALVE,
+ USB_DEVICE_ID_STEAM_CONTROLLER_IBEX),
+ .driver_data = STEAM_QUIRK_IBEX
+ },
+ { /* Steam Controller (2026) BLE */
+ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_VALVE,
+ USB_DEVICE_ID_STEAM_CONTROLLER_IBEX_BLE),
+ .driver_data = STEAM_QUIRK_IBEX | STEAM_QUIRK_BLE
+ },
+ { /* Steam Controller (2026) Puck */
+ HID_USB_DEVICE(USB_VENDOR_ID_VALVE,
+ USB_DEVICE_ID_STEAM_CONTROLLER_PROTEUS),
+ .driver_data = STEAM_QUIRK_IBEX | STEAM_QUIRK_WIRELESS
+ },
+ { /* Steam Controller (2026) Steam Machine internal receiver */
+ HID_USB_DEVICE(USB_VENDOR_ID_VALVE,
+ USB_DEVICE_ID_STEAM_CONTROLLER_NEREID),
+ .driver_data = STEAM_QUIRK_IBEX | STEAM_QUIRK_WIRELESS
+ },
{}
};
--
2.54.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v2 3/6] HID: steam: Fix wording of connect/disconnect logs
2026-08-06 2:26 [PATCH v2 0/6] HID: steam: Add 2026 Steam Controller support Vicki Pfau
2026-08-06 2:26 ` [PATCH v2 1/6] HID: steam: Unregister battery on partial registration error Vicki Pfau
2026-08-06 2:26 ` [PATCH v2 2/6] HID: steam: Initial 2026 Steam Controller support Vicki Pfau
@ 2026-08-06 2:26 ` Vicki Pfau
2026-08-06 2:37 ` sashiko-bot
2026-08-06 2:26 ` [PATCH v2 4/6] HID: steam: Don't set feature reports when disconnecting Vicki Pfau
` (2 subsequent siblings)
5 siblings, 1 reply; 11+ messages in thread
From: Vicki Pfau @ 2026-08-06 2:26 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau
It always said Controller, even on Deck. Since we special-case other
instances of Controller vs. Deck in strings, let's be consistent here too.
Signed-off-by: Vicki Pfau <vi@endrift.com>
---
drivers/hid/hid-steam.c | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
index 6476fbdc5888..38c00dab0b06 100644
--- a/drivers/hid/hid-steam.c
+++ b/drivers/hid/hid-steam.c
@@ -1394,7 +1394,8 @@ static int steam_register(struct steam_device *steam)
"%s:steam_get_attributes failed with error %d\n",
__func__, ret);
- hid_info(steam->hdev, "Steam Controller '%s' connected",
+ hid_info(steam->hdev, "Steam %s '%s' connected",
+ steam->quirks & STEAM_QUIRK_DECK ? "Deck" : "Controller",
steam->serial_no);
/* ignore battery errors, we can live without it */
@@ -1427,7 +1428,8 @@ static void steam_unregister(struct steam_device *steam)
if (!steam->serial_no[0])
return;
- hid_info(steam->hdev, "Steam Controller '%s' disconnected",
+ hid_info(steam->hdev, "Steam %s '%s' disconnected",
+ steam->quirks & STEAM_QUIRK_DECK ? "Deck" : "Controller",
steam->serial_no);
steam_battery_unregister(steam);
steam_sensors_unregister(steam);
--
2.54.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v2 4/6] HID: steam: Don't set feature reports when disconnecting
2026-08-06 2:26 [PATCH v2 0/6] HID: steam: Add 2026 Steam Controller support Vicki Pfau
` (2 preceding siblings ...)
2026-08-06 2:26 ` [PATCH v2 3/6] HID: steam: Fix wording of connect/disconnect logs Vicki Pfau
@ 2026-08-06 2:26 ` Vicki Pfau
2026-08-06 2:26 ` [PATCH v2 5/6] HID: steam: Clean up locking Vicki Pfau
2026-08-06 2:26 ` [PATCH v2 6/6] HID: steam: Zero out inputs when disabling gamepad mode Vicki Pfau
5 siblings, 0 replies; 11+ messages in thread
From: Vicki Pfau @ 2026-08-06 2:26 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau
When an input device is closed, we set a feature report to reset lizard
mode and IMU mode. However, if the input device is closed because it was
removed, then we will necessarily error out when sending this, resulting in
logged errors. Since an error here is expected, we should just fail
silently.
Signed-off-by: Vicki Pfau <vi@endrift.com>
---
drivers/hid/hid-steam.c | 14 +++++++++++---
1 file changed, 11 insertions(+), 3 deletions(-)
diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
index 38c00dab0b06..8c97302c35de 100644
--- a/drivers/hid/hid-steam.c
+++ b/drivers/hid/hid-steam.c
@@ -489,9 +489,13 @@ static int steam_recv_report_id(struct steam_device *steam,
}
kfree(buf);
- if (ret < 0)
+ /*
+ * Don't log if the failure is -ENODEV, as this
+ * can happen normally on disconnect.
+ */
+ if (ret < 0 && ret != -ENODEV)
hid_err(steam->hdev, "%s: error %d\n", __func__, ret);
- else
+ else if (ret > 0)
hid_dbg(steam->hdev, "Received report %*ph\n", size, data);
if (ret < 0)
return ret;
@@ -569,7 +573,11 @@ static int steam_send_report_id(struct steam_device *steam,
} while (--retries);
kfree(buf);
- if (ret < 0)
+ /*
+ * Don't log if the failure is -ENODEV, as this
+ * can happen normally on disconnect.
+ */
+ if (ret < 0 && ret != -ENODEV)
hid_err(steam->hdev, "%s: error %d (%*ph)\n", __func__,
ret, size, cmd);
return ret;
--
2.54.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v2 5/6] HID: steam: Clean up locking
2026-08-06 2:26 [PATCH v2 0/6] HID: steam: Add 2026 Steam Controller support Vicki Pfau
` (3 preceding siblings ...)
2026-08-06 2:26 ` [PATCH v2 4/6] HID: steam: Don't set feature reports when disconnecting Vicki Pfau
@ 2026-08-06 2:26 ` Vicki Pfau
2026-08-06 2:46 ` sashiko-bot
2026-08-06 2:26 ` [PATCH v2 6/6] HID: steam: Zero out inputs when disabling gamepad mode Vicki Pfau
5 siblings, 1 reply; 11+ messages in thread
From: Vicki Pfau @ 2026-08-06 2:26 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau
This cleans up several issues with locking behavior, including RCU accesses
not being guarded behind a lock.
Signed-off-by: Vicki Pfau <vi@endrift.com>
---
drivers/hid/hid-steam.c | 90 +++++++++++++++++++++--------------------
1 file changed, 47 insertions(+), 43 deletions(-)
diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
index 8c97302c35de..01801f2a5886 100644
--- a/drivers/hid/hid-steam.c
+++ b/drivers/hid/hid-steam.c
@@ -632,7 +632,6 @@ static int steam_exchange_report(struct steam_device *steam, u8 *cmd, int csize,
unsigned int retries = 5;
int ret;
- guard(mutex)(&steam->report_mutex);
do {
ret = steam_send_report(steam, cmd, csize);
if (ret < 0)
@@ -786,7 +785,6 @@ static inline int steam_haptic_pulse(struct steam_device *steam, u8 pad,
report[8] = count >> 8;
report[9] = gain;
- guard(mutex)(&steam->report_mutex);
ret = steam_send_report(steam, report, 10);
}
@@ -828,7 +826,6 @@ static inline int steam_haptic_rumble(struct steam_device *steam,
report[9] = left_gain;
report[10] = right_gain;
- guard(mutex)(&steam->report_mutex);
ret = steam_send_report(steam, report, sizeof(report));
}
return ret;
@@ -839,8 +836,10 @@ static void steam_haptic_rumble_cb(struct work_struct *work)
struct steam_device *steam = container_of(work, struct steam_device,
rumble_work);
+ mutex_lock(&steam->report_mutex);
steam_haptic_rumble(steam, 0, steam->rumble_left,
steam->rumble_right, 2, 0);
+ mutex_unlock(&steam->report_mutex);
}
static void steam_coalesce_rumble_cb(struct work_struct *work)
@@ -849,8 +848,10 @@ static void steam_coalesce_rumble_cb(struct work_struct *work)
struct steam_device,
coalesce_rumble_work);
+ mutex_lock(&steam->report_mutex);
steam_haptic_rumble(steam, 0, steam->rumble_left,
steam->rumble_right, 2, 0);
+ mutex_unlock(&steam->report_mutex);
if (steam->rumble_left || steam->rumble_right)
schedule_delayed_work(&steam->coalesce_rumble_work, HZ / 20);
@@ -882,7 +883,6 @@ static void steam_set_lizard_mode(struct steam_device *steam, bool enable)
if (steam->gamepad_mode)
enable = false;
- mutex_lock(&steam->report_mutex);
if (enable) {
/* enable esc, enter, cursors */
steam_send_report_byte(steam, ID_SET_DEFAULT_DIGITAL_MAPPINGS);
@@ -905,14 +905,13 @@ static void steam_set_lizard_mode(struct steam_device *steam, bool enable)
SETTING_RIGHT_TRACKPAD_MODE, TRACKPAD_NONE, /* disable mouse */
0);
}
- mutex_unlock(&steam->report_mutex);
}
static int steam_input_open(struct input_dev *dev)
{
struct steam_device *steam = input_get_drvdata(dev);
unsigned long flags;
- bool set_lizard_mode;
+ bool client_opened;
/*
* Disabling lizard mode automatically is only done on the Steam
@@ -921,9 +920,10 @@ static int steam_input_open(struct input_dev *dev)
*/
if (!(steam->quirks & (STEAM_QUIRK_DECK | STEAM_QUIRK_IBEX))) {
spin_lock_irqsave(&steam->lock, flags);
- set_lizard_mode = !steam->client_opened && lizard_mode;
+ client_opened = steam->client_opened;
spin_unlock_irqrestore(&steam->lock, flags);
- if (set_lizard_mode)
+ guard(mutex)(&steam->report_mutex);
+ if (!client_opened && lizard_mode)
steam_set_lizard_mode(steam, false);
}
@@ -934,13 +934,14 @@ static void steam_input_close(struct input_dev *dev)
{
struct steam_device *steam = input_get_drvdata(dev);
unsigned long flags;
- bool set_lizard_mode;
+ bool client_opened;
if (!(steam->quirks & (STEAM_QUIRK_DECK | STEAM_QUIRK_IBEX))) {
spin_lock_irqsave(&steam->lock, flags);
- set_lizard_mode = !steam->client_opened && lizard_mode;
+ client_opened = steam->client_opened;
spin_unlock_irqrestore(&steam->lock, flags);
- if (set_lizard_mode)
+ guard(mutex)(&steam->report_mutex);
+ if (!client_opened && lizard_mode)
steam_set_lizard_mode(steam, true);
}
}
@@ -948,14 +949,11 @@ static void steam_input_close(struct input_dev *dev)
static int steam_sensor_open(struct input_dev *dev)
{
struct steam_device *steam = input_get_drvdata(dev);
- unsigned long flags;
- bool client_opened;
- spin_lock_irqsave(&steam->lock, flags);
- client_opened = steam->client_opened;
- spin_unlock_irqrestore(&steam->lock, flags);
- if (client_opened)
- return 0;
+ scoped_guard(spinlock_irqsave, &steam->lock) {
+ if (steam->client_opened)
+ return 0;
+ }
guard(mutex)(&steam->report_mutex);
steam_write_settings(steam, SETTING_IMU_MODE,
@@ -968,14 +966,11 @@ static int steam_sensor_open(struct input_dev *dev)
static void steam_sensor_close(struct input_dev *dev)
{
struct steam_device *steam = input_get_drvdata(dev);
- unsigned long flags;
- bool client_opened;
- spin_lock_irqsave(&steam->lock, flags);
- client_opened = steam->client_opened;
- spin_unlock_irqrestore(&steam->lock, flags);
- if (client_opened)
- return;
+ scoped_guard(spinlock_irqsave, &steam->lock) {
+ if (steam->client_opened)
+ return;
+ }
guard(mutex)(&steam->report_mutex);
steam_write_settings(steam, SETTING_IMU_MODE, 0, 0);
@@ -1379,14 +1374,17 @@ static int steam_register(struct steam_device *steam)
{
int ret;
+ mutex_lock(&steam->report_mutex);
/*
* This function can be called several times in a row with the
* wireless adaptor, without steam_unregister() between them, because
* another client send a get_connection_status command, for example.
* The battery and serial number are set just once per device.
*/
- if (steam->serial_no[0])
+ if (steam->serial_no[0]) {
+ mutex_unlock(&steam->report_mutex);
return 0;
+ }
/*
* Unlikely, but getting the serial could fail, and it is not so
@@ -1418,6 +1416,7 @@ static int steam_register(struct steam_device *steam)
if (ret != 0)
goto steam_register_sensors_fail;
+ mutex_unlock(&steam->report_mutex);
mutex_lock(&steam_devices_lock);
if (list_empty(&steam->list))
list_add(&steam->list, &steam_devices);
@@ -1428,6 +1427,7 @@ static int steam_register(struct steam_device *steam)
steam_input_unregister(steam);
steam_register_input_fail:
steam_battery_unregister(steam);
+ mutex_unlock(&steam->report_mutex);
return ret;
}
@@ -1439,9 +1439,11 @@ static void steam_unregister(struct steam_device *steam)
hid_info(steam->hdev, "Steam %s '%s' disconnected",
steam->quirks & STEAM_QUIRK_DECK ? "Deck" : "Controller",
steam->serial_no);
+ mutex_lock(&steam->report_mutex);
steam_battery_unregister(steam);
steam_sensors_unregister(steam);
steam_input_unregister(steam);
+ mutex_unlock(&steam->report_mutex);
cancel_work_sync(&steam->rumble_work);
cancel_delayed_work_sync(&steam->mode_switch);
cancel_delayed_work_sync(&steam->coalesce_rumble_work);
@@ -1483,23 +1485,28 @@ static void steam_mode_switch_cb(struct work_struct *work)
struct steam_device, mode_switch);
unsigned long flags;
bool client_opened;
+ bool gamepad_mode;
+
if (!lizard_mode)
return;
+ spin_lock_irqsave(&steam->lock, flags);
steam->gamepad_mode = !steam->gamepad_mode;
+ gamepad_mode = steam->gamepad_mode;
+ client_opened = steam->client_opened;
+ spin_unlock_irqrestore(&steam->lock, flags);
+
+ guard(mutex)(&steam->report_mutex);
hid_dbg(steam->hdev, "%s: switching gamepad mode to %i\n", __func__, steam->gamepad_mode);
- if (steam->gamepad_mode)
+ if (gamepad_mode)
steam_set_lizard_mode(steam, false);
else {
- spin_lock_irqsave(&steam->lock, flags);
- client_opened = steam->client_opened;
- spin_unlock_irqrestore(&steam->lock, flags);
if (!client_opened)
steam_set_lizard_mode(steam, lizard_mode);
}
steam_haptic_pulse(steam, STEAM_PAD_RIGHT, 0x190, 0, 1, 0);
- if (steam->gamepad_mode) {
+ if (gamepad_mode) {
steam_haptic_pulse(steam, STEAM_PAD_LEFT, 0x14D, 0x14D, 0x2D, 0);
} else {
steam_haptic_pulse(steam, STEAM_PAD_LEFT, 0x1F4, 0x1F4, 0x1E, 0);
@@ -1794,13 +1801,10 @@ static void steam_remove(struct hid_device *hdev)
static void steam_do_connect_event(struct steam_device *steam, bool connected)
{
- unsigned long flags;
bool changed;
- spin_lock_irqsave(&steam->lock, flags);
changed = steam->connected != connected;
steam->connected = connected;
- spin_unlock_irqrestore(&steam->lock, flags);
if (changed && schedule_work(&steam->work_connect) == 0)
dbg_hid("%s: connected=%d event already queued\n",
@@ -2229,8 +2233,6 @@ static void steam_do_deck_sensors_event(struct steam_device *steam,
static void steam_do_battery_event(struct steam_device *steam,
struct power_supply *battery, u8 *data)
{
- unsigned long flags;
-
s16 volts = steam_le16(data + 12);
u8 batt = data[14];
@@ -2238,10 +2240,8 @@ static void steam_do_battery_event(struct steam_device *steam,
rcu_read_lock();
battery = rcu_dereference(steam->battery);
if (likely(battery)) {
- spin_lock_irqsave(&steam->lock, flags);
steam->battery_voltage = volts;
steam->battery_charge = batt;
- spin_unlock_irqrestore(&steam->lock, flags);
power_supply_changed(battery);
}
rcu_read_unlock();
@@ -2426,15 +2426,12 @@ static void steam_do_ibex_battery_event(struct steam_device *steam,
struct power_supply *battery,
const struct steam_ibex_battery_status *data)
{
- unsigned long flags;
-
/* Creating the battery may have failed */
guard(rcu)();
battery = rcu_dereference(steam->battery);
if (!likely(battery))
return;
- spin_lock_irqsave(&steam->lock, flags);
steam->battery_voltage = get_unaligned_le16(&data->battery_voltage);
steam->battery_current = get_unaligned_le16(&data->battery_current);
steam->battery_temp = get_unaligned_le16(&data->temperature);
@@ -2453,7 +2450,6 @@ static void steam_do_ibex_battery_event(struct steam_device *steam,
steam->battery_status = POWER_SUPPLY_STATUS_UNKNOWN;
break;
}
- spin_unlock_irqrestore(&steam->lock, flags);
power_supply_changed(battery);
}
@@ -2469,6 +2465,7 @@ static int steam_raw_event(struct hid_device *hdev,
if (!steam)
return 0;
+ guard(spinlock_irqsave)(&steam->lock);
if (steam->client_opened)
hid_input_report(steam->client_hdev, report->type, data, size, 0);
@@ -2632,6 +2629,8 @@ static int steam_param_set_lizard_mode(const char *val,
{
struct steam_device *steam;
int ret;
+ bool client_opened;
+ unsigned long flags;
ret = param_set_bool(val, kp);
if (ret)
@@ -2639,8 +2638,13 @@ static int steam_param_set_lizard_mode(const char *val,
mutex_lock(&steam_devices_lock);
list_for_each_entry(steam, &steam_devices, list) {
- if (!steam->client_opened)
+ spin_lock_irqsave(&steam->lock, flags);
+ client_opened = steam->client_opened;
+ spin_unlock_irqrestore(&steam->lock, flags);
+ if (!client_opened) {
+ guard(mutex)(&steam->report_mutex);
steam_set_lizard_mode(steam, lizard_mode);
+ }
}
mutex_unlock(&steam_devices_lock);
return 0;
--
2.54.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v2 6/6] HID: steam: Zero out inputs when disabling gamepad mode
2026-08-06 2:26 [PATCH v2 0/6] HID: steam: Add 2026 Steam Controller support Vicki Pfau
` (4 preceding siblings ...)
2026-08-06 2:26 ` [PATCH v2 5/6] HID: steam: Clean up locking Vicki Pfau
@ 2026-08-06 2:26 ` Vicki Pfau
5 siblings, 0 replies; 11+ messages in thread
From: Vicki Pfau @ 2026-08-06 2:26 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, linux-input; +Cc: Vicki Pfau
When gamepad mode is disabled the gamepad input devices will stop receiving
updates. However, in the case where there are buttons still pressed this
will appear as an indefinitely-held button. Instead we should zero out the
inputs to make it look like things are all released. We do the same thing
for gyroscope inputs to make sure it doesn't look like it's endlessly
rotating, but we freeze the accelerometer input since zero isn't a neutral
input on the surface of the Earth.
Signed-off-by: Vicki Pfau <vi@endrift.com>
---
drivers/hid/hid-steam.c | 69 +++++++++++++++++++++++++++++++++++++++--
1 file changed, 67 insertions(+), 2 deletions(-)
diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
index 01801f2a5886..0c159ae7bf1a 100644
--- a/drivers/hid/hid-steam.c
+++ b/drivers/hid/hid-steam.c
@@ -1498,11 +1498,76 @@ static void steam_mode_switch_cb(struct work_struct *work)
guard(mutex)(&steam->report_mutex);
hid_dbg(steam->hdev, "%s: switching gamepad mode to %i\n", __func__, steam->gamepad_mode);
- if (gamepad_mode)
+ if (gamepad_mode) {
steam_set_lizard_mode(steam, false);
- else {
+ } else {
+ struct input_dev *input;
+ struct input_dev *sensors;
+
if (!client_opened)
steam_set_lizard_mode(steam, lizard_mode);
+
+ /*
+ * Zero out inputs so it doesn't look like we're holding
+ * anything indefinitely.
+ */
+ guard(spinlock_irqsave)(&steam->lock);
+ rcu_read_lock();
+ input = rcu_dereference(steam->input);
+ if (likely(input)) {
+ input_report_key(input, BTN_TR2, 0);
+ input_report_key(input, BTN_TL2, 0);
+ input_report_key(input, BTN_TR, 0);
+ input_report_key(input, BTN_TL, 0);
+ input_report_key(input, BTN_Y, 0);
+ input_report_key(input, BTN_B, 0);
+ input_report_key(input, BTN_X, 0);
+ input_report_key(input, BTN_A, 0);
+ input_report_key(input, BTN_DPAD_UP, 0);
+ input_report_key(input, BTN_DPAD_RIGHT, 0);
+ input_report_key(input, BTN_DPAD_LEFT, 0);
+ input_report_key(input, BTN_DPAD_DOWN, 0);
+ input_report_key(input, BTN_SELECT, 0);
+ input_report_key(input, BTN_MODE, 0);
+ input_report_key(input, BTN_START, 0);
+ input_report_key(input, BTN_THUMBR, 0);
+ input_report_key(input, BTN_THUMBL, 0);
+ input_report_key(input, BTN_THUMB, 0);
+ input_report_key(input, BTN_THUMB2, 0);
+ input_report_key(input, BTN_GRIPL, 0);
+ input_report_key(input, BTN_GRIPR, 0);
+
+ input_report_abs(input, ABS_X, 0);
+ input_report_abs(input, ABS_Y, 0);
+ input_report_abs(input, ABS_RX, 0);
+ input_report_abs(input, ABS_RY, 0);
+ input_report_abs(input, ABS_HAT0X, 0);
+ input_report_abs(input, ABS_HAT0Y, 0);
+ input_report_abs(input, ABS_HAT2Y, 0);
+ input_report_abs(input, ABS_HAT2X, 0);
+
+ if (steam->quirks & (STEAM_QUIRK_DECK | STEAM_QUIRK_IBEX)) {
+ input_report_key(input, BTN_BASE, 0);
+ input_report_key(input, BTN_GRIPL2, 0);
+ input_report_key(input, BTN_GRIPR2, 0);
+
+ input_report_abs(input, ABS_HAT1X, 0);
+ input_report_abs(input, ABS_HAT1Y, 0);
+ }
+
+ input_sync(input);
+ }
+ sensors = rcu_dereference(steam->sensors);
+ if (likely(sensors)) {
+ /* Skip accelerometers since 0 isn't a neutral input */
+
+ input_report_abs(sensors, ABS_RX, 0);
+ input_report_abs(sensors, ABS_RY, 0);
+ input_report_abs(sensors, ABS_RZ, 0);
+
+ input_sync(sensors);
+ }
+ rcu_read_unlock();
}
steam_haptic_pulse(steam, STEAM_PAD_RIGHT, 0x190, 0, 1, 0);
--
2.54.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [PATCH v2 3/6] HID: steam: Fix wording of connect/disconnect logs
2026-08-06 2:26 ` [PATCH v2 3/6] HID: steam: Fix wording of connect/disconnect logs Vicki Pfau
@ 2026-08-06 2:37 ` sashiko-bot
0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-08-06 2:37 UTC (permalink / raw)
To: Vicki Pfau; +Cc: dmitry.torokhov, linux-input
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
Pre-existing issues:
- [High] Device sending an empty serial number causes silent resource leaks upon disconnection.
- [Medium] Registration failure leaves `steam->serial_no` populated, permanently breaking subsequent connections.
--
--- Patch [3]: [PATCH v2 3/6] HID: steam: Fix wording of connect/disconnect logs ---
commit 1846261fd57667e998e1416cd9dcdbedc57da4e8
Author: Vicki Pfau <vi@endrift.com>
HID: steam: Fix wording of connect/disconnect logs
This commit updates the connect and disconnect log messages to properly
identify the device as a Deck or a Controller depending on the device quirks.
It aligns these messages with other special-cased strings in the driver.
> diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
> index 6476fbdc58886..38c00dab0b067 100644
> --- a/drivers/hid/hid-steam.c
> +++ b/drivers/hid/hid-steam.c
> @@ -1394,7 +1394,8 @@ static int steam_register(struct steam_device *steam)
> "%s:steam_get_attributes failed with error %d\n",
> __func__, ret);
>
> - hid_info(steam->hdev, "Steam Controller '%s' connected",
> + hid_info(steam->hdev, "Steam %s '%s' connected",
> + steam->quirks & STEAM_QUIRK_DECK ? "Deck" : "Controller",
> steam->serial_no);
[Severity: Medium]
This is a pre-existing issue, but can a registration failure permanently
break subsequent connections for this device?
If steam_register() fails later in the function (for example, if
steam_input_register() fails), the error path:
steam_register_sensors_fail:
steam_input_unregister(steam);
steam_register_input_fail:
steam_battery_unregister(steam);
mutex_unlock(&steam->report_mutex);
return ret;
cleans up resources but does not clear steam->serial_no.
On the next connection attempt, steam_register() will see steam->serial_no[0]
is populated and immediately return 0 without setting up any input devices,
leaving the device permanently broken until replugged.
>
> /* ignore battery errors, we can live without it */
> @@ -1427,7 +1428,8 @@ static void steam_unregister(struct steam_device *steam)
> if (!steam->serial_no[0])
> return;
[Severity: High]
This is a pre-existing issue, but does this early return lead to a silent
resource leak if the device sends an empty serial number?
If steam_get_serial() successfully reads a valid HID reply but the string
payload is empty, it populates steam->serial_no with an empty string using
strscpy() and returns success, bypassing the fallback serial generation.
Later, on disconnection, steam_unregister() will hit this early return and
skip unregistering the input devices, sensors, and power supplies.
>
> - hid_info(steam->hdev, "Steam Controller '%s' disconnected",
> + hid_info(steam->hdev, "Steam %s '%s' disconnected",
> + steam->quirks & STEAM_QUIRK_DECK ? "Deck" : "Controller",
> steam->serial_no);
> steam_battery_unregister(steam);
> steam_sensors_unregister(steam);
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260806022653.93939-1-vi@endrift.com?part=3
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v2 1/6] HID: steam: Unregister battery on partial registration error
2026-08-06 2:26 ` [PATCH v2 1/6] HID: steam: Unregister battery on partial registration error Vicki Pfau
@ 2026-08-06 2:41 ` sashiko-bot
0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-08-06 2:41 UTC (permalink / raw)
To: Vicki Pfau; +Cc: linux-input, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
New issues:
- [Medium] Unregistering the battery in the error path prevents it from being re-registered on a subsequent retry of steam_register() because `steam->serial_no[0]` is not cleared.
Pre-existing issues:
- [High] The local variable `do_add` is conditionally initialized but unconditionally evaluated if `steam_register()` is called when `steam->serial_no[0]` is already set.
- [High] Teardown asymmetry in the `steam_probe` error path allows background work to register devices against a freed driver struct.
- [High] The unregister functions read RCU-protected pointers without writer-side serialization, leading to potential double-frees if executed concurrently.
--
--- Patch [1]: [PATCH v2 1/6] HID: steam: Unregister battery on partial registration error ---
commit 44ddde383b3d91088838e48fa3234971272a3bbc
Author: Vicki Pfau <vi@endrift.com>
HID: steam: Unregister battery on partial registration error
This commit adds missing code to unregister the battery if later
registration steps fail. This prevents a lingering power_supply object from
being left over.
> diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
> index 6199f67f3c4cd..967f12c7a599a 100644
> --- a/drivers/hid/hid-steam.c
> +++ b/drivers/hid/hid-steam.c
[ ... ]
> @@ -1199,6 +1199,7 @@ static int steam_register(struct steam_device *steam)
> steam_register_sensors_fail:
> steam_input_unregister(steam);
> steam_register_input_fail:
> + steam_battery_unregister(steam);
[Severity: Medium]
Will this prevent the battery from being re-registered on a subsequent retry?
If steam_register() fails, the error path now unregisters the battery.
However, steam->serial_no[0] remains populated.
On a subsequent retry from steam_work_connect_cb(), the populated serial
number bypasses the initialization block, skipping steam_battery_register().
> return ret;
> }
While reviewing this, I noticed a few pre-existing issues:
drivers/hid/hid-steam.c:steam_register() {
bool do_add;
...
if (!steam->serial_no[0]) {
...
do_add = true;
}
...
if (do_add) {
[Severity: High]
This is a pre-existing issue, but does this evaluate an uninitialized
variable?
If steam_register() is called when steam->serial_no[0] is already populated,
do_add is left uninitialized but is evaluated later in the function.
drivers/hid/hid-steam.c:steam_probe() {
...
err_steam_unregister:
if (steam->connected)
steam_unregister(steam);
err_hw_close:
hid_hw_close(hdev);
err_hw_stop:
hid_hw_stop(hdev);
err_cancel_work:
cancel_work_sync(&steam->work_connect);
...
}
[Severity: High]
This is a pre-existing issue, but could this error path lead to a
use-after-free?
If a wireless device schedules work_connect and probe fails,
steam_unregister() is called before cancel_work_sync().
If work_connect hasn't initialized the device yet, steam_unregister()
returns without effect. Then cancel_work_sync() waits for work_connect to
finish, allowing it to register input devices against the steam driver
struct which devres will free after probe returns.
drivers/hid/hid-steam.c:steam_battery_unregister() {
...
rcu_read_lock();
battery = rcu_dereference(steam->battery);
rcu_read_unlock();
if (!battery)
return;
RCU_INIT_POINTER(steam->battery, NULL);
...
}
[Severity: High]
This is a pre-existing issue, but can this lead to a double-free if executed
concurrently?
The unregister functions read the RCU-protected pointers without writer-side
serialization.
If steam_probe() error path calls steam_unregister() concurrently with
work_connect() calling a failure path in steam_register(), both threads
might read the same non-NULL pointer and concurrently free the same
resources.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260806022653.93939-1-vi@endrift.com?part=1
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v2 5/6] HID: steam: Clean up locking
2026-08-06 2:26 ` [PATCH v2 5/6] HID: steam: Clean up locking Vicki Pfau
@ 2026-08-06 2:46 ` sashiko-bot
0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-08-06 2:46 UTC (permalink / raw)
To: Vicki Pfau; +Cc: linux-input, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Guaranteed self-deadlock during device registration and unregistration due to recursive acquisition of `steam->report_mutex`.
--
--- Patch [5]: [PATCH v2 5/6] HID: steam: Clean up locking ---
commit f00f469b42e54055547cfa7401a79f8895766441
Author: Vicki Pfau <vi@endrift.com>
HID: steam: Clean up locking
This cleans up several issues with locking behavior, including RCU accesses
not being guarded behind a lock.
> diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
> index 8c97302c35deb..01801f2a58864 100644
> --- a/drivers/hid/hid-steam.c
> +++ b/drivers/hid/hid-steam.c
[ ... ]
> @@ -921,9 +920,10 @@ static int steam_input_open(struct input_dev *dev)
> */
> if (!(steam->quirks & (STEAM_QUIRK_DECK | STEAM_QUIRK_IBEX))) {
> spin_lock_irqsave(&steam->lock, flags);
> - set_lizard_mode = !steam->client_opened && lizard_mode;
> + client_opened = steam->client_opened;
> spin_unlock_irqrestore(&steam->lock, flags);
> - if (set_lizard_mode)
> + guard(mutex)(&steam->report_mutex);
[Severity: High]
Does this create a self-deadlock during device registration?
When steam_register() is called, it now holds steam->report_mutex. It then
calls steam_input_register(), which registers the input device. If an input
handler (like ledtrig-input-events) is present, the input core will
synchronously open the device, calling steam_input_open().
The call chain would look like:
steam_register()
mutex_lock(&steam->report_mutex);
steam_input_register(steam);
input_register_device()
... (input core handler connect)
input_open_device()
steam_input_open()
guard(mutex)(&steam->report_mutex); <-- deadlocks here
The same sequence applies during steam_unregister() when it calls
steam_input_close() and steam_sensor_close().
> + if (!client_opened && lizard_mode)
> steam_set_lizard_mode(steam, false);
> }
>
[ ... ]
> @@ -1379,14 +1374,17 @@ static int steam_register(struct steam_device *steam)
> {
> int ret;
>
> + mutex_lock(&steam->report_mutex);
> /*
> * This function can be called several times in a row with the
> * wireless adaptor, without steam_unregister() between them, because
[ ... ]
> @@ -1439,9 +1439,11 @@ static void steam_unregister(struct steam_device *steam)
> hid_info(steam->hdev, "Steam %s '%s' disconnected",
> steam->quirks & STEAM_QUIRK_DECK ? "Deck" : "Controller",
> steam->serial_no);
> + mutex_lock(&steam->report_mutex);
> steam_battery_unregister(steam);
> steam_sensors_unregister(steam);
> steam_input_unregister(steam);
> + mutex_unlock(&steam->report_mutex);
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260806022653.93939-1-vi@endrift.com?part=5
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v2 2/6] HID: steam: Initial 2026 Steam Controller support
2026-08-06 2:26 ` [PATCH v2 2/6] HID: steam: Initial 2026 Steam Controller support Vicki Pfau
@ 2026-08-06 2:49 ` sashiko-bot
0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-08-06 2:49 UTC (permalink / raw)
To: Vicki Pfau; +Cc: linux-input, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
New issues:
- [Medium] The removal of the explicit query workaround in `steam_write_settings` can cause driver initialization to fail on subsequent driver binds or module reloads due to lingering reports.
Pre-existing issues:
- [High] Use-After-Free due to HID events triggering work re-queuing during teardown because `hid_hw_stop` is called after `cancel_work_sync`.
- [High] Concurrent execution of `steam_register` leads to resource leaks and Use-After-Free/Double-Free on error paths due to missing locking.
- [High] Race condition in teardown functions (`steam_battery_unregister`, `steam_input_unregister`, etc.) leads to double-free of device components.
--
--- Patch [2]: [PATCH v2 2/6] HID: steam: Initial 2026 Steam Controller support ---
commit 4e3ca92dc2f8afcf2d70cdcfce3822923a4ccbfc
Author: Vicki Pfau <vi@endrift.com>
HID: steam: Initial 2026 Steam Controller support
This brings support for the 2026 Steam Controller, comparably featureful to
the existing support for the Steam Deck.
> diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
> index 967f12c7a599a..6476fbdc58886 100644
> --- a/drivers/hid/hid-steam.c
> +++ b/drivers/hid/hid-steam.c
[ ... ]
> @@ -489,16 +615,7 @@ static int steam_write_settings(struct steam_device *steam,
> }
> va_end(args);
>
> - ret = steam_send_report(steam, cmd, 2 + cmd[1]);
> - if (ret < 0)
> - return ret;
> -
> - /*
> - * Sometimes a lingering report for this command can
> - * get read back instead of the last set report if
> - * this isn't explicitly queried
> - */
> - return steam_recv_report(steam, cmd, 2 + cmd[1]);
> + return steam_send_report(steam, cmd, 2 + cmd[1]);
[Severity: Medium]
By removing the explicit steam_recv_report() call here, could a lingering
report be left in the device buffer?
If the module is later reloaded or the driver rebound, steam_probe() will
call steam_get_conn_status() which expects a wireless state report.
> }
[ ... ]
> -/*
> - * This command requests the wireless adaptor to post an event
> - * with the connection status. Useful if this driver is loaded when
> - * the controller is already connected.
> - */
> -static inline int steam_request_conn_status(struct steam_device *steam)
> +static int steam_get_conn_status(struct steam_device *steam)
> {
> + int ret = 0;
> + u8 cmd[] = {ID_DONGLE_GET_WIRELESS_STATE};
> + u8 reply[3] = {};
> + u8 report_id;
> +
> + if (steam->quirks & STEAM_QUIRK_IBEX)
> + report_id = REPORT_ID_FEATURES_DONGLE;
> + else
> + report_id = 0;
> +
> guard(mutex)(&steam->report_mutex);
> - return steam_send_report_byte(steam, ID_DONGLE_GET_WIRELESS_STATE);
> + ret = steam_send_report_id(steam, cmd, sizeof(cmd), report_id);
> + if (ret < 0)
> + return ret;
> + ret = steam_recv_report_id(steam, reply, sizeof(reply), report_id);
> + if (ret < 0)
> + return ret;
> + if (reply[0] != ID_DONGLE_GET_WIRELESS_STATE || reply[1] < 1) {
> + hid_err(steam->hdev, "%s: invalid reply (%*ph)\n", __func__,
> + (int)sizeof(reply), reply);
> + return -EIO;
> + }
> +
> + return reply[2];
> }
[Severity: Medium]
Does this function read the lingering ID_SET_SETTINGS_VALUES report instead of
the expected status?
Because this lacks a retry mechanism, it appears this would fail the
reply[0] != ID_DONGLE_GET_WIRELESS_STATE check and return -EIO, breaking
device initialization.
[ ... ]
> @@ -1149,51 +1377,42 @@ static int steam_register(struct steam_device *steam)
> * another client send a get_connection_status command, for example.
> * The battery and serial number are set just once per device.
> */
> - if (!steam->serial_no[0]) {
> - /*
> - * Unlikely, but getting the serial could fail, and it is not so
> - * important, so make up a serial number and go on.
> - */
> - if (steam_get_serial(steam) < 0)
> - strscpy(steam->serial_no, "XXXXXXXXXX",
> - sizeof(steam->serial_no));
> -
> - ret = steam_get_attributes(steam);
> - if (ret < 0)
> - hid_err(steam->hdev,
> - "%s:steam_get_attributes failed with error %d\n",
> - __func__, ret);
> + if (steam->serial_no[0])
> + return 0;
[Severity: High]
This is a pre-existing issue, but does this check allow concurrent execution?
If a wireless connect event triggers steam_work_connect_cb() simultaneously
with steam_probe() invoking steam_get_conn_status(), could both threads
evaluate steam->serial_no[0] as empty because there is no locking here?
If so, both could proceed to allocate and register input devices and
batteries.
Could this potentially overwrite the pointers and leak the first set, or
cause a use-after-free when one thread fails and runs the error cleanup
path on resources the other thread just allocated?
[ ... ]
> - if (do_add) {
> - mutex_lock(&steam_devices_lock);
> - if (list_empty(&steam->list))
> - list_add(&steam->list, &steam_devices);
> - mutex_unlock(&steam_devices_lock);
> - }
> + mutex_lock(&steam_devices_lock);
> + if (list_empty(&steam->list))
> + list_add(&steam->list, &steam_devices);
> + mutex_unlock(&steam_devices_lock);
> return 0;
>
> steam_register_sensors_fail:
[Severity: High]
This is a pre-existing issue, but does the teardown sequence in
steam_battery_unregister() and steam_input_unregister() have a race condition?
If these unregister functions are called concurrently (for example, if
steam_register() fails on one thread while steam_work_connect_cb()
processes a disconnect event on another), both threads could read the
same component pointer within the RCU read-side critical section before
either thread executes RCU_INIT_POINTER(..., NULL).
Would this cause both threads to call power_supply_unregister() and
devm_kfree() on the exact same pointer, leading to a double-free?
[ ... ]
[Severity: High]
This is a pre-existing issue, but can the sequence in steam_remove() lead to
a use-after-free?
drivers/hid/hid-steam.c:steam_remove() {
...
cancel_work_sync(&steam->work_connect);
...
hid_hw_stop(hdev);
}
Because hid_hw_stop() is called after cancel_work_sync(), the HID transport can
still deliver events during this window.
If an event like REPORT_ID_WIRELESS_EVENT arrives, steam_raw_event() will call
steam_do_connect_event(), which calls schedule_work(&steam->work_connect),
re-queuing the work after it was cancelled.
Once steam_remove() returns and devres frees the steam_device structure,
will the workqueue run steam_work_connect_cb() on the freed memory?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260806022653.93939-1-vi@endrift.com?part=2
^ permalink raw reply [flat|nested] 11+ messages in thread
end of thread, other threads:[~2026-08-06 2:49 UTC | newest]
Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-06 2:26 [PATCH v2 0/6] HID: steam: Add 2026 Steam Controller support Vicki Pfau
2026-08-06 2:26 ` [PATCH v2 1/6] HID: steam: Unregister battery on partial registration error Vicki Pfau
2026-08-06 2:41 ` sashiko-bot
2026-08-06 2:26 ` [PATCH v2 2/6] HID: steam: Initial 2026 Steam Controller support Vicki Pfau
2026-08-06 2:49 ` sashiko-bot
2026-08-06 2:26 ` [PATCH v2 3/6] HID: steam: Fix wording of connect/disconnect logs Vicki Pfau
2026-08-06 2:37 ` sashiko-bot
2026-08-06 2:26 ` [PATCH v2 4/6] HID: steam: Don't set feature reports when disconnecting Vicki Pfau
2026-08-06 2:26 ` [PATCH v2 5/6] HID: steam: Clean up locking Vicki Pfau
2026-08-06 2:46 ` sashiko-bot
2026-08-06 2:26 ` [PATCH v2 6/6] HID: steam: Zero out inputs when disabling gamepad mode Vicki Pfau
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox