Итак, вы потратили много денег на красивую, удобную игровую мышь. Мышь действительно хорошо сидит в руке, приятно светится всеми цветами радуги, имеет целых 6 дополнительных кнопок в дополнение к 5 стандартным, гибко настраивается... Разумеется, вы хотите использовать её возможности полностью! Стоп, гибко настраивается? Разумеется, разработчики ASUS так не считают!
Фирменный софт Armoury Crate (как и предыдущие версии) даёт вам довольно скромный выбор настроек кнопок мыши:
Функция мыши
Функция клавиатуры
Мультимедиа
Макрос

На первый взгляд, это нормальный набор, однако дьявол в деталях. Вы думаете, вы можете забиндить любую клавишу клавиатуры? Зависит от того, что вы считаете клавиатурой. Разработчики ASUS, например, считают стандартную 100% клавиатуру.

Возникает логичный вопрос - а зачем мне нужны дополнительные кнопки котрые могут только дублировать функциональность уже имеющихся? Не, для некоторых программ это имеет смысл, например при трассировке плат смену слоя, Num *, очень удобно иметь на мыши. Но это не серьёзно, раз мы купили все клавиши, то надо использовать все клавиши.
Push to Talk
Вот исти��ная причина, почему я вообще занялся тем, что описано в статье. Понимаете ли, я частенько зависаю в голосовых чатах. И у нас там не принято пользоваться активацией по голосу — просто как правило приличия. Для активации по кнопке, разумеется, нужна кнопка. Но какая? Я параллельно, зависая в чате, могу заниматься вообще чем угодно: играми, веб-сёрфингом, программированием или даже CAD. Собственно, необходимость набора текста сразу исключает все цифро-буквенные клавиши вместе с Ctrl и Shift, а остальные занятия — практически все оставшиеся клавиши! Единственной остаётся Scroll Lock, но согласитесь: постоянно мигать светодиодом на клавиатуре — такое себе.
Есть ли выход? Конечно, есть: ведь ваша клавиатура на самом деле неполная. Что? Да! Как минимум существуют ещё F13–F24. Поддерживаются ли они системами ввода? Естественно:
https://learn.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input
Даже существуют в дикой природе.

Итак, наша задача становится простой: надо забиндить F13 на дополнительную кнопку мыши. Но как именно?
Наивная попытка №1: Макросы
Собственно, в Armoury Crate можно забиндить клавишу клавиатуры двумя способами: выбрать её на виртуальной клавиатуре или же нажать на физической. Тогда просто ставим AHK, биндим её и Armoury Crate ��олучает нажатие нужной кнопки? Ага, сейчас, размечтались. Armoury Crate принимает только клавиши, которые есть выше на виртуальной клавиатуре.
Редактирование асусовских макросов также не дало результата, тем более что их в последних обновлениях вынесли в отдельный загружаемый модуль для Armoury Crate. Вы уже начинаете понимать уровень шизофрении у этих разработчиков, да?
Наивная попытка №2: Редактируем профиль руками
Разумеется если вы прогеймер и профессионально настроили мышь, вы бы хотели поделиться этими настройками с миром. И Armoury Crate предоставляет нам такую возможность:

Сохраняемый профиль идёт в формате .rog, внутри содержится странная, на первый взгляд, строка в UTF-8. Разумеется это base-64, в него закодирован url-закодированный json.

