[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)
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