* [PATCH v13 01/18] HID: asus: size feature reports from the HID report descriptor
2026-11-04 1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
@ 2026-11-04 1:39 ` Denis Benato
2026-11-04 1:39 ` [PATCH v13 02/18] HID: asus: add support for ROG Ally handhelds Denis Benato
` (16 subsequent siblings)
17 siblings, 0 replies; 25+ messages in thread
From: Denis Benato @ 2026-11-04 1:39 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
The ASUS vendor protocol uses the same feature report IDs across very
different devices: on N-KEY keyboards and ROG Ally report 0x5A/0x5E are
64-byte reports, while on XG Mobile stations report 0x5E is a 300-byte
report. The driver currently hardcodes transfer lengths (16 bytes for
the handshake, FEATURE_KBD_REPORT_SIZE for reads, ROG_XGM_REPORT_SIZE
for the XG Mobile LED, ROG_ALLY_REPORT_SIZE for the MCU version read),
which mismatches what the hardware expects and would break if a new
device used yet another size for the same report ID.
Derive the transfer length of every feature report from the HID report
descriptor of the device at hand:
- add asus_report_len(), which resolves a report ID to its struct
hid_report and returns hid_report_len();
- asus_kbd_set_report() now zero-pads short payloads to the exact
report size and rejects payloads that do not fit the report;
- add asus_kbd_get_report() and use it from asus_kbd_init() and
asus_kbd_get_functions() so reads are sized per report as well;
- asus_xgm_led_set() sends a minimal payload and checks the transfer
result against the descriptor length instead of the hardcoded
ROG_XGM_REPORT_SIZE, which is removed along with the fixed-size
buffers in asus_kbd_set_fn_lock() and asus_kbd_set_brightness();
- mcu_request_version() sizes the 0x0d response from the descriptor
and names the keyboard feature report FEATURE_KBD_REPORT_ID.
ROG_ALLY_REPORT_SIZE has no remaining user and is removed.
Captured USB traffic of the same commands issued by Windows shows they
are always written at the full report length, which this change now
matches.
Tested-by: Rebecca Mara Müller <rebecca.mara@posteo.de> # UX8407AA
Signed-off-by: Denis Benato <denis.benato@linux.dev>
---
drivers/hid/hid-asus.c | 181 ++++++++++++++++++++++++++++++-----------
1 file changed, 135 insertions(+), 46 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index f6aa1be09ad5..46643022b68d 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -22,6 +22,7 @@
#include <linux/acpi.h>
#include <linux/cleanup.h>
+#include <linux/container_of.h>
#include <linux/dmi.h>
#include <linux/hid.h>
#include <linux/module.h>
@@ -30,6 +31,7 @@
#include <linux/usb.h> /* For to_usb_interface for T100 touchpad intf check */
#include <linux/power_supply.h>
#include <linux/leds.h>
+#include <linux/types.h>
#include "hid-ids.h"
@@ -51,9 +53,6 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define FEATURE_KBD_LED_REPORT_ID1 0x5d
#define FEATURE_KBD_LED_REPORT_ID2 0x5e
-#define ROG_XGM_REPORT_SIZE 300
-
-#define ROG_ALLY_REPORT_SIZE 64
#define ROG_ALLY_X_MIN_MCU 313
#define ROG_ALLY_MIN_MCU 319
@@ -582,20 +581,104 @@ static int asus_raw_event(struct hid_device *hdev,
return 0;
}
+/**
+ * asus_find_report() - Find a report of the given type by its ID
+ * @hdev: HID device
+ * @type: report type (HID_INPUT_REPORT, HID_OUTPUT_REPORT or
+ * HID_FEATURE_REPORT)
+ * @report_id: report ID to look for
+ *
+ * Return: the report with the given ID, or NULL if the device does not
+ * declare such a report.
+ */
+static struct hid_report *asus_find_report(struct hid_device *hdev,
+ unsigned int type, u8 report_id)
+{
+ return hdev->report_enum[type].report_id_hash[report_id];
+}
+
+/**
+ * asus_report_len() - Length of a feature report for the device
+ * @hdev: HID device
+ * @report_id: report ID of the feature report
+ *
+ * The same ASUS report ID can have different sizes depending on the device:
+ * report 0x5E is a 64-byte report on laptops but a 300-byte report on XG
+ * Mobile stations. Always size transfers from the report descriptor of the
+ * device at hand instead of hardcoding lengths.
+ *
+ * Return: the length of the feature report in bytes, or 0 if the device
+ * does not declare the report.
+ */
+static size_t asus_report_len(struct hid_device *hdev, u8 report_id)
+{
+ struct hid_report *report;
+
+ report = asus_find_report(hdev, HID_FEATURE_REPORT, report_id);
+
+ return report ? hid_report_len(report) : 0;
+}
+
static int asus_kbd_set_report(struct hid_device *hdev, const u8 *buf, size_t buf_size)
{
- u8 *dmabuf __free(kfree) = kmemdup(buf, buf_size, GFP_KERNEL);
+ size_t hw_len = asus_report_len(hdev, buf[0]);
+
+ /* The payload (including the report ID) must fit the device report. */
+ if (!hw_len || buf_size > hw_len)
+ return -EINVAL;
+
+ u8 *dmabuf __free(kfree) = kzalloc(hw_len, GFP_KERNEL);
if (!dmabuf)
return -ENOMEM;
+ memcpy(dmabuf, buf, buf_size);
+
/*
- * The report ID should be set from the incoming buffer due to LED and key
- * interfaces having different pages
+ * The report ID should be set from the incoming buffer due to LED and
+ * key interfaces having different pages. Short payloads are zero-padded
+ * to the exact report size the hardware expects for this report ID.
*/
- return hid_hw_raw_request(hdev, buf[0], dmabuf, buf_size, HID_FEATURE_REPORT,
+ return hid_hw_raw_request(hdev, buf[0], dmabuf, hw_len, HID_FEATURE_REPORT,
HID_REQ_SET_REPORT);
}
+/**
+ * asus_kbd_get_report() - Read a report sized from the descriptor
+ * @hdev: HID device
+ * @report_id: report ID to read
+ * @buf: buffer to store the report in
+ * @buf_size: size of @buf
+ *
+ * The transfer is performed at the exact report size the device declares
+ * for @report_id. The device may answer with a short read, and only the
+ * bytes that fit in @buf are copied into it.
+ *
+ * Return: 0 on success, negative errno on failure.
+ */
+static int asus_kbd_get_report(struct hid_device *hdev, u8 report_id,
+ u8 *buf, size_t buf_size)
+{
+ size_t hw_len = asus_report_len(hdev, report_id);
+ int ret;
+
+ if (!hw_len)
+ return -EINVAL;
+
+ u8 *dmabuf __free(kfree) = kzalloc(hw_len, GFP_KERNEL);
+ if (!dmabuf)
+ return -ENOMEM;
+
+ ret = hid_hw_raw_request(hdev, report_id, dmabuf, hw_len,
+ HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
+ if (ret < 0)
+ return ret;
+
+ /* The device may return less than requested (short read). */
+ memcpy(buf, dmabuf, min(buf_size, hw_len));
+
+ return 0;
+}
+
static int asus_kbd_init(struct hid_device *hdev, u8 report_id)
{
/*
@@ -604,8 +687,13 @@ static int asus_kbd_init(struct hid_device *hdev, u8 report_id)
*/
const u8 buf[] = { report_id, 0x41, 0x53, 0x55, 0x53, 0x20, 0x54,
0x65, 0x63, 0x68, 0x2e, 0x49, 0x6e, 0x63, 0x2e, 0x00 };
+ size_t hw_len = asus_report_len(hdev, report_id);
+ size_t cmp_len;
int ret;
+ if (!hw_len)
+ return -EINVAL;
+
ret = asus_kbd_set_report(hdev, buf, sizeof(buf));
if (ret < 0) {
hid_err(hdev, "Asus handshake %02x failed to send: %d\n",
@@ -613,19 +701,20 @@ static int asus_kbd_init(struct hid_device *hdev, u8 report_id)
return ret;
}
- u8 *readbuf __free(kfree) = kzalloc(FEATURE_KBD_REPORT_SIZE, GFP_KERNEL);
+ u8 *readbuf __free(kfree) = kzalloc(hw_len, GFP_KERNEL);
if (!readbuf)
return -ENOMEM;
- ret = hid_hw_raw_request(hdev, report_id, readbuf,
- FEATURE_KBD_REPORT_SIZE, HID_FEATURE_REPORT,
- HID_REQ_GET_REPORT);
+ ret = asus_kbd_get_report(hdev, report_id, readbuf, hw_len);
if (ret < 0) {
hid_warn(hdev, "Asus handshake %02x failed to receive ack: %d\n",
report_id, ret);
- } else if (memcmp(readbuf, buf, sizeof(buf)) != 0) {
- hid_warn(hdev, "Asus handshake %02x returned invalid response: %*ph\n",
- report_id, FEATURE_KBD_REPORT_SIZE, readbuf);
+ } else {
+ cmp_len = min_t(size_t, sizeof(buf), hw_len);
+ if (memcmp(readbuf, buf, cmp_len) != 0) {
+ hid_warn(hdev, "Asus handshake %02x returned invalid response: %*ph\n",
+ report_id, (int)cmp_len, readbuf);
+ }
}
/*
@@ -640,32 +729,35 @@ static int asus_kbd_get_functions(struct hid_device *hdev,
u8 report_id)
{
const u8 buf[] = { report_id, 0x05, 0x20, 0x31, 0x00, 0x08 };
- u8 *readbuf;
+ size_t hw_len = asus_report_len(hdev, report_id);
int ret;
+ /*
+ * Function flags are at byte 6. A shorter report does not
+ * contain that byte.
+ */
+ if (hw_len < 7)
+ return -EINVAL;
+
ret = asus_kbd_set_report(hdev, buf, sizeof(buf));
if (ret < 0) {
hid_err(hdev, "Asus failed to send configuration command: %d\n", ret);
return ret;
}
- readbuf = kzalloc(FEATURE_KBD_REPORT_SIZE, GFP_KERNEL);
+ u8 *readbuf __free(kfree) = kzalloc(hw_len, GFP_KERNEL);
if (!readbuf)
return -ENOMEM;
- ret = hid_hw_raw_request(hdev, report_id, readbuf,
- FEATURE_KBD_REPORT_SIZE, HID_FEATURE_REPORT,
- HID_REQ_GET_REPORT);
+ ret = asus_kbd_get_report(hdev, report_id, readbuf, hw_len);
if (ret < 0) {
hid_err(hdev, "Asus failed to request functions: %d\n", ret);
- kfree(readbuf);
return ret;
}
*kbd_func = readbuf[6];
- kfree(readbuf);
- return ret;
+ return 0;
}
static int asus_kbd_disable_oobe(struct hid_device *hdev)
@@ -690,7 +782,7 @@ static int asus_kbd_disable_oobe(struct hid_device *hdev)
static void asus_kbd_set_fn_lock(struct hid_device *hdev, bool enabled)
{
- const u8 buf[FEATURE_KBD_REPORT_SIZE] = { FEATURE_KBD_REPORT_ID, 0xd0, 0x4e, !!enabled };
+ const u8 buf[] = { FEATURE_KBD_REPORT_ID, 0xd0, 0x4e, !!enabled };
int ret;
ret = asus_kbd_set_report(hdev, buf, sizeof(buf));
@@ -700,7 +792,7 @@ static void asus_kbd_set_fn_lock(struct hid_device *hdev, bool enabled)
static void asus_kbd_set_brightness(struct hid_device *hdev, u8 brightness)
{
- const u8 buf[FEATURE_KBD_REPORT_SIZE] = {
+ const u8 buf[] = {
FEATURE_KBD_REPORT_ID, 0xba, 0xc5, 0xc4, brightness
};
int ret;
@@ -900,10 +992,15 @@ static int mcu_parse_version_string(const u8 *response, size_t response_size)
static int mcu_request_version(struct hid_device *hdev)
{
- u8 *response __free(kfree) = kzalloc(ROG_ALLY_REPORT_SIZE, GFP_KERNEL);
- const u8 request[] = { 0x5a, 0x05, 0x03, 0x31, 0x00, 0x20 };
+ size_t hw_len = asus_report_len(hdev, FEATURE_KBD_REPORT_ID);
+ const u8 request[] = { FEATURE_KBD_REPORT_ID, 0x05, 0x03, 0x31, 0x00, 0x20 };
int ret;
+ if (!hw_len)
+ return -EINVAL;
+
+ u8 *response __free(kfree) = kzalloc(hw_len, GFP_KERNEL);
+
if (!response)
return -ENOMEM;
@@ -911,17 +1008,16 @@ static int mcu_request_version(struct hid_device *hdev)
if (ret < 0)
return ret;
- ret = hid_hw_raw_request(hdev, FEATURE_REPORT_ID, response,
- ROG_ALLY_REPORT_SIZE, HID_FEATURE_REPORT,
- HID_REQ_GET_REPORT);
+ ret = hid_hw_raw_request(hdev, FEATURE_REPORT_ID, response, hw_len,
+ HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
if (ret < 0)
return ret;
- ret = mcu_parse_version_string(response, ROG_ALLY_REPORT_SIZE);
+ ret = mcu_parse_version_string(response, hw_len);
if (ret < 0) {
pr_err("Failed to parse MCU version: %d\n", ret);
print_hex_dump(KERN_ERR, "MCU: ", DUMP_PREFIX_NONE,
- 16, 1, response, ROG_ALLY_REPORT_SIZE, false);
+ 16, 1, response, hw_len, false);
}
return ret;
@@ -958,17 +1054,12 @@ static void validate_mcu_fw_version(struct hid_device *hdev, int idProduct)
static bool asus_has_report_id(struct hid_device *hdev, u16 report_id)
{
- struct hid_report *report;
- int t;
+ if (report_id >= HID_MAX_IDS)
+ return false;
- for (t = HID_INPUT_REPORT; t <= HID_FEATURE_REPORT; t++) {
- list_for_each_entry(report, &hdev->report_enum[t].report_list, list) {
- if (report->id == report_id)
- return true;
- }
- }
-
- return false;
+ return asus_find_report(hdev, HID_INPUT_REPORT, report_id) ||
+ asus_find_report(hdev, HID_OUTPUT_REPORT, report_id) ||
+ asus_find_report(hdev, HID_FEATURE_REPORT, report_id);
}
static int asus_kbd_register_leds(struct hid_device *hdev)
@@ -1171,17 +1262,15 @@ static int asus_battery_probe(struct hid_device *hdev)
static int asus_xgm_led_set(struct led_classdev *led_cdev, enum led_brightness value)
{
- const u8 buf[ROG_XGM_REPORT_SIZE] = {
- FEATURE_KBD_LED_REPORT_ID2, 0xC5, (value) ? 0x50 : 0x00
- };
+ const u8 buf[] = { FEATURE_KBD_LED_REPORT_ID2, 0xC5, (value) ? 0x50 : 0x00 };
struct asus_xgm_led *xgm = container_of(led_cdev, struct asus_xgm_led, cdev);
int ret;
- ret = asus_kbd_set_report(xgm->hdev, buf, ROG_XGM_REPORT_SIZE);
+ ret = asus_kbd_set_report(xgm->hdev, buf, sizeof(buf));
if (ret < 0) {
hid_err(xgm->hdev, "Unable to set XG mobile led state: %d\n", ret);
return ret;
- } else if (ret != ROG_XGM_REPORT_SIZE) {
+ } else if (ret != (int)asus_report_len(xgm->hdev, FEATURE_KBD_LED_REPORT_ID2)) {
hid_err(xgm->hdev, "Unexpected partial transfer to XG mobile: %d\n", ret);
return -EIO;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH v13 02/18] HID: asus: add support for ROG Ally handhelds
2026-11-04 1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
2026-11-04 1:39 ` [PATCH v13 01/18] HID: asus: size feature reports from the HID report descriptor Denis Benato
@ 2026-11-04 1:39 ` Denis Benato
2026-11-04 2:07 ` sashiko-bot
2026-11-04 1:39 ` [PATCH v13 03/18] HID: asus: add gamepad configuration Denis Benato
` (15 subsequent siblings)
17 siblings, 1 reply; 25+ messages in thread
From: Denis Benato @ 2026-11-04 1:39 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
Implement the core functionality for ASUS handhelds:
- extend functionality of hid-asus if driver is enabled
- initialise the device to a "ready" state
- implement Ally X input mapping
- emit a single proper event for AC button long press
Each interface keeps its own state and reports on its own evdev.
Signed-off-by: Khamunetri Clark <khamunetriclark@gmail.com>
Signed-off-by: Denis Benato <denis.benato@linux.dev>
Signed-off-by: Matthew Schwartz <matthew.schwartz@linux.dev>
Co-developed-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Jonathan LoBue <jlobue10@gmail.com>
---
drivers/hid/hid-asus.c | 1008 ++++++++++++++++++++++++++++++++++++----
1 file changed, 909 insertions(+), 99 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 46643022b68d..2a3e8c399f0a 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -22,16 +22,23 @@
#include <linux/acpi.h>
#include <linux/cleanup.h>
+#include <linux/compiler_attributes.h>
#include <linux/container_of.h>
#include <linux/dmi.h>
+#include <linux/err.h>
#include <linux/hid.h>
+#include <linux/jiffies.h>
+#include <linux/list.h>
#include <linux/module.h>
+#include <linux/mutex.h>
#include <linux/platform_data/x86/asus-wmi.h>
#include <linux/input/mt.h>
#include <linux/usb.h> /* For to_usb_interface for T100 touchpad intf check */
#include <linux/power_supply.h>
+#include <linux/stddef.h>
#include <linux/leds.h>
#include <linux/types.h>
+#include <linux/unaligned.h>
#include "hid-ids.h"
@@ -39,6 +46,9 @@ MODULE_AUTHOR("Yusuke Fujimaki <usk.fujimaki@gmail.com>");
MODULE_AUTHOR("Brendan McGrath <redmcg@redmandi.dyndns.org>");
MODULE_AUTHOR("Victor Vlasenko <victor.vlasenko@sysgears.com>");
MODULE_AUTHOR("Frederik Wenigwieser <frederik.wenigwieser@gmail.com>");
+MODULE_AUTHOR("Denis Benato <denis.benato@linux.dev>");
+MODULE_AUTHOR("Luke Jones <luke@ljones.dev>");
+MODULE_AUTHOR("Khamunetri Clark <khamunetriclark@gmail.com>");
MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define T100_TPAD_INTF 2
@@ -56,6 +66,30 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define ROG_ALLY_X_MIN_MCU 313
#define ROG_ALLY_MIN_MCU 319
+#define HID_ALLY_INTF_KEYBOARD_IN 0x81
+#define HID_ALLY_INTF_CFG_IN 0x83
+#define HID_ALLY_X_INTF_IN 0x87
+
+#define HID_ALLY_GET_REPORT_ID 0x0D
+#define HID_ALLY_FEATURE_CODE_PAGE 0xD1
+
+#define HID_ALLY_X_INPUT_REPORT 0x0B
+
+#define HID_ALLY_READY_MAX_TRIES 6
+
+#define HID_INPUT_KEY_DELETE 0x4c
+#define HID_INPUT_KEY_F20 0x6f
+#define HID_INPUT_LCTRL_MODIFIER 0x01
+#define HID_INPUT_LALT_MODIFIER 0x04
+#define HID_INPUT_LCTRL_LALT (HID_INPUT_LCTRL_MODIFIER | HID_INPUT_LALT_MODIFIER)
+
+#define HID_INPUT_REPORT_ID_OFFSET 0
+#define HID_INPUT_MODIFIER_OFFSET 1
+#define HID_INPUT_RESERVED_OFFSET 2
+#define HID_INPUT_KEY_OFFSET 3
+
+#define ASUS_HID_INPUT_KBD_REPORT_ID 0x01
+
/* Spurious HID codes sent by QUIRK_ROG_NKEY_KEYBOARD devices */
#define ASUS_SPURIOUS_CODE_0XEA 0xea
#define ASUS_SPURIOUS_CODE_0XEC 0xec
@@ -160,6 +194,17 @@ struct asus_touchpad_info {
int report_size;
};
+struct ally_intf {
+ struct hid_device *hdev;
+ /* HID-core evdev of this interface. Reports for it land here. */
+ struct input_dev *input;
+ /* Private gamepad node for report 0x0B. NULL on the other interfaces. */
+ struct input_dev *gamepad;
+ u8 cad_state;
+ unsigned long cad_last_event_time;
+ struct delayed_work resume_work;
+};
+
struct asus_drvdata {
unsigned long quirks;
struct hid_device *hdev;
@@ -167,6 +212,7 @@ struct asus_drvdata {
struct input_dev *tp_kbd_input;
struct asus_worker *worker;
unsigned int kbd_backlight_brightness;
+ struct ally_intf *ally;
const struct asus_touchpad_info *tp;
struct power_supply *battery;
struct power_supply_desc battery_desc;
@@ -239,12 +285,816 @@ static const struct asus_touchpad_info medion_e1239t_tp = {
.report_size = 32 /* 2 byte header + 5 * 5 + 5 byte footer */,
};
+enum ally_command_codes {
+ CMD_SET_GAMEPAD_MODE = 0x01,
+ CMD_SET_MAPPING = 0x02,
+ CMD_SET_JOYSTICK_MAPPING = 0x03,
+ CMD_SET_JOYSTICK_DEADZONE = 0x04,
+ CMD_SET_TRIGGER_RANGE = 0x05,
+ CMD_SET_VIBRATION_INTENSITY = 0x06,
+ CMD_LED_CONTROL = 0x08,
+ CMD_CHECK_READY = 0x0A,
+ CMD_SET_XBOX_CONTROLLER = 0x0B,
+ CMD_CHECK_XBOX_SUPPORT = 0x0C,
+ CMD_USER_CAL_DATA = 0x0D,
+ CMD_CHECK_USER_CAL_SUPPORT = 0x0E,
+ CMD_SET_TURBO_PARAMS = 0x0F,
+ CMD_CHECK_TURBO_SUPPORT = 0x10,
+ CMD_CHECK_RESP_CURVE_SUPPORT = 0x12,
+ CMD_SET_RESP_CURVE = 0x13,
+ CMD_CHECK_DIR_TO_BTN_SUPPORT = 0x14,
+ CMD_SET_GYRO_PARAMS = 0x15,
+ CMD_CHECK_GYRO_TO_JOYSTICK = 0x16,
+ CMD_CHECK_ANTI_DEADZONE = 0x17,
+ CMD_SET_ANTI_DEADZONE = 0x18,
+};
+
+static const u8 ally_force_feedback_off[] = {
+ 0x0D, 0x0F, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xEB
+};
+
+/*
+ * Each Ally interface owns its own ally_intf. A report is delivered on the
+ * evdev of the interface that received it. Nothing here points at another
+ * interface.
+ */
+static void ally_release_vendor_keys(struct input_dev *input)
+{
+ if (!input)
+ return;
+
+ input_report_key(input, KEY_F16, 0);
+ input_report_key(input, KEY_F17, 0);
+ input_report_key(input, KEY_F18, 0);
+ input_report_key(input, KEY_PROG1, 0);
+ input_sync(input);
+}
+
+static void ally_resume_work_fn(struct work_struct *work)
+{
+ struct ally_intf *intf = container_of(work, struct ally_intf,
+ resume_work.work);
+
+ /*
+ * Force release vendor buttons to prevent stuck keys on resume
+ * (workaround for Ally X USB re-probing during suspend/resume).
+ */
+ ally_release_vendor_keys(intf->input);
+}
+
+static void ally_quiesce(struct ally_intf *intf)
+{
+ if (!intf)
+ return;
+
+ /*
+ * Disable so a concurrent asus_resume() cannot re-queue after the
+ * cancel. drvdata->ally is cleared before this runs.
+ */
+ disable_delayed_work_sync(&intf->resume_work);
+}
+
+static void ally_unregister_gamepad(struct ally_intf *intf)
+{
+ if (!intf || !intf->gamepad)
+ return;
+
+ input_unregister_device(intf->gamepad);
+ intf->gamepad = NULL;
+}
+
+static void ally_free(struct ally_intf *intf)
+{
+ kfree(intf);
+}
+
+/**
+ * handle_ctrl_alt_del() - detect a left button long press
+ * @intf: ally interface holding the sequence state
+ * @data: raw report buffer, rewritten in place when the sequence matches
+ * @size: length of @data in bytes
+ *
+ * The Ally left button emits a sequence of ctrl+alt+del events. Capture that
+ * and emit only a single code for that single event by rewriting @data in
+ * place. The return value tells the caller whether ally processing of this
+ * report should stop.
+ *
+ * Return: true when this step was recognized and @data was rewritten, so the
+ * caller stops and the generic parser consumes the rewritten report. false
+ * when @data was left unchanged and ally processing should continue.
+ */
+static bool handle_ctrl_alt_del(struct ally_intf *intf, u8 *data, int size)
+{
+ bool time_is_past = time_after(jiffies,
+ intf->cad_last_event_time + msecs_to_jiffies(100));
+
+ if (size < 4 || data[HID_INPUT_REPORT_ID_OFFSET] != ASUS_HID_INPUT_KBD_REPORT_ID)
+ return false;
+
+ if (intf->cad_state > 0 && time_is_past)
+ intf->cad_state = 0;
+
+ intf->cad_last_event_time = jiffies;
+ switch (intf->cad_state) {
+ case 0:
+ if ((data[HID_INPUT_MODIFIER_OFFSET] == HID_INPUT_LCTRL_MODIFIER) &&
+ (data[HID_INPUT_RESERVED_OFFSET] == 0x00) &&
+ (data[HID_INPUT_KEY_OFFSET] == 0x00)) {
+ intf->cad_state = 1;
+ data[HID_INPUT_MODIFIER_OFFSET] = 0x00;
+ return true;
+ }
+ break;
+ case 1:
+ if ((data[HID_INPUT_MODIFIER_OFFSET] == HID_INPUT_LCTRL_LALT) &&
+ (data[HID_INPUT_RESERVED_OFFSET] == 0x00) &&
+ (data[HID_INPUT_KEY_OFFSET] == 0x00)) {
+ intf->cad_state = 2;
+ data[HID_INPUT_MODIFIER_OFFSET] = 0x00;
+ return true;
+ }
+ break;
+ case 2:
+ if ((data[HID_INPUT_MODIFIER_OFFSET] == HID_INPUT_LCTRL_LALT) &&
+ (data[HID_INPUT_RESERVED_OFFSET] == 0x00) &&
+ (data[HID_INPUT_KEY_OFFSET] == HID_INPUT_KEY_DELETE)) {
+ intf->cad_state = 3;
+ data[HID_INPUT_MODIFIER_OFFSET] = 0x00;
+ data[HID_INPUT_KEY_OFFSET] = HID_INPUT_KEY_F20;
+ return true;
+ }
+ break;
+ case 3:
+ if ((data[HID_INPUT_MODIFIER_OFFSET] == HID_INPUT_LALT_MODIFIER) &&
+ (data[HID_INPUT_RESERVED_OFFSET] == 0x00) &&
+ (data[HID_INPUT_KEY_OFFSET] == HID_INPUT_KEY_DELETE)) {
+ intf->cad_state = 4;
+ data[HID_INPUT_MODIFIER_OFFSET] = 0x00;
+ data[HID_INPUT_KEY_OFFSET] = 0x00;
+ return true;
+ }
+ break;
+ case 4:
+ if ((data[HID_INPUT_MODIFIER_OFFSET] == 0x00) &&
+ (data[HID_INPUT_RESERVED_OFFSET] == 0x00) &&
+ (data[HID_INPUT_KEY_OFFSET] == HID_INPUT_KEY_DELETE)) {
+ intf->cad_state = 5;
+ data[HID_INPUT_KEY_OFFSET] = 0x00;
+ return true;
+ }
+ break;
+ }
+ intf->cad_state = 0;
+ return false;
+}
+
+/**
+ * ally_vendor_keys() - Report a vendor button on this interface's evdev
+ * @input: HID-core input device of the interface that received the report
+ * @data: raw report buffer
+ * @size: length of @data in bytes
+ *
+ * Handles report 0x5A. A vendor key is reported down while @data[1] matches
+ * its code (0x38 is the ROG key, KEY_PROG1; 0xA6, 0xA7 and 0xA8 are KEY_F16,
+ * KEY_F17 and KEY_F18). @data[1] == 0 releases every known vendor key. Any
+ * other code is ignored here and left to hid-core and the later filters. The
+ * same decode is used for every Ally interface.
+ *
+ * Return: true if @data[1] is one of the vendor codes (0x38, 0xA6, 0xA7,
+ * 0xA8), in which case the report is consumed. false otherwise, including
+ * the @data[1] == 0 report that releases the vendor keys, which must still
+ * reach hid-core.
+ */
+static bool ally_vendor_keys(struct input_dev *input, const u8 *data, int size)
+{
+ bool is_vendor;
+
+ if (size < 2 || data[0] != FEATURE_KBD_REPORT_ID)
+ return false;
+
+ is_vendor = data[1] == 0x38 || data[1] == 0xA6 ||
+ data[1] == 0xA7 || data[1] == 0xA8;
+
+ /* Neither a vendor press nor a release: leave it to the other handlers. */
+ if (!is_vendor && data[1] != 0)
+ return false;
+
+ if (input) {
+ /* AC short press */
+ input_report_key(input, KEY_PROG1, data[1] == 0x38);
+
+ /* CC short press & release */
+ input_report_key(input, KEY_F16, data[1] == 0xA6);
+
+ /* AC long press */
+ input_report_key(input, KEY_F17, data[1] == 0xA7);
+
+ /* AC long release */
+ input_report_key(input, KEY_F18, data[1] == 0xA8);
+
+ input_sync(input);
+ }
+
+ /*
+ * Only the vendor codes are consumed. They are reported on this
+ * interface's evdev above, so hid-core never sees them pressed. Note
+ * that 0xA8 ("AC long release") is itself a usage the firmware sends,
+ * not the report that clears one. Everything else, including the 0x00
+ * report that clears a held usage, must still reach hid-core: it can
+ * only release the usages it saw pressed if it sees the report that
+ * clears them.
+ */
+ return is_vendor;
+}
+
+/**
+ * asus_find_report() - Find a report of the given type by its ID
+ * @hdev: HID device
+ * @type: report type (HID_INPUT_REPORT, HID_OUTPUT_REPORT or
+ * HID_FEATURE_REPORT)
+ * @report_id: report ID to look for
+ *
+ * Return: the report with the given ID, or NULL if the device does not
+ * declare such a report.
+ */
+static struct hid_report *asus_find_report(struct hid_device *hdev,
+ unsigned int type, u8 report_id)
+{
+ return hdev->report_enum[type].report_id_hash[report_id];
+}
+
+/**
+ * asus_report_len() - Length of a feature report for the device
+ * @hdev: HID device
+ * @report_id: report ID of the feature report
+ *
+ * The same ASUS report ID can have different sizes depending on the device:
+ * report 0x5E is a 64-byte report on laptops but a 300-byte report on XG
+ * Mobile stations. Always size transfers from the report descriptor of the
+ * device at hand instead of hardcoding lengths.
+ *
+ * Return: the length of the feature report in bytes, or 0 if the device
+ * does not declare the report.
+ */
+static size_t asus_report_len(struct hid_device *hdev, u8 report_id)
+{
+ struct hid_report *report;
+
+ report = asus_find_report(hdev, HID_FEATURE_REPORT, report_id);
+
+ return report ? hid_report_len(report) : 0;
+}
+
+static int asus_kbd_set_report(struct hid_device *hdev, const u8 *buf, size_t buf_size)
+{
+ size_t hw_len = asus_report_len(hdev, buf[0]);
+
+ /* The payload (including the report ID) must fit the device report. */
+ if (!hw_len || buf_size > hw_len)
+ return -EINVAL;
+
+ u8 *dmabuf __free(kfree) = kzalloc(hw_len, GFP_KERNEL);
+ if (!dmabuf)
+ return -ENOMEM;
+
+ memcpy(dmabuf, buf, buf_size);
+
+ /*
+ * The report ID should be set from the incoming buffer due to LED and
+ * key interfaces having different pages. Short payloads are zero-padded
+ * to the exact report size the hardware expects for this report ID.
+ */
+ return hid_hw_raw_request(hdev, buf[0], dmabuf, hw_len, HID_FEATURE_REPORT,
+ HID_REQ_SET_REPORT);
+}
+
+/**
+ * asus_kbd_get_report() - Read a report sized from the descriptor
+ * @hdev: HID device
+ * @report_id: report ID to read
+ * @buf: buffer to store the report in
+ * @buf_size: size of @buf
+ *
+ * The transfer is performed at the exact report size the device declares
+ * for @report_id. The device may answer with a short read, and only the
+ * bytes that fit in @buf are copied into it.
+ *
+ * Return: 0 on success, negative errno on failure.
+ */
+static int asus_kbd_get_report(struct hid_device *hdev, u8 report_id,
+ u8 *buf, size_t buf_size)
+{
+ size_t hw_len = asus_report_len(hdev, report_id);
+ int ret;
+
+ if (!hw_len)
+ return -EINVAL;
+
+ u8 *dmabuf __free(kfree) = kzalloc(hw_len, GFP_KERNEL);
+ if (!dmabuf)
+ return -ENOMEM;
+
+ ret = hid_hw_raw_request(hdev, report_id, dmabuf, hw_len,
+ HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
+ if (ret < 0)
+ return ret;
+
+ /* The device may return less than requested (short read). */
+ memcpy(buf, dmabuf, min(buf_size, hw_len));
+
+ return 0;
+}
+
static const u8 asus_report_id_init[] = {
FEATURE_KBD_REPORT_ID,
FEATURE_KBD_LED_REPORT_ID1,
FEATURE_KBD_LED_REPORT_ID2
};
+static int ally_dev_set_report(struct hid_device *hdev, const u8 *buf, size_t len)
+{
+ return asus_kbd_set_report(hdev, buf, len);
+}
+
+static int ally_dev_get_report(struct hid_device *hdev, u8 *out, size_t len)
+{
+ return hid_hw_raw_request(hdev, HID_ALLY_GET_REPORT_ID, out, len,
+ HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
+}
+
+/**
+ * ally_gamepad_send_packet() - Send a raw packet to the gamepad device
+ * @hdev: HID device
+ * @buf: buffer containing the packet data
+ * @len: length of data to send
+ *
+ * Return: count of data transferred, negative if error.
+ */
+static int ally_gamepad_send_packet(struct hid_device *hdev, const u8 *buf, size_t len)
+{
+ return ally_dev_set_report(hdev, buf, len);
+}
+
+/**
+ * ally_gamepad_send_receive_packet() - Send a packet and receive the response
+ * @hdev: HID device
+ * @buf: buffer containing the packet data to send and receive response in
+ * @len: length of buffer
+ *
+ * Return: 0 on success, negative errno on failure.
+ */
+static int ally_gamepad_send_receive_packet(struct hid_device *hdev,
+ u8 *buf, size_t len)
+{
+ int ret;
+
+ ret = ally_dev_set_report(hdev, buf, len);
+ if (ret >= 0) {
+ memset(buf, 0, len);
+ ret = ally_dev_get_report(hdev, buf, len);
+ }
+
+ return ret;
+}
+
+/**
+ * ally_alloc_cmd() - Construct a command buffer for the gamepad
+ * @hdev: HID device the command is built for
+ * @cmd: Command code to send
+ * @payload: Optional payload data to include in the command
+ * @payload_size: Size of the payload data
+ * @size: Where to store the length of the allocated buffer
+ *
+ * The buffer is sized after the feature report the device declares for
+ * the gamepad commands, and it is the caller responsibility to free the
+ * buffer using kfree().
+ *
+ * Return: the newly allocated buffer containing the command, or NULL on
+ * allocation failure or if the payload would not fit in the report.
+ */
+static u8 *ally_alloc_cmd(struct hid_device *hdev, u8 cmd, const u8 *payload,
+ u8 payload_size, size_t *size)
+{
+ size_t hw_len = asus_report_len(hdev, FEATURE_KBD_REPORT_ID);
+ u8 *hidbuf;
+
+ /*
+ * The payload is written after the 4-byte command header:
+ * reject one that would spill past the end of the report.
+ */
+ if (hw_len < 4 || payload_size > hw_len - 4)
+ return NULL;
+
+ hidbuf = kzalloc(hw_len, GFP_KERNEL);
+ if (!hidbuf)
+ return NULL;
+
+ hidbuf[0] = FEATURE_KBD_REPORT_ID;
+ hidbuf[1] = HID_ALLY_FEATURE_CODE_PAGE;
+ hidbuf[2] = cmd;
+ hidbuf[3] = payload_size;
+
+ if (payload_size > 0 && payload)
+ memcpy(&hidbuf[4], payload, payload_size);
+
+ *size = hw_len;
+
+ return hidbuf;
+}
+
+/**
+ * ally_gamepad_check_ready() - Wait for the gamepad MCU to report ready
+ * @hdev: HID device
+ *
+ * This should be called before any remapping attempts, and on driver
+ * init/resume, after the asus handshake has been performed on the
+ * configuration endpoint.
+ *
+ * Return: 0 on success, negative errno on failure.
+ */
+static int ally_gamepad_check_ready(struct hid_device *hdev)
+{
+ const u8 payload[] = { 0x00 };
+ size_t buf_len;
+ int ret;
+
+ for (int i = 0; i < HID_ALLY_READY_MAX_TRIES; i++) {
+ u8 *buf __free(kfree) = ally_alloc_cmd(hdev, CMD_CHECK_READY,
+ payload, sizeof(payload),
+ &buf_len);
+ if (!buf)
+ return -ENOMEM;
+
+ ret = ally_gamepad_send_receive_packet(hdev, buf, buf_len);
+ if (ret < 0)
+ hid_dbg(hdev, "ROG Ally check %d/%d failed: %d\n", i,
+ HID_ALLY_READY_MAX_TRIES, ret);
+ else if (buf[2] == CMD_CHECK_READY)
+ return 0;
+
+ /* Back off on a bus error as well as on a not-ready answer. */
+ usleep_range(1000, 2000);
+ }
+
+ hid_err(hdev, "ROG Ally never responded with a ready\n");
+ return -ENODEV;
+}
+
+static int ally_get_endpoint_address(struct hid_device *hdev)
+{
+ struct usb_host_endpoint *ep;
+ struct usb_interface *intf;
+
+ if (!hid_is_usb(hdev))
+ return -ENODEV;
+
+ intf = to_usb_interface(hdev->dev.parent);
+ if (!intf || !intf->cur_altsetting)
+ return -ENODEV;
+
+ if (intf->cur_altsetting->desc.bNumEndpoints == 0)
+ return -ENODEV;
+
+ ep = intf->cur_altsetting->endpoint;
+ if (!ep)
+ return -ENODEV;
+
+ return ep->desc.bEndpointAddress;
+}
+
+struct ally_x_input_report {
+ __le16 x, y;
+ __le16 rx, ry;
+ __le16 z, rz;
+ u8 buttons[3];
+} __packed;
+
+/* The hatswitch outputs integers, we use them to index this X|Y pair */
+static const int hat_values[][2] = {
+ { 0, 0 }, { 0, -1 }, { 1, -1 }, { 1, 0 }, { 1, 1 },
+ { 0, 1 }, { -1, 1 }, { -1, 0 }, { -1, -1 },
+};
+
+/**
+ * ally_x_raw_event() - Parse and report an Ally X gamepad input report
+ * @input: input device to report the events on
+ * @data: raw report buffer
+ * @size: length of @data in bytes
+ *
+ * Return: true if the event was handled, otherwise false.
+ */
+static bool ally_x_raw_event(struct input_dev *input, u8 *data, int size)
+{
+ struct ally_x_input_report *in_report;
+ u16 x, y, rx, ry, z, rz;
+ u8 byte;
+
+ if (!input)
+ return false;
+
+ if (size < 1)
+ return false;
+
+ if (data[0] != HID_ALLY_X_INPUT_REPORT)
+ return false;
+
+ /*
+ * hid-core only guarantees size >= 1 and does not zero-pad short
+ * reports before ->raw_event, so a truncated transfer would leave the
+ * payload below pointing at stale DMA buffer contents.
+ */
+ if (size < 1 + sizeof(*in_report))
+ return false;
+
+ in_report = (struct ally_x_input_report *)&data[1];
+
+ /* USB HID payloads are little-endian: convert them explicitly. */
+ x = get_unaligned_le16(&in_report->x);
+ y = get_unaligned_le16(&in_report->y);
+ rx = get_unaligned_le16(&in_report->rx);
+ ry = get_unaligned_le16(&in_report->ry);
+ z = get_unaligned_le16(&in_report->z);
+ rz = get_unaligned_le16(&in_report->rz);
+
+ input_report_abs(input, ABS_X, x - 32768);
+ input_report_abs(input, ABS_Y, y - 32768);
+ input_report_abs(input, ABS_RX, rx - 32768);
+ input_report_abs(input, ABS_RY, ry - 32768);
+ input_report_abs(input, ABS_Z, z);
+ input_report_abs(input, ABS_RZ, rz);
+
+ byte = in_report->buttons[0];
+ input_report_key(input, BTN_A, byte & BIT(0));
+ input_report_key(input, BTN_B, byte & BIT(1));
+ input_report_key(input, BTN_X, byte & BIT(2));
+ input_report_key(input, BTN_Y, byte & BIT(3));
+ input_report_key(input, BTN_TL, byte & BIT(4));
+ input_report_key(input, BTN_TR, byte & BIT(5));
+ input_report_key(input, BTN_SELECT, byte & BIT(6));
+ input_report_key(input, BTN_START, byte & BIT(7));
+
+ byte = in_report->buttons[1];
+ input_report_key(input, BTN_THUMBL, byte & BIT(0));
+ input_report_key(input, BTN_THUMBR, byte & BIT(1));
+ input_report_key(input, BTN_MODE, byte & BIT(2));
+
+ /*
+ * The hatswitch byte is device-controlled; treat anything the table
+ * does not cover as centred rather than indexing out of bounds.
+ */
+ byte = in_report->buttons[2];
+ if (byte >= ARRAY_SIZE(hat_values))
+ byte = 0;
+ input_report_abs(input, ABS_HAT0X, hat_values[byte][0]);
+ input_report_abs(input, ABS_HAT0Y, hat_values[byte][1]);
+
+ input_sync(input);
+
+ return true;
+}
+
+static int ally_x_input_open(struct input_dev *dev)
+{
+ struct ally_intf *intf = input_get_drvdata(dev);
+
+ return hid_hw_open(intf->hdev);
+}
+
+static void ally_x_input_close(struct input_dev *dev)
+{
+ struct ally_intf *intf = input_get_drvdata(dev);
+
+ hid_hw_close(intf->hdev);
+}
+
+static struct input_dev *ally_x_alloc_input_dev(struct hid_device *hdev,
+ struct ally_intf *intf)
+{
+ struct input_dev *input_dev = input_allocate_device();
+
+ if (!input_dev)
+ return ERR_PTR(-ENOMEM);
+
+ input_dev->dev.parent = &hdev->dev;
+ input_dev->id.bustype = hdev->bus;
+ input_dev->id.vendor = hdev->vendor;
+ input_dev->id.product = hdev->product;
+ input_dev->id.version = hdev->version;
+ input_dev->uniq = hdev->uniq;
+ input_dev->name = "ASUS ROG Ally X Gamepad";
+
+ input_set_drvdata(input_dev, intf);
+ /*
+ * Let the input core hold the hardware open while the device node
+ * is in use: without these the interface could be runtime-suspended
+ * and drop events.
+ */
+ input_dev->open = ally_x_input_open;
+ input_dev->close = ally_x_input_close;
+
+ return input_dev;
+}
+
+static int ally_x_setup_input(struct hid_device *hdev, struct ally_intf *intf)
+{
+ struct input_dev *input = ally_x_alloc_input_dev(hdev, intf);
+ int ret;
+
+ if (IS_ERR(input))
+ return PTR_ERR(input);
+
+ input_set_abs_params(input, ABS_X, -32768, 32767, 0, 0);
+ input_set_abs_params(input, ABS_Y, -32768, 32767, 0, 0);
+ input_set_abs_params(input, ABS_RX, -32768, 32767, 0, 0);
+ input_set_abs_params(input, ABS_RY, -32768, 32767, 0, 0);
+ input_set_abs_params(input, ABS_Z, 0, 1023, 0, 0);
+ input_set_abs_params(input, ABS_RZ, 0, 1023, 0, 0);
+ input_set_abs_params(input, ABS_HAT0X, -1, 1, 0, 0);
+ input_set_abs_params(input, ABS_HAT0Y, -1, 1, 0, 0);
+ input_set_capability(input, EV_KEY, BTN_A);
+ input_set_capability(input, EV_KEY, BTN_B);
+ input_set_capability(input, EV_KEY, BTN_X);
+ input_set_capability(input, EV_KEY, BTN_Y);
+ input_set_capability(input, EV_KEY, BTN_TL);
+ input_set_capability(input, EV_KEY, BTN_TR);
+ input_set_capability(input, EV_KEY, BTN_SELECT);
+ input_set_capability(input, EV_KEY, BTN_START);
+ input_set_capability(input, EV_KEY, BTN_MODE);
+ input_set_capability(input, EV_KEY, BTN_THUMBL);
+ input_set_capability(input, EV_KEY, BTN_THUMBR);
+
+ ret = input_register_device(input);
+ if (ret) {
+ hid_err(hdev, "Failed to register Ally X gamepad device: %d\n", ret);
+ input_free_device(input);
+ return ret;
+ }
+
+ intf->gamepad = input;
+ return 0;
+}
+
+static int hid_asus_ally_init(struct hid_device *hdev)
+{
+ int ret;
+
+ /* Failure at this point is non-critical. The packet goes to this interface. */
+ ret = ally_gamepad_send_packet(hdev, ally_force_feedback_off,
+ sizeof(ally_force_feedback_off));
+ if (ret < 0)
+ hid_err(hdev, "Ally failed to init force-feedback off: %d\n", ret);
+
+ return 0;
+}
+
+/**
+ * hid_asus_ally_raw_event() - Route raw reports from one Ally interface
+ * @hdev: HID device the report arrived on
+ * @intf: that interface's ally state
+ * @data: raw report buffer
+ * @size: length of @data in bytes
+ *
+ * Called from the raw_event callback, which may run in atomic context.
+ * Only this interface's own input devices are used.
+ *
+ * Return: true if the event was handled, otherwise false.
+ */
+static bool hid_asus_ally_raw_event(struct hid_device *hdev, struct ally_intf *intf,
+ u8 *data, int size)
+{
+ if (!intf)
+ return false;
+
+ if (ally_vendor_keys(intf->input, data, size))
+ return true;
+
+ switch (ally_get_endpoint_address(hdev)) {
+ case HID_ALLY_X_INTF_IN:
+ return ally_x_raw_event(intf->gamepad, data, size);
+ case HID_ALLY_INTF_KEYBOARD_IN:
+ if (handle_ctrl_alt_del(intf, data, size))
+ return false;
+ break;
+ default:
+ break;
+ }
+
+ return false;
+}
+
+static struct input_dev *ally_interface_input(struct hid_device *hdev)
+{
+ struct hid_input *hidinput;
+
+ if (list_empty(&hdev->inputs))
+ return NULL;
+
+ hidinput = list_first_entry(&hdev->inputs, struct hid_input, list);
+ return hidinput->input;
+}
+
+/**
+ * hid_asus_ally_probe() - Initialize the ROG Ally HID extension
+ * @hdev: HID device
+ *
+ * The Ally code is a plugin of the Asus HID driver: one ally_intf per
+ * bound interface, freed by the caller after hid_hw_stop(). Interfaces
+ * do not share device pointers, so a second interface cannot evict the
+ * first.
+ *
+ * Return: an ally_intf pointer on success, NULL when this endpoint has no
+ * Ally role, or an ERR_PTR on failure. On failure the function frees
+ * anything it allocated.
+ */
+static struct ally_intf *hid_asus_ally_probe(struct hid_device *hdev)
+{
+ int ret, ep = ally_get_endpoint_address(hdev);
+
+ if (ep < 0)
+ return ERR_PTR(ep);
+
+ if (ep != HID_ALLY_INTF_CFG_IN && ep != HID_ALLY_X_INTF_IN &&
+ ep != HID_ALLY_INTF_KEYBOARD_IN)
+ return NULL;
+
+ struct ally_intf *intf __free(kfree) = kzalloc_obj(*intf);
+
+ if (!intf)
+ return ERR_PTR(-ENOMEM);
+
+ intf->hdev = hdev;
+ intf->input = ally_interface_input(hdev);
+ INIT_DELAYED_WORK(&intf->resume_work, ally_resume_work_fn);
+
+ switch (ep) {
+ case HID_ALLY_INTF_CFG_IN:
+ /*
+ * This function assumes the asus-specific initialization
+ * to have been performed already at this point.
+ */
+ ret = ally_gamepad_check_ready(hdev);
+ if (ret < 0) {
+ hid_err(hdev, "ROG Ally device is not ready: %d\n", ret);
+ return ERR_PTR(ret);
+ }
+
+ hid_asus_ally_init(hdev);
+ break;
+ case HID_ALLY_X_INTF_IN:
+ ret = ally_x_setup_input(hdev, intf);
+ if (ret) {
+ hid_err(hdev, "Failed to create Ally X gamepad device.\n");
+ return ERR_PTR(ret);
+ }
+ break;
+ case HID_ALLY_INTF_KEYBOARD_IN:
+ break;
+ default:
+ return NULL;
+ }
+
+ return no_free_ptr(intf);
+}
+
+static int hid_asus_ally_reset_resume(struct hid_device *hdev, struct ally_intf *intf)
+{
+ int ep;
+
+ /*
+ * The extensions failed to probe and the device is operating as a
+ * generic HID device: do not fail the resume because of that.
+ */
+ if (!intf)
+ return 0;
+
+ ep = ally_get_endpoint_address(hdev);
+ if (ep != HID_ALLY_INTF_CFG_IN)
+ return 0;
+
+ return hid_asus_ally_init(hdev);
+}
+
+static struct ally_intf *hid_asus_ally_terminate(struct asus_drvdata *drvdata)
+{
+ /*
+ * Pairs with smp_store_release() in asus_probe(): ensures the
+ * ally_intf fields set before publication (input, gamepad,
+ * resume_work) are visible before we quiesce and free it.
+ */
+ struct ally_intf *ally = smp_load_acquire(&drvdata->ally);
+
+ if (!ally)
+ return NULL;
+
+ /* Cut resume off before draining work or freeing intf. */
+ smp_store_release(&drvdata->ally, NULL);
+
+ ally_quiesce(ally);
+ ally_unregister_gamepad(ally);
+
+ return ally;
+}
+
/*
* Send events to asus-wmi driver for handling special keys
*/
@@ -496,6 +1346,7 @@ static int asus_raw_event(struct hid_device *hdev,
struct hid_report *report, u8 *data, int size)
{
struct asus_drvdata *drvdata = hid_get_drvdata(hdev);
+ struct ally_intf *intf;
int ret;
if (size < 2) {
@@ -512,6 +1363,19 @@ static int asus_raw_event(struct hid_device *hdev,
if (drvdata->quirks & QUIRK_MEDION_E1239T)
return asus_e1239t_event(drvdata, data, size);
+ if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) {
+ /*
+ * Return -1 to suppress further processing by the generic HID
+ * input parser for reports we fully handle. The interface is
+ * published with smp_store_release() only after its fields
+ * are set. Letting report 0x0B reach the default parser
+ * creates a generic gamepad and Steam Input overlaps.
+ */
+ intf = smp_load_acquire(&drvdata->ally); /* fields are published */
+ if (hid_asus_ally_raw_event(hdev, intf, data, size))
+ return -1;
+ }
+
/*
* Skip these report ID, the device emits a continuous stream associated
* with the AURA mode it is in which looks like an 'echo'.
@@ -581,104 +1445,6 @@ static int asus_raw_event(struct hid_device *hdev,
return 0;
}
-/**
- * asus_find_report() - Find a report of the given type by its ID
- * @hdev: HID device
- * @type: report type (HID_INPUT_REPORT, HID_OUTPUT_REPORT or
- * HID_FEATURE_REPORT)
- * @report_id: report ID to look for
- *
- * Return: the report with the given ID, or NULL if the device does not
- * declare such a report.
- */
-static struct hid_report *asus_find_report(struct hid_device *hdev,
- unsigned int type, u8 report_id)
-{
- return hdev->report_enum[type].report_id_hash[report_id];
-}
-
-/**
- * asus_report_len() - Length of a feature report for the device
- * @hdev: HID device
- * @report_id: report ID of the feature report
- *
- * The same ASUS report ID can have different sizes depending on the device:
- * report 0x5E is a 64-byte report on laptops but a 300-byte report on XG
- * Mobile stations. Always size transfers from the report descriptor of the
- * device at hand instead of hardcoding lengths.
- *
- * Return: the length of the feature report in bytes, or 0 if the device
- * does not declare the report.
- */
-static size_t asus_report_len(struct hid_device *hdev, u8 report_id)
-{
- struct hid_report *report;
-
- report = asus_find_report(hdev, HID_FEATURE_REPORT, report_id);
-
- return report ? hid_report_len(report) : 0;
-}
-
-static int asus_kbd_set_report(struct hid_device *hdev, const u8 *buf, size_t buf_size)
-{
- size_t hw_len = asus_report_len(hdev, buf[0]);
-
- /* The payload (including the report ID) must fit the device report. */
- if (!hw_len || buf_size > hw_len)
- return -EINVAL;
-
- u8 *dmabuf __free(kfree) = kzalloc(hw_len, GFP_KERNEL);
- if (!dmabuf)
- return -ENOMEM;
-
- memcpy(dmabuf, buf, buf_size);
-
- /*
- * The report ID should be set from the incoming buffer due to LED and
- * key interfaces having different pages. Short payloads are zero-padded
- * to the exact report size the hardware expects for this report ID.
- */
- return hid_hw_raw_request(hdev, buf[0], dmabuf, hw_len, HID_FEATURE_REPORT,
- HID_REQ_SET_REPORT);
-}
-
-/**
- * asus_kbd_get_report() - Read a report sized from the descriptor
- * @hdev: HID device
- * @report_id: report ID to read
- * @buf: buffer to store the report in
- * @buf_size: size of @buf
- *
- * The transfer is performed at the exact report size the device declares
- * for @report_id. The device may answer with a short read, and only the
- * bytes that fit in @buf are copied into it.
- *
- * Return: 0 on success, negative errno on failure.
- */
-static int asus_kbd_get_report(struct hid_device *hdev, u8 report_id,
- u8 *buf, size_t buf_size)
-{
- size_t hw_len = asus_report_len(hdev, report_id);
- int ret;
-
- if (!hw_len)
- return -EINVAL;
-
- u8 *dmabuf __free(kfree) = kzalloc(hw_len, GFP_KERNEL);
- if (!dmabuf)
- return -ENOMEM;
-
- ret = hid_hw_raw_request(hdev, report_id, dmabuf, hw_len,
- HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
- if (ret < 0)
- return ret;
-
- /* The device may return less than requested (short read). */
- memcpy(buf, dmabuf, min(buf_size, hw_len));
-
- return 0;
-}
-
static int asus_kbd_init(struct hid_device *hdev, u8 report_id)
{
/*
@@ -1395,6 +2161,7 @@ static int asus_input_mapping(struct hid_device *hdev,
case 0x5f: asus_map_key_clear(KEY_PROG2); break; /* S-shaped programmable key */
case 0x6b: asus_map_key_clear(KEY_F21); break; /* ASUS touchpad toggle */
case 0x38: asus_map_key_clear(KEY_PROG1); break; /* ROG key */
+ case 0x93: asus_map_key_clear(KEY_PROG1); break; /* ROG Ally X AC button */
case 0xba: asus_map_key_clear(KEY_PROG2); break; /* Fn+C ASUS Splendid */
case 0x5c: asus_map_key_clear(KEY_PROG3); break; /* Fn+Space Power4Gear */
case 0x99: asus_map_key_clear(KEY_PROG4); break; /* Fn+F5 "fan" symbol */
@@ -1569,6 +2336,8 @@ static int asus_xgm_init(struct hid_device *hdev, struct asus_drvdata *drvdata)
static int __maybe_unused asus_resume(struct hid_device *hdev)
{
struct asus_drvdata *drvdata = hid_get_drvdata(hdev);
+ /* Published with smp_store_release() after the fields are set. */
+ struct ally_intf *intf = smp_load_acquire(&drvdata->ally);
/*
* If we have a backlight listener registered, restore the previous state,
@@ -1578,12 +2347,16 @@ static int __maybe_unused asus_resume(struct hid_device *hdev)
if (drvdata->listener.brightness_set)
asus_kbd_backlight_set(&drvdata->listener, drvdata->kbd_backlight_brightness);
+ if (intf)
+ schedule_delayed_work(&intf->resume_work, msecs_to_jiffies(500));
+
return 0;
}
static int __maybe_unused asus_reset_resume(struct hid_device *hdev)
{
struct asus_drvdata *drvdata = hid_get_drvdata(hdev);
+ int ret;
if (!drvdata->tp)
asus_initialize_reports(hdev);
@@ -1591,6 +2364,15 @@ static int __maybe_unused asus_reset_resume(struct hid_device *hdev)
if (drvdata->tp)
return asus_start_multitouch(hdev);
+ if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) {
+ /* Published with smp_store_release() after the fields are set. */
+ ret = hid_asus_ally_reset_resume(hdev, smp_load_acquire(&drvdata->ally));
+ if (ret) {
+ hid_err(hdev, "Failed to resume ROG Ally HID extensions: %d\n", ret);
+ return ret;
+ }
+ }
+
return 0;
}
@@ -1598,6 +2380,7 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
{
struct hid_report_enum *rep_enum;
struct asus_drvdata *drvdata;
+ struct ally_intf *ally;
struct hid_report *rep;
bool is_vendor = false;
int ret;
@@ -1715,12 +2498,22 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
}
}
- /* Laptops keyboard backlight is always at 0x5a */
+ /* Laptops keyboard backlight is always FEATURE_KBD_REPORT_ID */
if (is_vendor && (drvdata->quirks & QUIRK_USE_KBD_BACKLIGHT) &&
(asus_has_report_id(hdev, FEATURE_KBD_REPORT_ID)) &&
(asus_kbd_register_leds(hdev)))
hid_warn(hdev, "Failed to initialize backlight.\n");
+ if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) {
+ ally = hid_asus_ally_probe(hdev);
+ if (IS_ERR(ally))
+ hid_err(hdev, "Failed to initialize ROG Ally HID extensions: %ld\n",
+ PTR_ERR(ally));
+ else
+ /* Readers take this with smp_load_acquire(). */
+ smp_store_release(&drvdata->ally, ally);
+ }
+
/*
* For ROG keyboards, skip rename for consistency and ->input check as
* some devices do not have inputs.
@@ -1755,6 +2548,14 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
if (drvdata->xgm_led)
devm_led_classdev_unregister(&hdev->dev, &drvdata->xgm_led->cdev);
+ if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) {
+ ally = hid_asus_ally_terminate(drvdata);
+ asus_worker_stop(drvdata->worker);
+ hid_hw_stop(hdev);
+ ally_free(ally);
+ return ret;
+ }
+
asus_worker_stop(drvdata->worker);
hid_hw_stop(hdev);
return ret;
@@ -1763,6 +2564,7 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
static void asus_remove(struct hid_device *hdev)
{
struct asus_drvdata *drvdata = hid_get_drvdata(hdev);
+ struct ally_intf *ally;
if (drvdata->listener.brightness_set)
asus_hid_unregister_listener(&drvdata->listener);
@@ -1770,6 +2572,14 @@ static void asus_remove(struct hid_device *hdev)
if (drvdata->xgm_led)
devm_led_classdev_unregister(&hdev->dev, &drvdata->xgm_led->cdev);
+ if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) {
+ ally = hid_asus_ally_terminate(drvdata);
+ asus_worker_stop(drvdata->worker);
+ hid_hw_stop(hdev);
+ ally_free(ally);
+ return;
+ }
+
asus_worker_stop(drvdata->worker);
hid_hw_stop(hdev);
}
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* Re: [PATCH v13 02/18] HID: asus: add support for ROG Ally handhelds
2026-11-04 1:39 ` [PATCH v13 02/18] HID: asus: add support for ROG Ally handhelds Denis Benato
@ 2026-11-04 2:07 ` sashiko-bot
0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-11-04 2:07 UTC (permalink / raw)
To: Denis Benato; +Cc: linux-input, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] HID: asus: use-after-free of struct ally_intf in asus_resume()
- [Medium] HID: asus: ally_vendor_keys() omits 0x93 (ROG Ally X AC button)
- [Medium] HID: asus: duplicate gamepad input_dev registered due to mapping report 0x0B
- [Medium] HID: asus: ally_dev_get_report() sizes 0x0D read from 0x5A report length
--
commit 964cb88b58989515f1ea0adc56e4d20719f664a7
Author: Denis Benato <denis.benato@linux.dev>
HID: asus: add support for ROG Ally handhelds
This commit adds core HID support for ASUS ROG Ally handhelds, including
device ready-state initialization, Ally X gamepad input mapping, vendor
key handling, and Ctrl+Alt+Del sequence filtering.
> diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
> index 46643022b68d0..2a3e8c399f0aa 100644
> --- a/drivers/hid/hid-asus.c
> +++ b/drivers/hid/hid-asus.c
[ ... ]
> @@ -239,12 +285,816 @@ static const struct asus_touchpad_info medion_e1239t_tp = {
[ ... ]
> +static bool ally_vendor_keys(struct input_dev *input, const u8 *data, int size)
> +{
> + bool is_vendor;
> +
> + if (size < 2 || data[0] != FEATURE_KBD_REPORT_ID)
> + return false;
> +
> + is_vendor = data[1] == 0x38 || data[1] == 0xA6 ||
> + data[1] == 0xA7 || data[1] == 0xA8;
> +
> + /* Neither a vendor press nor a release: leave it to the other handlers. */
> + if (!is_vendor && data[1] != 0)
> + return false;
> +
> + if (input) {
> + /* AC short press */
> + input_report_key(input, KEY_PROG1, data[1] == 0x38);
> +
> + /* CC short press & release */
> + input_report_key(input, KEY_F16, data[1] == 0xA6);
> +
> + /* AC long press */
> + input_report_key(input, KEY_F17, data[1] == 0xA7);
> +
> + /* AC long release */
> + input_report_key(input, KEY_F18, data[1] == 0xA8);
> +
> + input_sync(input);
> + }
> +
> + /*
> + * Only the vendor codes are consumed. They are reported on this
> + * interface's evdev above, so hid-core never sees them pressed. Note
> + * that 0xA8 ("AC long release") is itself a usage the firmware sends,
> + * not the report that clears one. Everything else, including the 0x00
> + * report that clears a held usage, must still reach hid-core: it can
> + * only release the usages it saw pressed if it sees the report that
> + * clears them.
> + */
> + return is_vendor;
> +}
[Severity: Medium]
Should ally_vendor_keys() also check for 0x93 when handling KEY_PROG1?
Later in asus_input_mapping(), this patch maps usage 0x93 to KEY_PROG1 as
the ROG Ally X AC button:
case 0x38: asus_map_key_clear(KEY_PROG1); break; /* ROG key */
case 0x93: asus_map_key_clear(KEY_PROG1); break; /* ROG Ally X AC button */
Because ally_vendor_keys() only checks data[1] against 0x38, 0xA6, 0xA7,
and 0xA8, a report with data[1] == 0x93 on the ROG Ally X returns false
and falls through to hid-core, which records 0x93 in field->value[0] and
reports KEY_PROG1 = 1.
If the release report (data[1] == 0x00) is missed across suspend/resume
(where ally_resume_work_fn() -> ally_release_vendor_keys() clears
KEY_PROG1 directly on intf->input without clearing hid-core's
field->value[]), or if the firmware transitions from 0x93 to 0xA7 (AC
long press, which ally_vendor_keys() consumes while reporting
KEY_PROG1 = 0), field->value[0] in hid-core will retain 0x93 while
intf->input has KEY_PROG1 released. Wouldn't hid-core then ignore the
next 0x93 press because 0x93 is still present in field->value[]?
[ ... ]
> +static int ally_dev_set_report(struct hid_device *hdev, const u8 *buf, size_t len)
> +{
> + return asus_kbd_set_report(hdev, buf, len);
> +}
> +
> +static int ally_dev_get_report(struct hid_device *hdev, u8 *out, size_t len)
> +{
> + return hid_hw_raw_request(hdev, HID_ALLY_GET_REPORT_ID, out, len,
> + HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
> +}
> +
> +/**
> + * ally_gamepad_send_packet() - Send a raw packet to the gamepad device
> + * @hdev: HID device
> + * @buf: buffer containing the packet data
> + * @len: length of data to send
> + *
> + * Return: count of data transferred, negative if error.
> + */
> +static int ally_gamepad_send_packet(struct hid_device *hdev, const u8 *buf, size_t len)
> +{
> + return ally_dev_set_report(hdev, buf, len);
> +}
> +
> +/**
> + * ally_gamepad_send_receive_packet() - Send a packet and receive the response
> + * @hdev: HID device
> + * @buf: buffer containing the packet data to send and receive response in
> + * @len: length of buffer
> + *
> + * Return: 0 on success, negative errno on failure.
> + */
> +static int ally_gamepad_send_receive_packet(struct hid_device *hdev,
> + u8 *buf, size_t len)
> +{
> + int ret;
> +
> + ret = ally_dev_set_report(hdev, buf, len);
> + if (ret >= 0) {
> + memset(buf, 0, len);
> + ret = ally_dev_get_report(hdev, buf, len);
> + }
> +
> + return ret;
> +}
[Severity: Medium]
As reported by Sashiko on v11 [1], ally_alloc_cmd() sizes the buffer from
FEATURE_KBD_REPORT_ID (0x5A) via asus_report_len(), and
ally_gamepad_send_receive_packet() passes that length to
ally_dev_get_report() to read HID_ALLY_GET_REPORT_ID (0x0D).
Since asus_kbd_get_report() was moved just above ally_dev_set_report(),
should ally_dev_get_report() call asus_kbd_get_report(hdev,
HID_ALLY_GET_REPORT_ID, out, len) instead of calling hid_hw_raw_request()
directly? That would both size the GET_REPORT transfer from report 0x0D's
descriptor entry and return 0 on success as documented in the kernel-doc
for ally_gamepad_send_receive_packet().
[ ... ]
> @@ -512,6 +1363,19 @@ static int asus_raw_event(struct hid_device *hdev,
> if (drvdata->quirks & QUIRK_MEDION_E1239T)
> return asus_e1239t_event(drvdata, data, size);
>
> + if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) {
> + /*
> + * Return -1 to suppress further processing by the generic HID
> + * input parser for reports we fully handle. The interface is
> + * published with smp_store_release() only after its fields
> + * are set. Letting report 0x0B reach the default parser
> + * creates a generic gamepad and Steam Input overlaps.
> + */
> + intf = smp_load_acquire(&drvdata->ally); /* fields are published */
> + if (hid_asus_ally_raw_event(hdev, intf, data, size))
> + return -1;
> + }
[Severity: Medium]
As reported by Sashiko on v12 [2], does returning -1 in asus_raw_event()
actually prevent hid-core from creating a duplicate generic gamepad input
device for report 0x0B?
During asus_probe(), hid_hw_start(hdev, HID_CONNECT_DEFAULT) runs
hidinput_connect() -> hidinput_configure_usages() -> asus_input_mapping()
before any raw events arrive. Because asus_input_mapping() does not return
-1 for usages in report 0x0B (HID_ALLY_X_INPUT_REPORT), hid-core maps all
of the standard gamepad usages in report 0x0B and registers a generic
gamepad input_dev on that interface in addition to the custom
intf->gamepad device registered by ally_x_setup_input().
Should asus_input_mapping() return -1 when QUIRK_ROG_ALLY_XPAD is set and
field->report->id == HID_ALLY_X_INPUT_REPORT so that the duplicate input
device is not created?
[ ... ]
> @@ -1569,6 +2336,8 @@ static int asus_xgm_init(struct hid_device *hdev, struct asus_drvdata *drvdata)
> static int __maybe_unused asus_resume(struct hid_device *hdev)
> {
> struct asus_drvdata *drvdata = hid_get_drvdata(hdev);
> + /* Published with smp_store_release() after the fields are set. */
> + struct ally_intf *intf = smp_load_acquire(&drvdata->ally);
>
> /*
> * If we have a backlight listener registered, restore the previous state,
> @@ -1578,12 +2347,16 @@ static int __maybe_unused asus_resume(struct hid_device *hdev)
> if (drvdata->listener.brightness_set)
> asus_kbd_backlight_set(&drvdata->listener, drvdata->kbd_backlight_brightness);
>
> + if (intf)
> + schedule_delayed_work(&intf->resume_work, msecs_to_jiffies(500));
> +
> return 0;
> }
[ ... ]
> @@ -1763,6 +2564,7 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
> static void asus_remove(struct hid_device *hdev)
> {
> struct asus_drvdata *drvdata = hid_get_drvdata(hdev);
> + struct ally_intf *ally;
>
> if (drvdata->listener.brightness_set)
> asus_hid_unregister_listener(&drvdata->listener);
> @@ -1770,6 +2572,14 @@ static void asus_remove(struct hid_device *hdev)
> if (drvdata->xgm_led)
> devm_led_classdev_unregister(&hdev->dev, &drvdata->xgm_led->cdev);
>
> + if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) {
> + ally = hid_asus_ally_terminate(drvdata);
> + asus_worker_stop(drvdata->worker);
> + hid_hw_stop(hdev);
> + ally_free(ally);
> + return;
> + }
[Severity: High]
As reported by Sashiko on v12 [3], can asus_resume() access freed memory
in intf->resume_work if it races with asus_remove() or the err_stop_hw
path in asus_probe()?
In hid_asus_ally_terminate() -> ally_quiesce(), disable_delayed_work_sync()
marks intf->resume_work disabled in intf->resume_work.work.data so a
concurrent asus_resume() cannot re-queue it, and then ally_free(ally)
frees the struct ally_intf with kfree().
However, if asus_resume() has already loaded the non-NULL drvdata->ally
pointer via smp_load_acquire(&drvdata->ally) before
hid_asus_ally_terminate() clears drvdata->ally, and reaches
schedule_delayed_work(&intf->resume_work, ...) after ally_free(ally) runs,
schedule_delayed_work() will access and modify intf->resume_work.work.data
after intf has been freed. Could intf be allocated with devm_kzalloc() on
&hdev->dev (or freed via kfree_rcu() with RCU read protection in
asus_resume()) so that intf->resume_work remains valid until the driver
is unbound?
[1] https://lore.kernel.org/r/20261025235232.129945-16-denis.benato@linux.dev
[2] https://lore.kernel.org/r/20261031174130.208240-3-denis.benato@linux.dev
[3] https://lore.kernel.org/r/20261031174130.208240-3-denis.benato@linux.dev
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261104013935.512713-1-denis.benato@linux.dev?part=2
^ permalink raw reply [flat|nested] 25+ messages in thread
* [PATCH v13 03/18] HID: asus: add gamepad configuration
2026-11-04 1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
2026-11-04 1:39 ` [PATCH v13 01/18] HID: asus: size feature reports from the HID report descriptor Denis Benato
2026-11-04 1:39 ` [PATCH v13 02/18] HID: asus: add support for ROG Ally handhelds Denis Benato
@ 2026-11-04 1:39 ` Denis Benato
2026-11-04 1:59 ` sashiko-bot
2026-11-04 1:39 ` [PATCH v13 04/18] HID: asus: add vibration strength configuration Denis Benato
` (14 subsequent siblings)
17 siblings, 1 reply; 25+ messages in thread
From: Denis Benato @ 2026-11-04 1:39 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
Add the base configuration structures for the gamepad configuration,
detect capabilities and initialize the device in a known state.
The configuration interface exposes xbox_controller. Reading it
reports whether the handheld is presenting an Xbox gamepad, and
writing it switches that mode.
hid_asus_ally_reset_resume() now waits for the gamepad MCU to report
ready and returns that error. A reset resume can fail with -ENODEV
where it previously always succeeded.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
Co-developed-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Jonathan LoBue <jlobue10@gmail.com>
---
drivers/hid/hid-asus.c | 312 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 311 insertions(+), 1 deletion(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 2a3e8c399f0a..8277d7108bb9 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -28,6 +28,7 @@
#include <linux/err.h>
#include <linux/hid.h>
#include <linux/jiffies.h>
+#include <linux/kstrtox.h>
#include <linux/list.h>
#include <linux/module.h>
#include <linux/mutex.h>
@@ -36,6 +37,7 @@
#include <linux/usb.h> /* For to_usb_interface for T100 touchpad intf check */
#include <linux/power_supply.h>
#include <linux/stddef.h>
+#include <linux/sysfs.h>
#include <linux/leds.h>
#include <linux/types.h>
#include <linux/unaligned.h>
@@ -77,6 +79,11 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define HID_ALLY_READY_MAX_TRIES 6
+#define ASUS_ALLY_XBOX_CONTROLLER_ENABLE 0x01
+#define ASUS_ALLY_XBOX_CONTROLLER_DISABLE 0x00
+
+#define ASUS_ALLY_CAPABILITY_SUPPORTED 0x01
+
#define HID_INPUT_KEY_DELETE 0x4c
#define HID_INPUT_KEY_F20 0x6f
#define HID_INPUT_LCTRL_MODIFIER 0x01
@@ -203,6 +210,13 @@ struct ally_intf {
u8 cad_state;
unsigned long cad_last_event_time;
struct delayed_work resume_work;
+ /*
+ * Sysfs groups hang off this interface's hdev. Numbers live in
+ * ally_prefs so a re-probe can restore them; ownership of the
+ * attributes is per device.
+ */
+ bool sysfs_live;
+ bool cal_sysfs_live;
};
struct asus_drvdata {
@@ -762,6 +776,281 @@ static int ally_get_endpoint_address(struct hid_device *hdev)
return ep->desc.bEndpointAddress;
}
+/*
+ * Settings that must survive the controller powering off and coming back.
+ * No device or input pointers live here: every transfer uses the interface
+ * that is bound right now.
+ *
+ * One set for the machine is by design. The Ally controller is
+ * part of the PC, so two ROG Ally HID devices are not bound at
+ * once. The only way to see a second one is to emulate it with
+ * uhid, but that would get rejected by the driver meaning that
+ * this struct can be used on a per-machine basis.
+ */
+static struct ally_prefs {
+ /* Guards every field below, including across the feature report. */
+ struct mutex lock;
+
+ bool initialized;
+
+ bool is_ally_x;
+ bool xbox_controller_support;
+ bool user_cal_support;
+ bool turbo_support;
+ bool resp_curve_support;
+ bool dir_to_btn_support;
+ bool gyro_support;
+ bool anti_deadzone_support;
+
+ bool xbox_controller_enabled;
+ u8 gamepad_mode;
+} ally_prefs = {
+ .lock = __MUTEX_INITIALIZER(ally_prefs.lock),
+};
+
+/**
+ * ally_check_capability() - Check if a specific capability is supported
+ * @hdev: HID device to query
+ * @check_cmd: capability command code
+ *
+ * Return: 1 if the capability is supported, 0 if not, negative on error.
+ */
+static int ally_check_capability(struct hid_device *hdev,
+ enum ally_command_codes check_cmd)
+{
+ const u8 payload[] = {};
+ size_t buf_len;
+ int ret;
+
+ u8 *buf __free(kfree) = ally_alloc_cmd(hdev, check_cmd, payload,
+ sizeof(payload), &buf_len);
+
+ if (!buf) {
+ hid_err(hdev, "Failed to allocate buffer for capability check.\n");
+ return -ENOMEM;
+ }
+
+ if (buf_len < 5) {
+ hid_err(hdev, "Capability check response too short.\n");
+ return -EINVAL;
+ }
+
+ ret = ally_gamepad_send_receive_packet(hdev, buf, buf_len);
+ if (ret < 0) {
+ hid_err(hdev, "Failed to check capability 0x%02x: %d\n", check_cmd, ret);
+ return ret;
+ }
+
+ return buf[1] == HID_ALLY_FEATURE_CODE_PAGE && buf[2] == check_cmd &&
+ buf[4] == ASUS_ALLY_CAPABILITY_SUPPORTED;
+}
+
+static int ally_detect_capabilities(struct hid_device *hdev)
+{
+ int ret;
+
+ guard(mutex)(&ally_prefs.lock);
+
+ ally_prefs.is_ally_x = (hdev->product == USB_DEVICE_ID_ASUSTEK_ROG_NKEY_ALLY_X);
+
+ ret = ally_check_capability(hdev, CMD_CHECK_XBOX_SUPPORT);
+ if (ret < 0)
+ goto ally_detect_capabilities_err;
+ ally_prefs.xbox_controller_support = ret;
+
+
+ ret = ally_check_capability(hdev, CMD_CHECK_USER_CAL_SUPPORT);
+ if (ret < 0)
+ goto ally_detect_capabilities_err;
+ ally_prefs.user_cal_support = ret;
+
+ ret = ally_check_capability(hdev, CMD_CHECK_TURBO_SUPPORT);
+ if (ret < 0)
+ goto ally_detect_capabilities_err;
+ ally_prefs.turbo_support = ret;
+
+ ret = ally_check_capability(hdev, CMD_CHECK_RESP_CURVE_SUPPORT);
+ if (ret < 0)
+ goto ally_detect_capabilities_err;
+ ally_prefs.resp_curve_support = ret;
+
+ ret = ally_check_capability(hdev, CMD_CHECK_DIR_TO_BTN_SUPPORT);
+ if (ret < 0)
+ goto ally_detect_capabilities_err;
+ ally_prefs.dir_to_btn_support = ret;
+
+ ret = ally_check_capability(hdev, CMD_CHECK_GYRO_TO_JOYSTICK);
+ if (ret < 0)
+ goto ally_detect_capabilities_err;
+ ally_prefs.gyro_support = ret;
+
+ ret = ally_check_capability(hdev, CMD_CHECK_ANTI_DEADZONE);
+ if (ret < 0)
+ goto ally_detect_capabilities_err;
+ ally_prefs.anti_deadzone_support = ret;
+
+ return 0;
+ally_detect_capabilities_err:
+ hid_err(hdev, "Failed to detect capabilities: %d\n", ret);
+ return ret;
+}
+
+/**
+ * ally_set_xbox_controller() - Enable or disable Xbox controller mode
+ * @hdev: HID device on which the command packet is sent
+ * @enabled: true to enable Xbox controller mode, false to disable it
+ *
+ * Builds a %CMD_SET_XBOX_CONTROLLER packet and sends it to the device. On
+ * success, the new state is stored in ally_prefs.xbox_controller_enabled.
+ *
+ * Context: Caller must hold ally_prefs.lock.
+ *
+ * Return: 0 on success, -%EOPNOTSUPP if Xbox controller mode is not
+ * supported, -%ENOMEM if the command buffer allocation fails, or a negative
+ * errno from ally_gamepad_send_packet() if sending the packet fails.
+ */
+static int ally_set_xbox_controller(struct hid_device *hdev, bool enabled)
+{
+ const u8 payload[] = { enabled ?
+ ASUS_ALLY_XBOX_CONTROLLER_ENABLE :
+ ASUS_ALLY_XBOX_CONTROLLER_DISABLE };
+ size_t buf_len;
+ int ret;
+
+ if (!ally_prefs.xbox_controller_support)
+ return -EOPNOTSUPP;
+
+ u8 *buf __free(kfree) = ally_alloc_cmd(hdev, CMD_SET_XBOX_CONTROLLER,
+ payload, sizeof(payload),
+ &buf_len);
+
+ if (!buf)
+ return -ENOMEM;
+
+ ret = ally_gamepad_send_packet(hdev, buf, buf_len);
+ if (ret < 0) {
+ hid_err(hdev, "Failed to set Xbox controller mode: %d\n", ret);
+ return ret;
+ }
+
+ ally_prefs.xbox_controller_enabled = enabled;
+ return 0;
+}
+
+static ssize_t xbox_controller_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ if (!ally_prefs.xbox_controller_support)
+ return -EOPNOTSUPP;
+
+ return sysfs_emit(buf, "%d\n", ally_prefs.xbox_controller_enabled ? 1 : 0);
+}
+
+static ssize_t xbox_controller_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ bool enabled;
+ int ret;
+
+ ret = kstrtobool(buf, &enabled);
+ if (ret)
+ return ret;
+
+ guard(mutex)(&ally_prefs.lock);
+
+ if (!ally_prefs.xbox_controller_support)
+ return -EOPNOTSUPP;
+
+ ret = ally_set_xbox_controller(hdev, enabled);
+ if (ret < 0)
+ return ret;
+
+ sysfs_notify(&dev->kobj, NULL, "xbox_controller");
+
+ return count;
+}
+
+static DEVICE_ATTR_RW(xbox_controller);
+
+static struct attribute *ally_config_attrs[] = {
+ &dev_attr_xbox_controller.attr,
+ NULL
+};
+
+static const struct attribute_group ally_attr_groups[] = {
+ {
+ .attrs = ally_config_attrs,
+ },
+};
+
+static int ally_prefs_setup(struct hid_device *hdev, struct ally_intf *intf)
+{
+ int ret, i;
+
+ ret = ally_detect_capabilities(hdev);
+ if (ret)
+ return ret;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ if (!ally_prefs.initialized) {
+ ally_prefs.gamepad_mode = 0x01;
+ ally_prefs.xbox_controller_enabled = true;
+ ally_prefs.initialized = true;
+ }
+
+ if (intf->sysfs_live)
+ return 0;
+ }
+
+ /*
+ * Create and remove sysfs groups without ally_prefs.lock. Attribute
+ * methods take that lock while holding a kernfs active reference;
+ * removing a group drains those references, so locking here would
+ * invert the order and deadlock.
+ */
+ for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++) {
+ ret = sysfs_create_group(&hdev->dev.kobj, &ally_attr_groups[i]);
+ if (ret) {
+ hid_err(hdev, "Failed to create Ally sysfs group '%s': %d\n",
+ ally_attr_groups[i].name ?: "", ret);
+ while (i--)
+ sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]);
+ return ret;
+ }
+ }
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ intf->sysfs_live = true;
+ }
+
+ return 0;
+}
+
+static void ally_sysfs_remove(struct hid_device *hdev, struct ally_intf *intf)
+{
+ int i;
+
+ if (!intf)
+ return;
+
+ if (ally_get_endpoint_address(hdev) != HID_ALLY_INTF_CFG_IN)
+ return;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ if (!intf->sysfs_live)
+ return;
+
+ intf->sysfs_live = false;
+ }
+
+ for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++)
+ sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]);
+}
+
struct ally_x_input_report {
__le16 x, y;
__le16 rx, ry;
@@ -944,6 +1233,14 @@ static int hid_asus_ally_init(struct hid_device *hdev)
if (ret < 0)
hid_err(hdev, "Ally failed to init force-feedback off: %d\n", ret);
+ guard(mutex)(&ally_prefs.lock);
+
+ if (ally_prefs.initialized && ally_prefs.xbox_controller_support) {
+ ret = ally_set_xbox_controller(hdev, ally_prefs.xbox_controller_enabled);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to apply Xbox controller mode: %d\n", ret);
+ }
+
return 0;
}
@@ -1038,6 +1335,12 @@ static struct ally_intf *hid_asus_ally_probe(struct hid_device *hdev)
return ERR_PTR(ret);
}
+ ret = ally_prefs_setup(hdev, intf);
+ if (ret < 0) {
+ hid_err(hdev, "Failed to create Ally cfg: %d\n", ret);
+ return ERR_PTR(ret);
+ }
+
hid_asus_ally_init(hdev);
break;
case HID_ALLY_X_INTF_IN:
@@ -1058,7 +1361,7 @@ static struct ally_intf *hid_asus_ally_probe(struct hid_device *hdev)
static int hid_asus_ally_reset_resume(struct hid_device *hdev, struct ally_intf *intf)
{
- int ep;
+ int ep, ret;
/*
* The extensions failed to probe and the device is operating as a
@@ -1071,6 +1374,12 @@ static int hid_asus_ally_reset_resume(struct hid_device *hdev, struct ally_intf
if (ep != HID_ALLY_INTF_CFG_IN)
return 0;
+ ret = ally_gamepad_check_ready(hdev);
+ if (ret < 0) {
+ hid_err(hdev, "ROG Ally device is not ready: %d\n", ret);
+ return ret;
+ }
+
return hid_asus_ally_init(hdev);
}
@@ -1091,6 +1400,7 @@ static struct ally_intf *hid_asus_ally_terminate(struct asus_drvdata *drvdata)
ally_quiesce(ally);
ally_unregister_gamepad(ally);
+ ally_sysfs_remove(drvdata->hdev, ally);
return ally;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* Re: [PATCH v13 03/18] HID: asus: add gamepad configuration
2026-11-04 1:39 ` [PATCH v13 03/18] HID: asus: add gamepad configuration Denis Benato
@ 2026-11-04 1:59 ` sashiko-bot
0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-11-04 1:59 UTC (permalink / raw)
To: Denis Benato; +Cc: linux-input, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] HID: asus: unlocked feature report transfers in hid_asus_ally_reset_resume()
- [Low] HID: asus: ally_detect_capabilities() mixes guard(mutex) with goto
- [Low] HID: asus: open-coded sysfs group helpers in ally_prefs_setup()
--
commit 23ba9584ee0ff2df9f8412928af4abbe35ed412d
Author: Denis Benato <denis.benato@linux.dev>
HID: asus: add gamepad configuration
This commit adds gamepad capability detection and preference storage for
ASUS ROG Ally devices, exposes the xbox_controller sysfs attribute, and
waits for the gamepad MCU to report ready during reset_resume.
> diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
> index 2a3e8c399f0aa..8277d7108bb97 100644
> --- a/drivers/hid/hid-asus.c
> +++ b/drivers/hid/hid-asus.c
[ ... ]
> @@ -762,6 +776,281 @@ static int ally_get_endpoint_address(struct hid_device *hdev)
> return ep->desc.bEndpointAddress;
> }
[ ... ]
> +static int ally_detect_capabilities(struct hid_device *hdev)
> +{
> + int ret;
> +
> + guard(mutex)(&ally_prefs.lock);
> +
> + ally_prefs.is_ally_x = (hdev->product == USB_DEVICE_ID_ASUSTEK_ROG_NKEY_ALLY_X);
> +
> + ret = ally_check_capability(hdev, CMD_CHECK_XBOX_SUPPORT);
> + if (ret < 0)
> + goto ally_detect_capabilities_err;
> + ally_prefs.xbox_controller_support = ret;
> +
> +
> + ret = ally_check_capability(hdev, CMD_CHECK_USER_CAL_SUPPORT);
> + if (ret < 0)
> + goto ally_detect_capabilities_err;
> + ally_prefs.user_cal_support = ret;
> +
> + ret = ally_check_capability(hdev, CMD_CHECK_TURBO_SUPPORT);
> + if (ret < 0)
> + goto ally_detect_capabilities_err;
> + ally_prefs.turbo_support = ret;
> +
> + ret = ally_check_capability(hdev, CMD_CHECK_RESP_CURVE_SUPPORT);
> + if (ret < 0)
> + goto ally_detect_capabilities_err;
> + ally_prefs.resp_curve_support = ret;
> +
> + ret = ally_check_capability(hdev, CMD_CHECK_DIR_TO_BTN_SUPPORT);
> + if (ret < 0)
> + goto ally_detect_capabilities_err;
> + ally_prefs.dir_to_btn_support = ret;
> +
> + ret = ally_check_capability(hdev, CMD_CHECK_GYRO_TO_JOYSTICK);
> + if (ret < 0)
> + goto ally_detect_capabilities_err;
> + ally_prefs.gyro_support = ret;
> +
> + ret = ally_check_capability(hdev, CMD_CHECK_ANTI_DEADZONE);
> + if (ret < 0)
> + goto ally_detect_capabilities_err;
> + ally_prefs.anti_deadzone_support = ret;
> +
> + return 0;
> +ally_detect_capabilities_err:
> + hid_err(hdev, "Failed to detect capabilities: %d\n", ret);
> + return ret;
> +}
[Severity: Low]
This isn't a bug, but ally_detect_capabilities() in drivers/hid/hid-asus.c
mixes guard(mutex)(&ally_prefs.lock) with goto ally_detect_capabilities_err,
contrary to the cleanup.h convention of not mixing scope-based cleanup
helpers with goto error paths in the same function.
Since ally_check_capability() and the caller hid_asus_ally_probe() already
log hid_err() on failure, and ally_detect_capabilities_err performs no
resource cleanup, could each failed check in ally_detect_capabilities()
simply return ret directly?
[ ... ]
> +static int ally_prefs_setup(struct hid_device *hdev, struct ally_intf *intf)
> +{
> + int ret, i;
> +
> + ret = ally_detect_capabilities(hdev);
> + if (ret)
> + return ret;
> +
> + scoped_guard(mutex, &ally_prefs.lock) {
> + if (!ally_prefs.initialized) {
> + ally_prefs.gamepad_mode = 0x01;
> + ally_prefs.xbox_controller_enabled = true;
> + ally_prefs.initialized = true;
> + }
> +
> + if (intf->sysfs_live)
> + return 0;
> + }
> +
> + /*
> + * Create and remove sysfs groups without ally_prefs.lock. Attribute
> + * methods take that lock while holding a kernfs active reference;
> + * removing a group drains those references, so locking here would
> + * invert the order and deadlock.
> + */
> + for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++) {
> + ret = sysfs_create_group(&hdev->dev.kobj, &ally_attr_groups[i]);
> + if (ret) {
> + hid_err(hdev, "Failed to create Ally sysfs group '%s': %d\n",
> + ally_attr_groups[i].name ?: "", ret);
> + while (i--)
> + sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]);
> + return ret;
> + }
> + }
> +
> + scoped_guard(mutex, &ally_prefs.lock) {
> + intf->sysfs_live = true;
> + }
> +
> + return 0;
> +}
> +
> +static void ally_sysfs_remove(struct hid_device *hdev, struct ally_intf *intf)
> +{
> + int i;
> +
> + if (!intf)
> + return;
> +
> + if (ally_get_endpoint_address(hdev) != HID_ALLY_INTF_CFG_IN)
> + return;
> +
> + scoped_guard(mutex, &ally_prefs.lock) {
> + if (!intf->sysfs_live)
> + return;
> +
> + intf->sysfs_live = false;
> + }
> +
> + for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++)
> + sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]);
> +}
[Severity: Low]
This isn't a bug, but ally_prefs_setup() and ally_sysfs_remove() in
drivers/hid/hid-asus.c open-code the iteration and error rollback over
ally_attr_groups using sysfs_create_group() and sysfs_remove_group().
Could ally_attr_groups be defined with ATTRIBUTE_GROUPS() (or a
NULL-terminated struct attribute_group pointer array) so ally_prefs_setup()
and ally_sysfs_remove() can use sysfs_create_groups() and
sysfs_remove_groups() instead?
[ ... ]
> @@ -944,6 +1233,14 @@ static int hid_asus_ally_init(struct hid_device *hdev)
> if (ret < 0)
> hid_err(hdev, "Ally failed to init force-feedback off: %d\n", ret);
>
> + guard(mutex)(&ally_prefs.lock);
> +
> + if (ally_prefs.initialized && ally_prefs.xbox_controller_support) {
> + ret = ally_set_xbox_controller(hdev, ally_prefs.xbox_controller_enabled);
> + if (ret < 0)
> + hid_warn(hdev, "Failed to apply Xbox controller mode: %d\n", ret);
> + }
> +
> return 0;
> }
>
> @@ -1038,6 +1335,12 @@ static struct ally_intf *hid_asus_ally_probe(struct hid_device *hdev)
> return ERR_PTR(ret);
> }
>
> + ret = ally_prefs_setup(hdev, intf);
> + if (ret < 0) {
> + hid_err(hdev, "Failed to create Ally cfg: %d\n", ret);
> + return ERR_PTR(ret);
> + }
> +
> hid_asus_ally_init(hdev);
> break;
> case HID_ALLY_X_INTF_IN:
> @@ -1058,7 +1361,7 @@ static struct ally_intf *hid_asus_ally_probe(struct hid_device *hdev)
>
> static int hid_asus_ally_reset_resume(struct hid_device *hdev, struct ally_intf *intf)
> {
> - int ep;
> + int ep, ret;
>
> /*
> * The extensions failed to probe and the device is operating as a
> @@ -1071,6 +1374,12 @@ static int hid_asus_ally_reset_resume(struct hid_device *hdev, struct ally_intf
> if (ep != HID_ALLY_INTF_CFG_IN)
> return 0;
>
> + ret = ally_gamepad_check_ready(hdev);
> + if (ret < 0) {
> + hid_err(hdev, "ROG Ally device is not ready: %d\n", ret);
> + return ret;
> + }
> +
> return hid_asus_ally_init(hdev);
> }
[Severity: Medium]
Can a concurrent write to xbox_controller_store() race with the unlocked
feature report transfers in hid_asus_ally_reset_resume() and
hid_asus_ally_init()?
Once ally_prefs_setup() exposes the sysfs group on hdev->dev.kobj,
xbox_controller_store() remains live across suspend and reset_resume (as
well as during the rest of hid_asus_ally_probe()):
xbox_controller_store()
guard(mutex)(&ally_prefs.lock)
ally_set_xbox_controller()
ally_gamepad_send_packet()
ally_dev_set_report() /* feature report 0x5A */
However, hid_asus_ally_reset_resume() runs ally_gamepad_check_ready()
without holding ally_prefs.lock, where each attempt performs a two-step
command and response sequence over USB control transfers:
hid_asus_ally_reset_resume()
ally_gamepad_check_ready()
ally_gamepad_send_receive_packet()
ally_dev_set_report() /* feature report 0x5A */
ally_dev_get_report() /* feature report 0x0D */
Similarly, hid_asus_ally_init() sends ally_force_feedback_off via
ally_gamepad_send_packet() before acquiring ally_prefs.lock.
If userspace writes to xbox_controller during hid_asus_ally_reset_resume()
or hid_asus_ally_init(), can CMD_SET_XBOX_CONTROLLER be sent before the MCU
is ready or interleave between ally_dev_set_report() and
ally_dev_get_report() in ally_gamepad_check_ready(), causing the readiness
check or configuration command to fail?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261104013935.512713-1-denis.benato@linux.dev?part=3
^ permalink raw reply [flat|nested] 25+ messages in thread
* [PATCH v13 04/18] HID: asus: add vibration strength configuration
2026-11-04 1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
` (2 preceding siblings ...)
2026-11-04 1:39 ` [PATCH v13 03/18] HID: asus: add gamepad configuration Denis Benato
@ 2026-11-04 1:39 ` Denis Benato
2026-11-04 1:39 ` [PATCH v13 05/18] HID: asus: add joysticks inner and outer range configuration Denis Benato
` (13 subsequent siblings)
17 siblings, 0 replies; 25+ messages in thread
From: Denis Benato @ 2026-11-04 1:39 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
ASUS ROG Ally handhelds support the vibration strength to be configured:
add sysfs attributes to allow userspace configure motors vibration
intensity.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
Co-developed-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Luke Jones <luke@ljones.dev>
---
drivers/hid/hid-asus.c | 171 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 171 insertions(+)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 8277d7108bb9..459061391c37 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -97,6 +97,9 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define ASUS_HID_INPUT_KBD_REPORT_ID 0x01
+#define ASUS_ALLY_VIBRATION_INTENSITY_MIN 0
+#define ASUS_ALLY_VIBRATION_INTENSITY_MAX 100
+
/* Spurious HID codes sent by QUIRK_ROG_NKEY_KEYBOARD devices */
#define ASUS_SPURIOUS_CODE_0XEA 0xea
#define ASUS_SPURIOUS_CODE_0XEC 0xec
@@ -804,6 +807,9 @@ static struct ally_prefs {
bool xbox_controller_enabled;
u8 gamepad_mode;
+
+ u8 vibration_intensity_left;
+ u8 vibration_intensity_right;
} ally_prefs = {
.lock = __MUTEX_INITIALIZER(ally_prefs.lock),
};
@@ -976,6 +982,149 @@ static ssize_t xbox_controller_store(struct device *dev,
static DEVICE_ATTR_RW(xbox_controller);
+/**
+ * ally_set_vibration_intensity() - Set the gamepad vibration intensity
+ * @hdev: HID device the command packet is sent on
+ * @left: left motor intensity
+ * @right: right motor intensity
+ *
+ * Context: Caller holds ally_prefs.lock.
+ * Return: 0 on success, negative errno on failure.
+ */
+static int ally_set_vibration_intensity(struct hid_device *hdev, u8 left, u8 right)
+{
+ const u8 data[] = { left, right };
+ size_t buf_len;
+ int ret;
+
+ u8 *buf __free(kfree) = ally_alloc_cmd(hdev, CMD_SET_VIBRATION_INTENSITY,
+ data, sizeof(data), &buf_len);
+
+ if (!buf)
+ return -ENOMEM;
+
+ ret = ally_gamepad_send_packet(hdev, buf, buf_len);
+ if (ret < 0) {
+ hid_err(hdev, "Failed to set vibration intensity: %d\n", ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+static void ally_vibration_defaults(void)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ if (ally_prefs.initialized)
+ return;
+
+ ally_prefs.vibration_intensity_left = 100;
+ ally_prefs.vibration_intensity_right = 100;
+}
+
+/* Indices into ally_vibration[], in the order ally_set_vibration_intensity() takes them. */
+enum ally_vibration {
+ ALLY_VIBRATION_LEFT,
+ ALLY_VIBRATION_RIGHT,
+};
+
+static u8 *const ally_vibration[] = {
+ [ALLY_VIBRATION_LEFT] = &ally_prefs.vibration_intensity_left,
+ [ALLY_VIBRATION_RIGHT] = &ally_prefs.vibration_intensity_right,
+};
+
+static ssize_t ally_vibration_intensity_show(char *buf, enum ally_vibration which)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ return sysfs_emit(buf, "%u\n", *ally_vibration[which]);
+}
+
+static ssize_t ally_vibration_intensity_store(struct device *dev, const char *buf,
+ size_t count, enum ally_vibration which,
+ const char *group)
+{
+ u8 value, v[ARRAY_SIZE(ally_vibration)];
+ struct hid_device *hdev = to_hid_device(dev);
+ size_t i;
+ int ret;
+
+ ret = kstrtou8(buf, 10, &value);
+ if (ret || value > ASUS_ALLY_VIBRATION_INTENSITY_MAX)
+ return -EINVAL;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ for (i = 0; i < ARRAY_SIZE(v); i++)
+ v[i] = *ally_vibration[i];
+ v[which] = value;
+
+ ret = ally_set_vibration_intensity(hdev,
+ v[ALLY_VIBRATION_LEFT],
+ v[ALLY_VIBRATION_RIGHT]);
+ if (ret < 0)
+ return ret;
+
+ *ally_vibration[which] = value;
+ }
+
+ sysfs_notify(&dev->kobj, group, "intensity");
+
+ return count;
+}
+
+/* Generates <side>_vibration_intensity_{show,store,range_show}. */
+#define ALLY_VIBRATION_FUNCS(side, SIDE) \
+static ssize_t side##_vibration_intensity_show(struct device *dev, \
+ struct device_attribute *attr, \
+ char *buf) \
+{ \
+ return ally_vibration_intensity_show(buf, ALLY_VIBRATION_##SIDE); \
+} \
+ \
+static ssize_t side##_vibration_intensity_store(struct device *dev, \
+ struct device_attribute *attr, \
+ const char *buf, size_t count) \
+{ \
+ return ally_vibration_intensity_store(dev, buf, count, ALLY_VIBRATION_##SIDE, \
+ #side "_vibration"); \
+} \
+ \
+static ssize_t side##_vibration_intensity_range_show(struct device *dev, \
+ struct device_attribute *attr, \
+ char *buf) \
+{ \
+ return sysfs_emit(buf, "%u %u\n", ASUS_ALLY_VIBRATION_INTENSITY_MIN, \
+ ASUS_ALLY_VIBRATION_INTENSITY_MAX); \
+}
+
+ALLY_VIBRATION_FUNCS(left, LEFT)
+ALLY_VIBRATION_FUNCS(right, RIGHT)
+
+static struct device_attribute dev_attr_left_vibration_intensity =
+ __ATTR(intensity, 0644, left_vibration_intensity_show, left_vibration_intensity_store);
+
+static struct device_attribute dev_attr_left_vibration_intensity_range =
+ __ATTR(intensity_range, 0444, left_vibration_intensity_range_show, NULL);
+
+static struct device_attribute dev_attr_right_vibration_intensity =
+ __ATTR(intensity, 0644, right_vibration_intensity_show, right_vibration_intensity_store);
+
+static struct device_attribute dev_attr_right_vibration_intensity_range =
+ __ATTR(intensity_range, 0444, right_vibration_intensity_range_show, NULL);
+
+static struct attribute *ally_left_vibration_attrs[] = {
+ &dev_attr_left_vibration_intensity.attr,
+ &dev_attr_left_vibration_intensity_range.attr,
+ NULL
+};
+
+static struct attribute *ally_right_vibration_attrs[] = {
+ &dev_attr_right_vibration_intensity.attr,
+ &dev_attr_right_vibration_intensity_range.attr,
+ NULL
+};
+
static struct attribute *ally_config_attrs[] = {
&dev_attr_xbox_controller.attr,
NULL
@@ -985,6 +1134,14 @@ static const struct attribute_group ally_attr_groups[] = {
{
.attrs = ally_config_attrs,
},
+ {
+ .name = "left_vibration",
+ .attrs = ally_left_vibration_attrs,
+ },
+ {
+ .name = "right_vibration",
+ .attrs = ally_right_vibration_attrs,
+ },
};
static int ally_prefs_setup(struct hid_device *hdev, struct ally_intf *intf)
@@ -995,6 +1152,8 @@ static int ally_prefs_setup(struct hid_device *hdev, struct ally_intf *intf)
if (ret)
return ret;
+ ally_vibration_defaults();
+
scoped_guard(mutex, &ally_prefs.lock) {
if (!ally_prefs.initialized) {
ally_prefs.gamepad_mode = 0x01;
@@ -1241,6 +1400,18 @@ static int hid_asus_ally_init(struct hid_device *hdev)
hid_warn(hdev, "Failed to apply Xbox controller mode: %d\n", ret);
}
+ /*
+ * The MCU does not keep the vibration intensity across a reset:
+ * send the recorded values again.
+ */
+ if (ally_prefs.initialized) {
+ ret = ally_set_vibration_intensity(hdev,
+ ally_prefs.vibration_intensity_left,
+ ally_prefs.vibration_intensity_right);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to restore vibration intensity: %d\n", ret);
+ }
+
return 0;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH v13 05/18] HID: asus: add joysticks inner and outer range configuration
2026-11-04 1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
` (3 preceding siblings ...)
2026-11-04 1:39 ` [PATCH v13 04/18] HID: asus: add vibration strength configuration Denis Benato
@ 2026-11-04 1:39 ` Denis Benato
2026-11-04 1:53 ` sashiko-bot
2026-11-04 1:39 ` [PATCH v13 06/18] HID: asus: add triggers " Denis Benato
` (12 subsequent siblings)
17 siblings, 1 reply; 25+ messages in thread
From: Denis Benato @ 2026-11-04 1:39 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
ROG Ally devices supports configuring joysticks inner and outer range:
add sysfs attributes to allow userspace modifying the sensitivity
of those controllers.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
Co-developed-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Luke Jones <luke@ljones.dev>
---
drivers/hid/hid-asus.c | 266 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 265 insertions(+), 1 deletion(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 459061391c37..a89ce83ae93f 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -100,6 +100,12 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define ASUS_ALLY_VIBRATION_INTENSITY_MIN 0
#define ASUS_ALLY_VIBRATION_INTENSITY_MAX 100
+#define ASUS_ALLY_OUTER_THRESHOLD_MIN 70
+#define ASUS_ALLY_OUTER_THRESHOLD_MAX 100
+
+#define ASUS_ALLY_INNER_THRESHOLD_MIN 0
+#define ASUS_ALLY_INNER_THRESHOLD_MAX 50
+
/* Spurious HID codes sent by QUIRK_ROG_NKEY_KEYBOARD devices */
#define ASUS_SPURIOUS_CODE_0XEA 0xea
#define ASUS_SPURIOUS_CODE_0XEC 0xec
@@ -810,6 +816,11 @@ static struct ally_prefs {
u8 vibration_intensity_left;
u8 vibration_intensity_right;
+
+ u8 left_deadzone;
+ u8 left_outer_threshold;
+ u8 right_deadzone;
+ u8 right_outer_threshold;
} ally_prefs = {
.lock = __MUTEX_INITIALIZER(ally_prefs.lock),
};
@@ -1125,6 +1136,212 @@ static struct attribute *ally_right_vibration_attrs[] = {
NULL
};
+/**
+ * ally_set_joystick_thresholds() - Set the joystick inner and outer thresholds
+ * @hdev: HID device the command packet is sent on
+ * @left_it: left joystick inner threshold (deadzone)
+ * @left_ot: left joystick outer threshold
+ * @right_it: right joystick inner threshold (deadzone)
+ * @right_ot: right joystick outer threshold
+ *
+ * Context: Caller holds ally_prefs.lock.
+ * Return: 0 on success, -ENOMEM if the command buffer cannot be allocated,
+ * or a negative errno if sending the packet fails.
+ */
+static int ally_set_joystick_thresholds(struct hid_device *hdev, u8 left_it,
+ u8 left_ot, u8 right_it, u8 right_ot)
+{
+ const u8 payload[] = { left_it, left_ot, right_it, right_ot };
+ size_t buf_len;
+ int ret;
+
+ u8 *buf __free(kfree) = ally_alloc_cmd(hdev, CMD_SET_JOYSTICK_DEADZONE,
+ payload, sizeof(payload),
+ &buf_len);
+
+ if (!buf)
+ return -ENOMEM;
+
+ ret = ally_gamepad_send_packet(hdev, buf, buf_len);
+ if (ret < 0) {
+ hid_err(hdev, "Failed to set joystick ranges: %d\n", ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+static void ally_joystick_defaults(void)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ if (ally_prefs.initialized)
+ return;
+
+ ally_prefs.left_deadzone = 10;
+ ally_prefs.left_outer_threshold = 90;
+ ally_prefs.right_deadzone = 10;
+ ally_prefs.right_outer_threshold = 90;
+}
+
+/* Indices into ally_thresholds[], in the order the MCU packet takes them. */
+enum ally_threshold {
+ ALLY_LEFT_INNER,
+ ALLY_LEFT_OUTER,
+ ALLY_RIGHT_INNER,
+ ALLY_RIGHT_OUTER,
+};
+
+static u8 *const ally_thresholds[] = {
+ [ALLY_LEFT_INNER] = &ally_prefs.left_deadzone,
+ [ALLY_LEFT_OUTER] = &ally_prefs.left_outer_threshold,
+ [ALLY_RIGHT_INNER] = &ally_prefs.right_deadzone,
+ [ALLY_RIGHT_OUTER] = &ally_prefs.right_outer_threshold,
+};
+
+static ssize_t ally_threshold_show(char *buf, enum ally_threshold which)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ if (!ally_prefs.user_cal_support)
+ return -EOPNOTSUPP;
+
+ return sysfs_emit(buf, "%u\n", *ally_thresholds[which]);
+}
+
+static ssize_t ally_threshold_store(struct device *dev, const char *buf,
+ size_t count, enum ally_threshold which,
+ int lo, int hi,
+ const char *group, const char *name)
+{
+ u8 value, v[ARRAY_SIZE(ally_thresholds)];
+ struct hid_device *hdev = to_hid_device(dev);
+ size_t i;
+ int ret;
+
+ ret = kstrtou8(buf, 10, &value);
+ if (ret || value < lo || value > hi)
+ return -EINVAL;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ if (!ally_prefs.user_cal_support)
+ return -EOPNOTSUPP;
+
+ for (i = 0; i < ARRAY_SIZE(v); i++)
+ v[i] = *ally_thresholds[i];
+ v[which] = value;
+
+ ret = ally_set_joystick_thresholds(hdev,
+ v[ALLY_LEFT_INNER],
+ v[ALLY_LEFT_OUTER],
+ v[ALLY_RIGHT_INNER],
+ v[ALLY_RIGHT_OUTER]);
+ if (ret)
+ return ret;
+
+ *ally_thresholds[which] = value;
+ }
+
+ sysfs_notify(&dev->kobj, group, name);
+
+ return count;
+}
+
+/* Generates <side>_joystick_<kind>_threshold_{show,store,range_show}. */
+#define ALLY_THRESHOLD_FUNCS(side, SIDE, kind, KIND) \
+static ssize_t side##_joystick_##kind##_threshold_show(struct device *dev, \
+ struct device_attribute *attr, \
+ char *buf) \
+{ \
+ return ally_threshold_show(buf, ALLY_##SIDE##_##KIND); \
+} \
+ \
+static ssize_t side##_joystick_##kind##_threshold_store(struct device *dev, \
+ struct device_attribute *attr, \
+ const char *buf, size_t count) \
+{ \
+ return ally_threshold_store(dev, buf, count, ALLY_##SIDE##_##KIND, \
+ ASUS_ALLY_##KIND##_THRESHOLD_MIN, \
+ ASUS_ALLY_##KIND##_THRESHOLD_MAX, \
+ #side "_joystick_axis", #kind "_threshold"); \
+} \
+ \
+static ssize_t side##_joystick_##kind##_threshold_range_show(struct device *dev, \
+ struct device_attribute *attr, \
+ char *buf) \
+{ \
+ return sysfs_emit(buf, "%u %u\n", ASUS_ALLY_##KIND##_THRESHOLD_MIN, \
+ ASUS_ALLY_##KIND##_THRESHOLD_MAX); \
+}
+
+ALLY_THRESHOLD_FUNCS(left, LEFT, inner, INNER)
+ALLY_THRESHOLD_FUNCS(left, LEFT, outer, OUTER)
+ALLY_THRESHOLD_FUNCS(right, RIGHT, inner, INNER)
+ALLY_THRESHOLD_FUNCS(right, RIGHT, outer, OUTER)
+
+static struct device_attribute dev_attr_left_joystick_inner_threshold =
+ __ATTR(inner_threshold, 0644, left_joystick_inner_threshold_show,
+ left_joystick_inner_threshold_store);
+
+static struct device_attribute dev_attr_left_joystick_inner_threshold_range =
+ __ATTR(inner_threshold_range, 0444, left_joystick_inner_threshold_range_show, NULL);
+
+static struct device_attribute dev_attr_left_joystick_outer_threshold =
+ __ATTR(outer_threshold, 0644, left_joystick_outer_threshold_show,
+ left_joystick_outer_threshold_store);
+
+static struct device_attribute dev_attr_left_joystick_outer_threshold_range =
+ __ATTR(outer_threshold_range, 0444, left_joystick_outer_threshold_range_show, NULL);
+
+static struct device_attribute dev_attr_right_joystick_inner_threshold =
+ __ATTR(inner_threshold, 0644, right_joystick_inner_threshold_show,
+ right_joystick_inner_threshold_store);
+
+static struct device_attribute dev_attr_right_joystick_inner_threshold_range =
+ __ATTR(inner_threshold_range, 0444, right_joystick_inner_threshold_range_show, NULL);
+
+static struct device_attribute dev_attr_right_joystick_outer_threshold =
+ __ATTR(outer_threshold, 0644, right_joystick_outer_threshold_show,
+ right_joystick_outer_threshold_store);
+
+static struct device_attribute dev_attr_right_joystick_outer_threshold_range =
+ __ATTR(outer_threshold_range, 0444, right_joystick_outer_threshold_range_show, NULL);
+
+static struct attribute *left_joystick_axis_attrs[] = {
+ &dev_attr_left_joystick_inner_threshold.attr,
+ &dev_attr_left_joystick_outer_threshold.attr,
+ &dev_attr_left_joystick_inner_threshold_range.attr,
+ &dev_attr_left_joystick_outer_threshold_range.attr,
+ NULL
+};
+
+static struct attribute *right_joystick_axis_attrs[] = {
+ &dev_attr_right_joystick_inner_threshold.attr,
+ &dev_attr_right_joystick_outer_threshold.attr,
+ &dev_attr_right_joystick_inner_threshold_range.attr,
+ &dev_attr_right_joystick_outer_threshold_range.attr,
+ NULL
+};
+
+/*
+ * Registered only when the device can configure these parameters. The
+ * callbacks still reject a missing capability.
+ */
+static const struct attribute_group ally_left_joystick_axis_group = {
+ .name = "left_joystick_axis",
+ .attrs = left_joystick_axis_attrs,
+};
+
+static const struct attribute_group ally_right_joystick_axis_group = {
+ .name = "right_joystick_axis",
+ .attrs = right_joystick_axis_attrs,
+};
+
+static const struct attribute_group *const ally_cal_attr_groups[] = {
+ &ally_left_joystick_axis_group,
+ &ally_right_joystick_axis_group,
+};
+
static struct attribute *ally_config_attrs[] = {
&dev_attr_xbox_controller.attr,
NULL
@@ -1146,13 +1363,15 @@ static const struct attribute_group ally_attr_groups[] = {
static int ally_prefs_setup(struct hid_device *hdev, struct ally_intf *intf)
{
- int ret, i;
+ bool cal_live = false;
+ int ret, i, cal_n = 0;
ret = ally_detect_capabilities(hdev);
if (ret)
return ret;
ally_vibration_defaults();
+ ally_joystick_defaults();
scoped_guard(mutex, &ally_prefs.lock) {
if (!ally_prefs.initialized) {
@@ -1182,15 +1401,44 @@ static int ally_prefs_setup(struct hid_device *hdev, struct ally_intf *intf)
}
}
+ scoped_guard(mutex, &ally_prefs.lock)
+ cal_live = ally_prefs.user_cal_support ||
+ ally_prefs.anti_deadzone_support ||
+ ally_prefs.resp_curve_support;
+
+ /* Skip the calibration groups when the capability is missing. */
+ if (cal_live) {
+ for (; cal_n < ARRAY_SIZE(ally_cal_attr_groups); cal_n++) {
+ ret = sysfs_create_group(&hdev->dev.kobj,
+ ally_cal_attr_groups[cal_n]);
+ if (ret) {
+ hid_err(hdev, "Failed to create Ally sysfs group '%s': %d\n",
+ ally_cal_attr_groups[cal_n]->name, ret);
+ goto err_cal_groups;
+ }
+ }
+ }
+
scoped_guard(mutex, &ally_prefs.lock) {
+ intf->cal_sysfs_live = cal_live;
intf->sysfs_live = true;
}
return 0;
+
+err_cal_groups:
+ while (cal_n--)
+ sysfs_remove_group(&hdev->dev.kobj, ally_cal_attr_groups[cal_n]);
+
+ for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++)
+ sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]);
+
+ return ret;
}
static void ally_sysfs_remove(struct hid_device *hdev, struct ally_intf *intf)
{
+ bool cal_live;
int i;
if (!intf)
@@ -1204,10 +1452,17 @@ static void ally_sysfs_remove(struct hid_device *hdev, struct ally_intf *intf)
return;
intf->sysfs_live = false;
+ cal_live = intf->cal_sysfs_live;
+ intf->cal_sysfs_live = false;
}
for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++)
sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]);
+
+ if (cal_live) {
+ for (i = 0; i < ARRAY_SIZE(ally_cal_attr_groups); i++)
+ sysfs_remove_group(&hdev->dev.kobj, ally_cal_attr_groups[i]);
+ }
}
struct ally_x_input_report {
@@ -1412,6 +1667,15 @@ static int hid_asus_ally_init(struct hid_device *hdev)
hid_warn(hdev, "Failed to restore vibration intensity: %d\n", ret);
}
+ if (ally_prefs.initialized && ally_prefs.user_cal_support) {
+ ret = ally_set_joystick_thresholds(hdev, ally_prefs.left_deadzone,
+ ally_prefs.left_outer_threshold,
+ ally_prefs.right_deadzone,
+ ally_prefs.right_outer_threshold);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to restore joystick thresholds: %d\n", ret);
+ }
+
return 0;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* Re: [PATCH v13 05/18] HID: asus: add joysticks inner and outer range configuration
2026-11-04 1:39 ` [PATCH v13 05/18] HID: asus: add joysticks inner and outer range configuration Denis Benato
@ 2026-11-04 1:53 ` sashiko-bot
0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-11-04 1:53 UTC (permalink / raw)
To: Denis Benato; +Cc: linux-input, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] HID: asus: ally_prefs_setup() mixes scoped_guard() with goto error handling
--
commit 23b7fd753beca8abc912e5e22d7a6deed94442a0
Author: Denis Benato <denis.benato@linux.dev>
HID: asus: add joysticks inner and outer range configuration
This commit adds sysfs attributes for configuring the inner and outer
joystick thresholds on ASUS ROG Ally devices and restores those threshold
settings on device initialization.
> diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
> index 459061391c378..a89ce83ae93f4 100644
> --- a/drivers/hid/hid-asus.c
> +++ b/drivers/hid/hid-asus.c
[ ... ]
> @@ -1182,15 +1401,44 @@ static int ally_prefs_setup(struct hid_device *hdev, struct ally_intf *intf)
> }
> }
>
> + scoped_guard(mutex, &ally_prefs.lock)
> + cal_live = ally_prefs.user_cal_support ||
> + ally_prefs.anti_deadzone_support ||
> + ally_prefs.resp_curve_support;
> +
> + /* Skip the calibration groups when the capability is missing. */
> + if (cal_live) {
> + for (; cal_n < ARRAY_SIZE(ally_cal_attr_groups); cal_n++) {
> + ret = sysfs_create_group(&hdev->dev.kobj,
> + ally_cal_attr_groups[cal_n]);
> + if (ret) {
> + hid_err(hdev, "Failed to create Ally sysfs group '%s': %d\n",
> + ally_cal_attr_groups[cal_n]->name, ret);
> + goto err_cal_groups;
> + }
> + }
> + }
> +
> scoped_guard(mutex, &ally_prefs.lock) {
> + intf->cal_sysfs_live = cal_live;
> intf->sysfs_live = true;
> }
>
> return 0;
> +
> +err_cal_groups:
> + while (cal_n--)
> + sysfs_remove_group(&hdev->dev.kobj, ally_cal_attr_groups[cal_n]);
> +
> + for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++)
> + sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]);
> +
> + return ret;
> }
[Severity: Low]
This isn't a bug, but ally_prefs_setup() uses scoped_guard() while also
introducing a goto err_cal_groups error path when sysfs_create_group()
fails for ally_cal_attr_groups.
Since the cleanup API guidelines discourage mixing scope-based cleanup
helpers with goto-based error handling in the same function, could the
unwinding be done inline (similar to the earlier ally_attr_groups loop in
ally_prefs_setup()), or by using sysfs_create_groups() and
sysfs_remove_groups() with a NULL-terminated array to avoid the goto label?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261104013935.512713-1-denis.benato@linux.dev?part=5
^ permalink raw reply [flat|nested] 25+ messages in thread
* [PATCH v13 06/18] HID: asus: add triggers inner and outer range configuration
2026-11-04 1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
` (4 preceding siblings ...)
2026-11-04 1:39 ` [PATCH v13 05/18] HID: asus: add joysticks inner and outer range configuration Denis Benato
@ 2026-11-04 1:39 ` Denis Benato
2026-11-04 1:39 ` [PATCH v13 07/18] HID: asus: add joysticks anti-deadzone configuration Denis Benato
` (11 subsequent siblings)
17 siblings, 0 replies; 25+ messages in thread
From: Denis Benato @ 2026-11-04 1:39 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
ROG Ally devices allows configuring inner and outer ranges for triggers
buttons on the back: allow userspace to configure the sensitivity by
exposing sysfs attributes.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
Co-developed-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Luke Jones <luke@ljones.dev>
---
drivers/hid/hid-asus.c | 225 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 225 insertions(+)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index a89ce83ae93f..8c49e9bdad50 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -106,6 +106,11 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define ASUS_ALLY_INNER_THRESHOLD_MIN 0
#define ASUS_ALLY_INNER_THRESHOLD_MAX 50
+#define ASUS_ALLY_TRIGGER_RANGE_LOWER_MIN 0
+#define ASUS_ALLY_TRIGGER_RANGE_LOWER_MAX 50
+#define ASUS_ALLY_TRIGGER_RANGE_UPPER_MIN 70
+#define ASUS_ALLY_TRIGGER_RANGE_UPPER_MAX 100
+
/* Spurious HID codes sent by QUIRK_ROG_NKEY_KEYBOARD devices */
#define ASUS_SPURIOUS_CODE_0XEA 0xea
#define ASUS_SPURIOUS_CODE_0XEC 0xec
@@ -821,6 +826,11 @@ static struct ally_prefs {
u8 left_outer_threshold;
u8 right_deadzone;
u8 right_outer_threshold;
+
+ u8 left_trigger_min;
+ u8 left_trigger_max;
+ u8 right_trigger_min;
+ u8 right_trigger_max;
} ally_prefs = {
.lock = __MUTEX_INITIALIZER(ally_prefs.lock),
};
@@ -1337,9 +1347,213 @@ static const struct attribute_group ally_right_joystick_axis_group = {
.attrs = right_joystick_axis_attrs,
};
+/**
+ * ally_set_trigger_ranges() - Set the gamepad trigger travel ranges
+ * @hdev: HID device the command packet is sent on
+ * @left_min: left trigger minimum (start of travel)
+ * @left_max: left trigger maximum (end of travel)
+ * @right_min: right trigger minimum (start of travel)
+ * @right_max: right trigger maximum (end of travel)
+ *
+ * Context: Caller holds ally_prefs.lock.
+ * Return: 0 on success, -ENOMEM if the command buffer cannot be allocated,
+ * or a negative errno if sending the packet fails.
+ */
+static int ally_set_trigger_ranges(struct hid_device *hdev, u8 left_min, u8 left_max,
+ u8 right_min, u8 right_max)
+{
+ const u8 payload[] = { left_min, left_max, right_min, right_max };
+ size_t buf_len;
+ int ret;
+
+ u8 *buf __free(kfree) = ally_alloc_cmd(hdev, CMD_SET_TRIGGER_RANGE,
+ payload, sizeof(payload),
+ &buf_len);
+
+ if (!buf)
+ return -ENOMEM;
+
+ ret = ally_gamepad_send_packet(hdev, buf, buf_len);
+ if (ret < 0) {
+ hid_err(hdev, "Failed to set trigger ranges: %d\n", ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+static void ally_trigger_defaults(void)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ if (ally_prefs.initialized)
+ return;
+
+ ally_prefs.left_trigger_min = ASUS_ALLY_TRIGGER_RANGE_LOWER_MIN;
+ ally_prefs.left_trigger_max = ASUS_ALLY_TRIGGER_RANGE_UPPER_MAX;
+ ally_prefs.right_trigger_min = ASUS_ALLY_TRIGGER_RANGE_LOWER_MIN;
+ ally_prefs.right_trigger_max = ASUS_ALLY_TRIGGER_RANGE_UPPER_MAX;
+}
+
+/* Indices into ally_trigger_limits[] */
+enum ally_trigger_limit {
+ ALLY_LEFT_MIN,
+ ALLY_LEFT_MAX,
+ ALLY_RIGHT_MIN,
+ ALLY_RIGHT_MAX,
+};
+
+static u8 * const ally_trigger_limits[] = {
+ [ALLY_LEFT_MIN] = &ally_prefs.left_trigger_min,
+ [ALLY_LEFT_MAX] = &ally_prefs.left_trigger_max,
+ [ALLY_RIGHT_MIN] = &ally_prefs.right_trigger_min,
+ [ALLY_RIGHT_MAX] = &ally_prefs.right_trigger_max,
+};
+
+static ssize_t ally_trigger_limit_show(char *buf, enum ally_trigger_limit which)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ if (!ally_prefs.user_cal_support)
+ return -EOPNOTSUPP;
+
+ return sysfs_emit(buf, "%u\n", *ally_trigger_limits[which]);
+}
+
+static ssize_t ally_trigger_limit_store(struct device *dev, const char *buf,
+ size_t count,
+ enum ally_trigger_limit which,
+ int lo, int hi,
+ const char *group, const char *name)
+{
+ u8 value, v[ARRAY_SIZE(ally_trigger_limits)];
+ struct hid_device *hdev = to_hid_device(dev);
+ size_t i;
+ int ret;
+
+ ret = kstrtou8(buf, 10, &value);
+ if (ret || value < lo || value > hi)
+ return -EINVAL;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ if (!ally_prefs.user_cal_support)
+ return -EOPNOTSUPP;
+
+ for (i = 0; i < ARRAY_SIZE(v); i++)
+ v[i] = *ally_trigger_limits[i];
+ v[which] = value;
+
+ ret = ally_set_trigger_ranges(hdev, v[ALLY_LEFT_MIN], v[ALLY_LEFT_MAX],
+ v[ALLY_RIGHT_MIN], v[ALLY_RIGHT_MAX]);
+ if (ret)
+ return ret;
+
+ *ally_trigger_limits[which] = value;
+ }
+
+ sysfs_notify(&dev->kobj, group, name);
+
+ return count;
+}
+
+/*
+ * Generates <side>_trigger_range_<limit>_limit_{show,store,range_show}.
+ * @lo is the lowest value the store accepts; it is 0 (no check) for the
+ * "lower" limits, matching the original code, which only checked the maximum.
+ */
+#define ALLY_TRIGGER_LIMIT_FUNCS(side, limit, LIMIT, which, lo) \
+static ssize_t side##_trigger_range_##limit##_limit_show(struct device *dev, \
+ struct device_attribute *attr, \
+ char *buf) \
+{ \
+ return ally_trigger_limit_show(buf, which); \
+} \
+ \
+static ssize_t side##_trigger_range_##limit##_limit_store(struct device *dev, \
+ struct device_attribute *attr, \
+ const char *buf, \
+ size_t count) \
+{ \
+ return ally_trigger_limit_store(dev, buf, count, which, lo, \
+ ASUS_ALLY_TRIGGER_RANGE_##LIMIT##_MAX, \
+ #side "_trigger", "range_" #limit "_limit"); \
+} \
+ \
+static ssize_t side##_trigger_range_##limit##_limit_range_show(struct device *dev, \
+ struct device_attribute *attr, \
+ char *buf) \
+{ \
+ return sysfs_emit(buf, "%u %u\n", ASUS_ALLY_TRIGGER_RANGE_##LIMIT##_MIN, \
+ ASUS_ALLY_TRIGGER_RANGE_##LIMIT##_MAX); \
+}
+
+ALLY_TRIGGER_LIMIT_FUNCS(left, lower, LOWER, ALLY_LEFT_MIN, 0)
+ALLY_TRIGGER_LIMIT_FUNCS(left, upper, UPPER, ALLY_LEFT_MAX, ASUS_ALLY_TRIGGER_RANGE_UPPER_MIN)
+ALLY_TRIGGER_LIMIT_FUNCS(right, lower, LOWER, ALLY_RIGHT_MIN, 0)
+ALLY_TRIGGER_LIMIT_FUNCS(right, upper, UPPER, ALLY_RIGHT_MAX, ASUS_ALLY_TRIGGER_RANGE_UPPER_MIN)
+
+static struct device_attribute dev_attr_left_trigger_range_lower_limit =
+ __ATTR(range_lower_limit, 0644, left_trigger_range_lower_limit_show,
+ left_trigger_range_lower_limit_store);
+
+static struct device_attribute dev_attr_left_trigger_range_lower_limit_range =
+ __ATTR(range_lower_limit_range, 0444,
+ left_trigger_range_lower_limit_range_show, NULL);
+
+static struct device_attribute dev_attr_left_trigger_range_upper_limit =
+ __ATTR(range_upper_limit, 0644, left_trigger_range_upper_limit_show,
+ left_trigger_range_upper_limit_store);
+
+static struct device_attribute dev_attr_left_trigger_range_upper_limit_range =
+ __ATTR(range_upper_limit_range, 0444,
+ left_trigger_range_upper_limit_range_show, NULL);
+
+static struct device_attribute dev_attr_right_trigger_range_lower_limit =
+ __ATTR(range_lower_limit, 0644, right_trigger_range_lower_limit_show,
+ right_trigger_range_lower_limit_store);
+
+static struct device_attribute dev_attr_right_trigger_range_lower_limit_range =
+ __ATTR(range_lower_limit_range, 0444,
+ right_trigger_range_lower_limit_range_show, NULL);
+
+static struct device_attribute dev_attr_right_trigger_range_upper_limit =
+ __ATTR(range_upper_limit, 0644, right_trigger_range_upper_limit_show,
+ right_trigger_range_upper_limit_store);
+
+static struct device_attribute dev_attr_right_trigger_range_upper_limit_range =
+ __ATTR(range_upper_limit_range, 0444,
+ right_trigger_range_upper_limit_range_show, NULL);
+
+static struct attribute *left_trigger_attrs[] = {
+ &dev_attr_left_trigger_range_lower_limit.attr,
+ &dev_attr_left_trigger_range_upper_limit.attr,
+ &dev_attr_left_trigger_range_lower_limit_range.attr,
+ &dev_attr_left_trigger_range_upper_limit_range.attr,
+ NULL
+};
+
+static struct attribute *right_trigger_attrs[] = {
+ &dev_attr_right_trigger_range_lower_limit.attr,
+ &dev_attr_right_trigger_range_upper_limit.attr,
+ &dev_attr_right_trigger_range_lower_limit_range.attr,
+ &dev_attr_right_trigger_range_upper_limit_range.attr,
+ NULL
+};
+
+static const struct attribute_group ally_left_trigger_group = {
+ .name = "left_trigger",
+ .attrs = left_trigger_attrs,
+};
+
+static const struct attribute_group ally_right_trigger_group = {
+ .name = "right_trigger",
+ .attrs = right_trigger_attrs,
+};
static const struct attribute_group *const ally_cal_attr_groups[] = {
&ally_left_joystick_axis_group,
&ally_right_joystick_axis_group,
+ &ally_left_trigger_group,
+ &ally_right_trigger_group,
};
static struct attribute *ally_config_attrs[] = {
@@ -1372,6 +1586,7 @@ static int ally_prefs_setup(struct hid_device *hdev, struct ally_intf *intf)
ally_vibration_defaults();
ally_joystick_defaults();
+ ally_trigger_defaults();
scoped_guard(mutex, &ally_prefs.lock) {
if (!ally_prefs.initialized) {
@@ -1676,6 +1891,16 @@ static int hid_asus_ally_init(struct hid_device *hdev)
hid_warn(hdev, "Failed to restore joystick thresholds: %d\n", ret);
}
+ if (ally_prefs.initialized && ally_prefs.user_cal_support) {
+ ret = ally_set_trigger_ranges(hdev,
+ ally_prefs.left_trigger_min,
+ ally_prefs.left_trigger_max,
+ ally_prefs.right_trigger_min,
+ ally_prefs.right_trigger_max);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to restore trigger ranges: %d\n", ret);
+ }
+
return 0;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH v13 07/18] HID: asus: add joysticks anti-deadzone configuration
2026-11-04 1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
` (5 preceding siblings ...)
2026-11-04 1:39 ` [PATCH v13 06/18] HID: asus: add triggers " Denis Benato
@ 2026-11-04 1:39 ` Denis Benato
2026-11-04 1:39 ` [PATCH v13 08/18] HID: asus: add support for response curve Denis Benato
` (10 subsequent siblings)
17 siblings, 0 replies; 25+ messages in thread
From: Denis Benato @ 2026-11-04 1:39 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
ROG Ally devices allow configuring the anti-deadzone parameter for
the resistive joysticks devices as over time those develops drift,
therefore allow userspace to configure the anti-deadzone by exposing
relevant sysfs attributes.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
Co-developed-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Luke Jones <luke@ljones.dev>
---
drivers/hid/hid-asus.c | 157 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 157 insertions(+)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 8c49e9bdad50..344abf1510c4 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -106,6 +106,9 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define ASUS_ALLY_INNER_THRESHOLD_MIN 0
#define ASUS_ALLY_INNER_THRESHOLD_MAX 50
+#define ASUS_ALLY_ANTI_DEADZONE_MIN 0
+#define ASUS_ALLY_ANTI_DEADZONE_MAX 100
+
#define ASUS_ALLY_TRIGGER_RANGE_LOWER_MIN 0
#define ASUS_ALLY_TRIGGER_RANGE_LOWER_MAX 50
#define ASUS_ALLY_TRIGGER_RANGE_UPPER_MIN 70
@@ -831,6 +834,9 @@ static struct ally_prefs {
u8 left_trigger_max;
u8 right_trigger_min;
u8 right_trigger_max;
+
+ u8 left_anti_deadzone;
+ u8 right_anti_deadzone;
} ally_prefs = {
.lock = __MUTEX_INITIALIZER(ally_prefs.lock),
};
@@ -1317,11 +1323,153 @@ static struct device_attribute dev_attr_right_joystick_outer_threshold =
static struct device_attribute dev_attr_right_joystick_outer_threshold_range =
__ATTR(outer_threshold_range, 0444, right_joystick_outer_threshold_range_show, NULL);
+/**
+ * ally_set_anti_deadzone() - Send the joystick anti-deadzone values to the device
+ * @hdev: HID device the packet is sent on
+ * @left: Anti-deadzone of the left joystick
+ * @right: Anti-deadzone of the right joystick
+ *
+ * Context: Caller must hold ally_prefs.lock.
+ *
+ * Return: 0 on success, -%EOPNOTSUPP if the device does not support
+ * anti-deadzone, -%ENOMEM if the command buffer cannot be allocated, or a
+ * negative error code if sending the packet fails.
+ */
+static int ally_set_anti_deadzone(struct hid_device *hdev, u8 left, u8 right)
+{
+ const u8 payload[] = { left, right };
+ size_t buf_len;
+ int ret;
+
+ if (!ally_prefs.anti_deadzone_support) {
+ hid_dbg(hdev, "Anti-deadzone not supported on this device\n");
+ return -EOPNOTSUPP;
+ }
+
+ u8 *buf __free(kfree) = ally_alloc_cmd(hdev, CMD_SET_ANTI_DEADZONE,
+ payload, sizeof(payload),
+ &buf_len);
+ if (!buf)
+ return -ENOMEM;
+
+ ret = ally_gamepad_send_packet(hdev, buf, buf_len);
+ if (ret < 0) {
+ hid_err(hdev, "Failed to set anti-deadzone values: %d\n", ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+/* Indices into ally_anti_deadzones[] */
+enum ally_joystick {
+ ALLY_LEFT_JOYSTICK,
+ ALLY_RIGHT_JOYSTICK,
+};
+
+static u8 *const ally_anti_deadzones[] = {
+ [ALLY_LEFT_JOYSTICK] = &ally_prefs.left_anti_deadzone,
+ [ALLY_RIGHT_JOYSTICK] = &ally_prefs.right_anti_deadzone,
+};
+
+static ssize_t ally_anti_deadzone_show(struct device *dev, char *buf,
+ enum ally_joystick which)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ if (!ally_prefs.anti_deadzone_support) {
+ hid_dbg(to_hid_device(dev), "Anti-deadzone not supported on this device\n");
+ return -EOPNOTSUPP;
+ }
+
+ return sysfs_emit(buf, "%u\n", *ally_anti_deadzones[which]);
+}
+
+static ssize_t ally_anti_deadzone_store(struct device *dev, const char *buf,
+ size_t count, enum ally_joystick which,
+ const char *group)
+{
+ u8 value, dz[ARRAY_SIZE(ally_anti_deadzones)];
+ struct hid_device *hdev = to_hid_device(dev);
+ unsigned int i;
+ int ret;
+
+ ret = kstrtou8(buf, 10, &value);
+ if (ret || value > ASUS_ALLY_ANTI_DEADZONE_MAX)
+ return -EINVAL;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ if (!ally_prefs.anti_deadzone_support) {
+ hid_dbg(hdev, "Anti-deadzone not supported on this device\n");
+ return -EOPNOTSUPP;
+ }
+
+ for (i = 0; i < ARRAY_SIZE(dz); i++)
+ dz[i] = *ally_anti_deadzones[i];
+ dz[which] = value;
+
+ ret = ally_set_anti_deadzone(hdev, dz[ALLY_LEFT_JOYSTICK],
+ dz[ALLY_RIGHT_JOYSTICK]);
+ if (ret)
+ return ret;
+
+ *ally_anti_deadzones[which] = value;
+ }
+
+ sysfs_notify(&dev->kobj, group, "anti_deadzone");
+
+ return count;
+}
+
+/* Generates <side>_joystick_anti_deadzone_{show,store,range_show}. */
+#define ALLY_ANTI_DEADZONE_FUNCS(side, SIDE) \
+static ssize_t side##_joystick_anti_deadzone_show(struct device *dev, \
+ struct device_attribute *attr, \
+ char *buf) \
+{ \
+ return ally_anti_deadzone_show(dev, buf, ALLY_##SIDE##_JOYSTICK); \
+} \
+ \
+static ssize_t side##_joystick_anti_deadzone_store(struct device *dev, \
+ struct device_attribute *attr, \
+ const char *buf, size_t count) \
+{ \
+ return ally_anti_deadzone_store(dev, buf, count, ALLY_##SIDE##_JOYSTICK, \
+ #side "_joystick_axis"); \
+} \
+ \
+static ssize_t side##_joystick_anti_deadzone_range_show(struct device *dev, \
+ struct device_attribute *attr, \
+ char *buf) \
+{ \
+ return sysfs_emit(buf, "%u %u\n", ASUS_ALLY_ANTI_DEADZONE_MIN, \
+ ASUS_ALLY_ANTI_DEADZONE_MAX); \
+}
+
+ALLY_ANTI_DEADZONE_FUNCS(left, LEFT)
+ALLY_ANTI_DEADZONE_FUNCS(right, RIGHT)
+
+static struct device_attribute dev_attr_left_joystick_anti_deadzone =
+ __ATTR(anti_deadzone, 0644, left_joystick_anti_deadzone_show,
+ left_joystick_anti_deadzone_store);
+
+static struct device_attribute dev_attr_left_joystick_anti_deadzone_range =
+ __ATTR(anti_deadzone_range, 0444, left_joystick_anti_deadzone_range_show, NULL);
+
+static struct device_attribute dev_attr_right_joystick_anti_deadzone =
+ __ATTR(anti_deadzone, 0644, right_joystick_anti_deadzone_show,
+ right_joystick_anti_deadzone_store);
+
+static struct device_attribute dev_attr_right_joystick_anti_deadzone_range =
+ __ATTR(anti_deadzone_range, 0444, right_joystick_anti_deadzone_range_show, NULL);
+
static struct attribute *left_joystick_axis_attrs[] = {
&dev_attr_left_joystick_inner_threshold.attr,
&dev_attr_left_joystick_outer_threshold.attr,
&dev_attr_left_joystick_inner_threshold_range.attr,
&dev_attr_left_joystick_outer_threshold_range.attr,
+ &dev_attr_left_joystick_anti_deadzone.attr,
+ &dev_attr_left_joystick_anti_deadzone_range.attr,
NULL
};
@@ -1330,6 +1478,8 @@ static struct attribute *right_joystick_axis_attrs[] = {
&dev_attr_right_joystick_outer_threshold.attr,
&dev_attr_right_joystick_inner_threshold_range.attr,
&dev_attr_right_joystick_outer_threshold_range.attr,
+ &dev_attr_right_joystick_anti_deadzone.attr,
+ &dev_attr_right_joystick_anti_deadzone_range.attr,
NULL
};
@@ -1901,6 +2051,13 @@ static int hid_asus_ally_init(struct hid_device *hdev)
hid_warn(hdev, "Failed to restore trigger ranges: %d\n", ret);
}
+ if (ally_prefs.initialized && ally_prefs.anti_deadzone_support) {
+ ret = ally_set_anti_deadzone(hdev, ally_prefs.left_anti_deadzone,
+ ally_prefs.right_anti_deadzone);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to restore anti-deadzone: %d\n", ret);
+ }
+
return 0;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH v13 08/18] HID: asus: add support for response curve
2026-11-04 1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
` (6 preceding siblings ...)
2026-11-04 1:39 ` [PATCH v13 07/18] HID: asus: add joysticks anti-deadzone configuration Denis Benato
@ 2026-11-04 1:39 ` Denis Benato
2026-11-04 1:39 ` [PATCH v13 09/18] HID: asus: add support to force feedback Denis Benato
` (9 subsequent siblings)
17 siblings, 0 replies; 25+ messages in thread
From: Denis Benato @ 2026-11-04 1:39 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
ROG ally devices allows configuring the response curve of both joysticks,
therefore add the ability of userspace to modify the response curve by
exposing relevant sysfs attributes.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
Co-developed-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Luke Jones <luke@ljones.dev>
---
drivers/hid/hid-asus.c | 271 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 271 insertions(+)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 344abf1510c4..b55fe10826a5 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -24,6 +24,7 @@
#include <linux/cleanup.h>
#include <linux/compiler_attributes.h>
#include <linux/container_of.h>
+#include <linux/ctype.h>
#include <linux/dmi.h>
#include <linux/err.h>
#include <linux/hid.h>
@@ -793,6 +794,18 @@ static int ally_get_endpoint_address(struct hid_device *hdev)
return ep->desc.bEndpointAddress;
}
+struct ally_joystick_resp_curve_param {
+ u8 move;
+ u8 resp;
+} __packed;
+
+struct ally_joystick_resp_curve {
+ struct ally_joystick_resp_curve_param entry_1;
+ struct ally_joystick_resp_curve_param entry_2;
+ struct ally_joystick_resp_curve_param entry_3;
+ struct ally_joystick_resp_curve_param entry_4;
+} __packed;
+
/*
* Settings that must survive the controller powering off and coming back.
* No device or input pointers live here: every transfer uses the interface
@@ -837,6 +850,9 @@ static struct ally_prefs {
u8 left_anti_deadzone;
u8 right_anti_deadzone;
+
+ struct ally_joystick_resp_curve left_curve;
+ struct ally_joystick_resp_curve right_curve;
} ally_prefs = {
.lock = __MUTEX_INITIALIZER(ally_prefs.lock),
};
@@ -1463,6 +1479,239 @@ static struct device_attribute dev_attr_right_joystick_anti_deadzone =
static struct device_attribute dev_attr_right_joystick_anti_deadzone_range =
__ATTR(anti_deadzone_range, 0444, right_joystick_anti_deadzone_range_show, NULL);
+enum ally_joystick_side {
+ JOYSTICK_LEFT = 0,
+ JOYSTICK_RIGHT,
+};
+
+/**
+ * ally_set_joystick_resp_curve() - Set a joystick response curve
+ * @hdev: HID device the command packet is sent on
+ * @side: joystick to program, %JOYSTICK_LEFT or %JOYSTICK_RIGHT
+ * @curve: response curve to apply
+ *
+ * Context: Caller holds ally_prefs.lock.
+ * Return: 0 on success, negative errno on failure.
+ */
+static int ally_set_joystick_resp_curve(struct hid_device *hdev,
+ enum ally_joystick_side side,
+ struct ally_joystick_resp_curve *curve)
+{
+ const u8 payload[] = {
+ side,
+ curve->entry_1.move, curve->entry_1.resp,
+ curve->entry_2.move, curve->entry_2.resp,
+ curve->entry_3.move, curve->entry_3.resp,
+ curve->entry_4.move, curve->entry_4.resp
+ };
+ size_t buf_len;
+ int ret;
+
+ u8 *buf __free(kfree) = ally_alloc_cmd(hdev, CMD_SET_RESP_CURVE, payload,
+ sizeof(payload), &buf_len);
+
+ if (!buf)
+ return -ENOMEM;
+
+ ret = ally_gamepad_send_packet(hdev, buf, buf_len);
+
+ /*
+ * Previous function returns the number of bytes sent, or a negative error code.
+ * From this function I only need either success or the failure.
+ */
+ return ret < 0 ? ret : 0;
+}
+
+static bool ally_resp_curve_is_valid(const struct ally_joystick_resp_curve *curve)
+{
+ return curve->entry_1.move < curve->entry_2.move &&
+ curve->entry_2.move < curve->entry_3.move &&
+ curve->entry_3.move < curve->entry_4.move;
+}
+
+/* Linear curve. */
+static const struct ally_joystick_resp_curve ally_linear_curve = {
+ .entry_1 = { .move = 0, .resp = 0 },
+ .entry_2 = { .move = 33, .resp = 33 },
+ .entry_3 = { .move = 66, .resp = 66 },
+ .entry_4 = { .move = 100, .resp = 100 },
+};
+
+static void ally_curve_defaults(void)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ if (ally_prefs.initialized)
+ return;
+
+ ally_prefs.left_curve = ally_linear_curve;
+ ally_prefs.right_curve = ally_linear_curve;
+}
+
+/* Indexed by enum ally_joystick_side */
+static struct ally_joystick_resp_curve * const ally_resp_curves[] = {
+ [JOYSTICK_LEFT] = &ally_prefs.left_curve,
+ [JOYSTICK_RIGHT] = &ally_prefs.right_curve,
+};
+
+static ssize_t response_curve_show(enum ally_joystick_side side, char *buf)
+{
+ const struct ally_joystick_resp_curve *curve = ally_resp_curves[side];
+
+ guard(mutex)(&ally_prefs.lock);
+
+ if (!ally_prefs.resp_curve_support)
+ return -EOPNOTSUPP;
+
+ return sysfs_emit(buf, "%u %u %u %u %u %u %u %u\n",
+ curve->entry_1.move, curve->entry_1.resp,
+ curve->entry_2.move, curve->entry_2.resp,
+ curve->entry_3.move, curve->entry_3.resp,
+ curve->entry_4.move, curve->entry_4.resp);
+}
+
+/**
+ * ally_parse_u8_token() - Parse one decimal u8 from a sysfs write
+ * @buf: write buffer
+ * @count: number of bytes in @buf
+ * @pos: offset in @buf to start scanning at; on success it is advanced to
+ * the first byte after the token
+ * @val: where the parsed value is stored; only written on success
+ *
+ * Skips leading whitespace, then parses an optional '+' followed by one or
+ * more decimal digits. The token, sign included, may be at most 15 characters
+ * long and its value must fit in a u8.
+ *
+ * The scan is bounded by @count, not by a terminating NUL, so an embedded NUL
+ * is a byte in the write, not the end of the buffer.
+ *
+ * Return: 0 on success, -%EINVAL if there is no number at @pos, the token is
+ * too long, or the value does not fit in a u8. On failure @pos is left at an
+ * unspecified offset.
+ */
+static int ally_parse_u8_token(const char *buf, size_t count, size_t *pos, u8 *val)
+{
+ char token[16];
+ size_t n = 0;
+
+ while (*pos < count && isspace((unsigned char)buf[*pos]))
+ (*pos)++;
+
+ if (*pos < count && buf[*pos] == '+')
+ token[n++] = buf[(*pos)++];
+
+ if (*pos >= count || !isdigit((unsigned char)buf[*pos]))
+ return -EINVAL;
+
+ while (*pos < count && isdigit((unsigned char)buf[*pos])) {
+ if (n >= sizeof(token) - 1)
+ return -EINVAL;
+ token[n++] = buf[(*pos)++];
+ }
+
+ token[n] = '\0';
+
+ return kstrtou8(token, 10, val) ? -EINVAL : 0;
+}
+
+static ssize_t response_curve_store(struct device *dev,
+ enum ally_joystick_side side,
+ const char *group, const char *buf,
+ size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct ally_joystick_resp_curve curve;
+ size_t pos = 0;
+ int ret, i;
+ u8 v[8];
+
+ /*
+ * Parse and validate the whole curve before taking the lock: a
+ * write is all-or-nothing and a rejected one must leave the
+ * recorded curve untouched.
+ */
+ for (i = 0; i < ARRAY_SIZE(v); i++) {
+ ret = ally_parse_u8_token(buf, count, &pos, &v[i]);
+ if (ret || v[i] > 100)
+ return -EINVAL;
+ }
+
+ for (; pos < count; pos++)
+ if (!isspace((unsigned char)buf[pos]))
+ return -EINVAL;
+
+ curve = (struct ally_joystick_resp_curve) {
+ .entry_1 = { .move = v[0], .resp = v[1] },
+ .entry_2 = { .move = v[2], .resp = v[3] },
+ .entry_3 = { .move = v[4], .resp = v[5] },
+ .entry_4 = { .move = v[6], .resp = v[7] },
+ };
+
+ if (!ally_resp_curve_is_valid(&curve))
+ return -EINVAL;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ if (!ally_prefs.resp_curve_support)
+ return -EOPNOTSUPP;
+
+ ret = ally_set_joystick_resp_curve(hdev, side, &curve);
+ if (ret < 0) {
+ hid_err(hdev, "Failed to set joystick response curve: %d\n",
+ ret);
+ return ret;
+ }
+
+ *ally_resp_curves[side] = curve;
+ }
+
+ sysfs_notify(&dev->kobj, group, "response_curve");
+
+ return count;
+}
+
+/* Generates <side>_response_curve_{show,store}. */
+#define ALLY_RESP_CURVE_FUNCS(side, SIDE) \
+static ssize_t side##_response_curve_show(struct device *dev, \
+ struct device_attribute *attr, \
+ char *buf) \
+{ \
+ return response_curve_show(JOYSTICK_##SIDE, buf); \
+} \
+ \
+static ssize_t side##_response_curve_store(struct device *dev, \
+ struct device_attribute *attr, \
+ const char *buf, size_t count) \
+{ \
+ return response_curve_store(dev, JOYSTICK_##SIDE, \
+ #side "_joystick_axis", buf, count); \
+}
+
+ALLY_RESP_CURVE_FUNCS(left, LEFT)
+ALLY_RESP_CURVE_FUNCS(right, RIGHT)
+
+static ssize_t response_curve_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf,
+ "move_1 resp_1 move_2 resp_2 move_3 resp_3 move_4 resp_4\n"
+ "\n"
+ "move_N: joystick deflection percentage at which curve point N\n"
+ "is placed (0-100, each move_N must be greater than the\n"
+ "previous one). resp_N: response percentage applied from\n"
+ "point N on (0-100).\n");
+}
+
+static struct device_attribute dev_attr_left_response_curve =
+ __ATTR(response_curve, 0644, left_response_curve_show,
+ left_response_curve_store);
+static struct device_attribute dev_attr_right_response_curve =
+ __ATTR(response_curve, 0644, right_response_curve_show,
+ right_response_curve_store);
+static struct device_attribute dev_attr_left_response_curve_index =
+ __ATTR(response_curve_index, 0444, response_curve_index_show, NULL);
+static struct device_attribute dev_attr_right_response_curve_index =
+ __ATTR(response_curve_index, 0444, response_curve_index_show, NULL);
+
static struct attribute *left_joystick_axis_attrs[] = {
&dev_attr_left_joystick_inner_threshold.attr,
&dev_attr_left_joystick_outer_threshold.attr,
@@ -1470,6 +1719,8 @@ static struct attribute *left_joystick_axis_attrs[] = {
&dev_attr_left_joystick_outer_threshold_range.attr,
&dev_attr_left_joystick_anti_deadzone.attr,
&dev_attr_left_joystick_anti_deadzone_range.attr,
+ &dev_attr_left_response_curve.attr,
+ &dev_attr_left_response_curve_index.attr,
NULL
};
@@ -1480,6 +1731,8 @@ static struct attribute *right_joystick_axis_attrs[] = {
&dev_attr_right_joystick_outer_threshold_range.attr,
&dev_attr_right_joystick_anti_deadzone.attr,
&dev_attr_right_joystick_anti_deadzone_range.attr,
+ &dev_attr_right_response_curve.attr,
+ &dev_attr_right_response_curve_index.attr,
NULL
};
@@ -1737,6 +1990,7 @@ static int ally_prefs_setup(struct hid_device *hdev, struct ally_intf *intf)
ally_vibration_defaults();
ally_joystick_defaults();
ally_trigger_defaults();
+ ally_curve_defaults();
scoped_guard(mutex, &ally_prefs.lock) {
if (!ally_prefs.initialized) {
@@ -2058,6 +2312,23 @@ static int hid_asus_ally_init(struct hid_device *hdev)
hid_warn(hdev, "Failed to restore anti-deadzone: %d\n", ret);
}
+ /*
+ * The MCU does not keep the response curve across a reset.
+ * A sysfs write validates the curve before it is recorded, so the
+ * cache always holds one the controller accepted.
+ */
+ if (ally_prefs.initialized && ally_prefs.resp_curve_support) {
+ ret = ally_set_joystick_resp_curve(hdev, JOYSTICK_LEFT,
+ &ally_prefs.left_curve);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to restore left response curve: %d\n", ret);
+
+ ret = ally_set_joystick_resp_curve(hdev, JOYSTICK_RIGHT,
+ &ally_prefs.right_curve);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to restore right response curve: %d\n", ret);
+ }
+
return 0;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH v13 09/18] HID: asus: add support to force feedback
2026-11-04 1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
` (7 preceding siblings ...)
2026-11-04 1:39 ` [PATCH v13 08/18] HID: asus: add support for response curve Denis Benato
@ 2026-11-04 1:39 ` Denis Benato
2026-11-04 1:39 ` [PATCH v13 10/18] HID: asus: add support for gamepad mode Denis Benato
` (8 subsequent siblings)
17 siblings, 0 replies; 25+ messages in thread
From: Denis Benato @ 2026-11-04 1:39 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
Unlike ROG ally the X version and following ones uses DInput protocol
and the force feedback needs to be implemented as its protocol is
vendor-specific, therefore add support for FF_RUMBLE with magnitude
scaling on a work-queue based approach to avoid using possibly
sleeping calls in atomic context.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
Signed-off-by: Khamunetri Clark <khamunetriclark@gmail.com>
Co-developed-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Luke Jones <luke@ljones.dev>
---
drivers/hid/Kconfig | 1 +
drivers/hid/hid-asus.c | 142 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 143 insertions(+)
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 2481bc01c435..dd411f6eff4f 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -189,6 +189,7 @@ config HID_ASUS
depends on USB_HID
depends on LEDS_CLASS
depends on ASUS_WMI || ASUS_WMI=n
+ select INPUT_FF_MEMLESS
select POWER_SUPPLY
help
Support for Asus notebook built-in keyboard and touchpad via i2c, and
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index b55fe10826a5..9c9ed75b98a4 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -22,6 +22,7 @@
#include <linux/acpi.h>
#include <linux/cleanup.h>
+#include <linux/compiler.h>
#include <linux/compiler_attributes.h>
#include <linux/container_of.h>
#include <linux/ctype.h>
@@ -30,6 +31,7 @@
#include <linux/hid.h>
#include <linux/jiffies.h>
#include <linux/kstrtox.h>
+#include <linux/limits.h>
#include <linux/list.h>
#include <linux/module.h>
#include <linux/mutex.h>
@@ -219,6 +221,23 @@ struct asus_touchpad_info {
int report_size;
};
+/* Output report 0x0D on the gamepad interface. */
+struct ally_ff_data {
+ u8 enable;
+ u8 magnitude_left;
+ u8 magnitude_right;
+ u8 magnitude_strong;
+ u8 magnitude_weak;
+ u8 pulse_sustain_10ms;
+ u8 pulse_release_10ms;
+ u8 loop_count;
+} __packed;
+
+struct ally_ff_report {
+ u8 report_id;
+ struct ally_ff_data ff;
+} __packed;
+
struct ally_intf {
struct hid_device *hdev;
/* HID-core evdev of this interface. Reports for it land here. */
@@ -235,6 +254,14 @@ struct ally_intf {
*/
bool sysfs_live;
bool cal_sysfs_live;
+ struct {
+ /* Guards live, update, and packet. */
+ spinlock_t lock;
+ bool live;
+ bool update;
+ struct ally_ff_report packet;
+ struct work_struct work;
+ } ff;
};
struct asus_drvdata {
@@ -341,10 +368,15 @@ enum ally_command_codes {
CMD_SET_ANTI_DEADZONE = 0x18,
};
+/* Rumble magnitudes use the hardware's 0..100 intensity range. */
+#define ALLY_FF_MAX_INTENSITY 100
+
static const u8 ally_force_feedback_off[] = {
0x0D, 0x0F, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xEB
};
+static_assert(sizeof(struct ally_ff_report) == sizeof(ally_force_feedback_off));
+
/*
* Each Ally interface owns its own ally_intf. A report is delivered on the
* evdev of the interface that received it. Nothing here points at another
@@ -2217,6 +2249,84 @@ static struct input_dev *ally_x_alloc_input_dev(struct hid_device *hdev,
return input_dev;
}
+static int ally_ff_output(struct hid_device *hdev, const void *buf, size_t len)
+{
+ u8 *dmabuf __free(kfree) = kmemdup(buf, len, GFP_KERNEL);
+
+ if (!dmabuf)
+ return -ENOMEM;
+
+ return hid_hw_output_report(hdev, dmabuf, len);
+}
+
+static int ally_ff_off(struct hid_device *hdev)
+{
+ return ally_ff_output(hdev, ally_force_feedback_off,
+ sizeof(ally_force_feedback_off));
+}
+
+static void ally_ff_work_fn(struct work_struct *work)
+{
+ struct ally_intf *intf = container_of(work, struct ally_intf, ff.work);
+ struct ally_ff_report report;
+ bool update = false;
+ int ret;
+
+ scoped_guard(spinlock_irqsave, &intf->ff.lock) {
+ if (!intf->ff.update)
+ break;
+
+ report = intf->ff.packet;
+ intf->ff.update = false;
+ update = true;
+ }
+
+ if (!update)
+ return;
+
+ ret = ally_ff_output(intf->hdev, &report, sizeof(report));
+ if (ret < 0)
+ hid_err(intf->hdev, "Failed to send force-feedback: %d\n", ret);
+}
+
+static int ally_ff_play(struct input_dev *idev, void *data, struct ff_effect *effect)
+{
+ struct ally_intf *intf = input_get_drvdata(idev);
+
+ if (effect->type != FF_RUMBLE)
+ return 0;
+
+ guard(spinlock_irqsave)(&intf->ff.lock);
+
+ if (!intf->ff.live)
+ return 0;
+
+ intf->ff.packet.ff.magnitude_strong =
+ effect->u.rumble.strong_magnitude * ALLY_FF_MAX_INTENSITY / U16_MAX;
+ intf->ff.packet.ff.magnitude_weak =
+ effect->u.rumble.weak_magnitude * ALLY_FF_MAX_INTENSITY / U16_MAX;
+ intf->ff.update = true;
+ schedule_work(&intf->ff.work);
+ return 0;
+}
+
+static void ally_ff_quiesce(struct ally_intf *intf)
+{
+ int ret;
+
+ if (!intf || !intf->gamepad)
+ return;
+
+ scoped_guard(spinlock_irqsave, &intf->ff.lock)
+ intf->ff.live = false;
+
+ cancel_work_sync(&intf->ff.work);
+
+ ret = ally_ff_off(intf->hdev);
+ if (ret < 0)
+ hid_warn(intf->hdev, "Failed to stop force-feedback\n");
+}
+
static int ally_x_setup_input(struct hid_device *hdev, struct ally_intf *intf)
{
struct input_dev *input = ally_x_alloc_input_dev(hdev, intf);
@@ -2245,8 +2355,24 @@ static int ally_x_setup_input(struct hid_device *hdev, struct ally_intf *intf)
input_set_capability(input, EV_KEY, BTN_THUMBL);
input_set_capability(input, EV_KEY, BTN_THUMBR);
+ spin_lock_init(&intf->ff.lock);
+ INIT_WORK(&intf->ff.work, ally_ff_work_fn);
+ memcpy(&intf->ff.packet, ally_force_feedback_off, sizeof(intf->ff.packet));
+ intf->ff.update = false;
+ intf->ff.live = false;
+
+ input_set_capability(input, EV_FF, FF_RUMBLE);
+ ret = input_ff_create_memless(input, NULL, ally_ff_play);
+ if (ret) {
+ hid_err(hdev, "Failed to create force-feedback: %d\n", ret);
+ input_free_device(input);
+ return ret;
+ }
+
+ intf->ff.live = true;
ret = input_register_device(input);
if (ret) {
+ intf->ff.live = false;
hid_err(hdev, "Failed to register Ally X gamepad device: %d\n", ret);
input_free_device(input);
return ret;
@@ -2437,6 +2563,14 @@ static struct ally_intf *hid_asus_ally_probe(struct hid_device *hdev)
hid_err(hdev, "Failed to create Ally X gamepad device.\n");
return ERR_PTR(ret);
}
+
+ /*
+ * This is the interface that owns the rumble output report.
+ * Failure to silence it is not fatal.
+ */
+ ret = ally_ff_off(hdev);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to disable force-feedback: %d\n", ret);
break;
case HID_ALLY_INTF_KEYBOARD_IN:
break;
@@ -2459,6 +2593,13 @@ static int hid_asus_ally_reset_resume(struct hid_device *hdev, struct ally_intf
return 0;
ep = ally_get_endpoint_address(hdev);
+ if (ep == HID_ALLY_X_INTF_IN) {
+ ret = ally_ff_off(hdev);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to disable force-feedback: %d\n", ret);
+ return 0;
+ }
+
if (ep != HID_ALLY_INTF_CFG_IN)
return 0;
@@ -2487,6 +2628,7 @@ static struct ally_intf *hid_asus_ally_terminate(struct asus_drvdata *drvdata)
smp_store_release(&drvdata->ally, NULL);
ally_quiesce(ally);
+ ally_ff_quiesce(ally);
ally_unregister_gamepad(ally);
ally_sysfs_remove(drvdata->hdev, ally);
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH v13 10/18] HID: asus: add support for gamepad mode
2026-11-04 1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
` (8 preceding siblings ...)
2026-11-04 1:39 ` [PATCH v13 09/18] HID: asus: add support to force feedback Denis Benato
@ 2026-11-04 1:39 ` Denis Benato
2026-11-04 1:39 ` [PATCH v13 11/18] HID: asus: add support for turbo buttons Denis Benato
` (7 subsequent siblings)
17 siblings, 0 replies; 25+ messages in thread
From: Denis Benato @ 2026-11-04 1:39 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
ROG Ally devices can emulate either a mouse+keyboard (desktop mode)
or an gamepad device (xbox360 controller in ROG ally and a custom
DInput device on newer models): add support for switching the current
controller mode via the gamepad_mode sysfs property.
Also list available models with gamepad_mode_index to align with
other handheld drivers.
Co-developed-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Denis Benato <denis.benato@linux.dev>
---
drivers/hid/hid-asus.c | 130 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 129 insertions(+), 1 deletion(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 9c9ed75b98a4..d3dbeba529c1 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -368,6 +368,20 @@ enum ally_command_codes {
CMD_SET_ANTI_DEADZONE = 0x18,
};
+enum ally_gamepad_mode_index {
+ ALLY_GAMEPAD_MODE_GAMEPAD = 0x01,
+ ALLY_GAMEPAD_MODE_KEYBOARD = 0x02,
+};
+
+static const char *const ally_gamepad_mode_text[] = {
+ "gamepad", "desktop"
+};
+
+static const u8 ally_gamepad_mode_value[] = {
+ ALLY_GAMEPAD_MODE_GAMEPAD,
+ ALLY_GAMEPAD_MODE_KEYBOARD
+};
+
/* Rumble magnitudes use the hardware's 0..100 intensity range. */
#define ALLY_FF_MAX_INTENSITY 100
@@ -1371,6 +1385,112 @@ static struct device_attribute dev_attr_right_joystick_outer_threshold =
static struct device_attribute dev_attr_right_joystick_outer_threshold_range =
__ATTR(outer_threshold_range, 0444, right_joystick_outer_threshold_range_show, NULL);
+/**
+ * ally_set_gamepad_mode() - Set the gamepad operating mode
+ * @hdev: HID device the command packet is sent on
+ * @mode: %ALLY_GAMEPAD_MODE_GAMEPAD or %ALLY_GAMEPAD_MODE_KEYBOARD
+ *
+ * Context: Caller holds ally_prefs.lock.
+ * Return: 0 on success, negative errno on failure.
+ */
+static int ally_set_gamepad_mode(struct hid_device *hdev, u8 mode)
+{
+ const u8 payload[] = { mode };
+ size_t buf_len;
+ int ret;
+
+ if (mode < ALLY_GAMEPAD_MODE_GAMEPAD || mode > ALLY_GAMEPAD_MODE_KEYBOARD) {
+ hid_err(hdev, "Invalid gamepad mode: %u\n", mode);
+ return -EINVAL;
+ }
+
+ u8 *buf __free(kfree) = ally_alloc_cmd(hdev, CMD_SET_GAMEPAD_MODE,
+ payload, sizeof(payload),
+ &buf_len);
+ if (!buf)
+ return -ENOMEM;
+
+ ret = ally_gamepad_send_packet(hdev, buf, buf_len);
+ if (ret < 0) {
+ hid_err(hdev, "Failed to set gamepad mode: %d\n", ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+static ssize_t gamepad_mode_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ u8 mode_byte;
+ int i;
+
+ guard(mutex)(&ally_prefs.lock);
+
+ mode_byte = ally_prefs.gamepad_mode;
+ for (i = 0; i < ARRAY_SIZE(ally_gamepad_mode_value); i++) {
+ if (ally_gamepad_mode_value[i] == mode_byte)
+ return sysfs_emit(buf, "%s\n", ally_gamepad_mode_text[i]);
+ }
+
+ /*
+ * The recorded mode only comes from the table above: an unknown
+ * byte means memory corruption, not a userspace-visible value.
+ */
+ return -EIO;
+}
+
+static ssize_t gamepad_mode_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ u8 mode_byte;
+ int mode, ret;
+
+ mode = sysfs_match_string(ally_gamepad_mode_text, buf);
+ if (mode < 0)
+ return mode;
+
+ /* The text index maps to the byte the MCU accepts. */
+ mode_byte = ally_gamepad_mode_value[mode];
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ ret = ally_set_gamepad_mode(hdev, mode_byte);
+ if (ret < 0)
+ return ret;
+
+ ally_prefs.gamepad_mode = mode_byte;
+ }
+
+ hid_dbg(hdev, "Set gamepad mode to %s\n", ally_gamepad_mode_text[mode]);
+ sysfs_notify(&dev->kobj, NULL, "gamepad_mode");
+
+ return count;
+}
+
+static ssize_t gamepad_mode_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ ssize_t len = 0;
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(ally_gamepad_mode_text); i++) {
+ if (!ally_gamepad_mode_text[i] || ally_gamepad_mode_text[i][0] == '\0')
+ continue;
+
+ len += sysfs_emit_at(buf, len, "%s ", ally_gamepad_mode_text[i]);
+ }
+
+ /* Replace the last space with a newline. */
+ if (len > 0)
+ buf[len - 1] = '\n';
+
+ return len;
+}
+
+static DEVICE_ATTR_RW(gamepad_mode);
+static DEVICE_ATTR_RO(gamepad_mode_index);
+
/**
* ally_set_anti_deadzone() - Send the joystick anti-deadzone values to the device
* @hdev: HID device the packet is sent on
@@ -1993,6 +2113,8 @@ static const struct attribute_group *const ally_cal_attr_groups[] = {
static struct attribute *ally_config_attrs[] = {
&dev_attr_xbox_controller.attr,
+ &dev_attr_gamepad_mode.attr,
+ &dev_attr_gamepad_mode_index.attr,
NULL
};
@@ -2026,7 +2148,7 @@ static int ally_prefs_setup(struct hid_device *hdev, struct ally_intf *intf)
scoped_guard(mutex, &ally_prefs.lock) {
if (!ally_prefs.initialized) {
- ally_prefs.gamepad_mode = 0x01;
+ ally_prefs.gamepad_mode = ALLY_GAMEPAD_MODE_GAMEPAD;
ally_prefs.xbox_controller_enabled = true;
ally_prefs.initialized = true;
}
@@ -2455,6 +2577,12 @@ static int hid_asus_ally_init(struct hid_device *hdev)
hid_warn(hdev, "Failed to restore right response curve: %d\n", ret);
}
+ if (ally_prefs.initialized) {
+ ret = ally_set_gamepad_mode(hdev, ally_prefs.gamepad_mode);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to restore gamepad mode: %d\n", ret);
+ }
+
return 0;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH v13 11/18] HID: asus: add support for turbo buttons
2026-11-04 1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
` (9 preceding siblings ...)
2026-11-04 1:39 ` [PATCH v13 10/18] HID: asus: add support for gamepad mode Denis Benato
@ 2026-11-04 1:39 ` Denis Benato
2026-11-04 1:52 ` sashiko-bot
2026-11-04 1:39 ` [PATCH v13 12/18] HID: asus: add support for btn remapping Denis Benato
` (6 subsequent siblings)
17 siblings, 1 reply; 25+ messages in thread
From: Denis Benato @ 2026-11-04 1:39 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
ROG ally devices supports a feature called turbo buttons that allows
the user to quickly toggle button state by keeping a button pressed,
therefore allow userspace to configure the feature by exposing
relevant sysfs attributes.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
Signed-off-by: Khamunetri Clark <khamunetriclark@gmail.com>
Co-developed-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Jonathan LoBue <jlobue10@gmail.com>
---
drivers/hid/hid-asus.c | 342 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 342 insertions(+)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index d3dbeba529c1..70c9d71fc5fa 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -238,6 +238,8 @@ struct ally_ff_report {
struct ally_ff_data ff;
} __packed;
+struct ally_btn_sysfs_entry;
+
struct ally_intf {
struct hid_device *hdev;
/* HID-core evdev of this interface. Reports for it land here. */
@@ -254,6 +256,7 @@ struct ally_intf {
*/
bool sysfs_live;
bool cal_sysfs_live;
+ struct ally_btn_sysfs_entry *button_entries;
struct {
/* Guards live, update, and packet. */
spinlock_t lock;
@@ -852,6 +855,80 @@ struct ally_joystick_resp_curve {
struct ally_joystick_resp_curve_param entry_4;
} __packed;
+enum ally_button_id {
+ ALLY_BTN_A,
+ ALLY_BTN_B,
+ ALLY_BTN_X,
+ ALLY_BTN_Y,
+ ALLY_BTN_LB,
+ ALLY_BTN_RB,
+ ALLY_BTN_DU,
+ ALLY_BTN_DD,
+ ALLY_BTN_DL,
+ ALLY_BTN_DR,
+ ALLY_BTN_J0B,
+ ALLY_BTN_J1B,
+ ALLY_BTN_MENU,
+ ALLY_BTN_VIEW,
+ ALLY_BTN_M1,
+ ALLY_BTN_M2,
+ ALLY_BTN_MAX
+};
+
+static const char *const ally_button_names[ALLY_BTN_MAX] = {
+ [ALLY_BTN_A] = "btn_a",
+ [ALLY_BTN_B] = "btn_b",
+ [ALLY_BTN_X] = "btn_x",
+ [ALLY_BTN_Y] = "btn_y",
+ [ALLY_BTN_LB] = "btn_lb",
+ [ALLY_BTN_RB] = "btn_rb",
+ [ALLY_BTN_DU] = "dpad_up",
+ [ALLY_BTN_DD] = "dpad_down",
+ [ALLY_BTN_DL] = "dpad_left",
+ [ALLY_BTN_DR] = "dpad_right",
+ [ALLY_BTN_J0B] = "btn_l3",
+ [ALLY_BTN_J1B] = "btn_r3",
+ [ALLY_BTN_MENU] = "btn_menu",
+ [ALLY_BTN_VIEW] = "btn_view",
+ [ALLY_BTN_M1] = "btn_m1",
+ [ALLY_BTN_M2] = "btn_m2",
+};
+
+struct ally_btn_turbo_params {
+ u8 turbo;
+ u8 toggle;
+} __packed;
+
+#define ALLY_TURBO_PERIOD_MAX 20
+
+/* Payload order. Not the same order as enum ally_button_id. */
+struct ally_turbo_config {
+ struct ally_btn_turbo_params btn_du;
+ struct ally_btn_turbo_params btn_dd;
+ struct ally_btn_turbo_params btn_dl;
+ struct ally_btn_turbo_params btn_dr;
+ struct ally_btn_turbo_params btn_j0b;
+ struct ally_btn_turbo_params btn_j1b;
+ struct ally_btn_turbo_params btn_lb;
+ struct ally_btn_turbo_params btn_rb;
+ struct ally_btn_turbo_params btn_a;
+ struct ally_btn_turbo_params btn_b;
+ struct ally_btn_turbo_params btn_x;
+ struct ally_btn_turbo_params btn_y;
+ struct ally_btn_turbo_params btn_view;
+ struct ally_btn_turbo_params btn_menu;
+ struct ally_btn_turbo_params btn_m2;
+ struct ally_btn_turbo_params btn_m1;
+};
+
+struct ally_btn_sysfs_entry {
+ struct attribute_group group;
+ struct attribute *attrs[5];
+ enum ally_button_id btn;
+ struct device_attribute attr_turbo_period;
+ struct device_attribute attr_toggle_period;
+};
+
/*
* Settings that must survive the controller powering off and coming back.
* No device or input pointers live here: every transfer uses the interface
@@ -899,6 +976,8 @@ static struct ally_prefs {
struct ally_joystick_resp_curve left_curve;
struct ally_joystick_resp_curve right_curve;
+
+ struct ally_turbo_config turbo;
} ally_prefs = {
.lock = __MUTEX_INITIALIZER(ally_prefs.lock),
};
@@ -2132,8 +2211,252 @@ static const struct attribute_group ally_attr_groups[] = {
},
};
+static struct ally_btn_turbo_params *ally_btn_param(enum ally_button_id btn)
+{
+ struct ally_btn_turbo_params *table[ALLY_BTN_MAX] = {
+ [ALLY_BTN_A] = &ally_prefs.turbo.btn_a,
+ [ALLY_BTN_B] = &ally_prefs.turbo.btn_b,
+ [ALLY_BTN_X] = &ally_prefs.turbo.btn_x,
+ [ALLY_BTN_Y] = &ally_prefs.turbo.btn_y,
+ [ALLY_BTN_LB] = &ally_prefs.turbo.btn_lb,
+ [ALLY_BTN_RB] = &ally_prefs.turbo.btn_rb,
+ [ALLY_BTN_DU] = &ally_prefs.turbo.btn_du,
+ [ALLY_BTN_DD] = &ally_prefs.turbo.btn_dd,
+ [ALLY_BTN_DL] = &ally_prefs.turbo.btn_dl,
+ [ALLY_BTN_DR] = &ally_prefs.turbo.btn_dr,
+ [ALLY_BTN_J0B] = &ally_prefs.turbo.btn_j0b,
+ [ALLY_BTN_J1B] = &ally_prefs.turbo.btn_j1b,
+ [ALLY_BTN_MENU] = &ally_prefs.turbo.btn_menu,
+ [ALLY_BTN_VIEW] = &ally_prefs.turbo.btn_view,
+ [ALLY_BTN_M1] = &ally_prefs.turbo.btn_m1,
+ [ALLY_BTN_M2] = &ally_prefs.turbo.btn_m2,
+ };
+
+ if (btn >= ALLY_BTN_MAX)
+ return NULL;
+
+ return table[btn];
+}
+
+/**
+ * ally_set_turbo_params() - Set the turbo parameters
+ * @hdev: The HID device to send the packet to.
+ *
+ * Context: Caller holds ally_prefs.lock.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+static int ally_set_turbo_params(struct hid_device *hdev)
+{
+ struct ally_turbo_config *turbo = &ally_prefs.turbo;
+ size_t buf_len;
+ int ret;
+
+ u8 *buf __free(kfree) = ally_alloc_cmd(hdev, CMD_SET_TURBO_PARAMS,
+ (const u8 *)turbo, sizeof(*turbo),
+ &buf_len);
+
+ if (!buf)
+ return -ENOMEM;
+
+ ret = ally_gamepad_send_packet(hdev, buf, buf_len);
+ if (ret < 0) {
+ hid_err(hdev, "Failed to set turbo parameters: %d\n", ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+/* Which per-button period a sysfs attribute operates on. */
+enum ally_btn_period {
+ ALLY_TURBO_PERIOD,
+ ALLY_TOGGLE_PERIOD,
+};
+
+static u8 *ally_btn_period_field(struct ally_btn_turbo_params *params,
+ enum ally_btn_period which)
+{
+ return which == ALLY_TURBO_PERIOD ? ¶ms->turbo : ¶ms->toggle;
+}
+
+static ssize_t ally_btn_period_show(struct ally_btn_sysfs_entry *entry, char *buf,
+ enum ally_btn_period which)
+{
+ struct ally_btn_turbo_params *params;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ if (!ally_prefs.turbo_support)
+ return -EOPNOTSUPP;
+
+ params = ally_btn_param(entry->btn);
+ if (!params)
+ return -ENODEV;
+ }
+
+ return sysfs_emit(buf, "%hhu\n", *ally_btn_period_field(params, which));
+}
+
+static ssize_t ally_btn_period_store(struct device *dev,
+ struct ally_btn_sysfs_entry *entry,
+ const char *buf, size_t count,
+ enum ally_btn_period which, u8 max,
+ const char *name)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct ally_btn_turbo_params *params;
+ u8 old_value, value, *field;
+ int ret;
+
+ ret = kstrtou8(buf, 10, &value);
+ if (ret)
+ return ret;
+ if (value > max)
+ return -EINVAL;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ if (!ally_prefs.turbo_support)
+ return -EOPNOTSUPP;
+
+ params = ally_btn_param(entry->btn);
+ if (!params)
+ return -ENODEV;
+
+ field = ally_btn_period_field(params, which);
+ old_value = *field;
+ *field = value;
+
+ ret = ally_set_turbo_params(hdev);
+ if (ret) {
+ *field = old_value;
+ return ret;
+ }
+ }
+
+ sysfs_notify(&dev->kobj, entry->group.name, name);
+
+ return count;
+}
+
+/* Generates btn_<kind>_period_{show,store,range_show}. */
+#define ALLY_BTN_PERIOD_FUNCS(kind, KIND, max) \
+static ssize_t btn_##kind##_period_show(struct device *dev, \
+ struct device_attribute *attr, char *buf) \
+{ \
+ struct ally_btn_sysfs_entry *entry = container_of(attr, \
+ struct ally_btn_sysfs_entry, attr_##kind##_period); \
+ \
+ return ally_btn_period_show(entry, buf, ALLY_##KIND##_PERIOD); \
+} \
+ \
+static ssize_t btn_##kind##_period_store(struct device *dev, \
+ struct device_attribute *attr, \
+ const char *buf, size_t count) \
+{ \
+ struct ally_btn_sysfs_entry *entry = container_of(attr, \
+ struct ally_btn_sysfs_entry, attr_##kind##_period); \
+ \
+ return ally_btn_period_store(dev, entry, buf, count, ALLY_##KIND##_PERIOD, \
+ max, #kind "_period"); \
+} \
+ \
+static ssize_t btn_##kind##_period_range_show(struct device *dev, \
+ struct device_attribute *attr, \
+ char *buf) \
+{ \
+ return sysfs_emit(buf, "%u %u\n", 0, (unsigned int)(max)); \
+}
+
+ALLY_BTN_PERIOD_FUNCS(turbo, TURBO, ALLY_TURBO_PERIOD_MAX)
+ALLY_BTN_PERIOD_FUNCS(toggle, TOGGLE, U8_MAX)
+
+static struct device_attribute dev_attr_btn_turbo_period_range =
+ __ATTR(turbo_period_range, 0444, btn_turbo_period_range_show, NULL);
+
+static struct device_attribute dev_attr_btn_toggle_period_range =
+ __ATTR(toggle_period_range, 0444, btn_toggle_period_range_show, NULL);
+
+static void ally_btn_turbo_init_attrs(struct ally_btn_sysfs_entry *entry)
+{
+ sysfs_attr_init(&entry->attr_turbo_period.attr);
+ entry->attr_turbo_period.attr.name = "turbo_period";
+ entry->attr_turbo_period.attr.mode = 0644;
+ entry->attr_turbo_period.show = btn_turbo_period_show;
+ entry->attr_turbo_period.store = btn_turbo_period_store;
+
+ sysfs_attr_init(&entry->attr_toggle_period.attr);
+ entry->attr_toggle_period.attr.name = "toggle_period";
+ entry->attr_toggle_period.attr.mode = 0644;
+ entry->attr_toggle_period.show = btn_toggle_period_show;
+ entry->attr_toggle_period.store = btn_toggle_period_store;
+
+ entry->attrs[0] = &entry->attr_turbo_period.attr;
+ entry->attrs[1] = &entry->attr_toggle_period.attr;
+ entry->attrs[2] = &dev_attr_btn_turbo_period_range.attr;
+ entry->attrs[3] = &dev_attr_btn_toggle_period_range.attr;
+ entry->attrs[4] = NULL;
+}
+
+static struct ally_btn_sysfs_entry *ally_create_button_attributes(struct hid_device *hdev)
+{
+ int i, ret;
+
+ scoped_guard(mutex, &ally_prefs.lock)
+ if (!ally_prefs.turbo_support)
+ return NULL;
+
+ struct ally_btn_sysfs_entry *entries __free(kfree) = kzalloc_objs(*entries, ALLY_BTN_MAX);
+ if (!entries)
+ return ERR_PTR(-ENOMEM);
+
+ for (i = 0; i < ALLY_BTN_MAX; i++) {
+ entries[i].btn = i;
+
+ if (!ally_btn_param(i)) {
+ hid_err(hdev, "Invalid button id %d for turbo attributes\n", i);
+ ret = -EINVAL;
+ goto ally_btn_err_cleanup;
+ }
+
+ ally_btn_turbo_init_attrs(&entries[i]);
+ entries[i].group.name = ally_button_names[i];
+ entries[i].group.attrs = entries[i].attrs;
+
+ ret = sysfs_create_group(&hdev->dev.kobj, &entries[i].group);
+ if (ret < 0) {
+ hid_err(hdev, "Failed to create sysfs group for %s: %d\n",
+ ally_button_names[i], ret);
+ goto ally_btn_err_cleanup;
+ }
+ }
+
+ return no_free_ptr(entries);
+
+ally_btn_err_cleanup:
+ while (i--)
+ sysfs_remove_group(&hdev->dev.kobj, &entries[i].group);
+
+ return ERR_PTR(ret);
+}
+
+static void ally_remove_button_attributes(struct hid_device *hdev,
+ struct ally_btn_sysfs_entry *entries)
+{
+ int i;
+
+ if (!entries)
+ return;
+
+ for (i = 0; i < ALLY_BTN_MAX; i++) {
+ sysfs_remove_group(&hdev->dev.kobj, &entries[i].group);
+ }
+
+ kfree(entries);
+}
+
static int ally_prefs_setup(struct hid_device *hdev, struct ally_intf *intf)
{
+ struct ally_btn_sysfs_entry *entries = NULL;
bool cal_live = false;
int ret, i, cal_n = 0;
@@ -2192,7 +2515,15 @@ static int ally_prefs_setup(struct hid_device *hdev, struct ally_intf *intf)
}
}
+ entries = ally_create_button_attributes(hdev);
+ if (IS_ERR(entries)) {
+ ret = PTR_ERR(entries);
+ hid_err(hdev, "Failed to create button attributes: %d\n", ret);
+ goto err_cal_groups;
+ }
+
scoped_guard(mutex, &ally_prefs.lock) {
+ intf->button_entries = entries;
intf->cal_sysfs_live = cal_live;
intf->sysfs_live = true;
}
@@ -2211,6 +2542,7 @@ static int ally_prefs_setup(struct hid_device *hdev, struct ally_intf *intf)
static void ally_sysfs_remove(struct hid_device *hdev, struct ally_intf *intf)
{
+ struct ally_btn_sysfs_entry *entries;
bool cal_live;
int i;
@@ -2225,10 +2557,14 @@ static void ally_sysfs_remove(struct hid_device *hdev, struct ally_intf *intf)
return;
intf->sysfs_live = false;
+ entries = intf->button_entries;
+ intf->button_entries = NULL;
cal_live = intf->cal_sysfs_live;
intf->cal_sysfs_live = false;
}
+ ally_remove_button_attributes(hdev, entries);
+
for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++)
sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]);
@@ -2583,6 +2919,12 @@ static int hid_asus_ally_init(struct hid_device *hdev)
hid_warn(hdev, "Failed to restore gamepad mode: %d\n", ret);
}
+ if (ally_prefs.initialized && ally_prefs.turbo_support) {
+ ret = ally_set_turbo_params(hdev);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to restore turbo parameters: %d\n", ret);
+ }
+
return 0;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* Re: [PATCH v13 11/18] HID: asus: add support for turbo buttons
2026-11-04 1:39 ` [PATCH v13 11/18] HID: asus: add support for turbo buttons Denis Benato
@ 2026-11-04 1:52 ` sashiko-bot
0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-11-04 1:52 UTC (permalink / raw)
To: Denis Benato; +Cc: linux-input, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] HID: asus: unlocked read of turbo period in ally_btn_period_show()
- [Low] HID: asus: mix of cleanup helpers and goto in ally_create_button_attributes()
- [Low] HID: asus: non-static lookup table on stack in ally_btn_param()
--
commit 3795dba8be2afc39142aa94e1d2e669448280abb
Author: Denis Benato <denis.benato@linux.dev>
HID: asus: add support for turbo buttons
This commit adds support for configuring ROG Ally turbo and toggle button
periods via per-button sysfs attribute groups and restores these settings on
controller initialization.
> diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
> index d3dbeba529c12..70c9d71fc5fa9 100644
> --- a/drivers/hid/hid-asus.c
> +++ b/drivers/hid/hid-asus.c
[ ... ]
> @@ -2132,8 +2211,252 @@ static const struct attribute_group ally_attr_groups[] = {
> },
> };
>
> +static struct ally_btn_turbo_params *ally_btn_param(enum ally_button_id btn)
> +{
> + struct ally_btn_turbo_params *table[ALLY_BTN_MAX] = {
> + [ALLY_BTN_A] = &ally_prefs.turbo.btn_a,
> + [ALLY_BTN_B] = &ally_prefs.turbo.btn_b,
> + [ALLY_BTN_X] = &ally_prefs.turbo.btn_x,
> + [ALLY_BTN_Y] = &ally_prefs.turbo.btn_y,
> + [ALLY_BTN_LB] = &ally_prefs.turbo.btn_lb,
> + [ALLY_BTN_RB] = &ally_prefs.turbo.btn_rb,
> + [ALLY_BTN_DU] = &ally_prefs.turbo.btn_du,
> + [ALLY_BTN_DD] = &ally_prefs.turbo.btn_dd,
> + [ALLY_BTN_DL] = &ally_prefs.turbo.btn_dl,
> + [ALLY_BTN_DR] = &ally_prefs.turbo.btn_dr,
> + [ALLY_BTN_J0B] = &ally_prefs.turbo.btn_j0b,
> + [ALLY_BTN_J1B] = &ally_prefs.turbo.btn_j1b,
> + [ALLY_BTN_MENU] = &ally_prefs.turbo.btn_menu,
> + [ALLY_BTN_VIEW] = &ally_prefs.turbo.btn_view,
> + [ALLY_BTN_M1] = &ally_prefs.turbo.btn_m1,
> + [ALLY_BTN_M2] = &ally_prefs.turbo.btn_m2,
> + };
> +
> + if (btn >= ALLY_BTN_MAX)
> + return NULL;
> +
> + return table[btn];
> +}
[Severity: Low]
As reported by Sashiko on v12 [1], in ally_btn_param() in
drivers/hid/hid-asus.c, table[ALLY_BTN_MAX] maps enum ally_button_id
values to fixed addresses inside the file-scope ally_prefs.turbo struct,
but it is not declared static const. As a result, the 16-pointer lookup
array is reconstructed on the stack on every sysfs show, store, and setup
call.
Could table be declared static struct ally_btn_turbo_params *const to
match the other lookup tables in this driver?
[ ... ]
> +static ssize_t ally_btn_period_show(struct ally_btn_sysfs_entry *entry, char *buf,
> + enum ally_btn_period which)
> +{
> + struct ally_btn_turbo_params *params;
> +
> + scoped_guard(mutex, &ally_prefs.lock) {
> + if (!ally_prefs.turbo_support)
> + return -EOPNOTSUPP;
> +
> + params = ally_btn_param(entry->btn);
> + if (!params)
> + return -ENODEV;
> + }
> +
> + return sysfs_emit(buf, "%hhu\n", *ally_btn_period_field(params, which));
> +}
> +
> +static ssize_t ally_btn_period_store(struct device *dev,
> + struct ally_btn_sysfs_entry *entry,
> + const char *buf, size_t count,
> + enum ally_btn_period which, u8 max,
> + const char *name)
> +{
> + struct hid_device *hdev = to_hid_device(dev);
> + struct ally_btn_turbo_params *params;
> + u8 old_value, value, *field;
> + int ret;
> +
> + ret = kstrtou8(buf, 10, &value);
> + if (ret)
> + return ret;
> + if (value > max)
> + return -EINVAL;
> +
> + scoped_guard(mutex, &ally_prefs.lock) {
> + if (!ally_prefs.turbo_support)
> + return -EOPNOTSUPP;
> +
> + params = ally_btn_param(entry->btn);
> + if (!params)
> + return -ENODEV;
> +
> + field = ally_btn_period_field(params, which);
> + old_value = *field;
> + *field = value;
> +
> + ret = ally_set_turbo_params(hdev);
> + if (ret) {
> + *field = old_value;
> + return ret;
> + }
> + }
[Severity: Medium]
In ally_btn_period_show() in drivers/hid/hid-asus.c, scoped_guard(mutex,
&ally_prefs.lock) only covers checking ally_prefs.turbo_support and
fetching the params pointer into ally_prefs.turbo via ally_btn_param().
The mutex is released when the scoped_guard block exits, before
sysfs_emit() dereferences *ally_btn_period_field(params, which).
Meanwhile, ally_btn_period_store() modifies *field in-place inside
ally_prefs.turbo under ally_prefs.lock before blocking on the feature
report transfer in ally_set_turbo_params(hdev), and rolls *field back to
old_value if that transfer fails.
Can a concurrent read on turbo_period or toggle_period race with
ally_btn_period_store() and observe an uncommitted value that is
subsequently rolled back on error? Should ally_btn_period_show() use
guard(mutex)(&ally_prefs.lock) (or read the byte into a local variable
inside the guarded scope) so the read is serialized by ally_prefs.lock?
[ ... ]
> +static struct ally_btn_sysfs_entry *ally_create_button_attributes(struct hid_device *hdev)
> +{
> + int i, ret;
> +
> + scoped_guard(mutex, &ally_prefs.lock)
> + if (!ally_prefs.turbo_support)
> + return NULL;
> +
> + struct ally_btn_sysfs_entry *entries __free(kfree) = kzalloc_objs(*entries, ALLY_BTN_MAX);
> + if (!entries)
> + return ERR_PTR(-ENOMEM);
> +
> + for (i = 0; i < ALLY_BTN_MAX; i++) {
> + entries[i].btn = i;
> +
> + if (!ally_btn_param(i)) {
> + hid_err(hdev, "Invalid button id %d for turbo attributes\n", i);
> + ret = -EINVAL;
> + goto ally_btn_err_cleanup;
> + }
> +
> + ally_btn_turbo_init_attrs(&entries[i]);
> + entries[i].group.name = ally_button_names[i];
> + entries[i].group.attrs = entries[i].attrs;
> +
> + ret = sysfs_create_group(&hdev->dev.kobj, &entries[i].group);
> + if (ret < 0) {
> + hid_err(hdev, "Failed to create sysfs group for %s: %d\n",
> + ally_button_names[i], ret);
> + goto ally_btn_err_cleanup;
> + }
> + }
> +
> + return no_free_ptr(entries);
> +
> +ally_btn_err_cleanup:
> + while (i--)
> + sysfs_remove_group(&hdev->dev.kobj, &entries[i].group);
> +
> + return ERR_PTR(ret);
> +}
[ ... ]
> @@ -2192,7 +2515,15 @@ static int ally_prefs_setup(struct hid_device *hdev, struct ally_intf *intf)
> }
> }
>
> + entries = ally_create_button_attributes(hdev);
> + if (IS_ERR(entries)) {
> + ret = PTR_ERR(entries);
> + hid_err(hdev, "Failed to create button attributes: %d\n", ret);
> + goto err_cal_groups;
> + }
> +
> scoped_guard(mutex, &ally_prefs.lock) {
> + intf->button_entries = entries;
> intf->cal_sysfs_live = cal_live;
> intf->sysfs_live = true;
> }
[Severity: Low]
As reported by Sashiko on v12 [2], in ally_create_button_attributes() in
drivers/hid/hid-asus.c, scope-based cleanup helpers (scoped_guard and
entries __free(kfree)) are mixed with goto ally_btn_err_cleanup in the
same function, and ally_prefs_setup() similarly mixes scoped_guard with
goto err_cal_groups.
The kernel cleanup API conventions require not mixing scope-based cleanup
helpers with goto-based error handling within the same function. Could
ally_create_button_attributes() either avoid goto (for example by calling
a helper to unwind the created groups before returning Directly) or manage
entries with explicit kfree() on the error path?
[1] https://lore.kernel.org/r/20261031174130.208240-12-denis.benato@linux.dev
[2] https://lore.kernel.org/r/20261031174130.208240-12-denis.benato@linux.dev
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261104013935.512713-1-denis.benato@linux.dev?part=11
^ permalink raw reply [flat|nested] 25+ messages in thread
* [PATCH v13 12/18] HID: asus: add support for btn remapping
2026-11-04 1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
` (10 preceding siblings ...)
2026-11-04 1:39 ` [PATCH v13 11/18] HID: asus: add support for turbo buttons Denis Benato
@ 2026-11-04 1:39 ` Denis Benato
2026-11-04 1:58 ` sashiko-bot
2026-11-04 1:39 ` [PATCH v13 13/18] HID: asus: identify XG Mobile stations and bind the LED to the reported model Denis Benato
` (5 subsequent siblings)
17 siblings, 1 reply; 25+ messages in thread
From: Denis Benato @ 2026-11-04 1:39 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
Add support for buttons remapping: the ability to assign different
button events to each one of the physical button on the device.
Each button exposes a remap_available sysfs attribute listing the
valid remap target names for the current gamepad mode: M1 and M2
can be remapped to anything at any time, while other buttons may
only be remapped to gamepad codes in gamepad mode and keyboard,
mouse and media codes in desktop mode.
When the gamepad mode is changed the firmware automatically restores
its internal defaults, so the software cache is refreshed to reflect
the hardware state.
Co-developed-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Denis Benato <denis.benato@linux.dev>
Signed-off-by: Jonathan LoBue <jlobue10@gmail.com>
---
drivers/hid/hid-asus.c | 848 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 840 insertions(+), 8 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 70c9d71fc5fa..dcb3c9f5d23e 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -40,6 +40,7 @@
#include <linux/usb.h> /* For to_usb_interface for T100 touchpad intf check */
#include <linux/power_supply.h>
#include <linux/stddef.h>
+#include <linux/string.h>
#include <linux/sysfs.h>
#include <linux/leds.h>
#include <linux/types.h>
@@ -117,6 +118,10 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define ASUS_ALLY_TRIGGER_RANGE_UPPER_MIN 70
#define ASUS_ALLY_TRIGGER_RANGE_UPPER_MAX 100
+#define ASUS_ALLY_BTN_CODE_LEN 0x2C
+
+#define ASUS_ALLY_BTN_FUNCTIONS_LEN 11
+
/* Spurious HID codes sent by QUIRK_ROG_NKEY_KEYBOARD devices */
#define ASUS_SPURIOUS_CODE_0XEA 0xea
#define ASUS_SPURIOUS_CODE_0XEC 0xec
@@ -921,12 +926,215 @@ struct ally_turbo_config {
struct ally_btn_turbo_params btn_m1;
};
+struct button_remap_attr {
+ struct device_attribute dev_attr;
+ enum ally_button_id button_id;
+ bool is_macro;
+};
+
+#define to_button_remap_attr(x) container_of(x, struct button_remap_attr, dev_attr)
+
struct ally_btn_sysfs_entry {
struct attribute_group group;
- struct attribute *attrs[5];
+ struct attribute *attrs[8];
enum ally_button_id btn;
struct device_attribute attr_turbo_period;
struct device_attribute attr_toggle_period;
+ struct button_remap_attr *remap_attr;
+ struct button_remap_attr *macro_attr;
+ struct device_attribute attr_remap_available;
+};
+
+enum btn_map_type {
+ BTN_TYPE_NONE = 0,
+ BTN_TYPE_PAD = 0x01,
+ BTN_TYPE_KB = 0x02,
+ BTN_TYPE_MOUSE = 0x03,
+ BTN_TYPE_MEDIA = 0x05,
+};
+
+struct btn_code_map {
+ unsigned char type;
+ unsigned char value;
+ const char *name;
+};
+
+static const struct btn_code_map ally_btn_codes[] = {
+ { BTN_TYPE_NONE, 0x00, "NONE" },
+ /* Gamepad button codes */
+ { BTN_TYPE_PAD, 0x01, "BTN_SOUTH" },
+ { BTN_TYPE_PAD, 0x02, "BTN_EAST" },
+ { BTN_TYPE_PAD, 0x03, "BTN_NORTH" },
+ { BTN_TYPE_PAD, 0x04, "BTN_WEST" },
+ { BTN_TYPE_PAD, 0x05, "BTN_TL" },
+ { BTN_TYPE_PAD, 0x06, "BTN_TR" },
+ { BTN_TYPE_PAD, 0x07, "BTN_THUMBL" },
+ { BTN_TYPE_PAD, 0x08, "BTN_THUMBR" },
+ { BTN_TYPE_PAD, 0x09, "ABS_HAT0Y_NEG" },
+ { BTN_TYPE_PAD, 0x0A, "ABS_HAT0Y_POS" },
+ { BTN_TYPE_PAD, 0x0B, "ABS_HAT0X_NEG" },
+ { BTN_TYPE_PAD, 0x0C, "ABS_HAT0X_POS" },
+ { BTN_TYPE_PAD, 0x0D, "ABS_Z" },
+ { BTN_TYPE_PAD, 0x0E, "ABS_RZ" },
+ { BTN_TYPE_PAD, 0x11, "BTN_SELECT" },
+ { BTN_TYPE_PAD, 0x12, "BTN_START" },
+ { BTN_TYPE_PAD, 0x13, "BTN_MODE" },
+
+ /* Keyboard button codes */
+ { BTN_TYPE_KB, 0x8E, "FN_M2" },
+ { BTN_TYPE_KB, 0x8F, "FN_M1" },
+ { BTN_TYPE_KB, 0x76, "KEY_ESC" },
+ { BTN_TYPE_KB, 0x05, "KEY_F1" },
+ { BTN_TYPE_KB, 0x06, "KEY_F2" },
+ { BTN_TYPE_KB, 0x04, "KEY_F3" },
+ { BTN_TYPE_KB, 0x0C, "KEY_F4" },
+ { BTN_TYPE_KB, 0x03, "KEY_F5" },
+ { BTN_TYPE_KB, 0x0B, "KEY_F6" },
+ { BTN_TYPE_KB, 0x80, "KEY_F7" },
+ { BTN_TYPE_KB, 0x0A, "KEY_F8" },
+ { BTN_TYPE_KB, 0x01, "KEY_F9" },
+ { BTN_TYPE_KB, 0x09, "KEY_F10" },
+ { BTN_TYPE_KB, 0x78, "KEY_F11" },
+ { BTN_TYPE_KB, 0x07, "KEY_F12" },
+ { BTN_TYPE_KB, 0x08, "KEY_F13" },
+ { BTN_TYPE_KB, 0x10, "KEY_F14" },
+ { BTN_TYPE_KB, 0x18, "KEY_F15" },
+ { BTN_TYPE_KB, 0x0E, "KEY_GRAVE" }, /* backtick */
+ { BTN_TYPE_KB, 0x16, "KEY_1" },
+ { BTN_TYPE_KB, 0x1E, "KEY_2" },
+ { BTN_TYPE_KB, 0x26, "KEY_3" },
+ { BTN_TYPE_KB, 0x25, "KEY_4" },
+ { BTN_TYPE_KB, 0x2E, "KEY_5" },
+ { BTN_TYPE_KB, 0x36, "KEY_6" },
+ { BTN_TYPE_KB, 0x3D, "KEY_7" },
+ { BTN_TYPE_KB, 0x3E, "KEY_8" },
+ { BTN_TYPE_KB, 0x46, "KEY_9" },
+ { BTN_TYPE_KB, 0x45, "KEY_0" },
+ { BTN_TYPE_KB, 0x4E, "KEY_MINUS" }, /* hyphen */
+ { BTN_TYPE_KB, 0x55, "KEY_EQUAL" },
+ { BTN_TYPE_KB, 0x66, "KEY_BACKSPACE" },
+ { BTN_TYPE_KB, 0x0D, "KEY_TAB" },
+ { BTN_TYPE_KB, 0x15, "KEY_Q" },
+ { BTN_TYPE_KB, 0x1D, "KEY_W" },
+ { BTN_TYPE_KB, 0x24, "KEY_E" },
+ { BTN_TYPE_KB, 0x2D, "KEY_R" },
+ { BTN_TYPE_KB, 0x2C, "KEY_T" },
+ { BTN_TYPE_KB, 0x35, "KEY_Y" },
+ { BTN_TYPE_KB, 0x3C, "KEY_U" },
+ { BTN_TYPE_KB, 0x43, "KEY_I" },
+ { BTN_TYPE_KB, 0x44, "KEY_O" },
+ { BTN_TYPE_KB, 0x4D, "KEY_P" },
+ { BTN_TYPE_KB, 0x54, "KEY_LEFTBRACE" },
+ { BTN_TYPE_KB, 0x5B, "KEY_RIGHTBRACE" },
+ { BTN_TYPE_KB, 0x5D, "KEY_BACKSLASH" },
+ { BTN_TYPE_KB, 0x58, "KEY_CAPSLOCK" },
+ { BTN_TYPE_KB, 0x1C, "KEY_A" },
+ { BTN_TYPE_KB, 0x1B, "KEY_S" },
+ { BTN_TYPE_KB, 0x23, "KEY_D" },
+ { BTN_TYPE_KB, 0x2B, "KEY_F" },
+ { BTN_TYPE_KB, 0x34, "KEY_G" },
+ { BTN_TYPE_KB, 0x33, "KEY_H" },
+ { BTN_TYPE_KB, 0x3B, "KEY_J" },
+ { BTN_TYPE_KB, 0x42, "KEY_K" },
+ { BTN_TYPE_KB, 0x4B, "KEY_L" },
+ { BTN_TYPE_KB, 0x4C, "KEY_SEMICOLON" },
+ { BTN_TYPE_KB, 0x52, "KEY_APOSTROPHE" },
+ { BTN_TYPE_KB, 0x5A, "KEY_ENTER" },
+ { BTN_TYPE_KB, 0x88, "KEY_LEFTSHIFT" },
+ { BTN_TYPE_KB, 0x1A, "KEY_Z" },
+ { BTN_TYPE_KB, 0x22, "KEY_X" },
+ { BTN_TYPE_KB, 0x21, "KEY_C" },
+ { BTN_TYPE_KB, 0x2A, "KEY_V" },
+ { BTN_TYPE_KB, 0x32, "KEY_B" },
+ { BTN_TYPE_KB, 0x31, "KEY_N" },
+ { BTN_TYPE_KB, 0x3A, "KEY_M" },
+ { BTN_TYPE_KB, 0x41, "KEY_COMMA" },
+ { BTN_TYPE_KB, 0x49, "KEY_DOT" },
+ { BTN_TYPE_KB, 0x4A, "KEY_SLASH" },
+ { BTN_TYPE_KB, 0x89, "KEY_RIGHTSHIFT" },
+ { BTN_TYPE_KB, 0x82, "KEY_LEFTMETA" },
+ { BTN_TYPE_KB, 0x8A, "KEY_LEFTALT" },
+ { BTN_TYPE_KB, 0x29, "KEY_SPACE" },
+ { BTN_TYPE_KB, 0x8B, "KEY_RIGHTALT" },
+ { BTN_TYPE_KB, 0x84, "KEY_COMPOSE" },
+ { BTN_TYPE_KB, 0x8D, "KEY_RIGHTCTRL" },
+ { BTN_TYPE_KB, 0xC3, "KEY_SYSRQ" },
+ { BTN_TYPE_KB, 0x7E, "KEY_SCROLLLOCK" },
+ { BTN_TYPE_KB, 0x91, "KEY_PAUSE" },
+ { BTN_TYPE_KB, 0xC2, "KEY_INSERT" },
+ { BTN_TYPE_KB, 0x94, "KEY_HOME" },
+ { BTN_TYPE_KB, 0x96, "KEY_PAGEUP" },
+ { BTN_TYPE_KB, 0xC0, "KEY_DELETE" },
+ { BTN_TYPE_KB, 0x95, "KEY_END" },
+ { BTN_TYPE_KB, 0x97, "KEY_PAGEDOWN" },
+ { BTN_TYPE_KB, 0x98, "KEY_UP" },
+ { BTN_TYPE_KB, 0x99, "KEY_DOWN" },
+ { BTN_TYPE_KB, 0x9A, "KEY_LEFT" },
+ { BTN_TYPE_KB, 0x9B, "KEY_RIGHT" },
+
+ /* Numpad button codes */
+ { BTN_TYPE_KB, 0x77, "KEY_NUMLOCK" },
+ { BTN_TYPE_KB, 0x90, "KEY_KPSLASH" },
+ { BTN_TYPE_KB, 0x7C, "KEY_KPASTERISK" },
+ { BTN_TYPE_KB, 0x7B, "KEY_KPMINUS" },
+ { BTN_TYPE_KB, 0x70, "KEY_KP0" },
+ { BTN_TYPE_KB, 0x69, "KEY_KP1" },
+ { BTN_TYPE_KB, 0x72, "KEY_KP2" },
+ { BTN_TYPE_KB, 0x7A, "KEY_KP3" },
+ { BTN_TYPE_KB, 0x6B, "KEY_KP4" },
+ { BTN_TYPE_KB, 0x73, "KEY_KP5" },
+ { BTN_TYPE_KB, 0x74, "KEY_KP6" },
+ { BTN_TYPE_KB, 0x6C, "KEY_KP7" },
+ { BTN_TYPE_KB, 0x75, "KEY_KP8" },
+ { BTN_TYPE_KB, 0x7D, "KEY_KP9" },
+ { BTN_TYPE_KB, 0x79, "KEY_KPPLUS" },
+ { BTN_TYPE_KB, 0x81, "KEY_KPENTER" },
+ { BTN_TYPE_KB, 0x71, "KEY_KPDOT" },
+
+ /* Mouse button codes */
+ { BTN_TYPE_MOUSE, 0x01, "BTN_LEFT" },
+ { BTN_TYPE_MOUSE, 0x02, "BTN_RIGHT" },
+ { BTN_TYPE_MOUSE, 0x03, "BTN_MIDDLE" },
+ { BTN_TYPE_MOUSE, 0x04, "REL_WHEEL_HI_RES_UP" },
+ { BTN_TYPE_MOUSE, 0x05, "REL_WHEEL_HI_RES_DOWN" },
+
+ /* Media button codes */
+ { BTN_TYPE_MEDIA, 0x16, "MEDIA_SCREENSHOT" },
+ { BTN_TYPE_MEDIA, 0x19, "MEDIA_SHOW_KEYBOARD" },
+ { BTN_TYPE_MEDIA, 0x1C, "MEDIA_SHOW_DESKTOP" },
+ { BTN_TYPE_MEDIA, 0x1E, "MEDIA_START_RECORDING" },
+ { BTN_TYPE_MEDIA, 0x01, "MEDIA_MIC_OFF" },
+ { BTN_TYPE_MEDIA, 0x02, "MEDIA_VOL_DOWN" },
+ { BTN_TYPE_MEDIA, 0x03, "MEDIA_VOL_UP" },
+};
+
+/* Button pair indexes for mapping commands */
+enum btn_pair_index {
+ BTN_PAIR_DPAD_UPDOWN = 0x01,
+ BTN_PAIR_DPAD_LEFTRIGHT = 0x02,
+ BTN_PAIR_STICK_LR = 0x03,
+ BTN_PAIR_BUMPER_LR = 0x04,
+ BTN_PAIR_AB = 0x05,
+ BTN_PAIR_XY = 0x06,
+ BTN_PAIR_VIEW_MENU = 0x07,
+ BTN_PAIR_M1M2 = 0x08,
+ BTN_PAIR_TRIGGER_LR = 0x09,
+};
+
+struct button_map {
+ const struct btn_code_map *remap;
+ const struct btn_code_map *macro;
+};
+
+struct button_pair_map {
+ enum btn_pair_index pair_index;
+ struct button_map first;
+ struct button_map second;
+};
+
+/* Mappings the firmware is using. One set; a mode switch replaces it. */
+struct ally_button_mapping {
+ struct button_pair_map button_pairs[9];
};
/*
@@ -978,10 +1186,587 @@ static struct ally_prefs {
struct ally_joystick_resp_curve right_curve;
struct ally_turbo_config turbo;
+ struct ally_button_mapping button_mappings;
} ally_prefs = {
.lock = __MUTEX_INITIALIZER(ally_prefs.lock),
};
+/**
+ * find_button_by_name() - Find a button code map by its name
+ * @name: name of the button code to look up
+ *
+ * This function deals with data from userspace therefore it
+ * must not trust the input blindly.
+ *
+ * Return: the matching button code map, or NULL if not found.
+ */
+static const struct btn_code_map *find_button_by_name(const char *name)
+{
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(ally_btn_codes); i++) {
+ if (!sysfs_streq(ally_btn_codes[i].name, name))
+ return &ally_btn_codes[i];
+ }
+
+ return NULL;
+}
+
+static void ally_set_button_pair_mappings_default(struct ally_button_mapping *map)
+{
+ const struct btn_code_map *none_btn = find_button_by_name("NONE");
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(map->button_pairs); i++) {
+ map->button_pairs[i].pair_index = i + 1;
+ map->button_pairs[i].first.remap = none_btn;
+ map->button_pairs[i].first.macro = none_btn;
+ map->button_pairs[i].second.remap = none_btn;
+ map->button_pairs[i].second.macro = none_btn;
+ }
+}
+
+static void ally_set_default_gamepad_mapping(struct ally_button_mapping *map)
+{
+ ally_set_button_pair_mappings_default(map);
+
+ map->button_pairs[BTN_PAIR_AB - 1].first.remap =
+ find_button_by_name("BTN_SOUTH");
+ map->button_pairs[BTN_PAIR_AB - 1].second.remap =
+ find_button_by_name("BTN_EAST");
+
+ map->button_pairs[BTN_PAIR_XY - 1].first.remap =
+ find_button_by_name("BTN_NORTH");
+ map->button_pairs[BTN_PAIR_XY - 1].second.remap =
+ find_button_by_name("BTN_WEST");
+
+ map->button_pairs[BTN_PAIR_BUMPER_LR - 1].first.remap =
+ find_button_by_name("BTN_TL");
+ map->button_pairs[BTN_PAIR_BUMPER_LR - 1].second.remap =
+ find_button_by_name("BTN_TR");
+
+ map->button_pairs[BTN_PAIR_STICK_LR - 1].first.remap =
+ find_button_by_name("BTN_THUMBL");
+ map->button_pairs[BTN_PAIR_STICK_LR - 1].second.remap =
+ find_button_by_name("BTN_THUMBR");
+
+ map->button_pairs[BTN_PAIR_DPAD_UPDOWN - 1].first.remap =
+ find_button_by_name("ABS_HAT0Y_NEG");
+ map->button_pairs[BTN_PAIR_DPAD_UPDOWN - 1].second.remap =
+ find_button_by_name("ABS_HAT0Y_POS");
+
+ map->button_pairs[BTN_PAIR_DPAD_LEFTRIGHT - 1].first.remap =
+ find_button_by_name("ABS_HAT0X_NEG");
+ map->button_pairs[BTN_PAIR_DPAD_LEFTRIGHT - 1].second.remap =
+ find_button_by_name("ABS_HAT0X_POS");
+
+ map->button_pairs[BTN_PAIR_TRIGGER_LR - 1].first.remap =
+ find_button_by_name("ABS_Z");
+ map->button_pairs[BTN_PAIR_TRIGGER_LR - 1].second.remap =
+ find_button_by_name("ABS_RZ");
+
+ map->button_pairs[BTN_PAIR_VIEW_MENU - 1].first.remap =
+ find_button_by_name("BTN_SELECT");
+ map->button_pairs[BTN_PAIR_VIEW_MENU - 1].second.remap =
+ find_button_by_name("BTN_START");
+
+ map->button_pairs[BTN_PAIR_M1M2 - 1].first.remap =
+ find_button_by_name("FN_M2");
+ map->button_pairs[BTN_PAIR_M1M2 - 1].second.remap =
+ find_button_by_name("FN_M1");
+}
+
+static void ally_set_default_keyboard_mapping(struct ally_button_mapping *map)
+{
+ ally_set_button_pair_mappings_default(map);
+
+ /*
+ * Desktop mode defaults: keyboard and mouse mappings.
+ * Buttons not listed here (dpad, view, menu) remain NONE.
+ */
+
+ /* btn_a => KEY_SPACE, btn_b => KEY_E */
+ map->button_pairs[BTN_PAIR_AB - 1].first.remap =
+ find_button_by_name("KEY_SPACE");
+ map->button_pairs[BTN_PAIR_AB - 1].second.remap =
+ find_button_by_name("KEY_E");
+
+ /* btn_x => KEY_R, btn_y => KEY_F */
+ map->button_pairs[BTN_PAIR_XY - 1].first.remap =
+ find_button_by_name("KEY_R");
+ map->button_pairs[BTN_PAIR_XY - 1].second.remap =
+ find_button_by_name("KEY_F");
+
+ /* LB => wheel up, RB => wheel down */
+ map->button_pairs[BTN_PAIR_BUMPER_LR - 1].first.remap =
+ find_button_by_name("REL_WHEEL_HI_RES_UP");
+ map->button_pairs[BTN_PAIR_BUMPER_LR - 1].second.remap =
+ find_button_by_name("REL_WHEEL_HI_RES_DOWN");
+
+ /* left stick click => left click, right stick click => left shift */
+ map->button_pairs[BTN_PAIR_STICK_LR - 1].first.remap =
+ find_button_by_name("BTN_LEFT");
+ map->button_pairs[BTN_PAIR_STICK_LR - 1].second.remap =
+ find_button_by_name("KEY_LEFTSHIFT");
+
+ /* LT => left click, RT => right click */
+ map->button_pairs[BTN_PAIR_TRIGGER_LR - 1].first.remap =
+ find_button_by_name("BTN_LEFT");
+ map->button_pairs[BTN_PAIR_TRIGGER_LR - 1].second.remap =
+ find_button_by_name("BTN_RIGHT");
+
+ /* M2 => FN_M2, M1 => FN_M1 */
+ map->button_pairs[BTN_PAIR_M1M2 - 1].first.remap =
+ find_button_by_name("FN_M2");
+ map->button_pairs[BTN_PAIR_M1M2 - 1].second.remap =
+ find_button_by_name("FN_M1");
+}
+
+/**
+ * ally_remap_code_valid() - Check if a remap target is valid for a button
+ * @btn: button to remap
+ * @gamepad_mode: gamepad mode to validate the remap against
+ * @code: remap target to check
+ * @is_macro: whether the target is assigned to the macro mapping
+ *
+ * M1 and M2 can be remapped to anything at any time.
+ * All other buttons may only be remapped to gamepad buttons (BTN_TYPE_PAD)
+ * when in gamepad mode, and to keyboard/mouse/media codes when in desktop
+ * (keyboard) mode. BTN_TYPE_NONE (unmapped) is always allowed.
+ * Macro mappings are executed by the MCU as input sequences on the virtual
+ * keyboard and are accepted regardless of the current gamepad mode.
+ *
+ * Return: true if the remap is valid, false otherwise.
+ */
+static bool ally_remap_code_valid(enum ally_button_id btn, u8 gamepad_mode,
+ const struct btn_code_map *code, bool is_macro)
+{
+ if (!code)
+ return false;
+
+ /* M1 and M2 can be remapped to everything */
+ if (btn == ALLY_BTN_M1 || btn == ALLY_BTN_M2)
+ return true;
+
+ /* NONE is always valid */
+ if (code->type == BTN_TYPE_NONE)
+ return true;
+
+ /* Macros are not restricted by the controller emulation mode */
+ if (is_macro)
+ return true;
+
+ if (gamepad_mode == ALLY_GAMEPAD_MODE_GAMEPAD)
+ return code->type == BTN_TYPE_PAD;
+
+ /* Desktop / keyboard mode: allow keyboard, mouse and media */
+ return code->type == BTN_TYPE_KB ||
+ code->type == BTN_TYPE_MOUSE ||
+ code->type == BTN_TYPE_MEDIA;
+}
+
+/**
+ * ally_encode_button_code() - Pack one button code into an MCU mapping slot
+ * @buf: command buffer to write
+ * @offset: byte offset of the slot in @buf
+ * @code: button code to encode
+ *
+ * The slot is %ASUS_ALLY_BTN_FUNCTIONS_LEN bytes. Byte 0 is the type. A pad
+ * value sits at byte 1, a keyboard value at byte 2, a media value at byte 3
+ * and a mouse value at byte 4.
+ */
+static void ally_encode_button_code(u8 *buf, unsigned int offset,
+ const struct btn_code_map *code)
+{
+ u8 bytes[ASUS_ALLY_BTN_FUNCTIONS_LEN] = {};
+
+ buf[offset] = code->type;
+ if (!code->type)
+ return;
+
+ bytes[0] = code->type;
+ switch (code->type) {
+ case BTN_TYPE_NONE:
+ break;
+ case BTN_TYPE_PAD:
+ bytes[1] = code->value;
+ break;
+ case BTN_TYPE_KB:
+ bytes[2] = code->value;
+ break;
+ case BTN_TYPE_MEDIA:
+ bytes[3] = code->value;
+ break;
+ case BTN_TYPE_MOUSE:
+ bytes[4] = code->value;
+ break;
+ }
+
+ memcpy(&buf[offset], bytes, sizeof(bytes));
+}
+
+/**
+ * ally_set_button_mapping() - Set the button mapping for a button pair
+ * @hdev: HID device
+ * @mapping: button pair mapping to send to the device
+ *
+ * Return: count of data transferred, negative if error.
+ */
+static int ally_set_button_mapping(struct hid_device *hdev,
+ struct button_pair_map *mapping)
+{
+ /*
+ * The MCU mapping block is four consecutive 11-byte entries
+ * starting at buf[5]: first remap 5-15, first macro 16-26,
+ * second remap 27-37, second macro 38-48. Each entry is one
+ * ally_encode_button_code() slot.
+ */
+ size_t buf_len;
+
+ if (!mapping || !mapping->first.remap || !mapping->first.macro ||
+ !mapping->second.remap || !mapping->second.macro)
+ return -EINVAL;
+
+ /*
+ * The mapping block spans buf[4]..buf[48]: give ally_alloc_cmd() its
+ * size so the fit check protects the fixed-offset writes below.
+ */
+ u8 *buf __free(kfree) = ally_alloc_cmd(hdev, CMD_SET_MAPPING, NULL,
+ 1 + ASUS_ALLY_BTN_CODE_LEN,
+ &buf_len);
+ if (!buf)
+ return -ENOMEM;
+
+ /*
+ * This packet is slightly different from the other
+ * as before the packet length there is an extra byte
+ * which is the pair index.
+ */
+ buf[3] = mapping->pair_index;
+ buf[4] = ASUS_ALLY_BTN_CODE_LEN;
+
+ /* First remap, first macro, second remap, second macro. */
+ ally_encode_button_code(buf, 5, mapping->first.remap);
+ ally_encode_button_code(buf, 16, mapping->first.macro);
+ ally_encode_button_code(buf, 27, mapping->second.remap);
+ ally_encode_button_code(buf, 38, mapping->second.macro);
+
+ return ally_gamepad_send_packet(hdev, buf, buf_len);
+}
+
+/**
+ * ally_apply_button_mappings() - Push the recorded mappings of the current
+ * gamepad mode to the controller
+ * @hdev: HID device
+ *
+ * An MCU reset restores the firmware default mappings: push the recorded
+ * ones again so that the hardware keeps matching what userspace
+ * configured. The caller holds ally_prefs.lock, as the sysfs stores that
+ * update a single pair do.
+ */
+static void ally_apply_button_mappings(struct hid_device *hdev)
+{
+ struct ally_button_mapping *mode_map;
+ int i, ret;
+
+ mode_map = &ally_prefs.button_mappings;
+
+ for (i = 0; i < ARRAY_SIZE(mode_map->button_pairs); i++) {
+ mode_map->button_pairs[i].pair_index = i + 1;
+
+ ret = ally_set_button_mapping(hdev,
+ &mode_map->button_pairs[i]);
+ if (ret < 0) {
+ hid_warn(hdev,
+ "Failed to restore button mappings: %d\n",
+ ret);
+ return;
+ }
+ }
+}
+
+/**
+ * get_button_pair_info() - Get the pair index and position of a button
+ * @button_id: button to look up
+ * @pair_idx: output pointer for the pair index of the button
+ * @is_first: output pointer for the position of the button within the pair
+ *
+ * Return: 0 on success, negative errno on failure.
+ */
+static int get_button_pair_info(enum ally_button_id button_id,
+ enum btn_pair_index *pair_idx,
+ bool *is_first)
+{
+ switch (button_id) {
+ case ALLY_BTN_DU:
+ *pair_idx = BTN_PAIR_DPAD_UPDOWN;
+ *is_first = true;
+ break;
+ case ALLY_BTN_DD:
+ *pair_idx = BTN_PAIR_DPAD_UPDOWN;
+ *is_first = false;
+ break;
+ case ALLY_BTN_DL:
+ *pair_idx = BTN_PAIR_DPAD_LEFTRIGHT;
+ *is_first = true;
+ break;
+ case ALLY_BTN_DR:
+ *pair_idx = BTN_PAIR_DPAD_LEFTRIGHT;
+ *is_first = false;
+ break;
+ case ALLY_BTN_J0B:
+ *pair_idx = BTN_PAIR_STICK_LR;
+ *is_first = true;
+ break;
+ case ALLY_BTN_J1B:
+ *pair_idx = BTN_PAIR_STICK_LR;
+ *is_first = false;
+ break;
+ case ALLY_BTN_LB:
+ *pair_idx = BTN_PAIR_BUMPER_LR;
+ *is_first = true;
+ break;
+ case ALLY_BTN_RB:
+ *pair_idx = BTN_PAIR_BUMPER_LR;
+ *is_first = false;
+ break;
+ case ALLY_BTN_A:
+ *pair_idx = BTN_PAIR_AB;
+ *is_first = true;
+ break;
+ case ALLY_BTN_B:
+ *pair_idx = BTN_PAIR_AB;
+ *is_first = false;
+ break;
+ case ALLY_BTN_X:
+ *pair_idx = BTN_PAIR_XY;
+ *is_first = true;
+ break;
+ case ALLY_BTN_Y:
+ *pair_idx = BTN_PAIR_XY;
+ *is_first = false;
+ break;
+ case ALLY_BTN_VIEW:
+ *pair_idx = BTN_PAIR_VIEW_MENU;
+ *is_first = true;
+ break;
+ case ALLY_BTN_MENU:
+ *pair_idx = BTN_PAIR_VIEW_MENU;
+ *is_first = false;
+ break;
+ case ALLY_BTN_M1:
+ *pair_idx = BTN_PAIR_M1M2;
+ *is_first = false;
+ break;
+ case ALLY_BTN_M2:
+ *pair_idx = BTN_PAIR_M1M2;
+ *is_first = true;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static ssize_t button_remap_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct button_remap_attr *btn_attr = to_button_remap_attr(attr);
+ const struct btn_code_map *code;
+ enum ally_button_id button_id = btn_attr->button_id;
+ enum btn_pair_index pair_idx;
+ struct button_pair_map *pair;
+ struct button_map *btn_map;
+ bool is_first;
+ int ret;
+
+ ret = get_button_pair_info(button_id, &pair_idx, &is_first);
+ if (ret < 0)
+ return ret;
+
+ guard(mutex)(&ally_prefs.lock);
+
+ pair = &ally_prefs.button_mappings.button_pairs[pair_idx - 1];
+ btn_map = is_first ? &pair->first : &pair->second;
+ code = btn_attr->is_macro ? btn_map->macro : btn_map->remap;
+
+ /*
+ * The mappings are never NULL: the defaults, the mode-switch
+ * re-defaults and the store rollback all guarantee it. A miss
+ * means a broken default table, so fail loudly instead of
+ * dereferencing.
+ */
+ if (!code)
+ return -EIO;
+
+ return sysfs_emit(buf, "%s\n", code->name);
+}
+
+static ssize_t button_remap_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct button_remap_attr *btn_attr = to_button_remap_attr(attr);
+ enum ally_button_id button_id = btn_attr->button_id;
+ const struct btn_code_map *old_code, *code;
+ const struct btn_code_map **target_map;
+ struct ally_button_mapping *mode_map;
+ enum btn_pair_index pair_idx;
+ struct button_pair_map *pair;
+ struct button_map *btn_map;
+ bool is_first;
+ int ret, i;
+
+ code = find_button_by_name(buf);
+ if (!code)
+ return -EINVAL;
+
+ ret = get_button_pair_info(button_id, &pair_idx, &is_first);
+ if (ret < 0)
+ return ret;
+
+ guard(mutex)(&ally_prefs.lock);
+
+ /*
+ * Validate the remap target while holding the lock: the
+ * gamepad mode can change concurrently, and the check must
+ * be done against the very mode the mapping is written to.
+ */
+ if (!ally_remap_code_valid(button_id, ally_prefs.gamepad_mode, code,
+ btn_attr->is_macro))
+ return -EINVAL;
+
+ /* Access the mapping for current gamepad mode */
+ pair = &ally_prefs.button_mappings.button_pairs[pair_idx - 1];
+ btn_map = is_first ? &pair->first : &pair->second;
+
+ target_map = btn_attr->is_macro ? &btn_map->macro : &btn_map->remap;
+ old_code = *target_map;
+ *target_map = code;
+
+ /* Update pair index */
+ pair->pair_index = pair_idx;
+
+ /*
+ * Send mapping to device with the caveat that first
+ * generation devices require individual button pair updates.
+ */
+ if (ally_prefs.is_ally_x) {
+ ret = ally_set_button_mapping(hdev, pair);
+ if (ret < 0)
+ goto err_restore;
+ } else {
+ mode_map = &ally_prefs.button_mappings;
+ for (i = 0; i < ARRAY_SIZE(mode_map->button_pairs); i++) {
+ mode_map->button_pairs[i].pair_index = i + 1;
+ ret = ally_set_button_mapping(hdev,
+ &mode_map->button_pairs[i]);
+ if (ret < 0) {
+ /*
+ * Pairs are pushed one at a time. This pair
+ * already reached the hardware when a later
+ * pair is the one that failed.
+ */
+ if (i > pair_idx - 1)
+ goto err_restore_hw;
+ goto err_restore;
+ }
+ }
+ }
+ sysfs_notify(&dev->kobj, ally_button_names[btn_attr->button_id],
+ btn_attr->is_macro ? "macro" : "remap");
+ return count;
+
+err_restore_hw:
+ *target_map = old_code;
+
+ /* Try to recover the mapping presented to the user. */
+ ally_set_button_mapping(hdev, pair);
+ goto err_out;
+
+err_restore:
+ *target_map = old_code;
+
+err_out:
+ hid_err(hdev, "Failed to set button mapping: %d\n", ret);
+ return ret;
+}
+
+/**
+ * button_remap_attr_create() - Create a button remap attribute
+ * @button_id: button the attribute is for
+ * @is_macro: whether the attribute controls the macro mapping
+ *
+ * Return: the newly created attribute, or NULL on allocation failure.
+ */
+static struct button_remap_attr *button_remap_attr_create(enum ally_button_id button_id,
+ bool is_macro)
+{
+ struct button_remap_attr *const attr = kzalloc_obj(*attr);
+
+ if (!attr)
+ return NULL;
+
+ attr->button_id = button_id;
+ attr->is_macro = is_macro;
+ sysfs_attr_init(&attr->dev_attr.attr);
+ attr->dev_attr.attr.name = is_macro ? "macro" : "remap";
+ attr->dev_attr.attr.mode = 0644;
+ attr->dev_attr.show = button_remap_show;
+ attr->dev_attr.store = button_remap_store;
+
+ return attr;
+}
+
+/**
+ * btn_remap_available_show() - Show the list of valid remap targets for a button
+ * @dev: device the attribute belongs to
+ * @attr: attribute being read
+ * @buf: buffer to write the list into
+ *
+ * M1/M2 list everything; other buttons list only the codes valid for the
+ * active mode (gamepad buttons in gamepad mode, keyboard / mouse / media
+ * in desktop mode).
+ *
+ * Return: number of characters written.
+ */
+static ssize_t btn_remap_available_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct ally_btn_sysfs_entry *const entry = container_of(attr,
+ struct ally_btn_sysfs_entry, attr_remap_available);
+ enum ally_button_id btn = entry->btn;
+ u8 mode;
+ ssize_t len = 0;
+ int i;
+
+ scoped_guard(mutex, &ally_prefs.lock)
+ mode = ally_prefs.gamepad_mode;
+
+ for (i = 0; i < ARRAY_SIZE(ally_btn_codes); i++) {
+ if (!ally_remap_code_valid(btn, mode, &ally_btn_codes[i], false))
+ continue;
+ len += sysfs_emit_at(buf, len, "%s ", ally_btn_codes[i].name);
+ }
+
+ if (len > 0)
+ buf[len - 1] = '\n';
+
+ return len;
+}
+
+static void ally_mapping_defaults(void)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ if (ally_prefs.initialized)
+ return;
+
+ ally_set_default_gamepad_mapping(&ally_prefs.button_mappings);
+}
+
/**
* ally_check_capability() - Check if a specific capability is supported
* @hdev: HID device to query
@@ -1523,8 +2308,8 @@ static ssize_t gamepad_mode_store(struct device *dev, struct device_attribute *a
const char *buf, size_t count)
{
struct hid_device *hdev = to_hid_device(dev);
+ int mode, ret, i;
u8 mode_byte;
- int mode, ret;
mode = sysfs_match_string(ally_gamepad_mode_text, buf);
if (mode < 0)
@@ -1539,11 +2324,27 @@ static ssize_t gamepad_mode_store(struct device *dev, struct device_attribute *a
return ret;
ally_prefs.gamepad_mode = mode_byte;
+
+ /*
+ * The MCU restores its own defaults for the new mode. Refresh the
+ * cache so a later read reports what the firmware is using, then
+ * wake anyone polling the remap, macro, and remap_available files.
+ */
+ if (mode_byte == ALLY_GAMEPAD_MODE_GAMEPAD)
+ ally_set_default_gamepad_mapping(&ally_prefs.button_mappings);
+ else if (mode_byte == ALLY_GAMEPAD_MODE_KEYBOARD)
+ ally_set_default_keyboard_mapping(&ally_prefs.button_mappings);
}
hid_dbg(hdev, "Set gamepad mode to %s\n", ally_gamepad_mode_text[mode]);
sysfs_notify(&dev->kobj, NULL, "gamepad_mode");
+ for (i = 0; i < ALLY_BTN_MAX; i++) {
+ sysfs_notify(&dev->kobj, ally_button_names[i], "remap");
+ sysfs_notify(&dev->kobj, ally_button_names[i], "macro");
+ sysfs_notify(&dev->kobj, ally_button_names[i], "remap_available");
+ }
+
return count;
}
@@ -2399,11 +3200,11 @@ static void ally_btn_turbo_init_attrs(struct ally_btn_sysfs_entry *entry)
static struct ally_btn_sysfs_entry *ally_create_button_attributes(struct hid_device *hdev)
{
+ size_t entries_attr_index;
int i, ret;
scoped_guard(mutex, &ally_prefs.lock)
- if (!ally_prefs.turbo_support)
- return NULL;
+ entries_attr_index = ally_prefs.turbo_support ? 4 : 0;
struct ally_btn_sysfs_entry *entries __free(kfree) = kzalloc_objs(*entries, ALLY_BTN_MAX);
if (!entries)
@@ -2412,13 +3213,33 @@ static struct ally_btn_sysfs_entry *ally_create_button_attributes(struct hid_dev
for (i = 0; i < ALLY_BTN_MAX; i++) {
entries[i].btn = i;
- if (!ally_btn_param(i)) {
- hid_err(hdev, "Invalid button id %d for turbo attributes\n", i);
- ret = -EINVAL;
+ if (ally_prefs.turbo_support) {
+ if (!ally_btn_param(i)) {
+ hid_err(hdev, "Invalid button id %d for turbo attributes\n", i);
+ ret = -EINVAL;
+ goto ally_btn_err_cleanup;
+ }
+
+ ally_btn_turbo_init_attrs(&entries[i]);
+ }
+
+ entries[i].remap_attr = button_remap_attr_create(i, false);
+ entries[i].macro_attr = button_remap_attr_create(i, true);
+ if (!entries[i].remap_attr || !entries[i].macro_attr) {
+ ret = -ENOMEM;
goto ally_btn_err_cleanup;
}
- ally_btn_turbo_init_attrs(&entries[i]);
+ sysfs_attr_init(&entries[i].attr_remap_available.attr);
+ entries[i].attr_remap_available.attr.name = "remap_available";
+ entries[i].attr_remap_available.attr.mode = 0444;
+ entries[i].attr_remap_available.show = btn_remap_available_show;
+
+ entries[i].attrs[entries_attr_index + 0] = &entries[i].remap_attr->dev_attr.attr;
+ entries[i].attrs[entries_attr_index + 1] = &entries[i].macro_attr->dev_attr.attr;
+ entries[i].attrs[entries_attr_index + 2] = &entries[i].attr_remap_available.attr;
+ entries[i].attrs[entries_attr_index + 3] = NULL;
+
entries[i].group.name = ally_button_names[i];
entries[i].group.attrs = entries[i].attrs;
@@ -2436,6 +3257,11 @@ static struct ally_btn_sysfs_entry *ally_create_button_attributes(struct hid_dev
while (i--)
sysfs_remove_group(&hdev->dev.kobj, &entries[i].group);
+ for (i = 0; i < ALLY_BTN_MAX; i++) {
+ kfree(entries[i].remap_attr);
+ kfree(entries[i].macro_attr);
+ }
+
return ERR_PTR(ret);
}
@@ -2449,6 +3275,8 @@ static void ally_remove_button_attributes(struct hid_device *hdev,
for (i = 0; i < ALLY_BTN_MAX; i++) {
sysfs_remove_group(&hdev->dev.kobj, &entries[i].group);
+ kfree(entries[i].remap_attr);
+ kfree(entries[i].macro_attr);
}
kfree(entries);
@@ -2468,6 +3296,7 @@ static int ally_prefs_setup(struct hid_device *hdev, struct ally_intf *intf)
ally_joystick_defaults();
ally_trigger_defaults();
ally_curve_defaults();
+ ally_mapping_defaults();
scoped_guard(mutex, &ally_prefs.lock) {
if (!ally_prefs.initialized) {
@@ -2925,6 +3754,9 @@ static int hid_asus_ally_init(struct hid_device *hdev)
hid_warn(hdev, "Failed to restore turbo parameters: %d\n", ret);
}
+ if (ally_prefs.initialized)
+ ally_apply_button_mappings(hdev);
+
return 0;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* Re: [PATCH v13 12/18] HID: asus: add support for btn remapping
2026-11-04 1:39 ` [PATCH v13 12/18] HID: asus: add support for btn remapping Denis Benato
@ 2026-11-04 1:58 ` sashiko-bot
0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-11-04 1:58 UTC (permalink / raw)
To: Denis Benato; +Cc: linux-input, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] HID: asus: inverted sysfs_streq() check in find_button_by_name()
- [Medium] HID: asus: inconsistent pair update and rollback in button_remap_store()
- [Low] HID: asus: mixed cleanup helpers and goto in button_remap_store()
--
commit 72acf6c4d3c76532f8563fc72f2841557666f18a
Author: Denis Benato <denis.benato@linux.dev>
HID: asus: add support for btn remapping
This commit adds sysfs support for remapping physical buttons and assigning
macro codes on Asus ROG Ally devices, updating the cached mappings when the
gamepad mode changes.
> diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
> index 70c9d71fc5fa9..dcb3c9f5d23e8 100644
> --- a/drivers/hid/hid-asus.c
> +++ b/drivers/hid/hid-asus.c
[ ... ]
> @@ -978,10 +1186,587 @@ static struct ally_prefs {
> struct ally_joystick_resp_curve right_curve;
>
> struct ally_turbo_config turbo;
> + struct ally_button_mapping button_mappings;
> } ally_prefs = {
> .lock = __MUTEX_INITIALIZER(ally_prefs.lock),
> };
>
> +/**
> + * find_button_by_name() - Find a button code map by its name
> + * @name: name of the button code to look up
> + *
> + * This function deals with data from userspace therefore it
> + * must not trust the input blindly.
> + *
> + * Return: the matching button code map, or NULL if not found.
> + */
> +static const struct btn_code_map *find_button_by_name(const char *name)
> +{
> + int i;
> +
> + for (i = 0; i < ARRAY_SIZE(ally_btn_codes); i++) {
> + if (!sysfs_streq(ally_btn_codes[i].name, name))
> + return &ally_btn_codes[i];
> + }
> +
> + return NULL;
> +}
[Severity: High]
Is the sysfs_streq() condition in find_button_by_name() inverted?
Unlike strcmp(), sysfs_streq() returns true when the strings match. Because
ally_btn_codes[0] is "NONE" and ally_btn_codes[1] is "BTN_SOUTH", negating
sysfs_streq() causes find_button_by_name("NONE") in
ally_set_button_pair_mappings_default() to return &ally_btn_codes[1]
(BTN_SOUTH), and causes lookups for any other button name (or invalid strings
written by userspace to button_remap_store()) to immediately return
&ally_btn_codes[0] (NONE).
When hid_asus_ally_init() calls ally_apply_button_mappings(), or when
gamepad_mode_store() or button_remap_store() updates mappings, all default
remaps resolve to NONE, all default macros resolve to BTN_SOUTH, and invalid
button names from userspace never fail with -EINVAL.
[ ... ]
> +static ssize_t button_remap_store(struct device *dev,
> + struct device_attribute *attr,
> + const char *buf, size_t count)
> +{
> + struct hid_device *hdev = to_hid_device(dev);
> + struct button_remap_attr *btn_attr = to_button_remap_attr(attr);
> + enum ally_button_id button_id = btn_attr->button_id;
> + const struct btn_code_map *old_code, *code;
> + const struct btn_code_map **target_map;
> + struct ally_button_mapping *mode_map;
> + enum btn_pair_index pair_idx;
> + struct button_pair_map *pair;
> + struct button_map *btn_map;
> + bool is_first;
> + int ret, i;
> +
> + code = find_button_by_name(buf);
> + if (!code)
> + return -EINVAL;
> +
> + ret = get_button_pair_info(button_id, &pair_idx, &is_first);
> + if (ret < 0)
> + return ret;
> +
> + guard(mutex)(&ally_prefs.lock);
> +
> + /*
> + * Validate the remap target while holding the lock: the
> + * gamepad mode can change concurrently, and the check must
> + * be done against the very mode the mapping is written to.
> + */
> + if (!ally_remap_code_valid(button_id, ally_prefs.gamepad_mode, code,
> + btn_attr->is_macro))
> + return -EINVAL;
> +
> + /* Access the mapping for current gamepad mode */
> + pair = &ally_prefs.button_mappings.button_pairs[pair_idx - 1];
> + btn_map = is_first ? &pair->first : &pair->second;
> +
> + target_map = btn_attr->is_macro ? &btn_map->macro : &btn_map->remap;
> + old_code = *target_map;
> + *target_map = code;
> +
> + /* Update pair index */
> + pair->pair_index = pair_idx;
> +
> + /*
> + * Send mapping to device with the caveat that first
> + * generation devices require individual button pair updates.
> + */
> + if (ally_prefs.is_ally_x) {
> + ret = ally_set_button_mapping(hdev, pair);
> + if (ret < 0)
> + goto err_restore;
> + } else {
> + mode_map = &ally_prefs.button_mappings;
> + for (i = 0; i < ARRAY_SIZE(mode_map->button_pairs); i++) {
> + mode_map->button_pairs[i].pair_index = i + 1;
> + ret = ally_set_button_mapping(hdev,
> + &mode_map->button_pairs[i]);
> + if (ret < 0) {
> + /*
> + * Pairs are pushed one at a time. This pair
> + * already reached the hardware when a later
> + * pair is the one that failed.
> + */
> + if (i > pair_idx - 1)
> + goto err_restore_hw;
> + goto err_restore;
> + }
> + }
> + }
> + sysfs_notify(&dev->kobj, ally_button_names[btn_attr->button_id],
> + btn_attr->is_macro ? "macro" : "remap");
> + return count;
> +
> +err_restore_hw:
> + *target_map = old_code;
> +
> + /* Try to recover the mapping presented to the user. */
> + ally_set_button_mapping(hdev, pair);
> + goto err_out;
> +
> +err_restore:
> + *target_map = old_code;
> +
> +err_out:
> + hid_err(hdev, "Failed to set button mapping: %d\n", ret);
> + return ret;
> +}
[Severity: Medium]
Does the err_restore_hw path in button_remap_store() properly roll back the
hardware state on first-generation ROG Ally devices?
In ally_detect_capabilities(), ally_prefs.is_ally_x is set only for the second-
generation ROG Ally X (USB_DEVICE_ID_ASUSTEK_ROG_NKEY_ALLY_X). In
button_remap_store(), the comment says first-generation devices require
individual button pair updates, yet the code sends a single pair when
ally_prefs.is_ally_x is true and loops over all 9 pairs in
mode_map->button_pairs when ally_prefs.is_ally_x is false.
Furthermore, err_restore_hw is only reachable from the !ally_prefs.is_ally_x
loop when ally_set_button_mapping() fails at iteration i > pair_idx - 1, but
err_restore_hw then calls ally_set_button_mapping(hdev, pair) for only that
single pair. If !ally_prefs.is_ally_x devices require all 9 pairs to be sent,
wouldn't a single-pair update in err_restore_hw fail to restore the hardware
mapping, or if single-pair updates work on !ally_prefs.is_ally_x, why does the
else branch send all 9 pairs?
[ ... ]
> @@ -2399,11 +3200,11 @@ static void ally_btn_turbo_init_attrs(struct ally_btn_sysfs_entry *entry)
>
> static struct ally_btn_sysfs_entry *ally_create_button_attributes(struct hid_device *hdev)
> {
> + size_t entries_attr_index;
> int i, ret;
>
> scoped_guard(mutex, &ally_prefs.lock)
> - if (!ally_prefs.turbo_support)
> - return NULL;
> + entries_attr_index = ally_prefs.turbo_support ? 4 : 0;
>
> struct ally_btn_sysfs_entry *entries __free(kfree) = kzalloc_objs(*entries, ALLY_BTN_MAX);
> if (!entries)
> @@ -2412,13 +3213,33 @@ static struct ally_btn_sysfs_entry *ally_create_button_attributes(struct hid_dev
> for (i = 0; i < ALLY_BTN_MAX; i++) {
> entries[i].btn = i;
>
> - if (!ally_btn_param(i)) {
> - hid_err(hdev, "Invalid button id %d for turbo attributes\n", i);
> - ret = -EINVAL;
> + if (ally_prefs.turbo_support) {
> + if (!ally_btn_param(i)) {
> + hid_err(hdev, "Invalid button id %d for turbo attributes\n", i);
> + ret = -EINVAL;
> + goto ally_btn_err_cleanup;
> + }
> +
> + ally_btn_turbo_init_attrs(&entries[i]);
> + }
> +
> + entries[i].remap_attr = button_remap_attr_create(i, false);
> + entries[i].macro_attr = button_remap_attr_create(i, true);
> + if (!entries[i].remap_attr || !entries[i].macro_attr) {
> + ret = -ENOMEM;
> goto ally_btn_err_cleanup;
> }
>
> - ally_btn_turbo_init_attrs(&entries[i]);
> + sysfs_attr_init(&entries[i].attr_remap_available.attr);
> + entries[i].attr_remap_available.attr.name = "remap_available";
> + entries[i].attr_remap_available.attr.mode = 0444;
> + entries[i].attr_remap_available.show = btn_remap_available_show;
> +
> + entries[i].attrs[entries_attr_index + 0] = &entries[i].remap_attr->dev_attr.attr;
> + entries[i].attrs[entries_attr_index + 1] = &entries[i].macro_attr->dev_attr.attr;
> + entries[i].attrs[entries_attr_index + 2] = &entries[i].attr_remap_available.attr;
> + entries[i].attrs[entries_attr_index + 3] = NULL;
> +
> entries[i].group.name = ally_button_names[i];
> entries[i].group.attrs = entries[i].attrs;
>
> @@ -2436,6 +3257,11 @@ static struct ally_btn_sysfs_entry *ally_create_button_attributes(struct hid_dev
> while (i--)
> sysfs_remove_group(&hdev->dev.kobj, &entries[i].group);
>
> + for (i = 0; i < ALLY_BTN_MAX; i++) {
> + kfree(entries[i].remap_attr);
> + kfree(entries[i].macro_attr);
> + }
> +
> return ERR_PTR(ret);
> }
[Severity: Low]
As reported by Sashiko on v12 [1], this isn't a bug, but button_remap_store()
mixes guard(mutex)(&ally_prefs.lock) with goto labels (err_restore_hw,
err_restore, and err_out) while also holding ally_prefs.lock across
sysfs_notify(), and ally_create_button_attributes() similarly mixes
scoped_guard() and __free(kfree) on entries with goto ally_btn_err_cleanup.
Could button_remap_store() and ally_create_button_attributes() avoid mixing
scope-based cleanup helpers with goto error paths (and could remap_attr and
macro_attr be embedded directly in struct ally_btn_sysfs_entry like
attr_turbo_period, attr_toggle_period, and attr_remap_available)?
[1] https://lore.kernel.org/r/sashiko-outbox-192087@kernel.org
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261104013935.512713-1-denis.benato@linux.dev?part=12
^ permalink raw reply [flat|nested] 25+ messages in thread
* [PATCH v13 13/18] HID: asus: identify XG Mobile stations and bind the LED to the reported model
2026-11-04 1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
` (11 preceding siblings ...)
2026-11-04 1:39 ` [PATCH v13 12/18] HID: asus: add support for btn remapping Denis Benato
@ 2026-11-04 1:39 ` Denis Benato
2026-11-04 1:39 ` [PATCH v13 14/18] HID: asus: expose XG Mobile identity and firmware information Denis Benato
` (4 subsequent siblings)
17 siblings, 0 replies; 25+ messages in thread
From: Denis Benato @ 2026-11-04 1:39 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
The XG Mobile LED interface is currently bound to two USB product IDs
(0x1970, 0x1a9a). The stations identify themselves through the vendor
report 0x5E: after the usual "ASUS Tech.Inc." unlock, the 0xCE 0x01
query answers with a NUL terminated string of the form
MB:<mainboard>;SN:<serial>;PRO:<model>;PNUM:<part number>;
Run this query at probe time to initialize the LED interface only on
supported models (GC31, GC32 and GC33). The LED name is the model
followed by the bus identity (asus:xgm-<model>-<uniq>:led).
The identity query is tried three times, one second apart, so one
missed transfer does not leave the LED unregistered.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
---
drivers/hid/hid-asus.c | 234 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 232 insertions(+), 2 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index dcb3c9f5d23e..eb9dd69fd605 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -26,6 +26,7 @@
#include <linux/compiler_attributes.h>
#include <linux/container_of.h>
#include <linux/ctype.h>
+#include <linux/delay.h>
#include <linux/dmi.h>
#include <linux/err.h>
#include <linux/hid.h>
@@ -69,6 +70,29 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define FEATURE_KBD_LED_REPORT_ID1 0x5d
#define FEATURE_KBD_LED_REPORT_ID2 0x5e
+/* XG Mobile station protocol on FEATURE_KBD_LED_REPORT_ID2. */
+#define ASUS_XGM_CMD_QUERY 0xce
+#define ASUS_XGM_QUERY_IDENTITY 0x01
+#define ASUS_XGM_INFO_LEN 32
+
+/* Identity string fields, each "TAG:value;" inside the query answer. */
+#define ASUS_XGM_TAG_MAINBOARD "MB:"
+#define ASUS_XGM_TAG_SERIAL "SN:"
+#define ASUS_XGM_TAG_MODEL "PRO:"
+#define ASUS_XGM_TAG_PART "PNUM:"
+
+/*
+ * Every station answer echoes the report id, the opcode and the
+ * subcommand, then the payload.
+ */
+enum {
+ ASUS_XGM_RESP_REPORT_ID,
+ ASUS_XGM_RESP_OPCODE,
+ ASUS_XGM_RESP_SUBCMD,
+ ASUS_XGM_RESP_PAYLOAD,
+ ASUS_XGM_RESP_HDR_LEN = ASUS_XGM_RESP_PAYLOAD,
+};
+
#define ROG_ALLY_X_MIN_MCU 313
#define ROG_ALLY_MIN_MCU 319
@@ -216,6 +240,13 @@ struct asus_xgm_led {
struct hid_device *hdev;
};
+struct asus_xgm_info {
+ char mainboard[ASUS_XGM_INFO_LEN];
+ char serial[ASUS_XGM_INFO_LEN];
+ char model[ASUS_XGM_INFO_LEN];
+ char part_number[ASUS_XGM_INFO_LEN];
+};
+
struct asus_touchpad_info {
int max_x;
int max_y;
@@ -290,6 +321,7 @@ struct asus_drvdata {
struct asus_hid_listener listener;
bool fn_lock;
struct asus_xgm_led *xgm_led;
+ struct asus_xgm_info *xgm_info;
};
static int asus_report_battery(struct asus_drvdata *, u8 *, int);
@@ -4868,6 +4900,169 @@ static int asus_battery_probe(struct hid_device *hdev)
return ret;
}
+/**
+ * asus_xgm_query() - Run a query command against an XG Mobile station
+ * @hdev: HID device
+ * @subcmd: query sub-command
+ * @buf: buffer to store the answer in
+ * @buf_size: size of @buf
+ *
+ * Windows re-sends the "ASUS Tech.Inc." unlock before every command
+ * burst, so do the same.
+ *
+ * Return: 0 on success, negative errno on failure.
+ */
+static int asus_xgm_query(struct hid_device *hdev, u8 subcmd,
+ u8 *buf, size_t buf_size)
+{
+ const u8 unlock[] = {
+ FEATURE_KBD_LED_REPORT_ID2,
+ 'A', 'S', 'U', 'S', ' ', 'T', 'e', 'c', 'h', '.', 'I', 'n', 'c', '.'
+ };
+ const u8 cmd[] = { FEATURE_KBD_LED_REPORT_ID2, ASUS_XGM_CMD_QUERY, subcmd };
+ int ret;
+
+ ret = asus_kbd_set_report(hdev, unlock, sizeof(unlock));
+ if (ret < 0)
+ return ret;
+
+ ret = asus_kbd_set_report(hdev, cmd, sizeof(cmd));
+ if (ret < 0)
+ return ret;
+
+ return asus_kbd_get_report(hdev, FEATURE_KBD_LED_REPORT_ID2, buf, buf_size);
+}
+
+/**
+ * asus_xgm_info_assign() - Copy an identity field up to its ';'
+ * terminator
+ * @dst: destination buffer
+ * @dst_len: size of @dst, including the trailing NUL
+ * @src: start of the field inside the identity string
+ * @src_len: number of bytes available at @src
+ *
+ * Copies up to @dst_len - 1 bytes from @src, stopping at the first ';'
+ * field separator, and NUL terminates @dst.
+ */
+static void asus_xgm_info_assign(char *dst, size_t dst_len,
+ const char *src, size_t src_len)
+{
+ const char *sep = strnchr(src, src_len, ';');
+ size_t n = sep ? (size_t)(sep - src) : src_len;
+
+ if (n >= dst_len)
+ n = dst_len - 1;
+
+ memcpy(dst, src, n);
+ dst[n] = '\0';
+}
+
+/**
+ * asus_xgm_take_field() - Copy one tagged field out of the identity string
+ * @dst: destination buffer
+ * @dst_len: size of @dst, including the trailing NUL
+ * @identity: identity string returned by the station
+ * @identity_len: number of bytes available at @identity
+ * @tag: field tag, including the colon
+ *
+ * A tag that is absent leaves @dst untouched.
+ */
+static void asus_xgm_take_field(char *dst, size_t dst_len,
+ const char *identity, size_t identity_len,
+ const char *tag)
+{
+ size_t tag_len = strlen(tag);
+ const char *found = strnstr(identity, tag, identity_len);
+ size_t value_off;
+
+ if (!found)
+ return;
+
+ value_off = (found - identity) + tag_len;
+ if (value_off > identity_len)
+ return;
+
+ asus_xgm_info_assign(dst, dst_len, identity + value_off,
+ identity_len - value_off);
+}
+
+/**
+ * asus_xgm_get_identity() - Query the identity of an XG Mobile station
+ * @hdev: HID device
+ * @info: identity structure to fill in
+ *
+ * The identity query answers with a NUL terminated string of the form
+ * "MB:<mainboard>;SN:<serial>;PRO:<model>;PNUM:<part number>;", which is
+ * split into the fields of @info. The model field is mandatory: a station
+ * that does not report one is treated as absent.
+ *
+ * Return: 0 on success, negative errno on failure.
+ */
+static int asus_xgm_get_identity(struct hid_device *hdev,
+ struct asus_xgm_info *info)
+{
+ size_t len = asus_report_len(hdev, FEATURE_KBD_LED_REPORT_ID2);
+ const char *payload;
+ size_t payload_len;
+ int ret;
+
+ if (!len)
+ return -EINVAL;
+
+ u8 *buf __free(kfree) = kzalloc(len, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+
+ ret = asus_xgm_query(hdev, ASUS_XGM_QUERY_IDENTITY, buf, len);
+ if (ret < 0)
+ return ret;
+
+ if (len <= ASUS_XGM_RESP_HDR_LEN ||
+ buf[ASUS_XGM_RESP_OPCODE] != ASUS_XGM_CMD_QUERY ||
+ buf[ASUS_XGM_RESP_SUBCMD] != ASUS_XGM_QUERY_IDENTITY)
+ return -ENODEV;
+
+ payload = (const char *)buf + ASUS_XGM_RESP_PAYLOAD;
+ payload_len = strnlen(payload, len - ASUS_XGM_RESP_HDR_LEN);
+ if (!payload_len)
+ return -ENODEV;
+
+ asus_xgm_take_field(info->mainboard, sizeof(info->mainboard),
+ payload, payload_len, ASUS_XGM_TAG_MAINBOARD);
+ asus_xgm_take_field(info->serial, sizeof(info->serial),
+ payload, payload_len, ASUS_XGM_TAG_SERIAL);
+ asus_xgm_take_field(info->model, sizeof(info->model),
+ payload, payload_len, ASUS_XGM_TAG_MODEL);
+ asus_xgm_take_field(info->part_number, sizeof(info->part_number),
+ payload, payload_len, ASUS_XGM_TAG_PART);
+
+ if (!info->model[0])
+ return -ENODEV;
+
+ return 0;
+}
+
+static const char * const asus_xgm_models[] = { "GC31", "GC32", "GC33" };
+
+/**
+ * asus_xgm_is_supported_model() - Check that a station is supported
+ * @info: identity of the station
+ *
+ * The supported families are the GC31, GC32 and GC33 stations.
+ *
+ * Return: true if the reported model belongs to one of the supported
+ * families, false otherwise.
+ */
+static bool asus_xgm_is_supported_model(const struct asus_xgm_info *info)
+{
+ for (size_t i = 0; i < ARRAY_SIZE(asus_xgm_models); i++)
+ if (strncmp(info->model, asus_xgm_models[i],
+ strlen(asus_xgm_models[i])) == 0)
+ return true;
+
+ return false;
+}
+
static int asus_xgm_led_set(struct led_classdev *led_cdev, enum led_brightness value)
{
const u8 buf[] = { FEATURE_KBD_LED_REPORT_ID2, 0xC5, (value) ? 0x50 : 0x00 };
@@ -5134,14 +5329,49 @@ static void asus_initialize_reports(struct hid_device *hdev)
static int asus_xgm_init(struct hid_device *hdev, struct asus_drvdata *drvdata)
{
+ struct asus_xgm_info *info;
const char *name;
- int ret;
+ int attempt, ret;
+
+ info = devm_kzalloc(&hdev->dev, sizeof(*info), GFP_KERNEL);
+ if (!info)
+ return -ENOMEM;
+
+ /*
+ * The station reports its identity through the query protocol: the
+ * reported model decides whether the LED interface is registered.
+ * The LED name is that model, then the bus identity. One missed
+ * transfer used to leave that LED unregistered, so try three times
+ * and wait a second between attempts.
+ */
+ for (attempt = 0; attempt < 3; attempt++) {
+ ret = asus_xgm_get_identity(hdev, info);
+ if (!ret)
+ break;
+ if (attempt < 2)
+ ssleep(1);
+ }
+ if (ret < 0) {
+ hid_warn(hdev, "Failed to query XG Mobile identity: %d\n", ret);
+ return 0;
+ }
+
+ hid_info(hdev, "XG Mobile: model %s, serial %s\n",
+ info->model, info->serial);
+
+ if (!asus_xgm_is_supported_model(info)) {
+ hid_warn(hdev, "Unsupported XG Mobile model %s\n", info->model);
+ return 0;
+ }
+
+ drvdata->xgm_info = info;
drvdata->xgm_led = devm_kzalloc(&hdev->dev, sizeof(*drvdata->xgm_led), GFP_KERNEL);
if (drvdata->xgm_led == NULL)
return -ENOMEM;
- name = devm_kasprintf(&hdev->dev, GFP_KERNEL, "asus:xgm-%s:led",
+ name = devm_kasprintf(&hdev->dev, GFP_KERNEL, "asus:xgm-%s-%s:led",
+ info->model,
strlen(hdev->uniq) ? hdev->uniq : dev_name(&hdev->dev));
if (name == NULL) {
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH v13 14/18] HID: asus: expose XG Mobile identity and firmware information
2026-11-04 1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
` (12 preceding siblings ...)
2026-11-04 1:39 ` [PATCH v13 13/18] HID: asus: identify XG Mobile stations and bind the LED to the reported model Denis Benato
@ 2026-11-04 1:39 ` Denis Benato
2026-11-04 1:39 ` [PATCH v13 15/18] HID: asus: document ROG XG Mobile sysfs attributes Denis Benato
` (3 subsequent siblings)
17 siblings, 0 replies; 25+ messages in thread
From: Denis Benato @ 2026-11-04 1:39 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
The identity query also reports the mainboard part number, the serial
number and the part number of the station, and the 0xCE 0x03 query
answers with an 8-byte binary blob followed by "tag, length, data"
fields where GEN holds the station generation and VER the firmware
version.
Collect this information at probe time and expose it read-only on the
HID device:
/sys/bus/hid/devices/<bus>:<addr>.<intf>/xgm_model (PRO)
.../xgm_serial (SN)
.../xgm_mainboard (MB)
.../xgm_part_number (PNUM)
.../xgm_firmware_version (VER)
.../xgm_generation (GEN)
Signed-off-by: Denis Benato <denis.benato@linux.dev>
---
drivers/hid/hid-asus.c | 188 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 186 insertions(+), 2 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index eb9dd69fd605..a24c64c86b5f 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -73,6 +73,7 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
/* XG Mobile station protocol on FEATURE_KBD_LED_REPORT_ID2. */
#define ASUS_XGM_CMD_QUERY 0xce
#define ASUS_XGM_QUERY_IDENTITY 0x01
+#define ASUS_XGM_QUERY_VERSION 0x03
#define ASUS_XGM_INFO_LEN 32
/* Identity string fields, each "TAG:value;" inside the query answer. */
@@ -80,6 +81,12 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define ASUS_XGM_TAG_SERIAL "SN:"
#define ASUS_XGM_TAG_MODEL "PRO:"
#define ASUS_XGM_TAG_PART "PNUM:"
+#define ASUS_XGM_TAG_GENERATION "GEN"
+#define ASUS_XGM_TAG_FIRMWARE "VER"
+
+/* A version field is a 3-character tag, one length byte, then the data. */
+#define ASUS_XGM_TLV_TAG_LEN 3
+#define ASUS_XGM_TLV_HDR_LEN (ASUS_XGM_TLV_TAG_LEN + 1)
/*
* Every station answer echoes the report id, the opcode and the
@@ -245,6 +252,8 @@ struct asus_xgm_info {
char serial[ASUS_XGM_INFO_LEN];
char model[ASUS_XGM_INFO_LEN];
char part_number[ASUS_XGM_INFO_LEN];
+ char firmware_version[ASUS_XGM_INFO_LEN];
+ char generation[ASUS_XGM_INFO_LEN];
};
struct asus_touchpad_info {
@@ -5063,6 +5072,153 @@ static bool asus_xgm_is_supported_model(const struct asus_xgm_info *info)
return false;
}
+/**
+ * asus_xgm_tlv_str() - Extract a "tag, length, data" field
+ * @dst: destination buffer
+ * @dst_len: size of @dst, including the trailing NUL
+ * @buf: answer of a version query
+ * @len: size of @buf
+ * @tag: three-character tag to look for
+ *
+ * Return: 0 on success, -ENOENT if @tag is missing or its data does not
+ * fit in @dst.
+ */
+static int asus_xgm_tlv_str(char *dst, size_t dst_len, const u8 *buf,
+ size_t len, const char *tag)
+{
+ const u8 *p = buf + ASUS_XGM_RESP_PAYLOAD;
+ const u8 *end = buf + len;
+ u8 n;
+
+ /*
+ * The payload starts with a binary prefix and then the fields.
+ * The station pads between fields with 0xff, so the tag is sought
+ * at every byte rather than walked as a packed list.
+ */
+ while (p + ASUS_XGM_TLV_HDR_LEN <= end) {
+ if (!memcmp(p, tag, ASUS_XGM_TLV_TAG_LEN)) {
+ n = p[ASUS_XGM_TLV_TAG_LEN];
+
+ if (n >= dst_len ||
+ end - (p + ASUS_XGM_TLV_HDR_LEN) < n)
+ return -ENOENT;
+
+ memcpy(dst, p + ASUS_XGM_TLV_HDR_LEN, n);
+ dst[n] = '\0';
+ return 0;
+ }
+
+ p++;
+ }
+
+ return -ENOENT;
+}
+
+/**
+ * asus_xgm_get_version() - Query the firmware version of a station
+ * @hdev: HID device
+ * @info: identity structure whose generation and firmware_version fields
+ * get filled in
+ *
+ * Return: 0 on success, negative errno on failure.
+ */
+static int asus_xgm_get_version(struct hid_device *hdev,
+ struct asus_xgm_info *info)
+{
+ size_t len = asus_report_len(hdev, FEATURE_KBD_LED_REPORT_ID2);
+ int ret;
+
+ if (!len)
+ return -EINVAL;
+
+ u8 *buf __free(kfree) = kzalloc(len, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+
+ ret = asus_xgm_query(hdev, ASUS_XGM_QUERY_VERSION, buf, len);
+ if (ret < 0)
+ return ret;
+
+ if (len <= ASUS_XGM_RESP_HDR_LEN ||
+ buf[ASUS_XGM_RESP_OPCODE] != ASUS_XGM_CMD_QUERY ||
+ buf[ASUS_XGM_RESP_SUBCMD] != ASUS_XGM_QUERY_VERSION)
+ return -ENODEV;
+
+ asus_xgm_tlv_str(info->generation, sizeof(info->generation),
+ buf, len, ASUS_XGM_TAG_GENERATION);
+ asus_xgm_tlv_str(info->firmware_version, sizeof(info->firmware_version),
+ buf, len, ASUS_XGM_TAG_FIRMWARE);
+
+ return 0;
+}
+
+/**
+ * asus_xgm_drvdata() - Get the drvdata behind an XG Mobile attribute
+ * @dev: HID device the attribute is attached to
+ *
+ * Return: the asus_drvdata of @dev.
+ */
+static struct asus_drvdata *asus_xgm_drvdata(struct device *dev)
+{
+ return hid_get_drvdata(to_hid_device(dev));
+}
+
+static ssize_t xgm_model_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%s\n", asus_xgm_drvdata(dev)->xgm_info->model);
+}
+static DEVICE_ATTR_RO(xgm_model);
+
+static ssize_t xgm_serial_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%s\n", asus_xgm_drvdata(dev)->xgm_info->serial);
+}
+static DEVICE_ATTR_RO(xgm_serial);
+
+static ssize_t xgm_mainboard_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%s\n", asus_xgm_drvdata(dev)->xgm_info->mainboard);
+}
+static DEVICE_ATTR_RO(xgm_mainboard);
+
+static ssize_t xgm_part_number_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%s\n", asus_xgm_drvdata(dev)->xgm_info->part_number);
+}
+static DEVICE_ATTR_RO(xgm_part_number);
+
+static ssize_t xgm_firmware_version_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%s\n", asus_xgm_drvdata(dev)->xgm_info->firmware_version);
+}
+static DEVICE_ATTR_RO(xgm_firmware_version);
+
+static ssize_t xgm_generation_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%s\n", asus_xgm_drvdata(dev)->xgm_info->generation);
+}
+static DEVICE_ATTR_RO(xgm_generation);
+
+static struct attribute *asus_xgm_attrs[] = {
+ &dev_attr_xgm_model.attr,
+ &dev_attr_xgm_serial.attr,
+ &dev_attr_xgm_mainboard.attr,
+ &dev_attr_xgm_part_number.attr,
+ &dev_attr_xgm_firmware_version.attr,
+ &dev_attr_xgm_generation.attr,
+ NULL
+};
+
+static const struct attribute_group asus_xgm_attr_group = {
+ .attrs = asus_xgm_attrs,
+};
+
static int asus_xgm_led_set(struct led_classdev *led_cdev, enum led_brightness value)
{
const u8 buf[] = { FEATURE_KBD_LED_REPORT_ID2, 0xC5, (value) ? 0x50 : 0x00 };
@@ -5366,9 +5522,29 @@ static int asus_xgm_init(struct hid_device *hdev, struct asus_drvdata *drvdata)
drvdata->xgm_info = info;
+ ret = asus_xgm_get_version(hdev, info);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to query XG Mobile version: %d\n", ret);
+ else
+ hid_info(hdev, "XG Mobile: firmware %s, generation %s\n",
+ info->firmware_version, info->generation);
+
+ /*
+ * The attribute group is not devm-managed: a creation failure is
+ * fatal and unwound through asus_xgm_init_err, while on the unbind
+ * path the removal is gated on xgm_info exactly like the creation.
+ */
+ ret = sysfs_create_group(&hdev->dev.kobj, &asus_xgm_attr_group);
+ if (ret) {
+ hid_err(hdev, "Failed to create XG Mobile attributes: %d\n", ret);
+ goto asus_xgm_init_err;
+ }
+
drvdata->xgm_led = devm_kzalloc(&hdev->dev, sizeof(*drvdata->xgm_led), GFP_KERNEL);
- if (drvdata->xgm_led == NULL)
- return -ENOMEM;
+ if (!drvdata->xgm_led) {
+ ret = -ENOMEM;
+ goto asus_xgm_init_err;
+ }
name = devm_kasprintf(&hdev->dev, GFP_KERNEL, "asus:xgm-%s-%s:led",
info->model,
@@ -5401,6 +5577,8 @@ static int asus_xgm_init(struct hid_device *hdev, struct asus_drvdata *drvdata)
return 0;
asus_xgm_init_err:
+ sysfs_remove_group(&hdev->dev.kobj, &asus_xgm_attr_group);
+ drvdata->xgm_info = NULL;
drvdata->xgm_led = NULL;
return ret;
}
@@ -5617,6 +5795,9 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
if (drvdata->listener.brightness_set)
asus_hid_unregister_listener(&drvdata->listener);
+ if (drvdata->xgm_info)
+ sysfs_remove_group(&hdev->dev.kobj, &asus_xgm_attr_group);
+
if (drvdata->xgm_led)
devm_led_classdev_unregister(&hdev->dev, &drvdata->xgm_led->cdev);
@@ -5641,6 +5822,9 @@ static void asus_remove(struct hid_device *hdev)
if (drvdata->listener.brightness_set)
asus_hid_unregister_listener(&drvdata->listener);
+ if (drvdata->xgm_info)
+ sysfs_remove_group(&hdev->dev.kobj, &asus_xgm_attr_group);
+
if (drvdata->xgm_led)
devm_led_classdev_unregister(&hdev->dev, &drvdata->xgm_led->cdev);
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH v13 15/18] HID: asus: document ROG XG Mobile sysfs attributes
2026-11-04 1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
` (13 preceding siblings ...)
2026-11-04 1:39 ` [PATCH v13 14/18] HID: asus: expose XG Mobile identity and firmware information Denis Benato
@ 2026-11-04 1:39 ` Denis Benato
2026-11-04 1:39 ` [PATCH v13 16/18] HID: asus: document ROG Ally " Denis Benato
` (2 subsequent siblings)
17 siblings, 0 replies; 25+ messages in thread
From: Denis Benato @ 2026-11-04 1:39 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
The driver publishes the station's model, serial, mainboard identifier,
part number, firmware version and hardware generation as read-only
sysfs files. Document each file: when it is created, what a read
returns, and when that read is an empty line.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
---
.../ABI/testing/sysfs-driver-hid-asus | 95 +++++++++++++++++++
1 file changed, 95 insertions(+)
create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-asus
diff --git a/Documentation/ABI/testing/sysfs-driver-hid-asus b/Documentation/ABI/testing/sysfs-driver-hid-asus
new file mode 100644
index 000000000000..30e0d17d4cec
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-driver-hid-asus
@@ -0,0 +1,95 @@
+What: /sys/bus/hid/drivers/asus/<dev>/xgm_model
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Model of the connected ASUS ROG XG Mobile station, as
+ reported by the station. An example is "GC33Y".
+
+ The file is created only for a supported station, one whose
+ model name starts with GC31, GC32 or GC33, and only while that
+ station is bound.
+ The value is one line and is never empty while the file
+ exists.
+
+ Access: Read only
+ Valid values: a single line of text
+
+What: /sys/bus/hid/drivers/asus/<dev>/xgm_serial
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Serial number of the connected ASUS ROG XG Mobile station,
+ as reported by the station.
+
+ The file is created only for a supported station, one whose
+ model name starts with GC31, GC32 or GC33, and only while that
+ station is bound.
+ The read is an empty line when the station did not report a
+ serial number.
+
+ Access: Read only
+ Valid values: a single line of text
+
+What: /sys/bus/hid/drivers/asus/<dev>/xgm_mainboard
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Mainboard identifier of the connected ASUS ROG XG Mobile
+ station, as reported by the station.
+
+ The file is created only for a supported station, one whose
+ model name starts with GC31, GC32 or GC33, and only while that
+ station is bound.
+ The read is an empty line when the station did not report a
+ mainboard identifier.
+
+ Access: Read only
+ Valid values: a single line of text
+
+What: /sys/bus/hid/drivers/asus/<dev>/xgm_part_number
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Part number of the connected ASUS ROG XG Mobile station, as
+ reported by the station.
+
+ The file is created only for a supported station, one whose
+ model name starts with GC31, GC32 or GC33, and only while that
+ station is bound.
+ The read is an empty line when the station did not report a
+ part number.
+
+ Access: Read only
+ Valid values: a single line of text
+
+What: /sys/bus/hid/drivers/asus/<dev>/xgm_firmware_version
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Firmware version of the connected ASUS ROG XG Mobile
+ station, as reported by the station.
+
+ The file is created only for a supported station, one whose
+ model name starts with GC31, GC32 or GC33, and only while that
+ station is bound.
+ The read is an empty line when the version query failed or
+ the station did not report a firmware version.
+
+ Access: Read only
+ Valid values: a single line of text
+
+What: /sys/bus/hid/drivers/asus/<dev>/xgm_generation
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Hardware generation of the connected ASUS ROG XG Mobile
+ station, as reported by the station.
+
+ The file is created only for a supported station, one whose
+ model name starts with GC31, GC32 or GC33, and only while that
+ station is bound.
+ The read is an empty line when the version query failed or
+ the station did not report a generation.
+
+ Access: Read only
+ Valid values: a single line of text
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH v13 16/18] HID: asus: document ROG Ally sysfs attributes
2026-11-04 1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
` (14 preceding siblings ...)
2026-11-04 1:39 ` [PATCH v13 15/18] HID: asus: document ROG XG Mobile sysfs attributes Denis Benato
@ 2026-11-04 1:39 ` Denis Benato
2026-11-04 1:55 ` sashiko-bot
2026-11-04 1:39 ` [PATCH v13 17/18] platform/x86: asus-armoury: pick the eGPU activation from the reported GPU Denis Benato
2026-11-04 1:39 ` [PATCH v13 18/18] MAINTAINERS: add maintainer for the ASUS HID driver Denis Benato
17 siblings, 1 reply; 25+ messages in thread
From: Denis Benato @ 2026-11-04 1:39 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
The driver publishes the gamepad configuration of the ROG Ally
handhelds through sysfs: the controller emulation and the
operating mode, the per-motor vibration strength, the per-axis
joystick calibration (dead zone, outer threshold, anti-dead
zone and response curve), the trigger activation ranges, and
the per-button mapping, macro and turbo settings.
Document each file: where it is created, what a read returns,
which values a write accepts, and the error returned when the
controller lacks the capability.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
---
.../ABI/testing/sysfs-driver-hid-asus | 389 ++++++++++++++++++
1 file changed, 389 insertions(+)
diff --git a/Documentation/ABI/testing/sysfs-driver-hid-asus b/Documentation/ABI/testing/sysfs-driver-hid-asus
index 30e0d17d4cec..515eb7c550fc 100644
--- a/Documentation/ABI/testing/sysfs-driver-hid-asus
+++ b/Documentation/ABI/testing/sysfs-driver-hid-asus
@@ -93,3 +93,392 @@ Description:
Access: Read only
Valid values: a single line of text
+
+What: /sys/bus/hid/drivers/asus/<dev>/xbox_controller
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Enable or disable the Xbox controller emulation of the
+ gamepad.
+
+ The file is created on the configuration interface of a
+ bound ASUS ROG Ally or ROG Ally X controller. A read
+ returns 1 when the emulation is enabled and 0 when it
+ is disabled; the emulation is enabled by default.
+
+ Reading and writing return -EOPNOTSUPP when the
+ controller does not report the capability.
+
+ Access: Read/Write
+ Valid values: 0, 1
+
+What: /sys/bus/hid/drivers/asus/<dev>/gamepad_mode
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Operating mode of the gamepad. In "gamepad" mode the
+ buttons report gamepad codes, in "desktop" mode they
+ report keyboard, mouse and media codes. The default
+ mode is gamepad.
+
+ The file is created on the configuration interface of a
+ bound ASUS ROG Ally or ROG Ally X controller. A switch
+ asks the controller to change mode and resets the
+ recorded button mappings to the firmware defaults of
+ the new mode, so the remap, macro and remap_available
+ files report those defaults afterwards.
+
+ Access: Read/Write
+ Valid values: "gamepad", "desktop"
+
+What: /sys/bus/hid/drivers/asus/<dev>/gamepad_mode_index
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ List of the operating modes the gamepad_mode file
+ accepts, space separated.
+
+ The file is created on the configuration interface of a
+ bound ASUS ROG Ally or ROG Ally X controller.
+
+ Access: Read only
+ Valid values: "gamepad desktop"
+
+What: /sys/bus/hid/drivers/asus/<dev>/left_vibration/intensity
+ /sys/bus/hid/drivers/asus/<dev>/right_vibration/intensity
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Strength of the left or the right rumble motor, in
+ percent of the maximum intensity. The controller does
+ not keep the strength across a reset: the driver sends
+ the recorded values again when it re-initializes the
+ controller.
+
+ The files are created on the configuration interface of
+ a bound ASUS ROG Ally or ROG Ally X controller. Both
+ motors default to 100 and a write is rejected when the
+ value is above 100.
+
+ Access: Read/Write
+ Valid values: an unsigned number 0..100
+
+What: /sys/bus/hid/drivers/asus/<dev>/left_vibration/intensity_range
+ /sys/bus/hid/drivers/asus/<dev>/right_vibration/intensity_range
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Range of the values the sibling intensity file accepts.
+
+ The files are created on the configuration interface of
+ a bound ASUS ROG Ally or ROG Ally X controller.
+
+ Access: Read only
+ Valid values: "0 100"
+
+What: /sys/bus/hid/drivers/asus/<dev>/left_joystick_axis/inner_threshold
+ /sys/bus/hid/drivers/asus/<dev>/right_joystick_axis/inner_threshold
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Size of the inner dead zone of the joystick, in percent
+ of the axis range. The default is 10.
+
+ The files are created on the configuration interface of
+ a bound ASUS ROG Ally or ROG Ally X controller. Reading
+ and writing return -EOPNOTSUPP when the controller does
+ not report user calibration support.
+
+ Access: Read/Write
+ Valid values: an unsigned number 0..50
+
+What: /sys/bus/hid/drivers/asus/<dev>/left_joystick_axis/outer_threshold
+ /sys/bus/hid/drivers/asus/<dev>/right_joystick_axis/outer_threshold
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Outer threshold of the joystick, in percent of the axis
+ range: at and beyond this deflection the joystick
+ reports full deflection. The default is 90.
+
+ The files are created on the configuration interface of
+ a bound ASUS ROG Ally or ROG Ally X controller. Reading
+ and writing return -EOPNOTSUPP when the controller does
+ not report user calibration support.
+
+ Access: Read/Write
+ Valid values: an unsigned number 70..100
+
+What: /sys/bus/hid/drivers/asus/<dev>/left_joystick_axis/inner_threshold_range
+ /sys/bus/hid/drivers/asus/<dev>/right_joystick_axis/inner_threshold_range
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Range of the values the sibling inner_threshold file
+ accepts.
+
+ The files are created on the configuration interface of
+ a bound ASUS ROG Ally or ROG Ally X controller.
+
+ Access: Read only
+ Valid values: "0 50"
+
+What: /sys/bus/hid/drivers/asus/<dev>/left_joystick_axis/outer_threshold_range
+ /sys/bus/hid/drivers/asus/<dev>/right_joystick_axis/outer_threshold_range
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Range of the values the sibling outer_threshold file
+ accepts.
+
+ The files are created on the configuration interface of
+ a bound ASUS ROG Ally or ROG Ally X controller.
+
+ Access: Read only
+ Valid values: "70 100"
+
+What: /sys/bus/hid/drivers/asus/<dev>/left_joystick_axis/anti_deadzone
+ /sys/bus/hid/drivers/asus/<dev>/right_joystick_axis/anti_deadzone
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Anti-dead zone of the joystick, in percent of the axis
+ range: the deflection the controller applies to
+ counteract the inner dead zone. The default is 0.
+
+ The files are created on the configuration interface of
+ a bound ASUS ROG Ally or ROG Ally X controller. Reading
+ and writing return -EOPNOTSUPP when the controller does
+ not report anti-dead zone support.
+
+ Access: Read/Write
+ Valid values: an unsigned number 0..100
+
+What: /sys/bus/hid/drivers/asus/<dev>/left_joystick_axis/anti_deadzone_range
+ /sys/bus/hid/drivers/asus/<dev>/right_joystick_axis/anti_deadzone_range
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Range of the values the sibling anti_deadzone file
+ accepts.
+
+ The files are created on the configuration interface of
+ a bound ASUS ROG Ally or ROG Ally X controller.
+
+ Access: Read only
+ Valid values: "0 100"
+
+What: /sys/bus/hid/drivers/asus/<dev>/left_joystick_axis/response_curve
+ /sys/bus/hid/drivers/asus/<dev>/right_joystick_axis/response_curve
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Response curve of the joystick, as four points. Each
+ point is a pair: a move percentage, the deflection at
+ which the point is placed, and a response percentage,
+ applied from that point on.
+
+ The write is all-or-nothing: it is rejected when any
+ value is out of range or the move percentages are not
+ strictly increasing, and the recorded curve is left
+ untouched. The default is the linear curve
+ "0 0 33 33 66 66 100 100".
+
+ The files are created on the configuration interface of
+ a bound ASUS ROG Ally or ROG Ally X controller. Reading
+ and writing return -EOPNOTSUPP when the controller does
+ not report response curve support.
+
+ Access: Read/Write
+ Valid values: eight unsigned numbers 0..100, the move
+ ones strictly increasing
+
+What: /sys/bus/hid/drivers/asus/<dev>/left_joystick_axis/response_curve_index
+ /sys/bus/hid/drivers/asus/<dev>/right_joystick_axis/response_curve_index
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Explanation of the format of the sibling response_curve
+ file.
+
+ The files are created on the configuration interface of
+ a bound ASUS ROG Ally or ROG Ally X controller.
+
+ Access: Read only
+ Valid values: a single line of text
+
+What: /sys/bus/hid/drivers/asus/<dev>/left_trigger/range_lower_limit
+ /sys/bus/hid/drivers/asus/<dev>/right_trigger/range_lower_limit
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Lower limit of the analog trigger activation range, in
+ percent of the trigger travel: below this deflection
+ the trigger reports zero. The default is 0.
+
+ The files are created on the configuration interface of
+ a bound ASUS ROG Ally or ROG Ally X controller. Reading
+ and writing return -EOPNOTSUPP when the controller does
+ not report user calibration support.
+
+ Access: Read/Write
+ Valid values: an unsigned number 0..50
+
+What: /sys/bus/hid/drivers/asus/<dev>/left_trigger/range_upper_limit
+ /sys/bus/hid/drivers/asus/<dev>/right_trigger/range_upper_limit
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Upper limit of the analog trigger activation range, in
+ percent of the trigger travel: at and beyond this
+ deflection the trigger reports full deflection. The
+ default is 100.
+
+ The files are created on the configuration interface of
+ a bound ASUS ROG Ally or ROG Ally X controller. Reading
+ and writing return -EOPNOTSUPP when the controller does
+ not report user calibration support.
+
+ Access: Read/Write
+ Valid values: an unsigned number 70..100
+
+What: /sys/bus/hid/drivers/asus/<dev>/left_trigger/range_lower_limit_range
+ /sys/bus/hid/drivers/asus/<dev>/right_trigger/range_lower_limit_range
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Range of the values the sibling range_lower_limit file
+ accepts.
+
+ The files are created on the configuration interface of
+ a bound ASUS ROG Ally or ROG Ally X controller.
+
+ Access: Read only
+ Valid values: "0 50"
+
+What: /sys/bus/hid/drivers/asus/<dev>/left_trigger/range_upper_limit_range
+ /sys/bus/hid/drivers/asus/<dev>/right_trigger/range_upper_limit_range
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Range of the values the sibling range_upper_limit file
+ accepts.
+
+ The files are created on the configuration interface of
+ a bound ASUS ROG Ally or ROG Ally X controller.
+
+ Access: Read only
+ Valid values: "70 100"
+
+What: /sys/bus/hid/drivers/asus/<dev>/<btn>/remap
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Target the button is remapped to, by name. A write
+ sends the mapping to the controller and is rejected
+ when the name is unknown or not valid for the button
+ in the current mode; the recorded mapping is restored
+ when the controller rejects it.
+
+ "NONE" is always valid. In "gamepad" mode only gamepad
+ codes are valid, in "desktop" mode only keyboard,
+ mouse and media codes are; btn_m1 and btn_m2 accept
+ every code.
+
+ The file is created in one directory per button of the
+ configuration interface of a bound ASUS ROG Ally or
+ ROG Ally X controller. <btn> is one of: btn_a, btn_b,
+ btn_x, btn_y, btn_lb, btn_rb, dpad_up, dpad_down,
+ dpad_left, dpad_right, btn_l3, btn_r3, btn_menu,
+ btn_view, btn_m1 and btn_m2.
+
+ Access: Read/Write
+ Valid values: one name from the sibling
+ remap_available file
+
+What: /sys/bus/hid/drivers/asus/<dev>/<btn>/macro
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Macro target of the button, by name. The macro target
+ is not restricted by the controller mode: every name
+ the controller knows is valid for every button.
+
+ The file is created in the <btn> directories listed
+ for the remap file.
+
+ Access: Read/Write
+ Valid values: one name; the btn_m1 and btn_m2
+ remap_available files list them all
+
+What: /sys/bus/hid/drivers/asus/<dev>/<btn>/remap_available
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Names the remap file of this button accepts in the
+ current mode, space separated. The btn_m1 and btn_m2
+ files list every known name in every mode.
+
+ The file is created in the <btn> directories listed
+ for the remap file.
+
+ Access: Read only
+ Valid values: a single line of text
+
+What: /sys/bus/hid/drivers/asus/<dev>/<btn>/turbo_period
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Repeat period of the turbo function of the button, in
+ firmware units: while turbo is on, the button is
+ automatically pressed again at this period. The period
+ 0, the default, disables the function.
+
+ The file is created in the <btn> directories listed
+ for the remap file, only when the controller reports
+ turbo support.
+
+ Access: Read/Write
+ Valid values: an unsigned number 0..20
+
+What: /sys/bus/hid/drivers/asus/<dev>/<btn>/toggle_period
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Toggle period of the turbo function of the button, in
+ firmware units.
+
+ The file is created in the <btn> directories listed
+ for the remap file, only when the controller reports
+ turbo support.
+
+ Access: Read/Write
+ Valid values: an unsigned number 0..255
+
+What: /sys/bus/hid/drivers/asus/<dev>/<btn>/turbo_period_range
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Range of the values the sibling turbo_period file
+ accepts.
+
+ The file is created in the <btn> directories listed
+ for the remap file, only when the controller reports
+ turbo support.
+
+ Access: Read only
+ Valid values: "0 20"
+
+What: /sys/bus/hid/drivers/asus/<dev>/<btn>/toggle_period_range
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Range of the values the sibling toggle_period file
+ accepts.
+
+ The file is created in the <btn> directories listed
+ for the remap file, only when the controller reports
+ turbo support.
+
+ Access: Read only
+ Valid values: "0 255"
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* Re: [PATCH v13 16/18] HID: asus: document ROG Ally sysfs attributes
2026-11-04 1:39 ` [PATCH v13 16/18] HID: asus: document ROG Ally " Denis Benato
@ 2026-11-04 1:55 ` sashiko-bot
0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-11-04 1:55 UTC (permalink / raw)
To: Denis Benato; +Cc: linux-input, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] HID: asus: inverted sysfs_streq() check in find_button_by_name()
- [Low] HID: asus: response_curve_index ABI doc mismatches multi-line output
--
commit 82b20f9d7b18cb0817f799140de2a8d33e0549b4
Author: Denis Benato <denis.benato@linux.dev>
HID: asus: document ROG Ally sysfs attributes
This commit documents the sysfs attributes exposed by hid-asus for configuring
the ASUS ROG Ally and ROG Ally X gamepad controller, including emulation mode,
vibration, joystick calibration, trigger limits, and button mappings.
> diff --git a/Documentation/ABI/testing/sysfs-driver-hid-asus b/Documentation/ABI/testing/sysfs-driver-hid-asus
> index 30e0d17d4cecc..515eb7c550fc5 100644
> --- a/Documentation/ABI/testing/sysfs-driver-hid-asus
> +++ b/Documentation/ABI/testing/sysfs-driver-hid-asus
> @@ -93,3 +93,392 @@ Description:
>
> Access: Read only
> Valid values: a single line of text
> +
[ ... ]
> +What: /sys/bus/hid/drivers/asus/<dev>/left_joystick_axis/response_curve_index
> + /sys/bus/hid/drivers/asus/<dev>/right_joystick_axis/response_curve_index
> +Date: October 2026
> +Contact: linux-input@vger.kernel.org
> +Description:
> + Explanation of the format of the sibling response_curve
> + file.
> +
> + The files are created on the configuration interface of
> + a bound ASUS ROG Ally or ROG Ally X controller.
> +
> + Access: Read only
> + Valid values: a single line of text
[Severity: Low]
Does response_curve_index actually return a single line of text?
In drivers/hid/hid-asus.c, response_curve_index_show() emits six lines of text
with embedded newlines:
drivers/hid/hid-asus.c:response_curve_index_show() {
return sysfs_emit(buf,
"move_1 resp_1 move_2 resp_2 move_3 resp_3 move_4 resp_4\n"
"\n"
"move_N: joystick deflection percentage at which curve point N\n"
"is placed (0-100, each move_N must be greater than the\n"
"previous one). resp_N: response percentage applied from\n"
"point N on (0-100).\n");
}
Should this entry describe the multi-line output, or should
response_curve_index_show() emit a single line?
[ ... ]
> +What: /sys/bus/hid/drivers/asus/<dev>/<btn>/remap
> +Date: October 2026
> +Contact: linux-input@vger.kernel.org
> +Description:
> + Target the button is remapped to, by name. A write
> + sends the mapping to the controller and is rejected
> + when the name is unknown or not valid for the button
> + in the current mode; the recorded mapping is restored
> + when the controller rejects it.
> +
> + "NONE" is always valid. In "gamepad" mode only gamepad
> + codes are valid, in "desktop" mode only keyboard,
> + mouse and media codes are; btn_m1 and btn_m2 accept
> + every code.
> +
> + The file is created in one directory per button of the
> + configuration interface of a bound ASUS ROG Ally or
> + ROG Ally X controller. <btn> is one of: btn_a, btn_b,
> + btn_x, btn_y, btn_lb, btn_rb, dpad_up, dpad_down,
> + dpad_left, dpad_right, btn_l3, btn_r3, btn_menu,
> + btn_view, btn_m1 and btn_m2.
> +
> + Access: Read/Write
> + Valid values: one name from the sibling
> + remap_available file
[Severity: High]
Does the driver implementation match this documented behavior for <btn>/remap
and <btn>/macro?
In drivers/hid/hid-asus.c, find_button_by_name() negates sysfs_streq():
drivers/hid/hid-asus.c:find_button_by_name() {
for (i = 0; i < ARRAY_SIZE(ally_btn_codes); i++) {
if (!sysfs_streq(ally_btn_codes[i].name, name))
return &ally_btn_codes[i];
}
return NULL;
}
Since sysfs_streq() returns true when the strings match, !sysfs_streq()
returns the first entry in ally_btn_codes[] whose name does not match name.
Because ally_btn_codes[0] is "NONE" and ally_btn_codes[1] is "BTN_SOUTH",
calling find_button_by_name("NONE") returns "BTN_SOUTH", while calling it with
any other string (including unknown names in button_remap_store()) matches
index 0 and returns "NONE" instead of NULL.
Consequently, ally_set_button_pair_mappings_default() initializes every button
and macro slot to "BTN_SOUTH", ally_set_default_gamepad_mapping() and
ally_set_default_keyboard_mapping() set explicitly mapped buttons to "NONE",
and writes to <btn>/remap or <btn>/macro never reject unknown names with
-EINVAL.
Should the negation in find_button_by_name() be removed?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261104013935.512713-1-denis.benato@linux.dev?part=16
^ permalink raw reply [flat|nested] 25+ messages in thread
* [PATCH v13 17/18] platform/x86: asus-armoury: pick the eGPU activation from the reported GPU
2026-11-04 1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
` (15 preceding siblings ...)
2026-11-04 1:39 ` [PATCH v13 16/18] HID: asus: document ROG Ally " Denis Benato
@ 2026-11-04 1:39 ` Denis Benato
2026-11-04 1:39 ` [PATCH v13 18/18] MAINTAINERS: add maintainer for the ASUS HID driver Denis Benato
17 siblings, 0 replies; 25+ messages in thread
From: Denis Benato @ 2026-11-04 1:39 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
The egpu_enable attribute used to take four values. The WMI method
returned 1 for every one of them, so that return never said which
activation had been used. The extra values stayed while the right
choice was still unknown. It is known now: 0x0001 is NVIDIA, 0x0101
is AMD and 0x0201 is Intel, and a wrong value must not be sent. The
tools that drive this attribute only write 1, so the AMD and Intel
values were unused unless someone requested them by hand.
Writing 0 turns the eGPU off and writing 1 turns it on. The value
sent for 1 is the one that matches the GPU vendor of the connected
station, or NVIDIA when no vendor has been reported.
hid-asus reads that vendor from the station version query and reports
it through asus-wmi. Removing the station stores 0, which restores
the default. One mutex serializes the report. The connector is
proprietary, so only one XG Mobile can be attached, and one vendor
word is the whole state. The armoury store reads it while holding
egpu_mutex, in the same section as the WMI call.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
---
drivers/hid/hid-asus.c | 38 ++++++++++++++-
drivers/platform/x86/asus-armoury.c | 57 +++++++++++++---------
drivers/platform/x86/asus-wmi.c | 43 ++++++++++++++++
include/linux/platform_data/x86/asus-wmi.h | 11 +++++
4 files changed, 125 insertions(+), 24 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index a24c64c86b5f..4af4c897cb05 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -88,6 +88,20 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define ASUS_XGM_TLV_TAG_LEN 3
#define ASUS_XGM_TLV_HDR_LEN (ASUS_XGM_TLV_TAG_LEN + 1)
+/*
+ * Four PCI IDs at the start of the version payload, high byte first:
+ * vendor, device, then the two subsystem IDs.
+ */
+enum {
+ ASUS_XGM_PCI_VENDOR,
+ ASUS_XGM_PCI_DEVICE,
+ ASUS_XGM_PCI_SUBSYS0,
+ ASUS_XGM_PCI_SUBSYS1,
+ ASUS_XGM_PCI_ID_COUNT,
+};
+
+#define ASUS_XGM_PCI_ID_LEN (ASUS_XGM_PCI_ID_COUNT * sizeof(u16))
+
/*
* Every station answer echoes the report id, the opcode and the
* subcommand, then the payload.
@@ -254,6 +268,7 @@ struct asus_xgm_info {
char part_number[ASUS_XGM_INFO_LEN];
char firmware_version[ASUS_XGM_INFO_LEN];
char generation[ASUS_XGM_INFO_LEN];
+ u16 pci_id[ASUS_XGM_PCI_ID_COUNT];
};
struct asus_touchpad_info {
@@ -5149,6 +5164,18 @@ static int asus_xgm_get_version(struct hid_device *hdev,
asus_xgm_tlv_str(info->firmware_version, sizeof(info->firmware_version),
buf, len, ASUS_XGM_TAG_FIRMWARE);
+ /*
+ * The IDs are stored high byte first. On a captured GC33Y the
+ * payload begins 10 de 27 17 21 fb 10 43: NVIDIA 0x10de, device
+ * 0x2717, 0x21fb and ASUS 0x1043.
+ */
+ if (len < ASUS_XGM_RESP_PAYLOAD + ASUS_XGM_PCI_ID_LEN)
+ return -ENODATA;
+
+ for (int i = 0; i < ASUS_XGM_PCI_ID_COUNT; i++)
+ info->pci_id[i] = get_unaligned_be16(buf + ASUS_XGM_RESP_PAYLOAD +
+ i * sizeof(u16));
+
return 0;
}
@@ -5529,6 +5556,8 @@ static int asus_xgm_init(struct hid_device *hdev, struct asus_drvdata *drvdata)
hid_info(hdev, "XG Mobile: firmware %s, generation %s\n",
info->firmware_version, info->generation);
+ asus_wmi_egpu_set_vendor(info->pci_id[ASUS_XGM_PCI_VENDOR]);
+
/*
* The attribute group is not devm-managed: a creation failure is
* fatal and unwound through asus_xgm_init_err, while on the unbind
@@ -5577,6 +5606,7 @@ static int asus_xgm_init(struct hid_device *hdev, struct asus_drvdata *drvdata)
return 0;
asus_xgm_init_err:
+ asus_wmi_egpu_set_vendor(0);
sysfs_remove_group(&hdev->dev.kobj, &asus_xgm_attr_group);
drvdata->xgm_info = NULL;
drvdata->xgm_led = NULL;
@@ -5795,8 +5825,10 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
if (drvdata->listener.brightness_set)
asus_hid_unregister_listener(&drvdata->listener);
- if (drvdata->xgm_info)
+ if (drvdata->xgm_info) {
+ asus_wmi_egpu_set_vendor(0);
sysfs_remove_group(&hdev->dev.kobj, &asus_xgm_attr_group);
+ }
if (drvdata->xgm_led)
devm_led_classdev_unregister(&hdev->dev, &drvdata->xgm_led->cdev);
@@ -5822,8 +5854,10 @@ static void asus_remove(struct hid_device *hdev)
if (drvdata->listener.brightness_set)
asus_hid_unregister_listener(&drvdata->listener);
- if (drvdata->xgm_info)
+ if (drvdata->xgm_info) {
+ asus_wmi_egpu_set_vendor(0);
sysfs_remove_group(&hdev->dev.kobj, &asus_xgm_attr_group);
+ }
if (drvdata->xgm_led)
devm_led_classdev_unregister(&hdev->dev, &drvdata->xgm_led->cdev);
diff --git a/drivers/platform/x86/asus-armoury.c b/drivers/platform/x86/asus-armoury.c
index fb382de5b57b..dcf2e761016d 100644
--- a/drivers/platform/x86/asus-armoury.c
+++ b/drivers/platform/x86/asus-armoury.c
@@ -86,6 +86,8 @@ struct asus_armoury_priv {
* sequences and dGPU connection status:
* do not allow concurrent changes or changes
* before a reboot if dGPU got disabled.
+ * The activation section reads the reported GPU vendor here.
+ * That word is covered by egpu_vendor_lock in asus-wmi.
*/
struct mutex egpu_mutex;
@@ -538,13 +540,28 @@ static ssize_t dgpu_disable_current_value_store(struct kobject *kobj,
ASUS_WMI_SHOW_INT(dgpu_disable_current_value, ASUS_WMI_DEVID_DGPU);
ASUS_ATTR_GROUP_BOOL(dgpu_disable, "dgpu_disable", "Disable the dGPU");
-/* Values map for eGPU activation requests. */
-static u32 egpu_status_map[] = {
- [0] = 0x00000000U,
- [1] = 0x00000001U,
- [2] = 0x00000101U,
- [3] = 0x00000201U,
-};
+/*
+ * egpu_default_enable_value() - Activation value for the connected station
+ *
+ * The eGPU switch activation value selects the vendor of the external
+ * GPU: 0x0001 activates NVIDIA, 0x0101 AMD and 0x0201 Intel. Pick the
+ * value matching the GPU vendor reported for the connected station,
+ * and fall back to NVIDIA when no vendor has been reported. The caller
+ * holds egpu_mutex. asus_wmi_egpu_vendor() takes egpu_vendor_lock.
+ *
+ * Return: the activation value for the reported GPU vendor
+ */
+static u32 egpu_default_enable_value(void)
+{
+ switch (asus_wmi_egpu_vendor()) {
+ case PCI_VENDOR_ID_ATI:
+ return 0x0101;
+ case PCI_VENDOR_ID_INTEL:
+ return 0x0201;
+ default:
+ return 0x0001;
+ }
+}
/*
* armoury_pci_rescan() - Performs a PCI rescan
@@ -578,11 +595,17 @@ static ssize_t egpu_enable_current_value_store(struct kobject *kobj, struct kobj
if (err)
return err;
- if (requested >= ARRAY_SIZE(egpu_status_map))
+ if (requested > 1)
return -EINVAL;
- enable = egpu_status_map[requested];
scoped_guard(mutex, &asus_armoury.egpu_mutex) {
+ /*
+ * 0 deactivates the eGPU. 1 activates it with the value for
+ * the GPU reported for the connected station. Read that
+ * vendor in this section.
+ */
+ enable = requested ? egpu_default_enable_value() : 0;
+
/* Ensure the eGPU is connected before attempting to activate it. */
if (enable) {
err = armoury_get_devstate(NULL, &result, ASUS_WMI_DEVID_EGPU_CONNECTED);
@@ -661,7 +684,7 @@ static ssize_t egpu_enable_current_value_store(struct kobject *kobj, struct kobj
static ssize_t egpu_enable_current_value_show(struct kobject *kobj, struct kobj_attribute *attr,
char *buf)
{
- int i, err;
+ int err;
u32 status;
scoped_guard(mutex, &asus_armoury.egpu_mutex) {
@@ -670,20 +693,10 @@ static ssize_t egpu_enable_current_value_show(struct kobject *kobj, struct kobj_
return err;
}
- for (i = 0; i < ARRAY_SIZE(egpu_status_map); i++) {
- if (egpu_status_map[i] == status)
- return sysfs_emit(buf, "%u\n", i);
- }
-
- return -EIO;
+ return sysfs_emit(buf, "%u\n", status != 0);
}
-static ssize_t egpu_enable_possible_values_show(struct kobject *kobj, struct kobj_attribute *attr,
- char *buf)
-{
- return armoury_attr_enum_list(buf, ARRAY_SIZE(egpu_status_map));
-}
-ASUS_ATTR_GROUP_ENUM(egpu_enable, "egpu_enable", "Enable the eGPU (also disables dGPU)");
+ASUS_ATTR_GROUP_BOOL(egpu_enable, "egpu_enable", "Enable the eGPU (also disables dGPU)");
/* Device memory available to APU */
diff --git a/drivers/platform/x86/asus-wmi.c b/drivers/platform/x86/asus-wmi.c
index a65090429ca7..16836e7ee3dc 100644
--- a/drivers/platform/x86/asus-wmi.c
+++ b/drivers/platform/x86/asus-wmi.c
@@ -881,6 +881,49 @@ static DEVICE_ATTR_RW(dgpu_disable);
#endif /* IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) */
/* eGPU ********************************************************************/
+
+/*
+ * One XG Mobile can be attached: the connector is proprietary, so
+ * this word is the whole report. 0 means the station is gone and
+ * the next activation uses the default value. egpu_vendor_lock
+ * covers the word. The armoury store holds egpu_mutex and then
+ * calls the getter, which takes this lock. The setter does not
+ * take egpu_mutex.
+ */
+static DEFINE_MUTEX(egpu_vendor_lock);
+static u16 egpu_gpu_vendor;
+
+/**
+ * asus_wmi_egpu_set_vendor() - Report the vendor of the external GPU
+ * @vendor: PCI vendor ID, or 0 when the station is removed
+ *
+ * Pass 0 when the station is removed. That restores the default
+ * activation value. egpu_vendor_lock serializes this with
+ * asus_wmi_egpu_vendor().
+ */
+void asus_wmi_egpu_set_vendor(u16 vendor)
+{
+ guard(mutex)(&egpu_vendor_lock);
+ egpu_gpu_vendor = vendor;
+}
+EXPORT_SYMBOL_NS_GPL(asus_wmi_egpu_set_vendor, "ASUS_WMI");
+
+/**
+ * asus_wmi_egpu_vendor() - Vendor of the external GPU
+ *
+ * Takes egpu_vendor_lock for the copy and releases it before
+ * returning. The armoury store calls this while holding egpu_mutex.
+ *
+ * Return: the PCI vendor ID last reported with
+ * asus_wmi_egpu_set_vendor(), or 0 when no station is known.
+ */
+u16 asus_wmi_egpu_vendor(void)
+{
+ guard(mutex)(&egpu_vendor_lock);
+ return egpu_gpu_vendor;
+}
+EXPORT_SYMBOL_NS_GPL(asus_wmi_egpu_vendor, "ASUS_WMI");
+
#if IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS)
static ssize_t egpu_enable_show(struct device *dev,
struct device_attribute *attr, char *buf)
diff --git a/include/linux/platform_data/x86/asus-wmi.h b/include/linux/platform_data/x86/asus-wmi.h
index b5ed8c83ace1..a53b2828bb1d 100644
--- a/include/linux/platform_data/x86/asus-wmi.h
+++ b/include/linux/platform_data/x86/asus-wmi.h
@@ -204,6 +204,8 @@ int asus_hid_register_listener(struct asus_hid_listener *cdev);
void asus_hid_unregister_listener(struct asus_hid_listener *cdev);
int asus_hid_event(enum asus_hid_event event);
bool asus_wmi_custom_fan_curve_is_enabled(void);
+void asus_wmi_egpu_set_vendor(u16 vendor);
+u16 asus_wmi_egpu_vendor(void);
#else
static inline void set_ally_mcu_hack(enum asus_ally_mcu_hack status)
{
@@ -239,6 +241,15 @@ static inline bool asus_wmi_custom_fan_curve_is_enabled(void)
{
return false;
}
+
+static inline void asus_wmi_egpu_set_vendor(u16 vendor)
+{
+}
+
+static inline u16 asus_wmi_egpu_vendor(void)
+{
+ return 0;
+}
#endif
#endif /* __PLATFORM_DATA_X86_ASUS_WMI_H */
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH v13 18/18] MAINTAINERS: add maintainer for the ASUS HID driver
2026-11-04 1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
` (16 preceding siblings ...)
2026-11-04 1:39 ` [PATCH v13 17/18] platform/x86: asus-armoury: pick the eGPU activation from the reported GPU Denis Benato
@ 2026-11-04 1:39 ` Denis Benato
17 siblings, 0 replies; 25+ messages in thread
From: Denis Benato @ 2026-11-04 1:39 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
I already maintain the ASUS drivers in platform/x86, know these
devices well, have contributed in reviewing and bugfixing and have a
lot more work pending so I volunteer on helping making this driver
better: give hid-asus.c and its sysfs ABI file a dedicated entry
and list me as its maintainer.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
---
MAINTAINERS | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 4a69da715c58..3dbf491046b9 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -4135,6 +4135,13 @@ L: linux-hwmon@vger.kernel.org
S: Maintained
F: drivers/hwmon/asus-ec-sensors.c
+ASUS HID DRIVER
+M: Denis Benato <denis.benato@linux.dev>
+L: linux-input@vger.kernel.org
+S: Maintained
+F: Documentation/ABI/testing/sysfs-driver-hid-asus
+F: drivers/hid/hid-asus.c
+
ASUS NOTEBOOKS AND EEEPC ACPI/WMI EXTRAS DRIVERS
M: Corentin Chary <corentin.chary@gmail.com>
M: Luke D. Jones <luke@ljones.dev>
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread