#!/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 import usb.util 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 _fb_read_status(ep_in, timeout_ms=5000): while True: resp = bytes(ep_in.read(64, timeout=timeout_ms)) if resp.startswith(b"INFO"): continue return resp def _fb_send_edl(fb_dev): cfg = fb_dev.get_active_configuration() intf = cfg[(0, 0)] ep_out = usb.util.find_descriptor( intf, custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_OUT, ) ep_in = usb.util.find_descriptor( intf, custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_IN, ) if ep_out is None or ep_in is None: return False for cmd in (b"oem reboot-edl", b"oem edl"): ep_out.write(cmd, timeout=1000) try: resp = _fb_read_status(ep_in, timeout_ms=1500) if resp.startswith(b"OKAY"): break except Exception: break return True def _fb_upgrade_lk2nd_and_edl(fb_dev): lk2nd_path = os.path.join(BASE_DIR, "bootloader", "lk2nd-msm8909.img") if not os.path.isfile(lk2nd_path): return with open(lk2nd_path, "rb") as f: lk2nd_data = f.read() print("[*] Updating lk2nd on boot partition via Fastboot to enable EDL reboot...") cfg = fb_dev.get_active_configuration() intf = cfg[(0, 0)] ep_out = usb.util.find_descriptor( intf, custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_OUT, ) ep_in = usb.util.find_descriptor( intf, custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_IN, ) if ep_out is None or ep_in is None: return ep_out.write(f"download:{len(lk2nd_data):08x}".encode("ascii"), timeout=2000) resp = _fb_read_status(ep_in, timeout_ms=2000) if not resp.startswith(b"DATA"): return chunk_sz = 16384 for off in range(0, len(lk2nd_data), chunk_sz): ep_out.write(lk2nd_data[off:off + chunk_sz], timeout=5000) if not _fb_read_status(ep_in, timeout_ms=5000).startswith(b"OKAY"): return ep_out.write(b"flash:boot", timeout=2000) if not _fb_read_status(ep_in, timeout_ms=10000).startswith(b"OKAY"): return ep_out.write(b"reboot-bootloader", timeout=2000) try: _fb_read_status(ep_in, timeout_ms=2000) except Exception: pass usb.util.dispose_resources(fb_dev) time.sleep(2.0) for _ in range(20): new_fb = usb.core.find(idVendor=0x18D1, idProduct=0xD00D) if new_fb is not None: time.sleep(0.3) _fb_send_edl(new_fb) usb.util.dispose_resources(new_fb) return time.sleep(0.5) _fb_edl_kicked = False def try_kick_into_edl(): global _fb_edl_kicked 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) return except Exception: pass # Direct USB Fastboot check (18d1:d00d) via pyusb so host fastboot CLI is optional try: fb_dev = usb.core.find(idVendor=0x18D1, idProduct=0xD00D) if fb_dev is not None: print("[*] Detected device in Fastboot mode (18d1:d00d). Sending 'oem reboot-edl'...") _fb_send_edl(fb_dev) usb.util.dispose_resources(fb_dev) 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 reboot-edl'...") rc = subprocess.run(["fastboot", "oem", "reboot-edl"], timeout=5) if rc.returncode != 0: subprocess.run(["fastboot", "oem", "edl"], timeout=5) except Exception: pass def wait_for_edl_usb(vid, pid, device_label, timeout_sec=0): cdc = usb_class(vid=vid, pid=pid) 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) From lk2nd Fastboot menu, select 'EDL' (or connect in Fastboot mode)") print(" 2) Power off, hold D-Pad Up + D-Pad Down, and plug in USB") print(" 3) Plug in via an EDL deep-flash cable") print(" 4) Run 'adb reboot edl' or 'fastboot oem reboot-edl'") try_kick_into_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.05) 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 % 100 == 0: try_kick_into_edl() if dots % 20 == 0: sys.stdout.write(".") sys.stdout.flush() dots += 1 if dots % 800 == 0: sys.stdout.write("\n") time.sleep(0.05) 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): return os.path.join(BASE_DIR, "images", f"bananarchy-{device}.img") 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()