Декодируем профиль парой строк на питоне js и кодируем обратно также.
Функции декодирования и кодирования
// Функция для декодирования: base64 -> URL-decode -> JSON -> файл
function decodeProfile(base64String, outputFile = 'decoded_profile.json') {
try {
// Шаг 1: Декодируем base64 в Buffer
const buffer = Buffer.from(base64String, 'base64');
// Шаг 2: Преобразуем в строку (UTF-8)
let decodedString = buffer.toString('utf8');
// Шаг 3: URL-декодируем
decodedString = decodeURIComponent(decodedString);
// Шаг 4: Парсим JSON
const jsonData = JSON.parse(decodedString);
// Шаг 5: Сохраняем pretty-printed JSON в файл
const prettyJson = JSON.stringify(jsonData, null, 2);
fs.writeFileSync(outputFile, prettyJson, 'utf8');
console.log(`Профиль успешно декодирован и сохранён в файл: ${outputFile}`);
console.log('Предпросмотр:');
console.log(prettyJson.substring(0, 500) + '...'); // Первые 500 символов для preview
return jsonData; // Возвращаем, если нужно дальше использовать
} catch (error) {
console.error('Ошибка декодирования:', error.message);
}
}// Функция для кодирования: JSON -> URL-encode -> base64 -> файл
function encodeProfile(jsonData, outputFile = 'encoded_profile.rog') {
try {
// Шаг 1: Преобразуем JSON в строку (minified для consistency, без пробелов)
const jsonString = JSON.stringify(jsonData);
// Шаг 2: URL-кодируем строку
const urlEncoded = encodeURIComponent(jsonString);
// Шаг 3: Преобразуем в Buffer (UTF-8)
const buffer = Buffer.from(urlEncoded, 'utf8');
// Шаг 4: Кодируем в base64
const base64String = buffer.toString('base64');
// Шаг 5: Сохраняем base64 в файл
fs.writeFileSync(outputFile, base64String, 'utf8');
console.log(`Профиль успешно закодирован и сохранён в файл: ${outputFile}`);
console.log('Предпросмотр base64 (первые 100 символов):');
console.log(base64String.substring(0, 100) + '...');
return base64String; // Возвращаем, если нужно дальше использовать
} catch (error) {
console.error('Ошибка кодирования:', error.message);
}
}Дальше надо выяснить используемые асусом коды клавиш. Вот так выглядит запись настройки одной кнопки:
"13": {
"buttonFunction": "1",
"key_1": 6,
"key_2": "-1",
"key_3": "-1",
"layout": "en-US"
}И тут нас ждёт сюрприз. Как вы думаете какая кнопка клавиатуры тут забиндена? А вот не угадали, это F6. Если немного побиндить разные кнопки, то вырисовывается довольно странная картина:
Scroll lock - 267
Pause - 523
F1 - 1
F3 - 3
F8 - 8
F9 - 9
F10 - 10
F11 - 1541
F12 - 1797
Insert - 1544
Home - 1545
PgUp - 1547
NumLock - 1036
Num/ - 1037
L-Alt - 1796
L-Shift - 1024
G - 771

Это не соответствует ни одной известной таблице сканкодов! Конечно можно найти какие-то закономерности, чатгпт и грок смело предположили что для F13 кодом должно быть 2053. Окей, попытка не пытка, пытаемся скоромить это Armoury Crate.

