481 lines
14 KiB
Python
Executable File
481 lines
14 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Automated Qualcomm EDL Sahara/Firehose Flasher for Bananarchy Linux (MSM8909)."""
|
|
|
|
import argparse
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import usb.core
|
|
|
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, os.path.join(BASE_DIR, "edl"))
|
|
|
|
from Library.usb import usb_class
|
|
from Library.gpt import gpt
|
|
from Library.sahara import qualcomm_sahara
|
|
from Library.firehose import qualcomm_firehose, xmlparser
|
|
|
|
|
|
DEVICE_LABELS = {
|
|
"nokia-argon": "Nokia 8110 4G (nokia-argon)",
|
|
"nokia-beatles": "Nokia 2720 Flip (nokia-beatles)",
|
|
"nokia-tough": "Nokia 800 Tough (nokia-coconut)",
|
|
"nokia-sparkler": "Nokia 8000 4G (nokia-sparkler)",
|
|
"nokia-leo": "Nokia 6300 4G (nokia-leo)",
|
|
}
|
|
|
|
|
|
def try_kick_into_edl():
|
|
try:
|
|
res = subprocess.run(
|
|
["adb", "get-state"],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.DEVNULL,
|
|
text=True,
|
|
timeout=2,
|
|
)
|
|
if res.returncode == 0 and "device" in res.stdout:
|
|
print("[*] Detected device in ADB mode. Sending 'adb reboot edl'...")
|
|
subprocess.run(["adb", "reboot", "edl"], timeout=5)
|
|
time.sleep(1)
|
|
return
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
res = subprocess.run(
|
|
["fastboot", "devices"],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.DEVNULL,
|
|
text=True,
|
|
timeout=2,
|
|
)
|
|
if res.returncode == 0 and res.stdout.strip():
|
|
print("[*] Detected device in Fastboot mode. Sending 'fastboot oem edl'...")
|
|
subprocess.run(["fastboot", "oem", "edl"], timeout=5)
|
|
time.sleep(1)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def wait_for_edl_usb(vid, pid, device_label, timeout_sec=0):
|
|
cdc = usb_class(vid=vid, pid=pid)
|
|
try_kick_into_edl()
|
|
|
|
print(
|
|
f"[*] Waiting for {device_label} in EDL mode "
|
|
f"(USB {vid:04x}:{pid:04x} Qualcomm QDLoader 9008)..."
|
|
)
|
|
print(" To enter EDL mode on the phone, use one of:")
|
|
print(" 1) Power off, hold D-Pad Up + D-Pad Down, and plug in USB")
|
|
print(" 2) Plug in via an EDL deep-flash cable")
|
|
print(" 3) Run 'adb reboot edl' or 'fastboot oem edl'")
|
|
|
|
start_ts = time.time()
|
|
dots = 0
|
|
while not cdc.connected:
|
|
try:
|
|
cdc.connected = cdc.connect()
|
|
except usb.core.USBError as exc:
|
|
if getattr(exc, "errno", None) == 13:
|
|
print(
|
|
"\n[!] Error: USB permission denied when opening 05c6:9008. "
|
|
"Please run via sudo (e.g. sudo ./flash-edl.sh)."
|
|
)
|
|
sys.exit(1)
|
|
time.sleep(0.5)
|
|
continue
|
|
|
|
if cdc.connected:
|
|
if dots > 0:
|
|
sys.stdout.write("\n")
|
|
print("[+] USB EDL device connected.")
|
|
return cdc
|
|
|
|
if timeout_sec > 0 and (time.time() - start_ts) >= timeout_sec:
|
|
if dots > 0:
|
|
sys.stdout.write("\n")
|
|
print(f"[!] Timed out after {timeout_sec} seconds waiting for EDL device.")
|
|
sys.exit(1)
|
|
|
|
if dots > 0 and dots % 10 == 0:
|
|
try_kick_into_edl()
|
|
|
|
sys.stdout.write(".")
|
|
sys.stdout.flush()
|
|
dots += 1
|
|
if dots % 40 == 0:
|
|
sys.stdout.write("\n")
|
|
time.sleep(1)
|
|
|
|
return cdc
|
|
|
|
|
|
def find_partition(partentries, name):
|
|
for entry in partentries:
|
|
if entry.name == name:
|
|
return entry
|
|
return None
|
|
|
|
|
|
def write_partition(fh, lun, part, filepath):
|
|
size = os.stat(filepath).st_size
|
|
sector_size = fh.cfg.SECTOR_SIZE_IN_BYTES
|
|
sectors = size // sector_size
|
|
if (size % sector_size) > 0:
|
|
sectors += 1
|
|
|
|
if sectors > part.sectors:
|
|
print(
|
|
f"[!] Error: {filepath} requires {sectors} sectors, "
|
|
f"but partition '{part.name}' only has {part.sectors} sectors."
|
|
)
|
|
return False
|
|
|
|
print(
|
|
f"[*] Flashing '{part.name}' (start sector {part.sector}, "
|
|
f"{sectors}/{part.sectors} sectors, {size / (1024 * 1024):.1f} MiB) "
|
|
f"from {filepath}..."
|
|
)
|
|
ok = fh.cmd_write(lun, part.sector, filepath, Display=True)
|
|
if not ok:
|
|
print(f"[!] Failed writing {filepath} to partition '{part.name}'.")
|
|
return False
|
|
print(f"[+] Successfully flashed '{part.name}'.")
|
|
return True
|
|
|
|
|
|
def dump_partition(fh, lun, part, filepath):
|
|
size_mib = (part.sectors * fh.cfg.SECTOR_SIZE_IN_BYTES) / (1024 * 1024)
|
|
print(
|
|
f"[*] Backing up '{part.name}' (sector {part.sector}, "
|
|
f"{part.sectors} sectors, {size_mib:.2f} MiB) -> {filepath}..."
|
|
)
|
|
res = fh.cmd_read(lun, part.sector, part.sectors, filepath, Display=True)
|
|
if res == "" or not os.path.exists(filepath) or os.stat(filepath).st_size == 0:
|
|
print(f"[!] Failed backing up partition '{part.name}'.")
|
|
return False
|
|
print(f"[+] Saved backup: {filepath}")
|
|
return True
|
|
|
|
|
|
def resolve_default_loader():
|
|
for cand in [
|
|
os.path.join(BASE_DIR, "bootloader", "loaders", "8110.mbn"),
|
|
os.path.join(BASE_DIR, "8110.mbn"),
|
|
]:
|
|
if os.path.isfile(cand):
|
|
return cand
|
|
return os.path.join(BASE_DIR, "bootloader", "loaders", "8110.mbn")
|
|
|
|
|
|
def resolve_default_bootloader():
|
|
for cand in [
|
|
os.path.join(BASE_DIR, "bootloader", "lk2nd-msm8909.img"),
|
|
os.path.join(BASE_DIR, "lk2nd-msm8909.img"),
|
|
]:
|
|
if os.path.isfile(cand):
|
|
return cand
|
|
return os.path.join(BASE_DIR, "bootloader", "lk2nd-msm8909.img")
|
|
|
|
|
|
def resolve_default_image(device):
|
|
candidates = [
|
|
os.path.join(BASE_DIR, "images", f"bananarchy-{device}.img"),
|
|
os.path.join(BASE_DIR, "images", "nokia-argon-sxmo.img"),
|
|
]
|
|
for cand in candidates:
|
|
if os.path.isfile(cand):
|
|
return cand
|
|
return candidates[0]
|
|
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser(
|
|
description="Automated EDL Flasher for Bananarchy Linux (MSM8909 / Sxmo)"
|
|
)
|
|
parser.add_argument(
|
|
"device_pos",
|
|
nargs="?",
|
|
choices=[
|
|
"sxmo",
|
|
"nokia-argon",
|
|
"nokia-beatles",
|
|
"nokia-tough",
|
|
"nokia-sparkler",
|
|
"nokia-leo",
|
|
],
|
|
default=None,
|
|
help="Target device to flash (default: nokia-argon)",
|
|
)
|
|
parser.add_argument(
|
|
"--device",
|
|
choices=[
|
|
"nokia-argon",
|
|
"nokia-beatles",
|
|
"nokia-tough",
|
|
"nokia-sparkler",
|
|
"nokia-leo",
|
|
],
|
|
default="nokia-argon",
|
|
help="Target MSM8909 device (default: nokia-argon)",
|
|
)
|
|
parser.add_argument(
|
|
"--image",
|
|
default="",
|
|
help="Explicit path to raw userdata image (overrides --device)",
|
|
)
|
|
parser.add_argument(
|
|
"--bootloader",
|
|
default=resolve_default_bootloader(),
|
|
help="Path to lk2nd boot image",
|
|
)
|
|
parser.add_argument(
|
|
"--loader",
|
|
default=resolve_default_loader(),
|
|
help="Path to Firehose programmer (.mbn)",
|
|
)
|
|
parser.add_argument(
|
|
"--backup-dir",
|
|
default=os.path.join(BASE_DIR, "backup"),
|
|
help="Directory for partition backups (default: ./backup)",
|
|
)
|
|
parser.add_argument(
|
|
"--skip-backup",
|
|
action="store_true",
|
|
help="Skip backing up critical partitions before flashing",
|
|
)
|
|
parser.add_argument(
|
|
"--force-backup",
|
|
action="store_true",
|
|
help="Re-dump backup partitions even if backup files already exist",
|
|
)
|
|
parser.add_argument(
|
|
"--backup-modem",
|
|
action="store_true",
|
|
help="Also back up the 64 MiB modem partition into backup-dir",
|
|
)
|
|
parser.add_argument(
|
|
"--no-wipe-system",
|
|
action="store_true",
|
|
help="Do not zero the stock system partition superblock",
|
|
)
|
|
parser.add_argument(
|
|
"--no-reset",
|
|
action="store_true",
|
|
help="Do not reboot the phone automatically after flashing",
|
|
)
|
|
parser.add_argument(
|
|
"--timeout",
|
|
type=int,
|
|
default=0,
|
|
help="Timeout in seconds while waiting for USB connection (0 = infinite)",
|
|
)
|
|
parser.add_argument(
|
|
"--vid",
|
|
default="0x05C6",
|
|
help="USB Vendor ID (default: 0x05C6)",
|
|
)
|
|
parser.add_argument(
|
|
"--pid",
|
|
default="0x9008",
|
|
help="USB Product ID (default: 0x9008)",
|
|
)
|
|
parser.add_argument(
|
|
"--lun",
|
|
type=int,
|
|
default=0,
|
|
help="Storage LUN number (default: 0)",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def main():
|
|
args = parse_args()
|
|
device = args.device_pos if args.device_pos and args.device_pos != "sxmo" else args.device
|
|
device_label = DEVICE_LABELS.get(device, device)
|
|
userdata_img = (
|
|
os.path.abspath(args.image)
|
|
if args.image
|
|
else os.path.abspath(resolve_default_image(device))
|
|
)
|
|
bootloader_img = os.path.abspath(args.bootloader)
|
|
loader_mbn = os.path.abspath(args.loader)
|
|
backup_dir = os.path.abspath(args.backup_dir)
|
|
|
|
print("============================================================")
|
|
print(" Bananarchy Linux (MSM8909 / Sxmo) Automated EDL Flasher")
|
|
print("============================================================")
|
|
print(f" Target Device : {device_label}")
|
|
print(f" Target UI : Sxmo (240x320 native, Multi-Mode T9)")
|
|
print(f" Firehose Loader : {loader_mbn}")
|
|
print(f" Bootloader (boot): {bootloader_img}")
|
|
print(f" System (userdata): {userdata_img}")
|
|
print(
|
|
f" Partition Backup: "
|
|
f"{'disabled (--skip-backup)' if args.skip_backup else backup_dir}"
|
|
)
|
|
print("============================================================")
|
|
|
|
for label, path in [
|
|
("Firehose programmer", loader_mbn),
|
|
("lk2nd bootloader image", bootloader_img),
|
|
("System userdata image", userdata_img),
|
|
]:
|
|
if not os.path.isfile(path) or os.stat(path).st_size == 0:
|
|
print(f"[!] Error: {label} not found or empty at: {path}")
|
|
sys.exit(1)
|
|
|
|
vid = int(args.vid, 16)
|
|
pid = int(args.pid, 16)
|
|
cdc = wait_for_edl_usb(vid, pid, device_label, timeout_sec=args.timeout)
|
|
|
|
sahara = qualcomm_sahara(cdc)
|
|
with open(loader_mbn, "rb") as rf:
|
|
sahara.programmer = rf.read()
|
|
|
|
mode = sahara.connect()
|
|
print(f"[*] Device protocol mode: {mode}")
|
|
|
|
if mode == "Sahara":
|
|
sahara.info()
|
|
print(f"[*] Uploading Firehose programmer ({os.path.basename(loader_mbn)})...")
|
|
if not sahara.upload_firehoseloader():
|
|
print("[!] Error: Failed to upload Firehose programmer via Sahara.")
|
|
sys.exit(1)
|
|
time.sleep(0.3)
|
|
mode = "Firehose"
|
|
print("[+] Firehose programmer uploaded and active.")
|
|
elif mode == "Firehose":
|
|
if len(sahara.programmer) > 4 and sahara.programmer[:4] == b"\x7fELF":
|
|
sahara.bit64 = sahara.programmer[4] == 2
|
|
else:
|
|
sahara.bit64 = False
|
|
print("[+] Device is already in Firehose mode.")
|
|
else:
|
|
print(
|
|
f"[!] Error: Could not establish Sahara or Firehose mode (got '{mode}'). "
|
|
"Please power-cycle the phone back into EDL mode and retry."
|
|
)
|
|
sys.exit(1)
|
|
|
|
xml = xmlparser()
|
|
cfg = qualcomm_firehose.cfg()
|
|
cfg.MemoryName = "eMMC"
|
|
cfg.ZLPAwareHost = 1
|
|
cfg.SkipStorageInit = False
|
|
cfg.SkipWrite = False
|
|
cfg.MaxPayloadSizeToTargetInBytes = 1048576
|
|
cfg.SECTOR_SIZE_IN_BYTES = 512
|
|
cfg.bit64 = sahara.bit64
|
|
|
|
fh = qualcomm_firehose(cdc, xml, cfg)
|
|
print("[*] Connecting to Firehose storage controller...")
|
|
fh.connect(0)
|
|
|
|
print("[*] Reading GPT partition table from eMMC...")
|
|
gpt_sectors = 0x4000 // fh.cfg.SECTOR_SIZE_IN_BYTES
|
|
gpt_raw = fh.cmd_read_buffer(args.lun, 0, gpt_sectors, Display=False)
|
|
if not gpt_raw:
|
|
print("[!] Error: Failed to read GPT partition table from device.")
|
|
sys.exit(1)
|
|
|
|
guid_gpt = gpt()
|
|
if not guid_gpt.parse(gpt_raw, fh.cfg.SECTOR_SIZE_IN_BYTES):
|
|
print("[!] Error: Failed to parse GPT partition table.")
|
|
sys.exit(1)
|
|
|
|
boot_part = find_partition(guid_gpt.partentries, "boot")
|
|
userdata_part = find_partition(guid_gpt.partentries, "userdata")
|
|
system_part = find_partition(guid_gpt.partentries, "system")
|
|
|
|
if not boot_part:
|
|
print("[!] Error: Partition 'boot' not found in device GPT.")
|
|
sys.exit(1)
|
|
if not userdata_part:
|
|
print("[!] Error: Partition 'userdata' not found in device GPT.")
|
|
sys.exit(1)
|
|
|
|
print(
|
|
f"[+] Found 'boot' : sector {boot_part.sector}, "
|
|
f"{boot_part.sectors} sectors "
|
|
f"({(boot_part.sectors * fh.cfg.SECTOR_SIZE_IN_BYTES) / (1024 * 1024):.1f} MiB)"
|
|
)
|
|
print(
|
|
f"[+] Found 'userdata' : sector {userdata_part.sector}, "
|
|
f"{userdata_part.sectors} sectors "
|
|
f"({(userdata_part.sectors * fh.cfg.SECTOR_SIZE_IN_BYTES) / (1024 * 1024):.1f} MiB)"
|
|
)
|
|
|
|
if not args.skip_backup:
|
|
os.makedirs(backup_dir, exist_ok=True)
|
|
gpt_backup_path = os.path.join(backup_dir, "gpt.bin")
|
|
if args.force_backup or not os.path.exists(gpt_backup_path):
|
|
with open(gpt_backup_path, "wb") as wf:
|
|
wf.write(gpt_raw)
|
|
print(f"[+] Saved GPT backup: {gpt_backup_path}")
|
|
|
|
backup_parts = ["fsc", "fsg", "modemst1", "modemst2", "persist", "boot"]
|
|
if args.backup_modem:
|
|
backup_parts.append("modem")
|
|
|
|
for pname in backup_parts:
|
|
part = find_partition(guid_gpt.partentries, pname)
|
|
if not part:
|
|
print(f"[!] Warning: Partition '{pname}' not found in GPT; skipping backup.")
|
|
continue
|
|
dest_file = os.path.join(backup_dir, f"{pname}.img")
|
|
if (
|
|
not args.force_backup
|
|
and os.path.exists(dest_file)
|
|
and os.stat(dest_file).st_size > 0
|
|
):
|
|
print(f"[*] Backup already exists for '{pname}' ({dest_file}); skipping.")
|
|
continue
|
|
if not dump_partition(fh, args.lun, part, dest_file):
|
|
sys.exit(1)
|
|
|
|
print("\n[*] Step 1/3: Flashing lk2nd secondary bootloader to 'boot'...")
|
|
if not write_partition(fh, args.lun, boot_part, bootloader_img):
|
|
sys.exit(1)
|
|
|
|
print(f"\n[*] Step 2/3: Flashing Bananarchy Linux ({device}) image to 'userdata'...")
|
|
if not write_partition(fh, args.lun, userdata_part, userdata_img):
|
|
sys.exit(1)
|
|
|
|
if not args.no_wipe_system and system_part:
|
|
print(
|
|
"\n[*] Step 3/3: Clearing stock 'system' superblock "
|
|
"(first 64 sectors / 32 KiB)..."
|
|
)
|
|
with tempfile.NamedTemporaryFile(prefix="wipe-system-", delete=False) as tf:
|
|
tf.write(b"\x00" * (64 * fh.cfg.SECTOR_SIZE_IN_BYTES))
|
|
zero_path = tf.name
|
|
try:
|
|
if not fh.cmd_write(args.lun, system_part.sector, zero_path, Display=False):
|
|
print("[!] Warning: Could not clear 'system' superblock; continuing.")
|
|
else:
|
|
print("[+] Cleared stock 'system' superblock.")
|
|
finally:
|
|
if os.path.exists(zero_path):
|
|
os.unlink(zero_path)
|
|
else:
|
|
print("\n[*] Step 3/3: Skipping 'system' superblock wipe.")
|
|
|
|
if not args.no_reset:
|
|
print(f"\n[*] Resetting and booting {device_label}...")
|
|
fh.cmd_reset()
|
|
|
|
print("\n============================================================")
|
|
print(" BANANARCHY EDL FLASHING COMPLETED SUCCESSFULLY")
|
|
print("============================================================")
|
|
print(" The phone will now boot via lk2nd into Bananarchy Linux (Sxmo).")
|
|
print(" Default login: user / 147147 | USB SSH: ssh user@172.16.42.1")
|
|
print("============================================================")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|