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