Именно так выглядит профессиональная обработка ошибок в профессиональном приложении на электроне для настройки железа! После этого пользоваться страницей устройства становится невозможно! Удалить данные UWP программы тот ещё квест, проще всего оказалось просто переустановить Armoury Crate, а для удаления вам ещё потребуется отдельно скачивать деинсталлятор который подотрёт за ней всё, штатные средства винды тут не помогут.
Попытка №3: опенсорс нас спасёт?
К этому моменту становится ясно, что ковыряться в софте ASUS дальше бесполезно. Мы, как обычно, оказываемся в мире без софта. Есть ли что‑то уже готовое? Ну, есть G‑Helper, но он позволяет настраивать только подсветку. Я уже хотел вооружиться Wireshark и полезть реверсить протокол ASUS, как мне в одном чате подсказали, что я — поехавший, но посоветовали вместе с тем проект libratbag, где уже всё отреверсили за меня.
Libratbag — это демон D‑Bus, позволяющий настраивать высококлассные игровые мыши, в том числе от ASUS. Хорошее начало, но использовать его как есть не выйдет — мы же на Винде. Конечно, можно пробросить мышь целиком в виртуалку, но может, сначала изучим, можно ли тут сделать то, что мне надо?
Находим issue как раз с моей проблемой: libratbag крашится при попытке забиндить F13. А в чем причина? Быстро находим в исходниках такую таблицу:
/* key mapping, the index is actual ASUS code */
static const unsigned char ASUS_KEY_MAPPING[] = {
/* 00 */ 0, 0, 0, 0,
/* 04 */ KEY_A, KEY_B, KEY_C, KEY_D,
/* 08 */ KEY_E, KEY_F, KEY_G, KEY_H,
/* 0C */ KEY_I, KEY_J, KEY_K, KEY_L,
/* 0E */ KEY_M, KEY_N, KEY_O, KEY_P,
/* 14 */ KEY_Q, KEY_R, KEY_S, KEY_T,
/* 18 */ KEY_U, KEY_V, KEY_W, KEY_X,
/* 1C */ KEY_Y, KEY_Z, KEY_1, KEY_2,
/* 1E */ KEY_3, KEY_4, KEY_5, KEY_6,
/* 24 */ KEY_7, KEY_8, KEY_9, KEY_0,
/* 28 */ KEY_ENTER, KEY_ESC, KEY_BACKSPACE, KEY_TAB,
/* 2C */ KEY_SPACE, KEY_MINUS, KEY_KPPLUS, 0,
/* 2E */ 0, 0, 0, 0,
/* 34 */ 0, KEY_GRAVE, KEY_EQUAL, 0,
/* 38 */ KEY_SLASH, 0, KEY_F1, KEY_F2,
/* 3C */ KEY_F3, KEY_F4, KEY_F5, KEY_F6,
/* 3E */ KEY_F7, KEY_F8, KEY_F9, KEY_F10,
/* 44 */ KEY_F11, KEY_F12, 0, 0,
/* 48 */ 0, 0, KEY_HOME, KEY_PAGEUP,
/* 4C */ KEY_DELETE, 0, KEY_PAGEDOWN, KEY_RIGHT,
/* 4E */ KEY_LEFT, KEY_DOWN, KEY_UP, 0,
/* 54 */ 0, 0, 0, 0,
/* 58 */ 0, KEY_KP1, KEY_KP2, KEY_KP3,
/* 5C */ KEY_KP4, KEY_KP5, KEY_KP6, KEY_KP7,
/* 5E */ KEY_KP8, KEY_KP9, 0,
};О нет! Неужели это всё, что можно забиндить? Неужели инженеры ASUS захардкодили таблицу допустимых клавиш напрямую в мышь? Хотя это нелогично: час работы хорошего эмбед‑разработчика стоит сильно больше, чем час работы веб‑индусов, которые писали Armoury Crate, а кое‑как делать прошивку для мыши не получится — в конце концов, их и покупают именно за высокую скорость отклика. Это не умная лампочка с линуксом, которая может переключаться за пару секунд. Вряд ли ASUS стали добавлять ещё и проверку на валидность клавиши — усложняет прошивку. Да и секундочку: а почему в таблице нет Ctrl, Shift и Alt, а разве не напоминает нам эта таблица что‑то знакомое? Да это же HID‑сканкоды клавиатуры в чистом виде!

Собственно, Ctrl, Shift и Alt находятся ближе к концу и начинаются с 0xE0. Вообще, мне сильно понравилась эта таблица: вы правда думаете, что F13–F24 — это что-то экзотическое? А как насчёт Keypad XOR? Или, скажем, Keyboard Execute, Currency Unit? У меня уже в голове смутно вырисовывается концепция библейски точной клавиатуры, на которой будут вообще все клавиши из стандарта!

