ACHTUNG : Die Lösung und dieser Artikel ist durch KI entstanden!

Ein offizielles Firmware-Update starten, kurz warten, weiterarbeiten: So war der Plan. Stattdessen war danach die Tastatur meines ASUS ROG Flow Z13 tot. Die Hintergrundbeleuchtung leuchtete noch, aber keine Taste reagierte. Auch das Touchpad war ausgefallen.

Am Ende ließen sich beide wiederherstellen, ohne Windows neu zu installieren oder die Tastatur auszutauschen. Der entscheidende Schritt war, den Controller trotz fehlerhafter USB-Erkennung direkt anzusprechen und in seinen Wiederherstellungsmodus zu versetzen.

Dieser Artikel dokumentiert die Reparatur vom 15. Oktober 2026. Sie gelang mit Unterstützung von AI, die lokale Protokolle und die ASUS-Software untersuchte und die Reparaturschritte ausführte.

Das Detail, das zunächst unterging

Mein Tablet ist ein ROG Flow Z13 GZ302EA. Daran steckt allerdings eine nachträglich angeschaffte deutsche Tastatur. Die ursprüngliche Tastatur hatte ein US-Layout.

Gestartet hatte ich das ASUS-Paket ROGNkeyFWupdate_ASUS_Z_V4.8.0.001_16686. Es enthielt die Firmware FGA01400.GZ302EA.410. Das passte zum Modellnamen des Tablets – aber, wie sich später herausstellte, nicht zur angeschlossenen Tastatur.

Ein Protokoll von vor dem Update zeigte für diese Tastatur FGA01400.GZ301V.407. Ein älteres Protokoll der ursprünglichen Tastatur enthielt dagegen die Kennung GZ302EA.410. Das war der erste konkrete Hinweis darauf, dass Tablet und Ersatz-Tastatur unterschiedlichen Modellgenerationen zugeordnet waren.

Licht an, Eingabe aus

Nach dem Update blieb die Beleuchtung eingeschaltet. Das machte zunächst Hoffnung: Ganz ohne Strom war die Tastatur offensichtlich nicht. Auf Tastendrücke, die Feststelltaste oder die Helligkeitskombinationen reagierte sie jedoch nicht.

Windows meldete beim USB-Gerät einen Startfehler, Code 10. Auch im BIOS funktionierte die Tastatur nicht. Damit sprach wenig dafür, dass lediglich eine Windows-Einstellung oder ein gewöhnlicher Tastaturtreiber die Ursache war.

Neustarts, erneutes Anstecken, ein EC-Reset und eine erneute Geräteerkennung brachten keine Änderung. Das ASUS-Tool meldete entweder einen Fehler oder blieb bei „Aktualisierung läuft“ stehen.

Eine Fehlermeldung führte zunächst auf eine Nebenbaustelle

Beim Start des Pakets erschien:

Der Befehl "wmic" ist entweder falsch geschrieben oder
konnte nicht gefunden werden.
cannot read baseboard product

Der Aufruf, mit dem das Installationsskript das Mainboard-Modell ermitteln wollte, funktionierte auf meinem Windows nicht. Nach Bereitstellung der Microsoft-Komponente ließ sich das Modell wieder auslesen. Die Tastatur blieb trotzdem ausgefallen.

Auch der ASUS HID Control Service wurde überprüft und installiert. Ein älteres ASUS-Hilfsprogramm für den EC-Zugriff scheiterte wiederum daran, dass Windows dessen Treiber d.sys nicht laden konnte.

Diese Probleme erklärten einzelne Fehlversuche des Reparaturwerkzeugs. Sie erklärten noch nicht, wie die Tastatur selbst wiederherzustellen war.

Was das erste Update tatsächlich getan hatte

Besonders aufschlussreich war das Protokoll des ursprünglichen Updates. Darin standen erfolgreiche Schritte für Löschen, Schreiben und Prüfen, gefolgt von:

