From nobody Fri Sep 25 20:46:57 2026 Received: from mta1.migadu.com (out-43.mta1.migadu.com [95.215.58.43]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 350C159B67C for ; Tue, 8 Sep 2026 17:44:42 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=95.215.58.43 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788889486; cv=none; b=sH2UXK550vBIdP5AW7dFMSVmbNGShr5avOduGEuNHp4vLwC39TqOi3sVMAAE+M/x4Ef7r3OQZtWoz7IaBiXJYvaB5J3xVCV08XTLu7sd/4n3I3Zjes1Y8Rub3X1yi7hL5xjjtwkvW0bmtjngw2uv2ozecVMDVsx03gWPKnwc8IM= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788889486; c=relaxed/simple; bh=zLK9ilxnouBl8OfzKtWT4IUHOCTEywTf0m+xYBuIXWw=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=rn36jAhc1e0Sx1/6h/OILq1cVQNEc5F+100MfZXCtCFHCVsm5G2BQVDL6y/fKPqfkCf8RyBoj0eKuHSlRFzNrVX1FyZcytWP65eViGuYEbqfDjzi31YvHODayar1e8Wdgpe+AnLBj1XHVunbDFWZV9NyuVBirk6+cHPpEWz68lM= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=HqCs0Iez; arc=none smtp.client-ip=95.215.58.43 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="HqCs0Iez" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=zLK9ilxnouBl8OfzKtWT4IUHOCTEywTf0m+xYBuIXWw=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1788889480; v=1; x=1789494280; b=HqCs0IezO0f3LQHksBjZd54u4bNx9c28PXVNZgolt1IRvCRjgnFghk7Q1Kmi0h7IdS6llbab Ipn0nxM0Ntc+S2Sex4ACu/XTMVB9InGn5fGm84WLlihXAGjqEVRhizbMzAByQ9N9p3ELhD1oe4+ LUpm3IrZBtrK4TOHr7oCE6Cs= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id 5043b55b054dda9e; Tue, 08 Sep 2026 17:44:40 +0000 X-Mizu-Trace-ID: 5043b55b054dda9e X-Migadu-Flow: FLOW_OUT From: Denis Benato To: linux-kernel@vger.kernel.org Cc: linux-input@vger.kernel.org, "Benjamin Tissoires" , "Jiri Kosina" , "Luke D . Jones" , "Mateusz Schwartz" , "Denis Benato" , "Jonathan LoBue" , "Khamunetri Clark" , "Derek J. Clark" , Panz Dev , stable@vger.kernel.org Subject: [PATCH v6 01/13] HID: asus: do not send keyboard init reports to touchpads Date: Tue, 8 Sep 2026 17:44:21 +0000 Message-ID: <20260908174433.74260-2-denis.benato@linux.dev> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260908174433.74260-1-denis.benato@linux.dev> References: <20260908174433.74260-1-denis.benato@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" From: Panz Dev Commit 0919db9f3583 ("HID: asus: always fully initialize devices") added a loop during asus_probe() to send keyboard feature report initializations (asus_kbd_init) to all ASUS HID devices. On ASUS laptops with I2C/HID touchpads (such as the ASUS E200HA), sending keyboard feature reports (FEATURE_KBD_REPORT_ID) to touchpad endpoints sends invalid feature requests to touchpad hardware, corrupting probe state and causing the touchpad to become unresponsive. Wrap the asus_report_id_init loop in an `if (!drvdata->tp)` check so keyboard feature initialization only runs for actual keyboards. Tested on ASUS E200HA (where touchpad functionality is fully restored) and ASUS VivoBook Flip 14 TP401MA (confirming zero regressions). Fixes: 0919db9f3583 ("HID: asus: always fully initialize devices") Cc: stable@vger.kernel.org Signed-off-by: Panz Dev --- drivers/hid/hid-asus.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c index ec966fc0a411..bd46aba6622a 100644 --- a/drivers/hid/hid-asus.c +++ b/drivers/hid/hid-asus.c @@ -1493,12 +1493,14 @@ static int asus_probe(struct hid_device *hdev, cons= t struct hid_device_id *id) return ret; } =20 - for (int r =3D 0; r < ARRAY_SIZE(asus_report_id_init); r++) { - if (asus_has_report_id(hdev, asus_report_id_init[r])) { - ret =3D asus_kbd_init(hdev, asus_report_id_init[r]); - if (ret < 0) - hid_warn(hdev, "Failed to initialize 0x%x: %d.\n", - asus_report_id_init[r], ret); + if (!drvdata->tp) { + for (int r =3D 0; r < ARRAY_SIZE(asus_report_id_init); r++) { + if (asus_has_report_id(hdev, asus_report_id_init[r])) { + ret =3D asus_kbd_init(hdev, asus_report_id_init[r]); + if (ret < 0) + hid_warn(hdev, "Failed to initialize 0x%x: %d.\n", + asus_report_id_init[r], ret); + } } } =20 --=20 2.47.3 From nobody Fri Sep 25 20:46:57 2026 Received: from mta1.migadu.com (out-48.mta1.migadu.com [95.215.58.48]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id A9297583AC7 for ; Tue, 8 Sep 2026 17:44:43 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=95.215.58.48 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788889486; cv=none; b=SB15LThvt0xRIa9g9xpTICpUUJNYEnw/cYM1d0M8CbD0ImW1801VtBxXKFEjrYyyLRhtP4PMffxye4tdvKdHFZXtjJRqrKyycI8aYfmlXM7BTTpItf3V3R5GSCS710BlKNRxwfFgAAAglNE8sICTTVXQWDewAJ+6dOFyYm2t9pQ= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788889486; c=relaxed/simple; bh=s1tCujWEhQUGKDrIj/SWc90xs4vo11d1o+0/tXUXAv4=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=lpFlgi2+op4fzp/y2dXYiCbOAgAb2fYdmxIKUWfd9IoOAr7VT+qLAs1CIv6KJ3Emq0bzEaUnomzn9oR81jAIODqtIUzvMT7yw6oMAOKwfBLudQARVw2gRVH6UGODwiHyAyHNnP96s/oIHaUUT486T7rMvHelbrXMptIQ0SIaVOo= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=lk86v6zO; arc=none smtp.client-ip=95.215.58.48 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="lk86v6zO" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=s1tCujWEhQUGKDrIj/SWc90xs4vo11d1o+0/tXUXAv4=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1788889481; v=1; x=1789494281; b=lk86v6zOGIY3zDGn18W0R5Hz3eyHAqdebnK8g6aM7T9gqA+mRMCKtWcdFRHTZlm1dcyBJyJL gfaXd6bRRR1MLwBFljFGXaN4UtlehvW7c7HhV0nqbYfdb1DgdeTbInL6G8GHbW8Lvn6RW1g44NX q9Ok1QP4qi/+PK0CgTKIRtfk= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id 42a778c2a7b93921; Tue, 08 Sep 2026 17:44:41 +0000 X-Mizu-Trace-ID: 42a778c2a7b93921 X-Migadu-Flow: FLOW_OUT From: Denis Benato To: linux-kernel@vger.kernel.org Cc: linux-input@vger.kernel.org, "Benjamin Tissoires" , "Jiri Kosina" , "Luke D . Jones" , "Mateusz Schwartz" , "Denis Benato" , "Jonathan LoBue" , "Khamunetri Clark" , "Derek J. Clark" , Denis Benato Subject: [PATCH v6 02/13] HID: asus: reinitialize the device after exiting a sleep state Date: Tue, 8 Sep 2026 17:44:22 +0000 Message-ID: <20260908174433.74260-3-denis.benato@linux.dev> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260908174433.74260-1-denis.benato@linux.dev> References: <20260908174433.74260-1-denis.benato@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" The ROG ally needs to have the EC string sent back after resuming from s2idle since the USB device can be turned completely off by the firmware when mcu_powersave firmware-attribute is set to 1. This may also be true for other laptops and certain features might stop working after the device exit from sleep. Assisted-by: opencode:glm-5.2 Signed-off-by: Denis Benato --- drivers/hid/hid-asus.c | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c index bd46aba6622a..2125680c7eff 100644 --- a/drivers/hid/hid-asus.c +++ b/drivers/hid/hid-asus.c @@ -1360,6 +1360,28 @@ static int asus_start_multitouch(struct hid_device *= hdev) return 0; } =20 +/* + * Initialize the reports of the device. + * + * Failures are intentionally not fatal: asus_kbd_init() tolerates a wrong + * handshake until this is verified to work for all devices, so a failure + * is only reported and the initialization of the remaining reports is + * still attempted. + */ +static void asus_initialize_reports(struct hid_device *hdev) +{ + int ret; + + for (int r =3D 0; r < ARRAY_SIZE(asus_report_id_init); r++) { + if (asus_has_report_id(hdev, asus_report_id_init[r])) { + ret =3D asus_kbd_init(hdev, asus_report_id_init[r]); + if (ret < 0) + hid_warn(hdev, "Failed to initialize 0x%x: %d.\n", + asus_report_id_init[r], ret); + } + } +} + static int __maybe_unused asus_resume(struct hid_device *hdev) { struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); @@ -1379,6 +1401,9 @@ static int __maybe_unused asus_reset_resume(struct hi= d_device *hdev) { struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); =20 + if (!drvdata->tp) + asus_initialize_reports(hdev); + if (drvdata->tp) return asus_start_multitouch(hdev); =20 @@ -1493,16 +1518,8 @@ static int asus_probe(struct hid_device *hdev, const= struct hid_device_id *id) return ret; } =20 - if (!drvdata->tp) { - for (int r =3D 0; r < ARRAY_SIZE(asus_report_id_init); r++) { - if (asus_has_report_id(hdev, asus_report_id_init[r])) { - ret =3D asus_kbd_init(hdev, asus_report_id_init[r]); - if (ret < 0) - hid_warn(hdev, "Failed to initialize 0x%x: %d.\n", - asus_report_id_init[r], ret); - } - } - } + if (!drvdata->tp) + asus_initialize_reports(hdev); =20 /* Laptops keyboard backlight is always at 0x5a */ if (is_vendor && (drvdata->quirks & QUIRK_USE_KBD_BACKLIGHT) && --=20 2.47.3 From nobody Fri Sep 25 20:46:57 2026 Received: from mta1.migadu.com (out-54.mta1.migadu.com [95.215.58.54]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 2C7F759C99F for ; Tue, 8 Sep 2026 17:44:47 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=95.215.58.54 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788889492; cv=none; b=gdqX4Fv5/IQxnBhKmMtTh3uKIC9+9px9CYfEx59folDLMOYJb/+FGDQdmQSZxtMOsZP0ampDFKVDz2YTwJ4qxVygb09+ELyJBXv85DzAVZgaopSm6LfLSpkrf2qxlQQ41yD9yP9/wjJcPXkZiFlYsTm5XUw6EYEjM0amhqEBoxw= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788889492; c=relaxed/simple; bh=3pWIbjs8VuEbURv3aWGMHAPP3FDn2KpYKYNfDeFaH8g=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=F1M0gPy4YXK2k0mnvEcVuYozgV4T+8gaGZ9I1mhZJXB0ZmcWXS1jSE8mhFxm3tcvcZOkPS/Zje3zTrRNTFY7w1Z80vxW/VAbiyMpmUycRSbtgTwKyg8yOFbOSrEIF1rfuj3AWw4creWArbhnkJRHuYJRQzwUYaBDKPUIoODiZs4= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=d2sPKrDr; arc=none smtp.client-ip=95.215.58.54 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="d2sPKrDr" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=3pWIbjs8VuEbURv3aWGMHAPP3FDn2KpYKYNfDeFaH8g=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1788889485; v=1; x=1789494285; b=d2sPKrDr6BxeAf7pOkoFAeS32DztRpnEuVCJrXuiGvooY2oECDQANdrjl2h1qgWCE/dVpt+5 n7Y9ZGCnCAW3eD0v28bjdhE2Sgvtf8sx7F+Tdc1rELQVDCJJ7DitqsEitzTxbwKGG/kd5p/8YTA r4I3PX/VvVqHo7fkAsNkIMVE= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id e432f3e78a4dd757; Tue, 08 Sep 2026 17:44:45 +0000 X-Mizu-Trace-ID: e432f3e78a4dd757 X-Migadu-Flow: FLOW_OUT From: Denis Benato To: linux-kernel@vger.kernel.org Cc: linux-input@vger.kernel.org, "Benjamin Tissoires" , "Jiri Kosina" , "Luke D . Jones" , "Mateusz Schwartz" , "Denis Benato" , "Jonathan LoBue" , "Khamunetri Clark" , "Derek J. Clark" , Denis Benato Subject: [PATCH v6 03/13] HID: asus: add support for ROG Ally handhelds Date: Tue, 8 Sep 2026 17:44:23 +0000 Message-ID: <20260908174433.74260-4-denis.benato@linux.dev> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260908174433.74260-1-denis.benato@linux.dev> References: <20260908174433.74260-1-denis.benato@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" 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 - reject probes from a different controller and from duplicated interfaces of the same one, which could evict the shared state Assisted-by: opencode:glm-5.2 Assisted-by: Claude:claude-fable-5 Signed-off-by: Khamunetri Clark Signed-off-by: Denis Benato Signed-off-by: Matthew Schwartz Signed-off-by: Luke Jones Signed-off-by: Jonathan LoBue --- drivers/hid/hid-asus.c | 940 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 940 insertions(+) diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c index 2125680c7eff..11dea2aec80b 100644 --- a/drivers/hid/hid-asus.c +++ b/drivers/hid/hid-asus.c @@ -24,12 +24,18 @@ #include #include #include +#include +#include #include +#include #include +#include #include #include /* For to_usb_interface for T100 touchpad intf chec= k */ #include +#include #include +#include =20 #include "hid-ids.h" =20 @@ -37,6 +43,9 @@ MODULE_AUTHOR("Yusuke Fujimaki "); MODULE_AUTHOR("Brendan McGrath "); MODULE_AUTHOR("Victor Vlasenko "); MODULE_AUTHOR("Frederik Wenigwieser "); +MODULE_AUTHOR("Denis Benato "); +MODULE_AUTHOR("Luke Jones "); +MODULE_AUTHOR("Khamunetri Clark "); MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad"); =20 #define T100_TPAD_INTF 2 @@ -55,6 +64,19 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad"); #define ROG_ALLY_X_MIN_MCU 313 #define ROG_ALLY_MIN_MCU 319 =20 +#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_SET_REPORT_ID 0x5A +#define HID_ALLY_FEATURE_CODE_PAGE 0xD1 + +#define HID_ALLY_X_INPUT_REPORT_SIZE 16 +#define HID_ALLY_X_INPUT_REPORT 0x0B + +#define HID_ALLY_READY_MAX_TRIES 6 + /* Spurious HID codes sent by QUIRK_ROG_NKEY_KEYBOARD devices */ #define ASUS_SPURIOUS_CODE_0XEA 0xea #define ASUS_SPURIOUS_CODE_0XEC 0xec @@ -154,6 +176,29 @@ struct asus_touchpad_info { int report_size; }; =20 +struct ally_handheld { + /* All read/write to IN interfaces must lock */ + struct mutex intf_mutex; + /* + * USB device of the connected controller, held with a reference: + * at most one ROG Ally can exist, so probes from any other USB + * device are rejected to protect the shared state. + */ + struct usb_device *udev; + struct hid_device *cfg_hdev; + + struct input_dev *ally_x_input; + struct hid_device *ally_x_hdev; + + struct hid_device *keyboard_hdev; + struct input_dev *keyboard_input; + + u8 cad_sequence_state; + unsigned long cad_last_event_time; + + struct delayed_work resume_work; +}; + struct asus_drvdata { unsigned long quirks; struct hid_device *hdev; @@ -161,6 +206,7 @@ struct asus_drvdata { struct input_dev *tp_kbd_input; struct asus_worker *worker; unsigned int kbd_backlight_brightness; + struct ally_handheld *rog_ally; const struct asus_touchpad_info *tp; struct power_supply *battery; struct power_supply_desc battery_desc; @@ -232,12 +278,861 @@ static const struct asus_touchpad_info medion_e1239t= _tp =3D { .report_size =3D 32 /* 2 byte header + 5 * 5 + 5 byte footer */, }; =20 +enum ally_command_codes { + CMD_SET_GAMEPAD_MODE =3D 0x01, + CMD_SET_MAPPING =3D 0x02, + CMD_SET_JOYSTICK_MAPPING =3D 0x03, + CMD_SET_JOYSTICK_DEADZONE =3D 0x04, + CMD_SET_TRIGGER_RANGE =3D 0x05, + CMD_SET_VIBRATION_INTENSITY =3D 0x06, + CMD_LED_CONTROL =3D 0x08, + CMD_CHECK_READY =3D 0x0A, + CMD_SET_XBOX_CONTROLLER =3D 0x0B, + CMD_CHECK_XBOX_SUPPORT =3D 0x0C, + CMD_USER_CAL_DATA =3D 0x0D, + CMD_CHECK_USER_CAL_SUPPORT =3D 0x0E, + CMD_SET_TURBO_PARAMS =3D 0x0F, + CMD_CHECK_TURBO_SUPPORT =3D 0x10, + CMD_CHECK_RESP_CURVE_SUPPORT =3D 0x12, + CMD_SET_RESP_CURVE =3D 0x13, + CMD_CHECK_DIR_TO_BTN_SUPPORT =3D 0x14, + CMD_SET_GYRO_PARAMS =3D 0x15, + CMD_CHECK_GYRO_TO_JOYSTICK =3D 0x16, + CMD_CHECK_ANTI_DEADZONE =3D 0x17, + CMD_SET_ANTI_DEADZONE =3D 0x18, +}; + +static const u8 ALLY_FORCE_FEEDBACK_OFF[] =3D { + 0x0D, 0x0F, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xEB +}; + +/* + * The ROG Ally device presents multiple USB interfaces (keyboard, mouse, = gamepad, + * and custom configuration interface) that bind to the same module. Since= only + * one ROG Ally device can be connected at a time, we use a single global = static + * ally_handheld structure to share state across these separate HID interf= aces. + */ +static void ally_resume_work_fn(struct work_struct *work); + +/* + * Changes to ally_drvdata must lock: the raw_event callbacks, which may + * run in atomic (URB completion) context, also take this lock, so it must + * be a spinlock. + */ +static DEFINE_SPINLOCK(ally_data_lock); +static struct ally_handheld ally_drvdata =3D { + .intf_mutex =3D __MUTEX_INITIALIZER(ally_drvdata.intf_mutex), + /* + * Initialised statically so it is always safe to cancel, whichever + * of the interfaces probed or failed to probe. + */ + .resume_work =3D __DELAYED_WORK_INITIALIZER(ally_drvdata.resume_work, + ally_resume_work_fn, 0), +}; + +/* + * Drop the recorded USB device if no Ally interface is bound anymore: + * probe failures never run remove(), so without this cleanup a failed + * probe would leave a stale record behind and the driver would then + * reject the real controller when it reconnects. + */ +static void ally_put_udev_if_orphaned(void) +{ + struct usb_device *udev; + unsigned long flags; + + spin_lock_irqsave(&ally_data_lock, flags); + if (!ally_drvdata.udev || ally_drvdata.keyboard_hdev || + ally_drvdata.cfg_hdev || ally_drvdata.ally_x_hdev) { + spin_unlock_irqrestore(&ally_data_lock, flags); + return; + } + + udev =3D ally_drvdata.udev; + ally_drvdata.udev =3D NULL; + spin_unlock_irqrestore(&ally_data_lock, flags); + + usb_put_dev(udev); +} + static const u8 asus_report_id_init[] =3D { FEATURE_KBD_REPORT_ID, FEATURE_KBD_LED_REPORT_ID1, FEATURE_KBD_LED_REPORT_ID2 }; =20 +static inline int ally_dev_set_report(struct hid_device *hdev, const u8 *b= uf, size_t len) +{ + u8 *dmabuf __free(kfree) =3D kmemdup(buf, len, GFP_KERNEL); + if (!dmabuf) + return -ENOMEM; + + return hid_hw_raw_request(hdev, buf[0], dmabuf, len, + HID_FEATURE_REPORT, HID_REQ_SET_REPORT); +} + +static inline int ally_dev_get_report(struct hid_device *hdev, u8 *out, si= ze_t len) +{ + return hid_hw_raw_request(hdev, HID_ALLY_GET_REPORT_ID, out, len, + HID_FEATURE_REPORT, HID_REQ_GET_REPORT); +} + +static void ally_resume_work_fn(struct work_struct *work) +{ + struct ally_handheld *ally =3D container_of(work, struct ally_handheld, + resume_work.work); + struct input_dev *keyboard_input, *x_input; + unsigned long flags; + + /* + * Snapshot the very pointers that get dereferenced under the lock + * protecting them, and take a reference on each input_dev: probe + * sets keyboard_hdev even when the interface exposes no input_dev, + * removal clears the two fields one after the other, and the input + * devices are freed with their own interface. + */ + spin_lock_irqsave(&ally_data_lock, flags); + keyboard_input =3D input_get_device(ally->keyboard_input); + x_input =3D input_get_device(ally->ally_x_input); + spin_unlock_irqrestore(&ally_data_lock, flags); + + /* + * Force release all vendor buttons to prevent "stuck" ghosting on + * resume (workaround for Ally X USB re-probing during suspend/resume). + */ + if (keyboard_input) { + input_report_key(keyboard_input, KEY_F16, 0); + input_report_key(keyboard_input, KEY_F17, 0); + input_report_key(keyboard_input, KEY_F18, 0); + input_report_key(keyboard_input, KEY_F19, 0); + input_report_key(keyboard_input, KEY_F20, 0); + input_report_key(keyboard_input, KEY_PROG1, 0); + input_sync(keyboard_input); + input_put_device(keyboard_input); + } + + if (x_input) { + input_report_key(x_input, KEY_F16, 0); + input_report_key(x_input, KEY_F17, 0); + input_report_key(x_input, KEY_F18, 0); + input_report_key(x_input, KEY_PROG1, 0); + input_sync(x_input); + input_put_device(x_input); + } +} + +/** + * handle_ctrl_alt_del() - detect a left button long press + * @ally: ally handheld structure 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 t= hat + * and emit only a single code for that single event. + * + * Return: true iff the event has been managed + */ +static bool handle_ctrl_alt_del(struct ally_handheld *ally, u8 *data, int = size) +{ + bool time_is_past =3D time_after(jiffies, ally->cad_last_event_time + mse= cs_to_jiffies(100)); + + if (size < 16 || data[0] !=3D 0x01) + return false; + + if (ally->cad_sequence_state > 0 && time_is_past) + ally->cad_sequence_state =3D 0; + + ally->cad_last_event_time =3D jiffies; + + switch (ally->cad_sequence_state) { + case 0: + if (data[1] =3D=3D 0x01 && data[2] =3D=3D 0x00 && data[3] =3D=3D 0x00) { + ally->cad_sequence_state =3D 1; + data[1] =3D 0x00; + return true; + } + break; + case 1: + if (data[1] =3D=3D 0x05 && data[2] =3D=3D 0x00 && data[3] =3D=3D 0x00) { + ally->cad_sequence_state =3D 2; + data[1] =3D 0x00; + return true; + } + break; + case 2: + if (data[1] =3D=3D 0x05 && data[2] =3D=3D 0x00 && data[3] =3D=3D 0x4c) { + ally->cad_sequence_state =3D 3; + data[1] =3D 0x00; + data[3] =3D 0x6F; /* F20 */ + return true; + } + break; + case 3: + if (data[1] =3D=3D 0x04 && data[2] =3D=3D 0x00 && data[3] =3D=3D 0x4c) { + ally->cad_sequence_state =3D 4; + data[1] =3D data[3] =3D 0x00; + return true; + } + break; + case 4: + if (data[1] =3D=3D 0x00 && data[2] =3D=3D 0x00 && data[3] =3D=3D 0x4c) { + ally->cad_sequence_state =3D 5; + data[3] =3D 0x00; + return true; + } + break; + } + ally->cad_sequence_state =3D 0; + return false; +} + +static bool handle_ally_event(struct hid_device *hdev, struct ally_handhel= d *ally, + u8 *data, int size) +{ + struct input_dev *keyboard_input; + unsigned long flags; + int keycode =3D 0; + + if (size < 2) + return false; + + if (data[0] =3D=3D 0x5A) { + switch (data[1]) { + case 0x38: + keycode =3D KEY_F19; + break; + case 0xA6: + keycode =3D KEY_F16; + break; + case 0xA7: + keycode =3D KEY_F17; + break; + case 0xA8: + keycode =3D KEY_F18; + break; + default: + return false; + } + + /* + * Take a reference on the input_dev before dropping the lock: + * the keyboard interface can be unbound concurrently, and its + * input_dev is freed with it. + */ + spin_lock_irqsave(&ally_data_lock, flags); + keyboard_input =3D input_get_device(ally->keyboard_input); + spin_unlock_irqrestore(&ally_data_lock, flags); + + if (!keyboard_input) + return false; + + input_report_key(keyboard_input, keycode, 1); + input_sync(keyboard_input); + input_report_key(keyboard_input, keycode, 0); + input_sync(keyboard_input); + input_put_device(keyboard_input); + return true; + } + return false; +} + +/** + * ally_gamepad_send_packet() - Send a raw packet to the gamepad device + * @ally: ally handheld structure + * @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 ally_handheld *ally, + struct hid_device *hdev, const u8 *buf, size_t len) +{ + guard(mutex)(&ally->intf_mutex); + return ally_dev_set_report(hdev, buf, len); +} + +/** + * ally_gamepad_send_receive_packet() - Send a packet and receive the resp= onse + * @ally: ally handheld structure + * @hdev: HID device + * @buf: buffer containing the packet data to send and receive response in + * @len: length of buffer + * + * Return: count of data transferred, negative if error + */ +static int ally_gamepad_send_receive_packet(struct ally_handheld *ally, + struct hid_device *hdev, + u8 *buf, size_t len) +{ + int ret; + + scoped_guard(mutex, &ally->intf_mutex) { + ret =3D ally_dev_set_report(hdev, buf, len); + if (ret >=3D 0) { + memset(buf, 0, len); + ret =3D ally_dev_get_report(hdev, buf, len); + } + } + + return ret; +} + +/** + * ally_alloc_cmd() - Construct a command buffer for the gamepad + * @cmd: Command code to send + * @payload: Optional payload data to include in the command + * @payload_size: Size of the payload data + * + * The constructed buffer is 64 bytes long, 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(u8 cmd, const u8 *payload, u8 payload_size) +{ + u8 *hidbuf; + + /* + * The payload is written after the 4-byte command header: + * reject one that would spill past the end of the report. + */ + if (payload_size > ROG_ALLY_REPORT_SIZE - 4) + return NULL; + + hidbuf =3D kzalloc(ROG_ALLY_REPORT_SIZE, GFP_KERNEL); + + if (!hidbuf) + return NULL; + + hidbuf[0] =3D HID_ALLY_SET_REPORT_ID; + hidbuf[1] =3D HID_ALLY_FEATURE_CODE_PAGE; + hidbuf[2] =3D cmd; + hidbuf[3] =3D payload_size; + + if (payload_size > 0 && payload) + memcpy(&hidbuf[4], payload, payload_size); + + return hidbuf; +} + +/** + * ally_gamepad_check_ready() - Wait for the gamepad MCU to report ready + * @ally: ally handheld structure + * @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 ally_handheld *ally, struct hid= _device *hdev) +{ + u8 payload[] =3D { 0x00 }; + int ret; + + for (int i =3D 0; i < HID_ALLY_READY_MAX_TRIES; i++) { + u8 *buf __free(kfree) =3D ally_alloc_cmd(CMD_CHECK_READY, payload, sizeo= f(payload)); + if (!buf) + return -ENOMEM; + + ret =3D ally_gamepad_send_receive_packet(ally, hdev, buf, ROG_ALLY_REPOR= T_SIZE); + if (ret < 0) { + hid_dbg(hdev, "ROG Ally check %d/%d failed: %d\n", i, + HID_ALLY_READY_MAX_TRIES, ret); + continue; + } + + if (buf[2] =3D=3D CMD_CHECK_READY) + return 0; + + 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 =3D to_usb_interface(hdev->dev.parent); + if (!intf || !intf->cur_altsetting) + return -ENODEV; + + if (intf->cur_altsetting->desc.bNumEndpoints =3D=3D 0) + return -ENODEV; + + ep =3D 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] =3D { + { 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; + + /* + * The size check above only guarantees one valid byte: do not + * read the code of a truncated report. + */ + if (data[0] =3D=3D 0x5A) { + if (size < 2) + return false; + + input_report_key(input, KEY_PROG1, data[1] =3D=3D 0x38); + input_report_key(input, KEY_F16, data[1] =3D=3D 0xA6); + input_report_key(input, KEY_F17, data[1] =3D=3D 0xA7); + input_report_key(input, KEY_F18, data[1] =3D=3D 0xA8); + input_sync(input); + + return data[1] =3D=3D 0xA6 || data[1] =3D=3D 0xA7 || data[1] =3D=3D 0xA8= || data[1] =3D=3D 0x38; + } + + if (data[0] !=3D HID_ALLY_X_INPUT_REPORT) + return false; + + /* + * hid-core only guarantees size >=3D 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 =3D (struct ally_x_input_report *)&data[1]; + + /* USB HID payloads are little-endian: convert them explicitly. */ + x =3D get_unaligned_le16(&in_report->x); + y =3D get_unaligned_le16(&in_report->y); + rx =3D get_unaligned_le16(&in_report->rx); + ry =3D get_unaligned_le16(&in_report->ry); + z =3D get_unaligned_le16(&in_report->z); + rz =3D 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 =3D 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 =3D 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 =3D in_report->buttons[2]; + if (byte >=3D ARRAY_SIZE(hat_values)) + byte =3D 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 hid_device *hdev =3D input_get_drvdata(dev); + + return hid_hw_open(hdev); +} + +static void ally_x_input_close(struct input_dev *dev) +{ + hid_hw_close(input_get_drvdata(dev)); +} + +static struct input_dev *ally_x_alloc_input_dev(struct hid_device *hdev) +{ + struct input_dev *input_dev =3D input_allocate_device(); + + if (!input_dev) + return ERR_PTR(-ENOMEM); + + input_dev->dev.parent =3D &hdev->dev; + input_dev->id.bustype =3D hdev->bus; + input_dev->id.vendor =3D hdev->vendor; + input_dev->id.product =3D hdev->product; + input_dev->id.version =3D hdev->version; + input_dev->uniq =3D hdev->uniq; + input_dev->name =3D "ASUS ROG Ally X Gamepad"; + + input_set_drvdata(input_dev, hdev); + /* + * 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 =3D ally_x_input_open; + input_dev->close =3D ally_x_input_close; + + return input_dev; +} + +static int ally_x_setup_input(struct hid_device *hdev, struct ally_handhel= d *ally) +{ + struct input_dev *input =3D ally_x_alloc_input_dev(hdev); + unsigned long flags; + 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); + + input_set_capability(input, EV_KEY, KEY_PROG1); + input_set_capability(input, EV_KEY, KEY_F16); + input_set_capability(input, EV_KEY, KEY_F17); + input_set_capability(input, EV_KEY, KEY_F18); + input_set_capability(input, EV_KEY, BTN_TRIGGER_HAPPY); + input_set_capability(input, EV_KEY, BTN_TRIGGER_HAPPY1); + + ret =3D input_register_device(input); + if (ret) { + hid_err(hdev, "Failed to register Ally X gamepad device: %d\n", ret); + goto ally_x_setup_input_err; + } + + /* Publish the input_dev only when it is fully set up. */ + spin_lock_irqsave(&ally_data_lock, flags); + ally->ally_x_input =3D input; + spin_unlock_irqrestore(&ally_data_lock, flags); + + return 0; +ally_x_setup_input_err: + input_free_device(input); + return ret; +} + +static int hid_asus_ally_init(struct hid_device *hdev, struct ally_handhel= d *ally) +{ + int ret; + + /* Failure at this point is non-critical */ + ret =3D ally_gamepad_send_packet(ally, 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 the Ally interfaces + * @hdev: HID device + * @ally: ally handheld structure + * @report: HID report the raw data arrived in + * @data: raw report buffer + * @size: length of @data in bytes + * + * Called from the raw_event callback, which may run in atomic (URB + * completion) context: only spinlock-protected accesses to the shared + * state are allowed here. + * + * Return: true if the event was handled, otherwise false + */ +static bool hid_asus_ally_raw_event(struct hid_device *hdev, struct ally_h= andheld *ally, + struct hid_report *report, u8 *data, int size) +{ + struct input_dev *x_input; + unsigned long flags; + bool handled; + + if (!ally) + return false; + + switch (ally_get_endpoint_address(hdev)) { + case HID_ALLY_X_INTF_IN: + /* + * Take a reference on the input_dev while using it: the + * gamepad interface can be unbound concurrently, and its + * input_dev is freed with it. + */ + spin_lock_irqsave(&ally_data_lock, flags); + x_input =3D input_get_device(ally->ally_x_input); + spin_unlock_irqrestore(&ally_data_lock, flags); + + handled =3D ally_x_raw_event(x_input, data, size); + input_put_device(x_input); + if (handled) + return true; + break; + case HID_ALLY_INTF_CFG_IN: + if (handle_ally_event(hdev, ally, data, size)) + return true; + break; + case HID_ALLY_INTF_KEYBOARD_IN: + if (handle_ctrl_alt_del(ally, data, size)) + return false; + break; + default: + break; + } + + return false; +} + +/** + * hid_asus_ally_probe() - Initialize the ROG Ally HID extension + * @hdev: HID device + * + * This module works alongside the main Asus HID driver to handle + * Ally-specific features and quirks. + * + * Return: an ally_handheld struct pointer on success, an ERR_PTR on + * failure. The caller is not expected to use the returned pointer, but it + * should check for errors by using IS_ERR and PTR_ERR and pass NULL to + * other functions if there was an error. + */ +static struct ally_handheld *hid_asus_ally_probe(struct hid_device *hdev) +{ + unsigned long flags; + int ret, ep =3D ally_get_endpoint_address(hdev); + struct usb_device *udev; + struct hid_input *hidinput; + + if (ep < 0) + return ERR_PTR(ep); + + /* + * The ROG Ally controller is integrated into a handheld PC, so at + * most one device can exist and the shared global state relies on + * that: reject a probe from a different USB device, or a spoofed + * peripheral could overwrite the state of the real controller. The + * recorded device is dropped when the last interface of the + * controller is unbound, so the re-enumeration the embedded + * controller performs after a suspend cycle (it cuts the controller + * power when powersave is enabled) is still accepted. + */ + udev =3D interface_to_usbdev(to_usb_interface(hdev->dev.parent)); + + spin_lock_irqsave(&ally_data_lock, flags); + if (ally_drvdata.udev && ally_drvdata.udev !=3D udev) { + spin_unlock_irqrestore(&ally_data_lock, flags); + hid_err(hdev, "A ROG Ally controller is already connected\n"); + return ERR_PTR(-ENODEV); + } + + /* + * Interfaces of an already connected controller are equally + * untrusted: a crafted descriptor could present the same + * endpoint twice, and a second probe of one role would evict + * the state recorded by the first one, whose input_dev would + * then never be unregistered. + */ + if ((ep =3D=3D HID_ALLY_INTF_CFG_IN && ally_drvdata.cfg_hdev) || + (ep =3D=3D HID_ALLY_X_INTF_IN && ally_drvdata.ally_x_hdev) || + (ep =3D=3D HID_ALLY_INTF_KEYBOARD_IN && ally_drvdata.keyboard_hdev)) { + spin_unlock_irqrestore(&ally_data_lock, flags); + hid_err(hdev, + "An interface of a ROG Ally controller is already connected\n"); + return ERR_PTR(-ENODEV); + } + + if (!ally_drvdata.udev) { + usb_get_dev(udev); + ally_drvdata.udev =3D udev; + } + spin_unlock_irqrestore(&ally_data_lock, flags); + + /* + * The interface initialization sleeps (it performs USB transfers), + * so it must run before taking the spinlock guarding the shared + * state; pointers are published only once it succeeded. + */ + switch (ep) { + case HID_ALLY_INTF_CFG_IN: + /* + * This function assumes the asus-specific initialization + * to have been performed already at this point. + */ + ret =3D ally_gamepad_check_ready(&ally_drvdata, hdev); + if (ret < 0) { + hid_err(hdev, "ROG Ally device is not ready: %d\n", ret); + ally_put_udev_if_orphaned(); + return ERR_PTR(ret); + } + + ret =3D hid_asus_ally_init(hdev, &ally_drvdata); + if (ret < 0) { + ally_put_udev_if_orphaned(); + return ERR_PTR(ret); + } + + spin_lock_irqsave(&ally_data_lock, flags); + ally_drvdata.cfg_hdev =3D hdev; + spin_unlock_irqrestore(&ally_data_lock, flags); + break; + case HID_ALLY_X_INTF_IN: + /* This will create and populate ally_x_input */ + ret =3D ally_x_setup_input(hdev, &ally_drvdata); + if (ret) { + hid_err(hdev, "Failed to create Ally X gamepad device.\n"); + ally_put_udev_if_orphaned(); + return ERR_PTR(ret); + } + + spin_lock_irqsave(&ally_data_lock, flags); + ally_drvdata.ally_x_hdev =3D hdev; + spin_unlock_irqrestore(&ally_data_lock, flags); + break; + case HID_ALLY_INTF_KEYBOARD_IN: + spin_lock_irqsave(&ally_data_lock, flags); + ally_drvdata.keyboard_hdev =3D hdev; + if (!list_empty(&hdev->inputs)) { + hidinput =3D list_first_entry(&hdev->inputs, struct hid_input, list); + ally_drvdata.keyboard_input =3D hidinput->input; + } + spin_unlock_irqrestore(&ally_data_lock, flags); + break; + default: + /* This is normally supposed to happen */ + break; + } + + return &ally_drvdata; +} + +static void hid_asus_ally_remove(struct hid_device *hdev, struct ally_hand= held *ally) +{ + struct input_dev *x_input =3D NULL; + unsigned long flags; + + if (!ally) + return; + + /* + * Any of the three interfaces can own an input_dev the resume work + * reports through, and they are torn down in an arbitrary order, so + * drain it before clearing anything. Cancel outside ally_data_lock so + * a handler that wants the lock cannot deadlock against us. + */ + cancel_delayed_work_sync(&ally->resume_work); + + spin_lock_irqsave(&ally_data_lock, flags); + if (ally->ally_x_hdev =3D=3D hdev) { + x_input =3D ally->ally_x_input; + ally->ally_x_input =3D NULL; + ally->ally_x_hdev =3D NULL; + } + + /* + * The keyboard interface is torn down before the config one, and + * its input_dev is freed with it. handle_ally_event() and + * ally_resume_work_fn() both report keys through it from the + * config endpoint, so drop the references here or they dangle. + */ + if (ally->keyboard_hdev =3D=3D hdev) { + ally->keyboard_input =3D NULL; + ally->keyboard_hdev =3D NULL; + } + spin_unlock_irqrestore(&ally_data_lock, flags); + + if (x_input) + input_unregister_device(x_input); + + /* + * Drop the recorded USB device when the last interface of the + * controller has been unbound: the driver is then ready to accept + * the controller again when it reconnects. + */ + ally_put_udev_if_orphaned(); +} + +static int hid_asus_ally_reset_resume(struct hid_device *hdev, struct ally= _handheld *ally) +{ + int ep, ret; + + /* + * The extensions failed to probe and the device is operating as a + * generic HID device: do not fail the resume because of that. + */ + if (!ally) + return 0; + + ep =3D ally_get_endpoint_address(hdev); + if (ep !=3D HID_ALLY_INTF_CFG_IN) + return 0; + + ret =3D hid_asus_ally_init(hdev, ally); + if (ret < 0) + return ret; + + return 0; +} + /* * Send events to asus-wmi driver for handling special keys */ @@ -505,6 +1400,17 @@ static int asus_raw_event(struct hid_device *hdev, if (drvdata->quirks & QUIRK_MEDION_E1239T) return asus_e1239t_event(drvdata, data, size); =20 + if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) { + /* + * Return -1 to suppress further processing by the generic HID + * input parser for reports we fully handle for the Gamepad (0x0B): + * letting 0x0B reach the default parser creates a generic gamepad + * causing Steam Input overlaps (i.e. L1 stuck on screenshot). + */ + if (hid_asus_ally_raw_event(hdev, drvdata->rog_ally, report, 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'. @@ -1254,6 +2160,7 @@ static int asus_input_mapping(struct hid_device *hdev, case 0x5f: asus_map_key_clear(KEY_PROG2); break; /* S-shaped programmabl= e 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 */ @@ -1385,6 +2292,8 @@ static void asus_initialize_reports(struct hid_device= *hdev) static int __maybe_unused asus_resume(struct hid_device *hdev) { struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *ally =3D drvdata->rog_ally; + int ep; =20 /* * If we have a backlight listener registered, restore the previous state, @@ -1394,12 +2303,19 @@ static int __maybe_unused asus_resume(struct hid_de= vice *hdev) if (drvdata->listener.brightness_set) asus_kbd_backlight_set(&drvdata->listener, drvdata->kbd_backlight_bright= ness); =20 + if (ally && (drvdata->quirks & QUIRK_ROG_ALLY_XPAD)) { + ep =3D ally_get_endpoint_address(hdev); + if (ep =3D=3D HID_ALLY_INTF_CFG_IN) + schedule_delayed_work(&ally->resume_work, msecs_to_jiffies(500)); + } + return 0; } =20 static int __maybe_unused asus_reset_resume(struct hid_device *hdev) { struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + int ret; =20 if (!drvdata->tp) asus_initialize_reports(hdev); @@ -1407,6 +2323,14 @@ static int __maybe_unused asus_reset_resume(struct h= id_device *hdev) if (drvdata->tp) return asus_start_multitouch(hdev); =20 + if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) { + ret =3D hid_asus_ally_reset_resume(hdev, drvdata->rog_ally); + if (ret) { + hid_err(hdev, "Failed to resume ROG Ally HID extensions: %d\n", ret); + return ret; + } + } + return 0; } =20 @@ -1414,6 +2338,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_handheld *ally; struct hid_report *rep; bool is_vendor =3D false; int ret; @@ -1527,6 +2452,15 @@ static int asus_probe(struct hid_device *hdev, const= struct hid_device_id *id) (asus_kbd_register_leds(hdev))) hid_warn(hdev, "Failed to initialize backlight.\n"); =20 + if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) { + ally =3D 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 + drvdata->rog_ally =3D ally; + } + /* * For ROG keyboards, skip rename for consistency and ->input check as * some devices do not have inputs. @@ -1558,6 +2492,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); =20 + if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) + hid_asus_ally_remove(hdev, drvdata->rog_ally); + asus_worker_stop(drvdata->worker); hid_hw_stop(hdev); return ret; @@ -1570,6 +2507,9 @@ static void asus_remove(struct hid_device *hdev) if (drvdata->listener.brightness_set) asus_hid_unregister_listener(&drvdata->listener); =20 + if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) + hid_asus_ally_remove(hdev, drvdata->rog_ally); + asus_worker_stop(drvdata->worker); hid_hw_stop(hdev); } --=20 2.47.3 From nobody Fri Sep 25 20:46:57 2026 Received: from mta0.migadu.com (out-224.mta0.migadu.com [91.218.175.224]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id F419F598C05 for ; Tue, 8 Sep 2026 17:44:52 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.224 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788889495; cv=none; b=LQ+qbDDtlZbS0A9Ekmp1fUgPzsgW01Ss8j3KiZbfig9ay0+eBVFJjWC4pAe7fRLpjllMinODWzDYzta16aOx1YkmcOrkNp54P0MOJc8Pj2kxFA0AnlzJHC5IFIPfu6xhmhNkKdDu97+BbGhcbc0bmvIs9eYoaT7KDkb1hNjyxkY= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788889495; c=relaxed/simple; bh=Btv4p86MU7EaCbd146hZ9oPrOI4ivG1s8RDFx2v47rc=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=Ag7XKwVQQ5G8ViiZeEepoDva3qU+Vw25KdJyaclyQbxgai7Y1yWOZnlNZOY+QyH15WA1nVIOqEtOE0kUkARNxaaajgfZJ3lz1No9w4Y/SgwEBm4E8txtKm/mA4pORjpsr0E4d24cNe4Q8P06EN7u7JdkkWltvW0eS+J+NnkwUwg= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=mY2qSclW; arc=none smtp.client-ip=91.218.175.224 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="mY2qSclW" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=Btv4p86MU7EaCbd146hZ9oPrOI4ivG1s8RDFx2v47rc=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1788889490; v=1; x=1789494290; b=mY2qSclW0R8adCA+bowREKCGAHj0on7uj64E9Vy1XOW9ilIR/PpC4qDA/SkwRuZTfdMlxdh1 /EW2hJghMpR35JYy/Ultz3DoLZ7cTxDfzeqKAXFI9Zip311gBYyAewmcKPmb9LbLPsOGghh2hS+ ENkANiHP53H0JWg9jomhwG5E= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id 11179b3360fe0618; Tue, 08 Sep 2026 17:44:50 +0000 X-Mizu-Trace-ID: 11179b3360fe0618 X-Migadu-Flow: FLOW_OUT From: Denis Benato To: linux-kernel@vger.kernel.org Cc: linux-input@vger.kernel.org, "Benjamin Tissoires" , "Jiri Kosina" , "Luke D . Jones" , "Mateusz Schwartz" , "Denis Benato" , "Jonathan LoBue" , "Khamunetri Clark" , "Derek J. Clark" , Denis Benato Subject: [PATCH v6 04/13] HID: asus: add gamepad configuration Date: Tue, 8 Sep 2026 17:44:24 +0000 Message-ID: <20260908174433.74260-5-denis.benato@linux.dev> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260908174433.74260-1-denis.benato@linux.dev> References: <20260908174433.74260-1-denis.benato@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Add the base configuration structures for the gamepad configuration, detect capabilities and initialize the device in a known state. Assisted-by: opencode:glm-5.2 Assisted-by: Claude:claude-fable-5 Signed-off-by: Denis Benato Signed-off-by: Luke Jones Signed-off-by: Jonathan LoBue --- drivers/hid/hid-asus.c | 390 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 389 insertions(+), 1 deletion(-) diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c index 11dea2aec80b..ac0c0a176857 100644 --- a/drivers/hid/hid-asus.c +++ b/drivers/hid/hid-asus.c @@ -22,6 +22,7 @@ =20 #include #include +#include #include #include #include @@ -34,6 +35,7 @@ #include /* For to_usb_interface for T100 touchpad intf chec= k */ #include #include +#include #include #include =20 @@ -176,6 +178,45 @@ struct asus_touchpad_info { int report_size; }; =20 +struct ally_config { + /* Must be locked if the data is being changed */ + struct mutex config_mutex; + bool initialized; + + /* Device capabilities flags */ + 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; + + /* Current settings */ + bool xbox_controller_enabled; + u8 gamepad_mode; + u8 left_deadzone; + u8 left_outer_threshold; + u8 right_deadzone; + u8 right_outer_threshold; + u8 left_anti_deadzone; + u8 right_anti_deadzone; + u8 left_trigger_min; + u8 left_trigger_max; + u8 right_trigger_min; + u8 right_trigger_max; +}; + +/* + * The Ally configuration is allocated when the controller first appears a= nd + * is released only on module removal: the embedded controller can disappe= ar + * and re-enumerate at any time (it powers off during suspend when powersa= ve + * is enabled), and on re-probe the driver must re-apply the recorded + * configuration instead of factory defaults. + */ +static struct ally_config *ally_config; + struct ally_handheld { /* All read/write to IN interfaces must lock */ struct mutex intf_mutex; @@ -197,6 +238,8 @@ struct ally_handheld { unsigned long cad_last_event_time; =20 struct delayed_work resume_work; + + struct ally_config *config; }; =20 struct asus_drvdata { @@ -617,6 +660,274 @@ static u8 *ally_alloc_cmd(u8 cmd, const u8 *payload, = u8 payload_size) return hidbuf; } =20 +/** + * ally_check_capability() - Check if a specific capability is supported + * @hdev: HID device + * @ally: ally handheld structure + * @check_cmd: capability command code to query + * + * Return: true if the capability is supported, false otherwise + */ +static bool ally_check_capability(struct hid_device *hdev, struct ally_han= dheld *ally, + enum ally_command_codes check_cmd) +{ + u8 payload[] =3D { 0x00 }; + int ret; + + u8 *buf __free(kfree) =3D ally_alloc_cmd(check_cmd, payload, sizeof(paylo= ad)); + if (!buf) { + hid_err(hdev, "Failed to allocate buffer for capability check.\n"); + return false; + } + + ret =3D ally_gamepad_send_receive_packet(ally, hdev, buf, ROG_ALLY_REPORT= _SIZE); + if (ret < 0) { + hid_err(hdev, "Failed to check capability 0x%02x: %d\n", check_cmd, ret); + return false; + } + + return buf[1] =3D=3D HID_ALLY_FEATURE_CODE_PAGE && buf[2] =3D=3D check_cm= d && + buf[4] =3D=3D 0x01; +} + +static int ally_detect_capabilities(struct hid_device *hdev, struct ally_h= andheld *ally, + struct ally_config *cfg) +{ + if (!hdev || !cfg || !ally) + return -EINVAL; + + scoped_guard(mutex, &cfg->config_mutex) { + cfg->is_ally_x =3D (hdev->product =3D=3D USB_DEVICE_ID_ASUSTEK_ROG_NKEY_= ALLY_X); + + cfg->xbox_controller_support =3D + ally_check_capability(hdev, ally, CMD_CHECK_XBOX_SUPPORT); + cfg->user_cal_support =3D + ally_check_capability(hdev, ally, CMD_CHECK_USER_CAL_SUPPORT); + cfg->turbo_support =3D + ally_check_capability(hdev, ally, CMD_CHECK_TURBO_SUPPORT); + cfg->resp_curve_support =3D + ally_check_capability(hdev, ally, CMD_CHECK_RESP_CURVE_SUPPORT); + cfg->dir_to_btn_support =3D + ally_check_capability(hdev, ally, CMD_CHECK_DIR_TO_BTN_SUPPORT); + cfg->gyro_support =3D + ally_check_capability(hdev, ally, CMD_CHECK_GYRO_TO_JOYSTICK); + cfg->anti_deadzone_support =3D + ally_check_capability(hdev, ally, CMD_CHECK_ANTI_DEADZONE); + } + + return 0; +} + +static int ally_set_xbox_controller(struct hid_device *hdev, + struct ally_handheld *ally, + struct ally_config *cfg, bool enabled) +{ + u8 payload[] =3D { enabled ? 0x01 : 0x00 }; + int ret; + + if (!cfg || !cfg->xbox_controller_support) + return -ENODEV; + + u8 *buf __free(kfree) =3D ally_alloc_cmd(CMD_SET_XBOX_CONTROLLER, payload= , sizeof(payload)); + if (!buf) + return -ENOMEM; + + ret =3D ally_gamepad_send_packet(ally, hdev, buf, ROG_ALLY_REPORT_SIZE); + if (ret < 0) { + hid_err(hdev, "Failed to set Xbox controller mode: %d\n", ret); + return ret; + } + + cfg->xbox_controller_enabled =3D enabled; + return 0; +} + +/** + * ally_get_config() - Get the configuration of the Ally device + * @ally: ally handheld structure + * + * Fetch the configuration published by hid_asus_ally_probe() under + * ally_data_lock: the pointer is also cleared by hid_asus_ally_remove(), + * so reading it without the lock would race with interface removal. + * + * The returned configuration outlives the sysfs callbacks using it: it is + * allocated once on the ally probe path and released only on module + * removal, when every sysfs attribute referencing it is gone. + * + * Return: the ally config, or NULL if no configuration is published + */ +static struct ally_config *ally_get_config(struct ally_handheld *ally) +{ + struct ally_config *cfg; + unsigned long flags; + + spin_lock_irqsave(&ally_data_lock, flags); + cfg =3D ally->config; + spin_unlock_irqrestore(&ally_data_lock, flags); + + return cfg; +} + +static ssize_t xbox_controller_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *ally =3D drvdata->rog_ally; + struct ally_config *cfg; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + guard(mutex)(&cfg->config_mutex); + + if (!cfg->xbox_controller_support) + return -ENODEV; + + return sysfs_emit(buf, "%d\n", cfg->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 =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *ally =3D drvdata->rog_ally; + struct ally_config *cfg; + bool enabled; + int ret; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + ret =3D kstrtobool(buf, &enabled); + if (ret) + return ret; + + guard(mutex)(&cfg->config_mutex); + + if (!cfg->xbox_controller_support) + return -ENODEV; + + ret =3D ally_set_xbox_controller(hdev, ally, cfg, enabled); + if (ret < 0) + return ret; + + return count; +} + +static DEVICE_ATTR_RW(xbox_controller); + +static struct attribute *ally_config_attrs[] =3D { + &dev_attr_xbox_controller.attr, + NULL +}; + +static const struct attribute_group ally_attr_groups[] =3D { + { + .attrs =3D ally_config_attrs, + }, +}; + +/** + * ally_config_create() - Initialize configuration and create sysfs entries + * @hdev: HID device + * @ally: non-NULL ally device data with uninitialized config pointer + * + * Return: valid pointer on success, error pointer on failure + */ +static struct ally_config *ally_config_create(struct hid_device *hdev, str= uct ally_handheld *ally) +{ + struct ally_config *cfg; + unsigned long flags; + int ret, sysfs_i; + + /* + * Allocate the configuration only once and keep it until module + * removal: the controller can disappear and re-enumerate while + * suspended, and the recorded settings must survive that to be + * re-applied when the controller comes back. + */ + if (!ally_config) { + ally_config =3D kzalloc_obj(*ally_config, GFP_KERNEL); + if (!ally_config) + return ERR_PTR(-ENOMEM); + + mutex_init(&ally_config->config_mutex); + } + + cfg =3D ally_config; + + ret =3D ally_detect_capabilities(hdev, ally, cfg); + if (ret < 0) { + hid_err(hdev, "Failed to detect Ally capabilities: %d\n", ret); + goto ally_config_create_err; + } + + if (!cfg->initialized) { + cfg->gamepad_mode =3D 0x01; + cfg->xbox_controller_enabled =3D true; + cfg->left_deadzone =3D 10; + cfg->left_outer_threshold =3D 90; + cfg->right_deadzone =3D 10; + cfg->right_outer_threshold =3D 90; + } + + spin_lock_irqsave(&ally_data_lock, flags); + ally->config =3D cfg; + spin_unlock_irqrestore(&ally_data_lock, flags); + + for (sysfs_i =3D 0; sysfs_i < ARRAY_SIZE(ally_attr_groups); sysfs_i++) { + ret =3D sysfs_create_group(&hdev->dev.kobj, + &ally_attr_groups[sysfs_i]); + if (ret < 0) { + hid_err(hdev, "Failed to create sysfs group '%s': %d\n", + ally_attr_groups[sysfs_i].name ?: "", ret); + goto ally_config_create_sysfs_err; + } + } + + cfg->initialized =3D true; + + return cfg; +ally_config_create_sysfs_err: + spin_lock_irqsave(&ally_data_lock, flags); + ally->config =3D NULL; + spin_unlock_irqrestore(&ally_data_lock, flags); + + /* The sysfs groups are not devm-managed: undo the created ones. */ + while (sysfs_i-- > 0) + sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[sysfs_i]); + +ally_config_create_err: + return ERR_PTR(ret); +} + +/** + * ally_config_remove() - Clean up configuration resources + * @hdev: HID device + * @cfg: ally config to clean up, may be NULL + */ +static void ally_config_remove(struct hid_device *hdev, struct ally_config= *cfg) +{ + int i; + + if (!cfg || !cfg->initialized) + return; + + for (i =3D 0; i < ARRAY_SIZE(ally_attr_groups); i++) + sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]); +} + /** * ally_gamepad_check_ready() - Wait for the gamepad MCU to report ready * @ally: ally handheld structure @@ -878,6 +1189,7 @@ static int ally_x_setup_input(struct hid_device *hdev,= struct ally_handheld *all =20 static int hid_asus_ally_init(struct hid_device *hdev, struct ally_handhel= d *ally) { + struct ally_config *cfg; int ret; =20 /* Failure at this point is non-critical */ @@ -886,6 +1198,21 @@ static int hid_asus_ally_init(struct hid_device *hdev= , struct ally_handheld *all if (ret < 0) hid_err(hdev, "Ally failed to init force-feedback off: %d\n", ret); =20 + cfg =3D ally_get_config(ally); + if (!cfg) + return 0; + + /* The MCU may have just been reset: restore the cached state. */ + guard(mutex)(&cfg->config_mutex); + + if (cfg->xbox_controller_support) { + ret =3D ally_set_xbox_controller(hdev, ally, cfg, + cfg->xbox_controller_enabled); + if (ret < 0) + hid_warn(hdev, "Failed to apply Xbox controller mode: %d\n", + ret); + } + return 0; } =20 @@ -961,11 +1288,15 @@ static struct ally_handheld *hid_asus_ally_probe(str= uct hid_device *hdev) unsigned long flags; int ret, ep =3D ally_get_endpoint_address(hdev); struct usb_device *udev; + struct ally_config *ally_cfg; struct hid_input *hidinput; =20 if (ep < 0) return ERR_PTR(ep); =20 + if (!hid_is_usb(hdev)) + return ERR_PTR(-ENODEV); + /* * The ROG Ally controller is integrated into a handheld PC, so at * most one device can exist and the shared global state relies on @@ -1025,6 +1356,14 @@ static struct ally_handheld *hid_asus_ally_probe(str= uct hid_device *hdev) return ERR_PTR(ret); } =20 + ally_cfg =3D ally_config_create(hdev, &ally_drvdata); + if (IS_ERR(ally_cfg)) { + hid_err(hdev, "Failed to create Ally cfg: %ld\n", + PTR_ERR(ally_cfg)); + ally_put_udev_if_orphaned(); + return ERR_PTR(PTR_ERR(ally_cfg)); + } + ret =3D hid_asus_ally_init(hdev, &ally_drvdata); if (ret < 0) { ally_put_udev_if_orphaned(); @@ -1068,7 +1407,9 @@ static struct ally_handheld *hid_asus_ally_probe(stru= ct hid_device *hdev) static void hid_asus_ally_remove(struct hid_device *hdev, struct ally_hand= held *ally) { struct input_dev *x_input =3D NULL; + struct ally_config *cfg =3D NULL; unsigned long flags; + bool owns_cfg; =20 if (!ally) return; @@ -1098,8 +1439,25 @@ static void hid_asus_ally_remove(struct hid_device *= hdev, struct ally_handheld * ally->keyboard_input =3D NULL; ally->keyboard_hdev =3D NULL; } + + owns_cfg =3D ally->cfg_hdev =3D=3D hdev; + if (owns_cfg) { + cfg =3D ally->config; + ally->cfg_hdev =3D NULL; + ally->config =3D NULL; + } + spin_unlock_irqrestore(&ally_data_lock, flags); =20 + /* + * The config teardown removes sysfs groups and takes sleeping locks: + * it must not run under ally_data_lock. The config pointer has been + * unpublished above, so no new sysfs callback can find it while the + * groups are being removed. + */ + if (owns_cfg) + ally_config_remove(hdev, cfg); + if (x_input) input_unregister_device(x_input); =20 @@ -1126,6 +1484,16 @@ static int hid_asus_ally_reset_resume(struct hid_dev= ice *hdev, struct ally_handh if (ep !=3D HID_ALLY_INTF_CFG_IN) return 0; =20 + /* + * This function assumes the asus-specific initialization + * to have been performed already at this point. + */ + ret =3D ally_gamepad_check_ready(ally, hdev); + if (ret < 0) { + hid_err(hdev, "ROG Ally device is not ready: %d\n", ret); + return ret; + } + ret =3D hid_asus_ally_init(hdev, ally); if (ret < 0) return ret; @@ -2709,7 +3077,27 @@ static struct hid_driver asus_driver =3D { .event =3D asus_event, .raw_event =3D asus_raw_event }; -module_hid_driver(asus_driver); +static int __init asus_init(void) +{ + return hid_register_driver(&asus_driver); +} + +/* + * The Ally configuration is not tied to any device lifetime: interface + * removal keeps it so that it can be re-applied on re-probe. Release it + * only when the module itself is removed, once no sysfs attribute can + * reach it anymore. + */ +static void __exit asus_exit(void) +{ + hid_unregister_driver(&asus_driver); + + kfree(ally_config); + ally_config =3D NULL; +} + +module_init(asus_init); +module_exit(asus_exit); =20 MODULE_IMPORT_NS("ASUS_WMI"); MODULE_LICENSE("GPL"); --=20 2.47.3 From nobody Fri Sep 25 20:46:57 2026 Received: from mta1.migadu.com (out-72.mta1.migadu.com [95.215.58.72]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 93680599A2A for ; Tue, 8 Sep 2026 17:44:53 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=95.215.58.72 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788889495; cv=none; b=eKPiW6Juady5hYe3723byI0i2kHpY5d8MqPp3LUgY35/U2IgUEKiM2f9DoOjH7pKGKYFZ0sP7g8TRc5uCG1XaS0oze/7feB2rva9BmDLk3eDQdRA8LaD0pyp5TPLjXZVg6isu7kMRdUSkEpOZoUnlajsSvlmkH0lwYRi4/7As8Q= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788889495; c=relaxed/simple; bh=shS8wJQrB4HWz/U934t8cM7y9gfZ75UCzUXY0q78QD0=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=aMu6t/USCaC4qxhuilKDvrJKXrscKCM6skprhrhiMuUl4OP2YbABzOFoVPlT+cbIy+ijpU2DWbZ0tcUQ7QbZGAUYTYfOfzCVpFT6hIcReO5WHOqweUAqtn/06FVGx1Iu9B+laNsyDnYGjLTKMWrQBfcsq+zXwkqr+4UulpzuYzI= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=kRy6nQ2D; arc=none smtp.client-ip=95.215.58.72 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="kRy6nQ2D" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=shS8wJQrB4HWz/U934t8cM7y9gfZ75UCzUXY0q78QD0=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1788889491; v=1; x=1789494291; b=kRy6nQ2DyX/kkdL6TmTdTvdyDor4u7pls8ejCJgjcTegpg+09SxTHhKIwP0ffK2Iy78r3oiQ 2ddFfSDbtrr+m6xEmNQ42XtTJYo9NNVjIsk5jxyp0s1kcNZCc8uLy6uiHlbf8EcLvKgFg9JLClI 4ZrlfhXRDCnqgZ4ko5vvKrnU= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id 3ca9f3d7e4d2c48b; Tue, 08 Sep 2026 17:44:51 +0000 X-Mizu-Trace-ID: 3ca9f3d7e4d2c48b X-Migadu-Flow: FLOW_OUT From: Denis Benato To: linux-kernel@vger.kernel.org Cc: linux-input@vger.kernel.org, "Benjamin Tissoires" , "Jiri Kosina" , "Luke D . Jones" , "Mateusz Schwartz" , "Denis Benato" , "Jonathan LoBue" , "Khamunetri Clark" , "Derek J. Clark" , Denis Benato Subject: [PATCH v6 05/13] HID: asus: add vibration strength configuration Date: Tue, 8 Sep 2026 17:44:25 +0000 Message-ID: <20260908174433.74260-6-denis.benato@linux.dev> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260908174433.74260-1-denis.benato@linux.dev> References: <20260908174433.74260-1-denis.benato@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" ASUS ROG Ally handhelds support the vibration strength to be configured: add sysfs attributes to allow userspace configure motors vibration intensity. Assisted-by: opencode:glm-5.2 Signed-off-by: Denis Benato Signed-off-by: Luke Jones --- drivers/hid/hid-asus.c | 195 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c index ac0c0a176857..866fb82b7d7d 100644 --- a/drivers/hid/hid-asus.c +++ b/drivers/hid/hid-asus.c @@ -206,6 +206,10 @@ struct ally_config { u8 left_trigger_max; u8 right_trigger_min; u8 right_trigger_max; + + /* Vibration settings */ + u8 vibration_intensity_left; + u8 vibration_intensity_right; }; =20 /* @@ -827,15 +831,193 @@ static ssize_t xbox_controller_store(struct device *= dev, =20 static DEVICE_ATTR_RW(xbox_controller); =20 +/** + * ally_set_vibration_intensity() - Set vibration intensity values + * @ally: ally handheld structure + * @hdev: HID device + * @left: Left motor intensity (0-100) + * @right: Right motor intensity (0-100) + * + * Return: 0 on success, negative errno on failure + */ +static int ally_set_vibration_intensity(struct ally_handheld *ally, + struct hid_device *hdev, u8 left, u8 right) +{ + const u8 data[] =3D { left, right }; + int ret; + + u8 *buf __free(kfree) =3D ally_alloc_cmd(CMD_SET_VIBRATION_INTENSITY, dat= a, sizeof(data)); + if (!buf) + return -ENOMEM; + + ret =3D ally_gamepad_send_packet(ally, hdev, buf, ROG_ALLY_REPORT_SIZE); + if (ret < 0) { + hid_err(hdev, "Failed to set vibration intensity: %d\n", ret); + return ret; + } + + return 0; +} + +static ssize_t left_vibration_intensity_show(struct device *dev, struct de= vice_attribute *attr, + char *buf) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *ally =3D drvdata->rog_ally; + struct ally_config *cfg; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + guard(mutex)(&cfg->config_mutex); + + return sysfs_emit(buf, "%u\n", cfg->vibration_intensity_left); +} + +static ssize_t left_vibration_intensity_store(struct device *dev, struct d= evice_attribute *attr, + const char *buf, size_t count) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *ally =3D drvdata->rog_ally; + struct ally_config *cfg; + u8 value; + int ret; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + ret =3D kstrtou8(buf, 10, &value); + if (ret || value > 100) + return -EINVAL; + + guard(mutex)(&cfg->config_mutex); + + ret =3D ally_set_vibration_intensity(ally, hdev, value, + cfg->vibration_intensity_right); + if (ret < 0) + return ret; + + cfg->vibration_intensity_left =3D value; + + 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 d= evice_attribute *attr, + char *buf) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *ally =3D drvdata->rog_ally; + struct ally_config *cfg; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + guard(mutex)(&cfg->config_mutex); + + return sysfs_emit(buf, "%u\n", cfg->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 =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *ally =3D drvdata->rog_ally; + struct ally_config *cfg; + u8 value; + int ret; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + ret =3D kstrtou8(buf, 10, &value); + if (ret || value > 100) + return -EINVAL; + + guard(mutex)(&cfg->config_mutex); + + ret =3D ally_set_vibration_intensity(ally, hdev, + cfg->vibration_intensity_left, value); + if (ret < 0) + return ret; + + cfg->vibration_intensity_right =3D value; + + 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 =3D + __ATTR(intensity, 0644, left_vibration_intensity_show, left_vibration_int= ensity_store); + +static struct device_attribute dev_attr_left_vibration_intensity_range =3D + __ATTR(intensity_range, 0444, left_vibration_intensity_range_show, NULL); + +static struct device_attribute dev_attr_right_vibration_intensity =3D + __ATTR(intensity, 0644, right_vibration_intensity_show, right_vibration_i= ntensity_store); + +static struct device_attribute dev_attr_right_vibration_intensity_range = =3D + __ATTR(intensity_range, 0444, right_vibration_intensity_range_show, NULL); + static struct attribute *ally_config_attrs[] =3D { &dev_attr_xbox_controller.attr, NULL }; =20 +static struct attribute *ally_left_vibration_attrs[] =3D { + &dev_attr_left_vibration_intensity.attr, + &dev_attr_left_vibration_intensity_range.attr, + NULL +}; + +static struct attribute *ally_right_vibration_attrs[] =3D { + &dev_attr_right_vibration_intensity.attr, + &dev_attr_right_vibration_intensity_range.attr, + NULL +}; + static const struct attribute_group ally_attr_groups[] =3D { { .attrs =3D ally_config_attrs, }, + { + .name =3D "left_vibration", + .attrs =3D ally_left_vibration_attrs, + }, + { + .name =3D "right_vibration", + .attrs =3D ally_right_vibration_attrs, + }, }; =20 /** @@ -880,6 +1062,8 @@ static struct ally_config *ally_config_create(struct h= id_device *hdev, struct al cfg->left_outer_threshold =3D 90; cfg->right_deadzone =3D 10; cfg->right_outer_threshold =3D 90; + cfg->vibration_intensity_left =3D 100; + cfg->vibration_intensity_right =3D 100; } =20 spin_lock_irqsave(&ally_data_lock, flags); @@ -1213,6 +1397,17 @@ static int hid_asus_ally_init(struct hid_device *hde= v, struct ally_handheld *all ret); } =20 + /* + * The MCU does not keep the vibration intensity across a reset: + * send the recorded values again, whichever interface probed. + */ + ret =3D ally_set_vibration_intensity(ally, hdev, + cfg->vibration_intensity_left, + cfg->vibration_intensity_right); + if (ret < 0) + hid_warn(hdev, "Failed to restore vibration intensity: %d\n", + ret); + return 0; } =20 --=20 2.47.3 From nobody Fri Sep 25 20:46:57 2026 Received: from mta0.migadu.com (out-228.mta0.migadu.com [91.218.175.228]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id D781659E324 for ; Tue, 8 Sep 2026 17:44:57 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.228 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788889500; cv=none; b=c0ZAzKWgdJ54c5swWproKI2e6JAoIQLxPlw0TZrQ9LwG21kW9KyqNEgJhtpJxFXtYdwrSHC1leafQDX1yTdL8dgBkSbtwB41V0x/8GeATvOgBWDMzTGKDfJYmJWLQak4n7svXq0M7/1m+oaehzU6t4W78lNNlXw9AcpqtgNZR4w= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788889500; c=relaxed/simple; bh=APe38Z/O0zC1Bzwo+ShVUi2iCtcq+UkhHydqFL4qXeg=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=BvFBAk4vfSJXFk+qFw5N/0RT/F8SYISCib26nfCEALMS9MCa1inwpsqtpl75r/YVllm8wL60slqwEXw6s3dbRsW3YfJEq5dh461vY8XShOTsuEjVW5nHCdYvjbeykMrcs9TJ8Ebk6xe40MOcPTqT94Zedv9GH4wnnGUl33QHC4U= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=Y4mFxD4f; arc=none smtp.client-ip=91.218.175.228 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="Y4mFxD4f" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=APe38Z/O0zC1Bzwo+ShVUi2iCtcq+UkhHydqFL4qXeg=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1788889495; v=1; x=1789494295; b=Y4mFxD4fTnCLRhqNpvTTjuFI+jq+uAgMBrBtxaTAPllbidjLezKwR0wBIzFdEW7M8TAdrHk8 v1njMWhnNkf3cMsVN+Rs4TwK5i5B9rIVy4WbYu67XpobuIAewJ/os7asqrsCkc7w+5uiyF4gGmP m5/lHZC7OkGZoUZAG2JlquuU= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id 9357519b46330949; Tue, 08 Sep 2026 17:44:55 +0000 X-Mizu-Trace-ID: 9357519b46330949 X-Migadu-Flow: FLOW_OUT From: Denis Benato To: linux-kernel@vger.kernel.org Cc: linux-input@vger.kernel.org, "Benjamin Tissoires" , "Jiri Kosina" , "Luke D . Jones" , "Mateusz Schwartz" , "Denis Benato" , "Jonathan LoBue" , "Khamunetri Clark" , "Derek J. Clark" , Denis Benato Subject: [PATCH v6 06/13] HID: asus: add joysticks inner and outer range configuration Date: Tue, 8 Sep 2026 17:44:26 +0000 Message-ID: <20260908174433.74260-7-denis.benato@linux.dev> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260908174433.74260-1-denis.benato@linux.dev> References: <20260908174433.74260-1-denis.benato@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" ROG Ally devices supports configuring joysticks inner and outer range: add sysfs attributes to allow userspace modifying the sensitivity of those controllers. Assisted-by: opencode:glm-5.2 Signed-off-by: Denis Benato Signed-off-by: Luke Jones --- drivers/hid/hid-asus.c | 425 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 425 insertions(+) diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c index 866fb82b7d7d..792131efdb9c 100644 --- a/drivers/hid/hid-asus.c +++ b/drivers/hid/hid-asus.c @@ -989,6 +989,350 @@ static struct device_attribute dev_attr_right_vibrati= on_intensity =3D static struct device_attribute dev_attr_right_vibration_intensity_range = =3D __ATTR(intensity_range, 0444, right_vibration_intensity_range_show, NULL); =20 +/** + * ally_set_joystick_thresholds() - Generic function to set joystick ranges + * @ally: ally handheld structure + * @hdev: HID device + * @left_it: inner threshold (deadzone) of the left stick (0-50) + * @left_ot: outer threshold of the left stick (70-100) + * @right_it: inner threshold (deadzone) of the right stick (0-50) + * @right_ot: outer threshold of the right stick (70-100) + * + * This function sends the command to set both inner and outer threshold + * for the left and right joysticks. + * + * Return: 0 on success, negative errno on failure + */ +static int ally_set_joystick_thresholds(struct ally_handheld *ally, + struct hid_device *hdev, + u8 left_it, u8 left_ot, u8 right_it, u8 right_ot) +{ + const u8 payload[] =3D { left_it, left_ot, right_it, right_ot }; + int ret; + + u8 *buf __free(kfree) =3D ally_alloc_cmd(CMD_SET_JOYSTICK_DEADZONE, paylo= ad, sizeof(payload)); + if (!buf) + return -ENOMEM; + + ret =3D ally_gamepad_send_packet(ally, hdev, buf, ROG_ALLY_REPORT_SIZE); + if (ret < 0) { + hid_err(hdev, "Failed to set joystick ranges: %d\n", ret); + return ret; + } + + return 0; +} + +static ssize_t left_joystick_inner_threshold_show(struct device *dev, stru= ct device_attribute *attr, + char *buf) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *const ally =3D drvdata->rog_ally; + struct ally_config *cfg; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + guard(mutex)(&cfg->config_mutex); + + if (!cfg->user_cal_support) + return -EOPNOTSUPP; + + return sysfs_emit(buf, "%u\n", cfg->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 =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *const ally =3D drvdata->rog_ally; + struct ally_config *cfg; + u8 value; + int ret; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + ret =3D kstrtou8(buf, 10, &value); + if (ret || value > 50) + return -EINVAL; + + guard(mutex)(&cfg->config_mutex); + + if (!cfg->user_cal_support) + return -EOPNOTSUPP; + + ret =3D ally_set_joystick_thresholds(ally, hdev, + value, + cfg->left_outer_threshold, + cfg->right_deadzone, + cfg->right_outer_threshold); + if (ret) + return ret; + + cfg->left_deadzone =3D value; + + 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, stru= ct device_attribute *attr, + char *buf) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *const ally =3D drvdata->rog_ally; + struct ally_config *cfg; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + guard(mutex)(&cfg->config_mutex); + + if (!cfg->user_cal_support) + return -EOPNOTSUPP; + + return sysfs_emit(buf, "%u\n", cfg->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 =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *const ally =3D drvdata->rog_ally; + struct ally_config *cfg; + u8 value; + int ret; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + ret =3D kstrtou8(buf, 10, &value); + if (ret || value < 70 || value > 100) + return -EINVAL; + + guard(mutex)(&cfg->config_mutex); + + if (!cfg->user_cal_support) + return -EOPNOTSUPP; + + ret =3D ally_set_joystick_thresholds(ally, hdev, + cfg->left_deadzone, + value, + cfg->right_deadzone, + cfg->right_outer_threshold); + if (ret) + return ret; + + cfg->left_outer_threshold =3D value; + + 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) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *const ally =3D drvdata->rog_ally; + struct ally_config *cfg; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + guard(mutex)(&cfg->config_mutex); + + if (!cfg->user_cal_support) + return -EOPNOTSUPP; + + return sysfs_emit(buf, "%u\n", cfg->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 =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *const ally =3D drvdata->rog_ally; + struct ally_config *cfg; + u8 value; + int ret; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + ret =3D kstrtou8(buf, 10, &value); + if (ret || value > 50) + return -EINVAL; + + guard(mutex)(&cfg->config_mutex); + + if (!cfg->user_cal_support) + return -EOPNOTSUPP; + + ret =3D ally_set_joystick_thresholds(ally, hdev, + cfg->left_deadzone, + cfg->left_outer_threshold, + value, + cfg->right_outer_threshold); + if (ret) + return ret; + + cfg->right_deadzone =3D value; + + return count; +} + +static ssize_t right_joystick_inner_threshold_range_show(struct device *de= v, + 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) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *const ally =3D drvdata->rog_ally; + struct ally_config *cfg; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + guard(mutex)(&cfg->config_mutex); + + if (!cfg->user_cal_support) + return -EOPNOTSUPP; + + return sysfs_emit(buf, "%u\n", cfg->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 =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *const ally =3D drvdata->rog_ally; + struct ally_config *cfg; + u8 value; + int ret; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + ret =3D kstrtou8(buf, 10, &value); + if (ret || value < 70 || value > 100) + return -EINVAL; + + guard(mutex)(&cfg->config_mutex); + + if (!cfg->user_cal_support) + return -EOPNOTSUPP; + + ret =3D ally_set_joystick_thresholds(ally, hdev, + cfg->left_deadzone, + cfg->left_outer_threshold, + cfg->right_deadzone, + value); + if (ret) + return ret; + + cfg->right_outer_threshold =3D value; + + return count; +} + +static ssize_t right_joystick_outer_threshold_range_show(struct device *de= v, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "70 100\n"); +} + +static struct device_attribute dev_attr_left_joystick_inner_threshold =3D + __ATTR(inner_threshold, 0644, left_joystick_inner_threshold_show, + left_joystick_inner_threshold_store); + +static struct device_attribute dev_attr_left_joystick_inner_threshold_rang= e =3D + __ATTR(inner_threshold_range, 0444, left_joystick_inner_threshold_range_s= how, NULL); + +static struct device_attribute dev_attr_left_joystick_outer_threshold =3D + __ATTR(outer_threshold, 0644, left_joystick_outer_threshold_show, + left_joystick_outer_threshold_store); + +static struct device_attribute dev_attr_left_joystick_outer_threshold_rang= e =3D + __ATTR(outer_threshold_range, 0444, left_joystick_outer_threshold_range_s= how, NULL); + +static struct device_attribute dev_attr_right_joystick_inner_threshold =3D + __ATTR(inner_threshold, 0644, right_joystick_inner_threshold_show, + right_joystick_inner_threshold_store); + +static struct device_attribute dev_attr_right_joystick_inner_threshold_ran= ge =3D + __ATTR(inner_threshold_range, 0444, right_joystick_inner_threshold_range_= show, NULL); + +static struct device_attribute dev_attr_right_joystick_outer_threshold =3D + __ATTR(outer_threshold, 0644, right_joystick_outer_threshold_show, + right_joystick_outer_threshold_store); + +static struct device_attribute dev_attr_right_joystick_outer_threshold_ran= ge =3D + __ATTR(outer_threshold_range, 0444, right_joystick_outer_threshold_range_= show, NULL); + static struct attribute *ally_config_attrs[] =3D { &dev_attr_xbox_controller.attr, NULL @@ -1006,6 +1350,22 @@ static struct attribute *ally_right_vibration_attrs[= ] =3D { NULL }; =20 +static struct attribute *left_joystick_axis_attrs[] =3D { + &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[] =3D { + &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 +}; + static const struct attribute_group ally_attr_groups[] =3D { { .attrs =3D ally_config_attrs, @@ -1020,6 +1380,28 @@ static const struct attribute_group ally_attr_groups= [] =3D { }, }; =20 +/* + * The joystick and trigger range calibration attributes are tied to the + * user-calibration capability: their sysfs groups are registered only + * when the device supports configuring those parameters, and the show + * and store callbacks reject accesses with -EOPNOTSUPP regardless, in + * case the groups are registered for another supported feature. + */ +static const struct attribute_group ally_left_joystick_axis_group =3D { + .name =3D "left_joystick_axis", + .attrs =3D left_joystick_axis_attrs, +}; + +static const struct attribute_group ally_right_joystick_axis_group =3D { + .name =3D "right_joystick_axis", + .attrs =3D right_joystick_axis_attrs, +}; + +static const struct attribute_group *const ally_cal_attr_groups[] =3D { + &ally_left_joystick_axis_group, + &ally_right_joystick_axis_group, +}; + /** * ally_config_create() - Initialize configuration and create sysfs entries * @hdev: HID device @@ -1080,6 +1462,25 @@ static struct ally_config *ally_config_create(struct= hid_device *hdev, struct al } } =20 + /* Skip the calibration groups when the capability is missing. */ + if (cfg->user_cal_support) { + int cal_i; + + for (cal_i =3D 0; cal_i < ARRAY_SIZE(ally_cal_attr_groups); + cal_i++) { + ret =3D sysfs_create_group(&hdev->dev.kobj, + ally_cal_attr_groups[cal_i]); + if (ret < 0) { + hid_err(hdev, "Failed to create sysfs group '%s': %d\n", + ally_cal_attr_groups[cal_i]->name, ret); + while (cal_i-- > 0) + sysfs_remove_group(&hdev->dev.kobj, + ally_cal_attr_groups[cal_i]); + goto ally_config_create_sysfs_err; + } + } + } + cfg->initialized =3D true; =20 return cfg; @@ -1110,6 +1511,18 @@ static void ally_config_remove(struct hid_device *hd= ev, struct ally_config *cfg) =20 for (i =3D 0; i < ARRAY_SIZE(ally_attr_groups); i++) sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]); + + /* + * The calibration groups are removed with the same gating used to + * create them, so a device without those capabilities does not + * trigger a "not found" warning. + */ + if (cfg->user_cal_support || cfg->anti_deadzone_support || + cfg->resp_curve_support) { + for (i =3D 0; i < ARRAY_SIZE(ally_cal_attr_groups); i++) + sysfs_remove_group(&hdev->dev.kobj, + ally_cal_attr_groups[i]); + } } =20 /** @@ -1408,6 +1821,18 @@ static int hid_asus_ally_init(struct hid_device *hde= v, struct ally_handheld *all hid_warn(hdev, "Failed to restore vibration intensity: %d\n", ret); =20 + if (cfg->user_cal_support) { + ret =3D ally_set_joystick_thresholds(ally, hdev, + cfg->left_deadzone, + cfg->left_outer_threshold, + cfg->right_deadzone, + cfg->right_outer_threshold); + if (ret < 0) + hid_warn(hdev, + "Failed to restore joystick thresholds: %d\n", + ret); + } + return 0; } =20 --=20 2.47.3 From nobody Fri Sep 25 20:46:57 2026 Received: from mta0.migadu.com (out-231.mta0.migadu.com [91.218.175.231]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id EF52B59E334 for ; Tue, 8 Sep 2026 17:44:58 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.231 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788889501; cv=none; b=iaGv/igkATBnRA5mOfRqxNgzzUtqcqUAWz9Dui1k2cxjpntjGl2LfEx9zokgvhTFxtRRFJjguBCA8K7F1namA+vKfNut1z+8nAkRw2TZkJU7WgCKsPDY4Gzfi0V/w2rC3yfB1ayJ8zBCzdQ8ZkHIN9vaTnHwzRWBOX10jee6tCQ= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788889501; c=relaxed/simple; bh=3j/QvirJGrN1L51a0CAHQPhpyIcbVldl/qb0WlAj7rE=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=oc14QEv0Z1+NU7HlNnpbPWvJ1X5em1x5RNVdHUfFxywkDoEjkhLXQWMO/qk0wjYaY0lqiAdVtwmQG/XyZLP/H2dsRh9imoslHPJUS/hDEMT8Ldip5Td9MDAsQhNapKPEeUILZvTqj/Q5vobGDXFDRiVnR3LoJFDOXLi/fQhjMvs= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=R+geqMxr; arc=none smtp.client-ip=91.218.175.231 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="R+geqMxr" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=3j/QvirJGrN1L51a0CAHQPhpyIcbVldl/qb0WlAj7rE=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1788889496; v=1; x=1789494296; b=R+geqMxrUp73lwYUkiP3DrijSNRaJNSMgNJaOP4BUibAaaGWHFSbg1bBaONKIFo9JT/xmpg9 u4Pcl9gED/wI9MIeJkm4HOv2o2o72MF7MIOTvn41Xx/juYO9e2BXPUcM04aJD10/s0X0X0fwZFI u6GXvS3WVckqY4K4300BtJQM= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id 4e9e35465ef46e63; Tue, 08 Sep 2026 17:44:56 +0000 X-Mizu-Trace-ID: 4e9e35465ef46e63 X-Migadu-Flow: FLOW_OUT From: Denis Benato To: linux-kernel@vger.kernel.org Cc: linux-input@vger.kernel.org, "Benjamin Tissoires" , "Jiri Kosina" , "Luke D . Jones" , "Mateusz Schwartz" , "Denis Benato" , "Jonathan LoBue" , "Khamunetri Clark" , "Derek J. Clark" , Denis Benato Subject: [PATCH v6 07/13] HID: asus: add triggers inner and outer range configuration Date: Tue, 8 Sep 2026 17:44:27 +0000 Message-ID: <20260908174433.74260-8-denis.benato@linux.dev> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260908174433.74260-1-denis.benato@linux.dev> References: <20260908174433.74260-1-denis.benato@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" 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. Assisted-by: opencode:glm-5.2 Signed-off-by: Denis Benato Signed-off-by: Luke Jones --- drivers/hid/hid-asus.c | 385 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 385 insertions(+) diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c index 792131efdb9c..370205ae7fc5 100644 --- a/drivers/hid/hid-asus.c +++ b/drivers/hid/hid-asus.c @@ -1333,6 +1333,349 @@ static struct device_attribute dev_attr_right_joyst= ick_outer_threshold =3D static struct device_attribute dev_attr_right_joystick_outer_threshold_ran= ge =3D __ATTR(outer_threshold_range, 0444, right_joystick_outer_threshold_range_= show, NULL); =20 +/** + * ally_set_trigger_ranges() - Generic function to set triggers ranges + * @ally: ally handheld structure + * @hdev: HID device + * @left_it: lower limit of the left trigger range (0-50) + * @left_ot: upper limit of the left trigger range (70-100) + * @right_it: lower limit of the right trigger range (0-50) + * @right_ot: upper limit of the right trigger range (70-100) + * + * This function sends the command to set both inner and outer threshold + * for the left and right triggers. + * + * Return: 0 on success, negative errno on failure + */ +static int ally_set_trigger_ranges(struct ally_handheld *ally, + struct hid_device *hdev, + u8 left_it, u8 left_ot, u8 right_it, u8 right_ot) +{ + const u8 payload[] =3D { left_it, left_ot, right_it, right_ot }; + int ret; + + u8 *buf __free(kfree) =3D ally_alloc_cmd(CMD_SET_TRIGGER_RANGE, payload, = sizeof(payload)); + if (!buf) + return -ENOMEM; + + ret =3D ally_gamepad_send_packet(ally, hdev, buf, ROG_ALLY_REPORT_SIZE); + if (ret < 0) { + hid_err(hdev, "Failed to set trigger ranges: %d\n", ret); + return ret; + } + + return 0; +} + +static ssize_t left_trigger_range_lower_limit_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *const ally =3D drvdata->rog_ally; + struct ally_config *cfg; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + guard(mutex)(&cfg->config_mutex); + + if (!cfg->user_cal_support) + return -EOPNOTSUPP; + + return sysfs_emit(buf, "%u\n", cfg->left_trigger_min); +} + +static ssize_t left_trigger_range_lower_limit_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *const ally =3D drvdata->rog_ally; + struct ally_config *cfg; + u8 value; + int ret; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + ret =3D kstrtou8(buf, 10, &value); + if (ret || value > 50) + return -EINVAL; + + guard(mutex)(&cfg->config_mutex); + + if (!cfg->user_cal_support) + return -EOPNOTSUPP; + + ret =3D ally_set_trigger_ranges(ally, hdev, + value, + cfg->left_trigger_max, + cfg->right_trigger_min, + cfg->right_trigger_max); + if (ret) + return ret; + + cfg->left_trigger_min =3D value; + + return count; +} + +static ssize_t left_trigger_range_lower_limit_range_show(struct device *de= v, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "0 50\n"); +} + +static ssize_t right_trigger_range_upper_limit_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *const ally =3D drvdata->rog_ally; + struct ally_config *cfg; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + guard(mutex)(&cfg->config_mutex); + + if (!cfg->user_cal_support) + return -EOPNOTSUPP; + + return sysfs_emit(buf, "%u\n", cfg->right_trigger_max); +} + +static ssize_t right_trigger_range_upper_limit_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *const ally =3D drvdata->rog_ally; + struct ally_config *cfg; + u8 value; + int ret; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + ret =3D kstrtou8(buf, 10, &value); + if (ret || value < 70 || value > 100) + return -EINVAL; + + guard(mutex)(&cfg->config_mutex); + + if (!cfg->user_cal_support) + return -EOPNOTSUPP; + + ret =3D ally_set_trigger_ranges(ally, hdev, + cfg->left_trigger_min, + cfg->left_trigger_max, + cfg->right_trigger_min, + value); + if (ret) + return ret; + + cfg->right_trigger_max =3D value; + + return count; +} + +static ssize_t right_trigger_range_upper_limit_range_show(struct device *d= ev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "70 100\n"); +} + +static ssize_t right_trigger_range_lower_limit_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *const ally =3D drvdata->rog_ally; + struct ally_config *cfg; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + guard(mutex)(&cfg->config_mutex); + + if (!cfg->user_cal_support) + return -EOPNOTSUPP; + + return sysfs_emit(buf, "%u\n", cfg->right_trigger_min); +} + +static ssize_t right_trigger_range_lower_limit_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *const ally =3D drvdata->rog_ally; + struct ally_config *cfg; + u8 value; + int ret; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + ret =3D kstrtou8(buf, 10, &value); + if (ret || value > 50) + return -EINVAL; + + guard(mutex)(&cfg->config_mutex); + + if (!cfg->user_cal_support) + return -EOPNOTSUPP; + + ret =3D ally_set_trigger_ranges(ally, hdev, + cfg->left_trigger_min, + cfg->left_trigger_max, + value, + cfg->right_trigger_max); + if (ret) + return ret; + + cfg->right_trigger_min =3D value; + + return count; +} + +static ssize_t right_trigger_range_lower_limit_range_show(struct device *d= ev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "0 50\n"); +} + +static ssize_t left_trigger_range_upper_limit_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *const ally =3D drvdata->rog_ally; + struct ally_config *cfg; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + guard(mutex)(&cfg->config_mutex); + + if (!cfg->user_cal_support) + return -EOPNOTSUPP; + + return sysfs_emit(buf, "%u\n", cfg->left_trigger_max); +} + +static ssize_t left_trigger_range_upper_limit_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *const ally =3D drvdata->rog_ally; + struct ally_config *cfg; + u8 value; + int ret; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + ret =3D kstrtou8(buf, 10, &value); + if (ret || value < 70 || value > 100) + return -EINVAL; + + guard(mutex)(&cfg->config_mutex); + + if (!cfg->user_cal_support) + return -EOPNOTSUPP; + + ret =3D ally_set_trigger_ranges(ally, hdev, + cfg->left_trigger_min, + value, + cfg->right_trigger_min, + cfg->right_trigger_max); + if (ret) + return ret; + + cfg->left_trigger_max =3D value; + + return count; +} + +static ssize_t left_trigger_range_upper_limit_range_show(struct device *de= v, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "70 100\n"); +} + +static struct device_attribute dev_attr_left_trigger_range_lower_limit =3D + __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_ran= ge =3D + __ATTR(range_lower_limit_range, 0444, left_trigger_range_lower_limit_rang= e_show, NULL); + +static struct device_attribute dev_attr_left_trigger_range_upper_limit =3D + __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_ran= ge =3D + __ATTR(range_upper_limit_range, 0444, left_trigger_range_upper_limit_rang= e_show, NULL); + +static struct device_attribute dev_attr_right_trigger_range_lower_limit = =3D + __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_ra= nge =3D + __ATTR(range_lower_limit_range, 0444, right_trigger_range_lower_limit_ran= ge_show, NULL); + +static struct device_attribute dev_attr_right_trigger_range_upper_limit = =3D + __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_ra= nge =3D + __ATTR(range_upper_limit_range, 0444, right_trigger_range_upper_limit_ran= ge_show, NULL); + static struct attribute *ally_config_attrs[] =3D { &dev_attr_xbox_controller.attr, NULL @@ -1366,6 +1709,22 @@ static struct attribute *right_joystick_axis_attrs[]= =3D { NULL }; =20 +static struct attribute *left_trigger_attrs[] =3D { + &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[] =3D { + &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_attr_groups[] =3D { { .attrs =3D ally_config_attrs, @@ -1397,9 +1756,21 @@ static const struct attribute_group ally_right_joyst= ick_axis_group =3D { .attrs =3D right_joystick_axis_attrs, }; =20 +static const struct attribute_group ally_left_trigger_group =3D { + .name =3D "left_trigger", + .attrs =3D left_trigger_attrs, +}; + +static const struct attribute_group ally_right_trigger_group =3D { + .name =3D "right_trigger", + .attrs =3D right_trigger_attrs, +}; + static const struct attribute_group *const ally_cal_attr_groups[] =3D { &ally_left_joystick_axis_group, &ally_right_joystick_axis_group, + &ally_left_trigger_group, + &ally_right_trigger_group, }; =20 /** @@ -1444,6 +1815,10 @@ static struct ally_config *ally_config_create(struct= hid_device *hdev, struct al cfg->left_outer_threshold =3D 90; cfg->right_deadzone =3D 10; cfg->right_outer_threshold =3D 90; + cfg->left_trigger_min =3D 0; + cfg->left_trigger_max =3D 100; + cfg->right_trigger_min =3D 0; + cfg->right_trigger_max =3D 100; cfg->vibration_intensity_left =3D 100; cfg->vibration_intensity_right =3D 100; } @@ -1831,6 +2206,16 @@ static int hid_asus_ally_init(struct hid_device *hde= v, struct ally_handheld *all hid_warn(hdev, "Failed to restore joystick thresholds: %d\n", ret); + + ret =3D ally_set_trigger_ranges(ally, hdev, + cfg->left_trigger_min, + cfg->left_trigger_max, + cfg->right_trigger_min, + cfg->right_trigger_max); + if (ret < 0) + hid_warn(hdev, + "Failed to restore trigger ranges: %d\n", + ret); } =20 return 0; --=20 2.47.3 From nobody Fri Sep 25 20:46:57 2026 Received: from mta0.migadu.com (out-235.mta0.migadu.com [91.218.175.235]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 6505759C99F for ; Tue, 8 Sep 2026 17:45:02 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.235 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788889507; cv=none; b=MR12qrWyG6wO2FDZIJ+OmzdZIFxeY/082H23qk7Ktcks8BcJIt4Pftx4XfeGuoSXRu0YuLe4AaX+amP/5Q1s10tVdgeQKelP/OGpB+pkAscBg1r4NncrU4nv1eS/6wbNQxgW90PnL/SZtdMFt+YQK3L+OlfK0iZM1s/nOGL2CHc= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788889507; c=relaxed/simple; bh=zCK1HQrOJajDikqd5dBajq1MMMf+0R5mpv85011QOxc=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=YIzKdoBbKJQNoOW1OxYJXqiUTk8bRFAVUFhnrV/i/fEAcjxliIJBxvHftmg1v/fpojLf5BReRe0UBtnonRT9mMlniIs1GtIPEGbALD1fenXBQr5rCmnZNNsu1s+dX7y+833ZrWZLjIQ/TvXvtIzQCGDtN9CaM3YxJEUlHfqarzk= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=Jj868Ruq; arc=none smtp.client-ip=91.218.175.235 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="Jj868Ruq" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=zCK1HQrOJajDikqd5dBajq1MMMf+0R5mpv85011QOxc=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1788889501; v=1; x=1789494301; b=Jj868Ruq49xcBXy/TuEkVNqZtfA0WQtquM1YnlXCXpmFCuDOOzeZn8CMGs3/Nrxc6gl0qqs/ 5ReTyFDy0cyfEXEvZAGl98L5K8vaH6NHcLeMItGeF7gpCDiZ64OTKZaLveHfypfONj2S3jFuuJ+ i7RW0fTf25wCtJCWEp7VB8mE= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id a72029a06fd6ac1e; Tue, 08 Sep 2026 17:45:00 +0000 X-Mizu-Trace-ID: a72029a06fd6ac1e X-Migadu-Flow: FLOW_OUT From: Denis Benato To: linux-kernel@vger.kernel.org Cc: linux-input@vger.kernel.org, "Benjamin Tissoires" , "Jiri Kosina" , "Luke D . Jones" , "Mateusz Schwartz" , "Denis Benato" , "Jonathan LoBue" , "Khamunetri Clark" , "Derek J. Clark" , Denis Benato Subject: [PATCH v6 08/13] HID: asus: add joysticks anti-deadzone configuration Date: Tue, 8 Sep 2026 17:44:28 +0000 Message-ID: <20260908174433.74260-9-denis.benato@linux.dev> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260908174433.74260-1-denis.benato@linux.dev> References: <20260908174433.74260-1-denis.benato@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" 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. Assisted-by: opencode:glm-5.2 Signed-off-by: Denis Benato Signed-off-by: Luke Jones --- drivers/hid/hid-asus.c | 211 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 207 insertions(+), 4 deletions(-) diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c index 370205ae7fc5..4ed438121cc9 100644 --- a/drivers/hid/hid-asus.c +++ b/drivers/hid/hid-asus.c @@ -1333,6 +1333,193 @@ static struct device_attribute dev_attr_right_joyst= ick_outer_threshold =3D static struct device_attribute dev_attr_right_joystick_outer_threshold_ran= ge =3D __ATTR(outer_threshold_range, 0444, right_joystick_outer_threshold_range_= show, NULL); =20 +/** + * ally_set_anti_deadzone() - Set anti-deadzone values for joysticks + * @ally: ally handheld structure + * @hdev: HID device + * @cfg: ally config + * @left_adz: left joystick anti-deadzone value (0-100) + * @right_adz: right joystick anti-deadzone value (0-100) + * + * Return: 0 on success, negative errno on failure + */ +static int ally_set_anti_deadzone(struct ally_handheld *ally, + struct hid_device *hdev, struct ally_config *cfg, + u8 left_adz, u8 right_adz) +{ + const u8 payload[] =3D { left_adz, right_adz }; + int ret; + + if (!cfg->anti_deadzone_support) { + hid_dbg(hdev, "Anti-deadzone not supported on this device\n"); + return -EOPNOTSUPP; + } + + u8 *buf __free(kfree) =3D ally_alloc_cmd(CMD_SET_ANTI_DEADZONE, payload, = sizeof(payload)); + if (!buf) + return -ENOMEM; + + ret =3D ally_gamepad_send_packet(ally, hdev, buf, ROG_ALLY_REPORT_SIZE); + if (ret < 0) { + hid_err(hdev, "Failed to set anti-deadzone values: %d\n", ret); + return ret; + } + + return 0; +} + +static ssize_t left_joystick_anti_deadzone_show(struct device *dev, struct= device_attribute *attr, + char *buf) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *const ally =3D drvdata->rog_ally; + struct ally_config *cfg; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + guard(mutex)(&cfg->config_mutex); + + if (!cfg->anti_deadzone_support) { + hid_dbg(hdev, "Anti-deadzone not supported on this device\n"); + return -EOPNOTSUPP; + } + + return sysfs_emit(buf, "%u\n", cfg->left_anti_deadzone); +} + +static ssize_t left_joystick_anti_deadzone_store(struct device *dev, struc= t device_attribute *attr, + const char *buf, size_t count) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *const ally =3D drvdata->rog_ally; + struct ally_config *cfg; + u8 value; + int ret; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + ret =3D kstrtou8(buf, 10, &value); + if (ret || value > 100) + return -EINVAL; + + guard(mutex)(&cfg->config_mutex); + + if (!cfg->anti_deadzone_support) { + hid_dbg(hdev, "Anti-deadzone not supported on this device\n"); + return -EOPNOTSUPP; + } + + ret =3D ally_set_anti_deadzone(ally, hdev, cfg, value, cfg->right_anti_de= adzone); + if (ret) + return ret; + + cfg->left_anti_deadzone =3D value; + + return count; +} + +static ssize_t left_joystick_anti_deadzone_range_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "0 100\n"); +} + +static ssize_t right_joystick_anti_deadzone_show(struct device *dev, struc= t device_attribute *attr, + char *buf) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *const ally =3D drvdata->rog_ally; + struct ally_config *cfg; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + guard(mutex)(&cfg->config_mutex); + + if (!cfg->anti_deadzone_support) { + hid_dbg(hdev, "Anti-deadzone not supported on this device\n"); + return -EOPNOTSUPP; + } + + return sysfs_emit(buf, "%u\n", cfg->right_anti_deadzone); +} + +static ssize_t right_joystick_anti_deadzone_store(struct device *dev, stru= ct device_attribute *attr, + const char *buf, size_t count) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *const ally =3D drvdata->rog_ally; + struct ally_config *cfg; + u8 value; + int ret; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + ret =3D kstrtou8(buf, 10, &value); + if (ret || value > 100) + return -EINVAL; + + guard(mutex)(&cfg->config_mutex); + + if (!cfg->anti_deadzone_support) { + hid_dbg(hdev, "Anti-deadzone not supported on this device\n"); + return -EOPNOTSUPP; + } + + ret =3D ally_set_anti_deadzone(ally, hdev, cfg, cfg->left_anti_deadzone, = value); + if (ret) + return ret; + + cfg->right_anti_deadzone =3D value; + + return count; +} + +static ssize_t right_joystick_anti_deadzone_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_joystick_anti_deadzone =3D + __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 = =3D + __ATTR(anti_deadzone_range, 0444, left_joystick_anti_deadzone_range_show,= NULL); + +static struct device_attribute dev_attr_right_joystick_anti_deadzone =3D + __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= =3D + __ATTR(anti_deadzone_range, 0444, right_joystick_anti_deadzone_range_show= , NULL); + /** * ally_set_trigger_ranges() - Generic function to set triggers ranges * @ally: ally handheld structure @@ -1698,6 +1885,8 @@ static struct attribute *left_joystick_axis_attrs[] = =3D { &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 }; =20 @@ -1706,6 +1895,8 @@ static struct attribute *right_joystick_axis_attrs[] = =3D { &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 }; =20 @@ -1837,8 +2028,11 @@ static struct ally_config *ally_config_create(struct= hid_device *hdev, struct al } } =20 - /* Skip the calibration groups when the capability is missing. */ - if (cfg->user_cal_support) { + /* + * Skip the calibration and anti-deadzone groups when none of the + * features they expose is supported. + */ + if (cfg->user_cal_support || cfg->anti_deadzone_support) { int cal_i; =20 for (cal_i =3D 0; cal_i < ARRAY_SIZE(ally_cal_attr_groups); @@ -1892,8 +2086,7 @@ static void ally_config_remove(struct hid_device *hde= v, struct ally_config *cfg) * create them, so a device without those capabilities does not * trigger a "not found" warning. */ - if (cfg->user_cal_support || cfg->anti_deadzone_support || - cfg->resp_curve_support) { + if (cfg->user_cal_support || cfg->anti_deadzone_support) { for (i =3D 0; i < ARRAY_SIZE(ally_cal_attr_groups); i++) sysfs_remove_group(&hdev->dev.kobj, ally_cal_attr_groups[i]); @@ -2218,6 +2411,16 @@ static int hid_asus_ally_init(struct hid_device *hde= v, struct ally_handheld *all ret); } =20 + if (cfg->anti_deadzone_support) { + ret =3D ally_set_anti_deadzone(ally, hdev, cfg, + cfg->left_anti_deadzone, + cfg->right_anti_deadzone); + if (ret < 0) + hid_warn(hdev, + "Failed to restore anti-deadzone: %d\n", + ret); + } + return 0; } =20 --=20 2.47.3 From nobody Fri Sep 25 20:46:57 2026 Received: from mta1.migadu.com (out-98.mta1.migadu.com [95.215.58.98]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id F2B8B360EC4 for ; Tue, 8 Sep 2026 17:45:19 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=95.215.58.98 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788889522; cv=none; b=Z3YOj/0SsGcx9YErEaZurO2f1U8r2fnWVm348ZKUn9gR0/SxdNhrxpxie5iBmo042Le75CHjR8yUYbL5Fgeh967KnCcj+wfSN1/OrEW4H4bERRTk2GAhXGL67LC3QWHIrU1igFTkgQGNtZr1+QldR+Ufrd+AYVAkDmp7r22BpSI= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788889522; c=relaxed/simple; bh=sy/8HKNE5RtnMZwFzo9oIYw91nMcdktxdT1eayu7uP4=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=glQkqes0HPdGgtoOXfXr4kP1d2JKAcWWtW84u1FE2Ekwown/IZM/hGq/G7XLzSlgy3derb6k0eylU/3uV1/YHLhfMCGmHAXPzvt5gzGIYZcWE1JVpnMait16JOlMzovYOJmLBVSgool3u+puuZLglwl0H5/SJLHkaU04gG8HLXM= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=Dw1zCyld; arc=none smtp.client-ip=95.215.58.98 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="Dw1zCyld" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=sy/8HKNE5RtnMZwFzo9oIYw91nMcdktxdT1eayu7uP4=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1788889517; v=1; x=1789494317; b=Dw1zCyldDVuaEAS1c+s3+7h0ZjPSk4HQ7/l9sMN/9Crr4yQ8pRsobBiU9Iem4ebvKeIZfTxl hbteIvMK5hAAjMvdV857GgzdShLUW/ELJyopOpXLAZsnoNvYUk1xSELnpltFFnnoG8GUSZGg8ZC 7ljDBPHrPf7OI2tZylr1xpZs= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id eac399c423dd0653; Tue, 08 Sep 2026 17:45:05 +0000 X-Mizu-Trace-ID: eac399c423dd0653 X-Migadu-Flow: FLOW_OUT From: Denis Benato To: linux-kernel@vger.kernel.org Cc: linux-input@vger.kernel.org, "Benjamin Tissoires" , "Jiri Kosina" , "Luke D . Jones" , "Mateusz Schwartz" , "Denis Benato" , "Jonathan LoBue" , "Khamunetri Clark" , "Derek J. Clark" , Denis Benato Subject: [PATCH v6 09/13] HID: asus: add support for response curve Date: Tue, 8 Sep 2026 17:44:29 +0000 Message-ID: <20260908174433.74260-10-denis.benato@linux.dev> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260908174433.74260-1-denis.benato@linux.dev> References: <20260908174433.74260-1-denis.benato@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" 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. Assisted-by: opencode:glm-5.2 Signed-off-by: Denis Benato Signed-off-by: Luke Jones --- drivers/hid/hid-asus.c | 489 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 486 insertions(+), 3 deletions(-) diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c index 4ed438121cc9..f338c0eed54b 100644 --- a/drivers/hid/hid-asus.c +++ b/drivers/hid/hid-asus.c @@ -134,6 +134,14 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad"); =20 #define TRKID_SGN ((TRKID_MAX + 1) >> 1) =20 +#define ALLY_DEVICE_ATTR_WO(_name, _sysfs_name) \ + struct device_attribute dev_attr_##_name =3D \ + __ATTR(_sysfs_name, 0200, NULL, _name##_store) + +#define ALLY_DEVICE_ATTR_RW(_name, _sysfs_name) \ + struct device_attribute dev_attr_##_name =3D \ + __ATTR(_sysfs_name, 0644, _name##_show, _name##_store) + enum asus_work_action_type { FN_LOCK_SYNC, BRIGHTNESS_SET, @@ -178,6 +186,18 @@ struct asus_touchpad_info { int report_size; }; =20 +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; + struct ally_config { /* Must be locked if the data is being changed */ struct mutex config_mutex; @@ -210,6 +230,9 @@ struct ally_config { /* Vibration settings */ u8 vibration_intensity_left; u8 vibration_intensity_right; + + struct ally_joystick_resp_curve left_curve; + struct ally_joystick_resp_curve right_curve; }; =20 /* @@ -1863,6 +1886,403 @@ static struct device_attribute dev_attr_right_trigg= er_range_upper_limit =3D static struct device_attribute dev_attr_right_trigger_range_upper_limit_ra= nge =3D __ATTR(range_upper_limit_range, 0444, right_trigger_range_upper_limit_ran= ge_show, NULL); =20 +enum ally_joystick_side { + JOYSTICK_LEFT =3D 0, + JOYSTICK_RIGHT, +}; + +/** + * ally_set_joystick_resp_curve() - Set joystick response curve parameters + * @ally: ally handheld structure + * @hdev: HID device + * @side: which joystick side (0=3Dleft, 1=3Dright) + * @curve: response curve parameter structure + * + * Return: 0 on success, negative errno on failure + */ +static int ally_set_joystick_resp_curve(struct ally_handheld *ally, + struct hid_device *hdev, enum ally_joystick_side side, + struct ally_joystick_resp_curve *curve) +{ + const u8 payload[] =3D { 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 + }; + int ret; + + u8 *buf __free(kfree) =3D ally_alloc_cmd(CMD_SET_RESP_CURVE, payload, siz= eof(payload)); + if (!buf) + return -ENOMEM; + + ret =3D ally_gamepad_send_packet(ally, hdev, buf, ROG_ALLY_REPORT_SIZE); + if (ret < 0) + return ret; + + return 0; +} + +/** + * ally_resp_curve_is_valid() - Check that a response curve is valid + * @curve: response curve parameter structure + * + * Return: true if the curve is valid, false otherwise + */ +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; +} + +static int response_curve_apply(struct ally_handheld *ally, + struct hid_device *hdev, + struct ally_config *cfg, bool is_left) +{ + struct ally_joystick_resp_curve curve; + int ret; + + guard(mutex)(&cfg->config_mutex); + + if (!cfg->resp_curve_support) + return -EOPNOTSUPP; + + curve =3D is_left ? cfg->left_curve : cfg->right_curve; + + if (!ally_resp_curve_is_valid(&curve)) + return -EINVAL; + + ret =3D ally_set_joystick_resp_curve(ally, hdev, + is_left ? JOYSTICK_LEFT : JOYSTICK_RIGHT, + &curve); + if (ret) { + hid_err(hdev, "Failed to set joystick response curve: %d\n", ret); + return ret; + } + + return 0; +} + +static ssize_t left_response_curve_apply_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *const ally =3D drvdata->rog_ally; + struct ally_config *cfg; + bool apply; + int ret; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + ret =3D kstrtobool(buf, &apply); + if (ret) + return ret; + + if (!apply) + return count; + + ret =3D response_curve_apply(ally, hdev, cfg, true); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t right_response_curve_apply_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *const ally =3D drvdata->rog_ally; + struct ally_config *cfg; + bool apply; + int ret; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + ret =3D kstrtobool(buf, &apply); + if (ret) + return ret; + + if (!apply) + return count; + + ret =3D response_curve_apply(ally, hdev, cfg, false); + if (ret < 0) + return ret; + + return count; +} + +static ALLY_DEVICE_ATTR_WO(left_response_curve_apply, response_curve_apply= ); +static ALLY_DEVICE_ATTR_WO(right_response_curve_apply, response_curve_appl= y); + +static ssize_t response_curve_pct_show(char *buf, + struct ally_config *cfg, bool is_left, + unsigned int idx) +{ + struct ally_joystick_resp_curve *curve; + int ret =3D -EINVAL; + + guard(mutex)(&cfg->config_mutex); + + if (!cfg->resp_curve_support) + return -EOPNOTSUPP; + + curve =3D is_left ? &cfg->left_curve : &cfg->right_curve; + + switch (idx) { + case 1: + ret =3D sysfs_emit(buf, "%u\n", curve->entry_1.resp); + break; + case 2: + ret =3D sysfs_emit(buf, "%u\n", curve->entry_2.resp); + break; + case 3: + ret =3D sysfs_emit(buf, "%u\n", curve->entry_3.resp); + break; + case 4: + ret =3D sysfs_emit(buf, "%u\n", curve->entry_4.resp); + break; + } + + return ret; +} + +static ssize_t response_curve_move_show(char *buf, + struct ally_config *cfg, bool is_left, + unsigned int idx) +{ + struct ally_joystick_resp_curve *curve; + int ret =3D -EINVAL; + + guard(mutex)(&cfg->config_mutex); + + if (!cfg->resp_curve_support) + return -EOPNOTSUPP; + + curve =3D is_left ? &cfg->left_curve : &cfg->right_curve; + + switch (idx) { + case 1: + ret =3D sysfs_emit(buf, "%u\n", curve->entry_1.move); + break; + case 2: + ret =3D sysfs_emit(buf, "%u\n", curve->entry_2.move); + break; + case 3: + ret =3D sysfs_emit(buf, "%u\n", curve->entry_3.move); + break; + case 4: + ret =3D sysfs_emit(buf, "%u\n", curve->entry_4.move); + break; + } + + return ret; +} + +static ssize_t response_curve_pct_store(const char *buf, size_t count, + struct ally_config *cfg, bool is_left, + unsigned int idx) +{ + struct ally_joystick_resp_curve *curve; + u8 value; + int ret; + + ret =3D kstrtou8(buf, 10, &value); + if (ret) + return ret; + + if (value > 100) + return -EINVAL; + + guard(mutex)(&cfg->config_mutex); + + if (!cfg->resp_curve_support) + return -EOPNOTSUPP; + + curve =3D is_left ? &cfg->left_curve : &cfg->right_curve; + + switch (idx) { + case 1: + curve->entry_1.resp =3D value; + break; + case 2: + curve->entry_2.resp =3D value; + break; + case 3: + curve->entry_3.resp =3D value; + break; + case 4: + curve->entry_4.resp =3D value; + break; + default: + return -EINVAL; + } + + return count; +} + +static ssize_t response_curve_move_store(const char *buf, size_t count, + struct ally_config *cfg, bool is_left, + unsigned int idx) +{ + struct ally_joystick_resp_curve *curve; + u8 value; + int ret; + + ret =3D kstrtou8(buf, 10, &value); + if (ret) + return ret; + + if (value > 100) + return -EINVAL; + + guard(mutex)(&cfg->config_mutex); + + if (!cfg->resp_curve_support) + return -EOPNOTSUPP; + + curve =3D is_left ? &cfg->left_curve : &cfg->right_curve; + + switch (idx) { + case 1: + curve->entry_1.move =3D value; + break; + case 2: + curve->entry_2.move =3D value; + break; + case 3: + curve->entry_3.move =3D value; + break; + case 4: + curve->entry_4.move =3D value; + break; + default: + return -EINVAL; + } + + return count; +} + +#define DEFINE_JS_CURVE_PCT_FOPS(region, side) \ + static ssize_t side##_response_curve_pct_##region##_show( \ + struct device *dev, struct device_attribute *attr, \ + char *buf) \ + { \ + struct hid_device *hdev =3D to_hid_device(dev); \ + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); \ + struct ally_handheld *ally =3D drvdata->rog_ally; \ + struct ally_config *cfg; \ + \ + if (!ally) \ + return -ENODEV; \ + \ + cfg =3D ally_get_config(ally); \ + if (!cfg) \ + return -ENODEV; \ + \ + return response_curve_pct_show(buf, cfg, \ + side##_is_left, region);\ + } \ + \ + static ssize_t side##_response_curve_pct_##region##_store( \ + struct device *dev, struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + struct hid_device *hdev =3D to_hid_device(dev); \ + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); \ + struct ally_handheld *ally =3D drvdata->rog_ally; \ + struct ally_config *cfg; \ + \ + if (!ally) \ + return -ENODEV; \ + \ + cfg =3D ally_get_config(ally); \ + if (!cfg) \ + return -ENODEV; \ + \ + return response_curve_pct_store(buf, count, cfg, \ + side##_is_left, region);\ + } + +#define DEFINE_JS_CURVE_MOVE_FOPS(region, side) \ + static ssize_t side##_response_curve_move_##region##_show( \ + struct device *dev, struct device_attribute *attr, \ + char *buf) \ + { \ + struct hid_device *hdev =3D to_hid_device(dev); \ + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); \ + struct ally_handheld *ally =3D drvdata->rog_ally; \ + struct ally_config *cfg; \ + \ + if (!ally) \ + return -ENODEV; \ + \ + cfg =3D ally_get_config(ally); \ + if (!cfg) \ + return -ENODEV; \ + \ + return response_curve_move_show(buf, cfg, \ + side##_is_left, region);\ + } \ + \ + static ssize_t side##_response_curve_move_##region##_store( \ + struct device *dev, struct device_attribute *attr, \ + const char *buf, size_t count) \ + { \ + struct hid_device *hdev =3D to_hid_device(dev); \ + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); \ + struct ally_handheld *ally =3D drvdata->rog_ally; \ + struct ally_config *cfg; \ + \ + if (!ally) \ + return -ENODEV; \ + \ + cfg =3D ally_get_config(ally); \ + if (!cfg) \ + return -ENODEV; \ + \ + return response_curve_move_store(buf, count, cfg, \ + side##_is_left, region);\ + } + +#define DEFINE_JS_CURVE_ATTRS(region, side) \ + DEFINE_JS_CURVE_PCT_FOPS(region, side) \ + DEFINE_JS_CURVE_MOVE_FOPS(region, side) \ + static ALLY_DEVICE_ATTR_RW(side##_response_curve_pct_##region, \ + response_curve_pct_##region); \ + static ALLY_DEVICE_ATTR_RW(side##_response_curve_move_##region, \ + response_curve_move_##region) + +/* Helper defines for "is_left" parameter in DEFINE_JS_CURVE_ATTRS macros = */ +#define left_is_left true +#define right_is_left false + +DEFINE_JS_CURVE_ATTRS(1, left); +DEFINE_JS_CURVE_ATTRS(2, left); +DEFINE_JS_CURVE_ATTRS(3, left); +DEFINE_JS_CURVE_ATTRS(4, left); + +DEFINE_JS_CURVE_ATTRS(1, right); +DEFINE_JS_CURVE_ATTRS(2, right); +DEFINE_JS_CURVE_ATTRS(3, right); +DEFINE_JS_CURVE_ATTRS(4, right); + static struct attribute *ally_config_attrs[] =3D { &dev_attr_xbox_controller.attr, NULL @@ -1887,6 +2307,15 @@ static struct attribute *left_joystick_axis_attrs[] = =3D { &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_pct_1.attr, + &dev_attr_left_response_curve_pct_2.attr, + &dev_attr_left_response_curve_pct_3.attr, + &dev_attr_left_response_curve_pct_4.attr, + &dev_attr_left_response_curve_move_1.attr, + &dev_attr_left_response_curve_move_2.attr, + &dev_attr_left_response_curve_move_3.attr, + &dev_attr_left_response_curve_move_4.attr, + &dev_attr_left_response_curve_apply.attr, NULL }; =20 @@ -1897,6 +2326,15 @@ static struct attribute *right_joystick_axis_attrs[]= =3D { &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_pct_1.attr, + &dev_attr_right_response_curve_pct_2.attr, + &dev_attr_right_response_curve_pct_3.attr, + &dev_attr_right_response_curve_pct_4.attr, + &dev_attr_right_response_curve_move_1.attr, + &dev_attr_right_response_curve_move_2.attr, + &dev_attr_right_response_curve_move_3.attr, + &dev_attr_right_response_curve_move_4.attr, + &dev_attr_right_response_curve_apply.attr, NULL }; =20 @@ -2012,6 +2450,25 @@ static struct ally_config *ally_config_create(struct= hid_device *hdev, struct al cfg->right_trigger_max =3D 100; cfg->vibration_intensity_left =3D 100; cfg->vibration_intensity_right =3D 100; + + /* Initialize default response curve values (linear) */ + cfg->left_curve.entry_1.move =3D 0; + cfg->left_curve.entry_1.resp =3D 0; + cfg->left_curve.entry_2.move =3D 33; + cfg->left_curve.entry_2.resp =3D 33; + cfg->left_curve.entry_3.move =3D 66; + cfg->left_curve.entry_3.resp =3D 66; + cfg->left_curve.entry_4.move =3D 100; + cfg->left_curve.entry_4.resp =3D 100; + + cfg->right_curve.entry_1.move =3D 0; + cfg->right_curve.entry_1.resp =3D 0; + cfg->right_curve.entry_2.move =3D 33; + cfg->right_curve.entry_2.resp =3D 33; + cfg->right_curve.entry_3.move =3D 66; + cfg->right_curve.entry_3.resp =3D 66; + cfg->right_curve.entry_4.move =3D 100; + cfg->right_curve.entry_4.resp =3D 100; } =20 spin_lock_irqsave(&ally_data_lock, flags); @@ -2029,10 +2486,11 @@ static struct ally_config *ally_config_create(struc= t hid_device *hdev, struct al } =20 /* - * Skip the calibration and anti-deadzone groups when none of the - * features they expose is supported. + * Skip the calibration, anti-deadzone and response curve groups when + * none of the features they expose is supported. */ - if (cfg->user_cal_support || cfg->anti_deadzone_support) { + if (cfg->user_cal_support || cfg->anti_deadzone_support || + cfg->resp_curve_support) { int cal_i; =20 for (cal_i =3D 0; cal_i < ARRAY_SIZE(ally_cal_attr_groups); @@ -2421,6 +2879,31 @@ static int hid_asus_ally_init(struct hid_device *hde= v, struct ally_handheld *all ret); } =20 + if (cfg->resp_curve_support) { + /* + * sysfs stages the entries without validating them: do not + * push a curve to the MCU until userspace has applied one + * that is valid. + */ + if (ally_resp_curve_is_valid(&cfg->left_curve)) { + ret =3D ally_set_joystick_resp_curve(ally, hdev, JOYSTICK_LEFT, + &cfg->left_curve); + if (ret < 0) + hid_warn(hdev, + "Failed to restore left response curve: %d\n", + ret); + } + + if (ally_resp_curve_is_valid(&cfg->right_curve)) { + ret =3D ally_set_joystick_resp_curve(ally, hdev, JOYSTICK_RIGHT, + &cfg->right_curve); + if (ret < 0) + hid_warn(hdev, + "Failed to restore right response curve: %d\n", + ret); + } + } + return 0; } =20 --=20 2.47.3 From nobody Fri Sep 25 20:46:57 2026 Received: from mta1.migadu.com (out-109.mta1.migadu.com [95.215.58.109]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 6DDDB598C04 for ; Tue, 8 Sep 2026 17:45:23 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=95.215.58.109 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788889525; cv=none; b=AZ/AvSR/WgxFpGdO9TgnXY5UDUaYJWURFqqrzudWfmFSRVqFzIqNM5CWZ9cbg0WfNgRBYhgv1Zk0ZizbH+RIHJK/VgfAihcYU3q6Zlm7KDK1O3DL2vrp8Wuqv7R1RSeOjEXQYdk/J3X6PZB3FeddsDwONkB2Dun6+ros5gh7UjU= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788889525; c=relaxed/simple; bh=Sttvu8l7uDAArZB4r9Sn2rMgLX2QTrQP5za+QkOtp2s=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=AIqEEEZkwMj7LUq2VZ+akORvmeQgENVjKeKwY4riLmAXn7ZOeM55Kd09cqZIO4c980liNCbulqbYzZ2ga/PQLz2WsyDdMVZJKw15b5qZZfYFx4Hx32jHEn57VmFW6DONP5wAdRxtrDYawHkAepCAiLSd9ScxkQ9B9qQ9neJSVwY= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=YMTEY/zj; arc=none smtp.client-ip=95.215.58.109 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="YMTEY/zj" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=Sttvu8l7uDAArZB4r9Sn2rMgLX2QTrQP5za+QkOtp2s=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1788889521; v=1; x=1789494321; b=YMTEY/zjqIhSdtDrYeb+ZkTAaLQN5efEFKMxNBkfdDwupIf4eNMXwpeL14J7GU3sy+7TcVwh T2LNfFv50UTwk+umbkzjfPXaZB3vQs+A1JOIXhSoKd9qfNALnMD7zqaNjnYYxrOsTDlk2wL7fl5 AXZsRHDZEaGph8I6DM39ryz8= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id 4b8c07b41c49f7dd; Tue, 08 Sep 2026 17:45:20 +0000 X-Mizu-Trace-ID: 4b8c07b41c49f7dd X-Migadu-Flow: FLOW_OUT From: Denis Benato To: linux-kernel@vger.kernel.org Cc: linux-input@vger.kernel.org, "Benjamin Tissoires" , "Jiri Kosina" , "Luke D . Jones" , "Mateusz Schwartz" , "Denis Benato" , "Jonathan LoBue" , "Khamunetri Clark" , "Derek J. Clark" , Denis Benato Subject: [PATCH v6 10/13] HID: asus: add support to force feedback Date: Tue, 8 Sep 2026 17:44:30 +0000 Message-ID: <20260908174433.74260-11-denis.benato@linux.dev> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260908174433.74260-1-denis.benato@linux.dev> References: <20260908174433.74260-1-denis.benato@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" 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. Assisted-by: opencode:glm-5.2 Assisted-by: VSCode:gpt-5.3-codex Signed-off-by: Denis Benato Signed-off-by: Khamunetri Clark Signed-off-by: Luke Jones --- drivers/hid/Kconfig | 1 + drivers/hid/hid-asus.c | 305 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 278 insertions(+), 28 deletions(-) diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig index a81bf51cbcf1..607befcda579 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=3Dn + 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 f338c0eed54b..a75e6bd94790 100644 --- a/drivers/hid/hid-asus.c +++ b/drivers/hid/hid-asus.c @@ -244,6 +244,23 @@ struct ally_config { */ static struct ally_config *ally_config; =20 +/* XInput force-feedback report (output report 0x0d, gamepad interface) */ +struct 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 ff_report { + u8 report_id; + struct ff_data ff; +} __packed; + struct ally_handheld { /* All read/write to IN interfaces must lock */ struct mutex intf_mutex; @@ -258,6 +275,13 @@ struct ally_handheld { struct input_dev *ally_x_input; struct hid_device *ally_x_hdev; =20 + struct ff_report ff_packet; + struct work_struct ff_work; + /* Serializes ff_packet and update_ff between play_effect and ff_work */ + spinlock_t ff_lock; + bool ff_work_initialized; + bool update_ff; + struct hid_device *keyboard_hdev; struct input_dev *keyboard_input; =20 @@ -372,9 +396,13 @@ enum ally_command_codes { CMD_SET_ANTI_DEADZONE =3D 0x18, }; =20 +/* XInput rumble magnitudes use the hardware's 0..100 intensity range. */ +#define ALLY_FF_MAX_INTENSITY 100 + static const u8 ALLY_FORCE_FEEDBACK_OFF[] =3D { 0x0D, 0x0F, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xEB }; +static_assert(sizeof(struct ff_report) =3D=3D sizeof(ALLY_FORCE_FEEDBACK_O= FF)); =20 /* * The ROG Ally device presents multiple USB interfaces (keyboard, mouse, = gamepad, @@ -383,6 +411,7 @@ static const u8 ALLY_FORCE_FEEDBACK_OFF[] =3D { * ally_handheld structure to share state across these separate HID interf= aces. */ static void ally_resume_work_fn(struct work_struct *work); +static void ally_x_ff_work_fn(struct work_struct *work); =20 /* * Changes to ally_drvdata must lock: the raw_event callbacks, which may @@ -398,6 +427,13 @@ static struct ally_handheld ally_drvdata =3D { */ .resume_work =3D __DELAYED_WORK_INITIALIZER(ally_drvdata.resume_work, ally_resume_work_fn, 0), + /* + * Initialised statically for the same reason as resume_work: remove() + * drains the work whichever of the interfaces probed or failed to + * probe, so it must be safe to cancel unconditionally. + */ + .ff_work =3D __WORK_INITIALIZER(ally_drvdata.ff_work, ally_x_ff_work_fn), + .ff_lock =3D __SPIN_LOCK_UNLOCKED(ally_drvdata.ff_lock), }; =20 /* @@ -2544,7 +2580,8 @@ static void ally_config_remove(struct hid_device *hde= v, struct ally_config *cfg) * create them, so a device without those capabilities does not * trigger a "not found" warning. */ - if (cfg->user_cal_support || cfg->anti_deadzone_support) { + if (cfg->user_cal_support || cfg->anti_deadzone_support || + cfg->resp_curve_support) { for (i =3D 0; i < ARRAY_SIZE(ally_cal_attr_groups); i++) sysfs_remove_group(&hdev->dev.kobj, ally_cal_attr_groups[i]); @@ -2730,6 +2767,106 @@ static void ally_x_input_close(struct input_dev *de= v) hid_hw_close(input_get_drvdata(dev)); } =20 +/** + * ally_x_send_ff_report() - Send a force-feedback report to the gamepad + * @hdev: HID device + * @buf: buffer containing the report to send + * @len: length of the report + * + * The gamepad interface consumes force-feedback packets as output reports, + * unlike the config interface which expects feature reports: a rumble pac= ket + * sent as HID_REQ_SET_REPORT would be rejected by the hardware. + * + * The caller must hold ally->intf_mutex, so that a send cannot race with + * the gamepad interface being unbound and its transport being stopped. + * + * Return: count of data transferred, negative if error + */ +static int ally_x_send_ff_report(struct hid_device *hdev, + const u8 *buf, size_t len) +{ + u8 *dmabuf __free(kfree) =3D kmemdup(buf, len, GFP_KERNEL); + if (!dmabuf) + return -ENOMEM; + + return hid_hw_output_report(hdev, dmabuf, len); +} + +static int ally_x_send_ff_off(struct hid_device *hdev) +{ + return ally_x_send_ff_report(hdev, ALLY_FORCE_FEEDBACK_OFF, + sizeof(ALLY_FORCE_FEEDBACK_OFF)); +} + +static void ally_x_ff_work_fn(struct work_struct *work) +{ + struct ally_handheld *ally =3D + container_of(work, struct ally_handheld, ff_work); + struct hid_device *hdev =3D NULL; + struct ff_report report; + unsigned long flags; + bool update =3D false; + int ret; + + scoped_guard(spinlock_irqsave, &ally->ff_lock) { + if (ally->update_ff) { + report =3D ally->ff_packet; + ally->update_ff =3D false; + update =3D true; + } + } + + if (!update) + return; + + /* + * The hdev pointer is published and cleared under ally_data_lock: + * take a reference on it so the gamepad interface cannot be freed + * under us while the report is being sent. + */ + spin_lock_irqsave(&ally_data_lock, flags); + hdev =3D ally->ally_x_hdev; + if (hdev) + get_device(&hdev->dev); + spin_unlock_irqrestore(&ally_data_lock, flags); + + if (!hdev) + return; + + /* Serialize with the interface removal paths and other senders. */ + scoped_guard(mutex, &ally->intf_mutex) { + ret =3D ally_x_send_ff_report(hdev, (u8 *)&report, + sizeof(report)); + if (ret < 0) + hid_err(hdev, "Failed to send force-feedback: %d\n", + ret); + } + + put_device(&hdev->dev); +} + +static int ally_x_play_effect(struct input_dev *idev, void *data, + struct ff_effect *effect) +{ + struct ally_handheld *ally =3D &ally_drvdata; + + if (effect->type !=3D FF_RUMBLE) + return 0; + + scoped_guard(spinlock_irqsave, &ally->ff_lock) { + ally->ff_packet.ff.magnitude_strong =3D + effect->u.rumble.strong_magnitude * ALLY_FF_MAX_INTENSITY / 65535; + ally->ff_packet.ff.magnitude_weak =3D + effect->u.rumble.weak_magnitude * ALLY_FF_MAX_INTENSITY / 65535; + ally->update_ff =3D true; + + if (ally->ff_work_initialized) + schedule_work(&ally->ff_work); + } + + return 0; +} + static struct input_dev *ally_x_alloc_input_dev(struct hid_device *hdev) { struct input_dev *input_dev =3D input_allocate_device(); @@ -2793,6 +2930,30 @@ static int ally_x_setup_input(struct hid_device *hde= v, struct ally_handheld *all input_set_capability(input, EV_KEY, BTN_TRIGGER_HAPPY); input_set_capability(input, EV_KEY, BTN_TRIGGER_HAPPY1); =20 + memcpy(&ally->ff_packet, ALLY_FORCE_FEEDBACK_OFF, sizeof(ally->ff_packet)= ); + ally->ff_work_initialized =3D true; + + input_set_capability(input, EV_FF, FF_RUMBLE); + + /* + * A registered device advertising FF_RUMBLE without the memless core + * behind it would crash input_ff_upload(): fail the whole setup instead. + */ + ret =3D input_ff_create_memless(input, NULL, ally_x_play_effect); + if (ret) { + hid_err(hdev, "Failed to create force-feedback: %d\n", ret); + goto ally_x_setup_input_err; + } + + /* + * Publish the interface before the input device becomes visible to + * userspace: an effect uploaded right after registration would + * otherwise find a NULL hdev and get dropped. + */ + spin_lock_irqsave(&ally_data_lock, flags); + ally->ally_x_hdev =3D hdev; + spin_unlock_irqrestore(&ally_data_lock, flags); + ret =3D input_register_device(input); if (ret) { hid_err(hdev, "Failed to register Ally X gamepad device: %d\n", ret); @@ -2806,20 +2967,49 @@ static int ally_x_setup_input(struct hid_device *hd= ev, struct ally_handheld *all =20 return 0; ally_x_setup_input_err: + spin_lock_irqsave(&ally_data_lock, flags); + if (ally->ally_x_hdev =3D=3D hdev) + ally->ally_x_hdev =3D NULL; + spin_unlock_irqrestore(&ally_data_lock, flags); + input_free_device(input); return ret; } =20 static int hid_asus_ally_init(struct hid_device *hdev, struct ally_handhel= d *ally) { + struct hid_device *x_hdev; struct ally_config *cfg; + unsigned long flags; int ret; =20 - /* Failure at this point is non-critical */ - ret =3D ally_gamepad_send_packet(ally, 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); + /* + * Serialize with hid_asus_ally_remove(): the gamepad interface can + * be unbound while this initialization runs, and its transport is + * stopped as soon as the remove callback returns. Holding intf_mutex + * across the snapshot and the send makes the two atomic: the packet + * either reaches a transport that is still running, or the gamepad + * pointers are unpublished first and it is not sent at all. + */ + scoped_guard(mutex, &ally->intf_mutex) { + spin_lock_irqsave(&ally_data_lock, flags); + x_hdev =3D ally->ally_x_hdev; + if (x_hdev) + get_device(&x_hdev->dev); + spin_unlock_irqrestore(&ally_data_lock, flags); + + if (x_hdev) { + /* Failure at this point is non-critical */ + ret =3D ally_x_send_ff_off(x_hdev); + + if (ret < 0) + hid_err(hdev, + "Ally failed to init force-feedback off: %d\n", + ret); + + put_device(&x_hdev->dev); + } + } =20 cfg =3D ally_get_config(ally); if (!cfg) @@ -3074,9 +3264,17 @@ static struct ally_handheld *hid_asus_ally_probe(str= uct hid_device *hdev) return ERR_PTR(ret); } =20 - spin_lock_irqsave(&ally_data_lock, flags); - ally_drvdata.ally_x_hdev =3D hdev; - spin_unlock_irqrestore(&ally_data_lock, flags); + /* + * Make sure rumble starts disabled: this is the interface that + * owns the force-feedback output report. Failure is non-critical. + */ + scoped_guard(mutex, &ally_drvdata.intf_mutex) { + ret =3D ally_x_send_ff_off(hdev); + if (ret < 0) + hid_warn(hdev, "Failed to disable force-feedback: %d\n", + ret); + } + break; case HID_ALLY_INTF_KEYBOARD_IN: spin_lock_irqsave(&ally_data_lock, flags); @@ -3100,6 +3298,7 @@ static void hid_asus_ally_remove(struct hid_device *h= dev, struct ally_handheld * struct input_dev *x_input =3D NULL; struct ally_config *cfg =3D NULL; unsigned long flags; + bool owns_xpad; bool owns_cfg; =20 if (!ally) @@ -3114,31 +3313,81 @@ static void hid_asus_ally_remove(struct hid_device = *hdev, struct ally_handheld * cancel_delayed_work_sync(&ally->resume_work); =20 spin_lock_irqsave(&ally_data_lock, flags); - if (ally->ally_x_hdev =3D=3D hdev) { - x_input =3D ally->ally_x_input; - ally->ally_x_input =3D NULL; - ally->ally_x_hdev =3D NULL; + owns_xpad =3D ally->ally_x_hdev =3D=3D hdev; + spin_unlock_irqrestore(&ally_data_lock, flags); + + if (owns_xpad) { + /* + * Stop queueing force-feedback work before the gamepad + * pointers are cleared: play_effect() tests the flag under the + * same lock, so no work can be queued past the cancel below. + */ + scoped_guard(spinlock_irqsave, &ally->ff_lock) + ally->ff_work_initialized =3D false; + + /* + * cancel_work_sync() may sleep: keep it out of ally_data_lock, + * but run it before the pointers are cleared so in-flight work + * cannot outlive the interface it sends through. + */ + cancel_work_sync(&ally->ff_work); + + /* + * The input core can no longer stop effects through the + * disabled work: quiesce any rumble still playing ourselves, + * or the device would keep vibrating after being unbound. + * + * Serialize the packet with the other force-feedback senders: + * intf_mutex is taken again below for the pointer + * unpublishing, but the two critical sections never nest. + */ + scoped_guard(mutex, &ally->intf_mutex) { + if (ally_x_send_ff_off(hdev) < 0) + hid_warn(hdev, + "Failed to stop force-feedback\n"); + } } =20 /* - * The keyboard interface is torn down before the config one, and - * its input_dev is freed with it. handle_ally_event() and - * ally_resume_work_fn() both report keys through it from the - * config endpoint, so drop the references here or they dangle. + * Serialize with hid_asus_ally_init(): it sends the force-feedback + * "off" packet through the gamepad interface recorded here, whose + * transport is stopped by hid_hw_stop() as soon as this function + * returns. Holding intf_mutex while the gamepad pointers are + * unpublished makes init's snapshot-and-send atomic with the + * removal: the packet either reaches a transport that is still + * running, or is not sent at all. The lock must be released before + * the sysfs teardown below, or an in-flight sysfs store blocked on + * it would deadlock against kernfs waiting for the callback. */ - if (ally->keyboard_hdev =3D=3D hdev) { - ally->keyboard_input =3D NULL; - ally->keyboard_hdev =3D NULL; - } + scoped_guard(mutex, &ally->intf_mutex) { + spin_lock_irqsave(&ally_data_lock, flags); + if (owns_xpad) { + x_input =3D ally->ally_x_input; + ally->ally_x_input =3D NULL; + ally->ally_x_hdev =3D NULL; + } =20 - owns_cfg =3D ally->cfg_hdev =3D=3D hdev; - if (owns_cfg) { - cfg =3D ally->config; - ally->cfg_hdev =3D NULL; - ally->config =3D NULL; - } + /* + * The keyboard interface is torn down before the + * config one, and its input_dev is freed with it. + * handle_ally_event() and ally_resume_work_fn() both + * report keys through it from the config endpoint, so + * drop the references here or they dangle. + */ + if (ally->keyboard_hdev =3D=3D hdev) { + ally->keyboard_input =3D NULL; + ally->keyboard_hdev =3D NULL; + } =20 - spin_unlock_irqrestore(&ally_data_lock, flags); + owns_cfg =3D ally->cfg_hdev =3D=3D hdev; + if (owns_cfg) { + cfg =3D ally->config; + ally->cfg_hdev =3D NULL; + ally->config =3D NULL; + } + + spin_unlock_irqrestore(&ally_data_lock, flags); + } =20 /* * The config teardown removes sysfs groups and takes sleeping locks: --=20 2.47.3 From nobody Fri Sep 25 20:46:57 2026 Received: from mta0.migadu.com (out-247.mta0.migadu.com [91.218.175.247]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id B7C69599A39 for ; Tue, 8 Sep 2026 17:45:24 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.247 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788889526; cv=none; b=Eel2tM8mZJI+OzFxd9J7C34hKsStlCTmGsyAN4ZGpF0N6cuLHOa4Vq7oJj0MvVrJ1c1g/AomE9frds+u2JQbpTcpJC4FpAjSvRd4RJmAfUO2WMx4Bzn3HaN3gHhYI2A52juhfSg7M+U1FX47ii3AbxD+b/ZdOqFN4kouRStJe/k= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788889526; c=relaxed/simple; bh=ZFbgIlx8rqoF24uX2DB586iMvjr/OVNPgr08evpBVtU=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=h2I/SRxFSnCH3/L2t/n2eOCcEbjEN1lV2wj4cF/oBo0ROd5vnCiTuSxJd0ctIQLCRPYg6Hjuk/tDcfNbPb6UCKY8FMDuW3cmlgW4hfXcKnsPsnnIdEYbr+7XA+GKJ75b+rp7dG4eU6brhNbss1fUtijaxWD2WgctQw0e9HNy+J4= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=GOdMQmDk; arc=none smtp.client-ip=91.218.175.247 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="GOdMQmDk" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=ZFbgIlx8rqoF24uX2DB586iMvjr/OVNPgr08evpBVtU=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1788889522; v=1; x=1789494322; b=GOdMQmDkSQu3rmAid7LqS/QxirPmDfIKA3qWnWkyPsb1U2xMpXwzyS6Sl+GoujLfHq+4DeKg Dv94eluB8rqT9wODOJdoy+PX6syRZas7G8NBdS+G6JRgSCah7Gzg96zS1+g+EqoJTj6ZFCBK4XK poWDyzwZDhzJAcfd9FZg3iww= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id 0ea1af67d6da38d1; Tue, 08 Sep 2026 17:45:22 +0000 X-Mizu-Trace-ID: 0ea1af67d6da38d1 X-Migadu-Flow: FLOW_OUT From: Denis Benato To: linux-kernel@vger.kernel.org Cc: linux-input@vger.kernel.org, "Benjamin Tissoires" , "Jiri Kosina" , "Luke D . Jones" , "Mateusz Schwartz" , "Denis Benato" , "Jonathan LoBue" , "Khamunetri Clark" , "Derek J. Clark" , Denis Benato Subject: [PATCH v6 11/13] HID: asus: add support for gamepad mode Date: Tue, 8 Sep 2026 17:44:31 +0000 Message-ID: <20260908174433.74260-12-denis.benato@linux.dev> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260908174433.74260-1-denis.benato@linux.dev> References: <20260908174433.74260-1-denis.benato@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" 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. Assisted-by: opencode:glm-5.2 Signed-off-by: Luke Jones Signed-off-by: Denis Benato --- drivers/hid/hid-asus.c | 152 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 151 insertions(+), 1 deletion(-) diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c index a75e6bd94790..60228dc84b50 100644 --- a/drivers/hid/hid-asus.c +++ b/drivers/hid/hid-asus.c @@ -198,6 +198,11 @@ struct ally_joystick_resp_curve { struct ally_joystick_resp_curve_param entry_4; } __packed; =20 +enum ally_gamepad_mode_index { + ALLY_GAMEPAD_MODE_GAMEPAD =3D 0x01, + ALLY_GAMEPAD_MODE_KEYBOARD =3D 0x02, +}; + struct ally_config { /* Must be locked if the data is being changed */ struct mutex config_mutex; @@ -399,6 +404,15 @@ enum ally_command_codes { /* XInput rumble magnitudes use the hardware's 0..100 intensity range. */ #define ALLY_FF_MAX_INTENSITY 100 =20 +static const char *const ally_gamepad_mode_text[] =3D { + "gamepad", "desktop" +}; + +static const u8 ally_gamepad_mode[] =3D { + ALLY_GAMEPAD_MODE_GAMEPAD, + ALLY_GAMEPAD_MODE_KEYBOARD +}; + static const u8 ALLY_FORCE_FEEDBACK_OFF[] =3D { 0x0D, 0x0F, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xEB }; @@ -890,6 +904,131 @@ static ssize_t xbox_controller_store(struct device *d= ev, =20 static DEVICE_ATTR_RW(xbox_controller); =20 +/** + * ally_set_gamepad_mode() - Set the gamepad operating mode + * @ally: ally handheld structure + * @hdev: HID device + * @mode: gamepad mode to set + * + * Return: 0 on success, negative errno on failure + */ +static int ally_set_gamepad_mode(struct ally_handheld *ally, struct hid_de= vice *hdev, u8 mode) +{ + u8 payload[] =3D { mode }; + 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) =3D ally_alloc_cmd(CMD_SET_GAMEPAD_MODE, payload, s= izeof(payload)); + if (!buf) + return -ENOMEM; + + ret =3D ally_gamepad_send_packet(ally, hdev, buf, ROG_ALLY_REPORT_SIZE); + 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_attribu= te *attr, char *buf) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *ally =3D drvdata->rog_ally; + struct ally_config *cfg; + u8 mode_byte; + int i; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + guard(mutex)(&cfg->config_mutex); + + mode_byte =3D cfg->gamepad_mode; + + for (i =3D 0; i < ARRAY_SIZE(ally_gamepad_mode); i++) { + if (ally_gamepad_mode[i] =3D=3D mode_byte) + return sysfs_emit(buf, "%s\n", ally_gamepad_mode_text[i]); + } + + return sysfs_emit(buf, "unsupported\n"); +} + +static ssize_t gamepad_mode_store(struct device *dev, struct device_attrib= ute *attr, + const char *buf, size_t count) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *ally =3D drvdata->rog_ally; + struct ally_config *cfg; + u8 mode_byte; + int mode; + int ret; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + mode =3D sysfs_match_string(ally_gamepad_mode_text, buf); + if (mode < 0) { + hid_err(hdev, "Unknown gamepad mode\n"); + return mode; + } + + /* Convert the index of the text mode array to the byte + * that will be accepted by the ally MCU. + */ + mode_byte =3D ally_gamepad_mode[mode]; + + guard(mutex)(&cfg->config_mutex); + + ret =3D ally_set_gamepad_mode(ally, hdev, mode_byte); + if (ret < 0) + return ret; + + cfg->gamepad_mode =3D mode_byte; + + hid_dbg(hdev, "Set gamepad mode to %s\n", ally_gamepad_mode_text[mode]); + + return count; +} + +static ssize_t gamepad_mode_index_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + int i; + ssize_t len =3D 0; + + for (i =3D 0; i < ARRAY_SIZE(ally_gamepad_mode_text); i++) { + if (!ally_gamepad_mode_text[i] || ally_gamepad_mode_text[i][0] =3D=3D '\= 0') + continue; + len +=3D sysfs_emit_at(buf, len, "%s ", ally_gamepad_mode_text[i]); + } + + /* Replace the last space with a newline */ + if (len > 0) + buf[len - 1] =3D '\n'; + + return len; +} + +static DEVICE_ATTR_RW(gamepad_mode); +static DEVICE_ATTR_RO(gamepad_mode_index); + /** * ally_set_vibration_intensity() - Set vibration intensity values * @ally: ally handheld structure @@ -2321,6 +2460,8 @@ DEFINE_JS_CURVE_ATTRS(4, right); =20 static struct attribute *ally_config_attrs[] =3D { &dev_attr_xbox_controller.attr, + &dev_attr_gamepad_mode.attr, + &dev_attr_gamepad_mode_index.attr, NULL }; =20 @@ -2474,7 +2615,7 @@ static struct ally_config *ally_config_create(struct = hid_device *hdev, struct al } =20 if (!cfg->initialized) { - cfg->gamepad_mode =3D 0x01; + cfg->gamepad_mode =3D ALLY_GAMEPAD_MODE_GAMEPAD; cfg->xbox_controller_enabled =3D true; cfg->left_deadzone =3D 10; cfg->left_outer_threshold =3D 90; @@ -3026,6 +3167,15 @@ static int hid_asus_ally_init(struct hid_device *hde= v, struct ally_handheld *all ret); } =20 + /* + * An MCU reset switches the gamepad mode back to its default: + * send the recorded one first, so that every other setting + * restored below is applied on top of the expected mode. + */ + ret =3D ally_set_gamepad_mode(ally, hdev, cfg->gamepad_mode); + if (ret < 0) + hid_warn(hdev, "Failed to restore gamepad mode: %d\n", ret); + /* * The MCU does not keep the vibration intensity across a reset: * send the recorded values again, whichever interface probed. --=20 2.47.3 From nobody Fri Sep 25 20:46:57 2026 Received: from mta1.migadu.com (out-124.mta1.migadu.com [95.215.58.124]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 833C55A0AC4 for ; Tue, 8 Sep 2026 17:45:28 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=95.215.58.124 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788889531; cv=none; b=jBgJnwwfxRCtwqIUgb1IvB72tcBBedU1K35G6ioximUI7dpImDEiszUOk0j3Y8rYkETLCy4CCn9/urRg+OggumpIpNLNsoDGJej2rGFWgiEh+ShiCTmNagPyRce7iPfLXYEd0hf11uZkk1vxkl8qwHWrkEqggelbakscfdzrMfI= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788889531; c=relaxed/simple; bh=/iujUPyEwPp1RR0qWbhEhbKKLRiYpprCojwI+S22kNU=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=HqrYgM/k1R44/WBWLhTdjIhxxyE2d3J9xY+u8V7sH35X4uRO4vD60sbPKxHIYiVwaAHovzartDQXZyci+Y54KN3wDaFZTcX+kEY7SKsc9Z0MafZXy6lC2hQpl8S0xKZQirOI6jKYS7RdacrhXXf9hhnHV30IReS/9nWdOGfYmb4= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=DoWq4KZr; arc=none smtp.client-ip=95.215.58.124 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="DoWq4KZr" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=/iujUPyEwPp1RR0qWbhEhbKKLRiYpprCojwI+S22kNU=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1788889526; v=1; x=1789494326; b=DoWq4KZrF6jTHZyAaqsBDS21uS+sAVf0rPWxnc3LSQq7Gh23Zb6wUk0tDu1IFKwFqMzsCdBK yhuyCHfp9GX0NUMG8S0CJ0WLN+0EgFgtrX+3yepXQUBWyvshKWHtBpJKcj5CO9MDkbTuKs/ADE1 BTPYYy0LgyCjNq1pysPsPWe8= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id cffe16609a9ef39f; Tue, 08 Sep 2026 17:45:26 +0000 X-Mizu-Trace-ID: cffe16609a9ef39f X-Migadu-Flow: FLOW_OUT From: Denis Benato To: linux-kernel@vger.kernel.org Cc: linux-input@vger.kernel.org, "Benjamin Tissoires" , "Jiri Kosina" , "Luke D . Jones" , "Mateusz Schwartz" , "Denis Benato" , "Jonathan LoBue" , "Khamunetri Clark" , "Derek J. Clark" , Denis Benato Subject: [PATCH v6 12/13] HID: asus: add support for turbo buttons Date: Tue, 8 Sep 2026 17:44:32 +0000 Message-ID: <20260908174433.74260-13-denis.benato@linux.dev> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260908174433.74260-1-denis.benato@linux.dev> References: <20260908174433.74260-1-denis.benato@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" 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. Assisted-by: opencode:glm-5.2 Assisted-by: Claude:claude-fable-5 Signed-off-by: Denis Benato Signed-off-by: Khamunetri Clark Signed-off-by: Luke Jones Signed-off-by: Jonathan LoBue --- drivers/hid/hid-asus.c | 459 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 459 insertions(+) diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c index 60228dc84b50..5cdbcc2bd1bc 100644 --- a/drivers/hid/hid-asus.c +++ b/drivers/hid/hid-asus.c @@ -198,6 +198,99 @@ struct ally_joystick_resp_curve { struct ally_joystick_resp_curve_param entry_4; } __packed; =20 +/* Button identifiers for the turbo attribute system */ +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 +}; + +/* Names for the button directories in sysfs */ +static const char *const ally_button_names[ALLY_BTN_MAX] =3D { + [ALLY_BTN_A] =3D "btn_a", + [ALLY_BTN_B] =3D "btn_b", + [ALLY_BTN_X] =3D "btn_x", + [ALLY_BTN_Y] =3D "btn_y", + [ALLY_BTN_LB] =3D "btn_lb", + [ALLY_BTN_RB] =3D "btn_rb", + [ALLY_BTN_DU] =3D "dpad_up", + [ALLY_BTN_DD] =3D "dpad_down", + [ALLY_BTN_DL] =3D "dpad_left", + [ALLY_BTN_DR] =3D "dpad_right", + [ALLY_BTN_J0B] =3D "btn_l3", + [ALLY_BTN_J1B] =3D "btn_r3", + [ALLY_BTN_MENU] =3D "btn_menu", + [ALLY_BTN_VIEW] =3D "btn_view", + [ALLY_BTN_M1] =3D "btn_m1", + [ALLY_BTN_M2] =3D "btn_m2", +}; + +/* + * Button turbo parameters structure + * Each button can have: + * - turbo: Turbo press interval in multiples of 50ms (0 =3D disabled, 1-2= 0 =3D 50ms-1000ms) + * - toggle: Toggle interval (0 =3D disabled) + */ +struct ally_btn_turbo_params { + u8 turbo; + u8 toggle; +} __packed; + +#define ALLY_TURBO_PERIOD_MIN 0 +#define ALLY_TURBO_PERIOD_MAX 20 +#define ALLY_TOGGLE_PERIOD_MIN 0 +#define ALLY_TOGGLE_PERIOD_MAX 255 + +/* Collection of all button turbo settings */ +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]; /* turbo_period + toggle_period + ranges + NU= LL */ + /* + * The entries are created while hid_asus_ally_probe() is still running, + * before asus_probe() publishes drvdata->rog_ally: carrying the shared + * state pointer here avoids dereferencing that not-yet-assigned field. + */ + struct ally_handheld *ally; + struct ally_config *cfg; + struct hid_device *hdev; + enum ally_button_id btn; + struct device_attribute attr_turbo_period; + struct device_attribute attr_toggle_period; +}; + enum ally_gamepad_mode_index { ALLY_GAMEPAD_MODE_GAMEPAD =3D 0x01, ALLY_GAMEPAD_MODE_KEYBOARD =3D 0x02, @@ -236,6 +329,9 @@ struct ally_config { u8 vibration_intensity_left; u8 vibration_intensity_right; =20 + struct ally_turbo_config turbo; + struct ally_btn_sysfs_entry *button_entries; + struct ally_joystick_resp_curve left_curve; struct ally_joystick_resp_curve right_curve; }; @@ -2579,6 +2675,339 @@ static const struct attribute_group *const ally_cal= _attr_groups[] =3D { &ally_right_trigger_group, }; =20 +/** + * ally_set_turbo_params() - Set turbo parameters for all buttons + * @ally: ally handheld structure + * @hdev: HID device + * @cfg: ally config structure + * + * The payload mirrors the whole per-button turbo configuration, so the + * caller must hold cfg->config_mutex: building it locklessly could send a + * mix of parameters from concurrent sysfs writes to the hardware. + * + * Return: 0 on success, negative errno on failure + */ +static int ally_set_turbo_params(struct ally_handheld *ally, + struct hid_device *hdev, + struct ally_config *cfg) +{ + struct ally_turbo_config *turbo =3D &cfg->turbo; + const u8 payload[] =3D { + 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, + }; + int ret; + + u8 *buf __free(kfree) =3D ally_alloc_cmd(CMD_SET_TURBO_PARAMS, payload, s= izeof(payload)); + if (!buf) + return -ENOMEM; + + ret =3D ally_gamepad_send_packet(ally, hdev, buf, ROG_ALLY_REPORT_SIZE); + if (ret < 0) { + hid_err(hdev, "Failed to set turbo parameters: %d\n", ret); + return ret; + } + + return 0; +} + +static struct ally_btn_turbo_params *ally_btn_get_turbo_params(struct ally= _config *cfg, + enum ally_button_id btn) +{ + switch (btn) { + case ALLY_BTN_DU: return &cfg->turbo.btn_du; + case ALLY_BTN_DD: return &cfg->turbo.btn_dd; + case ALLY_BTN_DL: return &cfg->turbo.btn_dl; + case ALLY_BTN_DR: return &cfg->turbo.btn_dr; + case ALLY_BTN_J0B: return &cfg->turbo.btn_j0b; + case ALLY_BTN_J1B: return &cfg->turbo.btn_j1b; + case ALLY_BTN_LB: return &cfg->turbo.btn_lb; + case ALLY_BTN_RB: return &cfg->turbo.btn_rb; + case ALLY_BTN_A: return &cfg->turbo.btn_a; + case ALLY_BTN_B: return &cfg->turbo.btn_b; + case ALLY_BTN_X: return &cfg->turbo.btn_x; + case ALLY_BTN_Y: return &cfg->turbo.btn_y; + case ALLY_BTN_VIEW: return &cfg->turbo.btn_view; + case ALLY_BTN_MENU: return &cfg->turbo.btn_menu; + case ALLY_BTN_M2: return &cfg->turbo.btn_m2; + case ALLY_BTN_M1: return &cfg->turbo.btn_m1; + default: return NULL; + } +} + +static ssize_t btn_turbo_period_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct ally_btn_sysfs_entry *entry =3D container_of(attr, struct ally_btn= _sysfs_entry, + attr_turbo_period); + struct ally_btn_turbo_params *params; + + if (!entry->cfg->turbo_support) + return -EOPNOTSUPP; + + params =3D ally_btn_get_turbo_params(entry->cfg, entry->btn); + if (!params) + return -ENODEV; + + guard(mutex)(&entry->cfg->config_mutex); + + return sysfs_emit(buf, "%hhu\n", params->turbo); +} + +static ssize_t btn_turbo_period_store(struct device *dev, struct device_at= tribute *attr, + const char *buf, size_t count) +{ + struct ally_btn_sysfs_entry *entry =3D container_of(attr, struct ally_btn= _sysfs_entry, + attr_turbo_period); + struct ally_btn_turbo_params *params; + u8 old_value, value; + int ret; + + if (!entry->cfg->turbo_support) + return -EOPNOTSUPP; + + params =3D ally_btn_get_turbo_params(entry->cfg, entry->btn); + if (!params) + return -ENODEV; + + ret =3D kstrtou8(buf, 10, &value); + if (ret) + return ret; + + if (value < ALLY_TURBO_PERIOD_MIN || value > ALLY_TURBO_PERIOD_MAX) + return -EINVAL; + + guard(mutex)(&entry->cfg->config_mutex); + + old_value =3D params->turbo; + params->turbo =3D value; + ret =3D ally_set_turbo_params(entry->ally, entry->hdev, entry->cfg); + if (ret) { + /* + * The hardware rejected the update: restore the old + * value or a later write to another button would + * flush this stale one to the device. + */ + params->turbo =3D old_value; + return ret; + } + + return count; +} + +static ssize_t btn_toggle_period_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct ally_btn_sysfs_entry *entry =3D container_of(attr, struct ally_btn= _sysfs_entry, + attr_toggle_period); + struct ally_btn_turbo_params *params; + + if (!entry->cfg->turbo_support) + return -EOPNOTSUPP; + + params =3D ally_btn_get_turbo_params(entry->cfg, entry->btn); + if (!params) + return -ENODEV; + + guard(mutex)(&entry->cfg->config_mutex); + + return sysfs_emit(buf, "%hhu\n", params->toggle); +} + +static ssize_t btn_toggle_period_store(struct device *dev, struct device_a= ttribute *attr, + const char *buf, size_t count) +{ + struct ally_btn_sysfs_entry *entry =3D container_of(attr, struct ally_btn= _sysfs_entry, + attr_toggle_period); + struct ally_btn_turbo_params *params; + u8 old_value, value; + int ret; + + if (!entry->cfg->turbo_support) + return -EOPNOTSUPP; + + params =3D ally_btn_get_turbo_params(entry->cfg, entry->btn); + if (!params) + return -ENODEV; + + ret =3D kstrtou8(buf, 10, &value); + if (ret) + return ret; + + if (value < ALLY_TOGGLE_PERIOD_MIN || value > ALLY_TOGGLE_PERIOD_MAX) + return -EINVAL; + + guard(mutex)(&entry->cfg->config_mutex); + + old_value =3D params->toggle; + params->toggle =3D value; + ret =3D ally_set_turbo_params(entry->ally, entry->hdev, entry->cfg); + if (ret) { + /* + * The hardware rejected the update: restore the old + * value or a later write to another button would + * flush this stale one to the device. + */ + params->toggle =3D old_value; + return ret; + } + + 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 =3D + __ATTR(turbo_period_range, 0444, btn_turbo_period_range_show, NULL); + +static struct device_attribute dev_attr_btn_toggle_period_range =3D + __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 =3D "turbo_period"; + entry->attr_turbo_period.attr.mode =3D 0644; + entry->attr_turbo_period.show =3D btn_turbo_period_show; + entry->attr_turbo_period.store =3D btn_turbo_period_store; + + sysfs_attr_init(&entry->attr_toggle_period.attr); + entry->attr_toggle_period.attr.name =3D "toggle_period"; + entry->attr_toggle_period.attr.mode =3D 0644; + entry->attr_toggle_period.show =3D btn_toggle_period_show; + entry->attr_toggle_period.store =3D btn_toggle_period_store; + entry->attrs[0] =3D &entry->attr_turbo_period.attr; + entry->attrs[1] =3D &entry->attr_toggle_period.attr; + entry->attrs[2] =3D &dev_attr_btn_turbo_period_range.attr; + entry->attrs[3] =3D &dev_attr_btn_toggle_period_range.attr; + entry->attrs[4] =3D NULL; +} + +/** + * ally_create_button_attributes() - Create turbo button attributes + * @hdev: HID device + * @ally: ally handheld structure + * @cfg: ally config structure + * + * Return: 0 on success, negative errno on failure + */ +static int ally_create_button_attributes(struct hid_device *hdev, + struct ally_handheld *ally, + struct ally_config *cfg) +{ + struct ally_btn_sysfs_entry *entries; + int i, ret; + + if (!cfg->turbo_support) + return 0; + + entries =3D kcalloc(ALLY_BTN_MAX, sizeof(*entries), GFP_KERNEL); + if (!entries) + return -ENOMEM; + + cfg->button_entries =3D entries; + + for (i =3D 0; i < ALLY_BTN_MAX; i++) { + entries[i].ally =3D ally; + entries[i].cfg =3D cfg; + entries[i].hdev =3D hdev; + entries[i].btn =3D i; + + if (!ally_btn_get_turbo_params(cfg, i)) { + hid_err(hdev, "Invalid button id %d for turbo attributes\n", i); + ret =3D -EINVAL; + goto err_cleanup; + } + + ally_btn_turbo_init_attrs(&entries[i]); + + entries[i].group.name =3D ally_button_names[i]; + entries[i].group.attrs =3D entries[i].attrs; + + ret =3D 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 err_cleanup; + } + } + + return 0; + +err_cleanup: + while (--i >=3D 0) + sysfs_remove_group(&hdev->dev.kobj, &entries[i].group); + + kfree(entries); + + cfg->button_entries =3D NULL; + + return ret; +} + +/** + * ally_remove_button_attributes() - Remove turbo button attributes + * @hdev: HID device + * @cfg: ally config structure + */ +static void ally_remove_button_attributes(struct hid_device *hdev, struct = ally_config *cfg) +{ + struct ally_btn_sysfs_entry *entries; + int i; + + if (!cfg || !cfg->button_entries) + return; + + entries =3D cfg->button_entries; + + for (i =3D 0; i < ALLY_BTN_MAX; i++) + sysfs_remove_group(&hdev->dev.kobj, &entries[i].group); + + kfree(entries); + + cfg->button_entries =3D NULL; +} + /** * ally_config_create() - Initialize configuration and create sysfs entries * @hdev: HID device @@ -2590,6 +3019,7 @@ static struct ally_config *ally_config_create(struct = hid_device *hdev, struct al { struct ally_config *cfg; unsigned long flags; + int cal_created =3D 0; int ret, sysfs_i; =20 /* @@ -2683,12 +3113,30 @@ static struct ally_config *ally_config_create(struc= t hid_device *hdev, struct al goto ally_config_create_sysfs_err; } } + + /* All the groups exist now: the error path must remove them. */ + cal_created =3D ARRAY_SIZE(ally_cal_attr_groups); + } + + if (cfg->turbo_support) { + ret =3D ally_create_button_attributes(hdev, ally, cfg); + if (ret < 0) { + hid_err(hdev, "Failed to create button attributes: %d\n", ret); + goto ally_config_create_sysfs_err; + } } =20 cfg->initialized =3D true; =20 return cfg; ally_config_create_sysfs_err: + if (cfg->button_entries) + ally_remove_button_attributes(hdev, cfg); + + while (cal_created-- > 0) + sysfs_remove_group(&hdev->dev.kobj, + ally_cal_attr_groups[cal_created]); + spin_lock_irqsave(&ally_data_lock, flags); ally->config =3D NULL; spin_unlock_irqrestore(&ally_data_lock, flags); @@ -2713,6 +3161,9 @@ static void ally_config_remove(struct hid_device *hde= v, struct ally_config *cfg) if (!cfg || !cfg->initialized) return; =20 + if (cfg->button_entries) + ally_remove_button_attributes(hdev, cfg); + for (i =3D 0; i < ARRAY_SIZE(ally_attr_groups); i++) sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]); =20 @@ -3244,6 +3695,14 @@ static int hid_asus_ally_init(struct hid_device *hde= v, struct ally_handheld *all } } =20 + if (cfg->turbo_support) { + ret =3D ally_set_turbo_params(ally, hdev, cfg); + if (ret < 0) + hid_warn(hdev, + "Failed to restore turbo parameters: %d\n", + ret); + } + return 0; } =20 --=20 2.47.3 From nobody Fri Sep 25 20:46:57 2026 Received: from mta1.migadu.com (out-131.mta1.migadu.com [95.215.58.131]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 1EC875A0AD4 for ; Tue, 8 Sep 2026 17:45:30 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=95.215.58.131 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788889534; cv=none; b=COPjhKjIJPpf5j6+i1bQf+1k7HEFuyjzOZL1K3JiGcT5fs6WgZdzmPtCb8M6v6Ouctgzn1tGH89lTbZ1ycA5XK7aOyLOD06ohpOsvVy6QL/3i9DRExxZXDXkgxnqBKH2F6gek9U7LytwT+So3zEN0PxpvIgdWkxDd7lOfvz2zrc= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788889534; c=relaxed/simple; bh=OLhuKjQH0XGpsZx4qgCcoA3we5eptwMSErErx50YYe4=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=qTgjd/+vmC6rclqwyKeYAuJ7q8QmdIJ4+cLLVHCz+NZEE3e5yM9cbyp02oM1rHLs7T8eoPwUXabhTtgXpjnSmhgYuS7H/7zzzR1MI5PX8QWkCWmhyk1/Oz6kOU7hVXoT7KdEwAkTeEcYvbRwaHEfg6OKDX5MrA0vL8xZ9TI2od4= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=IY/FlnJs; arc=none smtp.client-ip=95.215.58.131 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="IY/FlnJs" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=OLhuKjQH0XGpsZx4qgCcoA3we5eptwMSErErx50YYe4=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1788889528; v=1; x=1789494328; b=IY/FlnJs6MpkCpX8o1+tdXJx89NMOBsKp5rwYQzikCRVO7iMwVohUW3J6PUAchnnyEgKgxVy VRB5ci1s4QXYXQ3/mPd+EWXnXuMXPCdmruhoSmebJ1Hho2RCg7WkmWP+tVPSYIX5ipKl5f4WoLm QkAZdj0uMPqYvqk2phCQaIBs= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id 20351cc4c36e4b85; Tue, 08 Sep 2026 17:45:28 +0000 X-Mizu-Trace-ID: 20351cc4c36e4b85 X-Migadu-Flow: FLOW_OUT From: Denis Benato To: linux-kernel@vger.kernel.org Cc: linux-input@vger.kernel.org, "Benjamin Tissoires" , "Jiri Kosina" , "Luke D . Jones" , "Mateusz Schwartz" , "Denis Benato" , "Jonathan LoBue" , "Khamunetri Clark" , "Derek J. Clark" , Denis Benato Subject: [PATCH v6 13/13] HID: asus: add support for btn remapping Date: Tue, 8 Sep 2026 17:44:33 +0000 Message-ID: <20260908174433.74260-14-denis.benato@linux.dev> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260908174433.74260-1-denis.benato@linux.dev> References: <20260908174433.74260-1-denis.benato@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" 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. Assisted-by: opencode:glm-5.2 Assisted-by: Claude:claude-fable-5 Signed-off-by: Luke Jones Signed-off-by: Denis Benato Signed-off-by: Jonathan LoBue --- drivers/hid/hid-asus.c | 997 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 985 insertions(+), 12 deletions(-) diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c index 5cdbcc2bd1bc..f8655c1ab735 100644 --- a/drivers/hid/hid-asus.c +++ b/drivers/hid/hid-asus.c @@ -275,9 +275,25 @@ struct ally_turbo_config { struct ally_btn_turbo_params btn_m1; }; =20 +/* Button remap attribute structure */ +struct button_remap_attr { + struct device_attribute dev_attr; + enum ally_button_id button_id; + bool is_macro; + /* + * The attributes are created while hid_asus_ally_probe() is still + * running, before asus_probe() publishes drvdata->rog_ally: carry + * the pointer like the turbo entries do, so the callbacks work + * from the moment the files exist. + */ + struct ally_handheld *ally; +}; + +#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]; /* turbo_period + toggle_period + ranges + NU= LL */ + struct attribute *attrs[8]; /* turbo + ranges + remap + macro + remap_ind= ex + NULL */ /* * The entries are created while hid_asus_ally_probe() is still running, * before asus_probe() publishes drvdata->rog_ally: carrying the shared @@ -289,6 +305,9 @@ struct ally_btn_sysfs_entry { 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_index; }; =20 enum ally_gamepad_mode_index { @@ -296,6 +315,35 @@ enum ally_gamepad_mode_index { ALLY_GAMEPAD_MODE_KEYBOARD =3D 0x02, }; =20 +/* Button pair indexes for mapping commands */ +enum btn_pair_index { + BTN_PAIR_DPAD_UPDOWN =3D 0x01, + BTN_PAIR_DPAD_LEFTRIGHT =3D 0x02, + BTN_PAIR_STICK_LR =3D 0x03, + BTN_PAIR_BUMPER_LR =3D 0x04, + BTN_PAIR_AB =3D 0x05, + BTN_PAIR_XY =3D 0x06, + BTN_PAIR_VIEW_MENU =3D 0x07, + BTN_PAIR_M1M2 =3D 0x08, + BTN_PAIR_TRIGGER_LR =3D 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; +}; + +/* Store button mapping per gamepad mode */ +struct ally_button_mapping { + struct button_pair_map button_pairs[9]; /* 9 button pairs */ +}; + struct ally_config { /* Must be locked if the data is being changed */ struct mutex config_mutex; @@ -331,6 +379,8 @@ struct ally_config { =20 struct ally_turbo_config turbo; struct ally_btn_sysfs_entry *button_entries; + /* Button mappings of the current gamepad mode */ + struct ally_button_mapping button_mappings; =20 struct ally_joystick_resp_curve left_curve; struct ally_joystick_resp_curve right_curve; @@ -1032,6 +1082,295 @@ static int ally_set_gamepad_mode(struct ally_handhe= ld *ally, struct hid_device * return 0; } =20 +enum btn_map_type { + BTN_TYPE_NONE =3D 0, + BTN_TYPE_PAD =3D 0x01, + BTN_TYPE_KB =3D 0x02, + BTN_TYPE_MOUSE =3D 0x03, + BTN_TYPE_MEDIA =3D 0x05, +}; + +struct btn_code_map { + unsigned char type; + unsigned char value; + const char *name; +}; + +static const struct btn_code_map ally_btn_codes[] =3D { + { 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" }, +}; + +static const size_t keymap_len =3D ARRAY_SIZE(ally_btn_codes); + +static const struct btn_code_map *find_button_by_name(const char *name); + +static void ally_set_default_gamepad_mapping(struct ally_button_mapping *m= ap) +{ + int i; + + /* Set all pair indexes and initialize to NONE */ + for (i =3D 0; i < 9; i++) { + map->button_pairs[i].pair_index =3D i + 1; + map->button_pairs[i].first.remap =3D + find_button_by_name("NONE"); + map->button_pairs[i].first.macro =3D + find_button_by_name("NONE"); + map->button_pairs[i].second.remap =3D + find_button_by_name("NONE"); + map->button_pairs[i].second.macro =3D + find_button_by_name("NONE"); + } + + map->button_pairs[BTN_PAIR_AB - 1].first.remap =3D + find_button_by_name("BTN_SOUTH"); + map->button_pairs[BTN_PAIR_AB - 1].second.remap =3D + find_button_by_name("BTN_EAST"); + + map->button_pairs[BTN_PAIR_XY - 1].first.remap =3D + find_button_by_name("BTN_NORTH"); + map->button_pairs[BTN_PAIR_XY - 1].second.remap =3D + find_button_by_name("BTN_WEST"); + + map->button_pairs[BTN_PAIR_BUMPER_LR - 1].first.remap =3D + find_button_by_name("BTN_TL"); + map->button_pairs[BTN_PAIR_BUMPER_LR - 1].second.remap =3D + find_button_by_name("BTN_TR"); + + map->button_pairs[BTN_PAIR_STICK_LR - 1].first.remap =3D + find_button_by_name("BTN_THUMBL"); + map->button_pairs[BTN_PAIR_STICK_LR - 1].second.remap =3D + find_button_by_name("BTN_THUMBR"); + + map->button_pairs[BTN_PAIR_DPAD_UPDOWN - 1].first.remap =3D + find_button_by_name("ABS_HAT0Y_NEG"); + map->button_pairs[BTN_PAIR_DPAD_UPDOWN - 1].second.remap =3D + find_button_by_name("ABS_HAT0Y_POS"); + + map->button_pairs[BTN_PAIR_DPAD_LEFTRIGHT - 1].first.remap =3D + find_button_by_name("ABS_HAT0X_NEG"); + map->button_pairs[BTN_PAIR_DPAD_LEFTRIGHT - 1].second.remap =3D + find_button_by_name("ABS_HAT0X_POS"); + + map->button_pairs[BTN_PAIR_TRIGGER_LR - 1].first.remap =3D + find_button_by_name("ABS_Z"); + map->button_pairs[BTN_PAIR_TRIGGER_LR - 1].second.remap =3D + find_button_by_name("ABS_RZ"); + + map->button_pairs[BTN_PAIR_VIEW_MENU - 1].first.remap =3D + find_button_by_name("BTN_SELECT"); + map->button_pairs[BTN_PAIR_VIEW_MENU - 1].second.remap =3D + find_button_by_name("BTN_START"); + + map->button_pairs[BTN_PAIR_M1M2 - 1].first.remap =3D + find_button_by_name("FN_M2"); + map->button_pairs[BTN_PAIR_M1M2 - 1].second.remap =3D + find_button_by_name("FN_M1"); +} + +static void ally_set_default_keyboard_mapping(struct ally_button_mapping *= map) +{ + int i; + + /* Set all pair indexes and initialize to NONE */ + for (i =3D 0; i < 9; i++) { + map->button_pairs[i].pair_index =3D i + 1; + map->button_pairs[i].first.remap =3D + find_button_by_name("NONE"); + map->button_pairs[i].first.macro =3D + find_button_by_name("NONE"); + map->button_pairs[i].second.remap =3D + find_button_by_name("NONE"); + map->button_pairs[i].second.macro =3D + find_button_by_name("NONE"); + } + + /* + * Desktop mode defaults: keyboard and mouse mappings. + * Buttons not listed here (dpad, view, menu) remain NONE. + */ + + /* btn_a =3D> KEY_SPACE, btn_b =3D> KEY_E */ + map->button_pairs[BTN_PAIR_AB - 1].first.remap =3D + find_button_by_name("KEY_SPACE"); + map->button_pairs[BTN_PAIR_AB - 1].second.remap =3D + find_button_by_name("KEY_E"); + + /* btn_x =3D> KEY_R, btn_y =3D> KEY_F */ + map->button_pairs[BTN_PAIR_XY - 1].first.remap =3D + find_button_by_name("KEY_R"); + map->button_pairs[BTN_PAIR_XY - 1].second.remap =3D + find_button_by_name("KEY_F"); + + /* LB =3D> wheel up, RB =3D> wheel down */ + map->button_pairs[BTN_PAIR_BUMPER_LR - 1].first.remap =3D + find_button_by_name("REL_WHEEL_HI_RES_UP"); + map->button_pairs[BTN_PAIR_BUMPER_LR - 1].second.remap =3D + find_button_by_name("REL_WHEEL_HI_RES_DOWN"); + + /* left stick click =3D> left click, right stick click =3D> left shift */ + map->button_pairs[BTN_PAIR_STICK_LR - 1].first.remap =3D + find_button_by_name("BTN_LEFT"); + map->button_pairs[BTN_PAIR_STICK_LR - 1].second.remap =3D + find_button_by_name("KEY_LEFTSHIFT"); + + /* LT =3D> left click, RT =3D> right click */ + map->button_pairs[BTN_PAIR_TRIGGER_LR - 1].first.remap =3D + find_button_by_name("BTN_LEFT"); + map->button_pairs[BTN_PAIR_TRIGGER_LR - 1].second.remap =3D + find_button_by_name("BTN_RIGHT"); + + /* M2 =3D> FN_M2, M1 =3D> FN_M1 */ + map->button_pairs[BTN_PAIR_M1M2 - 1].first.remap =3D + find_button_by_name("FN_M2"); + map->button_pairs[BTN_PAIR_M1M2 - 1].second.remap =3D + find_button_by_name("FN_M1"); +} + static ssize_t gamepad_mode_show(struct device *dev, struct device_attribu= te *attr, char *buf) { struct hid_device *hdev =3D to_hid_device(dev); @@ -1097,6 +1436,20 @@ static ssize_t gamepad_mode_store(struct device *dev= , struct device_attribute *a =20 cfg->gamepad_mode =3D mode_byte; =20 + /* + * When the gamepad mode changes, the hardware automatically + * resets all button mappings to its internal defaults for the + * new mode. + * + * Refresh the software cache so that subsequent + * reads from sysfs reflect what the firmware is actually + * using. + */ + if (mode_byte =3D=3D ALLY_GAMEPAD_MODE_GAMEPAD) + ally_set_default_gamepad_mapping(&cfg->button_mappings); + else if (mode_byte =3D=3D ALLY_GAMEPAD_MODE_KEYBOARD) + ally_set_default_keyboard_mapping(&cfg->button_mappings); + hid_dbg(hdev, "Set gamepad mode to %s\n", ally_gamepad_mode_text[mode]); =20 return count; @@ -2923,6 +3276,563 @@ static void ally_btn_turbo_init_attrs(struct ally_b= tn_sysfs_entry *entry) entry->attrs[4] =3D NULL; } =20 +/** + * 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 =3D 0; i < keymap_len; i++) { + if (strcmp(ally_btn_codes[i].name, name) =3D=3D 0) + return &ally_btn_codes[i]; + } + + return NULL; +} + +/** + * 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 =3D=3D ALLY_BTN_M1 || btn =3D=3D ALLY_BTN_M2) + return true; + + /* NONE is always valid */ + if (code->type =3D=3D BTN_TYPE_NONE) + return true; + + /* Macros are not restricted by the controller emulation mode */ + if (is_macro) + return true; + + if (gamepad_mode =3D=3D ALLY_GAMEPAD_MODE_GAMEPAD) + return code->type =3D=3D BTN_TYPE_PAD; + + /* Desktop / keyboard mode: allow keyboard, mouse and media */ + return code->type =3D=3D BTN_TYPE_KB || + code->type =3D=3D BTN_TYPE_MOUSE || + code->type =3D=3D BTN_TYPE_MEDIA; +} + +/** + * ally_set_button_mapping() - Set the button mapping for a button pair + * @hdev: HID device + * @ally: ally handheld structure + * @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 ally_ha= ndheld *ally, + 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 (see hid-asus-ally __btn_pair_to_pkt, BTN_CODE_LEN). + */ + u8 macro_bytes[11] =3D {0}; + u8 btn_bytes[11] =3D {0}; + + if (!mapping || !mapping->first.remap || !mapping->first.macro || + !mapping->second.remap || !mapping->second.macro) + return -EINVAL; + + u8 *buf __free(kfree) =3D ally_alloc_cmd(CMD_SET_MAPPING, NULL, 0); + 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] =3D mapping->pair_index; + buf[4] =3D 0x2C; /* Length */ + + /* First button mapping */ + buf[5] =3D mapping->first.remap->type; + /* Fill in bytes 6-14 with button code */ + if (mapping->first.remap->type) { + memset(btn_bytes, 0, sizeof(btn_bytes)); + btn_bytes[0] =3D mapping->first.remap->type; + + /* Value byte position depends on type: pad=3D1, kb=3D2, media=3D3, + * mouse=3D4 (see hid-asus-ally BTN_CODE definitions). + */ + switch (mapping->first.remap->type) { + case BTN_TYPE_NONE: + break; + case BTN_TYPE_PAD: + btn_bytes[1] =3D mapping->first.remap->value; + break; + case BTN_TYPE_KB: + btn_bytes[2] =3D mapping->first.remap->value; + break; + case BTN_TYPE_MEDIA: + btn_bytes[3] =3D mapping->first.remap->value; + break; + case BTN_TYPE_MOUSE: + btn_bytes[4] =3D mapping->first.remap->value; + break; + } + memcpy(&buf[5], btn_bytes, 11); + } + + /* Macro mapping for first button if any */ + buf[16] =3D mapping->first.macro->type; + if (mapping->first.macro->type) { + memset(macro_bytes, 0, sizeof(macro_bytes)); + macro_bytes[0] =3D mapping->first.macro->type; + + switch (mapping->first.macro->type) { + case BTN_TYPE_NONE: + break; + case BTN_TYPE_PAD: + macro_bytes[1] =3D mapping->first.macro->value; + break; + case BTN_TYPE_KB: + macro_bytes[2] =3D mapping->first.macro->value; + break; + case BTN_TYPE_MEDIA: + macro_bytes[3] =3D mapping->first.macro->value; + break; + case BTN_TYPE_MOUSE: + macro_bytes[4] =3D mapping->first.macro->value; + break; + } + memcpy(&buf[16], macro_bytes, 11); + } + + /* Second button mapping */ + buf[27] =3D mapping->second.remap->type; + /* Fill in bytes 28-36 with button code */ + if (mapping->second.remap->type) { + memset(btn_bytes, 0, sizeof(btn_bytes)); + btn_bytes[0] =3D mapping->second.remap->type; + + switch (mapping->second.remap->type) { + case BTN_TYPE_NONE: + break; + case BTN_TYPE_PAD: + btn_bytes[1] =3D mapping->second.remap->value; + break; + case BTN_TYPE_KB: + btn_bytes[2] =3D mapping->second.remap->value; + break; + case BTN_TYPE_MEDIA: + btn_bytes[3] =3D mapping->second.remap->value; + break; + case BTN_TYPE_MOUSE: + btn_bytes[4] =3D mapping->second.remap->value; + break; + } + memcpy(&buf[27], btn_bytes, 11); + } + + /* Macro mapping for second button if any */ + buf[38] =3D mapping->second.macro->type; + if (mapping->second.macro->type) { + memset(macro_bytes, 0, sizeof(macro_bytes)); + macro_bytes[0] =3D mapping->second.macro->type; + + switch (mapping->second.macro->type) { + case BTN_TYPE_NONE: + break; + case BTN_TYPE_PAD: + macro_bytes[1] =3D mapping->second.macro->value; + break; + case BTN_TYPE_KB: + macro_bytes[2] =3D mapping->second.macro->value; + break; + case BTN_TYPE_MEDIA: + macro_bytes[3] =3D mapping->second.macro->value; + break; + case BTN_TYPE_MOUSE: + macro_bytes[4] =3D mapping->second.macro->value; + break; + } + memcpy(&buf[38], macro_bytes, 11); + } + + return ally_gamepad_send_packet(ally, hdev, buf, ROG_ALLY_REPORT_SIZE); +} + +/** + * ally_apply_button_mappings() - Push the recorded mappings of the current + * gamepad mode to the controller + * @hdev: HID device + * @ally: ally handheld structure + * @cfg: ally config structure + * + * An MCU reset restores the firmware default mappings: push the recorded + * ones again so that the hardware keeps matching what userspace + * configured. The caller must hold cfg->config_mutex, exactly like the + * sysfs store callbacks that update single pairs. + */ +static void ally_apply_button_mappings(struct hid_device *hdev, + struct ally_handheld *ally, + struct ally_config *cfg) +{ + struct ally_button_mapping *mode_map; + int i, ret; + + mode_map =3D &cfg->button_mappings; + + for (i =3D 0; i < ARRAY_SIZE(mode_map->button_pairs); i++) { + mode_map->button_pairs[i].pair_index =3D i + 1; + + ret =3D ally_set_button_mapping(hdev, ally, + &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 =3D BTN_PAIR_DPAD_UPDOWN; + *is_first =3D true; + break; + case ALLY_BTN_DD: + *pair_idx =3D BTN_PAIR_DPAD_UPDOWN; + *is_first =3D false; + break; + case ALLY_BTN_DL: + *pair_idx =3D BTN_PAIR_DPAD_LEFTRIGHT; + *is_first =3D true; + break; + case ALLY_BTN_DR: + *pair_idx =3D BTN_PAIR_DPAD_LEFTRIGHT; + *is_first =3D false; + break; + case ALLY_BTN_J0B: + *pair_idx =3D BTN_PAIR_STICK_LR; + *is_first =3D true; + break; + case ALLY_BTN_J1B: + *pair_idx =3D BTN_PAIR_STICK_LR; + *is_first =3D false; + break; + case ALLY_BTN_LB: + *pair_idx =3D BTN_PAIR_BUMPER_LR; + *is_first =3D true; + break; + case ALLY_BTN_RB: + *pair_idx =3D BTN_PAIR_BUMPER_LR; + *is_first =3D false; + break; + case ALLY_BTN_A: + *pair_idx =3D BTN_PAIR_AB; + *is_first =3D true; + break; + case ALLY_BTN_B: + *pair_idx =3D BTN_PAIR_AB; + *is_first =3D false; + break; + case ALLY_BTN_X: + *pair_idx =3D BTN_PAIR_XY; + *is_first =3D true; + break; + case ALLY_BTN_Y: + *pair_idx =3D BTN_PAIR_XY; + *is_first =3D false; + break; + case ALLY_BTN_VIEW: + *pair_idx =3D BTN_PAIR_VIEW_MENU; + *is_first =3D true; + break; + case ALLY_BTN_MENU: + *pair_idx =3D BTN_PAIR_VIEW_MENU; + *is_first =3D false; + break; + case ALLY_BTN_M1: + *pair_idx =3D BTN_PAIR_M1M2; + *is_first =3D false; + break; + case ALLY_BTN_M2: + *pair_idx =3D BTN_PAIR_M1M2; + *is_first =3D 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 =3D to_button_remap_attr(attr); + struct ally_handheld *ally =3D btn_attr->ally; + const struct btn_code_map *code; + struct ally_config *cfg; + enum ally_button_id button_id =3D 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; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + ret =3D get_button_pair_info(button_id, &pair_idx, &is_first); + if (ret < 0) + return ret; + + guard(mutex)(&cfg->config_mutex); + + pair =3D &cfg->button_mappings.button_pairs[pair_idx - 1]; + btn_map =3D is_first ? &pair->first : &pair->second; + code =3D btn_attr->is_macro ? btn_map->macro : btn_map->remap; + + if (code->type =3D=3D BTN_TYPE_NONE) + return sysfs_emit(buf, "NONE\n"); + + 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 =3D to_hid_device(dev); + struct button_remap_attr *btn_attr =3D to_button_remap_attr(attr); + struct ally_handheld *ally =3D btn_attr->ally; + enum ally_button_id button_id =3D 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; + struct ally_config *cfg; + char btn_name[32]; + bool is_first; + bool undo_hw; + int ret, i; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + if (sscanf(buf, "%31s", btn_name) !=3D 1) + return -EINVAL; + + /* Handle "NONE" specially */ + if (strcmp(btn_name, "NONE") =3D=3D 0) { + code =3D &ally_btn_codes[0]; /* NONE entry */ + } else { + code =3D find_button_by_name(btn_name); + if (!code) + return -EINVAL; + } + + ret =3D get_button_pair_info(button_id, &pair_idx, &is_first); + if (ret < 0) + return ret; + + guard(mutex)(&cfg->config_mutex); + + /* + * 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, cfg->gamepad_mode, code, + btn_attr->is_macro)) + return -EINVAL; + + /* Access the mapping for current gamepad mode */ + pair =3D &cfg->button_mappings.button_pairs[pair_idx - 1]; + btn_map =3D is_first ? &pair->first : &pair->second; + + if (btn_attr->is_macro) { + old_code =3D btn_map->macro; + btn_map->macro =3D code; + } else { + old_code =3D btn_map->remap; + btn_map->remap =3D code; + } + + /* Update pair index */ + pair->pair_index =3D pair_idx; + + /* + * Send mapping to device with the caveat that first + * generation devices require individual button pair updates. + */ + if (cfg->is_ally_x) { + ret =3D ally_set_button_mapping(hdev, ally, pair); + undo_hw =3D false; + } else { + mode_map =3D &cfg->button_mappings; + ret =3D 0; + for (i =3D 0; i < 9; i++) { + mode_map->button_pairs[i].pair_index =3D i + 1; + ret =3D ally_set_button_mapping(hdev, ally, + &mode_map->button_pairs[i]); + if (ret < 0) + break; + } + + /* + * Pairs are pushed one at a time: remember whether + * the target pair reached the hardware before a + * later pair failed, so the rollback below can undo + * it on the device too. + */ + undo_hw =3D ret < 0 && i > pair_idx - 1; + } + + /* + * The hardware rejected the update: restore the previous + * mapping so that the software cache keeps describing the + * state the device is actually in. + */ + if (ret < 0) { + if (btn_attr->is_macro) + btn_map->macro =3D old_code; + else + btn_map->remap =3D old_code; + + /* + * The target pair was already committed before a + * later pair failed: push the restored mapping once + * more so the hardware matches the cache again. Best + * effort only: the device is misbehaving already. + */ + if (undo_hw) + ally_set_button_mapping(hdev, ally, pair); + + hid_err(hdev, "Failed to set button mapping: %d\n", ret); + return ret; + } + + return count; +} + +/** + * button_remap_attr_create() - Create a button remap attribute + * @ally: ally handheld structure + * @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(struct ally_hand= held *ally, + enum ally_button_id button_id, + bool is_macro) +{ + struct button_remap_attr *attr __free(kfree) =3D kzalloc_obj(*attr); + if (!attr) + return NULL; + + attr->ally =3D ally; + attr->button_id =3D button_id; + attr->is_macro =3D is_macro; + sysfs_attr_init(&attr->dev_attr.attr); + attr->dev_attr.attr.name =3D is_macro ? "macro" : "remap"; + attr->dev_attr.attr.mode =3D 0644; + attr->dev_attr.show =3D button_remap_show; + attr->dev_attr.store =3D button_remap_store; + + return no_free_ptr(attr); +} + +/** + * btn_remap_index_show() - Show the list of valid remap targets for a but= ton + * @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_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct asus_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct ally_handheld *ally =3D drvdata->rog_ally; + struct ally_btn_sysfs_entry *entry =3D container_of(attr, + struct ally_btn_sysfs_entry, attr_remap_index); + struct ally_config *cfg; + enum ally_button_id btn =3D entry->btn; + u8 mode; + ssize_t len =3D 0; + int i; + + if (!ally) + return -ENODEV; + + cfg =3D ally_get_config(ally); + if (!cfg) + return -ENODEV; + + scoped_guard(mutex, &cfg->config_mutex) + mode =3D cfg->gamepad_mode; + + for (i =3D 0; i < keymap_len; i++) { + if (!ally_remap_code_valid(btn, mode, &ally_btn_codes[i], false)) + continue; + len +=3D sysfs_emit_at(buf, len, "%s ", ally_btn_codes[i].name); + } + + if (len > 0) + buf[len - 1] =3D '\n'; + + return len; +} + /** * ally_create_button_attributes() - Create turbo button attributes * @hdev: HID device @@ -2932,15 +3842,12 @@ static void ally_btn_turbo_init_attrs(struct ally_b= tn_sysfs_entry *entry) * Return: 0 on success, negative errno on failure */ static int ally_create_button_attributes(struct hid_device *hdev, - struct ally_handheld *ally, - struct ally_config *cfg) + struct ally_handheld *ally, + struct ally_config *cfg) { struct ally_btn_sysfs_entry *entries; int i, ret; =20 - if (!cfg->turbo_support) - return 0; - entries =3D kcalloc(ALLY_BTN_MAX, sizeof(*entries), GFP_KERNEL); if (!entries) return -ENOMEM; @@ -2953,13 +3860,53 @@ static int ally_create_button_attributes(struct hid= _device *hdev, entries[i].hdev =3D hdev; entries[i].btn =3D i; =20 - if (!ally_btn_get_turbo_params(cfg, i)) { - hid_err(hdev, "Invalid button id %d for turbo attributes\n", i); - ret =3D -EINVAL; + if (cfg->turbo_support) { + if (!ally_btn_get_turbo_params(cfg, i)) { + hid_err(hdev, "Invalid button id %d for turbo attributes\n", i); + ret =3D -EINVAL; + goto err_cleanup; + } + + ally_btn_turbo_init_attrs(&entries[i]); + } + + entries[i].remap_attr =3D button_remap_attr_create(ally, i, false); + if (!entries[i].remap_attr) { + ret =3D -ENOMEM; goto err_cleanup; } =20 - ally_btn_turbo_init_attrs(&entries[i]); + entries[i].macro_attr =3D button_remap_attr_create(ally, i, true); + if (!entries[i].macro_attr) { + ret =3D -ENOMEM; + goto err_cleanup; + } + + /* Initialize the remap_index attribute */ + sysfs_attr_init(&entries[i].attr_remap_index.attr); + entries[i].attr_remap_index.attr.name =3D "remap_index"; + entries[i].attr_remap_index.attr.mode =3D 0444; + entries[i].attr_remap_index.show =3D btn_remap_index_show; + entries[i].attr_remap_index.store =3D NULL; + + /* Set up attributes array based on what's supported */ + if (cfg->turbo_support) { + entries[i].attrs[4] =3D + &entries[i].remap_attr->dev_attr.attr; + entries[i].attrs[5] =3D + &entries[i].macro_attr->dev_attr.attr; + entries[i].attrs[6] =3D + &entries[i].attr_remap_index.attr; + entries[i].attrs[7] =3D NULL; + } else { + entries[i].attrs[0] =3D + &entries[i].remap_attr->dev_attr.attr; + entries[i].attrs[1] =3D + &entries[i].macro_attr->dev_attr.attr; + entries[i].attrs[2] =3D + &entries[i].attr_remap_index.attr; + entries[i].attrs[3] =3D NULL; + } =20 entries[i].group.name =3D ally_button_names[i]; entries[i].group.attrs =3D entries[i].attrs; @@ -2978,6 +3925,11 @@ static int ally_create_button_attributes(struct hid_= device *hdev, while (--i >=3D 0) sysfs_remove_group(&hdev->dev.kobj, &entries[i].group); =20 + for (i =3D 0; i < ALLY_BTN_MAX; i++) { + kfree(entries[i].remap_attr); + kfree(entries[i].macro_attr); + } + kfree(entries); =20 cfg->button_entries =3D NULL; @@ -3000,12 +3952,22 @@ static void ally_remove_button_attributes(struct hi= d_device *hdev, struct ally_c =20 entries =3D cfg->button_entries; =20 - for (i =3D 0; i < ALLY_BTN_MAX; i++) + for (i =3D 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); + } =20 kfree(entries); =20 - cfg->button_entries =3D NULL; + /* + * Unpublish the pointers under the lock, so that a sysfs callback + * which is still holding config_mutex observes the teardown + * instead of freed memory. + */ + scoped_guard(mutex, &cfg->config_mutex) { + cfg->button_entries =3D NULL; + } } =20 /** @@ -3046,6 +4008,13 @@ static struct ally_config *ally_config_create(struct= hid_device *hdev, struct al =20 if (!cfg->initialized) { cfg->gamepad_mode =3D ALLY_GAMEPAD_MODE_GAMEPAD; + /* + * Populate the button mappings for the default mode: a mode + * switch re-defaults them, while on a later probe they hold + * the values the user configured, which are what the restore + * path re-applies to the controller. + */ + ally_set_default_gamepad_mapping(&cfg->button_mappings); cfg->xbox_controller_enabled =3D true; cfg->left_deadzone =3D 10; cfg->left_outer_threshold =3D 90; @@ -3703,6 +4672,10 @@ static int hid_asus_ally_init(struct hid_device *hde= v, struct ally_handheld *all ret); } =20 + /* The mappings exist only if the button attributes were created. */ + if (cfg->button_entries) + ally_apply_button_mappings(hdev, ally, cfg); + return 0; } =20 --=20 2.47.3