Thread (44 messages) 44 messages, 5 authors, 1d ago

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

flat view
WARM1d

From: Denis Benato <denis.benato@linux.dev>
Date: 2026-10-25 23:53:05
Also in: lkml, platform-driver-x86
Subsystem: hid core layer, the rest · Maintainers: Jiri Kosina, Benjamin Tissoires, Linus Torvalds

Revision v11 (4 revisions archived).

Revisions (4)
  1. v4
  2. v5 [diff against previous]
  3. v6 [diff against previous]
  4. v11 current [diff against previous]
From: Denis Benato <redacted>

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@linux.dev>
---
 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
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help