Complete
Step1 Flash Success

Danach hing der Ablauf beim Zurücksetzen des Controllers.

Die zunächst naheliegende Beschreibung „Update mitten im Schreiben abgebrochen“ war deshalb ungenau. Laut Protokoll hatte das Tool die Firmware geschrieben und geprüft. Der anschließende Rückweg in einen funktionierenden Tastaturbetrieb gelang nicht.

Bei der USB-Diagnose zeigte sich außerdem eine fehlerhafte Konfigurationsbeschreibung: Die gemeldeten Schnittstellen passten nicht zusammen. Das erklärte, warum die normalen Treiber und das ASUS-Updatefenster nicht mehr zuverlässig an das Gerät herankamen.

Der Durchbruch: Erst WSL-Diagnose, dann direkter USB/IP-Zugriff

Ab hier wurde aus der Fehlersuche eine Untersuchung des USB-Protokolls. Entscheidend war die Frage: Antwortet der Controller noch auf Steuerbefehle, obwohl Windows keine funktionierende Tastatur mehr daraus machen kann?

Die Umgebung bestand aus Windows 11, bereits vorhandenem WSL 2 mit Ubuntu 22.04, Python 3 und libusb. Der verwendete WSL-Kernel meldete ​6.18.33.2-microsoft-standard-WSL2​. Dazu wurde das offizielle usbipd-win 5.3.0 installiert.

Dabei gab es drei klar getrennte Phasen:

Phase Umgebung Zweck
Diagnose Ubuntu unter WSL 2 USB-Deskriptoren und Fehler beim Konfigurieren untersuchen
Recovery-Einstieg Python unter Windows → lokaler usbipd-Dienst ASUS-Steuerbefehle trotz defekter USB-Konfiguration senden
Sicherung und Firmware-Reparatur Windows HID-API und originale ASUS-DLL Flash lesen, passende Firmware schreiben, prüfen und zurücksetzen

Die folgenden Befehle und Skripte dokumentieren genau diesen Fall. Bus-ID, HID-Pfad, Firmwarekennung und Sicherungs-Hash sind gerätespezifisch. Insbesondere die Recovery- und Flash-Skripte sind keine Programme, die ich nach der erfolgreichen Reparatur nochmals starten würde.

Die Tastatur gezielt an WSL durchreichen

Zunächst ließ sich mit PowerShell die vorhandene WSL-Umgebung und die USB-Geräteliste prüfen:

wsl --list --verbose
usbipd list

Die betroffene Tastatur hing bei mir an Bus-ID 1-4, mit VID:PID 0b05:1a30. Daneben gab es ein weiteres ASUS-Gerät mit ​0b05:18c6​, das nicht die ausgefallene Tastatur war. Eine externe Logitech-Tastatur blieb ebenfalls unangetastet.

Für die Weitergabe wurde gezielt diese Bus-ID gebunden. Im Verlauf war eine erzwungene Bindung nötig; die Bindungsbefehle benötigen Administratorrechte:

usbipd bind --busid 1-4
# Im weiteren Diagnoseverlauf:
usbipd bind --busid 1-4 --force

usbipd attach --wsl --busid 1-4

Das normale Verfahren – Gerät unter Windows freigeben, an WSL anhängen und dort untersuchen – ist in der WSL-Dokumentation von usbipd-win beschrieben.

Unter Ubuntu standen dann die üblichen Diagnosewerkzeuge zur Verfügung:

uname -r
lsusb
sudo dmesg | tail -n 80

Das Ergebnis war aufschlussreicher als der Windows-Code 10: Linux meldete sinngemäß, dass Konfiguration 1 fünf Interfaces ankündigte, aber nur vier unterschiedliche enthielt. Interface 3 mit Alternate Setting 0 erschien doppelt. Der normale Konfigurationsversuch scheiterte; ein nutzbares HID-Gerät entstand nicht.

