1
0
forked from drath/libratbag

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.
This commit is contained in:
haytham
2026-08-13 10:19:40 +08:00
committed by staticssleever668
parent 1f4efda955
commit d93a8bc474
7 changed files with 296 additions and 29 deletions

View File

@@ -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

View File

@@ -26,6 +26,7 @@
#include "asus.h" #include "asus.h"
#include <assert.h> #include <assert.h>
#include <string.h>
#include "libratbag-data.h" #include "libratbag-data.h"
#include "libratbag-private.h" #include "libratbag-private.h"
@@ -138,15 +139,37 @@ asus_query(struct ratbag_device *device,
union asus_request *request, union asus_response *response) union asus_request *request, union asus_response *response)
{ {
int rc; 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 (quirks & ASUS_QUIRK_OMNI_RECEIVER) {
if (rc < 0) /* devices behind the Omni receiver use report ID 0x03 and a
return rc; * 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_output_report(device, omni_tx_buf, ASUS_PACKET_SIZE);
rc = ratbag_hidraw_read_input_report(device, response->raw, ASUS_PACKET_SIZE, NULL); if (rc < 0)
if (rc < 0) return rc;
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 */ /* invalid state, disconnected or sleeping */
if (response->data.code == ASUS_STATUS_ERROR) { 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 */ case 4: /* 4 DPI presets */
if (sep_xy_dpi) { /* separate X & Y values */ if (sep_xy_dpi) { /* separate X & Y values */
for (i = 0; i < dpi_count; i++) { if (quirks & ASUS_QUIRK_SETTINGS_V2) {
data->data_xy.dpi[i].x = data->data_xy.dpi[i].x * 50 + 50; /* new layout: data starts at offset 4, each preset
data->data_xy.dpi[i].y = data->data_xy.dpi[i].y * 50 + 50; * is x lo/hi, y lo/hi */
if (quirks & ASUS_QUIRK_DOUBLE_DPI) { for (i = 0; i < dpi_count; i++) {
data->data_xy.dpi[i].x *= 2; uint16_t x = data->raw[4 + i * 4] |
data->data_xy.dpi[i].y *= 2; (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 { } else {
for (i = 0; i < dpi_count; i++) { if (quirks & ASUS_QUIRK_SETTINGS_V2) {
data->data4.dpi[i] = data->data4.dpi[i] * 50 + 50; /* new layout: data starts at offset 4, dpi presets
if (quirks & ASUS_QUIRK_DOUBLE_DPI) * are 2 bytes each, rate/debounce/snapping are
data->data4.dpi[i] *= 2; * 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; break;
@@ -378,10 +428,12 @@ asus_set_dpi(struct ratbag_device *device, unsigned int index, unsigned int dpi)
if (quirks & ASUS_QUIRK_DOUBLE_DPI) if (quirks & ASUS_QUIRK_DOUBLE_DPI)
idpi /= 2; idpi /= 2;
idpi = (idpi - 50) / 50;
union asus_request request = { union asus_request request = {
.data.cmd = ASUS_CMD_SET_SETTING, .data.cmd = ASUS_CMD_SET_SETTING,
.data.params[0] = index, .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); rc = asus_query(device, &request, &response);
@@ -511,3 +563,49 @@ asus_set_led(struct ratbag_device *device,
return 0; 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;
}

View File

@@ -13,6 +13,9 @@
#define ASUS_QUIRK_SEPARATE_XY_DPI 1 << 4 #define ASUS_QUIRK_SEPARATE_XY_DPI 1 << 4
#define ASUS_QUIRK_SEPARATE_LEDS 1 << 5 #define ASUS_QUIRK_SEPARATE_LEDS 1 << 5
#define ASUS_QUIRK_BUTTONS_SECONDARY 1 << 6 #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_PACKET_SIZE 64
#define ASUS_BUTTON_ACTION_TYPE_KEY 0 /* keyboard key */ #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 int
asus_query(struct ratbag_device *device, union asus_request *request, union asus_response *response); 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 */ /* commit */
int int

View File

@@ -212,7 +212,7 @@ asus_driver_load_profile(struct ratbag_device *device, struct ratbag_profile *pr
/* get LEDs */ /* 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"); log_debug(device->ratbag, "Loading LEDs data\n");
rc = asus_get_led_data(device, &led_data, 0); rc = asus_get_led_data(device, &led_data, 0);
if (rc) 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) { 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); log_debug(device->ratbag, "Loading LED %d data\n", led->index);
rc = asus_get_led_data(device, &led_data, led->index); rc = asus_get_led_data(device, &led_data, led->index);
if (rc) if (rc)
return 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]; led->mode = drv_data->led_modes[asus_led->mode];
if (quirks & ASUS_QUIRK_RAW_BRIGHTNESS) { if (quirks & ASUS_QUIRK_RAW_BRIGHTNESS) {
led->brightness = asus_led->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; uint8_t led_brightness;
if (quirks & ASUS_QUIRK_RAW_BRIGHTNESS) { if (quirks & ASUS_QUIRK_RAW_BRIGHTNESS) {
led_brightness = led->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 { } else {
/* convert brightness from 0-256 to 0-4 */ /* convert brightness from 0-256 to 0-4 */
led_brightness = (uint8_t)round((double)led->brightness / 64.0); 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_button *button;
struct ratbag_resolution *resolution; struct ratbag_resolution *resolution;
struct ratbag_led *led; struct ratbag_led *led;
uint32_t quirks = ratbag_device_data_asus_get_quirks(device->data);
rc = ratbag_open_hidraw(device); rc = ratbag_open_hidraw(device);
if (rc) if (rc)
return rc; return rc;
rc = asus_get_profile_data(device, &profile_data); /* The Omni receiver is a shared USB device for multiple mice, so the
if (rc) { * actual model has to be identified and resolved to its own .device
ratbag_close_hidraw(device); * data before anything else. Wrong hidraw nodes (keyboard, media
return -ENODEV; * 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 */ /* create device state data */
drv_data = zalloc(sizeof(*drv_data)); drv_data = zalloc(sizeof(*drv_data));
ratbag_set_drv_data(device, drv_data); ratbag_set_drv_data(device, drv_data);

View File

@@ -107,6 +107,7 @@ struct data_asus {
struct dpi_range *dpi_range; struct dpi_range *dpi_range;
uint32_t quirks; uint32_t quirks;
int led_modes[ASUS_MAX_NUM_LED_MODES]; int led_modes[ASUS_MAX_NUM_LED_MODES];
char *omni_signature; /* serial number prefix of mice behind an Omni receiver */
}; };
struct ratbag_device_data { struct ratbag_device_data {
@@ -441,6 +442,9 @@ init_data_asus(struct ratbag *ratbag,
data->asus.is_wireless = wireless; data->asus.is_wireless = wireless;
g_clear_error(&error); 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; gsize quirks_count = 0;
_cleanup_(g_strfreevp) char **quirks = g_key_file_get_string_list(keyfile, group, "Quirks", &quirks_count, &error); _cleanup_(g_strfreevp) char **quirks = g_key_file_get_string_list(keyfile, group, "Quirks", &quirks_count, &error);
if (!error && quirks) { if (!error && quirks) {
@@ -457,6 +461,12 @@ init_data_asus(struct ratbag *ratbag,
data->asus.quirks |= ASUS_QUIRK_SEPARATE_LEDS; data->asus.quirks |= ASUS_QUIRK_SEPARATE_LEDS;
} else if (streq(quirks[i], "BUTTONS_SECONDARY")) { } else if (streq(quirks[i], "BUTTONS_SECONDARY")) {
data->asus.quirks |= ASUS_QUIRK_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 { } else {
log_error(ratbag, "%s is invalid quirk. Ignoring...\n", quirks[i]); log_error(ratbag, "%s is invalid quirk. Ignoring...\n", quirks[i]);
} }
@@ -753,7 +763,79 @@ out:
return data; 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 */ /* HID++ 1.0 */
int int

View File

@@ -140,6 +140,19 @@ ratbag_device_data_steelseries_get_quirk(const struct ratbag_device_data *data);
/* ASUS */ /* 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 * @return Number of profiles
*/ */

View File

@@ -115,6 +115,7 @@ def check_section_asus(section: configparser.SectionProxy):
"Leds", "Leds",
"LedModes", "LedModes",
"Profiles", "Profiles",
"OmniSignature",
"Quirks", "Quirks",
"Wireless", "Wireless",
) )
@@ -135,6 +136,9 @@ def check_section_asus(section: configparser.SectionProxy):
"SEPARATE_XY_DPI", "SEPARATE_XY_DPI",
"SEPARATE_LEDS", "SEPARATE_LEDS",
"BUTTONS_SECONDARY", "BUTTONS_SECONDARY",
"OMNI_RECEIVER",
"LED_V2",
"SETTINGS_V2",
) )
for quirk in section["Quirks"].split(";"): for quirk in section["Quirks"].split(";"):
assert quirk in quirks assert quirk in quirks