[PATCH v11 13/18] HID: asus: identify XG Mobile stations and bind the LED to the reported model

From: Denis Benato

Date: Sun Oct 25 2026 - 19:56:46 EST


From: Denis Benato <benato.denis96@xxxxxxxxx>

The XG Mobile LED interface is currently bound to two USB product IDs
(0x1970, 0x1a9a). The stations identify themselves through the vendor
report 0x5E: after the usual "ASUS Tech.Inc." unlock, the 0xCE 0x01
query answers with a NUL terminated string of the form

MB:<mainboard>;SN:<serial>;PRO:<model>;PNUM:<part number>;

Run this query at probe time to initialize the LED interface only on
supported models (GC31, GC32 and GC33), and name the LED after the
reported model (asus:xgm-<model>:led).

Signed-off-by: Denis Benato <denis.benato@xxxxxxxxx>
---
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:<mainboard>;SN:<serial>;PRO:<model>;PNUM:<part number>;", which is
+ * split into the fields of @info. The model field is mandatory: a station
+ * that does not report one is treated as absent.
+ *
+ * Return: 0 on success, negative errno on failure
+ */
+static int asus_xgm_get_identity(struct hid_device *hdev,
+ struct asus_xgm_info *info)
+{
+ size_t len = asus_report_len(hdev, FEATURE_KBD_LED_REPORT_ID2);
+ const char *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