In der ausgelesenen Konfigurationsbeschreibung war das unmittelbar sichtbar. Der Anfang lautete:

09 02 8d 00 05 01 00 c0 32
            ^^
            bNumInterfaces = 5

Die weiteren Deskriptoren enthielten die Interface-Nummern 0, 1, 2, 3 und nochmals 3. Der letzte Interface-Deskriptor kündigte außerdem zwei Endpunkte an, während innerhalb der gemeldeten Gesamtlänge nur einer folgte.

Das war keine Reparatur durch einen anderen Betriebssystemtreiber. WSL machte den Fehler in den vom Gerät gelieferten Daten sichtbar.

Automatische Konfiguration zeitweise verhindern

Damit Linux das Gerät für die Diagnose nicht sofort konfigurierte, wurde vor einem erneuten Anhängen der Autorisierungsstandard des betreffenden virtuellen USB-Busses vorübergehend geändert:

# Als root in WSL, bevor das Gerät erneut angehängt wird:
echo 0 > /sys/bus/usb/devices/usb1/authorized_default

Mit dieser Einstellung blieb das Gerät für grundlegende Untersuchungen erreichbar, ohne dass Linux seine Interfaces automatisch in Betrieb nahm. Die Bedeutung der USB-Autorisierung beschreibt die Linux-Kernel-Dokumentation.

Über libusb ließen sich nun Deskriptoren lesen. HID-Klassenanfragen an die nicht erfolgreich konfigurierte Schnittstelle waren aber weiterhin ein Problem. Genau hier lag die Grenze des normalen WSL/libusb-Wegs.

Der erfolgreiche Client lief unter Windows

Die Lösung bestand darin, die relevanten Control Transfers direkt an den lokalen usbipd-Dienst auf 127.0.0.1:3240 zu senden. Dafür entstand ein kleiner USB/IP-Client in Python. WSL war für diese Phase nicht mehr der Client. Das Gerät musste freigegeben, aber für den direkten Zugriff nicht gleichzeitig von WSL importiert sein.

Eine bestehende WSL-Anbindung lässt sich vorher lösen:

usbipd detach --busid 1-4

Der Python-Client importierte genau Bus-ID ​1-4​, kontrollierte die Kennung ​0b05:1a30​ und sendete USB-Steuertransfers über Endpoint 0. Er umging damit den fehlgeschlagenen normalen HID-Konfigurationsweg. Er änderte keine USB-Deskriptoren und reparierte sie auch nicht im Arbeitsspeicher.

Das Paketformat stammt aus der USB/IP-Protokolldokumentation des Linux-Kernels. Der USB/IP-Header verwendet Network Byte Order; das darin eingebettete USB-Setup-Paket dagegen Little Endian.

Für die ASUS-Kommunikation waren diese Werte entscheidend:

Feld Wert
Interface 0
SET_REPORT bmRequestType ​0x21​, bRequest ​0x09​
GET_REPORT bmRequestType ​0xA1​, bRequest ​0x01​
wValue ​0x035A​: Feature Report, Report-ID ​0x5A​
Länge des gesendeten Reports 64 Byte
Tatsächlich empfangene Nutzdaten bei diesen Abfragen 32 Byte

Der erste erfolgreiche Handshake war:

handshake = b"\x5aASUS Tech.Inc.\x00".ljust(64, b"\x00")

Die Länge war hier entscheidend: Der erfolgreiche Versuch verwendete einen auf 64 Byte aufgefüllten Report. Die vorherige Variante mit nur 32 Byte hatte nicht zum erfolgreichen Handshake geführt.

Die Versionsabfrage begann mit:

5A 05 03 31 00 20

Die Antwort enthielt tatsächlich ​FGA01400.GZ302EA.410​. Damit war die falsche Firmware direkt am Controller bestätigt.

Die Recovery-Sequenz aus der ASUS-Bibliothek rekonstruieren

