Разведка боем: Скрытая карта режимов 8BitDo Zero v1 и прошивка по воздуху через Linux
Короткая заметка для тех, кто ковыряет ретро-железо. Стояла задача: использовать микро-геймпад 8BitDo Zero 1-го поколения (Bluetooth 2.1) для специфического HID-ввода, изменив раскладку “намертво” внутри устройства.
Официальный софт под Windows давно заброшен, поэтому ремап делался по воздуху через Linux (RFCOMM). В процессе удалось полностью разобрать структуру конфигурационного пакета, отправляемого контроллеру.
Структура KEYMAP-пакета
При отправке команды +KEYMAP; геймпад принимает массив, где блоки идут друг за другом в зависимости от режима удержания кнопок при включении (по 12 байт на режим в аппаратном порядке: Вверх/Вниз/Влево/Вправо/Select/Start/B/A/X/Y/L/R):
START — Режим стандартного джойстика (Usage Page 0x09)
R + START — Режим PC-джойстика
START + B — Стандартная клавиатура (Usage Page 0x07), выдает стрелки (52 51 50 4f) и Enter/Esc (28/29).
START + B + R — Расширенная клавиатура.
START + A — Режим iCade (раздельные HEX-коды на Press и Release).
START + X — Скрытый сервисный/Emu-Touch режим прошивки (0F 0E 0D 0C...).
Shutter (Селфи-пульт) — Комбинированный режим. Позволяет сочетать стандартные кнопки клавиатуры (Page 0x07) и мультимедиа-коды Consumer Page 0x0C (Громкость: 80/81, Яркость: 6F/70, Снимок: 65).
Как шить (Пайплайн в 3 окна)
В первом окне терминала поднимаем RFCOMM-канал на 1-м L2CAP канале геймпада: sudo rfcomm connect 0 [MAC_АДРЕС] 1
Во втором окне правим HEX-массив и инжектируем его Python-скриптом напрямую в /dev/rfcomm0 (скрипт прикреплен ниже). Контроллер должен вернуть успешную сигнатуру 0e0100a6.
В третьем окне контролируем прохождение пакетов через sudo btmon.
В конце пакета обязательно должны идти маркеры валидации aa 55 aa 55, иначе чип отвергнет прошивку. Мне удалось успешно собрать кастомный Shutter-режим, внедрив туда нужные клавиатурные скан-коды (30 и 40) рядом с системными. Код скрипта прилагаю.
import serial
import time
# Настройки порта
PORT = '/dev/rfcomm0'
# Ваши данные для записи (с 07 07 07 07)
DATA = b'\x00\xa6WR+KEYMAP;\x00\x00\x64\x00\x00\x00\x00\x00\x1a\x1b\x11\x10\x13\x14\x16\x17\x01\x01\x01\x01\x1a\x1b\x11\x10\x13\x14\x16\x17\x52\x51\x50\x4f\x28\x29\x4d\x4e\x4a\x4b\x0e\x10\x13\x14\x15\x04\x1b\x1c\x18\x05\x16\x17\x19\x1a\x1a\x1b\x04\x07\x0f\x12\x0b\x18\x0d\x1c\x0e\x0c\x08\x1d\x14\x06\x19\x0a\x15\x09\x11\x17\x13\x10\x0f\x0e\x0d\x0c\x09\x00\x08\x07\x05\x06\x04\x03\x52\x51\x50\x4f\x28\x29\x4d\x4e\x4a\x4b\xea\xe9\xaa\x55\xaa\x55'
try:
print(f"--- Попытка открыть {PORT} ---")
# Открываем порт с таймаутом 2 секунды
with serial.Serial(PORT, baudrate=115200, timeout=2) as ser:
print("Порт открыт. Отправка данных...")
ser.write(DATA)
ser.flush() # Гарантируем отправку всех байт
print("Данные отправлены. Ждем подтверждения от геймпада...")
response = ser.read(16) # Читаем ответ
if response:
print(f"ОТВЕТ ПОЛУЧЕН (HEX): {response.hex()}")
if "0e0100a6" in response.hex():
print("УСПЕХ: Геймпад подтвердил запись ремапа!")
else:
print("ВНИМАНИЕ: Ответ получен, но он не похож на стандартное подтверждение.")
else:
print("ОШИБКА: Геймпад ничего не ответил (таймаут).")
except Exception as e:
print(f"КРИТИЧЕСКАЯ ОШИБКА: {e}")
![]()