* [PATCH v12 01/17] HID: asus: size feature reports from the HID report descriptor
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 18:30 ` Rebecca Mara Müller
2026-10-31 17:41 ` [PATCH v12 02/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (15 subsequent siblings)
16 siblings, 1 reply; 19+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 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.
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..327bb4ab1a0d 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_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 [flat|nested] 19+ messages in thread* Re: [PATCH v12 01/17] HID: asus: size feature reports from the HID report descriptor
2026-10-31 17:41 ` [PATCH v12 01/17] HID: asus: size feature reports from the HID report descriptor Denis Benato
@ 2026-10-31 18:30 ` Rebecca Mara Müller
0 siblings, 0 replies; 19+ messages in thread
From: Rebecca Mara Müller @ 2026-10-31 18:30 UTC (permalink / raw)
To: Denis Benato
Cc: Paolo Pisati, linux-kernel, linux-input, platform-driver-x86,
kernel, Benjamin Tissoires, Jiri Kosina, Luke D . Jones,
Mateusz Schwartz, Denis Benato, Jonathan LoBue, Khamunetri Clark,
Derek J . Clark, Joshua Leivenzon, Nathan Chancellor,
Ivan Levchenko, Corentin Chary, Hans de Goede,
Ilpo Järvinen
Hi Denis,
as asked in the cover letter, I repeated the Zenbook Duo test on v12.
Everything that worked with v11 still works.
Setup as for v11: hid.git for-next (hid-asus.c f6aa1be09ad5), your
v12 01-12, then Paolo's v4 1-4 and 6-7, leaving out 5/7, which this
patch replaces. Your patches applied cleanly, and Paolo's went on top
of the reworked 01/17 without conflicts (4/7 with the same trivial
fixup as last time). Built out-of-tree with W=1 against a 7.2.6-based
distribution kernel, no warnings from hid-asus.c. UX8407AA, my
userspace workaround stopped.
- Docked (USB, 0b05:1cd7), captured with usbmon while reloading the
module and pressing the backlight key: 5 SET and 2 GET, every one of
them report 0x5a on interface 4 with wLength 16, all completed with
status 0. It is the same sequence as with v11: the handshake and its
read-back, the 0x05 function query and its GET, the initial
brightness and two changes. The backlight key and Fn+F (platform
profile) work.
- Bluetooth (0b05:1cd8): the backlight key and Fn+F work, no errors
in the log.
- After docking again: the backlight key and Fn+F still work.
Tested-by: Rebecca Mara Müller <rebecca.mara@posteo.de> # UX8407AA
One small thing I noticed while applying: 06/17 adds 14 lines that
contain only a single space, and git am warns about trailing
whitespace. checkpatch will probably flag them too.
Thanks,
Rebecca
^ permalink raw reply [flat|nested] 19+ messages in thread
* [PATCH v12 02/17] HID: asus: add support for ROG Ally handhelds
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
2026-10-31 17:41 ` [PATCH v12 01/17] HID: asus: size feature reports from the HID report descriptor Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 17:41 ` [PATCH v12 03/17] HID: asus: add gamepad configuration Denis Benato
` (14 subsequent siblings)
16 siblings, 0 replies; 19+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 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 | 969 ++++++++++++++++++++++++++++++++++++-----
1 file changed, 870 insertions(+), 99 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 327bb4ab1a0d..552d3b25a1d1 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -22,16 +22,24 @@
#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/types.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 +47,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 +67,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 +195,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 +213,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 +286,761 @@ 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_F19, 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;
+
+ cancel_delayed_work_sync(&intf->resume_work);
+}
+
+static void ally_free(struct ally_intf *intf)
+{
+ if (!intf)
+ return;
+
+ if (intf->gamepad)
+ input_unregister_device(intf->gamepad);
+ 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 < 16 || 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
+ *
+ * The key is down while @data[1] matches that code. The same decode is used
+ * for every Ally interface: 0x38 is the ROG key (KEY_PROG1 and KEY_F19).
+ *
+ * Return: true if the report was a known vendor button.
+ */
+static bool ally_vendor_keys(struct input_dev *input, const u8 *data, int size)
+{
+ if (size < 2 || data[0] != FEATURE_KBD_REPORT_ID)
+ return false;
+
+ if (data[1] != 0x38 && data[1] != 0xA6 && data[1] != 0xA7 &&
+ data[1] != 0xA8)
+ return false;
+
+ if (!input)
+ return true;
+
+ input_report_key(input, KEY_PROG1, data[1] == 0x38);
+ input_report_key(input, KEY_F16, data[1] == 0xA6);
+ input_report_key(input, KEY_F17, data[1] == 0xA7);
+ input_report_key(input, KEY_F18, data[1] == 0xA8);
+ input_report_key(input, KEY_F19, data[1] == 0x38);
+ input_sync(input);
+ return true;
+}
+
+/**
+ * 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);
+}
+
/*
* Send events to asus-wmi driver for handling special keys
*/
@@ -512,6 +1308,21 @@ 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) {
+ struct ally_intf *intf;
+
+ /*
+ * 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 +1392,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 +2108,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 */
@@ -1415,6 +2129,25 @@ static int asus_input_mapping(struct hid_device *hdev,
return -1;
}
+ /*
+ * Declare the Ally vendor keys before this input is registered.
+ * KEY_F19 is the second event of the ROG key (usage 0x38).
+ */
+ if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) {
+ switch (usage->hid & HID_USAGE) {
+ case 0x38:
+ case 0xa6:
+ case 0xa7:
+ case 0xa8:
+ input_set_capability(hi->input, EV_KEY, KEY_PROG1);
+ input_set_capability(hi->input, EV_KEY, KEY_F16);
+ input_set_capability(hi->input, EV_KEY, KEY_F17);
+ input_set_capability(hi->input, EV_KEY, KEY_F18);
+ input_set_capability(hi->input, EV_KEY, KEY_F19);
+ break;
+ }
+ }
+
set_bit(EV_REP, hi->input->evbit);
return 1;
}
@@ -1569,6 +2302,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 +2313,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 +2330,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 +2346,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 +2464,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,8 +2514,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_quiesce(drvdata->ally);
+
asus_worker_stop(drvdata->worker);
hid_hw_stop(hdev);
+
+ if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD)
+ ally_free(drvdata->ally);
return ret;
}
@@ -1770,8 +2535,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_quiesce(drvdata->ally);
+
asus_worker_stop(drvdata->worker);
hid_hw_stop(hdev);
+
+ if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD)
+ ally_free(drvdata->ally);
}
static const __u8 asus_g752_fixed_rdesc[] = {
--
2.47.3
^ permalink raw reply [flat|nested] 19+ messages in thread* [PATCH v12 03/17] HID: asus: add gamepad configuration
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
2026-10-31 17:41 ` [PATCH v12 01/17] HID: asus: size feature reports from the HID report descriptor Denis Benato
2026-10-31 17:41 ` [PATCH v12 02/17] HID: asus: add support for ROG Ally handhelds Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 17:41 ` [PATCH v12 04/17] HID: asus: add vibration strength configuration Denis Benato
` (13 subsequent siblings)
16 siblings, 0 replies; 19+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 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 | 255 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 252 insertions(+), 3 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 552d3b25a1d1..5f5393ea92b7 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>
@@ -37,6 +38,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>
@@ -78,6 +80,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
@@ -729,6 +736,224 @@ 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.
+ */
+static struct ally_prefs {
+ /* Guards every field below, including across the feature report. */
+ struct mutex lock;
+
+ bool initialized;
+ bool sysfs_live;
+
+ 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: true if the capability is supported, false otherwise.
+ */
+static bool 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 false;
+ }
+
+ 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 false;
+ }
+
+ 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)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ ally_prefs.is_ally_x = (hdev->product == USB_DEVICE_ID_ASUSTEK_ROG_NKEY_ALLY_X);
+ ally_prefs.xbox_controller_support =
+ ally_check_capability(hdev, CMD_CHECK_XBOX_SUPPORT);
+ ally_prefs.user_cal_support =
+ ally_check_capability(hdev, CMD_CHECK_USER_CAL_SUPPORT);
+ ally_prefs.turbo_support =
+ ally_check_capability(hdev, CMD_CHECK_TURBO_SUPPORT);
+ ally_prefs.resp_curve_support =
+ ally_check_capability(hdev, CMD_CHECK_RESP_CURVE_SUPPORT);
+ ally_prefs.dir_to_btn_support =
+ ally_check_capability(hdev, CMD_CHECK_DIR_TO_BTN_SUPPORT);
+ ally_prefs.gyro_support =
+ ally_check_capability(hdev, CMD_CHECK_GYRO_TO_JOYSTICK);
+ ally_prefs.anti_deadzone_support =
+ ally_check_capability(hdev, CMD_CHECK_ANTI_DEADZONE);
+
+ return 0;
+}
+
+/* Caller holds ally_prefs.lock. The packet is sent on @hdev. */
+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;
+
+ u8 *buf __free(kfree) = ally_alloc_cmd(hdev, CMD_SET_XBOX_CONTROLLER,
+ payload, sizeof(payload),
+ &buf_len);
+
+ if (!ally_prefs.xbox_controller_support)
+ return -EOPNOTSUPP;
+
+ 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)
+{
+ int ret, i;
+
+ ret = ally_detect_capabilities(hdev);
+ if (ret)
+ return ret;
+
+ 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 (ally_prefs.sysfs_live)
+ return 0;
+
+ 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;
+ }
+ }
+
+ ally_prefs.sysfs_live = true;
+ return 0;
+}
+
+static void ally_sysfs_remove(struct hid_device *hdev)
+{
+ int i;
+
+ if (ally_get_endpoint_address(hdev) != HID_ALLY_INTF_CFG_IN)
+ return;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ if (!ally_prefs.sysfs_live)
+ return;
+
+ ally_prefs.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;
@@ -911,6 +1136,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;
}
@@ -1005,6 +1238,12 @@ static struct ally_intf *hid_asus_ally_probe(struct hid_device *hdev)
return ERR_PTR(ret);
}
+ ret = ally_prefs_setup(hdev);
+ 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:
@@ -1025,7 +1264,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
@@ -1038,6 +1277,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);
}
@@ -2514,8 +2759,10 @@ 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)
+ if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) {
ally_quiesce(drvdata->ally);
+ ally_sysfs_remove(hdev);
+ }
asus_worker_stop(drvdata->worker);
hid_hw_stop(hdev);
@@ -2535,8 +2782,10 @@ 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)
+ if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) {
ally_quiesce(drvdata->ally);
+ ally_sysfs_remove(hdev);
+ }
asus_worker_stop(drvdata->worker);
hid_hw_stop(hdev);
--
2.47.3
^ permalink raw reply [flat|nested] 19+ messages in thread* [PATCH v12 04/17] HID: asus: add vibration strength configuration
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (2 preceding siblings ...)
2026-10-31 17:41 ` [PATCH v12 03/17] HID: asus: add gamepad configuration Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 17:41 ` [PATCH v12 05/17] HID: asus: add joysticks inner and outer range configuration Denis Benato
` (12 subsequent siblings)
16 siblings, 0 replies; 19+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 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 | 173 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 173 insertions(+)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 5f5393ea92b7..fe2008f9d51d 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -764,6 +764,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),
};
@@ -891,6 +894,154 @@ 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;
+}
+
+static ssize_t left_vibration_intensity_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ return sysfs_emit(buf, "%u\n", ally_prefs.vibration_intensity_left);
+}
+
+static ssize_t left_vibration_intensity_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ u8 value;
+ int ret;
+
+ ret = kstrtou8(buf, 10, &value);
+ if (ret || value > 100)
+ return -EINVAL;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ ret = ally_set_vibration_intensity(hdev, value,
+ ally_prefs.vibration_intensity_right);
+ if (ret < 0)
+ return ret;
+
+ ally_prefs.vibration_intensity_left = value;
+ }
+
+ sysfs_notify(&dev->kobj, "left_vibration", "intensity");
+
+ return count;
+}
+
+static ssize_t left_vibration_intensity_range_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ return sysfs_emit(buf, "0 100\n");
+}
+
+static ssize_t right_vibration_intensity_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ return sysfs_emit(buf, "%u\n", ally_prefs.vibration_intensity_right);
+}
+
+static ssize_t right_vibration_intensity_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ u8 value;
+ int ret;
+
+ ret = kstrtou8(buf, 10, &value);
+ if (ret || value > 100)
+ return -EINVAL;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ ret = ally_set_vibration_intensity(hdev,
+ ally_prefs.vibration_intensity_left,
+ value);
+ if (ret < 0)
+ return ret;
+
+ ally_prefs.vibration_intensity_right = value;
+ }
+
+ sysfs_notify(&dev->kobj, "right_vibration", "intensity");
+
+ return count;
+}
+
+static ssize_t right_vibration_intensity_range_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ return sysfs_emit(buf, "0 100\n");
+}
+
+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
@@ -900,6 +1051,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)
@@ -910,6 +1069,8 @@ static int ally_prefs_setup(struct hid_device *hdev)
if (ret)
return ret;
+ ally_vibration_defaults();
+
guard(mutex)(&ally_prefs.lock);
if (!ally_prefs.initialized) {
@@ -1144,6 +1305,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 [flat|nested] 19+ messages in thread* [PATCH v12 05/17] HID: asus: add joysticks inner and outer range configuration
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (3 preceding siblings ...)
2026-10-31 17:41 ` [PATCH v12 04/17] HID: asus: add vibration strength configuration Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 17:41 ` [PATCH v12 06/17] HID: asus: add triggers " Denis Benato
` (11 subsequent siblings)
16 siblings, 0 replies; 19+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 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 | 354 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 348 insertions(+), 6 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index fe2008f9d51d..50b0e6040595 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -98,6 +98,9 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define ASUS_HID_INPUT_KBD_REPORT_ID 0x01
+#define ASUS_ALLY_OUTER_THRESHOLD_MIN 70
+#define ASUS_ALLY_OUTER_THRESHOLD_MAX 100
+
/* Spurious HID codes sent by QUIRK_ROG_NKEY_KEYBOARD devices */
#define ASUS_SPURIOUS_CODE_0XEA 0xea
#define ASUS_SPURIOUS_CODE_0XEC 0xec
@@ -767,6 +770,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),
};
@@ -826,7 +834,6 @@ static int ally_detect_capabilities(struct hid_device *hdev)
return 0;
}
-/* Caller holds ally_prefs.lock. The packet is sent on @hdev. */
static int ally_set_xbox_controller(struct hid_device *hdev, bool enabled)
{
const u8 payload[] = { enabled ?
@@ -1042,6 +1049,304 @@ static struct attribute *ally_right_vibration_attrs[] = {
NULL
};
+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;
+}
+
+static ssize_t left_joystick_inner_threshold_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ if (!ally_prefs.user_cal_support)
+ return -EOPNOTSUPP;
+
+ return sysfs_emit(buf, "%u\n", ally_prefs.left_deadzone);
+}
+
+static ssize_t left_joystick_inner_threshold_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ u8 value;
+ int ret;
+
+ ret = kstrtou8(buf, 10, &value);
+ if (ret || value > 50)
+ return -EINVAL;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ if (!ally_prefs.user_cal_support)
+ return -EOPNOTSUPP;
+
+ ret = ally_set_joystick_thresholds(hdev, value,
+ ally_prefs.left_outer_threshold,
+ ally_prefs.right_deadzone,
+ ally_prefs.right_outer_threshold);
+ if (ret)
+ return ret;
+
+ ally_prefs.left_deadzone = value;
+ }
+
+ sysfs_notify(&dev->kobj, "left_joystick_axis", "inner_threshold");
+
+ return count;
+}
+
+static ssize_t left_joystick_inner_threshold_range_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ return sysfs_emit(buf, "0 50\n");
+}
+
+static ssize_t left_joystick_outer_threshold_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ if (!ally_prefs.user_cal_support)
+ return -EOPNOTSUPP;
+
+ return sysfs_emit(buf, "%u\n", ally_prefs.left_outer_threshold);
+}
+
+static ssize_t left_joystick_outer_threshold_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ u8 value;
+ int ret;
+
+ ret = kstrtou8(buf, 10, &value);
+ if (ret || value < 70 || value > 100)
+ return -EINVAL;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ if (!ally_prefs.user_cal_support)
+ return -EOPNOTSUPP;
+
+ ret = ally_set_joystick_thresholds(hdev, ally_prefs.left_deadzone,
+ value, ally_prefs.right_deadzone,
+ ally_prefs.right_outer_threshold);
+ if (ret)
+ return ret;
+
+ ally_prefs.left_outer_threshold = value;
+ }
+
+ sysfs_notify(&dev->kobj, "left_joystick_axis", "outer_threshold");
+
+ return count;
+}
+
+static ssize_t left_joystick_outer_threshold_range_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ return sysfs_emit(buf, "70 100\n");
+}
+
+static ssize_t right_joystick_inner_threshold_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ if (!ally_prefs.user_cal_support)
+ return -EOPNOTSUPP;
+
+ return sysfs_emit(buf, "%u\n", ally_prefs.right_deadzone);
+}
+
+static ssize_t right_joystick_inner_threshold_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ u8 value;
+ int ret;
+
+ ret = kstrtou8(buf, 10, &value);
+ if (ret || value > 50)
+ return -EINVAL;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ if (!ally_prefs.user_cal_support)
+ return -EOPNOTSUPP;
+
+ ret = ally_set_joystick_thresholds(hdev, ally_prefs.left_deadzone,
+ ally_prefs.left_outer_threshold,
+ value,
+ ally_prefs.right_outer_threshold);
+ if (ret)
+ return ret;
+
+ ally_prefs.right_deadzone = value;
+ }
+
+ sysfs_notify(&dev->kobj, "right_joystick_axis", "inner_threshold");
+
+ return count;
+}
+
+static ssize_t right_joystick_inner_threshold_range_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ return sysfs_emit(buf, "0 50\n");
+}
+
+static ssize_t right_joystick_outer_threshold_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ if (!ally_prefs.user_cal_support)
+ return -EOPNOTSUPP;
+
+ return sysfs_emit(buf, "%u\n", ally_prefs.right_outer_threshold);
+}
+
+static ssize_t right_joystick_outer_threshold_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ u8 value;
+ int ret;
+
+ ret = kstrtou8(buf, 10, &value);
+ if (ret || value < ASUS_ALLY_OUTER_THRESHOLD_MIN || value > ASUS_ALLY_OUTER_THRESHOLD_MAX)
+ return -EINVAL;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ if (!ally_prefs.user_cal_support)
+ return -EOPNOTSUPP;
+
+ ret = ally_set_joystick_thresholds(hdev, ally_prefs.left_deadzone,
+ ally_prefs.left_outer_threshold,
+ ally_prefs.right_deadzone, value);
+ if (ret)
+ return ret;
+
+ ally_prefs.right_outer_threshold = value;
+ }
+
+ sysfs_notify(&dev->kobj, "right_joystick_axis", "outer_threshold");
+
+ return count;
+}
+
+static ssize_t right_joystick_outer_threshold_range_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ return sysfs_emit(buf, "%u %u\n", ASUS_ALLY_OUTER_THRESHOLD_MIN,
+ ASUS_ALLY_OUTER_THRESHOLD_MAX);
+}
+
+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
@@ -1070,6 +1375,7 @@ static int ally_prefs_setup(struct hid_device *hdev)
return ret;
ally_vibration_defaults();
+ ally_joystick_defaults();
guard(mutex)(&ally_prefs.lock);
@@ -1093,6 +1399,27 @@ static int ally_prefs_setup(struct hid_device *hdev)
}
}
+ /* Skip the calibration groups when the capability is missing. */
+ if (ally_prefs.user_cal_support) {
+ int cal_i;
+
+ for (cal_i = 0; cal_i < ARRAY_SIZE(ally_cal_attr_groups); cal_i++) {
+ ret = sysfs_create_group(&hdev->dev.kobj,
+ ally_cal_attr_groups[cal_i]);
+ if (ret) {
+ hid_err(hdev, "Failed to create Ally sysfs group '%s': %d\n",
+ ally_cal_attr_groups[cal_i]->name, ret);
+ while (cal_i--)
+ sysfs_remove_group(&hdev->dev.kobj,
+ ally_cal_attr_groups[cal_i]);
+ for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++)
+ sysfs_remove_group(&hdev->dev.kobj,
+ &ally_attr_groups[i]);
+ return ret;
+ }
+ }
+ }
+
ally_prefs.sysfs_live = true;
return 0;
}
@@ -1104,15 +1431,21 @@ static void ally_sysfs_remove(struct hid_device *hdev)
if (ally_get_endpoint_address(hdev) != HID_ALLY_INTF_CFG_IN)
return;
- scoped_guard(mutex, &ally_prefs.lock) {
- if (!ally_prefs.sysfs_live)
- return;
+ guard(mutex)(&ally_prefs.lock);
- ally_prefs.sysfs_live = false;
- }
+ if (!ally_prefs.sysfs_live)
+ return;
+
+ ally_prefs.sysfs_live = false;
for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++)
sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]);
+
+ if (ally_prefs.user_cal_support || ally_prefs.anti_deadzone_support ||
+ ally_prefs.resp_curve_support) {
+ 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 {
@@ -1317,6 +1650,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 [flat|nested] 19+ messages in thread* [PATCH v12 06/17] HID: asus: add triggers inner and outer range configuration
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (4 preceding siblings ...)
2026-10-31 17:41 ` [PATCH v12 05/17] HID: asus: add joysticks inner and outer range configuration Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 17:41 ` [PATCH v12 07/17] HID: asus: add joysticks anti-deadzone configuration Denis Benato
` (10 subsequent siblings)
16 siblings, 0 replies; 19+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 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 | 212 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 212 insertions(+)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 50b0e6040595..5cf7258bc427 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -100,6 +100,10 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define ASUS_ALLY_OUTER_THRESHOLD_MIN 70
#define ASUS_ALLY_OUTER_THRESHOLD_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
+#define ASUS_ALLY_TRIGGER_RANGE_UPPER_MAX 100
/* Spurious HID codes sent by QUIRK_ROG_NKEY_KEYBOARD devices */
#define ASUS_SPURIOUS_CODE_0XEA 0xea
@@ -775,6 +779,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),
};
@@ -1342,9 +1351,201 @@ static const struct attribute_group ally_right_joystick_axis_group = {
.attrs = right_joystick_axis_attrs,
};
+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[] = {
@@ -1376,6 +1577,7 @@ static int ally_prefs_setup(struct hid_device *hdev)
ally_vibration_defaults();
ally_joystick_defaults();
+ ally_trigger_defaults();
guard(mutex)(&ally_prefs.lock);
@@ -1659,6 +1861,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 [flat|nested] 19+ messages in thread* [PATCH v12 07/17] HID: asus: add joysticks anti-deadzone configuration
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (5 preceding siblings ...)
2026-10-31 17:41 ` [PATCH v12 06/17] HID: asus: add triggers " Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 17:41 ` [PATCH v12 08/17] HID: asus: add support for response curve Denis Benato
` (9 subsequent siblings)
16 siblings, 0 replies; 19+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 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 | 158 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 155 insertions(+), 3 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 5cf7258bc427..9bfdde560834 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -784,6 +784,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),
};
@@ -1321,11 +1324,152 @@ 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 > 100)
+ 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, "0 100\n"); \
+}
+
+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
};
@@ -1334,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
};
@@ -1602,7 +1748,7 @@ static int ally_prefs_setup(struct hid_device *hdev)
}
/* Skip the calibration groups when the capability is missing. */
- if (ally_prefs.user_cal_support) {
+ if (ally_prefs.user_cal_support || ally_prefs.anti_deadzone_support) {
int cal_i;
for (cal_i = 0; cal_i < ARRAY_SIZE(ally_cal_attr_groups); cal_i++) {
@@ -1643,8 +1789,7 @@ static void ally_sysfs_remove(struct hid_device *hdev)
for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++)
sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]);
- if (ally_prefs.user_cal_support || ally_prefs.anti_deadzone_support ||
- ally_prefs.resp_curve_support) {
+ if (ally_prefs.user_cal_support || ally_prefs.anti_deadzone_support) {
for (i = 0; i < ARRAY_SIZE(ally_cal_attr_groups); i++)
sysfs_remove_group(&hdev->dev.kobj, ally_cal_attr_groups[i]);
}
@@ -1871,6 +2016,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 [flat|nested] 19+ messages in thread* [PATCH v12 08/17] HID: asus: add support for response curve
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (6 preceding siblings ...)
2026-10-31 17:41 ` [PATCH v12 07/17] HID: asus: add joysticks anti-deadzone configuration Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 17:41 ` [PATCH v12 09/17] HID: asus: add support to force feedback Denis Benato
` (8 subsequent siblings)
16 siblings, 0 replies; 19+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 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 | 277 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 275 insertions(+), 2 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 9bfdde560834..783c78160277 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>
@@ -753,6 +754,18 @@ static int ally_get_endpoint_address(struct hid_device *hdev)
* once. The only way to see a second one is to emulate it with
* uhid.
*/
+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;
+
static struct ally_prefs {
/* Guards every field below, including across the feature report. */
struct mutex lock;
@@ -787,6 +800,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
};
@@ -1724,6 +1977,7 @@ static int ally_prefs_setup(struct hid_device *hdev)
ally_vibration_defaults();
ally_joystick_defaults();
ally_trigger_defaults();
+ ally_curve_defaults();
guard(mutex)(&ally_prefs.lock);
@@ -1748,7 +2002,8 @@ static int ally_prefs_setup(struct hid_device *hdev)
}
/* Skip the calibration groups when the capability is missing. */
- if (ally_prefs.user_cal_support || ally_prefs.anti_deadzone_support) {
+ if (ally_prefs.user_cal_support || ally_prefs.anti_deadzone_support ||
+ ally_prefs.resp_curve_support) {
int cal_i;
for (cal_i = 0; cal_i < ARRAY_SIZE(ally_cal_attr_groups); cal_i++) {
@@ -1789,7 +2044,8 @@ static void ally_sysfs_remove(struct hid_device *hdev)
for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++)
sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]);
- if (ally_prefs.user_cal_support || ally_prefs.anti_deadzone_support) {
+ if (ally_prefs.user_cal_support || ally_prefs.anti_deadzone_support ||
+ ally_prefs.resp_curve_support) {
for (i = 0; i < ARRAY_SIZE(ally_cal_attr_groups); i++)
sysfs_remove_group(&hdev->dev.kobj, ally_cal_attr_groups[i]);
}
@@ -2023,6 +2279,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 [flat|nested] 19+ messages in thread* [PATCH v12 09/17] HID: asus: add support to force feedback
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (7 preceding siblings ...)
2026-10-31 17:41 ` [PATCH v12 08/17] HID: asus: add support for response curve Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 17:41 ` [PATCH v12 10/17] HID: asus: add support for gamepad mode Denis Benato
` (7 subsequent siblings)
16 siblings, 0 replies; 19+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 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 | 143 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 144 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 783c78160277..3ade3dbda72e 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>
@@ -210,6 +212,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. */
@@ -219,6 +238,14 @@ struct ally_intf {
u8 cad_state;
unsigned long cad_last_event_time;
struct delayed_work resume_work;
+ 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 {
@@ -325,10 +352,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
@@ -2184,6 +2216,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);
@@ -2212,8 +2322,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;
@@ -2404,6 +2530,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;
@@ -2426,6 +2560,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;
@@ -3914,6 +4055,7 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) {
ally_quiesce(drvdata->ally);
ally_sysfs_remove(hdev);
+ ally_ff_quiesce(drvdata->ally);
}
asus_worker_stop(drvdata->worker);
@@ -3937,6 +4079,7 @@ static void asus_remove(struct hid_device *hdev)
if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) {
ally_quiesce(drvdata->ally);
ally_sysfs_remove(hdev);
+ ally_ff_quiesce(drvdata->ally);
}
asus_worker_stop(drvdata->worker);
--
2.47.3
^ permalink raw reply [flat|nested] 19+ messages in thread* [PATCH v12 10/17] HID: asus: add support for gamepad mode
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (8 preceding siblings ...)
2026-10-31 17:41 ` [PATCH v12 09/17] HID: asus: add support to force feedback Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 17:41 ` [PATCH v12 11/17] HID: asus: add support for turbo buttons Denis Benato
` (6 subsequent siblings)
16 siblings, 0 replies; 19+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 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.
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 3ade3dbda72e..3e58c5ca0cbd 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -352,6 +352,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
@@ -1372,6 +1386,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_available_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_available);
+
/**
* ally_set_anti_deadzone() - Send the joystick anti-deadzone values to the device
* @hdev: HID device the packet is sent on
@@ -1981,6 +2101,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_available.attr,
NULL
};
@@ -2014,7 +2136,7 @@ static int ally_prefs_setup(struct hid_device *hdev)
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;
}
@@ -2422,6 +2544,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 [flat|nested] 19+ messages in thread* [PATCH v12 11/17] HID: asus: add support for turbo buttons
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (9 preceding siblings ...)
2026-10-31 17:41 ` [PATCH v12 10/17] HID: asus: add support for gamepad mode Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 17:41 ` [PATCH v12 12/17] HID: asus: add support for btn remapping Denis Benato
` (5 subsequent siblings)
16 siblings, 0 replies; 19+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 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 | 410 +++++++++++++++++++++++++++++++++++++++--
1 file changed, 397 insertions(+), 13 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 3e58c5ca0cbd..d20e433af574 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -812,6 +812,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;
+};
+
static struct ally_prefs {
/* Guards every field below, including across the feature report. */
struct mutex lock;
@@ -849,6 +923,9 @@ static struct ally_prefs {
struct ally_joystick_resp_curve left_curve;
struct ally_joystick_resp_curve right_curve;
+
+ struct ally_turbo_config turbo;
+ struct ally_btn_sysfs_entry *button_entries;
} ally_prefs = {
.lock = __MUTEX_INITIALIZER(ally_prefs.lock),
};
@@ -2120,9 +2197,297 @@ 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
+ *
+ * Caller holds ally_prefs.lock.
+ *
+ * @hdev: The HID device to send the packet to.
+ * 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;
+ const u8 payload[] = {
+ turbo->btn_du.turbo, turbo->btn_du.toggle,
+ turbo->btn_dd.turbo, turbo->btn_dd.toggle,
+ turbo->btn_dl.turbo, turbo->btn_dl.toggle,
+ turbo->btn_dr.turbo, turbo->btn_dr.toggle,
+ turbo->btn_j0b.turbo, turbo->btn_j0b.toggle,
+ turbo->btn_j1b.turbo, turbo->btn_j1b.toggle,
+ turbo->btn_lb.turbo, turbo->btn_lb.toggle,
+ turbo->btn_rb.turbo, turbo->btn_rb.toggle,
+ turbo->btn_a.turbo, turbo->btn_a.toggle,
+ turbo->btn_b.turbo, turbo->btn_b.toggle,
+ turbo->btn_x.turbo, turbo->btn_x.toggle,
+ turbo->btn_y.turbo, turbo->btn_y.toggle,
+ turbo->btn_view.turbo, turbo->btn_view.toggle,
+ turbo->btn_menu.turbo, turbo->btn_menu.toggle,
+ turbo->btn_m2.turbo, turbo->btn_m2.toggle,
+ turbo->btn_m1.turbo, turbo->btn_m1.toggle,
+ };
+ size_t buf_len;
+ int ret;
+
+ u8 *buf __free(kfree) = ally_alloc_cmd(hdev, CMD_SET_TURBO_PARAMS, 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 turbo parameters: %d\n", ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+static ssize_t btn_turbo_period_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct ally_btn_sysfs_entry *entry;
+ struct ally_btn_turbo_params *params;
+
+ entry = container_of(attr, struct ally_btn_sysfs_entry, attr_turbo_period);
+
+ 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", params->turbo);
+}
+
+static ssize_t btn_turbo_period_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct ally_btn_sysfs_entry *entry;
+ struct hid_device *hdev = to_hid_device(dev);
+ struct ally_btn_turbo_params *params;
+ u8 old_value, value;
+ int ret;
+
+ entry = container_of(attr, struct ally_btn_sysfs_entry, attr_turbo_period);
+
+ ret = kstrtou8(buf, 10, &value);
+ if (ret || value > ALLY_TURBO_PERIOD_MAX)
+ return ret ? ret : -EINVAL;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ if (!ally_prefs.turbo_support)
+ return -EOPNOTSUPP;
+
+ params = ally_btn_param(entry->btn);
+ if (!params)
+ return -ENODEV;
+
+ old_value = params->turbo;
+ params->turbo = value;
+ ret = ally_set_turbo_params(hdev);
+ if (ret) {
+ params->turbo = old_value;
+ return ret;
+ }
+ }
+
+ sysfs_notify(&dev->kobj, entry->group.name, "turbo_period");
+
+ return count;
+}
+
+static ssize_t btn_toggle_period_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct ally_btn_sysfs_entry *entry;
+ struct ally_btn_turbo_params *params;
+
+ entry = container_of(attr, struct ally_btn_sysfs_entry, attr_toggle_period);
+
+ 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", params->toggle);
+}
+
+static ssize_t btn_toggle_period_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct ally_btn_sysfs_entry *entry;
+ struct hid_device *hdev = to_hid_device(dev);
+ struct ally_btn_turbo_params *params;
+ u8 old_value, value;
+ int ret;
+
+ entry = container_of(attr, struct ally_btn_sysfs_entry, attr_toggle_period);
+
+ ret = kstrtou8(buf, 10, &value);
+ if (ret)
+ return ret;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ if (!ally_prefs.turbo_support)
+ return -EOPNOTSUPP;
+
+ params = ally_btn_param(entry->btn);
+ if (!params)
+ return -ENODEV;
+
+ old_value = params->toggle;
+ params->toggle = value;
+ ret = ally_set_turbo_params(hdev);
+ if (ret) {
+ params->toggle = old_value;
+ return ret;
+ }
+ }
+
+ sysfs_notify(&dev->kobj, entry->group.name, "toggle_period");
+
+ return count;
+}
+
+static ssize_t btn_turbo_period_range_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ return sysfs_emit(buf, "0 20\n");
+}
+
+static ssize_t btn_toggle_period_range_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ return sysfs_emit(buf, "0 255\n");
+}
+
+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;
+
+ 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;
+
+ entries = ally_prefs.button_entries;
+ if (!entries)
+ return;
+
+ for (i = 0; i < ALLY_BTN_MAX; i++)
+ sysfs_remove_group(&hdev->dev.kobj, &entries[i].group);
+
+ kfree(entries);
+ ally_prefs.button_entries = NULL;
+}
+
static int ally_prefs_setup(struct hid_device *hdev)
{
- int ret, i;
+ struct ally_btn_sysfs_entry *entries = NULL;
+ int ret, i, cal_n = 0;
ret = ally_detect_capabilities(hdev);
if (ret)
@@ -2158,27 +2523,38 @@ static int ally_prefs_setup(struct hid_device *hdev)
/* Skip the calibration groups when the capability is missing. */
if (ally_prefs.user_cal_support || ally_prefs.anti_deadzone_support ||
ally_prefs.resp_curve_support) {
- int cal_i;
-
- for (cal_i = 0; cal_i < ARRAY_SIZE(ally_cal_attr_groups); cal_i++) {
+ for (; cal_n < ARRAY_SIZE(ally_cal_attr_groups); cal_n++) {
ret = sysfs_create_group(&hdev->dev.kobj,
- ally_cal_attr_groups[cal_i]);
+ 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_i]->name, ret);
- while (cal_i--)
- sysfs_remove_group(&hdev->dev.kobj,
- ally_cal_attr_groups[cal_i]);
- for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++)
- sysfs_remove_group(&hdev->dev.kobj,
- &ally_attr_groups[i]);
- return ret;
+ ally_cal_attr_groups[cal_n]->name, ret);
+ goto err_cal_groups;
}
}
}
+ if (ally_prefs.turbo_support) {
+ 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;
+ }
+ }
+
+ ally_prefs.button_entries = entries;
ally_prefs.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)
@@ -2195,6 +2571,8 @@ static void ally_sysfs_remove(struct hid_device *hdev)
ally_prefs.sysfs_live = false;
+ ally_remove_button_attributes(hdev);
+
for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++)
sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]);
@@ -2550,6 +2928,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 [flat|nested] 19+ messages in thread* [PATCH v12 12/17] HID: asus: add support for btn remapping
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (10 preceding siblings ...)
2026-10-31 17:41 ` [PATCH v12 11/17] HID: asus: add support for turbo buttons Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 17:41 ` [PATCH v12 13/17] HID: asus: identify XG Mobile stations and bind the LED to the reported model Denis Benato
` (4 subsequent siblings)
16 siblings, 0 replies; 19+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 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_index 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/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 | 896 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 874 insertions(+), 22 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index d20e433af574..b94cb8c50b28 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -41,6 +41,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>
@@ -108,6 +109,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
@@ -878,12 +883,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];
};
static struct ally_prefs {
@@ -926,10 +1134,598 @@ static struct ally_prefs {
struct ally_turbo_config turbo;
struct ally_btn_sysfs_entry *button_entries;
+ 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
+ *
+ * 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 (!strcmp(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;
+ struct ally_button_mapping *mode_map;
+ const struct btn_code_map *code;
+ const struct btn_code_map *old_code;
+ enum btn_pair_index pair_idx;
+ struct button_pair_map *pair;
+ struct button_map *btn_map;
+ char btn_name[32];
+ bool is_first;
+ int ret, i;
+
+ if (sscanf(buf, "%31s", btn_name) != 1)
+ return -EINVAL;
+
+ code = find_button_by_name(btn_name);
+ 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;
+
+ if (btn_attr->is_macro) {
+ old_code = btn_map->macro;
+ btn_map->macro = code;
+ } else {
+ old_code = btn_map->remap;
+ btn_map->remap = 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:
+ if (btn_attr->is_macro)
+ btn_map->macro = old_code;
+ else
+ btn_map->remap = old_code;
+
+ /* Try to recover the mapping presented to the user. */
+ ally_set_button_mapping(hdev, pair);
+ goto err_out;
+
+err_restore:
+ if (btn_attr->is_macro)
+ btn_map->macro = old_code;
+ else
+ btn_map->remap = 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
@@ -1532,12 +2328,35 @@ static ssize_t gamepad_mode_store(struct device *dev, struct device_attribute *a
/* 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;
+ guard(mutex)(&ally_prefs.lock);
+
+ ret = ally_set_gamepad_mode(hdev, mode_byte);
+ if (ret < 0)
+ return ret;
- ally_prefs.gamepad_mode = mode_byte;
+ 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);
+
+ if (ally_prefs.button_entries) {
+ int i;
+
+ for (i = 0; i < ALLY_BTN_MAX; i++) {
+ sysfs_notify(&dev->kobj,
+ ally_prefs.button_entries[i].group.name, "remap");
+ sysfs_notify(&dev->kobj,
+ ally_prefs.button_entries[i].group.name, "macro");
+ sysfs_notify(&dev->kobj,
+ ally_prefs.button_entries[i].group.name, "remap_available");
+ }
}
hid_dbg(hdev, "Set gamepad mode to %s\n", ally_gamepad_mode_text[mode]);
@@ -2429,11 +3248,9 @@ 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)
{
+ const size_t entries_attr_index = ally_prefs.turbo_support ? 4 : 0;
int i, ret;
- 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);
@@ -2441,13 +3258,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;
@@ -2465,6 +3302,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);
}
@@ -2477,11 +3319,19 @@ static void ally_remove_button_attributes(struct hid_device *hdev)
if (!entries)
return;
- for (i = 0; i < ALLY_BTN_MAX; i++)
+ /*
+ * Unpublish first. A mode-store notify holds the prefs lock and must
+ * see either the live array or NULL.
+ */
+ ally_prefs.button_entries = NULL;
+
+ 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);
- ally_prefs.button_entries = NULL;
}
static int ally_prefs_setup(struct hid_device *hdev)
@@ -2497,6 +3347,7 @@ static int ally_prefs_setup(struct hid_device *hdev)
ally_joystick_defaults();
ally_trigger_defaults();
ally_curve_defaults();
+ ally_mapping_defaults();
guard(mutex)(&ally_prefs.lock);
@@ -2534,13 +3385,11 @@ static int ally_prefs_setup(struct hid_device *hdev)
}
}
- if (ally_prefs.turbo_support) {
- 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;
- }
+ 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;
}
ally_prefs.button_entries = entries;
@@ -2934,6 +3783,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_prefs.button_entries)
+ ally_apply_button_mappings(hdev);
+
return 0;
}
--
2.47.3
^ permalink raw reply [flat|nested] 19+ messages in thread* [PATCH v12 13/17] HID: asus: identify XG Mobile stations and bind the LED to the reported model
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (11 preceding siblings ...)
2026-10-31 17:41 ` [PATCH v12 12/17] HID: asus: add support for btn remapping Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 17:41 ` [PATCH v12 14/17] HID: asus: expose XG Mobile identity and firmware information Denis Benato
` (3 subsequent siblings)
16 siblings, 0 replies; 19+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 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 b94cb8c50b28..215628393765 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>
@@ -70,6 +71,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
@@ -207,6 +231,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;
@@ -271,6 +302,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);
@@ -4875,6 +4907,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 };
@@ -5160,14 +5355,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 [flat|nested] 19+ messages in thread* [PATCH v12 14/17] HID: asus: expose XG Mobile identity and firmware information
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (12 preceding siblings ...)
2026-10-31 17:41 ` [PATCH v12 13/17] HID: asus: identify XG Mobile stations and bind the LED to the reported model Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 17:41 ` [PATCH v12 15/17] HID: asus: document ROG XG Mobile sysfs attributes Denis Benato
` (2 subsequent siblings)
16 siblings, 0 replies; 19+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 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 215628393765..691303b1a13f 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -74,6 +74,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. */
@@ -81,6 +82,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
@@ -236,6 +243,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 {
@@ -5070,6 +5079,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 };
@@ -5392,9 +5548,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,
@@ -5427,6 +5603,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;
}
@@ -5643,6 +5821,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);
@@ -5667,6 +5848,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 [flat|nested] 19+ messages in thread* [PATCH v12 15/17] HID: asus: document ROG XG Mobile sysfs attributes
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (13 preceding siblings ...)
2026-10-31 17:41 ` [PATCH v12 14/17] HID: asus: expose XG Mobile identity and firmware information Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 17:41 ` [PATCH v12 16/17] platform/x86: asus-armoury: pick the eGPU activation from the reported GPU Denis Benato
2026-10-31 17:41 ` [PATCH v12 17/17] MAINTAINERS: add maintainer for the ASUS HID driver Denis Benato
16 siblings, 0 replies; 19+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 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 [flat|nested] 19+ messages in thread* [PATCH v12 16/17] platform/x86: asus-armoury: pick the eGPU activation from the reported GPU
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (14 preceding siblings ...)
2026-10-31 17:41 ` [PATCH v12 15/17] HID: asus: document ROG XG Mobile sysfs attributes Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 17:41 ` [PATCH v12 17/17] MAINTAINERS: add maintainer for the ASUS HID driver Denis Benato
16 siblings, 0 replies; 19+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 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 691303b1a13f..9431a620318e 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -89,6 +89,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.
@@ -245,6 +259,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 {
@@ -5156,6 +5171,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;
}
@@ -5555,6 +5582,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
@@ -5603,6 +5632,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;
@@ -5821,8 +5851,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);
@@ -5848,8 +5880,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 [flat|nested] 19+ messages in thread* [PATCH v12 17/17] MAINTAINERS: add maintainer for the ASUS HID driver
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (15 preceding siblings ...)
2026-10-31 17:41 ` [PATCH v12 16/17] platform/x86: asus-armoury: pick the eGPU activation from the reported GPU Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
16 siblings, 0 replies; 19+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 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 [flat|nested] 19+ messages in thread