Im ASUS-Paket lagen sowohl ein .NET-Programm als auch die native ​OEM_Library.dll​. Die Untersuchung mit ILDasm und Dumpbin zeigte, welche Bibliotheksfunktionen das Updateprogramm aufrief und welche Reports die native Bibliothek erzeugte.

Für das grundlegende Vorgehen reichten die klassischen Entwicklerwerkzeuge:

# In einer passenden Entwicklerumgebung:
ildasm.exe ROGKBFWUpdateTool.exe /text /out=updater.il
dumpbin.exe /exports OEM_Library.dll
dumpbin.exe /disasm OEM_Library.dll > OEM_Library.disasm.txt

Die ASUS-Konstanten ließen sich zusätzlich mit dem ASUS-HID-Protokoll im fwupd-Quellcode abgleichen. Der tatsächlich verwendete Ablauf wurde an der lokalen ASUS-Bibliothek geprüft; die fwupd-Implementierung wurde nicht einfach als fertiges Reparaturprogramm aufgerufen.

Die Recovery-Sequenz war:

  1. ASUS-Handshake und Versionsprüfung.
  2. Pre-Update-Kommando ​5A 05 00 20 00 20​.
  3. Register mit Adressbytes ​09 20​ lesen; den gelesenen Wert mit ​0x0F​ verodern und zurückschreiben.
  4. Register mit Adressbytes ​0B 26​ lesen; den gelesenen Wert mit ​0x02​ verodern und zurückschreiben.
  5. ROM-Wechsel mit ​5A E0 32 93 00 00​ auslösen.

Die Register-Lesebefehle beginnen mit ​0x07​, die Schreibbefehle mit ​0x08​. Bei beiden war die Datenlänge ein Byte. Das gelesene Registerbyte stand in der Antwort an Offset 6. Die Bitmasken wurden auf den vorhandenen Wert angewandt; es wurden keine ungeprüften Ersatzwerte eingesetzt.

Mein Protokoll zeigte:

Register 0920 original= 0x0 recovery= 0xf
Register 0b26 original= 0x2 recovery= 0x2

Dieses Skript führte den erfolgreichen Wechsel aus. Die darin enthaltene Versionsprüfung bezieht sich absichtlich auf den damaligen Fehlerzustand:

"""ASUS OEM recovery-entry sequence. No flash erase/program commands."""
import socket
import struct
import time

def receive(sock, length):
    data = b''
    while len(data) < length:
        part = sock.recv(length - len(data))
        if not part:
            raise RuntimeError('Local usbipd closed connection')
        data += part
    return data

