{"id":51631914,"url":"https://github.com/ajsb85/sprdflash-rs","last_synced_at":"2026-07-17T12:01:56.792Z","repository":{"id":370939764,"uuid":"1297893192","full_name":"ajsb85/sprdflash-rs","owner":"ajsb85","description":"Pure-Rust cross-platform (Windows/WSL) flasher for SPRD/UNISOC .pac firmware (RDA8910/UIS8910), built for the manufacturing line","archived":false,"fork":false,"pushed_at":"2026-07-12T13:02:59.000Z","size":170,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-14T09:31:40.981Z","etag":null,"topics":["air724ug","cli","cross-platform","embedded","firmware-flasher","manufacturing","mes","pac","rda8910","reverse-engineering","rust","serial","spreadtrum","trunk-based-development","uis8910","unisoc","usb"],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ajsb85.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-07-12T01:19:54.000Z","updated_at":"2026-07-13T20:31:27.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ajsb85/sprdflash-rs","commit_stats":null,"previous_names":["ajsb85/sprdflash-rs"],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/ajsb85/sprdflash-rs","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ajsb85%2Fsprdflash-rs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ajsb85%2Fsprdflash-rs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ajsb85%2Fsprdflash-rs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ajsb85%2Fsprdflash-rs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ajsb85","download_url":"https://codeload.github.com/ajsb85/sprdflash-rs/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ajsb85%2Fsprdflash-rs/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35499864,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-15T02:00:06.706Z","response_time":131,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["air724ug","cli","cross-platform","embedded","firmware-flasher","manufacturing","mes","pac","rda8910","reverse-engineering","rust","serial","spreadtrum","trunk-based-development","uis8910","unisoc","usb"],"created_at":"2026-07-13T08:30:59.497Z","updated_at":"2026-07-15T10:01:17.580Z","avatar_url":"https://github.com/ajsb85.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# SPRD Flash Tool\n\n[![CI](https://github.com/ajsb85/sprdflash-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/ajsb85/sprdflash-rs/actions/workflows/ci.yml)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n[![Rust 2024](https://img.shields.io/badge/rust-2024_edition-orange.svg)](https://doc.rust-lang.org/edition-guide/rust-2024/index.html)\n[![MSRV 1.85](https://img.shields.io/badge/MSRV-1.85-blue.svg)](https://blog.rust-lang.org/)\n[![Platforms](https://img.shields.io/badge/platforms-Windows_%7C_Linux_%7C_macOS-informational.svg)](#releases)\n[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://www.conventionalcommits.org)\n[![unsafe: forbidden in core](https://img.shields.io/badge/unsafe-forbidden_in_core-success.svg)](crates/sprdflash-core)\n\nPure-Rust, cross-platform (Windows 11 + Linux/WSL) flasher for SPRD/UNISOC\n`.pac` firmware — RDA8910 / UIS8910 (Air724UG and friends). Built for a\n**manufacturing line**: fast, robust, and observable enough to run thousands of\nflashes per day across many fixtures.\n\nIt is a port of the hardware-verified Python\n[`sprdflash`](https://github.com/ajsb85/sprdflash), which reverse-engineered the\nfull download protocol (PDL first stage → BSL/FDL2 → partitions → cross-SDK\nformat) byte-for-byte from the vendor tool. This project keeps that protocol in a\n**sans-I/O core** so it stays deterministic and fully unit-testable, and layers a\nfast, recoverable transport and a station orchestrator on top.\n\n## Why Rust, and where the speed comes from\n\nThe wire, not the CPU, is the bottleneck. The levers, biggest first:\n\n1. **`CHANGE_BAUD` to the FDL's maximum.** The reference flasher runs the whole\n   6 MB transfer at 115200 (~42 s). FDL2 supports far higher rates; pushing to\n   921600–3 M is an **8–25× wire speedup** — the single biggest win.\n2. **One station per fixture.** Serial I/O is blocking and latency-sensitive, so\n   each fixture gets its own OS thread; throughput scales linearly with fixtures.\n3. **Bigger MIDST chunks + `DISABLE_TRANSCODE` (0x21)** on the bulk data phase —\n   fewer round-trips, no HDLC escape expansion.\n4. **`mmap` the PAC once**, `Arc`-share zero-copy payload slices to every station.\n\n## Robustness (industrial)\n\n- **Sans-I/O core** (`sprdflash-core`): `#![forbid(unsafe_code)]`, no hardware\n  needed to test — the protocol is verified against captured vendor bytes.\n- **Hardware-free end-to-end tests**: the byte stream sits behind a `Transport`\n  trait, and a `MockTransport` drives a whole PDL → BSL → partitions → format\n  flash — including adaptive-retry and read-back fault injection — with no device.\n- Per-phase **timeouts + bounded retries + automatic device recovery** (Windows\n  `pnputil` re-enumerate; Linux/WSL `usbip` re-attach / `uhubctl` power-cycle).\n- **Pre-flight PAC CRC** — a corrupt image is never flashed.\n- **Optional post-write read-back verify** (`--verify-readback`): every partition\n  is read back off the device with `READ_FLASH` and compared byte-for-byte,\n  failing on any mismatch — hardware-verified on an Air724UG (RDA8910).\n- **In-band recovery** (`sprdflash reset`): reboot a module stuck in FDL2 after an\n  aborted flash, no physical power-cycle.\n- **Per-unit JSON-lines records** for MES, `tracing` spans per station,\n  Prometheus metrics (yield, throughput, phase timings).\n- **Post-flash boot verify** (ATI/IMEI on the AT port), append-only audit log.\n- **Unattended continuous mode** (`--loop`): run the line all shift, stopping\n  cleanly on Ctrl-C after the in-flight units finish.\n- `Cargo.lock` committed, `panic = \"abort\"` + `overflow-checks = on` in release.\n\n## Workspace\n\n| crate                | status | role                                                          |\n|----------------------|--------|---------------------------------------------------------------|\n| `sprdflash-core`     | ✅ done | sans-I/O protocol: PAC parse, PDL + BSL framing, checksums, plan |\n| `sprdflash-cli`      | ✅ `info`, `list-ports`, `flash`, `line`, `reset`, `dump`, `clone`, `parts`, `layout` | the `sprdflash` binary |\n| `sprdflash-transport`| ✅ done | `Transport` trait over `serialport`, port discovery, beacon-window connect, recovery |\n| `sprdflash-flash`    | ✅ done | device driver: PDL→BSL→partitions→format→reset, `CHANGE_BAUD`, read-back verify, `MockTransport` |\n| `sprdflash-line`     | ✅ done | parallel stations, boot-verify (ATI/IMEI), JSON-lines records, metrics, continuous mode |\n\nThe core is validated byte-for-byte against the real V4035 PAC and the reference\nPython implementation. The full driver is **hardware-verified on a real Air724UG\n(RDA8910)**, both a same-SDK reflash and a cross-SDK `--format` change\n(LuatOS V4035 ⇄ CSDK V302340) that boots directly on the soft reset with IMEI\nand NV intact.\n\n### Measured speed (single device, 6 MB)\n\n| tool / config                | time   | notes                                       |\n|------------------------------|--------|---------------------------------------------|\n| Python `sprdflash` (528 B)   | ~42 s  | reference                                   |\n| `sprdflash-rs` (2048 B)      | ~33 s  | **22% faster**; default                     |\n\nThe device is **flash-write-bound** (~186 KiB/s): MIDST chunks above ~2 KB\noverflow the FDL2 receive buffer, and the per-frame overhead is already small at\n2 KB — so single-device time is near its floor. **Line throughput comes from\nrunning one station per fixture in parallel** (the `sprdflash-line` crate), not\nfrom squeezing a single device.\n\n## Build \u0026 test\n\n```\ncargo test            # 35 tests: captured ground truth + hardware-free end-to-end\ncargo build --release\n\n# one device, auto mode-switch, boots into the new firmware\nsprdflash flash --enter-download firmware.pac\n# cross-SDK change: format FS + refresh NV (keeps IMEI)\nsprdflash flash --enter-download --format firmware.pac\n# high-assurance: read every partition back and compare after writing\nsprdflash flash --enter-download --format --verify-readback firmware.pac\nsprdflash info firmware.pac\nsprdflash list-ports\nsprdflash reset       # reboot a device stuck in FDL2 after an aborted flash\n\n# extract every partition off a device (read-only) — byte-exact backup/clone\nsprdflash dump --enter-download --pac firmware.pac --out ./dump\n# whole-flash backup — auto-discovers the size, no partition layout needed\nsprdflash dump --full --enter-download --pac firmware.pac --out ./dump\n# reconstruct a proposed layout (\u003cBMAConfig\u003e) from that dump — offline, no PAC\nsprdflash layout --flash ./dump/flash.bin --out layout.xml\n# capture a reference unit into a flashable golden .pac\nsprdflash clone --enter-download --pac firmware.pac --out golden.pac\n```\n\n`layout` recovers the partition geometry from a whole-flash dump *alone*: it\nclassifies on-flash content — U-Boot `uImage` code regions (CRC-verified exact\nsizes), LuatOS `luadb`, and the NOR filesystem — and emits a proposed\n`\u003cBMAConfig\u003e`. Verified byte-exact against the real PAC on an Air724UG (5/5\nflash-resident partition bases + sizes). Region **names** are generic\ncontent-type defaults (`uimage_0`, `luadb_0`, …), not PAC roles — the flash\nstores no role names, and roles are firmware-specific.\n\n`dump` reads each partition straight off the flash with `READ_FLASH` and writes\none `\u003cfile_id\u003e.bin` per partition — no writes to the device. The PAC supplies the\nFDL stages and the partition layout; the dumped bytes are byte-identical\n(verified by SHA-256 on an Air724UG) to what was flashed. Pass `--region\nADDR:SIZE` (repeatable) to read arbitrary flash ranges instead of the PAC's\npartitions, or `--full` for a **whole-chip backup**: since the RDA8910 FDL2 has no\ngeometry command, the flash size is discovered by binary-searching `READ_FLASH`\n(an out-of-range read replies `INVALID_CMD`) — hardware-verified at 8 MiB, with\nevery partition matching its offset in the image. **Any RDA8910 PAC works for the\nFDL stages** — they're interchangeable across firmware versions for the same chip\n(hardware-verified), so you don't need the device's exact firmware to extract it.\n`clone` goes one step further: it\nsplices the dumped partitions into a copy of the reference PAC and refreshes the\nCRCs, producing a **flashable golden `.pac`** that captures a configured reference\nunit — verified byte-identical round-trip on hardware.\n\nTargets: `x86_64-pc-windows-msvc` and `x86_64-unknown-linux-gnu` (WSL Ubuntu).\n\n## Flashing from WSL over usbipd\n\nOn a native Linux host the download gadget is a normal `/dev/ttyACM0`, and\nflashing works exactly as on Windows. To reach a Windows-attached device from\n**WSL**, forward it with [usbipd-win](https://github.com/dorssel/usbipd-win):\n\n```powershell\nusbipd bind   --busid \u003cBUSID\u003e            # once, elevated\nusbipd attach --wsl --busid \u003cBUSID\u003e      # module -\u003e /dev/ttyUSB*, download -\u003e /dev/ttyACM0\n```\n\n```bash\n# in WSL, as root (or add your user to the dialout group)\nsprdflash flash --port /dev/ttyACM0 --chunk 512 --format firmware.pac\n```\n\n**Use `--chunk 512` over usbipd.** The default 2 KB MIDST frames are ideal on a\ndirect connection, but the generic `cdc_acm` driver behind usbipd stalls on\nlarge frames at the tail of a multi-MB transfer; 512 B is reliable end-to-end\n(verified: full 6 MB flash + format + boot, IMEI intact). A native Linux\nmanufacturing host has no usbipd tunnel and runs the default 2 KB fine.\n\n## Running the line\n\nEach `--station` runs on its own thread; they flash and boot-verify in parallel,\nstream a JSON-lines record per unit, and report yield + throughput. Stations are\nindependent, so one bad unit never stalls the line. A wedged download agent is\nrecovered automatically (`pnputil` re-enumerate on Windows; wire a fixture\npower-cycle on Linux) with bounded retries.\n\n```\n\u003e sprdflash line firmware.pac \\\n    --station fixture-1:COM12 --station fixture-2:COM22 --station fixture-3:COM32 \\\n    --format --work-order WO-2026-0142 --operator alice \\\n    --records /var/log/flash/units.jsonl --metrics-addr 0.0.0.0:9184\n\n── results ──\n  [PASS] fixture-1     41.0s  LuatOS-Air_V4035_...  IMEI 8634880509...\n  [PASS] fixture-2     41.3s  LuatOS-Air_V4035_...  IMEI 8634880511...\n  [PASS] fixture-3     40.8s  LuatOS-Air_V4035_...  IMEI 8634880514...\n\n3/3 passed in 41.3s  →  ~261 good units/hour at this concurrency\n```\n\nEach record carries per-phase timings and the `--work-order`/`--operator`\ncontext; `--metrics-addr` serves live Prometheus counters at `/metrics`, and\n[`tools/mes_ingest.py`](tools/mes_ingest.py) loads the JSON lines into the\nPostgres KPI schema (see [docs/](docs/)).\n\nAdd `--loop` to run the line **unattended**: each station keeps flashing —\nwaiting for *its own* fixture's unit to be swapped between cycles (keyed on the\nstation's port, so fixtures never cross-trigger) — until you press Ctrl-C, which\nstops cleanly after the in-flight units finish. Pair it with `--metrics-addr` and\nimport the ready-made [Grafana dashboard + alert rules](docs/grafana/) so a board\nwatches yield and throughput live all shift. Add `--verify-readback` for\nhigh-assurance units: after writing each partition, its bytes are read back with\n`READ_FLASH` and compared, failing on any mismatch (roughly doubles the flash\ntime, so it is off by default).\n\nEach unit is one JSON line for the MES / audit log:\n\n```json\n{\"ts_ms\":1700000000000,\"station\":\"fixture-1\",\"port\":\"COM12\",\"product\":\"UIX8910_MODEM\",\"result\":\"pass\",\"attempts\":1,\"bytes\":6071296,\"flash_seconds\":33.2,\"total_seconds\":41.0,\"firmware\":\"LuatOS-Air_V4035_...\",\"imei\":\"863488050987562\",\"error\":null}\n```\n\nA device is flash-write-bound at ~33 s, so throughput scales with fixtures\n(~110/hour each) → **thousands/day across a modest bank of stations**.\n\n## Releases\n\nPrebuilt binaries for Windows, Linux, and macOS (Intel + Apple Silicon) are\nattached to every [GitHub release](https://github.com/ajsb85/sprdflash-rs/releases).\nEach one ships with a detached SSH signature (`.sig`, Ed25519, namespace `file`)\nand keyless [Sigstore build provenance](https://github.com/ajsb85/sprdflash-rs/attestations):\n\n```sh\n# one-time: trust the maintainer's signing key\necho 'ajsb85@firechip.dev ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJXmKlNp62mfIFNHT4Duv6vcTwfqb/M6OUs34upSpN/L' \u003e allowed_signers\n\n# verify the download\nssh-keygen -Y verify -f allowed_signers -I ajsb85@firechip.dev -n file \\\n    -s sprdflash-\u003cver\u003e-\u003ctarget\u003e.sig \u003c sprdflash-\u003cver\u003e-\u003ctarget\u003e\ngh attestation verify sprdflash-\u003cver\u003e-\u003ctarget\u003e --repo ajsb85/sprdflash-rs\n```\n\n## Protocol reference\n\nSee the Python project's README and `native.py` for the full reverse-engineering\nwriteup. Key facts encoded in `sprdflash-core`:\n\n- **PDL** (`0525:a4a7`, first stage): `ae`-framed, header and payload are\n  **separate writes**; loads `HOST_FDL`/`PDL1`, then hands over to BSL.\n- **BSL**: HDLC `0x7e` frames, `type|size` big-endian, **Spreadtrum sum**\n  checksum (not CRC) on RDA8910/UIS8910.\n- **Cross-SDK `--format`**: erase `FMT_FSSYS \"SYSF\"` + `FLASH 0` (deduped by\n  address), write the **NV template** with a refreshed **CRC-16-ARC** and a\n  **sum32**-checked 12-byte `START_DATA`, then `PREPACK`. IMEI/RF-cal live in a\n  separate `factorynv` region the format never touches.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fajsb85%2Fsprdflash-rs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fajsb85%2Fsprdflash-rs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fajsb85%2Fsprdflash-rs/lists"}