Автор драйвера для ROGатых мышей видимо составил эту таблицу экспериментально и не занимался выяснением, а не стандартная ли это вещь. Отлично, теперь кода мы точно знаем что можно забиндить кнопки в диапазоне от 0x03 до 0xE8, вероятность что в мыши всё же есть какой-то фильтр становится малой. Теперь надо проверить что мы можем это сделать в действительности. Как я уже говорил, libratbag это для линуксов, поэтому на винде нам потребуется написать свою минимальную реализацию драйвера.
Итак, приступим, драйвера конечно надо писать на лучшем для этого языке программирования, а именно python. Что? Это всего лишь пруф оф концепт. Так вот, берем python, к нему либу hidapi и начинаем.
Первым делом надо найти устройство с интересующим нас VID, PID и открыть его. Нам нужен интерфейс 02, т.к. это составное USB устройство:
def open_mi02_interface(vid, pid):
devices = hid.enumerate(vid, pid)
for dev_info in devices:
if dev_info.get('interface_number') == 2:
path = dev_info['path']
print(f"Открываем MI_02: {dev_info.get('product_string', 'Unknown')} (Path: {path})")
device = hid.device()
device.open_path(path)
return device, path
print("MI_02 не найден.")
return None, NoneДалее, прежде чем что-то писать, надо сначала что-то прочитать. Получим текущие бинды мыши:
def asus_get_binding_data(device, path, group=0, max_retries=2):
try:
cmd_bytes = struct.pack('<H', ASUS_CMD_GET_BUTTON_DATA)
request_data = [REPORT_ID] + list(cmd_bytes) + [group] + [0] * (ASUS_PACKET_SIZE - 3)
print(f"Output запрос на {path}: {request_data[:8]}... (65 байт)")
bytes_written = device.write(request_data)
if bytes_written != ASUS_PACKET_SIZE + 1:
return {'error': f'Write failed: {bytes_written} байт'}
print("Output отправлен. Читаем response...")
response = device.read(ASUS_PACKET_SIZE, timeout_ms=1000)
retry_count = 0
while retry_count < max_retries:
if response and len(response) == ASUS_PACKET_SIZE:
resp_code = struct.unpack('<H', bytes(response[0:2]))[0]
print(f"Read code: 0x{resp_code:04x} (ожидаемо 0x{ASUS_CMD_GET_BUTTON_DATA:04x})")
if resp_code == ASUS_CMD_GET_BUTTON_DATA:
print("Valid response.")
break
else:
print(f"Unexpected code 0x{resp_code:04x}, retry {retry_count+1}...")
time.sleep(0.1)
response = device.read(ASUS_PACKET_SIZE, timeout_ms=500)
else:
print(f"Read len={len(response) if response else 0}, retry {retry_count+1}...")
time.sleep(0.1)
response = device.read(ASUS_PACKET_SIZE, timeout_ms=500)
retry_count += 1
if not response or len(response) != ASUS_PACKET_SIZE:
return {'error': f'Read failed after retries (len={len(response) if response else 0})'}
raw_hex = ''.join(f'{b:02x}' for b in response)
print(f"Raw hex: {raw_hex}")
resp_code = struct.unpack('<H', bytes(response[0:2]))[0]
if resp_code == 0xaaff:
return {'error': 'ASUS_STATUS_ERROR'}
# Парсинг (offset=4)
bindings = []
for i in range(ASUS_MAX_NUM_BUTTON):
offset = 4 + i * 2
if offset + 2 > len(response):
break
action = response[offset]
typ = response[offset + 1]
if action == 0xff:
desc = 'Disabled'
elif typ == 0:
desc = HID_KEY_MAPPING.get(action, f'Unknown Key 0x{action:02x}')
else:
desc = ASUS_BUTTON_MAPPING.get(action, f'Unknown Button 0x{action:02x}')
ratbag_idx = BUTTON_MAPPING.get(action, 'N/A') if typ == 1 else 'N/A'
bindings.append({
'internal_index': i,
'action': hex(action),
'type': typ,
'description': desc,
'ratbag_index': ratbag_idx
})
return {
'group': group,
'path': path,
'bindings': bindings,
'raw_hex': raw_hex
}
except Exception as e:
return {'error': f'Ошибка: {str(e)}'}Выполняем, смотрим ответ:
Bindings:
Internal 0: 0xf0 — Mouse Left (button 1) (ratbag 0) [active (type=1)]
Internal 1: 0xf1 — Mouse Right (button 2) (ratbag 1) [active (type=1)]
Internal 2: 0xf2 — Mouse Middle (button 3) (ratbag 2) [active (type=1)]
Internal 3: 0x47 — ScrollLock [active (type=0)]
Internal 4: 0x55 — KP_Asterisk [active (type=0)]
Internal 5: 0xe6 — DPI Cycle Up (ratbag 5) [active (type=1)]
Internal 6: 0x0 — Unknown Key 0x00 [active (type=0)]
Internal 7: 0xe8 — Wheel Up (ratbag 7) [active (type=1)]
Internal 8: 0xe9 — Wheel Down (ratbag 8) [active (type=1)]
Internal 9: 0xe4 — Mouse Back (button 4) (ratbag 3) [active (type=1)]
Internal 10: 0xe5 — Mouse Forward (button 5) (ratbag 4) [active (type=1)]Ура! Мы успешно прочитали что есть что. Теперь попробуем забиндить клавишу Internal 4 на F13. Для этого уже потребуется 2 команды, настройка клавиши и сохранение профиля:
def asus_set_button_action(device, path, src_code, dst_code, asus_type):
try:
cmd_bytes = struct.pack('<H', ASUS_CMD_SET_BUTTON)
request_data = [REPORT_ID] + list(cmd_bytes) + [0, 0, src_code, 1, dst_code, asus_type] + [0] * (ASUS_PACKET_SIZE - 6)
print(f"Output set на {path}: src=0x{src_code:02x}, dst=0x{dst_code:02x}, type={asus_type}")
print(f"Request hex: {''.join(f'{b:02x}' for b in request_data[:10])}...")
bytes_written = device.write(request_data)
if bytes_written != ASUS_PACKET_SIZE + 1:
return {'error': f'Write failed: {bytes_written} байт'}
time.sleep(0.1)
response = device.read(ASUS_PACKET_SIZE, timeout_ms=500)
if response:
resp_code = struct.unpack('<H', bytes(response[0:2]))[0]
print(f"Ack code: 0x{resp_code:04x}")
print("Set отправлен.")
return {'success': True, 'path': path}
except Exception as e:
return {'error': f'Ошибка: {str(e)}'}
def asus_save_profile(device, path):
try:
cmd_bytes = struct.pack('<H', ASUS_CMD_SAVE)
request_data = [REPORT_ID] + list(cmd_bytes) + [0] * (ASUS_PACKET_SIZE - 2)
print(f"Save на {path}: (65 байт)")
bytes_written = device.write(request_data)
if bytes_written != ASUS_PACKET_SIZE + 1:
return {'error': f'Save write failed: {bytes_written} байт'}
time.sleep(0.5)
print("Save отправлен.")
return {'success': True, 'path': path}
except Exception as e:
return {'error': f'Ошибка: {str(e)}'}Выполняем, считываем снова настройки клавиш.
Bindings:
Internal 0: 0xf0 — Mouse Left (button 1) (ratbag 0) [active (type=1)]
Internal 1: 0xf1 — Mouse Right (button 2) (ratbag 1) [active (type=1)]
Internal 2: 0xf2 — Mouse Middle (button 3) (ratbag 2) [active (type=1)]
Internal 3: 0x68 — Unknown Key 0x68 [active (type=0)]
Internal 4: 0x55 — KP_Asterisk [active (type=0)]
Internal 5: 0xe6 — DPI Cycle Up (ratbag 5) [active (type=1)]
Internal 6: 0x0 — Unknown Key 0x00 [active (type=0)]
Internal 7: 0xe8 — Wheel Up (ratbag 7) [active (type=1)]
Internal 8: 0xe9 — Wheel Down (ratbag 8) [active (type=1)]
Internal 9: 0xe4 — Mouse Back (button 4) (ratbag 3) [active (type=1)]
Internal 10: 0xe5 — Mouse Forward (button 5) (ratbag 4) [active (type=1)]Проверим работу где-нибудь, под руку попался OBS Studio.