with socket.create_connection(('127.0.0.1', 3240), timeout=8) as sock:
    sock.sendall(struct.pack('!HHI32s', 0x0111, 0x8003, 0, b'1-4'))
    version, operation, status = struct.unpack('!HHI', receive(sock, 8))
    if status or operation != 3:
        raise RuntimeError(f'Import failed: {status}')
    device = receive(sock, 312)
    bus, number = struct.unpack_from('!II', device, 288)
    if struct.unpack_from('!HH', device, 300) != (0x0b05, 0x1a30):
        raise RuntimeError('Wrong device')
    sequence = 0

    def transfer(request_type, request, interface, payload=None):
        global sequence
        sequence += 1
        length = len(payload) if payload is not None else 64
        setup = struct.pack('<BBHHH', request_type, request, 0x035A, interface, length)
        direction = int(payload is None)
        header = struct.pack('!IIIII', 1, sequence, (bus << 16) | number, direction, 0)
        header += struct.pack('!IIIII', 0, length, 0, 0xFFFFFFFF, 0) + setup
        sock.sendall(header + (payload or b''))
        reply = receive(sock, 48)
        command, reply_sequence = struct.unpack_from('!II', reply)
        result, actual_length = struct.unpack_from('!iI', reply, 20)
        if command != 3 or reply_sequence != sequence or actual_length > 4096:
            raise RuntimeError('Unexpected response')
        data = receive(sock, actual_length) if direction and actual_length else b''
        print('interface=', interface, 'request=', request, 'status=', result, 'data=', repr(data), flush=True)
        return result, data

    def command(raw, read=True):
        result, _ = transfer(0x21, 9, 0, raw.ljust(64, b'\x00'))
        if result:
            raise RuntimeError(f'Control write failed: {result}')
        time.sleep(0.1)
        if not read:
            return b''
        result, response = transfer(0xA1, 1, 0)
        if result or len(response) < 7:
            raise RuntimeError('Control read failed')
        return response

    response = command(b'\x5aASUS Tech.Inc.\x00')
    if b'ASUS Tech.Inc.' not in response:
        raise RuntimeError('Handshake failed')
    response = command(bytes.fromhex('5a0503310020'))
    if b'FGA01400.GZ302EA.410' not in response:
        raise RuntimeError('Unexpected firmware; refusing recovery entry')
    print('Confirmed target firmware. Beginning OEM recovery entry.', flush=True)
    command(bytes.fromhex('5a0500200020'))
    for address, mask in ((bytes.fromhex('0920'), 0x0f), (bytes.fromhex('0b26'), 0x02)):
        raw = b'\x5a\x07' + address + b'\x00\x01'
        response = command(raw)
        if response[:6] != raw:
            raise RuntimeError('Register response does not match request')
        original = response[6]
        print('Register', address.hex(), 'original=', hex(original), 'recovery=', hex(original | mask), flush=True)
        command(b'\x5a\x08' + address + b'\x00\x01' + bytes([original | mask]), read=False)
        time.sleep(0.05)
    command(bytes.fromhex('5ae032930000'), read=False)
    print('OEM ROM switch submitted. No flash erase or programming performed.', flush=True)

Für diese Phase wurden Register verändert und der ROM-Wechsel angefordert. Es wurden noch keine Flash-Lösch- oder Flash-Programmierbefehle gesendet.

Unmittelbar danach verschwand ​0b05:1a30​ und an derselben physischen Verbindung erschien 048d:89db. Windows erkannte sowohl das USB-Eingabegerät als auch dessen herstellerspezifische HID-Schnittstelle mit Status OK. Der Controller war jetzt im zugänglichen Wiederherstellungsmodus.

Vor dem Zurückschreiben: Den gesamten Flash sichern

Ab diesem Punkt war der Umweg über USB/IP für die Sicherung nicht mehr nötig. Der Bootloader stellte eine funktionierende Windows-HID-Schnittstelle bereit.

Ausgelesen wurde über ​HidD_SetFeature​ und ​HidD_GetFeature​. Ein Read-Flash-Report bestand aus:

Byte 0:      00             HID-Report-ID
Byte 1:      D1             Flash lesen
Byte 2–4:    Adresse        24 Bit, Little Endian
Byte 5:      Länge          maximal 58 Byte
Byte 6–63:   Datenbereich   in der Antwort

Das Skript las insgesamt 262.144 Byte in Abschnitten bis 58 Byte. Vor jedem Anhängen an die Sicherung prüfte es, ob die Antwort die angeforderte Adresse und Länge zurückmeldete.

Der folgende HID-Pfad gehört zur damaligen Windows-Geräteinstanz. Auf einem anderen Rechner muss er neu ermittelt werden. Eine bereits vorhandene Sicherungsdatei überschreibt das Skript nicht.

"""Read-only ITE bootloader flash backup using ASUS D1 command."""
import ctypes as c
from ctypes import wintypes as w
from pathlib import Path
import hashlib
import time

path = r'\\?\HID#VID_048D&PID_89DB#7&2d910e5f&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}'
k = c.WinDLL('kernel32', use_last_error=True)
h = c.WinDLL('hid', use_last_error=True)
k.CreateFileW.argtypes = [w.LPCWSTR,w.DWORD,w.DWORD,c.c_void_p,w.DWORD,w.DWORD,w.HANDLE]
k.CreateFileW.restype = w.HANDLE
k.CloseHandle.argtypes = [w.HANDLE]
for f in (h.HidD_SetFeature,h.HidD_GetFeature):
    f.argtypes = [w.HANDLE,c.c_void_p,w.ULONG]
    f.restype = w.BOOLEAN
