From mboxrd@z Thu Jan 1 00:00:00 1970 Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.remote-ip=91.218.175.185 ARC-Seal: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; cv=none; t=1792972386; b=34IwBJpvhXDWoCdDjZjWAhAmVJUa1ho9t9C0eT1l9FmDWMXtj99diIAcPdabrZuf5FfI Eksjl+i4TpP7Sw7zEzZZRprwoCkdF0sK88aCIzzJH2gLcgLo1tGFQlJ6e6I6Kbx6sWeWN MJLt0yWPeI4tdozzLEPIlnhpMFItYEywxGTWmLiMlq5wJzmFS48gBeko6GoGJJFb5UmOi ICzv4MDP3K4UsqPC5XBo/7juZRZapOjDb6dWWt7N6/6vdnJnDNaXVevBxadTXCcZrwBT8 +IQo2N8ffZnVegirlFxztn2nrmDVxlxU8ijRJi4L1hsI4arcbpHH+I9n8WlYjjZIiOA== ARC-Message-Signature: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; c=relaxed/relaxed; t=1792972386; 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=q1jVuR+7SereoPAmroo1mFDZie0TS4p+iVnpoukwQAk=; b=CdJwmddJYfrst1HLHMZ803pNyQl0/n9xQ68TLk4txmbiLq2m5zZNlAZ53KoHq0ZeyHuv bznffgkph5Yt6qT75Q4etCzVWp+hG5LlTHEd9w8thtvUxvuOE8bhGkhCfipWgfcJ1skZY ylyCvE/bj3MtABF0SJOks8XNVWKiSK+JfSN92uv3qE6yVNGjrBKk5cc6cx5a2BwzzYSR5 nPsUyEtcWVkrf5L2MnwwRgMRStGXByq/K3WMHa6HItC382iozBknTPcvTDE+rIY22Jn6v tKgKwYB0TU4RNQMQSYWjy8+X+r+q//QOnezSA3VnA/fey6MJ6X/8CVzr/eVSIKH5vvQ== 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=cDNEl++j; arc=none smtp.remote-ip=91.218.175.185 Received: from mta0.migadu.com (out-185.mta0.migadu.com [91.218.175.185]) (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 2BDC160A2625 for ; Sun, 25 Oct 2026 23:53:05 +0000 (UTC) DMARC-Filter: OpenDMARC Filter v1.4.2 smtp.subspace.kernel.org 2BDC160A2625 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=cDNEl++j X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=TA069Yp6Ri3AwX+dumolaXc1HqbOA3IPqVuolFxXsS4=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1792972384; v=1; x=1793577184; b=cDNEl++jXZEAJwy2ljdWMXT3MY2VlT9hjoaRNCNYhbAwRT9pNcQxb4Dm5hCPscorrJwX+CaX MWCTagw3LI25zvfgPCzpWwBptoLSstgzHP6TEn78oNt/4UOZ0uWAgH20C9N+9cO8M6tzZefNWPa otheqHQtTz4GmnvjeBkbTS0k= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id cdb6c5a354d65986; Sun, 25 Oct 2026 23:53:04 +0000 X-Mizu-Trace-ID: cdb6c5a354d65986 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 v11 13/18] HID: asus: identify XG Mobile stations and bind the LED to the reported model Date: Sun, 25 Oct 2026 23:52:26 +0000 Message-ID: <20261025235232.129945-14-denis.benato@linux.dev> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20261025235232.129945-1-denis.benato@linux.dev> References: <20261025235232.129945-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 From: Denis Benato The XG Mobile LED interface is currently bound to two USB product IDs (0x1970, 0x1a9a). The stations identify themselves through the vendor report 0x5E: after the usual "ASUS Tech.Inc." unlock, the 0xCE 0x01 query answers with a NUL terminated string of the form MB:;SN:;PRO:;PNUM:; Run this query at probe time to initialize the LED interface only on supported models (GC31, GC32 and GC33), and name the LED after the reported model (asus:xgm-:led). Signed-off-by: Denis Benato --- drivers/hid/hid-asus.c | 183 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 182 insertions(+), 1 deletion(-) diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c index fe2a326ccd86..e751bf163434 100644 --- a/drivers/hid/hid-asus.c +++ b/drivers/hid/hid-asus.c @@ -63,6 +63,12 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad"); #define FEATURE_KBD_LED_REPORT_ID1 0x5d #define FEATURE_KBD_LED_REPORT_ID2 0x5e +/* XG Mobile station protocol on report 0x5E */ +#define ASUS_XGM_CMD_QUERY 0xce +#define ASUS_XGM_QUERY_IDENTITY 0x01 +#define ASUS_XGM_QUERY_VERSION 0x03 +#define ASUS_XGM_INFO_LEN 32 + #define ROG_ALLY_REPORT_SIZE 64 #define ROG_ALLY_X_MIN_MCU 313 #define ROG_ALLY_MIN_MCU 319 @@ -174,6 +180,13 @@ struct asus_xgm_led { struct hid_device *hdev; }; +struct asus_xgm_info { + char mainboard[ASUS_XGM_INFO_LEN]; + char serial[ASUS_XGM_INFO_LEN]; + char model[ASUS_XGM_INFO_LEN]; + char part_number[ASUS_XGM_INFO_LEN]; +}; + struct asus_touchpad_info { int max_x; int max_y; @@ -471,6 +484,7 @@ struct asus_drvdata { struct asus_hid_listener listener; bool fn_lock; struct asus_xgm_led *xgm_led; + struct asus_xgm_info *xgm_info; }; static int asus_report_battery(struct asus_drvdata *, u8 *, int); @@ -6049,6 +6063,147 @@ static int asus_battery_probe(struct hid_device *hdev) return ret; } +/** + * asus_xgm_query() - Run a query command against an XG Mobile station + * @hdev: HID device + * @subcmd: query sub-command + * @buf: buffer to store the answer in + * @buf_size: size of @buf + * + * Windows re-sends the "ASUS Tech.Inc." unlock before every command + * burst, so do the same. + * + * Return: 0 on success, negative errno on failure + */ +static int asus_xgm_query(struct hid_device *hdev, u8 subcmd, + u8 *buf, size_t buf_size) +{ + const u8 unlock[] = { + FEATURE_KBD_LED_REPORT_ID2, + 'A', 'S', 'U', 'S', ' ', 'T', 'e', 'c', 'h', '.', 'I', 'n', 'c', '.' + }; + const u8 cmd[] = { FEATURE_KBD_LED_REPORT_ID2, ASUS_XGM_CMD_QUERY, subcmd }; + int ret; + + ret = asus_kbd_set_report(hdev, unlock, sizeof(unlock)); + if (ret < 0) + return ret; + + ret = asus_kbd_set_report(hdev, cmd, sizeof(cmd)); + if (ret < 0) + return ret; + + return asus_kbd_get_report(hdev, FEATURE_KBD_LED_REPORT_ID2, buf, buf_size); +} + +/** + * asus_xgm_info_assign() - Copy an identity field up to its ';' + * terminator + * @dst: destination buffer + * @dst_len: size of @dst, including the trailing NUL + * @src: start of the field inside the identity string + * @src_len: number of bytes available at @src + * + * Copies up to @dst_len - 1 bytes from @src, stopping at the first ';' + * field separator, and NUL terminates @dst. + */ +static void asus_xgm_info_assign(char *dst, size_t dst_len, + const char *src, size_t src_len) +{ + const char *sep = strnchr(src, src_len, ';'); + size_t n = sep ? (size_t)(sep - src) : src_len; + + if (n >= dst_len) + n = dst_len - 1; + + memcpy(dst, src, n); + dst[n] = '\0'; +} + +/** + * asus_xgm_get_identity() - Query the identity of an XG Mobile station + * @hdev: HID device + * @info: identity structure to fill in + * + * The identity query answers with a NUL terminated string of the form + * "MB:;SN:;PRO:;PNUM:;", which is + * split into the fields of @info. The model field is mandatory: a station + * that does not report one is treated as absent. + * + * Return: 0 on success, negative errno on failure + */ +static int asus_xgm_get_identity(struct hid_device *hdev, + struct asus_xgm_info *info) +{ + size_t len = asus_report_len(hdev, FEATURE_KBD_LED_REPORT_ID2); + const char *start, *p; + size_t rem; + int ret; + + if (!len) + return -EINVAL; + + u8 *buf __free(kfree) = kzalloc(len, GFP_KERNEL); + if (!buf) + return -ENOMEM; + + ret = asus_xgm_query(hdev, ASUS_XGM_QUERY_IDENTITY, buf, len); + if (ret < 0) + return ret; + + if (len < 4 || buf[1] != ASUS_XGM_CMD_QUERY || + buf[2] != ASUS_XGM_QUERY_IDENTITY) + return -ENODEV; + + start = (char *)&buf[3]; + rem = strnlen(start, len - 3); + if (!rem) + return -ENODEV; + + p = strnstr(start, "MB:", rem); + if (p) + asus_xgm_info_assign(info->mainboard, sizeof(info->mainboard), + p + 3, rem - (p + 3 - start)); + p = strnstr(start, "SN:", rem); + if (p) + asus_xgm_info_assign(info->serial, sizeof(info->serial), + p + 3, rem - (p + 3 - start)); + p = strnstr(start, "PRO:", rem); + if (p) + asus_xgm_info_assign(info->model, sizeof(info->model), + p + 4, rem - (p + 4 - start)); + p = strnstr(start, "PNUM:", rem); + if (p) + asus_xgm_info_assign(info->part_number, sizeof(info->part_number), + p + 5, rem - (p + 5 - start)); + + if (!info->model[0]) + return -ENODEV; + + return 0; +} + +static const char * const asus_xgm_models[] = { "GC31", "GC32", "GC33" }; + +/** + * asus_xgm_is_supported_model() - Check that a station is supported + * @info: identity of the station + * + * The supported families are the GC31, GC32 and GC33 stations. + * + * Return: true if the reported model belongs to one of the supported + * families, false otherwise + */ +static bool asus_xgm_is_supported_model(const struct asus_xgm_info *info) +{ + for (size_t i = 0; i < ARRAY_SIZE(asus_xgm_models); i++) + if (strncmp(info->model, asus_xgm_models[i], + strlen(asus_xgm_models[i])) == 0) + return true; + + return false; +} + static int asus_xgm_led_set(struct led_classdev *led_cdev, enum led_brightness value) { const u8 buf[] = { FEATURE_KBD_LED_REPORT_ID2, 0xC5, (value) ? 0x50 : 0x00 }; @@ -6315,15 +6470,41 @@ static void asus_initialize_reports(struct hid_device *hdev) static int asus_xgm_init(struct hid_device *hdev, struct asus_drvdata *drvdata) { + struct asus_xgm_info *info; const char *name; int ret; + info = devm_kzalloc(&hdev->dev, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + /* + * The station reports its identity through the query protocol: the + * reported model decides whether the LED interface is registered + * and what name it gets. + */ + ret = asus_xgm_get_identity(hdev, info); + if (ret < 0) { + hid_warn(hdev, "Failed to query XG Mobile identity: %d\n", ret); + return 0; + } + + hid_info(hdev, "XG Mobile: model %s, serial %s\n", + info->model, info->serial); + + if (!asus_xgm_is_supported_model(info)) { + hid_warn(hdev, "Unsupported XG Mobile model %s\n", info->model); + return 0; + } + + drvdata->xgm_info = info; + drvdata->xgm_led = devm_kzalloc(&hdev->dev, sizeof(*drvdata->xgm_led), GFP_KERNEL); if (drvdata->xgm_led == NULL) return -ENOMEM; name = devm_kasprintf(&hdev->dev, GFP_KERNEL, "asus:xgm-%s:led", - strlen(hdev->uniq) ? hdev->uniq : dev_name(&hdev->dev)); + info->model); if (name == NULL) { ret = -ENOMEM; -- 2.47.3