Отлично! Вот теперь наша мышь стала действительно полезной!
Ложка дёгтя
Вроде всё хорошо, но мне стало интересно, а как ж себя поведет с этим Armoury Crate? При открытии страницы настройки мыши мы получим краш и наконец-то не сможем пользоваться этим отвратительным софтом? Учитывая результат попыток редактирования профиля вручную это было бы ожидаемым исходом, но здесь разработчики Armoury Crate пр��взошли все ожидания. Не произошло ничего. Вообще. Бинды стояли те же которые были установлены в ней в последний раз. Если в меню выбрать синхронизацию профилей, то после этого кнопка 4 снова станет ScrollLock. Вывод: Armoury Crate вообще не умеет читать конфигурацию с устройства.
И нет, на этом беды не заканчиваются. Я выше назвал разработчиков шизофрениками, так вот, здесь мои сомнения в их адекватности только усилились. При перезагрузке ПК бинд снова слетел на ту конфигурацию, которая сохранена в Armoury Crate. Просто гениально! Зачем вообще мыши нужна внутренняя память если софт всё равно затирает конфигурацию в ней каждый раз своей? Впрочем это ожидаемо, потому что найти хорошие отзывы об Armoury Crate достаточно сложно, а вот недовольства им хоть отбавляй, особенно на фоне устаревшей фирменной утилиты Armoury II.
Вместо заключения
Какие-то выводы сделать сложно. Ну кроме того, что, я занимаюсь опять какой-то ерундой и надо было брать Logitech. Насколько я глубоко искал в интернете, я первый кто смог забиндить нестандартную клавишу на кнопки мыши Asus, хотя тему поднимали, интересовались на форумах Asus и реддите.
Код был написан за один вечер с помощью Grok4 и представляет совсем уж proof-of-concept для ровно одной мыши, доступен ниже на github. Возможно кто-то с этими знаниями добавит в g-helper настройку клавиш мыши, возможно этим когда-нибудь даже займусь я.