handle = k.CreateFileW(path, 0xC0000000, 3, None, 3, 0, None)
if handle == c.c_void_p(-1).value:
    raise c.WinError(c.get_last_error())
try:
    result = bytearray()
    for offset in range(0, 262144, 58):
        size = min(58, 262144-offset)
        packet = b'\x00\xd1' + offset.to_bytes(3,'little') + bytes([size])
        buffer = c.create_string_buffer(packet.ljust(64,b'\x00'),64)
        if not h.HidD_SetFeature(handle, buffer,64):
            raise c.WinError(c.get_last_error())
        buffer = c.create_string_buffer(64)
        if not h.HidD_GetFeature(handle,buffer,64):
            raise c.WinError(c.get_last_error())
        data = buffer.raw
        if offset == 0:
            print('First read:',data.hex(),flush=True)
        if data[1:6] != packet[1:6]:
            raise RuntimeError(f'Unexpected read response at {offset:#x}: {data.hex()}')
        result.extend(data[6:6+size])
        if offset % (58*256) == 0:
            print(f'Read {len(result)}/262144',flush=True)
    output = Path(__file__).with_name('keyboard-flash-backup.bin')
    if output.exists():
        raise RuntimeError('Backup already exists; refusing overwrite')
    output.write_bytes(result)
    print('BACKUP SAVED',output,'SHA256',hashlib.sha256(result).hexdigest(),flush=True)
finally:
    k.CloseHandle(handle)

Die vollständige Sicherung erhielt diesen SHA-256-Hash:

8f5e2289dafdde61c9a0387e67ef015f2ea9ba5b485dc42f6c36e654d15bb1c8

In der Sicherung fand sich die laufende GZ302EA-Firmware bei ​0x2010​. Bei 0x3C000 war dagegen die vorherige Kennung FGA01400.GZ301V.407 erhalten geblieben.

Der Befund lässt sich an der gespeicherten Datei prüfen, ohne erneut auf die Tastatur zuzugreifen:

from pathlib import Path

flash = Path("keyboard-flash-backup.bin").read_bytes()
assert len(flash) == 262144

for address in (0x2010, 0x3C000):
    text = flash[address:address + 64].split(b"\x00", 1)[0]
    print(hex(address), repr(text))

Zusammen mit dem Updateprotokoll von vor dem Ausfall war das der belastbare Nachweis für die falsche Zuordnung. Die passende Modellkennung wurde nicht aus dem aktuellen, bereits falsch geflashten Versionsstring abgeleitet.

Die Wiederherstellung mit der originalen ASUS-Flash-Engine

Für die Reparatur wurde das offizielle ASUS-Paket ROGNkeyFWupdate_ASUS_Z_V3.7.0.001_15122_6.exe für die GZ301V-Reihe heruntergeladen, seine ASUS-Signatur geprüft und sein Inhalt entpackt. Darin lagen unter anderem:

ROGKBFWUpdateTool/
├── OEM_Library.dll
├── ROGKBFWUpdateTool.exe
├── RW_EC_MEM.exe
└── fw/
    ├── FGA01400.GZ301V.010.bin
    └── FGA01400.GZ301V.410.bin

Verwendet wurde FGA01400.GZ301V.410.bin, zusammen mit der originalen 64-Bit-OEM_Library.dll aus demselben Paket. Das ältere EC-Hilfsprogramm mit dem nicht ladbaren Treiber war für diesen erfolgreichen Weg nicht nötig.

