From d93a8bc47496129f11b544aa5ea46f9d269c6bab Mon Sep 17 00:00:00 2001 From: haytham Date: Thu, 13 Aug 2026 10:19:40 +0800 Subject: [PATCH] asus: add support for the Omni receiver and new data layouts Mice behind an ASUS Omni receiver all enumerate with the same usb id (0b05:1ace), so the regular bus:vid:pid DeviceMatch cannot tell models apart. Identify the model during probe instead: - asus-rog-omni-receiver.device is a minimal bootstrap file that only declares Quirks=OMNI_RECEIVER, so the asus driver gets probed on the receiver's hidraw nodes - the probe queries the serial number signature (03 12 12 02) and matches its prefix against the OmniSignature field of the .device files, then re-resolves the device data for the identified usb id - an unknown signature fails the probe with -ENODEV, which also drops the receiver's keyboard and media control nodes The Strix Impact III family answers with data two bytes earlier than the original protocol (code plus params echo instead of code plus pad), so two quirks cover the new layouts: - LED_V2: per-zone LED responses with data at offset 4, brightness in 0-100 - SETTINGS_V2: settings and DPI responses with data at offset 4, rate/debounce/snapping as single bytes Also write DPI values with two bytes so presets above 12850 dpi work. Verified on a ROG Strix Impact III Wireless: identification on hidraw4, all 5 profiles with 8 buttons, 1 LED and DPI presets 400/800/1600/3200, report rate 1000 Hz, debounce 12 ms, LED brightness 127 (50%). Writes round-trip and survive a ratbagd restart. --- data/devices/asus-rog-omni-receiver.device | 12 ++ src/asus.c | 138 ++++++++++++++++++--- src/asus.h | 8 ++ src/driver-asus.c | 68 ++++++++-- src/libratbag-data.c | 82 ++++++++++++ src/libratbag-data.h | 13 ++ test/data-parse-test.py | 4 + 7 files changed, 296 insertions(+), 29 deletions(-) create mode 100644 data/devices/asus-rog-omni-receiver.device diff --git a/data/devices/asus-rog-omni-receiver.device b/data/devices/asus-rog-omni-receiver.device new file mode 100644 index 00000000..f05cd2cf --- /dev/null +++ b/data/devices/asus-rog-omni-receiver.device @@ -0,0 +1,12 @@ +[Device] +Name=ASUS ROG Omni Receiver +DeviceMatch=usb:0b05:1ace +DeviceType=mouse +Driver=asus + +# The Omni receiver is a shared USB device for multiple mice (all of them +# enumerate with the same usb id), so the actual model is identified during +# probe and resolved to the model-specific .device data. This file only +# exists to get the asus driver probed on the receiver's hidraw nodes. +[Driver/asus] +Quirks=OMNI_RECEIVER diff --git a/src/asus.c b/src/asus.c index b8f96716..fe3bdad3 100644 --- a/src/asus.c +++ b/src/asus.c @@ -26,6 +26,7 @@ #include "asus.h" #include +#include #include "libratbag-data.h" #include "libratbag-private.h" @@ -138,15 +139,37 @@ asus_query(struct ratbag_device *device, union asus_request *request, union asus_response *response) { int rc; + uint32_t quirks = ratbag_device_data_asus_get_quirks(device->data); + uint8_t omni_tx_buf[ASUS_PACKET_SIZE]; + uint8_t omni_rx_buf[ASUS_PACKET_SIZE]; - rc = ratbag_hidraw_output_report(device, request->raw, ASUS_PACKET_SIZE); - if (rc < 0) - return rc; + if (quirks & ASUS_QUIRK_OMNI_RECEIVER) { + /* devices behind the Omni receiver use report ID 0x03 and a + * 64-byte report, the payload is shifted by one byte */ + memset(omni_tx_buf, 0, sizeof(omni_tx_buf)); + omni_tx_buf[0] = 0x03; + memcpy(omni_tx_buf + 1, request->raw, ASUS_PACKET_SIZE - 1); - memset(response, 0, sizeof(union asus_response)); - rc = ratbag_hidraw_read_input_report(device, response->raw, ASUS_PACKET_SIZE, NULL); - if (rc < 0) - return rc; + rc = ratbag_hidraw_output_report(device, omni_tx_buf, ASUS_PACKET_SIZE); + if (rc < 0) + return rc; + + memset(response, 0, sizeof(union asus_response)); + rc = ratbag_hidraw_read_input_report(device, omni_rx_buf, ASUS_PACKET_SIZE, NULL); + if (rc < 0) + return rc; + + memcpy(response->raw, omni_rx_buf + 1, ASUS_PACKET_SIZE - 1); + } else { + rc = ratbag_hidraw_output_report(device, request->raw, ASUS_PACKET_SIZE); + if (rc < 0) + return rc; + + memset(response, 0, sizeof(union asus_response)); + rc = ratbag_hidraw_read_input_report(device, response->raw, ASUS_PACKET_SIZE, NULL); + if (rc < 0) + return rc; + } /* invalid state, disconnected or sleeping */ if (response->data.code == ASUS_STATUS_ERROR) { @@ -339,22 +362,49 @@ asus_get_resolution_data(struct ratbag_device *device, union asus_resolution_dat case 4: /* 4 DPI presets */ if (sep_xy_dpi) { /* separate X & Y values */ - for (i = 0; i < dpi_count; i++) { - data->data_xy.dpi[i].x = data->data_xy.dpi[i].x * 50 + 50; - data->data_xy.dpi[i].y = data->data_xy.dpi[i].y * 50 + 50; - if (quirks & ASUS_QUIRK_DOUBLE_DPI) { - data->data_xy.dpi[i].x *= 2; - data->data_xy.dpi[i].y *= 2; + if (quirks & ASUS_QUIRK_SETTINGS_V2) { + /* new layout: data starts at offset 4, each preset + * is x lo/hi, y lo/hi */ + for (i = 0; i < dpi_count; i++) { + uint16_t x = data->raw[4 + i * 4] | + (data->raw[5 + i * 4] << 8); + uint16_t y = data->raw[6 + i * 4] | + (data->raw[7 + i * 4] << 8); + data->data_xy.dpi[i].x = x * 50 + 50; + data->data_xy.dpi[i].y = y * 50 + 50; + } + } else { + for (i = 0; i < dpi_count; i++) { + data->data_xy.dpi[i].x = data->data_xy.dpi[i].x * 50 + 50; + data->data_xy.dpi[i].y = data->data_xy.dpi[i].y * 50 + 50; + if (quirks & ASUS_QUIRK_DOUBLE_DPI) { + data->data_xy.dpi[i].x *= 2; + data->data_xy.dpi[i].y *= 2; + } } } } else { - for (i = 0; i < dpi_count; i++) { - data->data4.dpi[i] = data->data4.dpi[i] * 50 + 50; - if (quirks & ASUS_QUIRK_DOUBLE_DPI) - data->data4.dpi[i] *= 2; + if (quirks & ASUS_QUIRK_SETTINGS_V2) { + /* new layout: data starts at offset 4, dpi presets + * are 2 bytes each, rate/debounce/snapping are + * single bytes at offset 12/14/16 */ + for (i = 0; i < dpi_count; i++) { + uint16_t dpi = data->raw[4 + i * 2] | + (data->raw[5 + i * 2] << 8); + data->data4.dpi[i] = dpi * 50 + 50; + } + data->data4.rate = ASUS_POLLING_RATES[data->raw[12] & 0x07]; + data->data4.response = ASUS_DEBOUNCE_TIMES[data->raw[14]]; + data->data4.snapping = data->raw[16]; + } else { + for (i = 0; i < dpi_count; i++) { + data->data4.dpi[i] = data->data4.dpi[i] * 50 + 50; + if (quirks & ASUS_QUIRK_DOUBLE_DPI) + data->data4.dpi[i] *= 2; + } + data->data4.rate = ASUS_POLLING_RATES[data->data4.rate]; + data->data4.response = ASUS_DEBOUNCE_TIMES[data->data4.response]; } - data->data4.rate = ASUS_POLLING_RATES[data->data4.rate]; - data->data4.response = ASUS_DEBOUNCE_TIMES[data->data4.response]; } break; @@ -378,10 +428,12 @@ asus_set_dpi(struct ratbag_device *device, unsigned int index, unsigned int dpi) if (quirks & ASUS_QUIRK_DOUBLE_DPI) idpi /= 2; + idpi = (idpi - 50) / 50; union asus_request request = { .data.cmd = ASUS_CMD_SET_SETTING, .data.params[0] = index, - .data.params[2] = (idpi - 50) / 50, + .data.params[2] = idpi & 0xff, + .data.params[3] = (idpi >> 8) & 0xff, }; rc = asus_query(device, &request, &response); @@ -511,3 +563,49 @@ asus_set_led(struct ratbag_device *device, return 0; } + +/* ASUS Omni receiver: identify the mouse behind the shared receiver. + * + * All mice connected to an Omni receiver share the same USB id + * (0b05:1ace), so the model is queried with a signature request + * (03 12 12 02). The response carries a 12 byte ASCII serial number at + * offset 5 and the model is derived from its prefix. + * + * The serial number is matched against the OmniSignature field of the + * .device files, so supporting a new model does not require any code + * changes. On success the usb id of the identified model is returned in + * id, so that the caller can resolve the mouse-specific .device data. + */ +int +asus_omni_identify(struct ratbag_device *device, struct input_id *id) +{ + int rc; + union asus_request request = { + .data.cmd = 0x1212, + .data.params[0] = 0x02, + }; + union asus_response response; + char signature[13]; + + rc = asus_query(device, &request, &response); + if (rc) + return rc; + + /* signature is 12 ASCII bytes at offset 5 of the raw report, + * which is offset 4 after the report id is stripped */ + memcpy(signature, response.raw + 4, 12); + signature[12] = '\0'; + + rc = ratbag_device_data_omni_signature_match(device->ratbag, signature, id); + if (rc) { + log_debug(device->ratbag, + "Unknown ASUS Omni device serial '%s'. Please add an " + "OmniSignature entry to the .device file of the model.\n", + signature); + return rc; + } + + log_info(device->ratbag, + "ASUS Omni device identified (serial %s)\n", signature); + return 0; +} diff --git a/src/asus.h b/src/asus.h index 69befdca..240d47ff 100644 --- a/src/asus.h +++ b/src/asus.h @@ -13,6 +13,9 @@ #define ASUS_QUIRK_SEPARATE_XY_DPI 1 << 4 #define ASUS_QUIRK_SEPARATE_LEDS 1 << 5 #define ASUS_QUIRK_BUTTONS_SECONDARY 1 << 6 +#define ASUS_QUIRK_OMNI_RECEIVER 1 << 7 /* device is behind an ASUS Omni receiver, transport uses report ID 0x03 */ +#define ASUS_QUIRK_LED_V2 1 << 8 /* LED response is per-zone with data at offset 4, brightness is 0-100 */ +#define ASUS_QUIRK_SETTINGS_V2 1 << 9 /* settings/DPI response data starts at offset 4, rate/debounce/snapping are single bytes */ #define ASUS_PACKET_SIZE 64 #define ASUS_BUTTON_ACTION_TYPE_KEY 0 /* keyboard key */ @@ -222,6 +225,11 @@ asus_setup_led(struct ratbag_device *device, struct ratbag_led *led); int asus_query(struct ratbag_device *device, union asus_request *request, union asus_response *response); +/* ASUS Omni receiver: identify the mouse behind the shared receiver and + * return the usb id that matches its own .device file. */ +int +asus_omni_identify(struct ratbag_device *device, struct input_id *id); + /* commit */ int diff --git a/src/driver-asus.c b/src/driver-asus.c index e6f9d4db..e001a328 100644 --- a/src/driver-asus.c +++ b/src/driver-asus.c @@ -212,7 +212,7 @@ asus_driver_load_profile(struct ratbag_device *device, struct ratbag_profile *pr /* get LEDs */ - if (!(quirks & ASUS_QUIRK_SEPARATE_LEDS) && led_count) { + if (!(quirks & (ASUS_QUIRK_SEPARATE_LEDS | ASUS_QUIRK_LED_V2)) && led_count) { log_debug(device->ratbag, "Loading LEDs data\n"); rc = asus_get_led_data(device, &led_data, 0); if (rc) @@ -220,16 +220,29 @@ asus_driver_load_profile(struct ratbag_device *device, struct ratbag_profile *pr } ratbag_profile_for_each_led(profile, led) { - if (quirks & ASUS_QUIRK_SEPARATE_LEDS) { + if (quirks & (ASUS_QUIRK_SEPARATE_LEDS | ASUS_QUIRK_LED_V2)) { log_debug(device->ratbag, "Loading LED %d data\n", led->index); rc = asus_get_led_data(device, &led_data, led->index); if (rc) return rc; - asus_led = &led_data.data.led[0]; - } else { - asus_led = &led_data.data.led[led->index]; } + if (quirks & ASUS_QUIRK_LED_V2) { + /* new layout: a single zone per response with data at + * offset 4, brightness is 0-100 percent */ + led->mode = drv_data->led_modes[led_data.raw[4]]; + led->brightness = led_data.raw[5] * 255 / 100; + led->color.red = led_data.raw[6]; + led->color.green = led_data.raw[7]; + led->color.blue = led_data.raw[8]; + continue; + } + + if (quirks & ASUS_QUIRK_SEPARATE_LEDS) + asus_led = &led_data.data.led[0]; + else + asus_led = &led_data.data.led[led->index]; + led->mode = drv_data->led_modes[asus_led->mode]; if (quirks & ASUS_QUIRK_RAW_BRIGHTNESS) { led->brightness = asus_led->brightness; @@ -379,6 +392,9 @@ asus_driver_save_profile(struct ratbag_device *device, struct ratbag_profile *pr uint8_t led_brightness; if (quirks & ASUS_QUIRK_RAW_BRIGHTNESS) { led_brightness = led->brightness; + } else if (quirks & ASUS_QUIRK_LED_V2) { + /* convert brightness from 0-255 to 0-100 percent */ + led_brightness = (uint8_t)round((double)led->brightness * 100.0 / 255.0); } else { /* convert brightness from 0-256 to 0-4 */ led_brightness = (uint8_t)round((double)led->brightness / 64.0); @@ -515,17 +531,51 @@ asus_driver_probe(struct ratbag_device *device) struct ratbag_button *button; struct ratbag_resolution *resolution; struct ratbag_led *led; + uint32_t quirks = ratbag_device_data_asus_get_quirks(device->data); rc = ratbag_open_hidraw(device); if (rc) return rc; - rc = asus_get_profile_data(device, &profile_data); - if (rc) { - ratbag_close_hidraw(device); - return -ENODEV; + /* The Omni receiver is a shared USB device for multiple mice, so the + * actual model has to be identified and resolved to its own .device + * data before anything else. Wrong hidraw nodes (keyboard, media + * control, etc.) fail the identification and are rejected here. */ + if (quirks & ASUS_QUIRK_OMNI_RECEIVER) { + struct ratbag_device_data *data; + struct input_id id = device->ids; + + rc = asus_omni_identify(device, &id); + if (rc) { + log_debug(device->ratbag, + "Omni device is not a supported mouse\n"); + ratbag_close_hidraw(device); + return -ENODEV; + } + + data = ratbag_device_data_new_for_id(device->ratbag, &id); + if (!data) { + log_error(device->ratbag, + "No device data for Omni mouse %04x:%04x\n", + id.vendor, id.product); + ratbag_close_hidraw(device); + return -ENODEV; + } + + ratbag_device_data_unref(device->data); + device->data = data; + device->devicetype = ratbag_device_data_get_device_type(device->data); + free(device->name); + device->name = strdup_safe(ratbag_device_data_get_name(device->data)); + log_info(device->ratbag, + "ASUS Omni receiver identified device: %s\n", + device->name); + + quirks = ratbag_device_data_asus_get_quirks(device->data); } + rc = asus_get_profile_data(device, &profile_data); + /* create device state data */ drv_data = zalloc(sizeof(*drv_data)); ratbag_set_drv_data(device, drv_data); diff --git a/src/libratbag-data.c b/src/libratbag-data.c index 38ba119a..90e6de99 100644 --- a/src/libratbag-data.c +++ b/src/libratbag-data.c @@ -107,6 +107,7 @@ struct data_asus { struct dpi_range *dpi_range; uint32_t quirks; int led_modes[ASUS_MAX_NUM_LED_MODES]; + char *omni_signature; /* serial number prefix of mice behind an Omni receiver */ }; struct ratbag_device_data { @@ -441,6 +442,9 @@ init_data_asus(struct ratbag *ratbag, data->asus.is_wireless = wireless; g_clear_error(&error); + data->asus.omni_signature = g_key_file_get_string(keyfile, group, "OmniSignature", &error); + g_clear_error(&error); + gsize quirks_count = 0; _cleanup_(g_strfreevp) char **quirks = g_key_file_get_string_list(keyfile, group, "Quirks", &quirks_count, &error); if (!error && quirks) { @@ -457,6 +461,12 @@ init_data_asus(struct ratbag *ratbag, data->asus.quirks |= ASUS_QUIRK_SEPARATE_LEDS; } else if (streq(quirks[i], "BUTTONS_SECONDARY")) { data->asus.quirks |= ASUS_QUIRK_BUTTONS_SECONDARY; + } else if (streq(quirks[i], "OMNI_RECEIVER")) { + data->asus.quirks |= ASUS_QUIRK_OMNI_RECEIVER; + } else if (streq(quirks[i], "LED_V2")) { + data->asus.quirks |= ASUS_QUIRK_LED_V2; + } else if (streq(quirks[i], "SETTINGS_V2")) { + data->asus.quirks |= ASUS_QUIRK_SETTINGS_V2; } else { log_error(ratbag, "%s is invalid quirk. Ignoring...\n", quirks[i]); } @@ -753,7 +763,79 @@ out: return data; } +int +ratbag_device_data_omni_signature_match(struct ratbag *ratbag, + const char *signature, + struct input_id *id) +{ + struct dirent **files; + const char *datadir; + int n, nfiles; + size_t best_len = 0; + int rc = -ENODEV; + datadir = getenv("LIBRATBAG_DATA_DIR"); + if (!datadir) + datadir = LIBRATBAG_DATA_DIR; + + n = scandir(datadir, &files, filter_device_files, alphasort); + if (n <= 0) { + log_error(ratbag, "Unable to locate device files in %s: %s\n", + datadir, n == 0 ? "No files found" : strerror(errno)); + return rc; + } + + nfiles = n; + while (n--) { + _cleanup_(g_key_file_freep) GKeyFile *keyfile = NULL; + _cleanup_(g_error_freep) GError *error = NULL; + _cleanup_(g_strfreevp) char **match_strv = NULL; + _cleanup_free_ char *omni_signature = NULL; + _cleanup_(freep) char *file = NULL; + size_t len; + unsigned int vendor, product; + + if (xasprintf(&file, "%s/%s", datadir, files[n]->d_name) == -1) + goto out; + + keyfile = g_key_file_new(); + if (!g_key_file_load_from_file(keyfile, file, G_KEY_FILE_NONE, &error)) { + log_error(ratbag, "Failed to parse keyfile %s: %s\n", + file, error->message); + continue; + } + + omni_signature = g_key_file_get_string(keyfile, "Driver/asus", + "OmniSignature", NULL); + if (!omni_signature) + continue; + + len = strlen(omni_signature); + if (len <= best_len || strncmp(signature, omni_signature, len) != 0) + continue; + + match_strv = g_key_file_get_string_list(keyfile, GROUP_DEVICE, + "DeviceMatch", NULL, NULL); + if (!match_strv) + continue; + + /* the device must have a usb id to be resolvable */ + if (sscanf(match_strv[0], "usb:%x:%x", &vendor, &product) != 2) + continue; + + id->vendor = vendor; + id->product = product; + best_len = len; + rc = 0; + } + +out: + while (nfiles--) + free(files[nfiles]); + free(files); + + return rc; +} /* HID++ 1.0 */ int diff --git a/src/libratbag-data.h b/src/libratbag-data.h index fd82cf9b..2e01cae8 100644 --- a/src/libratbag-data.h +++ b/src/libratbag-data.h @@ -140,6 +140,19 @@ ratbag_device_data_steelseries_get_quirk(const struct ratbag_device_data *data); /* ASUS */ +/** + * Find the usb id of the device whose OmniSignature matches the serial + * number of a mouse behind an Omni receiver. The longest matching prefix + * wins. The bustype of id is left untouched, only vendor and product are + * set. + * + * @return 0 on success or -ENODEV if no .device file matches + */ +int +ratbag_device_data_omni_signature_match(struct ratbag *ratbag, + const char *signature, + struct input_id *id); + /** * @return Number of profiles */ diff --git a/test/data-parse-test.py b/test/data-parse-test.py index bd325799..adf841ab 100755 --- a/test/data-parse-test.py +++ b/test/data-parse-test.py @@ -115,6 +115,7 @@ def check_section_asus(section: configparser.SectionProxy): "Leds", "LedModes", "Profiles", + "OmniSignature", "Quirks", "Wireless", ) @@ -135,6 +136,9 @@ def check_section_asus(section: configparser.SectionProxy): "SEPARATE_XY_DPI", "SEPARATE_LEDS", "BUTTONS_SECONDARY", + "OMNI_RECEIVER", + "LED_V2", + "SETTINGS_V2", ) for quirk in section["Quirks"].split(";"): assert quirk in quirks