From mboxrd@z Thu Jan 1 00:00:00 1970 Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.remote-ip=91.218.175.209 ARC-Seal: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; cv=none; t=1793468513; b=dBtaPBpvQhoFUpy+YFrbH8n+T45grHosTH0/9LuvaiU80WDHfZE/dvI/iewxycI9uQHt km+BZWbsZrW5ZQErz3XXxn4YdOJ37gwwqeES7EJr2wkI1hQL6/40Gz6h+MoDLM3I8TYQU H/x2+3acPdcTdOKMxOsv5mShi3Ldh2c33097ZWciT53f7b4aw8WLQn/2RZvM+Q9Ny0DUv cZqtNbZQoPZJcFgE0ca8Vt39cbc7qXPVdFXUptfHZW/n5psyP/vF31020ovyqj4rKoDT1 ZYmjc/zYH1qg/hGJvnLeHrjnrokaYGjrbVX1TlCDfyJ9vSanoJDm4Uem92AEAF7dRcw== ARC-Message-Signature: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; c=relaxed/relaxed; t=1793468513; h=DMARC-Filter:X-Envelope-To:DKIM-Signature:X-Envelope-To:Received: X-Mizu-Trace-ID:X-Migadu-Flow:From:To:Cc:Subject:Date:Message-ID: X-Mailer:In-Reply-To:References:MIME-Version: Content-Transfer-Encoding; bh=JYYgw3wqGQaMtQShsNMzAmh+qbePsBb1mTCo+1jIrX4=; b=T4Rdp8Ib0YY+V56UqABRUIbF9i0U103h/KB5MYdtVlNNU1MV4xDa2MRD1/3e3zcmNwrA DzUQcxu4xfXJyasFNa5fiV6bDwWZ5GRWKbFODMgYjLp1OcDaMuBuq5bmU3fPX6pZ7ax7X ZJNfyRCx8plxTUEzbsNGtTSeygCi0TMG92/97rWWQe9ktukUP47JgnCS6BHalADhHzlkC K2LjMnWJPeFHLaQoZbZq8tyQ7o5AIkoKBA8QJ9erZdAKsCeaHjIaR1ILOGyLPWmC68kPG kbrZmb1HQaKjvklIpA/f+aBIhTpGgc//sMxu7bHD1G7aYEv5Gyd5SQrrkZgdMzbKWEA== ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass header.d=linux.dev header.i=@linux.dev header.a=rsa-sha256 header.s=key1 header.b=ox046GnP; arc=none smtp.remote-ip=91.218.175.209 Received: from mta0.migadu.com (out-209.mta0.migadu.com [91.218.175.209]) (using TLSv1.3 with cipher TLS_AES_128_GCM_SHA256 (128/128 bits) key-exchange X25519 server-signature ECDSA (prime256v1) server-digest SHA256) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id A3079606A31E for ; Sat, 31 Oct 2026 17:41:51 +0000 (UTC) DMARC-Filter: OpenDMARC Filter v1.4.2 smtp.subspace.kernel.org A3079606A31E 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, unprotected) header.d=linux.dev header.i=@linux.dev header.a=rsa-sha256 header.s=key1 header.b=ox046GnP X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=f340lj9wic7qiepIaoniVJ+1+XVQsWV3L+vMc61nHDo=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1793468509; v=1; x=1794073309; b=ox046GnPzqv/zsoC+LUMqA4bgz6Weh+LXyvPFebQymsvnTqZK6gvcI9rVluJzjrmuW7Gx0uJ YVEoeWvxAQMncvhmOlECxcjaM5544lC48pXf3DJQ+ACg1Crp8FwtBvG64KLtnLyMeCsiJDZaYeV fkFdmkqOtHL/CKme7OT6mtRs= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id bc279f98aaa957e4; Sat, 31 Oct 2026 17:41:49 +0000 X-Mizu-Trace-ID: bc279f98aaa957e4 X-Migadu-Flow: FLOW_OUT From: Denis Benato To: linux-kernel@vger.kernel.org Cc: platform-driver-x86@vger.kernel.org, linux-input@vger.kernel.org, kernel@lists.opengamingcollective.org, "Benjamin Tissoires" , "Jiri Kosina" , "Luke D . Jones" , "Mateusz Schwartz" , "Denis Benato" , "Jonathan LoBue" , "Khamunetri Clark" , "Derek J. Clark" , "Joshua Leivenzon" , "Nathan Chancellor" , =?UTF-8?q?Rebecca=20Mara=20M=C3=BCller?= , "Ivan Levchenko" , "Corentin Chary" , "Hans de Goede" , =?UTF-8?q?Ilpo=20J=C3=A4rvinen?= , Denis Benato Subject: [PATCH v12 02/17] HID: asus: add support for ROG Ally handhelds Date: Sat, 31 Oct 2026 17:41:15 +0000 Message-ID: <20261031174130.208240-3-denis.benato@linux.dev> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20261031174130.208240-1-denis.benato@linux.dev> References: <20261031174130.208240-1-denis.benato@linux.dev> X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Implement the core functionality for ASUS handhelds: - extend functionality of hid-asus if driver is enabled - initialise the device to a "ready" state - implement Ally X input mapping - emit a single proper event for AC button long press Each interface keeps its own state and reports on its own evdev. Signed-off-by: Khamunetri Clark Signed-off-by: Denis Benato Signed-off-by: Matthew Schwartz Co-developed-by: Luke Jones Signed-off-by: Luke Jones Signed-off-by: Jonathan LoBue --- drivers/hid/hid-asus.c | 969 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 870 insertions(+), 99 deletions(-) diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c index 327bb4ab1a0d..552d3b25a1d1 100644 --- a/drivers/hid/hid-asus.c +++ b/drivers/hid/hid-asus.c @@ -22,16 +22,24 @@ #include #include +#include #include #include +#include #include +#include +#include #include +#include #include +#include #include #include /* For to_usb_interface for T100 touchpad intf check */ #include +#include #include #include +#include #include "hid-ids.h" @@ -39,6 +47,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"); #define T100_TPAD_INTF 2 @@ -56,6 +67,30 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad"); #define ROG_ALLY_X_MIN_MCU 313 #define ROG_ALLY_MIN_MCU 319 +#define HID_ALLY_INTF_KEYBOARD_IN 0x81 +#define HID_ALLY_INTF_CFG_IN 0x83 +#define HID_ALLY_X_INTF_IN 0x87 + +#define HID_ALLY_GET_REPORT_ID 0x0D +#define HID_ALLY_FEATURE_CODE_PAGE 0xD1 + +#define HID_ALLY_X_INPUT_REPORT 0x0B + +#define HID_ALLY_READY_MAX_TRIES 6 + +#define HID_INPUT_KEY_DELETE 0x4c +#define HID_INPUT_KEY_F20 0x6f +#define HID_INPUT_LCTRL_MODIFIER 0x01 +#define HID_INPUT_LALT_MODIFIER 0x04 +#define HID_INPUT_LCTRL_LALT (HID_INPUT_LCTRL_MODIFIER | HID_INPUT_LALT_MODIFIER) + +#define HID_INPUT_REPORT_ID_OFFSET 0 +#define HID_INPUT_MODIFIER_OFFSET 1 +#define HID_INPUT_RESERVED_OFFSET 2 +#define HID_INPUT_KEY_OFFSET 3 + +#define ASUS_HID_INPUT_KBD_REPORT_ID 0x01 + /* Spurious HID codes sent by QUIRK_ROG_NKEY_KEYBOARD devices */ #define ASUS_SPURIOUS_CODE_0XEA 0xea #define ASUS_SPURIOUS_CODE_0XEC 0xec @@ -160,6 +195,17 @@ struct asus_touchpad_info { int report_size; }; +struct ally_intf { + struct hid_device *hdev; + /* HID-core evdev of this interface. Reports for it land here. */ + struct input_dev *input; + /* Private gamepad node for report 0x0B. NULL on the other interfaces. */ + struct input_dev *gamepad; + u8 cad_state; + unsigned long cad_last_event_time; + struct delayed_work resume_work; +}; + struct asus_drvdata { unsigned long quirks; struct hid_device *hdev; @@ -167,6 +213,7 @@ struct asus_drvdata { struct input_dev *tp_kbd_input; struct asus_worker *worker; unsigned int kbd_backlight_brightness; + struct ally_intf *ally; const struct asus_touchpad_info *tp; struct power_supply *battery; struct power_supply_desc battery_desc; @@ -239,12 +286,761 @@ static const struct asus_touchpad_info medion_e1239t_tp = { .report_size = 32 /* 2 byte header + 5 * 5 + 5 byte footer */, }; +enum ally_command_codes { + CMD_SET_GAMEPAD_MODE = 0x01, + CMD_SET_MAPPING = 0x02, + CMD_SET_JOYSTICK_MAPPING = 0x03, + CMD_SET_JOYSTICK_DEADZONE = 0x04, + CMD_SET_TRIGGER_RANGE = 0x05, + CMD_SET_VIBRATION_INTENSITY = 0x06, + CMD_LED_CONTROL = 0x08, + CMD_CHECK_READY = 0x0A, + CMD_SET_XBOX_CONTROLLER = 0x0B, + CMD_CHECK_XBOX_SUPPORT = 0x0C, + CMD_USER_CAL_DATA = 0x0D, + CMD_CHECK_USER_CAL_SUPPORT = 0x0E, + CMD_SET_TURBO_PARAMS = 0x0F, + CMD_CHECK_TURBO_SUPPORT = 0x10, + CMD_CHECK_RESP_CURVE_SUPPORT = 0x12, + CMD_SET_RESP_CURVE = 0x13, + CMD_CHECK_DIR_TO_BTN_SUPPORT = 0x14, + CMD_SET_GYRO_PARAMS = 0x15, + CMD_CHECK_GYRO_TO_JOYSTICK = 0x16, + CMD_CHECK_ANTI_DEADZONE = 0x17, + CMD_SET_ANTI_DEADZONE = 0x18, +}; + +static const u8 ally_force_feedback_off[] = { + 0x0D, 0x0F, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xEB +}; + +/* + * Each Ally interface owns its own ally_intf. A report is delivered on the + * evdev of the interface that received it. Nothing here points at another + * interface. + */ +static void ally_release_vendor_keys(struct input_dev *input) +{ + if (!input) + return; + + input_report_key(input, KEY_F16, 0); + input_report_key(input, KEY_F17, 0); + input_report_key(input, KEY_F18, 0); + input_report_key(input, KEY_F19, 0); + input_report_key(input, KEY_PROG1, 0); + input_sync(input); +} + +static void ally_resume_work_fn(struct work_struct *work) +{ + struct ally_intf *intf = container_of(work, struct ally_intf, + resume_work.work); + + /* + * Force release vendor buttons to prevent stuck keys on resume + * (workaround for Ally X USB re-probing during suspend/resume). + */ + ally_release_vendor_keys(intf->input); +} + +static void ally_quiesce(struct ally_intf *intf) +{ + if (!intf) + return; + + cancel_delayed_work_sync(&intf->resume_work); +} + +static void ally_free(struct ally_intf *intf) +{ + if (!intf) + return; + + if (intf->gamepad) + input_unregister_device(intf->gamepad); + kfree(intf); +} + +/** + * handle_ctrl_alt_del() - detect a left button long press + * @intf: ally interface holding the sequence state + * @data: raw report buffer, rewritten in place when the sequence matches + * @size: length of @data in bytes + * + * The Ally left button emits a sequence of ctrl+alt+del events. Capture that + * and emit only a single code for that single event by rewriting @data in + * place. The return value tells the caller whether ally processing of this + * report should stop. + * + * Return: true when this step was recognized and @data was rewritten, so the + * caller stops and the generic parser consumes the rewritten report. false + * when @data was left unchanged and ally processing should continue. + */ +static bool handle_ctrl_alt_del(struct ally_intf *intf, u8 *data, int size) +{ + bool time_is_past = time_after(jiffies, + intf->cad_last_event_time + msecs_to_jiffies(100)); + + if (size < 16 || data[HID_INPUT_REPORT_ID_OFFSET] != ASUS_HID_INPUT_KBD_REPORT_ID) + return false; + + if (intf->cad_state > 0 && time_is_past) + intf->cad_state = 0; + + intf->cad_last_event_time = jiffies; + switch (intf->cad_state) { + case 0: + if ((data[HID_INPUT_MODIFIER_OFFSET] == HID_INPUT_LCTRL_MODIFIER) && + (data[HID_INPUT_RESERVED_OFFSET] == 0x00) && + (data[HID_INPUT_KEY_OFFSET] == 0x00)) { + intf->cad_state = 1; + data[HID_INPUT_MODIFIER_OFFSET] = 0x00; + return true; + } + break; + case 1: + if ((data[HID_INPUT_MODIFIER_OFFSET] == HID_INPUT_LCTRL_LALT) && + (data[HID_INPUT_RESERVED_OFFSET] == 0x00) && + (data[HID_INPUT_KEY_OFFSET] == 0x00)) { + intf->cad_state = 2; + data[HID_INPUT_MODIFIER_OFFSET] = 0x00; + return true; + } + break; + case 2: + if ((data[HID_INPUT_MODIFIER_OFFSET] == HID_INPUT_LCTRL_LALT) && + (data[HID_INPUT_RESERVED_OFFSET] == 0x00) && + (data[HID_INPUT_KEY_OFFSET] == HID_INPUT_KEY_DELETE)) { + intf->cad_state = 3; + data[HID_INPUT_MODIFIER_OFFSET] = 0x00; + data[HID_INPUT_KEY_OFFSET] = HID_INPUT_KEY_F20; + return true; + } + break; + case 3: + if ((data[HID_INPUT_MODIFIER_OFFSET] == HID_INPUT_LALT_MODIFIER) && + (data[HID_INPUT_RESERVED_OFFSET] == 0x00) && + (data[HID_INPUT_KEY_OFFSET] == HID_INPUT_KEY_DELETE)) { + intf->cad_state = 4; + data[HID_INPUT_MODIFIER_OFFSET] = 0x00; + data[HID_INPUT_KEY_OFFSET] = 0x00; + return true; + } + break; + case 4: + if ((data[HID_INPUT_MODIFIER_OFFSET] == 0x00) && + (data[HID_INPUT_RESERVED_OFFSET] == 0x00) && + (data[HID_INPUT_KEY_OFFSET] == HID_INPUT_KEY_DELETE)) { + intf->cad_state = 5; + data[HID_INPUT_KEY_OFFSET] = 0x00; + return true; + } + break; + } + intf->cad_state = 0; + return false; +} + +/** + * ally_vendor_keys() - Report a vendor button on this interface's evdev + * @input: HID-core input device of the interface that received the report + * @data: raw report buffer + * @size: length of @data in bytes + * + * The key is down while @data[1] matches that code. The same decode is used + * for every Ally interface: 0x38 is the ROG key (KEY_PROG1 and KEY_F19). + * + * Return: true if the report was a known vendor button. + */ +static bool ally_vendor_keys(struct input_dev *input, const u8 *data, int size) +{ + if (size < 2 || data[0] != FEATURE_KBD_REPORT_ID) + return false; + + if (data[1] != 0x38 && data[1] != 0xA6 && data[1] != 0xA7 && + data[1] != 0xA8) + return false; + + if (!input) + return true; + + input_report_key(input, KEY_PROG1, data[1] == 0x38); + input_report_key(input, KEY_F16, data[1] == 0xA6); + input_report_key(input, KEY_F17, data[1] == 0xA7); + input_report_key(input, KEY_F18, data[1] == 0xA8); + input_report_key(input, KEY_F19, data[1] == 0x38); + input_sync(input); + return true; +} + +/** + * asus_find_report() - Find a report of the given type by its ID + * @hdev: HID device + * @type: report type (HID_INPUT_REPORT, HID_OUTPUT_REPORT or + * HID_FEATURE_REPORT) + * @report_id: report ID to look for + * + * Return: the report with the given ID, or NULL if the device does not + * declare such a report. + */ +static struct hid_report *asus_find_report(struct hid_device *hdev, + unsigned int type, u8 report_id) +{ + return hdev->report_enum[type].report_id_hash[report_id]; +} + +/** + * asus_report_len() - Length of a feature report for the device + * @hdev: HID device + * @report_id: report ID of the feature report + * + * The same ASUS report ID can have different sizes depending on the device: + * report 0x5E is a 64-byte report on laptops but a 300-byte report on XG + * Mobile stations. Always size transfers from the report descriptor of the + * device at hand instead of hardcoding lengths. + * + * Return: the length of the feature report in bytes, or 0 if the device + * does not declare the report. + */ +static size_t asus_report_len(struct hid_device *hdev, u8 report_id) +{ + struct hid_report *report; + + report = asus_find_report(hdev, HID_FEATURE_REPORT, report_id); + + return report ? hid_report_len(report) : 0; +} + +static int asus_kbd_set_report(struct hid_device *hdev, const u8 *buf, size_t buf_size) +{ + size_t hw_len = asus_report_len(hdev, buf[0]); + + /* The payload (including the report ID) must fit the device report. */ + if (!hw_len || buf_size > hw_len) + return -EINVAL; + + u8 *dmabuf __free(kfree) = kzalloc(hw_len, GFP_KERNEL); + if (!dmabuf) + return -ENOMEM; + + memcpy(dmabuf, buf, buf_size); + + /* + * The report ID should be set from the incoming buffer due to LED and + * key interfaces having different pages. Short payloads are zero-padded + * to the exact report size the hardware expects for this report ID. + */ + return hid_hw_raw_request(hdev, buf[0], dmabuf, hw_len, HID_FEATURE_REPORT, + HID_REQ_SET_REPORT); +} + +/** + * asus_kbd_get_report() - Read a report sized from the descriptor + * @hdev: HID device + * @report_id: report ID to read + * @buf: buffer to store the report in + * @buf_size: size of @buf + * + * The transfer is performed at the exact report size the device declares + * for @report_id. The device may answer with a short read, and only the + * bytes that fit in @buf are copied into it. + * + * Return: 0 on success, negative errno on failure. + */ +static int asus_kbd_get_report(struct hid_device *hdev, u8 report_id, + u8 *buf, size_t buf_size) +{ + size_t hw_len = asus_report_len(hdev, report_id); + int ret; + + if (!hw_len) + return -EINVAL; + + u8 *dmabuf __free(kfree) = kzalloc(hw_len, GFP_KERNEL); + if (!dmabuf) + return -ENOMEM; + + ret = hid_hw_raw_request(hdev, report_id, dmabuf, hw_len, + HID_FEATURE_REPORT, HID_REQ_GET_REPORT); + if (ret < 0) + return ret; + + /* The device may return less than requested (short read). */ + memcpy(buf, dmabuf, min(buf_size, hw_len)); + + return 0; +} + static const u8 asus_report_id_init[] = { FEATURE_KBD_REPORT_ID, FEATURE_KBD_LED_REPORT_ID1, FEATURE_KBD_LED_REPORT_ID2 }; +static int ally_dev_set_report(struct hid_device *hdev, const u8 *buf, size_t len) +{ + return asus_kbd_set_report(hdev, buf, len); +} + +static int ally_dev_get_report(struct hid_device *hdev, u8 *out, size_t len) +{ + return hid_hw_raw_request(hdev, HID_ALLY_GET_REPORT_ID, out, len, + HID_FEATURE_REPORT, HID_REQ_GET_REPORT); +} + +/** + * ally_gamepad_send_packet() - Send a raw packet to the gamepad device + * @hdev: HID device + * @buf: buffer containing the packet data + * @len: length of data to send + * + * Return: count of data transferred, negative if error. + */ +static int ally_gamepad_send_packet(struct hid_device *hdev, const u8 *buf, size_t len) +{ + return ally_dev_set_report(hdev, buf, len); +} + +/** + * ally_gamepad_send_receive_packet() - Send a packet and receive the response + * @hdev: HID device + * @buf: buffer containing the packet data to send and receive response in + * @len: length of buffer + * + * Return: 0 on success, negative errno on failure. + */ +static int ally_gamepad_send_receive_packet(struct hid_device *hdev, + u8 *buf, size_t len) +{ + int ret; + + ret = ally_dev_set_report(hdev, buf, len); + if (ret >= 0) { + memset(buf, 0, len); + ret = ally_dev_get_report(hdev, buf, len); + } + + return ret; +} + +/** + * ally_alloc_cmd() - Construct a command buffer for the gamepad + * @hdev: HID device the command is built for + * @cmd: Command code to send + * @payload: Optional payload data to include in the command + * @payload_size: Size of the payload data + * @size: Where to store the length of the allocated buffer + * + * The buffer is sized after the feature report the device declares for + * the gamepad commands, and it is the caller responsibility to free the + * buffer using kfree(). + * + * Return: the newly allocated buffer containing the command, or NULL on + * allocation failure or if the payload would not fit in the report. + */ +static u8 *ally_alloc_cmd(struct hid_device *hdev, u8 cmd, const u8 *payload, + u8 payload_size, size_t *size) +{ + size_t hw_len = asus_report_len(hdev, FEATURE_KBD_REPORT_ID); + u8 *hidbuf; + + /* + * The payload is written after the 4-byte command header: + * reject one that would spill past the end of the report. + */ + if (hw_len < 4 || payload_size > hw_len - 4) + return NULL; + + hidbuf = kzalloc(hw_len, GFP_KERNEL); + if (!hidbuf) + return NULL; + + hidbuf[0] = FEATURE_KBD_REPORT_ID; + hidbuf[1] = HID_ALLY_FEATURE_CODE_PAGE; + hidbuf[2] = cmd; + hidbuf[3] = payload_size; + + if (payload_size > 0 && payload) + memcpy(&hidbuf[4], payload, payload_size); + + *size = hw_len; + + return hidbuf; +} + +/** + * ally_gamepad_check_ready() - Wait for the gamepad MCU to report ready + * @hdev: HID device + * + * This should be called before any remapping attempts, and on driver + * init/resume, after the asus handshake has been performed on the + * configuration endpoint. + * + * Return: 0 on success, negative errno on failure. + */ +static int ally_gamepad_check_ready(struct hid_device *hdev) +{ + const u8 payload[] = { 0x00 }; + size_t buf_len; + int ret; + + for (int i = 0; i < HID_ALLY_READY_MAX_TRIES; i++) { + u8 *buf __free(kfree) = ally_alloc_cmd(hdev, CMD_CHECK_READY, + payload, sizeof(payload), + &buf_len); + if (!buf) + return -ENOMEM; + + ret = ally_gamepad_send_receive_packet(hdev, buf, buf_len); + if (ret < 0) + hid_dbg(hdev, "ROG Ally check %d/%d failed: %d\n", i, + HID_ALLY_READY_MAX_TRIES, ret); + else if (buf[2] == CMD_CHECK_READY) + return 0; + + /* Back off on a bus error as well as on a not-ready answer. */ + usleep_range(1000, 2000); + } + + hid_err(hdev, "ROG Ally never responded with a ready\n"); + return -ENODEV; +} + +static int ally_get_endpoint_address(struct hid_device *hdev) +{ + struct usb_host_endpoint *ep; + struct usb_interface *intf; + + if (!hid_is_usb(hdev)) + return -ENODEV; + + intf = to_usb_interface(hdev->dev.parent); + if (!intf || !intf->cur_altsetting) + return -ENODEV; + + if (intf->cur_altsetting->desc.bNumEndpoints == 0) + return -ENODEV; + + ep = intf->cur_altsetting->endpoint; + if (!ep) + return -ENODEV; + + return ep->desc.bEndpointAddress; +} + +struct ally_x_input_report { + __le16 x, y; + __le16 rx, ry; + __le16 z, rz; + u8 buttons[3]; +} __packed; + +/* The hatswitch outputs integers, we use them to index this X|Y pair */ +static const int hat_values[][2] = { + { 0, 0 }, { 0, -1 }, { 1, -1 }, { 1, 0 }, { 1, 1 }, + { 0, 1 }, { -1, 1 }, { -1, 0 }, { -1, -1 }, +}; + +/** + * ally_x_raw_event() - Parse and report an Ally X gamepad input report + * @input: input device to report the events on + * @data: raw report buffer + * @size: length of @data in bytes + * + * Return: true if the event was handled, otherwise false. + */ +static bool ally_x_raw_event(struct input_dev *input, u8 *data, int size) +{ + struct ally_x_input_report *in_report; + u16 x, y, rx, ry, z, rz; + u8 byte; + + if (!input) + return false; + + if (size < 1) + return false; + + if (data[0] != HID_ALLY_X_INPUT_REPORT) + return false; + + /* + * hid-core only guarantees size >= 1 and does not zero-pad short + * reports before ->raw_event, so a truncated transfer would leave the + * payload below pointing at stale DMA buffer contents. + */ + if (size < 1 + sizeof(*in_report)) + return false; + + in_report = (struct ally_x_input_report *)&data[1]; + + /* USB HID payloads are little-endian: convert them explicitly. */ + x = get_unaligned_le16(&in_report->x); + y = get_unaligned_le16(&in_report->y); + rx = get_unaligned_le16(&in_report->rx); + ry = get_unaligned_le16(&in_report->ry); + z = get_unaligned_le16(&in_report->z); + rz = get_unaligned_le16(&in_report->rz); + + input_report_abs(input, ABS_X, x - 32768); + input_report_abs(input, ABS_Y, y - 32768); + input_report_abs(input, ABS_RX, rx - 32768); + input_report_abs(input, ABS_RY, ry - 32768); + input_report_abs(input, ABS_Z, z); + input_report_abs(input, ABS_RZ, rz); + + byte = in_report->buttons[0]; + input_report_key(input, BTN_A, byte & BIT(0)); + input_report_key(input, BTN_B, byte & BIT(1)); + input_report_key(input, BTN_X, byte & BIT(2)); + input_report_key(input, BTN_Y, byte & BIT(3)); + input_report_key(input, BTN_TL, byte & BIT(4)); + input_report_key(input, BTN_TR, byte & BIT(5)); + input_report_key(input, BTN_SELECT, byte & BIT(6)); + input_report_key(input, BTN_START, byte & BIT(7)); + + byte = in_report->buttons[1]; + input_report_key(input, BTN_THUMBL, byte & BIT(0)); + input_report_key(input, BTN_THUMBR, byte & BIT(1)); + input_report_key(input, BTN_MODE, byte & BIT(2)); + + /* + * The hatswitch byte is device-controlled; treat anything the table + * does not cover as centred rather than indexing out of bounds. + */ + byte = in_report->buttons[2]; + if (byte >= ARRAY_SIZE(hat_values)) + byte = 0; + input_report_abs(input, ABS_HAT0X, hat_values[byte][0]); + input_report_abs(input, ABS_HAT0Y, hat_values[byte][1]); + + input_sync(input); + + return true; +} + +static int ally_x_input_open(struct input_dev *dev) +{ + struct ally_intf *intf = input_get_drvdata(dev); + + return hid_hw_open(intf->hdev); +} + +static void ally_x_input_close(struct input_dev *dev) +{ + struct ally_intf *intf = input_get_drvdata(dev); + + hid_hw_close(intf->hdev); +} + +static struct input_dev *ally_x_alloc_input_dev(struct hid_device *hdev, + struct ally_intf *intf) +{ + struct input_dev *input_dev = input_allocate_device(); + + if (!input_dev) + return ERR_PTR(-ENOMEM); + + input_dev->dev.parent = &hdev->dev; + input_dev->id.bustype = hdev->bus; + input_dev->id.vendor = hdev->vendor; + input_dev->id.product = hdev->product; + input_dev->id.version = hdev->version; + input_dev->uniq = hdev->uniq; + input_dev->name = "ASUS ROG Ally X Gamepad"; + + input_set_drvdata(input_dev, intf); + /* + * Let the input core hold the hardware open while the device node + * is in use: without these the interface could be runtime-suspended + * and drop events. + */ + input_dev->open = ally_x_input_open; + input_dev->close = ally_x_input_close; + + return input_dev; +} + +static int ally_x_setup_input(struct hid_device *hdev, struct ally_intf *intf) +{ + struct input_dev *input = ally_x_alloc_input_dev(hdev, intf); + int ret; + + if (IS_ERR(input)) + return PTR_ERR(input); + + input_set_abs_params(input, ABS_X, -32768, 32767, 0, 0); + input_set_abs_params(input, ABS_Y, -32768, 32767, 0, 0); + input_set_abs_params(input, ABS_RX, -32768, 32767, 0, 0); + input_set_abs_params(input, ABS_RY, -32768, 32767, 0, 0); + input_set_abs_params(input, ABS_Z, 0, 1023, 0, 0); + input_set_abs_params(input, ABS_RZ, 0, 1023, 0, 0); + input_set_abs_params(input, ABS_HAT0X, -1, 1, 0, 0); + input_set_abs_params(input, ABS_HAT0Y, -1, 1, 0, 0); + input_set_capability(input, EV_KEY, BTN_A); + input_set_capability(input, EV_KEY, BTN_B); + input_set_capability(input, EV_KEY, BTN_X); + input_set_capability(input, EV_KEY, BTN_Y); + input_set_capability(input, EV_KEY, BTN_TL); + input_set_capability(input, EV_KEY, BTN_TR); + input_set_capability(input, EV_KEY, BTN_SELECT); + input_set_capability(input, EV_KEY, BTN_START); + input_set_capability(input, EV_KEY, BTN_MODE); + input_set_capability(input, EV_KEY, BTN_THUMBL); + input_set_capability(input, EV_KEY, BTN_THUMBR); + + ret = input_register_device(input); + if (ret) { + hid_err(hdev, "Failed to register Ally X gamepad device: %d\n", ret); + input_free_device(input); + return ret; + } + + intf->gamepad = input; + return 0; +} + +static int hid_asus_ally_init(struct hid_device *hdev) +{ + int ret; + + /* Failure at this point is non-critical. The packet goes to this interface. */ + ret = ally_gamepad_send_packet(hdev, ally_force_feedback_off, + sizeof(ally_force_feedback_off)); + if (ret < 0) + hid_err(hdev, "Ally failed to init force-feedback off: %d\n", ret); + + return 0; +} + +/** + * hid_asus_ally_raw_event() - Route raw reports from one Ally interface + * @hdev: HID device the report arrived on + * @intf: that interface's ally state + * @data: raw report buffer + * @size: length of @data in bytes + * + * Called from the raw_event callback, which may run in atomic context. + * Only this interface's own input devices are used. + * + * Return: true if the event was handled, otherwise false. + */ +static bool hid_asus_ally_raw_event(struct hid_device *hdev, struct ally_intf *intf, + u8 *data, int size) +{ + if (!intf) + return false; + + if (ally_vendor_keys(intf->input, data, size)) + return true; + + switch (ally_get_endpoint_address(hdev)) { + case HID_ALLY_X_INTF_IN: + return ally_x_raw_event(intf->gamepad, data, size); + case HID_ALLY_INTF_KEYBOARD_IN: + if (handle_ctrl_alt_del(intf, data, size)) + return false; + break; + default: + break; + } + + return false; +} + +static struct input_dev *ally_interface_input(struct hid_device *hdev) +{ + struct hid_input *hidinput; + + if (list_empty(&hdev->inputs)) + return NULL; + + hidinput = list_first_entry(&hdev->inputs, struct hid_input, list); + return hidinput->input; +} + +/** + * hid_asus_ally_probe() - Initialize the ROG Ally HID extension + * @hdev: HID device + * + * The Ally code is a plugin of the Asus HID driver: one ally_intf per + * bound interface, freed by the caller after hid_hw_stop(). Interfaces + * do not share device pointers, so a second interface cannot evict the + * first. + * + * Return: an ally_intf pointer on success, NULL when this endpoint has no + * Ally role, or an ERR_PTR on failure. On failure the function frees + * anything it allocated. + */ +static struct ally_intf *hid_asus_ally_probe(struct hid_device *hdev) +{ + int ret, ep = ally_get_endpoint_address(hdev); + + if (ep < 0) + return ERR_PTR(ep); + + if (ep != HID_ALLY_INTF_CFG_IN && ep != HID_ALLY_X_INTF_IN && + ep != HID_ALLY_INTF_KEYBOARD_IN) + return NULL; + + struct ally_intf *intf __free(kfree) = kzalloc_obj(*intf); + + if (!intf) + return ERR_PTR(-ENOMEM); + + intf->hdev = hdev; + intf->input = ally_interface_input(hdev); + INIT_DELAYED_WORK(&intf->resume_work, ally_resume_work_fn); + + switch (ep) { + case HID_ALLY_INTF_CFG_IN: + /* + * This function assumes the asus-specific initialization + * to have been performed already at this point. + */ + ret = ally_gamepad_check_ready(hdev); + if (ret < 0) { + hid_err(hdev, "ROG Ally device is not ready: %d\n", ret); + return ERR_PTR(ret); + } + + hid_asus_ally_init(hdev); + break; + case HID_ALLY_X_INTF_IN: + ret = ally_x_setup_input(hdev, intf); + if (ret) { + hid_err(hdev, "Failed to create Ally X gamepad device.\n"); + return ERR_PTR(ret); + } + break; + case HID_ALLY_INTF_KEYBOARD_IN: + break; + default: + return NULL; + } + + return no_free_ptr(intf); +} + +static int hid_asus_ally_reset_resume(struct hid_device *hdev, struct ally_intf *intf) +{ + int ep; + + /* + * The extensions failed to probe and the device is operating as a + * generic HID device: do not fail the resume because of that. + */ + if (!intf) + return 0; + + ep = ally_get_endpoint_address(hdev); + if (ep != HID_ALLY_INTF_CFG_IN) + return 0; + + return hid_asus_ally_init(hdev); +} + /* * Send events to asus-wmi driver for handling special keys */ @@ -512,6 +1308,21 @@ static int asus_raw_event(struct hid_device *hdev, if (drvdata->quirks & QUIRK_MEDION_E1239T) return asus_e1239t_event(drvdata, data, size); + if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) { + struct ally_intf *intf; + + /* + * Return -1 to suppress further processing by the generic HID + * input parser for reports we fully handle. The interface is + * published with smp_store_release() only after its fields + * are set. Letting report 0x0B reach the default parser + * creates a generic gamepad and Steam Input overlaps. + */ + intf = smp_load_acquire(&drvdata->ally); /* fields are published */ + if (hid_asus_ally_raw_event(hdev, intf, data, size)) + return -1; + } + /* * Skip these report ID, the device emits a continuous stream associated * with the AURA mode it is in which looks like an 'echo'. @@ -581,104 +1392,6 @@ static int asus_raw_event(struct hid_device *hdev, return 0; } -/** - * asus_find_report() - Find a report of the given type by its ID - * @hdev: HID device - * @type: report type (HID_INPUT_REPORT, HID_OUTPUT_REPORT or - * HID_FEATURE_REPORT) - * @report_id: report ID to look for - * - * Return: the report with the given ID, or NULL if the device does not - * declare such a report. - */ -static struct hid_report *asus_find_report(struct hid_device *hdev, - unsigned int type, u8 report_id) -{ - return hdev->report_enum[type].report_id_hash[report_id]; -} - -/** - * asus_report_len() - Length of a feature report for the device - * @hdev: HID device - * @report_id: report ID of the feature report - * - * The same ASUS report ID can have different sizes depending on the device: - * report 0x5E is a 64-byte report on laptops but a 300-byte report on XG - * Mobile stations. Always size transfers from the report descriptor of the - * device at hand instead of hardcoding lengths. - * - * Return: the length of the feature report in bytes, or 0 if the device - * does not declare the report. - */ -static size_t asus_report_len(struct hid_device *hdev, u8 report_id) -{ - struct hid_report *report; - - report = asus_find_report(hdev, HID_FEATURE_REPORT, report_id); - - return report ? hid_report_len(report) : 0; -} - -static int asus_kbd_set_report(struct hid_device *hdev, const u8 *buf, size_t buf_size) -{ - size_t hw_len = asus_report_len(hdev, buf[0]); - - /* The payload (including the report ID) must fit the device report. */ - if (!hw_len || buf_size > hw_len) - return -EINVAL; - - u8 *dmabuf __free(kfree) = kzalloc(hw_len, GFP_KERNEL); - if (!dmabuf) - return -ENOMEM; - - memcpy(dmabuf, buf, buf_size); - - /* - * The report ID should be set from the incoming buffer due to LED and - * key interfaces having different pages. Short payloads are zero-padded - * to the exact report size the hardware expects for this report ID. - */ - return hid_hw_raw_request(hdev, buf[0], dmabuf, hw_len, HID_FEATURE_REPORT, - HID_REQ_SET_REPORT); -} - -/** - * asus_kbd_get_report() - Read a report sized from the descriptor - * @hdev: HID device - * @report_id: report ID to read - * @buf: buffer to store the report in - * @buf_size: size of @buf - * - * The transfer is performed at the exact report size the device declares - * for @report_id. The device may answer with a short read, and only the - * bytes that fit in @buf are copied into it. - * - * Return: 0 on success, negative errno on failure. - */ -static int asus_kbd_get_report(struct hid_device *hdev, u8 report_id, - u8 *buf, size_t buf_size) -{ - size_t hw_len = asus_report_len(hdev, report_id); - int ret; - - if (!hw_len) - return -EINVAL; - - u8 *dmabuf __free(kfree) = kzalloc(hw_len, GFP_KERNEL); - if (!dmabuf) - return -ENOMEM; - - ret = hid_hw_raw_request(hdev, report_id, dmabuf, hw_len, - HID_FEATURE_REPORT, HID_REQ_GET_REPORT); - if (ret < 0) - return ret; - - /* The device may return less than requested (short read). */ - memcpy(buf, dmabuf, min(buf_size, hw_len)); - - return 0; -} - static int asus_kbd_init(struct hid_device *hdev, u8 report_id) { /* @@ -1395,6 +2108,7 @@ static int asus_input_mapping(struct hid_device *hdev, case 0x5f: asus_map_key_clear(KEY_PROG2); break; /* S-shaped programmable key */ case 0x6b: asus_map_key_clear(KEY_F21); break; /* ASUS touchpad toggle */ case 0x38: asus_map_key_clear(KEY_PROG1); break; /* ROG key */ + case 0x93: asus_map_key_clear(KEY_PROG1); break; /* ROG Ally X AC button */ case 0xba: asus_map_key_clear(KEY_PROG2); break; /* Fn+C ASUS Splendid */ case 0x5c: asus_map_key_clear(KEY_PROG3); break; /* Fn+Space Power4Gear */ case 0x99: asus_map_key_clear(KEY_PROG4); break; /* Fn+F5 "fan" symbol */ @@ -1415,6 +2129,25 @@ static int asus_input_mapping(struct hid_device *hdev, return -1; } + /* + * Declare the Ally vendor keys before this input is registered. + * KEY_F19 is the second event of the ROG key (usage 0x38). + */ + if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) { + switch (usage->hid & HID_USAGE) { + case 0x38: + case 0xa6: + case 0xa7: + case 0xa8: + input_set_capability(hi->input, EV_KEY, KEY_PROG1); + input_set_capability(hi->input, EV_KEY, KEY_F16); + input_set_capability(hi->input, EV_KEY, KEY_F17); + input_set_capability(hi->input, EV_KEY, KEY_F18); + input_set_capability(hi->input, EV_KEY, KEY_F19); + break; + } + } + set_bit(EV_REP, hi->input->evbit); return 1; } @@ -1569,6 +2302,8 @@ static int asus_xgm_init(struct hid_device *hdev, struct asus_drvdata *drvdata) static int __maybe_unused asus_resume(struct hid_device *hdev) { struct asus_drvdata *drvdata = hid_get_drvdata(hdev); + /* Published with smp_store_release() after the fields are set. */ + struct ally_intf *intf = smp_load_acquire(&drvdata->ally); /* * If we have a backlight listener registered, restore the previous state, @@ -1578,12 +2313,16 @@ static int __maybe_unused asus_resume(struct hid_device *hdev) if (drvdata->listener.brightness_set) asus_kbd_backlight_set(&drvdata->listener, drvdata->kbd_backlight_brightness); + if (intf) + schedule_delayed_work(&intf->resume_work, msecs_to_jiffies(500)); + return 0; } static int __maybe_unused asus_reset_resume(struct hid_device *hdev) { struct asus_drvdata *drvdata = hid_get_drvdata(hdev); + int ret; if (!drvdata->tp) asus_initialize_reports(hdev); @@ -1591,6 +2330,15 @@ static int __maybe_unused asus_reset_resume(struct hid_device *hdev) if (drvdata->tp) return asus_start_multitouch(hdev); + if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) { + /* Published with smp_store_release() after the fields are set. */ + ret = hid_asus_ally_reset_resume(hdev, smp_load_acquire(&drvdata->ally)); + if (ret) { + hid_err(hdev, "Failed to resume ROG Ally HID extensions: %d\n", ret); + return ret; + } + } + return 0; } @@ -1598,6 +2346,7 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id) { struct hid_report_enum *rep_enum; struct asus_drvdata *drvdata; + struct ally_intf *ally; struct hid_report *rep; bool is_vendor = false; int ret; @@ -1715,12 +2464,22 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id) } } - /* Laptops keyboard backlight is always at 0x5a */ + /* Laptops keyboard backlight is always FEATURE_KBD_REPORT_ID */ if (is_vendor && (drvdata->quirks & QUIRK_USE_KBD_BACKLIGHT) && (asus_has_report_id(hdev, FEATURE_KBD_REPORT_ID)) && (asus_kbd_register_leds(hdev))) hid_warn(hdev, "Failed to initialize backlight.\n"); + if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) { + ally = hid_asus_ally_probe(hdev); + if (IS_ERR(ally)) + hid_err(hdev, "Failed to initialize ROG Ally HID extensions: %ld\n", + PTR_ERR(ally)); + else + /* Readers take this with smp_load_acquire(). */ + smp_store_release(&drvdata->ally, ally); + } + /* * For ROG keyboards, skip rename for consistency and ->input check as * some devices do not have inputs. @@ -1755,8 +2514,14 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id) if (drvdata->xgm_led) devm_led_classdev_unregister(&hdev->dev, &drvdata->xgm_led->cdev); + if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) + ally_quiesce(drvdata->ally); + asus_worker_stop(drvdata->worker); hid_hw_stop(hdev); + + if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) + ally_free(drvdata->ally); return ret; } @@ -1770,8 +2535,14 @@ static void asus_remove(struct hid_device *hdev) if (drvdata->xgm_led) devm_led_classdev_unregister(&hdev->dev, &drvdata->xgm_led->cdev); + if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) + ally_quiesce(drvdata->ally); + asus_worker_stop(drvdata->worker); hid_hw_stop(hdev); + + if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) + ally_free(drvdata->ally); } static const __u8 asus_g752_fixed_rdesc[] = { -- 2.47.3