Die Windows-Python-Installation war ebenfalls 64 Bit. Mit ​ctypes.CDLL​ ließen sich die C-Funktionen aufrufen, die auch das ASUS-Updateprogramm verwendet. ​OEM_GetITEDevice(4)​ bestätigte zuerst, dass die Bibliothek den vorhandenen Bootloader erkannte.

Vor dem Start prüfte das Skript zusätzlich die unveränderte Sicherung anhand ihres Hashs, die erhaltene GZ301V.407-Kennung sowie Größe und Kennung der ausgewählten GZ301V.410-Datei.

Ein wichtiges Detail trat beim ersten Aufruf auf: Die Rückkehr von OEM_MainUpdateTask bedeutete noch nicht, dass die Reparatur fertig war. Die Funktion lieferte bereits die Dateigröße ​262144​ zurück, während die ASUS-Engine noch Status 1 meldete. Das erste kurze Skript beendete sich deshalb zu früh. In der korrigierten Fassung blieb der Prozess aktiv und überwachte den ASUS-Ablauf bis zum bestätigten Abschluss.

Dies war die erfolgreich verwendete Fassung:

"""Run original ASUS GZ301 flash engine against official GZ301V.410 image."""
import ctypes as c
from pathlib import Path
import hashlib
import threading
import time

base = Path(__file__).resolve().parent
backup = (base / 'keyboard-flash-backup.bin').read_bytes()
if hashlib.sha256(backup).hexdigest() != '8f5e2289dafdde61c9a0387e67ef015f2ea9ba5b485dc42f6c36e654d15bb1c8':
    raise RuntimeError('Verified backup missing or changed')
if not backup[0x3c000:].startswith(b'FGA01400.GZ301V.407'):
    raise RuntimeError('Original hardware identity mismatch')
root = base / 'gz301-original' / 'ROGKBFWUpdateTool'
firmware = root / 'fw' / 'FGA01400.GZ301V.410.bin'
image = firmware.read_bytes()
if len(image) != 262144 or b'FGA01400.GZ301V.410' not in image[:0x3000]:
    raise RuntimeError('Wrong firmware image')
dll = c.CDLL(str(root / 'OEM_Library.dll'))
dll.OEM_GetITEDevice.argtypes = [c.c_int]
dll.OEM_GetITEDevice.restype = c.c_bool
dll.OEM_MainUpdateTask.argtypes = [c.c_char_p]
dll.OEM_MainUpdateTask.restype = c.c_int
dll.OEM_FlashStatus.restype = c.c_int
dll.OEM_FlashProgress.argtypes = [c.c_int]
dll.OEM_FlashProgress.restype = c.c_int
dll.OEM_FlashReset.restype = None
if not dll.OEM_GetITEDevice(4):
    raise RuntimeError('ASUS engine cannot find bootloader')
print('Official GZ301 engine detected bootloader. Verified backup and GZ301V.410 image.',flush=True)
results = []
def flash():
    results.append(dll.OEM_MainUpdateTask(str(firmware).encode('mbcs')))
worker = threading.Thread(target=flash)
worker.start()
previous = None
deadline = time.monotonic() + 600
while time.monotonic() < deadline:
    status = dll.OEM_FlashStatus()
    progress = dll.OEM_FlashProgress(status)
    state = (status,progress)
    if state != previous:
        print('ASUS status/progress:',state,flush=True)
        previous = state
    if status == 128:
        break
    if status not in (0, 1, 2, 3, 4):
        raise RuntimeError(f'ASUS flash engine error {status:#x}; preserving recovery mode')
    time.sleep(0.5)
worker.join()
status = dll.OEM_FlashStatus()
print('ASUS engine finished. Return=',results,'status=',status,'progress=',dll.OEM_FlashProgress(status),flush=True)
if status != 128:
    raise RuntimeError('ASUS did not report successful verification; leaving bootloader available')
print('ASUS flash verification successful. Sending official reset.',flush=True)
dll.OEM_FlashReset()
print('Official reset completed.',flush=True)

