initial aubox skeleton: web-UI, kirin DLOAD, firmware library
- FastAPI Web-UI auf 127.0.0.1:8080 mit Geräte-Live-Erkennung, sandboxed File-Browser, Firmware-Library (SQLite + Auto-Identifikation) - Huawei update.app Parser: extrahiert Hardware-ID, Section-Layout, BOOT/SYSTEM-Vorhandensein direkt aus den Headern - Kirin Download-Mode: hisi-idt-Protokoll-Implementation gegen pyusb - USB-Erkennung für Huawei (DLOAD/Fastboot-D), Google, MediaTek, Qualcomm EDL - Huawei-P10-Lite-Workflow (eRecovery + Testpoint-DLOAD-Pfade) - Docker-Compose mit USB-Passthrough (Major 189) für Re-Enumeration - udev-Regeln + Setup-Script für Debian/Ubuntu/Pi-OS Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d0386b3c53
commit
fb3534553b
|
|
@ -0,0 +1,8 @@
|
||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
loaders/
|
||||||
|
.git/
|
||||||
|
docs/
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Container für aubox. Funktioniert auf x86_64 und arm64 (Pi 4) gleichermaßen.
|
||||||
|
#
|
||||||
|
# Bauen:
|
||||||
|
# docker build -t aubox .
|
||||||
|
#
|
||||||
|
# Start mit USB-Passthrough siehe scripts/run-docker.sh.
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
python3 \
|
||||||
|
python3-pip \
|
||||||
|
android-tools-adb \
|
||||||
|
android-tools-fastboot \
|
||||||
|
libusb-1.0-0 \
|
||||||
|
usbutils \
|
||||||
|
ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt ./
|
||||||
|
RUN pip3 install --no-cache-dir --break-system-packages -r requirements.txt
|
||||||
|
|
||||||
|
COPY aubox/ ./aubox/
|
||||||
|
|
||||||
|
# loaders/ und firmware/ werden zur Laufzeit als Volumes gemountet — nicht ins
|
||||||
|
# Image kopieren. Damit sind Hersteller-Binaries nie im Image-Layer.
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
ENTRYPOINT ["python3", "-m", "aubox"]
|
||||||
|
CMD ["web", "--host", "0.0.0.0", "--port", "8080"]
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
# android-unlock-and-more-box
|
||||||
|
|
||||||
|
Open-Source-Werkzeugkasten für Android-Recovery (FRP, Bootloader-Tasks, NV-Reparatur).
|
||||||
|
Läuft auf jedem Linux-PC oder Pi 4 — der PC ist in der Regel die bessere Wahl
|
||||||
|
(schneller bei CRC, schneller bei Firmware-Dumps, mehr RAM). Pi 4 ist nett
|
||||||
|
wenn man ein dediziertes Werkstatt-Gerät will. Kein Voodoo, keine Wunder —
|
||||||
|
Aggregation öffentlicher Protokolle und Workflows.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Pre-Alpha. Aktueller Fokus: **Huawei P10 Lite (WAS-LX1, Kirin 658)** ohne
|
||||||
|
funktionierenden Bootloader-Unlock und ohne IMEI/NV.
|
||||||
|
|
||||||
|
Siehe [docs/huawei-p10-lite.md](docs/huawei-p10-lite.md).
|
||||||
|
|
||||||
|
## Was das hier ist — und was nicht
|
||||||
|
|
||||||
|
**Ist:**
|
||||||
|
- Lokale Web-UI (FastAPI, kein Build-Step, kein Node)
|
||||||
|
- Geräte-Erkennung über USB-VID:PID, live im Browser
|
||||||
|
- Datei-Browser sandboxed auf die Firmware-Library
|
||||||
|
- SQLite-Index der Firmware-Library mit Auto-Identifikation
|
||||||
|
(Huawei `update.app` voll, MTK/Samsung als Stub)
|
||||||
|
- Wrapper um `adb`, `fastboot`, `pyusb`
|
||||||
|
- Implementierung des Kirin-Download-Mode-Protokolls (hisi-idt)
|
||||||
|
- Geräte-spezifische Workflows mit klaren Schritt-für-Schritt-Anweisungen
|
||||||
|
- Läuft auf jedem Linux-System: PC, Pi 4, Container
|
||||||
|
|
||||||
|
**Ist nicht:**
|
||||||
|
- Ersatz für Octopus/UnlockTool/Z3X — die Datenbanken und Loader-Sammlungen
|
||||||
|
dort sind das eigentliche Asset. Hier baust du dir das selbst auf.
|
||||||
|
- Magic-Exploit-Sammlung. Wenn ein SoC keinen öffentlichen BROM-Bypass hat
|
||||||
|
(z.B. Kirin 658), braucht es leider Hersteller-Loader, die du selbst
|
||||||
|
beschaffen musst (siehe `loaders/README.md`).
|
||||||
|
|
||||||
|
## Zwei Wege zum Start
|
||||||
|
|
||||||
|
### A) Nativ (einfachste Variante)
|
||||||
|
|
||||||
|
Funktioniert auf Debian/Ubuntu/Mint/Pi OS gleichermaßen.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash scripts/setup-linux.sh # einmalig: Pakete + udev + venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
python -m aubox detect
|
||||||
|
python -m aubox p10lite frp-remove --method erecovery
|
||||||
|
```
|
||||||
|
|
||||||
|
### B) Web-UI im Container (empfohlen)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash scripts/setup-linux.sh # udev-Regeln müssen auf dem Host liegen
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Browser auf <http://127.0.0.1:8080>. Fertig. Smartphone per USB anstecken,
|
||||||
|
Geräte-Seite zeigt es live mit dem erkannten Modus.
|
||||||
|
|
||||||
|
Was die Compose-Datei macht:
|
||||||
|
- `./firmware` ↔ `/firmware` (read-write, hier liegt deine Firmware-Library + SQLite-Index)
|
||||||
|
- `./loaders` ↔ `/loaders` (read-only, Hersteller-Loader)
|
||||||
|
- `/dev/bus/usb` ↔ `/dev/bus/usb` (komplett, damit Re-Enumeration funktioniert)
|
||||||
|
- Port 8080 nur an 127.0.0.1 gebunden — nichts geht ins LAN raus
|
||||||
|
|
||||||
|
### C) Container per Wrapper-Script (für CLI-Nutzung)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/run-docker.sh detect
|
||||||
|
./scripts/run-docker.sh p10lite frp-remove --method dload-erase
|
||||||
|
```
|
||||||
|
|
||||||
|
### PC vs. Pi 4 — kurze Entscheidungshilfe
|
||||||
|
|
||||||
|
| Kriterium | Linux-PC | Pi 4 |
|
||||||
|
|-------------------------|--------------------|----------------------------|
|
||||||
|
| Geschwindigkeit | viel schneller | ok |
|
||||||
|
| Firmware-Dumps (>4 GB) | spürbar besser | langsam (USB shared) |
|
||||||
|
| Dediziertes Werkstatt-Gerät | nein | ja, kann immer laufen |
|
||||||
|
| Strom/Mobilität | schlechter | besser |
|
||||||
|
| USB-3-Bandbreite | volles xHCI | ~250 MB/s effektiv |
|
||||||
|
|
||||||
|
## Lizenz
|
||||||
|
|
||||||
|
Privat, noch nicht entschieden.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
__version__ = "0.0.1"
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from .cli import main
|
||||||
|
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
"""CLI-Einstiegspunkt: `python -m aubox <command>`."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
from . import p10lite, usb
|
||||||
|
from .kirin import KirinDload
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_detect(_args: argparse.Namespace) -> int:
|
||||||
|
devs = usb.scan()
|
||||||
|
if not devs:
|
||||||
|
print("Kein bekanntes Hersteller-Gerät angeschlossen.")
|
||||||
|
print("Probiere `lsusb` für die rohe Liste.")
|
||||||
|
return 1
|
||||||
|
for d in devs:
|
||||||
|
print(d)
|
||||||
|
if d.mode.notes:
|
||||||
|
print(f" {d.mode.notes}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_p10lite_frp(args: argparse.Namespace) -> int:
|
||||||
|
if args.method == "erecovery":
|
||||||
|
print(p10lite.erecovery_instructions())
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args.method == "dload-erase":
|
||||||
|
try:
|
||||||
|
plan = p10lite.dload_erase_plan()
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
print(str(e), file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
print("Plan:")
|
||||||
|
for stage in plan["stages"]:
|
||||||
|
print(f" - {stage['label']}: {stage['file']} -> 0x{stage['addr']:08x}")
|
||||||
|
print()
|
||||||
|
print("Bitte jetzt Testpoint setzen und USB anschließen.")
|
||||||
|
print("Warte auf Kirin DLOAD (12d1:1100)...")
|
||||||
|
|
||||||
|
for _ in range(60):
|
||||||
|
if usb.find_first("kirin-dload") is not None:
|
||||||
|
break
|
||||||
|
time.sleep(0.5)
|
||||||
|
else:
|
||||||
|
print("Timeout: kein Kirin DLOAD erschienen.", file=sys.stderr)
|
||||||
|
return 3
|
||||||
|
|
||||||
|
print("Gerät erkannt. Sende Loader-Stages...")
|
||||||
|
with KirinDload() as k:
|
||||||
|
for stage in plan["stages"]:
|
||||||
|
print(f" -> {stage['label']}")
|
||||||
|
k.send_loader(stage["file"], stage["addr"])
|
||||||
|
# Zwischen den Stages re-enumeriert das Gerät evtl.
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
print("Loader-Stages gesendet. Erwarteter Folgemodus:",
|
||||||
|
plan["post"])
|
||||||
|
print("Nächster Schritt (manuell, bis Workflow ausgereift):")
|
||||||
|
print(" fastboot devices")
|
||||||
|
print(f" fastboot erase {plan['frp_partition']}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
print(f"Methode {args.method!r} noch nicht implementiert.", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_web(args: argparse.Namespace) -> int:
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(
|
||||||
|
"aubox.web.app:app",
|
||||||
|
host=args.host,
|
||||||
|
port=args.port,
|
||||||
|
reload=args.reload,
|
||||||
|
log_level="info",
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
p = argparse.ArgumentParser(prog="aubox")
|
||||||
|
sub = p.add_subparsers(dest="cmd", required=True)
|
||||||
|
|
||||||
|
sp_detect = sub.add_parser("detect", help="Angeschlossene Geräte erkennen")
|
||||||
|
sp_detect.set_defaults(func=cmd_detect)
|
||||||
|
|
||||||
|
sp_web = sub.add_parser("web", help="Lokale Web-UI starten")
|
||||||
|
sp_web.add_argument("--host", default="127.0.0.1",
|
||||||
|
help="Bind-Adresse (Default: 127.0.0.1, im Container 0.0.0.0)")
|
||||||
|
sp_web.add_argument("--port", type=int, default=8080)
|
||||||
|
sp_web.add_argument("--reload", action="store_true",
|
||||||
|
help="Auto-Reload bei Code-Änderungen (Entwicklung)")
|
||||||
|
sp_web.set_defaults(func=cmd_web)
|
||||||
|
|
||||||
|
sp_p10 = sub.add_parser("p10lite", help="Workflows für Huawei P10 Lite")
|
||||||
|
p10_sub = sp_p10.add_subparsers(dest="p10_cmd", required=True)
|
||||||
|
|
||||||
|
sp_frp = p10_sub.add_parser("frp-remove", help="Google-Account-Sperre entfernen")
|
||||||
|
sp_frp.add_argument(
|
||||||
|
"--method",
|
||||||
|
choices=["erecovery", "dload-erase", "dload-flash"],
|
||||||
|
default="erecovery",
|
||||||
|
help="Welcher Pfad? Default: erecovery (sicherster, kein Loader nötig)",
|
||||||
|
)
|
||||||
|
sp_frp.set_defaults(func=cmd_p10lite_frp)
|
||||||
|
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
args = build_parser().parse_args(argv)
|
||||||
|
return args.func(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
"""Sandbox-fähiger Datei-Browser.
|
||||||
|
|
||||||
|
Egal welcher Pfad reinkommt — er wird gegen ein konfiguriertes Root-Verzeichnis
|
||||||
|
geprüft. Path-Traversal (`../..`) ist damit ausgeschlossen.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class PathEscapeError(PermissionError):
|
||||||
|
"""Pfad würde aus dem erlaubten Root ausbrechen."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Entry:
|
||||||
|
name: str
|
||||||
|
rel_path: str # relativ zum Root, mit '/' Separator
|
||||||
|
is_dir: bool
|
||||||
|
size: int # Bytes (0 für Dirs)
|
||||||
|
|
||||||
|
|
||||||
|
def safe_resolve(root: Path, rel: str) -> Path:
|
||||||
|
"""Pfad relativ zum Root auflösen und verifizieren, dass er drin bleibt."""
|
||||||
|
root = root.resolve()
|
||||||
|
rel_clean = rel.lstrip("/").strip()
|
||||||
|
target = (root / rel_clean).resolve()
|
||||||
|
try:
|
||||||
|
target.relative_to(root)
|
||||||
|
except ValueError as e:
|
||||||
|
raise PathEscapeError(f"Pfad {rel!r} verlässt Root {root}") from e
|
||||||
|
return target
|
||||||
|
|
||||||
|
|
||||||
|
def list_dir(root: Path, rel: str = "") -> tuple[Path, list[Entry]]:
|
||||||
|
"""Inhalt eines Verzeichnisses listen, Dirs zuerst, dann Files alphabetisch."""
|
||||||
|
target = safe_resolve(root, rel)
|
||||||
|
if not target.exists():
|
||||||
|
raise FileNotFoundError(target)
|
||||||
|
if not target.is_dir():
|
||||||
|
raise NotADirectoryError(target)
|
||||||
|
|
||||||
|
entries: list[Entry] = []
|
||||||
|
for child in sorted(target.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower())):
|
||||||
|
if child.name.startswith("."):
|
||||||
|
continue
|
||||||
|
rel_child = str(child.relative_to(root.resolve())).replace("\\", "/")
|
||||||
|
try:
|
||||||
|
size = child.stat().st_size if child.is_file() else 0
|
||||||
|
except OSError:
|
||||||
|
size = 0
|
||||||
|
entries.append(Entry(
|
||||||
|
name=child.name,
|
||||||
|
rel_path=rel_child,
|
||||||
|
is_dir=child.is_dir(),
|
||||||
|
size=size,
|
||||||
|
))
|
||||||
|
return target, entries
|
||||||
|
|
||||||
|
|
||||||
|
def breadcrumbs(rel: str) -> list[tuple[str, str]]:
|
||||||
|
"""Liste von (Label, Pfad) für die Anzeige als Breadcrumb-Navigation."""
|
||||||
|
parts = [p for p in rel.split("/") if p]
|
||||||
|
out = [("/", "")]
|
||||||
|
cur = ""
|
||||||
|
for p in parts:
|
||||||
|
cur = f"{cur}/{p}" if cur else p
|
||||||
|
out.append((p, cur))
|
||||||
|
return out
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
"""Kirin Download-Mode (HiSilicon) — hisi-idt-Protokoll.
|
||||||
|
|
||||||
|
Wenn ein Kirin-Gerät per Testpoint zur Erde gezogen und gebootet wird,
|
||||||
|
landet es im "DLOAD"-Modus mit USB-VID:PID 12d1:1100. In diesem Modus
|
||||||
|
hat es selbst noch keinen Speicher initialisiert — es wartet darauf,
|
||||||
|
dass der Host einen *xloader* (kleiner sekundärer Bootloader) per USB
|
||||||
|
schickt. Erst danach kommt der nächste Stage-Loader (`usb_loader.bin`),
|
||||||
|
und dann fährt das Gerät als erweiterter Fastboot hoch.
|
||||||
|
|
||||||
|
Das Protokoll dafür heißt im Huawei-Kosmos schlicht "hisi-idt". Es ist
|
||||||
|
nicht öffentlich dokumentiert, aber seit Jahren bekannt aus dem
|
||||||
|
hisi-idt.py-Skript, das u.a. im Kirin-Tools-Umfeld kursiert. Hier eine
|
||||||
|
saubere Implementation gegen pyusb.
|
||||||
|
|
||||||
|
WICHTIG:
|
||||||
|
Die Loader-Dateien (`hisi-sec_usb_xloader.bin`, `usb_loader.bin`)
|
||||||
|
sind Huawei-signierte Binaries. Du musst sie selbst beschaffen
|
||||||
|
und in `loaders/kirin/<soc>/` ablegen. Siehe loaders/README.md.
|
||||||
|
|
||||||
|
Diese Implementation orientiert sich am öffentlichen Protokoll-Wissen.
|
||||||
|
Auf realer P10-Lite-Hardware vor produktivem Einsatz validieren.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import struct
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import usb.core
|
||||||
|
import usb.util
|
||||||
|
|
||||||
|
KIRIN_DLOAD_VID = 0x12D1
|
||||||
|
KIRIN_DLOAD_PID = 0x1100
|
||||||
|
|
||||||
|
# Protokoll-Konstanten (hisi-idt)
|
||||||
|
HEAD_TAG = 0xFE
|
||||||
|
TAIL_TAG = 0xFE
|
||||||
|
TYPE_DATA = 0xDA
|
||||||
|
TYPE_HEAD = 0xA5
|
||||||
|
CHUNK = 0x800 # 2 KiB Payload pro Frame
|
||||||
|
TIMEOUT_MS = 5000
|
||||||
|
|
||||||
|
|
||||||
|
def _crc16_xmodem(data: bytes) -> int:
|
||||||
|
"""CRC-16/XMODEM (Polynom 0x1021, init 0)."""
|
||||||
|
crc = 0
|
||||||
|
for b in data:
|
||||||
|
crc ^= b << 8
|
||||||
|
for _ in range(8):
|
||||||
|
if crc & 0x8000:
|
||||||
|
crc = (crc << 1) ^ 0x1021
|
||||||
|
else:
|
||||||
|
crc <<= 1
|
||||||
|
crc &= 0xFFFF
|
||||||
|
return crc
|
||||||
|
|
||||||
|
|
||||||
|
def _frame(seq: int, payload: bytes, frame_type: int) -> bytes:
|
||||||
|
"""Ein hisi-idt-Frame bauen.
|
||||||
|
|
||||||
|
Layout:
|
||||||
|
HEAD (1) | TYPE (1) | SEQ (2 BE) | PAYLOAD | CRC16 (2 BE) | TAIL (1)
|
||||||
|
"""
|
||||||
|
body = struct.pack(">BBH", HEAD_TAG, frame_type, seq) + payload
|
||||||
|
crc = _crc16_xmodem(body[1:]) # CRC ohne HEAD
|
||||||
|
return body + struct.pack(">HB", crc, TAIL_TAG)
|
||||||
|
|
||||||
|
|
||||||
|
class KirinDload:
|
||||||
|
"""Verbindung zum Kirin im Download-Mode."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.dev: usb.core.Device | None = None
|
||||||
|
self.ep_out = None
|
||||||
|
self.ep_in = None
|
||||||
|
|
||||||
|
def open(self) -> None:
|
||||||
|
dev = usb.core.find(idVendor=KIRIN_DLOAD_VID, idProduct=KIRIN_DLOAD_PID)
|
||||||
|
if dev is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Kein Kirin-DLOAD-Gerät gefunden "
|
||||||
|
f"({KIRIN_DLOAD_VID:04x}:{KIRIN_DLOAD_PID:04x}). "
|
||||||
|
f"Testpoint sitzen? USB-Kabel datentauglich?"
|
||||||
|
)
|
||||||
|
if dev.is_kernel_driver_active(0):
|
||||||
|
dev.detach_kernel_driver(0)
|
||||||
|
dev.set_configuration()
|
||||||
|
cfg = dev.get_active_configuration()
|
||||||
|
intf = cfg[(0, 0)]
|
||||||
|
self.ep_out = usb.util.find_descriptor(
|
||||||
|
intf,
|
||||||
|
custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress)
|
||||||
|
== usb.util.ENDPOINT_OUT,
|
||||||
|
)
|
||||||
|
self.ep_in = usb.util.find_descriptor(
|
||||||
|
intf,
|
||||||
|
custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress)
|
||||||
|
== usb.util.ENDPOINT_IN,
|
||||||
|
)
|
||||||
|
if self.ep_out is None:
|
||||||
|
raise RuntimeError("Kein OUT-Endpunkt gefunden — falsche USB-Konfiguration?")
|
||||||
|
self.dev = dev
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
if self.dev is not None:
|
||||||
|
usb.util.dispose_resources(self.dev)
|
||||||
|
self.dev = None
|
||||||
|
|
||||||
|
def __enter__(self) -> "KirinDload":
|
||||||
|
self.open()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *exc) -> None:
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def send_loader(self, path: Path, load_addr: int) -> None:
|
||||||
|
"""Loader-Binary in einer Folge von Frames an das Gerät senden.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Pfad zur xloader/usb_loader-Datei.
|
||||||
|
load_addr: Zieladresse im SRAM. Pro SoC unterschiedlich.
|
||||||
|
Für Kirin 658 (P10 Lite, xloader) typischerweise 0x07012000.
|
||||||
|
In der Doku des SoCs verifizieren.
|
||||||
|
"""
|
||||||
|
data = path.read_bytes()
|
||||||
|
self._send_header(load_addr, len(data))
|
||||||
|
seq = 1
|
||||||
|
for offset in range(0, len(data), CHUNK):
|
||||||
|
chunk = data[offset : offset + CHUNK]
|
||||||
|
self._write(_frame(seq, chunk, TYPE_DATA))
|
||||||
|
seq = (seq + 1) & 0xFFFF
|
||||||
|
# Kleines Delay — Kirin akzeptiert zu schnelle Bursts nicht zuverlässig
|
||||||
|
time.sleep(0.001)
|
||||||
|
# Sequenz abschließen: einige Implementationen senden ein finales Frame
|
||||||
|
# mit Länge 0. Bei Bedarf hier ergänzen, je nach SoC-Verhalten.
|
||||||
|
|
||||||
|
def _send_header(self, addr: int, size: int) -> None:
|
||||||
|
"""Header-Frame: sagt dem Gerät, wohin und wieviel."""
|
||||||
|
payload = struct.pack(">II", addr, size)
|
||||||
|
self._write(_frame(0, payload, TYPE_HEAD))
|
||||||
|
|
||||||
|
def _write(self, buf: bytes) -> None:
|
||||||
|
assert self.dev is not None and self.ep_out is not None
|
||||||
|
self.ep_out.write(buf, timeout=TIMEOUT_MS)
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
"""Firmware-Library: SQLite-Index + Format-Identifikation.
|
||||||
|
|
||||||
|
Öffentliche API:
|
||||||
|
db.connect(path) -> sqlite3.Connection
|
||||||
|
db.upsert_firmware(conn, record)
|
||||||
|
db.list_firmware(conn, ...)
|
||||||
|
identify.identify(path) -> FirmwareInfo | None
|
||||||
|
"""
|
||||||
|
from . import db, identify # noqa: F401
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
"""SQLite-Index der Firmware-Library.
|
||||||
|
|
||||||
|
Die DB-Datei wandert standardmäßig nach `<firmware_root>/firmware.db`,
|
||||||
|
damit sie zur Library gehört und beim Mounten automatisch da ist.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Iterator
|
||||||
|
|
||||||
|
SCHEMA = """
|
||||||
|
CREATE TABLE IF NOT EXISTS firmware (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
rel_path TEXT NOT NULL UNIQUE, -- relativ zum firmware-Root
|
||||||
|
size INTEGER NOT NULL,
|
||||||
|
mtime REAL NOT NULL,
|
||||||
|
sha256 TEXT, -- erst nach erstem Hash gefüllt
|
||||||
|
format TEXT, -- 'huawei-update.app', 'mtk-scatter', 'unknown', ...
|
||||||
|
vendor TEXT,
|
||||||
|
model TEXT, -- z.B. 'WAS-LX1'
|
||||||
|
soc TEXT, -- z.B. 'kirin-658'
|
||||||
|
version TEXT, -- z.B. '8.0.0.367'
|
||||||
|
region TEXT, -- z.B. 'C432'
|
||||||
|
extra_json TEXT, -- alles weitere als JSON
|
||||||
|
added_at REAL DEFAULT (strftime('%s','now')),
|
||||||
|
last_seen_at REAL DEFAULT (strftime('%s','now'))
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_fw_model ON firmware(model);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_fw_format ON firmware(format);
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def connect(db_path: Path) -> sqlite3.Connection:
|
||||||
|
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
conn.executescript(SCHEMA)
|
||||||
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def open_db(db_path: Path) -> Iterator[sqlite3.Connection]:
|
||||||
|
"""Connection-Lifecycle für FastAPI-Handler: öffnet, gibt zurück, schließt."""
|
||||||
|
conn = connect(db_path)
|
||||||
|
try:
|
||||||
|
yield conn
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def transaction(conn: sqlite3.Connection) -> Iterator[sqlite3.Connection]:
|
||||||
|
try:
|
||||||
|
yield conn
|
||||||
|
conn.commit()
|
||||||
|
except Exception:
|
||||||
|
conn.rollback()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def upsert(conn: sqlite3.Connection, rec: dict) -> int:
|
||||||
|
"""Einfügen oder bei Konflikt auf rel_path aktualisieren."""
|
||||||
|
cols = ["rel_path", "size", "mtime", "sha256", "format",
|
||||||
|
"vendor", "model", "soc", "version", "region", "extra_json"]
|
||||||
|
placeholders = ",".join(f":{c}" for c in cols)
|
||||||
|
update = ",".join(f"{c}=excluded.{c}" for c in cols if c != "rel_path")
|
||||||
|
sql = (f"INSERT INTO firmware ({','.join(cols)}) VALUES ({placeholders}) "
|
||||||
|
f"ON CONFLICT(rel_path) DO UPDATE SET {update}, "
|
||||||
|
f"last_seen_at=strftime('%s','now')")
|
||||||
|
cur = conn.execute(sql, {c: rec.get(c) for c in cols})
|
||||||
|
return cur.lastrowid or 0
|
||||||
|
|
||||||
|
|
||||||
|
def list_all(conn: sqlite3.Connection) -> list[sqlite3.Row]:
|
||||||
|
return list(conn.execute(
|
||||||
|
"SELECT * FROM firmware ORDER BY vendor, model, version"
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def get_by_id(conn: sqlite3.Connection, fw_id: int) -> sqlite3.Row | None:
|
||||||
|
cur = conn.execute("SELECT * FROM firmware WHERE id = ?", (fw_id,))
|
||||||
|
return cur.fetchone()
|
||||||
|
|
||||||
|
|
||||||
|
def delete_missing(conn: sqlite3.Connection, present_rel_paths: set[str]) -> int:
|
||||||
|
"""Einträge entfernen, die beim letzten Scan nicht mehr da waren."""
|
||||||
|
cur = conn.execute("SELECT id, rel_path FROM firmware")
|
||||||
|
to_delete = [row["id"] for row in cur if row["rel_path"] not in present_rel_paths]
|
||||||
|
if to_delete:
|
||||||
|
conn.executemany("DELETE FROM firmware WHERE id = ?",
|
||||||
|
[(i,) for i in to_delete])
|
||||||
|
return len(to_delete)
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
"""Parser für Huawei `update.app`-Container.
|
||||||
|
|
||||||
|
Format-Beschreibung (öffentlich aus splituapp / Huawei Update Extractor):
|
||||||
|
|
||||||
|
Datei beginnt mit 0x5C Bytes Padding/Header, danach folgen Section-Header.
|
||||||
|
Jeder Section-Header ist 98 Bytes:
|
||||||
|
|
||||||
|
uint32 magic = 0xA55AAA55
|
||||||
|
uint32 header_length = 98
|
||||||
|
uint32 unknown1
|
||||||
|
char[8] hardware_id z.B. "WAS-LX1"
|
||||||
|
uint32 file_sequence
|
||||||
|
uint32 file_size
|
||||||
|
char[16] file_date z.B. "2018.04.16"
|
||||||
|
char[16] file_time z.B. "10:23:45"
|
||||||
|
char[16] file_type z.B. "BOOT", "SYSTEM", "USERDATA", "CRC"
|
||||||
|
char[16] blank1
|
||||||
|
uint16 header_checksum
|
||||||
|
uint16 block_size
|
||||||
|
uint16 blank2
|
||||||
|
|
||||||
|
Danach `file_size` Bytes Section-Daten, gefolgt von einer CRC-Tabelle
|
||||||
|
(eine uint16 pro `block_size` Bytes Daten), dann 4-Byte-Alignment auf
|
||||||
|
die nächste Section-Header-Position.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import struct
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
UPDATE_MAGIC = 0xA55AAA55
|
||||||
|
HEADER_FMT = "<III8sII16s16s16s16sHHH"
|
||||||
|
HEADER_SIZE = 98
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Section:
|
||||||
|
sequence: int
|
||||||
|
size: int
|
||||||
|
date: str
|
||||||
|
time: str
|
||||||
|
type: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UpdateAppInfo:
|
||||||
|
hardware_id: str
|
||||||
|
section_count: int
|
||||||
|
sections: list[Section] = field(default_factory=list)
|
||||||
|
has_boot: bool = False
|
||||||
|
has_system: bool = False
|
||||||
|
earliest_date: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _cstr(b: bytes) -> str:
|
||||||
|
return b.split(b"\x00", 1)[0].decode("ascii", errors="replace").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def parse(path: Path, max_sections: int = 500) -> UpdateAppInfo | None:
|
||||||
|
"""Header der ersten N Sections einlesen, ohne das ganze Image zu lesen."""
|
||||||
|
if not path.is_file():
|
||||||
|
return None
|
||||||
|
if path.stat().st_size < 0x100:
|
||||||
|
return None
|
||||||
|
|
||||||
|
with path.open("rb") as f:
|
||||||
|
# Magic suchen — sitzt meist bei 0x5C, manche Builds variieren
|
||||||
|
head = f.read(0x200)
|
||||||
|
idx = head.find(struct.pack("<I", UPDATE_MAGIC))
|
||||||
|
if idx < 0:
|
||||||
|
return None
|
||||||
|
f.seek(idx)
|
||||||
|
|
||||||
|
sections: list[Section] = []
|
||||||
|
hardware_id = ""
|
||||||
|
earliest: str | None = None
|
||||||
|
has_boot = has_system = False
|
||||||
|
|
||||||
|
for _ in range(max_sections):
|
||||||
|
buf = f.read(HEADER_SIZE)
|
||||||
|
if len(buf) < HEADER_SIZE:
|
||||||
|
break
|
||||||
|
(magic, _hlen, _u, hw, seq, size,
|
||||||
|
date, time_, ftype, _b, _crc, blksz, _b2) = struct.unpack(HEADER_FMT, buf)
|
||||||
|
if magic != UPDATE_MAGIC:
|
||||||
|
break
|
||||||
|
|
||||||
|
hw_str = _cstr(hw)
|
||||||
|
type_str = _cstr(ftype)
|
||||||
|
date_str = _cstr(date)
|
||||||
|
time_str = _cstr(time_)
|
||||||
|
|
||||||
|
if not hardware_id and hw_str:
|
||||||
|
hardware_id = hw_str
|
||||||
|
if earliest is None or (date_str and date_str < earliest):
|
||||||
|
if date_str:
|
||||||
|
earliest = date_str
|
||||||
|
if type_str.upper() in ("BOOT", "FASTBOOT", "XLOADER", "BOOTLOADER"):
|
||||||
|
has_boot = True
|
||||||
|
if type_str.upper() in ("SYSTEM", "SYSTEM_IMAGE"):
|
||||||
|
has_system = True
|
||||||
|
|
||||||
|
sections.append(Section(
|
||||||
|
sequence=seq, size=size, date=date_str,
|
||||||
|
time=time_str, type=type_str,
|
||||||
|
))
|
||||||
|
|
||||||
|
# Daten + CRC + Padding überspringen
|
||||||
|
if blksz <= 0:
|
||||||
|
break
|
||||||
|
crc_size = ((size + blksz - 1) // blksz) * 2
|
||||||
|
cur_after_data = f.tell() + size + crc_size
|
||||||
|
pad = (-cur_after_data) % 4
|
||||||
|
f.seek(size + crc_size + pad, 1)
|
||||||
|
|
||||||
|
if not sections:
|
||||||
|
return None
|
||||||
|
return UpdateAppInfo(
|
||||||
|
hardware_id=hardware_id,
|
||||||
|
section_count=len(sections),
|
||||||
|
sections=sections,
|
||||||
|
has_boot=has_boot,
|
||||||
|
has_system=has_system,
|
||||||
|
earliest_date=earliest,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Modell -> SoC-Mapping. Wird mit der Zeit erweitert; was hier nicht steht,
|
||||||
|
# kommt als 'unknown' raus und der User kann es manuell pflegen.
|
||||||
|
HUAWEI_MODEL_SOC = {
|
||||||
|
"WAS-LX1": "kirin-658",
|
||||||
|
"WAS-LX2": "kirin-658",
|
||||||
|
"WAS-LX3": "kirin-658",
|
||||||
|
"PRA-LX1": "kirin-655", # P8 Lite 2017
|
||||||
|
"VTR-L09": "kirin-960", # P10
|
||||||
|
"VTR-L29": "kirin-960",
|
||||||
|
"VKY-L09": "kirin-960", # P10 Plus
|
||||||
|
"EML-L09": "kirin-970", # P20
|
||||||
|
"EML-L29": "kirin-970",
|
||||||
|
"CLT-L29": "kirin-970", # P20 Pro
|
||||||
|
"ANE-LX1": "kirin-710", # P20 Lite
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def soc_for(hardware_id: str) -> str | None:
|
||||||
|
return HUAWEI_MODEL_SOC.get(hardware_id.upper())
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
"""Format-Erkennung. Dispatcher: schaut auf Magic + Dateinamen, leitet an
|
||||||
|
den passenden Parser weiter. Liefert ein einheitliches Dict mit den Feldern,
|
||||||
|
die in die DB gehen.
|
||||||
|
|
||||||
|
Aktuell unterstützt:
|
||||||
|
- Huawei update.app (vollständig)
|
||||||
|
|
||||||
|
Geplant (Stubs als Hinweis):
|
||||||
|
- MediaTek scatter (txt + bin)
|
||||||
|
- Samsung Odin (.tar.md5 mit AP/BL/CP/CSC)
|
||||||
|
- Qualcomm rawprogram*.xml
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import struct
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from . import huawei
|
||||||
|
|
||||||
|
HUAWEI_MAGIC = struct.pack("<I", huawei.UPDATE_MAGIC)
|
||||||
|
|
||||||
|
|
||||||
|
def _peek(path: Path, n: int = 0x200) -> bytes:
|
||||||
|
try:
|
||||||
|
with path.open("rb") as f:
|
||||||
|
return f.read(n)
|
||||||
|
except OSError:
|
||||||
|
return b""
|
||||||
|
|
||||||
|
|
||||||
|
def identify(path: Path, root: Path) -> dict:
|
||||||
|
"""Rückgabe: dict für db.upsert. Enthält mindestens rel_path, size, mtime,
|
||||||
|
format. Erkennungs-Misserfolg -> format='unknown'."""
|
||||||
|
rel = str(path.resolve().relative_to(root.resolve())).replace("\\", "/")
|
||||||
|
stat = path.stat()
|
||||||
|
base: dict = {
|
||||||
|
"rel_path": rel,
|
||||||
|
"size": stat.st_size,
|
||||||
|
"mtime": stat.st_mtime,
|
||||||
|
"sha256": None,
|
||||||
|
"format": "unknown",
|
||||||
|
"vendor": None,
|
||||||
|
"model": None,
|
||||||
|
"soc": None,
|
||||||
|
"version": None,
|
||||||
|
"region": None,
|
||||||
|
"extra_json": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
head = _peek(path)
|
||||||
|
|
||||||
|
# Huawei update.app — Magic taucht innerhalb der ersten 0x100 Bytes auf
|
||||||
|
if HUAWEI_MAGIC in head[:0x200]:
|
||||||
|
info = huawei.parse(path)
|
||||||
|
if info is not None:
|
||||||
|
base.update(
|
||||||
|
format="huawei-update.app",
|
||||||
|
vendor="Huawei",
|
||||||
|
model=info.hardware_id or None,
|
||||||
|
soc=huawei.soc_for(info.hardware_id) if info.hardware_id else None,
|
||||||
|
version=info.earliest_date, # bessere Quelle wenn verfügbar
|
||||||
|
region=_region_from_filename(path.name),
|
||||||
|
extra_json=json.dumps({
|
||||||
|
"section_count": info.section_count,
|
||||||
|
"has_boot": info.has_boot,
|
||||||
|
"has_system": info.has_system,
|
||||||
|
"first_sections": [
|
||||||
|
s.type for s in info.sections[:10]
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return base
|
||||||
|
|
||||||
|
# Fallback: Filename-Heuristik (MTK scatter etc. — TODO)
|
||||||
|
name = path.name.lower()
|
||||||
|
if name.startswith("mt") and name.endswith(".txt"):
|
||||||
|
base["format"] = "mtk-scatter-candidate"
|
||||||
|
elif name.endswith(".tar.md5"):
|
||||||
|
base["format"] = "samsung-odin-candidate"
|
||||||
|
base["vendor"] = "Samsung"
|
||||||
|
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
def _region_from_filename(name: str) -> str | None:
|
||||||
|
"""Huawei-Region oft im Filename: '...C432B198...' -> 'C432'."""
|
||||||
|
upper = name.upper()
|
||||||
|
for marker in ("C432", "C636", "C185", "C233", "C605", "C10", "C461"):
|
||||||
|
if marker in upper:
|
||||||
|
return marker
|
||||||
|
return None
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
"""Workflows für Huawei P10 Lite (WAS-LX1, Kirin 658, EMUI 8.x).
|
||||||
|
|
||||||
|
Drei FRP-Removal-Pfade in absteigender Erfolgswahrscheinlichkeit:
|
||||||
|
|
||||||
|
1. erecovery — eRecovery + update.app von SD-Karte. Kein Loader nötig.
|
||||||
|
Kein Bootloader-Unlock nötig. Funktioniert in der Regel.
|
||||||
|
2. dload-erase — Testpoint -> Kirin DLOAD -> xloader -> frp-Partition leeren.
|
||||||
|
Braucht passende Loader-Files (siehe loaders/README.md).
|
||||||
|
3. dload-flash — Testpoint -> Kirin DLOAD -> kompletter Reflash via update.app.
|
||||||
|
Maximaler Eingriff, höchstes Risiko, bricht Garantie endgültig.
|
||||||
|
|
||||||
|
Was hier *nicht* funktioniert:
|
||||||
|
- ADB/Fastboot-FRP-Tricks: das Gerät ist im Setup-Wizard, ADB ist aus.
|
||||||
|
- TalkBack/YouTube-Tricks: von Google/Huawei seit Jahren gepatcht.
|
||||||
|
- Bootloader-Unlock-Code von Huawei: seit Juli 2018 abgeschaltet.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
LOADER_ROOT = Path(__file__).resolve().parent.parent / "loaders" / "kirin" / "kirin960_lite"
|
||||||
|
# Kirin 658 wird intern oft mit dem Kirin-960-Loader-Set gefahren (Lite-Variante).
|
||||||
|
# Für 100%-Sicherheit: Loader vom exakten WAS-LX1-Firmware-Dump verwenden.
|
||||||
|
|
||||||
|
XLOADER_NAME = "hisi-sec_usb_xloader.bin"
|
||||||
|
USB_LOADER_NAME = "usb_loader.bin"
|
||||||
|
FASTBOOT_BIN_NAME = "fastboot.bin"
|
||||||
|
|
||||||
|
# SRAM-Adressen für die Loader-Stages (öffentlich aus Kirin-Reverse-Engineering)
|
||||||
|
# Im Zweifel mit dem Image abgleichen — manche WAS-Builds nutzen 0x07012000.
|
||||||
|
XLOADER_ADDR = 0x07012000
|
||||||
|
USB_LOADER_ADDR = 0x07012000
|
||||||
|
|
||||||
|
|
||||||
|
def erecovery_instructions() -> str:
|
||||||
|
"""Schritt-für-Schritt für den eRecovery-Pfad. Pure Anleitung, kein Code."""
|
||||||
|
return """
|
||||||
|
eRecovery-Methode (ohne Loader, ohne Bootloader-Unlock)
|
||||||
|
========================================================
|
||||||
|
|
||||||
|
Du brauchst:
|
||||||
|
- Eine FAT32-formatierte SD-Karte, max. 32 GB
|
||||||
|
- Die *exakte* update.app deiner Region (z.B. WAS-LX1 EMEA C432)
|
||||||
|
Bezug: huaweifirm.com, hovatek.com, stockromhuawei.com
|
||||||
|
- USB-Ladegerät (kein Datenkabel nötig)
|
||||||
|
|
||||||
|
Schritte:
|
||||||
|
1. Auf der SD-Karte einen Ordner `dload` (klein!) anlegen.
|
||||||
|
2. update.app dort hineinkopieren. Pfad: SD:/dload/update.app
|
||||||
|
3. SD-Karte in das ausgeschaltete P10 Lite stecken.
|
||||||
|
4. Beim Einschalten gleichzeitig halten: Power + Volume-Up + Volume-Down.
|
||||||
|
Halten bis das Huawei-Logo erscheint, dann nur noch Power weiter, andere lösen.
|
||||||
|
5. eRecovery erkennt die SD-Karte und bietet "Software-Update" / "SD-Karten-Update".
|
||||||
|
Bestätigen.
|
||||||
|
6. Nach Abschluss bootet das Gerät neu, FRP-Partition ist neu geschrieben,
|
||||||
|
Setup-Assistent startet ohne alten Google-Account.
|
||||||
|
|
||||||
|
Wenn eRecovery die Datei nicht erkennt:
|
||||||
|
- Region passt nicht (häufigster Fehler)
|
||||||
|
- update.app ist Multi-Part (.app + .lst). Beide nach /dload kopieren.
|
||||||
|
- SD-Karte nicht FAT32 oder > 32 GB
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def dload_erase_plan(loader_dir: Path = LOADER_ROOT) -> dict:
|
||||||
|
"""Plant den Testpoint -> DLOAD -> frp-erase-Pfad.
|
||||||
|
|
||||||
|
Liefert ein Dict mit Pfaden und Adressen, das vom CLI ausgeführt wird.
|
||||||
|
Wirft FileNotFoundError, wenn die Loader-Files fehlen — dann landet der
|
||||||
|
User bei loaders/README.md und weiß, was zu tun ist.
|
||||||
|
"""
|
||||||
|
xloader = loader_dir / XLOADER_NAME
|
||||||
|
usb_loader = loader_dir / USB_LOADER_NAME
|
||||||
|
|
||||||
|
missing = [str(p) for p in (xloader, usb_loader) if not p.is_file()]
|
||||||
|
if missing:
|
||||||
|
raise FileNotFoundError(
|
||||||
|
"Folgende Loader-Files fehlen:\n - "
|
||||||
|
+ "\n - ".join(missing)
|
||||||
|
+ "\nSiehe loaders/README.md für Beschaffung."
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"stages": [
|
||||||
|
{"file": xloader, "addr": XLOADER_ADDR, "label": "xloader"},
|
||||||
|
{"file": usb_loader, "addr": USB_LOADER_ADDR, "label": "usb_loader"},
|
||||||
|
],
|
||||||
|
"post": "huawei-fastboot-d", # erwarteter Modus nach dem Loaden
|
||||||
|
"frp_partition": "frp",
|
||||||
|
# Größe der FRP-Partition auf P10 Lite: meist 1 MiB. Beim ersten echten
|
||||||
|
# Lauf gegen die ptable des Geräts gegenchecken (gpt-Dump via fastboot).
|
||||||
|
"frp_size_bytes": 1024 * 1024,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
"""USB-Geräte-Erkennung über VID:PID.
|
||||||
|
|
||||||
|
Erkennt die wichtigsten Hersteller-Modi und ordnet sie einem semantischen
|
||||||
|
Status zu, damit Workflows wissen, in welchem Modus das Gerät gerade ist.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import usb.core
|
||||||
|
import usb.util
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class DeviceMode:
|
||||||
|
label: str
|
||||||
|
vendor: str
|
||||||
|
notes: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
# (vid, pid) -> Modus
|
||||||
|
KNOWN: dict[tuple[int, int], DeviceMode] = {
|
||||||
|
# Huawei / Kirin
|
||||||
|
(0x12D1, 0x1100): DeviceMode("kirin-dload", "Huawei",
|
||||||
|
"Kirin Download-Mode, wartet auf xloader via hisi-idt"),
|
||||||
|
(0x12D1, 0x1052): DeviceMode("huawei-fastboot-d", "Huawei",
|
||||||
|
"Erweiterter Fastboot-Mode (nach xloader)"),
|
||||||
|
(0x12D1, 0x3609): DeviceMode("kirin-dload-alt", "Huawei",
|
||||||
|
"Kirin DLOAD alternative PID"),
|
||||||
|
(0x12D1, 0x107E): DeviceMode("huawei-hisuite", "Huawei",
|
||||||
|
"HiSuite/MTP-Modus, kein Recovery-Zugriff"),
|
||||||
|
# Google / Android Standard
|
||||||
|
(0x18D1, 0x4EE0): DeviceMode("fastboot", "Google", "Standard-Fastboot"),
|
||||||
|
(0x18D1, 0x4EE7): DeviceMode("adb", "Google", "ADB"),
|
||||||
|
# MediaTek
|
||||||
|
(0x0E8D, 0x0003): DeviceMode("mtk-preloader", "MediaTek",
|
||||||
|
"Preloader (DA-Mode möglich)"),
|
||||||
|
(0x0E8D, 0x2000): DeviceMode("mtk-brom", "MediaTek",
|
||||||
|
"BROM — Ziel für mtkclient/kamakiri"),
|
||||||
|
# Qualcomm EDL
|
||||||
|
(0x05C6, 0x9008): DeviceMode("qc-edl", "Qualcomm",
|
||||||
|
"Emergency Download (Firehose-fähig)"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FoundDevice:
|
||||||
|
vid: int
|
||||||
|
pid: int
|
||||||
|
mode: DeviceMode
|
||||||
|
bus: int
|
||||||
|
address: int
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return (f"[{self.bus:03d}:{self.address:03d}] "
|
||||||
|
f"{self.vid:04x}:{self.pid:04x} {self.mode.vendor} "
|
||||||
|
f"{self.mode.label}")
|
||||||
|
|
||||||
|
|
||||||
|
def scan() -> list[FoundDevice]:
|
||||||
|
"""Alle bekannten Hersteller-Modi auflisten, die gerade angeschlossen sind."""
|
||||||
|
found: list[FoundDevice] = []
|
||||||
|
for dev in usb.core.find(find_all=True):
|
||||||
|
key = (dev.idVendor, dev.idProduct)
|
||||||
|
mode = KNOWN.get(key)
|
||||||
|
if mode is None:
|
||||||
|
continue
|
||||||
|
found.append(FoundDevice(
|
||||||
|
vid=dev.idVendor,
|
||||||
|
pid=dev.idProduct,
|
||||||
|
mode=mode,
|
||||||
|
bus=dev.bus,
|
||||||
|
address=dev.address,
|
||||||
|
))
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def find_first(label: str) -> FoundDevice | None:
|
||||||
|
for d in scan():
|
||||||
|
if d.mode.label == label:
|
||||||
|
return d
|
||||||
|
return None
|
||||||
|
|
@ -0,0 +1,163 @@
|
||||||
|
"""FastAPI-App. Lokale Web-UI für aubox.
|
||||||
|
|
||||||
|
Start:
|
||||||
|
uvicorn aubox.web.app:app --host 127.0.0.1 --port 8080
|
||||||
|
oder über CLI:
|
||||||
|
python -m aubox web
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from fastapi import FastAPI, HTTPException, Request
|
||||||
|
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
|
from .. import filebrowse, p10lite, usb
|
||||||
|
from ..library import db as fwdb
|
||||||
|
from ..library import identify as fwid
|
||||||
|
|
||||||
|
WEB_ROOT = Path(__file__).resolve().parent
|
||||||
|
FIRMWARE_ROOT = Path(os.environ.get("AUBOX_FIRMWARE_ROOT", "./firmware")).resolve()
|
||||||
|
LOADER_ROOT = Path(os.environ.get("AUBOX_LOADER_ROOT", "./loaders")).resolve()
|
||||||
|
DB_PATH = FIRMWARE_ROOT / "firmware.db"
|
||||||
|
|
||||||
|
app = FastAPI(title="aubox")
|
||||||
|
app.mount("/static", StaticFiles(directory=WEB_ROOT / "static"), name="static")
|
||||||
|
templates = Jinja2Templates(directory=WEB_ROOT / "templates")
|
||||||
|
|
||||||
|
|
||||||
|
def _human_size(n: int) -> str:
|
||||||
|
for unit in ("B", "KB", "MB", "GB", "TB"):
|
||||||
|
if n < 1024:
|
||||||
|
return f"{n:.1f} {unit}" if unit != "B" else f"{n} B"
|
||||||
|
n /= 1024
|
||||||
|
return f"{n:.1f} PB"
|
||||||
|
|
||||||
|
|
||||||
|
templates.env.filters["humansize"] = _human_size
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/", response_class=HTMLResponse)
|
||||||
|
async def index(request: Request):
|
||||||
|
devices = usb.scan()
|
||||||
|
with fwdb.open_db(DB_PATH) as conn:
|
||||||
|
fw_count = conn.execute("SELECT COUNT(*) FROM firmware").fetchone()[0]
|
||||||
|
return templates.TemplateResponse("index.html", {
|
||||||
|
"request": request,
|
||||||
|
"devices": devices,
|
||||||
|
"fw_count": fw_count,
|
||||||
|
"firmware_root": FIRMWARE_ROOT,
|
||||||
|
"loader_root": LOADER_ROOT,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Devices --------------------------------------------------------
|
||||||
|
|
||||||
|
@app.get("/devices", response_class=HTMLResponse)
|
||||||
|
async def devices_page(request: Request):
|
||||||
|
return templates.TemplateResponse("devices.html", {"request": request})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/devices/html", response_class=HTMLResponse)
|
||||||
|
async def devices_partial(request: Request):
|
||||||
|
return templates.TemplateResponse("_devices.html", {
|
||||||
|
"request": request,
|
||||||
|
"devices": usb.scan(),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Firmware Library ----------------------------------------------
|
||||||
|
|
||||||
|
@app.get("/firmware", response_class=HTMLResponse)
|
||||||
|
async def firmware_page(request: Request):
|
||||||
|
with fwdb.open_db(DB_PATH) as conn:
|
||||||
|
rows = fwdb.list_all(conn)
|
||||||
|
return templates.TemplateResponse("firmware.html", {
|
||||||
|
"request": request,
|
||||||
|
"firmware": rows,
|
||||||
|
"firmware_root": FIRMWARE_ROOT,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/firmware/scan")
|
||||||
|
async def firmware_scan():
|
||||||
|
"""Walkt FIRMWARE_ROOT, identifiziert jede Datei, schreibt in DB."""
|
||||||
|
if not FIRMWARE_ROOT.is_dir():
|
||||||
|
raise HTTPException(404, f"Firmware-Root {FIRMWARE_ROOT} nicht gefunden")
|
||||||
|
|
||||||
|
seen: set[str] = set()
|
||||||
|
added = updated = 0
|
||||||
|
with fwdb.open_db(DB_PATH) as conn:
|
||||||
|
with fwdb.transaction(conn):
|
||||||
|
for path in FIRMWARE_ROOT.rglob("*"):
|
||||||
|
if not path.is_file():
|
||||||
|
continue
|
||||||
|
if path.name == "firmware.db" or path.name.startswith("."):
|
||||||
|
continue
|
||||||
|
rec = fwid.identify(path, FIRMWARE_ROOT)
|
||||||
|
seen.add(rec["rel_path"])
|
||||||
|
existed = conn.execute(
|
||||||
|
"SELECT 1 FROM firmware WHERE rel_path = ?",
|
||||||
|
(rec["rel_path"],),
|
||||||
|
).fetchone()
|
||||||
|
fwdb.upsert(conn, rec)
|
||||||
|
if existed:
|
||||||
|
updated += 1
|
||||||
|
else:
|
||||||
|
added += 1
|
||||||
|
removed = fwdb.delete_missing(conn, seen)
|
||||||
|
|
||||||
|
return JSONResponse({
|
||||||
|
"scanned": len(seen),
|
||||||
|
"added": added,
|
||||||
|
"updated": updated,
|
||||||
|
"removed": removed,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/firmware/{fw_id}", response_class=HTMLResponse)
|
||||||
|
async def firmware_detail(request: Request, fw_id: int):
|
||||||
|
with fwdb.open_db(DB_PATH) as conn:
|
||||||
|
row = fwdb.get_by_id(conn, fw_id)
|
||||||
|
if row is None:
|
||||||
|
raise HTTPException(404)
|
||||||
|
return templates.TemplateResponse("firmware_detail.html", {
|
||||||
|
"request": request,
|
||||||
|
"fw": row,
|
||||||
|
"firmware_root": FIRMWARE_ROOT,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- File Browser (sandboxed auf FIRMWARE_ROOT) --------------------
|
||||||
|
|
||||||
|
@app.get("/browse", response_class=HTMLResponse)
|
||||||
|
async def browse(request: Request, path: str = ""):
|
||||||
|
try:
|
||||||
|
target, entries = filebrowse.list_dir(FIRMWARE_ROOT, path)
|
||||||
|
except (filebrowse.PathEscapeError, FileNotFoundError, NotADirectoryError) as e:
|
||||||
|
raise HTTPException(400, str(e))
|
||||||
|
return templates.TemplateResponse("browse.html", {
|
||||||
|
"request": request,
|
||||||
|
"rel": path,
|
||||||
|
"entries": entries,
|
||||||
|
"crumbs": filebrowse.breadcrumbs(path),
|
||||||
|
"firmware_root": FIRMWARE_ROOT,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Workflows ------------------------------------------------------
|
||||||
|
|
||||||
|
@app.get("/workflows/p10lite", response_class=HTMLResponse)
|
||||||
|
async def workflow_p10lite(request: Request):
|
||||||
|
return templates.TemplateResponse("p10lite.html", {
|
||||||
|
"request": request,
|
||||||
|
"instructions": p10lite.erecovery_instructions(),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health():
|
||||||
|
return {"ok": True}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
// Live-Refresh für Container mit data-refresh="<url>" und data-interval="<ms>"
|
||||||
|
function startAutoRefresh() {
|
||||||
|
document.querySelectorAll("[data-refresh]").forEach((el) => {
|
||||||
|
const url = el.dataset.refresh;
|
||||||
|
const interval = parseInt(el.dataset.interval || "2000", 10);
|
||||||
|
const tick = async () => {
|
||||||
|
try {
|
||||||
|
const r = await fetch(url, { headers: { "Accept": "text/html" } });
|
||||||
|
if (r.ok) el.innerHTML = await r.text();
|
||||||
|
} catch (e) { /* netzkurz weg, nicht weiter schlimm */ }
|
||||||
|
};
|
||||||
|
tick();
|
||||||
|
setInterval(tick, interval);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forms mit data-action posten und Antwort in data-target rendern
|
||||||
|
function wireScanForms() {
|
||||||
|
document.querySelectorAll("form[data-action]").forEach((form) => {
|
||||||
|
form.addEventListener("submit", async (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
const btn = form.querySelector("button[type=submit]");
|
||||||
|
const target = document.querySelector(form.dataset.target);
|
||||||
|
btn.disabled = true;
|
||||||
|
if (target) target.textContent = "läuft…";
|
||||||
|
try {
|
||||||
|
const r = await fetch(form.dataset.action, { method: "POST" });
|
||||||
|
const j = await r.json().catch(() => null);
|
||||||
|
if (target) {
|
||||||
|
if (j) {
|
||||||
|
target.textContent = `gescannt: ${j.scanned} · neu: ${j.added} · aktualisiert: ${j.updated} · entfernt: ${j.removed}`;
|
||||||
|
} else {
|
||||||
|
target.textContent = r.ok ? "fertig" : "Fehler";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Seite neu laden, damit die Tabelle die neuen Einträge zeigt
|
||||||
|
if (r.ok) setTimeout(() => location.reload(), 800);
|
||||||
|
} catch (e) {
|
||||||
|
if (target) target.textContent = "Fehler: " + e;
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
startAutoRefresh();
|
||||||
|
wireScanForms();
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
:root {
|
||||||
|
--bg: #1c1f24;
|
||||||
|
--bg-card: #262a31;
|
||||||
|
--fg: #e6e6e6;
|
||||||
|
--muted: #8a8f98;
|
||||||
|
--accent: #6cb4ff;
|
||||||
|
--accent-hover: #8fc8ff;
|
||||||
|
--border: #353a44;
|
||||||
|
--ok: #7adf7a;
|
||||||
|
--warn: #ffb86c;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--fg);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 { margin: 0; font-size: 1.4rem; }
|
||||||
|
header h1 a { color: var(--fg); text-decoration: none; }
|
||||||
|
|
||||||
|
nav { display: flex; gap: 1.5rem; }
|
||||||
|
nav a { color: var(--muted); text-decoration: none; font-weight: 500; }
|
||||||
|
nav a:hover { color: var(--accent); }
|
||||||
|
|
||||||
|
main { max-width: 1200px; margin: 0 auto; padding: 2rem; }
|
||||||
|
|
||||||
|
footer {
|
||||||
|
text-align: center; padding: 2rem; color: var(--muted);
|
||||||
|
border-top: 1px solid var(--border); margin-top: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a { color: var(--accent); }
|
||||||
|
a:hover { color: var(--accent-hover); }
|
||||||
|
|
||||||
|
code, pre {
|
||||||
|
font-family: "JetBrains Mono", Menlo, Consolas, monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
background: var(--bg-card);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow-x: auto;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
code { background: var(--bg-card); padding: 0.1em 0.4em; border-radius: 3px; }
|
||||||
|
|
||||||
|
.cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
.card h2 { margin-top: 0; font-size: 1rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; }
|
||||||
|
.card .big { font-size: 2.5rem; font-weight: 700; margin: 0.5rem 0; }
|
||||||
|
|
||||||
|
.info { margin-top: 3rem; }
|
||||||
|
.info ul { list-style: none; padding: 0; }
|
||||||
|
.info li { margin: 0.5rem 0; }
|
||||||
|
|
||||||
|
table.grid {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
table.grid th, table.grid td {
|
||||||
|
text-align: left;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
table.grid th { background: rgba(255,255,255,0.04); font-weight: 600; }
|
||||||
|
table.grid tbody tr:last-child td { border-bottom: none; }
|
||||||
|
table.grid tbody tr:hover { background: rgba(255,255,255,0.03); }
|
||||||
|
|
||||||
|
table.kv { border-collapse: collapse; }
|
||||||
|
table.kv th { text-align: left; padding: 0.5rem 1rem 0.5rem 0; color: var(--muted); font-weight: 500; }
|
||||||
|
table.kv td { padding: 0.5rem 0; }
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: var(--accent);
|
||||||
|
color: #0d1117;
|
||||||
|
border: 0;
|
||||||
|
padding: 0.6rem 1.2rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
button:hover { background: var(--accent-hover); }
|
||||||
|
button:disabled { opacity: 0.5; cursor: wait; }
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px dashed var(--border);
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--muted);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted { color: var(--muted); }
|
||||||
|
|
||||||
|
.crumbs {
|
||||||
|
background: var(--bg-card);
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
font-family: "JetBrains Mono", Menlo, monospace;
|
||||||
|
}
|
||||||
|
.crumbs a { color: var(--fg); text-decoration: none; }
|
||||||
|
.crumbs a:hover { color: var(--accent); }
|
||||||
|
|
||||||
|
#scan-result { margin-left: 1rem; color: var(--muted); }
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
{% if not devices %}
|
||||||
|
<p class="empty">Kein bekanntes Hersteller-Gerät am USB.</p>
|
||||||
|
{% else %}
|
||||||
|
<table class="grid">
|
||||||
|
<thead>
|
||||||
|
<tr><th>Bus:Addr</th><th>VID:PID</th><th>Hersteller</th><th>Modus</th><th>Hinweis</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for d in devices %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ "%03d"|format(d.bus) }}:{{ "%03d"|format(d.address) }}</td>
|
||||||
|
<td><code>{{ "%04x"|format(d.vid) }}:{{ "%04x"|format(d.pid) }}</code></td>
|
||||||
|
<td>{{ d.mode.vendor }}</td>
|
||||||
|
<td><strong>{{ d.mode.label }}</strong></td>
|
||||||
|
<td>{{ d.mode.notes }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>{% block title %}aubox{% endblock %}</title>
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1><a href="/">aubox</a></h1>
|
||||||
|
<nav>
|
||||||
|
<a href="/">Übersicht</a>
|
||||||
|
<a href="/devices">Geräte</a>
|
||||||
|
<a href="/firmware">Firmware</a>
|
||||||
|
<a href="/browse">Dateien</a>
|
||||||
|
<a href="/workflows/p10lite">P10 Lite</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
<small>aubox · lokale Web-UI</small>
|
||||||
|
</footer>
|
||||||
|
<script src="/static/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Dateien · aubox{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>Datei-Browser</h2>
|
||||||
|
<p class="muted">Sandboxed auf <code>{{ firmware_root }}</code> — Path-Traversal blockiert.</p>
|
||||||
|
|
||||||
|
<nav class="crumbs">
|
||||||
|
{% for label, p in crumbs %}
|
||||||
|
<a href="/browse?path={{ p }}">{{ label }}</a>
|
||||||
|
{% if not loop.last %} / {% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{% if not entries %}
|
||||||
|
<p class="empty">Verzeichnis ist leer.</p>
|
||||||
|
{% else %}
|
||||||
|
<table class="grid">
|
||||||
|
<thead><tr><th>Name</th><th>Typ</th><th>Größe</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for e in entries %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{% if e.is_dir %}
|
||||||
|
<a href="/browse?path={{ e.rel_path }}">{{ e.name }}/</a>
|
||||||
|
{% else %}
|
||||||
|
{{ e.name }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ "DIR" if e.is_dir else "FILE" }}</td>
|
||||||
|
<td>{{ e.size|humansize if not e.is_dir else "—" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Geräte · aubox{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>Angeschlossene Geräte</h2>
|
||||||
|
<p>Aktualisiert sich alle 2 Sekunden.</p>
|
||||||
|
<div id="devices" data-refresh="/api/devices/html" data-interval="2000">
|
||||||
|
Lade…
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Firmware · aubox{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>Firmware-Library</h2>
|
||||||
|
<p>Quelle: <code>{{ firmware_root }}</code></p>
|
||||||
|
|
||||||
|
<form id="scan-form" data-action="/firmware/scan" data-target="#scan-result">
|
||||||
|
<button type="submit">Library scannen</button>
|
||||||
|
<span id="scan-result"></span>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if not firmware %}
|
||||||
|
<p class="empty">Noch keine Einträge. Lege Firmware-Dateien unter
|
||||||
|
<code>{{ firmware_root }}</code> ab und klicke "Library scannen".</p>
|
||||||
|
{% else %}
|
||||||
|
<table class="grid">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Vendor</th><th>Modell</th><th>SoC</th><th>Region</th>
|
||||||
|
<th>Format</th><th>Größe</th><th>Pfad</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for fw in firmware %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ fw.vendor or "—" }}</td>
|
||||||
|
<td><a href="/firmware/{{ fw.id }}">{{ fw.model or "—" }}</a></td>
|
||||||
|
<td>{{ fw.soc or "—" }}</td>
|
||||||
|
<td>{{ fw.region or "—" }}</td>
|
||||||
|
<td>{{ fw.format }}</td>
|
||||||
|
<td>{{ fw.size|humansize }}</td>
|
||||||
|
<td><code>{{ fw.rel_path }}</code></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}{{ fw.model or fw.rel_path }} · aubox{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>{{ fw.model or "Unbekannt" }} <small>{{ fw.format }}</small></h2>
|
||||||
|
|
||||||
|
<table class="kv">
|
||||||
|
<tr><th>Pfad</th><td><code>{{ fw.rel_path }}</code></td></tr>
|
||||||
|
<tr><th>Vendor</th><td>{{ fw.vendor or "—" }}</td></tr>
|
||||||
|
<tr><th>Modell</th><td>{{ fw.model or "—" }}</td></tr>
|
||||||
|
<tr><th>SoC</th><td>{{ fw.soc or "—" }}</td></tr>
|
||||||
|
<tr><th>Region</th><td>{{ fw.region or "—" }}</td></tr>
|
||||||
|
<tr><th>Version</th><td>{{ fw.version or "—" }}</td></tr>
|
||||||
|
<tr><th>Größe</th><td>{{ fw.size|humansize }}</td></tr>
|
||||||
|
<tr><th>SHA-256</th><td><code>{{ fw.sha256 or "noch nicht berechnet" }}</code></td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% if fw.extra_json %}
|
||||||
|
<h3>Format-spezifische Daten</h3>
|
||||||
|
<pre>{{ fw.extra_json }}</pre>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p><a href="/firmware">← zurück</a></p>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}aubox · Übersicht{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<section class="cards">
|
||||||
|
<article class="card">
|
||||||
|
<h2>Geräte</h2>
|
||||||
|
<p class="big">{{ devices|length }}</p>
|
||||||
|
<p>{{ "angeschlossen" if devices else "keins erkannt" }}</p>
|
||||||
|
<p><a href="/devices">öffnen →</a></p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="card">
|
||||||
|
<h2>Firmware-Library</h2>
|
||||||
|
<p class="big">{{ fw_count }}</p>
|
||||||
|
<p>Einträge in der Datenbank</p>
|
||||||
|
<p><a href="/firmware">öffnen →</a></p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="card">
|
||||||
|
<h2>Dateien</h2>
|
||||||
|
<p>Sandbox-Browser für die Firmware-Library</p>
|
||||||
|
<p><a href="/browse">öffnen →</a></p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="card">
|
||||||
|
<h2>Huawei P10 Lite</h2>
|
||||||
|
<p>FRP-Removal · WAS-LX1 · Kirin 658</p>
|
||||||
|
<p><a href="/workflows/p10lite">öffnen →</a></p>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="info">
|
||||||
|
<h3>Aktive Pfade im Container</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Firmware:</strong> <code>{{ firmware_root }}</code></li>
|
||||||
|
<li><strong>Loader:</strong> <code>{{ loader_root }}</code></li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Huawei P10 Lite · aubox{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>Huawei P10 Lite — FRP-Removal</h2>
|
||||||
|
<p class="muted">WAS-LX1 · Kirin 658 · EMUI 8.x</p>
|
||||||
|
|
||||||
|
<h3>Methode 1: eRecovery + SD-Karte (empfohlen)</h3>
|
||||||
|
<pre>{{ instructions }}</pre>
|
||||||
|
|
||||||
|
<h3>Methode 2: Testpoint + Kirin DLOAD</h3>
|
||||||
|
<p>Über CLI vorbereitet, Web-UI-Button folgt:</p>
|
||||||
|
<pre>python -m aubox p10lite frp-remove --method dload-erase</pre>
|
||||||
|
<p>Voraussetzung: Loader-Files in <code>/loaders/kirin/kirin960_lite/</code>
|
||||||
|
(siehe Loader-README im Repo).</p>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Lokaler Start: docker compose up
|
||||||
|
# Web-UI: http://127.0.0.1:8080
|
||||||
|
#
|
||||||
|
# USB-Passthrough komplett, damit Re-Enumeration funktioniert
|
||||||
|
# (Kirin DLOAD -> Huawei Fastboot-D nach xloader).
|
||||||
|
|
||||||
|
services:
|
||||||
|
aubox:
|
||||||
|
build: .
|
||||||
|
container_name: aubox
|
||||||
|
ports:
|
||||||
|
# 127.0.0.1 only — Web-UI nicht ans LAN exponieren
|
||||||
|
- "127.0.0.1:8080:8080"
|
||||||
|
volumes:
|
||||||
|
# Firmware-Bibliothek: Host-Verzeichnis ./firmware -> /firmware im Container
|
||||||
|
- ./firmware:/firmware
|
||||||
|
# Loader-Files (read-only, sicherheitshalber)
|
||||||
|
- ./loaders:/loaders:ro
|
||||||
|
# USB-Geräte komplett, damit Hot-Plug + Re-Enumeration durchgeht
|
||||||
|
- /dev/bus/usb:/dev/bus/usb
|
||||||
|
device_cgroup_rules:
|
||||||
|
# USB-Major 189 — alle USB-Geräte für den Container freigeben
|
||||||
|
- 'c 189:* rmw'
|
||||||
|
environment:
|
||||||
|
AUBOX_FIRMWARE_ROOT: /firmware
|
||||||
|
AUBOX_LOADER_ROOT: /loaders
|
||||||
|
command: ["web", "--host", "0.0.0.0", "--port", "8080"]
|
||||||
|
restart: unless-stopped
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
# Huawei P10 Lite — FRP entfernen ohne IMEI / ohne Bootloader-Unlock
|
||||||
|
|
||||||
|
Modell: **WAS-LX1** (EU/EMEA), Kirin 658 (Hi3660 Lite), EMUI 8.x.
|
||||||
|
|
||||||
|
## Ausgangslage
|
||||||
|
|
||||||
|
- Google-Account in den FRP-Records vergessen
|
||||||
|
- Bootloader gelockt (Huawei hat 07/2018 die Codes abgeschaltet)
|
||||||
|
- Keine valide IMEI im NV-Speicher
|
||||||
|
- Gerät startet bei jedem Reset wieder im Setup-Wizard mit Account-Abfrage
|
||||||
|
- Bekannte Tricks (TalkBack-Browser, YouTube-Account-Switch) sind seit
|
||||||
|
Jahren von Google und Huawei gepatcht
|
||||||
|
|
||||||
|
## Drei Pfade, in dieser Reihenfolge probieren
|
||||||
|
|
||||||
|
### Pfad 1 — eRecovery + SD-Karte (empfohlen, kein Loader nötig)
|
||||||
|
|
||||||
|
Erfolgsquote bei intaktem Gerät: hoch. Risiko: niedrig.
|
||||||
|
Funktioniert *immer ohne* Bootloader-Unlock.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m aubox p10lite frp-remove --method erecovery
|
||||||
|
```
|
||||||
|
|
||||||
|
Das gibt dir die Schritt-für-Schritt-Anleitung aus. Kernpunkte nochmal:
|
||||||
|
|
||||||
|
1. SD-Karte FAT32, max. 32 GB
|
||||||
|
2. Ordner `dload` (klein!) anlegen
|
||||||
|
3. *Region-passende* `update.app` da rein. Bei Multi-Part auch die `.lst`.
|
||||||
|
4. Tasten: Power + Vol+ + Vol- gleichzeitig beim Einschalten
|
||||||
|
5. eRecovery erkennt SD und bietet Update an
|
||||||
|
|
||||||
|
Region-Codes im Dateinamen der update.app:
|
||||||
|
- C432 = EMEA (Deutschland/EU Open-Market)
|
||||||
|
- C636 = Asia Pacific
|
||||||
|
- C185 = Middle East
|
||||||
|
- C233 = Italien (TIM)
|
||||||
|
- usw.
|
||||||
|
|
||||||
|
**Wichtig:** Falsche Region = Anti-Rollback-Trigger oder Reject.
|
||||||
|
Im Zweifel die Region aus dem Aufkleber im Akkuschacht / hinter der
|
||||||
|
SIM-Schublade ablesen (steht als CLT bzw. C-Code drauf).
|
||||||
|
|
||||||
|
### Pfad 2 — Testpoint + Kirin DLOAD + frp-Erase
|
||||||
|
|
||||||
|
Wenn eRecovery nicht startet (defekte System-Partition o.ä.), der
|
||||||
|
nächste Schritt. Du machst das eh schon teilweise: Testpoint kurz auf
|
||||||
|
Masse legen, dann USB anstecken — Gerät erscheint als 12d1:1100.
|
||||||
|
|
||||||
|
**Was du brauchst:**
|
||||||
|
|
||||||
|
- `hisi-sec_usb_xloader.bin` (signierter Stage-1-Loader, Kirin 960 lite / 658)
|
||||||
|
- `usb_loader.bin` (Stage 2)
|
||||||
|
- Optional: `fastboot.bin` für volles Reflash
|
||||||
|
- Diese Files müssen passend zum WAS-LX1-Build sein. Aus einer
|
||||||
|
matching update.app extrahierbar (siehe `loaders/README.md`).
|
||||||
|
|
||||||
|
**Ablauf:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Loader-Files in loaders/kirin/kirin960_lite/ ablegen, dann:
|
||||||
|
python -m aubox p10lite frp-remove --method dload-erase
|
||||||
|
```
|
||||||
|
|
||||||
|
Was das Tool macht:
|
||||||
|
|
||||||
|
1. Wartet auf USB-VID:PID 12d1:1100 (Kirin DLOAD).
|
||||||
|
2. Sendet xloader via hisi-idt nach SRAM-Adresse 0x07012000.
|
||||||
|
3. Sendet usb_loader.
|
||||||
|
4. Gerät re-enumeriert als Huawei Fastboot-D (12d1:1052).
|
||||||
|
5. Zeigt dir den nächsten manuellen Schritt:
|
||||||
|
```
|
||||||
|
fastboot erase frp
|
||||||
|
fastboot reboot
|
||||||
|
```
|
||||||
|
|
||||||
|
**Warum noch manuell ab Schritt 5:**
|
||||||
|
Der `fastboot erase`-Aufruf hängt vom genauen Partitionsnamen ab
|
||||||
|
(`frp`, `frpcache`, manchmal nur in `userdata` enthalten). Beim
|
||||||
|
ersten echten Lauf solltest du erst `fastboot getvar all` machen
|
||||||
|
und das ptable prüfen, bevor du blind erase fährst.
|
||||||
|
|
||||||
|
### Pfad 3 — Kompletter Reflash via DLOAD
|
||||||
|
|
||||||
|
Wenn Pfad 2 nichts bringt (frp-Partition leer und Sperre bleibt =
|
||||||
|
Sperre liegt anderswo, evtl. in `persist` oder im Account-Manager-DB).
|
||||||
|
Dann hilft nur ein voller Wipe + Reflash via DLOAD und passender
|
||||||
|
update.app. Das macht das Tool noch nicht — kommt später.
|
||||||
|
|
||||||
|
## Was *garantiert* nicht funktioniert
|
||||||
|
|
||||||
|
- **TalkBack-Trick** im Setup-Wizard: Google hat das bei den
|
||||||
|
Sicherheits-Patches 2019+ entfernt. EMUI 8 mit Patch von 2019
|
||||||
|
oder neuer = tot.
|
||||||
|
- **YouTube-App-Update-Trick**: dito, gepatcht.
|
||||||
|
- **HiSuite-FRP-Reset**: braucht USB-Debugging, das im Setup aus ist.
|
||||||
|
- **OEM-Unlock-Code aus dem Internet** für 2018+ generierte Codes:
|
||||||
|
Server gegen, hat Huawei aktiv abgedreht.
|
||||||
|
- **Magisk-FRP-Module**: setzen gerootetes System voraus → Henne/Ei.
|
||||||
|
|
||||||
|
## IMEI-Frage
|
||||||
|
|
||||||
|
IMEI weg ist ein zweites, separates Problem. Auch wenn FRP entsperrt
|
||||||
|
ist, ohne IMEI hast du kein funktionierendes Mobilfunk-Modul.
|
||||||
|
Reparatur:
|
||||||
|
|
||||||
|
- NV-Backup vom selben Modell (WAS-LX1) → einfach restoren via
|
||||||
|
Potato_NV oder DC-Unlocker
|
||||||
|
- Ohne Backup: NV neu generieren mit dem auf dem Aufkleber
|
||||||
|
abgelesenen IMEI. Das Tool kann das später bekommen
|
||||||
|
(`aubox p10lite imei-write --imei <15 digits>`), aber dafür
|
||||||
|
muss erst der DLOAD-Pfad solide laufen.
|
||||||
|
|
||||||
|
## Quellen / Weiterlesen
|
||||||
|
|
||||||
|
- XDA WAS-LX1 Forum (alte Threads von 2018-2020 sind die Goldgrube)
|
||||||
|
- hovatek.com — Huawei-spezifische Tutorials
|
||||||
|
- Potato_NV (mkasu/potatonv auf GitHub)
|
||||||
|
- HCU-Client-Reverse-Engineering-Diskussionen (4PDA-Foren)
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Firmware-Library
|
||||||
|
|
||||||
|
Hier liegen die Firmware-Dateien, die aubox indexiert. Im Container ist
|
||||||
|
das Verzeichnis als `/firmware` gemountet.
|
||||||
|
|
||||||
|
## Layout-Vorschlag
|
||||||
|
|
||||||
|
Ablage frei wählbar — die Library scannt rekursiv und identifiziert über
|
||||||
|
Magic-Bytes, nicht über Pfad. Trotzdem hilfreich:
|
||||||
|
|
||||||
|
```
|
||||||
|
firmware/
|
||||||
|
├── huawei/
|
||||||
|
│ ├── WAS-LX1/
|
||||||
|
│ │ ├── EMUI8_C432_8.0.0.367.app
|
||||||
|
│ │ └── EMUI8_C432_8.0.0.367.app.sha256
|
||||||
|
│ └── EML-L29/
|
||||||
|
├── samsung/
|
||||||
|
│ └── SM-G960F_xx.tar.md5
|
||||||
|
├── mediatek/
|
||||||
|
└── firmware.db # ← SQLite-Index, wird beim ersten Scan erzeugt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Was wird erkannt
|
||||||
|
|
||||||
|
- **Huawei `update.app`**: Hardware-ID, Region, Section-Layout, BOOT/SYSTEM
|
||||||
|
vorhanden? — voll automatisch
|
||||||
|
- **MediaTek scatter**: aktuell nur Kandidaten-Erkennung über Filename
|
||||||
|
- **Samsung Odin (`*.tar.md5`)**: aktuell nur Kandidaten-Erkennung
|
||||||
|
|
||||||
|
Mehr Parser folgen, sobald der jeweilige Workflow gebraucht wird.
|
||||||
|
|
||||||
|
## Datenbank
|
||||||
|
|
||||||
|
Die SQLite-Datei (`firmware.db`) ist Teil der Library. Sie wandert mit
|
||||||
|
einem Backup oder Sync mit. Schema steht in `aubox/library/db.py`.
|
||||||
|
|
||||||
|
## Git und Größe
|
||||||
|
|
||||||
|
Standardmäßig sind die Binaries in `.gitignore` ausgenommen (typisch
|
||||||
|
2-8 GB pro update.app). Wenn du sie trotzdem ins Repo legen willst:
|
||||||
|
|
||||||
|
- **git-lfs** ist die einzig sinnvolle Option — `git lfs track "*.app"` etc.
|
||||||
|
- Direktes `git add` einer 4-GB-Datei macht den Repo-Pull für andere zur Hölle.
|
||||||
|
- Ein Backup-Repo extra für Firmware (LFS) ist sauberer als alles in einem.
|
||||||
|
|
||||||
|
Die `firmware.db` selbst ist klein (KB) und kann ohne LFS committet werden,
|
||||||
|
falls du das Inventory teilen willst — sie enthält nur Metadaten, nicht
|
||||||
|
die Binaries.
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
# Loader-Files
|
||||||
|
|
||||||
|
Hier kommen die hersteller-signierten Stage-Loader rein, ohne die im
|
||||||
|
Kirin-Download-Mode nichts geht. Diese Dateien sind **nicht** im
|
||||||
|
Repo — du musst sie selbst beschaffen. Drei Wege:
|
||||||
|
|
||||||
|
## Aus einer passenden update.app extrahieren (sauberster Weg)
|
||||||
|
|
||||||
|
Eine update.app ist ein Container, in dem alle Partitions-Images +
|
||||||
|
einige Loader liegen. Werkzeug: `splituapp` oder `Huawei Update Extractor`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install splituapp
|
||||||
|
splituapp -f update.app
|
||||||
|
```
|
||||||
|
|
||||||
|
In den extrahierten Files suchst du nach:
|
||||||
|
|
||||||
|
- `xloader.img` / `hisi-sec_usb_xloader.bin`
|
||||||
|
- `fastboot.img` -> wird zu `fastboot.bin`
|
||||||
|
- Manche Builds: `usb_loader.bin` separat
|
||||||
|
|
||||||
|
Vorteil: signiert + zur Hardware passend. Garantiert kompatibel.
|
||||||
|
|
||||||
|
## Aus Tool-Distributionen rausziehen
|
||||||
|
|
||||||
|
Tools wie HCU-Client, DC-Unlocker, manche Octopus-Plugins liefern
|
||||||
|
Loader-Sets im Installer mit. In `<install>/loaders/` oder
|
||||||
|
`<install>/data/`. Nach Filenamen wie oben suchen. Hash gegen einen
|
||||||
|
update.app-Auszug abgleichen, falls möglich — du willst kein
|
||||||
|
manipuliertes Binary auf den Phone-eMMC schreiben.
|
||||||
|
|
||||||
|
## Aus Forum-Mirrors
|
||||||
|
|
||||||
|
XDA-WAS-LX1-Threads, hovatek-Forum, 4PDA-Threads. Risiko: Manipuliert.
|
||||||
|
Immer SHA-256 gegen mehrere Quellen prüfen. Wenn nur eine Quelle, lass
|
||||||
|
es sein und nimm den update.app-Weg.
|
||||||
|
|
||||||
|
## Erwartete Struktur
|
||||||
|
|
||||||
|
```
|
||||||
|
loaders/
|
||||||
|
└── kirin/
|
||||||
|
└── kirin960_lite/ # für Kirin 658 (P10 Lite)
|
||||||
|
├── hisi-sec_usb_xloader.bin
|
||||||
|
├── usb_loader.bin
|
||||||
|
└── fastboot.bin # optional
|
||||||
|
```
|
||||||
|
|
||||||
|
Pro SoC eigenes Unterverzeichnis. Wenn du später z.B. einen Kirin 970
|
||||||
|
hinzufügst: `loaders/kirin/kirin970/`.
|
||||||
|
|
||||||
|
## Rechtliches
|
||||||
|
|
||||||
|
Diese Loader-Files sind urheberrechtlich Huawei. Aus *eigenen
|
||||||
|
Geräten*-Firmware extrahieren ist in DE/EU für private Reparatur
|
||||||
|
unproblematisch (Recht auf Reparatur, EU 2024/1799). Weiterverbreiten
|
||||||
|
ist es nicht — also: nicht ins Repo committen, nicht öffentlich
|
||||||
|
spiegeln. Das `loaders/`-Verzeichnis steht deshalb in `.gitignore`.
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
pyusb>=1.2.1
|
||||||
|
fastapi>=0.110
|
||||||
|
uvicorn[standard]>=0.27
|
||||||
|
jinja2>=3.1
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# aubox im Container starten, mit USB-Passthrough so dass Re-Enumeration
|
||||||
|
# (Kirin DLOAD -> Huawei Fastboot-D) korrekt durchläuft.
|
||||||
|
#
|
||||||
|
# Wichtig: Wir mounten /dev/bus/usb komplett (nicht ein einzelnes Device),
|
||||||
|
# damit nach Re-Enumeration das neue Device sichtbar ist. Dazu ist die
|
||||||
|
# device-cgroup-rule für USB (Major 189) nötig.
|
||||||
|
#
|
||||||
|
# udev-Regeln müssen auf dem HOST liegen, nicht im Container. Falls noch
|
||||||
|
# nicht installiert, einmalig: sudo bash scripts/setup-linux.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
IMAGE="${AUBOX_IMAGE:-aubox}"
|
||||||
|
|
||||||
|
# Image bauen, falls nicht vorhanden
|
||||||
|
if ! docker image inspect "$IMAGE" >/dev/null 2>&1; then
|
||||||
|
echo "Image $IMAGE nicht gefunden — baue..."
|
||||||
|
docker build -t "$IMAGE" "$PROJECT_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# USB-Major-Nummer für cgroup-rule
|
||||||
|
USB_MAJOR=189
|
||||||
|
|
||||||
|
exec docker run --rm -it \
|
||||||
|
--device-cgroup-rule="c ${USB_MAJOR}:* rmw" \
|
||||||
|
-v /dev/bus/usb:/dev/bus/usb \
|
||||||
|
-v "${PROJECT_DIR}/loaders:/app/loaders:ro" \
|
||||||
|
"$IMAGE" "$@"
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Setup für Debian/Ubuntu/Mint (inkl. Raspberry Pi OS). Muss als root laufen.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ $EUID -ne 0 ]]; then
|
||||||
|
echo "Bitte mit sudo aufrufen." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y \
|
||||||
|
android-tools-adb \
|
||||||
|
android-tools-fastboot \
|
||||||
|
libusb-1.0-0 \
|
||||||
|
libusb-1.0-0-dev \
|
||||||
|
python3 \
|
||||||
|
python3-pip \
|
||||||
|
python3-venv \
|
||||||
|
usbutils
|
||||||
|
|
||||||
|
# udev-Regel installieren, damit der normale User ohne sudo auf die
|
||||||
|
# Hersteller-spezifischen USB-Modi zugreifen kann (Kirin DLOAD, fastboot etc.)
|
||||||
|
install -m 0644 udev/51-android-unlock.rules /etc/udev/rules.d/
|
||||||
|
udevadm control --reload-rules
|
||||||
|
udevadm trigger
|
||||||
|
|
||||||
|
# Den aufrufenden User in die plugdev-Gruppe stecken
|
||||||
|
TARGET_USER="${SUDO_USER:-$USER}"
|
||||||
|
if [[ -n "$TARGET_USER" && "$TARGET_USER" != "root" ]]; then
|
||||||
|
usermod -aG plugdev "$TARGET_USER"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Python-Deps in eine venv im Projektverzeichnis
|
||||||
|
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
sudo -u "$TARGET_USER" python3 -m venv "$PROJECT_DIR/.venv"
|
||||||
|
sudo -u "$TARGET_USER" "$PROJECT_DIR/.venv/bin/pip" install -r "$PROJECT_DIR/requirements.txt"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Setup fertig. Einmal ab- und neu anmelden, damit plugdev greift."
|
||||||
|
echo "Dann: source .venv/bin/activate && python -m aubox detect"
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Huawei — alle Hersteller-Modi, plugdev-User darf rauf
|
||||||
|
# 12d1 = Huawei
|
||||||
|
SUBSYSTEM=="usb", ATTR{idVendor}=="12d1", MODE="0660", GROUP="plugdev"
|
||||||
|
|
||||||
|
# Kirin DLOAD (Download-Mode nach Testpoint)
|
||||||
|
# Typische PIDs: 1100 (DLOAD), 1052 (Fastboot-D), 3609 (manche Kirin)
|
||||||
|
SUBSYSTEM=="usb", ATTR{idVendor}=="12d1", ATTR{idProduct}=="1100", MODE="0660", GROUP="plugdev", SYMLINK+="kirin_dload"
|
||||||
|
SUBSYSTEM=="usb", ATTR{idVendor}=="12d1", ATTR{idProduct}=="1052", MODE="0660", GROUP="plugdev", SYMLINK+="huawei_fastbootd"
|
||||||
|
SUBSYSTEM=="usb", ATTR{idVendor}=="12d1", ATTR{idProduct}=="3609", MODE="0660", GROUP="plugdev", SYMLINK+="kirin_dload_alt"
|
||||||
|
|
||||||
|
# Generisch: Android Fastboot (18d1 = Google)
|
||||||
|
SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", MODE="0660", GROUP="plugdev"
|
||||||
|
|
||||||
|
# MediaTek Preloader / BROM (für später)
|
||||||
|
SUBSYSTEM=="usb", ATTR{idVendor}=="0e8d", MODE="0660", GROUP="plugdev"
|
||||||
|
|
||||||
|
# Qualcomm 9008 EDL (für später)
|
||||||
|
SUBSYSTEM=="usb", ATTR{idVendor}=="05c6", ATTR{idProduct}=="9008", MODE="0660", GROUP="plugdev"
|
||||||
Loading…
Reference in New Issue