Die Pfade darin beziehen sich auf die bei der Reparatur angelegte Verzeichnisstruktur. Die eigentliche Lösch-, Schreib- und Prüflogik führte die ASUS-Bibliothek aus. Es wurde kein selbst entwickelter Algorithmus zum pauschalen Überschreiben aller 256 KiB verwendet.

Während des erfolgreichen Durchlaufs wechselte die Engine durch die Statuswerte 1, 2, 3 und 4. Das Ende war eindeutig:

ASUS status/progress: (128, 100)
ASUS engine finished. Return= [262144] status= 128 progress= 100
ASUS flash verification successful. Sending official reset.
Official reset completed.

Erst nach dieser Erfolgsmeldung wurde ​OEM_FlashReset()​ aufgerufen.

Diagnosebindung entfernen und Windows wieder übernehmen lassen

Nach dem Reset erschien die normale Gerätekennung 0b05:1a30 wieder. Sie hing zunächst noch am temporären USB/IP-Treiber, mit dem Zustand Shared (forced). Auch nach einem erfolgreichen Flash musste diese Bindung deshalb entfernt werden.

In einer administrativen PowerShell:

usbipd unbind --busid 1-4
usbipd list

Get-PnpDevice -PresentOnly |
    Where-Object InstanceId -Match 'VID_0B05&PID_1A30' |
    Format-Table Status, Class, FriendlyName, InstanceId -AutoSize

Für die vorübergehende WSL-Einstellung ist der normale Autorisierungswert wieder ​1​, sofern der betreffende virtuelle Bus noch existiert:

# In WSL als root, falls die Datei noch vorhanden ist:
if [ -f /sys/bus/usb/devices/usb1/authorized_default ]; then
    echo 1 > /sys/bus/usb/devices/usb1/authorized_default
fi

Beim Aufräumen dieser Sitzung war der temporäre WSL-Bus bereits nicht mehr vorhanden.

Windows meldete nach dem Entfernen der Bindung wieder Not shared und erkannte die zugehörigen Funktionen fehlerfrei, darunter:

OK  Keyboard  HID-Tastatur
OK  HIDClass  ASUS Precision Touchpad
OK  Mouse     HID-konforme Maus

Anschließend kam der entscheidende praktische Test: Tastatur und Touchpad funktionierten wieder.

Der Weg war damit vollständig: WSL machte die fehlerhafte USB-Konfiguration sichtbar, der direkte lokale USB/IP-Client erreichte den ASUS-Steuerkanal, die geprüfte OEM-Sequenz öffnete den Bootloader, und die originale ASUS-Bibliothek stellte die passende Firmware wieder her.

Was ich daraus mitnehme

Bei einem Gerät mit abnehmbarer und austauschbarer Tastatur reicht der Modellname des Tablets offenbar nicht als verlässliche Grundlage für die Auswahl der Tastatur-Firmware. In meinem Fall unterschieden sich die Kennungen des Tablets und der nachgekauften Tastatur.

Ebenso hilfreich war, die einzelnen Ebenen auseinanderzuhalten: Ein nicht startender Windows-Treiber, eine fehlerhafte USB-Konfiguration und ein vollständig unerreichbarer Controller sind unterschiedliche Zustände. Hier war der normale Betrieb kaputt, der Steuerkanal aber noch ansprechbar.

Die beschriebene Reparatur ist deshalb ein dokumentierter Einzelfall und keine universelle Flash-Anleitung. Gerätekennung, vorhandene Firmware, Sicherung und passende ASUS-Datei waren entscheidend. Insbesondere werde ich das GZ302EA-Tastaturupdate auf dieser Tastatur nicht erneut ausführen.

Die offiziellen Downloadbereiche sind bei ASUS für das ROG Flow Z13 GZ301VV und das ROG Flow Z13 GZ302EA zu finden. Welche Datei passt, muss anhand der tatsächlich angeschlossenen Tastatur geprüft werden.